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

? sunnavy sunnavy at bestpractical.com
Thu Jun 13 19:39:12 EDT 2019


The branch, 4.4/columns-as-values-in-ticket-search has been created
        at  dc1a5204c687d3d00d1f76598862311a4b49a103 (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

commit 74280dc783422058a3b2c11c1b464d89f9deed2e
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Wed Jun 12 04:43:00 2019 +0800

    Tweak preparse logic when columns are used as custom field values in search
    
    Usually we don't need to preparse columns as they themselves are not
    real values.  An exception is Date cf: to compare date cfs with datetime
    columns(like "Created", "Due", etc), we only need to compare the date
    part.

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index 09a775ec3..f7c33342c 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -475,6 +475,7 @@ sub _LimitCustomField {
                  OPERATOR     => '=',
                  KEY          => undef,
                  PREPARSE     => 1,
+                 QUOTEVALUE   => 1,
                  @_ );
 
     my $op     = delete $args{OPERATOR};
@@ -552,7 +553,13 @@ sub _LimitCustomField {
     ########## Content pre-parsing if we know things about the CF
     if ( blessed($cf) and delete $args{PREPARSE} ) {
         my $type = $cf->Type;
-        if ( $type eq 'IPAddress' ) {
+
+        if ( !$args{QUOTEVALUE} ) {
+            if ( $type eq 'Date' ) {
+                $value = "SUBSTR($value, 1,  10)";
+            }
+        }
+        elsif ( $type eq 'IPAddress' ) {
             my $parsed = RT::ObjectCustomFieldValue->ParseIP($value);
             if ($parsed) {
                 $value = $parsed;

commit dc1a5204c687d3d00d1f76598862311a4b49a103
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Jun 14 04:34:29 2019 +0800

    Cast columns when they are used to compare cf values for Pg
    
    This is to support comparing cf values(which are stored as varchar/text)
    with columns of other types like Owner.  Without this, Pg will error
    out:
    
        operator does not exist: character varying <> integer

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index f7c33342c..e5ceed8fb 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -555,6 +555,10 @@ sub _LimitCustomField {
         my $type = $cf->Type;
 
         if ( !$args{QUOTEVALUE} ) {
+            if ( RT->Config->Get('DatabaseType') eq 'Pg' ) {
+                $value = "CAST($value AS VARCHAR)";
+            }
+
             if ( $type eq 'Date' ) {
                 $value = "SUBSTR($value, 1,  10)";
             }

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


More information about the rt-commit mailing list