]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Reporter/SQLBuilder.pm
input transforms
[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 resolve_param {
35         my $self = shift;
36         my $val = shift;
37
38         if ($val =~ /^::(.+)$/o) {
39                 $val = $self->get_param($1);
40         }
41
42         $val =~ s/\\/\\\\/go if (!ref($val));
43         $val =~ s/"/\\"/go if (!ref($val));
44         return $val;
45 }
46
47 sub parse_report {
48         my $self = shift;
49         my $report = shift;
50
51         my $rs = OpenILS::Reporter::SQLBuilder::ResultSet->new;
52
53         $rs->is_subquery( 1 ) if ( $report->{alias} );
54
55         $rs     ->set_builder( $self )
56                 ->set_subquery_alias( $report->{alias} )
57                 ->set_select( $report->{select} )
58                 ->set_from( $report->{from} )
59                 ->set_where( $report->{where} )
60                 ->set_having( $report->{having} )
61                 ->set_order_by( $report->{order_by} );
62
63         return $rs;
64 }
65
66
67 #-------------------------------------------------------------------------------------------------
68 package OpenILS::Reporter::SQLBuilder::ResultSet;
69 use base qw/OpenILS::Reporter::SQLBuilder/;
70
71 sub is_subquery {
72         my $self = shift;
73         my $flag = shift;
74         $self->{_is_subquery} = $flag if (defined $flag);
75         return $self->{_is_subquery};
76 }
77
78 sub set_subquery_alias {
79         my $self = shift;
80         my $alias = shift;
81         $self->{_alias} = $alias if (defined $alias);
82         return $self;
83 }
84
85 sub set_select {
86         my $self = shift;
87         my @cols = @_;
88
89         $self->{_select} = [];
90
91         return $self unless (@cols && defined($cols[0]));
92         @cols = @{ $cols[0] } if (@cols == 1 && ref($cols[0]) eq 'ARRAY');
93
94         push @{ $self->{_select} }, map { OpenILS::Reporter::SQLBuilder::Column::Select->new( $_ )->set_builder( $self->builder ) } @cols;
95
96         return $self;
97 }
98
99 sub set_from {
100         my $self = shift;
101         my $f = shift;
102
103         $self->{_from} = OpenILS::Reporter::SQLBuilder::Relation->parse( $f );
104
105         return $self;
106 }
107
108 sub set_where {
109         my $self = shift;
110         my @cols = @_;
111
112         $self->{_where} = [];
113
114         return $self unless (@cols && defined($cols[0]));
115         @cols = @{ $cols[0] } if (@cols == 1 && ref($cols[0]) eq 'ARRAY');
116
117         push @{ $self->{_where} }, map { OpenILS::Reporter::SQLBuilder::Column::Where->new( $_ )->set_builder( $self->builder ) } @cols;
118
119         return $self;
120 }
121
122 sub set_having {
123         my $self = shift;
124         my @cols = @_;
125
126         $self->{_having} = [];
127
128         return $self unless (@cols && defined($cols[0]));
129         @cols = @{ $cols[0] } if (@cols == 1 && ref($cols[0]) eq 'ARRAY');
130
131         push @{ $self->{_having} }, map { OpenILS::Reporter::SQLBuilder::Column::Having->new( $_ )->set_builder( $self->builder ) } @cols;
132
133         return $self;
134 }
135
136 sub set_order_by {
137         my $self = shift;
138         my @cols = @_;
139
140         $self->{_order_by} = [];
141
142         return $self unless (@cols && defined($cols[0]));
143         @cols = @{ $cols[0] } if (@cols == 1 && ref($cols[0]) eq 'ARRAY');
144
145         push @{ $self->{_order_by} }, map { OpenILS::Reporter::SQLBuilder::Column::OrderBy->new( $_ )->set_builder( $self->builder ) } @cols;
146
147         return $self;
148 }
149
150 sub toSQL {
151         my $self = shift;
152
153         return $self->{_sql} if ($self->{_sql});
154
155         my $sql = '';
156
157         if ($self->is_subquery) {
158                 $sql = '(';
159         }
160
161         $sql .= "SELECT\t" . join(",\n\t", map { $_->toSQL } @{ $self->{_select} }) . "\n" if (@{ $self->{_select} });
162         $sql .= "  FROM\t" . $self->{_from}->toSQL . "\n" if ($self->{_from});
163         $sql .= "  WHERE\t" . join("\n\tAND ", map { $_->toSQL } @{ $self->{_where} }) . "\n" if (@{ $self->{_where} });
164
165         my $gcount = 1;
166         my @group_by;
167         for my $c ( @{ $self->{_select} } ) {
168                 push @group_by, $gcount if (!$c->is_aggregate);
169                 $gcount++;
170         }
171
172         $sql .= '  GROUP BY ' . join(', ', @group_by) . "\n" if (@group_by);
173         $sql .= "  HAVING " . join("\n\tAND ", map { $_->toSQL } @{ $self->{_having} }) . "\n" if (@{ $self->{_having} });
174         $sql .= '  ORDER BY ' . join(', ', map { $_->toSQL } @{ $self->{_order_by} }) . "\n" if (@{ $self->{_order_by} });
175
176         if ($self->is_subquery) {
177                 $sql .= ') '. $self->{_alias} . "\n";
178         }
179
180         return $self->{_sql} = $sql;
181 }
182
183
184 #-------------------------------------------------------------------------------------------------
185 package OpenILS::Reporter::SQLBuilder::Input;
186 use base qw/OpenILS::Reporter::SQLBuilder/;
187
188 sub new {
189         my $class = shift;
190         my $self = $class->SUPER::new;
191
192         my $col_data = shift;
193
194         if (ref($col_data)) {
195                 $self->{params} = $col_data->{params};
196                 my $trans = $col_data->{transform} || 'Bare';
197                 my $pkg = "OpenILS::Reporter::SQLBuilder::Input::Transform::$trans";
198                 if (UNIVERSAL::can($pkg => 'toSQL')) {
199                         $self->{_transform} = $trans;
200                 } else {
201                         $self->{_transform} = 'GenericTransform';
202                 }
203         } else {
204                 $self->{_transform} = 'Bare';
205                 $self->{params} = $col_data;
206         }
207
208
209         return $self;
210 }
211
212 sub toSQL {
213         my $self = shift;
214         my $type = $self->{_transform};
215         return $self->{_sql} if ($self->{_sql});
216         my $toSQL = "OpenILS::Reporter::SQLBuilder::Input::Transform::${type}::toSQL";
217         return $self->{_sql} = $self->$toSQL;
218 }
219
220
221 #-------------------------------------------------------------------------------------------------
222 package OpenILS::Reporter::SQLBuilder::Input::Transform::Bare;
223
224 sub toSQL {
225         my $self = shift;
226
227         my $val = $self->{params};
228         $val = $$val[0] if (ref($val));
229         
230         $val =~ s/\\/\\\\/go;
231         $val =~ s/'/\\'/go;
232
233         return "'$val'";
234 }
235
236
237 #-------------------------------------------------------------------------------------------------
238 package OpenILS::Reporter::SQLBuilder::Input::Transform::relative_year;
239
240 sub toSQL {
241         my $self = shift;
242
243         my $val = $self->{params};
244         $val = $$val[0] if (ref($val));
245
246         $val =~ s/\\/\\\\/go;
247         $val =~ s/'/\\'/go;
248
249         return "EXTRACT(YEAR FROM NOW() + '$val years')";
250 }
251
252
253 #-------------------------------------------------------------------------------------------------
254 package OpenILS::Reporter::SQLBuilder::Input::Transform::relative_month;
255
256 sub toSQL {
257         my $self = shift;
258
259         my $val = $self->{params};
260         $val = $$val[0] if (ref($val));
261
262         $val =~ s/\\/\\\\/go;
263         $val =~ s/'/\\'/go;
264
265         return "EXTRACT(YEAR FROM NOW() + '$val months')" .
266                 " || '-' || LPAD(EXTRACT(MONTH FROM NOW() + '$val months'),2,'0')";
267 }
268
269
270 #-------------------------------------------------------------------------------------------------
271 package OpenILS::Reporter::SQLBuilder::Input::Transform::relative_date;
272
273 sub toSQL {
274         my $self = shift;
275
276         my $val = $self->{params};
277         $val = $$val[0] if (ref($val));
278
279         $val =~ s/\\/\\\\/go;
280         $val =~ s/'/\\'/go;
281
282         return "DATE(NOW() + '$val days')";
283 }
284
285
286 #-------------------------------------------------------------------------------------------------
287 package OpenILS::Reporter::SQLBuilder::Input::Transform::relative_week;
288
289 sub toSQL {
290         my $self = shift;
291
292         my $val = $self->{params};
293         $val = $$val[0] if (ref($val));
294
295         $val =~ s/\\/\\\\/go;
296         $val =~ s/'/\\'/go;
297
298         return "EXTRACT(WEEK FROM NOW() + '$val weeks')";
299 }
300
301
302 #-------------------------------------------------------------------------------------------------
303 package OpenILS::Reporter::SQLBuilder::Column;
304 use base qw/OpenILS::Reporter::SQLBuilder/;
305
306 sub new {
307         my $class = shift;
308         my $self = $class->SUPER::new;
309
310         my $col_data = shift;
311         $self->{_relation} = $col_data->{relation};
312         $self->{_column} = $col_data->{column};
313
314         $self->{_aggregate} = $col_data->{aggregate};
315
316         if (ref($self->{_column})) {
317                 my $trans = $self->{_column}->{transform} || 'Bare';
318                 my $pkg = "OpenILS::Reporter::SQLBuilder::Column::Transform::$trans";
319                 if (UNIVERSAL::can($pkg => 'toSQL')) {
320                         $self->{_transform} = $trans;
321                 } else {
322                         $self->{_transform} = 'GenericTransform';
323                 }
324         } else {
325                 $self->{_transform} = 'Bare';
326         }
327
328
329         return $self;
330 }
331
332 sub name {
333         my $self = shift;
334         if (ref($self->{_column})) {
335                  return $self->{_column}->{colname};
336         } else {
337                 return $self->{_column};
338         }
339 }
340
341 sub toSQL {
342         my $self = shift;
343         my $type = $self->{_transform};
344         return $self->{_sql} if ($self->{_sql});
345         my $toSQL = "OpenILS::Reporter::SQLBuilder::Column::Transform::${type}::toSQL";
346         return $self->{_sql} = $self->$toSQL;
347 }
348
349 sub is_aggregate {
350         my $self = shift;
351         my $type = $self->{_transform};
352         my $is_agg = "OpenILS::Reporter::SQLBuilder::Column::Transform::${type}::is_aggregate";
353         return $self->$is_agg;
354 }
355
356
357 #-------------------------------------------------------------------------------------------------
358 package OpenILS::Reporter::SQLBuilder::Column::OrderBy;
359 use base qw/OpenILS::Reporter::SQLBuilder::Column/;
360
361 sub new {
362         my $class = shift;
363         my $self = $class->SUPER::new(@_);
364
365         my $col_data = shift;
366         $self->{_direction} = $col_data->{direction} || 'ascending';
367         return $self;
368 }
369
370 sub toSQL {
371         my $self = shift;
372         my $dir = ($self->{_direction} =~ /^d/oi) ? 'DESC' : 'ASC';
373         return $self->{_sql} if ($self->{_sql});
374         return $self->{_sql} = $self->SUPER::toSQL .  " $dir";
375 }
376
377
378 #-------------------------------------------------------------------------------------------------
379 package OpenILS::Reporter::SQLBuilder::Column::Select;
380 use base qw/OpenILS::Reporter::SQLBuilder::Column/;
381
382 sub new {
383         my $class = shift;
384         my $self = $class->SUPER::new(@_);
385
386         my $col_data = shift;
387         $self->{_alias} = $col_data->{alias} || $self->name;
388         return $self;
389 }
390
391 sub toSQL {
392         my $self = shift;
393         return $self->{_sql} if ($self->{_sql});
394         return $self->{_sql} = $self->SUPER::toSQL .  ' AS "' . $self->resolve_param( $self->{_alias} ) . '"';
395 }
396
397
398 #-------------------------------------------------------------------------------------------------
399 package OpenILS::Reporter::SQLBuilder::Column::Transform::GenericTransform;
400
401 sub toSQL {
402         my $self = shift;
403         my $name = $self->name;
404         my ($func) = keys %{ $self->{_column} };
405
406         my @params;
407         @params = @{ $self->resolve_param( $self->{_column}->{params} ) } if ($self->{_column}->{params});
408
409         my $sql = $func . '("' . $self->{_relation} . '"."' . $self->name . '"';
410         $sql .= ",'" . join("','", @params) . "'" if (@params);
411         $sql .= ')';
412
413         return $sql;
414 }
415
416 sub is_aggregate { return $self->{_aggregate} }
417
418 #-------------------------------------------------------------------------------------------------
419 package OpenILS::Reporter::SQLBuilder::Column::Transform::Bare;
420
421 sub toSQL {
422         my $self = shift;
423         return '"' . $self->{_relation} . '"."' . $self->name . '"';
424 }
425
426 sub is_aggregate { return 0 }
427
428 #-------------------------------------------------------------------------------------------------
429 package OpenILS::Reporter::SQLBuilder::Column::Transform::substring;
430
431 sub toSQL {
432         my $self = shift;
433         my $params = $self->resolve_param( $self->{_column}->{params} );
434         my $start = $$params[0];
435         my $len = $$params[1];
436         return 'SUBSTRING("' . $self->{_relation} . '"."' . $self->name . "\",$start,$len)";
437 }
438
439 sub is_aggregate { return 0 }
440
441
442 #-------------------------------------------------------------------------------------------------
443 package OpenILS::Reporter::SQLBuilder::Column::Transform::day_name;
444
445 sub toSQL {
446         my $self = shift;
447         return 'TO_CHAR("' . $self->{_relation} . '"."' . $self->name . '", \'Day\')';
448 }
449
450 sub is_aggregate { return 0 }
451
452
453 #-------------------------------------------------------------------------------------------------
454 package OpenILS::Reporter::SQLBuilder::Column::Transform::month_name;
455
456 sub toSQL {
457         my $self = shift;
458         return 'TO_CHAR("' . $self->{_relation} . '"."' . $self->name . '", \'Month\')';
459 }
460
461 sub is_aggregate { return 0 }
462
463
464 #-------------------------------------------------------------------------------------------------
465 package OpenILS::Reporter::SQLBuilder::Column::Transform::doy;
466
467 sub toSQL {
468         my $self = shift;
469         return 'EXTRACT(DOY FROM "' . $self->{_relation} . '"."' . $self->name . '")';
470 }
471
472 sub is_aggregate { return 0 }
473
474
475 #-------------------------------------------------------------------------------------------------
476 package OpenILS::Reporter::SQLBuilder::Column::Transform::woy;
477
478 sub toSQL {
479         my $self = shift;
480         return 'EXTRACT(WEEK FROM "' . $self->{_relation} . '"."' . $self->name . '")';
481 }
482
483 sub is_aggregate { return 0 }
484
485
486 #-------------------------------------------------------------------------------------------------
487 package OpenILS::Reporter::SQLBuilder::Column::Transform::moy;
488
489 sub toSQL {
490         my $self = shift;
491         return 'EXTRACT(MONTH FROM "' . $self->{_relation} . '"."' . $self->name . '")';
492 }
493
494 sub is_aggregate { return 0 }
495
496
497 #-------------------------------------------------------------------------------------------------
498 package OpenILS::Reporter::SQLBuilder::Column::Transform::qoy;
499
500 sub toSQL {
501         my $self = shift;
502         return 'EXTRACT(QUARTER FROM "' . $self->{_relation} . '"."' . $self->name . '")';
503 }
504
505 sub is_aggregate { return 0 }
506
507
508 #-------------------------------------------------------------------------------------------------
509 package OpenILS::Reporter::SQLBuilder::Column::Transform::dom;
510
511 sub toSQL {
512         my $self = shift;
513         return 'EXTRACT(DAY FROM "' . $self->{_relation} . '"."' . $self->name . '")';
514 }
515
516 sub is_aggregate { return 0 }
517
518
519 #-------------------------------------------------------------------------------------------------
520 package OpenILS::Reporter::SQLBuilder::Column::Transform::dow;
521
522 sub toSQL {
523         my $self = shift;
524         return 'EXTRACT(DOW FROM "' . $self->{_relation} . '"."' . $self->name . '")';
525 }
526
527 sub is_aggregate { return 0 }
528
529
530 #-------------------------------------------------------------------------------------------------
531 package OpenILS::Reporter::SQLBuilder::Column::Transform::year_trunc;
532
533 sub toSQL {
534         my $self = shift;
535         return 'EXTRACT(YEAR FROM "' . $self->{_relation} . '"."' . $self->name . '")';
536 }
537
538 sub is_aggregate { return 0 }
539
540
541 #-------------------------------------------------------------------------------------------------
542 package OpenILS::Reporter::SQLBuilder::Column::Transform::month_trunc;
543
544 sub toSQL {
545         my $self = shift;
546         return 'EXTRACT(YEAR FROM "' . $self->{_relation} . '"."' . $self->name . '")' .
547                 ' || \'-\' || LPAD(EXTRACT(MONTH FROM "' . $self->{_relation} . '"."' . $self->name . '"),2,\'0\')';
548 }
549
550 sub is_aggregate { return 0 }
551
552
553 #-------------------------------------------------------------------------------------------------
554 package OpenILS::Reporter::SQLBuilder::Column::Transform::quarter;
555
556 sub toSQL {
557         my $self = shift;
558         return 'EXTRACT(YEAR FROM "' . $self->{_relation} . '"."' . $self->name . '")' .
559                 ' || \'-Q\' || EXTRACT(QUARTER FROM "' . $self->{_relation} . '"."' . $self->name . '")';
560 }
561
562 sub is_aggregate { return 0 }
563
564
565 #-------------------------------------------------------------------------------------------------
566 package OpenILS::Reporter::SQLBuilder::Column::Transform::months_ago;
567
568 sub toSQL {
569         my $self = shift;
570         return 'EXTRACT(MONTH FROM AGE(NOW(),"' . $self->{_relation} . '"."' . $self->name . '"))';
571 }
572
573 sub is_aggregate { return 0 }
574
575
576 #-------------------------------------------------------------------------------------------------
577 package OpenILS::Reporter::SQLBuilder::Column::Transform::quarters_ago;
578
579 sub toSQL {
580         my $self = shift;
581         return 'EXTRACT(QUARTER FROM AGE(NOW(),"' . $self->{_relation} . '"."' . $self->name . '"))';
582 }
583
584 sub is_aggregate { return 0 }
585
586
587 #-------------------------------------------------------------------------------------------------
588 package OpenILS::Reporter::SQLBuilder::Column::Transform::age;
589
590 sub toSQL {
591         my $self = shift;
592         return 'AGE(NOW(),"' . $self->{_relation} . '"."' . $self->name . '")';
593 }
594
595 sub is_aggregate { return 0 }
596
597
598 #-------------------------------------------------------------------------------------------------
599 package OpenILS::Reporter::SQLBuilder::Column::Transform::first;
600
601 sub toSQL {
602         my $self = shift;
603         return 'FIRST("' . $self->{_relation} . '"."' . $self->name . '")';
604 }
605
606 sub is_aggregate { return 1 }
607
608
609 #-------------------------------------------------------------------------------------------------
610 package OpenILS::Reporter::SQLBuilder::Column::Transform::last;
611
612 sub toSQL {
613         my $self = shift;
614         return 'LAST("' . $self->{_relation} . '"."' . $self->name . '")';
615 }
616
617 sub is_aggregate { return 1 }
618
619
620 #-------------------------------------------------------------------------------------------------
621 package OpenILS::Reporter::SQLBuilder::Column::Transform::min;
622
623 sub toSQL {
624         my $self = shift;
625         return 'MIN("' . $self->{_relation} . '"."' . $self->name . '")';
626 }
627
628 sub is_aggregate { return 1 }
629
630
631 #-------------------------------------------------------------------------------------------------
632 package OpenILS::Reporter::SQLBuilder::Column::Transform::max;
633
634 sub toSQL {
635         my $self = shift;
636         return 'MAX("' . $self->{_relation} . '"."' . $self->name . '")';
637 }
638
639 sub is_aggregate { return 1 }
640
641
642 #-------------------------------------------------------------------------------------------------
643 package OpenILS::Reporter::SQLBuilder::Column::Transform::count;
644
645 sub toSQL {
646         my $self = shift;
647         return 'COUNT("' . $self->{_relation} . '"."' . $self->name . '")';
648 }
649
650 sub is_aggregate { return 1 }
651
652
653 #-------------------------------------------------------------------------------------------------
654 package OpenILS::Reporter::SQLBuilder::Column::Transform::count_distinct;
655
656 sub toSQL {
657         my $self = shift;
658         return 'COUNT(DISTINCT "' . $self->{_relation} . '"."' . $self->name . '")';
659 }
660
661 sub is_aggregate { return 1 }
662
663
664 #-------------------------------------------------------------------------------------------------
665 package OpenILS::Reporter::SQLBuilder::Column::Transform::sum;
666
667 sub toSQL {
668         my $self = shift;
669         return 'SUM("' . $self->{_relation} . '"."' . $self->name . '")';
670 }
671
672 sub is_aggregate { return 1 }
673
674
675 #-------------------------------------------------------------------------------------------------
676 package OpenILS::Reporter::SQLBuilder::Column::Transform::average;
677
678 sub toSQL {
679         my $self = shift;
680         return 'AVG("' . $self->{_relation} . '"."' . $self->name .  '")';
681 }
682
683 sub is_aggregate { return 1 }
684
685
686 #-------------------------------------------------------------------------------------------------
687 package OpenILS::Reporter::SQLBuilder::Column::Where;
688 use base qw/OpenILS::Reporter::SQLBuilder::Column/;
689
690 sub new {
691         my $class = shift;
692         my $self = $class->SUPER::new(@_);
693
694         my $col_data = shift;
695         $self->{_condition} = $col_data->{condition};
696
697         return $self;
698 }
699
700 sub _flesh_conditions {
701         my $cond = shift;
702         $cond = [$cond] unless (ref($cond) eq 'ARRAY');
703
704         my @out;
705         for my $c (@$cond) {
706                 push @out, OpenILS::Reporter::SQLBuilder::Input->new( $c );
707         }
708
709         return \@out;
710 }
711
712 sub toSQL {
713         my $self = shift;
714
715         return $self->{_sql} if ($self->{_sql});
716         my $sql = $self->SUPER::toSQL;
717
718         my ($op) = keys %{ $self->{_condition} };
719         my $val = _flesh_conditions( $self->resolve_param( $self->{_condition}->{$op} ) );
720
721         if (lc($op) eq 'in') {
722                 $sql .= " IN (". join(",", map { $_->toSQL } @$val).")";
723         } elsif (lc($op) eq 'not in') {
724                 $sql .= " NOT IN (". join(",", map { $_->toSQL } @$val).")";
725         } elsif (lc($op) eq 'between') {
726                 $sql .= " BETWEEN ". join(" AND ", map { $_->toSQL } @$val);
727         } elsif (lc($op) eq 'not between') {
728                 $sql .= " NOT BETWEEN ". join(" AND ", map { $_->toSQL } @$val);
729         } else {
730                 $val = $$val[0] if (ref($val) eq 'ARRAY');
731                 $sql .= " $op " . $val->toSQL;
732         }
733
734         return $self->{_sql} = $sql;
735 }
736
737
738 #-------------------------------------------------------------------------------------------------
739 package OpenILS::Reporter::SQLBuilder::Column::Having;
740 use base qw/OpenILS::Reporter::SQLBuilder::Column::Where/;
741
742 #-------------------------------------------------------------------------------------------------
743 package OpenILS::Reporter::SQLBuilder::Relation;
744 use base qw/OpenILS::Reporter::SQLBuilder/;
745
746 sub parse {
747         my $self = shift;
748         $self = $self->SUPER::new if (!ref($self));
749
750         my $rel_data = shift;
751
752         $self->{_table} = $rel_data->{table};
753         $self->{_alias} = $rel_data->{alias} || $self->name;
754         $self->{_join} = [];
755         $self->{_columns} = [];
756
757         if ($rel_data->{join}) {
758                 $self->add_join(
759                         $_ => OpenILS::Reporter::SQLBuilder::Relation->parse( $rel_data->{join}->{$_} ) => $rel_data->{join}->{$_}->{key}
760                 ) for ( keys %{ $rel_data->{join} } );
761         }
762
763         return $self;
764 }
765
766 sub add_column {
767         my $self = shift;
768         my $col = shift;
769         
770         push @{ $self->{_columns} }, $col;
771 }
772
773 sub find_column {
774         my $self = shift;
775         my $col = shift;
776         return (grep { $_->name eq $col} @{ $self->{_columns} })[0];
777 }
778
779 sub add_join {
780         my $self = shift;
781         my $col = shift;
782         my $frel = shift;
783         my $fkey = shift;
784
785         if (ref($col) eq 'OpenILS::Reporter::SQLBuilder::Join') {
786                 push @{ $self->{_join} }, $col;
787         } else {
788                 push @{ $self->{_join} }, OpenILS::Reporter::SQLBuilder::Join->build( $self => $col, $frel => $fkey );
789         }
790
791         return $self;
792 }
793
794 sub is_join {
795         my $self = shift;
796         my $j = shift;
797         $self->{_is_join} = $j if ($j);
798         return $self->{_is_join};
799 }
800
801 sub toSQL {
802         my $self = shift;
803         return $self->{_sql} if ($self->{_sql});
804
805         my $sql = $self->{_table} .' AS "'. $self->{_alias} .'"';
806
807         if (!$self->is_join) {
808                 for my $j ( @{ $self->{_join} } ) {
809                         $sql .= $j->toSQL;
810                 }
811         }
812
813         return $self->{_sql} = $sql;
814 }
815
816 #-------------------------------------------------------------------------------------------------
817 package OpenILS::Reporter::SQLBuilder::Join;
818 use base qw/OpenILS::Reporter::SQLBuilder/;
819
820 sub build {
821         my $self = shift;
822         $self = $self->SUPER::new if (!ref($self));
823
824         $self->{_left_rel} = shift;
825         $self->{_left_col} = shift;
826
827         $self->{_right_rel} = shift;
828         $self->{_right_col} = shift;
829
830         $self->{_right_rel}->is_join(1);
831
832         return $self;
833 }
834
835 sub toSQL {
836         my $self = shift;
837         return $self->{_sql} if ($self->{_sql});
838
839         my $sql = "\n\tJOIN " . $self->{_right_rel}->toSQL .
840                 ' ON ("' . $self->{_left_rel}->{_alias} . '"."' . $self->{_left_col} .
841                 '" = "' . $self->{_right_rel}->{_alias} . '"."' . $self->{_right_col} . '")';
842
843         $sql .= $_->toSQL for (@{ $self->{_right_rel}->{_join} });
844
845         return $self->{_sql} = $sql;
846 }
847
848 1;