File Coverage

blib/lib/SQL/DB.pm
Criterion Covered Total %
statement 166 277 59.9
branch 29 76 38.1
condition 4 11 36.3
subroutine 34 65 52.3
pod 50 51 98.0
total 283 480 58.9


line stmt bran cond sub pod time code
1             package SQL::DB;
2 4     4   667721 use strict;
  4         10  
  4         175  
3 4     4   22 use warnings;
  4         9  
  4         148  
4 4     4   4275 use Moo;
  4         89026  
  4         25  
5 4     4   12424 use Log::Any qw/$log/;
  4         10554  
  4         18  
6 4     4   275 use Carp qw/croak carp confess/;
  4         8  
  4         360  
7 4     4   8892 use DBIx::Connector;
  4         84781  
  4         159  
8 4     4   2210 use SQL::DB::Schema;
  4         18  
  4         130  
9 4     4   31 use SQL::DB::Expr qw/:all/;
  4         11  
  4         33  
10 4     4   6684 use SQL::DB::Iter;
  4         19  
  4         234  
11              
12 4         445 use constant SQL_FUNCTIONS => qw/
13             bv
14             AND
15             OR
16             query
17             sql_and
18             sql_case
19             sql_cast
20             sql_coalesce
21             sql_concat
22             sql_count
23             sql_exists
24             sql_func
25             sql_hex
26             sql_length
27             sql_lower
28             sql_ltrim
29             sql_max
30             sql_min
31             sql_or
32             sql_replace
33             sql_rtrim
34             sql_substr
35             sql_sum
36             sql_table
37             sql_upper
38             sql_values
39 4     4   27 /;
  4         8  
40              
41 4         73 use Sub::Exporter -setup => {
42             exports => [SQL_FUNCTIONS],
43             groups => {
44             all => [SQL_FUNCTIONS],
45             default => [],
46             },
47 4     4   21 };
  4         7  
48              
49             our $VERSION = '0.971.0';
50              
51             ### CLASS FUNCTIONS ###
52              
53 1     1 1 7 sub bv { _bval(@_) }
54              
55             sub query {
56 0 0   0 1 0 confess 'query is not a method' if eval { $_[0]->isa('SQL::DB') };
  0         0  
57 0         0 return _query(@_);
58             }
59              
60 0     0 1 0 sub sql_and { _expr_join( ' AND ', @_ ) }
61              
62             sub sql_case {
63 0 0   0 1 0 @_ || croak 'case([$expr,] when => $expr, then => $val,[else...])';
64              
65 0         0 my $e = SQL::DB::Expr->new( _txt => ["CASE\n"] );
66              
67 0         0 while ( my ( $keyword, $item ) = splice( @_, 0, 2 ) ) {
68 0         0 $e .= ' ' . uc($keyword) . "\n ";
69              
70             # Need to do this separately because
71             # SQL::DB::Expr::Quote doesn't know how to '.='
72 0         0 $e .= _quote($item);
73 0         0 $e .= "\n";
74             }
75 0         0 $e .= ' END';
76 0         0 return $e;
77             }
78              
79 0     0 1 0 sub sql_coalesce { sql_func( 'COALESCE', @_ ) }
80              
81             sub sql_cast {
82 1     1 1 12 return _expr_join( ' ', 'CAST(', $_[0], 'AS', $_[2], ')' );
83             }
84              
85             sub sql_concat {
86 0     0 1 0 _expr_join( ' || ', map { _quote($_) } @_ );
  0         0  
87             }
88              
89             sub sql_count {
90 0     0 1 0 my $e = sql_func( 'COUNT', @_ );
91 0         0 $e->_type('integer');
92 0         0 return $e;
93             }
94              
95 0     0 1 0 sub sql_exists { 'EXISTS(' . _query(@_) . ')' }
96              
97             sub sql_func {
98 5     5 1 33 my $func = shift;
99 5         229 my $e = SQL::DB::Expr->new( _txt => [ $func . '(', ] );
100 5         37 $e .= _expr_join( ', ', map { _quote($_) } @_ ) . ')';
  10         1519  
101 5         26 return $e;
102             }
103              
104 0     0 1 0 sub sql_hex { sql_func( 'HEX', @_ ) }
105              
106 0     0 1 0 sub sql_length { sql_func( 'LENGTH', @_ ) }
107              
108 0     0 1 0 sub sql_lower { sql_func( 'LOWER', @_ ) }
109              
110 0     0 1 0 sub sql_ltrim { sql_func( 'LTRIM', @_ ) }
111              
112 0     0 1 0 sub sql_max { sql_func( 'MAX', @_ ) }
113              
114 0     0 1 0 sub sql_min { sql_func( 'MIN', @_ ) }
115              
116 0     0 1 0 sub sql_or { _expr_join( ' OR ', @_ ) }
117              
118 0     0 1 0 sub sql_replace { sql_func( 'REPLACE', @_ ) }
119              
120 0     0 1 0 sub sql_rtrim { sql_func( 'RTRIM', @_ ) }
121              
122 0     0 1 0 sub sql_substr { sql_func( 'SUBSTR', @_ ) }
123              
124 0     0 1 0 sub sql_sum { sql_func( 'SUM', @_ ) }
125              
126             sub sql_table {
127 3     3 1 13 my $table = shift;
128 3         194 return SQL::DB::Expr->new(
129             _txt => [ $table . '(' . join( ', ', @_ ) . ')' ] );
130             }
131              
132 0     0 1 0 sub sql_upper { sql_func( 'UPPER', @_ ) }
133              
134 5     5 1 1647 sub sql_values { sql_func( 'VALUES', @_ ) }
135              
136             ### OBJECT IMPLEMENTATION ###
137              
138             has 'conn' => ( is => 'ro' );
139              
140             has 'dbd' => ( is => 'ro' );
141              
142             has 'table_info' => (
143             is => 'ro',
144             isa => sub {
145             ref $_[0] eq 'HASH'
146             || confess 'table_info must be a HASH ref';
147             },
148             default => sub { {} },
149             );
150              
151             has 'schema' => ( is => 'ro' );
152              
153             has 'cache_sth' => (
154             is => 'rw',
155             default => sub { 1 },
156             );
157              
158             has '_current_timestamp' => ( is => 'rw', init_arg => undef );
159              
160             my %schemas;
161              
162             around BUILDARGS => sub {
163             my $orig = shift;
164             my $class = shift;
165             my %args = @_;
166              
167             $args{dsn} || confess 'Missing argument: dsn';
168             my ( $dbi, $dbd, @rest ) = DBI->parse_dsn( $args{dsn} );
169              
170             $args{dbd} = $dbd;
171              
172             # auto-generate the name in a semi-random way
173             $args{schema} = SQL::DB::Schema->new( name => \%args );
174              
175             my $attr = {
176             PrintError => 0,
177             ChopBlanks => 1,
178             $dbd eq 'Pg' ? ( pg_enable_utf8 => 1 ) : (),
179             $dbd eq 'SQLite' ? ( sqlite_unicode => 1 ) : (),
180             $dbd eq 'mysql' ? ( mysql_enable_utf8 => 1 ) : (),
181             %{ $args{attr} || {} },
182             RaiseError => 1,
183             AutoCommit => 1,
184             Callbacks => {
185             connected => sub {
186             my $h = shift;
187             if ( $dbd eq 'Pg' ) {
188             $h->do('SET client_min_messages = WARNING;');
189             $h->do("SET TIMEZONE TO 'UTC';");
190             }
191             elsif ( $dbd eq 'SQLite' ) {
192             $h->do('PRAGMA foreign_keys = ON;');
193             }
194             return;
195             },
196             }
197             };
198              
199             $args{conn} =
200             DBIx::Connector->new( $args{dsn}, $args{username}, $args{password},
201             $attr );
202              
203             $log->debug( 'Connected to ' . $args{dsn} );
204             $args{conn}->mode('fixup');
205              
206             return $class->$orig(%args);
207             };
208              
209             # For our extensions to 'around' or 'after'
210 5     5 0 174 sub BUILD {
211             }
212              
213             sub connect {
214 2     2 1 24279 my $class = shift;
215 2         16 my $dsn = shift;
216 2         15 my $username = shift;
217 2         8 my $password = shift;
218 2   50     42 my $attr = shift || {};
219              
220 2         42 return $class->new(
221             dsn => $dsn,
222             username => $username,
223             password => $password,
224             attr => $attr,
225             );
226             }
227              
228             sub _load_tables {
229 2     2   9 my $self = shift;
230              
231 2         6 my %seen;
232 2         16 foreach my $table (@_) {
233 2 50       17 next if $seen{$table}++;
234              
235 2 50       27 if ( my $info = $self->table_info->{$table} ) {
236 0         0 $self->schema->define($info);
237 0         0 next;
238             }
239              
240 2         19 $log->debug( 'Loading table schema: ' . $table );
241 2         33 my $sth = $self->conn->dbh->column_info( '%', '%', $table, '%' );
242 2         10481 $self->schema->define( $sth->fetchall_arrayref );
243             }
244 2         19 my @still_not_known = $self->schema->not_known(@_);
245 2 50       16 confess "tables not in database: @still_not_known" if @still_not_known;
246             }
247              
248             sub irows {
249 1     1 1 3 my $self = shift;
250              
251 1         4 my @results = eval { $self->schema->irows(@_) };
  1         22  
252 1 50       18 return @results unless $@;
253              
254 1         9 $self->_load_tables( $self->schema->not_known(@_) );
255 1         8 return $self->schema->irows(@_);
256             }
257              
258             sub irow {
259 1     1 1 3 my $self = shift;
260 1 50       7 croak 'irow($row)' if @_ != 1;
261 1         5 return ( $self->irows(shift) )[0];
262             }
263              
264             sub urows {
265 3     3 1 6 my $self = shift;
266              
267 3         6 my @results = eval { $self->schema->urows(@_) };
  3         34  
268 3 50       19 return @results unless $@;
269              
270 0         0 $self->_load_tables( $self->schema->not_known(@_) );
271 0         0 return $self->schema->urows(@_);
272             }
273              
274             sub urow {
275 3     3 1 10 my $self = shift;
276 3 50       15 croak 'urow($row)' if @_ != 1;
277 3         14 return ( $self->urows(shift) )[0];
278             }
279              
280             sub srows {
281 4     4 1 27 my $self = shift;
282              
283 4         7 my @results = eval { $self->schema->srows(@_) };
  4         52  
284 4 100       25 return @results unless $@;
285              
286 1         15 $self->_load_tables( $self->schema->not_known(@_) );
287 1         10 return $self->schema->srows(@_);
288             }
289              
290             sub srow {
291 4     4 1 118 my $self = shift;
292 4 50       21 croak 'srow($row)' if @_ != 1;
293 4         21 return ( $self->srows(shift) )[0];
294             }
295              
296             sub _prepare {
297 17     17   34 my $self = shift;
298 17         44 my $prepare = shift;
299 17         80 my $query = _query(@_);
300              
301             return $self->conn->run(
302             sub {
303 17     17   1240 my $dbh = $_;
304 17         88 my ( $sql, $values, $types ) = $query->_sql_values_types($dbh);
305              
306 17 50       171 $log->debugf(
307             "/* $prepare with %d bind values*/\n%s",
308             scalar @$values,
309             $query->_as_pretty($dbh)
310             ) if $log->is_debug;
311              
312 17         92 my $sth = eval { $dbh->$prepare($sql) };
  17         233  
313 17 50       2527 if ($@) {
314 0         0 confess $@ . "Statement was:\n" . $dbh->{Statement};
315             }
316              
317 17         38 my $i = 0;
318 17         51 foreach my $val (@$values) {
319 12         20 $i++;
320 12         23 my $type = shift @$types;
321 12         59 $log->debugf( 'binding param %d as %s', $i, $type );
322 12         114 $sth->bind_param( $i, $val, $type );
323             }
324              
325 17         103 return ( $query, $sth );
326             },
327 17         351 );
328             }
329              
330             sub prepare {
331 0     0 1 0 my $self = shift;
332 0         0 my ( $query, $sth ) = $self->_prepare( 'prepare', @_ );
333 0         0 return $sth;
334             }
335              
336             sub prepare_cached {
337 0     0 1 0 my $self = shift;
338 0         0 my ( $query, $sth ) = $self->_prepare( 'prepare_cached', @_ );
339 0         0 return $sth;
340             }
341              
342             sub sth {
343 0     0 1 0 my $self = shift;
344 0 0       0 my ( $query, $sth ) =
345             $self->cache_sth
346             ? $self->_prepare( 'prepare_cached', @_ )
347             : $self->_prepare( 'prepare', @_ );
348 0         0 my $rv = eval { $sth->execute() };
  0         0  
349 0 0       0 if ($@) {
350 0         0 confess 'Error: ' . $query->_as_pretty( $self->conn->dbh ) . "\n$@";
351             }
352 0         0 return $sth;
353             }
354              
355             sub do {
356 6     6 1 175 my $self = shift;
357 6 50       66 my ( $query, $sth ) =
358             $self->cache_sth
359             ? $self->_prepare( 'prepare_cached', @_ )
360             : $self->_prepare( 'prepare', @_ );
361 6         125 my $rv = eval { $sth->execute() };
  6         153239  
362 6 50       123 if ($@) {
363 0         0 confess 'Error: ' . $query->_as_pretty( $self->conn->dbh ) . "\n$@";
364             }
365 6         95 $log->debug( "-- Result:", $rv );
366 6         120 $sth->finish();
367 6         143 return $rv;
368             }
369              
370             sub iter {
371 11     11 1 40 my $self = shift;
372 11 50       38 confess 'iter(@query)' unless @_;
373              
374 11 50       105 my ( $query, $sth ) =
375             $self->cache_sth
376             ? $self->_prepare( 'prepare_cached', @_ )
377             : $self->_prepare( 'prepare', @_ );
378              
379 11         207 my $rv = eval { $sth->execute() };
  11         1651  
380 11 50       42 if ($@) {
381 0         0 confess 'Error: ' . $query->_as_pretty( $self->conn->dbh ) . "\n$@";
382             }
383 11         60 $log->debug( "-- Result:", $rv );
384 11         333 return SQL::DB::Iter->new( sth => $sth );
385             }
386              
387             sub fetch {
388 2     2 1 15 my $self = shift;
389 2         10 return $self->iter(@_)->all;
390             }
391              
392             sub fetch1 {
393 5     5 1 15 my $self = shift;
394 5         39 my $iter = $self->iter(@_);
395 5         30 my $first = $iter->next;
396              
397 5         32 $iter->finish;
398 5         23 return $first;
399             }
400              
401             sub array {
402 0     0 1 0 my $self = shift;
403 0         0 my $iter = $self->iter(@_);
404 0         0 my $ref = $iter->array;
405 0         0 $iter->finish;
406 0         0 return $ref;
407             }
408              
409             sub arrays {
410 0     0 1 0 my $self = shift;
411 0         0 return $self->iter(@_)->arrays;
412             }
413              
414             sub hash {
415 0     0 1 0 my $self = shift;
416 0         0 my $iter = $self->iter(@_);
417 0         0 my $ref = $iter->hash;
418 0         0 $iter->finish;
419 0         0 return $ref;
420             }
421              
422             sub hashes {
423 0     0 1 0 my $self = shift;
424 0         0 return $self->iter(@_)->hashes;
425             }
426              
427             sub object {
428 0     0 1 0 my $self = shift;
429 0         0 my $iter = $self->iter(@_);
430 0         0 my $object = $iter->object;
431 0         0 $iter->finish;
432 0         0 return $object;
433             }
434              
435             sub objects {
436 0     0 1 0 my $self = shift;
437 0         0 return $self->iter(@_)->objects;
438             }
439              
440             sub current_timestamp {
441 0     0 1 0 my $self = shift;
442 0 0       0 return $self->_current_timestamp if $self->_current_timestamp;
443              
444 0         0 my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = gmtime;
445 0         0 $mon += 1;
446 0         0 $year += 1900;
447 0         0 return sprintf( '%04d-%02d-%02d %02d:%02d:%02dZ',
448             $year, $mon, $mday, $hour, $min, $sec );
449             }
450              
451             sub txn {
452 0     0 1 0 my $wantarray = wantarray;
453              
454 0         0 my $self = shift;
455 0         0 my $set_timestamp = !$self->_current_timestamp;
456              
457 0 0       0 if ($set_timestamp) {
458 0         0 $log->debug('BEGIN TRANSACTION;');
459 0         0 $self->_current_timestamp( $self->current_timestamp );
460             }
461              
462 0         0 my @ret = $self->conn->txn(@_);
463              
464 0 0       0 if ($set_timestamp) {
465 0         0 $log->debug('COMMIT;');
466 0         0 $self->_current_timestamp(undef);
467             }
468              
469 0 0       0 return $wantarray ? @ret : $ret[0];
470             }
471              
472             # $db->insert_into('customers',
473             # values => {cid => 1, name => 'Mark'}
474             # );
475             sub insert {
476 2     2 1 6 my $self = shift;
477 2         5 my $str_into = shift;
478 2         101 my $table = shift;
479 2         4 my $str_values = shift;
480 2         5 my $values = shift;
481              
482 2 50 33     34 unless ($str_into eq 'into'
      33        
      33        
483             and $str_values eq 'values'
484             and ( ref $values eq 'HASH' || eval { $values->isa('HASH') } ) )
485             {
486 0         0 confess 'usage: insert(into => $table, values => $hashref)';
487             }
488              
489 2         10 my $urow = $self->urow($table);
490              
491 2         9 my @cols = sort grep { $urow->can($_) } keys %$values;
  4         30  
492 2         8 my @invalid = sort grep { !$urow->can($_) } keys %$values;
  4         18  
493 2         6 my @vals = map { _bval( $values->{$_}, $urow->$_->_type ) } @cols;
  4         21  
494              
495 2 50       9 $log->debug( "columns not in table '$table': @invalid\n at", caller )
496             if @invalid;
497 2 50       6 confess 'insert_into requires columns/values' unless @cols;
498              
499 2         9 return 0 + $self->do(
500             insert_into => sql_table( $table, @cols ),
501             sql_values(@vals),
502             );
503             }
504              
505             # $db->update('purchases',
506             # set => {pid => 2},
507             # where => {cid => 1},
508             # );
509             sub update {
510 0     0 1 0 my $self = shift;
511 0         0 my $table = shift;
512 0         0 shift;
513 0         0 my $set = shift;
514 0         0 shift;
515 0         0 my $where = shift;
516              
517 0         0 my $urow = $self->urow($table);
518 0 0       0 my @updates = map { $urow->$_( $set->{$_} ) }
  0         0  
519 0         0 grep { $urow->can($_) and !exists $where->{$_} } keys %$set;
520              
521 0 0       0 unless (@updates) {
522 0         0 $log->debug( "Nothing to update for table:", $table );
523 0         0 return 0;
524             }
525              
526 0         0 my $expr;
527 0 0       0 if ( my @keys = keys %$where ) {
528 0         0 $expr =
529             _expr_join( ' AND ',
530 0         0 map { $urow->$_ == $where->{$_} } grep { $urow->can($_) } @keys );
  0         0  
531             }
532              
533 0 0       0 return 0 + $self->do(
534             update => $urow,
535             set => \@updates,
536             $expr ? ( where => $expr ) : (),
537             );
538             }
539              
540             # $db->delete_from('purchases',
541             # where => {cid => 1},
542             # );
543              
544             sub delete {
545 1     1 1 2 my $self = shift;
546 1         3 shift;
547 1         3 my $table = shift;
548 1         2 shift;
549 1         2 my $where = shift;
550              
551 1         5 my $urow = $self->urow($table);
552              
553 1         2 my $expr;
554 1 50       7 if ( my @keys = keys %$where ) {
555 0         0 $expr = _expr_join( ' AND ', map { $urow->$_ == $where->{$_} } @keys );
  0         0  
556             }
557              
558 1 50       10 return 0 + $self->do(
559             delete_from => $urow,
560             $expr ? ( where => $expr ) : (),
561             );
562             }
563              
564             # my @objs = $db->select( ['pid','label],
565             # from => 'customers',
566             # where => {cid => 1},
567             # );
568             sub select {
569 2     2 1 4 my $self = shift;
570 2         5 my $list = shift;
571 2         4 shift;
572 2         4 my $table = shift;
573 2         2 shift;
574 2         5 my $where = shift;
575              
576 2         10 my $srow = $self->srow($table);
577 2         7 my @columns = map { $srow->$_ } @$list;
  4         28  
578              
579 2 50       18 @columns || confess 'select requires columns';
580              
581 2         3 my $expr;
582 2 100       11 if ( my @keys = keys %$where ) {
583 1         3 $expr = _expr_join( ' AND ', map { $srow->$_ == $where->{$_} } @keys );
  1         5  
584             }
585              
586 2 50       15 return $self->fetch(
    100          
587             select => \@columns,
588             from => $srow,
589             $expr ? ( where => $expr ) : (),
590             ) if wantarray;
591              
592 1 50       8 return $self->fetch1(
593             select => \@columns,
594             from => $srow,
595             $expr ? ( where => $expr ) : (),
596             );
597             }
598              
599             1;