File Coverage

blib/lib/DBIx/SearchBuilder/Handle.pm
Criterion Covered Total %
statement 423 664 63.7
branch 152 282 53.9
condition 44 85 51.7
subroutine 49 72 68.0
pod 47 51 92.1
total 715 1154 61.9


line stmt bran cond sub pod time code
1             package DBIx::SearchBuilder::Handle;
2              
3 25     25   212394 use strict;
  25         89  
  25         717  
4 25     25   130 use warnings;
  25         47  
  25         739  
5              
6 25     25   131 use Carp qw(croak cluck);
  25         47  
  25         1409  
7 25     25   7069 use DBI;
  25         71949  
  25         1109  
8 25     25   3484 use Class::ReturnValue;
  25         97525  
  25         2913  
9 25     25   4414 use Encode qw();
  25         69388  
  25         614  
10 25     25   10691 use version;
  25         47328  
  25         155  
11              
12 25     25   5262 use DBIx::SearchBuilder::Util qw/ sorted_values /;
  25         89  
  25         1449  
13              
14 25     25   163 use vars qw(@ISA %DBIHandle $PrevHandle $DEBUG %TRANSDEPTH %TRANSROLLBACK %FIELDS_IN_TABLE);
  25         59  
  25         60943  
15              
16              
17             =head1 NAME
18              
19             DBIx::SearchBuilder::Handle - Perl extension which is a generic DBI handle
20              
21             =head1 SYNOPSIS
22              
23             use DBIx::SearchBuilder::Handle;
24              
25             my $handle = DBIx::SearchBuilder::Handle->new();
26             $handle->Connect( Driver => 'mysql',
27             Database => 'dbname',
28             Host => 'hostname',
29             User => 'dbuser',
30             Password => 'dbpassword');
31             # now $handle isa DBIx::SearchBuilder::Handle::mysql
32              
33             =head1 DESCRIPTION
34              
35             This class provides a wrapper for DBI handles that can also perform a number of additional functions.
36              
37             =cut
38              
39              
40              
41             =head2 new
42              
43             Generic constructor
44              
45             =cut
46              
47             sub new {
48 23     23 1 1966 my $proto = shift;
49 23   33     221 my $class = ref($proto) || $proto;
50 23         60 my $self = {};
51 23         58 bless ($self, $class);
52              
53             # Enable quotes table names
54 23         87 my %args = ( QuoteTableNames => 0, @_ );
55 23         181 $self->{'QuoteTableNames'} = $args{QuoteTableNames};
56              
57 23         61 @{$self->{'StatementLog'}} = ();
  23         78  
58 23         106 return $self;
59             }
60              
61              
62              
63             =head2 Connect PARAMHASH: Driver, Database, Host, User, Password, QuoteTableNames
64              
65             Takes a paramhash and connects to your DBI datasource.
66              
67             You should _always_ set
68              
69             DisconnectHandleOnDestroy => 1
70              
71             unless you have a legacy app like RT2 or RT 3.0.{0,1,2} that depends on the broken behaviour.
72              
73             If you created the handle with
74             DBIx::SearchBuilder::Handle->new
75             and there is a DBIx::SearchBuilder::Handle::(Driver) subclass for the driver you have chosen,
76             the handle will be automatically "upgraded" into that subclass.
77              
78             QuoteTableNames option will force all table names to be quoted if the driver subclass has a method
79             for quoting implemented. The mysql subclass will detect mysql version 8 and set the flag.
80              
81             =cut
82              
83             sub Connect {
84 22     22 1 19115 my $self = shift;
85 22         198 my %args = (
86             Driver => undef,
87             Database => undef,
88             Host => undef,
89             SID => undef,
90             Port => undef,
91             User => undef,
92             Password => undef,
93             RequireSSL => undef,
94             DisconnectHandleOnDestroy => undef,
95             QuoteTableNames => undef,
96             @_
97             );
98              
99 22 100 66     288 if ( $args{'Driver'} && !$self->isa( __PACKAGE__ .'::'. $args{'Driver'} ) ) {
100 1 50       5 return $self->Connect( %args ) if $self->_UpgradeHandle( $args{'Driver'} );
101             }
102              
103             # Setting this actually breaks old RT versions in subtle ways.
104             # So we need to explicitly call it
105 21         72 $self->{'DisconnectHandleOnDestroy'} = $args{'DisconnectHandleOnDestroy'};
106              
107             # Enable optional quoted table names
108 21 50       77 $self->{'QuoteTableNames'} = delete $args{QuoteTableNames} if defined $args{QuoteTableNames};
109              
110 21   50     143 my $old_dsn = $self->DSN || '';
111 21         174 my $new_dsn = $self->BuildDSN( %args );
112              
113             # Only connect if we're not connected to this source already
114 21 0 33     117 return undef if $self->dbh && $self->dbh->ping && $new_dsn eq $old_dsn;
      33        
115              
116             my $handle = DBI->connect(
117 21 50       220 $new_dsn, $args{'User'}, $args{'Password'}
118             ) or croak "Connect Failed $DBI::errstr\n";
119              
120             # databases do case conversion on the name of columns returned.
121             # actually, some databases just ignore case. this smashes it to something consistent
122 21         50276 $handle->{FetchHashKeyName} ='NAME_lc';
123              
124             # Set the handle
125 21         154 $self->dbh($handle);
126              
127             # Cache version info
128 21         106 $self->DatabaseVersion;
129              
130             # force quoted tables for mysql 8
131 21 50       167 $self->{'QuoteTableNames'} = 1 if $self->_RequireQuotedTables;
132              
133 21         138 return 1;
134             }
135              
136              
137             =head2 _UpgradeHandle DRIVER
138              
139             This private internal method turns a plain DBIx::SearchBuilder::Handle into one
140             of the standard driver-specific subclasses.
141              
142             =cut
143              
144             sub _UpgradeHandle {
145 1     1   3 my $self = shift;
146              
147 1         1 my $driver = shift;
148 1         4 my $class = 'DBIx::SearchBuilder::Handle::' . $driver;
149 1         2 local $@;
150 1         71 eval "require $class";
151 1 50       7 return if $@;
152              
153 1         3 bless $self, $class;
154 1         14 return 1;
155             }
156              
157              
158             =head2 BuildDSN PARAMHASH
159              
160             Takes a bunch of parameters:
161              
162             Required: Driver, Database,
163             Optional: Host, Port and RequireSSL
164              
165             Builds a DSN suitable for a DBI connection
166              
167             =cut
168              
169             sub BuildDSN {
170 21     21 1 52 my $self = shift;
171 21         153 my %args = (
172             Driver => undef,
173             Database => undef,
174             Host => undef,
175             Port => undef,
176             SID => undef,
177             RequireSSL => undef,
178             @_
179             );
180              
181 21         87 my $dsn = "dbi:$args{'Driver'}:dbname=$args{'Database'}";
182 21 50       142 $dsn .= ";sid=$args{'SID'}" if $args{'SID'};
183 21 50       105 $dsn .= ";host=$args{'Host'}" if $args{'Host'};
184 21 50       70 $dsn .= ";port=$args{'Port'}" if $args{'Port'};
185 21 50       72 $dsn .= ";requiressl=1" if $args{'RequireSSL'};
186              
187 21         113 return $self->{'dsn'} = $dsn;
188             }
189              
190              
191             =head2 DSN
192              
193             Returns the DSN for this database connection.
194              
195             =cut
196              
197             sub DSN {
198 21     21 1 154 return shift->{'dsn'};
199             }
200              
201              
202              
203             =head2 RaiseError [MODE]
204              
205             Turns on the Database Handle's RaiseError attribute.
206              
207             =cut
208              
209             sub RaiseError {
210 0     0 1 0 my $self = shift;
211              
212 0         0 my $mode = 1;
213 0 0       0 $mode = shift if (@_);
214              
215 0         0 $self->dbh->{RaiseError}=$mode;
216             }
217              
218              
219              
220              
221             =head2 PrintError [MODE]
222              
223             Turns on the Database Handle's PrintError attribute.
224              
225             =cut
226              
227             sub PrintError {
228 0     0 1 0 my $self = shift;
229              
230 0         0 my $mode = 1;
231 0 0       0 $mode = shift if (@_);
232              
233 0         0 $self->dbh->{PrintError}=$mode;
234             }
235              
236              
237              
238             =head2 LogSQLStatements BOOL
239              
240             Takes a boolean argument. If the boolean is true, SearchBuilder will log all SQL
241             statements, as well as their invocation times and execution times.
242              
243             Returns whether we're currently logging or not as a boolean
244              
245             =cut
246              
247             sub LogSQLStatements {
248 832     832 1 1813 my $self = shift;
249 832 50       2713 if (@_) {
250 0         0 require Time::HiRes;
251 0         0 $self->{'_DoLogSQL'} = shift;
252             }
253 832         2808 return ($self->{'_DoLogSQL'});
254             }
255              
256             =head2 _LogSQLStatement STATEMENT DURATION
257              
258             Add an SQL statement to our query log
259              
260             =cut
261              
262             sub _LogSQLStatement {
263 0     0   0 my $self = shift;
264 0         0 my $statement = shift;
265 0         0 my $duration = shift;
266 0         0 my @bind = @_;
267 0         0 push @{$self->{'StatementLog'}} , ([Time::HiRes::time(), $statement, [@bind], $duration, Carp::longmess("Executed SQL query")]);
  0         0  
268              
269             }
270              
271             =head2 ClearSQLStatementLog
272              
273             Clears out the SQL statement log.
274              
275              
276             =cut
277              
278             sub ClearSQLStatementLog {
279 0     0 1 0 my $self = shift;
280 0         0 @{$self->{'StatementLog'}} = ();
  0         0  
281             }
282              
283              
284             =head2 SQLStatementLog
285              
286             Returns the current SQL statement log as an array of arrays. Each entry is a triple of
287              
288             (Time, Statement, Duration)
289              
290             =cut
291              
292             sub SQLStatementLog {
293 0     0 1 0 my $self = shift;
294 0         0 return (@{$self->{'StatementLog'}});
  0         0  
295              
296             }
297              
298              
299              
300             =head2 AutoCommit [MODE]
301              
302             Turns on the Database Handle's AutoCommit attribute.
303              
304             =cut
305              
306             sub AutoCommit {
307 0     0 1 0 my $self = shift;
308              
309 0         0 my $mode = 1;
310 0 0       0 $mode = shift if (@_);
311              
312 0         0 $self->dbh->{AutoCommit}=$mode;
313             }
314              
315              
316              
317              
318             =head2 Disconnect
319              
320             Disconnect from your DBI datasource
321              
322             =cut
323              
324             sub Disconnect {
325 0     0 1 0 my $self = shift;
326 0         0 my $dbh = $self->dbh;
327 0 0       0 return unless $dbh;
328 0         0 $self->Rollback(1);
329              
330 0         0 my $ret = $dbh->disconnect;
331              
332             # DBD::mysql with MariaDB 10.2+ could cause segment faults when
333             # interacting with a disconnected handle, here we unset
334             # dbh to inform other code that there is no connection any more.
335             # See also https://github.com/perl5-dbi/DBD-mysql/issues/306
336 0   0     0 my ($version) = ( $self->DatabaseVersion // '' ) =~ /^(\d+\.\d+)/;
337 0 0 0     0 if ( $self->isa('DBIx::SearchBuilder::Handle::mysql')
      0        
338             && $self->{'database_version'} =~ /mariadb/i
339             && version->parse('v'.$version) > version->parse('v10.2') )
340             {
341 0         0 $self->dbh(undef);
342             }
343              
344 0         0 return $ret;
345             }
346              
347              
348             =head2 dbh [HANDLE]
349              
350             Return the current DBI handle. If we're handed a parameter, make the database handle that.
351              
352             =cut
353              
354             # allow use of Handle as a synonym for DBH
355             *Handle=\&dbh;
356              
357             sub dbh {
358 823     823 1 2249 my $self=shift;
359              
360             #If we are setting the database handle, set it.
361 823 100       1963 if ( @_ ) {
362 21         74 $DBIHandle{$self} = $PrevHandle = shift;
363 21         88 %FIELDS_IN_TABLE = ();
364             }
365              
366 823   66     8644 return($DBIHandle{$self} ||= $PrevHandle);
367             }
368              
369              
370             =head2 Insert $TABLE_NAME @KEY_VALUE_PAIRS
371              
372             Takes a table name and a set of key-value pairs in an array.
373             Splits the key value pairs, constructs an INSERT statement
374             and performs the insert.
375              
376             Base class return statement handle object, while DB specific
377             subclass should return row id.
378              
379             =cut
380              
381             sub Insert {
382 152     152 1 364 my $self = shift;
383 152         497 return $self->SimpleQuery( $self->InsertQueryString(@_) );
384             }
385              
386             =head2 InsertQueryString $TABLE_NAME @KEY_VALUE_PAIRS
387              
388             Takes a table name and a set of key-value pairs in an array.
389             Splits the key value pairs, constructs an INSERT statement
390             and returns query string and set of bind values.
391              
392             This method is more useful for subclassing in DB specific
393             handles. L method is preferred for end users.
394              
395             =cut
396              
397             sub InsertQueryString {
398 152     152 1 661 my($self, $table, @pairs) = @_;
399 152         341 my(@cols, @vals, @bind);
400              
401 152         539 while ( my $key = shift @pairs ) {
402 391         798 push @cols, $key;
403 391         685 push @vals, '?';
404 391         1142 push @bind, shift @pairs;
405             }
406              
407 152 50       475 $table = $self->QuoteName($table) if $self->QuoteTableNames;
408 152         452 my $QueryString = "INSERT INTO $table";
409 152         707 $QueryString .= " (". join(", ", @cols) .")";
410 152         450 $QueryString .= " VALUES (". join(", ", @vals). ")";
411 152         695 return ($QueryString, @bind);
412             }
413              
414             =head2 InsertFromSelect
415              
416             Takes table name, array reference with columns, select query
417             and list of bind values. Inserts data select by the query
418             into the table.
419              
420             To make sure call is portable every column in result of
421             the query should have unique name or should be aliased.
422             See L for
423             details.
424              
425             =cut
426              
427             sub InsertFromSelect {
428 4     4 1 1258 my ($self, $table, $columns, $query, @binds) = @_;
429              
430 4 50       62 $columns = join ', ', @$columns
431             if $columns;
432              
433 4 50       22 $table = $self->QuoteName($table) if $self->{'QuoteTableNames'};
434 4         17 my $full_query = "INSERT INTO $table";
435 4 50       27 $full_query .= " ($columns)" if $columns;
436 4         22 $full_query .= ' '. $query;
437 4         26 my $sth = $self->SimpleQuery( $full_query, @binds );
438 4 50       21 return $sth unless $sth;
439              
440 4         37 my $rows = $sth->rows;
441 4 50       179 return $rows == 0? '0E0' : $rows;
442             }
443              
444             =head2 UpdateRecordValue
445              
446             Takes a hash with fields: Table, Column, Value PrimaryKeys, and
447             IsSQLFunction. Table, and Column should be obvious, Value is where you
448             set the new value you want the column to have. The primary_keys field should
449             be the lvalue of DBIx::SearchBuilder::Record::PrimaryKeys(). Finally
450             IsSQLFunction is set when the Value is a SQL function. For example, you
451             might have ('Value'=>'PASSWORD(string)'), by setting IsSQLFunction that
452             string will be inserted into the query directly rather then as a binding.
453              
454             =cut
455              
456             sub UpdateRecordValue {
457 19     19 1 42 my $self = shift;
458 19         95 my %args = ( Table => undef,
459             Column => undef,
460             IsSQLFunction => undef,
461             PrimaryKeys => undef,
462             @_ );
463              
464 19         42 my @bind = ();
465 19 50       63 $args{Table} = $self->QuoteName($args{Table}) if $self->{'QuoteTableNames'};
466 19         69 my $query = 'UPDATE ' . $args{'Table'} . ' ';
467 19         52 $query .= 'SET ' . $args{'Column'} . '=';
468              
469             ## Look and see if the field is being updated via a SQL function.
470 19 50       46 if ($args{'IsSQLFunction'}) {
471 0         0 $query .= $args{'Value'} . ' ';
472             }
473             else {
474 19         35 $query .= '? ';
475 19         46 push (@bind, $args{'Value'});
476             }
477              
478             ## Constructs the where clause.
479 19         37 my $where = 'WHERE ';
480 19         30 foreach my $key (sort keys %{$args{'PrimaryKeys'}}) {
  19         85  
481 19         46 $where .= $key . "=?" . " AND ";
482 19         52 push (@bind, $args{'PrimaryKeys'}{$key});
483             }
484 19         119 $where =~ s/AND\s$//;
485              
486 19         55 my $query_str = $query . $where;
487 19         68 return ($self->SimpleQuery($query_str, @bind));
488             }
489              
490              
491              
492              
493             =head2 UpdateTableValue TABLE COLUMN NEW_VALUE RECORD_ID IS_SQL
494              
495             Update column COLUMN of table TABLE where the record id = RECORD_ID. if IS_SQL is set,
496             don\'t quote the NEW_VALUE
497              
498             =cut
499              
500             sub UpdateTableValue {
501 0     0 1 0 my $self = shift;
502              
503             ## This is just a wrapper to UpdateRecordValue().
504 0         0 my %args = ();
505 0         0 $args{'Table'} = shift;
506 0         0 $args{'Column'} = shift;
507 0         0 $args{'Value'} = shift;
508 0         0 $args{'PrimaryKeys'} = shift;
509 0         0 $args{'IsSQLFunction'} = shift;
510              
511 0         0 return $self->UpdateRecordValue(%args)
512             }
513              
514             =head1 SimpleUpdateFromSelect
515              
516             Takes table name, hash reference with (column, value) pairs,
517             select query and list of bind values.
518              
519             Updates the table, but only records with IDs returned by the
520             selected query, eg:
521              
522             UPDATE $table SET %values WHERE id IN ( $query )
523              
524             It's simple as values are static and search only allowed
525             by id.
526              
527             =cut
528              
529             sub SimpleUpdateFromSelect {
530 1     1 0 912 my ($self, $table, $values, $query, @query_binds) = @_;
531              
532 1         4 my @columns; my @binds;
533 1         10 for my $k (sort keys %$values) {
534 2         8 push @columns, $k;
535 2         9 push @binds, $values->{$k};
536             }
537              
538 1 50       6 $table = $self->QuoteName($table) if $self->{'QuoteTableNames'};
539 1         6 my $full_query = "UPDATE $table SET ";
540 1         12 $full_query .= join ', ', map "$_ = ?", @columns;
541 1         12 $full_query .= ' WHERE id IN ('. $query .')';
542 1         5 my $sth = $self->SimpleQuery( $full_query, @binds, @query_binds );
543 1 50       10 return $sth unless $sth;
544              
545 1         12 my $rows = $sth->rows;
546 1 50       30 return $rows == 0? '0E0' : $rows;
547             }
548              
549             =head1 DeleteFromSelect
550              
551             Takes table name, select query and list of bind values.
552              
553             Deletes from the table, but only records with IDs returned by the
554             select query, eg:
555              
556             DELETE FROM $table WHERE id IN ($query)
557              
558             =cut
559              
560             sub DeleteFromSelect {
561 1     1 0 23 my ($self, $table, $query, @binds) = @_;
562 1 50       7 $table = $self->QuoteName($table) if $self->{'QuoteTableNames'};
563 1         8 my $sth = $self->SimpleQuery(
564             "DELETE FROM $table WHERE id IN ($query)",
565             @binds
566             );
567 1 50       15 return $sth unless $sth;
568              
569 1         14 my $rows = $sth->rows;
570 1 50       33 return $rows == 0? '0E0' : $rows;
571             }
572              
573             =head2 SimpleQuery QUERY_STRING, [ BIND_VALUE, ... ]
574              
575             Execute the SQL string specified in QUERY_STRING
576              
577             =cut
578              
579             sub SimpleQuery {
580 418     418 1 1688 my $self = shift;
581 418         754 my $QueryString = shift;
582 418         703 my @bind_values;
583 418 100       1308 @bind_values = (@_) if (@_);
584              
585 418         1199 my $sth = $self->dbh->prepare($QueryString);
586 418 100       43585 unless ($sth) {
587 2 50       11 if ($DEBUG) {
588 0         0 die "$self couldn't prepare the query '$QueryString'"
589             . $self->dbh->errstr . "\n";
590             }
591             else {
592 2         32 warn "$self couldn't prepare the query '$QueryString'"
593             . $self->dbh->errstr . "\n";
594 2         19 my $ret = Class::ReturnValue->new();
595 2         26 $ret->as_error(
596             errno => '-1',
597             message => "Couldn't prepare the query '$QueryString'."
598             . $self->dbh->errstr,
599             do_backtrace => undef
600             );
601 2         49 return ( $ret->return_value );
602             }
603             }
604              
605             # Check @bind_values for HASH refs
606 416         1522 for ( my $bind_idx = 0 ; $bind_idx < scalar @bind_values ; $bind_idx++ ) {
607 533 50       1873 if ( ref( $bind_values[$bind_idx] ) eq "HASH" ) {
608 0         0 my $bhash = $bind_values[$bind_idx];
609 0         0 $bind_values[$bind_idx] = $bhash->{'value'};
610 0         0 delete $bhash->{'value'};
611 0         0 $sth->bind_param( $bind_idx + 1, undef, $bhash );
612             }
613             }
614              
615 416         779 my $basetime;
616 416 50       1267 if ( $self->LogSQLStatements ) {
617 0         0 $basetime = Time::HiRes::time();
618             }
619 416         738 my $executed;
620             {
621 25     25   232 no warnings 'uninitialized' ; # undef in bind_values makes DBI sad
  25         68  
  25         144680  
  416         831  
622 416         817 eval { $executed = $sth->execute(@bind_values) };
  416         2794178  
623             }
624 416 50       3166 if ( $self->LogSQLStatements ) {
625 0         0 $self->_LogSQLStatement( $QueryString, Time::HiRes::time() - $basetime, @bind_values );
626             }
627              
628 416 50 33     2376 if ( $@ or !$executed ) {
629 0 0       0 if ($DEBUG) {
630 0         0 die "$self couldn't execute the query '$QueryString'"
631             . $self->dbh->errstr . "\n";
632              
633             }
634             else {
635 0         0 cluck "$self couldn't execute the query '$QueryString'";
636              
637 0         0 my $ret = Class::ReturnValue->new();
638 0         0 $ret->as_error(
639             errno => '-1',
640             message => "Couldn't execute the query '$QueryString'"
641             . $self->dbh->errstr,
642             do_backtrace => undef
643             );
644 0         0 return ( $ret->return_value );
645             }
646              
647             }
648 416         2548 return ($sth);
649              
650             }
651              
652              
653              
654             =head2 FetchResult QUERY, [ BIND_VALUE, ... ]
655              
656             Takes a SELECT query as a string, along with an array of BIND_VALUEs
657             If the select succeeds, returns the first row as an array.
658             Otherwise, returns a Class::ResturnValue object with the failure loaded
659             up.
660              
661             =cut
662              
663             sub FetchResult {
664 0     0 1 0 my $self = shift;
665 0         0 my $query = shift;
666 0         0 my @bind_values = @_;
667 0         0 my $sth = $self->SimpleQuery($query, @bind_values);
668 0 0       0 if ($sth) {
669 0         0 return ($sth->fetchrow);
670             }
671             else {
672 0         0 return($sth);
673             }
674             }
675              
676              
677             =head2 BinarySafeBLOBs
678              
679             Returns 1 if the current database supports BLOBs with embedded nulls.
680             Returns undef if the current database doesn't support BLOBs with embedded nulls
681              
682             =cut
683              
684             sub BinarySafeBLOBs {
685 0     0 1 0 my $self = shift;
686 0         0 return(1);
687             }
688              
689              
690              
691             =head2 KnowsBLOBs
692              
693             Returns 1 if the current database supports inserts of BLOBs automatically.
694             Returns undef if the current database must be informed of BLOBs for inserts.
695              
696             =cut
697              
698             sub KnowsBLOBs {
699 171     171 1 354 my $self = shift;
700 171         536 return(1);
701             }
702              
703              
704              
705             =head2 BLOBParams FIELD_NAME FIELD_TYPE
706              
707             Returns a hash ref for the bind_param call to identify BLOB types used by
708             the current database for a particular column type.
709              
710             =cut
711              
712             sub BLOBParams {
713 0     0 1 0 my $self = shift;
714             # Don't assign to key 'value' as it is defined later.
715 0         0 return ( {} );
716             }
717              
718              
719              
720             =head2 DatabaseVersion [Short => 1]
721              
722             Returns the database's version.
723              
724             If argument C is true returns short variant, in other
725             case returns whatever database handle/driver returns. By default
726             returns short version, e.g. '4.1.23' or '8.0-rc4'.
727              
728             Returns empty string on error or if database couldn't return version.
729              
730             The base implementation uses a C
731              
732             =cut
733              
734             sub DatabaseVersion {
735 0     0 1 0 my $self = shift;
736 0         0 my %args = ( Short => 1, @_ );
737              
738 0 0       0 unless ( defined $self->{'database_version'} ) {
739              
740             # turn off error handling, store old values to restore later
741 0         0 my $re = $self->RaiseError;
742 0         0 $self->RaiseError(0);
743 0         0 my $pe = $self->PrintError;
744 0         0 $self->PrintError(0);
745              
746 0         0 my $statement = "SELECT VERSION()";
747 0         0 my $sth = $self->SimpleQuery($statement);
748              
749 0         0 my $ver = '';
750 0 0 0     0 $ver = ( $sth->fetchrow_arrayref->[0] || '' ) if $sth;
751 0         0 $ver =~ /(\d+(?:\.\d+)*(?:-[a-z0-9]+)?)/i;
752 0         0 $self->{'database_version'} = $ver;
753 0   0     0 $self->{'database_version_short'} = $1 || $ver;
754              
755 0         0 $self->RaiseError($re);
756 0         0 $self->PrintError($pe);
757             }
758              
759 0 0       0 return $self->{'database_version_short'} if $args{'Short'};
760 0         0 return $self->{'database_version'};
761             }
762              
763             =head2 CaseSensitive
764              
765             Returns 1 if the current database's searches are case sensitive by default
766             Returns undef otherwise
767              
768             =cut
769              
770             sub CaseSensitive {
771 0     0 1 0 my $self = shift;
772 0         0 return(1);
773             }
774              
775             =head2 QuoteTableNames
776              
777             Returns 1 if table names will be quoted in queries, otherwise 0
778              
779             =cut
780              
781             sub QuoteTableNames {
782 248     248 1 901 return shift->{'QuoteTableNames'}
783             }
784              
785              
786              
787              
788              
789             =head2 _MakeClauseCaseInsensitive FIELD OPERATOR VALUE
790              
791             Takes a field, operator and value. performs the magic necessary to make
792             your database treat this clause as case insensitive.
793              
794             Returns a FIELD OPERATOR VALUE triple.
795              
796             =cut
797              
798             our $RE_CASE_INSENSITIVE_CHARS = qr/[-'"\d: ]/;
799              
800             sub _MakeClauseCaseInsensitive {
801 43     43   110 my $self = shift;
802 43         89 my $field = shift;
803 43         213 my $operator = shift;
804 43         95 my $value = shift;
805              
806             # don't downcase integer values and things that looks like dates
807 43 100       727 if ($value !~ /^$RE_CASE_INSENSITIVE_CHARS+$/o) {
808 30         98 $field = "lower($field)";
809 30         72 $value = lc($value);
810             }
811 43         242 return ($field, $operator, $value,undef);
812             }
813              
814             =head2 Transactions
815              
816             L emulates nested transactions,
817             by keeping a transaction stack depth.
818              
819             B In nested transactions you shouldn't mix rollbacks and commits,
820             because only last action really do commit/rollback. For example next code
821             would produce desired results:
822              
823             $handle->BeginTransaction;
824             $handle->BeginTransaction;
825             ...
826             $handle->Rollback;
827             $handle->BeginTransaction;
828             ...
829             $handle->Commit;
830             $handle->Commit;
831              
832             Only last action(Commit in example) finilize transaction in DB.
833              
834             =head3 BeginTransaction
835              
836             Tells DBIx::SearchBuilder to begin a new SQL transaction.
837             This will temporarily suspend Autocommit mode.
838              
839             =cut
840              
841             sub BeginTransaction {
842 10     10 1 2237 my $self = shift;
843              
844 10         24 my $depth = $self->TransactionDepth;
845 10 100       28 return unless defined $depth;
846              
847 9         27 $self->TransactionDepth(++$depth);
848 9 100       43 return 1 if $depth > 1;
849              
850 6         14 return $self->dbh->begin_work;
851             }
852              
853             =head3 EndTransaction [Action => 'commit'] [Force => 0]
854              
855             Tells to end the current transaction. Takes C argument
856             that could be C or C, the default value
857             is C.
858              
859             If C argument is true then all nested transactions
860             would be committed or rolled back.
861              
862             If there is no transaction in progress then method throw
863             warning unless action is forced.
864              
865             Method returns true on success or false if an error occurred.
866              
867             =cut
868              
869             sub EndTransaction {
870 20     20 1 34 my $self = shift;
871 20         80 my %args = ( Action => 'commit', Force => 0, @_ );
872 20 100       62 my $action = lc $args{'Action'} eq 'commit'? 'commit': 'rollback';
873              
874 20   100     44 my $depth = $self->TransactionDepth || 0;
875 20 100       42 unless ( $depth ) {
876 11 100       35 unless( $args{'Force'} ) {
877 4         517 Carp::cluck( "Attempted to $action a transaction with none in progress" );
878 4         428 return 0;
879             }
880 7         45 return 1;
881             } else {
882 9         15 $depth--;
883             }
884 9 50       20 $depth = 0 if $args{'Force'};
885              
886 9         23 $self->TransactionDepth( $depth );
887              
888 9         22 my $dbh = $self->dbh;
889 9         27 $TRANSROLLBACK{ $dbh }{ $action }++;
890 9 100       35 if ( $TRANSROLLBACK{ $dbh }{ $action eq 'commit'? 'rollback' : 'commit' } ) {
    100          
891 2         38 warn "Rollback and commit are mixed while escaping nested transaction";
892             }
893 9 100       50 return 1 if $depth;
894              
895 6         15 delete $TRANSROLLBACK{ $dbh };
896              
897 6 100       15 if ($action eq 'commit') {
898 4         17001 return $dbh->commit;
899             }
900             else {
901 2 50       27 DBIx::SearchBuilder::Record::Cachable->FlushCache
902             if DBIx::SearchBuilder::Record::Cachable->can('FlushCache');
903 2         21 return $dbh->rollback;
904             }
905             }
906              
907             =head3 Commit [FORCE]
908              
909             Tells to commit the current SQL transaction.
910              
911             Method uses C method, read its
912             L.
913              
914             =cut
915              
916             sub Commit {
917 10     10 1 591 my $self = shift;
918 10         24 $self->EndTransaction( Action => 'commit', Force => shift );
919             }
920              
921              
922             =head3 Rollback [FORCE]
923              
924             Tells to abort the current SQL transaction.
925              
926             Method uses C method, read its
927             L.
928              
929             =cut
930              
931             sub Rollback {
932 8     8 1 16 my $self = shift;
933 8         36 $self->EndTransaction( Action => 'rollback', Force => shift );
934             }
935              
936              
937             =head3 ForceRollback
938              
939             Force the handle to rollback.
940             Whether or not we're deep in nested transactions.
941              
942             =cut
943              
944             sub ForceRollback {
945 1     1 1 3 my $self = shift;
946 1         2 $self->Rollback(1);
947             }
948              
949              
950             =head3 TransactionDepth
951              
952             Returns the current depth of the nested transaction stack.
953             Returns C if there is no connection to database.
954              
955             =cut
956              
957             sub TransactionDepth {
958 61     61 1 2562 my $self = shift;
959              
960 61         140 my $dbh = $self->dbh;
961 61 100 66     322 return undef unless $dbh && $dbh->ping;
962              
963 51 100       1535 if ( @_ ) {
964 18         39 my $depth = shift;
965 18 100       36 if ( $depth ) {
966 12         36 $TRANSDEPTH{ $dbh } = $depth;
967             } else {
968 6         21 delete $TRANSDEPTH{ $dbh };
969             }
970             }
971 51   100     293 return $TRANSDEPTH{ $dbh } || 0;
972             }
973              
974              
975             =head2 ApplyLimits STATEMENTREF ROWS_PER_PAGE FIRST_ROW
976              
977             takes an SQL SELECT statement and massages it to return ROWS_PER_PAGE starting with FIRST_ROW;
978              
979             =cut
980              
981             sub ApplyLimits {
982 122     122 1 239 my $self = shift;
983 122         228 my $statementref = shift;
984 122         201 my $per_page = shift;
985 122         201 my $first = shift;
986 122         199 my $sb = shift;
987              
988 122         220 my $limit_clause = '';
989              
990 122 100       301 if ( $per_page) {
991 19         47 $limit_clause = " LIMIT ";
992 19 100       60 if ( $sb->{_bind_values} ) {
993 4   66     10 push @{$sb->{_bind_values}}, $first || (), $per_page;
  4         17  
994 4 100       11 $first = '?' if $first;
995 4         8 $per_page = '?';
996             }
997              
998 19 100       41 if ( $first ) {
999 12         24 $limit_clause .= $first . ", ";
1000             }
1001 19         49 $limit_clause .= $per_page;
1002             }
1003              
1004 122         311 $$statementref .= $limit_clause;
1005             }
1006              
1007              
1008              
1009              
1010              
1011             =head2 Join { Paramhash }
1012              
1013             Takes a paramhash of everything Searchbuildler::Record does
1014             plus a parameter called 'SearchBuilder' that contains a ref
1015             to a SearchBuilder object'.
1016              
1017             This performs the join.
1018              
1019              
1020             =cut
1021              
1022              
1023             sub Join {
1024              
1025 28     28 1 61 my $self = shift;
1026 28         226 my %args = (
1027             SearchBuilder => undef,
1028             TYPE => 'normal',
1029             ALIAS1 => 'main',
1030             FIELD1 => undef,
1031             TABLE2 => undef,
1032             COLLECTION2 => undef,
1033             FIELD2 => undef,
1034             ALIAS2 => undef,
1035             EXPRESSION => undef,
1036             @_
1037             );
1038              
1039              
1040 28         65 my $alias;
1041              
1042             #If we're handed in an ALIAS2, we need to go remove it from the Aliases array.
1043             # Basically, if anyone generates an alias and then tries to use it in a join later, we want to be smart about
1044             # creating joins, so we need to go rip it out of the old aliases table and drop it in as an explicit join
1045 28 100       128 if ( $args{'ALIAS2'} ) {
    50          
1046              
1047             # this code is slow and wasteful, but it's clear.
1048 4         7 my @aliases = @{ $args{'SearchBuilder'}->{'aliases'} };
  4         23  
1049 4         19 my @new_aliases;
1050 4         13 foreach my $old_alias (@aliases) {
1051 4 50       110 if ( $old_alias =~ /^(.*?) (\Q$args{'ALIAS2'}\E)$/ ) {
1052 4         21 $args{'TABLE2'} = $1;
1053 4         10 $alias = $2;
1054 4 50       15 $args{'TABLE2'} = $self->DequoteName($args{'TABLE2'}) if $self->QuoteTableNames;
1055             }
1056             else {
1057 0         0 push @new_aliases, $old_alias;
1058             }
1059             }
1060              
1061             # If we found an alias, great. let's just pull out the table and alias for the other item
1062 4 50       17 unless ($alias) {
1063              
1064             # if we can't do that, can we reverse the join and have it work?
1065 0         0 my $a1 = $args{'ALIAS1'};
1066 0         0 my $f1 = $args{'FIELD1'};
1067 0         0 $args{'ALIAS1'} = $args{'ALIAS2'};
1068 0         0 $args{'FIELD1'} = $args{'FIELD2'};
1069 0         0 $args{'ALIAS2'} = $a1;
1070 0         0 $args{'FIELD2'} = $f1;
1071              
1072 0         0 @aliases = @{ $args{'SearchBuilder'}->{'aliases'} };
  0         0  
1073 0         0 @new_aliases = ();
1074 0         0 foreach my $old_alias (@aliases) {
1075 0 0       0 if ( $old_alias =~ /^(.*?) ($args{'ALIAS2'})$/ ) {
1076 0         0 $args{'TABLE2'} = $1;
1077 0         0 $alias = $2;
1078 0 0       0 $args{'TABLE2'} = $self->DequoteName($args{'TABLE2'}) if $self->QuoteTableNames;
1079             }
1080             else {
1081 0         0 push @new_aliases, $old_alias;
1082             }
1083             }
1084              
1085             } else {
1086             # we found alias, so NewAlias should take care of distinctness
1087 4 50       24 $args{'DISTINCT'} = 1 unless exists $args{'DISTINCT'};
1088             }
1089              
1090 4 50       15 unless ( $alias ) {
1091             # XXX: this situation is really bug in the caller!!!
1092 0         0 return ( $self->_NormalJoin(%args) );
1093             }
1094 4         13 $args{'SearchBuilder'}->{'aliases'} = \@new_aliases;
1095             } elsif ( $args{'COLLECTION2'} ) {
1096             # We're joining to a pre-limited collection. We need to take
1097             # all clauses in the other collection, munge 'main.' to a new
1098             # alias, apply them locally, then proceed as usual.
1099 0         0 my $collection = delete $args{'COLLECTION2'};
1100 0         0 $alias = $args{ALIAS2} = $args{'SearchBuilder'}->_GetAlias( $collection->Table );
1101 0         0 $args{TABLE2} = $collection->Table;
1102              
1103 0         0 eval {$collection->_ProcessRestrictions}; # RT hate
  0         0  
1104              
1105             # Move over unused aliases
1106 0         0 push @{$args{SearchBuilder}{aliases}}, @{$collection->{aliases}};
  0         0  
  0         0  
1107              
1108             # Move over joins, as well
1109 0         0 for my $join (sort keys %{$collection->{left_joins}}) {
  0         0  
1110 0         0 my %alias = %{$collection->{left_joins}{$join}};
  0         0  
1111 0 0       0 $alias{depends_on} = $alias if $alias{depends_on} eq "main";
1112             $alias{criteria} = $self->_RenameRestriction(
1113             RESTRICTIONS => $alias{criteria},
1114 0         0 NEW => $alias
1115             );
1116 0         0 $args{SearchBuilder}{left_joins}{$join} = \%alias;
1117             }
1118              
1119             my $restrictions = $self->_RenameRestriction(
1120             RESTRICTIONS => $collection->{restrictions},
1121 0         0 NEW => $alias
1122             );
1123 0         0 $args{SearchBuilder}{restrictions}{$_} = $restrictions->{$_} for keys %{$restrictions};
  0         0  
1124             } else {
1125 24         141 $alias = $args{'SearchBuilder'}->_GetAlias( $args{'TABLE2'} );
1126             }
1127 28 50       104 $args{TABLE2} = $self->QuoteName($args{TABLE2}) if $self->QuoteTableNames;
1128              
1129 28   50     280 my $meta = $args{'SearchBuilder'}->{'left_joins'}{"$alias"} ||= {};
1130 28 100       169 if ( $args{'TYPE'} =~ /LEFT/i ) {
1131 11         60 $meta->{'alias_string'} = " LEFT JOIN " . $args{'TABLE2'} . " $alias ";
1132 11         26 $meta->{'type'} = 'LEFT';
1133             }
1134             else {
1135 17         93 $meta->{'alias_string'} = " JOIN " . $args{'TABLE2'} . " $alias ";
1136 17         61 $meta->{'type'} = 'NORMAL';
1137             }
1138 28         69 $meta->{'depends_on'} = $args{'ALIAS1'};
1139              
1140 28   33     141 my $criterion = $args{'EXPRESSION'} || $args{'ALIAS1'}.".".$args{'FIELD1'};
1141 28         187 $meta->{'criteria'}{'base_criterion'} =
1142             [ { field => "$alias.$args{'FIELD2'}", op => '=', value => $criterion } ];
1143              
1144 28 100 100     222 if ( $args{'DISTINCT'} && !defined $args{'SearchBuilder'}{'joins_are_distinct'} ) {
    100          
1145 1         6 $args{SearchBuilder}{joins_are_distinct} = 1;
1146             } elsif ( !$args{'DISTINCT'} ) {
1147 22         66 $args{SearchBuilder}{joins_are_distinct} = 0;
1148             }
1149              
1150 28         187 return ($alias);
1151             }
1152              
1153             sub _RenameRestriction {
1154 0     0   0 my $self = shift;
1155 0         0 my %args = (
1156             RESTRICTIONS => undef,
1157             OLD => "main",
1158             NEW => undef,
1159             @_,
1160             );
1161              
1162 0         0 my %return;
1163 0         0 for my $key ( keys %{$args{RESTRICTIONS}} ) {
  0         0  
1164 0         0 my $newkey = $key;
1165 0         0 $newkey =~ s/^\Q$args{OLD}\E\./$args{NEW}./;
1166 0         0 my @parts;
1167 0         0 for my $part ( @{ $args{RESTRICTIONS}{$key} } ) {
  0         0  
1168 0 0       0 if ( ref $part ) {
1169 0         0 my %part = %{$part};
  0         0  
1170 0         0 $part{field} =~ s/^\Q$args{OLD}\E\./$args{NEW}./;
1171 0         0 $part{value} =~ s/^\Q$args{OLD}\E\./$args{NEW}./;
1172 0         0 push @parts, \%part;
1173             } else {
1174 0         0 push @parts, $part;
1175             }
1176             }
1177 0         0 $return{$newkey} = \@parts;
1178             }
1179 0         0 return \%return;
1180             }
1181              
1182             sub _NormalJoin {
1183              
1184 0     0   0 my $self = shift;
1185 0         0 my %args = (
1186             SearchBuilder => undef,
1187             TYPE => 'normal',
1188             FIELD1 => undef,
1189             ALIAS1 => undef,
1190             TABLE2 => undef,
1191             FIELD2 => undef,
1192             ALIAS2 => undef,
1193             @_
1194             );
1195              
1196 0         0 my $sb = $args{'SearchBuilder'};
1197              
1198 0 0       0 if ( $args{'TYPE'} =~ /LEFT/i ) {
1199 0         0 my $alias = $sb->_GetAlias( $args{'TABLE2'} );
1200 0   0     0 my $meta = $sb->{'left_joins'}{"$alias"} ||= {};
1201 0 0       0 $args{TABLE2} = $self->QuoteName($args{TABLE2}) if $self->QuoteTableNames;
1202 0         0 $meta->{'alias_string'} = " LEFT JOIN $args{'TABLE2'} $alias ";
1203 0         0 $meta->{'depends_on'} = $args{'ALIAS1'};
1204 0         0 $meta->{'type'} = 'LEFT';
1205 0         0 $meta->{'criteria'}{'base_criterion'} = [ {
1206             field => "$args{'ALIAS1'}.$args{'FIELD1'}",
1207             op => '=',
1208             value => "$alias.$args{'FIELD2'}",
1209             } ];
1210              
1211 0         0 return ($alias);
1212             }
1213             else {
1214             $sb->DBIx::SearchBuilder::Limit(
1215             ENTRYAGGREGATOR => 'AND',
1216             QUOTEVALUE => 0,
1217             ALIAS => $args{'ALIAS1'},
1218             FIELD => $args{'FIELD1'},
1219 0         0 VALUE => $args{'ALIAS2'} . "." . $args{'FIELD2'},
1220             @_
1221             );
1222             }
1223             }
1224              
1225             # this code is all hacky and evil. but people desperately want _something_ and I'm
1226             # super tired. refactoring gratefully appreciated.
1227              
1228             sub _BuildJoins {
1229 180     180   347 my $self = shift;
1230 180         405 my $sb = shift;
1231              
1232 180         614 $self->OptimizeJoins( SearchBuilder => $sb );
1233 180 50       752 my $table = $self->{'QuoteTableNames'} ? $self->QuoteName($sb->Table) : $sb->Table;
1234              
1235 180         432 my $join_clause = join " CROSS JOIN ", ("$table main"), @{ $sb->{'aliases'} };
  180         527  
1236 180         353 my %processed = map { /^\S+\s+(\S+)$/; $1 => 1 } @{ $sb->{'aliases'} };
  2         10  
  2         8  
  180         406  
1237 180         387 $processed{'main'} = 1;
1238              
1239             # get a @list of joins that have not been processed yet, but depend on processed join
1240 180         341 my $joins = $sb->{'left_joins'};
1241 180   100     998 while ( my @list =
1242             grep !$processed{ $_ }
1243             && (!$joins->{ $_ }{'depends_on'} || $processed{ $joins->{ $_ }{'depends_on'} }),
1244             sort keys %$joins
1245             ) {
1246 53         128 foreach my $join ( @list ) {
1247 53         136 $processed{ $join }++;
1248              
1249 53         104 my $meta = $joins->{ $join };
1250 53   100     189 my $aggregator = $meta->{'entry_aggregator'} || 'AND';
1251              
1252 53         155 $join_clause .= $meta->{'alias_string'} . " ON ";
1253             my @tmp = map {
1254             ref($_)?
1255 248 100       653 $_->{'field'} .' '. $_->{'op'} .' '. $_->{'value'}:
1256             $_
1257             }
1258 53         189 map { ('(', @$_, ')', $aggregator) } sorted_values($meta->{'criteria'});
  62         196  
1259 53         122 pop @tmp;
1260 53         428 $join_clause .= join ' ', @tmp;
1261             }
1262             }
1263              
1264             # here we could check if there is recursion in joins by checking that all joins
1265             # are processed
1266 180 50       565 if ( my @not_processed = grep !$processed{ $_ }, keys %$joins ) {
1267 0         0 die "Unsatisfied dependency chain in joins @not_processed";
1268             }
1269 180         737 return $join_clause;
1270             }
1271              
1272             sub OptimizeJoins {
1273 180     180 0 310 my $self = shift;
1274 180         618 my %args = (SearchBuilder => undef, @_);
1275 180         538 my $joins = $args{'SearchBuilder'}->{'left_joins'};
1276              
1277 180         317 my %processed = map { /^\S+\s+(\S+)$/; $1 => 1 } @{ $args{'SearchBuilder'}->{'aliases'} };
  2         9  
  2         12  
  180         515  
1278 180         757 $processed{ $_ }++ foreach grep $joins->{ $_ }{'type'} ne 'LEFT', keys %$joins;
1279 180         482 $processed{'main'}++;
1280              
1281 180         327 my @ordered;
1282             # get a @list of joins that have not been processed yet, but depend on processed join
1283             # if we are talking about forest then we'll get the second level of the forest,
1284             # but we should process nodes on this level at the end, so we build FILO ordered list.
1285             # finally we'll get ordered list with leafes in the beginning and top most nodes at
1286             # the end.
1287 180   100     881 while ( my @list = grep !$processed{ $_ }
1288             && $processed{ $joins->{ $_ }{'depends_on'} }, sort keys %$joins )
1289             {
1290 15         123 unshift @ordered, @list;
1291 15         96 $processed{ $_ }++ foreach @list;
1292             }
1293              
1294 180         586 foreach my $join ( @ordered ) {
1295 15 100       67 next if $self->MayBeNull( SearchBuilder => $args{'SearchBuilder'}, ALIAS => $join );
1296              
1297 3         31 $joins->{ $join }{'alias_string'} =~ s/^\s*LEFT\s+/ /;
1298 3         12 $joins->{ $join }{'type'} = 'NORMAL';
1299             }
1300              
1301             # here we could check if there is recursion in joins by checking that all joins
1302             # are processed
1303              
1304             }
1305              
1306             =head2 MayBeNull
1307              
1308             Takes a C and C in a hash and resturns
1309             true if restrictions of the query allow NULLs in a table joined with
1310             the ALIAS, otherwise returns false value which means that you can
1311             use normal join instead of left for the aliased table.
1312              
1313             Works only for queries have been built with L and
1314             L methods, for other cases return true value to
1315             avoid fault optimizations.
1316              
1317             =cut
1318              
1319             sub MayBeNull {
1320 15     15 1 26 my $self = shift;
1321 15         54 my %args = (SearchBuilder => undef, ALIAS => undef, @_);
1322             # if we have at least one subclause that is not generic then we should get out
1323             # of here as we can't parse subclauses
1324 15 50       26 return 1 if grep $_ ne 'generic_restrictions', keys %{ $args{'SearchBuilder'}->{'subclauses'} };
  15         61  
1325              
1326             # build full list of generic conditions
1327 15         28 my @conditions;
1328 15         67 foreach ( grep @$_, sorted_values($args{'SearchBuilder'}->{'restrictions'}) ) {
1329 10 50       35 push @conditions, 'AND' if @conditions;
1330 10         26 push @conditions, '(', @$_, ')';
1331             }
1332              
1333             # find tables that depends on this alias and add their join conditions
1334 15         50 foreach my $join ( sorted_values($args{'SearchBuilder'}->{'left_joins'}) ) {
1335             # left joins on the left side so later we'll get 1 AND x expression
1336             # which equal to x, so we just skip it
1337 17 100       56 next if $join->{'type'} eq 'LEFT';
1338 1 50       6 next unless $join->{'depends_on'} eq $args{'ALIAS'};
1339              
1340 1         5 my @tmp = map { ('(', @$_, ')', $join->{'entry_aggregator'}) } sorted_values($join->{'criteria'});
  1         6  
1341 1         2 pop @tmp;
1342              
1343 1         5 @conditions = ('(', @conditions, ')', 'AND', '(', @tmp ,')');
1344              
1345             }
1346 15 100       55 return 1 unless @conditions;
1347              
1348             # replace conditions with boolean result: 1 - allows nulls, 0 - not
1349             # all restrictions on that don't act on required alias allow nulls
1350             # otherwise only IS NULL operator
1351 10         22 foreach ( splice @conditions ) {
1352 46 50       139 unless ( ref $_ ) {
    100          
    100          
    100          
1353 33         64 push @conditions, $_;
1354 0         0 } elsif ( rindex( $_->{'field'}, "$args{'ALIAS'}.", 0 ) == 0 ) {
1355             # field is alias.xxx op ... and only IS op allows NULLs
1356 9         27 push @conditions, lc $_->{op} eq 'is';
1357 0 50       0 } elsif ( $_->{'value'} && rindex( $_->{'value'}, "$args{'ALIAS'}.", 0 ) == 0 ) {
1358             # value is alias.xxx so it can not be IS op
1359 1         2 push @conditions, 0;
1360 0         0 } elsif ( $_->{'field'} =~ /^(?i:lower)\(\s*\Q$args{'ALIAS'}\./ ) {
1361             # handle 'LOWER(alias.xxx) OP VALUE' we use for case insensetive
1362 0         0 push @conditions, lc $_->{op} eq 'is';
1363             } else {
1364 3         9 push @conditions, 1;
1365             }
1366             }
1367              
1368             # resturns index of closing paren by index of openning paren
1369             my $closing_paren = sub {
1370 0     0   0 my $i = shift;
1371 0         0 my $count = 0;
1372 0         0 for ( ; $i < @conditions; $i++ ) {
1373 0 0       0 if ( $conditions[$i] eq '(' ) {
    0          
1374 0         0 $count++;
1375             }
1376             elsif ( $conditions[$i] eq ')' ) {
1377 0         0 $count--;
1378             }
1379 0 0       0 return $i unless $count;
1380             }
1381 0         0 die "lost in parens";
1382 10         75 };
1383              
1384             # solve boolean expression we have, an answer is our result
1385 10         19 my $parens_count = 0;
1386 10         16 my @tmp = ();
1387 10         25 while ( defined ( my $e = shift @conditions ) ) {
1388             #print "@tmp >>>$e<<< @conditions\n";
1389 48 100 100     188 return $e if !@conditions && !@tmp;
1390              
1391 38 50       93 unless ( $e ) {
    100          
    100          
    100          
1392 3 100       11 if ( $conditions[0] eq ')' ) {
1393 1         3 push @tmp, $e;
1394 1         3 next;
1395             }
1396              
1397 2         10 my $aggreg = uc shift @conditions;
1398 2 50       6 if ( $aggreg eq 'OR' ) {
    0          
1399             # 0 OR x == x
1400 2         5 next;
1401             } elsif ( $aggreg eq 'AND' ) {
1402             # 0 AND x == 0
1403 0         0 my $close_p = $closing_paren->(0);
1404 0         0 splice @conditions, 0, $close_p + 1, (0);
1405             } else {
1406 0         0 die "unknown aggregator: @tmp $e >>>$aggreg<<< @conditions";
1407             }
1408 0         0 } elsif ( $e eq '1' ) {
1409 6 100       23 if ( $conditions[0] eq ')' ) {
1410 5         12 push @tmp, $e;
1411 5         12 next;
1412             }
1413              
1414 1         4 my $aggreg = uc shift @conditions;
1415 1 50       6 if ( $aggreg eq 'OR' ) {
    50          
1416             # 1 OR x == 1
1417 0         0 my $close_p = $closing_paren->(0);
1418 0         0 splice @conditions, 0, $close_p + 1, (1);
1419             } elsif ( $aggreg eq 'AND' ) {
1420             # 1 AND x == x
1421 1         3 next;
1422             } else {
1423 0         0 die "unknown aggregator: @tmp $e >>>$aggreg<<< @conditions";
1424             }
1425 0         0 } elsif ( $e eq '(' ) {
1426 23 100       43 if ( $conditions[1] eq ')' ) {
1427 15         40 splice @conditions, 1, 1;
1428             } else {
1429 8         12 $parens_count++;
1430 8         20 push @tmp, $e;
1431             }
1432 0         0 } elsif ( $e eq ')' ) {
1433 6 50       14 die "extra closing paren: @tmp >>>$e<<< @conditions"
1434             if --$parens_count < 0;
1435              
1436 6         15 unshift @conditions, @tmp, $e;
1437 6         15 @tmp = ();
1438             } else {
1439 0         0 die "lost: @tmp >>>$e<<< @conditions";
1440             }
1441             }
1442 0         0 return 1;
1443             }
1444              
1445             =head2 DistinctQuery STATEMENTREF
1446              
1447             takes an incomplete SQL SELECT statement and massages it to return a DISTINCT result set.
1448              
1449              
1450             =cut
1451              
1452             sub DistinctQuery {
1453 16     16 1 41 my $self = shift;
1454 16         28 my $statementref = shift;
1455 16         30 my $sb = shift;
1456              
1457 16         42 my $QueryHint = $sb->QueryHint;
1458 16 50       43 $QueryHint = $QueryHint ? " /* $QueryHint */ " : " ";
1459              
1460             # Prepend select query for DBs which allow DISTINCT on all column types.
1461 16         70 $$statementref = "SELECT" . $QueryHint . "DISTINCT main.* FROM $$statementref";
1462 16         46 $$statementref .= $sb->_GroupClause;
1463 16         49 $$statementref .= $sb->_OrderClause;
1464             }
1465              
1466             =head2 DistinctQueryAndCount STATEMENTREF
1467              
1468             takes an incomplete SQL SELECT statement and massages it to return a
1469             DISTINCT result set and the total count of potential records.
1470              
1471             =cut
1472              
1473             sub DistinctQueryAndCount {
1474 1     1 1 4 my $self = shift;
1475 1         2 my $statementref = shift;
1476 1         2 my $sb = shift;
1477              
1478 1         6 $self->DistinctQuery($statementref, $sb);
1479              
1480             # Add the count part.
1481 1 50       4 if ( $sb->_OrderClause !~ /(?
1482             # Wrap it with another SELECT to get distinct count.
1483 1         7 $$statementref
1484             = 'SELECT main.*, COUNT(main.id) OVER() AS search_builder_count_all FROM (' . $$statementref . ') main';
1485             }
1486             else {
1487             # if order by other tables, then DistinctQuery already has an outer SELECT, which we can reuse
1488 0         0 $$statementref =~ s!(?= FROM)!, COUNT(main.id) OVER() AS search_builder_count_all!;
1489             }
1490             }
1491              
1492              
1493              
1494             =head2 DistinctCount STATEMENTREF
1495              
1496             takes an incomplete SQL SELECT statement and massages it to return a DISTINCT result set.
1497              
1498              
1499             =cut
1500              
1501             sub DistinctCount {
1502 0     0 1 0 my $self = shift;
1503 0         0 my $statementref = shift;
1504 0         0 my $sb = shift;
1505              
1506 0         0 my $QueryHint = $sb->QueryHint;
1507 0 0       0 $QueryHint = $QueryHint ? " /* $QueryHint */ " : " ";
1508              
1509             # Prepend select query for DBs which allow DISTINCT on all column types.
1510 0         0 $$statementref = "SELECT" . $QueryHint . "COUNT(DISTINCT main.id) FROM $$statementref";
1511              
1512             }
1513              
1514             sub Fields {
1515 0     0 0 0 my $self = shift;
1516 0         0 my $table = lc shift;
1517              
1518 0 0       0 unless ( $FIELDS_IN_TABLE{$table} ) {
1519 0         0 $FIELDS_IN_TABLE{ $table } = [];
1520 0 0       0 my $sth = $self->dbh->column_info( undef, '', $table, '%' )
1521             or return ();
1522 0         0 my $info = $sth->fetchall_arrayref({});
1523 0         0 foreach my $e ( @$info ) {
1524 0         0 push @{ $FIELDS_IN_TABLE{ $table } }, $e->{'COLUMN_NAME'};
  0         0  
1525             }
1526             }
1527              
1528 0         0 return @{ $FIELDS_IN_TABLE{ $table } };
  0         0  
1529             }
1530              
1531              
1532             =head2 Log MESSAGE
1533              
1534             Takes a single argument, a message to log.
1535              
1536             Currently prints that message to STDERR
1537              
1538             =cut
1539              
1540             sub Log {
1541 0     0 1 0 my $self = shift;
1542 0         0 my $msg = shift;
1543 0         0 warn $msg."\n";
1544             }
1545              
1546             =head2 SimpleDateTimeFunctions
1547              
1548             See L for details on supported functions.
1549             This method is for implementers of custom DB connectors.
1550              
1551             Returns hash reference with (function name, sql template) pairs.
1552              
1553             =cut
1554              
1555             sub SimpleDateTimeFunctions {
1556 1     1 1 2 my $self = shift;
1557             return {
1558 1         57 datetime => 'SUBSTR(?, 1, 19)',
1559             time => 'SUBSTR(?, 12, 8)',
1560              
1561             hourly => 'SUBSTR(?, 1, 13)',
1562             hour => 'SUBSTR(?, 12, 2 )',
1563              
1564             date => 'SUBSTR(?, 1, 10)',
1565             daily => 'SUBSTR(?, 1, 10)',
1566              
1567             day => 'SUBSTR(?, 9, 2 )',
1568             dayofmonth => 'SUBSTR(?, 9, 2 )',
1569              
1570             monthly => 'SUBSTR(?, 1, 7 )',
1571             month => 'SUBSTR(?, 6, 2 )',
1572              
1573             annually => 'SUBSTR(?, 1, 4 )',
1574             year => 'SUBSTR(?, 1, 4 )',
1575             };
1576             }
1577              
1578             =head2 DateTimeFunction
1579              
1580             Takes named arguments:
1581              
1582             =over 4
1583              
1584             =item * Field - SQL expression date/time function should be applied
1585             to. Note that this argument is used as is without any kind of quoting.
1586              
1587             =item * Type - name of the function, see supported values below.
1588              
1589             =item * Timezone - optional hash reference with From and To values,
1590             see L for details.
1591              
1592             =back
1593              
1594             Returns SQL statement. Returns NULL if function is not supported.
1595              
1596             =head3 Supported functions
1597              
1598             Type value in L is case insesitive. Spaces,
1599             underscores and dashes are ignored. So 'date time', 'DateTime'
1600             and 'date_time' are all synonyms. The following functions are
1601             supported:
1602              
1603             =over 4
1604              
1605             =item * date time - as is, no conversion, except applying timezone
1606             conversion if it's provided.
1607              
1608             =item * time - time only
1609              
1610             =item * hourly - datetime prefix up to the hours, e.g. '2010-03-25 16'
1611              
1612             =item * hour - hour, 0 - 23
1613              
1614             =item * date - date only
1615              
1616             =item * daily - synonym for date
1617              
1618             =item * day of week - 0 - 6, 0 - Sunday
1619              
1620             =item * day - day of month, 1 - 31
1621              
1622             =item * day of month - synonym for day
1623              
1624             =item * day of year - 1 - 366, support is database dependent
1625              
1626             =item * month - 1 - 12
1627              
1628             =item * monthly - year and month prefix, e.g. '2010-11'
1629              
1630             =item * year - e.g. '2023'
1631              
1632             =item * annually - synonym for year
1633              
1634             =item * week of year - 0-53, presence of zero week, 1st week meaning
1635             and whether week starts on Monday or Sunday heavily depends on database.
1636              
1637             =back
1638              
1639             =cut
1640              
1641             sub DateTimeFunction {
1642 20     20 1 596 my $self = shift;
1643 20         66 my %args = (
1644             Field => undef,
1645             Type => '',
1646             Timezone => undef,
1647             @_
1648             );
1649              
1650 20   50     73 my $res = $args{'Field'} || '?';
1651 20 50       44 if ( $args{'Timezone'} ) {
1652             $res = $self->ConvertTimezoneFunction(
1653 0         0 %{ $args{'Timezone'} },
  0         0  
1654             Field => $res,
1655             );
1656             }
1657              
1658 20         34 my $norm_type = lc $args{'Type'};
1659 20         78 $norm_type =~ s/[ _-]//g;
1660 20 100       66 if ( my $template = $self->SimpleDateTimeFunctions->{ $norm_type } ) {
1661 18         72 $template =~ s/\?/$res/;
1662 18         38 $res = $template;
1663             }
1664             else {
1665 2         16 return 'NULL';
1666             }
1667 18         62 return $res;
1668             }
1669              
1670             =head2 ConvertTimezoneFunction
1671              
1672             Generates a function applied to Field argument that converts timezone.
1673             By default converts from UTC. Examples:
1674              
1675             # UTC => Moscow
1676             $handle->ConvertTimezoneFunction( Field => '?', To => 'Europe/Moscow');
1677              
1678             If there is problem with arguments or timezones are equal
1679             then Field returned without any function applied. Field argument
1680             is not escaped in any way, it's your job.
1681              
1682             Implementation is very database specific. To be portable convert
1683             from UTC or to UTC. Some databases have internal storage for
1684             information about timezones that should be kept up to date.
1685             Read documentation for your DB.
1686              
1687             =cut
1688              
1689             sub ConvertTimezoneFunction {
1690 0     0 1 0 my $self = shift;
1691 0         0 my %args = (
1692             From => 'UTC',
1693             To => undef,
1694             Field => '',
1695             @_
1696             );
1697 0         0 return $args{'Field'};
1698             }
1699              
1700             =head2 DateTimeIntervalFunction
1701              
1702             Generates a function to calculate interval in seconds between two
1703             dates. Takes From and To arguments which can be either scalar or
1704             a hash. Hash is processed with L.
1705              
1706             Arguments are not quoted or escaped in any way. It's caller's job.
1707              
1708             =cut
1709              
1710             sub DateTimeIntervalFunction {
1711 2     2 1 10 my $self = shift;
1712 2         18 my %args = ( From => undef, To => undef, @_ );
1713              
1714             $_ = DBIx::SearchBuilder->CombineFunctionWithField(%$_)
1715 2         28 for grep ref, @args{'From', 'To'};
1716              
1717 2         37 return $self->_DateTimeIntervalFunction( %args );
1718             }
1719              
1720 0     0   0 sub _DateTimeIntervalFunction { return 'NULL' }
1721              
1722             =head2 NullsOrder
1723              
1724             Sets order of NULLs when sorting columns when called with mode,
1725             but only if DB supports it. Modes:
1726              
1727             =over 4
1728              
1729             =item * small
1730              
1731             NULLs are smaller then anything else, so come first when order
1732             is ASC and last otherwise.
1733              
1734             =item * large
1735              
1736             NULLs are larger then anything else.
1737              
1738             =item * first
1739              
1740             NULLs are always first.
1741              
1742             =item * last
1743              
1744             NULLs are always last.
1745              
1746             =item * default
1747              
1748             Return back to DB's default behaviour.
1749              
1750             =back
1751              
1752             When called without argument returns metadata required to generate
1753             SQL.
1754              
1755             =cut
1756              
1757             sub NullsOrder {
1758 54     54 1 95 my $self = shift;
1759              
1760 54 50       135 unless ($self->HasSupportForNullsOrder) {
1761 54 50       112 warn "No support for changing NULLs order" if @_;
1762 54         133 return undef;
1763             }
1764              
1765 0 0       0 if ( @_ ) {
1766 0   0     0 my $mode = shift || 'default';
1767 0 0       0 if ( $mode eq 'default' ) {
    0          
    0          
    0          
    0          
1768 0         0 delete $self->{'nulls_order'};
1769             }
1770             elsif ( $mode eq 'small' ) {
1771 0         0 $self->{'nulls_order'} = { ASC => 'NULLS FIRST', DESC => 'NULLS LAST' };
1772             }
1773             elsif ( $mode eq 'large' ) {
1774 0         0 $self->{'nulls_order'} = { ASC => 'NULLS LAST', DESC => 'NULLS FIRST' };
1775             }
1776             elsif ( $mode eq 'first' ) {
1777 0         0 $self->{'nulls_order'} = { ASC => 'NULLS FIRST', DESC => 'NULLS FIRST' };
1778             }
1779             elsif ( $mode eq 'last' ) {
1780 0         0 $self->{'nulls_order'} = { ASC => 'NULLS LAST', DESC => 'NULLS LAST' };
1781             }
1782             else {
1783 0         0 warn "'$mode' is not supported NULLs ordering mode";
1784 0         0 delete $self->{'nulls_order'};
1785             }
1786             }
1787              
1788 0 0       0 return undef unless $self->{'nulls_order'};
1789 0         0 return $self->{'nulls_order'};
1790             }
1791              
1792             =head2 HasSupportForNullsOrder
1793              
1794             Returns true value if DB supports adjusting NULLs order while sorting
1795             a column, for example C.
1796              
1797             =cut
1798              
1799             sub HasSupportForNullsOrder {
1800 55     55 1 135 return 0;
1801             }
1802              
1803             =head2 HasSupportForCombineSearchAndCount
1804              
1805             Returns true value if DB supports to combine search and count in single
1806             query.
1807              
1808             =cut
1809              
1810             sub HasSupportForCombineSearchAndCount {
1811 4     4 1 18 return 1;
1812             }
1813              
1814             =head2 QuoteName
1815              
1816             Quote table or column name to avoid reserved word errors.
1817              
1818             Returns same value passed unless over-ridden in database-specific subclass.
1819              
1820             =cut
1821              
1822             # over-ride in subclass
1823             sub QuoteName {
1824 0     0 1 0 my ($self, $name) = @_;
1825             # use dbi built in quoting if we have a connection,
1826 0 0       0 if ($self->dbh) {
1827 0         0 return $self->dbh->quote_identifier($name);
1828             }
1829 0         0 warn "QuoteName called without a db handle";
1830 0         0 return $name;
1831             }
1832              
1833             =head2 DequoteName
1834              
1835             Undo the effects of QuoteName by removing quoting.
1836              
1837             =cut
1838              
1839             sub DequoteName {
1840 0     0 1 0 my ($self, $name) = @_;
1841 0 0       0 if ($self->dbh) {
1842             # 29 = SQL_IDENTIFIER_QUOTE_CHAR; see "perldoc DBI"
1843 0         0 my $quote_char = $self->dbh->get_info( 29 );
1844              
1845 0 0       0 if ($quote_char) {
1846 0 0       0 if ($name =~ /^$quote_char(.*)$quote_char$/) {
1847 0         0 return $1;
1848             }
1849             }
1850 0         0 return $name;
1851             }
1852 0         0 warn "DequoteName called without a db handle";
1853 0         0 return $name;
1854             }
1855              
1856             sub _ExtractBindValues {
1857 32     32   53 my $self = shift;
1858 32         49 my $string = shift;
1859 32   50     102 my $default_escape_char = shift || q{'};
1860 32 50       76 return $string unless defined $string;
1861              
1862 32         46 my $placeholder = '';
1863              
1864 32         380 my @chars = split //, $string;
1865 32         109 my $value = '';
1866 32         44 my $escape_char = $default_escape_char;
1867              
1868 32         46 my @values;
1869 32         82 my $in = 0; # keep state in the loop: is it in a quote?
1870 32         81 while ( defined( my $c = shift @chars ) ) {
1871 2542         3250 my $escaped;
1872 2542 100 100     4603 if ( $c eq $escape_char && $in ) {
1873 68 50       138 if ( $escape_char eq q{'} ) {
1874 68 100 50     173 if ( ( $chars[0] || '' ) eq q{'} ) {
1875 18         28 $c = shift @chars;
1876 18         27 $escaped = 1;
1877             }
1878             }
1879             else {
1880 0         0 $c = shift @chars;
1881 0         0 $escaped = 1;
1882             }
1883             }
1884              
1885 2542 100       3690 if ($in) {
1886 660 100       1092 if ( $c eq q{'} ) {
1887 68 100       122 if ( !$escaped ) {
1888 50         94 push @values, $value;
1889 50         68 $in = 0;
1890 50         75 $value = '';
1891 50         74 $escape_char = $default_escape_char;
1892 50         78 $placeholder .= '?';
1893 50         120 next;
1894             }
1895             }
1896 610         1197 $value .= $c;
1897             }
1898             else {
1899 1882 100 50     5810 if ( $c eq q{'} ) {
    50 66        
    50 66        
1900 50         101 $in = 1;
1901             }
1902              
1903             # Handle quoted string like e'foo\\bar'
1904             elsif ( lc $c eq 'e' && ( $chars[0] // '' ) eq q{'} ) {
1905 0         0 $escape_char = '\\';
1906             }
1907              
1908             # Handle numbers
1909             elsif ( $c =~ /[\d.]/ && $placeholder !~ /\w$/ ) { # Do not catch Groups_1.Name
1910 0         0 $value .= $c;
1911 0   0     0 while ( ( $chars[0] // '' ) =~ /[\d.]/ ) {
1912 0         0 $value .= shift @chars;
1913             }
1914              
1915 0         0 push @values, $value;
1916 0         0 $placeholder .= '?';
1917 0         0 $value = '';
1918             }
1919             else {
1920 1832         3796 $placeholder .= $c;
1921             }
1922             }
1923             }
1924 32         148 return ( $placeholder, @values );
1925             }
1926              
1927 21     21   95 sub _RequireQuotedTables { return 0 };
1928              
1929             =head2 DESTROY
1930              
1931             When we get rid of the Searchbuilder::Handle, we need to disconnect from the database
1932              
1933             =cut
1934              
1935             sub DESTROY {
1936 22     22   8948 my $self = shift;
1937 22 50       146 $self->Disconnect if $self->{'DisconnectHandleOnDestroy'};
1938 22         470 delete $DBIHandle{$self};
1939             }
1940              
1941              
1942             1;