File Coverage

blib/lib/DBIx/FixtureLoader.pm
Criterion Covered Total %
statement 26 121 21.4
branch 0 80 0.0
condition 0 29 0.0
subroutine 9 14 64.2
pod 1 1 100.0
total 36 245 14.6


line stmt bran cond sub pod time code
1             use 5.008001;
2 7     7   700514 use strict;
  7         81  
3 7     7   36 use warnings;
  7         13  
  7         145  
4 7     7   32  
  7         12  
  7         332  
5             our $VERSION = "0.21";
6              
7             use Carp qw/croak/;
8 7     7   35 use DBIx::TransactionManager;
  7         12  
  7         331  
9 7     7   2786 use File::Basename qw/basename/;
  7         21266  
  7         232  
10 7     7   55  
  7         13  
  7         522  
11             use SQL::Maker;
12 7     7   3248 SQL::Maker->load_plugin('InsertMulti');
  7         93999  
  7         324  
13             SQL::Maker->load_plugin('InsertOnDuplicate');
14              
15             use Moo;
16 7     7   3738  
  7         64942  
  7         43  
17             has dbh => (
18             is => 'ro',
19             isa => sub { shift->isa('DBI::db') },
20             required => 1,
21             );
22              
23             has transaction_manager => (
24             is => 'lazy',
25             default => sub {
26             DBIx::TransactionManager->new(shift->dbh);
27             },
28             );
29              
30             has bulk_insert => (
31             is => 'lazy',
32             default => sub {
33             my $self = shift;
34             return undef if $self->skip_null_column;
35              
36             my $driver_name = $self->_driver_name;
37             my $dbh = $self->dbh;
38             $driver_name eq 'mysql' ? 1 :
39             $driver_name eq 'Pg' && $dbh->{ pg_server_version } >= 82000 ? 1 :
40             0 ;
41             },
42             );
43              
44             has update => (
45             is => 'ro',
46             default => sub { undef },
47             );
48              
49             has ignore => (
50             is => 'ro',
51             default => sub { undef },
52             );
53              
54             has delete => (
55             is => 'ro',
56             default => sub { undef },
57             );
58              
59             has skip_null_column => (
60             is => 'ro',
61             default => sub { undef },
62             );
63              
64             has csv_option => (
65             is => 'ro',
66             isa => sub { ref $_[0] eq 'HASH' },
67             default => sub { {} },
68             );
69              
70             has _driver_name => (
71             is => 'lazy',
72             default => sub {
73             shift->dbh->{Driver}{Name};
74             },
75             );
76              
77             has _sql_builder => (
78             is => 'lazy',
79             default => sub {
80             SQL::Maker->new(
81             driver => shift->_driver_name,
82             );
83             }
84             );
85              
86             no Moo;
87 7     7   11040  
  7         14  
  7         38  
88             my $self = shift;
89             my $file = shift;
90 0     0 1   my %opts = ref $_[0] ? %{$_[0]} : @_;
91 0            
92 0 0         my $update = $opts{update};
  0            
93             my $ignore = $opts{ignore};
94 0           croak '`update` and `ignore` are exclusive argument' if $update && $ignore;
95 0            
96 0 0 0       if (ref($file) =~ /^(?:ARRAY|HASH)$/) {
97             return $self->_load_fixture_from_data(data => $file, %opts);
98 0 0         }
99 0            
100             my $table = $opts{table};
101             my $format = lc($opts{format} || '');
102 0            
103 0   0       unless ($table) {
104             my $basename = basename($file);
105 0 0         ($table) = $basename =~ /^([_A-Za-z0-9]+)/;
106 0           }
107 0            
108             unless ($format) {
109             ($format) = $file =~ /\.([^.]*$)/;
110 0 0         }
111 0            
112             my $rows;
113             if ($format eq 'csv' || $format eq 'tsv') {
114 0           $rows = $self->_get_data_from_csv($file, $format);
115 0 0 0       }
116 0           else {
117             if ($format eq 'json') {
118             require JSON;
119 0 0         my $content = do {
    0          
120 0           local $/;
121 0           open my $fh, '<', $file or die $!;
122 0           <$fh>;
123 0 0         };
124 0           $rows = JSON::decode_json($content);
125             }
126 0           elsif ($format =~ /ya?ml/) {
127             require YAML::Tiny;
128             $rows = YAML::Tiny->read($file) or croak( YAML::Tiny->errstr );
129 0           $rows = $rows->[0];
130 0 0         }
131 0           }
132              
133             $self->load_fixture($rows,
134             table => $table,
135 0           %opts,
136             );
137             }
138              
139             my ($self, $file, $format) = @_;
140             require Text::CSV;
141              
142 0     0     my $csv = Text::CSV->new({
143 0           binary => 1,
144             blank_is_undef => 1,
145             sep_char => $format eq 'tsv' ? "\t" : ',',
146             %{ $self->csv_option },
147             }) or croak( Text::CSV->error_diag );
148              
149 0 0         open my $fh, '<', $file or die "$!";
  0 0          
150             my $columns = $csv->getline($fh);
151             my @records;
152 0 0         while ( my $row = $csv->getline($fh) ){
153 0           my %cols = map { $columns->[$_] => $row->[$_] } 0..$#$columns;
154 0           push @records, \%cols;
155 0           }
156 0           \@records;
  0            
157 0           }
158              
159 0           my ($self, %args) = @_;
160             my ($table, $data) = @args{qw/table data/};
161              
162             croak '`update` and `ignore` are exclusive option' if $args{update} && $args{ignore};
163 0     0      
164 0           my $update = $self->update;
165             my $ignore = $self->ignore;
166 0 0 0       croak '`update` and `ignore` are exclusive option' if $update && $ignore;
167              
168 0           my $bulk_insert = $self->bulk_insert;
169 0           my $skip_null_column = $self->skip_null_column;
170 0 0 0       croak '`bulk_insert` and `skip_null_column` are exclusive option' if $bulk_insert && $skip_null_column;
171              
172 0           # The $args has priority. So default object property is ignored.
173 0           if (exists $args{update}) {
174 0 0 0       $update = $args{update};
175             $ignore = undef if $update;
176             }
177 0 0         if (exists $args{ignore}) {
178 0           $ignore = $args{ignore};
179 0 0         $update = undef if $ignore;
180             }
181 0 0          
182 0           if ($update && $self->_driver_name ne 'mysql') {
183 0 0         croak '`update` option only support mysql'
184             }
185             my $delete = $self->delete || $args{delete};
186 0 0 0        
187 0           $data = $self->_normalize_data($data);
188              
189 0   0       my $dbh = $self->dbh;
190             # needs limit ?
191 0           my $txn = $self->transaction_manager->txn_scope or croak $dbh->errstr;
192              
193 0           if ($delete) {
194             my ($sql, @binds) = $self->_sql_builder->delete($table);
195 0 0         $dbh->do($sql, undef, @binds);
196             }
197 0 0          
198 0           unless (scalar @$data) {
199 0           my $ret = $txn->commit or croak $dbh->errstr;
200             return $ret;
201             }
202 0 0          
203 0 0         my $opt; $opt->{prefix} = 'INSERT IGNORE INTO' if $ignore;
204 0           if ($bulk_insert) {
205             $opt->{update} = _build_on_duplicate(keys %{$data->[0]}) if $update;
206              
207 0 0         my ($sql, @binds) = $self->_sql_builder->insert_multi($table, $data, $opt ? $opt : ());
  0            
208 0 0          
209 0 0         $dbh->do( $sql, undef, @binds ) or croak $dbh->errstr;
  0            
210             }
211 0 0         else {
212             my $method = $update ? 'insert_on_duplicate' : 'insert';
213 0 0         for my $row_orig (@$data) {
214             my $row = !$skip_null_column ? $row_orig : {map {
215             defined $row_orig->{$_} ? ($_ => $row_orig->{$_}) : ()
216 0 0         } keys %$row_orig};
217 0           $opt = _build_on_duplicate(keys %$row) if $update;
218             my ($sql, @binds) = $self->_sql_builder->$method($table, $row, $opt ? $opt : ());
219 0 0          
  0 0          
220             $dbh->do( $sql, undef, @binds ) or croak $dbh->errstr;
221 0 0         }
222 0 0         }
223             $txn->commit or croak $dbh->errstr;
224 0 0         }
225              
226             +{ map {($_ => \"VALUES(`$_`)")} @_ };
227 0 0         }
228              
229             my ($self, $data) = @_;
230             my @ret;
231 0     0     if (ref $data eq 'HASH') {
  0            
232             push @ret, $data->{$_} for keys %$data;
233             }
234             elsif (ref $data eq 'ARRAY') {
235 0     0     if ($data->[0] && $data->[0]{data} && ref $data->[0]{data} eq 'HASH') {
236 0           @ret = map { $_->{data} } @$data;
237 0 0         }
    0          
238 0           else {
239             @ret = @$data;
240             }
241 0 0 0       }
      0        
242 0           \@ret;
  0            
243             }
244              
245 0           1;
246              
247             =encoding utf-8
248 0            
249             =head1 NAME
250              
251             DBIx::FixtureLoader - Loading fixtures and inserting to your database
252              
253             =head1 SYNOPSIS
254              
255             use DBI;
256             use DBIx::FixtureLoader;
257              
258             my $dbh = DBI->connect(...);
259             my $loader = DBIx::FixtureLoader->new(dbh => $dbh);
260             $loader->load_fixture('item.csv');
261              
262             =head1 DESCRIPTION
263              
264             DBIx::FixtureLoader is to load fixture data and insert to your database.
265              
266             =head1 INTEFACE
267              
268             =head2 Constructor
269              
270             $loader = DBIx::FixtureLoader->new(%option)
271              
272             C<new> is Constructor method. Various options may be set in C<%option>, which affect
273             the behaviour of the object (Type and defaults in parentheses):
274              
275             =head3 C<< dbh (DBI::db) >>
276              
277             Required. Database handler.
278              
279             =head3 C<< bulk_insert (Bool) >>
280              
281             Using bulk_insert or not. Default value depends on your database.
282              
283             =head3 C<< update (Bool, Default: false) >>
284              
285             Using C<< INSERT ON DUPLICATE >> or not. It only works on MySQL.
286              
287             =head3 C<< ignore (Bool, Default: false) >>
288              
289             Using C<< INSERT IGNORE >> or not. This option is exclusive with C<update>.
290              
291             =head3 C<< delete (Bool, Default: false) >>
292              
293             DELETE all data from table before inserting or not.
294              
295             =head3 C<< csv_option (HashRef, Default: +{}) >>
296              
297             Specifying L<Text::CSV>'s option. C<binary> and C<blank_is_undef>
298             are automatically set.
299              
300             =head3 C<< skip_null_column (Bool, Default: false) >>
301              
302             If true, null data is not to be inserted or updated explicitly. It it for using default value.
303              
304             NOTE: If this option is true, data can't be overwritten by null value.
305              
306             =head2 Methods
307              
308             =head3 C<< $loader->load_fixture($file_or_data:(Str|HashRef|ArrayRef), [%option]) >>
309              
310             Loading fixture and inserting to your database. Table name and file format is guessed from
311             file name. For example, "item.csv" contains data of "item" table and format is "CSV".
312              
313             In most cases C<%option> is not needed. Available keys of C<%option> are as follows.
314              
315             =over
316              
317             =item C<table:Str>
318              
319             table name of database.
320              
321             =item C<format:Str>
322              
323             data format. "CSV", "YAML" and "JSON" are available.
324              
325             =item C<update:Bool>
326              
327             Using C<< ON DUPLICATE KEY UPDATE >> or not. Default value depends on object setting.
328              
329             =item C<< ignore:Bool >>
330              
331             Using C<< INSERT IGNORE >> or not.
332              
333             =item C<< delete:Bool >>
334              
335             DELETE all data from table before inserting or not.
336              
337             =back
338              
339             =head2 File Name and Data Format
340              
341             =head3 file name
342              
343             Data format is guessed from extension. Table name is guessed from basename. Leading alphabets,
344             underscores and numbers are considered table name. So, C<"user_item-2.csv"> is considered CSV format
345             and containing data of "user_item" table.
346              
347             =head3 data format
348              
349             "CSV", "YAML" and "JSON" are parsable. CSV file must have header line for determining column names.
350              
351             Datas in "YAML" or "JSON" must be ArrayRef or HashRef containing HashRefs. Each HashRef is the data
352             of database record and keys of HashRef is matching to column names of the table.
353              
354             =head1 LICENSE
355              
356             Copyright (C) Masayuki Matsuki.
357              
358             This library is free software; you can redistribute it and/or modify
359             it under the same terms as Perl itself.
360              
361             =head1 AUTHOR
362              
363             Masayuki Matsuki E<lt>y.songmu@gmail.comE<gt>
364              
365             =cut