608862a37f00ff1a3a5522553b4e81df37cc8761
[usit-rt.git] / lib / RT / Tickets_SQL.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
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
16 # from www.gnu.org.
17 #
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.
22 #
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.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
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.)
37 #
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.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 package RT::Tickets;
50
51 use strict;
52 use warnings;
53
54
55 use RT::SQL;
56
57 # Import configuration data from the lexcial scope of __PACKAGE__ (or
58 # at least where those two Subroutines are defined.)
59
60 our (%FIELD_METADATA, %LOWER_CASE_FIELDS, %dispatch, %can_bundle);
61
62 sub _InitSQL {
63   my $self = shift;
64
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'}    = {};
73 }
74
75 sub _SQLLimit {
76   my $self = shift;
77     my %args = (@_);
78     if ($args{'FIELD'} eq 'EffectiveId' &&
79          (!$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) ) {
80         $self->{'looking_at_effective_id'} = 1;
81     }      
82     
83     if ($args{'FIELD'} eq 'Type' &&
84          (!$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) ) {
85         $self->{'looking_at_type'} = 1;
86     }
87
88   # All SQL stuff goes into one SB subclause so we can deal with all
89   # the aggregation
90   $self->SUPER::Limit(%args,
91                       SUBCLAUSE => 'ticketsql');
92 }
93
94 sub _SQLJoin {
95   # All SQL stuff goes into one SB subclause so we can deal with all
96   # the aggregation
97   my $this = shift;
98
99   $this->SUPER::Join(@_,
100                      SUBCLAUSE => 'ticketsql');
101 }
102
103 # Helpers
104 sub _OpenParen {
105   $_[0]->SUPER::_OpenParen( 'ticketsql' );
106 }
107 sub _CloseParen {
108   $_[0]->SUPER::_CloseParen( 'ticketsql' );
109 }
110
111 =head1 SQL Functions
112
113 =cut
114
115 =head2 Robert's Simple SQL Parser
116
117 Documentation In Progress
118
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:
120
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.
126
127 And that stream of tokens is passed through the "machine" in order to build up a structure that looks like:
128
129        KEY OP VALUE
130   AND  KEY OP VALUE
131   OR   KEY OP VALUE
132
133 That also deals with parenthesis for nesting.  (The parentheses are
134 just handed off the SearchBuilder)
135
136 =cut
137
138 sub _close_bundle {
139     my ($self, @bundle) = @_;
140     return unless @bundle;
141
142     if ( @bundle == 1 ) {
143         $bundle[0]->{'dispatch'}->(
144             $self,
145             $bundle[0]->{'key'},
146             $bundle[0]->{'op'},
147             $bundle[0]->{'val'},
148             SUBCLAUSE       => '',
149             ENTRYAGGREGATOR => $bundle[0]->{ea},
150             SUBKEY          => $bundle[0]->{subkey},
151         );
152     }
153     else {
154         my @args;
155         foreach my $chunk (@bundle) {
156             push @args, [
157                 $chunk->{key},
158                 $chunk->{op},
159                 $chunk->{val},
160                 SUBCLAUSE       => '',
161                 ENTRYAGGREGATOR => $chunk->{ea},
162                 SUBKEY          => $chunk->{subkey},
163             ];
164         }
165         $bundle[0]->{dispatch}->( $self, \@args );
166     }
167 }
168
169 sub _parser {
170     my ($self,$string) = @_;
171     my @bundle;
172     my $ea = '';
173
174     my %callback;
175     $callback{'OpenParen'} = sub {
176       $self->_close_bundle(@bundle); @bundle = ();
177       $self->_OpenParen
178     };
179     $callback{'CloseParen'} = sub {
180       $self->_close_bundle(@bundle); @bundle = ();
181       $self->_CloseParen;
182     };
183     $callback{'EntryAggregator'} = sub { $ea = $_[0] || '' };
184     $callback{'Condition'} = sub {
185         my ($key, $op, $value) = @_;
186
187         # key has dot then it's compound variant and we have subkey
188         my $subkey = '';
189         ($key, $subkey) = ($1, $2) if $key =~ /^([^\.]+)\.(.+)$/;
190
191         # normalize key and get class (type)
192         my $class;
193         if (exists $LOWER_CASE_FIELDS{lc $key}) {
194             $key = $LOWER_CASE_FIELDS{lc $key};
195             $class = $FIELD_METADATA{$key}->[0];
196         }
197         die "Unknown field '$key' in '$string'" unless $class;
198
199         # replace __CurrentUser__ with id
200         $value = $self->CurrentUser->id if $value eq '__CurrentUser__';
201
202
203         unless( $dispatch{ $class } ) {
204             die "No dispatch method for class '$class'"
205         }
206         my $sub = $dispatch{ $class };
207
208         if ( $can_bundle{ $class }
209              && ( !@bundle
210                   || ( $bundle[-1]->{dispatch}  == $sub
211                        && $bundle[-1]->{key}    eq $key
212                        && $bundle[-1]->{subkey} eq $subkey
213                      )
214                 )
215            )
216         {
217             push @bundle, {
218                 dispatch => $sub,
219                 key      => $key,
220                 op       => $op,
221                 val      => $value,
222                 ea       => $ea,
223                 subkey   => $subkey,
224             };
225         }
226         else {
227             $self->_close_bundle(@bundle); @bundle = ();
228             $sub->( $self, $key, $op, $value,
229                     SUBCLAUSE       => '',  # don't need anymore
230                     ENTRYAGGREGATOR => $ea,
231                     SUBKEY          => $subkey,
232                   );
233         }
234         $self->{_sql_looking_at}{lc $key} = 1;
235         $ea = '';
236     };
237     RT::SQL::Parse($string, \%callback);
238     $self->_close_bundle(@bundle); @bundle = ();
239 }
240
241 =head2 ClausesToSQL
242
243 =cut
244
245 sub ClausesToSQL {
246   my $self = shift;
247   my $clauses = shift;
248   my @sql;
249
250   for my $f (keys %{$clauses}) {
251     my $sql;
252     my $first = 1;
253
254     # Build SQL from the data hash
255     for my $data ( @{ $clauses->{$f} } ) {
256       $sql .= $data->[0] unless $first; $first=0; # ENTRYAGGREGATOR
257       $sql .= " '". $data->[2] . "' ";            # FIELD
258       $sql .= $data->[3] . " ";                   # OPERATOR
259       $sql .= "'". $data->[4] . "' ";             # VALUE
260     }
261
262     push @sql, " ( " . $sql . " ) ";
263   }
264
265   return join("AND",@sql);
266 }
267
268 =head2 FromSQL
269
270 Convert a RT-SQL string into a set of SearchBuilder restrictions.
271
272 Returns (1, 'Status message') on success and (0, 'Error Message') on
273 failure.
274
275
276
277
278 =cut
279
280 sub FromSQL {
281     my ($self,$query) = @_;
282
283     {
284         # preserve first_row and show_rows across the CleanSlate
285         local ($self->{'first_row'}, $self->{'show_rows'});
286         $self->CleanSlate;
287     }
288     $self->_InitSQL();
289
290     return (1, $self->loc("No Query")) unless $query;
291
292     $self->{_sql_query} = $query;
293     eval { $self->_parser( $query ); };
294     if ( $@ ) {
295         $RT::Logger->error( $@ );
296         return (0, $@);
297     }
298
299     # We only want to look at EffectiveId's (mostly) for these searches.
300     unless ( exists $self->{_sql_looking_at}{'effectiveid'} ) {
301         #TODO, we shouldn't be hard #coding the tablename to main.
302         $self->SUPER::Limit( FIELD           => 'EffectiveId',
303                              VALUE           => 'main.id',
304                              ENTRYAGGREGATOR => 'AND',
305                              QUOTEVALUE      => 0,
306                            );
307     }
308     # FIXME: Need to bring this logic back in
309
310     #      if ($self->_isLimited && (! $self->{'looking_at_effective_id'})) {
311     #         $self->SUPER::Limit( FIELD => 'EffectiveId',
312     #               OPERATOR => '=',
313     #               QUOTEVALUE => 0,
314     #               VALUE => 'main.id');   #TODO, we shouldn't be hard coding the tablename to main.
315     #       }
316     # --- This is hardcoded above.  This comment block can probably go.
317     # Or, we need to reimplement the looking_at_effective_id toggle.
318
319     # Unless we've explicitly asked to look at a specific Type, we need
320     # to limit to it.
321     unless ( $self->{looking_at_type} ) {
322         $self->SUPER::Limit( FIELD => 'Type', VALUE => 'ticket' );
323     }
324
325     # We don't want deleted tickets unless 'allow_deleted_search' is set
326     unless( $self->{'allow_deleted_search'} ) {
327         $self->SUPER::Limit( FIELD    => 'Status',
328                              OPERATOR => '!=',
329                              VALUE => 'deleted',
330                            );
331     }
332
333     # set SB's dirty flag
334     $self->{'must_redo_search'} = 1;
335     $self->{'RecalcTicketLimits'} = 0;                                           
336
337     return (1, $self->loc("Valid Query"));
338 }
339
340 =head2 Query
341
342 Returns the query that this object was initialized with
343
344 =cut
345
346 sub Query {
347     return ($_[0]->{_sql_query});
348 }
349
350 {
351 my %inv = (
352     '=' => '!=', '!=' => '=', '<>' => '=',
353     '>' => '<=', '<' => '>=', '>=' => '<', '<=' => '>',
354     'is' => 'IS NOT', 'is not' => 'IS',
355     'like' => 'NOT LIKE', 'not like' => 'LIKE',
356     'matches' => 'NOT MATCHES', 'not matches' => 'MATCHES',
357     'startswith' => 'NOT STARTSWITH', 'not startswith' => 'STARTSWITH',
358     'endswith' => 'NOT ENDSWITH', 'not endswith' => 'ENDSWITH',
359 );
360
361 my %range = map { $_ => 1 } qw(> >= < <=);
362
363 sub ClassifySQLOperation {
364     my $self = shift;
365     my $op = shift;
366
367     my $is_negative = 0;
368     if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
369         $is_negative = 1;
370     }
371
372     my $is_null = 0;
373     if ( 'is not' eq lc($op) || 'is' eq lc($op) ) {
374         $is_null = 1;
375     }
376
377     return ($is_negative, $is_null, $inv{lc $op}, $range{lc $op});
378 } }
379
380 1;
381
382 =pod
383
384 =head2 Exceptions
385
386 Most of the RT code does not use Exceptions (die/eval) but it is used
387 in the TicketSQL code for simplicity and historical reasons.  Lest you
388 be worried that the dies will trigger user visible errors, all are
389 trapped via evals.
390
391 99% of the dies fall in subroutines called via FromSQL and then parse.
392 (This includes all of the _FooLimit routines in Tickets_Overlay.pm.)
393 The other 1% or so are via _ProcessRestrictions.
394
395 All dies are trapped by eval {}s, and will be logged at the 'error'
396 log level.  The general failure mode is to not display any tickets.
397
398 =head2 General Flow
399
400 Legacy Layer:
401
402    Legacy LimitFoo routines build up a RestrictionsHash
403
404    _ProcessRestrictions converts the Restrictions to Clauses
405    ([key,op,val,rest]).
406
407    Clauses are converted to RT-SQL (TicketSQL)
408
409 New RT-SQL Layer:
410
411    FromSQL calls the parser
412
413    The parser calls the _FooLimit routines to do DBIx::SearchBuilder
414    limits.
415
416 And then the normal SearchBuilder/Ticket routines are used for
417 display/navigation.
418
419 =cut
420