File Coverage

blib/lib/DBIx/SearchBuilder/Handle.pm
Criterion Covered Total %
statement 427 667 64.0
branch 153 282 54.2
condition 44 85 51.7
subroutine 49 72 68.0
pod 47 51 92.1
total 720 1157 62.2


line stmt bran cond sub pod time code
1             package DBIx::SearchBuilder::Handle;
2              
3 25     25   209370 use strict;
  25         81  
  25         736  
4 25     25   129 use warnings;
  25         47  
  25         708  
5              
6 25     25   125 use Carp qw(croak cluck);
  25         56  
  25         1384  
7 25     25   7166 use DBI;
  25         72862  
  25         1103  
8 25     25   3735 use Class::ReturnValue;
  25         99244  
  25         2804  
9 25     25   4156 use Encode qw();
  25         70337  
  25         610  
10 25     25   11103 use version;
  25         46858  
  25         145  
11              
12 25     25   5298 use DBIx::SearchBuilder::Util qw/ sorted_values /;
  25         66  
  25         1480  
13              
14 25     25   173 use vars qw(@ISA %DBIHandle $PrevHandle $DEBUG %TRANSDEPTH %TRANSROLLBACK %FIELDS_IN_TABLE);
  25         89  
  25         60496  
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 1979 my $proto = shift;
49 23   33     198 my $class = ref($proto) || $proto;
50 23         61 my $self = {};
51 23         69 bless ($self, $class);
52              
53             # Enable quotes table names
54 23         91 my %args = ( QuoteTableNames => 0, @_ );
55 23         155 $self->{'QuoteTableNames'} = $args{QuoteTableNames};
56              
57 23         52 @{$self->{'StatementLog'}} = ();
  23         79  
58 23         88 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 19597 my $self = shift;
85 22         205 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     282 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         69 $self->{'DisconnectHandleOnDestroy'} = $args{'DisconnectHandleOnDestroy'};
106              
107             # Enable optional quoted table names
108 21 50       74 $self->{'QuoteTableNames'} = delete $args{QuoteTableNames} if defined $args{QuoteTableNames};
109              
110 21   50     106 my $old_dsn = $self->DSN || '';
111 21         158 my $new_dsn = $self->BuildDSN( %args );
112              
113             # Only connect if we're not connected to this source already
114 21 0 33     114 return undef if $self->dbh && $self->dbh->ping && $new_dsn eq $old_dsn;
      33        
115              
116             my $handle = DBI->connect(
117 21 50       216 $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         50957 $handle->{FetchHashKeyName} ='NAME_lc';
123              
124             # Set the handle
125 21         146 $self->dbh($handle);
126              
127             # Cache version info
128 21         99 $self->DatabaseVersion;
129              
130             # force quoted tables for mysql 8
131 21 50       190 $self->{'QuoteTableNames'} = 1 if $self->_RequireQuotedTables;
132              
133 21         145 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         2 my $driver = shift;
148 1         3 my $class = 'DBIx::SearchBuilder::Handle::' . $driver;
149 1         2 local $@;
150 1         80 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 48 my $self = shift;
171 21         134 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         80 my $dsn = "dbi:$args{'Driver'}:dbname=$args{'Database'}";
182 21 50       81 $dsn .= ";sid=$args{'SID'}" if $args{'SID'};
183 21 50       81 $dsn .= ";host=$args{'Host'}" if $args{'Host'};
184 21 50       117 $dsn .= ";port=$args{'Port'}" if $args{'Port'};
185 21 50       59 $dsn .= ";requiressl=1" if $args{'RequireSSL'};
186              
187 21         84 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 139 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 1000     1000 1 2315 my $self = shift;
249 1000 50       3086 if (@_) {
250 0         0 require Time::HiRes;
251 0         0 $self->{'_DoLogSQL'} = shift;
252             }
253 1000         3263 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 946     946 1 2480 my $self=shift;
359              
360             #If we are setting the database handle, set it.
361 946 100       2242 if ( @_ ) {
362 21         145 $DBIHandle{$self} = $PrevHandle = shift;
363 21         61 %FIELDS_IN_TABLE = ();
364             }
365              
366 946   66     9496 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 159     159 1 331 my $self = shift;
383 159         529 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 159     159 1 589 my($self, $table, @pairs) = @_;
399 159         351 my(@cols, @vals, @bind);
400              
401 159         511 while ( my $key = shift @pairs ) {
402 412         768 push @cols, $key;
403 412         665 push @vals, '?';
404 412         1132 push @bind, shift @pairs;
405             }
406              
407 159 50       481 $table = $self->QuoteName($table) if $self->QuoteTableNames;
408 159         470 my $QueryString = "INSERT INTO $table";
409 159         778 $QueryString .= " (". join(", ", @cols) .")";
410 159         515 $QueryString .= " VALUES (". join(", ", @vals). ")";
411 159         819 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 716 my ($self, $table, $columns, $query, @binds) = @_;
429              
430 4 50       22 $columns = join ', ', @$columns
431             if $columns;
432              
433 4 50       15 $table = $self->QuoteName($table) if $self->{'QuoteTableNames'};
434 4         11 my $full_query = "INSERT INTO $table";
435 4 50       17 $full_query .= " ($columns)" if $columns;
436 4         12 $full_query .= ' '. $query;
437 4         11 my $sth = $self->SimpleQuery( $full_query, @binds );
438 4 50       18 return $sth unless $sth;
439              
440 4         27 my $rows = $sth->rows;
441 4 50       89 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 45 my $self = shift;
458 19         112 my %args = ( Table => undef,
459             Column => undef,
460             IsSQLFunction => undef,
461             PrimaryKeys => undef,
462             @_ );
463              
464 19         46 my @bind = ();
465 19 50       57 $args{Table} = $self->QuoteName($args{Table}) if $self->{'QuoteTableNames'};
466 19         59 my $query = 'UPDATE ' . $args{'Table'} . ' ';
467 19         47 $query .= 'SET ' . $args{'Column'} . '=';
468              
469             ## Look and see if the field is being updated via a SQL function.
470 19 50       59 if ($args{'IsSQLFunction'}) {
471 0         0 $query .= $args{'Value'} . ' ';
472             }
473             else {
474 19         38 $query .= '? ';
475 19         42 push (@bind, $args{'Value'});
476             }
477              
478             ## Constructs the where clause.
479 19         40 my $where = 'WHERE ';
480 19         31 foreach my $key (sort keys %{$args{'PrimaryKeys'}}) {
  19         87  
481 19         61 $where .= $key . "=?" . " AND ";
482 19         55 push (@bind, $args{'PrimaryKeys'}{$key});
483             }
484 19         123 $where =~ s/AND\s$//;
485              
486 19         59 my $query_str = $query . $where;
487 19         57 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 590 my ($self, $table, $values, $query, @query_binds) = @_;
531              
532 1         3 my @columns; my @binds;
533 1         7 for my $k (sort keys %$values) {
534 2         7 push @columns, $k;
535 2         4 push @binds, $values->{$k};
536             }
537              
538 1 50       6 $table = $self->QuoteName($table) if $self->{'QuoteTableNames'};
539 1         5 my $full_query = "UPDATE $table SET ";
540 1         11 $full_query .= join ', ', map "$_ = ?", @columns;
541 1         5 $full_query .= ' WHERE id IN ('. $query .')';
542 1         4 my $sth = $self->SimpleQuery( $full_query, @binds, @query_binds );
543 1 50       7 return $sth unless $sth;
544              
545 1         8 my $rows = $sth->rows;
546 1 50       20 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 18 my ($self, $table, $query, @binds) = @_;
562 1 50       5 $table = $self->QuoteName($table) if $self->{'QuoteTableNames'};
563 1         6 my $sth = $self->SimpleQuery(
564             "DELETE FROM $table WHERE id IN ($query)",
565             @binds
566             );
567 1 50       7 return $sth unless $sth;
568              
569 1         8 my $rows = $sth->rows;
570 1 50       18 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 502     502 1 1763 my $self = shift;
581 502         878 my $QueryString = shift;
582 502         842 my @bind_values;
583 502 100       1456 @bind_values = (@_) if (@_);
584              
585 502         1256 my $sth = $self->dbh->prepare($QueryString);
586 502 100       51561 unless ($sth) {
587 2 50       18 if ($DEBUG) {
588 0         0 die "$self couldn't prepare the query '$QueryString'"
589             . $self->dbh->errstr . "\n";
590             }
591             else {
592 2         14 warn "$self couldn't prepare the query '$QueryString'"
593             . $self->dbh->errstr . "\n";
594 2         21 my $ret = Class::ReturnValue->new();
595 2         38 $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         63 return ( $ret->return_value );
602             }
603             }
604              
605             # Check @bind_values for HASH refs
606 500         1856 for ( my $bind_idx = 0 ; $bind_idx < scalar @bind_values ; $bind_idx++ ) {
607 554 50       1961 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 500         859 my $basetime;
616 500 50       1454 if ( $self->LogSQLStatements ) {
617 0         0 $basetime = Time::HiRes::time();
618             }
619 500         850 my $executed;
620             {
621 25     25   281 no warnings 'uninitialized' ; # undef in bind_values makes DBI sad
  25         68  
  25         145042  
  500         971  
622 500         981 eval { $executed = $sth->execute(@bind_values) };
  500         2544962  
623             }
624 500 50       3864 if ( $self->LogSQLStatements ) {
625 0         0 $self->_LogSQLStatement( $QueryString, Time::HiRes::time() - $basetime, @bind_values );
626             }
627              
628 500 50 33     2566 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 500         2787 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 178     178 1 444 my $self = shift;
700 178         551 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 315     315 1 1049 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   89 my $self = shift;
802 43         78 my $field = shift;
803 43         78 my $operator = shift;
804 43         75 my $value = shift;
805              
806             # don't downcase integer values and things that looks like dates
807 43 100       801 if ($value !~ /^$RE_CASE_INSENSITIVE_CHARS+$/o) {
808 30         105 $field = "lower($field)";
809 30         79 $value = lc($value);
810             }
811 43         222 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 2512 my $self = shift;
843              
844 10         24 my $depth = $self->TransactionDepth;
845 10 100       31 return unless defined $depth;
846              
847 9         34 $self->TransactionDepth(++$depth);
848 9 100       33 return 1 if $depth > 1;
849              
850 6         16 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         74 my %args = ( Action => 'commit', Force => 0, @_ );
872 20 100       65 my $action = lc $args{'Action'} eq 'commit'? 'commit': 'rollback';
873              
874 20   100     48 my $depth = $self->TransactionDepth || 0;
875 20 100       41 unless ( $depth ) {
876 11 100       23 unless( $args{'Force'} ) {
877 4         565 Carp::cluck( "Attempted to $action a transaction with none in progress" );
878 4         436 return 0;
879             }
880 7         48 return 1;
881             } else {
882 9         15 $depth--;
883             }
884 9 50       20 $depth = 0 if $args{'Force'};
885              
886 9         25 $self->TransactionDepth( $depth );
887              
888 9         22 my $dbh = $self->dbh;
889 9         27 $TRANSROLLBACK{ $dbh }{ $action }++;
890 9 100       33 if ( $TRANSROLLBACK{ $dbh }{ $action eq 'commit'? 'rollback' : 'commit' } ) {
    100          
891 2         36 warn "Rollback and commit are mixed while escaping nested transaction";
892             }
893 9 100       53 return 1 if $depth;
894              
895 6         18 delete $TRANSROLLBACK{ $dbh };
896              
897 6 100       14 if ($action eq 'commit') {
898 4         29034 return $dbh->commit;
899             }
900             else {
901 2 50       29 DBIx::SearchBuilder::Record::Cachable->FlushCache
902             if DBIx::SearchBuilder::Record::Cachable->can('FlushCache');
903 2         22 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 516 my $self = shift;
918 10         51 $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 18 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         3 $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 2535 my $self = shift;
959              
960 61         140 my $dbh = $self->dbh;
961 61 100 66     360 return undef unless $dbh && $dbh->ping;
962              
963 51 100       1982 if ( @_ ) {
964 18         34 my $depth = shift;
965 18 100       37 if ( $depth ) {
966 12         38 $TRANSDEPTH{ $dbh } = $depth;
967             } else {
968 6         20 delete $TRANSDEPTH{ $dbh };
969             }
970             }
971 51   100     303 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 182     182 1 276 my $self = shift;
983 182         288 my $statementref = shift;
984 182         289 my $per_page = shift;
985 182         280 my $first = shift;
986 182         275 my $sb = shift;
987              
988 182         302 my $limit_clause = '';
989              
990 182 100       410 if ( $per_page) {
991 51         100 $limit_clause = " LIMIT ";
992 51 100       137 if ( $sb->{_bind_values} ) {
993 4   66     8 push @{$sb->{_bind_values}}, $first || (), $per_page;
  4         16  
994 4 100       11 $first = '?' if $first;
995 4         6 $per_page = '?';
996             }
997              
998 51 100       120 if ( $first ) {
999 28         66 $limit_clause .= $first . ", ";
1000             }
1001 51         86 $limit_clause .= $per_page;
1002             }
1003              
1004 182         450 $$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 88     88 1 146 my $self = shift;
1026 88         510 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 88         152 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 88 100       253 if ( $args{'ALIAS2'} ) {
    50          
1046              
1047             # this code is slow and wasteful, but it's clear.
1048 4         9 my @aliases = @{ $args{'SearchBuilder'}->{'aliases'} };
  4         13  
1049 4         16 my @new_aliases;
1050 4         13 foreach my $old_alias (@aliases) {
1051 4 50       118 if ( $old_alias =~ /^(.*?) (\Q$args{'ALIAS2'}\E)$/ ) {
1052 4         18 $args{'TABLE2'} = $1;
1053 4         11 $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       14 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       30 $args{'DISTINCT'} = 1 unless exists $args{'DISTINCT'};
1088             }
1089              
1090 4 50       14 unless ( $alias ) {
1091             # XXX: this situation is really bug in the caller!!!
1092 0         0 return ( $self->_NormalJoin(%args) );
1093             }
1094 4         14 $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 84         289 $alias = $args{'SearchBuilder'}->_GetAlias( $args{'TABLE2'} );
1126             }
1127 88 50       227 $args{TABLE2} = $self->QuoteName($args{TABLE2}) if $self->QuoteTableNames;
1128              
1129 88   50     467 my $meta = $args{'SearchBuilder'}->{'left_joins'}{"$alias"} ||= {};
1130 88 100       402 if ( $args{'TYPE'} =~ /LEFT/i ) {
1131 39         138 $meta->{'alias_string'} = " LEFT JOIN " . $args{'TABLE2'} . " $alias ";
1132 39         77 $meta->{'type'} = 'LEFT';
1133             }
1134             else {
1135 49         196 $meta->{'alias_string'} = " JOIN " . $args{'TABLE2'} . " $alias ";
1136 49         97 $meta->{'type'} = 'NORMAL';
1137             }
1138 88         177 $meta->{'depends_on'} = $args{'ALIAS1'};
1139              
1140 88   33     314 my $criterion = $args{'EXPRESSION'} || $args{'ALIAS1'}.".".$args{'FIELD1'};
1141 88         414 $meta->{'criteria'}{'base_criterion'} =
1142             [ { field => "$alias.$args{'FIELD2'}", op => '=', value => $criterion } ];
1143              
1144 88 100 100     357 if ( $args{'DISTINCT'} && !defined $args{'SearchBuilder'}{'joins_are_distinct'} ) {
    100          
1145 1         3 $args{SearchBuilder}{joins_are_distinct} = 1;
1146             } elsif ( !$args{'DISTINCT'} ) {
1147 82         158 $args{SearchBuilder}{joins_are_distinct} = 0;
1148             }
1149              
1150 88         396 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 256     256   422 my $self = shift;
1230 256         443 my $sb = shift;
1231              
1232 256         780 $self->OptimizeJoins( SearchBuilder => $sb );
1233 256 50       947 my $table = $self->{'QuoteTableNames'} ? $self->QuoteName($sb->Table) : $sb->Table;
1234              
1235 256         600 my $join_clause = join " CROSS JOIN ", ("$table main"), @{ $sb->{'aliases'} };
  256         708  
1236 256         420 my %processed = map { /^\S+\s+(\S+)$/; $1 => 1 } @{ $sb->{'aliases'} };
  2         9  
  2         9  
  256         513  
1237 256         468 $processed{'main'} = 1;
1238              
1239             # get a @list of joins that have not been processed yet, but depend on processed join
1240 256         439 my $joins = $sb->{'left_joins'};
1241 256   100     1420 while ( my @list =
1242             grep !$processed{ $_ }
1243             && (!$joins->{ $_ }{'depends_on'} || $processed{ $joins->{ $_ }{'depends_on'} }),
1244             sort keys %$joins
1245             ) {
1246 129         302 foreach my $join ( @list ) {
1247 129         235 $processed{ $join }++;
1248              
1249 129         230 my $meta = $joins->{ $join };
1250 129   100     407 my $aggregator = $meta->{'entry_aggregator'} || 'AND';
1251              
1252 129         371 $join_clause .= $meta->{'alias_string'} . " ON ";
1253             my @tmp = map {
1254             ref($_)?
1255 552 100       1385 $_->{'field'} .' '. $_->{'op'} .' '. $_->{'value'}:
1256             $_
1257             }
1258 129         393 map { ('(', @$_, ')', $aggregator) } sorted_values($meta->{'criteria'});
  138         391  
1259 129         275 pop @tmp;
1260 129         884 $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 256 50       778 if ( my @not_processed = grep !$processed{ $_ }, keys %$joins ) {
1267 0         0 die "Unsatisfied dependency chain in joins @not_processed";
1268             }
1269 256         968 return $join_clause;
1270             }
1271              
1272             sub OptimizeJoins {
1273 256     256 0 451 my $self = shift;
1274 256         794 my %args = (SearchBuilder => undef, @_);
1275 256         538 my $joins = $args{'SearchBuilder'}->{'left_joins'};
1276              
1277 256         437 my %processed = map { /^\S+\s+(\S+)$/; $1 => 1 } @{ $args{'SearchBuilder'}->{'aliases'} };
  2         11  
  2         10  
  256         663  
1278 256         1118 $processed{ $_ }++ foreach grep $joins->{ $_ }{'type'} ne 'LEFT', keys %$joins;
1279 256         592 $processed{'main'}++;
1280              
1281 256         387 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 256   100     1223 while ( my @list = grep !$processed{ $_ }
1288             && $processed{ $joins->{ $_ }{'depends_on'} }, sort keys %$joins )
1289             {
1290 51         124 unshift @ordered, @list;
1291 51         279 $processed{ $_ }++ foreach @list;
1292             }
1293              
1294 256         747 foreach my $join ( @ordered ) {
1295 51 100       147 next if $self->MayBeNull( SearchBuilder => $args{'SearchBuilder'}, ALIAS => $join );
1296              
1297 3         22 $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 51     51 1 81 my $self = shift;
1321 51         166 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 51 50       82 return 1 if grep $_ ne 'generic_restrictions', keys %{ $args{'SearchBuilder'}->{'subclauses'} };
  51         221  
1325              
1326             # build full list of generic conditions
1327 51         92 my @conditions;
1328 51         210 foreach ( grep @$_, sorted_values($args{'SearchBuilder'}->{'restrictions'}) ) {
1329 10 50       24 push @conditions, 'AND' if @conditions;
1330 10         30 push @conditions, '(', @$_, ')';
1331             }
1332              
1333             # find tables that depends on this alias and add their join conditions
1334 51         163 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 53 100       145 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         7 @conditions = ('(', @conditions, ')', 'AND', '(', @tmp ,')');
1344              
1345             }
1346 51 100       224 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         23 foreach ( splice @conditions ) {
1352 46 50       146 unless ( ref $_ ) {
    100          
    100          
    100          
1353 33         63 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         26 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         3 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         48 };
1383              
1384             # solve boolean expression we have, an answer is our result
1385 10         17 my $parens_count = 0;
1386 10         17 my @tmp = ();
1387 10         27 while ( defined ( my $e = shift @conditions ) ) {
1388             #print "@tmp >>>$e<<< @conditions\n";
1389 48 100 100     180 return $e if !@conditions && !@tmp;
1390              
1391 38 50       90 unless ( $e ) {
    100          
    100          
    100          
1392 3 100       10 if ( $conditions[0] eq ')' ) {
1393 1         3 push @tmp, $e;
1394 1         3 next;
1395             }
1396              
1397 2         6 my $aggreg = uc shift @conditions;
1398 2 50       6 if ( $aggreg eq 'OR' ) {
    0          
1399             # 0 OR x == x
1400 2         6 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       13 if ( $conditions[0] eq ')' ) {
1410 5         9 push @tmp, $e;
1411 5         11 next;
1412             }
1413              
1414 1         3 my $aggreg = uc shift @conditions;
1415 1 50       5 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       40 if ( $conditions[1] eq ')' ) {
1427 15         37 splice @conditions, 1, 1;
1428             } else {
1429 8         13 $parens_count++;
1430 8         19 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         14 unshift @conditions, @tmp, $e;
1437 6         17 @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 48     48 1 92 my $self = shift;
1454 48         72 my $statementref = shift;
1455 48         71 my $sb = shift;
1456 48         135 my %args = (
1457             Wrap => 0,
1458             @_
1459             );
1460              
1461 48         118 my $QueryHint = $sb->QueryHint;
1462 48 50       109 $QueryHint = $QueryHint ? " /* $QueryHint */ " : " ";
1463              
1464             # Prepend select query for DBs which allow DISTINCT on all column types.
1465 48         152 $$statementref = "SELECT" . $QueryHint . "DISTINCT main.* FROM $$statementref";
1466 48         130 $$statementref .= $sb->_GroupClause;
1467 48 100       132 if ( $args{'Wrap'} ) {
1468 17         43 $$statementref = "SELECT * FROM ($$statementref) main";
1469             }
1470 48         109 $$statementref .= $sb->_OrderClause;
1471             }
1472              
1473             =head2 DistinctQueryAndCount STATEMENTREF
1474              
1475             takes an incomplete SQL SELECT statement and massages it to return a
1476             DISTINCT result set and the total count of potential records.
1477              
1478             =cut
1479              
1480             sub DistinctQueryAndCount {
1481 33     33 1 55 my $self = shift;
1482 33         48 my $statementref = shift;
1483 33         43 my $sb = shift;
1484              
1485             # order by clause should be on outer most query
1486             # SQL standard: ORDER BY command cannot be used in a Subquery
1487             # mariadb explanation: https://mariadb.com/kb/en/why-is-order-by-in-a-from-subquery-ignored/
1488             # pg: actually keeps order of a subquery as long as some conditions in outer query are met, but
1489             # it's just a coincidence, not a feature
1490             # SQL server: https://learn.microsoft.com/en-us/sql/t-sql/queries/select-order-by-clause-transact-sql?view=sql-server-ver16
1491             # The ORDER BY clause is not valid in views, ... and *subqueries*, unless ...
1492              
1493 33         83 my $order = $sb->_OrderClause;
1494 33         142 my $wrap = $order !~ /(?
1495              
1496 33         133 $self->DistinctQuery($statementref, $sb, Wrap => $wrap);
1497              
1498             # DistinctQuery already has an outer SELECT, which we can reuse
1499 33         239 $$statementref =~ s!(?= FROM)!, COUNT(main.id) OVER() AS search_builder_count_all!;
1500             }
1501              
1502              
1503              
1504             =head2 DistinctCount STATEMENTREF
1505              
1506             takes an incomplete SQL SELECT statement and massages it to return a DISTINCT result set.
1507              
1508              
1509             =cut
1510              
1511             sub DistinctCount {
1512 0     0 1 0 my $self = shift;
1513 0         0 my $statementref = shift;
1514 0         0 my $sb = shift;
1515              
1516 0         0 my $QueryHint = $sb->QueryHint;
1517 0 0       0 $QueryHint = $QueryHint ? " /* $QueryHint */ " : " ";
1518              
1519             # Prepend select query for DBs which allow DISTINCT on all column types.
1520 0         0 $$statementref = "SELECT" . $QueryHint . "COUNT(DISTINCT main.id) FROM $$statementref";
1521              
1522             }
1523              
1524             sub Fields {
1525 0     0 0 0 my $self = shift;
1526 0         0 my $table = lc shift;
1527              
1528 0 0       0 unless ( $FIELDS_IN_TABLE{$table} ) {
1529 0         0 $FIELDS_IN_TABLE{ $table } = [];
1530 0 0       0 my $sth = $self->dbh->column_info( undef, '', $table, '%' )
1531             or return ();
1532 0         0 my $info = $sth->fetchall_arrayref({});
1533 0         0 foreach my $e ( @$info ) {
1534 0         0 push @{ $FIELDS_IN_TABLE{ $table } }, $e->{'COLUMN_NAME'};
  0         0  
1535             }
1536             }
1537              
1538 0         0 return @{ $FIELDS_IN_TABLE{ $table } };
  0         0  
1539             }
1540              
1541              
1542             =head2 Log MESSAGE
1543              
1544             Takes a single argument, a message to log.
1545              
1546             Currently prints that message to STDERR
1547              
1548             =cut
1549              
1550             sub Log {
1551 0     0 1 0 my $self = shift;
1552 0         0 my $msg = shift;
1553 0         0 warn $msg."\n";
1554             }
1555              
1556             =head2 SimpleDateTimeFunctions
1557              
1558             See L for details on supported functions.
1559             This method is for implementers of custom DB connectors.
1560              
1561             Returns hash reference with (function name, sql template) pairs.
1562              
1563             =cut
1564              
1565             sub SimpleDateTimeFunctions {
1566 1     1 1 4 my $self = shift;
1567             return {
1568 1         45 datetime => 'SUBSTR(?, 1, 19)',
1569             time => 'SUBSTR(?, 12, 8)',
1570              
1571             hourly => 'SUBSTR(?, 1, 13)',
1572             hour => 'SUBSTR(?, 12, 2 )',
1573              
1574             date => 'SUBSTR(?, 1, 10)',
1575             daily => 'SUBSTR(?, 1, 10)',
1576              
1577             day => 'SUBSTR(?, 9, 2 )',
1578             dayofmonth => 'SUBSTR(?, 9, 2 )',
1579              
1580             monthly => 'SUBSTR(?, 1, 7 )',
1581             month => 'SUBSTR(?, 6, 2 )',
1582              
1583             annually => 'SUBSTR(?, 1, 4 )',
1584             year => 'SUBSTR(?, 1, 4 )',
1585             };
1586             }
1587              
1588             =head2 DateTimeFunction
1589              
1590             Takes named arguments:
1591              
1592             =over 4
1593              
1594             =item * Field - SQL expression date/time function should be applied
1595             to. Note that this argument is used as is without any kind of quoting.
1596              
1597             =item * Type - name of the function, see supported values below.
1598              
1599             =item * Timezone - optional hash reference with From and To values,
1600             see L for details.
1601              
1602             =back
1603              
1604             Returns SQL statement. Returns NULL if function is not supported.
1605              
1606             =head3 Supported functions
1607              
1608             Type value in L is case insesitive. Spaces,
1609             underscores and dashes are ignored. So 'date time', 'DateTime'
1610             and 'date_time' are all synonyms. The following functions are
1611             supported:
1612              
1613             =over 4
1614              
1615             =item * date time - as is, no conversion, except applying timezone
1616             conversion if it's provided.
1617              
1618             =item * time - time only
1619              
1620             =item * hourly - datetime prefix up to the hours, e.g. '2010-03-25 16'
1621              
1622             =item * hour - hour, 0 - 23
1623              
1624             =item * date - date only
1625              
1626             =item * daily - synonym for date
1627              
1628             =item * day of week - 0 - 6, 0 - Sunday
1629              
1630             =item * day - day of month, 1 - 31
1631              
1632             =item * day of month - synonym for day
1633              
1634             =item * day of year - 1 - 366, support is database dependent
1635              
1636             =item * month - 1 - 12
1637              
1638             =item * monthly - year and month prefix, e.g. '2010-11'
1639              
1640             =item * year - e.g. '2023'
1641              
1642             =item * annually - synonym for year
1643              
1644             =item * week of year - 0-53, presence of zero week, 1st week meaning
1645             and whether week starts on Monday or Sunday heavily depends on database.
1646              
1647             =back
1648              
1649             =cut
1650              
1651             sub DateTimeFunction {
1652 20     20 1 960 my $self = shift;
1653 20         66 my %args = (
1654             Field => undef,
1655             Type => '',
1656             Timezone => undef,
1657             @_
1658             );
1659              
1660 20   50     71 my $res = $args{'Field'} || '?';
1661 20 50       46 if ( $args{'Timezone'} ) {
1662             $res = $self->ConvertTimezoneFunction(
1663 0         0 %{ $args{'Timezone'} },
  0         0  
1664             Field => $res,
1665             );
1666             }
1667              
1668 20         40 my $norm_type = lc $args{'Type'};
1669 20         78 $norm_type =~ s/[ _-]//g;
1670 20 100       64 if ( my $template = $self->SimpleDateTimeFunctions->{ $norm_type } ) {
1671 18         64 $template =~ s/\?/$res/;
1672 18         38 $res = $template;
1673             }
1674             else {
1675 2         14 return 'NULL';
1676             }
1677 18         76 return $res;
1678             }
1679              
1680             =head2 ConvertTimezoneFunction
1681              
1682             Generates a function applied to Field argument that converts timezone.
1683             By default converts from UTC. Examples:
1684              
1685             # UTC => Moscow
1686             $handle->ConvertTimezoneFunction( Field => '?', To => 'Europe/Moscow');
1687              
1688             If there is problem with arguments or timezones are equal
1689             then Field returned without any function applied. Field argument
1690             is not escaped in any way, it's your job.
1691              
1692             Implementation is very database specific. To be portable convert
1693             from UTC or to UTC. Some databases have internal storage for
1694             information about timezones that should be kept up to date.
1695             Read documentation for your DB.
1696              
1697             =cut
1698              
1699             sub ConvertTimezoneFunction {
1700 0     0 1 0 my $self = shift;
1701 0         0 my %args = (
1702             From => 'UTC',
1703             To => undef,
1704             Field => '',
1705             @_
1706             );
1707 0         0 return $args{'Field'};
1708             }
1709              
1710             =head2 DateTimeIntervalFunction
1711              
1712             Generates a function to calculate interval in seconds between two
1713             dates. Takes From and To arguments which can be either scalar or
1714             a hash. Hash is processed with L.
1715              
1716             Arguments are not quoted or escaped in any way. It's caller's job.
1717              
1718             =cut
1719              
1720             sub DateTimeIntervalFunction {
1721 2     2 1 5 my $self = shift;
1722 2         19 my %args = ( From => undef, To => undef, @_ );
1723              
1724             $_ = DBIx::SearchBuilder->CombineFunctionWithField(%$_)
1725 2         351 for grep ref, @args{'From', 'To'};
1726              
1727 2         11 return $self->_DateTimeIntervalFunction( %args );
1728             }
1729              
1730 0     0   0 sub _DateTimeIntervalFunction { return 'NULL' }
1731              
1732             =head2 NullsOrder
1733              
1734             Sets order of NULLs when sorting columns when called with mode,
1735             but only if DB supports it. Modes:
1736              
1737             =over 4
1738              
1739             =item * small
1740              
1741             NULLs are smaller then anything else, so come first when order
1742             is ASC and last otherwise.
1743              
1744             =item * large
1745              
1746             NULLs are larger then anything else.
1747              
1748             =item * first
1749              
1750             NULLs are always first.
1751              
1752             =item * last
1753              
1754             NULLs are always last.
1755              
1756             =item * default
1757              
1758             Return back to DB's default behaviour.
1759              
1760             =back
1761              
1762             When called without argument returns metadata required to generate
1763             SQL.
1764              
1765             =cut
1766              
1767             sub NullsOrder {
1768 266     266 1 402 my $self = shift;
1769              
1770 266 50       475 unless ($self->HasSupportForNullsOrder) {
1771 266 50       523 warn "No support for changing NULLs order" if @_;
1772 266         592 return undef;
1773             }
1774              
1775 0 0       0 if ( @_ ) {
1776 0   0     0 my $mode = shift || 'default';
1777 0 0       0 if ( $mode eq 'default' ) {
    0          
    0          
    0          
    0          
1778 0         0 delete $self->{'nulls_order'};
1779             }
1780             elsif ( $mode eq 'small' ) {
1781 0         0 $self->{'nulls_order'} = { ASC => 'NULLS FIRST', DESC => 'NULLS LAST' };
1782             }
1783             elsif ( $mode eq 'large' ) {
1784 0         0 $self->{'nulls_order'} = { ASC => 'NULLS LAST', DESC => 'NULLS FIRST' };
1785             }
1786             elsif ( $mode eq 'first' ) {
1787 0         0 $self->{'nulls_order'} = { ASC => 'NULLS FIRST', DESC => 'NULLS FIRST' };
1788             }
1789             elsif ( $mode eq 'last' ) {
1790 0         0 $self->{'nulls_order'} = { ASC => 'NULLS LAST', DESC => 'NULLS LAST' };
1791             }
1792             else {
1793 0         0 warn "'$mode' is not supported NULLs ordering mode";
1794 0         0 delete $self->{'nulls_order'};
1795             }
1796             }
1797              
1798 0 0       0 return undef unless $self->{'nulls_order'};
1799 0         0 return $self->{'nulls_order'};
1800             }
1801              
1802             =head2 HasSupportForNullsOrder
1803              
1804             Returns true value if DB supports adjusting NULLs order while sorting
1805             a column, for example C.
1806              
1807             =cut
1808              
1809             sub HasSupportForNullsOrder {
1810 267     267 1 589 return 0;
1811             }
1812              
1813             =head2 HasSupportForCombineSearchAndCount
1814              
1815             Returns true value if DB supports to combine search and count in single
1816             query.
1817              
1818             =cut
1819              
1820             sub HasSupportForCombineSearchAndCount {
1821 68     68 1 164 return 1;
1822             }
1823              
1824             =head2 QuoteName
1825              
1826             Quote table or column name to avoid reserved word errors.
1827              
1828             Returns same value passed unless over-ridden in database-specific subclass.
1829              
1830             =cut
1831              
1832             # over-ride in subclass
1833             sub QuoteName {
1834 0     0 1 0 my ($self, $name) = @_;
1835             # use dbi built in quoting if we have a connection,
1836 0 0       0 if ($self->dbh) {
1837 0         0 return $self->dbh->quote_identifier($name);
1838             }
1839 0         0 warn "QuoteName called without a db handle";
1840 0         0 return $name;
1841             }
1842              
1843             =head2 DequoteName
1844              
1845             Undo the effects of QuoteName by removing quoting.
1846              
1847             =cut
1848              
1849             sub DequoteName {
1850 0     0 1 0 my ($self, $name) = @_;
1851 0 0       0 if ($self->dbh) {
1852             # 29 = SQL_IDENTIFIER_QUOTE_CHAR; see "perldoc DBI"
1853 0         0 my $quote_char = $self->dbh->get_info( 29 );
1854              
1855 0 0       0 if ($quote_char) {
1856 0 0       0 if ($name =~ /^$quote_char(.*)$quote_char$/) {
1857 0         0 return $1;
1858             }
1859             }
1860 0         0 return $name;
1861             }
1862 0         0 warn "DequoteName called without a db handle";
1863 0         0 return $name;
1864             }
1865              
1866             sub _ExtractBindValues {
1867 32     32   55 my $self = shift;
1868 32         52 my $string = shift;
1869 32   50     88 my $default_escape_char = shift || q{'};
1870 32 50       68 return $string unless defined $string;
1871              
1872 32         66 my $placeholder = '';
1873              
1874 32         419 my @chars = split //, $string;
1875 32         115 my $value = '';
1876 32         45 my $escape_char = $default_escape_char;
1877              
1878 32         49 my @values;
1879 32         74 my $in = 0; # keep state in the loop: is it in a quote?
1880 32         84 while ( defined( my $c = shift @chars ) ) {
1881 2542         3335 my $escaped;
1882 2542 100 100     4603 if ( $c eq $escape_char && $in ) {
1883 68 50       116 if ( $escape_char eq q{'} ) {
1884 68 100 50     161 if ( ( $chars[0] || '' ) eq q{'} ) {
1885 18         29 $c = shift @chars;
1886 18         33 $escaped = 1;
1887             }
1888             }
1889             else {
1890 0         0 $c = shift @chars;
1891 0         0 $escaped = 1;
1892             }
1893             }
1894              
1895 2542 100       3784 if ($in) {
1896 660 100       1071 if ( $c eq q{'} ) {
1897 68 100       120 if ( !$escaped ) {
1898 50         90 push @values, $value;
1899 50         68 $in = 0;
1900 50         82 $value = '';
1901 50         77 $escape_char = $default_escape_char;
1902 50         74 $placeholder .= '?';
1903 50         123 next;
1904             }
1905             }
1906 610         1261 $value .= $c;
1907             }
1908             else {
1909 1882 100 50     5873 if ( $c eq q{'} ) {
    50 66        
    50 66        
1910 50         144 $in = 1;
1911             }
1912              
1913             # Handle quoted string like e'foo\\bar'
1914             elsif ( lc $c eq 'e' && ( $chars[0] // '' ) eq q{'} ) {
1915 0         0 $escape_char = '\\';
1916             }
1917              
1918             # Handle numbers
1919             elsif ( $c =~ /[\d.]/ && $placeholder !~ /\w$/ ) { # Do not catch Groups_1.Name
1920 0         0 $value .= $c;
1921 0   0     0 while ( ( $chars[0] // '' ) =~ /[\d.]/ ) {
1922 0         0 $value .= shift @chars;
1923             }
1924              
1925 0         0 push @values, $value;
1926 0         0 $placeholder .= '?';
1927 0         0 $value = '';
1928             }
1929             else {
1930 1832         3872 $placeholder .= $c;
1931             }
1932             }
1933             }
1934 32         156 return ( $placeholder, @values );
1935             }
1936              
1937 21     21   107 sub _RequireQuotedTables { return 0 };
1938              
1939             =head2 DESTROY
1940              
1941             When we get rid of the Searchbuilder::Handle, we need to disconnect from the database
1942              
1943             =cut
1944              
1945             sub DESTROY {
1946 22     22   9239 my $self = shift;
1947 22 50       138 $self->Disconnect if $self->{'DisconnectHandleOnDestroy'};
1948 22         434 delete $DBIHandle{$self};
1949             }
1950              
1951              
1952             1;