[Rt-commit] rt branch, 4.2/cf-searching, created. rt-4.1.8-182-g2b534e6

Alex Vandiver alexmv at bestpractical.com
Tue Apr 30 16:11:08 EDT 2013


The branch, 4.2/cf-searching has been created
        at  2b534e693a091a802589adecc1fb69fa36a0f949 (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 46f2e4c77aeb25aea99c249b1f7465fb033c72d9
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Apr 30 16:07:40 2013 -0400

    Ensure that ENTRYAGGRATOR does not mistakenly OR the ordering limit with some other limit

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index 1f6e01b..13afc88 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -135,6 +135,7 @@ sub _OrderByCF {
         OPERATOR   => 'IS NOT',
         VALUE      => 'NULL',
         ENTRYAGGREGATOR => 'AND',
+        SUBCLAUSE  => ".ordering",
     ) if $CFs;
     my $CFvs = $self->Join(
         TYPE   => 'LEFT',

commit 3845faa13486b1baf219287a491de8b662c29f92
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 52da8745631f69a3060baf58b98d0361cb5c9f60
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 13afc88..40db514 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -555,8 +555,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 ) {
@@ -568,11 +566,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
@@ -708,20 +705,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 and not $null_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 73afba7c021c8d84a9be49ed4596a8fbedc66f15
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 f252e458742185390b6f626a52d6da34f96f34d0
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 40db514..6e46fcb 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -472,16 +472,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,
         );
@@ -707,7 +707,7 @@ sub _LimitCustomField {
             $self->_CloseParen( $args{SUBCLAUSE} );
         }
 
-        if ($negative_op and not $null_op) {
+        if ($negative_op) {
             $self->Limit(
                 ALIAS           => $ocfvalias,
                 FIELD           => $column || 'Content',
diff --git a/t/ticket/search_by_cf_freeform_single.t b/t/ticket/search_by_cf_freeform_single.t
index 616552a..5ce8a09 100644
--- a/t/ticket/search_by_cf_freeform_single.t
+++ b/t/ticket/search_by_cf_freeform_single.t
@@ -36,33 +36,49 @@ subtest "Creating tickets" => sub {
 };
 
 my @tests = (
-    "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} 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_id}.LargeContent IS NULL"           => { '-' => 1, other => 1, x => 1, y => 1, z => 1 },
+    "'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.{$cf_name}.LargeContent' IS NULL"       => { '-' => 1, other => 1, x => 1, y => 1, z => 1 },
+    "'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_id}.LargeContent IS NOT NULL"       => { '-' => 0, other => 0, x => 0, y => 0, z => 0 },
+    "'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.{$cf_name}.LargeContent' IS NOT NULL"   => { '-' => 0, other => 0, x => 0, y => 0, z => 0 },
+    "'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_id}.LargeContent = 'x'"             => { '-' => 0, other => 0, x => 0, 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.{$cf_name}.LargeContent' = 'x'"         => { '-' => 0, other => 0, x => 0, 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_id}.LargeContent' = 'x'"    => { '-' => 0, other => 0, x => 0, 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.$queue.{$cf_name}.LargeContent' = 'x'"  => { '-' => 0, other => 0, x => 0, 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_id}.LargeContent != 'x'"            => { '-' => 1, other => 1, x => 1, 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.{$cf_name}.LargeContent' != 'x'"        => { '-' => 1, other => 1, x => 1, 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_id}.LargeContent' != 'x'"   => { '-' => 1, other => 1, x => 1, 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.$queue.{$cf_name}.LargeContent' != 'x'" => { '-' => 1, other => 1, x => 1, 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 },

commit d219100663635859ffa8f5b29561a084bbe998e0
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Apr 30 14:17:22 2013 -0400

    Factor out blessed($cf) check

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index 6e46fcb..bc08581 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -406,68 +406,61 @@ sub _LimitCustomField {
         return %args;
     };
 
-    if ( blessed($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");
+    ########## Content pre-parsing if we know things about the CF
+    if ( blessed($cf) ) {
+        if ( $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 ( blessed($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;
-        }
+        if ( $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 ) {
+            my ( $start_ip, $end_ip ) =
+              RT::ObjectCustomFieldValue->ParseIPRange($value);
+            if ( $start_ip && $end_ip ) {
+                if ( $op =~ /^<=?$/ ) {
                     $value = $start_ip;
-                }
-                else {
+                } elsif ($op =~ /^>=?$/ ) {
                     $value = $end_ip;
+                } else {
+                    $value = join '-', $start_ip, $end_ip;
                 }
-            }
-            else {
-                $value = join '-', $start_ip, $end_ip;
+            } else {
+                $RT::Logger->warn("$value is not a valid IPAddressRange");
             }
         }
-        else {
-            $RT::Logger->warn("$value is not a valid IPAddressRange");
-        }
-    }
 
-    if ( blessed($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;
+        if ( $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'
+                           # Heuristics to determine if a date, and not
+                           # a datetime, was entered:
+                    || $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");
             }
         }
-        else {
-            $RT::Logger->warn("$value is not a valid date string");
-        }
     }
 
     my $single_value = !blessed($cf) || $cf->SingleValue;

commit 91cda145ffcacef108d89574c43a45ffe5442328
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Apr 30 14:20:37 2013 -0400

    Move IPAddressRange recursive call earlier

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index bc08581..ac13c7c 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -438,6 +438,50 @@ sub _LimitCustomField {
             } else {
                 $RT::Logger->warn("$value is not a valid IPAddressRange");
             }
+
+            # Recurse if they want a range comparison
+            if ( $op !~ /^[<>]=?$/ ) {
+                my ($start_ip, $end_ip) = split /-/, $value;
+                $self->_OpenParen( $args{SUBCLAUSE} );
+                # Ideally we would limit >= 000.000.000.000 and <=
+                # 255.255.255.255 so DB optimizers could use better
+                # estimations and scan less rows, but this breaks with IPv6.
+                if ( $op !~ /NOT|!=|<>/i ) { # positive equation
+                    $self->_LimitCustomField(
+                        %args,
+                        OPERATOR    => '<=',
+                        VALUE       => $end_ip,
+                        CUSTOMFIELD => $cf,
+                        COLUMN      => 'Content',
+                    );
+                    $self->_LimitCustomField(
+                        %args,
+                        OPERATOR    => '>=',
+                        VALUE       => $start_ip,
+                        CUSTOMFIELD => $cf,
+                        COLUMN      => 'LargeContent',
+                        ENTRYAGGREGATOR => 'AND',
+                    );
+                } else { # negative equation
+                    $self->_LimitCustomField(
+                        %args,
+                        OPERATOR    => '>',
+                        VALUE       => $end_ip,
+                        CUSTOMFIELD => $cf,
+                        COLUMN      => 'Content',
+                    );
+                    $self->_LimitCustomField(
+                        %args,
+                        OPERATOR    => '<',
+                        VALUE       => $start_ip,
+                        CUSTOMFIELD => $cf,
+                        COLUMN      => 'LargeContent',
+                        ENTRYAGGREGATOR => 'OR',
+                    );
+                }
+                $self->_CloseParen( $args{SUBCLAUSE} );
+                return;
+            }
         }
 
         if ( $cf->Type =~ /^Date(?:Time)?$/ ) {
@@ -488,61 +532,6 @@ sub _LimitCustomField {
         ) if $CFs;
         $self->_CloseParen( $args{SUBCLAUSE} );
     }
-    elsif ( $op !~ /^[<>]=?$/ && (  blessed($cf) && $cf->Type eq 'IPAddressRange')) {
-        my ($start_ip, $end_ip) = split /-/, $value;
-        $self->_OpenParen( $args{SUBCLAUSE} );
-        if ( $op !~ /NOT|!=|<>/i ) { # positive equation
-            $self->_LimitCustomField(
-                %args,
-                OPERATOR    => '<=',
-                VALUE       => $end_ip,
-                CUSTOMFIELD => $cf,
-                COLUMN      => 'Content',
-            );
-            $self->_LimitCustomField(
-                %args,
-                OPERATOR    => '>=',
-                VALUE       => $start_ip,
-                CUSTOMFIELD => $cf,
-                COLUMN      => '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->_LimitCustomField(
-                %args,
-                OPERATOR    => '>',
-                VALUE       => $end_ip,
-                CUSTOMFIELD => $cf,
-                COLUMN      => 'Content',
-            );
-            $self->_LimitCustomField(
-                %args,
-                OPERATOR    => '<',
-                VALUE       => $start_ip,
-                CUSTOMFIELD => $cf,
-                COLUMN      => '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( $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 );

commit e6369aa46a944de73ec6e8bec5586eb3f05fb793
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Apr 30 14:23:32 2013 -0400

    Move Date range check for DateTimes into a recursive call, earlier

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index ac13c7c..6127d41 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -504,6 +504,36 @@ sub _LimitCustomField {
             } else {
                 $RT::Logger->warn("$value is not a valid date string");
             }
+
+            # Recurse if day equality is being checked on a datetime
+            if ( $cf->Type eq 'DateTime' and $op eq '=' && $value !~ /:/ ) {
+                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( $args{SUBCLAUSE} );
+                $self->_LimitCustomField(
+                    %args,
+                    OPERATOR        => ">=",
+                    VALUE           => $daystart,
+                    CUSTOMFIELD     => $cf,
+                    COLUMN          => 'Content',
+                    ENTRYAGGREGATOR => 'AND',
+                );
+
+                $self->_LimitCustomField(
+                    %args,
+                    OPERATOR        => "<",
+                    VALUE           => $dayend,
+                    CUSTOMFIELD     => $cf,
+                    COLUMN          => 'Content',
+                    ENTRYAGGREGATOR => 'AND',
+                );
+                $self->_CloseParen( $args{SUBCLAUSE} );
+                return;
+            }
         }
     }
 
@@ -552,39 +582,7 @@ sub _LimitCustomField {
         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
-                # 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( $args{SUBCLAUSE} );
-
-                $self->Limit(
-                    %args,
-                    ALIAS    => $ocfvalias,
-                    FIELD    => 'Content',
-                    OPERATOR => ">=",
-                    VALUE    => $daystart,
-                );
-
-                $self->Limit(
-                    %args,
-                    ALIAS    => $ocfvalias,
-                    FIELD    => 'Content',
-                    OPERATOR => "<",
-                    VALUE    => $dayend,
-                    ENTRYAGGREGATOR => 'AND',
-                );
-
-                $self->_CloseParen( $args{SUBCLAUSE} );
-            }
-            elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
+            if ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
                 if ( length( Encode::encode_utf8($value) ) < 256 ) {
                     $self->Limit(
                         %args,

commit f27403e5356986badd92016dcba8647f71264ff2
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Apr 30 14:24:35 2013 -0400

    Remove a mis-placed comment

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index 6127d41..d73186d 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -379,11 +379,6 @@ sub _LimitCustomField {
 
     $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
-# 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) );
 
@@ -392,7 +387,7 @@ sub _LimitCustomField {
 
         my %args = @_;
         return %args unless $args{'FIELD'} eq 'LargeContent';
-        
+
         my $op = $args{'OPERATOR'};
         if ( $op eq '=' ) {
             $args{'OPERATOR'} = 'MATCHES';

commit 8051261f2048cd2a74f0155b86a22a7bc0f34e1b
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Apr 30 14:31:26 2013 -0400

    Short-circuit the NULL tests

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index d73186d..8d61f27 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -380,7 +380,6 @@ sub _LimitCustomField {
     $args{SUBCLAUSE} ||= "cf-$cfkey";
 
     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';
@@ -534,10 +533,9 @@ sub _LimitCustomField {
 
     my $single_value = !blessed($cf) || $cf->SingleValue;
 
-    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.
+    ########## Limits
+    # IS NULL and IS NOT NULL checks
+    if ( $op =~ /^IS( NOT)?$/i ) {
         my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cf );
         $self->_OpenParen( $args{SUBCLAUSE} );
         $self->Limit(
@@ -547,6 +545,7 @@ sub _LimitCustomField {
             OPERATOR => $op,
             VALUE    => $value,
         );
+        # See below for an explanation of this limit
         $self->Limit(
             ALIAS      => $CFs,
             FIELD      => 'Name',
@@ -556,8 +555,9 @@ sub _LimitCustomField {
             SUBCLAUSE  => $args{SUBCLAUSE},
         ) if $CFs;
         $self->_CloseParen( $args{SUBCLAUSE} );
+        return;
     }
-    elsif ( !$negative_op || $single_value ) {
+    if ( !$negative_op || $single_value ) {
         $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if not $single_value and not $op =~ /^[<>]=?$/;
         my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cf );
 

commit d378b4f507382400b7e7fdbb6d133e0f5ab9307c
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Apr 30 14:34:06 2013 -0400

    Collect helper variables closer to where they are used

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index 8d61f27..ec36604 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -379,7 +379,6 @@ sub _LimitCustomField {
 
     $args{SUBCLAUSE} ||= "cf-$cfkey";
 
-    my $negative_op = ($op eq '!=' || $op =~ /\bNOT\b/i);
 
     my $fix_op = sub {
         return @_ unless RT->Config->Get('DatabaseType') eq 'Oracle';
@@ -531,8 +530,6 @@ sub _LimitCustomField {
         }
     }
 
-    my $single_value = !blessed($cf) || $cf->SingleValue;
-
     ########## Limits
     # IS NULL and IS NOT NULL checks
     if ( $op =~ /^IS( NOT)?$/i ) {
@@ -557,6 +554,10 @@ sub _LimitCustomField {
         $self->_CloseParen( $args{SUBCLAUSE} );
         return;
     }
+
+    my $single_value = !blessed($cf) || $cf->SingleValue;
+    my $negative_op = ($op eq '!=' || $op =~ /\bNOT\b/i);
+
     if ( !$negative_op || $single_value ) {
         $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if not $single_value and not $op =~ /^[<>]=?$/;
         my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cf );

commit cc89edc5b9cb3b789a7faf0aedcf90993ec310a7
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Apr 30 15:00:11 2013 -0400

    Apply Demorgan's to make the less common case clearer

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index ec36604..8e6adc5 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -558,7 +558,45 @@ sub _LimitCustomField {
     my $single_value = !blessed($cf) || $cf->SingleValue;
     my $negative_op = ($op eq '!=' || $op =~ /\bNOT\b/i);
 
-    if ( !$negative_op || $single_value ) {
+    # A negative limit on a multi-value CF means _none_ of the values
+    # are the given value
+    if ( $negative_op and not $single_value ) {
+        $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
+        my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cf );
+
+        # Reverse the limit we apply to the join, and check IS NULL
+        $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   => $ocfvalias,
+                ALIAS      => $ocfvalias,
+                FIELD      => $column,
+                OPERATOR   => $op,
+                VALUE      => $value,
+                CASESENSITIVE => 0,
+            ) );
+        }
+        else {
+            $self->Limit(
+                LEFTJOIN   => $ocfvalias,
+                ALIAS      => $ocfvalias,
+                FIELD      => 'Content',
+                OPERATOR   => $op,
+                VALUE      => $value,
+                CASESENSITIVE => 0,
+            );
+        }
+        $self->Limit(
+            %args,
+            ALIAS      => $ocfvalias,
+            FIELD      => 'id',
+            OPERATOR   => 'IS',
+            VALUE      => 'NULL',
+        );
+    } else {
         $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if not $single_value and not $op =~ /^[<>]=?$/;
         my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cf );
 
@@ -696,43 +734,6 @@ sub _LimitCustomField {
         $self->_CloseParen( $args{SUBCLAUSE} );
 
     }
-    else {
-        $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
-        my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cf );
-
-        # 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   => $ocfvalias,
-                ALIAS      => $ocfvalias,
-                FIELD      => $column,
-                OPERATOR   => $op,
-                VALUE      => $value,
-                CASESENSITIVE => 0,
-            ) );
-        }
-        else {
-            $self->Limit(
-                LEFTJOIN   => $ocfvalias,
-                ALIAS      => $ocfvalias,
-                FIELD      => 'Content',
-                OPERATOR   => $op,
-                VALUE      => $value,
-                CASESENSITIVE => 0,
-            );
-        }
-        $self->Limit(
-            %args,
-            ALIAS      => $ocfvalias,
-            FIELD      => 'id',
-            OPERATOR   => 'IS',
-            VALUE      => 'NULL',
-        );
-    }
 }
 
 =head2 Limit PARAMHASH

commit 07f5e37d3f91f20516f0c1ee7e8e6fcd59c88227
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Apr 30 15:02:07 2013 -0400

    Fold two parallel statements into one, removing a misleading and incorrect comment

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index 8e6adc5..0b8d85a 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -567,28 +567,14 @@ sub _LimitCustomField {
         # Reverse the limit we apply to the join, and check IS NULL
         $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   => $ocfvalias,
-                ALIAS      => $ocfvalias,
-                FIELD      => $column,
-                OPERATOR   => $op,
-                VALUE      => $value,
-                CASESENSITIVE => 0,
-            ) );
-        }
-        else {
-            $self->Limit(
-                LEFTJOIN   => $ocfvalias,
-                ALIAS      => $ocfvalias,
-                FIELD      => 'Content',
-                OPERATOR   => $op,
-                VALUE      => $value,
-                CASESENSITIVE => 0,
-            );
-        }
+        $self->Limit( $fix_op->(
+            LEFTJOIN   => $ocfvalias,
+            ALIAS      => $ocfvalias,
+            FIELD      => ($column || 'Content'),
+            OPERATOR   => $op,
+            VALUE      => $value,
+            CASESENSITIVE => 0,
+        ) );
         $self->Limit(
             %args,
             ALIAS      => $ocfvalias,

commit 3f11201eaa10e371a0074e614cc36b672192e2c0
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Apr 30 14:36:52 2013 -0400

    Factor out the common join to the OCFV table
    
    The two calls to _CustomFieldJoin can be moved out and into one call, as
    long as care is taken to preserve the logic as to when new indexes are
    necessary.  Specifically, new indexes are never necessary with
    single-value CFs, and are used if the operator is a negative operator OR
    is not /^[<>]=?$/ -- that is, if it is not a relative comparitor.

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index 0b8d85a..b78551f 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -558,12 +558,13 @@ sub _LimitCustomField {
     my $single_value = !blessed($cf) || $cf->SingleValue;
     my $negative_op = ($op eq '!=' || $op =~ /\bNOT\b/i);
 
+    $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++
+        if not $single_value and $op =~ /^(!?=|(NOT )?LIKE)$/i;
+    my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cf );
+
     # A negative limit on a multi-value CF means _none_ of the values
     # are the given value
     if ( $negative_op and not $single_value ) {
-        $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
-        my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cf );
-
         # Reverse the limit we apply to the join, and check IS NULL
         $op =~ s/!|NOT\s+//i;
 
@@ -583,8 +584,6 @@ sub _LimitCustomField {
             VALUE      => 'NULL',
         );
     } else {
-        $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if not $single_value and not $op =~ /^[<>]=?$/;
-        my ($ocfvalias, $CFs) = $self->_CustomFieldJoin( $cfkey, $cf );
 
         $self->_OpenParen( $args{SUBCLAUSE} );
         # if column is defined then deal only with it

commit 9e4b69793a04dbf867e189564d9e2e685df2f8b9
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Apr 30 15:12:41 2013 -0400

    Short-circuit from negative queries on multiple-value CFs

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index b78551f..7f3de96 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -583,66 +583,27 @@ sub _LimitCustomField {
             OPERATOR   => 'IS',
             VALUE      => 'NULL',
         );
-    } else {
+        return;
+    }
 
+    $self->_OpenParen( $args{SUBCLAUSE} );
+    # if column is defined then deal only with it
+    # otherwise search in Content and in LargeContent
+    if ( $column ) {
+        $self->Limit( $fix_op->(
+            %args,
+            ALIAS      => $ocfvalias,
+            FIELD      => $column,
+            OPERATOR   => $op,
+            VALUE      => $value,
+            CASESENSITIVE => 0,
+        ) );
+    }
+    else {
         $self->_OpenParen( $args{SUBCLAUSE} );
-        # if column is defined then deal only with it
-        # otherwise search in Content and in LargeContent
-        if ( $column ) {
-            $self->Limit( $fix_op->(
-                %args,
-                ALIAS      => $ocfvalias,
-                FIELD      => $column,
-                OPERATOR   => $op,
-                VALUE      => $value,
-                CASESENSITIVE => 0,
-            ) );
-        }
-        else {
-            $self->_OpenParen( $args{SUBCLAUSE} );
-            $self->_OpenParen( $args{SUBCLAUSE} );
-            if ( $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,
-                    );
-                }
-                else {
-                    $self->_OpenParen( $args{SUBCLAUSE} );
-                    $self->Limit(
-                        ALIAS           => $ocfvalias,
-                        FIELD           => 'Content',
-                        OPERATOR        => '=',
-                        VALUE           => '',
-                        ENTRYAGGREGATOR => 'OR',
-                        SUBCLAUSE       => $args{SUBCLAUSE},
-                    );
-                    $self->Limit(
-                        ALIAS           => $ocfvalias,
-                        FIELD           => 'Content',
-                        OPERATOR        => 'IS',
-                        VALUE           => 'NULL',
-                        ENTRYAGGREGATOR => 'OR',
-                        SUBCLAUSE       => $args{SUBCLAUSE},
-                    );
-                    $self->_CloseParen( $args{SUBCLAUSE} );
-                    $self->Limit( $fix_op->(
-                        ALIAS           => $ocfvalias,
-                        FIELD           => 'LargeContent',
-                        OPERATOR        => $op,
-                        VALUE           => $value,
-                        ENTRYAGGREGATOR => 'AND',
-                        SUBCLAUSE       => $args{SUBCLAUSE},
-                        CASESENSITIVE => 0,
-                    ) );
-                }
-            }
-            else {
+        $self->_OpenParen( $args{SUBCLAUSE} );
+        if ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
+            if ( length( Encode::encode_utf8($value) ) < 256 ) {
                 $self->Limit(
                     %args,
                     ALIAS    => $ocfvalias,
@@ -651,24 +612,24 @@ sub _LimitCustomField {
                     VALUE    => $value,
                     CASESENSITIVE => 0,
                 );
-
-                $self->_OpenParen( $args{SUBCLAUSE} );
+            }
+            else {
                 $self->_OpenParen( $args{SUBCLAUSE} );
                 $self->Limit(
                     ALIAS           => $ocfvalias,
                     FIELD           => 'Content',
                     OPERATOR        => '=',
                     VALUE           => '',
+                    ENTRYAGGREGATOR => 'OR',
                     SUBCLAUSE       => $args{SUBCLAUSE},
-                    ENTRYAGGREGATOR => 'OR'
                 );
                 $self->Limit(
                     ALIAS           => $ocfvalias,
                     FIELD           => 'Content',
                     OPERATOR        => 'IS',
                     VALUE           => 'NULL',
+                    ENTRYAGGREGATOR => 'OR',
                     SUBCLAUSE       => $args{SUBCLAUSE},
-                    ENTRYAGGREGATOR => 'OR'
                 );
                 $self->_CloseParen( $args{SUBCLAUSE} );
                 $self->Limit( $fix_op->(
@@ -680,45 +641,83 @@ sub _LimitCustomField {
                     SUBCLAUSE       => $args{SUBCLAUSE},
                     CASESENSITIVE => 0,
                 ) );
-                $self->_CloseParen( $args{SUBCLAUSE} );
             }
-            $self->_CloseParen( $args{SUBCLAUSE} );
+        }
+        else {
+            $self->Limit(
+                %args,
+                ALIAS    => $ocfvalias,
+                FIELD    => 'Content',
+                OPERATOR => $op,
+                VALUE    => $value,
+                CASESENSITIVE => 0,
+            );
 
-            # 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->_OpenParen( $args{SUBCLAUSE} );
+            $self->_OpenParen( $args{SUBCLAUSE} );
             $self->Limit(
-                ALIAS           => $CFs,
-                FIELD           => 'Name',
-                OPERATOR        => 'IS NOT',
-                VALUE           => 'NULL',
-                ENTRYAGGREGATOR => 'AND',
+                ALIAS           => $ocfvalias,
+                FIELD           => 'Content',
+                OPERATOR        => '=',
+                VALUE           => '',
                 SUBCLAUSE       => $args{SUBCLAUSE},
-            ) if $CFs;
-            $self->_CloseParen( $args{SUBCLAUSE} );
-        }
-
-        if ($negative_op) {
+                ENTRYAGGREGATOR => 'OR'
+            );
             $self->Limit(
                 ALIAS           => $ocfvalias,
-                FIELD           => $column || 'Content',
+                FIELD           => 'Content',
                 OPERATOR        => 'IS',
                 VALUE           => 'NULL',
-                ENTRYAGGREGATOR => 'OR',
                 SUBCLAUSE       => $args{SUBCLAUSE},
+                ENTRYAGGREGATOR => 'OR'
             );
+            $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( $args{SUBCLAUSE} );
         }
         $self->_CloseParen( $args{SUBCLAUSE} );
 
+        # 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',
+            ENTRYAGGREGATOR => 'AND',
+            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} );
 }
 
 =head2 Limit PARAMHASH

commit 837f698c5a1bdd414e10f19c02c590513831b7a2
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Apr 30 15:19:08 2013 -0400

    Push negative-op into we-have-a-column if/else to increase reading locality

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index 7f3de96..9d020d7 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -586,10 +586,9 @@ sub _LimitCustomField {
         return;
     }
 
-    $self->_OpenParen( $args{SUBCLAUSE} );
-    # if column is defined then deal only with it
-    # otherwise search in Content and in LargeContent
+    # If column is defined, then we just search it that, with no magic
     if ( $column ) {
+        $self->_OpenParen( $args{SUBCLAUSE} );
         $self->Limit( $fix_op->(
             %args,
             ALIAS      => $ocfvalias,
@@ -598,10 +597,20 @@ sub _LimitCustomField {
             VALUE      => $value,
             CASESENSITIVE => 0,
         ) );
+        $self->Limit(
+            ALIAS           => $ocfvalias,
+            FIELD           => $column,
+            OPERATOR        => 'IS',
+            VALUE           => 'NULL',
+            ENTRYAGGREGATOR => 'OR',
+            SUBCLAUSE       => $args{SUBCLAUSE},
+        ) if $negative_op;
+        $self->_CloseParen( $args{SUBCLAUSE} );
     }
     else {
         $self->_OpenParen( $args{SUBCLAUSE} );
         $self->_OpenParen( $args{SUBCLAUSE} );
+        $self->_OpenParen( $args{SUBCLAUSE} );
         if ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
             if ( length( Encode::encode_utf8($value) ) < 256 ) {
                 $self->Limit(
@@ -705,19 +714,19 @@ 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} );
 }
 
 =head2 Limit PARAMHASH

commit bc89fa128232b458971f39af409cca55b60191d2
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Apr 30 15:20:48 2013 -0400

    Short-circuit from column-specific limit

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index 9d020d7..f582870 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -606,53 +606,13 @@ sub _LimitCustomField {
             SUBCLAUSE       => $args{SUBCLAUSE},
         ) if $negative_op;
         $self->_CloseParen( $args{SUBCLAUSE} );
+        return;
     }
-    else {
-        $self->_OpenParen( $args{SUBCLAUSE} );
-        $self->_OpenParen( $args{SUBCLAUSE} );
-        $self->_OpenParen( $args{SUBCLAUSE} );
-        if ( $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,
-                );
-            }
-            else {
-                $self->_OpenParen( $args{SUBCLAUSE} );
-                $self->Limit(
-                    ALIAS           => $ocfvalias,
-                    FIELD           => 'Content',
-                    OPERATOR        => '=',
-                    VALUE           => '',
-                    ENTRYAGGREGATOR => 'OR',
-                    SUBCLAUSE       => $args{SUBCLAUSE},
-                );
-                $self->Limit(
-                    ALIAS           => $ocfvalias,
-                    FIELD           => 'Content',
-                    OPERATOR        => 'IS',
-                    VALUE           => 'NULL',
-                    ENTRYAGGREGATOR => 'OR',
-                    SUBCLAUSE       => $args{SUBCLAUSE},
-                );
-                $self->_CloseParen( $args{SUBCLAUSE} );
-                $self->Limit( $fix_op->(
-                    ALIAS           => $ocfvalias,
-                    FIELD           => 'LargeContent',
-                    OPERATOR        => $op,
-                    VALUE           => $value,
-                    ENTRYAGGREGATOR => 'AND',
-                    SUBCLAUSE       => $args{SUBCLAUSE},
-                    CASESENSITIVE => 0,
-                ) );
-            }
-        }
-        else {
+    $self->_OpenParen( $args{SUBCLAUSE} );
+    $self->_OpenParen( $args{SUBCLAUSE} );
+    $self->_OpenParen( $args{SUBCLAUSE} );
+    if ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
+        if ( length( Encode::encode_utf8($value) ) < 256 ) {
             $self->Limit(
                 %args,
                 ALIAS    => $ocfvalias,
@@ -661,24 +621,24 @@ sub _LimitCustomField {
                 VALUE    => $value,
                 CASESENSITIVE => 0,
             );
-
-            $self->_OpenParen( $args{SUBCLAUSE} );
+        }
+        else {
             $self->_OpenParen( $args{SUBCLAUSE} );
             $self->Limit(
                 ALIAS           => $ocfvalias,
                 FIELD           => 'Content',
                 OPERATOR        => '=',
                 VALUE           => '',
+                ENTRYAGGREGATOR => 'OR',
                 SUBCLAUSE       => $args{SUBCLAUSE},
-                ENTRYAGGREGATOR => 'OR'
             );
             $self->Limit(
                 ALIAS           => $ocfvalias,
                 FIELD           => 'Content',
                 OPERATOR        => 'IS',
                 VALUE           => 'NULL',
+                ENTRYAGGREGATOR => 'OR',
                 SUBCLAUSE       => $args{SUBCLAUSE},
-                ENTRYAGGREGATOR => 'OR'
             );
             $self->_CloseParen( $args{SUBCLAUSE} );
             $self->Limit( $fix_op->(
@@ -690,43 +650,81 @@ sub _LimitCustomField {
                 SUBCLAUSE       => $args{SUBCLAUSE},
                 CASESENSITIVE => 0,
             ) );
-            $self->_CloseParen( $args{SUBCLAUSE} );
         }
-        $self->_CloseParen( $args{SUBCLAUSE} );
+    }
+    else {
+        $self->Limit(
+            %args,
+            ALIAS    => $ocfvalias,
+            FIELD    => 'Content',
+            OPERATOR => $op,
+            VALUE    => $value,
+            CASESENSITIVE => 0,
+        );
 
-        # 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->_OpenParen( $args{SUBCLAUSE} );
+        $self->_OpenParen( $args{SUBCLAUSE} );
         $self->Limit(
-            ALIAS           => $CFs,
-            FIELD           => 'Name',
-            OPERATOR        => 'IS NOT',
+            ALIAS           => $ocfvalias,
+            FIELD           => 'Content',
+            OPERATOR        => '=',
+            VALUE           => '',
+            SUBCLAUSE       => $args{SUBCLAUSE},
+            ENTRYAGGREGATOR => 'OR'
+        );
+        $self->Limit(
+            ALIAS           => $ocfvalias,
+            FIELD           => 'Content',
+            OPERATOR        => 'IS',
             VALUE           => 'NULL',
-            ENTRYAGGREGATOR => 'AND',
             SUBCLAUSE       => $args{SUBCLAUSE},
-        ) if $CFs;
+            ENTRYAGGREGATOR => 'OR'
+        );
         $self->_CloseParen( $args{SUBCLAUSE} );
-        if ($negative_op) {
-            $self->Limit(
-                ALIAS           => $ocfvalias,
-                FIELD           => $column || 'Content',
-                OPERATOR        => 'IS',
-                VALUE           => 'NULL',
-                ENTRYAGGREGATOR => 'OR',
-                SUBCLAUSE       => $args{SUBCLAUSE},
-            );
-        }
+        $self->Limit( $fix_op->(
+            ALIAS           => $ocfvalias,
+            FIELD           => 'LargeContent',
+            OPERATOR        => $op,
+            VALUE           => $value,
+            ENTRYAGGREGATOR => 'AND',
+            SUBCLAUSE       => $args{SUBCLAUSE},
+            CASESENSITIVE => 0,
+        ) );
         $self->_CloseParen( $args{SUBCLAUSE} );
     }
-
+    $self->_CloseParen( $args{SUBCLAUSE} );
+
+    # 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',
+        ENTRYAGGREGATOR => 'AND',
+        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} );
 }
 
 =head2 Limit PARAMHASH

commit 2b534e693a091a802589adecc1fb69fa36a0f949
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Apr 30 15:29:09 2013 -0400

    Merge two parallel content search branches
    
    The only occasion upon which Content should not be searched is if the
    search is an equality search and the value we are matching is certainly
    larger than would fit into Content.

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index f582870..b09194c 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -608,91 +608,50 @@ sub _LimitCustomField {
         $self->_CloseParen( $args{SUBCLAUSE} );
         return;
     }
-    $self->_OpenParen( $args{SUBCLAUSE} );
-    $self->_OpenParen( $args{SUBCLAUSE} );
-    $self->_OpenParen( $args{SUBCLAUSE} );
-    if ( $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,
-            );
-        }
-        else {
-            $self->_OpenParen( $args{SUBCLAUSE} );
-            $self->Limit(
-                ALIAS           => $ocfvalias,
-                FIELD           => 'Content',
-                OPERATOR        => '=',
-                VALUE           => '',
-                ENTRYAGGREGATOR => 'OR',
-                SUBCLAUSE       => $args{SUBCLAUSE},
-            );
-            $self->Limit(
-                ALIAS           => $ocfvalias,
-                FIELD           => 'Content',
-                OPERATOR        => 'IS',
-                VALUE           => 'NULL',
-                ENTRYAGGREGATOR => 'OR',
-                SUBCLAUSE       => $args{SUBCLAUSE},
-            );
-            $self->_CloseParen( $args{SUBCLAUSE} );
-            $self->Limit( $fix_op->(
-                ALIAS           => $ocfvalias,
-                FIELD           => 'LargeContent',
-                OPERATOR        => $op,
-                VALUE           => $value,
-                ENTRYAGGREGATOR => 'AND',
-                SUBCLAUSE       => $args{SUBCLAUSE},
-                CASESENSITIVE => 0,
-            ) );
-        }
-    }
-    else {
-        $self->Limit(
-            %args,
-            ALIAS    => $ocfvalias,
-            FIELD    => 'Content',
-            OPERATOR => $op,
-            VALUE    => $value,
-            CASESENSITIVE => 0,
-        );
 
-        $self->_OpenParen( $args{SUBCLAUSE} );
-        $self->_OpenParen( $args{SUBCLAUSE} );
-        $self->Limit(
-            ALIAS           => $ocfvalias,
-            FIELD           => 'Content',
-            OPERATOR        => '=',
-            VALUE           => '',
-            SUBCLAUSE       => $args{SUBCLAUSE},
-            ENTRYAGGREGATOR => 'OR'
-        );
-        $self->Limit(
-            ALIAS           => $ocfvalias,
-            FIELD           => 'Content',
-            OPERATOR        => 'IS',
-            VALUE           => 'NULL',
-            SUBCLAUSE       => $args{SUBCLAUSE},
-            ENTRYAGGREGATOR => 'OR'
-        );
-        $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( $args{SUBCLAUSE} );
-    }
-    $self->_CloseParen( $args{SUBCLAUSE} );
+    $self->_OpenParen( $args{SUBCLAUSE} ); # For negative_op "OR it is null" clause
+    $self->_OpenParen( $args{SUBCLAUSE} ); # NAME IS NOT NULL clause
+
+    $self->_OpenParen( $args{SUBCLAUSE} ); # Check Content / LargeContent
+    $self->Limit(
+        %args,
+        ALIAS    => $ocfvalias,
+        FIELD    => 'Content',
+        OPERATOR => $op,
+        VALUE    => $value,
+        CASESENSITIVE => 0,
+    ) unless ($op =~ /^(=|!=|<>)$/ and length( Encode::encode_utf8($value) ) > 256);
+    $self->_OpenParen( $args{SUBCLAUSE} ); # LargeContent check
+    $self->_OpenParen( $args{SUBCLAUSE} ); # Content is null?
+    $self->Limit(
+        ALIAS           => $ocfvalias,
+        FIELD           => 'Content',
+        OPERATOR        => '=',
+        VALUE           => '',
+        ENTRYAGGREGATOR => 'OR',
+        SUBCLAUSE       => $args{SUBCLAUSE},
+    );
+    $self->Limit(
+        ALIAS           => $ocfvalias,
+        FIELD           => 'Content',
+        OPERATOR        => 'IS',
+        VALUE           => 'NULL',
+        ENTRYAGGREGATOR => 'OR',
+        SUBCLAUSE       => $args{SUBCLAUSE},
+    );
+    $self->_CloseParen( $args{SUBCLAUSE} ); # Content is null?
+    $self->Limit( $fix_op->(
+        ALIAS           => $ocfvalias,
+        FIELD           => 'LargeContent',
+        OPERATOR        => $op,
+        VALUE           => $value,
+        ENTRYAGGREGATOR => 'AND',
+        SUBCLAUSE       => $args{SUBCLAUSE},
+        CASESENSITIVE => 0,
+    ) );
+    $self->_CloseParen( $args{SUBCLAUSE} ); # LargeContent check
+
+    $self->_CloseParen( $args{SUBCLAUSE} ); # Check Content/LargeContent
 
     # XXX: if we join via CustomFields table then
     # because of order of left joins we get NULLs in
@@ -713,18 +672,19 @@ sub _LimitCustomField {
         ENTRYAGGREGATOR => 'AND',
         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} );
+    $self->_CloseParen( $args{SUBCLAUSE} ); # Name IS NOT NULL clause
+
+    # If we were looking for != or NOT LIKE, we need to include the
+    # possibility that the row had no value.
+    $self->Limit(
+        ALIAS           => $ocfvalias,
+        FIELD           => $column || 'Content',
+        OPERATOR        => 'IS',
+        VALUE           => 'NULL',
+        ENTRYAGGREGATOR => 'OR',
+        SUBCLAUSE       => $args{SUBCLAUSE},
+    ) if $negative_op;
+    $self->_CloseParen( $args{SUBCLAUSE} ); # negative_op clause
 }
 
 =head2 Limit PARAMHASH

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


More information about the Rt-commit mailing list