File Coverage

blib/lib/DBIx/TableLoader.pm
Criterion Covered Total %
statement 138 138 100.0
branch 41 44 93.1
condition 29 38 76.3
subroutine 41 41 100.0
pod 28 28 100.0
total 277 289 95.8


line stmt bran cond sub pod time code
1             # vim: set ts=2 sts=2 sw=2 expandtab smarttab:
2             #
3             # This file is part of DBIx-TableLoader
4             #
5             # This software is copyright (c) 2011 by Randy Stauner.
6             #
7             # This is free software; you can redistribute it and/or modify it under
8             # the same terms as the Perl 5 programming language system itself.
9             #
10 7     7   57904 use strict;
  7         17  
  7         300  
11 7     7   39 use warnings;
  7         12  
  7         644  
12              
13             package DBIx::TableLoader;
14             {
15             $DBIx::TableLoader::VERSION = '1.100';
16             }
17             # git description: v1.003-5-gb3c3fb7
18              
19             BEGIN {
20 7     7   143 $DBIx::TableLoader::AUTHORITY = 'cpan:RWSTAUNER';
21             }
22             # ABSTRACT: Easily load a database table from a data set
23              
24 7     7   39 use Carp qw(croak);
  7         13  
  7         600  
25             #use DBI 1.13 (); # oldest DBI on CPAN as of 2011-02-15; Has SQL_LONGVARCHAR
26 7     7   9321 use Try::Tiny 0.09;
  7         13689  
  7         18831  
27              
28              
29             sub new {
30 44     44 1 28343 my $class = shift;
31 44         152 my $self = bless {}, $class;
32              
33 44 100       596 my %opts = @_ == 1 ? %{$_[0]} : @_;
  16         81  
34              
35 44         68 my %defaults = (%{ $self->base_defaults }, %{ $self->defaults });
  44         148  
  44         112  
36 44         538 while( my ($key, $value) = each %defaults ){
37 1102 100       4118 $self->{$key} = exists($opts{$key})
38             ? delete $opts{$key}
39             : $value;
40             }
41              
42             # be loud about typos
43 44 100       113 croak("Unknown options: ${\join(', ', keys %opts)}")
  1         245  
44             if %opts;
45              
46             # custom routine to handle type of input data (hook for subclasses)
47 43         122 $self->prepare_data();
48              
49             # normalize 'columns' attribute
50 43         118 $self->determine_column_types();
51              
52 41         241 return $self;
53             }
54              
55              
56             sub base_defaults {
57             return {
58 44     44 1 1000 catalog => undef,
59             columns => undef,
60             create => 1,
61             create_prefix => '',
62             create_sql => '',
63             create_suffix => '',
64             # 'data' attribute may not be useful in subclasses
65             data => undef,
66             dbh => undef,
67             default_column_type => '',
68             default_sql_data_type => '',
69             drop => 0,
70             drop_prefix => '',
71             drop_sql => '',
72             drop_suffix => '',
73             get_row => undef,
74             grep_rows => undef,
75             handle_invalid_row => undef,
76             map_rows => undef,
77             # default_name() method will default to 'data' if 'name' is blank
78             # this way subclasses don't have to override this value in defaults()
79             name => '',
80             name_prefix => '',
81             name_suffix => '',
82             quoted_name => undef,
83             schema => undef,
84             table_type => '', # TEMP, TEMPORARY, VIRTUAL?
85             transaction => 1,
86             };
87             }
88              
89              
90             sub defaults {
91 42     42 1 426 return {};
92             }
93              
94              
95             sub columns {
96 146     146 1 2032 my ($self) = @_;
97             # by default the column names are found in the first row of the data
98             # (but circumvent get_row() to avoid any grep or map subs)
99 146   100     668 return $self->{columns} ||= $self->_get_custom_or_raw_row();
100             }
101              
102              
103             sub column_names {
104 29     29 1 3869 my ($self) = @_;
105             # return the first element of each arrayref
106 29         45 return [ map { $$_[0] } @{ $self->columns } ];
  74         226  
  29         64  
107             }
108              
109              
110             sub create {
111 10     10 1 18 my ($self) = @_;
112 10         34 $self->{dbh}->do($self->create_sql);
113             }
114              
115              
116             sub create_prefix {
117 16     16 1 4557 my ($self) = @_;
118 16   66     116 return $self->{create_prefix} ||=
119             "CREATE $self->{table_type} TABLE " .
120             $self->quoted_name . " (";
121             }
122              
123              
124             sub create_sql {
125 16     16 1 35 my ($self) = @_;
126 20         268 $self->{create_sql} ||=
127             join(' ',
128             $self->create_prefix,
129              
130             # column definitions (each element is: [name, data_type])
131             join(', ', map {
132 10         89 $self->{dbh}->quote_identifier($_->[0]) . ' ' . $_->[1]
133 16   66     124 } @{ $self->columns }),
134              
135             $self->create_suffix
136             );
137             }
138              
139              
140             sub create_suffix {
141 16     16 1 4676 my ($self) = @_;
142 16   100     588 return $self->{create_suffix} ||=
143             ')';
144             }
145              
146             # ask the driver what data type it uses for the desired SQL standard type
147              
148             sub _data_type_from_driver {
149 27     27   423 my ($self, $data_type) = @_;
150 27 100       176 if( my $type = $self->{dbh}->type_info($data_type) ){
151 17         176 return $type->{TYPE_NAME};
152             }
153 3         28 return;
154             }
155              
156              
157             sub default_name {
158 16     16 1 158 return 'data';
159             }
160              
161              
162             sub default_column_type {
163 46     46 1 1426 my ($self) = @_;
164             return $self->{default_column_type} ||= try {
165 27     27   532 $self->_data_type_from_driver($self->default_sql_data_type);
166             }
167             # outside the eval in case there was an error
168 46   100     289 || 'text';
      66        
169             }
170              
171              
172             sub default_sql_data_type {
173 28     28 1 468 my ($self) = @_;
174             $self->{default_sql_data_type} ||= try {
175             # if this doesn't work default_column_type will just use 'text'
176 27     27   8010 require DBI;
177 27         62504 DBI::SQL_LONGVARCHAR();
178 28   66     189 };
179             }
180              
181              
182             sub determine_column_types {
183 43     43 1 64 my ($self) = @_;
184 43         106 my ($columns, $type) = ($self->columns, $self->default_column_type);
185              
186 43 100 100     1073 croak("Unable to determine columns!")
187             unless $columns && @$columns;
188              
189             # break reference
190 41         125 $columns = [@$columns];
191              
192             # reset each element to an arrayref if it isn't already
193 41         95 foreach my $column ( @$columns ){
194             # upgrade lone string to arrayref otherwise break reference
195 84 100       256 $column = ref $column ? [@$column] : [$column];
196             # append column type if missing
197 84 100       291 push(@$column, $type)
198             unless @$column > 1;
199             }
200              
201             # restore changes
202 41         87 $self->{columns} = $columns;
203 41         72 return;
204             }
205              
206              
207             sub drop {
208 8     8 1 16 my ($self) = @_;
209 8         32 $self->{dbh}->do($self->drop_sql);
210             }
211              
212              
213             sub drop_prefix {
214 16     16 1 1890 my ($self) = @_;
215             # default to "DROP TABLE" since SQLite, PostgreSQL, and MySQL
216             # all accept it (rather than "DROP $table_type TABLE")
217 16   100     634 $self->{drop_prefix} ||= 'DROP TABLE';
218             }
219              
220              
221             sub drop_sql {
222 16     16 1 32 my ($self) = @_;
223 16   66     109 return $self->{drop_sql} ||= join(' ',
224             $self->drop_prefix,
225             $self->quoted_name,
226             $self->drop_suffix,
227             );
228             }
229              
230              
231             sub drop_suffix {
232 16     16 1 50 my ($self) = @_;
233             # default is blank
234 16         603 return $self->{drop_suffix};
235             }
236              
237             # call get_raw_row unless a custom 'get_row' is defined
238             # (this is the essence of get_row() but without the grep/map subs)
239              
240             sub _get_custom_or_raw_row {
241 88     88   107 my ($self) = @_;
242             # considered { $self->{get_row} ||= $self->can('get_raw_row'); } in new()
243             # but it just seemed a little strange... this is more normal/clear
244 88 100       301 return $self->{get_row}
245             ? $self->{get_row}->($self)
246             : $self->get_raw_row();
247             }
248              
249              
250             sub get_raw_row {
251 67     67 1 98 my ($self) = @_;
252             # It would be simpler to shift the data but I don't think it actually
253             # gains us anything. This way we're not modifying anything unexpectedly.
254             # Besides subclasses will likely be more useful than this one.
255 67         340 return $self->{data}->[ $self->{row_index}++ ];
256             }
257              
258              
259             sub get_row {
260 69     69 1 20698 my ($self) = @_;
261 69         83 my $row;
262              
263 72 100       569 GETROW: {
264 69         76 $row = $self->_get_custom_or_raw_row()
265             or last GETROW;
266              
267             # call grep_rows with the same semantics as map_rows (below)
268 55 100       272 if( $self->{grep_rows} ){
269 6         9 local $_ = $row;
270             # if grep returns false try the block again
271             redo GETROW
272 6 100       12 unless $self->{grep_rows}->($row, $self);
273             }
274              
275             # Send the row first since it's the important part.
276             # This isn't a method call, and $self will likely be seldom used.
277 53 100       127 if( $self->{map_rows} ){
278             # localize $_ to the $row for consistency with the built in map()
279 11         22 local $_ = $row;
280             # also pass row as the first argument to simulate a normal function call
281 11         26 $row = $self->{map_rows}->($row, $self);
282             }
283              
284             # validate the row before passing a bad value to the DBI
285             $row = try {
286 53     53   1582 $self->validate_row($row);
287             }
288             catch {
289             # file/line position is unhelpful, but so is the newline
290 12     12   136 chomp(my $e = $_[0]);
291             # if there was an error, pass it through the handler
292             # the handler should die, return a row, or return false to skip
293 12         36 $self->handle_invalid_row($e, $row);
294             }
295 53 100       480 or redo GETROW;
296             }
297              
298 65         1773 return $row;
299             }
300              
301              
302             sub handle_invalid_row {
303 15     15 1 1537 my ($self, $error, $row) = @_;
304              
305 15 100       60 if( my $handler = $self->{handle_invalid_row} ){
306             # should this be croak/carp?
307 14 100       50 if( $handler eq 'die' ){
    100          
308 3         23 die $error . "\n";
309             }
310             elsif( $handler eq 'warn' ){
311 1         9 warn $error . "\n";
312 1         10 return $row;
313             }
314             # otherwise it should be a coderef (or a method name (for a subclass maybe))
315             else {
316 10         27 return $self->$handler($error, $row);
317             }
318             }
319              
320             # pass through if no handler was defined
321 1         5 return $row;
322             }
323              
324              
325             sub insert_sql {
326 12     12 1 4737 my ($self) = @_;
327 12         96 join(' ',
328             'INSERT INTO',
329             $self->quoted_name,
330             '(',
331 12         513 join(', ', @{ $self->quoted_column_names } ),
332             ')',
333             'VALUES(',
334 12         42 join(', ', ('?') x @{ $self->columns }),
335             ')'
336             );
337             }
338              
339              
340             sub insert_all {
341 7     7 1 6721 my ($self) = @_;
342              
343 7         14 my $rows = 0;
344 7         28 my $sth = $self->{dbh}->prepare($self->insert_sql);
345 7         82 while( my $row = $self->get_row() ){
346 8         40 $sth->execute(@$row);
347 8         76 ++$rows;
348             }
349              
350 6         25 return $rows;
351             }
352              
353              
354             sub load {
355 4     4 1 2392 my ($self) = @_;
356 4         34 my $rows;
357              
358             # is it appropriate/sufficient to call prepare_data() from new()?
359              
360             try {
361              
362 4 100   4   139 $self->{dbh}->begin_work()
363             if $self->{transaction};
364              
365 4 50       26 $self->drop()
366             if $self->{drop};
367              
368 4 50       20 $self->create()
369             if $self->{create};
370              
371 4         29 $rows = $self->insert_all();
372              
373 3 100       20 $self->{dbh}->commit()
374             if $self->{transaction};
375              
376             }
377             catch {
378             # explicitly end the transaction that we started
379             # in case this isn't the last thing being done with the dbh
380 1 50   1   19 $self->{dbh}->rollback()
381             if $self->{transaction};
382              
383             # propagate the exception
384 1         13 die $_[0];
385 4         41 };
386              
387 3         58 return $rows;
388             }
389              
390              
391             sub name {
392 24     24 1 3590 my ($self) = @_;
393 24   66     212 return $self->{_name} ||=
      66        
394             $self->{name_prefix} .
395             ($self->{name} || $self->default_name) .
396             $self->{name_suffix};
397             }
398              
399              
400             sub prepare_data {
401 42     42 1 63 my ($self) = @_;
402 42         100 $self->{row_index} = 0;
403             }
404              
405              
406             sub quoted_name {
407 33     33 1 48 my ($self) = @_;
408             # allow quoted name to be passed in to handle edge cases
409 33   66     182 return $self->{quoted_name} ||=
410             $self->{dbh}->quote_identifier(
411             $self->{catalog}, $self->{schema}, $self->name);
412             }
413              
414              
415             sub quoted_column_names {
416 16     16 1 27 my ($self) = @_;
417 36         229 return $self->{quoted_column_names} ||= [
418 16         44 map { $self->{dbh}->quote_identifier($_) }
419 16   50     73 @{ $self->column_names }
420             ];
421             }
422              
423              
424             sub validate_row {
425 48     48 1 108 my ($self, $row) = @_;
426              
427             # DBI will croak if exec'd with different numbers
428 48         48 my $num_columns = @{ $self->columns };
  48         93  
429              
430 48 100       179 die 'Row has ' . @$row . ' fields when ' . $num_columns . " are expected\n"
431             if @$row != $num_columns;
432              
433             # are there other validation checks we can do?
434              
435 36         111 return $row;
436             }
437              
438             1;
439              
440             __END__