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   211022 use strict;
  25         78  
  25         720  
4 25     25   122 use warnings;
  25         49  
  25         736  
5              
6 25     25   133 use Carp qw(croak cluck);
  25         57  
  25         1387  
7 25     25   6944 use DBI;
  25         72936  
  25         1071  
8 25     25   3919 use Class::ReturnValue;
  25         99572  
  25         2833  
9 25     25   4368 use Encode qw();
  25         70617  
  25         545  
10 25     25   10988 use version;
  25         47824  
  25         153  
11              
12 25     25   5284 use DBIx::SearchBuilder::Util qw/ sorted_values /;
  25         67  
  25         1618  
13              
14 25     25   159 use vars qw(@ISA %DBIHandle $PrevHandle $DEBUG %TRANSDEPTH %TRANSROLLBACK %FIELDS_IN_TABLE);
  25         51  
  25         61692  
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 2146 my $proto = shift;
49 23   33     193 my $class = ref($proto) || $proto;
50 23         65 my $self = {};
51 23         57 bless ($self, $class);
52              
53             # Enable quotes table names
54 23         83 my %args = ( QuoteTableNames => 0, @_ );
55 23         145 $self->{'QuoteTableNames'} = $args{QuoteTableNames};
56              
57 23         56 @{$self->{'StatementLog'}} = ();
  23         72  
58 23         86 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 19891 my $self = shift;
85 22         197 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     289 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         80 $self->{'DisconnectHandleOnDestroy'} = $args{'DisconnectHandleOnDestroy'};
106              
107             # Enable optional quoted table names
108 21 50       99 $self->{'QuoteTableNames'} = delete $args{QuoteTableNames} if defined $args{QuoteTableNames};
109              
110 21   50     107 my $old_dsn = $self->DSN || '';
111 21         171 my $new_dsn = $self->BuildDSN( %args );
112              
113             # Only connect if we're not connected to this source already
114 21 0 33     107 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         52710 $handle->{FetchHashKeyName} ='NAME_lc';
123              
124             # Set the handle
125 21         123 $self->dbh($handle);
126              
127             # Cache version info
128 21         100 $self->DatabaseVersion;
129              
130             # force quoted tables for mysql 8
131 21 50       196 $self->{'QuoteTableNames'} = 1 if $self->_RequireQuotedTables;
132              
133 21         144 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         4 my $class = 'DBIx::SearchBuilder::Handle::' . $driver;
149 1         2 local $@;
150 1         81 eval "require $class";
151 1 50       8 return if $@;
152              
153 1         5 bless $self, $class;
154 1         19 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 53 my $self = shift;
171 21         133 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         96 my $dsn = "dbi:$args{'Driver'}:dbname=$args{'Database'}";
182 21 50       108 $dsn .= ";sid=$args{'SID'}" if $args{'SID'};
183 21 50       77 $dsn .= ";host=$args{'Host'}" if $args{'Host'};
184 21 50       92 $dsn .= ";port=$args{'Port'}" if $args{'Port'};
185 21 50       66 $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 137 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 1058     1058 1 2569 my $self = shift;
249 1058 50       3346 if (@_) {
250 0         0 require Time::HiRes;
251 0         0 $self->{'_DoLogSQL'} = shift;
252             }
253 1058         3664 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 992     992 1 2745 my $self=shift;
359              
360             #If we are setting the database handle, set it.
361 992 100       2392 if ( @_ ) {
362 21         100 $DBIHandle{$self} = $PrevHandle = shift;
363 21         69 %FIELDS_IN_TABLE = ();
364             }
365              
366 992   66     12714 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 160     160 1 421 my $self = shift;
383 160         903 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 160     160 1 667 my($self, $table, @pairs) = @_;
399 160         354 my(@cols, @vals, @bind);
400              
401 160         535 while ( my $key = shift @pairs ) {
402 415         854 push @cols, $key;
403 415         846 push @vals, '?';
404 415         1258 push @bind, shift @pairs;
405             }
406              
407 160 50       675 $table = $self->QuoteName($table) if $self->QuoteTableNames;
408 160         552 my $QueryString = "INSERT INTO $table";
409 160         895 $QueryString .= " (". join(", ", @cols) .")";
410 160         581 $QueryString .= " VALUES (". join(", ", @vals). ")";
411 160         901 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 811 my ($self, $table, $columns, $query, @binds) = @_;
429              
430 4 50       21 $columns = join ', ', @$columns
431             if $columns;
432              
433 4 50       21 $table = $self->QuoteName($table) if $self->{'QuoteTableNames'};
434 4         13 my $full_query = "INSERT INTO $table";
435 4 50       19 $full_query .= " ($columns)" if $columns;
436 4         14 $full_query .= ' '. $query;
437 4         25 my $sth = $self->SimpleQuery( $full_query, @binds );
438 4 50       21 return $sth unless $sth;
439              
440 4         36 my $rows = $sth->rows;
441 4 50       143 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         119 my %args = ( Table => undef,
459             Column => undef,
460             IsSQLFunction => undef,
461             PrimaryKeys => undef,
462             @_ );
463              
464 19         50 my @bind = ();
465 19 50       75 $args{Table} = $self->QuoteName($args{Table}) if $self->{'QuoteTableNames'};
466 19         66 my $query = 'UPDATE ' . $args{'Table'} . ' ';
467 19         51 $query .= 'SET ' . $args{'Column'} . '=';
468              
469             ## Look and see if the field is being updated via a SQL function.
470 19 50       57 if ($args{'IsSQLFunction'}) {
471 0         0 $query .= $args{'Value'} . ' ';
472             }
473             else {
474 19         54 $query .= '? ';
475 19         96 push (@bind, $args{'Value'});
476             }
477              
478             ## Constructs the where clause.
479 19         47 my $where = 'WHERE ';
480 19         53 foreach my $key (sort keys %{$args{'PrimaryKeys'}}) {
  19         98  
481 19         59 $where .= $key . "=?" . " AND ";
482 19         89 push (@bind, $args{'PrimaryKeys'}{$key});
483             }
484 19         153 $where =~ s/AND\s$//;
485              
486 19         68 my $query_str = $query . $where;
487 19         81 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 712 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         6 push @columns, $k;
535 2         5 push @binds, $values->{$k};
536             }
537              
538 1 50       4 $table = $self->QuoteName($table) if $self->{'QuoteTableNames'};
539 1         5 my $full_query = "UPDATE $table SET ";
540 1         7 $full_query .= join ', ', map "$_ = ?", @columns;
541 1         7 $full_query .= ' WHERE id IN ('. $query .')';
542 1         5 my $sth = $self->SimpleQuery( $full_query, @binds, @query_binds );
543 1 50       19 return $sth unless $sth;
544              
545 1         9 my $rows = $sth->rows;
546 1 50       23 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 29 my ($self, $table, $query, @binds) = @_;
562 1 50       5 $table = $self->QuoteName($table) if $self->{'QuoteTableNames'};
563 1         9 my $sth = $self->SimpleQuery(
564             "DELETE FROM $table WHERE id IN ($query)",
565             @binds
566             );
567 1 50       16 return $sth unless $sth;
568              
569 1         7 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 531     531 1 1925 my $self = shift;
581 531         1014 my $QueryString = shift;
582 531         945 my @bind_values;
583 531 100       1658 @bind_values = (@_) if (@_);
584              
585 531         1467 my $sth = $self->dbh->prepare($QueryString);
586 531 100       59034 unless ($sth) {
587 2 50       8 if ($DEBUG) {
588 0         0 die "$self couldn't prepare the query '$QueryString'"
589             . $self->dbh->errstr . "\n";
590             }
591             else {
592 2         15 warn "$self couldn't prepare the query '$QueryString'"
593             . $self->dbh->errstr . "\n";
594 2         19 my $ret = Class::ReturnValue->new();
595 2         24 $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         53 return ( $ret->return_value );
602             }
603             }
604              
605             # Check @bind_values for HASH refs
606 529         2153 for ( my $bind_idx = 0 ; $bind_idx < scalar @bind_values ; $bind_idx++ ) {
607 557 50       2296 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 529         966 my $basetime;
616 529 50       1616 if ( $self->LogSQLStatements ) {
617 0         0 $basetime = Time::HiRes::time();
618             }
619 529         1059 my $executed;
620             {
621 25     25   228 no warnings 'uninitialized' ; # undef in bind_values makes DBI sad
  25         62  
  25         148192  
  529         1011  
622 529         1056 eval { $executed = $sth->execute(@bind_values) };
  529         2902835  
623             }
624 529 50       4281 if ( $self->LogSQLStatements ) {
625 0         0 $self->_LogSQLStatement( $QueryString, Time::HiRes::time() - $basetime, @bind_values );
626             }
627              
628 529 50 33     4915 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 529         3201 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 179     179 1 435 my $self = shift;
700 179         633 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 332     332 1 1156 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   86 my $self = shift;
802 43         73 my $field = shift;
803 43         84 my $operator = shift;
804 43         79 my $value = shift;
805              
806             # don't downcase integer values and things that looks like dates
807 43 100       715 if ($value !~ /^$RE_CASE_INSENSITIVE_CHARS+$/o) {
808 30         104 $field = "lower($field)";
809 30         87 $value = lc($value);
810             }
811 43         204 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 2231 my $self = shift;
843              
844 10         24 my $depth = $self->TransactionDepth;
845 10 100       30 return unless defined $depth;
846              
847 9         25 $self->TransactionDepth(++$depth);
848 9 100       30 return 1 if $depth > 1;
849              
850 6         13 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 29 my $self = shift;
871 20         101 my %args = ( Action => 'commit', Force => 0, @_ );
872 20 100       70 my $action = lc $args{'Action'} eq 'commit'? 'commit': 'rollback';
873              
874 20   100     44 my $depth = $self->TransactionDepth || 0;
875 20 100       47 unless ( $depth ) {
876 11 100       22 unless( $args{'Force'} ) {
877 4         551 Carp::cluck( "Attempted to $action a transaction with none in progress" );
878 4         418 return 0;
879             }
880 7         33 return 1;
881             } else {
882 9         11 $depth--;
883             }
884 9 50       21 $depth = 0 if $args{'Force'};
885              
886 9         24 $self->TransactionDepth( $depth );
887              
888 9         19 my $dbh = $self->dbh;
889 9         32 $TRANSROLLBACK{ $dbh }{ $action }++;
890 9 100       30 if ( $TRANSROLLBACK{ $dbh }{ $action eq 'commit'? 'rollback' : 'commit' } ) {
    100          
891 2         31 warn "Rollback and commit are mixed while escaping nested transaction";
892             }
893 9 100       61 return 1 if $depth;
894              
895 6         17 delete $TRANSROLLBACK{ $dbh };
896              
897 6 100       18 if ($action eq 'commit') {
898 4         14881 return $dbh->commit;
899             }
900             else {
901 2 50       22 DBIx::SearchBuilder::Record::Cachable->FlushCache
902             if DBIx::SearchBuilder::Record::Cachable->can('FlushCache');
903 2         24 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         27 $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 27 my $self = shift;
933 8         27 $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 5 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 2441 my $self = shift;
959              
960 61         133 my $dbh = $self->dbh;
961 61 100 66     328 return undef unless $dbh && $dbh->ping;
962              
963 51 100       1480 if ( @_ ) {
964 18         32 my $depth = shift;
965 18 100       38 if ( $depth ) {
966 12         37 $TRANSDEPTH{ $dbh } = $depth;
967             } else {
968 6         39 delete $TRANSDEPTH{ $dbh };
969             }
970             }
971 51   100     270 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 198     198 1 394 my $self = shift;
983 198         302 my $statementref = shift;
984 198         299 my $per_page = shift;
985 198         296 my $first = shift;
986 198         277 my $sb = shift;
987              
988 198         346 my $limit_clause = '';
989              
990 198 100       413 if ( $per_page) {
991 67         108 $limit_clause = " LIMIT ";
992 67 100       149 if ( $sb->{_bind_values} ) {
993 4   66     8 push @{$sb->{_bind_values}}, $first || (), $per_page;
  4         13  
994 4 100       10 $first = '?' if $first;
995 4         7 $per_page = '?';
996             }
997              
998 67 100       146 if ( $first ) {
999 36         74 $limit_clause .= $first . ", ";
1000             }
1001 67         106 $limit_clause .= $per_page;
1002             }
1003              
1004 198         481 $$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 104     104 1 188 my $self = shift;
1026 104         659 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 104         190 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 104 100       312 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         17  
1049 4         10 my @new_aliases;
1050 4         26 foreach my $old_alias (@aliases) {
1051 4 50       109 if ( $old_alias =~ /^(.*?) (\Q$args{'ALIAS2'}\E)$/ ) {
1052 4         16 $args{'TABLE2'} = $1;
1053 4         12 $alias = $2;
1054 4 50       14 $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       12 unless ( $alias ) {
1091             # XXX: this situation is really bug in the caller!!!
1092 0         0 return ( $self->_NormalJoin(%args) );
1093             }
1094 4         17 $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 100         320 $alias = $args{'SearchBuilder'}->_GetAlias( $args{'TABLE2'} );
1126             }
1127 104 50       283 $args{TABLE2} = $self->QuoteName($args{TABLE2}) if $self->QuoteTableNames;
1128              
1129 104   50     608 my $meta = $args{'SearchBuilder'}->{'left_joins'}{"$alias"} ||= {};
1130 104 100       521 if ( $args{'TYPE'} =~ /LEFT/i ) {
1131 39         143 $meta->{'alias_string'} = " LEFT JOIN " . $args{'TABLE2'} . " $alias ";
1132 39         85 $meta->{'type'} = 'LEFT';
1133             }
1134             else {
1135 65         254 $meta->{'alias_string'} = " JOIN " . $args{'TABLE2'} . " $alias ";
1136 65         127 $meta->{'type'} = 'NORMAL';
1137             }
1138 104         228 $meta->{'depends_on'} = $args{'ALIAS1'};
1139              
1140 104   33     396 my $criterion = $args{'EXPRESSION'} || $args{'ALIAS1'}.".".$args{'FIELD1'};
1141 104         525 $meta->{'criteria'}{'base_criterion'} =
1142             [ { field => "$alias.$args{'FIELD2'}", op => '=', value => $criterion } ];
1143              
1144 104 100 100     445 if ( $args{'DISTINCT'} && !defined $args{'SearchBuilder'}{'joins_are_distinct'} ) {
    100          
1145 1         7 $args{SearchBuilder}{joins_are_distinct} = 1;
1146             } elsif ( !$args{'DISTINCT'} ) {
1147 98         193 $args{SearchBuilder}{joins_are_distinct} = 0;
1148             }
1149              
1150 104         464 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 284     284   470 my $self = shift;
1230 284         463 my $sb = shift;
1231              
1232 284         791 $self->OptimizeJoins( SearchBuilder => $sb );
1233 284 50       1072 my $table = $self->{'QuoteTableNames'} ? $self->QuoteName($sb->Table) : $sb->Table;
1234              
1235 284         643 my $join_clause = join " CROSS JOIN ", ("$table main"), @{ $sb->{'aliases'} };
  284         757  
1236 284         431 my %processed = map { /^\S+\s+(\S+)$/; $1 => 1 } @{ $sb->{'aliases'} };
  2         15  
  2         7  
  284         546  
1237 284         533 $processed{'main'} = 1;
1238              
1239             # get a @list of joins that have not been processed yet, but depend on processed join
1240 284         459 my $joins = $sb->{'left_joins'};
1241 284   100     1780 while ( my @list =
1242             grep !$processed{ $_ }
1243             && (!$joins->{ $_ }{'depends_on'} || $processed{ $joins->{ $_ }{'depends_on'} }),
1244             sort keys %$joins
1245             ) {
1246 157         330 foreach my $join ( @list ) {
1247 157         297 $processed{ $join }++;
1248              
1249 157         258 my $meta = $joins->{ $join };
1250 157   100     508 my $aggregator = $meta->{'entry_aggregator'} || 'AND';
1251              
1252 157         450 $join_clause .= $meta->{'alias_string'} . " ON ";
1253             my @tmp = map {
1254             ref($_)?
1255 664 100       1625 $_->{'field'} .' '. $_->{'op'} .' '. $_->{'value'}:
1256             $_
1257             }
1258 157         544 map { ('(', @$_, ')', $aggregator) } sorted_values($meta->{'criteria'});
  166         503  
1259 157         317 pop @tmp;
1260 157         2644 $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 284 50       848 if ( my @not_processed = grep !$processed{ $_ }, keys %$joins ) {
1267 0         0 die "Unsatisfied dependency chain in joins @not_processed";
1268             }
1269 284         1176 return $join_clause;
1270             }
1271              
1272             sub OptimizeJoins {
1273 284     284 0 595 my $self = shift;
1274 284         854 my %args = (SearchBuilder => undef, @_);
1275 284         646 my $joins = $args{'SearchBuilder'}->{'left_joins'};
1276              
1277 284         519 my %processed = map { /^\S+\s+(\S+)$/; $1 => 1 } @{ $args{'SearchBuilder'}->{'aliases'} };
  2         15  
  2         11  
  284         707  
1278 284         1198 $processed{ $_ }++ foreach grep $joins->{ $_ }{'type'} ne 'LEFT', keys %$joins;
1279 284         631 $processed{'main'}++;
1280              
1281 284         462 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 284   100     1368 while ( my @list = grep !$processed{ $_ }
1288             && $processed{ $joins->{ $_ }{'depends_on'} }, sort keys %$joins )
1289             {
1290 51         142 unshift @ordered, @list;
1291 51         281 $processed{ $_ }++ foreach @list;
1292             }
1293              
1294 284         849 foreach my $join ( @ordered ) {
1295 51 100       159 next if $self->MayBeNull( SearchBuilder => $args{'SearchBuilder'}, ALIAS => $join );
1296              
1297 3         35 $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 95 my $self = shift;
1321 51         171 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       84 return 1 if grep $_ ne 'generic_restrictions', keys %{ $args{'SearchBuilder'}->{'subclauses'} };
  51         235  
1325              
1326             # build full list of generic conditions
1327 51         96 my @conditions;
1328 51         229 foreach ( grep @$_, sorted_values($args{'SearchBuilder'}->{'restrictions'}) ) {
1329 10 50       27 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         178 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       170 next if $join->{'type'} eq 'LEFT';
1338 1 50       5 next unless $join->{'depends_on'} eq $args{'ALIAS'};
1339              
1340 1         4 my @tmp = map { ('(', @$_, ')', $join->{'entry_aggregator'}) } sorted_values($join->{'criteria'});
  1         8  
1341 1         5 pop @tmp;
1342              
1343 1         7 @conditions = ('(', @conditions, ')', 'AND', '(', @tmp ,')');
1344              
1345             }
1346 51 100       240 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         27 foreach ( splice @conditions ) {
1352 46 50       148 unless ( ref $_ ) {
    100          
    100          
    100          
1353 33         57 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         24 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         11 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         61 };
1383              
1384             # solve boolean expression we have, an answer is our result
1385 10         18 my $parens_count = 0;
1386 10         15 my @tmp = ();
1387 10         28 while ( defined ( my $e = shift @conditions ) ) {
1388             #print "@tmp >>>$e<<< @conditions\n";
1389 48 100 100     196 return $e if !@conditions && !@tmp;
1390              
1391 38 50       94 unless ( $e ) {
    100          
    100          
    100          
1392 3 100       8 if ( $conditions[0] eq ')' ) {
1393 1         2 push @tmp, $e;
1394 1         7 next;
1395             }
1396              
1397 2         6 my $aggreg = uc shift @conditions;
1398 2 50       8 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       17 if ( $conditions[0] eq ')' ) {
1410 5         10 push @tmp, $e;
1411 5         11 next;
1412             }
1413              
1414 1         7 my $aggreg = uc shift @conditions;
1415 1 50       8 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       44 if ( $conditions[1] eq ')' ) {
1427 15         38 splice @conditions, 1, 1;
1428             } else {
1429 8         13 $parens_count++;
1430 8         22 push @tmp, $e;
1431             }
1432 0         0 } elsif ( $e eq ')' ) {
1433 6 50       15 die "extra closing paren: @tmp >>>$e<<< @conditions"
1434             if --$parens_count < 0;
1435              
1436 6         16 unshift @conditions, @tmp, $e;
1437 6         14 @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 64     64 1 121 my $self = shift;
1454 64         99 my $statementref = shift;
1455 64         113 my $sb = shift;
1456 64         175 my %args = (
1457             Wrap => 0,
1458             @_
1459             );
1460              
1461 64         158 my $QueryHint = $sb->QueryHint;
1462 64 50       149 $QueryHint = $QueryHint ? " /* $QueryHint */ " : " ";
1463              
1464             # Prepend select query for DBs which allow DISTINCT on all column types.
1465 64         202 $$statementref = "SELECT" . $QueryHint . "DISTINCT main.* FROM $$statementref";
1466 64         1026 $$statementref .= $sb->_GroupClause;
1467 64 100       171 if ( $args{'Wrap'} ) {
1468 25         68 $$statementref = "SELECT * FROM ($$statementref) main";
1469             }
1470 64         155 $$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 41     41 1 69 my $self = shift;
1482 41         63 my $statementref = shift;
1483 41         57 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 41         91 my $order = $sb->_OrderClause;
1494 41         209 my $wrap = $order !~ /(?
1495              
1496 41         168 $self->DistinctQuery($statementref, $sb, Wrap => $wrap);
1497              
1498             # DistinctQuery already has an outer SELECT, which we can reuse
1499 41         296 $$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 2 my $self = shift;
1567             return {
1568 1         38 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 434 my $self = shift;
1653 20         66 my %args = (
1654             Field => undef,
1655             Type => '',
1656             Timezone => undef,
1657             @_
1658             );
1659              
1660 20   50     76 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         37 my $norm_type = lc $args{'Type'};
1669 20         79 $norm_type =~ s/[ _-]//g;
1670 20 100       65 if ( my $template = $self->SimpleDateTimeFunctions->{ $norm_type } ) {
1671 18         68 $template =~ s/\?/$res/;
1672 18         36 $res = $template;
1673             }
1674             else {
1675 2         13 return 'NULL';
1676             }
1677 18         59 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         10 my %args = ( From => undef, To => undef, @_ );
1723              
1724             $_ = DBIx::SearchBuilder->CombineFunctionWithField(%$_)
1725 2         15 for grep ref, @args{'From', 'To'};
1726              
1727 2         12 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 322     322 1 536 my $self = shift;
1769              
1770 322 50       619 unless ($self->HasSupportForNullsOrder) {
1771 322 50       634 warn "No support for changing NULLs order" if @_;
1772 322         707 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 323     323 1 683 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 84     84 1 212 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   47 my $self = shift;
1868 32         72 my $string = shift;
1869 32   50     101 my $default_escape_char = shift || q{'};
1870 32 50       67 return $string unless defined $string;
1871              
1872 32         99 my $placeholder = '';
1873              
1874 32         444 my @chars = split //, $string;
1875 32         93 my $value = '';
1876 32         80 my $escape_char = $default_escape_char;
1877              
1878 32         48 my @values;
1879 32         48 my $in = 0; # keep state in the loop: is it in a quote?
1880 32         84 while ( defined( my $c = shift @chars ) ) {
1881 2542         3239 my $escaped;
1882 2542 100 100     4535 if ( $c eq $escape_char && $in ) {
1883 68 50       124 if ( $escape_char eq q{'} ) {
1884 68 100 50     158 if ( ( $chars[0] || '' ) eq q{'} ) {
1885 18         35 $c = shift @chars;
1886 18         27 $escaped = 1;
1887             }
1888             }
1889             else {
1890 0         0 $c = shift @chars;
1891 0         0 $escaped = 1;
1892             }
1893             }
1894              
1895 2542 100       3672 if ($in) {
1896 660 100       1079 if ( $c eq q{'} ) {
1897 68 100       111 if ( !$escaped ) {
1898 50         88 push @values, $value;
1899 50         67 $in = 0;
1900 50         70 $value = '';
1901 50         66 $escape_char = $default_escape_char;
1902 50         73 $placeholder .= '?';
1903 50         116 next;
1904             }
1905             }
1906 610         1509 $value .= $c;
1907             }
1908             else {
1909 1882 100 50     5774 if ( $c eq q{'} ) {
    50 66        
    50 66        
1910 50         108 $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         3862 $placeholder .= $c;
1931             }
1932             }
1933             }
1934 32         158 return ( $placeholder, @values );
1935             }
1936              
1937 21     21   110 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   10414 my $self = shift;
1947 22 50       130 $self->Disconnect if $self->{'DisconnectHandleOnDestroy'};
1948 22         449 delete $DBIHandle{$self};
1949             }
1950              
1951              
1952             1;