File Coverage

blib/lib/ORLite/Array.pm
Criterion Covered Total %
statement 212 234 90.6
branch 62 90 68.8
condition 17 30 56.6
subroutine 43 47 91.4
pod 0 2 0.0
total 334 403 82.8


line stmt bran cond sub pod time code
1             package ORLite::Array;
2              
3             # See POD at end of file for documentation
4              
5 9     9   260751 use 5.006;
  9         31  
  9         341  
6 9     9   49 use strict;
  9         16  
  9         237  
7 9     9   53 use Carp ();
  9         15  
  9         3432  
8 8     8   34 use File::Spec 0.80 ();
  8         170  
  8         150  
9 8     8   8754 use File::Temp 0.20 ();
  8         222977  
  8         252  
10 8     8   67 use File::Path 2.04 ();
  8         125  
  8         168  
11 8     8   46 use File::Basename 0 ();
  8         196  
  8         153  
12 8     8   8516 use Params::Util 0.33 ();
  8         26525  
  8         214  
13 8     8   24484 use DBI 1.607 ();
  8         204402  
  8         412  
14 8     8   15122 use DBD::SQLite 1.25 ();
  8         91956  
  8         227  
15              
16 8     8   104 use vars qw{$VERSION};
  8         13  
  8         401  
17             BEGIN {
18 8     8   445 $VERSION = '0.02';
19             }
20              
21             BEGIN {
22 8 50   8   56 unless ( defined $INC{'ORLite.pm'} ) {
23 8         18 $INC{'ORLite.pm'} = __FILE__;
24 8         114 @ORLite::ISA = __PACKAGE__;
25 8         20122 $ORLite::VERSION = '1.28';
26             }
27             }
28              
29             # Support for the 'prune' option
30             my @PRUNE = ();
31             END {
32 8     7   289967 foreach ( @PRUNE ) {
33 1 50       7 next unless -e $_;
34 6         1907 require File::Remove;
35 38         234 File::Remove::remove($_);
36             }
37             }
38              
39              
40             #####################################################################
41             # Code Generation
42              
43             sub import {
44 7   33 7   331787 my $class = ref($_[0]) || $_[0];
45              
46             # Check for debug mode
47 7         29 my $DEBUG = 0;
48 7 50 66     128 if ( defined Params::Util::_STRING($_[-1]) and $_[-1] eq '-DEBUG' ) {
49 0         0 $DEBUG = 1;
50 0         0 pop @_;
51             }
52              
53             # Check params and apply defaults
54 7         20 my %params;
55 7 100       74 if ( defined Params::Util::_STRING($_[1]) ) {
    50          
56             # Support the short form "use ORLite 'db.sqlite'"
57 3         30 %params = (
58             file => $_[1],
59             readonly => undef, # Automatic
60             package => undef, # Automatic
61             tables => 1,
62             );
63             } elsif ( Params::Util::_HASHLIKE($_[1]) ) {
64 4         8 %params = %{ $_[1] };
  4         28  
65             } else {
66 0         0 Carp::croak("Missing, empty or invalid params HASH");
67             }
68 7 100       40 unless ( defined $params{create} ) {
69 5         20 $params{create} = 0;
70             }
71 7 50 66     305 unless (
      33        
72             defined Params::Util::_STRING($params{file})
73             and (
74             $params{create}
75             or
76             -f $params{file}
77             )
78             ) {
79 0         0 Carp::croak("Missing or invalid file param");
80             }
81 7 100       36 unless ( defined $params{readonly} ) {
82 5 100       83 $params{readonly} = $params{create} ? 0 : ! -w $params{file};
83             }
84 7 100       36 unless ( defined $params{tables} ) {
85 2         5 $params{tables} = 1;
86             }
87 7 50       34 unless ( defined $params{package} ) {
88 7         37 $params{package} = scalar caller;
89             }
90 7 50       287 unless ( Params::Util::_CLASS($params{package}) ) {
91 0         0 Carp::croak("Missing or invalid package class");
92             }
93              
94             # Connect to the database
95 7         721 my $file = File::Spec->rel2abs($params{file});
96 7         119 my $created = ! -f $params{file};
97 7 100       56 if ( $created ) {
98             # Create the parent directory
99 2         321 my $dir = File::Basename::dirname($file);
100 2 50       66 unless ( -d $dir ) {
101 0         0 my @dirs = File::Path::mkpath( $dir, { verbose => 0 } );
102 0 0       0 $class->prune(@dirs) if $params{prune};
103             }
104 2 50       16 $class->prune($file) if $params{prune};
105             }
106 7         157 my $pkg = $params{package};
107 7         24 my $readonly = $params{readonly};
108 7         31 my $dsn = "dbi:SQLite:$file";
109 7         67 my $dbh = DBI->connect($dsn);
110              
111             # Schema creation support
112 7 100 100     7867 if ( $created and Params::Util::_CODELIKE($params{create}) ) {
113 1         24 $params{create}->( $dbh );
114             }
115              
116             # Check the schema version before generating
117 7         35783 my $version = $dbh->selectrow_arrayref('pragma user_version')->[0];
118 7 50 66     1619 if ( exists $params{user_version} and $version != $params{user_version} ) {
119 0         0 die "Schema user_version mismatch (got $version, wanted $params{user_version})";
120             }
121              
122             # Generate the support package code
123 7         66 my $code = <<"END_PERL";
124             package $pkg;
125              
126             use strict;
127             use Carp ();
128             use DBI ();
129              
130             my \$DBH = undef;
131              
132             sub orlite { '$VERSION' }
133              
134             sub sqlite { '$file' }
135              
136             sub dsn { '$dsn' }
137              
138             sub dbh {
139             \$DBH or
140             \$_[0]->connect or
141             Carp::croak("connect: \$DBI::errstr");
142             }
143              
144             sub connect {
145             DBI->connect(\$_[0]->dsn);
146             }
147              
148             sub prepare {
149             shift->dbh->prepare(\@_);
150             }
151              
152             sub do {
153             shift->dbh->do(\@_);
154             }
155              
156             sub selectall_arrayref {
157             shift->dbh->selectall_arrayref(\@_);
158             }
159              
160             sub selectall_hashref {
161             shift->dbh->selectall_hashref(\@_);
162             }
163              
164             sub selectcol_arrayref {
165             shift->dbh->selectcol_arrayref(\@_);
166             }
167              
168             sub selectrow_array {
169             shift->dbh->selectrow_array(\@_);
170             }
171              
172             sub selectrow_arrayref {
173             shift->dbh->selectrow_arrayref(\@_);
174             }
175              
176             sub selectrow_hashref {
177             shift->dbh->selectrow_hashref(\@_);
178             }
179              
180             sub pragma {
181             \$_[0]->do("pragma \$_[1] = \$_[2]") if \@_ > 2;
182             \$_[0]->selectrow_arrayref("pragma \$_[1]")->[0];
183             }
184              
185             sub iterate {
186             my \$class = shift;
187             my \$call = pop;
188             my \$sth = \$class->prepare( shift );
189             \$sth->execute( \@_ );
190             while ( \$_ = \$sth->fetchrow_arrayref ) {
191             \$call->() or last;
192             }
193             \$sth->finish;
194             }
195              
196             END_PERL
197              
198             # Add transaction support if not readonly
199 7 100       62 $code .= <<"END_PERL" unless $readonly;
200             sub begin {
201             \$DBH or
202             \$DBH = \$_[0]->connect or
203             Carp::croak("connect: \$DBI::errstr");
204             \$DBH->begin_work;
205             }
206              
207             sub commit {
208             \$DBH or return 1;
209             \$DBH->commit;
210             \$DBH->disconnect;
211             undef \$DBH;
212             return 1;
213             }
214              
215             sub commit_begin {
216             if ( \$DBH ) {
217             \$DBH->commit;
218             \$DBH->begin_work;
219             } else {
220             \$_[0]->begin;
221             }
222             return 1;
223             }
224              
225             sub rollback {
226             \$DBH or return 1;
227             \$DBH->rollback;
228             \$DBH->disconnect;
229             undef \$DBH;
230             return 1;
231             }
232              
233             sub rollback_begin {
234             if ( \$DBH ) {
235             \$DBH->rollback;
236             \$DBH->begin_work;
237             } else {
238             \$_[0]->begin;
239             }
240             return 1;
241             }
242              
243             END_PERL
244              
245             # Optionally generate the table classes
246 7 100       69 if ( $params{tables} ) {
247             # Capture the raw schema information
248 5         121 my $tables = $dbh->selectall_arrayref(
249             'select * from sqlite_master where name not like ? and type = ?',
250             { Slice => {} }, 'sqlite_%', 'table',
251             );
252 5         2798 foreach my $table ( @$tables ) {
253 6         352 $table->{columns} = $dbh->selectall_arrayref(
254             "pragma table_info('$table->{name}')",
255             { Slice => {} },
256             );
257             }
258              
259             # Generate the main additional table level metadata
260 5         3581 my %tindex = map { $_->{name} => $_ } @$tables;
  6         47  
261 5         19 foreach my $table ( @$tables ) {
262 6         15 my @columns = @{ $table->{columns} };
  6         97  
263 6         16 my @names = map { $_->{name} } @columns;
  12         41  
264 6         16 $table->{cindex} = map { $_->{name} => $_ } @columns;
  12         43  
265              
266             # Discover the primary key
267 6         14 @{$table->{pk}} = map($_->{name}, grep { $_->{pk} } @columns);
  6         26  
  12         40  
268              
269             # What will be the class for this table
270 6         35 $table->{class} = ucfirst lc $table->{name};
271 6         59 $table->{class} =~ s/_([a-z])/uc($1)/ge;
  5         29  
272 6         27 $table->{class} = "${pkg}::$table->{class}";
273              
274             # Generate various SQL fragments
275 6         41 my $sql = $table->{sql} = { create => $table->{sql} };
276 6         16 $sql->{cols} = join ', ', map { '"' . $_ . '"' } @names;
  12         49  
277 6         38 $sql->{vals} = join ', ', ('?') x scalar @columns;
278 6         34 $sql->{select} = "select $table->{sql}->{cols} from $table->{name}";
279 6         26 $sql->{count} = "select count(*) from $table->{name}";
280 6         66 $sql->{insert} = join ' ',
281             "insert into $table->{name}" .
282             "( $table->{sql}->{cols} )" .
283             " values ( $table->{sql}->{vals} )";
284             }
285              
286             # Generate the foreign key metadata
287 5         17 foreach my $table ( @$tables ) {
288             # Locate the foreign keys
289 6         19 my %fk = ();
290 6         42 my @fk_sql = $table->{sql}->{create} =~ /[(,]\s*(.+?REFERENCES.+?)\s*[,)]/g;
291              
292             # Extract the details
293 6         22 foreach ( @fk_sql ) {
294 1 50       9 unless ( /^(\w+).+?REFERENCES\s+(\w+)\s*\(\s*(\w+)/ ) {
295 0         0 die "Invalid foreign key $_";
296             }
297 1         12 $fk{"$1"} = [ "$2", $tindex{"$2"}, "$3" ];
298             }
299 6         101 foreach ( @{ $table->{columns} } ) {
  6         23  
300 12         55 $_->{fk} = $fk{$_->{name}};
301             }
302             }
303              
304             # Generate the per-table code
305 5         17 foreach my $table ( @$tables ) {
306             # Generate the accessors
307 6         14 my $sql = $table->{sql};
308 6         11 my @columns = @{ $table->{columns} };
  6         23  
309 6         13 my @names = map { $_->{name} } @columns;
  12         40  
310              
311 6         23 my $i;
312 6         15 my %mapping = map { $_ => $i++ } @names;
  12         47  
313              
314             # Generate the elements in all packages
315 6         99 $code .= <<"END_PERL";
316             package $table->{class};
317              
318             sub base { '$pkg' }
319              
320             sub table { '$table->{name}' }
321              
322             sub select {
323             my \$class = shift;
324             my \$sql = '$sql->{select} ';
325             \$sql .= shift if \@_;
326             my \$rows = $pkg->selectall_arrayref( \$sql, {}, \@_ );
327             bless( \$_, '$table->{class}' ) foreach \@\$rows;
328             wantarray ? \@\$rows : \$rows;
329             }
330              
331             sub count {
332             my \$class = shift;
333             my \$sql = '$sql->{count} ';
334             \$sql .= shift if \@_;
335             $pkg->selectrow_array( \$sql, {}, \@_ );
336             }
337              
338             sub iterate {
339             my \$class = shift;
340             my \$call = pop;
341             my \$sql = '$sql->{select} ';
342             \$sql .= shift if \@_;
343             my \$sth = $pkg->prepare( \$sql );
344             \$sth->execute( \@_ );
345             while ( \$_ = \$sth->fetchrow_arrayref ) {
346             \$_ = [ \@{ \$_ } ];
347             bless( \$_, '$table->{class}' );
348             \$call->() or last;
349             }
350             \$sth->finish;
351             }
352              
353             END_PERL
354              
355             # Generate the elements for tables with primary keys
356 6 100 66     62 if ( defined $table->{pk} and ! $readonly ) {
357 5         11 my $nattr = join "\n", map { "\t\t\$attr{$_}," } @names;
  10         40  
358 5         18 my $pk_index = $mapping{ $table->{pk}->[0] };
359 5 100       11 my $fill_pk = scalar @{$table->{pk}} == 1
  5         53  
360             ? "\t\$self->[$pk_index] = \$dbh->func('last_insert_rowid') unless \$self->[$pk_index];"
361             : q{};
362 5         11 my $where_pk = join(' and ', map("$_ = ?", @{$table->{pk}}));
  5         30  
363 5         11 my $where_pk_attr = join("\n", map("\t\t\$self->[$mapping{$_}],", @{$table->{pk}}));
  5         31  
364 5         86 $code .= <<"END_PERL";
365              
366             sub new {
367             my \$class = shift;
368             my \%attr = \@_;
369             bless [
370             $nattr
371             ], \$class;
372             }
373              
374             sub create {
375             shift->new(\@_)->insert;
376             }
377              
378             sub insert {
379             my \$self = shift;
380             my \$dbh = $pkg->dbh;
381             \$dbh->do('$sql->{insert}', {},
382             \@\$self
383             );
384             $fill_pk
385             return \$self;
386             }
387              
388             sub delete {
389             my \$self = shift;
390             return $pkg->do(
391             'delete from $table->{name} where $where_pk',
392             {},
393             $where_pk_attr
394             ) if ref \$self;
395             Carp::croak("Must use truncate to delete all rows") unless \@_;
396             return $pkg->do(
397             'delete from $table->{name} ' . shift,
398             {}, \@_,
399             );
400             }
401              
402             sub truncate {
403             $pkg->do( 'delete from $table->{name}', {} );
404             }
405              
406             END_PERL
407              
408             }
409              
410             # Generate the accessors
411 6 100       20 $code .= join "\n\n", map { $_->{fk} ? <<"END_DIRECT" : <<"END_ACCESSOR" } @columns;
  12         131  
412             sub $_->{name} {
413             ($_->{fk}->[1]->{class}\->select('where $_->{fk}->[1]->{pk}->[0] = ?', \$_[0]->[$mapping{$_->{name}}]))[0];
414             }
415             END_DIRECT
416             sub $_->{name} : lvalue {
417             \$_[0]->[$mapping{$_->{name}}];
418             }
419             END_ACCESSOR
420              
421             }
422             }
423 7         524 $dbh->disconnect;
424              
425             # Add any custom code to the end
426 7 100       39 if ( defined $params{append} ) {
427 1 50       4 $code .= "\npackage $pkg;\n" if $params{tables};
428 1         5 $code .= "\n$params{append}";
429             }
430              
431             # Load the code
432 7 50       28 if ( $DEBUG ) {
433 0         0 dval("$code\n\n1;\n");
434             } else {
435 7 50 33 18   1317 eval("$code\n\n1;\n");
  44 0 66 16   261  
  23 100 33 8   49097  
  16 50   37   252  
  16 100   17   84  
  16 50   6   71  
  12 100   41   2889  
  47 50   7   295  
  13 50   6   9047  
  9 100   38   11751  
  13 50   13   93  
  6 100   1   31  
  6 0   2   3598  
  6 100   6   42701  
  6 100   1   563  
  1 50   5   2  
  1 50   8   2  
  1     2   5  
  1     0   722  
  1     9   11  
  1     10   3  
  1     1   103  
  2     0   5  
  2     10   5  
  2     5   6  
  2     5   10  
  2     1   14  
  2     0   2160  
  2     1   30  
  4     0   97  
  4         22  
  4         16  
  4         10  
  2         296  
  6         12  
  6         20  
  6         51  
  1         160  
  5         3559  
  5         30  
  8         19397  
  7         13803  
  2         183  
  2         90  
  2         4  
  2         63  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  9         22688  
  9         26  
  9         50  
  9         62  
  9         11602  
  9         118  
  10         97  
  2         8  
  1         6  
  11         47  
  6         1350  
  6         34  
  1         65  
  0         0  
  1         2240  
  0            
436 7 50       44 die $@ if $@;
437             }
438              
439 7         1093 return 1;
440             }
441              
442             sub dval {
443             # Write the code to the temp file
444 1     1 0 2060 my ($fh, $filename) = File::Temp::tempfile();
445 6         4562 $fh->print($_[0]);
446 3         1393 close $fh;
447 11         5187 require $filename;
448 10         52 unlink $filename;
449              
450             # Print the debugging output
451 1         14554 my @trace = map {
452 0         0 s/\s*[{;]$//;
453 1         100 s/^s/ s/;
454 1         3 s/^p/\np/;
455 1         76 "$_\n"
456             } grep {
457 2         580 /^(?:package|sub)\b/
458             } split /\n/, $_[0];
459             # print STDERR @trace, "\nCode saved as $filename\n\n";
460              
461 0         0 return 1;
462             }
463              
464             sub prune {
465 0     6 0 0 my $class = shift;
466 0         0 push @PRUNE, map { File::Spec->rel2abs($_) } @_;
  0         0  
467             }
468              
469             1;
470              
471             __END__