]> git.uio.no Git - usit-rt.git/blame - lib/RT/Tickets_SQL.pm
Upgrade to 4.0.10.
[usit-rt.git] / lib / RT / Tickets_SQL.pm
CommitLineData
84fb5b46
MKG
1# BEGIN BPS TAGGED BLOCK {{{
2#
3# COPYRIGHT:
4#
403d7b0b 5# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
84fb5b46
MKG
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
49package RT::Tickets;
50
51use strict;
52use warnings;
53
54
55use RT::SQL;
56
57# Import configuration data from the lexcial scope of __PACKAGE__ (or
58# at least where those two Subroutines are defined.)
59
403d7b0b 60our (%FIELD_METADATA, %LOWER_CASE_FIELDS, %dispatch, %can_bundle);
84fb5b46
MKG
61
62sub _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
75sub _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
94sub _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
104sub _OpenParen {
105 $_[0]->SUPER::_OpenParen( 'ticketsql' );
106}
107sub _CloseParen {
108 $_[0]->SUPER::_CloseParen( 'ticketsql' );
109}
110
111=head1 SQL Functions
112
113=cut
114
115=head2 Robert's Simple SQL Parser
116
117Documentation In Progress
118
119The 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
127And 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
133That also deals with parenthesis for nesting. (The parentheses are
134just handed off the SearchBuilder)
135
136=cut
137
138sub _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
169sub _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;
403d7b0b
MKG
193 if (exists $LOWER_CASE_FIELDS{lc $key}) {
194 $key = $LOWER_CASE_FIELDS{lc $key};
84fb5b46
MKG
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
245sub 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
270Convert a RT-SQL string into a set of SearchBuilder restrictions.
271
272Returns (1, 'Status message') on success and (0, 'Error Message') on
273failure.
274
275
276
277
278=cut
279
280sub 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
342Returns the query that this object was initialized with
343
344=cut
345
346sub Query {
347 return ($_[0]->{_sql_query});
348}
349
350{
351my %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
361my %range = map { $_ => 1 } qw(> >= < <=);
362
363sub 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
3801;
381
382=pod
383
384=head2 Exceptions
385
386Most of the RT code does not use Exceptions (die/eval) but it is used
387in the TicketSQL code for simplicity and historical reasons. Lest you
388be worried that the dies will trigger user visible errors, all are
389trapped via evals.
390
39199% of the dies fall in subroutines called via FromSQL and then parse.
392(This includes all of the _FooLimit routines in Tickets_Overlay.pm.)
393The other 1% or so are via _ProcessRestrictions.
394
395All dies are trapped by eval {}s, and will be logged at the 'error'
396log level. The general failure mode is to not display any tickets.
397
398=head2 General Flow
399
400Legacy 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
409New RT-SQL Layer:
410
411 FromSQL calls the parser
412
413 The parser calls the _FooLimit routines to do DBIx::SearchBuilder
414 limits.
415
416And then the normal SearchBuilder/Ticket routines are used for
417display/navigation.
418
419=cut
420