1 #-------------------------------------------------------------------------------------------------
2 package OpenILS::Reporter::SQLBuilder;
6 $class = ref($class) || $class;
8 return bless { _sql => undef } => $class;
14 $self->{_params} = $p;
20 return $self->{_builder}->{_params}->{$p};
25 $self->{_builder} = shift;
33 if ($val =~ /^::(.+)$/o) {
34 $val = $self->get_param($1);
46 $self->set_select( $report->{select} );
47 $self->set_from( $report->{from} );
48 $self->set_where( $report->{where} );
49 $self->set_having( $report->{having} );
50 $self->set_order_by( $report->{order_by} );
59 $self->{_select} = [];
61 return $self unless (@cols && defined($cols[0]));
62 @cols = @{ $cols[0] } if (@cols == 1 && ref($cols[0]) eq 'ARRAY');
64 push @{ $self->{_select} }, map { OpenILS::Reporter::SQLBuilder::Column::Select->new( $_ )->set_builder( $self ) } @cols;
73 $self->{_from} = OpenILS::Reporter::SQLBuilder::Relation->parse( $f );
84 return $self unless (@cols && defined($cols[0]));
85 @cols = @{ $cols[0] } if (@cols == 1 && ref($cols[0]) eq 'ARRAY');
87 push @{ $self->{_where} }, map { OpenILS::Reporter::SQLBuilder::Column::Where->new( $_ )->set_builder( $self ) } @cols;
96 $self->{_having} = [];
98 return $self unless (@cols && defined($cols[0]));
99 @cols = @{ $cols[0] } if (@cols == 1 && ref($cols[0]) eq 'ARRAY');
101 push @{ $self->{_having} }, map { OpenILS::Reporter::SQLBuilder::Column::Having->new( $_ )->set_builder( $self ) } @cols;
110 $self->{_order_by} = [];
112 return $self unless (@cols && defined($cols[0]));
113 @cols = @{ $cols[0] } if (@cols == 1 && ref($cols[0]) eq 'ARRAY');
115 push @{ $self->{_order_by} }, map { OpenILS::Reporter::SQLBuilder::Column::OrderBy->new( $_ )->set_builder( $self ) } @cols;
123 my $sql = "SELECT\t" . join(",\n\t", map { $_->toSQL } @{ $self->{_select} }) . "\n" if (@{ $self->{_select} });
125 $sql .= " FROM\t" . $self->{_from}->toSQL . "\n" if ($self->{_from});
127 $sql .= " WHERE\t" . join("\n\tAND ", map { $_->toSQL } @{ $self->{_where} }) . "\n" if (@{ $self->{_where} });
131 for my $c ( @{ $self->{_select} } ) {
132 push @group_by, $gcount if (!$c->is_aggregate);
136 $sql .= ' GROUP BY ' . join(', ', @group_by) . "\n" if (@group_by);
137 $sql .= " HAVING " . join("\n\tAND ", map { $_->toSQL } @{ $self->{_having} }) . "\n" if (@{ $self->{_having} });
138 $sql .= ' ORDER BY ' . join(', ', map { $_->toSQL } @{ $self->{_order_by} }) . "\n" if (@{ $self->{_order_by} });
144 #-------------------------------------------------------------------------------------------------
145 package OpenILS::Reporter::SQLBuilder::Column;
146 use base qw/OpenILS::Reporter::SQLBuilder/;
150 my $self = $class->SUPER::new;
152 my $col_data = shift;
153 $self->{_relation} = $col_data->{relation};
154 $self->{_column} = $col_data->{column};
156 $self->{_aggregate} = $col_data->{aggregate};
158 if (ref($self->{_column})) {
159 my ($trans) = keys %{ $self->{_column} };
160 my $pkg = "OpenILS::Reporter::SQLBuilder::Column::Transform::$trans";
161 if (UNIVERSAL::can($pkg => 'toSQL')) {
162 $self->{_transform} = $trans;
164 $self->{_transform} = 'GenericTransform';
167 $self->{_transform} = 'Bare';
176 if (ref($self->{_column})) {
177 my ($k) = keys %{$self->{_column}};
178 if (ref($self->{_column}->{$k})) {
179 return $self->resolve_param( $self->{_column}->{$k}->[0] );
181 return $self->resolve_param( $self->{_column}->{$k} );
184 return $self->resolve_param( $self->{_column} );
190 my $type = $self->{_transform};
191 my $toSQL = "OpenILS::Reporter::SQLBuilder::Column::Transform::${type}::toSQL";
192 return $self->$toSQL;
197 my $type = $self->{_transform};
198 my $is_agg = "OpenILS::Reporter::SQLBuilder::Column::Transform::${type}::is_aggregate";
199 return $self->$is_agg;
203 #-------------------------------------------------------------------------------------------------
204 package OpenILS::Reporter::SQLBuilder::Column::OrderBy;
205 use base qw/OpenILS::Reporter::SQLBuilder::Column/;
209 my $self = $class->SUPER::new(@_);
211 my $col_data = shift;
212 $self->{_direction} = $col_data->{direction} || 'ascending';
218 my $dir = ($self->{_direction} =~ /^d/oi) ? 'DESC' : 'ASC';
219 return $self->SUPER::toSQL . " $dir";
223 #-------------------------------------------------------------------------------------------------
224 package OpenILS::Reporter::SQLBuilder::Column::Select;
225 use base qw/OpenILS::Reporter::SQLBuilder::Column/;
229 my $self = $class->SUPER::new(@_);
231 my $col_data = shift;
232 $self->{_alias} = $col_data->{alias};
238 return $self->SUPER::toSQL . ' AS "' . $self->resolve_param( $self->{_alias} ) . '"';
242 #-------------------------------------------------------------------------------------------------
243 package OpenILS::Reporter::SQLBuilder::Column::Transform::GenericTransform;
247 my $name = $self->name;
248 my ($func) = keys %{ $self->{_column} };
251 @params = @{ $self->{_column}->{$func} } if (ref($self->{_column}->{$func}));
252 shift @params if (@params);
254 my $sql = $func . '("' . $self->{_relation} . '"."' . $self->name . '"';
255 $sql .= ",'" . join("','", @params) . "'" if (@params);
261 sub is_aggregate { return $self->{_aggregate} }
263 #-------------------------------------------------------------------------------------------------
264 package OpenILS::Reporter::SQLBuilder::Column::Transform::Bare;
268 return '"' . $self->{_relation} . '"."' . $self->name . '"';
271 sub is_aggregate { return 0 }
273 #-------------------------------------------------------------------------------------------------
274 package OpenILS::Reporter::SQLBuilder::Column::Transform::substring;
278 my ($params) = values %{ $self->{_column} };
279 my $start = $$params[1];
280 my $len = $$params[2];
281 return 'SUBSTRING("' . $self->{_relation} . '"."' . $self->name . "\",$start,$len)";
284 sub is_aggregate { return 0 }
287 #-------------------------------------------------------------------------------------------------
288 package OpenILS::Reporter::SQLBuilder::Column::Transform::day_name;
292 return 'TO_CHAR("' . $self->{_relation} . '"."' . $self->name . '", \'Day\')';
295 sub is_aggregate { return 0 }
298 #-------------------------------------------------------------------------------------------------
299 package OpenILS::Reporter::SQLBuilder::Column::Transform::month_name;
303 return 'TO_CHAR("' . $self->{_relation} . '"."' . $self->name . '", \'Month\')';
306 sub is_aggregate { return 0 }
309 #-------------------------------------------------------------------------------------------------
310 package OpenILS::Reporter::SQLBuilder::Column::Transform::doy;
314 return 'EXTRACT(DOY FROM "' . $self->{_relation} . '"."' . $self->name . '")';
317 sub is_aggregate { return 0 }
320 #-------------------------------------------------------------------------------------------------
321 package OpenILS::Reporter::SQLBuilder::Column::Transform::woy;
325 return 'EXTRACT(WEEK FROM "' . $self->{_relation} . '"."' . $self->name . '")';
328 sub is_aggregate { return 0 }
331 #-------------------------------------------------------------------------------------------------
332 package OpenILS::Reporter::SQLBuilder::Column::Transform::moy;
336 return 'EXTRACT(MONTH FROM "' . $self->{_relation} . '"."' . $self->name . '")';
339 sub is_aggregate { return 0 }
342 #-------------------------------------------------------------------------------------------------
343 package OpenILS::Reporter::SQLBuilder::Column::Transform::qoy;
347 return 'EXTRACT(QUARTER FROM "' . $self->{_relation} . '"."' . $self->name . '")';
350 sub is_aggregate { return 0 }
353 #-------------------------------------------------------------------------------------------------
354 package OpenILS::Reporter::SQLBuilder::Column::Transform::dom;
358 return 'EXTRACT(DAY FROM "' . $self->{_relation} . '"."' . $self->name . '")';
361 sub is_aggregate { return 0 }
364 #-------------------------------------------------------------------------------------------------
365 package OpenILS::Reporter::SQLBuilder::Column::Transform::dow;
369 return 'EXTRACT(DOW FROM "' . $self->{_relation} . '"."' . $self->name . '")';
372 sub is_aggregate { return 0 }
375 #-------------------------------------------------------------------------------------------------
376 package OpenILS::Reporter::SQLBuilder::Column::Transform::year_trunc;
380 return 'EXTRACT(YEAR FROM "' . $self->{_relation} . '"."' . $self->name . '")';
383 sub is_aggregate { return 0 }
386 #-------------------------------------------------------------------------------------------------
387 package OpenILS::Reporter::SQLBuilder::Column::Transform::month_trunc;
391 return 'EXTRACT(YEAR FROM "' . $self->{_relation} . '"."' . $self->name . '")' .
392 ' || \'-\' || LPAD(EXTRACT(MONTH FROM "' . $self->{_relation} . '"."' . $self->name . '"),2,\'0\')';
395 sub is_aggregate { return 0 }
398 #-------------------------------------------------------------------------------------------------
399 package OpenILS::Reporter::SQLBuilder::Column::Transform::quarter;
403 return 'EXTRACT(YEAR FROM "' . $self->{_relation} . '"."' . $self->name . '")' .
404 ' || \'-Q\' || EXTRACT(QUARTER FROM "' . $self->{_relation} . '"."' . $self->name . '")';
407 sub is_aggregate { return 0 }
410 #-------------------------------------------------------------------------------------------------
411 package OpenILS::Reporter::SQLBuilder::Column::Transform::months_ago;
415 return 'EXTRACT(MONTH FROM AGE(NOW(),"' . $self->{_relation} . '"."' . $self->name . '"))';
418 sub is_aggregate { return 0 }
421 #-------------------------------------------------------------------------------------------------
422 package OpenILS::Reporter::SQLBuilder::Column::Transform::quarters_ago;
426 return 'EXTRACT(QUARTER FROM AGE(NOW(),"' . $self->{_relation} . '"."' . $self->name . '"))';
429 sub is_aggregate { return 0 }
432 #-------------------------------------------------------------------------------------------------
433 package OpenILS::Reporter::SQLBuilder::Column::Transform::age;
437 return 'AGE(NOW(),"' . $self->{_relation} . '"."' . $self->name . '")';
440 sub is_aggregate { return 0 }
443 #-------------------------------------------------------------------------------------------------
444 package OpenILS::Reporter::SQLBuilder::Column::Transform::min;
448 return 'MIN("' . $self->{_relation} . '"."' . $self->name . '")';
451 sub is_aggregate { return 1 }
454 #-------------------------------------------------------------------------------------------------
455 package OpenILS::Reporter::SQLBuilder::Column::Transform::max;
459 return 'MAX("' . $self->{_relation} . '"."' . $self->name . '")';
462 sub is_aggregate { return 1 }
465 #-------------------------------------------------------------------------------------------------
466 package OpenILS::Reporter::SQLBuilder::Column::Transform::count;
470 return 'COUNT("' . $self->{_relation} . '"."' . $self->name . '")';
473 sub is_aggregate { return 1 }
476 #-------------------------------------------------------------------------------------------------
477 package OpenILS::Reporter::SQLBuilder::Column::Transform::count_distinct;
481 return 'COUNT(DISTINCT "' . $self->{_relation} . '"."' . $self->name . '")';
484 sub is_aggregate { return 1 }
487 #-------------------------------------------------------------------------------------------------
488 package OpenILS::Reporter::SQLBuilder::Column::Transform::sum;
492 return 'SUM("' . $self->{_relation} . '"."' . $self->name . '")';
495 sub is_aggregate { return 1 }
498 #-------------------------------------------------------------------------------------------------
499 package OpenILS::Reporter::SQLBuilder::Column::Transform::average;
503 return 'AVG("' . $self->{_relation} . '"."' . $self->name . '")';
506 sub is_aggregate { return 1 }
509 #-------------------------------------------------------------------------------------------------
510 package OpenILS::Reporter::SQLBuilder::Column::Having;
511 use base qw/OpenILS::Reporter::SQLBuilder::Column/;
515 my $self = $class->SUPER::new(@_);
517 my $col_data = shift;
518 $self->{_condition} = $col_data->{condition};
526 my $sql = $self->SUPER::toSQL;
528 my ($op) = keys %{ $self->{_condition} };
529 my $val = $self->resolve_param( values %{ $self->{_condition} } );
531 $val =~ s/'/\\'/go; $val =~ s/\\/\\\\/go;
532 $sql .= " $op '$val'";
538 #-------------------------------------------------------------------------------------------------
539 package OpenILS::Reporter::SQLBuilder::Column::Where;
540 use base qw/OpenILS::Reporter::SQLBuilder::Column/;
544 my $self = $class->SUPER::new(@_);
546 my $col_data = shift;
547 $self->{_condition} = $col_data->{condition};
555 my $sql = $self->SUPER::toSQL;
557 my ($op) = keys %{ $self->{_condition} };
558 my $val = $self->resolve_param( values %{ $self->{_condition} } );
560 if (lc($op) eq 'in') {
561 $val = [$val] unless (ref($val));
562 $sql .= " IN ('". join("','", map { $_ =~ s/'/\\'/go; $_ =~ s/\\/\\\\/go; $_ } @$val)."')";
563 } elsif (lc($op) eq 'not in') {
564 $val = [$val] unless (ref($val));
565 $sql .= " NOT IN ('". join("','", map { $_ =~ s/'/\\'/go; $_ =~ s/\\/\\\\/go; $_ } @$val)."')";
566 } elsif (lc($op) eq 'between') {
567 $val = [$val] unless (ref($val));
568 $sql .= " BETWEEN '". join("' AND '", map { $_ =~ s/'/\\'/go; $_ =~ s/\\/\\\\/go; $_ } @$val)."'";
569 } elsif (lc($op) eq 'not between') {
570 $val = [$val] unless (ref($val));
571 $sql .= " NOT BETWEEN '". join("' AND '", map { $_ =~ s/'/\\'/go; $_ =~ s/\\/\\\\/go; $_ } @$val)."'";
573 $val =~ s/'/\\'/go; $val =~ s/\\/\\\\/go;
574 $sql .= " $op '$val'";
581 #-------------------------------------------------------------------------------------------------
582 package OpenILS::Reporter::SQLBuilder::Relation;
583 use base qw/OpenILS::Reporter::SQLBuilder/;
587 $self = $self->SUPER::new if (!ref($self));
589 my $rel_data = shift;
591 $self->{_table} = $rel_data->{table};
592 $self->{_alias} = $rel_data->{alias};
594 $self->{_columns} = [];
596 if ($rel_data->{join}) {
598 $_ => OpenILS::Reporter::SQLBuilder::Relation->parse( $rel_data->{join}->{$_} ) => $rel_data->{join}->{$_}->{key}
599 ) for ( keys %{ $rel_data->{join} } );
609 push @{ $self->{_columns} }, $col;
615 return (grep { $_->name eq $col} @{ $self->{_columns} })[0];
624 if (ref($col) eq 'OpenILS::Reporter::SQLBuilder::Join') {
625 push @{ $self->{_join} }, $col;
627 push @{ $self->{_join} }, OpenILS::Reporter::SQLBuilder::Join->build( $self => $col, $frel => $fkey );
636 $self->{_is_join} = $j if ($j);
637 return $self->{_is_join};
642 my $sql = $self->{_table} .' AS "'. $self->{_alias} .'"';
644 if (!$self->is_join) {
645 for my $j ( @{ $self->{_join} } ) {
653 #-------------------------------------------------------------------------------------------------
654 package OpenILS::Reporter::SQLBuilder::Join;
655 use base qw/OpenILS::Reporter::SQLBuilder/;
659 $self = $self->SUPER::new if (!ref($self));
661 $self->{_left_rel} = shift;
662 $self->{_left_col} = shift;
664 $self->{_right_rel} = shift;
665 $self->{_right_col} = shift;
667 $self->{_right_rel}->is_join(1);
674 my $sql = "\n\tJOIN " . $self->{_right_rel}->toSQL .
675 ' ON ("' . $self->{_left_rel}->{_alias} . '"."' . $self->{_left_col} .
676 '" = "' . $self->{_right_rel}->{_alias} . '"."' . $self->{_right_col} . '")';
678 $sql .= $_->toSQL for (@{ $self->{_right_rel}->{_join} });