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