rt 4.0.23
[freeside.git] / rt / lib / RT / Tickets_SQL.pm
index ec1bb49..77313c3 100644 (file)
@@ -2,7 +2,7 @@
 #
 # COPYRIGHT:
 #
-# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
 #                                          <sales@bestpractical.com>
 #
 # (Except where explicitly superseded by other copyright notices)
@@ -57,10 +57,7 @@ use RT::SQL;
 # Import configuration data from the lexcial scope of __PACKAGE__ (or
 # at least where those two Subroutines are defined.)
 
-our (%FIELD_METADATA, %dispatch, %can_bundle);
-
-# Lower Case version of FIELDS, for case insensitivity
-my %lcfields = map { ( lc($_) => $_ ) } (keys %FIELD_METADATA);
+our (%FIELD_METADATA, %LOWER_CASE_FIELDS, %dispatch, %can_bundle);
 
 sub _InitSQL {
   my $self = shift;
@@ -77,7 +74,7 @@ sub _InitSQL {
 
 sub _SQLLimit {
   my $self = shift;
-    my %args = (@_);
+    my %args = (FIELD => '', @_);
     if ($args{'FIELD'} eq 'EffectiveId' &&
          (!$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) ) {
         $self->{'looking_at_effective_id'} = 1;
@@ -174,27 +171,77 @@ sub _parser {
     my @bundle;
     my $ea = '';
 
+    # Bundling of joins is implemented by dynamically tracking a parallel query
+    # tree in %sub_tree as the TicketSQL is parsed.  Don't be fooled by
+    # _close_bundle(), @bundle, and %can_bundle; they are completely unused for
+    # quite a long time and removed in RT 4.2.  For now they stay, a useless
+    # relic.
+    #
+    # Only positive, OR'd watcher conditions are bundled currently.  Each key
+    # in %sub_tree is a watcher type (Requestor, Cc, AdminCc) or the generic
+    # "Watcher" for any watcher type.  Owner is not bundled because it is
+    # denormalized into a Tickets column and doesn't need a join.  AND'd
+    # conditions are not bundled since a record may have multiple watchers
+    # which independently match the conditions, thus necessitating two joins.
+    #
+    # The values of %sub_tree are arrayrefs made up of:
+    #
+    #   * Open parentheses "(" pushed on by the OpenParen callback
+    #   * Arrayrefs of bundled join aliases pushed on by the Condition callback
+    #   * Entry aggregators (AND/OR) pushed on by the EntryAggregator callback
+    #
+    # The CloseParen callback takes care of backing off the query trees until
+    # outside of the just-closed parenthetical, thus restoring the tree state
+    # an equivalent of before the parenthetical was entered.
+    #
+    # The Condition callback handles starting a new subtree or extending an
+    # existing one, determining if bundling the current condition with any
+    # subtree is possible, and pruning any dangling entry aggregators from
+    # trees.
+    #
+
+    my %sub_tree;
+    my $depth = 0;
+
     my %callback;
     $callback{'OpenParen'} = sub {
       $self->_close_bundle(@bundle); @bundle = ();
-      $self->_OpenParen
+      $self->_OpenParen;
+      $depth++;
+      push @$_, '(' foreach values %sub_tree;
     };
     $callback{'CloseParen'} = sub {
       $self->_close_bundle(@bundle); @bundle = ();
       $self->_CloseParen;
+      $depth--;
+      foreach my $list ( values %sub_tree ) {
+          if ( $list->[-1] eq '(' ) {
+              pop @$list;
+              pop @$list if $list->[-1] =~ /^(?:AND|OR)$/i;
+          }
+          else {
+              pop @$list while $list->[-2] ne '(';
+              $list->[-1] = pop @$list;
+          }
+      }
+    };
+    $callback{'EntryAggregator'} = sub {
+      $ea = $_[0] || '';
+      push @$_, $ea foreach grep @$_ && $_->[-1] ne '(', values %sub_tree;
     };
-    $callback{'EntryAggregator'} = sub { $ea = $_[0] || '' };
     $callback{'Condition'} = sub {
         my ($key, $op, $value) = @_;
 
+        my ($negative_op, $null_op, $inv_op, $range_op)
+            = $self->ClassifySQLOperation( $op );
         # key has dot then it's compound variant and we have subkey
         my $subkey = '';
         ($key, $subkey) = ($1, $2) if $key =~ /^([^\.]+)\.(.+)$/;
 
         # normalize key and get class (type)
         my $class;
-        if (exists $lcfields{lc $key}) {
-            $key = $lcfields{lc $key};
+        if (exists $LOWER_CASE_FIELDS{lc $key}) {
+            $key = $LOWER_CASE_FIELDS{lc $key};
             $class = $FIELD_METADATA{$key}->[0];
         }
         die "Unknown field '$key' in '$string'" unless $class;
@@ -228,10 +275,28 @@ sub _parser {
         }
         else {
             $self->_close_bundle(@bundle); @bundle = ();
-            $sub->( $self, $key, $op, $value,
+            my @res; my $bundle_with;
+            if ( $class eq 'WATCHERFIELD' && $key ne 'Owner' && !$negative_op && (!$null_op || $subkey) ) {
+                if ( !$sub_tree{$key} ) {
+                  $sub_tree{$key} = [ ('(')x$depth, \@res ];
+                } else {
+                  $bundle_with = $self->_check_bundling_possibility( $string, @{ $sub_tree{$key} } );
+                  if ( $sub_tree{$key}[-1] eq '(' ) {
+                        push @{ $sub_tree{$key} }, \@res;
+                  }
+                }
+            }
+
+            # Remove our aggregator from subtrees where our condition didn't get added
+            pop @$_ foreach grep @$_ && $_->[-1] =~ /^(?:AND|OR)$/i, values %sub_tree;
+
+            # A reference to @res may be pushed onto $sub_tree{$key} from
+            # above, and we fill it here.
+            @res = $sub->( $self, $key, $op, $value,
                     SUBCLAUSE       => '',  # don't need anymore
                     ENTRYAGGREGATOR => $ea,
                     SUBKEY          => $subkey,
+                    BUNDLE          => $bundle_with,
                   );
         }
         $self->{_sql_looking_at}{lc $key} = 1;
@@ -241,6 +306,29 @@ sub _parser {
     $self->_close_bundle(@bundle); @bundle = ();
 }
 
+sub _check_bundling_possibility {
+    my $self = shift;
+    my $string = shift;
+    my @list = reverse @_;
+    while (my $e = shift @list) {
+        next if $e eq '(';
+        if ( lc($e) eq 'and' ) {
+            return undef;
+        }
+        elsif ( lc($e) eq 'or' ) {
+            return shift @list;
+        }
+        else {
+            # should not happen
+            $RT::Logger->error(
+                "Joins optimization failed when parsing '$string'. It's bug in RT, contact Best Practical"
+            );
+            die "Internal error. Contact your system administrator.";
+        }
+    }
+    return undef;
+}
+
 =head2 ClausesToSQL
 
 =cut
@@ -295,8 +383,9 @@ sub FromSQL {
     $self->{_sql_query} = $query;
     eval { $self->_parser( $query ); };
     if ( $@ ) {
-        $RT::Logger->error( $@ );
-        return (0, $@);
+        my $error = "$@";
+        $RT::Logger->error("Couldn't parse query: $error");
+        return (0, $error);
     }
 
     # We only want to look at EffectiveId's (mostly) for these searches.