File Coverage

lib/DBIx/OptimalQuery.pm
Criterion Covered Total %
statement 33 914 3.6
branch 0 402 0.0
condition 0 181 0.0
subroutine 11 58 18.9
pod 0 8 0.0
total 44 1563 2.8


line stmt bran cond sub pod time code
1             package DBIx::OptimalQuery::sth;
2              
3 1     1   588 use strict;
  1         1  
  1         23  
4 1     1   3 use warnings;
  1         1  
  1         21  
5 1     1   8 no warnings qw( uninitialized once );
  1         1  
  1         27  
6              
7 1     1   1319 use DBI();
  1         11041  
  1         22  
8 1     1   6 use Carp;
  1         0  
  1         50  
9 1     1   1032 use Parse::RecDescent;
  1         28295  
  1         7  
10 1     1   889 use Data::Dumper();
  1         4045  
  1         5454  
11              
12             sub Dumper {
13 0     0     local $Data::Dumper::Indent = 1;
14 0           local $Data::Dumper::SortKeys = 1;
15 0           Data::Dumper::Dumper(@_);
16             }
17              
18              
19             =comment
20             prepare a DBI sth from user defined selects, filters, sorts
21              
22             this constructor 'new' is called when a DBIx::OptimalQuery->prepare method
23             call is issued.
24              
25             my %opts = (
26             show => []
27             filter => ""
28             sort => ""
29             );
30              
31             $sth = $oq->prepare(%opts);
32             - same as -
33             $sth = DBIx::OptimalQuery::sth->new($oq,%opts);
34              
35             $sth->execute( limit => [0, 10]);
36             =cut
37             sub new {
38 0     0     my $class = shift;
39 0           my $oq = shift;
40 0           my %args = @_;
41              
42             #$$oq{error_handler}->("DEBUG: \$sth = $class->new(\$oq,\n".Dumper(\%args).")\n") if $$oq{debug};
43              
44 0           my $sth = bless \%args, $class;
45 0           $sth->{oq} = $oq;
46 0           $sth->_normalize();
47 0           $sth->create_select();
48 0           $sth->create_where();
49 0           $sth->create_order_by();
50              
51 0           return $sth;
52             }
53              
54 0     0     sub get_lo_rec { $_[0]{limit}[0] }
55 0     0     sub get_hi_rec { $_[0]{limit}[1] }
56              
57             sub set_limit {
58 0     0     my ($sth, $limit) = @_;
59 0           $$sth{limit} = $limit;
60 0           return undef;
61             }
62              
63             # execute statement
64             # notice that we can't execute other child cursors
65             # because their bind params are dependant on
66             # their parent cursor value
67             sub execute {
68 0     0     my ($sth) = @_;
69 0 0         return undef if $$sth{_already_executed};
70 0           $$sth{_already_executed}=1;
71              
72             #$$sth{oq}{error_handler}->("DEBUG: \$sth->execute()\n") if $$sth{oq}{debug};
73 0 0         return undef if $sth->count()==0;
74              
75 0           local $$sth{oq}{dbh}{LongReadLen};
76              
77             # build SQL for main cursor
78 0           { my $c = $sth->{cursors}->[0];
  0            
79 0           my @all_deps = (@{$c->{select_deps}}, @{$c->{where_deps}}, @{$c->{order_by_deps}});
  0            
  0            
  0            
80 0           my ($order) = @{ $sth->{oq}->_order_deps(\@all_deps) };
  0            
81 0           my @from_deps; push @from_deps, @$_ for @$order;
  0            
82              
83             # create from_sql, from_binds
84             # vars prefixed with old_ is used for supported non sql-92 joins
85 0           my ($from_sql, @from_binds, $old_join_sql, @old_join_binds );
86              
87 0           foreach my $from_dep (@from_deps) {
88 0           my ($sql, @binds) = @{ $sth->{oq}->{joins}->{$from_dep}->[1] };
  0            
89 0 0         push @from_binds, @binds if @binds;
90              
91             # if this is the driving table join
92 0 0         if (! $sth->{oq}->{joins}->{$from_dep}->[0]) {
    0          
93              
94             # alias it if not already aliased in sql
95 0           $from_sql .= $sql.' ';
96 0 0         $from_sql .= "$from_dep" unless $sql =~ /\b$from_dep\s*$/;
97 0           $from_sql .= "\n";
98             }
99            
100            
101             # if SQL-92 type join?
102             elsif (! defined $sth->{oq}->{joins}->{$from_dep}->[2]) {
103 0           $from_sql .= $sql."\n";
104             }
105            
106             # old style join
107             else {
108 0           $from_sql .= ", ".$sql.' '.$from_dep."\n";
109 0           my ($sql, @binds) = @{ $sth->{oq}->{joins}->{$from_dep}->[2] };
  0            
110 0 0         $old_join_sql .= " AND " if $old_join_sql ne '';
111 0           $old_join_sql .= $sql;
112 0           push @old_join_binds, @binds;
113             }
114             }
115            
116            
117             # construct where clause
118 0           my $where;
119 0           { my @where;
  0            
120 0 0         push @where, '('.$old_join_sql.') ' if $old_join_sql;
121 0 0         push @where, '('.$c->{where_sql}.') ' if $c->{where_sql};
122 0 0         $where = ' WHERE '.join("\nAND ", @where) if @where;
123             }
124            
125             # generate sql and bind params
126 0           $$c{sql} = "SELECT ".join(',', @{ $c->{select_sql} })." FROM $from_sql $where ".
127 0 0         (($c->{order_by_sql}) ? "ORDER BY ".$c->{order_by_sql} : '');
128              
129 0           my @binds = (@{ $c->{select_binds} }, @from_binds, @old_join_binds,
130 0           @{$c->{where_binds}}, @{$c->{order_by_binds}} );
  0            
  0            
131 0           $$c{binds} = \@binds;
132              
133             # if clobs have been selected, find & set LongReadLen
134 0 0 0       if ($$sth{oq}{dbtype} eq 'Oracle' &&
      0        
135             $$sth{'oq'}{'AutoSetLongReadLen'} &&
136 0           scalar(@{$$c{'selected_lobs'}})) {
137              
138             my $maxlenlobsql = "SELECT greatest(".join(',',
139 0           map { "nvl(max(DBMS_LOB.GETLENGTH($_)),0)" } @{$$c{'selected_lobs'}}
  0            
140 0           ).") FROM (".$$c{'sql'}.")";
141              
142 0           my ($SetLongReadLen) = $$sth{oq}{dbh}->selectrow_array($maxlenlobsql, undef, @{$$c{'binds'}});
  0            
143              
144 0 0 0       if (! $$sth{oq}{dbh}{LongReadLen} || $SetLongReadLen > $$sth{oq}{dbh}{LongReadLen}) {
145 0           $$sth{oq}{dbh}{LongReadLen} = $SetLongReadLen;
146             }
147             }
148              
149 0           $sth->add_limit_sql();
150             }
151              
152              
153             # build children cursors
154 0           my $cursors = $sth->{cursors};
155 0           foreach my $i (1 .. $#$cursors) {
156 0           my $c = $sth->{cursors}->[$i];
157 0           my $sd = $c->{select_deps};
158              
159             # define sql and binds for joins for this child cursor
160             # in the following vars
161 0           my ($from_sql, @from_binds, $where_sql, @where_binds );
162              
163             # define vars for child cursor driving table
164             # these are handled differently since we aren't joining in parent deps
165             # they were precomputed in _normalize method when constructing $oq
166              
167             ($from_sql, @from_binds) =
168 0           @{ $sth->{oq}->{joins}->{$sd->[0]}->[3]->{new_cursor}->{sql} };
  0            
169 0           $where_sql = $sth->{oq}->{joins}->{$sd->[0]}->[3]->{new_cursor}->{'join'};
170 0           my $order_by_sql = '';
171 0 0         if ($sth->{oq}->{joins}->{$sd->[0]}->[3]->{new_cursor_order_by}) {
172 0           $order_by_sql = " ORDER BY ".$sth->{oq}->{joins}->{$sd->[0]}->[3]->{new_cursor_order_by};
173             }
174              
175 0           $from_sql .= "\n";
176              
177             # now join in all other deps normally for this cursor
178 0           foreach my $i (1 .. $#$sd) {
179 0           my $joinAlias = $sd->[$i];
180              
181 0           my ($sql, @binds) = @{ $sth->{oq}->{joins}->{$joinAlias}->[1] };
  0            
182              
183             # these will NOT be defined for sql-92 type joins
184             my ($joinWhereSql, @joinWhereBinds) =
185 0           @{ $sth->{oq}->{joins}->{$joinAlias}->[2] }
186 0 0         if defined $sth->{oq}->{joins}->{$joinAlias}->[2];
187              
188             # if SQL-92 type join?
189 0 0         if (! defined $joinWhereSql) {
190 0           $from_sql .= $sql."\n";
191 0           push @from_binds, @binds;
192             }
193              
194             # old style join
195             else {
196 0           $from_sql .= ",\n$sql $joinAlias";
197 0           push @from_binds, @binds;
198 0 0         if ($joinWhereSql) {
199 0 0         $where_sql .= " AND " if $where_sql;
200 0           $where_sql .= $joinWhereSql;
201             }
202 0           push @where_binds, @joinWhereBinds;
203             }
204             }
205              
206             # build child cursor sql
207             $c->{sql} = "
208 0           SELECT ".join(',', @{ $c->{select_sql} })."
  0            
209             FROM $from_sql
210             WHERE $where_sql
211             $order_by_sql ";
212 0           $c->{binds} = [ @{ $c->{select_binds} }, @from_binds, @where_binds ];
  0            
213              
214             # if clobs have been selected, find & set LongReadLen
215 0 0 0       if ($$sth{oq}{dbtype} eq 'Oracle' &&
      0        
216             $$sth{'oq'}{'AutoSetLongReadLen'} &&
217 0           scalar(@{$$c{'selected_lobs'}})) {
218             my ($SetLongReadLen) = $$sth{oq}{dbh}->selectrow_array("
219             SELECT greatest(".join(',',
220 0           map { "nvl(max(DBMS_LOB.GETLENGTH($_)),0)" } @{$$c{'selected_lobs'}}
  0            
221             ).")
222 0           FROM (".$$c{'sql'}.")", undef, @{$$c{'binds'}});
  0            
223 0 0 0       if (! $$sth{oq}{dbh}{LongReadLen} || $SetLongReadLen > $$sth{oq}{dbh}{LongReadLen}) {
224 0           $$sth{oq}{dbh}{LongReadLen} = $SetLongReadLen;
225             }
226             }
227             }
228              
229 0           eval {
230 0           my $c;
231              
232             # prepare all cursors
233 0           foreach $c (@$cursors) {
234 0 0         $$sth{oq}->{error_handler}->("SQL:\n".$c->{sql}."\nBINDS:\n".Dumper($c->{binds})."\n") if $$sth{oq}{debug};
235 0           $c->{sth} = $sth->{oq}->{dbh}->prepare($c->{sql});
236             }
237 0           $c = $$cursors[0];
238 0           $c->{sth}->execute( @{ $c->{binds} } );
  0            
239 0           my @fieldnames = @{ $$c{select_field_order} };
  0            
240 0           my %rec;
241 0           my @bindcols = \( @rec{ @fieldnames } );
242 0           $c->{sth}->bind_columns(@bindcols);
243 0           $c->{bind_hash} = \%rec;
244             };
245 0 0         if ($@) {
246 0           die "Problem with SQL; $@\n";
247             }
248 0           return undef;
249             }
250              
251             # function to add limit sql
252             # $sth->add_limit_sql()
253             sub add_limit_sql {
254 0     0     my ($sth) = @_;
255              
256             #$$sth{oq}{error_handler}->("DEBUG: \$sth->add_limit_sql()\n") if $$sth{oq}{debug};
257 0   0       my $lo_limit = $$sth{limit}[0] || 0;
258 0   0       my $hi_limit = $$sth{limit}[1] || $sth->count();
259 0           my $c = $sth->{cursors}->[0];
260              
261 0 0         if ($$sth{oq}{dbtype} eq 'Oracle') {
    0          
    0          
262             $c->{sql} = "
263             SELECT *
264             FROM (
265             SELECT tablernk1.*, rownum RANK
266             FROM (
267 0           ".$c->{sql}."
268             ) tablernk1
269             WHERE rownum <= ?
270             ) tablernk2
271             WHERE tablernk2.RANK >= ? ";
272 0           push @{$$c{binds}}, ($hi_limit, $lo_limit);
  0            
273 0           push @{$$c{select_field_order}}, "DBIXOQRANK";
  0            
274             }
275              
276             # sqlserver doesn't support limit/offset until Sql Server 2012 (which I don't have to test)
277             # the workaround is this ugly hack...
278             elsif ($$sth{oq}{dbtype} eq 'Microsoft SQL Server') {
279 0 0         die "missing required U_ID in select" unless exists $$sth{oq}{select}{U_ID};
280              
281 0           my $sql = $c->{sql};
282              
283             # extract order by sql, and binds in order by from sql
284 0           my $orderbysql;
285 0 0         if ($sql =~ s/\ (ORDER BY\ .*?)$//) {
    0          
286 0           $orderbysql = $1;
287 0           my $copy = $orderbysql;
288 0           my $bindCount = $copy =~ tr/,//;
289 0 0         if ($bindCount > 0) {
290 0           my @newBinds;
291 0           push @newBinds, pop @{$$c{binds}} for 1 .. $bindCount;
  0            
292 0           @{$$c{binds}} = (reverse @newBinds, @{$$c{binds}});
  0            
  0            
293             }
294 0           $orderbysql .= ", ".$$sth{oq}{select}{U_ID}[1][0];
295             } elsif (exists $$sth{oq}{select}{U_ID}) {
296 0           $orderbysql = " ORDER BY ".$$sth{oq}{select}{U_ID}[1][0];
297             }
298              
299             # remove first select keyword, and add new one with windowing
300 0 0         if ($sql =~ s/^(\s*SELECT\s*)//) {
301 0           my $limit = int($hi_limit - $lo_limit + 1);
302 0           my $lo_limit = int($lo_limit);
303              
304             # sqlserver doesn't allow placeholders for limit and offset here
305 0           $c->{sql} = "SELECT TOP $limit * FROM (SELECT ROW_NUMBER() OVER ($orderbysql) AS RANK, $sql) tablerank1 WHERE tablerank1.RANK >= $lo_limit";
306 0           unshift @{$$c{select_field_order}}, "DBIXOQRANK";
  0            
307             }
308             }
309              
310             elsif ($$sth{oq}{dbtype} eq 'Pg') {
311 0           my $a = $lo_limit - 1;
312 0           my $b = $hi_limit - $lo_limit + 1;
313 0           $c->{sql} .= "\nLIMIT ? OFFSET ?";
314 0           push @{$$c{binds}}, ($b, $a);
  0            
315             }
316              
317             else {
318 0           my $a = $lo_limit - 1;
319 0           my $b = $hi_limit - $lo_limit + 1;
320 0           $c->{sql} .= "\nLIMIT ?,?";
321 0           push @{$$c{binds}}, ($a, $b);
  0            
322             }
323              
324 0           return undef;
325             }
326              
327              
328             # normalize member variables
329             sub _normalize {
330 0     0     my $sth = shift;
331             #$$sth{oq}{error_handler}->("DEBUG: \$sth->_normalize()\n") if $$sth{oq}{debug};
332              
333             # if show is not defined - then define it
334 0 0         if (! exists $sth->{show}) {
335 0           my @select;
336 0           foreach my $select (@{ $sth->{oq}->{'select'} } ) {
  0            
337 0           push @select, $select;
338             }
339 0           $sth->{show} = \@select;
340             }
341              
342             # define filter & sort if not defined
343 0 0         $sth->{'filter'} = "" if ! exists $sth->{'filter'};
344 0 0         $sth->{'sort'} = "" if ! exists $sth->{'sort'};
345 0           $sth->{'fetch_index'} = 0;
346 0           $sth->{'count'} = undef;
347 0           $sth->{'cursors'} = undef;
348              
349 0           return undef;
350             }
351              
352              
353              
354             # define @select & @select_binds, and add deps
355             sub create_select {
356 0     0     my $sth = shift;
357             #$$sth{oq}{error_handler}->("DEBUG: \$sth->create_select()\n") if $$sth{oq}{debug};
358              
359            
360             # find all of the columns that need to be shown
361 0           my %show;
362              
363             # find all deps to be used in select including cols marked always_select
364 0           my (@deps, @select_sql, @select_binds);
365 0           { my %deps;
  0            
366              
367             # add deps, @select, @select_binds for items in show
368 0           foreach my $show (@{ $sth->{show} }) {
  0            
369 0 0         $show{$show} = 1 if exists $sth->{'oq'}->{'select'}->{$show};
370 0           foreach my $dep (@{ $sth->{'oq'}->{'select'}->{$show}->[0] }) {
  0            
371 0           $deps{$dep} = 1;
372             }
373             }
374              
375             # add deps used in always_select
376 0           foreach my $colAlias (keys %{ $sth->{'oq'}->{'select'} }) {
  0            
377 0 0         if ($sth->{'oq'}->{'select'}->{$colAlias}->[3]->{always_select} ) {
378 0           $show{$colAlias} = 1;
379 0           $deps{$_} = 1 for @{ $sth->{'oq'}->{'select'}->{$colAlias}->[0] };
  0            
380             }
381             }
382 0           @deps = keys %deps;
383             }
384              
385             # order and index deps into appropriate cursors
386 0           my ($dep_order, $dep_idx) = @{ $sth->{oq}->_order_deps(\@deps) };
  0            
387              
388             # look though select again and add all cols with is_hidden option
389             # if all their deps have been fulfilled
390 0           foreach my $colAlias (keys %{ $sth->{'oq'}->{'select'} }) {
  0            
391 0 0         if ($sth->{'oq'}->{'select'}->{$colAlias}->[3]->{is_hidden}) {
392 0           my $deps = $sth->{'oq'}->{'select'}->{$colAlias}->[0];
393 0           my $all_deps_met = 1;
394 0           for (@$deps) {
395 0 0         if (! exists $dep_idx->{$_}) {
396 0           $all_deps_met = 0;
397 0           last;
398             }
399             }
400 0 0         $show{$colAlias} = 1 if $all_deps_met;
401             }
402             }
403              
404             # create main cursor structure & attach deps for main cursor
405 0           $sth->{'cursors'} = [ $sth->_get_main_cursor_template() ];
406 0           $sth->{'cursors'}->[0]->{'select_deps'} = $dep_order->[0];
407              
408             # unique counter that is used to uniquely identify cols in parent cursors
409             # to their children cursors
410 0           my $parent_bind_tag_idx = 0;
411              
412             # create other cursors (if they exist)
413             # and define how they join to their parent cursors
414             # by defining parent_join, parent_keys
415 0           foreach my $i (1 .. $#$dep_order) {
416 0           push @{ $sth->{'cursors'} }, $sth->_get_sub_cursor_template();
  0            
417 0           $sth->{'cursors'}->[$i]->{'select_deps'} = $dep_order->[$i];
418              
419             # add parent_join, parent_keys for this child cursor
420 0           my $driving_child_join_alias = $dep_order->[$i]->[0];
421 0           my $cursor_opts = $sth->{'oq'}->{'joins'}->{$driving_child_join_alias}->[3]->{new_cursor};
422 0           foreach my $part (@{ $cursor_opts->{'keys'} } ) {
  0            
423 0           my ($dep,$sql) = @$part;
424 0           my $key = 'DBIXOQMJK'.$parent_bind_tag_idx; $parent_bind_tag_idx++;
  0            
425 0           my $parent_cursor_idx = $dep_idx->{$dep};
426 0 0         die "could not find dep: $dep for new cursor" if $parent_cursor_idx eq '';
427 0           push @{ $sth->{'cursors'}->[$parent_cursor_idx]->{select_field_order} }, $key;
  0            
428 0           push @{ $sth->{'cursors'}->[$parent_cursor_idx]->{select_sql} }, "$dep.$sql AS $key";
  0            
429 0           push @{ $sth->{'cursors'}->[$i]->{'parent_keys'} }, $key;
  0            
430             }
431 0           $sth->{'cursors'}->[$i]->{'parent_join'} = $cursor_opts->{'join'};
432             }
433            
434             # plug in select_sql, select_binds for cursors
435 0           foreach my $show (keys %show) {
436 0           my $select = $sth->{'oq'}->{'select'}->{$show};
437 0 0         next if ! $select;
438              
439 0           my $cursor = $sth->{'cursors'}->[$dep_idx->{$select->[0]->[0]}];
440              
441 0           my $select_sql;
442              
443             # if type is date then use specified date format
444 0 0 0       if (! $$select[3]{select_sql} && $$select[3]{date_format}) {
445 0           my @tmp = @{ $select->[1] }; $select_sql = \ @tmp; # need a real copy cause we are going to mutate it
  0            
  0            
446 0 0 0       if ($$sth{oq}{dbtype} eq 'Oracle' ||
    0          
447             $$sth{oq}{dbtype} eq 'Pg') {
448 0           $$select_sql[0] = "to_char(".$$select_sql[0].",'".$$select[3]{date_format}."')";
449             } elsif ($$sth{oq}{dbtype} eq 'mysql') {
450 0           $$select_sql[0] = "date_format(".$$select_sql[0].",'".$$select[3]{date_format}."')";
451             } else {
452 0           die "unsupported DB";
453             }
454             }
455              
456             # else just copy the select
457             else {
458 0   0       $select_sql = $select->[3]->{select_sql} || $select->[1];
459             }
460              
461             # remember if a lob is selected
462 0 0 0       if ($$sth{oq}{dbtype} eq 'Oracle' &&
463             $sth->{oq}->get_col_types('select')->{$show} eq 'clob') {
464 0           push @{ $cursor->{selected_lobs} }, $show;
  0            
465             #$select_sql->[0] = 'to_char('.$select_sql->[0].')';
466             }
467              
468 0 0         if ($select_sql->[0] ne '') {
469 0           push @{ $cursor->{select_field_order} }, $show;
  0            
470 0           push @{ $cursor->{select_sql} }, $select_sql->[0].' AS '.$show;
  0            
471 0           push @{ $cursor->{select_binds} }, @$select_sql[1 .. $#$select_sql];
  0            
472             }
473             }
474              
475 0           return undef;
476             }
477            
478              
479              
480              
481             # template for the main cursor
482             sub _get_main_cursor_template {
483 0     0     { sth => undef,
484             sql => "",
485             binds => [],
486             selected_lobs => [],
487             select_field_order => [],
488             select_sql => [],
489             select_binds => [],
490             select_deps => [],
491             where_sql => "",
492             where_binds => [],
493             where_deps => [],
494             where_name => "",
495             order_by_sql => "",
496             order_by_binds => [],
497             order_by_deps => [],
498             order_by_name => []
499             };
500             }
501              
502             # template for explicitly defined additional cursors
503             sub _get_sub_cursor_template {
504 0     0     { sth => undef,
505             sql => "",
506             binds => [],
507             selected_lobs => [],
508             select_field_order => [],
509             select_sql => [],
510             select_deps => [],
511             select_binds => [],
512             parent_join => "",
513             parent_keys => [],
514             };
515             }
516              
517              
518              
519              
520            
521            
522              
523              
524             # modify cursor and add where clause data
525             sub create_where {
526 0     0     my $sth = shift;
527              
528             #$$sth{oq}{error_handler}->("DEBUG: \$sth->create_where()\n") if $$sth{oq}{debug};
529 0 0 0       return undef if $sth->{'filter'} eq '' && $sth->{'hiddenFilter'} eq '' && $sth->{'forceFilter'} eq '';
      0        
530              
531             # this sub glues together a parsed expression
532             # basically is glues statements that look like:
533             # '(' { sql => '', binds => [], deps => [], name => '' } 'LIKE'
534             # { sql => '', binds => [], deps => [], name => '' } ')'
535             # and then returns a single hash
536             my $glue_exp = sub {
537 0     0     my @deps;
538 0           my $sql = '';
539 0           my @binds;
540             my $name;
541 0           foreach my $i (@_) {
542 0 0         if (! ref($i)) {
543 0           $sql .= $i.' ';
544 0           $name .= $i.' ';
545             } else {
546 0 0         push @deps, @{ $$i{deps} } if ref($$i{deps}) eq 'ARRAY';
  0            
547 0 0         push @binds, @{ $$i{binds} } if ref($$i{binds}) eq 'ARRAY';
  0            
548 0 0         $sql .= $$i{sql}.' ' if exists $$i{sql};
549 0 0         $name .= $$i{name}.' ' if exists $$i{name};
550             }
551             }
552 0           my $rv = { deps=> \@deps, sql => $sql,
553             binds => \@binds, name => $name};
554 0           return $rv;
555 0           };
556              
557             my %translations = (
558 0     0     '*default*' => sub { $_[2] },
559 0     0     'logicOp' => sub { "\n$_[2]" },
560             'compOp' => sub {
561 0     0     my $rv = { name => lc($_[2]), sql => uc($_[2]) };
562 0 0         if (uc($_[2]) eq 'CONTAINS') { $$rv{sql} = 'LIKE'; }
  0 0          
563 0           elsif (uc($_[2]) eq 'NOT CONTAINS') { $$rv{sql} = 'NOT LIKE'; }
564 0           return $rv;
565             },
566              
567             'colAlias' => sub {
568 0     0     my $oq = $_[0];
569 0           my $colAlias = $_[3];
570 0 0         die "could not find colAlias $colAlias" unless exists $$oq{select}{$colAlias};
571 0           my $deps = $$oq{select}{$colAlias}[0];
572 0 0         my @tmp = @{ $$oq{select}{$colAlias}[3]{filter_sql} || $$oq{select}{$colAlias}[1] };
  0            
573 0           my $sql = shift @tmp;
574 0           my $binds = \ @tmp;
575 0           my $name = $$oq{select}{$colAlias}[2];
576 0           my $rv = { colAlias => $colAlias, deps => $deps, sql => $sql, binds => $binds, name => '['.$name.']'};
577 0           return $rv;
578             },
579              
580             'bindVal' => sub {
581 0     0     my $val = $_[2];
582 0           my $nice = $val;
583 0 0         $nice = "'".$nice."'" if $nice !~ /^[\d\.\-]+/;
584 0           return { sql => '?', binds => [$val], name => $nice };
585             },
586              
587             'quotedString' => sub {
588 0     0     my ($v) = $_[2];
589 0 0 0       ($v=~s/^\'// && $v=~s/\'$//) || ($v=~s/^\"// && $v=~s/\"$//);
      0        
590 0           return $v;
591             },
592              
593             'exp' => sub {
594 0     0     my $oq = shift;
595 0           my $rule = shift;
596 0           return $glue_exp->(@_);
597             },
598              
599              
600             'comparisonExp' => sub {
601 0     0     my $oq = shift;
602 0           my $rule = shift;
603 0           my @token = @_;
604              
605             # if doing empty string comparison
606 0 0 0       if ($token[2]{sql} eq '?' && $token[2]{binds}[0] eq '') {
    0 0        
607 0           my $t0 = $oq->get_col_type($token[0]{colAlias},'filter');
608 0           my $op = $token[1]{sql};
609              
610              
611             # if character field coalesce to empty string
612 0 0 0       if ($t0 eq 'char' || $t0 eq 'clob') {
613             # oracle treats empty string as null so coalesce null to '_ _'
614 0 0         if ($$oq{dbtype} eq 'Oracle') {
615 0 0 0       if ($op =~ /NOT\ /i || $op =~ /\!/) {
616 0           $token[0]{sql} = "COALESCE(TO_CHAR($token[0]{sql}),'_ _')";
617 0           $token[1]{sql} = '!=';
618 0           $token[1]{name} = '!=';
619 0           $token[2]{binds}[0] = '_ _';
620 0           $token[2]{name} = '""';
621             } else {
622 0           $token[0]{sql} = "COALESCE(TO_CHAR($token[0]{sql}),'_ _')";
623 0           $token[1]{sql} = '=';
624 0           $token[1]{name} = '=';
625 0           $token[2]{binds}[0] = '_ _';
626 0           $token[2]{name} = '""';
627             }
628             }
629             else {
630 0 0 0       if ($op =~ /NOT\ /i || $op =~ /\!/) {
631 0           $token[0]{sql} = "COALESCE($token[0]{sql},'')";
632 0           $token[1]{sql} = '!=';
633 0           $token[1]{name} = '!=';
634 0           $token[2]{binds}[0] = '';
635 0           $token[2]{name} = '""';
636             } else {
637 0           $token[0]{sql} = "COALESCE($token[0]{sql},'')";
638 0           $token[1]{sql} = '=';
639 0           $token[1]{name} = '=';
640 0           $token[2]{binds}[0] = '';
641 0           $token[2]{name} = '""';
642             }
643             }
644             }
645              
646             # else not char data so use IS NULL / IS NOT NULL operator
647             else {
648 0           pop @token;
649 0 0 0       if ($op =~ /NOT\ /i || $op =~ /\!/) {
650 0           $token[1]{sql} = "IS NOT NULL";
651 0           $token[1]{name} = '!=';
652 0           $token[2] = { name => '""' };
653             } else {
654 0           $token[1]{sql} = "IS NULL";
655 0           $token[1]{name} = '=';
656 0           $token[2] = { name => '""' };
657             }
658             }
659             }
660              
661             # if we are comparing 2 cols
662             elsif ($token[0]{colAlias} && $token[2]{colAlias}) {
663 0           my $t0 = $oq->get_col_type($token[0]{colAlias},'filter');
664 0           my $t1 = $oq->get_col_type($token[2]{colAlias},'filter');
665              
666             # if types are equal
667 0 0         if ($t0 ne $t1) {
668 0 0         if ($$oq{dbtype} eq 'Oracle') {
669 0 0         $token[0]{sql} = "TO_CHAR(".$token[0]{sql}.")" unless $t0 eq 'char';
670 0 0         $token[2]{sql} = "TO_CHAR(".$token[2]{sql}.")" unless $t1 eq 'char';
671             }
672             }
673 0 0         if ($token[1]{name} =~ /contains/) {
674 0           $token[0]{sql} = "UPPER(".$token[0]{sql}.")";
675 0           $token[2]{sql} = "UPPER(".$token[2]{sql}.")";
676              
677 0 0 0       if ($$oq{dbtype} eq 'Oracle' || $$oq{dbtype} eq 'SQLite') {
678 0           $token[2]{sql} = "'%'||".$token[2]{sql}."||'%'";
679             } else {
680 0           $token[2]{sql} = "CONCAT('%',".$token[2]{sql}.",'%')";
681             }
682             }
683             }
684              
685             # else we are comparing a column to a value
686             else {
687              
688             # add some code to support contains operator
689             # basically rewritten as a fuzzy search
690 0 0 0       if ($token[1]{name} =~ /contains/) {
    0          
691 0 0         if (! exists $$oq{select}{$token[0]{colAlias}}[3]{date_format}) {
692 0           $token[0]{sql} = 'UPPER('.$token[0]{sql}.')';
693             }
694 0 0         if ($token[2]{sql} eq '?') {
695 0           $token[2]{binds}[0] =~ s/\*/\%/g;
696 0           $token[2]{binds}[0] = '%'.uc($token[2]{binds}[0]).'%';
697 0           $token[2]{binds}[0] =~ s/\%\%/\%/g;
698             } else {
699 0 0         if (! exists $$oq{select}{$token[0]{colAlias}}[3]{date_format}) {
700 0           $token[2]{sql} = 'UPPER('.$token[2]{sql}.')';
701             }
702             }
703             }
704              
705             # if like search convert all * to wildcard %
706             elsif ($token[1]{sql} =~ /like/i && $token[2]{sql} eq '?') {
707 0           $token[2]{binds}[0] =~ s/\*/\%/g;
708             }
709              
710             # if lval is a date and we are doing a like comparison and rval is a value
711             # convert rval to a string using date_format
712 0 0 0       if (exists $$oq{select}{$token[0]{colAlias}}[3]{date_format} &&
    0 0        
    0 0        
    0 0        
      0        
      0        
      0        
713             $token[1]{sql} =~ /like/i && $token[2]{sql} eq '?') {
714 0 0         if ($$oq{dbtype} eq 'Oracle') {
    0          
715 0           $token[0]{sql} = "to_char(".$token[0]{sql}.",'".$$oq{select}{$token[0]{colAlias}}[3]{date_format}."')";
716             } elsif ($$oq{dbtype} eq 'mysql') {
717 0           $token[0]{sql} = "date_format(".$token[0]{sql}.",'".$$oq{select}{$token[0]{colAlias}}[3]{date_format}."')";
718             }
719             }
720            
721              
722             # if lval is a date and we are doing a numerical comparison and rval is a value
723             # convert rval to a date using date_format
724             elsif (exists $$oq{select}{$token[0]{colAlias}}[3]{date_format} &&
725             $token[1]{sql} !~ /like/i && $token[2]{sql} eq '?') {
726 0 0         if ($$oq{dbtype} eq 'Oracle') {
    0          
727 0           $token[2]{sql} = "to_date(?,'".$$oq{select}{$token[0]{colAlias}}[3]{date_format}."')";
728             } elsif ($$oq{dbtype} eq 'mysql') {
729 0           $token[2]{sql} = "str_to_date(?,'".$$oq{select}{$token[0]{colAlias}}[3]{date_format}."')";
730             }
731             }
732              
733             # if this is a numerical compare expression and the left side
734             # is a number force the right side to also be a number
735             elsif ($token[1]{sql} =~ /\=|\<|\>/ &&
736             $oq->get_col_type($token[0]{colAlias},'filter') eq 'num') {
737 0           my $v = $token[2]{binds}[0];
738 0           $v =~ s/[^\d\.\-]//g;
739 0 0         $v = 0 unless $v =~ /^\-?(\d*\.\d+|\d+)$/;
740 0           $token[2]{binds}[0] = $v;
741 0           $token[2]{name} = $v;
742             }
743              
744             # if numeric operator and field is an oracle clob, convert using to_char
745             elsif ($token[1]{sql} =~ /\=|\<|\>/ &&
746             $$oq{dbtype} eq 'Oracle' &&
747             $oq->get_col_type($token[0]{colAlias},'filter') eq 'clob') {
748 0           $token[0]{sql} = "to_char(".$token[0]{sql}.")";
749             }
750             }
751              
752             # if this field comes from a dep with new_cursor => 1
753             # token 0 is the left side of the expression realized as a hashref:
754             # { sql => '', binds => [], deps => [], name => '' }
755             # we need to add additional tokens if a filter is done on a field
756             # with an ancestor dependancy with option new_cursor => 1
757              
758             # get ancestor path from newest to oldest new_cursor dep
759 0           my @path = ( $$oq{select}{$token[0]{colAlias}}[0][0] );
760 0           { my $joinDep = $path[0];
  0            
761 0           while (1) {
762 0           my $parentDep = $$oq{joins}{$joinDep}[0][0];
763 0 0         if ($parentDep) {
764 0           push @path, $parentDep;
765 0           $joinDep = $parentDep;
766             } else {
767 0           last;
768             }
769             }
770             }
771              
772             # remove all oldest parents until we find a new_cursor (keep that one)
773 0           while (@path) {
774 0 0         if ($$oq{joins}{$path[-1]}[3]{new_cursor}) {
775 0           last;
776             } else {
777 0           pop @path;
778             }
779             }
780              
781             # if ancestors with new_cursor option exists
782 0 0         if (@path) {
783 0           @path = reverse @path;
784 0           my ($preSql, $postSql, @preBinds);
785 0           foreach my $joinDep (@path) {
786 0           my ($fromSql, @fromBinds) = @{ $$oq{joins}{$joinDep}[1] };
  0            
787              
788             # unwrap SQL-92 join and add join to where
789 0           $fromSql =~ s/^\s+//;
790 0           $fromSql =~ s/^LEFT\s*//i;
791 0           $fromSql =~ s/^OUTER\s*//i;
792 0           $fromSql =~ s/^JOIN\s*//i;
793              
794 0           my $corelatedJoin;
795 0 0         if ($fromSql =~ /^(.*)\bON\s*\((.*)\)\s*$/is) {
796 0           $fromSql = $1;
797 0           $corelatedJoin = $2;
798             } else {
799 0           die "could not parse for corelated join";
800             }
801              
802             # in a one2many filter that has a negative operator, we need to use
803             # a NOT EXISTS and unnegate the operator
804 0 0         if ($token[2]{name} eq '""') {
    0          
    0          
805 0 0         if ($token[1]{sql} eq '=') {
    0          
806 0           $preSql .= "NOT ";
807 0           $token[1]{sql} = '!=';
808             }
809             elsif ($token[1]{sql} eq 'IS NULL') {
810 0           $preSql .= "NOT ";
811 0           $token[1]{sql} = 'IS NOT NULL';
812             }
813             }
814             elsif ($token[1]{sql} eq '!=') {
815 0           $token[1]{sql} = '=';
816 0           $preSql .= "NOT ";
817             }
818             elsif ($token[1]{sql} =~ s/NOT\ //) {
819 0           $preSql .= "NOT ";
820             }
821 0           $preSql .= "EXISTS (\n SELECT 1\n FROM $fromSql\n WHERE ($corelatedJoin)\n AND ";
822 0           $postSql .= ')';
823 0           push @preBinds, @fromBinds;
824             }
825              
826             # update left expression deps and binds
827 0           $token[0]{deps} = $$oq{joins}{$path[0]}[0];
828 0 0         unshift @{ $token[0]{binds} }, @preBinds if @preBinds;
  0            
829              
830             # add new pre/post sql tokens
831 0           unshift @token, { sql => $preSql, name => '' };
832 0           push @token, { sql => $postSql, name => '' };
833             }
834              
835 0           return $glue_exp->(@token);
836             },
837              
838             'namedFilter' => sub {
839 0     0     my $oq = $_[0];
840 0           my $namedFilterAlias = $_[2];
841 0           my $args = $_[4];
842 0 0         die "was expecting that namedFilter args would be an array ref"
843             unless ref($args) eq 'ARRAY';
844 0           my $r = $$oq{named_filters}{$namedFilterAlias};
845 0           my ($deps, $sql, $binds, $name);
846 0 0         if (ref($r) eq 'ARRAY') {
    0          
847 0           $deps= $$r[0];
848 0           my @tmp = @{ $$r[1] };
  0            
849 0           $sql = shift @tmp;
850 0           $binds = \@tmp;
851 0           $name = $$r[2];
852             } elsif (ref($r) eq 'HASH') {
853             die "could not find sql_generator for named_filter $namedFilterAlias"
854 0 0         unless ref($$r{sql_generator}) eq 'CODE';
855 0           ($deps, $binds, $name) = @{ $$r{sql_generator}->(@$args) };
  0            
856 0 0         $deps = [$deps] if ! ref $deps;
857 0 0         if (ref($binds) eq 'ARRAY') {
858 0           $sql = shift @$binds;
859             } else {
860 0           $sql = $binds;
861 0           $binds = [];
862             }
863             } else {
864 0 0         die "could not find named_filter $namedFilterAlias" unless ref $r;
865             }
866 0           return { deps => $deps, sql => '('.$sql.')', binds => $binds, name => $name };
867             }
868 0           );
869              
870              
871 0           my $c = $sth->{cursors}->[0];
872              
873             # add filter parts to cursor's where parts
874 0 0         if ($sth->{'filter'} ne '') {
875             my $filter = $$sth{oq}->parse($DBIx::OptimalQuery::filterGrammar, $sth->{'filter'}, \%translations)
876 0 0         or die "could not parse filter: ".$sth->{'filter'};
877              
878 0           push @{ $c->{where_deps} }, @{ $$filter{deps} };
  0            
  0            
879 0           $c->{where_sql} = $$filter{sql};
880 0           push @{ $c->{where_binds} }, @{ $$filter{binds} };
  0            
  0            
881 0           $c->{where_name} = $$filter{name};
882             }
883              
884             # add hidden filter parts to cursor's where parts
885 0 0         if ($sth->{'hiddenFilter'} ne '') {
886             my $hiddenFilter = $$sth{oq}->parse($DBIx::OptimalQuery::filterGrammar, $sth->{'hiddenFilter'}, \%translations)
887 0 0         or die "could not parse hiddenFilter: ".$sth->{'hiddenFilter'};
888 0           push @{ $c->{where_deps} }, @{ $$hiddenFilter{deps} };
  0            
  0            
889 0 0         $c->{where_sql} = '('.$c->{where_sql}.")\nAND " if $c->{where_sql} ne '';
890 0           $c->{where_sql} .= '('.$$hiddenFilter{sql}.')';
891 0           push @{ $c->{where_binds} }, @{ $$hiddenFilter{binds} };
  0            
  0            
892             }
893              
894             # add system filter parts to cursor's where parts
895 0 0         if ($sth->{'forceFilter'} ne '') {
896             my $forceFilter = $$sth{oq}->parse($DBIx::OptimalQuery::filterGrammar, $sth->{'forceFilter'}, \%translations)
897 0 0         or die "could not parse forceFilter: ".$sth->{'forceFilter'};
898 0           push @{ $c->{where_deps} }, @{ $$forceFilter{deps} };
  0            
  0            
899 0 0         $c->{where_sql} = '('.$c->{where_sql}.")\nAND " if $c->{where_sql} ne '';
900 0           $c->{where_sql} .= '('.$$forceFilter{sql}.')';
901 0           push @{ $c->{where_binds} }, @{ $$forceFilter{binds} };
  0            
  0            
902             }
903              
904 0           return undef;
905             }
906              
907              
908              
909              
910             # modify cursor and add order by data
911             sub create_order_by {
912 0     0     my $sth = shift;
913              
914 0 0         if ($sth->{'sort'} ne '') {
915             #$$sth{oq}{error_handler}->("DEBUG: \$sth->create_order_by()\n") if $$sth{oq}{debug};
916             my %translations = (
917 0     0     '*default*' => sub { $_[2] },
918              
919             'expList' => sub {
920 0     0     my ($oq) = @_;
921 0           my (%deps, @sql, @binds, @nice);
922 0 0         die "was expecting an array ref!" unless ref($_[2]) eq 'ARRAY';
923 0           foreach my $sort (@{ $_[2] }) {
  0            
924 0 0         die "was expecting a hash ref!" unless ref($sort) eq 'HASH';
925 0           $deps{$_} = 1 for @{ $$sort{deps} };
  0            
926 0           push @sql, $$sort{sql};
927 0           push @binds, @{ $$sort{binds} };
  0            
928 0           push @nice, $$sort{nice};
929             }
930 0           my @deps = keys %deps;
931 0           return [ \@deps, join(', ', @sql), \@binds, \@nice ];
932             },
933              
934             'expression' => sub {
935 0     0     my $oq = $_[0];
936 0           my $def = $_[2];
937 0           my $sql_sort_opts_to_append = lc(join(' ', @{$_[3]}));
  0            
938              
939 0 0         if ($sql_sort_opts_to_append) {
940 0           $$def{sql} .= ' '.$sql_sort_opts_to_append;
941 0 0         $$def{nice} .= ($sql_sort_opts_to_append =~ /desc/) ?
942             ' (reverse)' : $sql_sort_opts_to_append;
943             }
944 0           return $def;
945             },
946              
947             'quotedString' => sub {
948 0 0 0 0     $_ = $_[2]; (s/^\'// && s/\'$//) || (s/^\"// && s/\"$//); $_;
  0   0        
  0            
949             },
950              
951             'namedSort' => sub {
952 0     0     my $oq = $_[0];
953 0           my $namedSortAlias = $_[2];
954 0           my $args = $_[4];
955 0 0         die "was expecting that namedSort args would be an array ref"
956             unless ref($args) eq 'ARRAY';
957 0           my $r = $$oq{named_sorts}{$namedSortAlias};
958 0           my ($deps, $sql, $binds, $nice);
959 0 0         if (ref($r) eq 'ARRAY') {
    0          
960 0           $deps = $$r[0];
961 0           my @tmp = @{ $$r[1] };
  0            
962 0           $sql = shift @tmp;
963 0           $binds = \ @tmp;
964 0           $nice = $$r[3];
965             } elsif (ref($r) eq 'HASH') {
966             die "could not find sql_generator for named_sort $namedSortAlias"
967 0 0         unless ref($$r{sql_generator}) eq 'CODE';
968 0           ($deps, $binds, $nice) = @{ $$r{sql_generator}->(@$args) };
  0            
969 0 0         $deps = [$deps] if ! ref $deps;
970 0           $sql = shift @$binds;
971             } else {
972 0 0         die "could not find named_sort $namedSortAlias" unless ref $r;
973             }
974 0           return { deps => $deps, sql => $sql, binds => $binds, nice => $nice };
975             },
976              
977             'colAlias' => sub {
978 0     0     my $oq = $_[0];
979 0           my $colAlias = $_[3];
980             die "could not find colAlias $colAlias"
981 0 0         unless exists $$oq{select}{$colAlias};
982 0           my $deps = $$oq{select}{$colAlias}[0];
983             my @tmp = @{
984 0 0         $$oq{select}{$colAlias}[3]{sort_sql} || $$oq{select}{$colAlias}[1] };
  0            
985 0           my $sql = shift @tmp;
986 0           my $binds = \@tmp;
987 0 0         die "could not find nice name" if $$oq{select}{$colAlias}[2] eq '';
988 0           my $nice = '['.$$oq{select}{$colAlias}[2].']';
989              
990 0 0 0       if ($$sth{oq}{dbtype} eq 'Oracle' &&
      0        
991             $sth->{oq}->get_col_types('select')->{$colAlias} eq 'clob' &&
992             $sql !~ /^cast\(/i) {
993 0           $sql = "cast($sql as varchar2(100))";
994             }
995 0           return { deps => $deps, sql => $sql, binds => $binds, nice => $nice };
996             }
997 0           );
998              
999             my $result = $$sth{oq}->parse($DBIx::OptimalQuery::sortGrammar, $sth->{'sort'}, \%translations)
1000 0 0         or die "could not parse sort: ".$sth->{'sort'};
1001              
1002 0           my $c = $sth->{cursors}->[0];
1003             ($c->{order_by_deps}, $c->{order_by_sql},
1004 0           $c->{order_by_binds}, $c->{order_by_name}) = @$result;
1005             }
1006 0           return undef;
1007             }
1008              
1009              
1010              
1011              
1012              
1013              
1014              
1015              
1016              
1017            
1018              
1019              
1020             # fetch next row or return undef when done
1021             sub fetchrow_hashref {
1022 0     0     my ($sth) = @_;
1023 0 0         return undef unless $sth->count() > 0;
1024 0           $sth->execute(); # execute if not already existed
1025              
1026             #$$sth{oq}{error_handler}->("DEBUG: \$sth->fetchrow_hashref()\n") if $$sth{oq}{debug};
1027              
1028 0           my $cursors = $sth->{cursors};
1029 0           my $c = $cursors->[0];
1030              
1031             # bind hash value to column data
1032 0           my $rec = $$c{bind_hash};
1033              
1034             # fetch record
1035 0 0         if (my $v = $c->{sth}->fetch()) {
1036              
1037 0           foreach my $i (0 .. $#$v) {
1038              
1039             # if col type is decimal auto trim 0s after decimal
1040 0 0 0       if ($c->{sth}->{TYPE}->[$i] eq '3' && $$v[$i] =~ /\./) {
1041 0           $$v[$i] =~ s/0+$//;
1042 0           $$v[$i] =~ s/\.$//;
1043             }
1044             }
1045            
1046 0           $sth->{'fetch_index'}++;
1047              
1048             # execute other cursors
1049 0           foreach my $i (1 .. $#$cursors) {
1050 0           $c = $cursors->[$i];
1051              
1052 0           $c->{sth}->execute( @{ $c->{binds} },
1053 0           map { $$rec{$_} } @{ $c->{parent_keys} } );
  0            
  0            
1054              
1055 0           my $cols = $$c{select_field_order};
1056 0           @$rec{ @$cols } = [];
1057              
1058 0           while (my @vals = $c->{sth}->fetchrow_array()) {
1059 0           for (my $i=0; $i <= $#$cols; $i++) {
1060 0           push @{ $$rec{$$cols[$i]} }, $vals[$i];
  0            
1061             }
1062             }
1063 0           $c->{sth}->finish();
1064             }
1065 0           return $rec;
1066             } else {
1067 0           return undef;
1068             }
1069             }
1070              
1071             # finish sth
1072             sub finish {
1073 0     0     my ($sth) = @_;
1074             #$$sth{oq}{error_handler}->("DEBUG: \$sth->finish()\n") if $$sth{oq}{debug};
1075 0           foreach my $c (@{$$sth{cursors}}) {
  0            
1076 0 0         $$c{sth}->finish() if $$c{sth};
1077 0           undef $$c{sth};
1078             }
1079 0           return undef;
1080             }
1081              
1082             # get count for sth
1083             sub count {
1084 0     0     my $sth = shift;
1085              
1086             # if count is not already defined, define it
1087 0 0         if (! defined $sth->{count}) {
1088             #$$sth{oq}{error_handler}->("DEBUG: \$sth->count()\n") if $$sth{oq}{debug};
1089              
1090 0           my $c = $sth->{cursors}->[0];
1091              
1092 0           my $drivingTable = $c->{select_deps}->[0];
1093              
1094             # only need to join in driving table with
1095             # deps used in where clause
1096 0           my $deps = [ $drivingTable, @{$c->{where_deps}} ];
  0            
1097 0           ($deps) = @{ $sth->{oq}->_order_deps($deps) };
  0            
1098 0           my @from_deps; push @from_deps, @$_ for @$deps;
  0            
1099              
1100             # create from_sql, from_binds
1101             # vars prefixed with old_ is used for supported non sql-92 joins
1102 0           my ($from_sql, @from_binds, $old_join_sql, @old_join_binds );
1103 0           foreach my $from_dep (@from_deps) {
1104 0           my ($sql, @binds) = @{ $sth->{oq}->{joins}->{$from_dep}->[1] };
  0            
1105 0 0         push @from_binds, @binds if @binds;
1106              
1107             # if this is the driving table join
1108 0 0         if (! $sth->{oq}->{joins}->{$from_dep}->[0]) {
    0          
1109              
1110             # alias it if not already aliased in sql
1111 0 0         $sql .= " $from_dep" unless $sql =~ /\b$from_dep\s*$/;
1112 0           $from_sql .= $sql;
1113             }
1114              
1115             # if SQL-92 type join?
1116             elsif (! $sth->{oq}->{joins}->{$from_dep}->[2]) {
1117 0           $from_sql .= "\n".$sql;
1118             }
1119              
1120             # old style join
1121             else {
1122 0           $from_sql .= ",\n".$sql.' '.$from_dep;
1123 0           my ($sql, @binds) = @{ $sth->{oq}->{joins}->{$from_dep}->[2] };
  0            
1124 0 0         if ($sql) {
1125 0 0         $old_join_sql .= " AND " if $old_join_sql ne '';
1126 0           $old_join_sql .= $sql;
1127             }
1128 0           push @old_join_binds, @binds;
1129             }
1130             }
1131              
1132              
1133             # construct where clause
1134 0           my $where;
1135 0           { my @where;
  0            
1136 0 0         push @where, '('.$old_join_sql.') ' if $old_join_sql;
1137 0 0         push @where, '('.$c->{where_sql}.') ' if $c->{where_sql};
1138 0 0         $where = 'WHERE '.join("\nAND ", @where) if @where;
1139             }
1140              
1141             # generate sql and bind params
1142 0           my $sql = "
1143             SELECT count(*)
1144             FROM (
1145             SELECT $drivingTable.*
1146             FROM $from_sql
1147             $where
1148             ) cnt_query";
1149 0           my @binds = (@from_binds, @old_join_binds, @{$c->{where_binds}});
  0            
1150              
1151 0           eval {
1152 0 0         $$sth{oq}->{error_handler}->("SQL:\n$sql\nBINDS:\n".Dumper(\@binds)."\n") if $$sth{oq}{debug};
1153 0           ($sth->{count}) = $sth->{oq}->{dbh}->selectrow_array($sql, undef, @binds);
1154 0 0         }; if ($@) {
1155 0           die "Problem finding count for SQL:\n$sql\nBINDS:\n".join(',',@binds)."\n\n$@\n";
1156             }
1157             }
1158              
1159 0           return $sth->{count};
1160             }
1161              
1162 0     0     sub fetch_index { $_->{'fetch_index'} }
1163              
1164             sub filter_descr {
1165 0     0     my $sth = shift;
1166 0           return $sth->{cursors}->[0]->{'where_name'};
1167             }
1168              
1169             sub sort_descr {
1170 0     0     my $sth = shift;
1171 0 0         if (wantarray) {
1172 0           return @{ $sth->{cursors}->[0]->{'order_by_name'} };
  0            
1173             } else {
1174 0           return join(', ', @{ $sth->{cursors}->[0]->{'order_by_name'} });
  0            
1175             }
1176             }
1177              
1178              
1179              
1180              
1181              
1182              
1183              
1184              
1185              
1186              
1187              
1188              
1189              
1190              
1191              
1192              
1193              
1194              
1195              
1196              
1197              
1198              
1199              
1200              
1201              
1202              
1203              
1204              
1205              
1206              
1207              
1208              
1209              
1210              
1211              
1212              
1213              
1214              
1215              
1216             package DBIx::OptimalQuery;
1217              
1218             =comment
1219              
1220             use DBIx::OptimalQuery;
1221             my $oq = DBIx::OptimalQuery->new(
1222             select => {
1223             'alias' => [dep, sql, nice_name, { OPTIONS } ]
1224             }
1225              
1226             joins => {
1227             'alias' => [dep, join_sql, where_sql, { OPTIONS } ]
1228             }
1229              
1230             named_filters => {
1231             'name' => [dep, sql, nice]
1232             'name' => {
1233             sql_generator => sub {
1234             my %args = @_;
1235             return [dep, sql, name]
1236             }
1237             title => "text displayed on interactive filter"
1238             }
1239             },
1240              
1241             named_sorts => {
1242             'name' => [dep, sql, nice]
1243             'name' => { sql_generator => sub { return [dep, sql, name] } }
1244             },
1245              
1246            
1247              
1248             debug => 0 | 1
1249             );
1250             =cut
1251              
1252 1     1   5 use strict;
  1         2  
  1         26  
1253 1     1   3 use Carp;
  1         2  
  1         45  
1254 1     1   3 use Data::Dumper;
  1         1  
  1         37  
1255 1     1   4 use DBI();
  1         1  
  1         2685  
1256              
1257             sub new {
1258 0     0 0   my $class = shift;
1259 0           my %args = @_;
1260 0           my $oq = bless \%args, $class;
1261              
1262 0   0       $$oq{debug} ||= 0;
1263             #$$oq{error_handler}->("DEBUG: $class->new(".Dumper(\%args).")\n") if $$oq{debug};
1264              
1265             die "BAD_PARAMS - must provide a dbh!"
1266 0 0         unless $oq->{'dbh'};
1267             die "BAD_PARAMS - must define a select key in call to constructor"
1268 0 0         unless ref($oq->{'select'}) eq 'HASH';
1269             die "BAD_PARAMS - must define a joins key in call to constructor"
1270 0 0         unless ref($oq->{'joins'}) eq 'HASH';
1271              
1272              
1273 0           $oq->_normalize();
1274              
1275 0           $$oq{dbtype} = $$oq{dbh}{Driver}{Name};
1276 0 0         $$oq{dbtype} = $$oq{dbh}->get_info(17) if $$oq{dbtype} eq 'ODBC';
1277              
1278 0           return $oq;
1279             }
1280              
1281              
1282              
1283              
1284              
1285              
1286              
1287              
1288              
1289              
1290              
1291             our $filterGrammar = <<'TILEND';
1292             start: exp /^$/
1293              
1294             exp:
1295             '(' exp ')' logicOp exp
1296             | '(' exp ')'
1297             | comparisonExp logicOp exp
1298             | comparisonExp
1299              
1300             comparisonExp:
1301             namedFilter
1302             | colAlias compOp colAlias
1303             | colAlias compOp bindVal
1304              
1305             bindVal: float | quotedString
1306              
1307             logicOp:
1308             /and/i
1309             | /or/i
1310              
1311             namedFilter: /\w+/ '(' namedFilterArg(s? /,/) ')'
1312              
1313             namedFilterArg: quotedString | float | unquotedIdentifier
1314              
1315             unquotedIdentifier: /\w+/
1316              
1317             colAlias: '[' /\w+/ ']'
1318              
1319             float:
1320             /\-?\d*\.?\d+/
1321             | /\-?\d+\.?\d*/
1322              
1323             quotedString:
1324             /'.*?'/
1325             | /".*?"/
1326              
1327             compOp:
1328             '<=' | '>=' | '=' | '!=' | '<' | '>' |
1329             /contains/i | /not\ contains/i | /like/i | /not\ like/i
1330              
1331             TILEND
1332              
1333              
1334             our $sortGrammar = <<'TILEND';
1335             start: expList /^$/
1336              
1337             expList: expression(s? /,/)
1338              
1339             expression:
1340             namedSort opt(?)
1341             | colAlias opt(?)
1342              
1343             opt: /desc/i
1344              
1345             namedSort: /\w+/ '(' namedSortArg(s? /,/) ')'
1346             namedSortArg: quotedString | float
1347              
1348             colAlias: '[' /\w+/ ']'
1349              
1350             float:
1351             /\-?\d*\.?\d+/
1352             | /\-?\d+\.?\d*/
1353              
1354             quotedString:
1355             /'.*?(?
1356             | /".*?(?
1357              
1358             TILEND
1359              
1360             our (%cached_parsers, $oq, $translations);
1361             sub translator_callback {
1362 0 0   0 0   return $$translations{$_[0]}->($oq, @_) if exists $$translations{$_[0]};
1363 0 0         return $$translations{'*default*'}->($oq, @_) if exists $$translations{'*default*'};
1364 0           return 1;
1365             }
1366              
1367             sub parse {
1368 0     0 0   local $oq = shift;
1369 0           my $grammar = shift;
1370 0           my $string = shift;
1371 0           local $translations = shift;
1372 0   0       my $start_rule = shift || 'start';
1373 0           local $::RD_AUTOACTION = 'DBIx::OptimalQuery::translator_callback(@item);';
1374 0   0       $cached_parsers{$grammar} ||= Parse::RecDescent->new($grammar);
1375 0           return $cached_parsers{$grammar}->$start_rule($string);
1376             }
1377              
1378              
1379              
1380              
1381              
1382              
1383             # normalize member variables
1384             sub _normalize {
1385 0     0     my $oq = shift;
1386             #$$oq{error_handler}->("DEBUG: \$oq->_normalize()\n") if $$oq{debug};
1387              
1388 0 0         $oq->{'AutoSetLongReadLen'} = 1 unless exists $oq->{'AutoSetLongReadLen'};
1389              
1390             # make sure all option hash refs exist
1391 0   0       $oq->{'select'}->{$_}->[3] ||= {} for keys %{ $oq->{'select'} };
  0            
1392 0   0       $oq->{'joins' }->{$_}->[3] ||= {} for keys %{ $oq->{'joins'} };
  0            
1393              
1394              
1395             # since the sql & deps definitions can optionally be entered as arrays
1396             # turn all into arrays if not already
1397 0           for ( # key, index
1398             ['select', 0], ['select', 1],
1399             ['joins', 0], ['joins', 1], ['joins', 2],
1400             ['named_filters', 0], ['named_filters', 1],
1401             ['named_sorts', 0], ['named_sorts', 1] ) {
1402 0           my ($key, $i) = @$_;
1403 0   0       $oq->{$key} ||= {};
1404 0           foreach my $alias (keys %{ $oq->{$key} }) {
  0            
1405 0 0 0       if (ref($oq->{$key}->{$alias}) eq 'ARRAY' &&
      0        
1406             defined $oq->{$key}->{$alias}->[$i] &&
1407             ref($oq->{$key}->{$alias}->[$i]) ne 'ARRAY') {
1408 0           $oq->{$key}->{$alias}->[$i] = [$oq->{$key}->{$alias}->[$i]];
1409             }
1410             }
1411             }
1412              
1413             # make sure the following select options, if they exist are array references
1414 0           foreach my $col (keys %{ $oq->{'select'} }) {
  0            
1415 0           my $opts = $oq->{'select'}->{$col}->[3];
1416 0           foreach my $opt (qw( select_sql sort_sql filter_sql )) {
1417             $opts->{$opt} = [$opts->{$opt}]
1418 0 0 0       if exists $opts->{$opt} && ref($opts->{$opt}) ne 'ARRAY';
1419             }
1420              
1421             # make sure defined deps exist
1422 0           foreach my $dep (@{ $$oq{'select'}{$col}[0] }) {
  0            
1423             die "dep $dep for select $col does not exist"
1424 0 0 0       if defined $dep && ! exists $$oq{'joins'}{$dep};
1425             }
1426             }
1427              
1428             # look for new cursors and define parent child links if not already defined
1429 0           foreach my $join (keys %{ $oq->{'joins'} }) {
  0            
1430 0           my $opts = $oq->{'joins'}->{$join}->[3];
1431 0 0         if (exists $opts->{new_cursor}) {
1432 0 0         if (ref($opts->{new_cursor}) ne 'HASH') {
1433 0           $oq->_formulate_new_cursor($join);
1434             } else {
1435             die "could not find keys, join, and sql for new cursor in $join"
1436             unless exists $opts->{new_cursor}->{'keys'} &&
1437             exists $opts->{new_cursor}->{'join'} &&
1438 0 0 0       exists $opts->{new_cursor}->{'sql'};
      0        
1439             }
1440             }
1441              
1442             # make sure defined deps exist
1443 0           foreach my $dep (@{ $$oq{'joins'}{$join}[0] }) {
  0            
1444             die "dep $dep for join $join does not exist"
1445 0 0 0       if defined $dep && ! exists $$oq{'joins'}{$dep};
1446             }
1447             }
1448              
1449             # make sure deps for named_sorts exist
1450 0           foreach my $named_sort (keys %{ $$oq{'named_sorts'} }) {
  0            
1451 0           foreach my $dep (@{ $$oq{'named_sorts'}{$named_sort}->[0] }) {
  0            
1452             die "dep $dep for named_sort $named_sort does not exist"
1453 0 0 0       if defined $dep && ! exists $$oq{'joins'}{$dep};
1454             }
1455             }
1456              
1457             # make sure deps for named_filter exist
1458 0           foreach my $named_filter (keys %{ $$oq{'named_filters'} }) {
  0            
1459 0 0         if (ref($$oq{'named_filters'}{$named_filter}) eq 'ARRAY') {
1460 0           foreach my $dep (@{ $$oq{'named_filters'}{$named_filter}->[0] }) {
  0            
1461             die "dep $dep for named_sort $named_filter does not exist"
1462 0 0 0       if defined $dep && ! exists $$oq{'joins'}{$dep};
1463             }
1464             }
1465             }
1466              
1467 0           $oq->{'col_types'} = undef;
1468              
1469 0           return undef;
1470             }
1471              
1472              
1473              
1474              
1475              
1476              
1477              
1478             # defines how a child cursor joins to its parent cursor
1479             # by defining keys, join, sql in child cursor
1480             # called from the _normalize method
1481             sub _formulate_new_cursor {
1482 0     0     my $oq = shift;
1483 0           my $joinAlias = shift;
1484              
1485             #$$oq{error_handler}->("DEBUG: \$oq->_formulate_new_cursor('$joinAlias')\n") if $$oq{debug};
1486              
1487             # vars to define
1488 0           my (@keys, $join, $sql, @sqlBinds);
1489              
1490             # get join definition
1491 0           my ($fromSql, @fromBinds) = @{ $oq->{joins}->{$joinAlias}->[1] };
  0            
1492              
1493 0           my ($whereSql, @whereBinds);
1494 0           ($whereSql, @whereBinds) = @{ $oq->{joins}->{$joinAlias}->[2] }
1495 0 0         if defined $oq->{joins}->{$joinAlias}->[2];
1496              
1497             # if NOT an SQL-92 type join
1498 0 0         if (defined $whereSql) {
1499 0           $whereSql =~ s/\(\+\)/\ /g; # remove outer join notation
1500 0 0         die "BAD_PARAMS - where binds not allowed in 'new_cursor' joins"
1501             if scalar(@whereBinds);
1502             }
1503              
1504             # else is SQL-92 so separate out joins from table definition
1505             # do this by making it a pre SQL-92 type join
1506             # by defining $whereSql
1507             # and removing join sql from $fromSql
1508             else {
1509 0           $_ = $fromSql;
1510 0           m/\G\s*left\b/sicg;
1511 0           m/\G\s*join\b/sicg;
1512              
1513             # parse inline view
1514 0 0         if (m/\G\s*\(/scg) {
    0          
1515 0           $fromSql = '(';
1516 0           my $p=1;
1517 0           my $q;
1518 0   0       while ($p > 0 && m/\G(.)/scg) {
1519 0           my $c = $1;
1520 0 0 0       if ($q) { $q = '' if $c eq $q; } # if end of quote
  0 0          
    0          
    0          
    0          
1521 0           elsif ($c eq "'" || $c eq '"') { $q = $c; } # if start of quote
1522 0           elsif ($c eq '(') { $p++; } # if left paren
1523 0           elsif ($c eq ')') { $p--; } # if right paren
1524 0           $fromSql .= $c;
1525             }
1526             }
1527              
1528             # parse table name
1529             elsif (m/\G\s*(\w+)\b/scg) {
1530 0           $fromSql = $1;
1531             }
1532              
1533             else {
1534 0           die "could not parse tablename";
1535             }
1536              
1537             # include alias if it exists
1538 0 0 0       if (m/\G\s*([\d\w\_]+)\s*/scg && lc($1) ne 'on') {
1539 0           $fromSql .= ' '.$1;
1540 0           m/\G\s*on\b/cgi;
1541             }
1542              
1543             # get the whereSql
1544 0 0         if (m/\G\s*\((.*)\)\s*$/cgs) {
1545 0           $whereSql = $1;
1546             }
1547             }
1548              
1549             # define sql & sqlBinds
1550 0           $sql = $fromSql;
1551 0           @sqlBinds = @fromBinds;
1552            
1553             # parse $whereSql to create $join, and @keys
1554 0           foreach my $part (split /\b([\w\d\_]+\.[\w\d\_]+)\b/,$whereSql) {
1555 0 0         if ($part =~ /\b([\w\d\_]+)\.([\w\d\_]+)\b/) {
1556 0           my $dep = $1;
1557 0           my $sql = $2;
1558 0 0         if ($dep eq $joinAlias) {
1559 0           $join .= $part;
1560             } else {
1561 0           push @keys, [$dep, $sql];
1562 0           $join .= '?';
1563             }
1564             } else {
1565 0           $join .= $part;
1566             }
1567             }
1568              
1569             # fill in options
1570 0           $oq->{joins}->{$joinAlias}->[3]->{'new_cursor'} = {
1571             'keys' => \@keys, 'join' => $join, 'sql' => [$sql, @sqlBinds] };
1572              
1573 0           return undef;
1574             }
1575              
1576              
1577              
1578              
1579             # make sure the join counts are the same
1580             # throws exception with error when there is a problem
1581             # this can be an expensive wasteful operation and should not be done in a production env
1582             sub check_join_counts {
1583 0     0 0   my $oq = shift;
1584              
1585             #$$oq{error_handler}->("DEBUG: \$oq->check_join_counts()\n") if $$oq{debug};
1586              
1587              
1588             # since driving table count is computed first this will get set first
1589 0           my $drivingTableCount;
1590              
1591 0           foreach my $join (keys %{ $oq->{joins} }) {
  0            
1592 0           my ($cursors) = @{ $oq->_order_deps($join) };
  0            
1593 0           my @deps = map { @$_ } @$cursors; # flatten deps in cursors
  0            
1594 0           my $drivingTable = $deps[0];
1595              
1596             # now create from clause
1597 0           my ($fromSql, @fromBinds, @whereSql, @whereBinds);
1598 0           foreach my $joinAlias (@deps) {
1599 0           my ($sql, @sqlBinds) = @{ $oq->{joins}->{$joinAlias}->[1] };
  0            
1600              
1601             # if this is the driving table
1602 0 0         if (! $oq->{joins}->{$joinAlias}->[0]) {
    0          
1603             # alias it if not already aliased in sql
1604 0 0         $fromSql .= " $joinAlias" unless $sql =~ /\b$joinAlias\s*$/;
1605             }
1606              
1607             # if NOT sql-92 join
1608             elsif (defined $oq->{joins}->{$joinAlias}->[2]) {
1609 0           $fromSql .= ",\n $sql $joinAlias";
1610 0           push @fromBinds, @sqlBinds;
1611 0           my ($where_sql, @where_sqlBinds) = @{ $oq->{joins}->{$joinAlias}->[2] };
  0            
1612 0           push @whereSql, $where_sql;
1613 0           push @whereBinds, @where_sqlBinds;
1614             }
1615              
1616             # else this is an SQL-92 type join
1617             else {
1618 0           $fromSql .= "\n$sql ";
1619             }
1620             }
1621              
1622 0 0         my $where = 'WHERE '.join("\nAND ", @whereSql) if @whereSql;
1623              
1624 0           my $sql = "
1625             SELECT count(*)
1626             FROM (
1627             SELECT $drivingTable.*
1628             FROM $fromSql
1629             $where
1630             ) OPTIMALQUERYCNTCK ";
1631 0           my @binds = (@fromBinds,@whereBinds);
1632 0           my $count;
1633 0           eval { ($count) = $oq->{dbh}->selectrow_array($sql, undef, @binds); };
  0            
1634 0 0         die "Problem executing ERROR: $@\nSQL: $sql\nBINDS: ".join(',', @binds)."\n" if $@;
1635 0 0         $drivingTableCount = $count unless defined $drivingTableCount;
1636 0 0         confess "BAD_JOIN_COUNT - driving table $drivingTable count ".
1637             "($drivingTableCount) != driving table joined with $join".
1638             " count ($count)" if $count != $drivingTableCount;
1639             }
1640              
1641 0           return undef;
1642             }
1643              
1644              
1645              
1646             =comment
1647             $oq->get_col_type($alias,$context);
1648             =cut
1649             sub type_map {
1650 0     0 0   my $oq = shift;
1651             return {
1652 0           -1 => 'char',
1653             -4 => 'clob',
1654             -5 => 'num',
1655             -6 => 'num',
1656             -9 => 'char',
1657             0 => 'char',
1658             1 => 'char',
1659             3 => 'num', # is decimal type
1660             4 => 'num',
1661             6 => 'num', # float
1662             7 => 'num',
1663             8 => 'num',
1664             9 => 'date',
1665             11 => 'datetime',
1666             10 => 'char',
1667             12 => 'char',
1668             16 => 'date',
1669             30 => 'clob',
1670             40 => 'clob',
1671             91 => 'date',
1672             93 => 'date',
1673             95 => 'date',
1674             'INTEGER' => 'num',
1675             'TEXT' => 'char',
1676             'VARCHAR' => 'char',
1677             'varchar' => 'char'
1678             };
1679             }
1680              
1681             # $type = $oq->get_col_type($alias,$context);
1682             sub get_col_type {
1683 0     0 0   my $oq = shift;
1684 0           my $alias = shift;
1685 0   0       my $context = shift || 'default';
1686             #$$oq{error_handler}->("DEBUG: \$oq->get_col_type($alias, $context)\n") if $$oq{debug};
1687              
1688             return $oq->{'select'}->{$alias}->[3]->{'col_type'} ||
1689 0   0       $oq->get_col_types($context)->{$alias};
1690             }
1691              
1692             #{ ColAlias => Type, .. } = $oq->get_col_types($context)
1693             # where $content in ('default','sort','filter','select')
1694             sub get_col_types {
1695 0     0 0   my $oq = shift;
1696 0   0       my $context = shift || 'default';
1697             #$$oq{error_handler}->("DEBUG: \$oq->get_col_types($context)\n") if $$oq{debug};
1698             return $oq->{'col_types'}->{$context}
1699 0 0         if defined $oq->{'col_types'};
1700              
1701 0           $oq->{'col_types'} = {
1702             'default' => {}, 'sort' => {},
1703             'filter' => {}, 'select' => {} };
1704              
1705 0           my (%deps, @selectColTypeOrder, @selectColAliasOrder, @select, @selectBinds, @where);
1706 0           foreach my $selectAlias (keys %{ $oq->{'select'} } ) {
  0            
1707 0           my $s = $oq->{'select'}->{$selectAlias};
1708              
1709             # did user already define this type?
1710 0 0         if (exists $s->[3]->{'col_type'}) {
1711 0           $oq->{'col_types'}->{'default'}->{$selectAlias} = $s->[3]->{'col_type'};
1712 0           $oq->{'col_types'}->{'select' }->{$selectAlias} = $s->[3]->{'col_type'};
1713 0           $oq->{'col_types'}->{'filter' }->{$selectAlias} = $s->[3]->{'col_type'};
1714 0           $oq->{'col_types'}->{'sort' }->{$selectAlias} = $s->[3]->{'col_type'};
1715             }
1716              
1717             # else write sql to determine type with context
1718             else {
1719 0           $deps{$_} = 1 for @{ $s->[0] };
  0            
1720              
1721 0           foreach my $type (
1722             ['default', $s->[1]],
1723             ['select', $s->[3]->{'select_sql'}],
1724             ['filter', $s->[3]->{'filter_sql'}],
1725             ['sort', $s->[3]->{'sort_sql'}] ) {
1726 0 0         next if ! defined $type->[1];
1727 0           push @selectColTypeOrder, $type->[0];
1728 0           push @selectColAliasOrder, $selectAlias;
1729 0           my ($sql, @binds) = @{ $type->[1] };
  0            
1730 0           push @select, $sql;
1731 0           push @selectBinds, @binds;
1732              
1733             # this next one is needed for oracle so inline views don't get processed
1734             # kinda stupid if you ask me
1735             # don't bother though if there is binds
1736             # this isn't neccessary for mysql since an explicit limit is
1737             # defined latter
1738 0 0 0       if ($$oq{dbtype} eq 'Oracle' && $#binds == -1) {
1739 0           push @where, "to_char($sql) = NULL";
1740             }
1741             }
1742             }
1743             }
1744              
1745             # are there unknown deps?
1746 0 0         if (%deps) {
1747              
1748             # order and flatten deps
1749 0           my @deps = keys %deps;
1750 0           my ($deps) = @{ $oq->_order_deps(\@deps) };
  0            
1751              
1752              
1753 0           @deps = ();
1754 0           push @deps, @$_ for @$deps;
1755              
1756             # now create from clause
1757 0           my ($fromSql, @fromBinds);
1758 0           foreach my $joinAlias (@deps) {
1759 0           my ($sql, @sqlBinds) = @{ $oq->{joins}->{$joinAlias}->[1] };
  0            
1760 0           push @fromBinds, @sqlBinds;
1761              
1762             # if this is the driving table join
1763 0 0         if (! $oq->{joins}->{$joinAlias}->[0]) {
    0          
1764              
1765             # alias it if not already aliased in sql
1766 0           $fromSql .= $sql;
1767 0 0         $fromSql .= " $joinAlias" unless $sql =~ /\b$joinAlias\s*$/;
1768             }
1769              
1770             # if NOT sql-92 join
1771             elsif (defined $oq->{joins}->{$joinAlias}->[2]) {
1772 0           $fromSql .= ",\n $sql $joinAlias";
1773             }
1774              
1775             # else this is an SQL-92 type join
1776             else {
1777 0           $fromSql .= "\n$sql ";
1778             }
1779              
1780             }
1781              
1782 0           my $where;
1783 0 0         $where .= "\nAND " if $#where > -1;
1784 0           $where .= join("\nAND ", @where);
1785              
1786 0           my @binds = (@selectBinds, @fromBinds);
1787 0           my $sql = "
1788             SELECT ".join(',', @select)."
1789             FROM $fromSql";
1790              
1791 0 0 0       if ($$oq{dbtype} eq 'Oracle' || $$oq{dbtype} eq 'Microsoft SQL Server') {
    0          
1792 0           $sql .= "
1793             WHERE 1=2
1794             $where ";
1795             }
1796              
1797             elsif ($$oq{dbtype} eq 'mysql') {
1798 0           $sql .= "
1799             LIMIT 0 ";
1800             }
1801              
1802 0           my $sth;
1803 0           eval {
1804 0           local $oq->{dbh}->{PrintError} = 0;
1805 0           local $oq->{dbh}->{RaiseError} = 1;
1806 0           $sth = $oq->{dbh}->prepare($sql);
1807 0           $sth->execute(@binds);
1808 0 0         }; if ($@) {
1809 0           confess "SQL Error in get_col_types:\n$@\n$sql\n(".join(",",@binds).")";
1810             }
1811              
1812             # read types into col_types cache in object
1813 0           my $type_map = $oq->type_map();
1814 0           for (my $i=0; $i < scalar(@selectColAliasOrder); $i++) {
1815 0           my $name = $selectColAliasOrder[$i];
1816 0           my $type_code = $sth->{TYPE}->[$i];
1817              
1818             # remove parenthesis in type_code from sqlite
1819 0           $type_code =~ s/\([^\)]*\)//;
1820            
1821 0 0         my $type = $type_map->{$type_code} or
1822             die "could not find type code: $type_code for col $name";
1823 0           $oq->{'col_types'}->{$selectColTypeOrder[$i]}->{$name} = $type;
1824              
1825             # set the type for select, filter, and sort to the default
1826             # unless they are already defined
1827 0 0         if ($selectColTypeOrder[$i] eq 'default') {
1828 0   0       $oq->{'col_types'}->{'select' }->{$name} ||= $type;
1829 0   0       $oq->{'col_types'}->{'filter' }->{$name} ||= $type;
1830 0   0       $oq->{'col_types'}->{'sort' }->{$name} ||= $type;
1831             }
1832             }
1833              
1834 0           $sth->finish();
1835             }
1836              
1837 0           return $oq->{'col_types'}->{$context};
1838             }
1839              
1840              
1841              
1842              
1843             # prepare an sth
1844             sub prepare {
1845 0     0 0   my $oq = shift;
1846             #$$oq{error_handler}->("DEBUG: \$oq->prepare(".Dumper(\@_).")\n") if $$oq{debug};
1847 0           return DBIx::OptimalQuery::sth->new($oq,@_);
1848             }
1849              
1850              
1851              
1852             # returns ARRAYREF: [order,idx]
1853             # order is [ [dep1,dep2,dep3], [dep4,dep5,dep6] ], # cursor/dep order
1854             # idx is { dep1 => 0, dep4 => 1, .. etc .. } # index of what cursor dep is in
1855             sub _order_deps {
1856 0     0     my $oq = shift;
1857             #$$oq{error_handler}->("DEBUG: \$oq->_order_deps(".Dumper(\@_).")\n") if $$oq{debug};
1858 0           my $deps = shift;
1859 0 0         $deps = [$deps] unless ref($deps) eq 'ARRAY';
1860              
1861             # @order is an array of array refs. Where each array ref represents deps
1862             # for a separate cursor
1863             # %idx is a hash of scalars where the hash key is the dep name and the
1864             # hash value is what index into order (which cursor number)
1865             # where you find the dep
1866 0           my (@order, %idx);
1867              
1868             # var to detect infinite recursion
1869 0           my $maxRecurse = 1000;
1870              
1871             # recursive function to order deps
1872             # each dep calls this again on all parent deps until all deps are fulfilled
1873             # then the dep is added
1874             # modfies @order & %idx
1875 0           my $place_missing_deps;
1876             $place_missing_deps = sub {
1877 0     0     my $dep = shift;
1878              
1879             # detect infinite recursion
1880 0           $maxRecurse--;
1881 0 0         die "BAD_JOINS - could not link joins to meet all deps" if $maxRecurse == 0;
1882              
1883             # recursion to make sure parent deps are added first
1884 0 0         if (defined $oq->{'joins'}->{$dep}->[0]) {
1885 0           foreach my $parent_dep (@{ $oq->{'joins'}->{$dep}->[0] } ) {
  0            
1886 0 0         $place_missing_deps->($parent_dep) if ! exists $idx{$parent_dep};
1887             }
1888             }
1889              
1890             # at this point all parent deps have been added,
1891             # now add this dep if it has not already been added
1892 0 0         if (! exists $idx{$dep}) {
1893              
1894             # add new cursor if dep is main driving table or has option new_cursor
1895 0 0 0       if (! defined $oq->{'joins'}->{$dep}->[0] ||
1896             exists $oq->{'joins'}->{$dep}->[3]->{new_cursor}) {
1897 0           push @order, [$dep];
1898 0           $idx{$dep} = $#order;
1899             }
1900              
1901             # place dep in @order & %idx
1902             # uses the same cursor as its parent dep
1903             # this is found by looking at the parent_idx
1904             else {
1905 0   0       my $parent_idx = $idx{$oq->{'joins'}->{$dep}->[0]->[0]} || 0;
1906 0           push @{ $order[ $parent_idx ] }, $dep;
  0            
1907 0           $idx{$dep} = $parent_idx;
1908             }
1909             }
1910 0           return undef;
1911 0           };
1912              
1913 0           $place_missing_deps->($_) for @$deps;
1914              
1915 0           return [\@order, \%idx];
1916             }
1917              
1918              
1919             1;