[Rt-commit] rt branch, 4.2/cf-searching, created. rt-4.1.8-168-g30c3ee6

Alex Vandiver alexmv at bestpractical.com
Sat Apr 27 02:13:51 EDT 2013


The branch, 4.2/cf-searching has been created
        at  30c3ee61bbc7b4c7b3f99826a8db86c8c8991eb6 (commit)

- Log -----------------------------------------------------------------
commit 890d47ef1013760ae225ce8190e0528cc1543871
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Jan 29 01:10:13 2013 -0500

    Move custom field search code into RT::SearchBuilder
    
    This merely moves two functions as-is, in preparation for refactoring
    them to be useful on non-Ticket classes.  As such, the code is placed in
    _LimitCustomField, and the existing LimitCustomField implementation is
    unchanged -- _LimitCustomField is still Ticket-specific.

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index cd1c089..bc47576 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -202,6 +202,105 @@ sub _SingularClass {
     return $class;
 }
 
+=head2 _CustomFieldJoin
+
+Factor out the Join of custom fields so we can use it for sorting too
+
+=cut
+
+sub _CustomFieldJoin {
+    my ($self, $cfkey, $cfid, $field) = @_;
+    # Perform one Join per CustomField
+    if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
+         $self->{_sql_cf_alias}{$cfkey} )
+    {
+        return ( $self->{_sql_object_cfv_alias}{$cfkey},
+                 $self->{_sql_cf_alias}{$cfkey} );
+    }
+
+    my ($TicketCFs, $CFs);
+    if ( $cfid ) {
+        $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
+            TYPE   => 'LEFT',
+            ALIAS1 => 'main',
+            FIELD1 => 'id',
+            TABLE2 => 'ObjectCustomFieldValues',
+            FIELD2 => 'ObjectId',
+        );
+        $self->Limit(
+            LEFTJOIN        => $TicketCFs,
+            FIELD           => 'CustomField',
+            VALUE           => $cfid,
+            ENTRYAGGREGATOR => 'AND'
+        );
+    }
+    else {
+        my $ocfalias = $self->Join(
+            TYPE       => 'LEFT',
+            FIELD1     => 'Queue',
+            TABLE2     => 'ObjectCustomFields',
+            FIELD2     => 'ObjectId',
+        );
+
+        $self->Limit(
+            LEFTJOIN        => $ocfalias,
+            ENTRYAGGREGATOR => 'OR',
+            FIELD           => 'ObjectId',
+            VALUE           => '0',
+        );
+
+        $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
+            TYPE       => 'LEFT',
+            ALIAS1     => $ocfalias,
+            FIELD1     => 'CustomField',
+            TABLE2     => 'CustomFields',
+            FIELD2     => 'id',
+        );
+        $self->Limit(
+            LEFTJOIN        => $CFs,
+            ENTRYAGGREGATOR => 'AND',
+            FIELD           => 'LookupType',
+            VALUE           => 'RT::Queue-RT::Ticket',
+        );
+        $self->Limit(
+            LEFTJOIN        => $CFs,
+            ENTRYAGGREGATOR => 'AND',
+            FIELD           => 'Name',
+            VALUE           => $field,
+        );
+
+        $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
+            TYPE   => 'LEFT',
+            ALIAS1 => $CFs,
+            FIELD1 => 'id',
+            TABLE2 => 'ObjectCustomFieldValues',
+            FIELD2 => 'CustomField',
+        );
+        $self->Limit(
+            LEFTJOIN        => $TicketCFs,
+            FIELD           => 'ObjectId',
+            VALUE           => 'main.id',
+            QUOTEVALUE      => 0,
+            ENTRYAGGREGATOR => 'AND',
+        );
+    }
+    $self->Limit(
+        LEFTJOIN        => $TicketCFs,
+        FIELD           => 'ObjectType',
+        VALUE           => 'RT::Ticket',
+        ENTRYAGGREGATOR => 'AND'
+    );
+    $self->Limit(
+        LEFTJOIN        => $TicketCFs,
+        FIELD           => 'Disabled',
+        OPERATOR        => '=',
+        VALUE           => '0',
+        ENTRYAGGREGATOR => 'AND'
+    );
+
+    return ($TicketCFs, $CFs);
+}
+
 sub LimitCustomField {
     my $self = shift;
     my %args = ( VALUE        => undef,
@@ -242,6 +341,380 @@ sub LimitCustomField {
     );
 }
 
+use Regexp::Common qw(RE_net_IPv4);
+use Regexp::Common::net::CIDR;
+
+sub _LimitCustomField {
+    my $self = shift;
+    my ( $field, $queue, $cfid, $cf, $column, $op, $value, %rest ) = @_;
+
+
+# If we're trying to find custom fields that don't match something, we
+# want tickets where the custom field has no value at all.  Note that
+# we explicitly don't include the "IS NULL" case, since we would
+# otherwise end up with a redundant clause.
+
+    my $negative_op = ($op eq '!=' || $op =~ /\bNOT\b/i);
+    my $null_op = ( 'is not' eq lc($op) || 'is' eq lc($op) );
+
+    my $fix_op = sub {
+        return @_ unless RT->Config->Get('DatabaseType') eq 'Oracle';
+
+        my %args = @_;
+        return %args unless $args{'FIELD'} eq 'LargeContent';
+        
+        my $op = $args{'OPERATOR'};
+        if ( $op eq '=' ) {
+            $args{'OPERATOR'} = 'MATCHES';
+        }
+        elsif ( $op eq '!=' ) {
+            $args{'OPERATOR'} = 'NOT MATCHES';
+        }
+        elsif ( $op =~ /^[<>]=?$/ ) {
+            $args{'FUNCTION'} = "TO_CHAR( $args{'ALIAS'}.LargeContent )";
+        }
+        return %args;
+    };
+
+    if ( $cf && $cf->Type eq 'IPAddress' ) {
+        my $parsed = RT::ObjectCustomFieldValue->ParseIP($value);
+        if ($parsed) {
+            $value = $parsed;
+        }
+        else {
+            $RT::Logger->warn("$value is not a valid IPAddress");
+        }
+    }
+
+    if ( $cf && $cf->Type eq 'IPAddressRange' ) {
+
+        if ( $value =~ /^\s*$RE{net}{CIDR}{IPv4}{-keep}\s*$/o ) {
+
+            # convert incomplete 192.168/24 to 192.168.0.0/24 format
+            $value =
+              join( '.', map $_ || 0, ( split /\./, $1 )[ 0 .. 3 ] ) . "/$2"
+              || $value;
+        }
+
+        my ( $start_ip, $end_ip ) =
+          RT::ObjectCustomFieldValue->ParseIPRange($value);
+        if ( $start_ip && $end_ip ) {
+            if ( $op =~ /^([<>])=?$/ ) {
+                my $is_less = $1 eq '<' ? 1 : 0;
+                if ( $is_less ) {
+                    $value = $start_ip;
+                }
+                else {
+                    $value = $end_ip;
+                }
+            }
+            else {
+                $value = join '-', $start_ip, $end_ip;
+            }
+        }
+        else {
+            $RT::Logger->warn("$value is not a valid IPAddressRange");
+        }
+    }
+
+    if ( $cf && $cf->Type =~ /^Date(?:Time)?$/ ) {
+        my $date = RT::Date->new( $self->CurrentUser );
+        $date->Set( Format => 'unknown', Value => $value );
+        if ( $date->Unix ) {
+
+            if (
+                   $cf->Type eq 'Date'
+                || $value =~ /^\s*(?:today|tomorrow|yesterday)\s*$/i
+                || (   $value !~ /midnight|\d+:\d+:\d+/i
+                    && $date->Time( Timezone => 'user' ) eq '00:00:00' )
+              )
+            {
+                $value = $date->Date( Timezone => 'user' );
+            }
+            else {
+                $value = $date->DateTime;
+            }
+        }
+        else {
+            $RT::Logger->warn("$value is not a valid date string");
+        }
+    }
+
+    my $single_value = !$cf || !$cfid || $cf->SingleValue;
+
+    my $cfkey = $cfid ? $cfid : "$queue.$field";
+
+    if ( $null_op && !$column ) {
+        # IS[ NOT] NULL without column is the same as has[ no] any CF value,
+        # we can reuse our default joins for this operation
+        # with column specified we have different situation
+        my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
+        $self->_OpenParen;
+        $self->Limit(
+            ALIAS    => $TicketCFs,
+            FIELD    => 'id',
+            OPERATOR => $op,
+            VALUE    => $value,
+            %rest
+        );
+        $self->Limit(
+            ALIAS      => $CFs,
+            FIELD      => 'Name',
+            OPERATOR   => 'IS NOT',
+            VALUE      => 'NULL',
+            QUOTEVALUE => 0,
+            ENTRYAGGREGATOR => 'AND',
+        ) if $CFs;
+        $self->_CloseParen;
+    }
+    elsif ( $op !~ /^[<>]=?$/ && (  $cf && $cf->Type eq 'IPAddressRange')) {
+    
+        my ($start_ip, $end_ip) = split /-/, $value;
+        
+        $self->_OpenParen;
+        if ( $op !~ /NOT|!=|<>/i ) { # positive equation
+            $self->_LimitCustomField(
+                $field, $queue, $cfid, $cf, 'Content', '<=', $end_ip, %rest,
+            );
+            $self->_LimitCustomField(
+                $field, $queue, $cfid, $cf, 'LargeContent', '>=', $start_ip, %rest,
+                ENTRYAGGREGATOR => 'AND',
+            ); 
+            # as well limit borders so DB optimizers can use better
+            # estimations and scan less rows
+# have to disable this tweak because of ipv6
+#            $self->_CustomFieldLimit(
+#                $field, '>=', '000.000.000.000', %rest,
+#                SUBKEY          => $rest{'SUBKEY'}. '.Content',
+#                ENTRYAGGREGATOR => 'AND',
+#            );
+#            $self->_CustomFieldLimit(
+#                $field, '<=', '255.255.255.255', %rest,
+#                SUBKEY          => $rest{'SUBKEY'}. '.LargeContent',
+#                ENTRYAGGREGATOR => 'AND',
+#            );  
+        }       
+        else { # negative equation
+            $self->_LimitCustomField( $field, $queue, $cfid, $cf, 'Content', '>', $end_ip, %rest);
+            $self->_LimitCustomField(
+                $field, $queue, $cfid, $cf, 'LargeContent', '<', $start_ip, %rest,
+                ENTRYAGGREGATOR => 'OR',
+            );  
+            # TODO: as well limit borders so DB optimizers can use better
+            # estimations and scan less rows, but it's harder to do
+            # as we have OR aggregator
+        }
+        $self->_CloseParen;
+    } 
+    elsif ( !$negative_op || $single_value ) {
+        $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if not $single_value and not $op =~ /^[<>]=?$/;
+        my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
+
+        $self->_OpenParen;
+
+        $self->_OpenParen;
+
+        $self->_OpenParen;
+        # if column is defined then deal only with it
+        # otherwise search in Content and in LargeContent
+        if ( $column ) {
+            $self->Limit( $fix_op->(
+                ALIAS      => $TicketCFs,
+                FIELD      => $column,
+                OPERATOR   => $op,
+                VALUE      => $value,
+                CASESENSITIVE => 0,
+                %rest
+            ) );
+            $self->_CloseParen;
+            $self->_CloseParen;
+            $self->_CloseParen;
+        }
+        else {
+            # need special treatment for Date
+            if ( $cf and $cf->Type eq 'DateTime' and $op eq '=' && $value !~ /:/ ) {
+                # no time specified, that means we want everything on a
+                # particular day.  in the database, we need to check for >
+                # and < the edges of that day.
+                my $date = RT::Date->new( $self->CurrentUser );
+                $date->Set( Format => 'unknown', Value => $value );
+                my $daystart = $date->ISO;
+                $date->AddDay;
+                my $dayend = $date->ISO;
+
+                $self->_OpenParen;
+
+                $self->Limit(
+                    ALIAS    => $TicketCFs,
+                    FIELD    => 'Content',
+                    OPERATOR => ">=",
+                    VALUE    => $daystart,
+                    %rest,
+                );
+
+                $self->Limit(
+                    ALIAS    => $TicketCFs,
+                    FIELD    => 'Content',
+                    OPERATOR => "<",
+                    VALUE    => $dayend,
+                    %rest,
+                    ENTRYAGGREGATOR => 'AND',
+                );
+
+                $self->_CloseParen;
+            }
+            elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
+                if ( length( Encode::encode_utf8($value) ) < 256 ) {
+                    $self->Limit(
+                        ALIAS    => $TicketCFs,
+                        FIELD    => 'Content',
+                        OPERATOR => $op,
+                        VALUE    => $value,
+                        CASESENSITIVE => 0,
+                        %rest
+                    );
+                }
+                else {
+                    $self->_OpenParen;
+                    $self->Limit(
+                        ALIAS           => $TicketCFs,
+                        FIELD           => 'Content',
+                        OPERATOR        => '=',
+                        VALUE           => '',
+                        ENTRYAGGREGATOR => 'OR'
+                    );
+                    $self->Limit(
+                        ALIAS           => $TicketCFs,
+                        FIELD           => 'Content',
+                        OPERATOR        => 'IS',
+                        VALUE           => 'NULL',
+                        ENTRYAGGREGATOR => 'OR'
+                    );
+                    $self->_CloseParen;
+                    $self->Limit( $fix_op->(
+                        ALIAS           => $TicketCFs,
+                        FIELD           => 'LargeContent',
+                        OPERATOR        => $op,
+                        VALUE           => $value,
+                        ENTRYAGGREGATOR => 'AND',
+                        CASESENSITIVE => 0,
+                    ) );
+                }
+            }
+            else {
+                $self->Limit(
+                    ALIAS    => $TicketCFs,
+                    FIELD    => 'Content',
+                    OPERATOR => $op,
+                    VALUE    => $value,
+                    CASESENSITIVE => 0,
+                    %rest
+                );
+
+                $self->_OpenParen;
+                $self->_OpenParen;
+                $self->Limit(
+                    ALIAS           => $TicketCFs,
+                    FIELD           => 'Content',
+                    OPERATOR        => '=',
+                    VALUE           => '',
+                    ENTRYAGGREGATOR => 'OR'
+                );
+                $self->Limit(
+                    ALIAS           => $TicketCFs,
+                    FIELD           => 'Content',
+                    OPERATOR        => 'IS',
+                    VALUE           => 'NULL',
+                    ENTRYAGGREGATOR => 'OR'
+                );
+                $self->_CloseParen;
+                $self->Limit( $fix_op->(
+                    ALIAS           => $TicketCFs,
+                    FIELD           => 'LargeContent',
+                    OPERATOR        => $op,
+                    VALUE           => $value,
+                    ENTRYAGGREGATOR => 'AND',
+                    CASESENSITIVE => 0,
+                ) );
+                $self->_CloseParen;
+            }
+            $self->_CloseParen;
+
+            # XXX: if we join via CustomFields table then
+            # because of order of left joins we get NULLs in
+            # CF table and then get nulls for those records
+            # in OCFVs table what result in wrong results
+            # as decifer method now tries to load a CF then
+            # we fall into this situation only when there
+            # are more than one CF with the name in the DB.
+            # the same thing applies to order by call.
+            # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
+            # we want treat IS NULL as (not applies or has
+            # no value)
+            $self->Limit(
+                ALIAS           => $CFs,
+                FIELD           => 'Name',
+                OPERATOR        => 'IS NOT',
+                VALUE           => 'NULL',
+                QUOTEVALUE      => 0,
+                ENTRYAGGREGATOR => 'AND',
+            ) if $CFs;
+            $self->_CloseParen;
+
+            if ($negative_op) {
+                $self->Limit(
+                    ALIAS           => $TicketCFs,
+                    FIELD           => $column || 'Content',
+                    OPERATOR        => 'IS',
+                    VALUE           => 'NULL',
+                    QUOTEVALUE      => 0,
+                    ENTRYAGGREGATOR => 'OR',
+                );
+            }
+
+            $self->_CloseParen;
+        }
+    }
+    else {
+        $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
+        my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
+
+        # reverse operation
+        $op =~ s/!|NOT\s+//i;
+
+        # if column is defined then deal only with it
+        # otherwise search in Content and in LargeContent
+        if ( $column ) {
+            $self->Limit( $fix_op->(
+                LEFTJOIN   => $TicketCFs,
+                ALIAS      => $TicketCFs,
+                FIELD      => $column,
+                OPERATOR   => $op,
+                VALUE      => $value,
+                CASESENSITIVE => 0,
+            ) );
+        }
+        else {
+            $self->Limit(
+                LEFTJOIN   => $TicketCFs,
+                ALIAS      => $TicketCFs,
+                FIELD      => 'Content',
+                OPERATOR   => $op,
+                VALUE      => $value,
+                CASESENSITIVE => 0,
+            );
+        }
+        $self->Limit(
+            %rest,
+            ALIAS      => $TicketCFs,
+            FIELD      => 'id',
+            OPERATOR   => 'IS',
+            VALUE      => 'NULL',
+            QUOTEVALUE => 0,
+        );
+    }
+}
+
 =head2 Limit PARAMHASH
 
 This Limit sub calls SUPER::Limit, but defaults "CASESENSITIVE" to 1, thus
diff --git a/lib/RT/Tickets.pm b/lib/RT/Tickets.pm
index 6437d78..26b84bf 100644
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@ -1083,105 +1083,6 @@ sub _CustomFieldDecipher {
     return ($queue, $field, $cf, $column);
 }
 
-=head2 _CustomFieldJoin
-
-Factor out the Join of custom fields so we can use it for sorting too
-
-=cut
-
-sub _CustomFieldJoin {
-    my ($self, $cfkey, $cfid, $field) = @_;
-    # Perform one Join per CustomField
-    if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
-         $self->{_sql_cf_alias}{$cfkey} )
-    {
-        return ( $self->{_sql_object_cfv_alias}{$cfkey},
-                 $self->{_sql_cf_alias}{$cfkey} );
-    }
-
-    my ($TicketCFs, $CFs);
-    if ( $cfid ) {
-        $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
-            TYPE   => 'LEFT',
-            ALIAS1 => 'main',
-            FIELD1 => 'id',
-            TABLE2 => 'ObjectCustomFieldValues',
-            FIELD2 => 'ObjectId',
-        );
-        $self->Limit(
-            LEFTJOIN        => $TicketCFs,
-            FIELD           => 'CustomField',
-            VALUE           => $cfid,
-            ENTRYAGGREGATOR => 'AND'
-        );
-    }
-    else {
-        my $ocfalias = $self->Join(
-            TYPE       => 'LEFT',
-            FIELD1     => 'Queue',
-            TABLE2     => 'ObjectCustomFields',
-            FIELD2     => 'ObjectId',
-        );
-
-        $self->Limit(
-            LEFTJOIN        => $ocfalias,
-            ENTRYAGGREGATOR => 'OR',
-            FIELD           => 'ObjectId',
-            VALUE           => '0',
-        );
-
-        $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
-            TYPE       => 'LEFT',
-            ALIAS1     => $ocfalias,
-            FIELD1     => 'CustomField',
-            TABLE2     => 'CustomFields',
-            FIELD2     => 'id',
-        );
-        $self->Limit(
-            LEFTJOIN        => $CFs,
-            ENTRYAGGREGATOR => 'AND',
-            FIELD           => 'LookupType',
-            VALUE           => 'RT::Queue-RT::Ticket',
-        );
-        $self->Limit(
-            LEFTJOIN        => $CFs,
-            ENTRYAGGREGATOR => 'AND',
-            FIELD           => 'Name',
-            VALUE           => $field,
-        );
-
-        $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
-            TYPE   => 'LEFT',
-            ALIAS1 => $CFs,
-            FIELD1 => 'id',
-            TABLE2 => 'ObjectCustomFieldValues',
-            FIELD2 => 'CustomField',
-        );
-        $self->Limit(
-            LEFTJOIN        => $TicketCFs,
-            FIELD           => 'ObjectId',
-            VALUE           => 'main.id',
-            QUOTEVALUE      => 0,
-            ENTRYAGGREGATOR => 'AND',
-        );
-    }
-    $self->Limit(
-        LEFTJOIN        => $TicketCFs,
-        FIELD           => 'ObjectType',
-        VALUE           => 'RT::Ticket',
-        ENTRYAGGREGATOR => 'AND'
-    );
-    $self->Limit(
-        LEFTJOIN        => $TicketCFs,
-        FIELD           => 'Disabled',
-        OPERATOR        => '=',
-        VALUE           => '0',
-        ENTRYAGGREGATOR => 'AND'
-    );
-
-    return ($TicketCFs, $CFs);
-}
-
 =head2 _CustomFieldLimit
 
 Limit based on CustomFields
@@ -1191,10 +1092,6 @@ Meta Data:
 
 =cut
 
-use Regexp::Common qw(RE_net_IPv4);
-use Regexp::Common::net::CIDR;
-
-
 sub _CustomFieldLimit {
     my ( $self, $_field, $op, $value, %rest ) = @_;
 
@@ -1206,373 +1103,7 @@ sub _CustomFieldLimit {
     ($queue, $field, $cf, $column) = $self->_CustomFieldDecipher( $field );
     $cfid = $cf ? $cf->id  : 0 ;
 
-# If we're trying to find custom fields that don't match something, we
-# want tickets where the custom field has no value at all.  Note that
-# we explicitly don't include the "IS NULL" case, since we would
-# otherwise end up with a redundant clause.
-
-    my $negative_op = ($op eq '!=' || $op =~ /\bNOT\b/i);
-    my $null_op = ( 'is not' eq lc($op) || 'is' eq lc($op) );
-
-    my $fix_op = sub {
-        return @_ unless RT->Config->Get('DatabaseType') eq 'Oracle';
-
-        my %args = @_;
-        return %args unless $args{'FIELD'} eq 'LargeContent';
-        
-        my $op = $args{'OPERATOR'};
-        if ( $op eq '=' ) {
-            $args{'OPERATOR'} = 'MATCHES';
-        }
-        elsif ( $op eq '!=' ) {
-            $args{'OPERATOR'} = 'NOT MATCHES';
-        }
-        elsif ( $op =~ /^[<>]=?$/ ) {
-            $args{'FUNCTION'} = "TO_CHAR( $args{'ALIAS'}.LargeContent )";
-        }
-        return %args;
-    };
-
-    if ( $cf && $cf->Type eq 'IPAddress' ) {
-        my $parsed = RT::ObjectCustomFieldValue->ParseIP($value);
-        if ($parsed) {
-            $value = $parsed;
-        }
-        else {
-            $RT::Logger->warn("$value is not a valid IPAddress");
-        }
-    }
-
-    if ( $cf && $cf->Type eq 'IPAddressRange' ) {
-
-        if ( $value =~ /^\s*$RE{net}{CIDR}{IPv4}{-keep}\s*$/o ) {
-
-            # convert incomplete 192.168/24 to 192.168.0.0/24 format
-            $value =
-              join( '.', map $_ || 0, ( split /\./, $1 )[ 0 .. 3 ] ) . "/$2"
-              || $value;
-        }
-
-        my ( $start_ip, $end_ip ) =
-          RT::ObjectCustomFieldValue->ParseIPRange($value);
-        if ( $start_ip && $end_ip ) {
-            if ( $op =~ /^([<>])=?$/ ) {
-                my $is_less = $1 eq '<' ? 1 : 0;
-                if ( $is_less ) {
-                    $value = $start_ip;
-                }
-                else {
-                    $value = $end_ip;
-                }
-            }
-            else {
-                $value = join '-', $start_ip, $end_ip;
-            }
-        }
-        else {
-            $RT::Logger->warn("$value is not a valid IPAddressRange");
-        }
-    }
-
-    if ( $cf && $cf->Type =~ /^Date(?:Time)?$/ ) {
-        my $date = RT::Date->new( $self->CurrentUser );
-        $date->Set( Format => 'unknown', Value => $value );
-        if ( $date->Unix ) {
-
-            if (
-                   $cf->Type eq 'Date'
-                || $value =~ /^\s*(?:today|tomorrow|yesterday)\s*$/i
-                || (   $value !~ /midnight|\d+:\d+:\d+/i
-                    && $date->Time( Timezone => 'user' ) eq '00:00:00' )
-              )
-            {
-                $value = $date->Date( Timezone => 'user' );
-            }
-            else {
-                $value = $date->DateTime;
-            }
-        }
-        else {
-            $RT::Logger->warn("$value is not a valid date string");
-        }
-    }
-
-    my $single_value = !$cf || !$cfid || $cf->SingleValue;
-
-    my $cfkey = $cfid ? $cfid : "$queue.$field";
-
-    if ( $null_op && !$column ) {
-        # IS[ NOT] NULL without column is the same as has[ no] any CF value,
-        # we can reuse our default joins for this operation
-        # with column specified we have different situation
-        my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
-        $self->_OpenParen;
-        $self->Limit(
-            ALIAS    => $TicketCFs,
-            FIELD    => 'id',
-            OPERATOR => $op,
-            VALUE    => $value,
-            %rest
-        );
-        $self->Limit(
-            ALIAS      => $CFs,
-            FIELD      => 'Name',
-            OPERATOR   => 'IS NOT',
-            VALUE      => 'NULL',
-            QUOTEVALUE => 0,
-            ENTRYAGGREGATOR => 'AND',
-        ) if $CFs;
-        $self->_CloseParen;
-    }
-    elsif ( $op !~ /^[<>]=?$/ && (  $cf && $cf->Type eq 'IPAddressRange')) {
-    
-        my ($start_ip, $end_ip) = split /-/, $value;
-        
-        $self->_OpenParen;
-        if ( $op !~ /NOT|!=|<>/i ) { # positive equation
-            $self->_CustomFieldLimit(
-                'CF', '<=', $end_ip, %rest,
-                SUBKEY => $rest{'SUBKEY'}. '.Content',
-            );
-            $self->_CustomFieldLimit(
-                'CF', '>=', $start_ip, %rest,
-                SUBKEY          => $rest{'SUBKEY'}. '.LargeContent',
-                ENTRYAGGREGATOR => 'AND',
-            ); 
-            # as well limit borders so DB optimizers can use better
-            # estimations and scan less rows
-# have to disable this tweak because of ipv6
-#            $self->_CustomFieldLimit(
-#                $field, '>=', '000.000.000.000', %rest,
-#                SUBKEY          => $rest{'SUBKEY'}. '.Content',
-#                ENTRYAGGREGATOR => 'AND',
-#            );
-#            $self->_CustomFieldLimit(
-#                $field, '<=', '255.255.255.255', %rest,
-#                SUBKEY          => $rest{'SUBKEY'}. '.LargeContent',
-#                ENTRYAGGREGATOR => 'AND',
-#            );  
-        }       
-        else { # negative equation
-            $self->_CustomFieldLimit($field, '>', $end_ip, %rest);
-            $self->_CustomFieldLimit(
-                $field, '<', $start_ip, %rest,
-                SUBKEY          => $rest{'SUBKEY'}. '.LargeContent',
-                ENTRYAGGREGATOR => 'OR',
-            );  
-            # TODO: as well limit borders so DB optimizers can use better
-            # estimations and scan less rows, but it's harder to do
-            # as we have OR aggregator
-        }
-        $self->_CloseParen;
-    } 
-    elsif ( !$negative_op || $single_value ) {
-        $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if not $single_value and not $op =~ /^[<>]=?$/;
-        my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
-
-        $self->_OpenParen;
-
-        $self->_OpenParen;
-
-        $self->_OpenParen;
-        # if column is defined then deal only with it
-        # otherwise search in Content and in LargeContent
-        if ( $column ) {
-            $self->Limit( $fix_op->(
-                ALIAS      => $TicketCFs,
-                FIELD      => $column,
-                OPERATOR   => $op,
-                VALUE      => $value,
-                CASESENSITIVE => 0,
-                %rest
-            ) );
-            $self->_CloseParen;
-            $self->_CloseParen;
-            $self->_CloseParen;
-        }
-        else {
-            # need special treatment for Date
-            if ( $cf and $cf->Type eq 'DateTime' and $op eq '=' && $value !~ /:/ ) {
-                # no time specified, that means we want everything on a
-                # particular day.  in the database, we need to check for >
-                # and < the edges of that day.
-                    my $date = RT::Date->new( $self->CurrentUser );
-                    $date->Set( Format => 'unknown', Value => $value );
-                    my $daystart = $date->ISO;
-                    $date->AddDay;
-                    my $dayend = $date->ISO;
-
-                    $self->_OpenParen;
-
-                    $self->Limit(
-                        ALIAS    => $TicketCFs,
-                        FIELD    => 'Content',
-                        OPERATOR => ">=",
-                        VALUE    => $daystart,
-                        %rest,
-                    );
-
-                    $self->Limit(
-                        ALIAS    => $TicketCFs,
-                        FIELD    => 'Content',
-                        OPERATOR => "<",
-                        VALUE    => $dayend,
-                        %rest,
-                        ENTRYAGGREGATOR => 'AND',
-                    );
-
-                    $self->_CloseParen;
-            }
-            elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
-                if ( length( Encode::encode_utf8($value) ) < 256 ) {
-                    $self->Limit(
-                        ALIAS    => $TicketCFs,
-                        FIELD    => 'Content',
-                        OPERATOR => $op,
-                        VALUE    => $value,
-                        CASESENSITIVE => 0,
-                        %rest
-                    );
-                }
-                else {
-                    $self->_OpenParen;
-                    $self->Limit(
-                        ALIAS           => $TicketCFs,
-                        FIELD           => 'Content',
-                        OPERATOR        => '=',
-                        VALUE           => '',
-                        ENTRYAGGREGATOR => 'OR'
-                    );
-                    $self->Limit(
-                        ALIAS           => $TicketCFs,
-                        FIELD           => 'Content',
-                        OPERATOR        => 'IS',
-                        VALUE           => 'NULL',
-                        ENTRYAGGREGATOR => 'OR'
-                    );
-                    $self->_CloseParen;
-                    $self->Limit( $fix_op->(
-                        ALIAS           => $TicketCFs,
-                        FIELD           => 'LargeContent',
-                        OPERATOR        => $op,
-                        VALUE           => $value,
-                        ENTRYAGGREGATOR => 'AND',
-                        CASESENSITIVE => 0,
-                    ) );
-                }
-            }
-            else {
-                $self->Limit(
-                    ALIAS    => $TicketCFs,
-                    FIELD    => 'Content',
-                    OPERATOR => $op,
-                    VALUE    => $value,
-                    CASESENSITIVE => 0,
-                    %rest
-                );
-
-                $self->_OpenParen;
-                $self->_OpenParen;
-                $self->Limit(
-                    ALIAS           => $TicketCFs,
-                    FIELD           => 'Content',
-                    OPERATOR        => '=',
-                    VALUE           => '',
-                    ENTRYAGGREGATOR => 'OR'
-                );
-                $self->Limit(
-                    ALIAS           => $TicketCFs,
-                    FIELD           => 'Content',
-                    OPERATOR        => 'IS',
-                    VALUE           => 'NULL',
-                    ENTRYAGGREGATOR => 'OR'
-                );
-                $self->_CloseParen;
-                $self->Limit( $fix_op->(
-                    ALIAS           => $TicketCFs,
-                    FIELD           => 'LargeContent',
-                    OPERATOR        => $op,
-                    VALUE           => $value,
-                    ENTRYAGGREGATOR => 'AND',
-                    CASESENSITIVE => 0,
-                ) );
-                $self->_CloseParen;
-            }
-            $self->_CloseParen;
-
-            # XXX: if we join via CustomFields table then
-            # because of order of left joins we get NULLs in
-            # CF table and then get nulls for those records
-            # in OCFVs table what result in wrong results
-            # as decifer method now tries to load a CF then
-            # we fall into this situation only when there
-            # are more than one CF with the name in the DB.
-            # the same thing applies to order by call.
-            # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
-            # we want treat IS NULL as (not applies or has
-            # no value)
-            $self->Limit(
-                ALIAS           => $CFs,
-                FIELD           => 'Name',
-                OPERATOR        => 'IS NOT',
-                VALUE           => 'NULL',
-                QUOTEVALUE      => 0,
-                ENTRYAGGREGATOR => 'AND',
-            ) if $CFs;
-            $self->_CloseParen;
-
-            if ($negative_op) {
-                $self->Limit(
-                    ALIAS           => $TicketCFs,
-                    FIELD           => $column || 'Content',
-                    OPERATOR        => 'IS',
-                    VALUE           => 'NULL',
-                    QUOTEVALUE      => 0,
-                    ENTRYAGGREGATOR => 'OR',
-                );
-            }
-
-            $self->_CloseParen;
-        }
-    }
-    else {
-        $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
-        my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
-
-        # reverse operation
-        $op =~ s/!|NOT\s+//i;
-
-        # if column is defined then deal only with it
-        # otherwise search in Content and in LargeContent
-        if ( $column ) {
-            $self->Limit( $fix_op->(
-                LEFTJOIN   => $TicketCFs,
-                ALIAS      => $TicketCFs,
-                FIELD      => $column,
-                OPERATOR   => $op,
-                VALUE      => $value,
-                CASESENSITIVE => 0,
-            ) );
-        }
-        else {
-            $self->Limit(
-                LEFTJOIN   => $TicketCFs,
-                ALIAS      => $TicketCFs,
-                FIELD      => 'Content',
-                OPERATOR   => $op,
-                VALUE      => $value,
-                CASESENSITIVE => 0,
-            );
-        }
-        $self->Limit(
-            %rest,
-            ALIAS      => $TicketCFs,
-            FIELD      => 'id',
-            OPERATOR   => 'IS',
-            VALUE      => 'NULL',
-            QUOTEVALUE => 0,
-        );
-    }
+    $self->_LimitCustomField( $field, $queue, $cfid, $cf, $column, $op, $value, %rest );
 }
 
 sub _HasAttributeLimit {

commit 12fc2f4ac64c26e3d77bbd071ace76def757239a
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Jan 29 01:18:52 2013 -0500

    Rename $TicketCFs variable to be more general

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index bc47576..8c11caf 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -218,9 +218,9 @@ sub _CustomFieldJoin {
                  $self->{_sql_cf_alias}{$cfkey} );
     }
 
-    my ($TicketCFs, $CFs);
+    my ($ocfvalias, $CFs);
     if ( $cfid ) {
-        $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
+        $ocfvalias = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
             TYPE   => 'LEFT',
             ALIAS1 => 'main',
             FIELD1 => 'id',
@@ -228,7 +228,7 @@ sub _CustomFieldJoin {
             FIELD2 => 'ObjectId',
         );
         $self->Limit(
-            LEFTJOIN        => $TicketCFs,
+            LEFTJOIN        => $ocfvalias,
             FIELD           => 'CustomField',
             VALUE           => $cfid,
             ENTRYAGGREGATOR => 'AND'
@@ -269,7 +269,7 @@ sub _CustomFieldJoin {
             VALUE           => $field,
         );
 
-        $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
+        $ocfvalias = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
             TYPE   => 'LEFT',
             ALIAS1 => $CFs,
             FIELD1 => 'id',
@@ -277,7 +277,7 @@ sub _CustomFieldJoin {
             FIELD2 => 'CustomField',
         );
         $self->Limit(
-            LEFTJOIN        => $TicketCFs,
+            LEFTJOIN        => $ocfvalias,
             FIELD           => 'ObjectId',
             VALUE           => 'main.id',
             QUOTEVALUE      => 0,
@@ -285,20 +285,20 @@ sub _CustomFieldJoin {
         );
     }
     $self->Limit(
-        LEFTJOIN        => $TicketCFs,
+        LEFTJOIN        => $ocfvalias,
         FIELD           => 'ObjectType',
         VALUE           => 'RT::Ticket',
         ENTRYAGGREGATOR => 'AND'
     );
     $self->Limit(
-        LEFTJOIN        => $TicketCFs,
+        LEFTJOIN        => $ocfvalias,
         FIELD           => 'Disabled',
         OPERATOR        => '=',
         VALUE           => '0',
         ENTRYAGGREGATOR => 'AND'
     );
 
-    return ($TicketCFs, $CFs);
+    return ($ocfvalias, $CFs);
 }
 
 sub LimitCustomField {
@@ -448,10 +448,10 @@ sub _LimitCustomField {
         # IS[ NOT] NULL without column is the same as has[ no] any CF value,
         # we can reuse our default joins for this operation
         # with column specified we have different situation
-        my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
+        my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
         $self->_OpenParen;
         $self->Limit(
-            ALIAS    => $TicketCFs,
+            ALIAS    => $ocfvalias,
             FIELD    => 'id',
             OPERATOR => $op,
             VALUE    => $value,
@@ -508,7 +508,7 @@ sub _LimitCustomField {
     } 
     elsif ( !$negative_op || $single_value ) {
         $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if not $single_value and not $op =~ /^[<>]=?$/;
-        my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
+        my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
 
         $self->_OpenParen;
 
@@ -519,7 +519,7 @@ sub _LimitCustomField {
         # otherwise search in Content and in LargeContent
         if ( $column ) {
             $self->Limit( $fix_op->(
-                ALIAS      => $TicketCFs,
+                ALIAS      => $ocfvalias,
                 FIELD      => $column,
                 OPERATOR   => $op,
                 VALUE      => $value,
@@ -545,7 +545,7 @@ sub _LimitCustomField {
                 $self->_OpenParen;
 
                 $self->Limit(
-                    ALIAS    => $TicketCFs,
+                    ALIAS    => $ocfvalias,
                     FIELD    => 'Content',
                     OPERATOR => ">=",
                     VALUE    => $daystart,
@@ -553,7 +553,7 @@ sub _LimitCustomField {
                 );
 
                 $self->Limit(
-                    ALIAS    => $TicketCFs,
+                    ALIAS    => $ocfvalias,
                     FIELD    => 'Content',
                     OPERATOR => "<",
                     VALUE    => $dayend,
@@ -566,7 +566,7 @@ sub _LimitCustomField {
             elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
                 if ( length( Encode::encode_utf8($value) ) < 256 ) {
                     $self->Limit(
-                        ALIAS    => $TicketCFs,
+                        ALIAS    => $ocfvalias,
                         FIELD    => 'Content',
                         OPERATOR => $op,
                         VALUE    => $value,
@@ -577,14 +577,14 @@ sub _LimitCustomField {
                 else {
                     $self->_OpenParen;
                     $self->Limit(
-                        ALIAS           => $TicketCFs,
+                        ALIAS           => $ocfvalias,
                         FIELD           => 'Content',
                         OPERATOR        => '=',
                         VALUE           => '',
                         ENTRYAGGREGATOR => 'OR'
                     );
                     $self->Limit(
-                        ALIAS           => $TicketCFs,
+                        ALIAS           => $ocfvalias,
                         FIELD           => 'Content',
                         OPERATOR        => 'IS',
                         VALUE           => 'NULL',
@@ -592,7 +592,7 @@ sub _LimitCustomField {
                     );
                     $self->_CloseParen;
                     $self->Limit( $fix_op->(
-                        ALIAS           => $TicketCFs,
+                        ALIAS           => $ocfvalias,
                         FIELD           => 'LargeContent',
                         OPERATOR        => $op,
                         VALUE           => $value,
@@ -603,7 +603,7 @@ sub _LimitCustomField {
             }
             else {
                 $self->Limit(
-                    ALIAS    => $TicketCFs,
+                    ALIAS    => $ocfvalias,
                     FIELD    => 'Content',
                     OPERATOR => $op,
                     VALUE    => $value,
@@ -614,14 +614,14 @@ sub _LimitCustomField {
                 $self->_OpenParen;
                 $self->_OpenParen;
                 $self->Limit(
-                    ALIAS           => $TicketCFs,
+                    ALIAS           => $ocfvalias,
                     FIELD           => 'Content',
                     OPERATOR        => '=',
                     VALUE           => '',
                     ENTRYAGGREGATOR => 'OR'
                 );
                 $self->Limit(
-                    ALIAS           => $TicketCFs,
+                    ALIAS           => $ocfvalias,
                     FIELD           => 'Content',
                     OPERATOR        => 'IS',
                     VALUE           => 'NULL',
@@ -629,7 +629,7 @@ sub _LimitCustomField {
                 );
                 $self->_CloseParen;
                 $self->Limit( $fix_op->(
-                    ALIAS           => $TicketCFs,
+                    ALIAS           => $ocfvalias,
                     FIELD           => 'LargeContent',
                     OPERATOR        => $op,
                     VALUE           => $value,
@@ -663,7 +663,7 @@ sub _LimitCustomField {
 
             if ($negative_op) {
                 $self->Limit(
-                    ALIAS           => $TicketCFs,
+                    ALIAS           => $ocfvalias,
                     FIELD           => $column || 'Content',
                     OPERATOR        => 'IS',
                     VALUE           => 'NULL',
@@ -677,7 +677,7 @@ sub _LimitCustomField {
     }
     else {
         $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
-        my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
+        my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
 
         # reverse operation
         $op =~ s/!|NOT\s+//i;
@@ -686,8 +686,8 @@ sub _LimitCustomField {
         # otherwise search in Content and in LargeContent
         if ( $column ) {
             $self->Limit( $fix_op->(
-                LEFTJOIN   => $TicketCFs,
-                ALIAS      => $TicketCFs,
+                LEFTJOIN   => $ocfvalias,
+                ALIAS      => $ocfvalias,
                 FIELD      => $column,
                 OPERATOR   => $op,
                 VALUE      => $value,
@@ -696,8 +696,8 @@ sub _LimitCustomField {
         }
         else {
             $self->Limit(
-                LEFTJOIN   => $TicketCFs,
-                ALIAS      => $TicketCFs,
+                LEFTJOIN   => $ocfvalias,
+                ALIAS      => $ocfvalias,
                 FIELD      => 'Content',
                 OPERATOR   => $op,
                 VALUE      => $value,
@@ -706,7 +706,7 @@ sub _LimitCustomField {
         }
         $self->Limit(
             %rest,
-            ALIAS      => $TicketCFs,
+            ALIAS      => $ocfvalias,
             FIELD      => 'id',
             OPERATOR   => 'IS',
             VALUE      => 'NULL',

commit 84998f28400cd21fb7eaa07f17a7baa6b327a59b
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Jan 29 01:23:18 2013 -0500

    Simplify _CustomFieldJoin to take a key and a CF||string
    
    The logic surrounding the arguments for _CustomFieldJoin is more complex
    than necessary; simplify it by expecting either a CF name of a CF object.

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index c39fa90..3d3336c 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -246,7 +246,7 @@ sub _FieldToFunction {
         unless ( $cf->id ) {
             $RT::Logger->error("Couldn't load CustomField #$cf_name");
         } else {
-            my ($ticket_cf_alias, $cf_alias) = $self->_CustomFieldJoin($cf->id, $cf->id, $cf_name);
+            my ($ticket_cf_alias, $cf_alias) = $self->_CustomFieldJoin($cf->id, $cf);
             @args{qw(ALIAS FIELD)} = ($ticket_cf_alias, 'Content');
         }
     } elsif ( $field =~ /^(?:(Owner|Creator|LastUpdatedBy))(?:\.(.*))?$/ ) {
diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index 8c11caf..0e8dccd 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -72,6 +72,8 @@ use base qw(DBIx::SearchBuilder RT::Base);
 use RT::Base;
 use DBIx::SearchBuilder "1.40";
 
+use Scalar::Util qw/blessed/;
+
 sub _Init  {
     my $self = shift;
     
@@ -209,7 +211,7 @@ Factor out the Join of custom fields so we can use it for sorting too
 =cut
 
 sub _CustomFieldJoin {
-    my ($self, $cfkey, $cfid, $field) = @_;
+    my ($self, $cfkey, $cf) = @_;
     # Perform one Join per CustomField
     if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
          $self->{_sql_cf_alias}{$cfkey} )
@@ -219,7 +221,7 @@ sub _CustomFieldJoin {
     }
 
     my ($ocfvalias, $CFs);
-    if ( $cfid ) {
+    if ( blessed($cf) ) {
         $ocfvalias = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
             TYPE   => 'LEFT',
             ALIAS1 => 'main',
@@ -230,7 +232,7 @@ sub _CustomFieldJoin {
         $self->Limit(
             LEFTJOIN        => $ocfvalias,
             FIELD           => 'CustomField',
-            VALUE           => $cfid,
+            VALUE           => $cf->id,
             ENTRYAGGREGATOR => 'AND'
         );
     }
@@ -266,7 +268,7 @@ sub _CustomFieldJoin {
             LEFTJOIN        => $CFs,
             ENTRYAGGREGATOR => 'AND',
             FIELD           => 'Name',
-            VALUE           => $field,
+            VALUE           => $cf,
         );
 
         $ocfvalias = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
@@ -448,7 +450,7 @@ sub _LimitCustomField {
         # IS[ NOT] NULL without column is the same as has[ no] any CF value,
         # we can reuse our default joins for this operation
         # with column specified we have different situation
-        my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
+        my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf || $field) );
         $self->_OpenParen;
         $self->Limit(
             ALIAS    => $ocfvalias,
@@ -508,7 +510,7 @@ sub _LimitCustomField {
     } 
     elsif ( !$negative_op || $single_value ) {
         $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if not $single_value and not $op =~ /^[<>]=?$/;
-        my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
+        my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf || $field) );
 
         $self->_OpenParen;
 
@@ -677,7 +679,7 @@ sub _LimitCustomField {
     }
     else {
         $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
-        my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
+        my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf || $field) );
 
         # reverse operation
         $op =~ s/!|NOT\s+//i;
diff --git a/lib/RT/Tickets.pm b/lib/RT/Tickets.pm
index 26b84bf..3a8e6e1 100644
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@ -1212,7 +1212,7 @@ sub OrderByCols {
            my ($queue, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
            my $cfkey = $cf_obj ? $cf_obj->id : "$queue.$field";
            $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
-           my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
+           my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj || $field) );
            # this is described in _CustomFieldLimit
            $self->Limit(
                ALIAS      => $CFs,

commit d799fe63fd85ca4576e613c1ec8e4fc4d98e3fb7
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Jan 29 01:29:18 2013 -0500

    Switch to a more standard paramhash for arguments
    
    This also collapses the $cfid, $cf, $queue, and $field arguments down
    into the more understandable KEY and CUSTOMFIELD.

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index 0e8dccd..a750ccb 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -348,8 +348,22 @@ use Regexp::Common::net::CIDR;
 
 sub _LimitCustomField {
     my $self = shift;
-    my ( $field, $queue, $cfid, $cf, $column, $op, $value, %rest ) = @_;
+    my %args = ( VALUE        => undef,
+                 CUSTOMFIELD  => undef,
+                 OPERATOR     => '=',
+                 KEY          => undef,
+                 @_ );
 
+    my $op     = delete $args{OPERATOR};
+    my $value  = delete $args{VALUE};
+    my $cf     = delete $args{CUSTOMFIELD};
+    my $column = delete $args{COLUMN};
+    my $cfkey  = delete $args{KEY};
+    if (blessed($cf) and $cf->id) {
+        $cfkey ||= $cf->id;
+    } else {
+        $cfkey ||= $cf;
+    }
 
 # If we're trying to find custom fields that don't match something, we
 # want tickets where the custom field has no value at all.  Note that
@@ -378,7 +392,7 @@ sub _LimitCustomField {
         return %args;
     };
 
-    if ( $cf && $cf->Type eq 'IPAddress' ) {
+    if ( blessed($cf) && $cf->Type eq 'IPAddress' ) {
         my $parsed = RT::ObjectCustomFieldValue->ParseIP($value);
         if ($parsed) {
             $value = $parsed;
@@ -388,7 +402,7 @@ sub _LimitCustomField {
         }
     }
 
-    if ( $cf && $cf->Type eq 'IPAddressRange' ) {
+    if ( blessed($cf) && $cf->Type eq 'IPAddressRange' ) {
 
         if ( $value =~ /^\s*$RE{net}{CIDR}{IPv4}{-keep}\s*$/o ) {
 
@@ -419,7 +433,7 @@ sub _LimitCustomField {
         }
     }
 
-    if ( $cf && $cf->Type =~ /^Date(?:Time)?$/ ) {
+    if ( blessed($cf) && $cf->Type =~ /^Date(?:Time)?$/ ) {
         my $date = RT::Date->new( $self->CurrentUser );
         $date->Set( Format => 'unknown', Value => $value );
         if ( $date->Unix ) {
@@ -442,22 +456,20 @@ sub _LimitCustomField {
         }
     }
 
-    my $single_value = !$cf || !$cfid || $cf->SingleValue;
-
-    my $cfkey = $cfid ? $cfid : "$queue.$field";
+    my $single_value = !blessed($cf) || $cf->SingleValue;
 
     if ( $null_op && !$column ) {
         # IS[ NOT] NULL without column is the same as has[ no] any CF value,
         # we can reuse our default joins for this operation
         # with column specified we have different situation
-        my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf || $field) );
+        my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cf );
         $self->_OpenParen;
         $self->Limit(
             ALIAS    => $ocfvalias,
             FIELD    => 'id',
             OPERATOR => $op,
             VALUE    => $value,
-            %rest
+            %args
         );
         $self->Limit(
             ALIAS      => $CFs,
@@ -469,19 +481,26 @@ sub _LimitCustomField {
         ) if $CFs;
         $self->_CloseParen;
     }
-    elsif ( $op !~ /^[<>]=?$/ && (  $cf && $cf->Type eq 'IPAddressRange')) {
-    
+    elsif ( $op !~ /^[<>]=?$/ && (  blessed($cf) && $cf->Type eq 'IPAddressRange')) {
         my ($start_ip, $end_ip) = split /-/, $value;
         
         $self->_OpenParen;
         if ( $op !~ /NOT|!=|<>/i ) { # positive equation
             $self->_LimitCustomField(
-                $field, $queue, $cfid, $cf, 'Content', '<=', $end_ip, %rest,
+                OPERATOR    => '<=',
+                VALUE       => $end_ip,
+                CUSTOMFIELD => $cf,
+                COLUMN      => 'Content',
+                %args,
             );
             $self->_LimitCustomField(
-                $field, $queue, $cfid, $cf, 'LargeContent', '>=', $start_ip, %rest,
+                OPERATOR    => '>=',
+                VALUE       => $start_ip,
+                CUSTOMFIELD => $cf,
+                COLUMN      => 'LargeContent',
+                %args,
                 ENTRYAGGREGATOR => 'AND',
-            ); 
+            );
             # as well limit borders so DB optimizers can use better
             # estimations and scan less rows
 # have to disable this tweak because of ipv6
@@ -497,11 +516,21 @@ sub _LimitCustomField {
 #            );  
         }       
         else { # negative equation
-            $self->_LimitCustomField( $field, $queue, $cfid, $cf, 'Content', '>', $end_ip, %rest);
             $self->_LimitCustomField(
-                $field, $queue, $cfid, $cf, 'LargeContent', '<', $start_ip, %rest,
+                OPERATOR    => '>',
+                VALUE       => $end_ip,
+                CUSTOMFIELD => $cf,
+                COLUMN      => 'Content',
+                %args,
+            );
+            $self->_LimitCustomField(
+                OPERATOR    => '<',
+                VALUE       => $start_ip,
+                CUSTOMFIELD => $cf,
+                COLUMN      => 'LargeContent',
+                %args,
                 ENTRYAGGREGATOR => 'OR',
-            );  
+            );
             # TODO: as well limit borders so DB optimizers can use better
             # estimations and scan less rows, but it's harder to do
             # as we have OR aggregator
@@ -510,7 +539,7 @@ sub _LimitCustomField {
     } 
     elsif ( !$negative_op || $single_value ) {
         $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if not $single_value and not $op =~ /^[<>]=?$/;
-        my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf || $field) );
+        my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cf );
 
         $self->_OpenParen;
 
@@ -526,7 +555,7 @@ sub _LimitCustomField {
                 OPERATOR   => $op,
                 VALUE      => $value,
                 CASESENSITIVE => 0,
-                %rest
+                %args
             ) );
             $self->_CloseParen;
             $self->_CloseParen;
@@ -534,7 +563,7 @@ sub _LimitCustomField {
         }
         else {
             # need special treatment for Date
-            if ( $cf and $cf->Type eq 'DateTime' and $op eq '=' && $value !~ /:/ ) {
+            if ( blessed($cf) and $cf->Type eq 'DateTime' and $op eq '=' && $value !~ /:/ ) {
                 # no time specified, that means we want everything on a
                 # particular day.  in the database, we need to check for >
                 # and < the edges of that day.
@@ -551,7 +580,7 @@ sub _LimitCustomField {
                     FIELD    => 'Content',
                     OPERATOR => ">=",
                     VALUE    => $daystart,
-                    %rest,
+                    %args,
                 );
 
                 $self->Limit(
@@ -559,7 +588,7 @@ sub _LimitCustomField {
                     FIELD    => 'Content',
                     OPERATOR => "<",
                     VALUE    => $dayend,
-                    %rest,
+                    %args,
                     ENTRYAGGREGATOR => 'AND',
                 );
 
@@ -573,7 +602,7 @@ sub _LimitCustomField {
                         OPERATOR => $op,
                         VALUE    => $value,
                         CASESENSITIVE => 0,
-                        %rest
+                        %args
                     );
                 }
                 else {
@@ -610,7 +639,7 @@ sub _LimitCustomField {
                     OPERATOR => $op,
                     VALUE    => $value,
                     CASESENSITIVE => 0,
-                    %rest
+                    %args
                 );
 
                 $self->_OpenParen;
@@ -679,7 +708,7 @@ sub _LimitCustomField {
     }
     else {
         $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
-        my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf || $field) );
+        my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cf );
 
         # reverse operation
         $op =~ s/!|NOT\s+//i;
@@ -707,7 +736,7 @@ sub _LimitCustomField {
             );
         }
         $self->Limit(
-            %rest,
+            %args,
             ALIAS      => $ocfvalias,
             FIELD      => 'id',
             OPERATOR   => 'IS',
diff --git a/lib/RT/Tickets.pm b/lib/RT/Tickets.pm
index 3a8e6e1..cb7f6da 100644
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@ -1101,9 +1101,17 @@ sub _CustomFieldLimit {
 
     my ($queue, $cfid, $cf, $column);
     ($queue, $field, $cf, $column) = $self->_CustomFieldDecipher( $field );
-    $cfid = $cf ? $cf->id  : 0 ;
 
-    $self->_LimitCustomField( $field, $queue, $cfid, $cf, $column, $op, $value, %rest );
+
+    $self->_LimitCustomField(
+        %rest,
+        CUSTOMFIELD => $cf || $field,
+        KEY      => $cf ? $cf->id : "$queue.$field",
+        OPERATOR => $op,
+        VALUE    => $value,
+        COLUMN   => $column,
+        SUBCLAUSE => "ticketsql",
+    );
 }
 
 sub _HasAttributeLimit {

commit 4dff5928064de2375399b0b37f1069ff7485d2a7
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Fri Apr 26 19:07:54 2013 -0400

    Prevent confusion about what %args can override
    
    As the basic arguments are delete()'d from %args, all the remains
    therein are extra arguments, like ENTRYAGGREGATOR or SUBCLAUSE.  Clarify
    the precedence of the arguments by moving %args to the beginning of the
    Limit() call; this changes no functionality.

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index a750ccb..285a8ac 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -465,11 +465,11 @@ sub _LimitCustomField {
         my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cf );
         $self->_OpenParen;
         $self->Limit(
+            %args,
             ALIAS    => $ocfvalias,
             FIELD    => 'id',
             OPERATOR => $op,
             VALUE    => $value,
-            %args
         );
         $self->Limit(
             ALIAS      => $CFs,
@@ -487,18 +487,18 @@ sub _LimitCustomField {
         $self->_OpenParen;
         if ( $op !~ /NOT|!=|<>/i ) { # positive equation
             $self->_LimitCustomField(
+                %args,
                 OPERATOR    => '<=',
                 VALUE       => $end_ip,
                 CUSTOMFIELD => $cf,
                 COLUMN      => 'Content',
-                %args,
             );
             $self->_LimitCustomField(
+                %args,
                 OPERATOR    => '>=',
                 VALUE       => $start_ip,
                 CUSTOMFIELD => $cf,
                 COLUMN      => 'LargeContent',
-                %args,
                 ENTRYAGGREGATOR => 'AND',
             );
             # as well limit borders so DB optimizers can use better
@@ -517,18 +517,18 @@ sub _LimitCustomField {
         }       
         else { # negative equation
             $self->_LimitCustomField(
+                %args,
                 OPERATOR    => '>',
                 VALUE       => $end_ip,
                 CUSTOMFIELD => $cf,
                 COLUMN      => 'Content',
-                %args,
             );
             $self->_LimitCustomField(
+                %args,
                 OPERATOR    => '<',
                 VALUE       => $start_ip,
                 CUSTOMFIELD => $cf,
                 COLUMN      => 'LargeContent',
-                %args,
                 ENTRYAGGREGATOR => 'OR',
             );
             # TODO: as well limit borders so DB optimizers can use better
@@ -550,12 +550,12 @@ sub _LimitCustomField {
         # otherwise search in Content and in LargeContent
         if ( $column ) {
             $self->Limit( $fix_op->(
+                %args,
                 ALIAS      => $ocfvalias,
                 FIELD      => $column,
                 OPERATOR   => $op,
                 VALUE      => $value,
                 CASESENSITIVE => 0,
-                %args
             ) );
             $self->_CloseParen;
             $self->_CloseParen;
@@ -576,19 +576,19 @@ sub _LimitCustomField {
                 $self->_OpenParen;
 
                 $self->Limit(
+                    %args,
                     ALIAS    => $ocfvalias,
                     FIELD    => 'Content',
                     OPERATOR => ">=",
                     VALUE    => $daystart,
-                    %args,
                 );
 
                 $self->Limit(
+                    %args,
                     ALIAS    => $ocfvalias,
                     FIELD    => 'Content',
                     OPERATOR => "<",
                     VALUE    => $dayend,
-                    %args,
                     ENTRYAGGREGATOR => 'AND',
                 );
 
@@ -597,12 +597,12 @@ sub _LimitCustomField {
             elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
                 if ( length( Encode::encode_utf8($value) ) < 256 ) {
                     $self->Limit(
+                        %args,
                         ALIAS    => $ocfvalias,
                         FIELD    => 'Content',
                         OPERATOR => $op,
                         VALUE    => $value,
                         CASESENSITIVE => 0,
-                        %args
                     );
                 }
                 else {
@@ -634,12 +634,12 @@ sub _LimitCustomField {
             }
             else {
                 $self->Limit(
+                    %args,
                     ALIAS    => $ocfvalias,
                     FIELD    => 'Content',
                     OPERATOR => $op,
                     VALUE    => $value,
                     CASESENSITIVE => 0,
-                    %args
                 );
 
                 $self->_OpenParen;

commit 8ee65ec20ab1b85b3895d84069f5b3671e4e7711
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Jan 29 01:30:20 2013 -0500

    Generalize _CustomFieldJoin somewhat
    
    This replaces explicit RT::Ticket ObjectTypes and LookupTypes with their
    more general forms.  It also factors out the concept of listing all
    applicable CFs that match a given name -- which may be additionally
    limited by queues, in the case of tickets.

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index 285a8ac..f77e01c 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -237,59 +237,14 @@ sub _CustomFieldJoin {
         );
     }
     else {
-        my $ocfalias = $self->Join(
-            TYPE       => 'LEFT',
-            FIELD1     => 'Queue',
-            TABLE2     => 'ObjectCustomFields',
-            FIELD2     => 'ObjectId',
-        );
-
-        $self->Limit(
-            LEFTJOIN        => $ocfalias,
-            ENTRYAGGREGATOR => 'OR',
-            FIELD           => 'ObjectId',
-            VALUE           => '0',
-        );
-
-        $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
-            TYPE       => 'LEFT',
-            ALIAS1     => $ocfalias,
-            FIELD1     => 'CustomField',
-            TABLE2     => 'CustomFields',
-            FIELD2     => 'id',
-        );
-        $self->Limit(
-            LEFTJOIN        => $CFs,
-            ENTRYAGGREGATOR => 'AND',
-            FIELD           => 'LookupType',
-            VALUE           => 'RT::Queue-RT::Ticket',
-        );
-        $self->Limit(
-            LEFTJOIN        => $CFs,
-            ENTRYAGGREGATOR => 'AND',
-            FIELD           => 'Name',
-            VALUE           => $cf,
-        );
-
-        $ocfvalias = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
-            TYPE   => 'LEFT',
-            ALIAS1 => $CFs,
-            FIELD1 => 'id',
-            TABLE2 => 'ObjectCustomFieldValues',
-            FIELD2 => 'CustomField',
-        );
-        $self->Limit(
-            LEFTJOIN        => $ocfvalias,
-            FIELD           => 'ObjectId',
-            VALUE           => 'main.id',
-            QUOTEVALUE      => 0,
-            ENTRYAGGREGATOR => 'AND',
-        );
+        ($ocfvalias, $CFs) = $self->_CustomFieldJoinByName( $cf );
+        $self->{_sql_cf_alias}{$cfkey} = $CFs;
+        $self->{_sql_object_cfv_alias}{$cfkey} = $ocfvalias;
     }
     $self->Limit(
         LEFTJOIN        => $ocfvalias,
         FIELD           => 'ObjectType',
-        VALUE           => 'RT::Ticket',
+        VALUE           => ref($self->NewItem),
         ENTRYAGGREGATOR => 'AND'
     );
     $self->Limit(
@@ -303,6 +258,54 @@ sub _CustomFieldJoin {
     return ($ocfvalias, $CFs);
 }
 
+sub _CustomFieldJoinByName {
+    my $self = shift;
+    my ($cf) = @_;
+    my $ocfalias = $self->Join(
+        TYPE       => 'LEFT',
+        EXPRESSION => q|'0'|,
+        TABLE2     => 'ObjectCustomFields',
+        FIELD2     => 'ObjectId',
+    );
+
+    my $CFs = $self->Join(
+        TYPE       => 'LEFT',
+        ALIAS1     => $ocfalias,
+        FIELD1     => 'CustomField',
+        TABLE2     => 'CustomFields',
+        FIELD2     => 'id',
+    );
+    $self->Limit(
+        LEFTJOIN        => $CFs,
+        ENTRYAGGREGATOR => 'AND',
+        FIELD           => 'LookupType',
+        VALUE           => $self->NewItem->CustomFieldLookupType,
+    );
+    $self->Limit(
+        LEFTJOIN        => $CFs,
+        ENTRYAGGREGATOR => 'AND',
+        FIELD           => 'Name',
+        VALUE           => $cf,
+    );
+
+    my $ocfvalias = $self->Join(
+        TYPE   => 'LEFT',
+        ALIAS1 => $CFs,
+        FIELD1 => 'id',
+        TABLE2 => 'ObjectCustomFieldValues',
+        FIELD2 => 'CustomField',
+    );
+    $self->Limit(
+        LEFTJOIN        => $ocfvalias,
+        FIELD           => 'ObjectId',
+        VALUE           => 'main.id',
+        QUOTEVALUE      => 0,
+        ENTRYAGGREGATOR => 'AND',
+    );
+
+    return ($ocfvalias, $CFs, $ocfalias);
+}
+
 sub LimitCustomField {
     my $self = shift;
     my %args = ( VALUE        => undef,
diff --git a/lib/RT/Tickets.pm b/lib/RT/Tickets.pm
index cb7f6da..ed00341 100644
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@ -86,6 +86,8 @@ use base 'RT::SearchBuilder';
 use Role::Basic 'with';
 with 'RT::SearchBuilder::Role::Roles';
 
+use Scalar::Util qw/blessed/;
+
 use RT::Ticket;
 use RT::SQL;
 
@@ -1114,6 +1116,21 @@ sub _CustomFieldLimit {
     );
 }
 
+sub _CustomFieldJoinByName {
+    my $self = shift;
+    my ($cf) = @_;
+
+    my ($ocfvalias, $CFs, $ocfalias) = $self->SUPER::_CustomFieldJoinByName($cf);
+    $self->Limit(
+        LEFTJOIN        => $ocfalias,
+        ENTRYAGGREGATOR => 'OR',
+        FIELD           => 'ObjectId',
+        VALUE           => 'main.Queue',
+        QUOTEVALUE      => 0,
+    );
+    return ($ocfvalias, $CFs, $ocfalias);
+}
+
 sub _HasAttributeLimit {
     my ( $self, $field, $op, $value, %rest ) = @_;
 

commit c558d15e413c6fc0588dd474e974bcfebd3dc81f
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Jan 29 01:31:32 2013 -0500

    Add explicit subclauses to all limits
    
    RT::Tickets assumes a subclause of "ticketsql" for all Limit and
    _OpenParen / _CloseParen calls.  Allow calling on non-Tickets
    collections by providing a default SUBCLAUSE name, and performing all
    Limit, _OpenParen, and _CloseParen adjustments therein.

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index f77e01c..abaea52 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -368,6 +368,8 @@ sub _LimitCustomField {
         $cfkey ||= $cf;
     }
 
+    $args{SUBCLAUSE} ||= "cf-$cfkey";
+
 # If we're trying to find custom fields that don't match something, we
 # want tickets where the custom field has no value at all.  Note that
 # we explicitly don't include the "IS NULL" case, since we would
@@ -466,7 +468,7 @@ sub _LimitCustomField {
         # we can reuse our default joins for this operation
         # with column specified we have different situation
         my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cf );
-        $self->_OpenParen;
+        $self->_OpenParen( $args{SUBCLAUSE} );
         $self->Limit(
             %args,
             ALIAS    => $ocfvalias,
@@ -481,13 +483,13 @@ sub _LimitCustomField {
             VALUE      => 'NULL',
             QUOTEVALUE => 0,
             ENTRYAGGREGATOR => 'AND',
+            SUBCLAUSE  => $args{SUBCLAUSE},
         ) if $CFs;
-        $self->_CloseParen;
+        $self->_CloseParen( $args{SUBCLAUSE} );
     }
     elsif ( $op !~ /^[<>]=?$/ && (  blessed($cf) && $cf->Type eq 'IPAddressRange')) {
         my ($start_ip, $end_ip) = split /-/, $value;
-        
-        $self->_OpenParen;
+        $self->_OpenParen( $args{SUBCLAUSE} );
         if ( $op !~ /NOT|!=|<>/i ) { # positive equation
             $self->_LimitCustomField(
                 %args,
@@ -538,17 +540,15 @@ sub _LimitCustomField {
             # estimations and scan less rows, but it's harder to do
             # as we have OR aggregator
         }
-        $self->_CloseParen;
+        $self->_CloseParen( $args{SUBCLAUSE} );
     } 
     elsif ( !$negative_op || $single_value ) {
         $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if not $single_value and not $op =~ /^[<>]=?$/;
         my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cf );
 
-        $self->_OpenParen;
-
-        $self->_OpenParen;
-
-        $self->_OpenParen;
+        $self->_OpenParen( $args{SUBCLAUSE} );
+        $self->_OpenParen( $args{SUBCLAUSE} );
+        $self->_OpenParen( $args{SUBCLAUSE} );
         # if column is defined then deal only with it
         # otherwise search in Content and in LargeContent
         if ( $column ) {
@@ -560,9 +560,9 @@ sub _LimitCustomField {
                 VALUE      => $value,
                 CASESENSITIVE => 0,
             ) );
-            $self->_CloseParen;
-            $self->_CloseParen;
-            $self->_CloseParen;
+            $self->_CloseParen( $args{SUBCLAUSE} );
+            $self->_CloseParen( $args{SUBCLAUSE} );
+            $self->_CloseParen( $args{SUBCLAUSE} );
         }
         else {
             # need special treatment for Date
@@ -576,7 +576,7 @@ sub _LimitCustomField {
                 $date->AddDay;
                 my $dayend = $date->ISO;
 
-                $self->_OpenParen;
+                $self->_OpenParen( $args{SUBCLAUSE} );
 
                 $self->Limit(
                     %args,
@@ -595,7 +595,7 @@ sub _LimitCustomField {
                     ENTRYAGGREGATOR => 'AND',
                 );
 
-                $self->_CloseParen;
+                $self->_CloseParen( $args{SUBCLAUSE} );
             }
             elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
                 if ( length( Encode::encode_utf8($value) ) < 256 ) {
@@ -609,28 +609,31 @@ sub _LimitCustomField {
                     );
                 }
                 else {
-                    $self->_OpenParen;
+                    $self->_OpenParen( $args{SUBCLAUSE} );
                     $self->Limit(
                         ALIAS           => $ocfvalias,
                         FIELD           => 'Content',
                         OPERATOR        => '=',
                         VALUE           => '',
-                        ENTRYAGGREGATOR => 'OR'
+                        ENTRYAGGREGATOR => 'OR',
+                        SUBCLAUSE       => $args{SUBCLAUSE},
                     );
                     $self->Limit(
                         ALIAS           => $ocfvalias,
                         FIELD           => 'Content',
                         OPERATOR        => 'IS',
                         VALUE           => 'NULL',
-                        ENTRYAGGREGATOR => 'OR'
+                        ENTRYAGGREGATOR => 'OR',
+                        SUBCLAUSE       => $args{SUBCLAUSE},
                     );
-                    $self->_CloseParen;
+                    $self->_CloseParen( $args{SUBCLAUSE} );
                     $self->Limit( $fix_op->(
                         ALIAS           => $ocfvalias,
                         FIELD           => 'LargeContent',
                         OPERATOR        => $op,
                         VALUE           => $value,
                         ENTRYAGGREGATOR => 'AND',
+                        SUBCLAUSE       => $args{SUBCLAUSE},
                         CASESENSITIVE => 0,
                     ) );
                 }
@@ -645,13 +648,14 @@ sub _LimitCustomField {
                     CASESENSITIVE => 0,
                 );
 
-                $self->_OpenParen;
-                $self->_OpenParen;
+                $self->_OpenParen( $args{SUBCLAUSE} );
+                $self->_OpenParen( $args{SUBCLAUSE} );
                 $self->Limit(
                     ALIAS           => $ocfvalias,
                     FIELD           => 'Content',
                     OPERATOR        => '=',
                     VALUE           => '',
+                    SUBCLAUSE       => $args{SUBCLAUSE},
                     ENTRYAGGREGATOR => 'OR'
                 );
                 $self->Limit(
@@ -659,20 +663,22 @@ sub _LimitCustomField {
                     FIELD           => 'Content',
                     OPERATOR        => 'IS',
                     VALUE           => 'NULL',
+                    SUBCLAUSE       => $args{SUBCLAUSE},
                     ENTRYAGGREGATOR => 'OR'
                 );
-                $self->_CloseParen;
+                $self->_CloseParen( $args{SUBCLAUSE} );
                 $self->Limit( $fix_op->(
                     ALIAS           => $ocfvalias,
                     FIELD           => 'LargeContent',
                     OPERATOR        => $op,
                     VALUE           => $value,
                     ENTRYAGGREGATOR => 'AND',
+                    SUBCLAUSE       => $args{SUBCLAUSE},
                     CASESENSITIVE => 0,
                 ) );
-                $self->_CloseParen;
+                $self->_CloseParen( $args{SUBCLAUSE} );
             }
-            $self->_CloseParen;
+            $self->_CloseParen( $args{SUBCLAUSE} );
 
             # XXX: if we join via CustomFields table then
             # because of order of left joins we get NULLs in
@@ -692,8 +698,9 @@ sub _LimitCustomField {
                 VALUE           => 'NULL',
                 QUOTEVALUE      => 0,
                 ENTRYAGGREGATOR => 'AND',
+                SUBCLAUSE       => $args{SUBCLAUSE},
             ) if $CFs;
-            $self->_CloseParen;
+            $self->_CloseParen( $args{SUBCLAUSE} );
 
             if ($negative_op) {
                 $self->Limit(
@@ -703,10 +710,11 @@ sub _LimitCustomField {
                     VALUE           => 'NULL',
                     QUOTEVALUE      => 0,
                     ENTRYAGGREGATOR => 'OR',
+                    SUBCLAUSE       => $args{SUBCLAUSE},
                 );
             }
 
-            $self->_CloseParen;
+            $self->_CloseParen( $args{SUBCLAUSE} );
         }
     }
     else {
diff --git a/lib/RT/Tickets.pm b/lib/RT/Tickets.pm
index ed00341..694cdae 100644
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@ -1328,10 +1328,10 @@ sub _SQLJoin {
 }
 
 sub _OpenParen {
-    $_[0]->SUPER::_OpenParen( 'ticketsql' );
+    $_[0]->SUPER::_OpenParen( $_[1] || 'ticketsql' );
 }
 sub _CloseParen {
-    $_[0]->SUPER::_CloseParen( 'ticketsql' );
+    $_[0]->SUPER::_CloseParen( $_[1] || 'ticketsql' );
 }
 
 sub Limit {

commit 53b2a9a8d6974f21fff475f1d1c5f4df2ca96bb3
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Sat Apr 27 00:40:55 2013 -0400

    Treat CF ids as specific identifiers like objects, not as names

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index abaea52..0cedc0f 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -364,6 +364,15 @@ sub _LimitCustomField {
     my $cfkey  = delete $args{KEY};
     if (blessed($cf) and $cf->id) {
         $cfkey ||= $cf->id;
+    } elsif ($cf =~ /^\d+$/) {
+        my $obj = RT::CustomField->new( $self->CurrentUser );
+        $obj->Load($cf);
+        if ($obj->id) {
+            $cf = $obj;
+            $cfkey ||= $cf->id;
+        } else {
+            $cfkey ||= $cf;
+        }
     } else {
         $cfkey ||= $cf;
     }

commit 3a70b3c8127220ab3a557340e94d322e1a7b74fc
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Jan 29 01:32:04 2013 -0500

    Switch the general LimitCustomField to the new implementation
    
    Use the new _LimitCustomField implementation for RT::SearchBuilder's
    LimitCustomField.  The former is not simply renamed to the latter
    because RT::Tickets contains a LimitCustomField which acts on
    "restrictions", and must continue to do so.  As such, _LimitCustomField
    (and its recursive calls) must continue to occupy some other name.
    
    This removes the (dubious and unused) ability to call LimitCustomField
    with no CUSTOMFIELD provided, which searches within all custom fields.
    It adds all of the complex date and IP parsing capabilities that Tickets
    have enjoyed.

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index 0cedc0f..9340637 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -308,42 +308,7 @@ sub _CustomFieldJoinByName {
 
 sub LimitCustomField {
     my $self = shift;
-    my %args = ( VALUE        => undef,
-                 CUSTOMFIELD  => undef,
-                 OPERATOR     => '=',
-                 @_ );
-
-    my $alias = $self->Join(
-        TYPE       => 'left',
-        ALIAS1     => 'main',
-        FIELD1     => 'id',
-        TABLE2     => 'ObjectCustomFieldValues',
-        FIELD2     => 'ObjectId'
-    );
-    $self->Limit(
-        ALIAS      => $alias,
-        FIELD      => 'CustomField',
-        OPERATOR   => '=',
-        VALUE      => $args{'CUSTOMFIELD'},
-    ) if ($args{'CUSTOMFIELD'});
-    $self->Limit(
-        ALIAS      => $alias,
-        FIELD      => 'ObjectType',
-        OPERATOR   => '=',
-        VALUE      => $self->_SingularClass,
-    );
-    $self->Limit(
-        ALIAS      => $alias,
-        FIELD      => 'Content',
-        OPERATOR   => $args{'OPERATOR'},
-        VALUE      => $args{'VALUE'},
-    );
-    $self->Limit(
-        ALIAS => $alias,
-        FIELD => 'Disabled',
-        OPERATOR => '=',
-        VALUE => 0,
-    );
+    return $self->_LimitCustomField( @_ );
 }
 
 use Regexp::Common qw(RE_net_IPv4);

commit 0f6ee87be9a7c399d18e8c05f9ad90888b9ff6d2
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Jan 29 15:08:45 2013 -0500

    Refactor CF ordering into a method on SearchBuilder

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index 9340637..3b2d637 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -121,6 +121,51 @@ sub JoinTransactions {
     return $alias;
 }
 
+sub _OrderByCF {
+    my $self = shift;
+    my ($row, $cf) = @_;
+
+    my $cfkey = blessed($cf) ? $cf->id : $cf;
+    $cfkey .= ".ordering" if !blessed($cf) || ($cf->MaxValues||0) != 1;
+    my ($ocfvs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cf );
+    # this is described in _LimitCustomField
+    $self->Limit(
+        ALIAS      => $CFs,
+        FIELD      => 'Name',
+        OPERATOR   => 'IS NOT',
+        VALUE      => 'NULL',
+        QUOTEVALUE => 1,
+        ENTRYAGGREGATOR => 'AND',
+    ) if $CFs;
+    unless (blessed($cf)) {
+        # For those cases where we are doing a join against the
+        # CF name, and don't have a CFid, use Unique to make sure
+        # we don't show duplicate tickets.  NOTE: I'm pretty sure
+        # this will stay mixed in for the life of the
+        # class/package, and not just for the life of the object.
+        # Potential performance issue.
+        require DBIx::SearchBuilder::Unique;
+        DBIx::SearchBuilder::Unique->import;
+    }
+    my $CFvs = $self->Join(
+        TYPE   => 'LEFT',
+        ALIAS1 => $ocfvs,
+        FIELD1 => 'CustomField',
+        TABLE2 => 'CustomFieldValues',
+        FIELD2 => 'CustomField',
+    );
+    $self->Limit(
+        LEFTJOIN        => $CFvs,
+        FIELD           => 'Name',
+        QUOTEVALUE      => 0,
+        VALUE           => "$ocfvs.Content",
+        ENTRYAGGREGATOR => 'AND'
+    );
+
+    return { %$row, ALIAS => $CFvs,  FIELD => 'SortOrder' },
+           { %$row, ALIAS => $ocfvs, FIELD => 'Content' };
+}
+
 sub OrderByCols {
     my $self = shift;
     my @sort;
diff --git a/lib/RT/Tickets.pm b/lib/RT/Tickets.pm
index 694cdae..9d3abf1 100644
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@ -1234,46 +1234,8 @@ sub OrderByCols {
             }
             push @res, { %$row, ALIAS => $users, FIELD => $subkey };
        } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
-           my ($queue, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
-           my $cfkey = $cf_obj ? $cf_obj->id : "$queue.$field";
-           $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
-           my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj || $field) );
-           # this is described in _CustomFieldLimit
-           $self->Limit(
-               ALIAS      => $CFs,
-               FIELD      => 'Name',
-               OPERATOR   => 'IS NOT',
-               VALUE      => 'NULL',
-               QUOTEVALUE => 1,
-               ENTRYAGGREGATOR => 'AND',
-           ) if $CFs;
-           unless ($cf_obj) {
-               # For those cases where we are doing a join against the
-               # CF name, and don't have a CFid, use Unique to make sure
-               # we don't show duplicate tickets.  NOTE: I'm pretty sure
-               # this will stay mixed in for the life of the
-               # class/package, and not just for the life of the object.
-               # Potential performance issue.
-               require DBIx::SearchBuilder::Unique;
-               DBIx::SearchBuilder::Unique->import;
-           }
-           my $CFvs = $self->Join(
-               TYPE   => 'LEFT',
-               ALIAS1 => $TicketCFs,
-               FIELD1 => 'CustomField',
-               TABLE2 => 'CustomFieldValues',
-               FIELD2 => 'CustomField',
-           );
-           $self->Limit(
-               LEFTJOIN        => $CFvs,
-               FIELD           => 'Name',
-               QUOTEVALUE      => 0,
-               VALUE           => $TicketCFs . ".Content",
-               ENTRYAGGREGATOR => 'AND'
-           );
-
-           push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
-           push @res, { %$row, ALIAS => $TicketCFs, FIELD => 'Content' };
+           my ($queue, $field, $cf, $column) = $self->_CustomFieldDecipher( $subkey );
+           push @res, $self->_OrderByCF( $row, ($cf || "$queue.$field") );
        } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
            # PAW logic is "reversed"
            my $order = "ASC";

commit 604a8ec5141fd88348b63ae4dc52afdc8bdfcfa3
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Fri Apr 26 18:34:14 2013 -0400

    SearchBuilder forces QUOTEVALUE => 0 for IS and IS NOT limits
    
    Remove the unnecessary (and in the first hunk, misleading) QUOTEVALUEs.

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index 3b2d637..38fdbb4 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -134,7 +134,6 @@ sub _OrderByCF {
         FIELD      => 'Name',
         OPERATOR   => 'IS NOT',
         VALUE      => 'NULL',
-        QUOTEVALUE => 1,
         ENTRYAGGREGATOR => 'AND',
     ) if $CFs;
     unless (blessed($cf)) {
@@ -500,7 +499,6 @@ sub _LimitCustomField {
             FIELD      => 'Name',
             OPERATOR   => 'IS NOT',
             VALUE      => 'NULL',
-            QUOTEVALUE => 0,
             ENTRYAGGREGATOR => 'AND',
             SUBCLAUSE  => $args{SUBCLAUSE},
         ) if $CFs;
@@ -715,7 +713,6 @@ sub _LimitCustomField {
                 FIELD           => 'Name',
                 OPERATOR        => 'IS NOT',
                 VALUE           => 'NULL',
-                QUOTEVALUE      => 0,
                 ENTRYAGGREGATOR => 'AND',
                 SUBCLAUSE       => $args{SUBCLAUSE},
             ) if $CFs;
@@ -727,7 +724,6 @@ sub _LimitCustomField {
                     FIELD           => $column || 'Content',
                     OPERATOR        => 'IS',
                     VALUE           => 'NULL',
-                    QUOTEVALUE      => 0,
                     ENTRYAGGREGATOR => 'OR',
                     SUBCLAUSE       => $args{SUBCLAUSE},
                 );
@@ -771,7 +767,6 @@ sub _LimitCustomField {
             FIELD      => 'id',
             OPERATOR   => 'IS',
             VALUE      => 'NULL',
-            QUOTEVALUE => 0,
         );
     }
 }

commit bcd79f3afc85d8976a65b94c5de9c158b6d1962f
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Fri Apr 26 19:06:28 2013 -0400

    Remove uses of ::Unique; our SELECT DISTINCT is sufficient
    
    Rather than import a mixin (twice, in Tickets' case (?!)) which
    unique-ifies the results, rely on our SELECT DISCINCT machinery.  The
    original code was added when DistinctQuery was unimplemented for some
    database handles.

diff --git a/lib/RT/CustomFields.pm b/lib/RT/CustomFields.pm
index 48e571d..6834751 100644
--- a/lib/RT/CustomFields.pm
+++ b/lib/RT/CustomFields.pm
@@ -70,8 +70,6 @@ use warnings;
 
 use base 'RT::SearchBuilder';
 
-use DBIx::SearchBuilder::Unique;
-
 use RT::CustomField;
 
 sub Table { 'CustomFields'}
diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 3d3336c..afd79e2 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -275,15 +275,6 @@ sub _FieldToFunction {
     return %args;
 }
 
-
-# Override the AddRecord from DBI::SearchBuilder::Unique. id isn't id here
-# wedon't want to disambiguate all the items with a count of 1.
-sub AddRecord {
-    my $self = shift;
-    my $record = shift;
-    push @{$self->{'items'}}, $record;
-}
-
 1;
 
 
diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index 38fdbb4..1f6e01b 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -136,16 +136,6 @@ sub _OrderByCF {
         VALUE      => 'NULL',
         ENTRYAGGREGATOR => 'AND',
     ) if $CFs;
-    unless (blessed($cf)) {
-        # For those cases where we are doing a join against the
-        # CF name, and don't have a CFid, use Unique to make sure
-        # we don't show duplicate tickets.  NOTE: I'm pretty sure
-        # this will stay mixed in for the life of the
-        # class/package, and not just for the life of the object.
-        # Potential performance issue.
-        require DBIx::SearchBuilder::Unique;
-        DBIx::SearchBuilder::Unique->import;
-    }
     my $CFvs = $self->Join(
         TYPE   => 'LEFT',
         ALIAS1 => $ocfvs,
diff --git a/lib/RT/Tickets.pm b/lib/RT/Tickets.pm
index 9d3abf1..1dcccfb 100644
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@ -94,7 +94,6 @@ use RT::SQL;
 sub Table { 'Tickets'}
 
 use RT::CustomFields;
-use DBIx::SearchBuilder::Unique;
 
 # Configuration Tables:
 

commit 01a48263b3d2d6481b095a398e20110ea12b9864
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Fri Apr 26 22:44:00 2013 -0400

    Clean up tests to run in a predictable order

diff --git a/t/ticket/search_by_cf_freeform_single.t b/t/ticket/search_by_cf_freeform_single.t
index f8462a9..102db18 100644
--- a/t/ticket/search_by_cf_freeform_single.t
+++ b/t/ticket/search_by_cf_freeform_single.t
@@ -2,7 +2,7 @@
 use strict;
 use warnings;
 
-use RT::Test nodata => 1, tests => 106;
+use RT::Test nodata => 1, tests => undef;
 use RT::Ticket;
 
 my $q = RT::Test->load_or_create_queue( Name => 'Regression' );
@@ -22,39 +22,19 @@ my ($cf_name, $cf_id, $cf) = ("Test", 0, undef);
     $cf_id = $cf->id;
 }
 
-my ($total, @data, @tickets, %test) = (0, ());
+my $other_q = RT::Test->load_or_create_queue( Name => 'Other' );
+ok $other_q && $other_q->id, 'loaded or created queue';
 
-sub run_tests {
-    my $query_prefix = join ' OR ', map 'id = '. $_->id, @tickets;
-    foreach my $key ( sort keys %test ) {
-        my $tix = RT::Tickets->new(RT->SystemUser);
-        $tix->FromSQL( "( $query_prefix ) AND ( $key )" );
-
-        my $error = 0;
-
-        my $count = 0;
-        $count++ foreach grep $_, values %{ $test{$key} };
-        is($tix->Count, $count, "found correct number of ticket(s) by '$key'") or $error = 1;
-
-        my $good_tickets = ($tix->Count == $count);
-        while ( my $ticket = $tix->Next ) {
-            next if $test{$key}->{ $ticket->Subject };
-            diag $ticket->Subject ." ticket has been found when it's not expected";
-            $good_tickets = 0;
-        }
-        ok( $good_tickets, "all tickets are good with '$key'" ) or $error = 1;
-
-        diag "Wrong SQL query for '$key':". $tix->BuildSelectQuery if $error;
-    }
-}
+subtest "Creating tickets" => sub {
+    RT::Test->create_tickets( { Queue => $q->id },
+        { Subject => '-' },
+        { Subject => 'x', "CustomField-$cf_id" => 'x', },
+        { Subject => 'y', "CustomField-$cf_id" => 'y', },
+        { Subject => 'z', "CustomField-$cf_id" => 'z', },
+    );
+};
 
- at data = (
-    { Subject => '-' },
-    { Subject => 'x', "CustomField-$cf_id" => 'x', },
-    { Subject => 'y', "CustomField-$cf_id" => 'y', },
-    { Subject => 'z', "CustomField-$cf_id" => 'z', },
-);
-%test = (
+my @tests = (
     "CF.{$cf_id} IS NULL"                 => { '-' => 1, x => 0, y => 0, z => 0 },
     "'CF.{$cf_name}' IS NULL"             => { '-' => 1, x => 0, y => 0, z => 0 },
     "'CF.$queue.{$cf_id}' IS NULL"        => { '-' => 1, x => 0, y => 0, z => 0 },
@@ -109,16 +89,37 @@ sub run_tests {
     "'CF.{$cf_name}' = 'x' OR 'CF.{$cf_name}' IS NOT NULL"                => { '-' => 0, x => 1, y => 1, z => 1 },
     "'CF.$queue.{$cf_id}' = 'x' OR 'CF.$queue.{$cf_id}' IS NOT NULL"      => { '-' => 0, x => 1, y => 1, z => 1 },
     "'CF.$queue.{$cf_name}' = 'x' OR 'CF.$queue.{$cf_name}' IS NOT NULL"  => { '-' => 0, x => 1, y => 1, z => 1 },
-
 );
- at tickets = RT::Test->create_tickets( { Queue => $q->id }, @data);
-$total = scalar @tickets;
-{
-    my $tix = RT::Tickets->new(RT->SystemUser);
-    $tix->FromSQL("Queue = '$queue'");
-    is($tix->Count, $total, "found $total tickets");
+run_tests(@tests);
+
+
+sub run_tests {
+    my @tests = @_;
+    while (@tests) {
+        my $query = shift @tests;
+        my %results = %{ shift @tests };
+        subtest $query => sub {
+            my $tix = RT::Tickets->new(RT->SystemUser);
+            $tix->FromSQL( "$query" );
+
+            my $error = 0;
+
+            my $count = 0;
+            $count++ foreach grep $_, values %results;
+            is($tix->Count, $count, "found correct number of ticket(s)") or $error = 1;
+
+            my $good_tickets = ($tix->Count == $count);
+            while ( my $ticket = $tix->Next ) {
+                next if $results{ $ticket->Subject };
+                diag $ticket->Subject ." ticket has been found when it's not expected";
+                $good_tickets = 0;
+            }
+            ok( $good_tickets, "all tickets are good" ) or $error = 1;
+
+            diag "Wrong SQL: ". $tix->BuildSelectQuery if $error;
+        };
+    }
 }
-run_tests();
 
- at tickets = ();
 
+done_testing;

commit bfcb8b2c3dcf62e96ba6544a80f519fdf7814924
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Fri Apr 26 22:52:34 2013 -0400

    Fix != conditions with an explicit column provided
    
    Previously, the check for "or allow a row which has no value" failed to
    apply if an explicit column was provided.  Refactor the parenthization
    slightly, and allow the possibility of no-value even in the case of
    searching through particular columns.

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index 1f6e01b..f58e07d 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -554,8 +554,6 @@ sub _LimitCustomField {
         my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cf );
 
         $self->_OpenParen( $args{SUBCLAUSE} );
-        $self->_OpenParen( $args{SUBCLAUSE} );
-        $self->_OpenParen( $args{SUBCLAUSE} );
         # if column is defined then deal only with it
         # otherwise search in Content and in LargeContent
         if ( $column ) {
@@ -567,11 +565,10 @@ sub _LimitCustomField {
                 VALUE      => $value,
                 CASESENSITIVE => 0,
             ) );
-            $self->_CloseParen( $args{SUBCLAUSE} );
-            $self->_CloseParen( $args{SUBCLAUSE} );
-            $self->_CloseParen( $args{SUBCLAUSE} );
         }
         else {
+            $self->_OpenParen( $args{SUBCLAUSE} );
+            $self->_OpenParen( $args{SUBCLAUSE} );
             # need special treatment for Date
             if ( blessed($cf) and $cf->Type eq 'DateTime' and $op eq '=' && $value !~ /:/ ) {
                 # no time specified, that means we want everything on a
@@ -707,20 +704,20 @@ sub _LimitCustomField {
                 SUBCLAUSE       => $args{SUBCLAUSE},
             ) if $CFs;
             $self->_CloseParen( $args{SUBCLAUSE} );
+        }
 
-            if ($negative_op) {
-                $self->Limit(
-                    ALIAS           => $ocfvalias,
-                    FIELD           => $column || 'Content',
-                    OPERATOR        => 'IS',
-                    VALUE           => 'NULL',
-                    ENTRYAGGREGATOR => 'OR',
-                    SUBCLAUSE       => $args{SUBCLAUSE},
-                );
-            }
-
-            $self->_CloseParen( $args{SUBCLAUSE} );
+        if ($negative_op) {
+            $self->Limit(
+                ALIAS           => $ocfvalias,
+                FIELD           => $column || 'Content',
+                OPERATOR        => 'IS',
+                VALUE           => 'NULL',
+                ENTRYAGGREGATOR => 'OR',
+                SUBCLAUSE       => $args{SUBCLAUSE},
+            );
         }
+        $self->_CloseParen( $args{SUBCLAUSE} );
+
     }
     else {
         $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
diff --git a/t/ticket/search_by_cf_freeform_single.t b/t/ticket/search_by_cf_freeform_single.t
index 102db18..80349ad 100644
--- a/t/ticket/search_by_cf_freeform_single.t
+++ b/t/ticket/search_by_cf_freeform_single.t
@@ -46,14 +46,22 @@ my @tests = (
     "'CF.$queue.{$cf_name}' IS NOT NULL"  => { '-' => 0, x => 1, y => 1, z => 1 },
 
     "CF.{$cf_id} = 'x'"                   => { '-' => 0, x => 1, y => 0, z => 0 },
+    "CF.{$cf_id}.Content = 'x'"           => { '-' => 0, x => 1, y => 0, z => 0 },
     "'CF.{$cf_name}' = 'x'"               => { '-' => 0, x => 1, y => 0, z => 0 },
+    "'CF.{$cf_name}.Content' = 'x'"       => { '-' => 0, x => 1, y => 0, z => 0 },
     "'CF.$queue.{$cf_id}' = 'x'"          => { '-' => 0, x => 1, y => 0, z => 0 },
+    "'CF.$queue.{$cf_id}.Content' = 'x'"  => { '-' => 0, x => 1, y => 0, z => 0 },
     "'CF.$queue.{$cf_name}' = 'x'"        => { '-' => 0, x => 1, y => 0, z => 0 },
+    "'CF.$queue.{$cf_name}.Content' = 'x'" => { '-' => 0, x => 1, y => 0, z => 0 },
 
     "CF.{$cf_id} != 'x'"                  => { '-' => 1, x => 0, y => 1, z => 1 },
+    "CF.{$cf_id}.Content != 'x'"          => { '-' => 1, x => 0, y => 1, z => 1 },
     "'CF.{$cf_name}' != 'x'"              => { '-' => 1, x => 0, y => 1, z => 1 },
+    "'CF.{$cf_name}.Content' != 'x'"      => { '-' => 1, x => 0, y => 1, z => 1 },
     "'CF.$queue.{$cf_id}' != 'x'"         => { '-' => 1, x => 0, y => 1, z => 1 },
+    "'CF.$queue.{$cf_id}.Content' != 'x'" => { '-' => 1, x => 0, y => 1, z => 1 },
     "'CF.$queue.{$cf_name}' != 'x'"       => { '-' => 1, x => 0, y => 1, z => 1 },
+    "'CF.$queue.{$cf_name}.Content' != 'x'" => { '-' => 1, x => 0, y => 1, z => 1 },
 
     "CF.{$cf_id} = 'x' OR CF.{$cf_id} = 'y'"                        => { '-' => 0, x => 1, y => 1, z => 0 },
     "'CF.{$cf_name}' = 'x' OR 'CF.{$cf_name}' = 'y'"                => { '-' => 0, x => 1, y => 1, z => 0 },

commit c5a4c82e01e4c5c6248c27e1ca9459d6938c3e41
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Fri Apr 26 22:56:16 2013 -0400

    Tests for tickets in alternate queues, without the CF applied
    
    A queue without the CF applied should act identically to a ticket with
    no value for that CF, in all cases.

diff --git a/t/ticket/search_by_cf_freeform_single.t b/t/ticket/search_by_cf_freeform_single.t
index 80349ad..616552a 100644
--- a/t/ticket/search_by_cf_freeform_single.t
+++ b/t/ticket/search_by_cf_freeform_single.t
@@ -28,6 +28,7 @@ ok $other_q && $other_q->id, 'loaded or created queue';
 subtest "Creating tickets" => sub {
     RT::Test->create_tickets( { Queue => $q->id },
         { Subject => '-' },
+        { Subject => "other", Queue => $other_q->id },
         { Subject => 'x', "CustomField-$cf_id" => 'x', },
         { Subject => 'y', "CustomField-$cf_id" => 'y', },
         { Subject => 'z', "CustomField-$cf_id" => 'z', },
@@ -35,68 +36,68 @@ subtest "Creating tickets" => sub {
 };
 
 my @tests = (
-    "CF.{$cf_id} IS NULL"                 => { '-' => 1, x => 0, y => 0, z => 0 },
-    "'CF.{$cf_name}' IS NULL"             => { '-' => 1, x => 0, y => 0, z => 0 },
-    "'CF.$queue.{$cf_id}' IS NULL"        => { '-' => 1, x => 0, y => 0, z => 0 },
-    "'CF.$queue.{$cf_name}' IS NULL"      => { '-' => 1, x => 0, y => 0, z => 0 },
-
-    "CF.{$cf_id} IS NOT NULL"             => { '-' => 0, x => 1, y => 1, z => 1 },
-    "'CF.{$cf_name}' IS NOT NULL"         => { '-' => 0, x => 1, y => 1, z => 1 },
-    "'CF.$queue.{$cf_id}' IS NOT NULL"    => { '-' => 0, x => 1, y => 1, z => 1 },
-    "'CF.$queue.{$cf_name}' IS NOT NULL"  => { '-' => 0, x => 1, y => 1, z => 1 },
-
-    "CF.{$cf_id} = 'x'"                   => { '-' => 0, x => 1, y => 0, z => 0 },
-    "CF.{$cf_id}.Content = 'x'"           => { '-' => 0, x => 1, y => 0, z => 0 },
-    "'CF.{$cf_name}' = 'x'"               => { '-' => 0, x => 1, y => 0, z => 0 },
-    "'CF.{$cf_name}.Content' = 'x'"       => { '-' => 0, x => 1, y => 0, z => 0 },
-    "'CF.$queue.{$cf_id}' = 'x'"          => { '-' => 0, x => 1, y => 0, z => 0 },
-    "'CF.$queue.{$cf_id}.Content' = 'x'"  => { '-' => 0, x => 1, y => 0, z => 0 },
-    "'CF.$queue.{$cf_name}' = 'x'"        => { '-' => 0, x => 1, y => 0, z => 0 },
-    "'CF.$queue.{$cf_name}.Content' = 'x'" => { '-' => 0, x => 1, y => 0, z => 0 },
-
-    "CF.{$cf_id} != 'x'"                  => { '-' => 1, x => 0, y => 1, z => 1 },
-    "CF.{$cf_id}.Content != 'x'"          => { '-' => 1, x => 0, y => 1, z => 1 },
-    "'CF.{$cf_name}' != 'x'"              => { '-' => 1, x => 0, y => 1, z => 1 },
-    "'CF.{$cf_name}.Content' != 'x'"      => { '-' => 1, x => 0, y => 1, z => 1 },
-    "'CF.$queue.{$cf_id}' != 'x'"         => { '-' => 1, x => 0, y => 1, z => 1 },
-    "'CF.$queue.{$cf_id}.Content' != 'x'" => { '-' => 1, x => 0, y => 1, z => 1 },
-    "'CF.$queue.{$cf_name}' != 'x'"       => { '-' => 1, x => 0, y => 1, z => 1 },
-    "'CF.$queue.{$cf_name}.Content' != 'x'" => { '-' => 1, x => 0, y => 1, z => 1 },
-
-    "CF.{$cf_id} = 'x' OR CF.{$cf_id} = 'y'"                        => { '-' => 0, x => 1, y => 1, z => 0 },
-    "'CF.{$cf_name}' = 'x' OR 'CF.{$cf_name}' = 'y'"                => { '-' => 0, x => 1, y => 1, z => 0 },
-    "'CF.$queue.{$cf_id}' = 'x' OR 'CF.$queue.{$cf_id}' = 'y'"      => { '-' => 0, x => 1, y => 1, z => 0 },
-    "'CF.$queue.{$cf_name}' = 'x' OR 'CF.$queue.{$cf_name}' = 'y'"  => { '-' => 0, x => 1, y => 1, z => 0 },
-
-    "CF.{$cf_id} = 'x' AND CF.{$cf_id} = 'y'"                        => { '-' => 0, x => 0, y => 0, z => 0 },
-    "'CF.{$cf_name}' = 'x' AND 'CF.{$cf_name}' = 'y'"                => { '-' => 0, x => 0, y => 0, z => 0 },
-    "'CF.$queue.{$cf_id}' = 'x' AND 'CF.$queue.{$cf_id}' = 'y'"      => { '-' => 0, x => 0, y => 0, z => 0 },
-    "'CF.$queue.{$cf_name}' = 'x' AND 'CF.$queue.{$cf_name}' = 'y'"  => { '-' => 0, x => 0, y => 0, z => 0 },
-
-    "CF.{$cf_id} != 'x' AND CF.{$cf_id} != 'y'"                        => { '-' => 1, x => 0, y => 0, z => 1 },
-    "'CF.{$cf_name}' != 'x' AND 'CF.{$cf_name}' != 'y'"                => { '-' => 1, x => 0, y => 0, z => 1 },
-    "'CF.$queue.{$cf_id}' != 'x' AND 'CF.$queue.{$cf_id}' != 'y'"      => { '-' => 1, x => 0, y => 0, z => 1 },
-    "'CF.$queue.{$cf_name}' != 'x' AND 'CF.$queue.{$cf_name}' != 'y'"  => { '-' => 1, x => 0, y => 0, z => 1 },
-
-    "CF.{$cf_id} = 'x' AND CF.{$cf_id} IS NULL"                        => { '-' => 0, x => 0, y => 0, z => 0 },
-    "'CF.{$cf_name}' = 'x' AND 'CF.{$cf_name}' IS NULL"                => { '-' => 0, x => 0, y => 0, z => 0 },
-    "'CF.$queue.{$cf_id}' = 'x' AND 'CF.$queue.{$cf_id}' IS NULL"      => { '-' => 0, x => 0, y => 0, z => 0 },
-    "'CF.$queue.{$cf_name}' = 'x' AND 'CF.$queue.{$cf_name}' IS NULL"  => { '-' => 0, x => 0, y => 0, z => 0 },
-
-    "CF.{$cf_id} = 'x' OR CF.{$cf_id} IS NULL"                        => { '-' => 1, x => 1, y => 0, z => 0 },
-    "'CF.{$cf_name}' = 'x' OR 'CF.{$cf_name}' IS NULL"                => { '-' => 1, x => 1, y => 0, z => 0 },
-    "'CF.$queue.{$cf_id}' = 'x' OR 'CF.$queue.{$cf_id}' IS NULL"      => { '-' => 1, x => 1, y => 0, z => 0 },
-    "'CF.$queue.{$cf_name}' = 'x' OR 'CF.$queue.{$cf_name}' IS NULL"  => { '-' => 1, x => 1, y => 0, z => 0 },
-
-    "CF.{$cf_id} = 'x' AND CF.{$cf_id} IS NOT NULL"                        => { '-' => 0, x => 1, y => 0, z => 0 },
-    "'CF.{$cf_name}' = 'x' AND 'CF.{$cf_name}' IS NOT NULL"                => { '-' => 0, x => 1, y => 0, z => 0 },
-    "'CF.$queue.{$cf_id}' = 'x' AND 'CF.$queue.{$cf_id}' IS NOT NULL"      => { '-' => 0, x => 1, y => 0, z => 0 },
-    "'CF.$queue.{$cf_name}' = 'x' AND 'CF.$queue.{$cf_name}' IS NOT NULL"  => { '-' => 0, x => 1, y => 0, z => 0 },
-
-    "CF.{$cf_id} = 'x' OR CF.{$cf_id} IS NOT NULL"                        => { '-' => 0, x => 1, y => 1, z => 1 },
-    "'CF.{$cf_name}' = 'x' OR 'CF.{$cf_name}' IS NOT NULL"                => { '-' => 0, x => 1, y => 1, z => 1 },
-    "'CF.$queue.{$cf_id}' = 'x' OR 'CF.$queue.{$cf_id}' IS NOT NULL"      => { '-' => 0, x => 1, y => 1, z => 1 },
-    "'CF.$queue.{$cf_name}' = 'x' OR 'CF.$queue.{$cf_name}' IS NOT NULL"  => { '-' => 0, x => 1, y => 1, z => 1 },
+    "CF.{$cf_id} IS NULL"                 => { '-' => 1, other => 1, x => 0, y => 0, z => 0 },
+    "'CF.{$cf_name}' IS NULL"             => { '-' => 1, other => 1, x => 0, y => 0, z => 0 },
+    "'CF.$queue.{$cf_id}' IS NULL"        => { '-' => 1, other => 1, x => 0, y => 0, z => 0 },
+    "'CF.$queue.{$cf_name}' IS NULL"      => { '-' => 1, other => 1, x => 0, y => 0, z => 0 },
+
+    "CF.{$cf_id} IS NOT NULL"             => { '-' => 0, other => 0, x => 1, y => 1, z => 1 },
+    "'CF.{$cf_name}' IS NOT NULL"         => { '-' => 0, other => 0, x => 1, y => 1, z => 1 },
+    "'CF.$queue.{$cf_id}' IS NOT NULL"    => { '-' => 0, other => 0, x => 1, y => 1, z => 1 },
+    "'CF.$queue.{$cf_name}' IS NOT NULL"  => { '-' => 0, other => 0, x => 1, y => 1, z => 1 },
+
+    "CF.{$cf_id} = 'x'"                   => { '-' => 0, other => 0, x => 1, y => 0, z => 0 },
+    "CF.{$cf_id}.Content = 'x'"           => { '-' => 0, other => 0, x => 1, y => 0, z => 0 },
+    "'CF.{$cf_name}' = 'x'"               => { '-' => 0, other => 0, x => 1, y => 0, z => 0 },
+    "'CF.{$cf_name}.Content' = 'x'"       => { '-' => 0, other => 0, x => 1, y => 0, z => 0 },
+    "'CF.$queue.{$cf_id}' = 'x'"          => { '-' => 0, other => 0, x => 1, y => 0, z => 0 },
+    "'CF.$queue.{$cf_id}.Content' = 'x'"  => { '-' => 0, other => 0, x => 1, y => 0, z => 0 },
+    "'CF.$queue.{$cf_name}' = 'x'"        => { '-' => 0, other => 0, x => 1, y => 0, z => 0 },
+    "'CF.$queue.{$cf_name}.Content' = 'x'" => { '-' => 0, other => 0, x => 1, y => 0, z => 0 },
+
+    "CF.{$cf_id} != 'x'"                  => { '-' => 1, other => 1, x => 0, y => 1, z => 1 },
+    "CF.{$cf_id}.Content != 'x'"          => { '-' => 1, other => 1, x => 0, y => 1, z => 1 },
+    "'CF.{$cf_name}' != 'x'"              => { '-' => 1, other => 1, x => 0, y => 1, z => 1 },
+    "'CF.{$cf_name}.Content' != 'x'"      => { '-' => 1, other => 1, x => 0, y => 1, z => 1 },
+    "'CF.$queue.{$cf_id}' != 'x'"         => { '-' => 1, other => 1, x => 0, y => 1, z => 1 },
+    "'CF.$queue.{$cf_id}.Content' != 'x'" => { '-' => 1, other => 1, x => 0, y => 1, z => 1 },
+    "'CF.$queue.{$cf_name}' != 'x'"       => { '-' => 1, other => 1, x => 0, y => 1, z => 1 },
+    "'CF.$queue.{$cf_name}.Content' != 'x'" => { '-' => 1, other => 1, x => 0, y => 1, z => 1 },
+
+    "CF.{$cf_id} = 'x' OR CF.{$cf_id} = 'y'"                        => { '-' => 0, other => 0, x => 1, y => 1, z => 0 },
+    "'CF.{$cf_name}' = 'x' OR 'CF.{$cf_name}' = 'y'"                => { '-' => 0, other => 0, x => 1, y => 1, z => 0 },
+    "'CF.$queue.{$cf_id}' = 'x' OR 'CF.$queue.{$cf_id}' = 'y'"      => { '-' => 0, other => 0, x => 1, y => 1, z => 0 },
+    "'CF.$queue.{$cf_name}' = 'x' OR 'CF.$queue.{$cf_name}' = 'y'"  => { '-' => 0, other => 0, x => 1, y => 1, z => 0 },
+
+    "CF.{$cf_id} = 'x' AND CF.{$cf_id} = 'y'"                        => { '-' => 0, other => 0, x => 0, y => 0, z => 0 },
+    "'CF.{$cf_name}' = 'x' AND 'CF.{$cf_name}' = 'y'"                => { '-' => 0, other => 0, x => 0, y => 0, z => 0 },
+    "'CF.$queue.{$cf_id}' = 'x' AND 'CF.$queue.{$cf_id}' = 'y'"      => { '-' => 0, other => 0, x => 0, y => 0, z => 0 },
+    "'CF.$queue.{$cf_name}' = 'x' AND 'CF.$queue.{$cf_name}' = 'y'"  => { '-' => 0, other => 0, x => 0, y => 0, z => 0 },
+
+    "CF.{$cf_id} != 'x' AND CF.{$cf_id} != 'y'"                        => { '-' => 1, other => 1, x => 0, y => 0, z => 1 },
+    "'CF.{$cf_name}' != 'x' AND 'CF.{$cf_name}' != 'y'"                => { '-' => 1, other => 1, x => 0, y => 0, z => 1 },
+    "'CF.$queue.{$cf_id}' != 'x' AND 'CF.$queue.{$cf_id}' != 'y'"      => { '-' => 1, other => 1, x => 0, y => 0, z => 1 },
+    "'CF.$queue.{$cf_name}' != 'x' AND 'CF.$queue.{$cf_name}' != 'y'"  => { '-' => 1, other => 1, x => 0, y => 0, z => 1 },
+
+    "CF.{$cf_id} = 'x' AND CF.{$cf_id} IS NULL"                        => { '-' => 0, other => 0, x => 0, y => 0, z => 0 },
+    "'CF.{$cf_name}' = 'x' AND 'CF.{$cf_name}' IS NULL"                => { '-' => 0, other => 0, x => 0, y => 0, z => 0 },
+    "'CF.$queue.{$cf_id}' = 'x' AND 'CF.$queue.{$cf_id}' IS NULL"      => { '-' => 0, other => 0, x => 0, y => 0, z => 0 },
+    "'CF.$queue.{$cf_name}' = 'x' AND 'CF.$queue.{$cf_name}' IS NULL"  => { '-' => 0, other => 0, x => 0, y => 0, z => 0 },
+
+    "CF.{$cf_id} = 'x' OR CF.{$cf_id} IS NULL"                        => { '-' => 1, other => 1, x => 1, y => 0, z => 0 },
+    "'CF.{$cf_name}' = 'x' OR 'CF.{$cf_name}' IS NULL"                => { '-' => 1, other => 1, x => 1, y => 0, z => 0 },
+    "'CF.$queue.{$cf_id}' = 'x' OR 'CF.$queue.{$cf_id}' IS NULL"      => { '-' => 1, other => 1, x => 1, y => 0, z => 0 },
+    "'CF.$queue.{$cf_name}' = 'x' OR 'CF.$queue.{$cf_name}' IS NULL"  => { '-' => 1, other => 1, x => 1, y => 0, z => 0 },
+
+    "CF.{$cf_id} = 'x' AND CF.{$cf_id} IS NOT NULL"                        => { '-' => 0, other => 0, x => 1, y => 0, z => 0 },
+    "'CF.{$cf_name}' = 'x' AND 'CF.{$cf_name}' IS NOT NULL"                => { '-' => 0, other => 0, x => 1, y => 0, z => 0 },
+    "'CF.$queue.{$cf_id}' = 'x' AND 'CF.$queue.{$cf_id}' IS NOT NULL"      => { '-' => 0, other => 0, x => 1, y => 0, z => 0 },
+    "'CF.$queue.{$cf_name}' = 'x' AND 'CF.$queue.{$cf_name}' IS NOT NULL"  => { '-' => 0, other => 0, x => 1, y => 0, z => 0 },
+
+    "CF.{$cf_id} = 'x' OR CF.{$cf_id} IS NOT NULL"                        => { '-' => 0, other => 0, x => 1, y => 1, z => 1 },
+    "'CF.{$cf_name}' = 'x' OR 'CF.{$cf_name}' IS NOT NULL"                => { '-' => 0, other => 0, x => 1, y => 1, z => 1 },
+    "'CF.$queue.{$cf_id}' = 'x' OR 'CF.$queue.{$cf_id}' IS NOT NULL"      => { '-' => 0, other => 0, x => 1, y => 1, z => 1 },
+    "'CF.$queue.{$cf_name}' = 'x' OR 'CF.$queue.{$cf_name}' IS NOT NULL"  => { '-' => 0, other => 0, x => 1, y => 1, z => 1 },
 );
 run_tests(@tests);
 

commit 30c3ee61bbc7b4c7b3f99826a8db86c8c8991eb6
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Fri Apr 26 22:58:20 2013 -0400

    Condense IS NULL and IS NOT NULL logic
    
    There is no hint as to why with a column specified, null tests should
    act differently than without -- which the recently added tests bear out.
    Simplify the logic by removing the $column check.

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index f58e07d..e7039fd 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -471,16 +471,16 @@ sub _LimitCustomField {
 
     my $single_value = !blessed($cf) || $cf->SingleValue;
 
-    if ( $null_op && !$column ) {
-        # IS[ NOT] NULL without column is the same as has[ no] any CF value,
-        # we can reuse our default joins for this operation
-        # with column specified we have different situation
+    if ( $null_op ) {
+        # IS NULL is the same as lacks a CF value, and IS NOT NULL means
+        # has any value.  We can reuse our default joins for this
+        # operation.
         my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cf );
         $self->_OpenParen( $args{SUBCLAUSE} );
         $self->Limit(
             %args,
             ALIAS    => $ocfvalias,
-            FIELD    => 'id',
+            FIELD    => ($column || 'id'),
             OPERATOR => $op,
             VALUE    => $value,
         );
diff --git a/t/ticket/search_by_cf_freeform_single.t b/t/ticket/search_by_cf_freeform_single.t
index 616552a..9e1dbbc 100644
--- a/t/ticket/search_by_cf_freeform_single.t
+++ b/t/ticket/search_by_cf_freeform_single.t
@@ -37,12 +37,16 @@ subtest "Creating tickets" => sub {
 
 my @tests = (
     "CF.{$cf_id} IS NULL"                 => { '-' => 1, other => 1, x => 0, y => 0, z => 0 },
+    "CF.{$cf_id}.Content IS NULL"         => { '-' => 1, other => 1, x => 0, y => 0, z => 0 },
     "'CF.{$cf_name}' IS NULL"             => { '-' => 1, other => 1, x => 0, y => 0, z => 0 },
+    "'CF.{$cf_name}.Content' IS NULL"     => { '-' => 1, other => 1, x => 0, y => 0, z => 0 },
     "'CF.$queue.{$cf_id}' IS NULL"        => { '-' => 1, other => 1, x => 0, y => 0, z => 0 },
     "'CF.$queue.{$cf_name}' IS NULL"      => { '-' => 1, other => 1, x => 0, y => 0, z => 0 },
 
     "CF.{$cf_id} IS NOT NULL"             => { '-' => 0, other => 0, x => 1, y => 1, z => 1 },
+    "CF.{$cf_id}.Content IS NOT NULL"     => { '-' => 0, other => 0, x => 1, y => 1, z => 1 },
     "'CF.{$cf_name}' IS NOT NULL"         => { '-' => 0, other => 0, x => 1, y => 1, z => 1 },
+    "'CF.{$cf_name}.Content' IS NOT NULL" => { '-' => 0, other => 0, x => 1, y => 1, z => 1 },
     "'CF.$queue.{$cf_id}' IS NOT NULL"    => { '-' => 0, other => 0, x => 1, y => 1, z => 1 },
     "'CF.$queue.{$cf_name}' IS NOT NULL"  => { '-' => 0, other => 0, x => 1, y => 1, z => 1 },
 

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


More information about the Rt-commit mailing list