query optimization for #25459
authorMark Wells <mark@freeside.biz>
Tue, 14 Jan 2014 02:55:29 +0000 (18:55 -0800)
committerMark Wells <mark@freeside.biz>
Tue, 14 Jan 2014 02:55:29 +0000 (18:55 -0800)
FS/FS/part_pkg_report_option.pm
httemplate/graph/cust_bill_pkg.cgi

index 16a4c98..372b119 100644 (file)
@@ -2,7 +2,7 @@ package FS::part_pkg_report_option;
 
 use strict;
 use base qw( FS::Record );
-use FS::Record qw( qsearch qsearchs );
+use FS::Record qw( qsearch qsearchs dbh );
 
 =head1 NAME
 
@@ -111,6 +111,48 @@ sub check {
 
 =back
 
+=head1 CLASS METHODS
+
+=over 4
+
+=item subsets OPTIONNUM, ...
+
+Given a list of report_option numbers, determines all combinations of those
+numbers that exist on actual package definitions.  For each such combination,
+returns an arrayref of report_option numbers, followed by an arrayref of
+corresponding report class names.  This is used for a search optimization.
+
+=cut
+
+# probably doesn't belong here, but there's not a better place for it
+# and optimizations are, by nature, hackish
+
+sub subsets {
+  my ($self, @nums) = @_;
+  my @optionnames = map { "'report_option_$_'" } @nums;
+  my $where = "WHERE optionname IN(".join(',',@optionnames).")"
+    if @nums;
+  my $subselect =
+    "SELECT pkgpart, replace(optionname, 'report_option_', '')::int AS num ".
+    "FROM part_pkg_option $where ".
+    "ORDER BY pkgpart, num";
+  my $select =
+    "SELECT DISTINCT array_to_string(array_agg(num), ','), ".
+    "array_to_string(array_agg(name), ',') ".
+    "FROM ($subselect) AS x JOIN part_pkg_report_option USING (num) ".
+    "GROUP BY pkgpart";
+  my $dbh = dbh;
+  my $sth = $dbh->prepare($select)
+    or die $dbh->errstr; # seriously, this should never happen
+  $sth->execute
+    or die $sth->errstr;
+  # return the first (only) column
+  map { [ split(',',$_->[0]) ],
+        [ split(',',$_->[1]) ] } @{ $sth->fetchall_arrayref };
+}
+
+=back
+
 =head1 BUGS
 
 Overlaps somewhat with pkg_class and pkg_category
index 1b31955..44498c8 100644 (file)
@@ -103,7 +103,7 @@ if ( $cgi->param('class_mode') eq 'report' ) {
   $value_col = 'classnum';
 }
 
-my @classnums = grep /^\d+$/, $cgi->param($class_param);
+my @classnums = sort {$a <=> $b} grep /^\d+$/, $cgi->param($class_param);
 my @classnames = map { if ( $_ ) {
                          my $class = qsearchs($class_table, {$value_col=>$_} );
                          $class->$name_col;
@@ -128,22 +128,24 @@ if ( $cgi->param('class_agg_break') eq 'aggregate' or
   if ( $cgi->param('class_mode') eq 'report' ) {
     # The new way:
     # Actually break down all subsets of the (selected) report classes.
-    my $powerset = sub {
-      my @set = [];
-      foreach my $x (@_) {
-        @set = map { $_, [ @$_, $x ] } @set;
+    my @subsets = FS::part_pkg_report_option->subsets(@classnums);
+    warn "SUBSETS:\n".Dumper(\@subsets)."\n\n";
+    my @classnum_space = @classnums;
+    @classnums = @classnames = ();
+    while(@subsets) {
+      my $these = shift @subsets;
+      # applied topology!
+      my $not_these = [ @classnum_space ];
+      my $i = 0;
+      foreach (@$these) {
+        $i++ until $not_these->[$i] == $_;
+        splice($not_these, $i, 1);
       }
-      @set;
-    };
-    @classnums = $powerset->(@classnums);
-    @classnames = $powerset->(@classnames);
-    # this is pairwise complementary to @classnums, because math
-    @not_classnums = reverse(@classnums);
-warn Dumper(\@classnums, \@classnames, \@not_classnums);
-    # remove the null set
-    shift @classnums;
-    shift @classnames;
-    shift @not_classnums;
+      push @classnums, $these;
+      push @not_classnums, $not_these;
+      push @classnames, shift @subsets;
+    } #while subsets
+    warn "COMPLEMENTS:\n".Dumper(\@not_classnums)."\n\n";
   }
   # else it's 'pkg', i.e. part_pkg.classnum, which is singular on pkgpart
   # and much simpler