File Coverage

blib/lib/DBIx/FixtureLoader.pm
Criterion Covered Total %
statement 27 120 22.5
branch 0 78 0.0
condition 0 29 0.0
subroutine 9 14 64.2
pod 1 1 100.0
total 37 242 15.2


line stmt bran cond sub pod time code
1             package DBIx::FixtureLoader;
2 7     7   159800 use 5.008001;
  7         19  
  7         184  
3 7     7   23 use strict;
  7         8  
  7         139  
4 7     7   22 use warnings;
  7         11  
  7         244  
5              
6             our $VERSION = "0.12";
7              
8 7     7   27 use Carp qw/croak/;
  7         6  
  7         318  
9 7     7   2943 use DBIx::TransactionManager;
  7         13580  
  7         178  
10 7     7   33 use File::Basename qw/basename/;
  7         8  
  7         387  
11              
12 7     7   3138 use SQL::Maker;
  7         78473  
  7         281  
13             SQL::Maker->load_plugin('InsertMulti');
14             SQL::Maker->load_plugin('InsertOnDuplicate');
15              
16 7     7   8336 use Moo;
  7         71396  
  7         28  
17              
18             has dbh => (
19             is => 'ro',
20             isa => sub { shift->isa('DBI::db') },
21             required => 1,
22             );
23              
24             has transaction_manager => (
25             is => 'lazy',
26             default => sub {
27             DBIx::TransactionManager->new(shift->dbh);
28             },
29             );
30              
31             has bulk_insert => (
32             is => 'lazy',
33             default => sub {
34             my $self = shift;
35             return undef if $self->skip_null_column;
36              
37             my $driver_name = $self->_driver_name;
38             my $dbh = $self->dbh;
39             $driver_name eq 'mysql' ? 1 :
40             $driver_name eq 'Pg' && $dbh->{ pg_server_version } >= 82000 ? 1 :
41             0 ;
42             },
43             );
44              
45             has update => (
46             is => 'ro',
47             default => sub { undef },
48             );
49              
50             has ignore => (
51             is => 'ro',
52             default => sub { undef },
53             );
54              
55             has delete => (
56             is => 'ro',
57             default => sub { undef },
58             );
59              
60             has skip_null_column => (
61             is => 'ro',
62             default => sub { undef },
63             );
64              
65             has csv_option => (
66             is => 'ro',
67             isa => sub { ref $_[0] eq 'HASH' },
68             default => sub { {} },
69             );
70              
71             has _driver_name => (
72             is => 'lazy',
73             default => sub {
74             shift->dbh->{Driver}{Name};
75             },
76             );
77              
78             has _sql_builder => (
79             is => 'lazy',
80             default => sub {
81             SQL::Maker->new(
82             driver => shift->_driver_name,
83             );
84             }
85             );
86              
87 7     7   8771 no Moo;
  7         55  
  7         25  
88              
89             sub load_fixture {
90 0     0 1   my $self = shift;
91 0           my $file = shift;
92 0 0         my %opts = ref $_[0] ? %{$_[0]} : @_;
  0            
93              
94 0           my $update = $opts{update};
95 0           my $ignore = $opts{ignore};
96 0 0 0       croak '`update` and `ignore` are exclusive argument' if $update && $ignore;
97              
98 0 0         if (ref($file) =~ /^(?:ARRAY|HASH)$/) {
99 0           return $self->_load_fixture_from_data(data => $file, %opts);
100             }
101              
102 0           my $table = $opts{table};
103 0   0       my $format = lc($opts{format} || '');
104              
105 0 0         unless ($table) {
106 0           my $basename = basename($file);
107 0           ($table) = $basename =~ /^([_A-Za-z0-9]+)/;
108             }
109              
110 0 0         unless ($format) {
111 0           ($format) = $file =~ /\.([^.]*$)/;
112             }
113              
114 0           my $rows;
115 0 0 0       if ($format eq 'csv' || $format eq 'tsv') {
116 0           $rows = $self->_get_data_from_csv($file, $format);
117             }
118             else {
119 0 0         if ($format eq 'json') {
    0          
120 0           require JSON;
121 0           my $content = do {
122 0           local $/;
123 0 0         open my $fh, '<', $file or die $!;
124 0           <$fh>;
125             };
126 0           $rows = JSON::decode_json($content);
127             }
128             elsif ($format =~ /ya?ml/) {
129 0           require YAML::Tiny;
130 0 0         $rows = YAML::Tiny->read($file) or croak( YAML::Tiny->errstr );
131 0           $rows = $rows->[0];
132             }
133             }
134              
135 0           $self->load_fixture($rows,
136             table => $table,
137             %opts,
138             );
139             }
140              
141             sub _get_data_from_csv {
142 0     0     my ($self, $file, $format) = @_;
143 0           require Text::CSV;
144              
145 0           my $csv = Text::CSV->new({
146             binary => 1,
147             blank_is_undef => 1,
148             sep_char => $format eq 'tsv' ? "\t" : ',',
149 0 0         %{ $self->csv_option },
    0          
150             }) or croak( Text::CSV->error_diag );
151              
152 0 0         open my $fh, '<', $file or die "$!";
153 0           my $columns = $csv->getline($fh);
154 0           my @records;
155 0           while ( my $row = $csv->getline($fh) ){
156 0           my %cols = map { $columns->[$_] => $row->[$_] } 0..$#$columns;
  0            
157 0           push @records, \%cols;
158             }
159 0           \@records;
160             }
161              
162             sub _load_fixture_from_data {
163 0     0     my ($self, %args) = @_;
164 0           my ($table, $data) = @args{qw/table data/};
165              
166 0 0 0       croak '`update` and `ignore` are exclusive option' if $args{update} && $args{ignore};
167              
168 0           my $update = $self->update;
169 0           my $ignore = $self->ignore;
170 0 0 0       croak '`update` and `ignore` are exclusive option' if $update && $ignore;
171              
172 0           my $bulk_insert = $self->bulk_insert;
173 0           my $skip_null_column = $self->skip_null_column;
174 0 0 0       croak '`bulk_insert` and `skip_null_column` are exclusive option' if $bulk_insert && $skip_null_column;
175              
176             # The $args has priority. So default object property is ignored.
177 0 0         if (exists $args{update}) {
178 0           $update = $args{update};
179 0 0         $ignore = undef if $update;
180             }
181 0 0         if (exists $args{ignore}) {
182 0           $ignore = $args{ignore};
183 0 0         $update = undef if $ignore;
184             }
185              
186 0 0 0       if ($update && $self->_driver_name ne 'mysql') {
187 0           croak '`update` option only support mysql'
188             }
189 0   0       my $delete = $self->delete || $args{delete};
190              
191 0           $data = $self->_normalize_data($data);
192 0 0         return unless @$data;
193              
194 0           my $dbh = $self->dbh;
195             # needs limit ?
196 0 0         my $txn = $self->transaction_manager->txn_scope or croak $dbh->errstr;
197              
198 0 0         if ($delete) {
199 0           my ($sql, @binds) = $self->_sql_builder->delete($table);
200 0           $dbh->do($sql, undef, @binds);
201             }
202              
203 0 0         my $opt; $opt->{prefix} = 'INSERT IGNORE INTO' if $ignore;
  0            
204 0 0         if ($bulk_insert) {
205 0 0         $opt->{update} = _build_on_duplicate(keys %{$data->[0]}) if $update;
  0            
206              
207 0 0         my ($sql, @binds) = $self->_sql_builder->insert_multi($table, $data, $opt ? $opt : ());
208              
209 0 0         $dbh->do( $sql, undef, @binds ) or croak $dbh->errstr;
210             }
211             else {
212 0 0         my $method = $update ? 'insert_on_duplicate' : 'insert';
213 0           for my $row_orig (@$data) {
214 0 0         my $row = !$skip_null_column ? $row_orig : {map {
215 0 0         defined $row_orig->{$_} ? ($_ => $row_orig->{$_}) : ()
216             } keys %$row_orig};
217 0 0         $opt = _build_on_duplicate(keys %$row) if $update;
218 0 0         my ($sql, @binds) = $self->_sql_builder->$method($table, $row, $opt ? $opt : ());
219              
220 0 0         $dbh->do( $sql, undef, @binds ) or croak $dbh->errstr;
221             }
222             }
223 0 0         $txn->commit or croak $dbh->errstr;
224             }
225              
226             sub _build_on_duplicate {
227 0     0     +{ map {($_ => \"VALUES(`$_`)")} @_ };
  0            
228             }
229              
230             sub _normalize_data {
231 0     0     my ($self, $data) = @_;
232 0           my @ret;
233 0 0         if (ref $data eq 'HASH') {
    0          
234 0           push @ret, $data->{$_} for keys %$data;
235             }
236             elsif (ref $data eq 'ARRAY') {
237 0 0 0       if ($data->[0] && $data->[0]{data} && ref $data->[0]{data} eq 'HASH') {
      0        
238 0           @ret = map { $_->{data} } @$data;
  0            
239             }
240             else {
241 0           @ret = @$data;
242             }
243             }
244 0           \@ret;
245             }
246              
247             1;
248             __END__