This commit was manufactured by cvs2svn to create tag 'freeside_2_1_1'.
[freeside.git] / rt / lib / RT / Tickets_Overlay.pm
index 1c31f3f..f2949ed 100644 (file)
@@ -1,38 +1,40 @@
 # BEGIN BPS TAGGED BLOCK {{{
-#
+# 
 # COPYRIGHT:
-#
-# This software is Copyright (c) 1996-2005 Best Practical Solutions, LLC
+# 
+# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
 #                                          <jesse@bestpractical.com>
-#
+# 
 # (Except where explicitly superseded by other copyright notices)
-#
-#
+# 
+# 
 # LICENSE:
-#
+# 
 # This work is made available to you under the terms of Version 2 of
 # the GNU General Public License. A copy of that license should have
 # been provided with this software, but in any event can be snarfed
 # from www.gnu.org.
-#
+# 
 # This work is distributed in the hope that it will be useful, but
 # WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 # General Public License for more details.
-#
+# 
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
-#
-#
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+# 
+# 
 # CONTRIBUTION SUBMISSION POLICY:
-#
+# 
 # (The following paragraph is not intended to limit the rights granted
 # to you to modify and distribute this software under the terms of
 # the GNU General Public License and is only of importance to you if
 # you choose to contribute your changes and enhancements to the
 # community by submitting them to Best Practical Solutions, LLC.)
-#
+# 
 # By intentionally submitting any modifications, corrections or
 # derivatives to this work, or any other work intended for use with
 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
@@ -41,8 +43,9 @@
 # royalty-free, perpetual, license to use, copy, create derivative
 # works based on those contributions, and sublicense and distribute
 # those contributions and any derivatives thereof.
-#
+# 
 # END BPS TAGGED BLOCK }}}
+
 # Major Changes:
 
 # - Decimated ProcessRestrictions and broke it into multiple
 
 =head1 METHODS
 
-=begin testing
-
-ok (require RT::Tickets);
-ok( my $testtickets = RT::Tickets->new( $RT::SystemUser ) );
-ok( $testtickets->LimitStatus( VALUE => 'deleted' ) );
-# Should be zero until 'allow_deleted_search'
-ok( $testtickets->Count == 0 );
-
-=end testing
 
 =cut
 
 package RT::Tickets;
 
 use strict;
-
-package RT::Tickets;
-
 no warnings qw(redefine);
-use vars qw(@SORTFIELDS);
+
 use RT::CustomFields;
+use DBIx::SearchBuilder::Unique;
 
 # Configuration Tables:
 
-# FIELDS is a mapping of searchable Field name, to Type, and other
+# FIELD_METADATA is a mapping of searchable Field name, to Type, and other
 # metadata.
 
-my %FIELDS = (
-    Status          => ['ENUM'],
-    Queue           => [ 'ENUM' => 'Queue', ],
-    Type            => [ 'ENUM', ],
-    Creator         => [ 'ENUM' => 'User', ],
-    LastUpdatedBy   => [ 'ENUM' => 'User', ],
-    Owner           => [ 'WATCHERFIELD' => 'Owner', ],
-    EffectiveId     => [ 'INT', ],
-    id              => [ 'INT', ],
-    InitialPriority => [ 'INT', ],
-    FinalPriority   => [ 'INT', ],
-    Priority        => [ 'INT', ],
-    TimeLeft        => [ 'INT', ],
-    TimeWorked      => [ 'INT', ],
-    MemberOf        => [ 'LINK' => To => 'MemberOf', ],
-    DependsOn       => [ 'LINK' => To => 'DependsOn', ],
-    RefersTo        => [ 'LINK' => To => 'RefersTo', ],
-    HasMember       => [ 'LINK' => From => 'MemberOf', ],
-    DependentOn     => [ 'LINK' => From => 'DependsOn', ],
-    DependedOnBy    => [ 'LINK' => From => 'DependsOn', ],
-    ReferredToBy    => [ 'LINK' => From => 'RefersTo', ],
-    Told             => [ 'DATE'            => 'Told', ],
-    Starts           => [ 'DATE'            => 'Starts', ],
-    Started          => [ 'DATE'            => 'Started', ],
-    Due              => [ 'DATE'            => 'Due', ],
-    Resolved         => [ 'DATE'            => 'Resolved', ],
-    LastUpdated      => [ 'DATE'            => 'LastUpdated', ],
-    Created          => [ 'DATE'            => 'Created', ],
-    Subject          => [ 'STRING', ],
-    Content          => [ 'TRANSFIELD', ],
-    ContentType      => [ 'TRANSFIELD', ],
-    Filename         => [ 'TRANSFIELD', ],
-    TransactionDate  => [ 'TRANSDATE', ],
-    Requestor        => [ 'WATCHERFIELD'    => 'Requestor', ],
-    Requestors       => [ 'WATCHERFIELD'    => 'Requestor', ],
-    Cc               => [ 'WATCHERFIELD'    => 'Cc', ],
-    AdminCc          => [ 'WATCHERFIELD'    => 'AdminCc', ],
-    Watcher          => ['WATCHERFIELD'],
-    LinkedTo         => [ 'LINKFIELD', ],
-    CustomFieldValue => [ 'CUSTOMFIELD', ],
-    CF               => [ 'CUSTOMFIELD', ],
-    Updated          => [ 'TRANSDATE', ],
-    RequestorGroup   => [ 'MEMBERSHIPFIELD' => 'Requestor', ],
-    CCGroup          => [ 'MEMBERSHIPFIELD' => 'Cc', ],
-    AdminCCGroup     => [ 'MEMBERSHIPFIELD' => 'AdminCc', ],
-    WatcherGroup     => [ 'MEMBERSHIPFIELD', ],
+our %FIELD_METADATA = (
+    Status          => [ 'ENUM', ], #loc_left_pair
+    Queue           => [ 'ENUM' => 'Queue', ], #loc_left_pair
+    Type            => [ 'ENUM', ], #loc_left_pair
+    Creator         => [ 'ENUM' => 'User', ], #loc_left_pair
+    LastUpdatedBy   => [ 'ENUM' => 'User', ], #loc_left_pair
+    Owner           => [ 'WATCHERFIELD' => 'Owner', ], #loc_left_pair
+    EffectiveId     => [ 'INT', ], #loc_left_pair
+    id              => [ 'ID', ], #loc_left_pair
+    InitialPriority => [ 'INT', ], #loc_left_pair
+    FinalPriority   => [ 'INT', ], #loc_left_pair
+    Priority        => [ 'INT', ], #loc_left_pair
+    TimeLeft        => [ 'INT', ], #loc_left_pair
+    TimeWorked      => [ 'INT', ], #loc_left_pair
+    TimeEstimated   => [ 'INT', ], #loc_left_pair
+
+    Linked          => [ 'LINK' ], #loc_left_pair
+    LinkedTo        => [ 'LINK' => 'To' ], #loc_left_pair
+    LinkedFrom      => [ 'LINK' => 'From' ], #loc_left_pair
+    MemberOf        => [ 'LINK' => To => 'MemberOf', ], #loc_left_pair
+    DependsOn       => [ 'LINK' => To => 'DependsOn', ], #loc_left_pair
+    RefersTo        => [ 'LINK' => To => 'RefersTo', ], #loc_left_pair
+    HasMember       => [ 'LINK' => From => 'MemberOf', ], #loc_left_pair
+    DependentOn     => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
+    DependedOnBy    => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
+    ReferredToBy    => [ 'LINK' => From => 'RefersTo', ], #loc_left_pair
+    Told             => [ 'DATE'            => 'Told', ], #loc_left_pair
+    Starts           => [ 'DATE'            => 'Starts', ], #loc_left_pair
+    Started          => [ 'DATE'            => 'Started', ], #loc_left_pair
+    Due              => [ 'DATE'            => 'Due', ], #loc_left_pair
+    Resolved         => [ 'DATE'            => 'Resolved', ], #loc_left_pair
+    LastUpdated      => [ 'DATE'            => 'LastUpdated', ], #loc_left_pair
+    Created          => [ 'DATE'            => 'Created', ], #loc_left_pair
+    Subject          => [ 'STRING', ], #loc_left_pair
+    Content          => [ 'TRANSFIELD', ], #loc_left_pair
+    ContentType      => [ 'TRANSFIELD', ], #loc_left_pair
+    Filename         => [ 'TRANSFIELD', ], #loc_left_pair
+    TransactionDate  => [ 'TRANSDATE', ], #loc_left_pair
+    Requestor        => [ 'WATCHERFIELD'    => 'Requestor', ], #loc_left_pair
+    Requestors       => [ 'WATCHERFIELD'    => 'Requestor', ], #loc_left_pair
+    Cc               => [ 'WATCHERFIELD'    => 'Cc', ], #loc_left_pair
+    AdminCc          => [ 'WATCHERFIELD'    => 'AdminCc', ], #loc_left_pair
+    Watcher          => [ 'WATCHERFIELD', ], #loc_left_pair
+    QueueCc          => [ 'WATCHERFIELD'    => 'Cc'      => 'Queue', ], #loc_left_pair
+    QueueAdminCc     => [ 'WATCHERFIELD'    => 'AdminCc' => 'Queue', ], #loc_left_pair
+    QueueWatcher     => [ 'WATCHERFIELD'    => undef     => 'Queue', ], #loc_left_pair
+    CustomFieldValue => [ 'CUSTOMFIELD', ], #loc_left_pair
+    DateCustomFieldValue => [ 'DATECUSTOMFIELD', ],
+    CustomField      => [ 'CUSTOMFIELD', ], #loc_left_pair
+    CF               => [ 'CUSTOMFIELD', ], #loc_left_pair
+    Updated          => [ 'TRANSDATE', ], #loc_left_pair
+    RequestorGroup   => [ 'MEMBERSHIPFIELD' => 'Requestor', ], #loc_left_pair
+    CCGroup          => [ 'MEMBERSHIPFIELD' => 'Cc', ], #loc_left_pair
+    AdminCCGroup     => [ 'MEMBERSHIPFIELD' => 'AdminCc', ], #loc_left_pair
+    WatcherGroup     => [ 'MEMBERSHIPFIELD', ], #loc_left_pair
+    HasAttribute     => [ 'HASATTRIBUTE', 1 ],
+    HasNoAttribute     => [ 'HASATTRIBUTE', 0 ],
 );
 
 # Mapping of Field Type to Function
-my %dispatch = (
+our %dispatch = (
     ENUM            => \&_EnumLimit,
     INT             => \&_IntLimit,
+    ID              => \&_IdLimit,
     LINK            => \&_LinkLimit,
     DATE            => \&_DateLimit,
     STRING          => \&_StringLimit,
@@ -156,10 +160,11 @@ my %dispatch = (
     TRANSDATE       => \&_TransDateLimit,
     WATCHERFIELD    => \&_WatcherLimit,
     MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
-    LINKFIELD       => \&_LinkFieldLimit,
     CUSTOMFIELD     => \&_CustomFieldLimit,
+    DATECUSTOMFIELD => \&_DateCustomFieldLimit,
+    HASATTRIBUTE    => \&_HasAttributeLimit,
 );
-my %can_bundle = ( WATCHERFIELD => "yes", );
+our %can_bundle = ();# WATCHERFIELD => "yes", );
 
 # Default EntryAggregator per type
 # if you specify OP, you must specify all valid OPs
@@ -195,12 +200,17 @@ my %DefaultEA = (
         'NOT LIKE' => 'AND'
     },
 
+    HASATTRIBUTE => {
+        '='        => 'AND',
+        '!='       => 'AND',
+    },
+
     CUSTOMFIELD => 'OR',
 );
 
 # Helper functions for passing the above lexically scoped tables above
 # into Tickets_Overlay_SQL.
-sub FIELDS     { return \%FIELDS }
+sub FIELDS     { return \%FIELD_METADATA }
 sub dispatch   { return \%dispatch }
 sub can_bundle { return \%can_bundle }
 
@@ -209,7 +219,7 @@ require RT::Tickets_Overlay_SQL;
 
 # {{{ sub SortFields
 
-@SORTFIELDS = qw(id Status
+our @SORTFIELDS = qw(id Status
     Queue Subject
     Owner Created Due Starts Started
     Told
@@ -230,6 +240,23 @@ sub SortFields {
 
 # BEGIN SQL STUFF *********************************
 
+
+sub CleanSlate {
+    my $self = shift;
+    $self->SUPER::CleanSlate( @_ );
+    delete $self->{$_} foreach qw(
+        _sql_cf_alias
+        _sql_group_members_aliases
+        _sql_object_cfv_alias
+        _sql_role_group_aliases
+        _sql_transalias
+        _sql_trattachalias
+        _sql_u_watchers_alias_for_sort
+        _sql_u_watchers_aliases
+        _sql_current_user_can_see_applied
+    );
+}
+
 =head1 Limit Helper Routines
 
 These routines are the targets of a dispatch table depending on the
@@ -244,6 +271,58 @@ Essentially they are an expanded/broken out (and much simplified)
 version of what ProcessRestrictions used to do.  They're also much
 more clearly delineated by the TYPE of field being processed.
 
+=head2 _IdLimit
+
+Handle ID field.
+
+=cut
+
+sub _IdLimit {
+    my ( $sb, $field, $op, $value, @rest ) = @_;
+
+    return $sb->_IntLimit( $field, $op, $value, @rest ) unless $value eq '__Bookmarked__';
+
+    die "Invalid operator $op for __Bookmarked__ search on $field"
+        unless $op =~ /^(=|!=)$/;
+
+    my @bookmarks = do {
+        my $tmp = $sb->CurrentUser->UserObj->FirstAttribute('Bookmarks');
+        $tmp = $tmp->Content if $tmp;
+        $tmp ||= {};
+        grep $_, keys %$tmp;
+    };
+
+    return $sb->_SQLLimit(
+        FIELD    => $field,
+        OPERATOR => $op,
+        VALUE    => 0,
+        @rest,
+    ) unless @bookmarks;
+
+    # as bookmarked tickets can be merged we have to use a join
+    # but it should be pretty lightweight
+    my $tickets_alias = $sb->Join(
+        TYPE   => 'LEFT',
+        ALIAS1 => 'main',
+        FIELD1 => 'id',
+        TABLE2 => 'Tickets',
+        FIELD2 => 'EffectiveId',
+    );
+    $sb->_OpenParen;
+    my $first = 1;
+    my $ea = $op eq '='? 'OR': 'AND';
+    foreach my $id ( sort @bookmarks ) {
+        $sb->_SQLLimit(
+            ALIAS    => $tickets_alias,
+            FIELD    => 'id',
+            OPERATOR => $op,
+            VALUE    => $id,
+            $first? (@rest): ( ENTRYAGGREGATOR => $ea )
+        );
+    }
+    $sb->_CloseParen;
+}
+
 =head2 _EnumLimit
 
 Handle Fields which are limited to certain values, and potentially
@@ -271,8 +350,8 @@ sub _EnumLimit {
         unless $op eq "="
         or $op     eq "!=";
 
-    my $meta = $FIELDS{$field};
-    if ( defined $meta->[1] ) {
+    my $meta = $FIELD_METADATA{$field};
+    if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
         my $class = "RT::" . $meta->[1];
         my $o     = $class->new( $sb->CurrentUser );
         $o->Load($value);
@@ -315,119 +394,111 @@ sub _IntLimit {
 Handle fields which deal with links between tickets.  (MemberOf, DependsOn)
 
 Meta Data:
-  1: Direction (From,To)
-  2: Link Type (MemberOf, DependsOn,RefersTo)
+  1: Direction (From, To)
+  2: Link Type (MemberOf, DependsOn, RefersTo)
 
 =cut
 
 sub _LinkLimit {
     my ( $sb, $field, $op, $value, @rest ) = @_;
 
-    my $meta = $FIELDS{$field};
-    die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS)/io;
+    my $meta = $FIELD_METADATA{$field};
+    die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS|IS NOT)$/io;
 
-    die "Incorrect Metadata for $field"
-        unless ( defined $meta->[1] and defined $meta->[2] );
-
-    my $direction = $meta->[1];
+    my $is_negative = 0;
+    if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
+        $is_negative = 1;
+    }
+    my $is_null = 0;
+    $is_null = 1 if !$value || $value =~ /^null$/io;
 
-    my $matchfield;
-    my $linkfield;
-    my $is_local = 1;
-    my $is_null  = 0;
+    my $direction = $meta->[1] || '';
+    my ($matchfield, $linkfield) = ('', '');
     if ( $direction eq 'To' ) {
-        $matchfield = "Target";
-        $linkfield  = "Base";
-
+        ($matchfield, $linkfield) = ("Target", "Base");
     }
     elsif ( $direction eq 'From' ) {
-        $linkfield  = "Target";
-        $matchfield = "Base";
-
+        ($matchfield, $linkfield) = ("Base", "Target");
     }
-    else {
-        die "Invalid link direction '$meta->[1]' for $field\n";
+    elsif ( $direction ) {
+        die "Invalid link direction '$direction' for $field\n";
+    } else {
+        $sb->_OpenParen;
+        $sb->_LinkLimit( 'LinkedTo', $op, $value, @rest );
+        $sb->_LinkLimit(
+            'LinkedFrom', $op, $value, @rest,
+            ENTRYAGGREGATOR => (($is_negative && $is_null) || (!$is_null && !$is_negative))? 'OR': 'AND',
+        );
+        $sb->_CloseParen;
+        return;
     }
 
-    if ( $op eq '=' || $op =~ /^is/oi ) {
-        if ( $value eq '' || $value =~ /^null$/io ) {
-            $is_null = 1;
-        }
-        elsif ( $value =~ /\D/o ) {
-            $is_local = 0;
-        }
-        else {
-            $is_local = 1;
-        }
+    my $is_local = 1;
+    if ( $is_null ) {
+        $op = ($op =~ /^(=|IS)$/)? 'IS': 'IS NOT';
+    }
+    elsif ( $value =~ /\D/ ) {
+        $is_local = 0;
     }
+    $matchfield = "Local$matchfield" if $is_local;
 
 #For doing a left join to find "unlinked tickets" we want to generate a query that looks like this
 #    SELECT main.* FROM Tickets main
 #        LEFT JOIN Links Links_1 ON (     (Links_1.Type = 'MemberOf')
 #                                      AND(main.id = Links_1.LocalTarget))
-#        WHERE   ((main.EffectiveId = main.id))
-#            AND ((main.Status != 'deleted'))
-#            AND (Links_1.LocalBase IS NULL);
+#        WHERE Links_1.LocalBase IS NULL;
 
-    if ($is_null) {
+    if ( $is_null ) {
         my $linkalias = $sb->Join(
-            TYPE   => 'left',
+            TYPE   => 'LEFT',
             ALIAS1 => 'main',
             FIELD1 => 'id',
             TABLE2 => 'Links',
             FIELD2 => 'Local' . $linkfield
         );
-
         $sb->SUPER::Limit(
             LEFTJOIN => $linkalias,
             FIELD    => 'Type',
             OPERATOR => '=',
             VALUE    => $meta->[2],
-            @rest,
-        );
-
+        ) if $meta->[2];
         $sb->_SQLLimit(
-            ALIAS           => $linkalias,
-            ENTRYAGGREGATOR => 'AND',
-            FIELD      => ( $is_local ? "Local$matchfield" : $matchfield ),
-            OPERATOR   => 'IS',
+            @rest,
+            ALIAS      => $linkalias,
+            FIELD      => $matchfield,
+            OPERATOR   => $op,
             VALUE      => 'NULL',
-            QUOTEVALUE => '0',
+            QUOTEVALUE => 0,
         );
-
     }
     else {
-
-        $sb->{_sql_linkalias} = $sb->NewAlias('Links')
-            unless defined $sb->{_sql_linkalias};
-
-        $sb->_OpenParen();
-
-        $sb->_SQLLimit(
-            ALIAS    => $sb->{_sql_linkalias},
+        my $linkalias = $sb->Join(
+            TYPE   => 'LEFT',
+            ALIAS1 => 'main',
+            FIELD1 => 'id',
+            TABLE2 => 'Links',
+            FIELD2 => 'Local' . $linkfield
+        );
+        $sb->SUPER::Limit(
+            LEFTJOIN => $linkalias,
             FIELD    => 'Type',
             OPERATOR => '=',
             VALUE    => $meta->[2],
-            @rest,
-        );
-
-        $sb->_SQLLimit(
-            ALIAS           => $sb->{_sql_linkalias},
-            ENTRYAGGREGATOR => 'AND',
-            FIELD    => ( $is_local ? "Local$matchfield" : $matchfield ),
+        ) if $meta->[2];
+        $sb->SUPER::Limit(
+            LEFTJOIN => $linkalias,
+            FIELD    => $matchfield,
             OPERATOR => '=',
             VALUE    => $value,
         );
-
-        #If we're searching on target, join the base to ticket.id
-        $sb->_SQLJoin(
-            ALIAS1 => 'main',
-            FIELD1 => $sb->{'primary_key'},
-            ALIAS2 => $sb->{_sql_linkalias},
-            FIELD2 => 'Local' . $linkfield
+        $sb->_SQLLimit(
+            @rest,
+            ALIAS      => $linkalias,
+            FIELD      => $matchfield,
+            OPERATOR   => $is_negative? 'IS': 'IS NOT',
+            VALUE      => 'NULL',
+            QUOTEVALUE => 0,
         );
-
-        $sb->_CloseParen();
     }
 }
 
@@ -446,15 +517,12 @@ sub _DateLimit {
     die "Invalid Date Op: $op"
         unless $op =~ /^(=|>|<|>=|<=)$/;
 
-    my $meta = $FIELDS{$field};
+    my $meta = $FIELD_METADATA{$field};
     die "Incorrect Meta Data for $field"
         unless ( defined $meta->[1] );
 
-    use POSIX 'strftime';
-
     my $date = RT::Date->new( $sb->CurrentUser );
     $date->Set( Format => 'unknown', Value => $value );
-    my $time = $date->Unix;
 
     if ( $op eq "=" ) {
 
@@ -462,10 +530,10 @@ sub _DateLimit {
         # particular single day.  in the database, we need to check for >
         # and < the edges of that day.
 
-        my $daystart = strftime( "%Y-%m-%d %H:%M",
-            gmtime( $time - ( $time % 86400 ) ) );
-        my $dayend = strftime( "%Y-%m-%d %H:%M",
-            gmtime( $time + ( 86399 - $time % 86400 ) ) );
+        $date->SetToMidnight( Timezone => 'server' );
+        my $daystart = $date->ISO;
+        $date->AddDay;
+        my $dayend = $date->ISO;
 
         $sb->_OpenParen;
 
@@ -478,7 +546,7 @@ sub _DateLimit {
 
         $sb->_SQLLimit(
             FIELD    => $meta->[1],
-            OPERATOR => "<=",
+            OPERATOR => "<",
             VALUE    => $dayend,
             @rest,
             ENTRYAGGREGATOR => 'AND',
@@ -488,11 +556,10 @@ sub _DateLimit {
 
     }
     else {
-        $value = strftime( "%Y-%m-%d %H:%M", gmtime($time) );
         $sb->_SQLLimit(
             FIELD    => $meta->[1],
             OPERATOR => $op,
-            VALUE    => $value,
+            VALUE    => $date->ISO,
             @rest,
         );
     }
@@ -513,6 +580,14 @@ sub _StringLimit {
     # FIXME:
     # Valid Operators:
     #  =, !=, LIKE, NOT LIKE
+    if ( (!defined $value || !length $value)
+        && lc($op) ne 'is' && lc($op) ne 'is not'
+        && RT->Config->Get('DatabaseType') eq 'Oracle'
+    ) {
+        my $negative = 1 if $op eq '!=' || $op =~ /^NOT\s/;
+        $op = $negative? 'IS NOT': 'IS';
+        $value = 'NULL';
+    }
 
     $sb->_SQLLimit(
         FIELD         => $field,
@@ -540,12 +615,23 @@ sub _TransDateLimit {
 
     # See the comments for TransLimit, they apply here too
 
-    $sb->{_sql_transalias} = $sb->NewAlias('Transactions')
-        unless defined $sb->{_sql_transalias};
+    unless ( $sb->{_sql_transalias} ) {
+        $sb->{_sql_transalias} = $sb->Join(
+            ALIAS1 => 'main',
+            FIELD1 => 'id',
+            TABLE2 => 'Transactions',
+            FIELD2 => 'ObjectId',
+        );
+        $sb->SUPER::Limit(
+            ALIAS           => $sb->{_sql_transalias},
+            FIELD           => 'ObjectType',
+            VALUE           => 'RT::Ticket',
+            ENTRYAGGREGATOR => 'AND',
+        );
+    }
 
     my $date = RT::Date->new( $sb->CurrentUser );
     $date->Set( Format => 'unknown', Value => $value );
-    my $time = $date->Unix;
 
     $sb->_OpenParen;
     if ( $op eq "=" ) {
@@ -554,10 +640,10 @@ sub _TransDateLimit {
         # particular single day.  in the database, we need to check for >
         # and < the edges of that day.
 
-        my $daystart = strftime( "%Y-%m-%d %H:%M",
-            gmtime( $time - ( $time % 86400 ) ) );
-        my $dayend = strftime( "%Y-%m-%d %H:%M",
-            gmtime( $time + ( 86399 - $time % 86400 ) ) );
+        $date->SetToMidnight( Timezone => 'server' );
+        my $daystart = $date->ISO;
+        $date->AddDay;
+        my $dayend = $date->ISO;
 
         $sb->_SQLLimit(
             ALIAS         => $sb->{_sql_transalias},
@@ -587,26 +673,12 @@ sub _TransDateLimit {
             ALIAS         => $sb->{_sql_transalias},
             FIELD         => 'Created',
             OPERATOR      => $op,
-            VALUE         => $value,
+            VALUE         => $date->ISO,
             CASESENSITIVE => 0,
             @rest
         );
     }
 
-    # Join Transactions to Tickets
-    $sb->_SQLJoin(
-        ALIAS1 => 'main',
-        FIELD1 => $sb->{'primary_key'},     # UGH!
-        ALIAS2 => $sb->{_sql_transalias},
-        FIELD2 => 'ObjectId'
-    );
-
-    $sb->SUPER::Limit(
-        ALIAS => $sb->{_sql_transalias},
-        FIELD => 'ObjectType',
-        VALUE => 'RT::Ticket'
-    );
-
     $sb->_CloseParen;
 }
 
@@ -653,48 +725,62 @@ sub _TransLimit {
     # them all into the same subclause when you have (A op B op C) - the
     # way they get parsed in the tree they're in different subclauses.
 
-    my ( $self, $field, $op, $value, @rest ) = @_;
-
-    $self->{_sql_transalias} = $self->NewAlias('Transactions')
-        unless defined $self->{_sql_transalias};
-    $self->{_sql_trattachalias} = $self->NewAlias('Attachments')
-        unless defined $self->{_sql_trattachalias};
+    my ( $self, $field, $op, $value, %rest ) = @_;
 
-    $self->_OpenParen;
+    unless ( $self->{_sql_transalias} ) {
+        $self->{_sql_transalias} = $self->Join(
+            ALIAS1 => 'main',
+            FIELD1 => 'id',
+            TABLE2 => 'Transactions',
+            FIELD2 => 'ObjectId',
+        );
+        $self->SUPER::Limit(
+            ALIAS           => $self->{_sql_transalias},
+            FIELD           => 'ObjectType',
+            VALUE           => 'RT::Ticket',
+            ENTRYAGGREGATOR => 'AND',
+        );
+    }
+    unless ( defined $self->{_sql_trattachalias} ) {
+        $self->{_sql_trattachalias} = $self->_SQLJoin(
+            TYPE   => 'LEFT', # not all txns have an attachment
+            ALIAS1 => $self->{_sql_transalias},
+            FIELD1 => 'id',
+            TABLE2 => 'Attachments',
+            FIELD2 => 'TransactionId',
+        );
+    }
 
     #Search for the right field
-    $self->_SQLLimit(
-        ALIAS         => $self->{_sql_trattachalias},
-        FIELD         => $field,
-        OPERATOR      => $op,
-        VALUE         => $value,
-        CASESENSITIVE => 0,
-        @rest
-    );
-
-    $self->_SQLJoin(
-        ALIAS1 => $self->{_sql_trattachalias},
-        FIELD1 => 'TransactionId',
-        ALIAS2 => $self->{_sql_transalias},
-        FIELD2 => 'id'
-    );
-
-    # Join Transactions to Tickets
-    $self->_SQLJoin(
-        ALIAS1 => 'main',
-        FIELD1 => $self->{'primary_key'},     # Why not use "id" here?
-        ALIAS2 => $self->{_sql_transalias},
-        FIELD2 => 'ObjectId'
-    );
-
-    $self->SUPER::Limit(
-        ALIAS           => $self->{_sql_transalias},
-        FIELD           => 'ObjectType',
-        VALUE           => 'RT::Ticket',
-        ENTRYAGGREGATOR => 'AND'
-    );
+    if ( $field eq 'Content' and RT->Config->Get('DontSearchFileAttachments') ) {
+        $self->_OpenParen;
+        $self->_SQLLimit(
+                       %rest,
+                       ALIAS         => $self->{_sql_trattachalias},
+                       FIELD         => $field,
+                       OPERATOR      => $op,
+                       VALUE         => $value,
+                       CASESENSITIVE => 0,
+                      );
+        $self->_SQLLimit(
+                       ENTRYAGGREGATOR => 'AND',
+                       ALIAS           => $self->{_sql_trattachalias},
+                       FIELD           => 'Filename',
+                       OPERATOR        => 'IS',
+                       VALUE           => 'NULL',
+                      );
+        $self->_CloseParen;
+    } else {
+        $self->_SQLLimit(
+                       %rest,
+                       ALIAS         => $self->{_sql_trattachalias},
+                       FIELD         => $field,
+                       OPERATOR      => $op,
+                       VALUE         => $value,
+                       CASESENSITIVE => 0,
+        );
+    }
 
-    $self->_CloseParen;
 
 }
 
@@ -706,71 +792,6 @@ Meta Data:
   1: Field to query on
 
 
-=begin testing
-
-# Test to make sure that you can search for tickets by requestor address and
-# by requestor name.
-
-my ($id,$msg);
-my $u1 = RT::User->new($RT::SystemUser);
-($id, $msg) = $u1->Create( Name => 'RequestorTestOne', EmailAddress => 'rqtest1@example.com');
-ok ($id,$msg);
-my $u2 = RT::User->new($RT::SystemUser);
-($id, $msg) = $u2->Create( Name => 'RequestorTestTwo', EmailAddress => 'rqtest2@example.com');
-ok ($id,$msg);
-
-my $t1 = RT::Ticket->new($RT::SystemUser);
-my ($trans);
-($id,$trans,$msg) =$t1->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u1->EmailAddress]);
-ok ($id, $msg);
-
-my $t2 = RT::Ticket->new($RT::SystemUser);
-($id,$trans,$msg) =$t2->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u2->EmailAddress]);
-ok ($id, $msg);
-
-
-my $t3 = RT::Ticket->new($RT::SystemUser);
-($id,$trans,$msg) =$t3->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u2->EmailAddress, $u1->EmailAddress]);
-ok ($id, $msg);
-
-
-my $tix1 = RT::Tickets->new($RT::SystemUser);
-$tix1->FromSQL('Requestor.EmailAddress LIKE "rqtest1" OR Requestor.EmailAddress LIKE "rqtest2"');
-
-is ($tix1->Count, 3);
-
-my $tix2 = RT::Tickets->new($RT::SystemUser);
-$tix2->FromSQL('Requestor.Name LIKE "TestOne" OR Requestor.Name LIKE "TestTwo"');
-
-is ($tix2->Count, 3);
-
-
-my $tix3 = RT::Tickets->new($RT::SystemUser);
-$tix3->FromSQL('Requestor.EmailAddress LIKE "rqtest1"');
-
-is ($tix3->Count, 2);
-
-my $tix4 = RT::Tickets->new($RT::SystemUser);
-$tix4->FromSQL('Requestor.Name LIKE "TestOne" ');
-
-is ($tix4->Count, 2);
-
-# Searching for tickets that have two requestors isn't supported
-# There's no way to differentiate "one requestor name that matches foo and bar"
-# and "two requestors, one matching foo and one matching bar"
-
-# my $tix5 = RT::Tickets->new($RT::SystemUser);
-# $tix5->FromSQL('Requestor.Name LIKE "TestOne" AND Requestor.Name LIKE "TestTwo"');
-# 
-# is ($tix5->Count, 1);
-# 
-# my $tix6 = RT::Tickets->new($RT::SystemUser);
-# $tix6->FromSQL('Requestor.EmailAddress LIKE "rqtest1" AND Requestor.EmailAddress LIKE "rqtest2"');
-# 
-# is ($tix6->Count, 1);
-
-
-=end testing
 
 =cut
 
@@ -781,136 +802,245 @@ sub _WatcherLimit {
     my $value = shift;
     my %rest  = (@_);
 
-    # Find out what sort of watcher we're looking for
-    my $fieldname;
-    if ( ref $field ) {
-        $fieldname = $field->[0]->[0];
-    }
-    else {
-        $fieldname = $field;
-        $field = [ [ $field, $op, $value, %rest ] ];    # gross hack
-    }
-    my $meta = $FIELDS{$fieldname};
-    my $type = ( defined $meta->[1] ? $meta->[1] : undef );
+    my $meta = $FIELD_METADATA{ $field };
+    my $type = $meta->[1] || '';
+    my $class = $meta->[2] || 'Ticket';
 
     # Owner was ENUM field, so "Owner = 'xxx'" allowed user to
     # search by id and Name at the same time, this is workaround
     # to preserve backward compatibility
-    if ( $fieldname eq 'Owner' ) {
-        my $flag = 0;
-        for my $chunk ( splice @$field ) {
-            my ( $f, $op, $value, %rest ) = @$chunk;
-            if ( !$rest{SUBKEY} && $op =~ /^!?=$/ ) {
-                $self->_OpenParen unless $flag++;
-                my $o = RT::User->new( $self->CurrentUser );
-                $o->Load($value);
-                $value = $o->Id;
-                $self->_SQLLimit(
-                    FIELD    => 'Owner',
-                    OPERATOR => $op,
-                    VALUE    => $value,
-                    %rest,
-                );
-            }
-            else {
-                push @$field, $chunk;
-            }
+    if ( $field eq 'Owner' ) {
+        if ( $op =~ /^!?=$/ && (!$rest{'SUBKEY'} || $rest{'SUBKEY'} eq 'Name' || $rest{'SUBKEY'} eq 'EmailAddress') ) {
+            my $o = RT::User->new( $self->CurrentUser );
+            my $method = ($rest{'SUBKEY'}||'') eq 'EmailAddress' ? 'LoadByEmail': 'Load';
+            $o->$method( $value );
+            $self->_SQLLimit(
+                FIELD    => 'Owner',
+                OPERATOR => $op,
+                VALUE    => $o->id,
+                %rest,
+            );
+            return;
+        }
+        if ( ($rest{'SUBKEY'}||'') eq 'id' ) {
+            $self->_SQLLimit(
+                FIELD    => 'Owner',
+                OPERATOR => $op,
+                VALUE    => $value,
+                %rest,
+            );
+            return;
         }
-        $self->_CloseParen if $flag;
-        return unless @$field;
     }
+    $rest{SUBKEY} ||= 'EmailAddress';
 
-    my $users = $self->_WatcherJoin($type);
+    my $groups = $self->_RoleGroupsJoin( Type => $type, Class => $class );
 
-    # If we're looking for multiple watchers of a given type,
-    # TicketSQL will be handing it to us as an array of clauses in
-    # $field
     $self->_OpenParen;
-    for my $chunk (@$field) {
-        ( $field, $op, $value, %rest ) = @$chunk;
-        $rest{SUBKEY} ||= 'EmailAddress';
-
-        my $re_negative_op = qr[!=|NOT LIKE];
-        $self->_OpenParen if $op =~ /$re_negative_op/;
-
+    if ( $op =~ /^IS(?: NOT)?$/ ) {
+        my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
+        # to avoid joining the table Users into the query, we just join GM
+        # and make sure we don't match records where group is member of itself
+        $self->SUPER::Limit(
+            LEFTJOIN   => $group_members,
+            FIELD      => 'GroupId',
+            OPERATOR   => '!=',
+            VALUE      => "$group_members.MemberId",
+            QUOTEVALUE => 0,
+        );
         $self->_SQLLimit(
-            ALIAS         => $users,
-            FIELD         => $rest{SUBKEY},
+            ALIAS         => $group_members,
+            FIELD         => 'GroupId',
+            OPERATOR      => $op,
             VALUE         => $value,
+            %rest,
+        );
+    }
+    elsif ( $op =~ /^!=$|^NOT\s+/i ) {
+        # reverse op
+        $op =~ s/!|NOT\s+//i;
+
+        # XXX: we have no way to build correct "Watcher.X != 'Y'" when condition
+        # "X = 'Y'" matches more then one user so we try to fetch two records and
+        # do the right thing when there is only one exist and semi-working solution
+        # otherwise.
+        my $users_obj = RT::Users->new( $self->CurrentUser );
+        $users_obj->Limit(
+            FIELD         => $rest{SUBKEY},
             OPERATOR      => $op,
-            CASESENSITIVE => 0,
-            %rest
+            VALUE         => $value,
         );
-
-        if ( $op =~ /$re_negative_op/ ) {
+        $users_obj->OrderBy;
+        $users_obj->RowsPerPage(2);
+        my @users = @{ $users_obj->ItemsArrayRef };
+
+        my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
+        if ( @users <= 1 ) {
+            my $uid = 0;
+            $uid = $users[0]->id if @users;
+            $self->SUPER::Limit(
+                LEFTJOIN      => $group_members,
+                ALIAS         => $group_members,
+                FIELD         => 'MemberId',
+                VALUE         => $uid,
+            );
             $self->_SQLLimit(
-                ALIAS           => $users,
-                FIELD           => $rest{SUBKEY},
+                %rest,
+                ALIAS           => $group_members,
+                FIELD           => 'id',
                 OPERATOR        => 'IS',
                 VALUE           => 'NULL',
-                ENTRYAGGREGATOR => 'OR',
             );
-            $self->_CloseParen;
+        } else {
+            $self->SUPER::Limit(
+                LEFTJOIN   => $group_members,
+                FIELD      => 'GroupId',
+                OPERATOR   => '!=',
+                VALUE      => "$group_members.MemberId",
+                QUOTEVALUE => 0,
+            );
+            my $users = $self->Join(
+                TYPE            => 'LEFT',
+                ALIAS1          => $group_members,
+                FIELD1          => 'MemberId',
+                TABLE2          => 'Users',
+                FIELD2          => 'id',
+            );
+            $self->SUPER::Limit(
+                LEFTJOIN      => $users,
+                ALIAS         => $users,
+                FIELD         => $rest{SUBKEY},
+                OPERATOR      => $op,
+                VALUE         => $value,
+                CASESENSITIVE => 0,
+            );
+            $self->_SQLLimit(
+                %rest,
+                ALIAS         => $users,
+                FIELD         => 'id',
+                OPERATOR      => 'IS',
+                VALUE         => 'NULL',
+            );
         }
+    } else {
+        my $group_members = $self->_GroupMembersJoin(
+            GroupsAlias => $groups,
+            New => 0,
+        );
+
+        my $users = $self->{'_sql_u_watchers_aliases'}{$group_members};
+        unless ( $users ) {
+            $users = $self->{'_sql_u_watchers_aliases'}{$group_members} = 
+                $self->NewAlias('Users');
+            $self->SUPER::Limit(
+                LEFTJOIN      => $group_members,
+                ALIAS         => $group_members,
+                FIELD         => 'MemberId',
+                VALUE         => "$users.id",
+                QUOTEVALUE    => 0,
+            );
+        }
+
+        # we join users table without adding some join condition between tables,
+        # the only conditions we have are conditions on the table iteslf,
+        # for example Users.EmailAddress = 'x'. We should add this condition to
+        # the top level of the query and bundle it with another similar conditions,
+        # for example "Users.EmailAddress = 'x' OR Users.EmailAddress = 'Y'".
+        # To achive this goal we use own SUBCLAUSE for conditions on the users table.
+        $self->SUPER::Limit(
+            %rest,
+            SUBCLAUSE       => '_sql_u_watchers_'. $users,
+            ALIAS           => $users,
+            FIELD           => $rest{'SUBKEY'},
+            VALUE           => $value,
+            OPERATOR        => $op,
+            CASESENSITIVE   => 0,
+        );
+        # A condition which ties Users and Groups (role groups) is a left join condition
+        # of CachedGroupMembers table. To get correct results of the query we check
+        # if there are matches in CGM table or not using 'cgm.id IS NOT NULL'.
+        $self->_SQLLimit(
+            %rest,
+            ALIAS           => $group_members,
+            FIELD           => 'id',
+            OPERATOR        => 'IS NOT',
+            VALUE           => 'NULL',
+        );
     }
     $self->_CloseParen;
 }
 
-=head2 _WatcherJoin
-
-Helper function which provides joins to a watchers table both for limits
-and for ordering.
-
-=cut
-
-sub _WatcherJoin {
+sub _RoleGroupsJoin {
     my $self = shift;
-    my $type = shift;
-
-    # we cache joins chain per watcher type
-    # if we limit by requestor then we shouldn't join requestors again
-    # for sort or limit on other requestors
-    if ( $self->{'_watcher_join_users_alias'}{ $type || 'any' } ) {
-        return $self->{'_watcher_join_users_alias'}{ $type || 'any' };
-    }
-
-# we always have watcher groups for ticket
-# this join should be NORMAL
-# XXX: if we change this from Join to NewAlias+Limit
-# then Pg will complain because SB build wrong query.
-# Query looks like "FROM (Tickets LEFT JOIN CGM ON(Groups.id = CGM.GroupId)), Groups"
-# Pg doesn't like that fact that it doesn't know about Groups table yet when
-# join CGM table into Tickets. Problem is in Join method which doesn't use
-# ALIAS1 argument when build braces.
+    my %args = (New => 0, Class => 'Ticket', Type => '', @_);
+    return $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
+        if $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
+           && !$args{'New'};
+
+    # we always have watcher groups for ticket, so we use INNER join
     my $groups = $self->Join(
         ALIAS1          => 'main',
-        FIELD1          => 'id',
+        FIELD1          => $args{'Class'} eq 'Queue'? 'Queue': 'id',
         TABLE2          => 'Groups',
         FIELD2          => 'Instance',
-        ENTRYAGGREGATOR => 'AND'
+        ENTRYAGGREGATOR => 'AND',
     );
     $self->SUPER::Limit(
+        LEFTJOIN        => $groups,
         ALIAS           => $groups,
         FIELD           => 'Domain',
-        VALUE           => 'RT::Ticket-Role',
-        ENTRYAGGREGATOR => 'AND'
+        VALUE           => 'RT::'. $args{'Class'} .'-Role',
     );
     $self->SUPER::Limit(
+        LEFTJOIN        => $groups,
         ALIAS           => $groups,
         FIELD           => 'Type',
-        VALUE           => $type,
-        ENTRYAGGREGATOR => 'AND'
-        )
-        if ($type);
+        VALUE           => $args{'Type'},
+    ) if $args{'Type'};
 
-    my $groupmembers = $self->Join(
-        TYPE   => 'LEFT',
-        ALIAS1 => $groups,
-        FIELD1 => 'id',
-        TABLE2 => 'CachedGroupMembers',
-        FIELD2 => 'GroupId'
+    $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} } = $groups
+        unless $args{'New'};
+
+    return $groups;
+}
+
+sub _GroupMembersJoin {
+    my $self = shift;
+    my %args = (New => 1, GroupsAlias => undef, @_);
+
+    return $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
+        if $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
+            && !$args{'New'};
+
+    my $alias = $self->Join(
+        TYPE            => 'LEFT',
+        ALIAS1          => $args{'GroupsAlias'},
+        FIELD1          => 'id',
+        TABLE2          => 'CachedGroupMembers',
+        FIELD2          => 'GroupId',
+        ENTRYAGGREGATOR => 'AND',
     );
 
+    $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
+        unless $args{'New'};
+
+    return $alias;
+}
+
+=head2 _WatcherJoin
+
+Helper function which provides joins to a watchers table both for limits
+and for ordering.
+
+=cut
+
+sub _WatcherJoin {
+    my $self = shift;
+    my $type = shift || '';
+
+
+    my $groups = $self->_RoleGroupsJoin( Type => $type );
+    my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
     # XXX: work around, we must hide groups that
     # are members of the role group we search in,
     # otherwise them result in wrong NULLs in Users
@@ -918,21 +1048,21 @@ sub _WatcherJoin {
     # RT doesn't allow to add groups as members of the
     # ticket roles, so we just hide entries in CGM table
     # with MemberId == GroupId from results
-    my $groupmembers = $self->SUPER::Limit(
-        LEFTJOIN   => $groupmembers,
+    $self->SUPER::Limit(
+        LEFTJOIN   => $group_members,
         FIELD      => 'GroupId',
         OPERATOR   => '!=',
-        VALUE      => "$groupmembers.MemberId",
+        VALUE      => "$group_members.MemberId",
         QUOTEVALUE => 0,
     );
     my $users = $self->Join(
-        TYPE   => 'LEFT',
-        ALIAS1 => $groupmembers,
-        FIELD1 => 'MemberId',
-        TABLE2 => 'Users',
-        FIELD2 => 'id'
+        TYPE            => 'LEFT',
+        ALIAS1          => $group_members,
+        FIELD1          => 'MemberId',
+        TABLE2          => 'Users',
+        FIELD2          => 'id',
     );
-    return $self->{'_watcher_join_users_alias'}{ $type || 'any' } = $users;
+    return ($groups, $group_members, $users);
 }
 
 =head2 _WatcherMembershipLimit
@@ -1028,7 +1158,7 @@ sub _WatcherMembershipLimit {
     # }}}
 
     # If we care about which sort of watcher
-    my $meta = $FIELDS{$field};
+    my $meta = $FIELD_METADATA{$field};
     my $type = ( defined $meta->[1] ? $meta->[1] : undef );
 
     if ($type) {
@@ -1065,85 +1195,251 @@ sub _WatcherMembershipLimit {
 
 }
 
-sub _LinkFieldLimit {
-    my $restriction;
-    my $self;
-    my $LinkAlias;
-    my %args;
-    if ( $restriction->{'TYPE'} ) {
-        $self->SUPER::Limit(
-            ALIAS           => $LinkAlias,
-            ENTRYAGGREGATOR => 'AND',
-            FIELD           => 'Type',
-            OPERATOR        => '=',
-            VALUE           => $restriction->{'TYPE'}
-        );
-    }
+=head2 _CustomFieldDecipher
+
+Try and turn a CF descriptor into (cfid, cfname) object pair.
+
+=cut
 
-    #If we're trying to limit it to things that are target of
-    if ( $restriction->{'TARGET'} ) {
+sub _CustomFieldDecipher {
+    my ($self, $string) = @_;
 
-        # If the TARGET is an integer that means that we want to look at
-        # the LocalTarget field. otherwise, we want to look at the
-        # "Target" field
-        my ($matchfield);
-        if ( $restriction->{'TARGET'} =~ /^(\d+)$/ ) {
-            $matchfield = "LocalTarget";
+    my ($queue, $field, $column) = ($string =~ /^(?:(.+?)\.)?{(.+)}(?:\.(.+))?$/);
+    $field ||= ($string =~ /^{(.*?)}$/)[0] || $string;
+
+    my $cf;
+    if ( $queue ) {
+        my $q = RT::Queue->new( $self->CurrentUser );
+        $q->Load( $queue );
+
+        if ( $q->id ) {
+            # $queue = $q->Name; # should we normalize the queue?
+            $cf = $q->CustomField( $field );
         }
         else {
-            $matchfield = "Target";
+            $RT::Logger->warning("Queue '$queue' doesn't exist, parsed from '$string'");
+            $queue = 0;
+        }
+    }
+    elsif ( $field =~ /\D/ ) {
+        $queue = '';
+        my $cfs = RT::CustomFields->new( $self->CurrentUser );
+        $cfs->Limit( FIELD => 'Name', VALUE => $field );
+        $cfs->LimitToLookupType('RT::Queue-RT::Ticket');
+
+        # if there is more then one field the current user can
+        # see with the same name then we shouldn't return cf object
+        # as we don't know which one to use
+        $cf = $cfs->First;
+        if ( $cf ) {
+            $cf = undef if $cfs->Next;
         }
+    }
+    else {
+        $cf = RT::CustomField->new( $self->CurrentUser );
+        $cf->Load( $field );
+    }
+
+    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->SUPER::Limit(
-            ALIAS           => $LinkAlias,
+            LEFTJOIN        => $TicketCFs,
+            FIELD           => 'CustomField',
+            VALUE           => $cfid,
+            ENTRYAGGREGATOR => 'AND'
+        );
+    }
+    else {
+        my $ocfalias = $self->Join(
+            TYPE       => 'LEFT',
+            FIELD1     => 'Queue',
+            TABLE2     => 'ObjectCustomFields',
+            FIELD2     => 'ObjectId',
+        );
+
+        $self->SUPER::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->SUPER::Limit(
+            LEFTJOIN        => $CFs,
             ENTRYAGGREGATOR => 'AND',
-            FIELD           => $matchfield,
-            OPERATOR        => '=',
-            VALUE           => $restriction->{'TARGET'}
+            FIELD           => 'LookupType',
+            VALUE           => 'RT::Queue-RT::Ticket',
+        );
+        $self->SUPER::Limit(
+            LEFTJOIN        => $CFs,
+            ENTRYAGGREGATOR => 'AND',
+            FIELD           => 'Name',
+            VALUE           => $field,
         );
 
-        #If we're searching on target, join the base to ticket.id
-        $self->_SQLJoin(
-            ALIAS1 => 'main',
-            FIELD1 => $self->{'primary_key'},
-            ALIAS2 => $LinkAlias,
-            FIELD2 => 'LocalBase'
+        $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
+            TYPE   => 'LEFT',
+            ALIAS1 => $CFs,
+            FIELD1 => 'id',
+            TABLE2 => 'ObjectCustomFieldValues',
+            FIELD2 => 'CustomField',
+        );
+        $self->SUPER::Limit(
+            LEFTJOIN        => $TicketCFs,
+            FIELD           => 'ObjectId',
+            VALUE           => 'main.id',
+            QUOTEVALUE      => 0,
+            ENTRYAGGREGATOR => 'AND',
         );
     }
+    $self->SUPER::Limit(
+        LEFTJOIN        => $TicketCFs,
+        FIELD           => 'ObjectType',
+        VALUE           => 'RT::Ticket',
+        ENTRYAGGREGATOR => 'AND'
+    );
+    $self->SUPER::Limit(
+        LEFTJOIN        => $TicketCFs,
+        FIELD           => 'Disabled',
+        OPERATOR        => '=',
+        VALUE           => '0',
+        ENTRYAGGREGATOR => 'AND'
+    );
 
-    #If we're trying to limit it to things that are base of
-    elsif ( $restriction->{'BASE'} ) {
+    return ($TicketCFs, $CFs);
+}
 
-        # If we're trying to match a numeric link, we want to look at
-        # LocalBase, otherwise we want to look at "Base"
-        my ($matchfield);
-        if ( $restriction->{'BASE'} =~ /^(\d+)$/ ) {
-            $matchfield = "LocalBase";
-        }
-        else {
-            $matchfield = "Base";
-        }
+=head2 _DateCustomFieldLimit
+
+Limit based on CustomFields of type Date
+
+Meta Data:
+  none
+
+=cut
+
+sub _DateCustomFieldLimit {
+    my ( $self, $_field, $op, $value, %rest ) = @_;
+
+    my $field = $rest{'SUBKEY'} || die "No field specified";
+
+    # For our sanity, we can only limit on one queue at a time
+
+    my ($queue, $cfid, $column);
+    ($queue, $field, $cfid, $column) = $self->_CustomFieldDecipher( $field );
 
+# 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 $null_columns_ok;
+    if ( ( $op =~ /^NOT LIKE$/i ) or ( $op eq '!=' ) ) {
+        $null_columns_ok = 1;
+    }
+
+    my $cfkey = $cfid ? $cfid : "$queue.$field";
+    my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
+
+    $self->_OpenParen;
+
+    if ( $CFs && !$cfid ) {
         $self->SUPER::Limit(
-            ALIAS           => $LinkAlias,
+            ALIAS           => $CFs,
+            FIELD           => 'Name',
+            VALUE           => $field,
             ENTRYAGGREGATOR => 'AND',
-            FIELD           => $matchfield,
-            OPERATOR        => '=',
-            VALUE           => $restriction->{'BASE'}
         );
+    }
 
-        #If we're searching on base, join the target to ticket.id
-        $self->_SQLJoin(
-            ALIAS1 => 'main',
-            FIELD1 => $self->{'primary_key'},
-            ALIAS2 => $LinkAlias,
-            FIELD2 => 'LocalTarget'
+    $self->_OpenParen if $null_columns_ok;
+
+    my $date = RT::Date->new( $self->CurrentUser );
+    $date->Set( Format => 'unknown', Value => $value );
+
+    if ( $op eq "=" ) {
+
+        # if we're specifying =, that means we want everything on a
+        # particular single day.  in the database, we need to check for >
+        # and < the edges of that day.
+
+        $date->SetToMidnight( Timezone => 'server' );
+        my $daystart = $date->ISO;
+        $date->AddDay;
+        my $dayend = $date->ISO;
+
+        $self->_OpenParen;
+
+        $self->_SQLLimit(
+            ALIAS    => $TicketCFs,
+            FIELD    => 'Content',
+            OPERATOR => ">=",
+            VALUE    => $daystart,
+            %rest,
+        );
+
+        $self->_SQLLimit(
+            ALIAS    => $TicketCFs,
+            FIELD    => 'Content',
+            OPERATOR => "<=",
+            VALUE    => $dayend,
+            %rest,
+            ENTRYAGGREGATOR => 'AND',
+        );
+
+        $self->_CloseParen;
+
+    }
+    else {
+        $self->_SQLLimit(
+            ALIAS    => $TicketCFs,
+            FIELD    => 'Content',
+            OPERATOR => $op,
+            VALUE    => $date->ISO,
+            %rest,
         );
     }
+
+    $self->_CloseParen;
+
 }
 
-=head2 KeywordLimit
+=head2 _CustomFieldLimit
 
-Limit based on Keywords
+Limit based on CustomFields
 
 Meta Data:
   none
@@ -1151,140 +1447,452 @@ Meta Data:
 =cut
 
 sub _CustomFieldLimit {
-    my ( $self, $_field, $op, $value, @rest ) = @_;
+    my ( $self, $_field, $op, $value, %rest ) = @_;
 
-    my %rest  = @rest;
-    my $field = $rest{SUBKEY} || die "No field specified";
+    my $field = $rest{'SUBKEY'} || die "No field specified";
 
     # For our sanity, we can only limit on one queue at a time
-    my $queue = 0;
 
-    if ( $field =~ /^(.+?)\.{(.+)}$/ ) {
-        $queue = $1;
-        $field = $2;
-    }
-    $field = $1 if $field =~ /^{(.+)}$/;    # trim { }
+    my ($queue, $cfid, $cf, $column);
+    ($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.
+# 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 $null_columns_ok;
-    if ( ( $op =~ /^NOT LIKE$/i ) or ( $op eq '!=' ) ) {
-        $null_columns_ok = 1;
+    my ($negative_op, $null_op, $inv_op, $range_op)
+        = $self->ClassifySQLOperation( $op );
+
+    my $fix_op = sub {
+        my $op = shift;
+        return $op unless RT->Config->Get('DatabaseType') eq 'Oracle';
+        return 'MATCHES' if $op eq '=';
+        return 'NOT MATCHES' if $op eq '!=';
+        return $op;
+    };
+
+    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->_SQLLimit(
+            ALIAS    => $TicketCFs,
+            FIELD    => 'id',
+            OPERATOR => $op,
+            VALUE    => $value,
+            %rest
+        );
+        $self->_SQLLimit(
+            ALIAS      => $CFs,
+            FIELD      => 'Name',
+            OPERATOR   => 'IS NOT',
+            VALUE      => 'NULL',
+            QUOTEVALUE => 0,
+            ENTRYAGGREGATOR => 'AND',
+        ) if $CFs;
+        $self->_CloseParen;
     }
+    elsif ( !$negative_op || $single_value ) {
+        $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
+        my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
 
-    my $cfid = 0;
-    if ($queue) {
+        $self->_OpenParen;
 
-        my $q = RT::Queue->new( $self->CurrentUser );
-        $q->Load($queue) if ($queue);
+        $self->_OpenParen;
 
-        my $cf;
-        if ( $q->id ) {
-            $cf = $q->CustomField($field);
+        $self->_OpenParen;
+        # if column is defined then deal only with it
+        # otherwise search in Content and in LargeContent
+        if ( $column ) {
+            $self->_SQLLimit(
+                ALIAS      => $TicketCFs,
+                FIELD      => $column,
+                OPERATOR   => ($column ne 'LargeContent'? $op : $fix_op->($op)),
+                VALUE      => $value,
+                %rest
+            );
         }
-        else {
-            $cf = RT::CustomField->new( $self->CurrentUser );
-            $cf->LoadByNameAndQueue( Queue => '0', Name => $field );
+        elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
+            unless ( length( Encode::encode_utf8($value) ) > 255 ) {
+                $self->_SQLLimit(
+                    ALIAS      => $TicketCFs,
+                    FIELD      => 'Content',
+                    OPERATOR   => $op,
+                    VALUE      => $value,
+                    %rest
+                );
+            } else {
+                $self->_OpenParen;
+                $self->_SQLLimit(
+                    ALIAS      => $TicketCFs,
+                    FIELD      => 'Content',
+                    OPERATOR   => '=',
+                    VALUE      => '',
+                    ENTRYAGGREGATOR => 'OR'
+                );
+                $self->_SQLLimit(
+                    ALIAS      => $TicketCFs,
+                    FIELD      => 'Content',
+                    OPERATOR   => 'IS',
+                    VALUE      => 'NULL',
+                    ENTRYAGGREGATOR => 'OR'
+                );
+                $self->_CloseParen;
+                $self->_SQLLimit(
+                    ALIAS => $TicketCFs,
+                    FIELD => 'LargeContent',
+                    OPERATOR => $fix_op->($op),
+                    VALUE => $value,
+                    ENTRYAGGREGATOR => 'AND',
+                );
+            }
         }
+        else {
+            $self->_SQLLimit(
+                ALIAS      => $TicketCFs,
+                FIELD      => 'Content',
+                OPERATOR   => $op,
+                VALUE      => $value,
+                %rest
+            );
 
-        $cfid = $cf->id;
+            $self->_OpenParen;
+            $self->_OpenParen;
+            $self->_SQLLimit(
+                ALIAS      => $TicketCFs,
+                FIELD      => 'Content',
+                OPERATOR   => '=',
+                VALUE      => '',
+                ENTRYAGGREGATOR => 'OR'
+            );
+            $self->_SQLLimit(
+                ALIAS      => $TicketCFs,
+                FIELD      => 'Content',
+                OPERATOR   => 'IS',
+                VALUE      => 'NULL',
+                ENTRYAGGREGATOR => 'OR'
+            );
+            $self->_CloseParen;
+            $self->_SQLLimit(
+                ALIAS => $TicketCFs,
+                FIELD => 'LargeContent',
+                OPERATOR => $fix_op->($op),
+                VALUE => $value,
+                ENTRYAGGREGATOR => 'AND',
+            );
+            $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->_SQLLimit(
+            ALIAS      => $CFs,
+            FIELD      => 'Name',
+            OPERATOR   => 'IS NOT',
+            VALUE      => 'NULL',
+            QUOTEVALUE => 0,
+            ENTRYAGGREGATOR => 'AND',
+        ) if $CFs;
+        $self->_CloseParen;
 
-    my $TicketCFs;
-    my $cfkey = $cfid ? $cfid : "$queue.$field";
+        if ($negative_op) {
+            $self->_SQLLimit(
+                ALIAS           => $TicketCFs,
+                FIELD           => $column || 'Content',
+                OPERATOR        => 'IS',
+                VALUE           => 'NULL',
+                QUOTEVALUE      => 0,
+                ENTRYAGGREGATOR => 'OR',
+            );
+        }
 
-    # Perform one Join per CustomField
-    if ( $self->{_sql_object_cf_alias}{$cfkey} ) {
-        $TicketCFs = $self->{_sql_object_cf_alias}{$cfkey};
+        $self->_CloseParen;
     }
     else {
-        if ($cfid) {
-            $TicketCFs = $self->{_sql_object_cf_alias}{$cfkey} = $self->Join(
-                TYPE   => 'left',
-                ALIAS1 => 'main',
-                FIELD1 => 'id',
-                TABLE2 => 'ObjectCustomFieldValues',
-                FIELD2 => 'ObjectId',
-            );
+        $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->SUPER::Limit(
-                LEFTJOIN        => $TicketCFs,
-                FIELD           => 'CustomField',
-                VALUE           => $cfid,
-                ENTRYAGGREGATOR => 'AND'
+                LEFTJOIN   => $TicketCFs,
+                ALIAS      => $TicketCFs,
+                FIELD      => $column,
+                OPERATOR   => ($column ne 'LargeContent'? $op : $fix_op->($op)),
+                VALUE      => $value,
             );
         }
         else {
-            my $cfalias = $self->Join(
-                TYPE       => 'left',
-                EXPRESSION => "'$field'",
-                TABLE2     => 'CustomFields',
-                FIELD2     => 'Name',
-            );
-
-            $TicketCFs = $self->{_sql_object_cf_alias}{$cfkey} = $self->Join(
-                TYPE   => 'left',
-                ALIAS1 => $cfalias,
-                FIELD1 => 'id',
-                TABLE2 => 'ObjectCustomFieldValues',
-                FIELD2 => 'CustomField',
-            );
             $self->SUPER::Limit(
-                LEFTJOIN        => $TicketCFs,
-                FIELD           => 'ObjectId',
-                VALUE           => 'main.id',
-                QUOTEVALUE      => 0,
-                ENTRYAGGREGATOR => 'AND',
+                LEFTJOIN   => $TicketCFs,
+                ALIAS      => $TicketCFs,
+                FIELD      => 'Content',
+                OPERATOR   => $op,
+                VALUE      => $value,
             );
         }
-        $self->SUPER::Limit(
-            LEFTJOIN => $TicketCFs,
-            FIELD    => 'ObjectType',
-            VALUE    => ref( $self->NewItem )
-            ,    # we want a single item, not a collection
-            ENTRYAGGREGATOR => 'AND'
-        );
-        $self->SUPER::Limit(
-            LEFTJOIN        => $TicketCFs,
-            FIELD           => 'Disabled',
-            OPERATOR        => '=',
-            VALUE           => '0',
-            ENTRYAGGREGATOR => 'AND'
+        $self->_SQLLimit(
+            %rest,
+            ALIAS      => $TicketCFs,
+            FIELD      => 'id',
+            OPERATOR   => 'IS',
+            VALUE      => 'NULL',
+            QUOTEVALUE => 0,
         );
     }
+}
 
-    $self->_OpenParen if ($null_columns_ok);
+sub _HasAttributeLimit {
+    my ( $self, $field, $op, $value, %rest ) = @_;
 
+    my $alias = $self->Join(
+        TYPE   => 'LEFT',
+        ALIAS1 => 'main',
+        FIELD1 => 'id',
+        TABLE2 => 'Attributes',
+        FIELD2 => 'ObjectId',
+    );
+    $self->SUPER::Limit(
+        LEFTJOIN        => $alias,
+        FIELD           => 'ObjectType',
+        VALUE           => 'RT::Ticket',
+        ENTRYAGGREGATOR => 'AND'
+    );
+    $self->SUPER::Limit(
+        LEFTJOIN        => $alias,
+        FIELD           => 'Name',
+        OPERATOR        => $op,
+        VALUE           => $value,
+        ENTRYAGGREGATOR => 'AND'
+    );
     $self->_SQLLimit(
-        ALIAS      => $TicketCFs,
-        FIELD      => 'Content',
-        OPERATOR   => $op,
-        VALUE      => $value,
-        QUOTEVALUE => 1,
-        @rest
+        %rest,
+        ALIAS      => $alias,
+        FIELD      => 'id',
+        OPERATOR   => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
+        VALUE      => 'NULL',
+        QUOTEVALUE => 0,
     );
-
-    if ($null_columns_ok) {
-        $self->_SQLLimit(
-            ALIAS           => $TicketCFs,
-            FIELD           => 'Content',
-            OPERATOR        => 'IS',
-            VALUE           => 'NULL',
-            QUOTEVALUE      => 0,
-            ENTRYAGGREGATOR => 'OR',
-        );
-    }
-    $self->_CloseParen if ($null_columns_ok);
-
 }
 
+
 # End Helper Functions
 
 # End of SQL Stuff -------------------------------------------------
 
+# {{{ Allow sorting on watchers
+
+=head2 OrderByCols ARRAY
+
+A modified version of the OrderBy method which automatically joins where
+C<ALIAS> is set to the name of a watcher type.
+
+=cut
+
+sub OrderByCols {
+    my $self = shift;
+    my @args = @_;
+    my $clause;
+    my @res   = ();
+    my $order = 0;
+
+    foreach my $row (@args) {
+        if ( $row->{ALIAS} ) {
+            push @res, $row;
+            next;
+        }
+        if ( $row->{FIELD} !~ /\./ ) {
+            my $meta = $self->FIELDS->{ $row->{FIELD} };
+            unless ( $meta ) {
+                push @res, $row;
+                next;
+            }
+
+            if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
+                my $alias = $self->Join(
+                    TYPE   => 'LEFT',
+                    ALIAS1 => 'main',
+                    FIELD1 => $row->{'FIELD'},
+                    TABLE2 => 'Queues',
+                    FIELD2 => 'id',
+                );
+                push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
+            } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
+                || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
+            ) {
+                my $alias = $self->Join(
+                    TYPE   => 'LEFT',
+                    ALIAS1 => 'main',
+                    FIELD1 => $row->{'FIELD'},
+                    TABLE2 => 'Users',
+                    FIELD2 => 'id',
+                );
+                push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
+            } else {
+                push @res, $row;
+            }
+            next;
+        }
+
+        my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
+        my $meta = $self->FIELDS->{$field};
+        if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
+            # cache alias as we want to use one alias per watcher type for sorting
+            my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
+            unless ( $users ) {
+                $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
+                    = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
+            }
+            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 ?$cf_obj->id :0) , $field );
+           # this is described in _CustomFieldLimit
+           $self->_SQLLimit(
+               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->SUPER::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' };
+       } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
+           # PAW logic is "reversed"
+           my $order = "ASC";
+           if (exists $row->{ORDER} ) {
+               my $o = $row->{ORDER};
+               delete $row->{ORDER};
+               $order = "DESC" if $o =~ /asc/i;
+           }
+
+           # Ticket.Owner    1 0 X
+           # Unowned Tickets 0 1 X
+           # Else            0 0 X
+
+           foreach my $uid ( $self->CurrentUser->Id, $RT::Nobody->Id ) {
+               if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
+                   my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
+                   push @res, { %$row, ALIAS => '', FIELD => "CASE WHEN $f=$uid THEN 1 ELSE 0 END", ORDER => $order } ;
+               } else {
+                   push @res, { %$row, FIELD => "Owner=$uid", ORDER => $order } ;
+               }
+           }
+
+           push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
+
+       } elsif ( $field eq 'Customer' ) { #Freeside
+
+           my $linkalias = $self->Join(
+               TYPE   => 'LEFT',
+               ALIAS1 => 'main',
+               FIELD1 => 'id',
+               TABLE2 => 'Links',
+               FIELD2 => 'LocalBase'
+           );
+
+           $self->SUPER::Limit(
+               LEFTJOIN => $linkalias,
+               FIELD    => 'Type',
+               OPERATOR => '=',
+               VALUE    => 'MemberOf',
+           );
+           $self->SUPER::Limit(
+               LEFTJOIN => $linkalias,
+               FIELD    => 'Target',
+               OPERATOR => 'STARTSWITH',
+               VALUE    => 'freeside://freeside/cust_main/',
+           );
+
+           #if there was a Links.RemoteTarget int, this bs wouldn't be necessary
+           my $custnum_sql = "CAST(SUBSTR($linkalias.Target,31) AS INTEGER)";
+
+           if ( $subkey eq 'Number' ) {
+
+               push @res, { %$row,
+                            ALIAS => '',
+                            FIELD => $custnum_sql,
+                          };
+
+           } elsif ( $subkey eq 'Name' ) {
+
+              my $custalias = $self->Join(
+                  TYPE       => 'LEFT',
+                  EXPRESSION => $custnum_sql,
+                  TABLE2     => 'cust_main',
+                  FIELD2     => 'custnum',
+                  
+              );
+
+              my $field = "COALESCE( $custalias.company,
+                                     $custalias.last || ', ' || $custalias.first
+                                   )";
+
+              push @res, { %$row, ALIAS => '', FIELD => $field };
+
+           }
+
+       } #Freeside
+
+       else {
+           push @res, $row;
+       }
+    }
+    return $self->SUPER::OrderByCols(@res);
+}
+
+# }}}
+
 # {{{ Limit the result set based on content
 
 # {{{ sub Limit
@@ -1313,7 +1921,7 @@ sub Limit {
 
     my $index = $self->_NextIndex;
 
-#make the TicketRestrictions hash the equivalent of whatever we just passed in;
+# make the TicketRestrictions hash the equivalent of whatever we just passed in;
 
     %{ $self->{'TicketRestrictions'}{$index} } = %args;
 
@@ -1413,12 +2021,11 @@ sub LimitQueue {
         @_
     );
 
-    #TODO  VALUE should also take queue names and queue objects
-    #TODO FIXME why are we canonicalizing to name, not id, robrt?
-    if ( $args{VALUE} =~ /^\d+$/ ) {
+    #TODO  VALUE should also take queue objects
+    if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
         my $queue = new RT::Queue( $self->CurrentUser );
         $queue->Load( $args{'VALUE'} );
-        $args{VALUE} = $queue->Name;
+        $args{'VALUE'} = $queue->Id;
     }
 
     # What if they pass in an Id?  Check for isNum() and convert to
@@ -1428,10 +2035,10 @@ sub LimitQueue {
 
     $self->Limit(
         FIELD       => 'Queue',
-        VALUE       => $args{VALUE},
+        VALUE       => $args{'VALUE'},
         OPERATOR    => $args{'OPERATOR'},
         DESCRIPTION => join(
-            ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{VALUE},
+            ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
         ),
     );
 
@@ -1680,7 +2287,7 @@ sub LimitTimeWorked {
         VALUE       => $args{'VALUE'},
         OPERATOR    => $args{'OPERATOR'},
         DESCRIPTION => join( ' ',
-            $self->loc('Time worked'),
+            $self->loc('Time Worked'),
             $args{'OPERATOR'}, $args{'VALUE'}, ),
     );
 }
@@ -1705,7 +2312,7 @@ sub LimitTimeLeft {
         VALUE       => $args{'VALUE'},
         OPERATOR    => $args{'OPERATOR'},
         DESCRIPTION => join( ' ',
-            $self->loc('Time left'),
+            $self->loc('Time Left'),
             $args{'OPERATOR'}, $args{'VALUE'}, ),
     );
 }
@@ -1838,12 +2445,6 @@ sub LimitOwner {
   VALUE is a value to match the ticket\'s watcher email addresses against
   TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
 
-=begin testing
-
-my $t1 = RT::Ticket->new($RT::SystemUser);
-$t1->Create(Queue => 'general', Subject => "LimitWatchers test", Requestors => \['requestor1@example.com']);
-
-=end testing
 
 =cut
 
@@ -1876,16 +2477,6 @@ sub LimitWatcher {
     );
 }
 
-sub LimitRequestor {
-    my $self = shift;
-    my %args = (@_);
-    $RT::Logger->error( "Tickets->LimitRequestor is deprecated  at ("
-            . join( ":", caller )
-            . ")" );
-    $self->LimitWatcher( TYPE => 'Requestor', @_ );
-
-}
-
 # }}}
 
 # }}}
@@ -1904,29 +2495,29 @@ TYPE limits the sort of link we want to search on
 TYPE = { RefersTo, MemberOf, DependsOn }
 
 TARGET is the id or URI of the TARGET of the link
-(TARGET used to be 'TICKET'.  'TICKET' is deprecated, but will be treated as TARGET
 
 =cut
 
 sub LimitLinkedTo {
     my $self = shift;
     my %args = (
-        TICKET => undef,
-        TARGET => undef,
-        TYPE   => undef,
+        TARGET   => undef,
+        TYPE     => undef,
+        OPERATOR => '=',
         @_
     );
 
     $self->Limit(
         FIELD       => 'LinkedTo',
         BASE        => undef,
-        TARGET      => ( $args{'TARGET'} || $args{'TICKET'} ),
+        TARGET      => $args{'TARGET'},
         TYPE        => $args{'TYPE'},
         DESCRIPTION => $self->loc(
             "Tickets [_1] by [_2]",
             $self->loc( $args{'TYPE'} ),
-            ( $args{'TARGET'} || $args{'TICKET'} )
+            $args{'TARGET'}
         ),
+        OPERATOR    => $args{'OPERATOR'},
     );
 }
 
@@ -1941,17 +2532,15 @@ TYPE limits the sort of link we want to search on
 
 
 BASE is the id or URI of the BASE of the link
-(BASE used to be 'TICKET'.  'TICKET' is deprecated, but will be treated as BASE
-
 
 =cut
 
 sub LimitLinkedFrom {
     my $self = shift;
     my %args = (
-        BASE   => undef,
-        TICKET => undef,
-        TYPE   => undef,
+        BASE     => undef,
+        TYPE     => undef,
+        OPERATOR => '=',
         @_
     );
 
@@ -1966,13 +2555,14 @@ sub LimitLinkedFrom {
     $self->Limit(
         FIELD       => 'LinkedTo',
         TARGET      => undef,
-        BASE        => ( $args{'BASE'} || $args{'TICKET'} ),
+        BASE        => $args{'BASE'},
         TYPE        => $type,
         DESCRIPTION => $self->loc(
             "Tickets [_1] [_2]",
             $self->loc( $args{'TYPE'} ),
-            ( $args{'BASE'} || $args{'TICKET'} )
+            $args{'BASE'},
         ),
+        OPERATOR    => $args{'OPERATOR'},
     );
 }
 
@@ -1982,11 +2572,11 @@ sub LimitLinkedFrom {
 sub LimitMemberOf {
     my $self      = shift;
     my $ticket_id = shift;
-    $self->LimitLinkedTo(
-        TARGET => "$ticket_id",
+    return $self->LimitLinkedTo(
+        @_,
+        TARGET => $ticket_id,
         TYPE   => 'MemberOf',
     );
-
 }
 
 # }}}
@@ -1995,7 +2585,8 @@ sub LimitMemberOf {
 sub LimitHasMember {
     my $self      = shift;
     my $ticket_id = shift;
-    $self->LimitLinkedFrom(
+    return $self->LimitLinkedFrom(
+        @_,
         BASE => "$ticket_id",
         TYPE => 'HasMember',
     );
@@ -2009,8 +2600,9 @@ sub LimitHasMember {
 sub LimitDependsOn {
     my $self      = shift;
     my $ticket_id = shift;
-    $self->LimitLinkedTo(
-        TARGET => "$ticket_id",
+    return $self->LimitLinkedTo(
+        @_,
+        TARGET => $ticket_id,
         TYPE   => 'DependsOn',
     );
 
@@ -2023,8 +2615,9 @@ sub LimitDependsOn {
 sub LimitDependedOnBy {
     my $self      = shift;
     my $ticket_id = shift;
-    $self->LimitLinkedFrom(
-        BASE => "$ticket_id",
+    return $self->LimitLinkedFrom(
+        @_,
+        BASE => $ticket_id,
         TYPE => 'DependentOn',
     );
 
@@ -2037,8 +2630,9 @@ sub LimitDependedOnBy {
 sub LimitRefersTo {
     my $self      = shift;
     my $ticket_id = shift;
-    $self->LimitLinkedTo(
-        TARGET => "$ticket_id",
+    return $self->LimitLinkedTo(
+        @_,
+        TARGET => $ticket_id,
         TYPE   => 'RefersTo',
     );
 
@@ -2051,11 +2645,11 @@ sub LimitRefersTo {
 sub LimitReferredToBy {
     my $self      = shift;
     my $ticket_id = shift;
-    $self->LimitLinkedFrom(
-        BASE => "$ticket_id",
+    return $self->LimitLinkedFrom(
+        @_,
+        BASE => $ticket_id,
         TYPE => 'ReferredToBy',
     );
-
 }
 
 # }}}
@@ -2223,6 +2817,11 @@ sub LimitCustomField {
         $args{CUSTOMFIELD} = $CF->Id;
     }
 
+    # Handle special customfields types
+    if ($CF->Type eq 'Date') {
+        $args{FIELD} = 'DateCustomFieldValue';
+    }
+
     #If we are looking to compare with a null value.
     if ( $args{'OPERATOR'} =~ /^is$/i ) {
         $args{'DESCRIPTION'}
@@ -2239,12 +2838,12 @@ sub LimitCustomField {
             $CF->Name, $args{OPERATOR}, $args{VALUE} );
     }
 
-    my $q = "";
-    if ( $CF->Queue ) {
-        my $qo = new RT::Queue( $self->CurrentUser );
-        $qo->Load( $CF->Queue );
-        $q = $qo->Name;
+    if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
+        my $QueueObj = RT::Queue->new( $self->CurrentUser );
+        $QueueObj->Load( $args{'QUEUE'} );
+        $args{'QUEUE'} = $QueueObj->Id;
     }
+    delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
 
     my @rest;
     @rest = ( ENTRYAGGREGATOR => 'AND' )
@@ -2252,12 +2851,9 @@ sub LimitCustomField {
 
     $self->Limit(
         VALUE => $args{VALUE},
-        FIELD => "CF."
-            . (
-              $q
-            ? $q . ".{" . $CF->Name . "}"
-            : $CF->Name
-            ),
+        FIELD => "CF"
+            .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
+            .".{" . $CF->Name . "}",
         OPERATOR    => $args{OPERATOR},
         CUSTOMFIELD => 1,
         @rest,
@@ -2336,20 +2932,41 @@ Returns a reference to the set of all items found in this search
 
 sub ItemsArrayRef {
     my $self = shift;
-    my @items;
 
-    unless ( $self->{'items_array'} ) {
+    return $self->{'items_array'} if $self->{'items_array'};
 
-        my $placeholder = $self->_ItemsCounter;
-        $self->GotoFirstItem();
-        while ( my $item = $self->Next ) {
-            push( @{ $self->{'items_array'} }, $item );
-        }
-        $self->GotoItem($placeholder);
-        $self->{'items_array'}
-            = $self->ItemsOrderBy( $self->{'items_array'} );
+    my $placeholder = $self->_ItemsCounter;
+    $self->GotoFirstItem();
+    while ( my $item = $self->Next ) {
+        push( @{ $self->{'items_array'} }, $item );
+    }
+    $self->GotoItem($placeholder);
+    $self->{'items_array'}
+        = $self->ItemsOrderBy( $self->{'items_array'} );
+
+    return $self->{'items_array'};
+}
+
+sub ItemsArrayRefWindow {
+    my $self = shift;
+    my $window = shift;
+
+    my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
+
+    $self->RowsPerPage( $window );
+    $self->FirstRow(1);
+    $self->GotoFirstItem;
+
+    my @res;
+    while ( my $item = $self->Next ) {
+        push @res, $item;
     }
-    return ( $self->{'items_array'} );
+
+    $self->RowsPerPage( $old[1] );
+    $self->FirstRow( $old[2] );
+    $self->GotoItem( $old[0] );
+
+    return \@res;
 }
 
 # }}}
@@ -2360,50 +2977,285 @@ sub Next {
 
     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
 
-    my $Ticket = $self->SUPER::Next();
-    if ( ( defined($Ticket) ) and ( ref($Ticket) ) ) {
+    my $Ticket = $self->SUPER::Next;
+    return $Ticket unless $Ticket;
 
-        if ( $Ticket->__Value('Status') eq 'deleted'
-            && !$self->{'allow_deleted_search'} )
-        {
-            return ( $self->Next() );
-        }
+    if ( $Ticket->__Value('Status') eq 'deleted'
+        && !$self->{'allow_deleted_search'} )
+    {
+        return $self->Next;
+    }
+    elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
+        # if we found a ticket with this option enabled then
+        # all tickets we found are ACLed, cache this fact
+        my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
+        $RT::Principal::_ACL_CACHE->set( $key => 1 );
+        return $Ticket;
+    }
+    elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
+        # has rights
+        return $Ticket;
+    }
+    else {
+        # If the user doesn't have the right to show this ticket
+        return $self->Next;
+    }
+}
+
+sub _DoSearch {
+    my $self = shift;
+    $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
+    return $self->SUPER::_DoSearch( @_ );
+}
+
+sub _DoCount {
+    my $self = shift;
+    $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
+    return $self->SUPER::_DoCount( @_ );
+}
+
+sub _RolesCanSee {
+    my $self = shift;
 
-        # Since Ticket could be granted with more rights instead
-        # of being revoked, it's ok if queue rights allow
-        # ShowTicket.  It seems need another query, but we have
-        # rights cache in Principal::HasRight.
-        elsif ($Ticket->QueueObj->CurrentUserHasRight('ShowTicket')
-            || $Ticket->CurrentUserHasRight('ShowTicket') )
-        {
-            return ($Ticket);
+    my $cache_key = 'RolesHasRight;:;ShowTicket';
+    if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
+        return %$cached;
+    }
+
+    my $ACL = RT::ACL->new( $RT::SystemUser );
+    $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
+    $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
+    my $principal_alias = $ACL->Join(
+        ALIAS1 => 'main',
+        FIELD1 => 'PrincipalId',
+        TABLE2 => 'Principals',
+        FIELD2 => 'id',
+    );
+    $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
+
+    my %res = ();
+    while ( my $ACE = $ACL->Next ) {
+        my $role = $ACE->PrincipalType;
+        my $type = $ACE->ObjectType;
+        if ( $type eq 'RT::System' ) {
+            $res{ $role } = 1;
         }
+        elsif ( $type eq 'RT::Queue' ) {
+            next if $res{ $role } && !ref $res{ $role };
+            push @{ $res{ $role } ||= [] }, $ACE->ObjectId;
+        }
+        else {
+            $RT::Logger->error('ShowTicket right is granted on unsupported object');
+        }
+    }
+    $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
+    return %res;
+}
+
+sub _DirectlyCanSeeIn {
+    my $self = shift;
+    my $id = $self->CurrentUser->id;
+
+    my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
+    if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
+        return @$cached;
+    }
 
-        if ( $Ticket->__Value('Status') eq 'deleted' ) {
-            return ( $self->Next() );
+    my $ACL = RT::ACL->new( $RT::SystemUser );
+    $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
+    my $principal_alias = $ACL->Join(
+        ALIAS1 => 'main',
+        FIELD1 => 'PrincipalId',
+        TABLE2 => 'Principals',
+        FIELD2 => 'id',
+    );
+    $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
+    my $cgm_alias = $ACL->Join(
+        ALIAS1 => 'main',
+        FIELD1 => 'PrincipalId',
+        TABLE2 => 'CachedGroupMembers',
+        FIELD2 => 'GroupId',
+    );
+    $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
+    $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
+
+    my @res = ();
+    while ( my $ACE = $ACL->Next ) {
+        my $type = $ACE->ObjectType;
+        if ( $type eq 'RT::System' ) {
+            # If user is direct member of a group that has the right
+            # on the system then he can see any ticket
+            $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
+            return (-1);
+        }
+        elsif ( $type eq 'RT::Queue' ) {
+            push @res, $ACE->ObjectId;
+        }
+        else {
+            $RT::Logger->error('ShowTicket right is granted on unsupported object');
         }
+    }
+    $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
+    return @res;
+}
+
+sub CurrentUserCanSee {
+    my $self = shift;
+    return if $self->{'_sql_current_user_can_see_applied'};
+
+    return $self->{'_sql_current_user_can_see_applied'} = 1
+        if $self->CurrentUser->UserObj->HasRight(
+            Right => 'SuperUser', Object => $RT::System
+        );
+
+    my $id = $self->CurrentUser->id;
 
-        # Since Ticket could be granted with more rights instead
-        # of being revoked, it's ok if queue rights allow
-        # ShowTicket.  It seems need another query, but we have
-        # rights cache in Principal::HasRight.
-        elsif ($Ticket->QueueObj->CurrentUserHasRight('ShowTicket')
-            || $Ticket->CurrentUserHasRight('ShowTicket') )
-        {
-            return ($Ticket);
+    # directly can see in all queues then we have nothing to do
+    my @direct_queues = $self->_DirectlyCanSeeIn;
+    return $self->{'_sql_current_user_can_see_applied'} = 1
+        if @direct_queues && $direct_queues[0] == -1;
+
+    my %roles = $self->_RolesCanSee;
+    {
+        my %skip = map { $_ => 1 } @direct_queues;
+        foreach my $role ( keys %roles ) {
+            next unless ref $roles{ $role };
+
+            my @queues = grep !$skip{$_}, @{ $roles{ $role } };
+            if ( @queues ) {
+                $roles{ $role } = \@queues;
+            } else {
+                delete $roles{ $role };
+            }
         }
+    }
 
-        #If the user doesn't have the right to show this ticket
-        else {
-            return ( $self->Next() );
+# there is no global watchers, only queues and tickes, if at
+# some point we will add global roles then it's gonna blow
+# the idea here is that if the right is set globaly for a role
+# and user plays this role for a queue directly not a ticket
+# then we have to check in advance
+    if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
+
+        my $groups = RT::Groups->new( $RT::SystemUser );
+        $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
+        foreach ( @tmp ) {
+            $groups->Limit( FIELD => 'Type', VALUE => $_ );
+        }
+        my $principal_alias = $groups->Join(
+            ALIAS1 => 'main',
+            FIELD1 => 'id',
+            TABLE2 => 'Principals',
+            FIELD2 => 'id',
+        );
+        $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
+        my $cgm_alias = $groups->Join(
+            ALIAS1 => 'main',
+            FIELD1 => 'id',
+            TABLE2 => 'CachedGroupMembers',
+            FIELD2 => 'GroupId',
+        );
+        $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
+        $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
+        while ( my $group = $groups->Next ) {
+            push @direct_queues, $group->Instance;
         }
     }
 
-    #if there never was any ticket
-    else {
-        return (undef);
+    unless ( @direct_queues || keys %roles ) {
+        $self->SUPER::Limit(
+            SUBCLAUSE => 'ACL',
+            ALIAS => 'main',
+            FIELD => 'id',
+            VALUE => 0,
+            ENTRYAGGREGATOR => 'AND',
+        );
+        return $self->{'_sql_current_user_can_see_applied'} = 1;
     }
 
+    {
+        my $join_roles = keys %roles;
+        $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
+        my ($role_group_alias, $cgm_alias);
+        if ( $join_roles ) {
+            $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
+            $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
+            $self->SUPER::Limit(
+                LEFTJOIN   => $cgm_alias,
+                FIELD      => 'MemberId',
+                OPERATOR   => '=',
+                VALUE      => $id,
+            );
+        }
+        my $limit_queues = sub {
+            my $ea = shift;
+            my @queues = @_;
+
+            return unless @queues;
+            if ( @queues == 1 ) {
+                $self->SUPER::Limit(
+                    SUBCLAUSE => 'ACL',
+                    ALIAS => 'main',
+                    FIELD => 'Queue',
+                    VALUE => $_[0],
+                    ENTRYAGGREGATOR => $ea,
+                );
+            } else {
+                $self->SUPER::_OpenParen('ACL');
+                foreach my $q ( @queues ) {
+                    $self->SUPER::Limit(
+                        SUBCLAUSE => 'ACL',
+                        ALIAS => 'main',
+                        FIELD => 'Queue',
+                        VALUE => $q,
+                        ENTRYAGGREGATOR => $ea,
+                    );
+                    $ea = 'OR';
+                }
+                $self->SUPER::_CloseParen('ACL');
+            }
+            return 1;
+        };
+
+        $self->SUPER::_OpenParen('ACL');
+        my $ea = 'AND';
+        $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
+        while ( my ($role, $queues) = each %roles ) {
+            $self->SUPER::_OpenParen('ACL');
+            if ( $role eq 'Owner' ) {
+                $self->SUPER::Limit(
+                    SUBCLAUSE => 'ACL',
+                    FIELD           => 'Owner',
+                    VALUE           => $id,
+                    ENTRYAGGREGATOR => $ea,
+                );
+            }
+            else {
+                $self->SUPER::Limit(
+                    SUBCLAUSE       => 'ACL',
+                    ALIAS           => $cgm_alias,
+                    FIELD           => 'MemberId',
+                    OPERATOR        => 'IS NOT',
+                    VALUE           => 'NULL',
+                    QUOTEVALUE      => 0,
+                    ENTRYAGGREGATOR => $ea,
+                );
+                $self->SUPER::Limit(
+                    SUBCLAUSE       => 'ACL',
+                    ALIAS           => $role_group_alias,
+                    FIELD           => 'Type',
+                    VALUE           => $role,
+                    ENTRYAGGREGATOR => 'AND',
+                );
+            }
+            $limit_queues->( 'AND', @$queues ) if ref $queues;
+            $ea = 'OR' if $ea eq 'AND';
+            $self->SUPER::_CloseParen('ACL');
+        }
+        $self->SUPER::_CloseParen('ACL');
+    }
+    return $self->{'_sql_current_user_can_see_applied'} = 1;
 }
 
 # }}}
@@ -2519,12 +3371,9 @@ sub _RestrictionsToClauses {
     foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
         my $restriction = $self->{'TicketRestrictions'}{$row};
 
-        #use Data::Dumper;
-        #print Dumper($restriction),"\n";
-
-   # We need to reimplement the subclause aggregation that SearchBuilder does.
-   # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
-   # Then SB AND's the different Subclauses together.
+        # We need to reimplement the subclause aggregation that SearchBuilder does.
+        # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
+        # Then SB AND's the different Subclauses together.
 
         # So, we want to group things into Subclauses, convert them to
         # SQL, and then join them with the appropriate DefaultEA.
@@ -2547,10 +3396,10 @@ sub _RestrictionsToClauses {
         }
 
         die "I don't know about $field yet"
-            unless ( exists $FIELDS{$realfield}
-            or $restriction->{CUSTOMFIELD} );
+            unless ( exists $FIELD_METADATA{$realfield}
+                or $restriction->{CUSTOMFIELD} );
 
-        my $type = $FIELDS{$realfield}->[0];
+        my $type = $FIELD_METADATA{$realfield}->[0];
         my $op   = $restriction->{'OPERATOR'};
 
         my $value = (
@@ -2598,7 +3447,6 @@ sub _RestrictionsToClauses {
         # here is where we store extra data, say if it's a keyword or
         # something.  (I.e. "TYPE SPECIFIC STUFF")
 
-        #print Dumper($data);
         push @{ $clause{$realfield} }, $data;
     }
     return \%clause;
@@ -2648,70 +3496,80 @@ sub _ProcessRestrictions {
 
 =head2 _BuildItemMap
 
-    # Build up a map of first/last/next/prev items, so that we can display search nav quickly
+Build up a L</ItemMap> of first/last/next/prev items, so that we can
+display search nav quickly.
 
 =cut
 
 sub _BuildItemMap {
     my $self = shift;
 
-    my $items = $self->ItemsArrayRef;
-    my $prev  = 0;
+    my $window = RT->Config->Get('TicketsItemMapSize');
 
-    delete $self->{'item_map'};
-    if ( $items->[0] ) {
-        $self->{'item_map'}->{'first'} = $items->[0]->EffectiveId;
-        while ( my $item = shift @$items ) {
-            my $id = $item->EffectiveId;
-            $self->{'item_map'}->{$id}->{'defined'} = 1;
-            $self->{'item_map'}->{$id}->{prev}      = $prev;
-            $self->{'item_map'}->{$id}->{next}      = $items->[0]->EffectiveId
-                if ( $items->[0] );
-            $prev = $id;
-        }
-        $self->{'item_map'}->{'last'} = $prev;
+    $self->{'item_map'} = {};
+
+    my $items = $self->ItemsArrayRefWindow( $window );
+    return unless $items && @$items;
+
+    my $prev = 0;
+    $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
+    for ( my $i = 0; $i < @$items; $i++ ) {
+        my $item = $items->[$i];
+        my $id = $item->EffectiveId;
+        $self->{'item_map'}{$id}{'defined'} = 1;
+        $self->{'item_map'}{$id}{'prev'}    = $prev;
+        $self->{'item_map'}{$id}{'next'}    = $items->[$i+1]->EffectiveId
+            if $items->[$i+1];
+        $prev = $id;
     }
+    $self->{'item_map'}{'last'} = $prev
+        if !$window || @$items < $window;
 }
 
 =head2 ItemMap
 
-Returns an a map of all items found by this search. The map is of the form
+Returns an a map of all items found by this search. The map is a hash
+of the form:
 
-$ItemMap->{'first'} = first ticketid found
-$ItemMap->{'last'} = last ticketid found
-$ItemMap->{$id}->{prev} = the ticket id found before $id
-$ItemMap->{$id}->{next} = the ticket id found after $id
+    {
+        first => <first ticket id found>,
+        last => <last ticket id found or undef>,
+
+        <ticket id> => {
+            prev => <the ticket id found before>,
+            next => <the ticket id found after>,
+        },
+        <ticket id> => {
+            prev => ...,
+            next => ...,
+        },
+    }
 
 =cut
 
 sub ItemMap {
     my $self = shift;
-    $self->_BuildItemMap()
-        unless ( $self->{'items_array'} and $self->{'item_map'} );
-    return ( $self->{'item_map'} );
-}
-
-=cut
-
-
-
+    $self->_BuildItemMap unless $self->{'item_map'};
+    return $self->{'item_map'};
 }
 
 
-
 # }}}
 
 # }}}
 
 =head2 PrepForSerialization
 
-You don't want to serialize a big tickets object, as the {items} hash will be instantly invalid _and_ eat lots of space
+You don't want to serialize a big tickets object, as
+the {items} hash will be instantly invalid _and_ eat
+lots of space
 
 =cut
 
 sub PrepForSerialization {
     my $self = shift;
     delete $self->{'items'};
+    delete $self->{'items_array'};
     $self->RedoSearch();
 }
 
@@ -2729,16 +3587,9 @@ $tickets->{'flagname'} = 1;
 
 BUG: There should be an API for this
 
-=cut
-
-=begin testing
 
-# We assume that we've got some tickets hanging around from before.
-ok( my $unlimittickets = RT::Tickets->new( $RT::SystemUser ) );
-ok( $unlimittickets->UnLimit );
-ok( $unlimittickets->Count > 0, "UnLimited tickets object should return tickets" );
 
-=end testing
+=cut
 
 1;