summaryrefslogtreecommitdiff
path: root/rt/lib/RT/Tickets_Overlay_SQL.pm
diff options
context:
space:
mode:
Diffstat (limited to 'rt/lib/RT/Tickets_Overlay_SQL.pm')
-rw-r--r--rt/lib/RT/Tickets_Overlay_SQL.pm534
1 files changed, 183 insertions, 351 deletions
diff --git a/rt/lib/RT/Tickets_Overlay_SQL.pm b/rt/lib/RT/Tickets_Overlay_SQL.pm
index 1112430..487bb48 100644
--- a/rt/lib/RT/Tickets_Overlay_SQL.pm
+++ b/rt/lib/RT/Tickets_Overlay_SQL.pm
@@ -1,8 +1,8 @@
# BEGIN BPS TAGGED BLOCK {{{
#
# COPYRIGHT:
-#
-# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
+#
+# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
# <jesse@bestpractical.com>
#
# (Except where explicitly superseded by other copyright notices)
@@ -45,17 +45,18 @@
# those contributions and any derivatives thereof.
#
# END BPS TAGGED BLOCK }}}
+
package RT::Tickets;
use strict;
use warnings;
+use RT::SQL;
+
# Import configuration data from the lexcial scope of __PACKAGE__ (or
# at least where those two Subroutines are defined.)
-my %FIELD_METADATA = %{FIELDS()};
-my %dispatch = %{dispatch()};
-my %can_bundle = %{can_bundle()};
+our (%FIELD_METADATA, %dispatch, %can_bundle);
# Lower Case version of FIELDS, for case insensitivity
my %lcfields = map { ( lc($_) => $_ ) } (keys %FIELD_METADATA);
@@ -63,25 +64,14 @@ my %lcfields = map { ( lc($_) => $_ ) } (keys %FIELD_METADATA);
sub _InitSQL {
my $self = shift;
- # How many of these do we actually still use?
-
- # Private Member Variales (which should get cleaned)
- $self->{'_sql_linksc'} = 0;
- $self->{'_sql_watchersc'} = 0;
- $self->{'_sql_keywordsc'} = 0;
- $self->{'_sql_subclause'} = "a";
- $self->{'_sql_first'} = 0;
- $self->{'_sql_opstack'} = [''];
- $self->{'_sql_linkalias'} = undef;
+ # Private Member Variables (which should get cleaned)
$self->{'_sql_transalias'} = undef;
$self->{'_sql_trattachalias'} = undef;
- $self->{'_sql_object_cf_alias'} = undef;
- $self->{'_sql_depth'} = 0;
- $self->{'_sql_localdepth'} = 0;
+ $self->{'_sql_cf_alias'} = undef;
+ $self->{'_sql_object_cfv_alias'} = undef;
+ $self->{'_sql_watcher_join_users_alias'} = undef;
$self->{'_sql_query'} = '';
$self->{'_sql_looking_at'} = {};
- $self->{'_sql_columns_to_display'} = [];
-
}
sub _SQLLimit {
@@ -147,229 +137,109 @@ just handed off the SearchBuilder)
=cut
-use Regexp::Common qw /delimited/;
-
-# States
-use constant VALUE => 1;
-use constant AGGREG => 2;
-use constant OP => 4;
-use constant OPEN_PAREN => 8;
-use constant CLOSE_PAREN => 16;
-use constant KEYWORD => 32;
-my @tokens = qw[VALUE AGGREG OP OPEN_PAREN CLOSE_PAREN KEYWORD];
-
-my $re_aggreg = qr[(?i:AND|OR)];
-my $re_delim = qr[$RE{delimited}{-delim=>qq{\'\"}}];
-my $re_value = qr[$re_delim|\d+|NULL];
-my $re_keyword = qr[$re_delim|(?:\{|\}|\w|\.)+];
-my $re_op = qr[=|!=|>=|<=|>|<|(?i:IS NOT)|(?i:IS)|(?i:NOT LIKE)|(?i:LIKE)]; # long to short
-my $re_open_paren = qr'\(';
-my $re_close_paren = qr'\)';
-
-sub _close_bundle
-{
- my ($self, @bundle) = @_;
- return unless @bundle;
- if (@bundle == 1) {
- $bundle[0]->{dispatch}->(
- $self,
- $bundle[0]->{key},
- $bundle[0]->{op},
- $bundle[0]->{val},
- SUBCLAUSE => "",
- ENTRYAGGREGATOR => $bundle[0]->{ea},
- SUBKEY => $bundle[0]->{subkey},
- );
- } else {
- my @args;
- for my $chunk (@bundle) {
- push @args, [
- $chunk->{key},
- $chunk->{op},
- $chunk->{val},
- SUBCLAUSE => "",
- ENTRYAGGREGATOR => $chunk->{ea},
- SUBKEY => $chunk->{subkey},
- ];
+sub _close_bundle {
+ my ($self, @bundle) = @_;
+ return unless @bundle;
+
+ if ( @bundle == 1 ) {
+ $bundle[0]->{'dispatch'}->(
+ $self,
+ $bundle[0]->{'key'},
+ $bundle[0]->{'op'},
+ $bundle[0]->{'val'},
+ SUBCLAUSE => '',
+ ENTRYAGGREGATOR => $bundle[0]->{ea},
+ SUBKEY => $bundle[0]->{subkey},
+ );
+ }
+ else {
+ my @args;
+ foreach my $chunk (@bundle) {
+ push @args, [
+ $chunk->{key},
+ $chunk->{op},
+ $chunk->{val},
+ SUBCLAUSE => '',
+ ENTRYAGGREGATOR => $chunk->{ea},
+ SUBKEY => $chunk->{subkey},
+ ];
+ }
+ $bundle[0]->{dispatch}->( $self, \@args );
}
- $bundle[0]->{dispatch}->(
- $self, \@args,
- );
- }
}
sub _parser {
- my ($self,$string) = @_;
- my $want = KEYWORD | OPEN_PAREN;
- my $last = undef;
-
- my $depth = 0;
- my @bundle;
-
- my ($ea,$key,$op,$value) = ("","","","");
-
- # order of matches in the RE is important.. op should come early,
- # because it has spaces in it. otherwise "NOT LIKE" might be parsed
- # as a keyword or value.
-
-
-
-
-
- while ($string =~ /(
- $re_aggreg
- |$re_op
- |$re_keyword
- |$re_value
- |$re_open_paren
- |$re_close_paren
- )/iogx ) {
- my $val = $1;
- my $current = 0;
-
- # Highest priority is last
- $current = OP if ($want & OP) && $val =~ /^$re_op$/io;
- $current = VALUE if ($want & VALUE) && $val =~ /^$re_value$/io;
- $current = KEYWORD if ($want & KEYWORD) && $val =~ /^$re_keyword$/io;
- $current = AGGREG if ($want & AGGREG) && $val =~ /^$re_aggreg$/io;
- $current = OPEN_PAREN if ($want & OPEN_PAREN) && $val =~ /^$re_open_paren$/io;
- $current = CLOSE_PAREN if ($want & CLOSE_PAREN) && $val =~ /^$re_close_paren$/io;
-
-
- unless ($current && $want & $current) {
- # Error
- # FIXME: I will only print out the highest $want value
- die "Error near ->$val<- expecting a ", $tokens[((log $want)/(log 2))], " in $string\n";
- }
-
- # State Machine:
-
- #$RT::Logger->debug("We've just found a '$current' called '$val'");
-
- # Parens are highest priority
- if ($current & OPEN_PAREN) {
- $self->_close_bundle(@bundle); @bundle = ();
- $depth++;
- $self->_OpenParen;
-
- $want = KEYWORD | OPEN_PAREN;
- }
- elsif ( $current & CLOSE_PAREN ) {
- $self->_close_bundle(@bundle); @bundle = ();
- $depth--;
+ my ($self,$string) = @_;
+ my @bundle;
+ my $ea = '';
+
+ my %callback;
+ $callback{'OpenParen'} = sub {
+ $self->_close_bundle(@bundle); @bundle = ();
+ $self->_OpenParen
+ };
+ $callback{'CloseParen'} = sub {
+ $self->_close_bundle(@bundle); @bundle = ();
$self->_CloseParen;
-
- $want = CLOSE_PAREN | AGGREG;
- }
- elsif ( $current & AGGREG ) {
- $ea = $val;
- $want = KEYWORD | OPEN_PAREN;
- }
- elsif ( $current & KEYWORD ) {
- $key = $val;
- $want = OP;
- }
- elsif ( $current & OP ) {
- $op = $val;
- $want = VALUE;
- }
- elsif ( $current & VALUE ) {
- $value = $val;
-
- # Remove surrounding quotes from $key, $val
- # (in future, simplify as for($key,$val) { action on $_ })
- if ($key =~ /$re_delim/o) {
- substr($key,0,1) = "";
- substr($key,-1,1) = "";
- }
- if ($val =~ /$re_delim/o) {
- substr($val,0,1) = "";
- substr($val,-1,1) = "";
- }
- # Unescape escaped characters
- $key =~ s!\\(.)!$1!g;
- $val =~ s!\\(.)!$1!g;
- # print "$ea Key=[$key] op=[$op] val=[$val]\n";
-
- # replace __CurrentUser__ with id
- $val = $self->CurrentUser->id if $val eq '__CurrentUser__';
-
-
- my $subkey = '';
- if ($key =~ /^(.+?)\.(.+)$/) {
- $key = $1;
- $subkey = $2;
- }
-
- my $class;
- if (exists $lcfields{lc $key}) {
- $key = $lcfields{lc $key};
- $class = $FIELD_METADATA{$key}->[0];
- }
- # no longer have a default, since CF's are now a real class, not fallthrough
- # fixme: "default class" is not Generic.
-
-
- die "Unknown field: $key" unless $class;
-
- $self->{_sql_localdepth} = 0;
- die "No such dispatch method: $class"
- unless exists $dispatch{$class};
- my $sub = $dispatch{$class} || die;;
- if ($can_bundle{$class} &&
- (!@bundle ||
- ($bundle[-1]->{dispatch} == $sub &&
- $bundle[-1]->{key} eq $key &&
- $bundle[-1]->{subkey} eq $subkey)))
- {
- push @bundle, {
- dispatch => $sub,
- key => $key,
- op => $op,
- val => $val,
- ea => $ea || "",
- subkey => $subkey,
- };
- } else {
- $self->_close_bundle(@bundle); @bundle = ();
- $sub->(
- $self,
- $key,
- $op,
- $val,
- SUBCLAUSE => "", # don't need anymore
- ENTRYAGGREGATOR => $ea || "",
- SUBKEY => $subkey,
- );
- }
-
- $self->{_sql_looking_at}{lc $key} = 1;
-
- ($ea,$key,$op,$value) = ("","","","");
-
- $want = CLOSE_PAREN | AGGREG;
- } else {
- die "I'm lost";
- }
-
- $last = $current;
- } # while
-
- $self->_close_bundle(@bundle); @bundle = ();
-
- die "Incomplete query"
- unless (($want | CLOSE_PAREN) || ($want | KEYWORD));
-
- die "Incomplete Query"
- unless ($last && ($last | CLOSE_PAREN) || ($last || VALUE));
-
- # This will never happen, because the parser will complain
- die "Mismatched parentheses"
- unless $depth == 0;
-
+ };
+ $callback{'EntryAggregator'} = sub { $ea = $_[0] || '' };
+ $callback{'Condition'} = sub {
+ my ($key, $op, $value) = @_;
+
+ # 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};
+ $class = $FIELD_METADATA{$key}->[0];
+ }
+ die "Unknown field '$key' in '$string'" unless $class;
+
+ # replace __CurrentUser__ with id
+ $value = $self->CurrentUser->id if $value eq '__CurrentUser__';
+
+
+ unless( $dispatch{ $class } ) {
+ die "No dispatch method for class '$class'"
+ }
+ my $sub = $dispatch{ $class };
+
+ if ( $can_bundle{ $class }
+ && ( !@bundle
+ || ( $bundle[-1]->{dispatch} == $sub
+ && $bundle[-1]->{key} eq $key
+ && $bundle[-1]->{subkey} eq $subkey
+ )
+ )
+ )
+ {
+ push @bundle, {
+ dispatch => $sub,
+ key => $key,
+ op => $op,
+ val => $value,
+ ea => $ea,
+ subkey => $subkey,
+ };
+ }
+ else {
+ $self->_close_bundle(@bundle); @bundle = ();
+ $sub->( $self, $key, $op, $value,
+ SUBCLAUSE => '', # don't need anymore
+ ENTRYAGGREGATOR => $ea,
+ SUBKEY => $subkey,
+ );
+ }
+ $self->{_sql_looking_at}{lc $key} = 1;
+ $ea = '';
+ };
+ RT::SQL::Parse($string, \%callback);
+ $self->_close_bundle(@bundle); @bundle = ();
}
-
=head2 ClausesToSQL
=cut
@@ -405,133 +275,68 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on
failure.
-=begin testing
-
-use RT::Tickets;
-use strict;
-
-my $tix = RT::Tickets->new($RT::SystemUser);
-{
- my $query = "Status = 'open'";
- my ($status, $msg) = $tix->FromSQL($query);
- ok ($status, "correct query") or diag("error: $msg");
-}
-my (@created,%created);
-my $string = 'subject/content SQL test';
-{
- my $t = RT::Ticket->new($RT::SystemUser);
- ok( $t->Create(Queue => 'General', Subject => $string), "Ticket Created");
- $created{ $t->Id }++; push @created, $t->Id;
-}
-
-{
- my $Message = MIME::Entity->build(
- Subject => 'this is my subject',
- From => 'jesse@example.com',
- Data => [ $string ],
- );
-
- my $t = RT::Ticket->new($RT::SystemUser);
- ok( $t->Create( Queue => 'General',
- Subject => 'another ticket',
- MIMEObj => $Message,
- MemberOf => $created[0]
- ),
- "Ticket Created"
- );
- $created{ $t->Id }++; push @created, $t->Id;
-}
+=cut
-{
- my $query = ("Subject LIKE '$string' OR Content LIKE '$string'");
- my ($status, $msg) = $tix->FromSQL($query);
- ok ($status, "correct query") or diag("error: $msg");
+sub FromSQL {
+ my ($self,$query) = @_;
- my $count = 0;
- while (my $tick = $tix->Next) {
- $count++ if $created{ $tick->id };
+ {
+ # preserve first_row and show_rows across the CleanSlate
+ local ($self->{'first_row'}, $self->{'show_rows'});
+ $self->CleanSlate;
}
- is ($count, scalar @created, "number of returned tickets same as entered");
-}
+ $self->_InitSQL();
-{
- my $query = "id = $created[0] OR MemberOf = $created[0]";
- my ($status, $msg) = $tix->FromSQL($query);
- ok ($status, "correct query") or diag("error: $msg");
+ return (1, $self->loc("No Query")) unless $query;
- my $count = 0;
- while (my $tick = $tix->Next) {
- $count++ if $created{ $tick->id };
+ $self->{_sql_query} = $query;
+ eval { $self->_parser( $query ); };
+ if ( $@ ) {
+ $RT::Logger->error( $@ );
+ return (0, $@);
}
- is ($count, scalar @created, "number of returned tickets same as entered");
-}
-
-
-=end testing
-
-=cut
-
-sub FromSQL {
- my ($self,$query) = @_;
-
- {
- # preserve first_row and show_rows across the CleanSlate
- local($self->{'first_row'}, $self->{'show_rows'});
- $self->CleanSlate;
- }
- $self->_InitSQL();
-
- return (1,$self->loc("No Query")) unless $query;
-
- $self->{_sql_query} = $query;
- eval { $self->_parser( $query ); };
- if ($@) {
- $RT::Logger->error( "Query error in <<$query>>:\n$@" );
- return(0,$@);
+ # We only want to look at EffectiveId's (mostly) for these searches.
+ unless ( exists $self->{_sql_looking_at}{'effectiveid'} ) {
+ #TODO, we shouldn't be hard #coding the tablename to main.
+ $self->SUPER::Limit( FIELD => 'EffectiveId',
+ VALUE => 'main.id',
+ ENTRYAGGREGATOR => 'AND',
+ QUOTEVALUE => 0,
+ );
}
- # We only want to look at EffectiveId's (mostly) for these searches.
- unless (exists $self->{_sql_looking_at}{'effectiveid'}) {
- $self->SUPER::Limit( FIELD => 'EffectiveId',
- ENTRYAGGREGATOR => 'AND',
- OPERATOR => '=',
- QUOTEVALUE => 0,
- VALUE => 'main.id'
- ); #TODO, we shouldn't be hard #coding the tablename to main.
+ # FIXME: Need to bring this logic back in
+
+ # if ($self->_isLimited && (! $self->{'looking_at_effective_id'})) {
+ # $self->SUPER::Limit( FIELD => 'EffectiveId',
+ # OPERATOR => '=',
+ # QUOTEVALUE => 0,
+ # VALUE => 'main.id'); #TODO, we shouldn't be hard coding the tablename to main.
+ # }
+ # --- This is hardcoded above. This comment block can probably go.
+ # Or, we need to reimplement the looking_at_effective_id toggle.
+
+ # Unless we've explicitly asked to look at a specific Type, we need
+ # to limit to it.
+ unless ( $self->{looking_at_type} ) {
+ $self->SUPER::Limit( FIELD => 'Type', VALUE => 'ticket' );
}
- # FIXME: Need to bring this logic back in
-
- # if ($self->_isLimited && (! $self->{'looking_at_effective_id'})) {
- # $self->SUPER::Limit( FIELD => 'EffectiveId',
- # OPERATOR => '=',
- # QUOTEVALUE => 0,
- # VALUE => 'main.id'); #TODO, we shouldn't be hard coding the tablename to main.
- # }
- # --- This is hardcoded above. This comment block can probably go.
- # Or, we need to reimplement the looking_at_effective_id toggle.
-
- # Unless we've explicitly asked to look at a specific Type, we need
- # to limit to it.
- unless ($self->{looking_at_type}) {
- $self->SUPER::Limit( FIELD => 'Type', OPERATOR => '=', VALUE => 'ticket');
- }
-
- # We don't want deleted tickets unless 'allow_deleted_search' is set
- unless( $self->{'allow_deleted_search'} ) {
- $self->SUPER::Limit(FIELD => 'Status',
- OPERATOR => '!=',
- VALUE => 'deleted');
- }
+ # We don't want deleted tickets unless 'allow_deleted_search' is set
+ unless( $self->{'allow_deleted_search'} ) {
+ $self->SUPER::Limit( FIELD => 'Status',
+ OPERATOR => '!=',
+ VALUE => 'deleted',
+ );
+ }
- # set SB's dirty flag
- $self->{'must_redo_search'} = 1;
- $self->{'RecalcTicketLimits'} = 0;
-
- return (1,$self->loc("Valid Query"));
+ # set SB's dirty flag
+ $self->{'must_redo_search'} = 1;
+ $self->{'RecalcTicketLimits'} = 0;
+ return (1, $self->loc("Valid Query"));
}
=head2 Query
@@ -541,11 +346,38 @@ Returns the query that this object was initialized with
=cut
sub Query {
- my $self = shift;
- return ($self->{_sql_query});
+ return ($_[0]->{_sql_query});
}
+{
+my %inv = (
+ '=' => '!=', '!=' => '=', '<>' => '=',
+ '>' => '<=', '<' => '>=', '>=' => '<', '<=' => '>',
+ 'is' => 'IS NOT', 'is not' => 'IS',
+ 'like' => 'NOT LIKE', 'not like' => 'LIKE',
+ 'matches' => 'NOT MATCHES', 'not matches' => 'MATCHES',
+ 'startswith' => 'NOT STARTSWITH', 'not startswith' => 'STARTSWITH',
+ 'endswith' => 'NOT ENDSWITH', 'not endswith' => 'ENDSWITH',
+);
+
+my %range = map { $_ => 1 } qw(> >= < <=);
+
+sub ClassifySQLOperation {
+ my $self = shift;
+ my $op = shift;
+
+ my $is_negative = 0;
+ if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
+ $is_negative = 1;
+ }
+
+ my $is_null = 0;
+ if ( 'is not' eq lc($op) || 'is' eq lc($op) ) {
+ $is_null = 1;
+ }
+ return ($is_negative, $is_null, $inv{lc $op}, $range{lc $op});
+} }
1;