File Coverage

lib/SQL/Bibliosoph.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package SQL::Bibliosoph; {
2 1     1   1485 use Moose;
  0            
  0            
3              
4             use Data::Dumper;
5             use Digest::MD5 qw/ md5_hex /;
6             use Cache::Memcached::Fast;
7             use Storable;
8             use Log::Contextual::WarnLogger;
9             use Log::Contextual qw(:log),
10             -default_logger => Log::Contextual::WarnLogger->new({
11             env_prefix => 'Bibliosoph'
12             });
13              
14              
15              
16             use SQL::Bibliosoph::Query;
17             use SQL::Bibliosoph::CatalogFile;
18              
19             our $VERSION = "2.55";
20              
21              
22             has 'dbh' => ( is => 'ro', isa => 'DBI::db', required=> 1);
23             has 'catalog' => ( is => 'ro', isa => 'ArrayRef', default => sub { return [] } );
24             has 'catalog_str'=>( is => 'ro', isa => 'Maybe[Str]');
25             has 'memcached_address' => ( is => 'ro');
26              
27             has 'constants_from' =>( is => 'ro', isa => 'Maybe[Str]');
28              
29             has 'delayed' => ( is => 'ro', isa => 'Bool', default=> 0);
30             has 'debug' => ( is => 'ro', isa => 'Bool', default=> 0);
31             has 'benchmark' => ( is => 'ro', isa => 'Num', default=> 0);
32              
33             has 'queries' => ( is => 'rw', default=> sub { return {}; } );
34             has 'memc' => ( is => 'rw');
35             has 'log_prefix'=> ( is => 'rw');
36             has throw_errors=> ( is => 'rw', default=> 1);
37              
38             ## OLD (just for backwards compat)
39             has 'path' => ( is => 'rw', isa => 'Str', default=> '');
40              
41             sub d {
42             my $self = shift;
43             my $name = shift;
44             my @all = @_;
45             log_debug {
46             $self->log_prefix()
47             . $name
48             . join (':', map { $_ // 'NULL' } @all )
49             } if $self->debug();
50             }
51              
52             #------------------------------------------------------------------
53              
54             sub BUILD {
55             my ($self) = @_;
56              
57             $self->log_prefix('') if ! $self->log_prefix();
58              
59             $self->path( $self->path() . '/' ) if $self->path() ;
60              
61             # Start Strings
62             $self->do_all_for(SQL::Bibliosoph::CatalogFile->_parse($self->catalog_str()))
63             if $self->catalog_str();
64              
65              
66             # Start files
67             foreach my $fname (@{ $self->catalog() }) {
68             $self->do_all_for(
69             SQL::Bibliosoph::CatalogFile->new(
70             file => $self->path() . $fname,
71             constants_from => $self->constants_from(),
72             )->read()
73             );
74             }
75              
76             #
77             if (my $s = $self->memcached_address() ) {
78              
79             my $servers;
80              
81             if ( ref($s) ) {
82             $servers = $s;
83             }
84             else {
85             $servers = [ { address => $s } ],
86             }
87              
88             $self->d('Using memcached: ' . join (',', $servers) );
89              
90             $self->memc( new Cache::Memcached::Fast({
91             servers => $servers,
92             namespace => 'biblio:',
93             compress_threshold => 100_000,
94             failure_timeout => 5,
95             # hash_namespace => 1, #default => 0
96             # serialize_methods => [ \&Storable::freeze, \&Storable::thaw ],
97             # max_size => 512 * 1024, #default => 1024* 1024
98             # nowait => 1,
99             # max_failures => 3,
100             # utf8 => 1,
101             }));
102              
103             $self->d('Could not connect to memcached') if ! $self->memc();
104             }
105              
106             # $self->dbg($self->dump());
107             }
108            
109             # -------------------------------------------------------------------------
110             # Extra
111             # -------------------------------------------------------------------------
112              
113             sub dump {
114             my ($self) = @_;
115              
116             my $str='';
117              
118             foreach (values %{ $self->queries() }) {
119             $str .= $_->dump();
120             }
121              
122             return $str;
123             }
124              
125             # -------------------------------------------------------------------------
126             # Privates
127             # -------------------------------------------------------------------------
128             sub do_all_for {
129             my ($self,$qs) = @_;
130              
131             $self->create_queries_from($qs);
132             $self->create_methods_from($qs);
133             }
134              
135             sub expire_group {
136             my ($self, $group) = @_;
137              
138             if ( $self->memc() ) {
139             $self->d("Expiring group $group");
140              
141            
142             if (my $md5s = $self->memc()->get($group . '-g') ) {
143              
144              
145             foreach (split /:/, $md5s) {
146             next if ! $_;
147             #$self->d("\t\t expiring query in group $group : $_");
148              
149             $self->memc()->delete($_);
150             }
151             }
152              
153             $self->memc()->delete( $group . '-g' );
154             }
155             else {
156             $self->d("Could not expire \"$group\" -> Memcached not configured");
157             }
158             }
159              
160              
161             sub add_to_group {
162             my ($self, $group, $md5, $md5c) = @_;
163              
164              
165             $group = $group . '-g';
166              
167             my $all_md5s = $self->memc()->get( $group);
168              
169             if ( $all_md5s && index($all_md5s, $md5) >= 0 ) {
170             #$self->d("\t\t already stored:g: ".$group ." md5: $md5");
171             return;
172             }
173              
174              
175             $md5 .= ':' . $md5c if $md5c;
176             $md5 .= ':';
177              
178             $self->memc()->append( $group, $md5)
179             || $self->memc()->set( $group, $md5)
180             ;
181              
182              
183             #$self->d("\t\t storing query in group ".$group ."$md5");
184             }
185              
186             sub create_methods_from {
187             my ($self,$q) = @_;
188              
189             while ( my ($name,$st) = each (%$q) ) {
190             next if !$st;
191             my $type;
192             if ($st =~ /^\s*\(?\s*(\w+)/ ) {
193             $type = $1;
194             }
195              
196             # Small exception: if it is an INSERT but has ON DUPLICATE KEY
197             # set as UPDATE (treated as REPLACE)
198             if ($st =~ /ON\b.*DUPLICATE\b.*KEY\b.*UPDATE\b/is ) {
199             $type = 'UPDATE';
200             }
201              
202             # Small exception2: if it is a SELECT with SQL_CALC_FOUND_ROWS
203             elsif ($st =~ /SQL_CALC_FOUND_ROWS/is ) {
204             $type = 'SELECT_CALC';
205             }
206              
207             # Small exception3:
208             # USE LIKE THAT : /* SELECT */ CALL => Possible RESULT SET
209             elsif ($st =~ /\bSELECT\b.*\bCALL\b/is ) {
210             $type = 'SELECT';
211             }
212              
213             # Small exception4:
214             # SELECTs in Postgres can define subqueries with WITH.
215             elsif (lc($type) eq 'with') {
216             $type = 'SELECT';
217             }
218              
219             $self->create_method_for(uc($type||''),$name);
220             }
221              
222             # $self->d("Created methods for [".(keys %$q)."] queries");
223             }
224              
225            
226             sub create_method_for {
227             my ($self,$type,$name) = @_;
228             $_ = $type;
229             SW: {
230             no strict 'refs';
231             no warnings 'redefine';
232              
233             # TODO change to $self->meta->create_method();
234              
235             /^SELECT\b/ && do {
236             # Returns
237             # scalar : results
238            
239             # Many
240             *$name = sub {
241             my ($that) = shift;
242             $self->d('Q ',$name,@_);
243             return $self->queries()->{$name}->select_many([@_]);
244             };
245              
246             # Many, hash
247             my $name_row = 'h_'.$name;
248             # Many
249             *$name_row = sub {
250             my ($that) = shift;
251             $self->d('Q h_',$name,@_);
252             return $self->queries()->{$name}->select_many([@_],{});
253             };
254              
255             # Row
256             $name_row = 'row_'.$name;
257              
258             *$name_row = sub {
259             my ($that) = shift;
260             $self->d('Q row_',$name,@_);
261             return $self->queries()->{$name}->select_row([@_]);
262             };
263              
264             # Row hash
265             $name_row = 'rowh_'.$name;
266              
267             *$name_row = sub {
268             my ($that) = shift;
269             $self->d('Q rowh_',$name,@_);
270             return $self->queries()->{$name}->select_row_hash([@_]);
271             };
272              
273             # Many, hash, memcached
274             $name_row = 'ch_'.$name;
275              
276             # Many
277             *$name_row = sub {
278             my ($that) = shift;
279             my $ttl;
280             my $cfg = shift @_;
281              
282             my @log = ('Q ch_',$name,@_);
283              
284             SQL::Bibliosoph::Exception::CallError->throw(
285             desc => "when calling a ch_* function, first argument must be a hash_ref and must have a 'ttl' keyword"
286             ) if ref ($cfg) ne 'HASH' || ! ( $ttl = $cfg->{ttl} );
287              
288             if (! $self->memc() ) {
289             $self->d(@log, " [Memcached is NOT used, no server is defined]");
290             return $self->queries()->{$name}->select_many([@_],{});
291             }
292              
293              
294             ## check memcached
295             my $md5 = md5_hex( join (',', $name, map { $_ // 'NULL' } @_ ));
296              
297             my $ret;
298              
299             if (! $cfg->{force} ) {
300             $ret = $self->memc()->get($md5);
301             }
302             else {
303             $self->d(@log," [forced to run]");
304             }
305            
306             if (! defined $ret ) {
307             $self->d(@log," [running & storing]");
308              
309             #print "cfg:" . Dumper($cfg);
310             #print "ret:" . Dumper($ret);
311              
312             $ret = $self->queries()->{$name}->select_many([@_],{});
313              
314             #print "AFTER: ret:" . Dumper($ret);
315             # $ret could be undefined is query had an error!
316             if (defined $ret) {
317             $self->memc()->set($md5, $ret, $ttl);
318              
319             $self->add_to_group($cfg->{group}, $md5) if $cfg->{group};
320             }
321              
322             ##
323             }
324             else {
325             $self->d(@log," [from memc]");
326             }
327              
328             return $ret || [];
329             };
330              
331             # Get statement handle instead of results.
332             $name_row = $name . '_sth';
333             *$name_row = sub {
334             my ($that) = shift;
335             $self->d('Q ', $name, @_);
336             return $self->queries()->{$name}->select_do([@_]);
337             };
338              
339             last SW;
340             };
341              
342              
343             /^SELECT_CALC\b/ && do {
344             # Returns
345             # scalar : results
346             # array : (results, found_rows)
347            
348             # Many
349             *$name = sub {
350             my ($that) = shift;
351             $self->d('Q ',$name,@_);
352              
353              
354             return wantarray
355             ? $self->queries()->{$name}->select_many2([@_])
356             : $self->queries()->{$name}->select_many([@_])
357             ;
358             };
359              
360             # Many, hash
361             my $nameh = 'h_'.$name;
362             *$nameh = sub {
363             my ($that) = shift;
364              
365             $self->d('Q h_',$name,@_);
366              
367             return wantarray
368             ? $self->queries()->{$name}->select_many2([@_],{})
369             : $self->queries()->{$name}->select_many([@_],{})
370             ;
371             };
372              
373             # Many, hash, memcached
374             my $nameh2 = 'ch_'.$name;
375              
376             # Many
377             *$nameh2 = sub {
378             my ($that) = shift;
379             my $ttl;
380             my $cfg = shift @_;
381              
382             my @log = ('Q ch_',$name,@_);
383              
384             SQL::Bibliosoph::Exception::CallError->throw(
385             desc => "when calling a ch_* function, first argument must be a hash_ref and must have a 'ttl' keyword"
386             ) if ref ($cfg) ne 'HASH' || ! ( $ttl = $cfg->{ttl} );
387              
388             if (! $self->memc() ) {
389             $self->d(@log, " [Memcached is NOT used, no server is defined]");
390             return wantarray
391             ? $self->queries()->{$name}->select_many2([@_],{})
392             : $self->queries()->{$name}->select_many([@_],{})
393             ;
394             }
395              
396             my ($val, $count);
397             my $md5 = md5_hex( join (',', $name, map { $_ // 'NULL' } @_ ));
398             my $md5c = $md5 . '_count';
399              
400             if (! $cfg->{force} ) {
401             ## check memcached
402             my $ret = {};
403            
404             $ret = $self->memc()->get_multi($md5, $md5c) ;
405              
406             if ($ret) {
407             $val = $ret->{$md5};
408             $count = $ret->{$md5c};
409             }
410             }
411             else {
412             $self->d(@log," [forced to run]");
413             }
414              
415             if (! defined $val ) {
416             $self->d(@log," [running & storing]");
417              
418             ($val, $count)
419             = $self->queries()->{$name}->select_many2([@_],{});
420              
421             if ( defined $val) {
422             $self->memc()->set_multi(
423             [ $md5, $val, $ttl],
424             [ $md5c, $count, $ttl],
425             );
426              
427              
428             $self->add_to_group($cfg->{group}, $md5, $md5c ) if $cfg->{group};
429             }
430             }
431             else {
432             $self->d(@log," [from memc]");
433             }
434              
435             $val //= [];
436              
437             return wantarray
438             ? ($val, $count)
439             : $val
440             ;
441             };
442              
443            
444             last SW;
445             };
446              
447              
448             /^INSERT/ && do {
449             # Returns
450             # scalar : last_insert_id (only mysql)
451             # array : (last insert_id (only mysql), row_count)
452              
453             # do
454             *$name = sub {
455             my ($that) = shift;
456             $self->d('Q ',$name,@_);
457            
458             my $ret = $self->queries()
459             ->{$name}
460             ->select_do([@_]);
461              
462             return 0 if ($ret->rows() || 0) == -1;
463            
464             return wantarray
465             ? ($ret->{mysql_insertid}, $ret->rows() )
466             : $ret->{mysql_insertid}
467             ;
468              
469             };
470              
471              
472             last SW;
473             };
474              
475             if ( /^UPDATE/ ) {
476             # Update has the same query than unknown
477             }
478              
479             # Returns
480             # scalar : SQL_ROWS (modified rows)
481             *$name = sub {
482             my ($that) = shift;
483             $self->d('Q ',$name,@_);
484              
485             return $self->queries()
486             ->{$name}
487             ->select_do([@_])
488             ->rows();
489             };
490             }
491             }
492              
493             #------------------------------------------------------------------
494             sub create_queries_from {
495             my ($self,$qs) = @_;
496             my $i = 0;
497              
498             while ( my ($name,$st) = each (%$qs) ) {
499             next if !$st;
500              
501             # Previous exists?
502             if ( $self->queries()->{$name} ) {
503             delete $self->queries()->{$name};
504             }
505              
506             my $args = {
507             dbh => $self->dbh(),
508             st => $st,
509             name => $name,
510             delayed => $self->delayed(),
511             debug => $self->debug(),
512             benchmark=> $self->benchmark(),
513             throw_errors => $self->throw_errors(),
514             };
515              
516             # Prepare the statement
517             $self->queries()->{$name} = SQL::Bibliosoph::Query->new( $args );
518              
519             $i++;
520             }
521             $self->d( __PACKAGE__ . ": Prepared $i Statements". ( $self->delayed() ? " (delayed) " : '' ));
522             }
523             }
524              
525             1;
526              
527             __END__
528              
529             =head1 NAME
530              
531             SQL::Bibliosoph - A SQL Statements Library
532              
533             =head1 SYNOPSIS
534              
535             use SQL::Bibliosoph;
536              
537              
538             my $bs = SQL::Bibliosoph->new(
539             dbh => $database_handle,
540             catalog => [ qw(users products <billing) ],
541              
542             # enables statement benchmarking and debug
543             # (0.5 = logs queries that takes more than half second)
544             benchmark=> 0.5,
545              
546             # enables debug using Log::Contextual
547             debug => 1,
548              
549             # enables memcached usage
550             memcached_address => '127.0.0.1:11322',
551              
552             # enables memcached usage (multiple servers)
553             memcached_address => ['127.0.0.1:11322','127.0.0.2:11322']
554             );
555              
556              
557              
558             # Using dynamic generated functions. Wrapper funtions
559             # are automaticaly created on module initialization.
560              
561             # A query should something like:
562              
563             --[ get_products ]
564             SELECT id,name FROM product WHERE country = ?
565            
566             # Then ...
567             my $products_ref = $bs->get_products($country);
568              
569             # Forcing numbers in parameters
570             # Query:
571              
572             --[ get_products ]
573             SELECT id,name FROM product WHERE country = ? LIMIT #?,#?
574              
575             # Parameter ordering and repeating
576             # Query:
577            
578             --[ get_products ]
579             SELECT id,name
580             FROM product
581             WHERE 1? IS NULL OR country = 1?
582             AND price > 2? * 0.9 AND print > 2? * 1.1
583             LIMIT #3?,#4?
584            
585             # then ...
586             my $products_ref = $bs->get_products($country,$price,$start,$limit);
587              
588             # The same, but with an array of hashs result (add h_ at the begining)
589              
590             my $products_array_of_hash_ref
591             = $bs->h_get_products($country,$price,$start,$limit);
592              
593              
594             # To get a prepared and executed statement handle, append '_sth':
595             my $sth = $bs->get_products_sth($country, $price, $start, $limit);
596              
597              
598             # Selecting only one row (add row_ at the begining)
599             # Query:
600            
601             --[ get_one ]
602             SELECT name,age FROM person where id = ?;
603            
604             # then ...
605             my $product_ref = $bs->row_get_one($product_id);
606            
607             # Selecting only one value (same query as above)
608             my $product_name = $bs->row_get_one($product_id)->[1];
609              
610              
611             # Selecting only one row, but with HASH ref results
612             # (same query as above) (add rowh_ at the begining)
613             my $product_hash_ref = $bs->rowh_get_one($product_id);
614            
615              
616             # Inserting a row, with an auto_increment PK.
617             # Query:
618            
619             --[ insert_person ]
620             INSERT INTO person (name,age) VALUES (?,?);
621            
622             # then ...
623             my $last_insert_id = $bs->insert_person($name,$age);
624              
625              
626             # Usefull when no primary key is defined
627             my ($dummy_last_insert_id, $total_inserted) = $bs->insert_person($name,$age);
628              
629             Note that last_insert_id is only returned when using MYSQL (undef in other case).
630             When using other engine you need to call an other query to get the last value.
631             For example, in ProgreSQL you can define:
632            
633             --[ LAST_VAL ]
634             SELECT lastval()
635            
636             and then call LAST_VAL after an insert.
637              
638              
639             # Updating some rows
640             # Query:
641            
642             --[ age_persons ]
643             UPDATE person SET age = age + 1 WHERE birthday = ?
644            
645             # then ...
646             my $updated_persons = $bs->age_persons($today);
647              
648              
649              
650              
651             Memcached usage
652              
653             # Mmemcached queries are only generated for hash, multiple rows, results h_QUERY, using de "ch_" prefix.
654              
655             my $products_array_of_hash_ref = $bs->ch_get_products({ttl => 10 }, $country,$price,$start,$limit);
656            
657             # To define a group of query (for later simulaneous expiration) use:
658            
659             my $products_array_of_hash_ref = $bs->ch_get_products(
660             {ttl => 3600, group => 'product_of_'.$country },
661             $country,$price,$start,$limit);
662              
663             my $products_array_of_hash_ref = $bs->ch_get_prices(
664             {ttl => 3600, group => 'product_of_'.$country },
665             $country,$price,$start,$limit);
666            
667             # Then, to force refresh in the two previous queries next time they are called, just use:
668             #
669             $bs->expire_group('product_of_'.$country);
670            
671              
672             =head1 DESCRIPTION
673              
674             SQL::Bibliosoph is a SQL statement library engine that allow to clearly separate
675             SQL statements from PERL code. It is currently tested on MySQL 5.x, but it
676             should be easly ported to other engines.
677              
678             The catalog files are prepared a the initialization, for performance reasons.
679             The use of prepared statement also helps to prevents SQL injection attacks.
680             SQL::Bibliosoph supports bind parameters in statements definition and bind
681             parements reordering (See SQL::Bibliosoph::CatalogFile for details).
682              
683              
684             All functions throw 'SQL::Bibliosoph::Exception::QuerySyntaxError' on error. The
685             error message is 'SQL ERROR' and the mysql error reported by the driver.
686              
687             =head1 Constructor parameters
688              
689             =head3 dsn
690              
691             The database handler. For example:
692              
693             my $dbh = DBI->connect($dsn, ...);
694             my $bb = SQL::Bibliosoph(dbh => $dbh, ...);
695              
696             =head3 catalog
697            
698             An array ref containg filenames with the queries. This files should use they
699             SQL::Bibliosoph::CatalogFile format (SEE Perldoc for details). The suggested
700             extension for these files is 'bb'. The name can be preceded with a "<" forcing
701             the catalog the be open in "read-only" mode. In the mode, UPDATE, INSERT and
702             REPLACE statement will be parsed. Note the calling a SQL procedure or function
703             that actually modifies the DB is still allowed!
704              
705             All the catalogs will be merged, be carefull with namespace collisions. the
706             statement will be prepared at module constuction.
707              
708             =head3 catalog_str
709            
710             Allows to define a SQL catalog using a string (not a file). The queries will be
711             merged with Catalog files (if any).
712              
713            
714             =head3 constants_from
715              
716             In order to use the same constants in your PERL code and your SQL modules, you
717             can declare a module using `constants_from` paramenter. Constants exported in
718             that module (using @EXPORT) will be replaced in all catalog file before SQL
719             preparation. The module must be in the @INC path.
720              
721             Note: constants_from() is ignored in 'catalog_str' queries (sorry, not implemented, yet)
722              
723             =head3 delayed
724              
725             Do not prepare all the statements at startup. They will be prepared individualy,
726             when they are used for the first time. Defaults to false(0).
727              
728             =head3 benchmark
729              
730             Use this to enable Query profilling. The elapsed time (in miliseconds) will be
731             printed with Log::Contextual after each query execution, if the time is bigger that
732             `benchmark` (must be given in SECONDS, can be a floating point number).
733              
734             =head3 debug
735              
736             To enable debug (prints each query, and arguments, very useful during
737             development).
738              
739             =head3 throw_errors
740             Enable by default. Will throw SQL::Bibliosoph::Exceptions on errors. If disabled,
741             will print with Log::Contextual. By default, duplicate key errors are not throwed are exception
742             set this variable to '2' if you want that.
743              
744              
745             =head3 duplicate_key
746              
747             =head1 Bibliosoph
748              
749             n. person having deep knowledge of books. bibliognostic.
750              
751             =head1 AUTHORS
752              
753             SQL::Bibliosoph by Matias Alejo Garcia (matiu at cpan.org) and Lucas Lain.
754              
755             =head1 CONTRIBUTORS
756              
757             Juan Ladetto
758              
759             WOLS
760              
761             =head1 COPYRIGHT
762              
763             Copyright (c) 2007-2010 Matias Alejo Garcia. All rights reserved. This program
764             is free software; you can redistribute it and/or modify it under the same terms
765             as Perl itself.
766              
767             =head1 SUPPORT / WARRANTY
768              
769             The SQL::Bibliosoph is free Open Source software. IT COMES WITHOUT WARRANTY OF
770             ANY KIND.
771              
772             =head1 SEE ALSO
773            
774             SQL::Bibliosoph::CatalogFile
775              
776             At http://nits.com.ar/bibliosoph you can find:
777              
778             * Examples
779             * VIM syntax highlighting definitions for bb files
780             * CTAGS examples for indexing bb files.
781              
782             You can also find the vim and ctags files in the /etc subdirectory.
783              
784             Lasted version at: http://github.com/matiu/SQL--Bibliosoph/tree/master
785              
786             =head1 BUGS
787              
788             This module have been tested with MySQL, PosgreSQL and SQL Server. Migration to other DB engines
789             should be simple accomplished. If you would like to use Bibliosoph with other DB, please
790             let me know and we can help you if you do the testing.