[Rt-commit] rt branch, 4.4/columns-as-values-in-ticket-search, updated. rt-4.4.4-19-ge21fdf534

? sunnavy sunnavy at bestpractical.com
Tue Aug 13 16:58:22 EDT 2019


The branch, 4.4/columns-as-values-in-ticket-search has been updated
       via  e21fdf5346e44547c8100262846a81f0bbaa2608 (commit)
       via  2da669e38137c87646afd938293ea2a9e42e6ed6 (commit)
       via  0adc04189f71102052b19653f057722fead0ace2 (commit)
       via  ed6f82c59d37fe970bfb42b5005ad55d9fa331f2 (commit)
       via  7a74a0e07b81001c7710cdc2ad93264f3646d763 (commit)
       via  f790e5d499723552cb42568bed56b0c0fc8239de (commit)
      from  670d88dc7ebb8a0461c4055ebc9e20d62d04307c (commit)

Summary of changes:
 docs/query_builder.pod             | 15 +++---
 lib/RT/SQL.pm                      | 12 ++++-
 lib/RT/SearchBuilder.pm            | 33 ++++++++++++-
 lib/RT/SearchBuilder/Role/Roles.pm |  4 +-
 lib/RT/Tickets.pm                  | 60 +++++++++++++++++++++++-
 t/api/tickets.t                    | 95 +++++++++++++++++++++++++++++++++++++-
 6 files changed, 204 insertions(+), 15 deletions(-)

- Log -----------------------------------------------------------------
commit f790e5d499723552cb42568bed56b0c0fc8239de
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Sat Aug 10 00:04:46 2019 +0800

    Support a friendly syntax for custom field columns as values in ticket search
    
    The syntax is like:
    
        Subject = CF.Foo
        Subject = CF.Foo.Content
        Due < CF.{Beta Date}
        CF.IP = CF.IPRange.LargeContent

diff --git a/docs/query_builder.pod b/docs/query_builder.pod
index d778312d1..ae0ffd82b 100644
--- a/docs/query_builder.pod
+++ b/docs/query_builder.pod
@@ -194,12 +194,15 @@ the relative date value from a ticket instead of the literal string "Resolved".
 
 =item Search tickets where custom fields Foo and Bar have the same value
 
-    CF.Foo IS NOT NULL AND CF.Bar = ObjectCustomFieldValues_1.Content
+    CF.Foo = CF.Bar
 
-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.
+This is equal to:
+
+    CF.{Foo} = CF.{Bar}
+    CF.Foo = CF.Bar.Content
+
+To compare LargeContent instead:
+
+    CF.IP = CF.IPRange.LargeContent
 
 =back
diff --git a/lib/RT/SQL.pm b/lib/RT/SQL.pm
index d4c7c8057..e6545e32a 100644
--- a/lib/RT/SQL.pm
+++ b/lib/RT/SQL.pm
@@ -64,8 +64,16 @@ 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[[\w\.]+|[+-]?\d+|(?i:NULL)|$re_delim];
-my $re_keyword     = qr[[{}\w\.]+|$re_delim];
+
+# We need to support bare(not quoted) strings like CF.{Beta Date} to use the
+# content of related custom field as the value to compare, e.g.
+#
+#       Due < CF.{Beta Date}
+#
+# Support it in keyword part is mainly for consistency.
+
+my $re_value       = qr[(?i:CF)\.\{.+?\}(?:\.(?i:Content|LargeContent))?|[\w\.]+|[+-]?\d+|(?i:NULL)|$re_delim];
+my $re_keyword     = qr[(?i:CF)\.\{.+?\}(?:\.(?i:Content|LargeContent))?|[{}\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[\(];
 my $re_close_paren = qr[\)];
diff --git a/lib/RT/Tickets.pm b/lib/RT/Tickets.pm
index a88323627..eb404e609 100644
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@ -3186,8 +3186,16 @@ sub _parser {
             if ( !$quote_value && $value !~ /^(?:[+-]?[0-9]+|NULL)$/i ) {
                 my ( $class, $field );
 
+                # e.g. CF.Foo or CF.{Beta Date}
+                if ( $value =~ /^CF\.(?:\{(.*)}|(.*?))(?:\.(Content|LargeContent))?$/i ) {
+                    my $cf = $1 || $2;
+                    $field = $3 || 'Content';
+                    my ( $ocfvalias ) = $self->_CustomFieldJoin( $cf, $cf );
+                    $value = "$ocfvalias.$field";
+                    $class = 'RT::ObjectCustomFieldValues';
+                }
                 # e.g. ObjectCustomFieldValues_1.Content
-                if ( $value =~ /^(\w+?)(?:_\d+)?\.(\w+)$/ ) {
+                elsif ( $value =~ /^(\w+?)(?:_\d+)?\.(\w+)$/ ) {
                     my $table = $1;
                     $field = $2;
                     $class = $table =~ /main/i ? 'RT::Tickets' : "RT::$table";

commit 7a74a0e07b81001c7710cdc2ad93264f3646d763
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Sat Aug 10 01:13:13 2019 +0800

    Allow to specify CF Content/LargeContent columns in the keyword part of SQL
    
    So we can flexibly choose which column we want to compare, e.g. to
    check if 2 IPRange CFs are equal:
    
        CF.IPRange1.Content = CF.IPRange2.Content AND
        CF.IPRange1.LargeContent = CF.IPRange2.LargeContent

diff --git a/lib/RT/Tickets.pm b/lib/RT/Tickets.pm
index eb404e609..5c5a721ea 100644
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@ -1211,7 +1211,15 @@ sub _CustomFieldDecipher {
     $lookuptype ||= $self->_SingularClass->CustomFieldLookupType;
 
     my ($object, $field, $column) = ($string =~ /^(?:(.+?)\.)?\{(.+)\}(?:\.(Content|LargeContent))?$/);
-    $field ||= ($string =~ /^\{(.*?)\}$/)[0] || $string;
+    if ( !$field ) {
+        if ($string =~ /^(?:(\w+?)|\{(.*?)\})(?:\.(Content|LargeContent))?$/) {
+            $field = $1 || $2;
+            $column = $3;
+        }
+        else {
+            $field = $string;
+        }
+    }
 
     my ($cf, $applied_to);
 

commit ed6f82c59d37fe970bfb42b5005ad55d9fa331f2
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Wed Aug 14 04:39:58 2019 +0800

    Cast CF columns for Pg/Oracle when they are used as values in search
    
    Searches affected here are like Due = 'CF.{Beta Date}'

diff --git a/lib/RT/Tickets.pm b/lib/RT/Tickets.pm
index 5c5a721ea..cd9fbbb41 100644
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@ -3222,6 +3222,46 @@ sub _parser {
                 }
 
                 die $self->loc( "Wrong query, no such column '[_1]' in '[_2]'", $value, $string ) unless $valid;
+
+                if ( $class eq 'RT::ObjectCustomFieldValues' ) {
+                    if ( RT->Config->Get('DatabaseType') eq 'Pg' ) {
+                        my $cast_to;
+                        if ($subkey) {
+
+                            # like Requestor.id
+                            if ( $subkey eq 'id' ) {
+                                $cast_to = 'INTEGER';
+                            }
+                        }
+                        elsif ( my $meta = $self->RecordClass->_ClassAccessible->{$key} ) {
+                            if ( $meta->{is_numeric} ) {
+                                $cast_to = 'INTEGER';
+                            }
+                            elsif ( $meta->{type} eq 'datetime' ) {
+                                $cast_to = 'TIMESTAMP';
+                            }
+                        }
+
+                        $value = "CAST($value AS $cast_to)" if $cast_to;
+                    }
+                    elsif ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
+                        if ($subkey) {
+
+                            # like Requestor.id
+                            if ( $subkey eq 'id' ) {
+                                $value = "TO_NUMBER($value)";
+                            }
+                        }
+                        elsif ( my $meta = $self->RecordClass->_ClassAccessible->{$key} ) {
+                            if ( $meta->{is_numeric} ) {
+                                $value = "TO_NUMBER($value)";
+                            }
+                            elsif ( $meta->{type} eq 'datetime' ) {
+                                $value = "TO_DATE($value,'YYYY-MM-DD HH24:MI:SS')";
+                            }
+                        }
+                    }
+                }
             }
 
             # A reference to @res may be pushed onto $sub_tree{$key} from

commit 0adc04189f71102052b19653f057722fead0ace2
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Wed Aug 14 04:44:28 2019 +0800

    Cast columns for Pg/Oracle when they are used to compare with cf values
    
    Previously we had a rough version for Pg that casted all the columns no
    matter if it's necessary.  This commits tweaks this to cast only when
    necessary and also adds Oracle support.
    
    Searches affected here are like 'CF.{Beta Date}' = Due

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index 85322a497..b38590aa8 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -555,8 +555,37 @@ sub _LimitCustomField {
         my $type = $cf->Type;
 
         if ( !$args{QUOTEVALUE} ) {
-            if ( RT->Config->Get('DatabaseType') eq 'Pg' ) {
-                $value = "CAST($value AS VARCHAR)";
+            my ( $class, $field );
+
+            # e.g. Users_3.Name
+            if ( $value =~ /^(\w+?)(?:_\d+)?\.(\w+)$/ ) {
+                my $table = $1;
+                $field = $2;
+                $class = $table =~ /main/i ? 'RT::Tickets' : "RT::$table";
+            }
+            else {
+                $class = ref $self;
+                $field = $value;
+            }
+
+            if ( $class->can('RecordClass')
+                and ( my $record_class = $class->RecordClass ) )
+            {
+                if ( my $meta = $record_class->_ClassAccessible->{$field} ) {
+                    if ( RT->Config->Get('DatabaseType') eq 'Pg' ) {
+                        if ( $meta->{is_numeric} || $meta->{type} eq 'datetime' ) {
+                            $value = "CAST($value AS VARCHAR)";
+                        }
+                    }
+                    elsif ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
+                        if ( $meta->{is_numeric} ) {
+                            $value = "TO_CHAR($value)";
+                        }
+                        elsif ( $type eq 'datetime' ) {
+                            $value = "TO_CHAR($value, 'YYYY-MM-DD HH24:MI:SS')";
+                        }
+                    }
+                }
             }
 
             if ( $type eq 'Date' ) {

commit 2da669e38137c87646afd938293ea2a9e42e6ed6
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Wed Aug 14 04:46:17 2019 +0800

    Support role searches like Owner = CF.cid or Owner = Creator
    
    Previously RoleLimit did't consider the value could be a column name, so
    it always tried to load the user using the value as Name/EmailAddress.
    
    This commit skips loading user for columns(not quoted values) and sets
    field to compare to id as usually we store id in columns like Owner,
    Creator, etc.

diff --git a/lib/RT/SearchBuilder/Role/Roles.pm b/lib/RT/SearchBuilder/Role/Roles.pm
index 5980882d3..dd140514f 100644
--- a/lib/RT/SearchBuilder/Role/Roles.pm
+++ b/lib/RT/SearchBuilder/Role/Roles.pm
@@ -237,7 +237,7 @@ sub RoleLimit {
 
     # if it's equality op and search by Email or Name then we can preload user
     # we do it to help some DBs better estimate number of rows and get better plans
-    if ( $args{OPERATOR} =~ /^!?=$/
+    if ( $args{QUOTEVALUE} && $args{OPERATOR} =~ /^!?=$/
              && (!$args{FIELD} || $args{FIELD} eq 'Name' || $args{FIELD} eq 'EmailAddress') ) {
         my $o = RT::User->new( $self->CurrentUser );
         my $method =
@@ -257,7 +257,7 @@ sub RoleLimit {
         return;
     }
 
-    $args{FIELD} ||= 'EmailAddress';
+    $args{FIELD} ||= $args{QUOTEVALUE} ? 'EmailAddress' : 'id';
 
     my ($groups, $group_members, $users);
     if ( $args{'BUNDLE'} and @{$args{'BUNDLE'}}) {

commit e21fdf5346e44547c8100262846a81f0bbaa2608
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Wed Aug 14 04:47:47 2019 +0800

    More custom field related tests for columns as values in ticket search

diff --git a/t/api/tickets.t b/t/api/tickets.t
index 38dd0247e..895aca1f1 100644
--- a/t/api/tickets.t
+++ b/t/api/tickets.t
@@ -175,7 +175,100 @@ ok( $unlimittickets->Count > 0, "UnLimited tickets object should return tickets"
     ok( $ret, 'Ran query with Due as searched value' );
     my $count = $tickets->Count();
     ok $count == 1, "Found one ticket";
-    undef $count;
+
+    my $cf_foo = RT::Test->load_or_create_custom_field( Name => 'foo', Type => 'FreeformSingle', Queue => 0 );
+    my $cf_bar = RT::Test->load_or_create_custom_field( Name => 'bar', Type => 'FreeformSingle', Queue => 0 );
+    ok( $ticket->AddCustomFieldValue( Field => $cf_foo, Value => 'this rocks!' ) );
+
+    ( $ret, $msg ) = $tickets->FromSQL('CF.foo = CF.bar');
+    ok( $ret, 'Ran query with CF.foo = CF.bar' );
+    $count = $tickets->Count();
+    is( $count, 0, 'Found 0 tickets' );
+
+    ok( $ticket->AddCustomFieldValue( Field => $cf_bar, Value => 'this does not rock' ) );
+
+    ( $ret, $msg ) = $tickets->FromSQL('CF.foo = CF.bar');
+    ok( $ret, 'Ran query with CF.foo = CF.bar' );
+    $count = $tickets->Count();
+    is( $count, 0, 'Found 0 tickets' );
+
+    ok( $ticket->AddCustomFieldValue( Field => $cf_bar, Value => 'this rocks!' ) );
+
+    ( $ret, $msg ) = $tickets->FromSQL('CF.foo = CF.bar');
+    ok( $ret, 'Ran query with CF.foo = CF.bar' );
+    $count = $tickets->Count();
+    is( $count, 1, 'Found 1 ticket' );
+
+    ( $ret, $msg ) = $tickets->FromSQL('CF.foo = Owner');
+    ok( $ret, 'Ran query with CF.foo = Owner' );
+    $count = $tickets->Count();
+    is( $count, 0, 'Found 0 tickets' );
+
+    ok( $ticket->AddCustomFieldValue( Field => $cf_foo, Value => RT->Nobody->id ) );
+    ( $ret, $msg ) = $tickets->FromSQL('CF.foo = Owner');
+    ok( $ret, 'Ran query with CF.foo = Owner' );
+    $count = $tickets->Count();
+    is( $count, 1, 'Found 1 ticket' );
+
+    my $cf_beta = RT::Test->load_or_create_custom_field( Name => 'Beta Date', Type => 'DateTime', Queue => 0 );
+    ( $ret, $msg ) = $tickets->FromSQL('Due = CF.{Beta Date}');
+    ok( $ret, 'Ran query with Due = CF.{Beta Date}' );
+    $count = $tickets->Count();
+    is( $count, 0, 'Found 0 tickets' );
+
+    ok( $ticket->AddCustomFieldValue( Field => $cf_beta, Value => $date->ISO( Timezone => 'user' ) ) );
+    ( $ret, $msg ) = $tickets->FromSQL('Due = CF.{Beta Date}');
+    ok( $ret, 'Ran query with Due = CF.{Beta Date}' );
+    $count = $tickets->Count();
+    is( $count, 1, 'Found 1 ticket' );
+
+    ok( $ticket->AddCustomFieldValue( Field => $cf_beta, Value => $date->ISO( Timezone => 'user' ) ) );
+    ( $ret, $msg ) = $tickets->FromSQL('Due = CF.{Beta Date}.Content');
+    ok( $ret, 'Ran query with Due = CF.{Beta Date}.Content' );
+    $count = $tickets->Count();
+    is( $count, 1, 'Found 1 ticket' );
+
+    ok( $ticket->AddCustomFieldValue( Field => $cf_beta, Value => $date->ISO( Timezone => 'user' ) ) );
+    ( $ret, $msg ) = $tickets->FromSQL('CF.{Beta Date} = Due');
+    ok( $ret, 'Ran query with CF.{Beta Date} = Due' );
+    $count = $tickets->Count();
+    is( $count, 1, 'Found 1 ticket' );
+
+    my $cf_ip1  = RT::Test->load_or_create_custom_field( Name => 'IPRange 1', Type => 'IPAddressRangeSingle', Queue => 0 );
+    my $cf_ip2  = RT::Test->load_or_create_custom_field( Name => 'IPRange 2', Type => 'IPAddressRangeSingle', Queue => 0 );
+
+    ( $ret, $msg ) = $tickets->FromSQL('CF.{IPRange 1} = CF.{IPRange 2}');
+    ok( $ret, 'Ran query with CF.{IPRange 1} = CF.{IPRange 2}' );
+    $count = $tickets->Count();
+    is( $count, 0, 'Found 0 tickets' );
+
+    ok( $ticket->AddCustomFieldValue( Field => $cf_ip1, Value => '192.168.1.1-192.168.1.5' ));
+    ok( $ticket->AddCustomFieldValue( Field => $cf_ip2, Value => '192.168.1.1-192.168.1.6' ));
+
+    ( $ret, $msg ) = $tickets->FromSQL('CF.{IPRange 1}.Content = CF.{IPRange 2}.Content');
+    ok( $ret, 'Ran query with CF.{IPRange 1}.Content = CF.{IPRange 2}.Content' );
+    $count = $tickets->Count();
+    is( $count, 1, 'Found 1 ticket' );
+
+    ( $ret, $msg ) = $tickets->FromSQL('CF.{IPRange 1} = CF.{IPRange 2}');
+    ok( $ret, 'Ran query with CF.{IPRange 1} = CF.{IPRange 2}' );
+    $count = $tickets->Count();
+    TODO: {
+        local $TODO
+            = "It'll be great if we can automatially compare both Content and LargeContent for queries like CF.{IPRange 1} = CF.{IPRange 2}";
+        is( $count, 0, 'Found 0 tickets' );
+    }
+
+    ok( $ticket->AddCustomFieldValue( Field => $cf_ip2, Value => '192.168.1.1-192.168.1.5' ) );
+    ( $ret, $msg )
+        = $tickets->FromSQL(
+        'CF.{IPRange 1}.Content = CF.{IPRange 2}.Content AND CF.{IPRange 1}.LargeContent = CF.{IPRange 2}.LargeContent'
+        );
+    ok( $ret,
+        'Ran query with CF.{IPRange 1}.Content = CF.{IPRange 2}.Content AND CF.{IPRange 1}.LargeContent = CF.{IPRange 2}.LargeContent'
+      );
+    $count = $tickets->Count();
+    is( $count, 1, 'Found 1 ticket' );
 }
 
 done_testing;

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


More information about the rt-commit mailing list