]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Reporter/SQLBuilder.pm
completing support for nullable joins
[Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Reporter / SQLBuilder.pm
1 #-------------------------------------------------------------------------------------------------
2 package OpenILS::Reporter::SQLBuilder;
3
4 sub new {
5         my $class = shift;
6         $class = ref($class) || $class;
7
8         return bless { _sql => undef } => $class;
9 }
10
11 sub register_params {
12         my $self  = shift;
13         my $p = shift;
14         $self->{_params} = $p;
15 }
16
17 sub get_param {
18         my $self = shift;
19         my $p = shift;
20         return $self->{_builder}->{_params}->{$p};
21 }
22
23 sub set_builder {
24         my $self = shift;
25         $self->{_builder} = shift;
26         return $self;
27 }
28
29 sub builder {
30         my $self = shift;
31         return $self->{_builder};
32 }
33
34 sub relative_time {
35         my $self = shift;
36         my $t = shift;
37         $self->builder->{_relative_time} = $t if (defined $t);
38         return $self->builder->{_relative_time};
39 }
40
41 sub resolve_param {
42         my $self = shift;
43         my $val = shift;
44
45         if (defined($val) && $val =~ /^::(.+)$/o) {
46                 $val = $self->get_param($1);
47         }
48
49         if (defined($val) && !ref($val)) {
50                 $val =~ s/\\/\\\\/go;
51                 $val =~ s/"/\\"/go;
52         }
53
54         return $val;
55 }
56
57 sub parse_report {
58         my $self = shift;
59         my $report = shift;
60
61         my $rs = OpenILS::Reporter::SQLBuilder::ResultSet->new;
62
63         if (!$report->{order_by} || @{$report->{order_by}} == 0) {
64                 $report->{order_by} = $report->{select};
65         }
66
67         $rs->is_subquery( 1 ) if ( $report->{alias} );
68
69         $rs     ->set_builder( $self )
70                 ->set_subquery_alias( $report->{alias} )
71                 ->set_select( $report->{select} )
72                 ->set_from( $report->{from} )
73                 ->set_where( $report->{where} )
74                 ->set_having( $report->{having} )
75                 ->set_order_by( $report->{order_by} )
76                 ->set_pivot_data( $report->{pivot_data} )
77                 ->set_pivot_label( $report->{pivot_label} )
78                 ->set_pivot_default( $report->{pivot_default} );
79
80         return $rs;
81 }
82
83
84 #-------------------------------------------------------------------------------------------------
85 package OpenILS::Reporter::SQLBuilder::ResultSet;
86 use base qw/OpenILS::Reporter::SQLBuilder/;
87
88 sub is_subquery {
89         my $self = shift;
90         my $flag = shift;
91         $self->{_is_subquery} = $flag if (defined $flag);
92         return $self->{_is_subquery};
93 }
94
95 sub pivot_data {
96         my $self = shift;
97         return $self->builder->{_pivot_data};
98 }
99
100 sub pivot_label {
101         my $self = shift;
102         return $self->builder->{_pivot_label};
103 }
104
105 sub pivot_default {
106         my $self = shift;
107         return $self->builder->{_pivot_default};
108 }
109
110 sub set_pivot_default {
111         my $self = shift;
112         my $p = shift;
113         $self->builder->{_pivot_default} = $p if (defined $p);
114         return $self;
115 }
116
117 sub set_pivot_data {
118         my $self = shift;
119         my $p = shift;
120         $self->builder->{_pivot_data} = $p if (defined $p);
121         return $self;
122 }
123
124 sub set_pivot_label {
125         my $self = shift;
126         my $p = shift;
127         $self->builder->{_pivot_label} = $p if (defined $p);
128         return $self;
129 }
130
131 sub set_subquery_alias {
132         my $self = shift;
133         my $alias = shift;
134         $self->{_alias} = $alias if (defined $alias);
135         return $self;
136 }
137
138 sub set_select {
139         my $self = shift;
140         my @cols = @_;
141
142         $self->{_select} = [];
143
144         return $self unless (@cols && defined($cols[0]));
145         @cols = @{ $cols[0] } if (@cols == 1 && ref($cols[0]) eq 'ARRAY');
146
147         push @{ $self->{_select} }, map { OpenILS::Reporter::SQLBuilder::Column::Select->new( $_ )->set_builder( $self->builder ) } @cols;
148
149         return $self;
150 }
151
152 sub set_from {
153         my $self = shift;
154         my $f = shift;
155
156         $self->{_from} = OpenILS::Reporter::SQLBuilder::Relation->parse( $f, $self );
157
158         return $self;
159 }
160
161 sub set_where {
162         my $self = shift;
163         my @cols = @_;
164
165         $self->{_where} = [];
166
167         return $self unless (@cols && defined($cols[0]));
168         @cols = @{ $cols[0] } if (@cols == 1 && ref($cols[0]) eq 'ARRAY');
169
170         push @{ $self->{_where} }, map { OpenILS::Reporter::SQLBuilder::Column::Where->new( $_, $self->{_from}->builder->{_rels} )->set_builder( $self->builder ) } @cols;
171
172         return $self;
173 }
174
175 sub set_having {
176         my $self = shift;
177         my @cols = @_;
178
179         $self->{_having} = [];
180
181         return $self unless (@cols && defined($cols[0]));
182         @cols = @{ $cols[0] } if (@cols == 1 && ref($cols[0]) eq 'ARRAY');
183
184         push @{ $self->{_having} }, map { OpenILS::Reporter::SQLBuilder::Column::Having->new( $_ )->set_builder( $self->builder ) } @cols;
185
186         return $self;
187 }
188
189 sub set_order_by {
190         my $self = shift;
191         my @cols = @_;
192
193         $self->{_order_by} = [];
194
195         return $self unless (@cols && defined($cols[0]));
196         @cols = @{ $cols[0] } if (@cols == 1 && ref($cols[0]) eq 'ARRAY');
197
198         push @{ $self->{_order_by} }, map { OpenILS::Reporter::SQLBuilder::Column::OrderBy->new( $_ )->set_builder( $self->builder ) } @cols;
199
200         return $self;
201 }
202
203 sub column_label_list {
204         my $self = shift;
205
206         my @labels;
207         push @labels, $self->resolve_param( $_->{_alias} ) for ( @{ $self->{_select} } );
208         return @labels;
209 }
210
211 sub group_by_list {
212         my $self = shift;
213         my $base = shift;
214         $base = 1 unless (defined $base);
215
216         my $seen_label = 0;
217         my $gcount = $base;
218         my @group_by;
219         for my $c ( @{ $self->{_select} } ) {
220                 if ($base == 0 && !$seen_label  && defined($self->pivot_label) && $gcount == $self->pivot_label - 1) {
221                         $seen_label++;
222                         next;
223                 }
224                 push @group_by, $gcount if (!$c->is_aggregate);
225                 $gcount++;
226         }
227
228         return @group_by;
229 }
230
231 sub toSQL {
232         my $self = shift;
233
234         return $self->{_sql} if ($self->{_sql});
235
236         my $sql = '';
237
238         if ($self->is_subquery) {
239                 $sql = '(';
240         }
241
242         $sql .= "SELECT\t" . join(",\n\t", map { $_->toSQL } @{ $self->{_select} }) . "\n" if (@{ $self->{_select} });
243         $sql .= "  FROM\t" . $self->{_from}->toSQL . "\n" if ($self->{_from});
244         $sql .= "  WHERE\t" . join("\n\tAND ", map { $_->toSQL } @{ $self->{_where} }) . "\n" if (@{ $self->{_where} });
245
246         my @group_by = $self->group_by_list;
247
248         $sql .= '  GROUP BY ' . join(', ', @group_by) . "\n" if (@group_by);
249         $sql .= "  HAVING " . join("\n\tAND ", map { $_->toSQL } @{ $self->{_having} }) . "\n" if (@{ $self->{_having} });
250         $sql .= '  ORDER BY ' . join(', ', map { $_->toSQL } @{ $self->{_order_by} }) . "\n" if (@{ $self->{_order_by} });
251
252         if ($self->is_subquery) {
253                 $sql .= ') '. $self->{_alias} . "\n";
254         }
255
256         return $self->{_sql} = $sql;
257 }
258
259
260 #-------------------------------------------------------------------------------------------------
261 package OpenILS::Reporter::SQLBuilder::Input;
262 use base qw/OpenILS::Reporter::SQLBuilder/;
263
264 sub new {
265         my $class = shift;
266         my $self = $class->SUPER::new;
267
268         my $col_data = shift;
269
270         if (ref($col_data)) {
271                 $self->{params} = $col_data->{params};
272                 my $trans = $col_data->{transform} || 'Bare';
273                 my $pkg = "OpenILS::Reporter::SQLBuilder::Input::Transform::$trans";
274                 if (UNIVERSAL::can($pkg => 'toSQL')) {
275                         $self->{_transform} = $trans;
276                 } else {
277                         $self->{_transform} = 'GenericTransform';
278                 }
279         } elsif( defined($col_data) ) {
280                 $self->{_transform} = 'Bare';
281                 $self->{params} = $col_data;
282         } else {
283                 $self->{_transform} = 'NULL';
284         }
285
286
287
288         return $self;
289 }
290
291 sub toSQL {
292         my $self = shift;
293         my $type = $self->{_transform};
294         return $self->{_sql} if ($self->{_sql});
295         my $toSQL = "OpenILS::Reporter::SQLBuilder::Input::Transform::${type}::toSQL";
296         return $self->{_sql} = $self->$toSQL;
297 }
298
299
300 #-------------------------------------------------------------------------------------------------
301 package OpenILS::Reporter::SQLBuilder::Input::Transform::NULL;
302
303 sub toSQL {
304         return "NULL";
305 }
306
307
308 #-------------------------------------------------------------------------------------------------
309 package OpenILS::Reporter::SQLBuilder::Input::Transform::Bare;
310
311 sub toSQL {
312         my $self = shift;
313
314         my $val = $self->{params};
315         $val = $$val[0] if (ref($val));
316         
317         $val =~ s/\\/\\\\/go;
318         $val =~ s/'/\\'/go;
319
320         return "'$val'";
321 }
322
323
324 #-------------------------------------------------------------------------------------------------
325 package OpenILS::Reporter::SQLBuilder::Input::Transform::age;
326
327 sub toSQL {
328         my $self = shift;
329
330         my $val = $self->{params};
331         $val = $$val[0] if (ref($val));
332
333         $val =~ s/\\/\\\\/go;
334         $val =~ s/'/\\'/go;
335
336         return "AGE(NOW(),'" . $val . "'::TIMESTAMPTZ)";
337 }
338
339 sub is_aggregate { return 0 }
340
341
342 #-------------------------------------------------------------------------------------------------
343 package OpenILS::Reporter::SQLBuilder::Input::Transform::relative_year;
344
345 sub toSQL {
346         my $self = shift;
347
348         my $rtime = $self->relative_time || 'now';
349
350         $rtime =~ s/\\/\\\\/go;
351         $rtime =~ s/'/\\'/go;
352
353         my $val = $self->{params};
354         $val = $$val[0] if (ref($val));
355
356         $val =~ s/\\/\\\\/go;
357         $val =~ s/'/\\'/go;
358
359         return "EXTRACT(YEAR FROM '$rtime'::TIMESTAMPTZ + '$val years')";
360 }
361
362
363 #-------------------------------------------------------------------------------------------------
364 package OpenILS::Reporter::SQLBuilder::Input::Transform::relative_month;
365
366 sub toSQL {
367         my $self = shift;
368
369         my $rtime = $self->relative_time || 'now';
370
371         $rtime =~ s/\\/\\\\/go;
372         $rtime =~ s/'/\\'/go;
373
374         my $val = $self->{params};
375         $val = $$val[0] if (ref($val));
376
377         $val =~ s/\\/\\\\/go;
378         $val =~ s/'/\\'/go;
379
380         return "EXTRACT(YEAR FROM '$rtime'::TIMESTAMPTZ + '$val months')" .
381                 " || '-' || LPAD(EXTRACT(MONTH FROM '$rtime'::TIMESTAMPTZ + '$val months'),2,'0')";
382 }
383
384
385 #-------------------------------------------------------------------------------------------------
386 package OpenILS::Reporter::SQLBuilder::Input::Transform::relative_date;
387
388 sub toSQL {
389         my $self = shift;
390
391         my $rtime = $self->relative_time || 'now';
392
393         $rtime =~ s/\\/\\\\/go;
394         $rtime =~ s/'/\\'/go;
395
396         my $val = $self->{params};
397         $val = $$val[0] if (ref($val));
398
399         $val =~ s/\\/\\\\/go;
400         $val =~ s/'/\\'/go;
401
402         return "DATE('$rtime'::TIMESTAMPTZ + '$val days')";
403 }
404
405
406 #-------------------------------------------------------------------------------------------------
407 package OpenILS::Reporter::SQLBuilder::Input::Transform::relative_week;
408
409 sub toSQL {
410         my $self = shift;
411
412         my $rtime = $self->relative_time || 'now';
413
414         $rtime =~ s/\\/\\\\/go;
415         $rtime =~ s/'/\\'/go;
416
417         my $val = $self->{params};
418         $val = $$val[0] if (ref($val));
419
420         $val =~ s/\\/\\\\/go;
421         $val =~ s/'/\\'/go;
422
423         return "EXTRACT(WEEK FROM '$rtime'::TIMESTAMPTZ + '$val weeks')";
424 }
425
426
427 #-------------------------------------------------------------------------------------------------
428 package OpenILS::Reporter::SQLBuilder::Column;
429 use base qw/OpenILS::Reporter::SQLBuilder/;
430
431 sub new {
432         my $class = shift;
433         my $self = $class->SUPER::new;
434
435         my $col_data = shift;
436         $self->{_relation} = $col_data->{relation};
437         $self->{_column} = $col_data->{column};
438
439         $self->{_aggregate} = $col_data->{aggregate};
440
441         $self->{_rels} = shift;
442
443         if (ref($self->{_column})) {
444                 my $trans = $self->{_column}->{transform} || 'Bare';
445                 my $pkg = "OpenILS::Reporter::SQLBuilder::Column::Transform::$trans";
446                 if (UNIVERSAL::can($pkg => 'toSQL')) {
447                         $self->{_transform} = $trans;
448                 } else {
449                         $self->{_transform} = 'GenericTransform';
450                 }
451         } elsif( defined($self->{_column}) ) {
452                 $self->{_transform} = 'Bare';
453         } else {
454                 $self->{_transform} = 'NULL';
455         }
456
457
458         return $self;
459 }
460
461 sub find_relation {
462         my $self = shift;
463         return $self->{_rels}->{$self->{_relation}};
464 }
465
466 sub name {
467         my $self = shift;
468         if (ref($self->{_column})) {
469                  return $self->{_column}->{colname};
470         } else {
471                 return $self->{_column};
472         }
473 }
474
475 sub toSQL {
476         my $self = shift;
477         my $type = $self->{_transform};
478         return $self->{_sql} if ($self->{_sql});
479         my $toSQL = "OpenILS::Reporter::SQLBuilder::Column::Transform::${type}::toSQL";
480         return $self->{_sql} = $self->$toSQL;
481 }
482
483 sub is_aggregate {
484         my $self = shift;
485         my $type = $self->{_transform};
486         my $is_agg = "OpenILS::Reporter::SQLBuilder::Column::Transform::${type}::is_aggregate";
487         return $self->$is_agg;
488 }
489
490
491 #-------------------------------------------------------------------------------------------------
492 package OpenILS::Reporter::SQLBuilder::Column::OrderBy;
493 use base qw/OpenILS::Reporter::SQLBuilder::Column/;
494
495 sub new {
496         my $class = shift;
497         my $self = $class->SUPER::new(@_);
498
499         my $col_data = shift;
500         $self->{_direction} = $col_data->{direction} || 'ascending';
501         return $self;
502 }
503
504 sub toSQL {
505         my $self = shift;
506         my $dir = ($self->{_direction} =~ /^d/oi) ? 'DESC' : 'ASC';
507         return $self->{_sql} if ($self->{_sql});
508         return $self->{_sql} = $self->SUPER::toSQL .  " $dir";
509 }
510
511
512 #-------------------------------------------------------------------------------------------------
513 package OpenILS::Reporter::SQLBuilder::Column::Select;
514 use base qw/OpenILS::Reporter::SQLBuilder::Column/;
515
516 sub new {
517         my $class = shift;
518         my $self = $class->SUPER::new(@_);
519
520         my $col_data = shift;
521         $self->{_alias} = $col_data->{alias} || $self->name;
522         return $self;
523 }
524
525 sub toSQL {
526         my $self = shift;
527         return $self->{_sql} if ($self->{_sql});
528         return $self->{_sql} = $self->SUPER::toSQL .  ' AS "' . $self->resolve_param( $self->{_alias} ) . '"';
529 }
530
531
532 #-------------------------------------------------------------------------------------------------
533 package OpenILS::Reporter::SQLBuilder::Column::Transform::GenericTransform;
534
535 sub toSQL {
536         my $self = shift;
537         my $name = $self->name;
538         my $func = $self->{_column}->{transform};
539
540         my @params;
541         @params = @{ $self->resolve_param( $self->{_column}->{params} ) } if ($self->{_column}->{params});
542
543         my $sql = $func . '("' . $self->{_relation} . '"."' . $self->name . '"';
544         $sql .= ",'" . join("','", @params) . "'" if (@params);
545         $sql .= ')';
546
547         return $sql;
548 }
549
550 sub is_aggregate { return $self->{_aggregate} }
551
552 #-------------------------------------------------------------------------------------------------
553 package OpenILS::Reporter::SQLBuilder::Column::Transform::Bare;
554
555 sub toSQL {
556         my $self = shift;
557         return '"' . $self->{_relation} . '"."' . $self->name . '"';
558 }
559
560 sub is_aggregate { return 0 }
561
562 #-------------------------------------------------------------------------------------------------
563 package OpenILS::Reporter::SQLBuilder::Column::Transform::upper;
564
565 sub toSQL {
566         my $self = shift;
567         my $params = $self->resolve_param( $self->{_column}->{params} );
568         my $start = $$params[0];
569         my $len = $$params[1];
570         return 'UPPER("' . $self->{_relation} . '"."' . $self->name . '")';
571 }
572
573 sub is_aggregate { return 0 }
574
575
576 #-------------------------------------------------------------------------------------------------
577 package OpenILS::Reporter::SQLBuilder::Column::Transform::lower;
578
579 sub toSQL {
580         my $self = shift;
581         my $params = $self->resolve_param( $self->{_column}->{params} );
582         my $start = $$params[0];
583         my $len = $$params[1];
584         return 'LOWER("' . $self->{_relation} . '"."' . $self->name . '")';
585 }
586
587 sub is_aggregate { return 0 }
588
589
590 #-------------------------------------------------------------------------------------------------
591 package OpenILS::Reporter::SQLBuilder::Column::Transform::substring;
592
593 sub toSQL {
594         my $self = shift;
595         my $params = $self->resolve_param( $self->{_column}->{params} );
596         my $start = $$params[0];
597         my $len = $$params[1];
598         return 'SUBSTRING("' . $self->{_relation} . '"."' . $self->name . "\",$start,$len)";
599 }
600
601 sub is_aggregate { return 0 }
602
603
604 #-------------------------------------------------------------------------------------------------
605 package OpenILS::Reporter::SQLBuilder::Column::Transform::day_name;
606
607 sub toSQL {
608         my $self = shift;
609         return 'TO_CHAR("' . $self->{_relation} . '"."' . $self->name . '", \'Day\')';
610 }
611
612 sub is_aggregate { return 0 }
613
614
615 #-------------------------------------------------------------------------------------------------
616 package OpenILS::Reporter::SQLBuilder::Column::Transform::month_name;
617
618 sub toSQL {
619         my $self = shift;
620         return 'TO_CHAR("' . $self->{_relation} . '"."' . $self->name . '", \'Month\')';
621 }
622
623 sub is_aggregate { return 0 }
624
625
626 #-------------------------------------------------------------------------------------------------
627 package OpenILS::Reporter::SQLBuilder::Column::Transform::doy;
628
629 sub toSQL {
630         my $self = shift;
631         return 'EXTRACT(DOY FROM "' . $self->{_relation} . '"."' . $self->name . '")';
632 }
633
634 sub is_aggregate { return 0 }
635
636
637 #-------------------------------------------------------------------------------------------------
638 package OpenILS::Reporter::SQLBuilder::Column::Transform::woy;
639
640 sub toSQL {
641         my $self = shift;
642         return 'EXTRACT(WEEK FROM "' . $self->{_relation} . '"."' . $self->name . '")';
643 }
644
645 sub is_aggregate { return 0 }
646
647
648 #-------------------------------------------------------------------------------------------------
649 package OpenILS::Reporter::SQLBuilder::Column::Transform::moy;
650
651 sub toSQL {
652         my $self = shift;
653         return 'EXTRACT(MONTH FROM "' . $self->{_relation} . '"."' . $self->name . '")';
654 }
655
656 sub is_aggregate { return 0 }
657
658
659 #-------------------------------------------------------------------------------------------------
660 package OpenILS::Reporter::SQLBuilder::Column::Transform::qoy;
661
662 sub toSQL {
663         my $self = shift;
664         return 'EXTRACT(QUARTER FROM "' . $self->{_relation} . '"."' . $self->name . '")';
665 }
666
667 sub is_aggregate { return 0 }
668
669
670 #-------------------------------------------------------------------------------------------------
671 package OpenILS::Reporter::SQLBuilder::Column::Transform::dom;
672
673 sub toSQL {
674         my $self = shift;
675         return 'EXTRACT(DAY FROM "' . $self->{_relation} . '"."' . $self->name . '")';
676 }
677
678 sub is_aggregate { return 0 }
679
680
681 #-------------------------------------------------------------------------------------------------
682 package OpenILS::Reporter::SQLBuilder::Column::Transform::dow;
683
684 sub toSQL {
685         my $self = shift;
686         return 'EXTRACT(DOW FROM "' . $self->{_relation} . '"."' . $self->name . '")';
687 }
688
689 sub is_aggregate { return 0 }
690
691
692 #-------------------------------------------------------------------------------------------------
693 package OpenILS::Reporter::SQLBuilder::Column::Transform::year_trunc;
694
695 sub toSQL {
696         my $self = shift;
697         return 'EXTRACT(YEAR FROM "' . $self->{_relation} . '"."' . $self->name . '")';
698 }
699
700 sub is_aggregate { return 0 }
701
702
703 #-------------------------------------------------------------------------------------------------
704 package OpenILS::Reporter::SQLBuilder::Column::Transform::month_trunc;
705
706 sub toSQL {
707         my $self = shift;
708         return 'EXTRACT(YEAR FROM "' . $self->{_relation} . '"."' . $self->name . '")' .
709                 ' || \'-\' || LPAD(EXTRACT(MONTH FROM "' . $self->{_relation} . '"."' . $self->name . '"),2,\'0\')';
710 }
711
712 sub is_aggregate { return 0 }
713
714
715 #-------------------------------------------------------------------------------------------------
716 package OpenILS::Reporter::SQLBuilder::Column::Transform::date_trunc;
717
718 sub toSQL {
719         my $self = shift;
720         return 'DATE("' . $self->{_relation} . '"."' . $self->name . '")';
721 }
722
723 sub is_aggregate { return 0 }
724
725
726 #-------------------------------------------------------------------------------------------------
727 package OpenILS::Reporter::SQLBuilder::Column::Transform::hour_trunc;
728
729 sub toSQL {
730         my $self = shift;
731         return 'DATE_TRUNC("' . $self->{_relation} . '"."' . $self->name . '")';
732 }
733
734 sub is_aggregate { return 0 }
735
736
737 #-------------------------------------------------------------------------------------------------
738 package OpenILS::Reporter::SQLBuilder::Column::Transform::quarter;
739
740 sub toSQL {
741         my $self = shift;
742         return 'EXTRACT(YEAR FROM "' . $self->{_relation} . '"."' . $self->name . '")' .
743                 ' || \'-Q\' || EXTRACT(QUARTER FROM "' . $self->{_relation} . '"."' . $self->name . '")';
744 }
745
746 sub is_aggregate { return 0 }
747
748
749 #-------------------------------------------------------------------------------------------------
750 package OpenILS::Reporter::SQLBuilder::Column::Transform::months_ago;
751
752 sub toSQL {
753         my $self = shift;
754         return 'EXTRACT(MONTH FROM AGE(NOW(),"' . $self->{_relation} . '"."' . $self->name . '"))';
755 }
756
757 sub is_aggregate { return 0 }
758
759
760 #-------------------------------------------------------------------------------------------------
761 package OpenILS::Reporter::SQLBuilder::Column::Transform::hod;
762
763 sub toSQL {
764         my $self = shift;
765         return 'EXTRACT(HOUR FROM "' . $self->{_relation} . '"."' . $self->name . '")';
766 }
767
768 sub is_aggregate { return 0 }
769
770
771 #-------------------------------------------------------------------------------------------------
772 package OpenILS::Reporter::SQLBuilder::Column::Transform::quarters_ago;
773
774 sub toSQL {
775         my $self = shift;
776         return 'EXTRACT(QUARTER FROM AGE(NOW(),"' . $self->{_relation} . '"."' . $self->name . '"))';
777 }
778
779 sub is_aggregate { return 0 }
780
781
782 #-------------------------------------------------------------------------------------------------
783 package OpenILS::Reporter::SQLBuilder::Column::Transform::age;
784
785 sub toSQL {
786         my $self = shift;
787         return 'AGE(NOW(),"' . $self->{_relation} . '"."' . $self->name . '")';
788 }
789
790 sub is_aggregate { return 0 }
791
792
793 #-------------------------------------------------------------------------------------------------
794 package OpenILS::Reporter::SQLBuilder::Column::Transform::first;
795
796 sub toSQL {
797         my $self = shift;
798         return 'FIRST("' . $self->{_relation} . '"."' . $self->name . '")';
799 }
800
801 sub is_aggregate { return 1 }
802
803
804 #-------------------------------------------------------------------------------------------------
805 package OpenILS::Reporter::SQLBuilder::Column::Transform::last;
806
807 sub toSQL {
808         my $self = shift;
809         return 'LAST("' . $self->{_relation} . '"."' . $self->name . '")';
810 }
811
812 sub is_aggregate { return 1 }
813
814
815 #-------------------------------------------------------------------------------------------------
816 package OpenILS::Reporter::SQLBuilder::Column::Transform::min;
817
818 sub toSQL {
819         my $self = shift;
820         return 'MIN("' . $self->{_relation} . '"."' . $self->name . '")';
821 }
822
823 sub is_aggregate { return 1 }
824
825
826 #-------------------------------------------------------------------------------------------------
827 package OpenILS::Reporter::SQLBuilder::Column::Transform::max;
828
829 sub toSQL {
830         my $self = shift;
831         return 'MAX("' . $self->{_relation} . '"."' . $self->name . '")';
832 }
833
834 sub is_aggregate { return 1 }
835
836
837 #-------------------------------------------------------------------------------------------------
838 package OpenILS::Reporter::SQLBuilder::Column::Transform::count;
839
840 sub toSQL {
841         my $self = shift;
842         return 'COUNT("' . $self->{_relation} . '"."' . $self->name . '")';
843 }
844
845 sub is_aggregate { return 1 }
846
847
848 #-------------------------------------------------------------------------------------------------
849 package OpenILS::Reporter::SQLBuilder::Column::Transform::count_distinct;
850
851 sub toSQL {
852         my $self = shift;
853         return 'COUNT(DISTINCT "' . $self->{_relation} . '"."' . $self->name . '")';
854 }
855
856 sub is_aggregate { return 1 }
857
858
859 #-------------------------------------------------------------------------------------------------
860 package OpenILS::Reporter::SQLBuilder::Column::Transform::sum;
861
862 sub toSQL {
863         my $self = shift;
864         return 'SUM("' . $self->{_relation} . '"."' . $self->name . '")';
865 }
866
867 sub is_aggregate { return 1 }
868
869
870 #-------------------------------------------------------------------------------------------------
871 package OpenILS::Reporter::SQLBuilder::Column::Transform::average;
872
873 sub toSQL {
874         my $self = shift;
875         return 'AVG("' . $self->{_relation} . '"."' . $self->name .  '")';
876 }
877
878 sub is_aggregate { return 1 }
879
880
881 #-------------------------------------------------------------------------------------------------
882 package OpenILS::Reporter::SQLBuilder::Column::Where;
883 use base qw/OpenILS::Reporter::SQLBuilder::Column/;
884
885 sub new {
886         my $class = shift;
887         my $self = $class->SUPER::new(@_);
888
889         my $col_data = shift;
890         $self->{_condition} = $col_data->{condition};
891
892         return $self;
893 }
894
895 sub _flesh_conditions {
896         my $cond = shift;
897         my $builder = shift;
898         $cond = [$cond] unless (ref($cond) eq 'ARRAY');
899
900         my @out;
901         for my $c (@$cond) {
902                 push @out, OpenILS::Reporter::SQLBuilder::Input->new( $c )->set_builder( $builder );
903         }
904
905         return \@out;
906 }
907
908 sub toSQL {
909         my $self = shift;
910
911         return $self->{_sql} if ($self->{_sql});
912
913         my $sql;
914
915         my $rel = $self->find_relation();
916         if ($rel && $rel->is_nullable) {
917                 $sql = "(". $self->SUPER::toSQL ." IS NULL OR ";
918         }
919
920         $sql .= $self->SUPER::toSQL;
921
922         my ($op) = keys %{ $self->{_condition} };
923         my $val = _flesh_conditions( $self->resolve_param( $self->{_condition}->{$op} ), $self->builder );
924
925         if (lc($op) eq 'in') {
926                 $sql .= " IN (". join(",", map { $_->toSQL } @$val).")";
927         } elsif (lc($op) eq 'not in') {
928                 $sql .= " NOT IN (". join(",", map { $_->toSQL } @$val).")";
929         } elsif (lc($op) eq 'between') {
930                 $sql .= " BETWEEN ". join(" AND ", map { $_->toSQL } @$val);
931         } elsif (lc($op) eq 'not between') {
932                 $sql .= " NOT BETWEEN ". join(" AND ", map { $_->toSQL } @$val);
933         } elsif (lc($op) eq 'like') {
934                 $val = $$val[0] if (ref($val) eq 'ARRAY');
935                 $val =~ s/^'(.*)'$/$1/o;
936                 $val =~ s/%/\\\\%/o;
937                 $val =~ s/_/\\\\_/o;
938                 $sql .= " LIKE '\%$val\%'";
939         } elsif (lc($op) eq 'ilike') {
940                 $val = $$val[0] if (ref($val) eq 'ARRAY');
941                 $val =~ s/^'(.*)'$/$1/o;
942                 $val =~ s/%/\\\\%/o;
943                 $val =~ s/_/\\\\_/o;
944                 $sql .= " ILIKE '\%$val\%'";
945         } else {
946                 $val = $$val[0] if (ref($val) eq 'ARRAY');
947                 $sql .= " $op " . $val->toSQL;
948         }
949
950         if ($rel && $rel->is_nullable) {
951                 $sql .= ")";
952         }
953
954         return $self->{_sql} = $sql;
955 }
956
957
958 #-------------------------------------------------------------------------------------------------
959 package OpenILS::Reporter::SQLBuilder::Column::Having;
960 use base qw/OpenILS::Reporter::SQLBuilder::Column::Where/;
961
962 #-------------------------------------------------------------------------------------------------
963 package OpenILS::Reporter::SQLBuilder::Relation;
964 use base qw/OpenILS::Reporter::SQLBuilder/;
965
966 sub parse {
967         my $self = shift;
968         $self = $self->SUPER::new if (!ref($self));
969
970         my $rel_data = shift;
971         my $b = shift;
972         $self->set_builder($b);
973
974         $self->{_table} = $rel_data->{table};
975         $self->{_alias} = $rel_data->{alias} || $self->name;
976         $self->{_join} = [];
977         $self->{_columns} = [];
978
979         $self->builder->{_rels}{$self->{_alias}} = $self;
980
981         if ($rel_data->{join}) {
982                 $self->add_join(
983                         $_ => OpenILS::Reporter::SQLBuilder::Relation->parse( $rel_data->{join}->{$_}, $b ) => $rel_data->{join}->{$_}->{key} => $rel_data->{join}->{$_}->{type}
984                 ) for ( keys %{ $rel_data->{join} } );
985         }
986
987         return $self;
988 }
989
990 sub add_column {
991         my $self = shift;
992         my $col = shift;
993         
994         push @{ $self->{_columns} }, $col;
995 }
996
997 sub find_column {
998         my $self = shift;
999         my $col = shift;
1000         return (grep { $_->name eq $col} @{ $self->{_columns} })[0];
1001 }
1002
1003 sub add_join {
1004         my $self = shift;
1005         my $col = shift;
1006         my $frel = shift;
1007         my $fkey = shift;
1008         my $type = lc(shift()) || 'inner';
1009
1010         if (UNIVERSAL::isa($col,'OpenILS::Reporter::SQLBuilder::Join')) {
1011                 push @{ $self->{_join} }, $col;
1012         } else {
1013                 push @{ $self->{_join} }, OpenILS::Reporter::SQLBuilder::Join->build( $self => $col, $frel => $fkey, $type );
1014         }
1015
1016         return $self;
1017 }
1018
1019 sub is_nullable {
1020         my $self = shift;
1021         return $self->{_nullable};
1022 }
1023
1024 sub is_join {
1025         my $self = shift;
1026         my $j = shift;
1027         $self->{_is_join} = $j if ($j);
1028         return $self->{_is_join};
1029 }
1030
1031 sub join_type {
1032         my $self = shift;
1033         my $j = shift;
1034         $self->{_join_type} = $j if ($j);
1035         return $self->{_join_type};
1036 }
1037
1038 sub toSQL {
1039         my $self = shift;
1040         return $self->{_sql} if ($self->{_sql});
1041
1042         my $sql = $self->{_table} .' AS "'. $self->{_alias} .'"';
1043
1044         if (!$self->is_join) {
1045                 for my $j ( @{ $self->{_join} } ) {
1046                         $sql .= $j->toSQL;
1047                 }
1048         }
1049
1050         return $self->{_sql} = $sql;
1051 }
1052
1053 #-------------------------------------------------------------------------------------------------
1054 package OpenILS::Reporter::SQLBuilder::Join;
1055 use base qw/OpenILS::Reporter::SQLBuilder/;
1056
1057 sub build {
1058         my $class = shift;
1059         my $self = $class->SUPER::new if (!ref($class));
1060
1061         $self->{_left_rel} = shift;
1062         ($self->{_left_col}) = split(/-/,shift());
1063
1064         $self->{_right_rel} = shift;
1065         $self->{_right_col} = shift;
1066
1067         $self->{_join_type} = shift;
1068
1069         $self->{_right_rel}->set_builder($self->{_left_rel}->builder);
1070
1071         $self->{_right_rel}->is_join(1);
1072         $self->{_right_rel}->join_type($self->{_join_type});
1073
1074         bless $self => "OpenILS::Reporter::SQLBuilder::Join::$self->{_join_type}";
1075
1076         return $self;
1077 }
1078
1079 sub toSQL {
1080         my $self = shift;
1081         my $dir = shift;
1082
1083         my $sql = "JOIN " . $self->{_right_rel}->toSQL .
1084                 ' ON ("' . $self->{_left_rel}->{_alias} . '"."' . $self->{_left_col} .
1085                 '" = "' . $self->{_right_rel}->{_alias} . '"."' . $self->{_right_col} . '")';
1086
1087         $sql .= $_->toSQL($dir) for (@{ $self->{_right_rel}->{_join} });
1088
1089         return $sql;
1090 }
1091
1092 #-------------------------------------------------------------------------------------------------
1093 package OpenILS::Reporter::SQLBuilder::Join::left;
1094 use base qw/OpenILS::Reporter::SQLBuilder::Join/;
1095
1096 sub toSQL {
1097         my $self = shift;
1098         my $dir = shift;
1099         #return $self->{_sql} if ($self->{_sql});
1100
1101         my $_nullable_rel = $dir && $dir eq 'r' ? '_left_rel' : '_right_rel';
1102         $self->{_right_rel}->{_nullable} = 'l';
1103         $self->{$_nullable_rel}->{_nullable} = $dir;
1104
1105         my $j = $dir && $dir eq 'r' ? 'FULL OUTER' : 'LEFT OUTER';
1106
1107         my $sql = "\n\t$j ". $self->SUPER::toSQL('l');
1108
1109         #$sql .= $_->toSQL for (@{ $self->{_right_rel}->{_join} });
1110
1111         return $self->{_sql} = $sql;
1112 }
1113
1114 #-------------------------------------------------------------------------------------------------
1115 package OpenILS::Reporter::SQLBuilder::Join::right;
1116 use base qw/OpenILS::Reporter::SQLBuilder::Join/;
1117
1118 sub toSQL {
1119         my $self = shift;
1120         my $dir = shift;
1121         #return $self->{_sql} if ($self->{_sql});
1122
1123         my $_nullable_rel = $dir && $dir eq 'l' ? '_right_rel' : '_left_rel';
1124         $self->{_left_rel}->{_nullable} = 'r';
1125         $self->{$_nullable_rel}->{_nullable} = $dir;
1126
1127         my $j = $dir && $dir eq 'l' ? 'FULL OUTER' : 'RIGHT OUTER';
1128
1129         my $sql = "\n\t$j ". $self->SUPER::toSQL('r');
1130
1131         #$sql .= $_->toSQL for (@{ $self->{_right_rel}->{_join} });
1132
1133         return $self->{_sql} = $sql;
1134 }
1135
1136 #-------------------------------------------------------------------------------------------------
1137 package OpenILS::Reporter::SQLBuilder::Join::inner;
1138 use base qw/OpenILS::Reporter::SQLBuilder::Join/;
1139
1140 sub toSQL {
1141         my $self = shift;
1142         my $dir = shift;
1143         #return $self->{_sql} if ($self->{_sql});
1144
1145         my $_nullable_rel = $dir && $dir eq 'l' ? '_right_rel' : '_left_rel';
1146         $self->{$_nullable_rel}->{_nullable} = $dir;
1147
1148         my $j = $dir ? ( $dir eq 'l' ? 'LEFT OUTER' : ( $dir eq 'r' ? 'RIGHT OUTER' : 'FULL OUTER' ) ) : 'INNER';
1149
1150         my $sql = "\n\t$j ". $self->SUPER::toSQL;
1151
1152         #$sql .= $_->toSQL for (@{ $self->{_right_rel}->{_join} });
1153
1154         return $self->{_sql} = $sql;
1155 }
1156
1157 #-------------------------------------------------------------------------------------------------
1158 package OpenILS::Reporter::SQLBuilder::Join::cross;
1159 use base qw/OpenILS::Reporter::SQLBuilder::Join/;
1160
1161 sub toSQL {
1162         my $self = shift;
1163         #return $self->{_sql} if ($self->{_sql});
1164
1165         $self->{_right_rel}->{_nullable} = 'f';
1166         $self->{_left_rel}->{_nullable} = 'f';
1167
1168         my $sql = "\n\tFULL OUTER ". $self->SUPER::toSQL('f');
1169
1170         #$sql .= $_->toSQL for (@{ $self->{_right_rel}->{_join} });
1171
1172         return $self->{_sql} = $sql;
1173 }
1174
1175 1;