1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
57 # Import configuration data from the lexcial scope of __PACKAGE__ (or
58 # at least where those two Subroutines are defined.)
60 our (%FIELD_METADATA, %LOWER_CASE_FIELDS, %dispatch, %can_bundle);
65 # Private Member Variables (which should get cleaned)
66 $self->{'_sql_transalias'} = undef;
67 $self->{'_sql_trattachalias'} = undef;
68 $self->{'_sql_cf_alias'} = undef;
69 $self->{'_sql_object_cfv_alias'} = undef;
70 $self->{'_sql_watcher_join_users_alias'} = undef;
71 $self->{'_sql_query'} = '';
72 $self->{'_sql_looking_at'} = {};
77 my %args = (FIELD => '', @_);
78 if ($args{'FIELD'} eq 'EffectiveId' &&
79 (!$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) ) {
80 $self->{'looking_at_effective_id'} = 1;
83 if ($args{'FIELD'} eq 'Type' &&
84 (!$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) ) {
85 $self->{'looking_at_type'} = 1;
88 # All SQL stuff goes into one SB subclause so we can deal with all
90 $self->SUPER::Limit(%args,
91 SUBCLAUSE => 'ticketsql');
95 # All SQL stuff goes into one SB subclause so we can deal with all
99 $this->SUPER::Join(@_,
100 SUBCLAUSE => 'ticketsql');
105 $_[0]->SUPER::_OpenParen( 'ticketsql' );
108 $_[0]->SUPER::_CloseParen( 'ticketsql' );
115 =head2 Robert's Simple SQL Parser
117 Documentation In Progress
119 The Parser/Tokenizer is a relatively simple state machine that scans through a SQL WHERE clause type string extracting a token at a time (where a token is:
121 VALUE -> quoted string or number
122 AGGREGator -> AND or OR
123 KEYWORD -> quoted string or single word
124 OPerator -> =,!=,LIKE,etc..
125 PARENthesis -> open or close.
127 And that stream of tokens is passed through the "machine" in order to build up a structure that looks like:
133 That also deals with parenthesis for nesting. (The parentheses are
134 just handed off the SearchBuilder)
139 my ($self, @bundle) = @_;
140 return unless @bundle;
142 if ( @bundle == 1 ) {
143 $bundle[0]->{'dispatch'}->(
149 ENTRYAGGREGATOR => $bundle[0]->{ea},
150 SUBKEY => $bundle[0]->{subkey},
155 foreach my $chunk (@bundle) {
161 ENTRYAGGREGATOR => $chunk->{ea},
162 SUBKEY => $chunk->{subkey},
165 $bundle[0]->{dispatch}->( $self, \@args );
170 my ($self,$string) = @_;
174 # Bundling of joins is implemented by dynamically tracking a parallel query
175 # tree in %sub_tree as the TicketSQL is parsed. Don't be fooled by
176 # _close_bundle(), @bundle, and %can_bundle; they are completely unused for
177 # quite a long time and removed in RT 4.2. For now they stay, a useless
180 # Only positive, OR'd watcher conditions are bundled currently. Each key
181 # in %sub_tree is a watcher type (Requestor, Cc, AdminCc) or the generic
182 # "Watcher" for any watcher type. Owner is not bundled because it is
183 # denormalized into a Tickets column and doesn't need a join. AND'd
184 # conditions are not bundled since a record may have multiple watchers
185 # which independently match the conditions, thus necessitating two joins.
187 # The values of %sub_tree are arrayrefs made up of:
189 # * Open parentheses "(" pushed on by the OpenParen callback
190 # * Arrayrefs of bundled join aliases pushed on by the Condition callback
191 # * Entry aggregators (AND/OR) pushed on by the EntryAggregator callback
193 # The CloseParen callback takes care of backing off the query trees until
194 # outside of the just-closed parenthetical, thus restoring the tree state
195 # an equivalent of before the parenthetical was entered.
197 # The Condition callback handles starting a new subtree or extending an
198 # existing one, determining if bundling the current condition with any
199 # subtree is possible, and pruning any dangling entry aggregators from
207 $callback{'OpenParen'} = sub {
208 $self->_close_bundle(@bundle); @bundle = ();
211 push @$_, '(' foreach values %sub_tree;
213 $callback{'CloseParen'} = sub {
214 $self->_close_bundle(@bundle); @bundle = ();
217 foreach my $list ( values %sub_tree ) {
218 if ( $list->[-1] eq '(' ) {
220 pop @$list if $list->[-1] =~ /^(?:AND|OR)$/i;
223 pop @$list while $list->[-2] ne '(';
224 $list->[-1] = pop @$list;
228 $callback{'EntryAggregator'} = sub {
230 push @$_, $ea foreach grep @$_ && $_->[-1] ne '(', values %sub_tree;
232 $callback{'Condition'} = sub {
233 my ($key, $op, $value) = @_;
235 my ($negative_op, $null_op, $inv_op, $range_op)
236 = $self->ClassifySQLOperation( $op );
237 # key has dot then it's compound variant and we have subkey
239 ($key, $subkey) = ($1, $2) if $key =~ /^([^\.]+)\.(.+)$/;
241 # normalize key and get class (type)
243 if (exists $LOWER_CASE_FIELDS{lc $key}) {
244 $key = $LOWER_CASE_FIELDS{lc $key};
245 $class = $FIELD_METADATA{$key}->[0];
247 die "Unknown field '$key' in '$string'" unless $class;
249 # replace __CurrentUser__ with id
250 $value = $self->CurrentUser->id if $value eq '__CurrentUser__';
253 unless( $dispatch{ $class } ) {
254 die "No dispatch method for class '$class'"
256 my $sub = $dispatch{ $class };
258 if ( $can_bundle{ $class }
260 || ( $bundle[-1]->{dispatch} == $sub
261 && $bundle[-1]->{key} eq $key
262 && $bundle[-1]->{subkey} eq $subkey
277 $self->_close_bundle(@bundle); @bundle = ();
278 my @res; my $bundle_with;
279 if ( $class eq 'WATCHERFIELD' && $key ne 'Owner' && !$negative_op && (!$null_op || $subkey) ) {
280 if ( !$sub_tree{$key} ) {
281 $sub_tree{$key} = [ ('(')x$depth, \@res ];
283 $bundle_with = $self->_check_bundling_possibility( $string, @{ $sub_tree{$key} } );
284 if ( $sub_tree{$key}[-1] eq '(' ) {
285 push @{ $sub_tree{$key} }, \@res;
290 # Remove our aggregator from subtrees where our condition didn't get added
291 pop @$_ foreach grep @$_ && $_->[-1] =~ /^(?:AND|OR)$/i, values %sub_tree;
293 # A reference to @res may be pushed onto $sub_tree{$key} from
294 # above, and we fill it here.
295 @res = $sub->( $self, $key, $op, $value,
296 SUBCLAUSE => '', # don't need anymore
297 ENTRYAGGREGATOR => $ea,
299 BUNDLE => $bundle_with,
302 $self->{_sql_looking_at}{lc $key} = 1;
305 RT::SQL::Parse($string, \%callback);
306 $self->_close_bundle(@bundle); @bundle = ();
309 sub _check_bundling_possibility {
312 my @list = reverse @_;
313 while (my $e = shift @list) {
315 if ( lc($e) eq 'and' ) {
318 elsif ( lc($e) eq 'or' ) {
324 "Joins optimization failed when parsing '$string'. It's bug in RT, contact Best Practical"
326 die "Internal error. Contact your system administrator.";
341 for my $f (keys %{$clauses}) {
345 # Build SQL from the data hash
346 for my $data ( @{ $clauses->{$f} } ) {
347 $sql .= $data->[0] unless $first; $first=0; # ENTRYAGGREGATOR
348 $sql .= " '". $data->[2] . "' "; # FIELD
349 $sql .= $data->[3] . " "; # OPERATOR
350 $sql .= "'". $data->[4] . "' "; # VALUE
353 push @sql, " ( " . $sql . " ) ";
356 return join("AND",@sql);
361 Convert a RT-SQL string into a set of SearchBuilder restrictions.
363 Returns (1, 'Status message') on success and (0, 'Error Message') on
372 my ($self,$query) = @_;
375 # preserve first_row and show_rows across the CleanSlate
376 local ($self->{'first_row'}, $self->{'show_rows'});
381 return (1, $self->loc("No Query")) unless $query;
383 $self->{_sql_query} = $query;
384 eval { $self->_parser( $query ); };
387 $RT::Logger->error("Couldn't parse query: $error");
391 # We only want to look at EffectiveId's (mostly) for these searches.
392 unless ( exists $self->{_sql_looking_at}{'effectiveid'} ) {
393 #TODO, we shouldn't be hard #coding the tablename to main.
394 $self->SUPER::Limit( FIELD => 'EffectiveId',
396 ENTRYAGGREGATOR => 'AND',
400 # FIXME: Need to bring this logic back in
402 # if ($self->_isLimited && (! $self->{'looking_at_effective_id'})) {
403 # $self->SUPER::Limit( FIELD => 'EffectiveId',
406 # VALUE => 'main.id'); #TODO, we shouldn't be hard coding the tablename to main.
408 # --- This is hardcoded above. This comment block can probably go.
409 # Or, we need to reimplement the looking_at_effective_id toggle.
411 # Unless we've explicitly asked to look at a specific Type, we need
413 unless ( $self->{looking_at_type} ) {
414 $self->SUPER::Limit( FIELD => 'Type', VALUE => 'ticket' );
417 # We don't want deleted tickets unless 'allow_deleted_search' is set
418 unless( $self->{'allow_deleted_search'} ) {
419 $self->SUPER::Limit( FIELD => 'Status',
425 # set SB's dirty flag
426 $self->{'must_redo_search'} = 1;
427 $self->{'RecalcTicketLimits'} = 0;
429 return (1, $self->loc("Valid Query"));
434 Returns the query that this object was initialized with
439 return ($_[0]->{_sql_query});
444 '=' => '!=', '!=' => '=', '<>' => '=',
445 '>' => '<=', '<' => '>=', '>=' => '<', '<=' => '>',
446 'is' => 'IS NOT', 'is not' => 'IS',
447 'like' => 'NOT LIKE', 'not like' => 'LIKE',
448 'matches' => 'NOT MATCHES', 'not matches' => 'MATCHES',
449 'startswith' => 'NOT STARTSWITH', 'not startswith' => 'STARTSWITH',
450 'endswith' => 'NOT ENDSWITH', 'not endswith' => 'ENDSWITH',
453 my %range = map { $_ => 1 } qw(> >= < <=);
455 sub ClassifySQLOperation {
460 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
465 if ( 'is not' eq lc($op) || 'is' eq lc($op) ) {
469 return ($is_negative, $is_null, $inv{lc $op}, $range{lc $op});
478 Most of the RT code does not use Exceptions (die/eval) but it is used
479 in the TicketSQL code for simplicity and historical reasons. Lest you
480 be worried that the dies will trigger user visible errors, all are
483 99% of the dies fall in subroutines called via FromSQL and then parse.
484 (This includes all of the _FooLimit routines in Tickets_Overlay.pm.)
485 The other 1% or so are via _ProcessRestrictions.
487 All dies are trapped by eval {}s, and will be logged at the 'error'
488 log level. The general failure mode is to not display any tickets.
494 Legacy LimitFoo routines build up a RestrictionsHash
496 _ProcessRestrictions converts the Restrictions to Clauses
499 Clauses are converted to RT-SQL (TicketSQL)
503 FromSQL calls the parser
505 The parser calls the _FooLimit routines to do DBIx::SearchBuilder
508 And then the normal SearchBuilder/Ticket routines are used for