This commit was generated by cvs2svn to compensate for changes in r4407,
[freeside.git] / rt / lib / RT / Tickets_Overlay.pm
index d8a1ac8..0e6585c 100644 (file)
@@ -1,8 +1,14 @@
-# BEGIN LICENSE BLOCK
+# BEGIN BPS TAGGED BLOCK {{{
 # 
-# Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
+# COPYRIGHT:
+#  
+# This software is Copyright (c) 1996-2005 Best Practical Solutions, LLC 
+#                                          <jesse@bestpractical.com>
 # 
-# (Except where explictly superceded by other copyright notices)
+# (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
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 # General Public License for more details.
 # 
-# Unless otherwise specified, all modifications, corrections or
-# extensions to this work which alter its source code become the
-# property of Best Practical Solutions, LLC when submitted for
-# inclusion in the work.
+# 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.
+# 
+# 
+# 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
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# 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 LICENSE BLOCK
+# END BPS TAGGED BLOCK }}}
 # Major Changes:
 
 # - Decimated ProcessRestrictions and broke it into multiple
 =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;
 
 # Configuration Tables:
 
 # FIELDS 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          => ['ENUM' => 'User',],
-    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',],
-    ReferredTo      => ['LINK' => From => 'RefersTo',],
-#   HasDepender            => ['LINK',],
-#   RelatedTo      => ['LINK',],
+my %FIELDS = (
+    Status          => ['ENUM'],
+    Queue           => [ 'ENUM' => 'Queue', ],
+    Type            => [ 'ENUM', ],
+    Creator         => [ 'ENUM' => 'User', ],
+    LastUpdatedBy   => [ 'ENUM' => 'User', ],
+    Owner           => [ 'ENUM' => 'User', ],
+    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',],
@@ -95,65 +126,83 @@ my %FIELDS =
     LastUpdated            => ['DATE' => 'LastUpdated',],
     Created        => ['DATE' => 'Created',],
     Subject        => ['STRING',],
-    Type           => ['STRING',],
     Content        => ['TRANSFIELD',],
     ContentType            => ['TRANSFIELD',],
     Filename        => ['TRANSFIELD',],
     TransactionDate => ['TRANSDATE',],
     Requestor       => ['WATCHERFIELD' => 'Requestor',],
-    CC              => ['WATCHERFIELD' => 'Cc',],
-    AdminCC         => ['WATCHERFIELD' => 'AdminCC',],
+    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', ],
+);
 
 # Mapping of Field Type to Function
-my %dispatch =
-  ( ENUM           => \&_EnumLimit,
-    INT                    => \&_IntLimit,
-    LINK           => \&_LinkLimit,
-    DATE           => \&_DateLimit,
-    STRING         => \&_StringLimit,
-    TRANSFIELD     => \&_TransLimit,
-    TRANSDATE      => \&_TransDateLimit,
+my %dispatch = (
+    ENUM            => \&_EnumLimit,
+    INT             => \&_IntLimit,
+    LINK            => \&_LinkLimit,
+    DATE            => \&_DateLimit,
+    STRING          => \&_StringLimit,
+    TRANSFIELD      => \&_TransLimit,
+    TRANSDATE       => \&_TransDateLimit,
     WATCHERFIELD    => \&_WatcherLimit,
-    LINKFIELD      => \&_LinkFieldLimit,
-    CUSTOMFIELD    => \&_CustomFieldLimit,
-  );
+    MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
+    LINKFIELD       => \&_LinkFieldLimit,
+    CUSTOMFIELD     => \&_CustomFieldLimit,
+);
+my %can_bundle = ( WATCHERFIELD => "yeps", );
 
 # Default EntryAggregator per type
+# if you specify OP, you must specify all valid OPs
 my %DefaultEA = (
-                 INT           => 'AND',
-                 ENUM          => { '=' => 'OR',
-                                    '!='=> 'AND'
-                                  },
-                 DATE          => 'AND',
-                 STRING                => { '=' => 'OR',
-                                    '!='=> 'AND',
-                                    'LIKE'=> 'AND',
-                                    'NOT LIKE' => 'AND'
-                                  },
-                 TRANSFIELD    => 'AND',
-                 TRANSDATE     => 'AND',
-                 LINKFIELD     => 'AND',
-                 TARGET                => 'AND',
-                 BASE          => 'AND',
-                 WATCHERFIELD  => { '=' => 'OR',
-                                    '!='=> 'AND',
-                                    'LIKE'=> 'OR',
-                                    'NOT LIKE' => 'AND'
-                                  },
-
-                 CUSTOMFIELD   => 'OR',
-                );
-
+    INT  => 'AND',
+    ENUM => {
+        '='  => 'OR',
+        '!=' => 'AND'
+    },
+    DATE => {
+        '='  => 'OR',
+        '>=' => 'AND',
+        '<=' => 'AND',
+        '>'  => 'AND',
+        '<'  => 'AND'
+    },
+    STRING => {
+        '='        => 'OR',
+        '!='       => 'AND',
+        'LIKE'     => 'AND',
+        'NOT LIKE' => 'AND'
+    },
+    TRANSFIELD   => 'AND',
+    TRANSDATE    => 'AND',
+    LINK         => 'OR',
+    LINKFIELD    => 'AND',
+    TARGET       => 'AND',
+    BASE         => 'AND',
+    WATCHERFIELD => {
+        '='        => 'OR',
+        '!='       => 'AND',
+        'LIKE'     => 'OR',
+        'NOT LIKE' => 'AND'
+    },
+
+    CUSTOMFIELD => 'OR',
+);
 
 # Helper functions for passing the above lexically scoped tables above
 # into Tickets_Overlay_SQL.
-sub FIELDS   { return \%FIELDS   }
-sub dispatch { return \%dispatch }
+sub FIELDS     { return \%FIELDS }
+sub dispatch   { return \%dispatch }
+sub can_bundle { return \%can_bundle }
 
 # Bring in the clowns.
 require RT::Tickets_Overlay_SQL;
@@ -161,10 +210,10 @@ require RT::Tickets_Overlay_SQL;
 # {{{ sub SortFields
 
 @SORTFIELDS = qw(id Status
-                Queue Subject
-         Owner Created Due Starts Started
-         Told
-                Resolved LastUpdated Priority TimeWorked TimeLeft);
+  Queue Subject
+  Owner Created Due Starts Started
+  Told
+  Resolved LastUpdated Priority TimeWorked TimeLeft);
 
 =head2 SortFields
 
@@ -173,14 +222,12 @@ Returns the list of fields that lists of tickets can easily be sorted by
 =cut
 
 sub SortFields {
-       my $self = shift;
-       return(@SORTFIELDS);
+    my $self = shift;
+    return (@SORTFIELDS);
 }
 
-
 # }}}
 
-
 # BEGIN SQL STUFF *********************************
 
 =head1 Limit Helper Routines
@@ -215,26 +262,27 @@ Meta Data:
 =cut
 
 sub _EnumLimit {
-  my ($sb,$field,$op,$value,@rest) = @_;
+    my ( $sb, $field, $op, $value, @rest ) = @_;
 
-  # SQL::Statement changes != to <>.  (Can we remove this now?)
-  $op = "!=" if $op eq "<>";
+    # SQL::Statement changes != to <>.  (Can we remove this now?)
+    $op = "!=" if $op eq "<>";
 
-  die "Invalid Operation: $op for $field"
-    unless $op eq "=" or $op eq "!=";
+    die "Invalid Operation: $op for $field"
+      unless $op eq "=" or $op eq "!=";
 
-  my $meta = $FIELDS{$field};
-  if (defined $meta->[1]) {
-    my $class = "RT::" . $meta->[1];
-    my $o = $class->new($sb->CurrentUser);
-    $o->Load( $value );
-    $value = $o->Id;
-  }
-  $sb->_SQLLimit( FIELD => $field,,
-             VALUE => $value,
-             OPERATOR => $op,
-             @rest,
-           );
+    my $meta = $FIELDS{$field};
+    if ( defined $meta->[1] ) {
+        my $class = "RT::" . $meta->[1];
+        my $o     = $class->new( $sb->CurrentUser );
+        $o->Load($value);
+        $value = $o->Id;
+    }
+    $sb->_SQLLimit(
+        FIELD    => $field,
+        VALUE    => $value,
+        OPERATOR => $op,
+        @rest,
+    );
 }
 
 =head2 _IntLimit
@@ -248,88 +296,138 @@ Meta Data:
 =cut
 
 sub _IntLimit {
-  my ($sb,$field,$op,$value,@rest) = @_;
+    my ( $sb, $field, $op, $value, @rest ) = @_;
 
-  die "Invalid Operator $op for $field"
-    unless $op =~ /^(=|!=|>|<|>=|<=)$/;
+    die "Invalid Operator $op for $field"
+      unless $op =~ /^(=|!=|>|<|>=|<=)$/;
 
-  $sb->_SQLLimit(
-            FIELD => $field,
-            VALUE => $value,
-            OPERATOR => $op,
-            @rest,
-           );
+    $sb->_SQLLimit(
+        FIELD    => $field,
+        VALUE    => $value,
+        OPERATOR => $op,
+        @rest,
+    );
 }
 
-
 =head2 _LinkLimit
 
 Handle fields which deal with links between tickets.  (MemberOf, DependsOn)
 
 Meta Data:
   1: Direction (From,To)
-  2: Relationship Type (MemberOf, DependsOn,RefersTo)
+  2: Link Type (MemberOf, DependsOn,RefersTo)
 
 =cut
 
 sub _LinkLimit {
-  my ($sb,$field,$op,$value,@rest) = @_;
+    my ( $sb, $field, $op, $value, @rest ) = @_;
 
-  die "Op must be ="
-    unless $op eq "=";
+    my $meta = $FIELDS{$field};
+    die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS)/io;
 
-  my $meta = $FIELDS{$field};
-  die "Incorrect Meta Data for $field"
-    unless (defined $meta->[1] and defined $meta->[2]);
+    die "Incorrect Metadata for $field"
+      unless ( defined $meta->[1] and defined $meta->[2] );
 
-  my $LinkAlias = $sb->NewAlias ('Links');
+    my $direction = $meta->[1];
 
-  $sb->_OpenParen();
+    my $matchfield;
+    my $linkfield;
+    my $is_local = 1;
+    my $is_null  = 0;
+    if ( $direction eq 'To' ) {
+        $matchfield = "Target";
+        $linkfield  = "Base";
 
-  $sb->_SQLLimit(
-            ALIAS => $LinkAlias,
-            FIELD =>   'Type',
-            OPERATOR => '=',
-            VALUE => $meta->[2],
-            @rest,
-           );
-
-  if ($meta->[1] eq "To") {
-    my $matchfield = ( $value  =~ /^(\d+)$/ ? "LocalTarget" : "Target" );
-
-    $sb->_SQLLimit(
-              ALIAS => $LinkAlias,
-              ENTRYAGGREGATOR => 'AND',
-              FIELD =>   $matchfield,
-              OPERATOR => '=',
-              VALUE => $value ,
-             );
-
-    #If we're searching on target, join the base to ticket.id
-    $sb->Join( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'},
-              ALIAS2 => $LinkAlias,     FIELD2 => 'LocalBase');
-
-  } elsif ( $meta->[1] eq "From" ) {
-    my $matchfield = ( $value  =~ /^(\d+)$/ ? "LocalBase" : "Base" );
+    }
+    elsif ( $direction eq 'From' ) {
+        $linkfield  = "Target";
+        $matchfield = "Base";
 
-    $sb->_SQLLimit(
-              ALIAS => $LinkAlias,
-              ENTRYAGGREGATOR => 'AND',
-              FIELD =>   $matchfield,
-              OPERATOR => '=',
-              VALUE => $value ,
-             );
+    }
+    else {
+        die "Invalid link direction '$meta->[1]' for $field\n";
+    }
 
-    #If we're searching on base, join the target to ticket.id
-    $sb->Join( ALIAS1 => 'main',     FIELD1 => $sb->{'primary_key'},
-              ALIAS2 => $LinkAlias, FIELD2 => 'LocalTarget');
+    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;
+        }
+    }
 
-  } else {
-    die "Invalid link direction '$meta->[1]' for $field\n";
-  }
+#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);
+
+    if ($is_null) {
+        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           => $linkalias,
+            ENTRYAGGREGATOR => 'AND',
+            FIELD           => ( $is_local ? "Local$matchfield" : $matchfield ),
+            OPERATOR        => 'IS',
+            VALUE           => 'NULL',
+            QUOTEVALUE      => '0',
+        );
 
-  $sb->_CloseParen();
+    }
+    else {
 
+        $sb->{_sql_linkalias} = $sb->NewAlias('Links')
+          unless defined $sb->{_sql_linkalias};
+
+        $sb->_OpenParen();
+
+        $sb->_SQLLimit(
+            ALIAS    => $sb->{_sql_linkalias},
+            FIELD    => 'Type',
+            OPERATOR => '=',
+            VALUE    => $meta->[2],
+            @rest,
+        );
+
+        $sb->_SQLLimit(
+            ALIAS           => $sb->{_sql_linkalias},
+            ENTRYAGGREGATOR => 'AND',
+            FIELD           => ( $is_local ? "Local$matchfield" : $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->_CloseParen();
+    }
 }
 
 =head2 _DateLimit
@@ -337,35 +435,66 @@ sub _LinkLimit {
 Handle date fields.  (Created, LastTold..)
 
 Meta Data:
-  1: type of relationship.  (Probably not necessary.)
+  1: type of link.  (Probably not necessary.)
 
 =cut
 
 sub _DateLimit {
-  my ($sb,$field,$op,$value,@rest) = @_;
+    my ( $sb, $field, $op, $value, @rest ) = @_;
 
-  die "Invalid Date Op: $op"
-     unless $op =~ /^(=|!=|>|<|>=|<=)$/;
+    die "Invalid Date Op: $op"
+      unless $op =~ /^(=|>|<|>=|<=)$/;
 
-  my $meta = $FIELDS{$field};
-  die "Incorrect Meta Data for $field"
-    unless (defined $meta->[1]);
+    my $meta = $FIELDS{$field};
+    die "Incorrect Meta Data for $field"
+      unless ( defined $meta->[1] );
 
-  require Time::ParseDate;
-  use POSIX 'strftime';
+    use POSIX 'strftime';
+    
+    my $date = RT::Date->new($sb->CurrentUser);
+    $date->Set(Format => 'unknown', Value => $value); 
+    my $time = $date->Unix;
+
+    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.
+
+        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 ) ) );
+
+        $sb->_OpenParen;
 
-  my $time = Time::ParseDate::parsedate( $value,
-                       UK => $RT::DateDayBeforeMonth,
-                       PREFER_PAST => $RT::AmbiguousDayInPast,
-                       PREFER_FUTURE => !($RT::AmbiguousDayInPast));
-  $value = strftime("%Y-%m-%d %H:%M",localtime($time));
+        $sb->_SQLLimit(
+            FIELD    => $meta->[1],
+            OPERATOR => ">=",
+            VALUE    => $daystart,
+            @rest,
+        );
 
-  $sb->_SQLLimit(
-            FIELD => $meta->[1],
-            OPERATOR => $op,
-            VALUE => $value,
-            @rest,
-           );
+        $sb->_SQLLimit(
+            FIELD    => $meta->[1],
+            OPERATOR => "<=",
+            VALUE    => $dayend,
+            @rest,
+            ENTRYAGGREGATOR => 'AND',
+        );
+
+        $sb->_CloseParen;
+
+    }
+    else {
+        $value = strftime( "%Y-%m-%d %H:%M", gmtime($time) );
+        $sb->_SQLLimit(
+            FIELD    => $meta->[1],
+            OPERATOR => $op,
+            VALUE    => $value,
+            @rest,
+        );
+    }
 }
 
 =head2 _StringLimit
@@ -378,19 +507,19 @@ Meta Data:
 =cut
 
 sub _StringLimit {
-  my ($sb,$field,$op,$value,@rest) = @_;
+    my ( $sb, $field, $op, $value, @rest ) = @_;
 
-  # FIXME:
-  # Valid Operators:
-  #  =, !=, LIKE, NOT LIKE
+    # FIXME:
+    # Valid Operators:
+    #  =, !=, LIKE, NOT LIKE
 
-  $sb->_SQLLimit(
-            FIELD => $field,
-            OPERATOR => $op,
-            VALUE => $value,
-            CASESENSITIVE => 0,
-            @rest,
-           );
+    $sb->_SQLLimit(
+        FIELD         => $field,
+        OPERATOR      => $op,
+        VALUE         => $value,
+        CASESENSITIVE => 0,
+        @rest,
+    );
 }
 
 =head2 _TransDateLimit
@@ -404,40 +533,91 @@ Meta Data:
 
 =cut
 
+# This routine should really be factored into translimit.
 sub _TransDateLimit {
-  my ($sb,$field,$op,$value,@rest) = @_;
+    my ( $sb, $field, $op, $value, @rest ) = @_;
+
+    # See the comments for TransLimit, they apply here too
+
+    $sb->{_sql_transalias} = $sb->NewAlias('Transactions')
+        unless defined $sb->{_sql_transalias};
+    $sb->{_sql_trattachalias} = $sb->NewAlias('Attachments')
+        unless defined $sb->{_sql_trattachalias};
+
+    my $date = RT::Date->new( $sb->CurrentUser );
+    $date->Set( Format => 'unknown', Value => $value );
+    my $time = $date->Unix;
+
+    $sb->_OpenParen;
+    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.
+
+        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 ) ) );
+
+        $sb->_SQLLimit(
+            ALIAS         => $sb->{_sql_transalias},
+            FIELD         => 'Created',
+            OPERATOR      => ">=",
+            VALUE         => $daystart,
+            CASESENSITIVE => 0,
+            @rest
+        );
+        $sb->_SQLLimit(
+            ALIAS           => $sb->{_sql_transalias},
+            FIELD           => 'Created',
+            OPERATOR        => "<=",
+            VALUE           => $dayend,
+            CASESENSITIVE   => 0,
+            @rest,
+            ENTRYAGGREGATOR => 'AND',
+        );
 
-  # See the comments for TransLimit, they apply here too
+    }
 
-  $sb->{_sql_transalias} = $sb->NewAlias ('Transactions')
-    unless defined $sb->{_sql_transalias};
-  $sb->{_sql_trattachalias} = $sb->NewAlias ('Attachments')
-    unless defined $sb->{_sql_trattachalias};
+    # not searching for a single day
+    else {
 
-  $sb->_OpenParen;
+        #Search for the right field
+        $sb->_SQLLimit(
+            ALIAS         => $sb->{_sql_transalias},
+            FIELD         => 'Created',
+            OPERATOR      => $op,
+            VALUE         => $value,
+            CASESENSITIVE => 0,
+            @rest
+        );
+    }
 
-  # Join Transactions To Attachments
-  $sb->Join( ALIAS1 => $sb->{_sql_trattachalias}, FIELD1 => 'TransactionId',
-            ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'id');
+    # Join Transactions To Attachments
 
-  # Join Transactions to Tickets
-  $sb->Join( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'}, # UGH!
-            ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'Ticket');
+    $sb->_SQLJoin(
+        ALIAS1 => $sb->{_sql_trattachalias},
+        FIELD1 => 'TransactionId',
+        ALIAS2 => $sb->{_sql_transalias},
+        FIELD2 => 'id',
+    );
 
-  my $d = new RT::Date( $sb->CurrentUser );
-  $d->Set($value);
-  $value = $d->ISO;
+    # Join Transactions to Tickets
+    $sb->_SQLJoin(
+        ALIAS1 => 'main',
+        FIELD1 => $sb->{'primary_key'},     # UGH!
+        ALIAS2 => $sb->{_sql_transalias},
+        FIELD2 => 'ObjectId'
+    );
 
-  #Search for the right field
-  $sb->_SQLLimit(ALIAS => $sb->{_sql_trattachalias},
-                FIELD =>    'Created',
-                OPERATOR => $op,
-                VALUE =>    $value,
-                CASESENSITIVE => 0,
-                @rest
-               );
+    $sb->SUPER::Limit(
+        ALIAS => $sb->{_sql_transalias},
+        FIELD => 'ObjectType',
+        VALUE => 'RT::Ticket'
+    );
 
-  $sb->_CloseParen;
+    $sb->_CloseParen;
 }
 
 =head2 _TransLimit
@@ -450,65 +630,81 @@ Meta Data:
 =cut
 
 sub _TransLimit {
-  # Content, ContentType, Filename
 
-  # If only this was this simple.  We've got to do something
-  # complicated here:
+    # Content, ContentType, Filename
 
-            #Basically, we want to make sure that the limits apply to
-            #the same attachment, rather than just another attachment
-            #for the same ticket, no matter how many clauses we lump
-            #on. We put them in TicketAliases so that they get nuked
-            #when we redo the join.
+    # If only this was this simple.  We've got to do something
+    # complicated here:
 
-  # In the SQL, we might have
-  #       (( Content = foo ) or ( Content = bar AND Content = baz ))
-  # The AND group should share the same Alias.
+    #Basically, we want to make sure that the limits apply to
+    #the same attachment, rather than just another attachment
+    #for the same ticket, no matter how many clauses we lump
+    #on. We put them in TicketAliases so that they get nuked
+    #when we redo the join.
 
-  # Actually, maybe it doesn't matter.  We use the same alias and it
-  # works itself out? (er.. different.)
+    # In the SQL, we might have
+    #       (( Content = foo ) or ( Content = bar AND Content = baz ))
+    # The AND group should share the same Alias.
 
-  # Steal more from _ProcessRestrictions
+    # Actually, maybe it doesn't matter.  We use the same alias and it
+    # works itself out? (er.. different.)
 
-  # FIXME: Maybe look at the previous FooLimit call, and if it was a
-  # TransLimit and EntryAggregator == AND, reuse the Aliases?
+    # Steal more from _ProcessRestrictions
 
-  # Or better - store the aliases on a per subclause basis - since
-  # those are going to be the things we want to relate to each other,
-  # anyway.
+    # FIXME: Maybe look at the previous FooLimit call, and if it was a
+    # TransLimit and EntryAggregator == AND, reuse the Aliases?
 
-  # maybe we should not allow certain kinds of aggregation of these
-  # clauses and do a psuedo regex instead? - the problem is getting
-  # 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.
+    # Or better - store the aliases on a per subclause basis - since
+    # those are going to be the things we want to relate to each other,
+    # anyway.
 
-  my ($sb,$field,$op,$value,@rest) = @_;
+    # maybe we should not allow certain kinds of aggregation of these
+    # clauses and do a psuedo regex instead? - the problem is getting
+    # 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.
 
-  $sb->{_sql_transalias} = $sb->NewAlias ('Transactions')
-    unless defined $sb->{_sql_transalias};
-  $sb->{_sql_trattachalias} = $sb->NewAlias ('Attachments')
-    unless defined $sb->{_sql_trattachalias};
+    my ( $self, $field, $op, $value, @rest ) = @_;
 
-  $sb->_OpenParen;
+    $self->{_sql_transalias} = $self->NewAlias('Transactions')
+      unless defined $self->{_sql_transalias};
+    $self->{_sql_trattachalias} = $self->NewAlias('Attachments')
+      unless defined $self->{_sql_trattachalias};
 
-  # Join Transactions To Attachments
-  $sb->Join( ALIAS1 => $sb->{_sql_trattachalias}, FIELD1 => 'TransactionId',
-            ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'id');
+    $self->_OpenParen;
 
-  # Join Transactions to Tickets
-  $sb->Join( ALIAS1 => 'main', FIELD1 => $sb->{'primary_key'}, # UGH!
-            ALIAS2 => $sb->{_sql_transalias}, FIELD2 => 'Ticket');
+    #Search for the right field
+    $self->_SQLLimit(
+        ALIAS         => $self->{_sql_trattachalias},
+        FIELD         => $field,
+        OPERATOR      => $op,
+        VALUE         => $value,
+        CASESENSITIVE => 0,
+        @rest
+    );
 
-  #Search for the right field
-  $sb->_SQLLimit(ALIAS => $sb->{_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'
+    );
 
-  $sb->_CloseParen;
+    # 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'
+    );
+
+    $self->_CloseParen;
 
 }
 
@@ -519,134 +715,376 @@ Handle watcher limits.  (Requestor, CC, etc..)
 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
 
 sub _WatcherLimit {
-  my ($self,$field,$op,$value,@rest) = @_;
-  my %rest = @rest;
+    my $self  = shift;
+    my $field = shift;
+    my $op    = shift;
+    my $value = shift;
+    my %rest  = (@_);
 
-  $self->_OpenParen;
+    $self->_OpenParen;
 
-  my $groups       = $self->NewAlias('Groups');
-  my $group_princs  = $self->NewAlias('Principals');
-  my $groupmembers  = $self->NewAlias('CachedGroupMembers');
-  my $member_princs = $self->NewAlias('Principals');
-  my $users        = $self->NewAlias('Users');
+    # Find out what sort of watcher we're looking for
+    my $fieldname;
+    if ( ref $field ) {
+        $fieldname = $field->[0]->[0];
+    }
+    else {
+        $fieldname = $field;
+    }
+    my $meta = $FIELDS{$fieldname};
+    my $type = ( defined $meta->[1] ? $meta->[1] : undef );
+
+# We only want _one_ clause for all of requestors, cc, admincc
+# It's less flexible than what we used to do, but now it sort of actually works. (no huge cartesian products that hose the db)
+    my $groups = $self->{ 'watcherlimit_' . ('global') . "_groups" } ||=
+      $self->NewAlias('Groups');
+    my $groupmembers =
+      $self->{ 'watcherlimit_' . ('global') . "_groupmembers" } ||=
+      $self->NewAlias('CachedGroupMembers');
+    my $users = $self->{ 'watcherlimit_' . ('global') . "_users" } ||=
+      $self->NewAlias('Users');
+
+# Use regular joins instead of SQL joins since we don't want the joins inside ticketsql or we get a huge cartesian product
+    $self->SUPER::Limit(
+        ALIAS           => $groups,
+        FIELD           => 'Domain',
+        VALUE           => 'RT::Ticket-Role',
+        ENTRYAGGREGATOR => 'AND'
+    );
+    $self->Join(
+        ALIAS1 => $groups,
+        FIELD1 => 'Instance',
+        ALIAS2 => 'main',
+        FIELD2 => 'id'
+    );
+    $self->Join(
+        ALIAS1 => $groups,
+        FIELD1 => 'id',
+        ALIAS2 => $groupmembers,
+        FIELD2 => 'GroupId'
+    );
+    $self->Join(
+        ALIAS1 => $groupmembers,
+        FIELD1 => 'MemberId',
+        ALIAS2 => $users,
+        FIELD2 => 'id'
+    );
+
+    # 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
+    if ( ref $field ) {    # gross hack
+        $self->_OpenParen;
+        for my $chunk (@$field) {
+            ( $field, $op, $value, %rest ) = @$chunk;
+            $self->_SQLLimit(
+                ALIAS         => $users,
+                FIELD         => $rest{SUBKEY} || 'EmailAddress',
+                VALUE         => $value,
+                OPERATOR      => $op,
+                CASESENSITIVE => 0,
+                %rest
+            );
+        }
+        $self->_CloseParen;
+    }
+    else {
+        $self->_SQLLimit(
+            ALIAS         => $users,
+            FIELD         => $rest{SUBKEY} || 'EmailAddress',
+            VALUE         => $value,
+            OPERATOR      => $op,
+            CASESENSITIVE => 0,
+            %rest
+        );
+    }
 
+    $self->_SQLLimit(
+        ALIAS           => $groups,
+        FIELD           => 'Type',
+        VALUE           => $type,
+        ENTRYAGGREGATOR => 'AND'
+      )
+      if ($type);
 
-  #Find user watchers
-#  my $subclause = undef;
-#  my $aggregator = 'OR';
-#  if ($restriction->{'OPERATOR'} =~ /!|NOT/i ){
-#    $subclause = 'AndEmailIsNot';
-#    $aggregator = 'AND';
-#  }
+    $self->_CloseParen;
+}
 
+=head2 _WatcherMembershipLimit
 
-  $self->_SQLLimit(ALIAS => $users,
-                  FIELD => $rest{SUBKEY} || 'EmailAddress',
-                  VALUE           => $value,
-                  OPERATOR        => $op,
-                  CASESENSITIVE   => 0,
-                  @rest,
-                 );
+Handle watcher membership limits, i.e. whether the watcher belongs to a
+specific group or not.
 
-  # {{{ Tie to groups for tickets we care about
-  $self->_SQLLimit(ALIAS => $groups,
-                  FIELD => 'Domain',
-                  VALUE => 'RT::Ticket-Role',
-                  ENTRYAGGREGATOR => 'AND');
+Meta Data:
+  1: Field to query on
 
-  $self->Join(ALIAS1 => $groups, FIELD1 => 'Instance',
-             ALIAS2 => 'main',   FIELD2 => 'id');
-  # }}}
+SELECT DISTINCT main.*
+FROM
+    Tickets main,
+    Groups Groups_1,
+    CachedGroupMembers CachedGroupMembers_2,
+    Users Users_3
+WHERE (
+    (main.EffectiveId = main.id)
+) AND (
+    (main.Status != 'deleted')
+) AND (
+    (main.Type = 'ticket')
+) AND (
+    (
+       (Users_3.EmailAddress = '22')
+           AND
+       (Groups_1.Domain = 'RT::Ticket-Role')
+           AND
+       (Groups_1.Type = 'RequestorGroup')
+    )
+) AND
+    Groups_1.Instance = main.id
+AND
+    Groups_1.id = CachedGroupMembers_2.GroupId
+AND
+    CachedGroupMembers_2.MemberId = Users_3.id
+ORDER BY main.id ASC
+LIMIT 25
 
-  # If we care about which sort of watcher
-  my $meta = $FIELDS{$field};
-  my $type = ( defined $meta->[1] ? $meta->[1] : undef );
+=cut
 
-  if ( $type ) {
-    $self->_SQLLimit(ALIAS => $groups,
-                    FIELD => 'Type',
-                    VALUE => $type,
-                    ENTRYAGGREGATOR => 'AND');
-  }
+sub _WatcherMembershipLimit {
+    my ( $self, $field, $op, $value, @rest ) = @_;
+    my %rest = @rest;
+
+    $self->_OpenParen;
+
+    my $groups       = $self->NewAlias('Groups');
+    my $groupmembers = $self->NewAlias('CachedGroupMembers');
+    my $users        = $self->NewAlias('Users');
+    my $memberships  = $self->NewAlias('CachedGroupMembers');
+
+    if ( ref $field ) {    # gross hack
+        my @bundle = @$field;
+        $self->_OpenParen;
+        for my $chunk (@bundle) {
+            ( $field, $op, $value, @rest ) = @$chunk;
+            $self->_SQLLimit(
+                ALIAS    => $memberships,
+                FIELD    => 'GroupId',
+                VALUE    => $value,
+                OPERATOR => $op,
+                @rest,
+            );
+        }
+        $self->_CloseParen;
+    }
+    else {
+        $self->_SQLLimit(
+            ALIAS    => $memberships,
+            FIELD    => 'GroupId',
+            VALUE    => $value,
+            OPERATOR => $op,
+            @rest,
+        );
+    }
 
-  $self->Join (ALIAS1 => $groups,  FIELD1 => 'id',
-              ALIAS2 => $group_princs, FIELD2 => 'ObjectId');
-  $self->_SQLLimit(ALIAS => $group_princs,
-                  FIELD => 'PrincipalType',
-                  VALUE => 'Group',
-                  ENTRYAGGREGATOR => 'AND');
-  $self->Join( ALIAS1 => $group_princs, FIELD1 => 'id',
-              ALIAS2 => $groupmembers, FIELD2 => 'GroupId');
+    # {{{ Tie to groups for tickets we care about
+    $self->_SQLLimit(
+        ALIAS           => $groups,
+        FIELD           => 'Domain',
+        VALUE           => 'RT::Ticket-Role',
+        ENTRYAGGREGATOR => 'AND'
+    );
+
+    $self->Join(
+        ALIAS1 => $groups,
+        FIELD1 => 'Instance',
+        ALIAS2 => 'main',
+        FIELD2 => 'id'
+    );
+
+    # }}}
+
+    # If we care about which sort of watcher
+    my $meta = $FIELDS{$field};
+    my $type = ( defined $meta->[1] ? $meta->[1] : undef );
+
+    if ($type) {
+        $self->_SQLLimit(
+            ALIAS           => $groups,
+            FIELD           => 'Type',
+            VALUE           => $type,
+            ENTRYAGGREGATOR => 'AND'
+        );
+    }
 
-  $self->Join( ALIAS1 => $groupmembers, FIELD1 => 'MemberId',
-              ALIAS2 => $member_princs, FIELD2 => 'id');
-  $self->Join (ALIAS1 => $member_princs, FIELD1 => 'ObjectId',
-              ALIAS2 => $users, FIELD2 => 'id');
+    $self->Join(
+        ALIAS1 => $groups,
+        FIELD1 => 'id',
+        ALIAS2 => $groupmembers,
+        FIELD2 => 'GroupId'
+    );
 
- $self->_CloseParen;
+    $self->Join(
+        ALIAS1 => $groupmembers,
+        FIELD1 => 'MemberId',
+        ALIAS2 => $users,
+        FIELD2 => 'id'
+    );
+
+    $self->Join(
+        ALIAS1 => $memberships,
+        FIELD1 => 'MemberId',
+        ALIAS2 => $users,
+        FIELD2 => 'id'
+    );
+
+    $self->_CloseParen;
 
 }
 
 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'} );
-  }
-
-   #If we're trying to limit it to things that are target of
-  if ($restriction->{'TARGET'}) {
-    # 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";
-    } else {
-      $matchfield = "Target";
+    my $restriction;
+    my $self;
+    my $LinkAlias;
+    my %args;
+    if ( $restriction->{'TYPE'} ) {
+        $self->SUPER::Limit(
+            ALIAS           => $LinkAlias,
+            ENTRYAGGREGATOR => 'AND',
+            FIELD           => 'Type',
+            OPERATOR        => '=',
+            VALUE           => $restriction->{'TYPE'}
+        );
     }
-    $self->SUPER::Limit(ALIAS => $LinkAlias,
-                       ENTRYAGGREGATOR => 'AND',
-                       FIELD =>   $matchfield,
-                       OPERATOR => '=',
-                       VALUE =>    $restriction->{'TARGET'} );
-    #If we're searching on target, join the base to ticket.id
-    $self->Join( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'},
-                ALIAS2 => $LinkAlias,
-                FIELD2 => 'LocalBase');
-  }
-  #If we're trying to limit it to things that are base of
-  elsif ($restriction->{'BASE'}) {
-    # 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";
+
+    #If we're trying to limit it to things that are target of
+    if ( $restriction->{'TARGET'} ) {
+
+        # 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";
+        }
+        else {
+            $matchfield = "Target";
+        }
+        $self->SUPER::Limit(
+            ALIAS           => $LinkAlias,
+            ENTRYAGGREGATOR => 'AND',
+            FIELD           => $matchfield,
+            OPERATOR        => '=',
+            VALUE           => $restriction->{'TARGET'}
+        );
+
+        #If we're searching on target, join the base to ticket.id
+        $self->_SQLJoin(
+            ALIAS1 => 'main',
+            FIELD1 => $self->{'primary_key'},
+            ALIAS2 => $LinkAlias,
+            FIELD2 => 'LocalBase'
+        );
     }
 
-    $self->SUPER::Limit(ALIAS => $LinkAlias,
-                       ENTRYAGGREGATOR => 'AND',
-                       FIELD => $matchfield,
-                       OPERATOR => '=',
-                       VALUE =>    $restriction->{'BASE'} );
-    #If we're searching on base, join the target to ticket.id
-    $self->Join( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'},
-                ALIAS2 => $LinkAlias,
-                FIELD2 => 'LocalTarget')
-  }
-}
+    #If we're trying to limit it to things that are base of
+    elsif ( $restriction->{'BASE'} ) {
 
+        # 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";
+        }
+
+        $self->SUPER::Limit(
+            ALIAS           => $LinkAlias,
+            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'
+        );
+    }
+}
 
 =head2 KeywordLimit
 
@@ -658,91 +1096,136 @@ Meta Data:
 =cut
 
 sub _CustomFieldLimit {
-  my ($self,$_field,$op,$value,@rest) = @_;
-
-  my %rest = @rest;
-  my $field = $rest{SUBKEY} || die "No field specified";
-
-  # For our sanity, we can only limit on one queue at a time
-  my $queue = undef;
-  # Ugh.    This will not do well for things with underscores in them
-
-  use RT::CustomFields;
-  my $CF = RT::CustomFields->new( $self->CurrentUser );
-  #$CF->Load( $cfid} );
-
-  my $q;
-  if ($field =~ /^(.+?)\.{(.+)}$/) {
-    my $q = RT::Queue->new($self->CurrentUser);
-    $q->Load($1);
-    $field = $2;
-    $CF->LimitToQueue( $q->Id );
-    $queue = $q->Id;
-  } else {
-    $CF->LimitToGlobal;
-  }
-  $CF->FindAllRows;
-
-  my $cfid = 0;
-
-  while ( my $CustomField = $CF->Next ) {
-    if ($CustomField->Name eq $field) {
-      $cfid = $CustomField->Id;
-      last;
+    my ( $self, $_field, $op, $value, @rest ) = @_;
+
+    my %rest  = @rest;
+    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;
     }
-  }
-  die "No custom field named $field found\n"
-    unless $cfid;
+    $field = $1 if $field =~ /^{(.+)}$/;    # trim { }
 
-#   use RT::CustomFields;
-#   my $CF = RT::CustomField->new( $self->CurrentUser );
-#   $CF->Load( $cfid );
 
+# 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;
-  my $TicketCFs = $self->Join( TYPE   => 'left',
-                              ALIAS1 => 'main',
-                              FIELD1 => 'id',
-                              TABLE2 => 'TicketCustomFieldValues',
-                              FIELD2 => 'Ticket' );
+    my $null_columns_ok;
+    if ( ( $op =~ /^NOT LIKE$/i ) or ( $op eq '!=' ) ) {
+        $null_columns_ok = 1;
+    }
 
-  $self->_OpenParen;
+    my $cfid = 0;
+    if ($queue) {
 
-  $self->_SQLLimit( ALIAS      => $TicketCFs,
-                   FIELD      => 'Content',
-                   OPERATOR   => $op,
-                   VALUE      => $value,
-                   QUOTEVALUE => 1,
-                   @rest );
+        my $q = RT::Queue->new( $self->CurrentUser );
+        $q->Load($queue) if ($queue);
 
-  if (   $op =~ /^IS$/i
-        or ( $op eq '!=' ) ) {
-    $null_columns_ok = 1;
-  }
+        my $cf;
+        if ( $q->id ) {
+            $cf = $q->CustomField($field);
+        }
+        else {
+            $cf = RT::CustomField->new( $self->CurrentUser );
+            $cf->LoadByNameAndQueue( Queue => '0', Name => $field );
+        }
 
-  #If we're trying to find tickets where the keyword isn't somethng,
-  #also check ones where it _IS_ null
+        $cfid = $cf->id;
 
-  if ( $op eq '!=' ) {
-    $self->_SQLLimit( ALIAS           => $TicketCFs,
-                     FIELD           => 'Content',
-                     OPERATOR        => 'IS',
-                     VALUE           => 'NULL',
-                     QUOTEVALUE      => 0,
-                     ENTRYAGGREGATOR => 'OR', );
-  }
+    }
 
-  $self->_SQLLimit( LEFTJOIN => $TicketCFs,
-                   FIELD    => 'CustomField',
-                   VALUE    => $cfid,
-                   ENTRYAGGREGATOR => 'OR' );
+    my $TicketCFs;
+    my $cfkey = $cfid ? $cfid : "$queue.$field";
 
+    # Perform one Join per CustomField
+    if ( $self->{_sql_object_cf_alias}{$cfkey} ) {
+        $TicketCFs = $self->{_sql_object_cf_alias}{$cfkey};
+    }
+    else {
+        if ($cfid) {
+            $TicketCFs = $self->{_sql_object_cf_alias}{$cfkey} = $self->Join(
+                TYPE   => 'left',
+                ALIAS1 => 'main',
+                FIELD1 => 'id',
+                TABLE2 => 'ObjectCustomFieldValues',
+                FIELD2 => 'ObjectId',
+            );
+            $self->SUPER::Limit(
+                LEFTJOIN        => $TicketCFs,
+                FIELD           => 'CustomField',
+                VALUE           => $cfid,
+                ENTRYAGGREGATOR => 'AND'
+            );
+        } 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',
+            );
+        }
+        $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->_OpenParen if ($null_columns_ok);
+
+    $self->_SQLLimit(
+        ALIAS      => $TicketCFs,
+        FIELD      => 'Content',
+        OPERATOR   => $op,
+        VALUE      => $value,
+        QUOTEVALUE => 1,
+        @rest
+    );
+
+    if ($null_columns_ok) {
+        $self->_SQLLimit(
+            ALIAS           => $TicketCFs,
+            FIELD           => 'Content',
+            OPERATOR        => 'IS',
+            VALUE           => 'NULL',
+            QUOTEVALUE      => 0,
+            ENTRYAGGREGATOR => 'OR',
+        );
+    }
+    $self->_CloseParen if ($null_columns_ok);
 
-  $self->_CloseParen;
 
-}
 
+}
 
 # End Helper Functions
 
@@ -758,33 +1241,35 @@ Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
 Generally best called from LimitFoo methods
 
 =cut
+
 sub Limit {
     my $self = shift;
-    my %args = ( FIELD => undef,
-                OPERATOR => '=',
-                VALUE => undef,
-                DESCRIPTION => undef,
-                @_
-              );
-    $args{'DESCRIPTION'} = $self->loc(
-       "[_1] [_2] [_3]", $args{'FIELD'}, $args{'OPERATOR'}, $args{'VALUE'}
-    ) if (!defined $args{'DESCRIPTION'}) ;
+    my %args = (
+        FIELD       => undef,
+        OPERATOR    => '=',
+        VALUE       => undef,
+        DESCRIPTION => undef,
+        @_
+    );
+    $args{'DESCRIPTION'} = $self->loc( "[_1] [_2] [_3]",
+        $args{'FIELD'}, $args{'OPERATOR'}, $args{'VALUE'} )
+      if ( !defined $args{'DESCRIPTION'} );
 
     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;
+    %{ $self->{'TicketRestrictions'}{$index} } = %args;
 
     $self->{'RecalcTicketLimits'} = 1;
 
-    # If we're looking at the effective id, we don't want to append the other clause
-    # which limits us to tickets where id = effective id
-    if ($args{'FIELD'} eq 'EffectiveId') {
+# If we're looking at the effective id, we don't want to append the other clause
+# which limits us to tickets where id = effective id
+    if ( $args{'FIELD'} eq 'EffectiveId' ) {
         $self->{'looking_at_effective_id'} = 1;
     }
 
-    if ($args{'FIELD'} eq 'Type') {
+    if ( $args{'FIELD'} eq 'Type' ) {
         $self->{'looking_at_type'} = 1;
     }
 
@@ -793,22 +1278,25 @@ sub Limit {
 
 # }}}
 
-
-
-
 =head2 FreezeLimits
 
 Returns a frozen string suitable for handing back to ThawLimits.
 
 =cut
+
+sub _FreezeThawKeys {
+    'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
+      'looking_at_type';
+}
+
 # {{{ sub FreezeLimits
 
 sub FreezeLimits {
-       my $self = shift;
-       require FreezeThaw;
-       return (FreezeThaw::freeze($self->{'TicketRestrictions'},
-                                  $self->{'restriction_index'}
-                                 ));
+    my $self = shift;
+    require Storable;
+    require MIME::Base64;
+    MIME::Base64::base64_encode(
+        Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
 }
 
 # }}}
@@ -819,26 +1307,26 @@ Take a frozen Limits string generated by FreezeLimits and make this tickets
 object have that set of limits.
 
 =cut
+
 # {{{ sub ThawLimits
 
 sub ThawLimits {
-       my $self = shift;
-       my $in = shift;
-       
-       #if we don't have $in, get outta here.
-       return undef unless ($in);
+    my $self = shift;
+    my $in   = shift;
+
+    #if we don't have $in, get outta here.
+    return undef unless ($in);
 
-       $self->{'RecalcTicketLimits'} = 1;
+    $self->{'RecalcTicketLimits'} = 1;
+
+    require Storable;
+    require MIME::Base64;
 
-       require FreezeThaw;
-       
-       #We don't need to die if the thaw fails.
-       
-       eval {
-               ($self->{'TicketRestrictions'},
-               $self->{'restriction_index'}
-               ) = FreezeThaw::thaw($in);
-       }
+    #We don't need to die if the thaw fails.
+    @{$self}{ $self->_FreezeThawKeys } =
+      eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
+
+    $RT::Logger->error($@) if $@;
 
 }
 
@@ -859,16 +1347,18 @@ VALUE is a queue id or Name.
 
 sub LimitQueue {
     my $self = shift;
-    my %args = (VALUE => undef,
-               OPERATOR => '=',
-               @_);
+    my %args = (
+        VALUE    => undef,
+        OPERATOR => '=',
+        @_
+    );
 
     #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+$/) {
-      my $queue = new RT::Queue($self->CurrentUser);
-      $queue->Load($args{'VALUE'});
-      $args{VALUE} = $queue->Name;
+    if ( $args{VALUE} =~ /^\d+$/ ) {
+        my $queue = new RT::Queue( $self->CurrentUser );
+        $queue->Load( $args{'VALUE'} );
+        $args{VALUE} = $queue->Name;
     }
 
     # What if they pass in an Id?  Check for isNum() and convert to
@@ -876,15 +1366,16 @@ sub LimitQueue {
 
     #TODO check for a valid queue here
 
-    $self->Limit (FIELD => 'Queue',
-                 VALUE => $args{VALUE},
-                 OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => join(
-                  ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{VALUE},
-                 ),
-                );
+    $self->Limit(
+        FIELD       => 'Queue',
+        VALUE       => $args{VALUE},
+        OPERATOR    => $args{'OPERATOR'},
+        DESCRIPTION =>
+          join( ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{VALUE}, ),
+    );
 
 }
+
 # }}}
 
 # {{{ sub LimitStatus
@@ -895,19 +1386,27 @@ Takes a paramhash with the fields OPERATOR and VALUE.
 OPERATOR is one of = or !=.
 VALUE is a status.
 
+RT adds Status != 'deleted' until object has
+allow_deleted_search internal property set.
+$tickets->{'allow_deleted_search'} = 1;
+$tickets->LimitStatus( VALUE => 'deleted' );
+
 =cut
 
 sub LimitStatus {
     my $self = shift;
-    my %args = ( OPERATOR => '=',
-                  @_);
-    $self->Limit (FIELD => 'Status',
-                 VALUE => $args{'VALUE'},
-                 OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => join(
-                  ' ', $self->loc('Status'), $args{'OPERATOR'}, $self->loc($args{'VALUE'})
-                 ),
-                );
+    my %args = (
+        OPERATOR => '=',
+        @_
+    );
+    $self->Limit(
+        FIELD       => 'Status',
+        VALUE       => $args{'VALUE'},
+        OPERATOR    => $args{'OPERATOR'},
+        DESCRIPTION => join( ' ',
+            $self->loc('Status'), $args{'OPERATOR'},
+            $self->loc( $args{'VALUE'} ) ),
+    );
 }
 
 # }}}
@@ -949,16 +1448,18 @@ VALUE is a string to search for in the type of the ticket.
 
 sub LimitType {
     my $self = shift;
-    my %args = (OPERATOR => '=',
-               VALUE => undef,
-               @_);
-    $self->Limit (FIELD => 'Type',
-                  VALUE => $args{'VALUE'},
-                  OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => join(
-                  ' ', $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'},
-                 ),
-                 );
+    my %args = (
+        OPERATOR => '=',
+        VALUE    => undef,
+        @_
+    );
+    $self->Limit(
+        FIELD       => 'Type',
+        VALUE       => $args{'VALUE'},
+        OPERATOR    => $args{'OPERATOR'},
+        DESCRIPTION =>
+          join( ' ', $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
+    );
 }
 
 # }}}
@@ -980,13 +1481,14 @@ VALUE is a string to search for in the subject of the ticket.
 sub LimitSubject {
     my $self = shift;
     my %args = (@_);
-    $self->Limit (FIELD => 'Subject',
-                 VALUE => $args{'VALUE'},
-                 OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => join(
-                  ' ', $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'},
-                 ),
-                );
+    $self->Limit(
+        FIELD       => 'Subject',
+        VALUE       => $args{'VALUE'},
+        OPERATOR    => $args{'OPERATOR'},
+        DESCRIPTION => join(
+            ' ', $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'},
+        ),
+    );
 }
 
 # }}}
@@ -1008,16 +1510,18 @@ VALUE is a ticket Id to search for
 
 sub LimitId {
     my $self = shift;
-    my %args = (OPERATOR => '=',
-                @_);
+    my %args = (
+        OPERATOR => '=',
+        @_
+    );
 
-    $self->Limit (FIELD => 'id',
-                  VALUE => $args{'VALUE'},
-                  OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => join(
-                  ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'},
-                 ),
-                 );
+    $self->Limit(
+        FIELD       => 'id',
+        VALUE       => $args{'VALUE'},
+        OPERATOR    => $args{'OPERATOR'},
+        DESCRIPTION =>
+          join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
+    );
 }
 
 # }}}
@@ -1035,13 +1539,14 @@ VALUE is a value to match the ticket\'s priority against
 sub LimitPriority {
     my $self = shift;
     my %args = (@_);
-    $self->Limit (FIELD => 'Priority',
-                 VALUE => $args{'VALUE'},
-                 OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => join(
-                  ' ', $self->loc('Priority'), $args{'OPERATOR'}, $args{'VALUE'},
-                 ),
-                );
+    $self->Limit(
+        FIELD       => 'Priority',
+        VALUE       => $args{'VALUE'},
+        OPERATOR    => $args{'OPERATOR'},
+        DESCRIPTION => join( ' ',
+            $self->loc('Priority'),
+            $args{'OPERATOR'}, $args{'VALUE'}, ),
+    );
 }
 
 # }}}
@@ -1060,13 +1565,14 @@ VALUE is a value to match the ticket\'s initial priority against
 sub LimitInitialPriority {
     my $self = shift;
     my %args = (@_);
-    $self->Limit (FIELD => 'InitialPriority',
-                 VALUE => $args{'VALUE'},
-                 OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => join(
-                  ' ', $self->loc('Initial Priority'), $args{'OPERATOR'}, $args{'VALUE'},
-                 ),
-                );
+    $self->Limit(
+        FIELD       => 'InitialPriority',
+        VALUE       => $args{'VALUE'},
+        OPERATOR    => $args{'OPERATOR'},
+        DESCRIPTION => join( ' ',
+            $self->loc('Initial Priority'), $args{'OPERATOR'},
+            $args{'VALUE'}, ),
+    );
 }
 
 # }}}
@@ -1084,13 +1590,14 @@ VALUE is a value to match the ticket\'s final priority against
 sub LimitFinalPriority {
     my $self = shift;
     my %args = (@_);
-    $self->Limit (FIELD => 'FinalPriority',
-                 VALUE => $args{'VALUE'},
-                 OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => join(
-                  ' ', $self->loc('Final Priority'), $args{'OPERATOR'}, $args{'VALUE'},
-                 ),
-                );
+    $self->Limit(
+        FIELD       => 'FinalPriority',
+        VALUE       => $args{'VALUE'},
+        OPERATOR    => $args{'OPERATOR'},
+        DESCRIPTION => join( ' ',
+            $self->loc('Final Priority'),
+            $args{'OPERATOR'}, $args{'VALUE'}, ),
+    );
 }
 
 # }}}
@@ -1108,13 +1615,14 @@ VALUE is a value to match the ticket's TimeWorked attribute
 sub LimitTimeWorked {
     my $self = shift;
     my %args = (@_);
-    $self->Limit (FIELD => 'TimeWorked',
-                 VALUE => $args{'VALUE'},
-                 OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => join(
-                  ' ', $self->loc('Time worked'), $args{'OPERATOR'}, $args{'VALUE'},
-                 ),
-                );
+    $self->Limit(
+        FIELD       => 'TimeWorked',
+        VALUE       => $args{'VALUE'},
+        OPERATOR    => $args{'OPERATOR'},
+        DESCRIPTION => join( ' ',
+            $self->loc('Time worked'),
+            $args{'OPERATOR'}, $args{'VALUE'}, ),
+    );
 }
 
 # }}}
@@ -1132,13 +1640,14 @@ VALUE is a value to match the ticket's TimeLeft attribute
 sub LimitTimeLeft {
     my $self = shift;
     my %args = (@_);
-    $self->Limit (FIELD => 'TimeLeft',
-                 VALUE => $args{'VALUE'},
-                 OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => join(
-                  ' ', $self->loc('Time left'), $args{'OPERATOR'}, $args{'VALUE'},
-                 ),
-                );
+    $self->Limit(
+        FIELD       => 'TimeLeft',
+        VALUE       => $args{'VALUE'},
+        OPERATOR    => $args{'OPERATOR'},
+        DESCRIPTION => join( ' ',
+            $self->loc('Time left'),
+            $args{'OPERATOR'}, $args{'VALUE'}, ),
+    );
 }
 
 # }}}
@@ -1156,16 +1665,18 @@ OPERATOR is one of =, LIKE, NOT LIKE or !=.
 VALUE is a string to search for in the body of the ticket
 
 =cut
+
 sub LimitContent {
     my $self = shift;
     my %args = (@_);
-    $self->Limit (FIELD => 'Content',
-                 VALUE => $args{'VALUE'},
-                 OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => join(
-                  ' ', $self->loc('Ticket content'), $args{'OPERATOR'}, $args{'VALUE'},
-                 ),
-                );
+    $self->Limit(
+        FIELD       => 'Content',
+        VALUE       => $args{'VALUE'},
+        OPERATOR    => $args{'OPERATOR'},
+        DESCRIPTION => join( ' ',
+            $self->loc('Ticket content'),
+            $args{'OPERATOR'}, $args{'VALUE'}, ),
+    );
 }
 
 # }}}
@@ -1179,16 +1690,18 @@ OPERATOR is one of =, LIKE, NOT LIKE or !=.
 VALUE is a string to search for in the body of the ticket
 
 =cut
+
 sub LimitFilename {
     my $self = shift;
     my %args = (@_);
-    $self->Limit (FIELD => 'Filename',
-                 VALUE => $args{'VALUE'},
-                 OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => join(
-                  ' ', $self->loc('Attachment filename'), $args{'OPERATOR'}, $args{'VALUE'},
-                 ),
-                );
+    $self->Limit(
+        FIELD       => 'Filename',
+        VALUE       => $args{'VALUE'},
+        OPERATOR    => $args{'OPERATOR'},
+        DESCRIPTION => join( ' ',
+            $self->loc('Attachment filename'), $args{'OPERATOR'},
+            $args{'VALUE'}, ),
+    );
 }
 
 # }}}
@@ -1205,14 +1718,16 @@ VALUE is a content type to search ticket attachments for
 sub LimitContentType {
     my $self = shift;
     my %args = (@_);
-    $self->Limit (FIELD => 'ContentType',
-                 VALUE => $args{'VALUE'},
-                 OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => join(
-                  ' ', $self->loc('Ticket content type'), $args{'OPERATOR'}, $args{'VALUE'},
-                 ),
-                );
+    $self->Limit(
+        FIELD       => 'ContentType',
+        VALUE       => $args{'VALUE'},
+        OPERATOR    => $args{'OPERATOR'},
+        DESCRIPTION => join( ' ',
+            $self->loc('Ticket content type'), $args{'OPERATOR'},
+            $args{'VALUE'}, ),
+    );
 }
+
 # }}}
 
 # }}}
@@ -1231,19 +1746,22 @@ VALUE is a user id.
 
 sub LimitOwner {
     my $self = shift;
-    my %args = ( OPERATOR => '=',
-                 @_);
+    my %args = (
+        OPERATOR => '=',
+        @_
+    );
+
+    my $owner = new RT::User( $self->CurrentUser );
+    $owner->Load( $args{'VALUE'} );
 
-    my $owner = new RT::User($self->CurrentUser);
-    $owner->Load($args{'VALUE'});
     # FIXME: check for a valid $owner
-    $self->Limit (FIELD => 'Owner',
-                 VALUE => $args{'VALUE'},
-                 OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => join(
-                  ' ', $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(),
-                 ),
-                );
+    $self->Limit(
+        FIELD       => 'Owner',
+        VALUE       => $args{'VALUE'},
+        OPERATOR    => $args{'OPERATOR'},
+        DESCRIPTION =>
+          join( ' ', $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
+    );
 
 }
 
@@ -1253,7 +1771,6 @@ sub LimitOwner {
 
 # {{{ sub LimitWatcher
 
-
 =head2 LimitWatcher
 
   Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
@@ -1272,44 +1789,46 @@ $t1->Create(Queue => 'general', Subject => "LimitWatchers test", Requestors => \
 
 sub LimitWatcher {
     my $self = shift;
-    my %args = ( OPERATOR => '=',
-                VALUE => undef,
-                TYPE => undef,
-               @_);
-
+    my %args = (
+        OPERATOR => '=',
+        VALUE    => undef,
+        TYPE     => undef,
+        @_
+    );
 
     #build us up a description
-    my ($watcher_type, $desc);
-    if ($args{'TYPE'}) {
-       $watcher_type = $args{'TYPE'};
+    my ( $watcher_type, $desc );
+    if ( $args{'TYPE'} ) {
+        $watcher_type = $args{'TYPE'};
     }
     else {
-       $watcher_type = "Watcher";
+        $watcher_type = "Watcher";
     }
 
-    $self->Limit (FIELD => $watcher_type,
-                 VALUE => $args{'VALUE'},
-                 OPERATOR => $args{'OPERATOR'},
-                 TYPE => $args{'TYPE'},
-                 DESCRIPTION => join(
-                  ' ', $self->loc($watcher_type), $args{'OPERATOR'}, $args{'VALUE'},
-                 ),
-                );
+    $self->Limit(
+        FIELD       => $watcher_type,
+        VALUE       => $args{'VALUE'},
+        OPERATOR    => $args{'OPERATOR'},
+        TYPE        => $args{'TYPE'},
+        DESCRIPTION => join( ' ',
+            $self->loc($watcher_type),
+            $args{'OPERATOR'}, $args{'VALUE'}, ),
+    );
 }
 
-
 sub LimitRequestor {
     my $self = shift;
     my %args = (@_);
-  my ($package, $filename, $line) = caller;
-    $RT::Logger->error("Tickets->LimitRequestor is deprecated. please rewrite call at  $package - $filename: $line");
-    $self->LimitWatcher(TYPE => 'Requestor', @_);
+    my ( $package, $filename, $line ) = caller;
+    $RT::Logger->error(
+"Tickets->LimitRequestor is deprecated. please rewrite call at  $package - $filename: $line"
+    );
+    $self->LimitWatcher( TYPE => 'Requestor', @_ );
 
 }
 
 # }}}
 
-
 # }}}
 
 # }}}
@@ -1321,7 +1840,7 @@ sub LimitRequestor {
 =head2 LimitLinkedTo
 
 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
-TYPE limits the sort of relationship we want to search on
+TYPE limits the sort of link we want to search on
 
 TYPE = { RefersTo, MemberOf, DependsOn }
 
@@ -1333,23 +1852,25 @@ TARGET is the id or URI of the TARGET of the link
 sub LimitLinkedTo {
     my $self = shift;
     my %args = (
-               TICKET => undef,
-               TARGET => undef,
-               TYPE => undef,
-                @_);
+        TICKET => undef,
+        TARGET => undef,
+        TYPE   => undef,
+        @_
+    );
 
     $self->Limit(
-                FIELD => 'LinkedTo',
-                BASE => undef,
-                TARGET => ($args{'TARGET'} || $args{'TICKET'}),
-                TYPE => $args{'TYPE'},
-                DESCRIPTION => $self->loc(
-                  "Tickets [_1] by [_2]", $self->loc($args{'TYPE'}), ($args{'TARGET'} || $args{'TICKET'})
-                 ),
-               );
+        FIELD       => 'LinkedTo',
+        BASE        => undef,
+        TARGET      => ( $args{'TARGET'} || $args{'TICKET'} ),
+        TYPE        => $args{'TYPE'},
+        DESCRIPTION => $self->loc(
+            "Tickets [_1] by [_2]",
+            $self->loc( $args{'TYPE'} ),
+            ( $args{'TARGET'} || $args{'TICKET'} )
+        ),
+    );
 }
 
-
 # }}}
 
 # {{{ LimitLinkedFrom
@@ -1357,7 +1878,7 @@ sub LimitLinkedTo {
 =head2 LimitLinkedFrom
 
 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
-TYPE limits the sort of relationship we want to search on
+TYPE limits the sort of link we want to search on
 
 
 BASE is the id or URI of the BASE of the link
@@ -1368,55 +1889,71 @@ BASE is the id or URI of the BASE of the link
 
 sub LimitLinkedFrom {
     my $self = shift;
-    my %args = ( BASE => undef,
-                TICKET => undef,
-                TYPE => undef,
-                @_);
+    my %args = (
+        BASE   => undef,
+        TICKET => undef,
+        TYPE   => undef,
+        @_
+    );
 
+    # translate RT2 From/To naming to RT3 TicketSQL naming
+    my %fromToMap = qw(DependsOn DependentOn
+      MemberOf  HasMember
+      RefersTo  ReferredToBy);
 
-    $self->Limit( FIELD => 'LinkedTo',
-                 TARGET => undef,
-                 BASE => ($args{'BASE'} || $args{'TICKET'}),
-                 TYPE => $args{'TYPE'},
-                 DESCRIPTION => $self->loc(
-                  "Tickets [_1] [_2]", $self->loc($args{'TYPE'}), ($args{'BASE'} || $args{'TICKET'})
-                 ),
-               );
-}
+    my $type = $args{'TYPE'};
+    $type = $fromToMap{$type} if exists( $fromToMap{$type} );
 
+    $self->Limit(
+        FIELD       => 'LinkedTo',
+        TARGET      => undef,
+        BASE        => ( $args{'BASE'} || $args{'TICKET'} ),
+        TYPE        => $type,
+        DESCRIPTION => $self->loc(
+            "Tickets [_1] [_2]",
+            $self->loc( $args{'TYPE'} ),
+            ( $args{'BASE'} || $args{'TICKET'} )
+        ),
+    );
+}
 
 # }}}
 
 # {{{ LimitMemberOf
 sub LimitMemberOf {
-    my $self = shift;
+    my $self      = shift;
     my $ticket_id = shift;
-    $self->LimitLinkedTo ( TARGET=> "$ticket_id",
-                          TYPE => 'MemberOf',
-                         );
+    $self->LimitLinkedTo(
+        TARGET => "$ticket_id",
+        TYPE   => 'MemberOf',
+    );
 
 }
+
 # }}}
 
 # {{{ LimitHasMember
 sub LimitHasMember {
-    my $self = shift;
-    my $ticket_id =shift;
-    $self->LimitLinkedFrom ( BASE => "$ticket_id",
-                            TYPE => 'HasMember',
-                            );
+    my $self      = shift;
+    my $ticket_id = shift;
+    $self->LimitLinkedFrom(
+        BASE => "$ticket_id",
+        TYPE => 'HasMember',
+    );
 
 }
+
 # }}}
 
 # {{{ LimitDependsOn
 
 sub LimitDependsOn {
-    my $self = shift;
+    my $self      = shift;
     my $ticket_id = shift;
-    $self->LimitLinkedTo ( TARGET => "$ticket_id",
-                           TYPE => 'DependsOn',
-                          );
+    $self->LimitLinkedTo(
+        TARGET => "$ticket_id",
+        TYPE   => 'DependsOn',
+    );
 
 }
 
@@ -1425,25 +1962,26 @@ sub LimitDependsOn {
 # {{{ LimitDependedOnBy
 
 sub LimitDependedOnBy {
-    my $self = shift;
+    my $self      = shift;
     my $ticket_id = shift;
-    $self->LimitLinkedFrom (  BASE => "$ticket_id",
-                               TYPE => 'DependentOn',
-                            );
+    $self->LimitLinkedFrom(
+        BASE => "$ticket_id",
+        TYPE => 'DependentOn',
+    );
 
 }
 
 # }}}
 
-
 # {{{ LimitRefersTo
 
 sub LimitRefersTo {
-    my $self = shift;
+    my $self      = shift;
     my $ticket_id = shift;
-    $self->LimitLinkedTo ( TARGET => "$ticket_id",
-                           TYPE => 'RefersTo',
-                          );
+    $self->LimitLinkedTo(
+        TARGET => "$ticket_id",
+        TYPE   => 'RefersTo',
+    );
 
 }
 
@@ -1452,11 +1990,12 @@ sub LimitRefersTo {
 # {{{ LimitReferredToBy
 
 sub LimitReferredToBy {
-    my $self = shift;
+    my $self      = shift;
     my $ticket_id = shift;
-    $self->LimitLinkedFrom (  BASE=> "$ticket_id",
-                               TYPE => 'ReferredTo',
-                            );
+    $self->LimitLinkedFrom(
+        BASE => "$ticket_id",
+        TYPE => 'ReferredToBy',
+    );
 
 }
 
@@ -1484,56 +2023,64 @@ the need to pass in a FIELD argument.
 sub LimitDate {
     my $self = shift;
     my %args = (
-                  FIELD => undef,
-                 VALUE => undef,
-                 OPERATOR => undef,
+        FIELD    => undef,
+        VALUE    => undef,
+        OPERATOR => undef,
 
-                  @_);
+        @_
+    );
 
     #Set the description if we didn't get handed it above
-    unless ($args{'DESCRIPTION'} ) {
-       $args{'DESCRIPTION'} = $args{'FIELD'} . " " .$args{'OPERATOR'}. " ". $args{'VALUE'} . " GMT"
+    unless ( $args{'DESCRIPTION'} ) {
+        $args{'DESCRIPTION'} =
+            $args{'FIELD'} . " "
+          . $args{'OPERATOR'} . " "
+          . $args{'VALUE'} . " GMT";
     }
 
-    $self->Limit (%args);
+    $self->Limit(%args);
 
 }
 
 # }}}
 
-
-
-
 sub LimitCreated {
     my $self = shift;
-    $self->LimitDate( FIELD => 'Created', @_);
+    $self->LimitDate( FIELD => 'Created', @_ );
 }
+
 sub LimitDue {
     my $self = shift;
-    $self->LimitDate( FIELD => 'Due', @_);
+    $self->LimitDate( FIELD => 'Due', @_ );
 
 }
+
 sub LimitStarts {
     my $self = shift;
-    $self->LimitDate( FIELD => 'Starts', @_);
+    $self->LimitDate( FIELD => 'Starts', @_ );
 
 }
+
 sub LimitStarted {
     my $self = shift;
-    $self->LimitDate( FIELD => 'Started', @_);
+    $self->LimitDate( FIELD => 'Started', @_ );
 }
+
 sub LimitResolved {
     my $self = shift;
-    $self->LimitDate( FIELD => 'Resolved', @_);
+    $self->LimitDate( FIELD => 'Resolved', @_ );
 }
+
 sub LimitTold {
     my $self = shift;
-    $self->LimitDate( FIELD => 'Told', @_);
+    $self->LimitDate( FIELD => 'Told', @_ );
 }
+
 sub LimitLastUpdated {
     my $self = shift;
-    $self->LimitDate( FIELD => 'LastUpdated', @_);
+    $self->LimitDate( FIELD => 'LastUpdated', @_ );
 }
+
 #
 # {{{ sub LimitTransactionDate
 
@@ -1550,21 +2097,25 @@ VALUE is a date and time in ISO format in GMT
 sub LimitTransactionDate {
     my $self = shift;
     my %args = (
-                  FIELD => 'TransactionDate',
-                 VALUE => undef,
-                 OPERATOR => undef,
+        FIELD    => 'TransactionDate',
+        VALUE    => undef,
+        OPERATOR => undef,
 
-                  @_);
+        @_
+    );
 
     #  <20021217042756.GK28744@pallas.fsck.com>
     #    "Kill It" - Jesse.
 
     #Set the description if we didn't get handed it above
-    unless ($args{'DESCRIPTION'} ) {
-       $args{'DESCRIPTION'} = $args{'FIELD'} . " " .$args{'OPERATOR'}. " ". $args{'VALUE'} . " GMT"
+    unless ( $args{'DESCRIPTION'} ) {
+        $args{'DESCRIPTION'} =
+            $args{'FIELD'} . " "
+          . $args{'OPERATOR'} . " "
+          . $args{'VALUE'} . " GMT";
     }
 
-    $self->Limit (%args);
+    $self->Limit(%args);
 
 }
 
@@ -1581,11 +2132,11 @@ Takes a paramhash of key/value pairs with the following keys:
 
 =over 4
 
-=item KEYWORDSELECT - KeywordSelect id
+=item CUSTOMFIELD - CustomField name or id.  If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
 
-=item OPERATOR - (for KEYWORD only - KEYWORDSELECT operator is always `=')
+=item OPERATOR - The usual Limit operators
 
-=item KEYWORD - Keyword id
+=item VALUE - The value to compare against
 
 =back
 
@@ -1593,60 +2144,74 @@ Takes a paramhash of key/value pairs with the following keys:
 
 sub LimitCustomField {
     my $self = shift;
-    my %args = ( VALUE        => undef,
-                 CUSTOMFIELD   => undef,
-                 OPERATOR      => '=',
-                 DESCRIPTION   => undef,
-                 FIELD         => 'CustomFieldValue',
-                 QUOTEVALUE    => 1,
-                 @_ );
-
-    use RT::CustomFields;
+    my %args = (
+        VALUE       => undef,
+        CUSTOMFIELD => undef,
+        OPERATOR    => '=',
+        DESCRIPTION => undef,
+        FIELD       => 'CustomFieldValue',
+        QUOTEVALUE  => 1,
+        @_
+    );
+
     my $CF = RT::CustomField->new( $self->CurrentUser );
-    $CF->Load( $args{CUSTOMFIELD} );
+    if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
+        $CF->Load( $args{CUSTOMFIELD} );
+    }
+    else {
+        $CF->LoadByNameAndQueue(
+            Name  => $args{CUSTOMFIELD},
+            Queue => $args{QUEUE}
+        );
+        $args{CUSTOMFIELD} = $CF->Id;
+    }
 
     #If we are looking to compare with a null value.
     if ( $args{'OPERATOR'} =~ /^is$/i ) {
-      $args{'DESCRIPTION'} ||= $self->loc("Custom field [_1] has no value.", $CF->Name);
+        $args{'DESCRIPTION'} ||=
+          $self->loc( "Custom field [_1] has no value.", $CF->Name );
     }
     elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
-      $args{'DESCRIPTION'} ||= $self->loc("Custom field [_1] has a value.", $CF->Name);
+        $args{'DESCRIPTION'} ||=
+          $self->loc( "Custom field [_1] has a value.", $CF->Name );
     }
 
     # if we're not looking to compare with a null value
     else {
-        $args{'DESCRIPTION'} ||= $self->loc("Custom field [_1] [_2] [_3]",  $CF->Name , $args{OPERATOR} , $args{VALUE});
+        $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
+            $CF->Name, $args{OPERATOR}, $args{VALUE} );
     }
 
-#    my $index = $self->_NextIndex;
-#    %{ $self->{'TicketRestrictions'}{$index} } = %args;
-
-
     my $q = "";
-    if ($CF->Queue) {
-      my $qo = new RT::Queue( $self->CurrentUser );
-      $qo->load( $CF->Queue );
-      $q = $qo->Name;
+    if ( $CF->Queue ) {
+        my $qo = new RT::Queue( $self->CurrentUser );
+        $qo->load( $CF->Queue );
+        $q = $qo->Name;
     }
 
-    $self->Limit( VALUE => $args{VALUE},
-                 FIELD => "CF.".( $q
-                            ? $q . ".{" . $CF->Name . "}"
-                            : $CF->Name
-                          ),
-                 OPERATOR => $args{OPERATOR},
-                 CUSTOMFIELD => 1,
-               );
+    my @rest;
+    @rest = ( ENTRYAGGREGATOR => 'AND' )
+      if ( $CF->Type eq 'SelectMultiple' );
 
+    $self->Limit(
+        VALUE => $args{VALUE},
+        FIELD => "CF."
+          . (
+              $q
+            ? $q . ".{" . $CF->Name . "}"
+            : $CF->Name
+          ),
+        OPERATOR    => $args{OPERATOR},
+        CUSTOMFIELD => 1,
+        @rest,
+    );
 
     $self->{'RecalcTicketLimits'} = 1;
-  #  return ($index);
 }
 
 # }}}
 # }}}
 
-
 # {{{ sub _NextIndex
 
 =head2 _NextIndex
@@ -1657,8 +2222,9 @@ Keep track of the counter for the array of restrictions
 
 sub _NextIndex {
     my $self = shift;
-    return ($self->{'restriction_index'}++);
+    return ( $self->{'restriction_index'}++ );
 }
+
 # }}}
 
 # }}}
@@ -1666,39 +2232,42 @@ sub _NextIndex {
 # {{{ Core bits to make this a DBIx::SearchBuilder object
 
 # {{{ sub _Init
-sub _Init  {
+sub _Init {
     my $self = shift;
-    $self->{'table'} = "Tickets";
-    $self->{'RecalcTicketLimits'} = 1;
+    $self->{'table'}                   = "Tickets";
+    $self->{'RecalcTicketLimits'}      = 1;
     $self->{'looking_at_effective_id'} = 0;
-    $self->{'looking_at_type'} = 0;
-    $self->{'restriction_index'} =1;
-    $self->{'primary_key'} = "id";
+    $self->{'looking_at_type'}         = 0;
+    $self->{'restriction_index'}       = 1;
+    $self->{'primary_key'}             = "id";
     delete $self->{'items_array'};
     delete $self->{'item_map'};
+    delete $self->{'columns_to_display'};
     $self->SUPER::_Init(@_);
 
     $self->_InitSQL;
 
 }
+
 # }}}
 
 # {{{ sub Count
 sub Count {
-  my $self = shift;
-  $self->_ProcessRestrictions() if ($self->{'RecalcTicketLimits'} == 1 );
-  return($self->SUPER::Count());
+    my $self = shift;
+    $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
+    return ( $self->SUPER::Count() );
 }
+
 # }}}
 
 # {{{ sub CountAll
 sub CountAll {
-  my $self = shift;
-  $self->_ProcessRestrictions() if ($self->{'RecalcTicketLimits'} == 1 );
-  return($self->SUPER::CountAll());
+    my $self = shift;
+    $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
+    return ( $self->SUPER::CountAll() );
 }
-# }}}
 
+# }}}
 
 # {{{ sub ItemsArrayRef
 
@@ -1717,28 +2286,27 @@ sub ItemsArrayRef {
         my $placeholder = $self->_ItemsCounter;
         $self->GotoFirstItem();
         while ( my $item = $self->Next ) {
-            push ( @{ $self->{'items_array'} }, $item );
+            push( @{ $self->{'items_array'} }, $item );
         }
         $self->GotoItem($placeholder);
+        $self->{'items_array'} = $self->ItemsOrderBy( $self->{'items_array'} );
     }
     return ( $self->{'items_array'} );
 }
+
 # }}}
 
 # {{{ sub Next
 sub Next {
-       my $self = shift;
-       
-       $self->_ProcessRestrictions() if ($self->{'RecalcTicketLimits'} == 1 );
+    my $self = shift;
 
-       my $Ticket = $self->SUPER::Next();
-       if ((defined($Ticket)) and (ref($Ticket))) {
+    $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
 
-           #Make sure we _never_ show deleted tickets
-           #TODO we should be doing this in the where clause.
-           #but you can't do multiple clauses on the same field just yet :/
+    my $Ticket = $self->SUPER::Next();
+    if ( ( defined($Ticket) ) and ( ref($Ticket) ) ) {
 
-           if ($Ticket->__Value('Status') eq 'deleted') {
+           if ( $Ticket->__Value('Status') eq 'deleted' &&
+                       !$self->{'allow_deleted_search'} ) {
                return($self->Next());
            }
             # Since Ticket could be granted with more rights instead
@@ -1750,17 +2318,33 @@ sub Next {
                return($Ticket);
            }
 
-           #If the user doesn't have the right to show this ticket
-           else {      
-               return($self->Next());
-           }
-       }
-       #if there never was any ticket
-       else {
-               return(undef);
-       }       
+        if ( $Ticket->__Value('Status') eq 'deleted' ) {
+            return ( $self->Next() );
+        }
+
+        # 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);
+        }
+
+        #If the user doesn't have the right to show this ticket
+        else {
+            return ( $self->Next() );
+        }
+    }
+
+    #if there never was any ticket
+    else {
+        return (undef);
+    }
 
 }
+
 # }}}
 
 # }}}
@@ -1789,16 +2373,17 @@ is a description of the purpose of that TicketRestriction
 
 =cut
 
-sub DescribeRestrictions  {
+sub DescribeRestrictions {
     my $self = shift;
 
-    my ($row, %listing);
+    my ( $row, %listing );
 
-    foreach $row (keys %{$self->{'TicketRestrictions'}}) {
-       $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
+    foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
+        $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
     }
     return (%listing);
 }
+
 # }}}
 
 # {{{ sub RestrictionValues
@@ -1811,14 +2396,13 @@ to.
 =cut
 
 sub RestrictionValues {
-    my $self = shift;
+    my $self  = shift;
     my $field = shift;
-    map $self->{'TicketRestrictions'}{$_}{'VALUE'},
-      grep {
-             $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
-             && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
-           }
-        keys %{$self->{'TicketRestrictions'}};
+    map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
+             $self->{'TicketRestrictions'}{$_}{'FIELD'}    eq $field
+          && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
+      }
+      keys %{ $self->{'TicketRestrictions'} };
 }
 
 # }}}
@@ -1835,8 +2419,8 @@ sub ClearRestrictions {
     my $self = shift;
     delete $self->{'TicketRestrictions'};
     $self->{'looking_at_effective_id'} = 0;
-    $self->{'looking_at_type'} = 0;
-    $self->{'RecalcTicketLimits'} =1;
+    $self->{'looking_at_type'}         = 0;
+    $self->{'RecalcTicketLimits'}      = 1;
 }
 
 # }}}
@@ -1850,13 +2434,13 @@ Removes that restriction from the session's limits.
 
 =cut
 
-
 sub DeleteRestriction {
     my $self = shift;
-    my $row = shift;
+    my $row  = shift;
     delete $self->{'TicketRestrictions'}{$row};
 
     $self->{'RecalcTicketLimits'} = 1;
+
     #make the underlying easysearch object forget all its preconceptions
 }
 
@@ -1867,82 +2451,93 @@ sub DeleteRestriction {
 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
 
 sub _RestrictionsToClauses {
-  my $self = shift;
-
-  my $row;
-  my %clause;
-  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.
-
-      # So, we want to group things into Subclauses, convert them to
-      # SQL, and then join them with the appropriate DefaultEA.
-      # Then join each subclause group with AND.
-
-    my $field = $restriction->{'FIELD'};
-    my $realfield = $field;    # CustomFields fake up a fieldname, so
-                                # we need to figure that out
-
-    # One special case
-    # Rewrite LinkedTo meta field to the real field
-    if ($field =~ /LinkedTo/) {
-      $realfield = $field = $restriction->{'TYPE'};
-    }
+    my $self = shift;
 
-    # Two special case
-    # CustomFields have a different real field
-    if ($field =~ /^CF\./) {
-      $realfield = "CF"
-    }
+    my $row;
+    my %clause;
+    foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
+        my $restriction = $self->{'TicketRestrictions'}{$row};
 
-    die "I don't know about $field yet"
-      unless (exists $FIELDS{$realfield} or $restriction->{CUSTOMFIELD});
-
-    my $type = $FIELDS{$realfield}->[0];
-    my $op   = $restriction->{'OPERATOR'};
-
-    my $value = ( grep { defined }
-                 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET))[0];
-
-    # this performs the moral equivalent of defined or/dor/C<//>,
-    # without the short circuiting.You need to use a 'defined or'
-    # type thing instead of just checking for truth values, because
-    # VALUE could be 0.(i.e. "false")
-
-    # You could also use this, but I find it less aesthetic:
-    # (although it does short circuit)
-    #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
-    # defined $restriction->{'TICKET'} ?
-    # $restriction->{TICKET} :
-    # defined $restriction->{'BASE'} ?
-    # $restriction->{BASE} :
-    # defined $restriction->{'TARGET'} ?
-    # $restriction->{TARGET} )
-
-    my $ea = $DefaultEA{$type};
-    if ( ref $ea ) {
-      die "Invalid operator $op for $field ($type)"
-       unless exists $ea->{$op};
-      $ea = $ea->{$op};
-    }
-    exists $clause{$realfield} or $clause{$realfield} = [];
-    # Escape Quotes
-    $field =~ s!(['"])!\\$1!g;
-    $value =~ s!(['"])!\\$1!g;
-    my $data = [ $ea, $type, $field, $op, $value ];
+        #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.
 
-    # here is where we store extra data, say if it's a keyword or
-    # something.  (I.e. "TYPE SPECIFIC STUFF")
+        # So, we want to group things into Subclauses, convert them to
+        # SQL, and then join them with the appropriate DefaultEA.
+        # Then join each subclause group with AND.
 
-    #print Dumper($data);
-    push @{$clause{$realfield}}, $data;
-  }
-  return \%clause;
+        my $field = $restriction->{'FIELD'};
+        my $realfield = $field;    # CustomFields fake up a fieldname, so
+                                   # we need to figure that out
+
+        # One special case
+        # Rewrite LinkedTo meta field to the real field
+        if ( $field =~ /LinkedTo/ ) {
+            $realfield = $field = $restriction->{'TYPE'};
+        }
+
+        # Two special case
+        # Handle subkey fields with a different real field
+        if ( $field =~ /^(\w+)\./ ) {
+            $realfield = $1;
+        }
+
+        die "I don't know about $field yet"
+          unless ( exists $FIELDS{$realfield} or $restriction->{CUSTOMFIELD} );
+
+        my $type = $FIELDS{$realfield}->[0];
+        my $op   = $restriction->{'OPERATOR'};
+
+        my $value = (
+            grep  { defined }
+              map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
+        )[0];
+
+        # this performs the moral equivalent of defined or/dor/C<//>,
+        # without the short circuiting.You need to use a 'defined or'
+        # type thing instead of just checking for truth values, because
+        # VALUE could be 0.(i.e. "false")
+
+        # You could also use this, but I find it less aesthetic:
+        # (although it does short circuit)
+        #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
+        # defined $restriction->{'TICKET'} ?
+        # $restriction->{TICKET} :
+        # defined $restriction->{'BASE'} ?
+        # $restriction->{BASE} :
+        # defined $restriction->{'TARGET'} ?
+        # $restriction->{TARGET} )
+
+        my $ea = $restriction->{ENTRYAGGREGATOR} || $DefaultEA{$type} || "AND";
+        if ( ref $ea ) {
+            die "Invalid operator $op for $field ($type)"
+              unless exists $ea->{$op};
+            $ea = $ea->{$op};
+        }
+
+        # Each CustomField should be put into a different Clause so they
+        # are ANDed together.
+        if ( $restriction->{CUSTOMFIELD} ) {
+            $realfield = $field;
+        }
+
+        exists $clause{$realfield} or $clause{$realfield} = [];
+
+        # Escape Quotes
+        $field =~ s!(['"])!\\$1!g;
+        $value =~ s!(['"])!\\$1!g;
+        my $data = [ $ea, $type, $field, $op, $value ];
+
+        # 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;
 }
 
 # }}}
@@ -1958,25 +2553,31 @@ sub _RestrictionsToClauses {
 
 sub _ProcessRestrictions {
     my $self = shift;
-    
+
     #Blow away ticket aliases since we'll need to regenerate them for
     #a new search
     delete $self->{'TicketAliases'};
-    delete $self->{'items_array'};                                                                                                                   
-    my $sql = $self->{_sql_query}; # Violating the _SQL namespace
-    if (!$sql||$self->{'RecalcTicketLimits'}) {
-      #  "Restrictions to Clauses Branch\n";
-      my $clauseRef = eval { $self->_RestrictionsToClauses; };
-      if ($@) {
-       $RT::Logger->error( "RestrictionsToClauses: " . $@ );
-       $self->FromSQL("");
-      } else {
-       $sql = $self->ClausesToSQL($clauseRef);
-       $self->FromSQL($sql);
-      }
+    delete $self->{'items_array'};
+    delete $self->{'item_map'};
+    delete $self->{'raw_rows'};
+    delete $self->{'rows'};
+    delete $self->{'count_all'};
+
+    my $sql = $self->Query;    # Violating the _SQL namespace
+    if ( !$sql || $self->{'RecalcTicketLimits'} ) {
+
+        #  "Restrictions to Clauses Branch\n";
+        my $clauseRef = eval { $self->_RestrictionsToClauses; };
+        if ($@) {
+            $RT::Logger->error( "RestrictionsToClauses: " . $@ );
+            $self->FromSQL("");
+        }
+        else {
+            $sql = $self->ClausesToSQL($clauseRef);
+            $self->FromSQL($sql);
+        }
     }
 
-
     $self->{'RecalcTicketLimits'} = 0;
 
 }
@@ -1991,22 +2592,22 @@ sub _BuildItemMap {
     my $self = shift;
 
     my $items = $self->ItemsArrayRef;
-    my $prev = 0 ;
+    my $prev  = 0;
 
     delete $self->{'item_map'};
-    if ($items->[0]) {
-    $self->{'item_map'}->{'first'} = $items->[0]->Id;
-    while (my $item = shift @$items ) {
-        my $id = $item->Id;
-        $self->{'item_map'}->{$id}->{'defined'} = 1;
-        $self->{'item_map'}->{$id}->{prev}  = $prev;
-        $self->{'item_map'}->{$id}->{next}  = $items->[0]->Id if ($items->[0]);
-        $prev = $id;
-    }
-    $self->{'item_map'}->{'last'} = $prev;
+    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;
     }
-} 
-
+}
 
 =head2 ItemMap
 
@@ -2014,22 +2615,21 @@ Returns an a map of all items found by this search. The map is of the form
 
 $ItemMap->{'first'} = first ticketid found
 $ItemMap->{'last'} = last ticketid found
-$ItemMap->{$id}->{prev} = the tikcet id found before $id
-$ItemMap->{$id}->{next} = the tikcet id found after $id
+$ItemMap->{$id}->{prev} = the ticket id found before $id
+$ItemMap->{$id}->{next} = the ticket id found after $id
 
 =cut
 
 sub ItemMap {
     my $self = shift;
-    $self->_BuildItemMap() unless ($self->{'item_map'});
-    return ($self->{'item_map'});
+    $self->_BuildItemMap()
+      unless ( $self->{'items_array'} and $self->{'item_map'} );
+    return ( $self->{'item_map'} );
 }
 
-
-
-
 =cut
 
+
 }
 
 
@@ -2044,12 +2644,30 @@ You don't want to serialize a big tickets object, as the {items} hash will be in
 
 =cut
 
-
 sub PrepForSerialization {
     my $self = shift;
     delete $self->{'items'};
     $self->RedoSearch();
 }
 
+
+=head1 FLAGS
+
+RT::Tickets supports several flags which alter search behavior:
+
+
+allow_deleted_search  (Otherwise never show deleted tickets in search results)
+looking_at_type (otherwise limit to type=ticket)
+
+These flags are set by calling 
+
+$tickets->{'flagname'} = 1;
+
+BUG: There should be an API for this
+
+=cut
+
 1;
 
+
+