[Rt-commit] rt branch, 4.4/columns-as-values-in-ticket-search, created. rt-4.4.4-8-g1cad1ce87

Jim Brandt jbrandt at bestpractical.com
Fri Apr 5 16:27:09 EDT 2019


The branch, 4.4/columns-as-values-in-ticket-search has been created
        at  1cad1ce87520dffaab889966870ee9b6a9da523c (commit)

- Log -----------------------------------------------------------------
commit 528277f60977be25aa66d44a3d91bdd8619999e1
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Wed Mar 27 05:11:08 2019 +0800

    Support columns as values in ticket search
    
    So we can compare 2 fields directly, e.g.
    
        LastUpdated < Due
    
    To search tickets with multiple CFs being set to the same value, e.g.
    
        CF.Foo IS NOT NULL AND CF.Bar = ObjectCustomFieldValues_1.Content

diff --git a/lib/RT/Interface/Web/QueryBuilder/Tree.pm b/lib/RT/Interface/Web/QueryBuilder/Tree.pm
index 1ab187794..1440d2866 100644
--- a/lib/RT/Interface/Web/QueryBuilder/Tree.pm
+++ b/lib/RT/Interface/Web/QueryBuilder/Tree.pm
@@ -220,7 +220,7 @@ sub __LinearizeTree {
 
             if ( $op =~ /^IS( NOT)?$/i ) {
                 $value = 'NULL';
-            } elsif ( $value !~ /^[+-]?[0-9]+$/ ) {
+            } elsif ( $clause->{QuoteValue} ) {
                 $value =~ s/(['\\])/\\$1/g;
                 $value = "'$value'";
             }
@@ -269,7 +269,7 @@ sub ParseSQL {
     $callback{'CloseParen'} = sub { $node = $node->getParent };
     $callback{'EntryAggregator'} = sub { $node->setNodeValue( $_[0] ) };
     $callback{'Condition'} = sub {
-        my ($key, $op, $value) = @_;
+        my ($key, $op, $value, $value_is_quoted) = @_;
 
         my ($main_key, $subkey) = split /[.]/, $key, 2;
 
@@ -281,9 +281,14 @@ sub ParseSQL {
         # Hardcode value for IS / IS NOT
         $value = 'NULL' if $op =~ /^IS( NOT)?$/i;
 
-        my $clause = { Key => $main_key, Subkey => $subkey,
-                       Meta => $field{ $main_key },
-                       Op => $op, Value => $value };
+        my $clause = {
+            Key           => $main_key,
+            Subkey        => $subkey,
+            Meta          => $field{$main_key},
+            Op            => $op,
+            Value         => $value,
+            QuoteValue    => $value_is_quoted,
+        };
         $node->addChild( __PACKAGE__->new( $clause ) );
     };
     $callback{'Error'} = sub { push @results, @_ };
diff --git a/lib/RT/SQL.pm b/lib/RT/SQL.pm
index b3a396a99..d4c7c8057 100644
--- a/lib/RT/SQL.pm
+++ b/lib/RT/SQL.pm
@@ -64,7 +64,7 @@ my @tokens = qw[VALUE AGGREGATOR OPERATOR OPEN_PAREN CLOSE_PAREN KEYWORD];
 use Regexp::Common qw /delimited/;
 my $re_aggreg      = qr[(?i:AND|OR)];
 my $re_delim       = qr[$RE{delimited}{-delim=>qq{\'\"}}];
-my $re_value       = qr[[+-]?\d+|(?i:NULL)|$re_delim];
+my $re_value       = qr[[\w\.]+|[+-]?\d+|(?i:NULL)|$re_delim];
 my $re_keyword     = qr[[{}\w\.]+|$re_delim];
 my $re_op          = qr[=|!=|>=|<=|>|<|(?i:IS NOT)|(?i:IS)|(?i:NOT LIKE)|(?i:LIKE)|(?i:NOT STARTSWITH)|(?i:STARTSWITH)|(?i:NOT ENDSWITH)|(?i:ENDSWITH)]; # long to short
 my $re_open_paren  = qr[\(];
@@ -169,7 +169,8 @@ sub Parse {
                 s!\\(.)!$1!g;
             }
 
-            $cb->{'Condition'}->( $key, $op, $value );
+            my $value_is_quoted = $match =~ $re_delim ? 1 : 0;
+            $cb->{'Condition'}->( $key, $op, $value, $value_is_quoted );
 
             ($key,$op,$value) = ("","","");
             $want = AGGREG;
diff --git a/lib/RT/Tickets.pm b/lib/RT/Tickets.pm
index 5dcd50f07..6cce1f595 100644
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@ -631,10 +631,14 @@ sub _DateLimit {
         );
     }
 
-    my $date = RT::Date->new( $sb->CurrentUser );
-    $date->Set( Format => 'unknown', Value => $value );
+    my $date;
+    if ( $rest{QUOTEVALUE} ) {
+        $date = RT::Date->new( $sb->CurrentUser );
+        $date->Set( Format => 'unknown', Value => $value );
+    }
 
-    if ( $op eq "=" ) {
+
+    if ( $op eq "=" && $date ) {
 
         # if we're specifying =, that means we want everything on a
         # particular single day.  in the database, we need to check for >
@@ -667,10 +671,9 @@ sub _DateLimit {
     }
     else {
         $sb->Limit(
-            FUNCTION => $sb->NotSetDateToNullFunction,
             FIELD    => $meta->[1],
             OPERATOR => $op,
-            VALUE    => $date->ISO,
+            VALUE    => $date ? $date->ISO : $value,
             %rest,
         );
     }
@@ -3161,8 +3164,8 @@ sub _parser {
             $ea = $node->getParent->getNodeValue if $node->getIndex > 0;
             return $self->_OpenParen unless $node->isLeaf;
 
-            my ($key, $subkey, $meta, $op, $value, $bundle)
-                = @{$node->getNodeValue}{qw/Key Subkey Meta Op Value Bundle/};
+            my ($key, $subkey, $meta, $op, $value, $bundle, $quote_value)
+                = @{$node->getNodeValue}{qw/Key Subkey Meta Op Value Bundle QuoteValue/};
 
             # normalize key and get class (type)
             my $class = $meta->[0];
@@ -3176,12 +3179,38 @@ sub _parser {
             my $sub = $dispatch{ $class }
                 or die "No dispatch method for class '$class'";
 
+            if ( !$quote_value && $value !~ /^(?:[+-]?[0-9]+|NULL)$/i ) {
+                my ( $class, $field );
+
+                # e.g. ObjectCustomFieldValues_1.Content
+                if ( $value =~ /^(\w+?)(?:_\d+)?\.(\w+)$/ ) {
+                    my $table = $1;
+                    $field = $2;
+                    $class = $table =~ /main/i ? 'RT::Tickets' : "RT::$table";
+                }
+                else {
+                    $class = 'RT::Tickets';
+                    $field = $value;
+                }
+
+                my $valid;
+                if ( $class->can('RecordClass')
+                    and ( my $record_class = $class->RecordClass ) )
+                {
+                    $valid = $record_class->_ClassAccessible->{$field}
+                        && $record_class->_ClassAccessible->{$field}{read};
+                }
+
+                die $self->loc( "Wrong query, no such column '[_1]' in '[_2]'", $value, $string ) unless $valid;
+            }
+
             # A reference to @res may be pushed onto $sub_tree{$key} from
             # above, and we fill it here.
             $sub->( $self, $key, $op, $value,
                     ENTRYAGGREGATOR => $ea,
                     SUBKEY          => $subkey,
                     BUNDLE          => $bundle,
+                    QUOTEVALUE      => $quote_value,
                   );
         },
         sub {
diff --git a/share/html/Search/Build.html b/share/html/Search/Build.html
index b887b81e9..0acb07ae9 100644
--- a/share/html/Search/Build.html
+++ b/share/html/Search/Build.html
@@ -235,6 +235,7 @@ foreach my $arg ( keys %ARGS ) {
             Key   => $keyword,
             Op    => $op,
             Value => $value,
+            QuoteValue => $value =~ /^[+-]?[0-9]+$/ ? 0 : 1,
         };
 
         push @new_values, RT::Interface::Web::QueryBuilder::Tree->new($clause);

commit 10c640ef34bf49171934cceab4edee91072c99af
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Wed Mar 27 05:16:09 2019 +0800

    Add/Update tests for columns as values in ticket search

diff --git a/t/api/tickets.t b/t/api/tickets.t
index 407981baa..38dd0247e 100644
--- a/t/api/tickets.t
+++ b/t/api/tickets.t
@@ -151,14 +151,31 @@ ok( $unlimittickets->Count > 0, "UnLimited tickets object should return tickets"
     warning_like {
         ( $ret, $msg ) = $tickets->FromSQL( "LastUpdated < yesterday" );
     }
-    qr/Couldn't parse query: Wrong query, expecting a VALUE in 'LastUpdated < >yesterday<--here'/;
+    qr/Wrong query, no such column 'yesterday' in 'LastUpdated < yesterday'/;
 
     ok( !$ret, 'Invalid query' );
     like(
         $msg,
-        qr/Wrong query, expecting a VALUE in 'LastUpdated < >yesterday<--here'/,
+        qr/Wrong query, no such column 'yesterday' in 'LastUpdated < yesterday'/,
         'Invalid query message'
     );
 }
 
+{
+    my $ticket = RT::Ticket->new( RT->SystemUser );
+    ok $ticket->Load(1), "Loaded test ticket 1";
+    my $date = RT::Date->new(RT->SystemUser);
+    $date->SetToNow();
+    $date->AddDays(1);
+
+    ok $ticket->SetDue( $date->ISO ), "Set Due to tomorrow";
+    my $tickets = RT::Tickets->new( RT->SystemUser );
+    my ( $ret, $msg ) = $tickets->FromSQL("LastUpdated < Due");
+
+    ok( $ret, 'Ran query with Due as searched value' );
+    my $count = $tickets->Count();
+    ok $count == 1, "Found one ticket";
+    undef $count;
+}
+
 done_testing;

commit 1cad1ce87520dffaab889966870ee9b6a9da523c
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Tue Apr 2 22:20:24 2019 +0800

    Document columns as values in ticket search

diff --git a/docs/query_builder.pod b/docs/query_builder.pod
index 1195ea06e..d778312d1 100644
--- a/docs/query_builder.pod
+++ b/docs/query_builder.pod
@@ -158,3 +158,48 @@ piece of RT metadata, please see the Definitions of Ticket Metadata document.
 
 =cut
 
+=head1 Advanced
+
+In addition to the graphical query builder, RT also has an Advanced page
+where you can write and modify queries and formats directly. For example,
+you can type the following query directly in the Query box:
+
+    Status = 'resolved' AND Resolved > '2019-04-01' AND Queue = 'RT'
+
+and then click "Apply" to let RT validate it. RT will display any syntax errors
+if you make any mistakes so you can fix them.
+
+=head2 Valid Search Values
+
+In the above example, search values like C<'resolved'>, C<'2019-04-01'>,
+and C<'RT'> are all literal search terms: a status, a date, and a string
+representing the name of a queue. These are all static values that will then
+return matching tickets. However, sometimes you want to compare 2 columns
+in RT tables without knowing exact values. This is also supported, for
+example:
+
+=over
+
+=item Search tickets where LastUpdated is after (later than) Resolved
+
+    LastUpdated > Resolved
+
+This finds tickets that have been commented on or otherwise updated after
+they were resolved. Note that C<Resolved> is not quoted, indicating that it's
+the relative date value from a ticket instead of the literal string "Resolved".
+
+=item Search tickets where the Requestor is also the Owner
+
+    Requestor.id = Owner
+
+=item Search tickets where custom fields Foo and Bar have the same value
+
+    CF.Foo IS NOT NULL AND CF.Bar = ObjectCustomFieldValues_1.Content
+
+This advanced query requires some knowledge of RT's search internals,
+but following this example should allow most users to construct a
+similar query by changing the custom field names. It works because
+C<CF.Foo> appears first as a custom field in the query and RT then
+sets I<ObjectCustomFieldValues_1.Content> as its Content.
+
+=back

-----------------------------------------------------------------------


More information about the rt-commit mailing list