File Coverage

blib/lib/DBIx/FixtureLoader.pm
Criterion Covered Total %
statement 26 119 21.8
branch 0 78 0.0
condition 0 32 0.0
subroutine 9 14 64.2
pod 1 1 100.0
total 36 244 14.7


line stmt bran cond sub pod time code
1             package DBIx::FixtureLoader;
2 7     7   812598 use 5.008001;
  7         91  
3 7     7   38 use strict;
  7         18  
  7         154  
4 7     7   33 use warnings;
  7         14  
  7         337  
5              
6             our $VERSION = "0.20";
7              
8 7     7   42 use Carp qw/croak/;
  7         15  
  7         422  
9 7     7   3514 use DBIx::TransactionManager;
  7         25700  
  7         235  
10 7     7   51 use File::Basename qw/basename/;
  7         16  
  7         622  
11              
12 7     7   3577 use SQL::Maker;
  7         112969  
  7         343  
13             SQL::Maker->load_plugin('InsertMulti');
14             SQL::Maker->load_plugin('InsertOnDuplicate');
15              
16 7     7   3961 use Moo;
  7         77788  
  7         36  
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   13325 no Moo;
  7         19  
  7         38  
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             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 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              
193 0           my $dbh = $self->dbh;
194             # needs limit ?
195 0 0         my $txn = $self->transaction_manager->txn_scope or croak $dbh->errstr;
196              
197 0 0         if ($delete) {
198 0           my ($sql, @binds) = $self->_sql_builder->delete($table);
199 0           $dbh->do($sql, undef, @binds);
200             }
201              
202 0 0 0       return $txn->commit or croak $dbh->errstr unless scalar @$data;
203              
204 0 0         my $opt; $opt->{prefix} = 'INSERT IGNORE INTO' if $ignore;
  0            
205 0 0         if ($bulk_insert) {
206 0 0         $opt->{update} = _build_on_duplicate(keys %{$data->[0]}) if $update;
  0            
207              
208 0 0         my ($sql, @binds) = $self->_sql_builder->insert_multi($table, $data, $opt ? $opt : ());
209              
210 0 0         $dbh->do( $sql, undef, @binds ) or croak $dbh->errstr;
211             }
212             else {
213 0 0         my $method = $update ? 'insert_on_duplicate' : 'insert';
214 0           for my $row_orig (@$data) {
215             my $row = !$skip_null_column ? $row_orig : {map {
216 0 0         defined $row_orig->{$_} ? ($_ => $row_orig->{$_}) : ()
  0 0          
217             } keys %$row_orig};
218 0 0         $opt = _build_on_duplicate(keys %$row) if $update;
219 0 0         my ($sql, @binds) = $self->_sql_builder->$method($table, $row, $opt ? $opt : ());
220              
221 0 0         $dbh->do( $sql, undef, @binds ) or croak $dbh->errstr;
222             }
223             }
224 0 0         $txn->commit or croak $dbh->errstr;
225             }
226              
227             sub _build_on_duplicate {
228 0     0     +{ map {($_ => \"VALUES(`$_`)")} @_ };
  0            
229             }
230              
231             sub _normalize_data {
232 0     0     my ($self, $data) = @_;
233 0           my @ret;
234 0 0         if (ref $data eq 'HASH') {
    0          
235 0           push @ret, $data->{$_} for keys %$data;
236             }
237             elsif (ref $data eq 'ARRAY') {
238 0 0 0       if ($data->[0] && $data->[0]{data} && ref $data->[0]{data} eq 'HASH') {
      0        
239 0           @ret = map { $_->{data} } @$data;
  0            
240             }
241             else {
242 0           @ret = @$data;
243             }
244             }
245 0           \@ret;
246             }
247              
248             1;
249             __END__
250              
251             =encoding utf-8
252              
253             =head1 NAME
254              
255             DBIx::FixtureLoader - Loading fixtures and inserting to your database
256              
257             =head1 SYNOPSIS
258              
259             use DBI;
260             use DBIx::FixtureLoader;
261              
262             my $dbh = DBI->connect(...);
263             my $loader = DBIx::FixtureLoader->new(dbh => $dbh);
264             $loader->load_fixture('item.csv');
265              
266             =head1 DESCRIPTION
267              
268             DBIx::FixtureLoader is to load fixture data and insert to your database.
269              
270             =head1 INTEFACE
271              
272             =head2 Constructor
273              
274             $loader = DBIx::FixtureLoader->new(%option)
275              
276             C<new> is Constructor method. Various options may be set in C<%option>, which affect
277             the behaviour of the object (Type and defaults in parentheses):
278              
279             =head3 C<< dbh (DBI::db) >>
280              
281             Required. Database handler.
282              
283             =head3 C<< bulk_insert (Bool) >>
284              
285             Using bulk_insert or not. Default value depends on your database.
286              
287             =head3 C<< update (Bool, Default: false) >>
288              
289             Using C<< INSERT ON DUPLICATE >> or not. It only works on MySQL.
290              
291             =head3 C<< ignore (Bool, Default: false) >>
292              
293             Using C<< INSERT IGNORE >> or not. This option is exclusive with C<update>.
294              
295             =head3 C<< delete (Bool, Default: false) >>
296              
297             DELETE all data from table before inserting or not.
298              
299             =head3 C<< csv_option (HashRef, Default: +{}) >>
300              
301             Specifying L<Text::CSV>'s option. C<binary> and C<blank_is_undef>
302             are automatically set.
303              
304             =head3 C<< skip_null_column (Bool, Default: false) >>
305              
306             If true, null data is not to be inserted or updated explicitly. It it for using default value.
307              
308             NOTE: If this option is true, data can't be overwritten by null value.
309              
310             =head2 Methods
311              
312             =head3 C<< $loader->load_fixture($file_or_data:(Str|HashRef|ArrayRef), [%option]) >>
313              
314             Loading fixture and inserting to your database. Table name and file format is guessed from
315             file name. For example, "item.csv" contains data of "item" table and format is "CSV".
316              
317             In most cases C<%option> is not needed. Available keys of C<%option> are as follows.
318              
319             =over
320              
321             =item C<table:Str>
322              
323             table name of database.
324              
325             =item C<format:Str>
326              
327             data format. "CSV", "YAML" and "JSON" are available.
328              
329             =item C<update:Bool>
330              
331             Using C<< ON DUPLICATE KEY UPDATE >> or not. Default value depends on object setting.
332              
333             =item C<< ignore:Bool >>
334              
335             Using C<< INSERT IGNORE >> or not.
336              
337             =item C<< delete:Bool >>
338              
339             DELETE all data from table before inserting or not.
340              
341             =back
342              
343             =head2 File Name and Data Format
344              
345             =head3 file name
346              
347             Data format is guessed from extension. Table name is guessed from basename. Leading alphabets,
348             underscores and numbers are considered table name. So, C<"user_item-2.csv"> is considered CSV format
349             and containing data of "user_item" table.
350              
351             =head3 data format
352              
353             "CSV", "YAML" and "JSON" are parsable. CSV file must have header line for determining column names.
354              
355             Datas in "YAML" or "JSON" must be ArrayRef or HashRef containing HashRefs. Each HashRef is the data
356             of database record and keys of HashRef is matching to column names of the table.
357              
358             =head1 LICENSE
359              
360             Copyright (C) Masayuki Matsuki.
361              
362             This library is free software; you can redistribute it and/or modify
363             it under the same terms as Perl itself.
364              
365             =head1 AUTHOR
366              
367             Masayuki Matsuki E<lt>y.songmu@gmail.comE<gt>
368              
369             =cut