File Coverage

blib/lib/Acrux/DBI.pm
Criterion Covered Total %
statement 96 233 41.2
branch 16 98 16.3
condition 17 76 22.3
subroutine 25 45 55.5
pod 28 28 100.0
total 182 480 37.9


line stmt bran cond sub pod time code
1             package Acrux::DBI;
2 5     5   510181 use strict;
  5         8  
  5         184  
3 5     5   1976 use utf8;
  5         1153  
  5         34  
4              
5             =encoding utf8
6              
7             =head1 NAME
8              
9             Acrux::DBI - Database independent interface for Acrux applications
10              
11             =head1 SYNOPSIS
12              
13             use Acrux::DBI;
14              
15             =head1 DESCRIPTION
16              
17             Database independent interface for Acrux applications
18              
19             =head2 new
20              
21             my $dbi = Acrux::DBI->new( $db_url );
22             my $dbi = Acrux::DBI->new( $db_url, { ... options ... });
23             my $dbi = Acrux::DBI->new( $db_url, ... options ...);
24              
25             Build new Acrux::DBI object
26              
27             B
28              
29             =over 8
30              
31             =item autoclean
32              
33             This options turns on auto disconnecting on DESTROY phase
34              
35             =back
36              
37             See also list of default options in L
38              
39             =head1 METHODS
40              
41             This class implements the following methods
42              
43             =head2 begin
44              
45             $dbi->begin;
46             # ...
47             $dbi->commit; # ..or $dbi->rollback
48              
49             This is a transaction method!
50              
51             This method marks the starting point for the start of a transaction
52              
53             $dbi->begin;
54             $dbi->query('insert into test values (?)', 'Foo');
55             $dbi->query('insert into test values (?)', 'Bar');
56             $dbi->commit;
57              
58             See slso L, L, L
59              
60             =head2 cache
61              
62             my $cache = $dbi->cache;
63              
64             Returns the L object
65              
66             =head2 cachekey
67              
68             my $cachekey = $dbi->cachekey;
69              
70             Returns the key name of the cached connect (See L)
71              
72             =head2 cleanup
73              
74             $dbi = $dbi->cleanup;
75              
76             This internal method to cleanup database handler
77              
78             =head2 commit
79              
80             $dbi->begin;
81             # ...
82             $dbi->commit;
83              
84             This is a transaction method!
85              
86             This method accepts all changes to the database and marks the end
87             point for the transaction to complete
88              
89             See also L, L
90              
91             =head2 connect
92              
93             my $dbi = $dbi->connect;
94             die $dbi->error if $dbi->error;
95              
96             This method makes a connection to the database
97              
98             =head2 connect_cached
99              
100             my $dbi = $dbi->connect_cached;
101             die $dbi->error if $dbi->error;
102              
103             This method makes a cached connection to the database. See L for details
104              
105             =head2 database
106              
107             my $database = $dbi->database;
108              
109             This method returns the database that will be used for generating the connection DSN
110             This will be used as L
111              
112             Default: none
113              
114             =head2 dbh
115              
116             my $dbh = $dbi->dbh;
117              
118             Returns database handle used for all queries
119              
120             =head2 disconnect
121              
122             my $dbi = $dbi->disconnect;
123              
124             This method disconnects from the database
125              
126             =head2 driver
127              
128             my $driver = $dbi->driver;
129              
130             This is the L that will be used for generating the connection DSN
131              
132             Default: C
133              
134             =head2 dsn
135              
136             my $dsn = $dbi->dsn;
137             my $dsn = $dbi->dsn('DBI:SQLite::memory:');
138              
139             This method generates the connection DSN and returns it or
140             returns already generated earley.
141              
142             =head2 dump
143              
144             my $dump = $dbi->dump;
145             my $dump = $dbi->dump(name => 'schema');
146              
147             This method returns instance of L class that you
148             can use to change your database schema more easily
149              
150             # Load SQL dump file and import schema to database
151             $dbi->dump->from_file('/tmp/schema.sql')->poke('foo');
152              
153             See L for details
154              
155             =head2 err
156              
157             my $err = $dbi->err;
158              
159             This method just returns C<$DBI::err> value
160              
161             =head2 errstr
162              
163             my $errstr = $dbi->errstr;
164              
165             This method just returns C<$DBI::errstr> value
166              
167             =head2 error
168              
169             my $error = $dbi->error;
170              
171             Returns error string if occurred any errors while working with database
172              
173             $dbi = $dbi->error( "error text" );
174              
175             Sets new error message and returns object
176              
177             =head2 host
178              
179             my $host = $dbi->host;
180              
181             This is the L that will be used for generating the connection DSN
182              
183             Default: C
184              
185             =head2 options
186              
187             my $options = $dbi->options;
188              
189             This method returns options that will be used for generating the connection DSN
190              
191             Default: all passed options to constructor merged with system defaults:
192              
193             RaiseError => 0,
194             PrintError => 0,
195             PrintWarn => 0,
196              
197             =head2 password
198              
199             my $password = $dbi->password;
200              
201             This is the L that will be used for generating the connection DSN
202              
203             default: none
204              
205             =head2 ping
206              
207             $dbi->ping ? 'OK' : 'Database session is expired';
208              
209             Checks the connection to database
210              
211             =head2 port
212              
213             my $port = $dbi->port;
214              
215             This is the L that will be used for generating the connection DSN
216              
217             Default: none
218              
219             =head2 query
220              
221             my $res = $dbi->query('select * from test');
222             my $res = $dbi->query('insert into test values (?, ?)', @values);
223              
224             Execute a blocking statement and return a L object with the results.
225             You can also append a 'bind_callback' to perform binding value manually:
226              
227             my $res = $dbi->query('insert into test values (?, ?)', {
228             bind_callback => sub {
229             my $sth = shift;
230             $sth->bind_param( ... );
231             }
232             });
233              
234             =head2 rollback
235              
236             $dbi->begin;
237             # ...
238             $dbi->rollback;
239              
240             This is a transaction method!
241              
242             This method discards all changes to the database and marks the end
243             point for the transaction to complete
244              
245             See also L, L
246              
247             =head2 transaction
248              
249             my $tx = $dbi->transaction;
250              
251             Begin transaction and return L object, which will automatically
252             roll back the transaction unless L has been called before
253             it is destroyed
254              
255             # Insert rows in a transaction
256             eval {
257             my $tx = $dbi->transaction;
258             $dbi->query( ... );
259             $dbi->query( ... );
260             $tx->commit;
261             };
262             say $@ if $@;
263              
264             =head2 url
265              
266             my $url = $dbi->url;
267             $dbi = $dbi->url('sqlite:///tmp/test.db?sqlite_unicode=1');
268             $dbi = $dbi->url('sqlite:///./test.db?sqlite_unicode=1'); # '/./' will be removed
269             $dbi = $dbi->url('postgres://foo:pass@localhost/mydb?PrintError=1');
270             $dbi = $dbi->url('mysql://foo:pass@localhost/test?mysql_enable_utf8=1');
271              
272             Database connect url
273              
274             The database connection URL from which all other attributes can be derived.
275             C<"url"> must be specified before the first call to C<"connect"> is made,
276             otherwise it will have no effect on setting the defaults.
277              
278             For using SQLite databases with files relative to current directory you cat use '/./' prefix:
279              
280             # '/./' will be removed automatically
281             $dbi = $dbi->url('sqlite:///./test.db?sqlite_unicode=1');
282              
283             Default: C<"sponge://">
284              
285             =head2 username
286              
287             my $username = $dbi->username;
288              
289             This is the L that will be used for generating the connection DSN
290              
291             default: none
292              
293             =head2 userinfo
294              
295             my $userinfo = $dbi->userinfo;
296              
297             This is the L that will be used for generating the connection DSN
298              
299             default: none
300              
301             =head1 HISTORY
302              
303             See C file
304              
305             =head1 TO DO
306              
307             See C file
308              
309             =head1 SEE ALSO
310              
311             L, L, L, L, L
312              
313             =head1 AUTHOR
314              
315             Serż Minus (Sergey Lepenkov) L Eabalama@cpan.orgE
316              
317             =head1 COPYRIGHT
318              
319             Copyright (C) 1998-2026 D&D Corporation
320              
321             =head1 LICENSE
322              
323             This program is distributed under the terms of the Artistic License Version 2.0
324              
325             See the C file or L for details
326              
327             =cut
328              
329             our $VERSION = '0.04';
330              
331 5     5   430 use Carp qw/carp croak/;
  5         8  
  5         297  
332 5     5   23 use Scalar::Util 'weaken';
  5         14  
  5         178  
333 5     5   7318 use DBI qw//;
  5         92803  
  5         267  
334 5     5   3692 use Mojo::Util qw/monkey_patch md5_sum/;
  5         1080572  
  5         681  
335 5     5   2710 use Mojo::URL qw//;
  5         49340  
  5         212  
336 5     5   2449 use Mojo::Cache;
  5         2966  
  5         36  
337 5     5   3065 use Acrux::Util qw//;
  5         182516  
  5         209  
338 5     5   2817 use Acrux::RefUtil qw/is_array_ref is_code_ref/;
  5         12528  
  5         603  
339 5     5   3121 use Acrux::DBI::Res;
  5         62  
  5         303  
340 5     5   2969 use Acrux::DBI::Tx;
  5         20  
  5         186  
341 5     5   3280 use Acrux::DBI::Dump;
  5         40  
  5         98  
342              
343             use constant {
344 5   50     18198 DEBUG => $ENV{ACRUX_DBI_DEBUG} || 0,
345             DEFAULT_DBI_URL => 'sponge://',
346             DEFAULT_DBI_DSN => 'DBI:Sponge:',
347             DEFAULT_DBI_OPTS => {
348             RaiseError => 0,
349             PrintError => 0,
350             PrintWarn => 0,
351             },
352 5     5   411 };
  5         20  
353              
354             # Set method ping to DBD::Sponge
355 0     0   0 monkey_patch 'DBD::Sponge::db', ping => sub { 1 };
356              
357             sub new {
358 1     1 1 207897 my $class = shift;
359 1   50     9 my $url = shift || DEFAULT_DBI_URL;
360 1 50       4 croak 'Invalid DBI URL' unless $url;
361 1 50       9 my $opts = scalar(@_) ? scalar(@_) > 1 ? {@_} : {%{$_[0]}} : {};
  0 50       0  
362 1         14 my $uri = Mojo::URL->new($url);
363              
364             # Default attributes
365 1         529 my %_opts = (%{(DEFAULT_DBI_OPTS)}, %$opts);
  1         10  
366 1         4 my $autoclean = delete $_opts{autoclean};
367              
368 1 50       16 my $self = bless {
369             url => $url,
370             uri => $uri,
371             dsn => '',
372             cachekey=> '',
373             driver => '',
374             dbh => undef,
375             error => "", # Ok
376             autoclean => $autoclean ? 1 : 0,
377             opts => {%_opts},
378             cache => Mojo::Cache->new,
379             }, $class;
380 1         21 return $self;
381             }
382              
383             # Attributes
384             sub url {
385 1     1 1 7 my $self = shift;
386 1 50       5 if (scalar(@_) >= 1) {
387 1         6 $self->{url} = shift;
388 1         5 $self->{uri}->parse($self->{url});
389 1         214 $self->{dsn} = '';
390 1         2 $self->{cachekey} = '';
391 1         3 $self->{driver} = '';
392 1         3 return $self;
393             }
394 0         0 return $self->{url};
395             }
396             sub driver { # scheme
397 4     4 1 11 my $self = shift;
398 4   66     20 $self->{driver} ||= $self->{uri}->protocol;
399             }
400             sub host {
401 2     2 1 1022 my $self = shift;
402 2   50     11 return $self->{uri}->host || 'localhost';
403             }
404             sub port {
405 2     2 1 6 my $self = shift;
406 2   50     13 return $self->{uri}->port // '';
407             }
408             sub options {
409 0     0 1 0 my $self = shift;
410 0         0 my $opts = $self->{opts}; # defaults
411 0         0 my $query = $self->{uri}->query;
412 0         0 my %params = ();
413 0         0 $params{$_} = $query->param($_) for @{$query->names};
  0         0  
414 0         0 return { (%$opts, %params) } ; # merge defaults and URL params
415             }
416             sub username {
417 1     1 1 3 my $self = shift;
418 1   50     7 return $self->{uri}->username // '';
419             }
420             sub password {
421 1     1 1 2 my $self = shift;
422 1   50     7 return $self->{uri}->password // '';
423             }
424             sub userinfo {
425 1     1 1 4 my $self = shift;
426 1   50     7 return $self->{uri}->userinfo // '';
427             }
428             sub database {
429 2     2 1 6 my $self = shift;
430 2         4 my $u = $self->{uri};
431 2         10 my $dr = $self->driver;
432 2         61 my $db = '';
433 2 50 33     16 if ($dr eq 'sqlite' or $dr eq 'file') {
434 0   0     0 $db = $u->path->leading_slash(1)->trailing_slash(0)->to_string // '';
435 0         0 $db =~ s/^\/+\.\///;
436             } else {
437 2   50     13 $db = $u->path->leading_slash(0)->trailing_slash(0)->to_string // '';
438             }
439 2         722 return $db;
440             }
441             sub dsn {
442 1     1 1 3 my $self = shift;
443 1 50       6 $self->{dsn} = shift if scalar(@_) >= 1;
444 1 50       5 return $self->{dsn} if $self->{dsn};
445 1         4 my $dr = $self->driver;
446              
447             # Set DSN
448 1         3 my @params = ();
449 1         2 my $dsn = '';
450 1         3 my $db = $self->database;
451 1 50 33     26 if ($dr eq 'sqlite' or $dr eq 'file') {
    50 33        
    50 33        
    50 33        
    0 33        
452 0         0 $dsn = sprintf('DBI:SQLite:dbname=%s', $db);
453             } elsif ($dr eq 'mysql') {
454 0 0       0 push @params, sprintf("%s=%s", "database", $db) if length $db;
455 0         0 push @params, sprintf("%s=%s", "host", $self->host);
456 0 0       0 push @params, sprintf("%s=%s", "port", $self->port) if $self->port;
457 0   0     0 $dsn = sprintf('DBI:mysql:%s', join(";", @params) || '');
458             } elsif ($dr eq 'maria' or $dr eq 'mariadb') {
459 0 0       0 push @params, sprintf("%s=%s", "database", $db) if length $db;
460 0         0 push @params, sprintf("%s=%s", "host", $self->host);
461 0 0       0 push @params, sprintf("%s=%s", "port", $self->port) if $self->port;
462 0   0     0 $dsn = sprintf('DBI:MariaDB:%s', join(";", @params) || '');
463             } elsif ($dr eq 'pg' or $dr eq 'pgsql' or $dr eq 'postgres' or $dr eq 'postgresql') {
464 1 50       6 push @params, sprintf("%s=%s", "dbname", $db) if length $db;
465 1         5 push @params, sprintf("%s=%s", "host", $self->host);
466 1 50       12 push @params, sprintf("%s=%s", "port", $self->port) if $self->port;
467 1   50     12 $dsn = sprintf('DBI:Pg:%s', join(";", @params) || '');
468             } elsif ($dr eq 'oracle') {
469 0         0 push @params, sprintf("%s=%s", "host", $self->host);
470 0 0       0 push @params, sprintf("%s=%s", "sid", $db) if length $db;
471 0 0       0 push @params, sprintf("%s=%s", "port", $self->port) if $self->port;
472 0   0     0 $dsn = sprintf('DBI:Oracle:%s', join(";", @params) || '');
473             } else {
474 0         0 $dsn = DEFAULT_DBI_DSN;
475             }
476              
477 1         6 $self->{dsn} = $dsn;
478             }
479 0     0 1 0 sub cache { shift->{cache} }
480             sub cachekey {
481 0     0 1 0 my $self = shift;
482 0 0       0 return $self->{cachekey} if $self->{cachekey};
483              
484             # Generate cachekey data
485 0         0 my $opts = $self->{opts}; # defaults
486 0         0 my @pairs = ();
487 0         0 foreach my $k (sort { $a cmp $b } keys %$opts) {
  0         0  
488 0   0     0 push @pairs, "$k=" . ($opts->{$k} // '');
489             }
490 0         0 my $sfx = join ";", @pairs;
491 0         0 $self->{cachekey} = md5_sum($self->{url} . $sfx);
492             }
493 0     0 1 0 sub dbh { shift->{dbh} }
494              
495             # Methods
496             sub error {
497 0     0 1 0 my $self = shift;
498 0 0       0 if (scalar(@_) >= 1) {
499 0         0 $self->{error} = shift;
500 0         0 return $self;
501             }
502 0         0 return $self->{error};
503             }
504             sub err {
505 0     0 1 0 my $self = shift;
506 0 0 0     0 return $self->dbh->err // $DBI::err if defined($self->dbh) && $self->dbh->can('err');
      0        
507 0         0 return $DBI::err;
508             }
509             sub errstr {
510 0     0 1 0 my $self = shift;
511 0 0 0     0 return $self->dbh->errstr // $DBI::errstr if defined($self->dbh) && $self->dbh->can('errstr');
      0        
512 0         0 return $DBI::errstr;
513             }
514              
515             # Database methods
516             sub connect {
517 0     0 1 0 my $self = shift;
518 0         0 $self->{error} = '';
519 0         0 my $dbh = DBI->connect($self->dsn, $self->username, $self->password, $self->options);
520 0 0       0 if ($dbh) {
521 0         0 $self->{dbh} = $dbh;
522 0         0 printf STDERR "Connected to '%s'\n", $self->dsn if DEBUG;
523             } else {
524 0   0     0 $self->{error} = $DBI::errstr || "DBI->connect failed";
525 0         0 $self->{dbh} = undef;
526             }
527 0         0 return $self;
528             }
529             sub connect_cached {
530 0     0 1 0 my $self = shift;
531 0         0 $self->{error} = '';
532 0         0 my %opts = %{($self->options)};
  0         0  
533 0         0 $opts{private_cachekey} = $self->cachekey;
534 0         0 my $dbh = DBI->connect_cached($self->dsn, $self->username, $self->password, {%opts});
535 0 0       0 if ($dbh) {
536 0         0 $self->{dbh} = $dbh;
537 0         0 printf STDERR "Connected (cached) to '%s'\n", $self->dsn if DEBUG;
538             } else {
539 0   0     0 $self->{error} = $DBI::errstr || "DBI->connect failed";
540 0         0 $self->{dbh} = undef;
541             }
542 0         0 return $self;
543             }
544             sub disconnect {
545 0     0 1 0 my $self = shift;
546 0 0       0 return unless my $dbh = $self->dbh;
547 0         0 $dbh->disconnect;
548 0         0 printf STDERR "Disconnected from '%s'\n", $self->dsn if DEBUG;
549 0         0 $self->cleanup;
550             }
551             sub ping {
552 0     0 1 0 my $self = shift;
553 0 0       0 return 0 unless $self->{dsn};
554 0 0       0 return 0 unless my $dbh = $self->dbh;
555 0 0       0 return 0 unless $dbh->can('ping');
556 0         0 return $dbh->ping();
557             }
558              
559             # Transaction methods
560             sub transaction {
561 0     0 1 0 my $tx = Acrux::DBI::Tx->new(dbi => shift);
562 0         0 weaken $tx->{dbi};
563 0         0 return $tx;
564             }
565             sub begin {
566 0     0 1 0 my $self = shift;
567 0 0       0 return unless my $dbh = $self->dbh;
568 0         0 $dbh->begin_work;
569 0         0 return $self;
570             }
571             sub commit {
572 0     0 1 0 my $self = shift;
573 0 0       0 return unless my $dbh = $self->dbh;
574 0         0 $dbh->commit;
575 0         0 return $self;
576             }
577             sub rollback {
578 0     0 1 0 my $self = shift;
579 0 0       0 return unless my $dbh = $self->dbh;
580 0         0 $dbh->rollback;
581 0         0 return $self;
582             }
583              
584             # Request methods
585             sub query { # SQL, { args }
586 0     0 1 0 my $self = shift;
587 0   0     0 my $sql = shift // '';
588             my $args = @_
589             ? @_ > 1
590             ? {bind_values => [@_]}
591             : ref($_[0]) eq 'HASH'
592 0 0       0 ? {%{$_[0]}}
  0 0       0  
    0          
593             : {bind_values => [@_]}
594             : {};
595 0         0 $self->{error} = '';
596 0 0       0 return unless my $dbh = $self->dbh;
597 0 0       0 unless (length($sql)) {
598 0         0 $self->error("No statement specified");
599 0         0 return;
600             }
601              
602             # Prepare
603 0         0 my $sth = $dbh->prepare($sql);
604 0 0       0 unless ($sth) {
605 0   0     0 $self->error(sprintf("Can't prepare statement \"%s\": %s", $sql,
606             $dbh->errstr || $DBI::errstr || 'unknown error'));
607 0         0 return;
608             }
609              
610             # HandleError
611 0     0   0 local $sth->{HandleError} = sub { $_[0] = Carp::shortmess($_[0]); 0 };
  0         0  
  0         0  
612              
613             # Binding params and execute
614 0   0     0 my $bind_values = $args->{bind_values} || [];
615 0 0       0 unless (is_array_ref($bind_values)) {
616 0         0 $self->error("Invalid list of binding values. Array ref expected");
617 0         0 return;
618             }
619 0         0 my $rv;
620 0         0 my $argb = '';
621 0 0 0     0 if (scalar @$bind_values) {
    0          
622             $argb = sprintf(" with bind values: %s",
623 0 0       0 join(", ", map {defined($_) ? sprintf("'%s\'", $_) : 'undef'} @$bind_values));
  0         0  
624              
625 0         0 $rv = $sth->execute(@$bind_values);
626             } elsif (my $cb = $args->{bind_callback} || $args->{bind_cb}) {
627 0 0       0 unless (is_code_ref($cb)) {
628 0         0 $self->error("Invalid binding callback function. Code ref expected");
629 0         0 return;
630             }
631 0         0 $cb->($sth); # Callback! bind params
632 0         0 $rv = $sth->execute;
633             } else {
634 0         0 $rv = $sth->execute; # Without bindings
635             }
636 0 0       0 unless (defined $rv) {
637 0   0     0 $self->error(sprintf("Can't execute statement \"%s\"%s: %s", $sql, $argb,
638             $sth->errstr || $dbh->errstr || $DBI::errstr || 'unknown error'));
639 0         0 return;
640             }
641              
642             # Result
643 0 0       0 return Acrux::DBI::Res->new(
644             dbi => $self,
645             sth => $sth,
646             affected_rows => $rv >= 0 ? 0 + $rv : -1,
647             );
648             }
649              
650             # Working with dumps
651             sub dump {
652 0     0 1 0 my $self = shift;
653 0         0 return Acrux::DBI::Dump->new(dbi => $self, @_)
654             }
655              
656             sub cleanup {
657 0     0 1 0 my $self = shift;
658 0         0 undef $self->{dbh};
659 0         0 return $self;
660             }
661             sub DESTROY {
662 1     1   916 my $self = shift;
663 1         2 printf STDERR "DESTROY on phase %s\n", ${^GLOBAL_PHASE} if DEBUG;
664 1 50       7 return if ${^GLOBAL_PHASE} eq 'DESTRUCT';
665 1 50       23 return unless $self->{autoclean};
666 0           $self->disconnect;
667 0           printf STDERR "Auto cleanup on DESTROY completed\n" if DEBUG;
668             }
669              
670              
671             1;
672              
673             __END__