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