rt 4.0.23
[freeside.git] / rt / lib / RT / Tickets.pm
index dd91126..c826b6f 100755 (executable)
@@ -1,4 +1,61 @@
-#$Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Tickets.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+#                                          <sales@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# 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 BPS TAGGED BLOCK }}}
+
+# Major Changes:
+
+# - Decimated ProcessRestrictions and broke it into multiple
+# functions joined by a LUT
+# - Semi-Generic SQL stuff moved to another file
+
+# Known Issues: FIXME!
+
+# - ClearRestrictions and Reinitialization is messy and unclear.  The
+# only good way to do it is to create a new RT::Tickets object.
 
 =head1 NAME
 
 
 =head1 NAME
 
@@ -8,7 +65,7 @@
 =head1 SYNOPSIS
 
   use RT::Tickets;
 =head1 SYNOPSIS
 
   use RT::Tickets;
-  my $tickets = new RT::Tickets($CurrentUser);
+  my $tickets = RT::Tickets->new($CurrentUser);
 
 =head1 DESCRIPTION
 
 
 =head1 DESCRIPTION
 
 
 =head1 METHODS
 
 
 =head1 METHODS
 
-=begin testing
-
-ok (require RT::TestHarness);
-ok (require RT::Tickets);
-
-=end testing
 
 =cut
 
 package RT::Tickets;
 
 =cut
 
 package RT::Tickets;
-use RT::EasySearch;
+
+use strict;
+use warnings;
+
+
 use RT::Ticket;
 use RT::Ticket;
-@ISA= qw(RT::EasySearch);
-
-use vars qw(%TYPES @SORTFIELDS);
-
-# {{{ TYPES
-
-%TYPES =    ( Status => 'ENUM',
-             Queue  => 'ENUM',
-             Type => 'ENUM',
-             Creator => 'ENUM',
-             LastUpdatedBy => 'ENUM',
-             Owner => 'ENUM',
-             EffectiveId => 'INT',
-             id => 'INT',
-             InitialPriority => 'INT',
-             FinalPriority => 'INT',
-             Priority => 'INT',
-             TimeLeft => 'INT',
-             TimeWorked => 'INT',
-             MemberOf => 'LINK',
-             DependsOn => 'LINK',
-             HasMember => 'LINK',
-             HasDepender => 'LINK',
-             RelatedTo => 'LINK',
-              Told => 'DATE',
-              StartsBy => 'DATE',
-              Started => 'DATE',
-              Due  => 'DATE',
-              Resolved => 'DATE',
-              LastUpdated => 'DATE',
-              Created => 'DATE',
-              Subject => 'STRING',
-             Type => 'STRING',
-              Content => 'TRANSFIELD',
-             ContentType => 'TRANSFIELD',
-             TransactionDate => 'TRANSDATE',
-             Watcher => 'WATCHERFIELD',
-             LinkedTo => 'LINKFIELD',
-              Keyword => 'KEYWORDFIELD'
-
-           );
-
-
-# }}}
-
-# {{{ sub SortFields
-
-@SORTFIELDS = qw(id Status Owner Created Due Starts Started
-                Queue Subject Told Started 
-                   Resolved LastUpdated Priority TimeWorked TimeLeft);
+
+use base 'RT::SearchBuilder';
+
+sub Table { 'Tickets'}
+
+use RT::CustomFields;
+
+# Configuration Tables:
+
+# FIELD_METADATA is a mapping of searchable Field name, to Type, and other
+# metadata.
+
+our %FIELD_METADATA = (
+    Status          => [ 'ENUM', ], #loc_left_pair
+    Queue           => [ 'ENUM' => 'Queue', ], #loc_left_pair
+    Type            => [ 'ENUM', ], #loc_left_pair
+    Creator         => [ 'ENUM' => 'User', ], #loc_left_pair
+    LastUpdatedBy   => [ 'ENUM' => 'User', ], #loc_left_pair
+    Owner           => [ 'WATCHERFIELD' => 'Owner', ], #loc_left_pair
+    EffectiveId     => [ 'INT', ], #loc_left_pair
+    id              => [ 'ID', ], #loc_left_pair
+    InitialPriority => [ 'INT', ], #loc_left_pair
+    FinalPriority   => [ 'INT', ], #loc_left_pair
+    Priority        => [ 'INT', ], #loc_left_pair
+    TimeLeft        => [ 'INT', ], #loc_left_pair
+    TimeWorked      => [ 'INT', ], #loc_left_pair
+    TimeEstimated   => [ 'INT', ], #loc_left_pair
+
+    Linked          => [ 'LINK' ], #loc_left_pair
+    LinkedTo        => [ 'LINK' => 'To' ], #loc_left_pair
+    LinkedFrom      => [ 'LINK' => 'From' ], #loc_left_pair
+    MemberOf        => [ 'LINK' => To => 'MemberOf', ], #loc_left_pair
+    DependsOn       => [ 'LINK' => To => 'DependsOn', ], #loc_left_pair
+    RefersTo        => [ 'LINK' => To => 'RefersTo', ], #loc_left_pair
+    HasMember       => [ 'LINK' => From => 'MemberOf', ], #loc_left_pair
+    DependentOn     => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
+    DependedOnBy    => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
+    ReferredToBy    => [ 'LINK' => From => 'RefersTo', ], #loc_left_pair
+    Told             => [ 'DATE'            => 'Told', ], #loc_left_pair
+    Starts           => [ 'DATE'            => 'Starts', ], #loc_left_pair
+    Started          => [ 'DATE'            => 'Started', ], #loc_left_pair
+    Due              => [ 'DATE'            => 'Due', ], #loc_left_pair
+    Resolved         => [ 'DATE'            => 'Resolved', ], #loc_left_pair
+    LastUpdated      => [ 'DATE'            => 'LastUpdated', ], #loc_left_pair
+    Created          => [ 'DATE'            => 'Created', ], #loc_left_pair
+    Subject          => [ 'STRING', ], #loc_left_pair
+    Content          => [ 'TRANSCONTENT', ], #loc_left_pair
+    ContentType      => [ 'TRANSFIELD', ], #loc_left_pair
+    Filename         => [ 'TRANSFIELD', ], #loc_left_pair
+    TransactionDate  => [ 'TRANSDATE', ], #loc_left_pair
+    Requestor        => [ 'WATCHERFIELD'    => 'Requestor', ], #loc_left_pair
+    Requestors       => [ 'WATCHERFIELD'    => 'Requestor', ], #loc_left_pair
+    Cc               => [ 'WATCHERFIELD'    => 'Cc', ], #loc_left_pair
+    AdminCc          => [ 'WATCHERFIELD'    => 'AdminCc', ], #loc_left_pair
+    Watcher          => [ 'WATCHERFIELD', ], #loc_left_pair
+    QueueCc          => [ 'WATCHERFIELD'    => 'Cc'      => 'Queue', ], #loc_left_pair
+    QueueAdminCc     => [ 'WATCHERFIELD'    => 'AdminCc' => 'Queue', ], #loc_left_pair
+    QueueWatcher     => [ 'WATCHERFIELD'    => undef     => 'Queue', ], #loc_left_pair
+    CustomFieldValue => [ 'CUSTOMFIELD' => 'Ticket' ], #loc_left_pair
+    CustomField      => [ 'CUSTOMFIELD' => 'Ticket' ], #loc_left_pair
+    CF               => [ 'CUSTOMFIELD' => 'Ticket' ], #loc_left_pair
+    Updated          => [ 'TRANSDATE', ], #loc_left_pair
+    RequestorGroup   => [ 'MEMBERSHIPFIELD' => 'Requestor', ], #loc_left_pair
+    CCGroup          => [ 'MEMBERSHIPFIELD' => 'Cc', ], #loc_left_pair
+    AdminCCGroup     => [ 'MEMBERSHIPFIELD' => 'AdminCc', ], #loc_left_pair
+    WatcherGroup     => [ 'MEMBERSHIPFIELD', ], #loc_left_pair
+    HasAttribute     => [ 'HASATTRIBUTE', 1 ],
+    HasNoAttribute     => [ 'HASATTRIBUTE', 0 ],
+    #freeside
+    Customer         => [ 'FREESIDEFIELD' => 'Customer' ],
+    Service          => [ 'FREESIDEFIELD' => 'Service' ],
+    WillResolve      => [ 'DATE'            => 'WillResolve', ], #loc_left_pair
+);
+
+# Lower Case version of FIELDS, for case insensitivity
+our %LOWER_CASE_FIELDS = map { ( lc($_) => $_ ) } (keys %FIELD_METADATA);
+
+our %SEARCHABLE_SUBFIELDS = (
+    User => [qw(
+        EmailAddress Name RealName Nickname Organization Address1 Address2
+        WorkPhone HomePhone MobilePhone PagerPhone id
+    )],
+);
+
+# Mapping of Field Type to Function
+our %dispatch = (
+    ENUM            => \&_EnumLimit,
+    INT             => \&_IntLimit,
+    ID              => \&_IdLimit,
+    LINK            => \&_LinkLimit,
+    DATE            => \&_DateLimit,
+    STRING          => \&_StringLimit,
+    TRANSFIELD      => \&_TransLimit,
+    TRANSCONTENT    => \&_TransContentLimit,
+    TRANSDATE       => \&_TransDateLimit,
+    WATCHERFIELD    => \&_WatcherLimit,
+    MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
+    CUSTOMFIELD     => \&_CustomFieldLimit,
+    HASATTRIBUTE    => \&_HasAttributeLimit,
+    FREESIDEFIELD   => \&_FreesideFieldLimit,
+);
+our %can_bundle = ();# WATCHERFIELD => "yes", );
+
+# Default EntryAggregator per type
+# if you specify OP, you must specify all valid OPs
+my %DefaultEA = (
+    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'
+    },
+
+    HASATTRIBUTE => {
+        '='        => 'AND',
+        '!='       => 'AND',
+    },
+
+    CUSTOMFIELD => 'OR',
+);
+
+# Helper functions for passing the above lexically scoped tables above
+# into Tickets_SQL.
+sub FIELDS     { return \%FIELD_METADATA }
+sub dispatch   { return \%dispatch }
+sub can_bundle { return \%can_bundle }
+
+# Bring in the clowns.
+require RT::Tickets_SQL;
+
+
+our @SORTFIELDS = qw(id Status
+    Queue Subject
+    Owner Created Due Starts Started
+    Told
+    Resolved LastUpdated Priority TimeWorked TimeLeft);
 
 =head2 SortFields
 
 
 =head2 SortFields
 
@@ -85,140 +250,2177 @@ Returns the list of fields that lists of tickets can easily be sorted by
 
 =cut
 
 
 =cut
 
-
 sub SortFields {
 sub SortFields {
-       my $self = shift;
-       return(@SORTFIELDS);
+    my $self = shift;
+    return (@SORTFIELDS);
 }
 
 
 }
 
 
-# }}}
+# BEGIN SQL STUFF *********************************
 
 
-# {{{ Limit the result set based on content
 
 
-# {{{ sub Limit 
+sub CleanSlate {
+    my $self = shift;
+    $self->SUPER::CleanSlate( @_ );
+    delete $self->{$_} foreach qw(
+        _sql_cf_alias
+        _sql_group_members_aliases
+        _sql_object_cfv_alias
+        _sql_role_group_aliases
+        _sql_trattachalias
+        _sql_u_watchers_alias_for_sort
+        _sql_u_watchers_aliases
+        _sql_current_user_can_see_applied
+    );
+}
 
 
-=head2 Limit
+=head1 Limit Helper Routines
 
 
-Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
-Generally best called from LimitFoo methods
+These routines are the targets of a dispatch table depending on the
+type of field.  They all share the same signature:
+
+  my ($self,$field,$op,$value,@rest) = @_;
+
+The values in @rest should be suitable for passing directly to
+DBIx::SearchBuilder::Limit.
+
+Essentially they are an expanded/broken out (and much simplified)
+version of what ProcessRestrictions used to do.  They're also much
+more clearly delineated by the TYPE of field being processed.
+
+=head2 _IdLimit
+
+Handle ID field.
 
 =cut
 
 =cut
-sub Limit {
+
+sub _IdLimit {
+    my ( $sb, $field, $op, $value, @rest ) = @_;
+
+    if ( $value eq '__Bookmarked__' ) {
+        return $sb->_BookmarkLimit( $field, $op, $value, @rest );
+    } else {
+        return $sb->_IntLimit( $field, $op, $value, @rest );
+    }
+}
+
+sub _BookmarkLimit {
+    my ( $sb, $field, $op, $value, @rest ) = @_;
+
+    die "Invalid operator $op for __Bookmarked__ search on $field"
+        unless $op =~ /^(=|!=)$/;
+
+    my @bookmarks = do {
+        my $tmp = $sb->CurrentUser->UserObj->FirstAttribute('Bookmarks');
+        $tmp = $tmp->Content if $tmp;
+        $tmp ||= {};
+        grep $_, keys %$tmp;
+    };
+
+    return $sb->_SQLLimit(
+        FIELD    => $field,
+        OPERATOR => $op,
+        VALUE    => 0,
+        @rest,
+    ) unless @bookmarks;
+
+    # as bookmarked tickets can be merged we have to use a join
+    # but it should be pretty lightweight
+    my $tickets_alias = $sb->Join(
+        TYPE   => 'LEFT',
+        ALIAS1 => 'main',
+        FIELD1 => 'id',
+        TABLE2 => 'Tickets',
+        FIELD2 => 'EffectiveId',
+    );
+    $sb->_OpenParen;
+    my $first = 1;
+    my $ea = $op eq '='? 'OR': 'AND';
+    foreach my $id ( sort @bookmarks ) {
+        $sb->_SQLLimit(
+            ALIAS    => $tickets_alias,
+            FIELD    => 'id',
+            OPERATOR => $op,
+            VALUE    => $id,
+            $first? (@rest): ( ENTRYAGGREGATOR => $ea )
+        );
+        $first = 0 if $first;
+    }
+    $sb->_CloseParen;
+}
+
+=head2 _EnumLimit
+
+Handle Fields which are limited to certain values, and potentially
+need to be looked up from another class.
+
+This subroutine actually handles two different kinds of fields.  For
+some the user is responsible for limiting the values.  (i.e. Status,
+Type).
+
+For others, the value specified by the user will be looked by via
+specified class.
+
+Meta Data:
+  name of class to lookup in (Optional)
+
+=cut
+
+sub _EnumLimit {
+    my ( $sb, $field, $op, $value, @rest ) = @_;
+
+    # 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 "!=";
+
+    my $meta = $FIELD_METADATA{$field};
+    if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
+        my $class = "RT::" . $meta->[1];
+        my $o     = $class->new( $sb->CurrentUser );
+        $o->Load($value);
+        $value = $o->Id || 0;
+    } elsif ( $field eq "Type" ) {
+        $value = lc $value if $value =~ /^(ticket|approval|reminder)$/i;
+    } elsif ($field eq "Status") {
+        $value = lc $value;
+    }
+    $sb->_SQLLimit(
+        FIELD    => $field,
+        VALUE    => $value,
+        OPERATOR => $op,
+        @rest,
+    );
+}
+
+=head2 _IntLimit
+
+Handle fields where the values are limited to integers.  (For example,
+Priority, TimeWorked.)
+
+Meta Data:
+  None
+
+=cut
+
+sub _IntLimit {
+    my ( $sb, $field, $op, $value, @rest ) = @_;
+
+    die "Invalid Operator $op for $field"
+        unless $op =~ /^(=|!=|>|<|>=|<=)$/;
+
+    $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: Link Type (MemberOf, DependsOn, RefersTo)
+
+=cut
+
+sub _LinkLimit {
+    my ( $sb, $field, $op, $value, @rest ) = @_;
+
+    my $meta = $FIELD_METADATA{$field};
+    die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS|IS NOT)$/io;
+
+    my $is_negative = 0;
+    if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
+        $is_negative = 1;
+    }
+    my $is_null = 0;
+    $is_null = 1 if !$value || $value =~ /^null$/io;
+
+    my $direction = $meta->[1] || '';
+    my ($matchfield, $linkfield) = ('', '');
+    if ( $direction eq 'To' ) {
+        ($matchfield, $linkfield) = ("Target", "Base");
+    }
+    elsif ( $direction eq 'From' ) {
+        ($matchfield, $linkfield) = ("Base", "Target");
+    }
+    elsif ( $direction ) {
+        die "Invalid link direction '$direction' for $field\n";
+    } else {
+        $sb->_OpenParen;
+        $sb->_LinkLimit( 'LinkedTo', $op, $value, @rest );
+        $sb->_LinkLimit(
+            'LinkedFrom', $op, $value, @rest,
+            ENTRYAGGREGATOR => (($is_negative && $is_null) || (!$is_null && !$is_negative))? 'OR': 'AND',
+        );
+        $sb->_CloseParen;
+        return;
+    }
+
+    my $is_local = 1;
+    if ( $is_null ) {
+        $op = ($op =~ /^(=|IS)$/i)? 'IS': 'IS NOT';
+    }
+    elsif ( $value =~ /\D/ ) {
+        $value = RT::URI->new( $sb->CurrentUser )->CanonicalizeURI( $value );
+        $is_local = 0;
+    }
+    $matchfield = "Local$matchfield" if $is_local;
+
+#For doing a left join to find "unlinked tickets" we want to generate a query that looks like this
+#    SELECT main.* FROM Tickets main
+#        LEFT JOIN Links Links_1 ON (     (Links_1.Type = 'MemberOf')
+#                                      AND(main.id = Links_1.LocalTarget))
+#        WHERE 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],
+        ) if $meta->[2];
+        $sb->_SQLLimit(
+            @rest,
+            ALIAS      => $linkalias,
+            FIELD      => $matchfield,
+            OPERATOR   => $op,
+            VALUE      => 'NULL',
+            QUOTEVALUE => 0,
+        );
+    }
+    else {
+        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],
+        ) if $meta->[2];
+        $sb->SUPER::Limit(
+            LEFTJOIN => $linkalias,
+            FIELD    => $matchfield,
+            OPERATOR => '=',
+            VALUE    => $value,
+        );
+        $sb->_SQLLimit(
+            @rest,
+            ALIAS      => $linkalias,
+            FIELD      => $matchfield,
+            OPERATOR   => $is_negative? 'IS': 'IS NOT',
+            VALUE      => 'NULL',
+            QUOTEVALUE => 0,
+        );
+    }
+}
+
+=head2 _DateLimit
+
+Handle date fields.  (Created, LastTold..)
+
+Meta Data:
+  1: type of link.  (Probably not necessary.)
+
+=cut
+
+sub _DateLimit {
+    my ( $sb, $field, $op, $value, @rest ) = @_;
+
+    die "Invalid Date Op: $op"
+        unless $op =~ /^(=|>|<|>=|<=)$/;
+
+    my $meta = $FIELD_METADATA{$field};
+    die "Incorrect Meta Data for $field"
+        unless ( defined $meta->[1] );
+
+    $sb->_DateFieldLimit( $meta->[1], $op, $value, @rest );
+}
+
+# Factor this out for use by custom fields
+
+sub _DateFieldLimit {
+    my ( $sb, $field, $op, $value, @rest ) = @_;
+
+    my $date = RT::Date->new( $sb->CurrentUser );
+    $date->Set( Format => 'unknown', Value => $value );
+
+    if ( $op eq "=" ) {
+
+        # if we're specifying =, that means we want everything on a
+        # particular single day.  in the database, we need to check for >
+        # and < the edges of that day.
+        #
+        # Except if the value is 'this month' or 'last month', check 
+        # > and < the edges of the month.
+       
+        my ($daystart, $dayend);
+        if ( lc($value) eq 'this month' ) { 
+            $date->SetToNow;
+            $date->SetToStart('month', Timezone => 'server');
+            $daystart = $date->ISO;
+            $date->AddMonth(Timezone => 'server');
+            $dayend = $date->ISO;
+        }
+        elsif ( lc($value) eq 'last month' ) {
+            $date->SetToNow;
+            $date->SetToStart('month', Timezone => 'server');
+            $dayend = $date->ISO;
+            $date->AddDays(-1);
+            $date->SetToStart('month', Timezone => 'server');
+            $daystart = $date->ISO;
+        }
+        else {
+            $date->SetToMidnight( Timezone => 'server' );
+            $daystart = $date->ISO;
+            $date->AddDay;
+            $dayend = $date->ISO;
+        }
+
+        $sb->_OpenParen;
+
+        $sb->_SQLLimit(
+            FIELD    => $field,
+            OPERATOR => ">=",
+            VALUE    => $daystart,
+            @rest,
+        );
+
+        $sb->_SQLLimit(
+            FIELD    => $field,
+            OPERATOR => "<",
+            VALUE    => $dayend,
+            @rest,
+            ENTRYAGGREGATOR => 'AND',
+        );
+
+        $sb->_CloseParen;
+
+    }
+    else {
+        $sb->_SQLLimit(
+            FIELD    => $field,
+            OPERATOR => $op,
+            VALUE    => $date->ISO,
+            @rest,
+        );
+    }
+}
+
+=head2 _StringLimit
+
+Handle simple fields which are just strings.  (Subject,Type)
+
+Meta Data:
+  None
+
+=cut
+
+sub _StringLimit {
+    my ( $sb, $field, $op, $value, @rest ) = @_;
+
+    # FIXME:
+    # Valid Operators:
+    #  =, !=, LIKE, NOT LIKE
+    if ( RT->Config->Get('DatabaseType') eq 'Oracle'
+        && (!defined $value || !length $value)
+        && lc($op) ne 'is' && lc($op) ne 'is not'
+    ) {
+        if ($op eq '!=' || $op =~ /^NOT\s/i) {
+            $op = 'IS NOT';
+        } else {
+            $op = 'IS';
+        }
+        $value = 'NULL';
+    }
+
+    $sb->_SQLLimit(
+        FIELD         => $field,
+        OPERATOR      => $op,
+        VALUE         => $value,
+        CASESENSITIVE => 0,
+        @rest,
+    );
+}
+
+=head2 _TransDateLimit
+
+Handle fields limiting based on Transaction Date.
+
+The inpupt value must be in a format parseable by Time::ParseDate
+
+Meta Data:
+  None
+
+=cut
+
+# This routine should really be factored into translimit.
+sub _TransDateLimit {
+    my ( $sb, $field, $op, $value, @rest ) = @_;
+
+    # See the comments for TransLimit, they apply here too
+
+    my $txn_alias = $sb->JoinTransactions;
+
+    my $date = RT::Date->new( $sb->CurrentUser );
+    $date->Set( Format => 'unknown', Value => $value );
+
+    $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.
+
+        $date->SetToMidnight( Timezone => 'server' );
+        my $daystart = $date->ISO;
+        $date->AddDay;
+        my $dayend = $date->ISO;
+
+        $sb->_SQLLimit(
+            ALIAS         => $txn_alias,
+            FIELD         => 'Created',
+            OPERATOR      => ">=",
+            VALUE         => $daystart,
+            @rest
+        );
+        $sb->_SQLLimit(
+            ALIAS         => $txn_alias,
+            FIELD         => 'Created',
+            OPERATOR      => "<=",
+            VALUE         => $dayend,
+            @rest,
+            ENTRYAGGREGATOR => 'AND',
+        );
+
+    }
+
+    # not searching for a single day
+    else {
+
+        #Search for the right field
+        $sb->_SQLLimit(
+            ALIAS         => $txn_alias,
+            FIELD         => 'Created',
+            OPERATOR      => $op,
+            VALUE         => $date->ISO,
+            @rest
+        );
+    }
+
+    $sb->_CloseParen;
+}
+
+=head2 _TransLimit
+
+Limit based on the ContentType or the Filename of a transaction.
+
+=cut
+
+sub _TransLimit {
+    my ( $self, $field, $op, $value, %rest ) = @_;
+
+    my $txn_alias = $self->JoinTransactions;
+    unless ( defined $self->{_sql_trattachalias} ) {
+        $self->{_sql_trattachalias} = $self->_SQLJoin(
+            TYPE   => 'LEFT', # not all txns have an attachment
+            ALIAS1 => $txn_alias,
+            FIELD1 => 'id',
+            TABLE2 => 'Attachments',
+            FIELD2 => 'TransactionId',
+        );
+    }
+
+    $self->_SQLLimit(
+        %rest,
+        ALIAS         => $self->{_sql_trattachalias},
+        FIELD         => $field,
+        OPERATOR      => $op,
+        VALUE         => $value,
+        CASESENSITIVE => 0,
+    );
+}
+
+=head2 _TransContentLimit
+
+Limit based on the Content of a transaction.
+
+=cut
+
+sub _TransContentLimit {
+
+    # Content search
+
+    # If only this was this simple.  We've got to do something
+    # complicated here:
+
+    #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.
+
+    # In the SQL, we might have
+    #       (( Content = foo ) or ( Content = bar AND Content = baz ))
+    # The AND group should share the same Alias.
+
+    # Actually, maybe it doesn't matter.  We use the same alias and it
+    # works itself out? (er.. different.)
+
+    # Steal more from _ProcessRestrictions
+
+    # FIXME: Maybe look at the previous FooLimit call, and if it was a
+    # TransLimit and EntryAggregator == AND, reuse the Aliases?
+
+    # 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.
+
+    # 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.
+
+    my ( $self, $field, $op, $value, %rest ) = @_;
+    $field = 'Content' if $field =~ /\W/;
+
+    my $config = RT->Config->Get('FullTextSearch') || {};
+    unless ( $config->{'Enable'} ) {
+        $self->_SQLLimit( %rest, FIELD => 'id', VALUE => 0 );
+        return;
+    }
+
+    my $txn_alias = $self->JoinTransactions;
+    unless ( defined $self->{_sql_trattachalias} ) {
+        $self->{_sql_trattachalias} = $self->_SQLJoin(
+            TYPE   => 'LEFT', # not all txns have an attachment
+            ALIAS1 => $txn_alias,
+            FIELD1 => 'id',
+            TABLE2 => 'Attachments',
+            FIELD2 => 'TransactionId',
+        );
+    }
+
+    $self->_OpenParen;
+    if ( $config->{'Indexed'} ) {
+        my $db_type = RT->Config->Get('DatabaseType');
+
+        my $alias;
+        if ( $config->{'Table'} and $config->{'Table'} ne "Attachments") {
+            $alias = $self->{'_sql_aliases'}{'full_text'} ||= $self->_SQLJoin(
+                TYPE   => 'LEFT',
+                ALIAS1 => $self->{'_sql_trattachalias'},
+                FIELD1 => 'id',
+                TABLE2 => $config->{'Table'},
+                FIELD2 => 'id',
+            );
+        } else {
+            $alias = $self->{'_sql_trattachalias'};
+        }
+
+        #XXX: handle negative searches
+        my $index = $config->{'Column'};
+        if ( $db_type eq 'Oracle' ) {
+            my $dbh = $RT::Handle->dbh;
+            my $alias = $self->{_sql_trattachalias};
+            $self->_SQLLimit(
+                %rest,
+                FUNCTION      => "CONTAINS( $alias.$field, ".$dbh->quote($value) .")",
+                OPERATOR      => '>',
+                VALUE         => 0,
+                QUOTEVALUE    => 0,
+                CASESENSITIVE => 1,
+            );
+            # this is required to trick DBIx::SB's LEFT JOINS optimizer
+            # into deciding that join is redundant as it is
+            $self->_SQLLimit(
+                ENTRYAGGREGATOR => 'AND',
+                ALIAS           => $self->{_sql_trattachalias},
+                FIELD           => 'Content',
+                OPERATOR        => 'IS NOT',
+                VALUE           => 'NULL',
+            );
+        }
+        elsif ( $db_type eq 'Pg' ) {
+            my $dbh = $RT::Handle->dbh;
+            $self->_SQLLimit(
+                %rest,
+                ALIAS       => $alias,
+                FIELD       => $index,
+                OPERATOR    => '@@',
+                VALUE       => 'plainto_tsquery('. $dbh->quote($value) .')',
+                QUOTEVALUE  => 0,
+            );
+        }
+        elsif ( $db_type eq 'mysql' ) {
+            # XXX: We could theoretically skip the join to Attachments,
+            # and have Sphinx simply index and group by the TicketId,
+            # and join Ticket.id to that attribute, which would be much
+            # more efficient -- however, this is only a possibility if
+            # there are no other transaction limits.
+
+            # This is a special character.  Note that \ does not escape
+            # itself (in Sphinx 2.1.0, at least), so 'foo\;bar' becoming
+            # 'foo\\;bar' is not a vulnerability, and is still parsed as
+            # "foo, \, ;, then bar".  Happily, the default mode is
+            # "all", meaning that boolean operators are not special.
+            $value =~ s/;/\\;/g;
+
+            my $max = $config->{'MaxMatches'};
+            $self->_SQLLimit(
+                %rest,
+                ALIAS       => $alias,
+                FIELD       => 'query',
+                OPERATOR    => '=',
+                VALUE       => "$value;limit=$max;maxmatches=$max",
+            );
+        }
+    } else {
+        $self->_SQLLimit(
+            %rest,
+            ALIAS         => $self->{_sql_trattachalias},
+            FIELD         => $field,
+            OPERATOR      => $op,
+            VALUE         => $value,
+            CASESENSITIVE => 0,
+        );
+    }
+    if ( RT->Config->Get('DontSearchFileAttachments') ) {
+        $self->_SQLLimit(
+            ENTRYAGGREGATOR => 'AND',
+            ALIAS           => $self->{_sql_trattachalias},
+            FIELD           => 'Filename',
+            OPERATOR        => 'IS',
+            VALUE           => 'NULL',
+        );
+    }
+    $self->_CloseParen;
+}
+
+=head2 _WatcherLimit
+
+Handle watcher limits.  (Requestor, CC, etc..)
+
+Meta Data:
+  1: Field to query on
+
+
+
+=cut
+
+sub _WatcherLimit {
+    my $self  = shift;
+    my $field = shift;
+    my $op    = shift;
+    my $value = shift;
+    my %rest  = (@_);
+
+    my $meta = $FIELD_METADATA{ $field };
+    my $type = $meta->[1] || '';
+    my $class = $meta->[2] || 'Ticket';
+
+    # Bail if the subfield is not allowed
+    if (    $rest{SUBKEY}
+        and not grep { $_ eq $rest{SUBKEY} } @{$SEARCHABLE_SUBFIELDS{'User'}})
+    {
+        die "Invalid watcher subfield: '$rest{SUBKEY}'";
+    }
+
+    # if it's equality op and search by Email or Name then we can preload user
+    # we do it to help some DBs better estimate number of rows and get better plans
+    if ( $op =~ /^!?=$/ && (!$rest{'SUBKEY'} || $rest{'SUBKEY'} eq 'Name' || $rest{'SUBKEY'} eq 'EmailAddress') ) {
+        my $o = RT::User->new( $self->CurrentUser );
+        my $method =
+            !$rest{'SUBKEY'}
+            ? $field eq 'Owner'? 'Load' : 'LoadByEmail'
+            : $rest{'SUBKEY'} eq 'EmailAddress' ? 'LoadByEmail': 'Load';
+        $o->$method( $value );
+        $rest{'SUBKEY'} = 'id';
+        $value = $o->id || 0;
+    }
+
+    # Owner was ENUM field, so "Owner = 'xxx'" allowed user to
+    # search by id and Name at the same time, this is workaround
+    # to preserve backward compatibility
+    if ( $field eq 'Owner' ) {
+        if ( ($rest{'SUBKEY'}||'') eq 'id' ) {
+            $self->_SQLLimit(
+                FIELD    => 'Owner',
+                OPERATOR => $op,
+                VALUE    => $value,
+                %rest,
+            );
+            return;
+        }
+    }
+    $rest{SUBKEY} ||= 'EmailAddress';
+
+    my ($groups, $group_members, $users);
+    if ( $rest{'BUNDLE'} ) {
+        ($groups, $group_members, $users) = @{ $rest{'BUNDLE'} };
+    } else {
+        $groups = $self->_RoleGroupsJoin( Type => $type, Class => $class, New => !$type );
+    }
+
+    $self->_OpenParen;
+    if ( $op =~ /^IS(?: NOT)?$/i ) {
+        # is [not] empty case
+
+        $group_members ||= $self->_GroupMembersJoin( GroupsAlias => $groups );
+        # to avoid joining the table Users into the query, we just join GM
+        # and make sure we don't match records where group is member of itself
+        $self->SUPER::Limit(
+            LEFTJOIN   => $group_members,
+            FIELD      => 'GroupId',
+            OPERATOR   => '!=',
+            VALUE      => "$group_members.MemberId",
+            QUOTEVALUE => 0,
+        );
+        $self->_SQLLimit(
+            ALIAS         => $group_members,
+            FIELD         => 'GroupId',
+            OPERATOR      => $op,
+            VALUE         => $value,
+            %rest,
+        );
+    }
+    elsif ( $op =~ /^!=$|^NOT\s+/i ) {
+        # negative condition case
+
+        # reverse op
+        $op =~ s/!|NOT\s+//i;
+
+        # XXX: we have no way to build correct "Watcher.X != 'Y'" when condition
+        # "X = 'Y'" matches more then one user so we try to fetch two records and
+        # do the right thing when there is only one exist and semi-working solution
+        # otherwise.
+        my $users_obj = RT::Users->new( $self->CurrentUser );
+        $users_obj->Limit(
+            FIELD         => $rest{SUBKEY},
+            OPERATOR      => $op,
+            VALUE         => $value,
+        );
+        $users_obj->OrderBy;
+        $users_obj->RowsPerPage(2);
+        my @users = @{ $users_obj->ItemsArrayRef };
+
+        $group_members ||= $self->_GroupMembersJoin( GroupsAlias => $groups );
+        if ( @users <= 1 ) {
+            my $uid = 0;
+            $uid = $users[0]->id if @users;
+            $self->SUPER::Limit(
+                LEFTJOIN      => $group_members,
+                ALIAS         => $group_members,
+                FIELD         => 'MemberId',
+                VALUE         => $uid,
+            );
+            $self->_SQLLimit(
+                %rest,
+                ALIAS           => $group_members,
+                FIELD           => 'id',
+                OPERATOR        => 'IS',
+                VALUE           => 'NULL',
+            );
+        } else {
+            $self->SUPER::Limit(
+                LEFTJOIN   => $group_members,
+                FIELD      => 'GroupId',
+                OPERATOR   => '!=',
+                VALUE      => "$group_members.MemberId",
+                QUOTEVALUE => 0,
+            );
+            $users ||= $self->Join(
+                TYPE            => 'LEFT',
+                ALIAS1          => $group_members,
+                FIELD1          => 'MemberId',
+                TABLE2          => 'Users',
+                FIELD2          => 'id',
+            );
+            $self->SUPER::Limit(
+                LEFTJOIN      => $users,
+                ALIAS         => $users,
+                FIELD         => $rest{SUBKEY},
+                OPERATOR      => $op,
+                VALUE         => $value,
+                CASESENSITIVE => 0,
+            );
+            $self->_SQLLimit(
+                %rest,
+                ALIAS         => $users,
+                FIELD         => 'id',
+                OPERATOR      => 'IS',
+                VALUE         => 'NULL',
+            );
+        }
+    } else {
+        # positive condition case
+
+        $group_members ||= $self->_GroupMembersJoin(
+            GroupsAlias => $groups, New => 1, Left => 0
+        );
+        $users ||= $self->Join(
+            TYPE            => 'LEFT',
+            ALIAS1          => $group_members,
+            FIELD1          => 'MemberId',
+            TABLE2          => 'Users',
+            FIELD2          => 'id',
+        );
+        $self->_SQLLimit(
+            %rest,
+            ALIAS           => $users,
+            FIELD           => $rest{'SUBKEY'},
+            VALUE           => $value,
+            OPERATOR        => $op,
+            CASESENSITIVE   => 0,
+        );
+    }
+    $self->_CloseParen;
+    return ($groups, $group_members, $users);
+}
+
+sub _RoleGroupsJoin {
     my $self = shift;
     my $self = shift;
-    my %args = ( FIELD => undef,
-                OPERATOR => '=',
-                VALUE => undef,
-                DESCRIPTION => undef,
-                @_
-              );
-   $args{'DESCRIPTION'} = "Autodescribed: ".$args{'FIELD'} . $args{'OPERATOR'} . $args{'VALUE'},
-    if (!defined $args{'DESCRIPTION'}) ;
+    my %args = (New => 0, Class => 'Ticket', Type => '', @_);
+    return $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
+        if $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
+           && !$args{'New'};
+
+    # we always have watcher groups for ticket, so we use INNER join
+    my $groups = $self->Join(
+        ALIAS1          => 'main',
+        FIELD1          => $args{'Class'} eq 'Queue'? 'Queue': 'id',
+        TABLE2          => 'Groups',
+        FIELD2          => 'Instance',
+        ENTRYAGGREGATOR => 'AND',
+    );
+    $self->SUPER::Limit(
+        LEFTJOIN        => $groups,
+        ALIAS           => $groups,
+        FIELD           => 'Domain',
+        VALUE           => 'RT::'. $args{'Class'} .'-Role',
+    );
+    $self->SUPER::Limit(
+        LEFTJOIN        => $groups,
+        ALIAS           => $groups,
+        FIELD           => 'Type',
+        VALUE           => $args{'Type'},
+    ) if $args{'Type'};
+
+    $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} } = $groups
+        unless $args{'New'};
+
+    return $groups;
+}
 
 
-    my $index = $self->_NextIndex;
-    
-    #make the TicketRestrictions hash the equivalent of whatever we just passed in;
-    
-    %{$self->{'TicketRestrictions'}{$index}} = %args;
+sub _GroupMembersJoin {
+    my $self = shift;
+    my %args = (New => 1, GroupsAlias => undef, Left => 1, @_);
+
+    return $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
+        if $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
+            && !$args{'New'};
+
+    my $alias = $self->Join(
+        $args{'Left'} ? (TYPE            => 'LEFT') : (),
+        ALIAS1          => $args{'GroupsAlias'},
+        FIELD1          => 'id',
+        TABLE2          => 'CachedGroupMembers',
+        FIELD2          => 'GroupId',
+        ENTRYAGGREGATOR => 'AND',
+    );
+    $self->SUPER::Limit(
+        $args{'Left'} ? (LEFTJOIN => $alias) : (),
+        ALIAS => $alias,
+        FIELD => 'Disabled',
+        VALUE => 0,
+    );
+
+    $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
+        unless $args{'New'};
+
+    return $alias;
+}
 
 
-    $self->{'RecalcTicketLimits'} = 1;
+=head2 _WatcherJoin
 
 
-    # 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;
+Helper function which provides joins to a watchers table both for limits
+and for ordering.
+
+=cut
+
+sub _WatcherJoin {
+    my $self = shift;
+    my $type = shift || '';
+
+
+    my $groups = $self->_RoleGroupsJoin( Type => $type );
+    my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
+    # XXX: work around, we must hide groups that
+    # are members of the role group we search in,
+    # otherwise them result in wrong NULLs in Users
+    # table and break ordering. Now, we know that
+    # RT doesn't allow to add groups as members of the
+    # ticket roles, so we just hide entries in CGM table
+    # with MemberId == GroupId from results
+    $self->SUPER::Limit(
+        LEFTJOIN   => $group_members,
+        FIELD      => 'GroupId',
+        OPERATOR   => '!=',
+        VALUE      => "$group_members.MemberId",
+        QUOTEVALUE => 0,
+    );
+    my $users = $self->Join(
+        TYPE            => 'LEFT',
+        ALIAS1          => $group_members,
+        FIELD1          => 'MemberId',
+        TABLE2          => 'Users',
+        FIELD2          => 'id',
+    );
+    return ($groups, $group_members, $users);
+}
+
+=head2 _WatcherMembershipLimit
+
+Handle watcher membership limits, i.e. whether the watcher belongs to a
+specific group or not.
+
+Meta Data:
+  1: Field to query on
+
+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
+
+=cut
+
+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,
+        );
     }
 
     }
 
-    return ($index);
+    # 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 = $FIELD_METADATA{$field};
+    my $type = ( defined $meta->[1] ? $meta->[1] : undef );
+
+    if ($type) {
+        $self->_SQLLimit(
+            ALIAS           => $groups,
+            FIELD           => 'Type',
+            VALUE           => $type,
+            ENTRYAGGREGATOR => 'AND'
+        );
+    }
+
+    $self->Join(
+        ALIAS1 => $groups,
+        FIELD1 => 'id',
+        ALIAS2 => $groupmembers,
+        FIELD2 => 'GroupId'
+    );
+
+    $self->Join(
+        ALIAS1 => $groupmembers,
+        FIELD1 => 'MemberId',
+        ALIAS2 => $users,
+        FIELD2 => 'id'
+    );
+
+    $self->Limit(
+        ALIAS => $groupmembers,
+        FIELD => 'Disabled',
+        VALUE => 0,
+    );
+
+    $self->Join(
+        ALIAS1 => $memberships,
+        FIELD1 => 'MemberId',
+        ALIAS2 => $users,
+        FIELD2 => 'id'
+    );
+
+    $self->Limit(
+        ALIAS => $memberships,
+        FIELD => 'Disabled',
+        VALUE => 0,
+    );
+
+
+    $self->_CloseParen;
+
 }
 
 }
 
-# }}}
+=head2 _CustomFieldDecipher
+
+Try and turn a CF descriptor into (cfid, cfname) object pair.
 
 
+Takes an optional second parameter of the CF LookupType, defaults to Ticket CFs.
 
 
+=cut
 
 
+sub _CustomFieldDecipher {
+    my ($self, $string, $lookuptype) = @_;
+    $lookuptype ||= $self->_SingularClass->CustomFieldLookupType;
 
 
-=head2 FreezeLimits
+    my ($object, $field, $column) = ($string =~ /^(?:(.+?)\.)?\{(.+)\}(?:\.(Content|LargeContent))?$/);
+    $field ||= ($string =~ /^{(.*?)}$/)[0] || $string;
 
 
-Returns a frozen string suitable for handing back to ThawLimits.
+    my ($cf, $applied_to);
+
+    if ( $object ) {
+        my $record_class = RT::CustomField->RecordClassFromLookupType($lookuptype);
+        $applied_to = $record_class->new( $self->CurrentUser );
+        $applied_to->Load( $object );
+
+        if ( $applied_to->id ) {
+            RT->Logger->debug("Limiting to CFs identified by '$field' applied to $record_class #@{[$applied_to->id]} (loaded via '$object')");
+        }
+        else {
+            RT->Logger->warning("$record_class '$object' doesn't exist, parsed from '$string'");
+            $object = 0;
+            undef $applied_to;
+        }
+    }
+
+    if ( $field =~ /\D/ ) {
+        $object ||= '';
+        my $cfs = RT::CustomFields->new( $self->CurrentUser );
+        $cfs->Limit( FIELD => 'Name', VALUE => $field, ($applied_to ? (CASESENSITIVE => 0) : ()) );
+        $cfs->LimitToLookupType($lookuptype);
+
+        if ($applied_to) {
+            $cfs->SetContextObject($applied_to);
+            $cfs->LimitToObjectId($applied_to->id);
+        }
+
+        # if there is more then one field the current user can
+        # see with the same name then we shouldn't return cf object
+        # as we don't know which one to use
+        $cf = $cfs->First;
+        if ( $cf ) {
+            $cf = undef if $cfs->Next;
+        }
+    }
+    else {
+        $cf = RT::CustomField->new( $self->CurrentUser );
+        $cf->Load( $field );
+        $cf->SetContextObject($applied_to)
+            if $cf->id and $applied_to;
+    }
+
+    return ($object, $field, $cf, $column);
+}
+
+=head2 _CustomFieldJoin
+
+Factor out the Join of custom fields so we can use it for sorting too
 
 =cut
 
 =cut
-# {{{ sub FreezeLimits
 
 
-sub FreezeLimits {
-       my $self = shift;
-       require FreezeThaw;
-       return (FreezeThaw::freeze($self->{'TicketRestrictions'},
-                                  $self->{'restriction_index'}
-                                 ));
+our %JOIN_ALIAS_FOR_LOOKUP_TYPE = (
+    RT::Ticket->CustomFieldLookupType      => sub { "main" },
+);
+
+sub _CustomFieldJoin {
+    my ($self, $cfkey, $cfid, $field, $type) = @_;
+    $type ||= RT::Ticket->CustomFieldLookupType;
+
+    # Perform one Join per CustomField
+    if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
+         $self->{_sql_cf_alias}{$cfkey} )
+    {
+        return ( $self->{_sql_object_cfv_alias}{$cfkey},
+                 $self->{_sql_cf_alias}{$cfkey} );
+    }
+
+    my $ObjectAlias = $JOIN_ALIAS_FOR_LOOKUP_TYPE{$type}
+        ? $JOIN_ALIAS_FOR_LOOKUP_TYPE{$type}->($self)
+        : die "We don't know how to join on $type";
+
+    my ($ObjectCFs, $CFs);
+    if ( $cfid ) {
+        $ObjectCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
+            TYPE   => 'LEFT',
+            ALIAS1 => $ObjectAlias,
+            FIELD1 => 'id',
+            TABLE2 => 'ObjectCustomFieldValues',
+            FIELD2 => 'ObjectId',
+        );
+        $self->SUPER::Limit(
+            LEFTJOIN        => $ObjectCFs,
+            FIELD           => 'CustomField',
+            VALUE           => $cfid,
+            ENTRYAGGREGATOR => 'AND'
+        );
+    }
+    else {
+        my $ocfalias = $self->Join(
+            TYPE       => 'LEFT',
+            FIELD1     => 'Queue',
+            TABLE2     => 'ObjectCustomFields',
+            FIELD2     => 'ObjectId',
+        );
+
+        $self->SUPER::Limit(
+            LEFTJOIN        => $ocfalias,
+            ENTRYAGGREGATOR => 'OR',
+            FIELD           => 'ObjectId',
+            VALUE           => '0',
+        );
+
+        $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
+            TYPE       => 'LEFT',
+            ALIAS1     => $ocfalias,
+            FIELD1     => 'CustomField',
+            TABLE2     => 'CustomFields',
+            FIELD2     => 'id',
+        );
+        $self->SUPER::Limit(
+            LEFTJOIN        => $CFs,
+            ENTRYAGGREGATOR => 'AND',
+            FIELD           => 'LookupType',
+            VALUE           => $type,
+        );
+        $self->SUPER::Limit(
+            LEFTJOIN        => $CFs,
+            ENTRYAGGREGATOR => 'AND',
+            FIELD           => 'Name',
+            VALUE           => $field,
+        );
+
+        $ObjectCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
+            TYPE   => 'LEFT',
+            ALIAS1 => $CFs,
+            FIELD1 => 'id',
+            TABLE2 => 'ObjectCustomFieldValues',
+            FIELD2 => 'CustomField',
+        );
+        $self->SUPER::Limit(
+            LEFTJOIN        => $ObjectCFs,
+            FIELD           => 'ObjectId',
+            VALUE           => "$ObjectAlias.id",
+            QUOTEVALUE      => 0,
+            ENTRYAGGREGATOR => 'AND',
+        );
+    }
+
+    $self->SUPER::Limit(
+        LEFTJOIN        => $ObjectCFs,
+        FIELD           => 'ObjectType',
+        VALUE           => RT::CustomField->ObjectTypeFromLookupType($type),
+        ENTRYAGGREGATOR => 'AND'
+    );
+    $self->SUPER::Limit(
+        LEFTJOIN        => $ObjectCFs,
+        FIELD           => 'Disabled',
+        OPERATOR        => '=',
+        VALUE           => '0',
+        ENTRYAGGREGATOR => 'AND'
+    );
+
+    return ($ObjectCFs, $CFs);
 }
 
 }
 
-# }}}
+=head2 _CustomFieldLimit
 
 
-=head2 ThawLimits
+Limit based on CustomFields
 
 
-Take a frozen Limits string generated by FreezeLimits and make this tickets
-object have that set of limits.
+Meta Data:
+  none
 
 =cut
 
 =cut
-# {{{ sub ThawLimits
 
 
-sub ThawLimits {
-       my $self = shift;
-       my $in = shift;
-       
-       #if we don't have $in, get outta here.
-       return undef unless ($in);
+use Regexp::Common qw(RE_net_IPv4);
+use Regexp::Common::net::CIDR;
+
+
+sub _CustomFieldLimit {
+    my ( $self, $_field, $op, $value, %rest ) = @_;
+
+    my $meta  = $FIELD_METADATA{ $_field };
+    my $class = $meta->[1] || 'Ticket';
+    my $type  = "RT::$class"->CustomFieldLookupType;
 
 
-       $self->{'RecalcTicketLimits'} = 1;
+    my $field = $rest{'SUBKEY'} || die "No field specified";
 
 
-       require FreezeThaw;
-       
-       #We don't need to die if the thaw fails.
-       
-       eval {
-               ($self->{'TicketRestrictions'},
-               $self->{'restriction_index'}
-               ) = FreezeThaw::thaw($in);
-       }
+    # For our sanity, we can only limit on one queue at a time
 
 
+    my ($object, $cfid, $cf, $column);
+    ($object, $field, $cf, $column) = $self->_CustomFieldDecipher( $field, $type );
+    $cfid = $cf ? $cf->id  : 0 ;
+
+# If we're trying to find custom fields that don't match something, we
+# want tickets where the custom field has no value at all.  Note that
+# we explicitly don't include the "IS NULL" case, since we would
+# otherwise end up with a redundant clause.
+
+    my ($negative_op, $null_op, $inv_op, $range_op)
+        = $self->ClassifySQLOperation( $op );
+
+    my $fix_op = sub {
+        return @_ unless RT->Config->Get('DatabaseType') eq 'Oracle';
+
+        my %args = @_;
+        return %args unless $args{'FIELD'} eq 'LargeContent';
+        
+        my $op = $args{'OPERATOR'};
+        if ( $op eq '=' ) {
+            $args{'OPERATOR'} = 'MATCHES';
+        }
+        elsif ( $op eq '!=' ) {
+            $args{'OPERATOR'} = 'NOT MATCHES';
+        }
+        elsif ( $op =~ /^[<>]=?$/ ) {
+            $args{'FUNCTION'} = "TO_CHAR( $args{'ALIAS'}.LargeContent )";
+        }
+        return %args;
+    };
+
+    if ( $cf && $cf->Type eq 'IPAddress' ) {
+        my $parsed = RT::ObjectCustomFieldValue->ParseIP($value);
+        if ($parsed) {
+            $value = $parsed;
+        }
+        else {
+            $RT::Logger->warn("$value is not a valid IPAddress");
+        }
+    }
+
+    if ( $cf && $cf->Type eq 'IPAddressRange' ) {
+        my ( $start_ip, $end_ip ) =
+          RT::ObjectCustomFieldValue->ParseIPRange($value);
+        if ( $start_ip && $end_ip ) {
+            if ( $op =~ /^([<>])=?$/ ) {
+                my $is_less = $1 eq '<' ? 1 : 0;
+                if ( $is_less ) {
+                    $value = $start_ip;
+                }
+                else {
+                    $value = $end_ip;
+                }
+            }
+            else {
+                $value = join '-', $start_ip, $end_ip;
+            }
+        }
+        else {
+            $RT::Logger->warn("$value is not a valid IPAddressRange");
+        }
+    }
+
+    if ( $cf && $cf->Type =~ /^Date(?:Time)?$/ ) {
+        my $date = RT::Date->new( $self->CurrentUser );
+        $date->Set( Format => 'unknown', Value => $value );
+        if ( $date->Unix ) {
+
+            if (
+                   $cf->Type eq 'Date'
+                || $value =~ /^\s*(?:today|tomorrow|yesterday)\s*$/i
+                || (   $value !~ /midnight|\d+:\d+:\d+/i
+                    && $date->Time( Timezone => 'user' ) eq '00:00:00' )
+              )
+            {
+                $value = $date->Date( Timezone => 'user' );
+            }
+            else {
+                $value = $date->DateTime;
+            }
+        }
+        else {
+            $RT::Logger->warn("$value is not a valid date string");
+        }
+    }
+
+    my $single_value = !$cf || !$cfid || $cf->SingleValue;
+
+    my $cfkey = $cfid ? $cfid : "$type-$object.$field";
+
+    if ( $null_op && !$column ) {
+        # IS[ NOT] NULL without column is the same as has[ no] any CF value,
+        # we can reuse our default joins for this operation
+        # with column specified we have different situation
+        my ($ObjectCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field, $type );
+        $self->_OpenParen;
+        $self->_SQLLimit(
+            ALIAS    => $ObjectCFs,
+            FIELD    => 'id',
+            OPERATOR => $op,
+            VALUE    => $value,
+            %rest
+        );
+        $self->_SQLLimit(
+            ALIAS      => $CFs,
+            FIELD      => 'Name',
+            OPERATOR   => 'IS NOT',
+            VALUE      => 'NULL',
+            QUOTEVALUE => 0,
+            ENTRYAGGREGATOR => 'AND',
+        ) if $CFs;
+        $self->_CloseParen;
+    }
+    elsif ( $op !~ /^[<>]=?$/ && (  $cf && $cf->Type eq 'IPAddressRange')) {
+    
+        my ($start_ip, $end_ip) = split /-/, $value;
+        
+        $self->_OpenParen;
+        if ( $op !~ /NOT|!=|<>/i ) { # positive equation
+            $self->_CustomFieldLimit(
+                $_field, '<=', $end_ip, %rest,
+                SUBKEY => $rest{'SUBKEY'}. '.Content',
+            );
+            $self->_CustomFieldLimit(
+                $_field, '>=', $start_ip, %rest,
+                SUBKEY          => $rest{'SUBKEY'}. '.LargeContent',
+                ENTRYAGGREGATOR => 'AND',
+            ); 
+            # as well limit borders so DB optimizers can use better
+            # estimations and scan less rows
+# have to disable this tweak because of ipv6
+#            $self->_CustomFieldLimit(
+#                $_field, '>=', '000.000.000.000', %rest,
+#                SUBKEY          => $rest{'SUBKEY'}. '.Content',
+#                ENTRYAGGREGATOR => 'AND',
+#            );
+#            $self->_CustomFieldLimit(
+#                $_field, '<=', '255.255.255.255', %rest,
+#                SUBKEY          => $rest{'SUBKEY'}. '.LargeContent',
+#                ENTRYAGGREGATOR => 'AND',
+#            );  
+        }       
+        else { # negative equation
+            $self->_CustomFieldLimit($_field, '>', $end_ip, %rest);
+            $self->_CustomFieldLimit(
+                $_field, '<', $start_ip, %rest,
+                SUBKEY          => $rest{'SUBKEY'}. '.LargeContent',
+                ENTRYAGGREGATOR => 'OR',
+            );  
+            # TODO: as well limit borders so DB optimizers can use better
+            # estimations and scan less rows, but it's harder to do
+            # as we have OR aggregator
+        }
+        $self->_CloseParen;
+    } 
+    elsif ( !$negative_op || $single_value ) {
+        $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
+        my ($ObjectCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field, $type );
+
+        $self->_OpenParen;
+
+        $self->_OpenParen;
+
+        $self->_OpenParen;
+        # if column is defined then deal only with it
+        # otherwise search in Content and in LargeContent
+        if ( $column ) {
+            $self->_SQLLimit( $fix_op->(
+                ALIAS      => $ObjectCFs,
+                FIELD      => $column,
+                OPERATOR   => $op,
+                VALUE      => $value,
+                CASESENSITIVE => 0,
+                %rest
+            ) );
+            $self->_CloseParen;
+            $self->_CloseParen;
+            $self->_CloseParen;
+        }
+        else {
+            # need special treatment for Date
+            if ( $cf and $cf->Type eq 'DateTime' and $op eq '=' && $value !~ /:/ ) {
+                # no time specified, that means we want everything on a
+                # particular day.  in the database, we need to check for >
+                # and < the edges of that day.
+                    my $date = RT::Date->new( $self->CurrentUser );
+                    $date->Set( Format => 'unknown', Value => $value );
+                    my $daystart = $date->ISO;
+                    $date->AddDay;
+                    my $dayend = $date->ISO;
+
+                    $self->_OpenParen;
+
+                    $self->_SQLLimit(
+                        ALIAS    => $ObjectCFs,
+                        FIELD    => 'Content',
+                        OPERATOR => ">=",
+                        VALUE    => $daystart,
+                        %rest,
+                    );
+
+                    $self->_SQLLimit(
+                        ALIAS    => $ObjectCFs,
+                        FIELD    => 'Content',
+                        OPERATOR => "<",
+                        VALUE    => $dayend,
+                        %rest,
+                        ENTRYAGGREGATOR => 'AND',
+                    );
+
+                    $self->_CloseParen;
+            }
+            elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
+                if ( length( Encode::encode( "UTF-8", $value) ) < 256 ) {
+                    $self->_SQLLimit(
+                        ALIAS    => $ObjectCFs,
+                        FIELD    => 'Content',
+                        OPERATOR => $op,
+                        VALUE    => $value,
+                        CASESENSITIVE => 0,
+                        %rest
+                    );
+                }
+                else {
+                    $self->_OpenParen;
+                    $self->_SQLLimit(
+                        ALIAS           => $ObjectCFs,
+                        FIELD           => 'Content',
+                        OPERATOR        => '=',
+                        VALUE           => '',
+                        ENTRYAGGREGATOR => 'OR'
+                    );
+                    $self->_SQLLimit(
+                        ALIAS           => $ObjectCFs,
+                        FIELD           => 'Content',
+                        OPERATOR        => 'IS',
+                        VALUE           => 'NULL',
+                        ENTRYAGGREGATOR => 'OR'
+                    );
+                    $self->_CloseParen;
+                    $self->_SQLLimit( $fix_op->(
+                        ALIAS           => $ObjectCFs,
+                        FIELD           => 'LargeContent',
+                        OPERATOR        => $op,
+                        VALUE           => $value,
+                        ENTRYAGGREGATOR => 'AND',
+                        CASESENSITIVE => 0,
+                    ) );
+                }
+            }
+            else {
+                $self->_SQLLimit(
+                    ALIAS    => $ObjectCFs,
+                    FIELD    => 'Content',
+                    OPERATOR => $op,
+                    VALUE    => $value,
+                    CASESENSITIVE => 0,
+                    %rest
+                );
+
+                $self->_OpenParen;
+                $self->_OpenParen;
+                $self->_SQLLimit(
+                    ALIAS           => $ObjectCFs,
+                    FIELD           => 'Content',
+                    OPERATOR        => '=',
+                    VALUE           => '',
+                    ENTRYAGGREGATOR => 'OR'
+                );
+                $self->_SQLLimit(
+                    ALIAS           => $ObjectCFs,
+                    FIELD           => 'Content',
+                    OPERATOR        => 'IS',
+                    VALUE           => 'NULL',
+                    ENTRYAGGREGATOR => 'OR'
+                );
+                $self->_CloseParen;
+                $self->_SQLLimit( $fix_op->(
+                    ALIAS           => $ObjectCFs,
+                    FIELD           => 'LargeContent',
+                    OPERATOR        => $op,
+                    VALUE           => $value,
+                    ENTRYAGGREGATOR => 'AND',
+                    CASESENSITIVE => 0,
+                ) );
+                $self->_CloseParen;
+            }
+            $self->_CloseParen;
+
+            # XXX: if we join via CustomFields table then
+            # because of order of left joins we get NULLs in
+            # CF table and then get nulls for those records
+            # in OCFVs table what result in wrong results
+            # as decifer method now tries to load a CF then
+            # we fall into this situation only when there
+            # are more than one CF with the name in the DB.
+            # the same thing applies to order by call.
+            # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
+            # we want treat IS NULL as (not applies or has
+            # no value)
+            $self->_SQLLimit(
+                ALIAS           => $CFs,
+                FIELD           => 'Name',
+                OPERATOR        => 'IS NOT',
+                VALUE           => 'NULL',
+                QUOTEVALUE      => 0,
+                ENTRYAGGREGATOR => 'AND',
+            ) if $CFs;
+            $self->_CloseParen;
+
+            if ($negative_op) {
+                $self->_SQLLimit(
+                    ALIAS           => $ObjectCFs,
+                    FIELD           => $column || 'Content',
+                    OPERATOR        => 'IS',
+                    VALUE           => 'NULL',
+                    QUOTEVALUE      => 0,
+                    ENTRYAGGREGATOR => 'OR',
+                );
+            }
+
+            $self->_CloseParen;
+        }
+    }
+    else {
+        $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
+        my ($ObjectCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field, $type );
+
+        # reverse operation
+        $op =~ s/!|NOT\s+//i;
+
+        # if column is defined then deal only with it
+        # otherwise search in Content and in LargeContent
+        if ( $column ) {
+            $self->SUPER::Limit( $fix_op->(
+                LEFTJOIN   => $ObjectCFs,
+                ALIAS      => $ObjectCFs,
+                FIELD      => $column,
+                OPERATOR   => $op,
+                VALUE      => $value,
+                CASESENSITIVE => 0,
+            ) );
+        }
+        else {
+            $self->SUPER::Limit(
+                LEFTJOIN   => $ObjectCFs,
+                ALIAS      => $ObjectCFs,
+                FIELD      => 'Content',
+                OPERATOR   => $op,
+                VALUE      => $value,
+                CASESENSITIVE => 0,
+            );
+        }
+        $self->_SQLLimit(
+            %rest,
+            ALIAS      => $ObjectCFs,
+            FIELD      => 'id',
+            OPERATOR   => 'IS',
+            VALUE      => 'NULL',
+            QUOTEVALUE => 0,
+        );
+    }
+}
+
+sub _HasAttributeLimit {
+    my ( $self, $field, $op, $value, %rest ) = @_;
+
+    my $alias = $self->Join(
+        TYPE   => 'LEFT',
+        ALIAS1 => 'main',
+        FIELD1 => 'id',
+        TABLE2 => 'Attributes',
+        FIELD2 => 'ObjectId',
+    );
+    $self->SUPER::Limit(
+        LEFTJOIN        => $alias,
+        FIELD           => 'ObjectType',
+        VALUE           => 'RT::Ticket',
+        ENTRYAGGREGATOR => 'AND'
+    );
+    $self->SUPER::Limit(
+        LEFTJOIN        => $alias,
+        FIELD           => 'Name',
+        OPERATOR        => $op,
+        VALUE           => $value,
+        ENTRYAGGREGATOR => 'AND'
+    );
+    $self->_SQLLimit(
+        %rest,
+        ALIAS      => $alias,
+        FIELD      => 'id',
+        OPERATOR   => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
+        VALUE      => 'NULL',
+        QUOTEVALUE => 0,
+    );
+}
+
+# End Helper Functions
+
+# End of SQL Stuff -------------------------------------------------
+
+
+=head2 OrderByCols ARRAY
+
+A modified version of the OrderBy method which automatically joins where
+C<ALIAS> is set to the name of a watcher type.
+
+=cut
+
+sub OrderByCols {
+    my $self = shift;
+    my @args = @_;
+    my $clause;
+    my @res   = ();
+    my $order = 0;
+
+    foreach my $row (@args) {
+        if ( $row->{ALIAS} ) {
+            push @res, $row;
+            next;
+        }
+        if ( $row->{FIELD} !~ /\./ ) {
+            my $meta = $self->FIELDS->{ $row->{FIELD} };
+            unless ( $meta ) {
+                push @res, $row;
+                next;
+            }
+
+            if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
+                my $alias = $self->Join(
+                    TYPE   => 'LEFT',
+                    ALIAS1 => 'main',
+                    FIELD1 => $row->{'FIELD'},
+                    TABLE2 => 'Queues',
+                    FIELD2 => 'id',
+                );
+                push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
+            } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
+                || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
+            ) {
+                my $alias = $self->Join(
+                    TYPE   => 'LEFT',
+                    ALIAS1 => 'main',
+                    FIELD1 => $row->{'FIELD'},
+                    TABLE2 => 'Users',
+                    FIELD2 => 'id',
+                );
+                push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
+            } else {
+                push @res, $row;
+            }
+            next;
+        }
+
+        my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
+        my $meta = $self->FIELDS->{$field};
+        if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
+            # cache alias as we want to use one alias per watcher type for sorting
+            my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
+            unless ( $users ) {
+                $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
+                    = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
+            }
+            push @res, { %$row, ALIAS => $users, FIELD => $subkey };
+       } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
+           my ($object, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
+           my $cfkey = $cf_obj ? $cf_obj->id : "$object.$field";
+           $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
+           my ($ObjectCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
+           # this is described in _CustomFieldLimit
+           $self->_SQLLimit(
+               ALIAS      => $CFs,
+               FIELD      => 'Name',
+               OPERATOR   => 'IS NOT',
+               VALUE      => 'NULL',
+               QUOTEVALUE => 1,
+               ENTRYAGGREGATOR => 'AND',
+           ) if $CFs;
+           my $CFvs = $self->Join(
+               TYPE   => 'LEFT',
+               ALIAS1 => $ObjectCFs,
+               FIELD1 => 'CustomField',
+               TABLE2 => 'CustomFieldValues',
+               FIELD2 => 'CustomField',
+           );
+           $self->SUPER::Limit(
+               LEFTJOIN        => $CFvs,
+               FIELD           => 'Name',
+               QUOTEVALUE      => 0,
+               VALUE           => $ObjectCFs . ".Content",
+               ENTRYAGGREGATOR => 'AND'
+           );
+
+           push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
+           push @res, { %$row, ALIAS => $ObjectCFs, FIELD => 'Content' };
+       } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
+           # PAW logic is "reversed"
+           my $order = "ASC";
+           if (exists $row->{ORDER} ) {
+               my $o = $row->{ORDER};
+               delete $row->{ORDER};
+               $order = "DESC" if $o =~ /asc/i;
+           }
+
+           # Ticket.Owner    1 0 X
+           # Unowned Tickets 0 1 X
+           # Else            0 0 X
+
+           foreach my $uid ( $self->CurrentUser->Id, RT->Nobody->Id ) {
+               if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
+                   my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
+                   push @res, {
+                       %$row,
+                       FIELD => undef,
+                       ALIAS => '',
+                       FUNCTION => "CASE WHEN $f=$uid THEN 1 ELSE 0 END",
+                       ORDER => $order
+                   };
+               } else {
+                   push @res, {
+                       %$row,
+                       FIELD => undef,
+                       FUNCTION => "Owner=$uid",
+                       ORDER => $order
+                   };
+               }
+           }
+
+           push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
+
+       } elsif ( $field eq 'Customer' ) { #Freeside
+           # OrderBy(FIELD => expression) doesn't work, it has to be 
+           # an actual field, so we have to do the join even if sorting
+           # by custnum
+           my $custalias = $self->JoinToCustomer;
+           my $cust_field = lc($subkey);
+           if ( !$cust_field or $cust_field eq 'number' ) {
+               $cust_field = 'custnum';
+           }
+           elsif ( $cust_field eq 'name' ) {
+               $cust_field = "COALESCE( $custalias.company,
+               $custalias.last || ', ' || $custalias.first
+               )";
+           }
+           else { # order by cust_main fields directly: 'Customer.agentnum'
+               $cust_field = $subkey;
+           }
+           push @res, { %$row, ALIAS => $custalias, FIELD => $cust_field };
+
+      } elsif ( $field eq 'Service' ) {
+          
+          my $svcalias = $self->JoinToService;
+          my $svc_field = lc($subkey);
+          if ( !$svc_field or $svc_field eq 'number' ) {
+              $svc_field = 'svcnum';
+          }
+          push @res, { %$row, ALIAS => $svcalias, FIELD => $svc_field };
+
+       } #Freeside
+
+       else {
+           push @res, $row;
+       }
+    }
+    return $self->SUPER::OrderByCols(@res);
+}
+
+#Freeside
+
+sub JoinToCustLinks {
+    # Set up join to links (id = localbase),
+    # limit link type to 'MemberOf',
+    # and target value to any Freeside custnum URI.
+    # Return the linkalias for further join/limit action,
+    # and an sql expression to retrieve the custnum.
+    my $self = shift;
+    # only join once for each RT::Tickets object
+    my $linkalias = $self->{cust_main_linkalias};
+    if (!$linkalias) {
+        $linkalias = $self->Join(
+            TYPE   => 'LEFT',
+            ALIAS1 => 'main',
+            FIELD1 => 'id',
+            TABLE2 => 'Links',
+            FIELD2 => 'LocalBase',
+        );
+       $self->SUPER::Limit(
+         LEFTJOIN => $linkalias,
+         FIELD    => 'Base',
+         OPERATOR => 'LIKE',
+         VALUE    => 'fsck.com-rt://%/ticket/%',
+       );
+        $self->SUPER::Limit(
+            LEFTJOIN => $linkalias,
+            FIELD    => 'Type',
+            OPERATOR => '=',
+            VALUE    => 'MemberOf',
+        );
+        $self->SUPER::Limit(
+            LEFTJOIN => $linkalias,
+            FIELD    => 'Target',
+            OPERATOR => 'STARTSWITH',
+            VALUE    => 'freeside://freeside/cust_main/',
+        );
+        $self->{cust_main_linkalias} = $linkalias;
+    }
+    my $custnum_sql = "CAST(SUBSTR($linkalias.Target,31) AS ";
+    if ( RT->Config->Get('DatabaseType') eq 'mysql' ) {
+        $custnum_sql .= 'SIGNED INTEGER)';
+    }
+    else {
+        $custnum_sql .= 'INTEGER)';
+    }
+    return ($linkalias, $custnum_sql);
+}
+
+sub JoinToCustomer {
+    my $self = shift;
+    my ($linkalias, $custnum_sql) = $self->JoinToCustLinks;
+    # don't reuse this join, though--negative queries need 
+    # independent joins
+    my $custalias = $self->Join(
+        TYPE       => 'LEFT',
+        EXPRESSION => $custnum_sql,
+        TABLE2     => 'cust_main',
+        FIELD2     => 'custnum',
+    );
+    return $custalias;
+}
+
+sub JoinToSvcLinks {
+    my $self = shift;
+    my $linkalias = $self->{cust_svc_linkalias};
+    if (!$linkalias) {
+        $linkalias = $self->Join(
+            TYPE   => 'LEFT',
+            ALIAS1 => 'main',
+            FIELD1 => 'id',
+            TABLE2 => 'Links',
+            FIELD2 => 'LocalBase',
+        );
+       $self->SUPER::Limit(
+         LEFTJOIN => $linkalias,
+         FIELD    => 'Base',
+         OPERATOR => 'LIKE',
+         VALUE    => 'fsck.com-rt://%/ticket/%',
+       );
+
+        $self->SUPER::Limit(
+            LEFTJOIN => $linkalias,
+            FIELD    => 'Type',
+            OPERATOR => '=',
+            VALUE    => 'MemberOf',
+        );
+        $self->SUPER::Limit(
+            LEFTJOIN => $linkalias,
+            FIELD    => 'Target',
+            OPERATOR => 'STARTSWITH',
+            VALUE    => 'freeside://freeside/cust_svc/',
+        );
+        $self->{cust_svc_linkalias} = $linkalias;
+    }
+    my $svcnum_sql = "CAST(SUBSTR($linkalias.Target,30) AS ";
+    if ( RT->Config->Get('DatabaseType') eq 'mysql' ) {
+        $svcnum_sql .= 'SIGNED INTEGER)';
+    }
+    else {
+        $svcnum_sql .= 'INTEGER)';
+    }
+    return ($linkalias, $svcnum_sql);
+}
+
+sub JoinToService {
+    my $self = shift;
+    my ($linkalias, $svcnum_sql) = $self->JoinToSvcLinks;
+    $self->Join(
+        TYPE       => 'LEFT',
+        EXPRESSION => $svcnum_sql,
+        TABLE2     => 'cust_svc',
+        FIELD2     => 'svcnum',
+    );
+}
+
+# This creates an alternate left join path to cust_main via cust_svc.
+# _FreesideFieldLimit needs to add this as a separate, independent join
+# and include all tickets that have a matching cust_main record via 
+# either path.
+sub JoinToCustomerViaService {
+    my $self = shift;
+    my $svcalias = $self->JoinToService;
+    my $cust_pkg = $self->Join(
+        TYPE      => 'LEFT',
+        ALIAS1    => $svcalias,
+        FIELD1    => 'pkgnum',
+        TABLE2    => 'cust_pkg',
+        FIELD2    => 'pkgnum',
+    );
+    my $cust_main = $self->Join(
+        TYPE      => 'LEFT',
+        ALIAS1    => $cust_pkg,
+        FIELD1    => 'custnum',
+        TABLE2    => 'cust_main',
+        FIELD2    => 'custnum',
+    );
+    $cust_main;
+}
+
+sub _FreesideFieldLimit {
+    my ( $self, $field, $op, $value, %rest ) = @_;
+    my $is_negative = 0;
+    if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
+        # if the op is negative, do the join as though
+        # the op were positive, then accept only records
+        # where the right-side join key is null.
+        $is_negative = 1;
+        $op = '=' if $op eq '!=';
+        $op =~ s/\bNOT\b//;
+    }
+
+    my (@alias, $table2, $subfield, $pkey);
+    if ( $field eq 'Customer' ) {
+      push @alias, $self->JoinToCustomer;
+      push @alias, $self->JoinToCustomerViaService;
+      $pkey = 'custnum';
+    }
+    elsif ( $field eq 'Service' ) {
+      push @alias, $self->JoinToService;
+      $pkey = 'svcnum';
+    }
+    else {
+      die "malformed Freeside query: $field";
+    }
+
+    $subfield = $rest{SUBKEY} || $pkey;
+    # compound subkey: separate into table name and field in that table
+    # (must be linked by custnum)
+    $subfield = lc($subfield);
+    ($table2, $subfield) = ($1, $2) if $subfield =~ /^(\w+)?\.(\w+)$/;
+    $subfield = $pkey if $subfield eq 'number';
+
+    # if it's compound, create a join from cust_main or cust_svc to that 
+    # table, using custnum or svcnum, and Limit on that table instead.
+    my @_SQLLimit = ();
+    foreach my $a (@alias) {
+      if ( $table2 ) {
+          $a = $self->Join(
+              TYPE        => 'LEFT',
+              ALIAS1      => $a,
+              FIELD1      => $pkey,
+              TABLE2      => $table2,
+              FIELD2      => $pkey,
+          );
+      }
+
+      # do the actual Limit
+      $self->SUPER::Limit(
+          LEFTJOIN        => $a,
+          FIELD           => $subfield,
+          OPERATOR        => $op,
+          VALUE           => $value,
+          ENTRYAGGREGATOR => 'AND',
+          # no SUBCLAUSE needed, limits on different aliases across left joins
+          # are inherently independent
+      );
+
+      # then, since it's a left join, exclude tickets for which there is now 
+      # no matching record in the table we just limited on.  (Or where there 
+      # is a matching record, if $is_negative.)
+      # For a cust_main query (where there are two different aliases), this 
+      # will produce a subclause: "cust_main_1.custnum IS NOT NULL OR 
+      # cust_main_2.custnum IS NOT NULL" (or "IS NULL AND..." for a negative
+      # query).
+      #$self->_SQLLimit(
+      push @_SQLLimit, {
+          %rest,
+          ALIAS           => $a,
+          FIELD           => $pkey,
+          OPERATOR        => $is_negative ? 'IS' : 'IS NOT',
+          VALUE           => 'NULL',
+          QUOTEVALUE      => 0,
+          ENTRYAGGREGATOR => $is_negative ? 'AND' : 'OR',
+          SUBCLAUSE       => 'fs_limit',
+      };
+    }
+
+    $self->_OpenParen;
+    foreach my $_SQLLimit (@_SQLLimit) {
+      $self->_SQLLimit( %$_SQLLimit);
+    }
+    $self->_CloseParen;
+
+}
+
+#Freeside
+
+=head2 Limit
+
+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 $index = $self->_NextIndex;
+
+# make the TicketRestrictions hash the equivalent of whatever we just passed in;
+
+    %{ $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'
+        && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
+    {
+        $self->{'looking_at_effective_id'} = 1;
+    }
+
+    if ( $args{'FIELD'} eq 'Type'
+        && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
+    {
+        $self->{'looking_at_type'} = 1;
+    }
+
+    return ($index);
 }
 
 }
 
-# }}}
 
 
-# {{{ Limit by enum or foreign key
 
 
-# {{{ sub LimitQueue
 
 =head2 LimitQueue
 
 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
 OPERATOR is one of = or !=. (It defaults to =).
 
 =head2 LimitQueue
 
 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
 OPERATOR is one of = or !=. (It defaults to =).
-VALUE is a queue id. 
+VALUE is a queue id or Name.
+
 
 =cut
 
 sub LimitQueue {
     my $self = shift;
 
 =cut
 
 sub LimitQueue {
     my $self = shift;
-    my %args = (VALUE => undef,
-               OPERATOR => '=',
-               @_);
+    my %args = (
+        VALUE    => undef,
+        OPERATOR => '=',
+        @_
+    );
+
+    #TODO  VALUE should also take queue objects
+    if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
+        my $queue = RT::Queue->new( $self->CurrentUser );
+        $queue->Load( $args{'VALUE'} );
+        $args{'VALUE'} = $queue->Id;
+    }
+
+    # What if they pass in an Id?  Check for isNum() and convert to
+    # string.
 
 
-    #TODO  VALUE should also take queue names and queue objects
-    my $queue = new RT::Queue($self->CurrentUser);
-    $queue->Load($args{'VALUE'});
-    
     #TODO check for a valid queue here
 
     #TODO check for a valid queue here
 
-    $self->Limit (FIELD => 'Queue',
-                 VALUE => $queue->id(),
-                 OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => 'Queue ' .  $args{'OPERATOR'}. " ". $queue->Name
-                );
-    
+    $self->Limit(
+        FIELD       => 'Queue',
+        VALUE       => $args{'VALUE'},
+        OPERATOR    => $args{'OPERATOR'},
+        DESCRIPTION => join(
+            ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
+        ),
+    );
+
 }
 }
-# }}}
 
 
-# {{{ sub LimitStatus
+
 
 =head2 LimitStatus
 
 
 =head2 LimitStatus
 
@@ -226,22 +2428,51 @@ Takes a paramhash with the fields OPERATOR and VALUE.
 OPERATOR is one of = or !=.
 VALUE is a status.
 
 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;
 =cut
 
 sub LimitStatus {
     my $self = shift;
-    my %args = ( OPERATOR => '=',
-                  @_);
-    $self->Limit (FIELD => 'Status',
-                 VALUE => $args{'VALUE'},
-                 OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => 'Status ' .  $args{'OPERATOR'}. " ". $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'} ) ),
+    );
+}
+
+
+
+=head2 IgnoreType
+
+If called, this search will not automatically limit the set of results found
+to tickets of type "Ticket". Tickets of other types, such as "project" and
+"approval" will be found.
+
+=cut
+
+sub IgnoreType {
+    my $self = shift;
+
+    # Instead of faking a Limit that later gets ignored, fake up the
+    # fact that we're already looking at type, so that the check in
+    # Tickets_SQL/FromSQL goes down the right branch
+
+    #  $self->LimitType(VALUE => '__any');
+    $self->{looking_at_type} = 1;
 }
 
 }
 
-# }}}
 
 
-# {{{ sub LimitType
 
 =head2 LimitType
 
 
 =head2 LimitType
 
@@ -249,27 +2480,29 @@ Takes a paramhash with the fields OPERATOR and VALUE.
 OPERATOR is one of = or !=, it defaults to "=".
 VALUE is a string to search for in the type of the ticket.
 
 OPERATOR is one of = or !=, it defaults to "=".
 VALUE is a string to search for in the type of the ticket.
 
+
+
 =cut
 
 sub LimitType {
     my $self = shift;
 =cut
 
 sub LimitType {
     my $self = shift;
-    my %args = (OPERATOR => '=',
-               VALUE => undef,
-               @_);
-    $self->Limit (FIELD => 'Type',
-                  VALUE => $args{'VALUE'},
-                  OPERATOR => $args{'OPERATOR'},
-                  DESCRIPTION => '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{'VALUE'}, ),
+    );
 }
 
 }
 
-# }}}
 
 
-# }}}
 
 
-# {{{ Limit by string field
 
 
-# {{{ sub LimitSubject
 
 =head2 LimitSubject
 
 
 =head2 LimitSubject
 
@@ -282,21 +2515,19 @@ VALUE is a string to search for in the subject of the ticket.
 sub LimitSubject {
     my $self = shift;
     my %args = (@_);
 sub LimitSubject {
     my $self = shift;
     my %args = (@_);
-    $self->Limit (FIELD => 'Subject',
-                 VALUE => $args{'VALUE'},
-                 OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => 'Subject ' .  $args{'OPERATOR'}. " ". $args{'VALUE'},
-                );
+    $self->Limit(
+        FIELD       => 'Subject',
+        VALUE       => $args{'VALUE'},
+        OPERATOR    => $args{'OPERATOR'},
+        DESCRIPTION => join( ' ',
+            $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
+    );
 }
 
 }
 
-# }}}
 
 
-# }}}
 
 
-# {{{ Limit based on ticket numerical attributes
 # Things that can be > < = !=
 
 # Things that can be > < = !=
 
-# {{{ sub LimitId
 
 =head2 LimitId
 
 
 =head2 LimitId
 
@@ -308,47 +2539,50 @@ VALUE is a ticket Id to search for
 
 sub LimitId {
     my $self = shift;
 
 sub LimitId {
     my $self = shift;
-    my %args = (OPERATOR => '=',
-                @_);
-    
-    $self->Limit (FIELD => 'id',
-                  VALUE => $args{'VALUE'},
-                  OPERATOR => $args{'OPERATOR'},
-                  DESCRIPTION => 'Id ' .  $args{'OPERATOR'}. " ". $args{'VALUE'},
-                 );
+    my %args = (
+        OPERATOR => '=',
+        @_
+    );
+
+    $self->Limit(
+        FIELD       => 'id',
+        VALUE       => $args{'VALUE'},
+        OPERATOR    => $args{'OPERATOR'},
+        DESCRIPTION =>
+            join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
+    );
 }
 
 }
 
-# }}}
 
 
-# {{{ sub LimitPriority
 
 =head2 LimitPriority
 
 Takes a paramhash with the fields OPERATOR and VALUE.
 OPERATOR is one of =, >, < or !=.
 
 =head2 LimitPriority
 
 Takes a paramhash with the fields OPERATOR and VALUE.
 OPERATOR is one of =, >, < or !=.
-VALUE is a value to match the ticket\'s priority against
+VALUE is a value to match the ticket's priority against
 
 =cut
 
 sub LimitPriority {
     my $self = shift;
     my %args = (@_);
 
 =cut
 
 sub LimitPriority {
     my $self = shift;
     my %args = (@_);
-    $self->Limit (FIELD => 'Priority',
-                 VALUE => $args{'VALUE'},
-                 OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => 'Priority ' .  $args{'OPERATOR'}. " ". $args{'VALUE'},
-                );
+    $self->Limit(
+        FIELD       => 'Priority',
+        VALUE       => $args{'VALUE'},
+        OPERATOR    => $args{'OPERATOR'},
+        DESCRIPTION => join( ' ',
+            $self->loc('Priority'),
+            $args{'OPERATOR'}, $args{'VALUE'}, ),
+    );
 }
 
 }
 
-# }}}
 
 
-# {{{ sub LimitInitialPriority
 
 =head2 LimitInitialPriority
 
 Takes a paramhash with the fields OPERATOR and VALUE.
 OPERATOR is one of =, >, < or !=.
 
 =head2 LimitInitialPriority
 
 Takes a paramhash with the fields OPERATOR and VALUE.
 OPERATOR is one of =, >, < or !=.
-VALUE is a value to match the ticket\'s initial priority against
+VALUE is a value to match the ticket's initial priority against
 
 
 =cut
 
 
 =cut
@@ -356,38 +2590,40 @@ VALUE is a value to match the ticket\'s initial priority against
 sub LimitInitialPriority {
     my $self = shift;
     my %args = (@_);
 sub LimitInitialPriority {
     my $self = shift;
     my %args = (@_);
-    $self->Limit (FIELD => 'InitialPriority',
-                 VALUE => $args{'VALUE'},
-                 OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => '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'}, ),
+    );
 }
 
 }
 
-# }}}
 
 
-# {{{ sub LimitFinalPriority
 
 =head2 LimitFinalPriority
 
 Takes a paramhash with the fields OPERATOR and VALUE.
 OPERATOR is one of =, >, < or !=.
 
 =head2 LimitFinalPriority
 
 Takes a paramhash with the fields OPERATOR and VALUE.
 OPERATOR is one of =, >, < or !=.
-VALUE is a value to match the ticket\'s final priority against
+VALUE is a value to match the ticket's final priority against
 
 =cut
 
 sub LimitFinalPriority {
     my $self = shift;
     my %args = (@_);
 
 =cut
 
 sub LimitFinalPriority {
     my $self = shift;
     my %args = (@_);
-    $self->Limit (FIELD => 'FinalPriority',
-                 VALUE => $args{'VALUE'},
-                 OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => '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'}, ),
+    );
 }
 
 }
 
-# }}}
 
 
-# {{{ sub LimitTimeWorked
 
 =head2 LimitTimeWorked
 
 
 =head2 LimitTimeWorked
 
@@ -400,16 +2636,17 @@ VALUE is a value to match the ticket's TimeWorked attribute
 sub LimitTimeWorked {
     my $self = shift;
     my %args = (@_);
 sub LimitTimeWorked {
     my $self = shift;
     my %args = (@_);
-    $self->Limit (FIELD => 'TimeWorked',
-                 VALUE => $args{'VALUE'},
-                 OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => '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'}, ),
+    );
 }
 
 }
 
-# }}}
 
 
-# {{{ sub LimitTimeLeft
 
 =head2 LimitTimeLeft
 
 
 =head2 LimitTimeLeft
 
@@ -422,20 +2659,19 @@ VALUE is a value to match the ticket's TimeLeft attribute
 sub LimitTimeLeft {
     my $self = shift;
     my %args = (@_);
 sub LimitTimeLeft {
     my $self = shift;
     my %args = (@_);
-    $self->Limit (FIELD => 'TimeLeft',
-                 VALUE => $args{'VALUE'},
-                 OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => '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'}, ),
+    );
 }
 
 }
 
-# }}}
 
 
-# }}}
 
 
-# {{{ Limiting based on attachment attributes
 
 
-# {{{ sub LimitContent
 
 =head2 LimitContent
 
 
 =head2 LimitContent
 
@@ -444,315 +2680,305 @@ OPERATOR is one of =, LIKE, NOT LIKE or !=.
 VALUE is a string to search for in the body of the ticket
 
 =cut
 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 => 'Ticket content ' .  $args{'OPERATOR'}. " ". $args{'VALUE'},
-                );
-}
-
-# }}}
-# {{{ sub LimitContentType
 
 
-=head2 LimitContentType
-
-Takes a paramhash with the fields OPERATOR and VALUE.
-OPERATOR is one of =, LIKE, NOT LIKE or !=.
-VALUE is a content type to search ticket attachments for
-
-=cut
-  
-sub LimitContentType {
+sub LimitContent {
     my $self = shift;
     my %args = (@_);
     my $self = shift;
     my %args = (@_);
-    $self->Limit (FIELD => 'ContentType',
-                 VALUE => $args{'VALUE'},
-                 OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => 'Ticket content type ' .  $args{'OPERATOR'}. " ". $args{'VALUE'},
-                );
+    $self->Limit(
+        FIELD       => 'Content',
+        VALUE       => $args{'VALUE'},
+        OPERATOR    => $args{'OPERATOR'},
+        DESCRIPTION => join( ' ',
+            $self->loc('Ticket content'), $args{'OPERATOR'},
+            $args{'VALUE'}, ),
+    );
 }
 }
-# }}}
-
-# }}}
 
 
-# {{{ Limiting based on people
 
 
-# {{{ sub LimitOwner
 
 
-=head2 LimitOwner
+=head2 LimitFilename
 
 Takes a paramhash with the fields OPERATOR and VALUE.
 
 Takes a paramhash with the fields OPERATOR and VALUE.
-OPERATOR is one of = or !=.
-VALUE is a user id.
-
-=cut
-
-sub LimitOwner {
-    my $self = shift;
-    my %args = ( OPERATOR => '=',
-                 @_);
-    
-    my $owner = new RT::User($self->CurrentUser);
-    $owner->Load($args{'VALUE'});
-    $self->Limit (FIELD => 'Owner',
-                 VALUE => $owner->Id,
-                 OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => 'Owner ' .  $args{'OPERATOR'}. " ". $owner->Name()
-                );
-    
-}
-
-# }}}
-
-# {{{ Limiting watchers
-
-# {{{ sub LimitWatcher
-
-
-=head2 LimitWatcher
-  
-  Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
-  OPERATOR is one of =, LIKE, NOT LIKE or !=.
-  VALUE is a value to match the ticket\'s watcher email addresses against
-  TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
-
-=cut
-   
-sub LimitWatcher {
-    my $self = shift;
-    my %args = ( OPERATOR => '=',
-                VALUE => undef,
-                TYPE => undef,
-               @_);
-
-
-    #build us up a description
-    my ($watcher_type, $desc);
-    if ($args{'TYPE'}) {
-       $watcher_type = $args{'TYPE'};
-    }
-    else {
-       $watcher_type = "Watcher";
-    }
-    $desc = "$watcher_type ".$args{'OPERATOR'}." ".$args{'VALUE'};
+OPERATOR is one of =, LIKE, NOT LIKE or !=.
+VALUE is a string to search for in the body of the ticket
 
 
+=cut
 
 
-    $self->Limit (FIELD => 'Watcher',
-                 VALUE => $args{'VALUE'},
-                 OPERATOR => $args{'OPERATOR'},
-                 TYPE => $args{'TYPE'},
-                 DESCRIPTION => "$desc"
-                );
+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'}, ),
+    );
 }
 
 }
 
-# }}}
-
-# {{{ sub LimitRequestor
 
 
-=head2 LimitRequestor
+=head2 LimitContentType
 
 
-It\'s like LimitWatcher, but it presets TYPE to Requestor
+Takes a paramhash with the fields OPERATOR and VALUE.
+OPERATOR is one of =, LIKE, NOT LIKE or !=.
+VALUE is a content type to search ticket attachments for
 
 =cut
 
 
 =cut
 
-
-sub LimitRequestor {
+sub LimitContentType {
     my $self = shift;
     my $self = shift;
-    $self->LimitWatcher(TYPE=> 'Requestor', @_);
+    my %args = (@_);
+    $self->Limit(
+        FIELD       => 'ContentType',
+        VALUE       => $args{'VALUE'},
+        OPERATOR    => $args{'OPERATOR'},
+        DESCRIPTION => join( ' ',
+            $self->loc('Ticket content type'), $args{'OPERATOR'},
+            $args{'VALUE'}, ),
+    );
 }
 
 }
 
-# }}}
 
 
-# {{{ sub LimitCc
 
 
-=head2 LimitCC
 
 
-It\'s like LimitWatcher, but it presets TYPE to Cc
+
+=head2 LimitOwner
+
+Takes a paramhash with the fields OPERATOR and VALUE.
+OPERATOR is one of = or !=.
+VALUE is a user id.
 
 =cut
 
 
 =cut
 
-sub LimitCc {
+sub LimitOwner {
     my $self = shift;
     my $self = shift;
-    $self->LimitWatcher(TYPE=> 'Cc', @_);
+    my %args = (
+        OPERATOR => '=',
+        @_
+    );
+
+    my $owner = RT::User->new( $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(), ),
+    );
+
 }
 
 }
 
-# }}}
 
 
-# {{{ sub LimitAdminCc
 
 
-=head2 LimitAdminCc
 
 
-It\'s like LimitWatcher, but it presets TYPE to AdminCc
+=head2 LimitWatcher
+
+  Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
+  OPERATOR is one of =, LIKE, NOT LIKE or !=.
+  VALUE is a value to match the ticket's watcher email addresses against
+  TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
+
 
 =cut
 
 =cut
-  
-sub LimitAdminCc {
+
+sub LimitWatcher {
     my $self = shift;
     my $self = shift;
-    $self->LimitWatcher(TYPE=> 'AdminCc', @_);
+    my %args = (
+        OPERATOR => '=',
+        VALUE    => undef,
+        TYPE     => undef,
+        @_
+    );
+
+    #build us up a description
+    my ( $watcher_type, $desc );
+    if ( $args{'TYPE'} ) {
+        $watcher_type = $args{'TYPE'};
+    }
+    else {
+        $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'}, ),
+    );
 }
 
 }
 
-# }}}
 
 
-# }}}
 
 
-# }}}
 
 
-# {{{ Limiting based on links
 
 
-# {{{ LimitLinkedTo
 
 =head2 LimitLinkedTo
 
 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
 
 =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 }
 
 TARGET is the id or URI of the TARGET of the link
 
 TARGET is the id or URI of the TARGET of the link
-(TARGET used to be 'TICKET'.  'TICKET' is deprecated, but will be treated as TARGET
 
 =cut
 
 sub LimitLinkedTo {
     my $self = shift;
 
 =cut
 
 sub LimitLinkedTo {
     my $self = shift;
-    my %args = ( 
-               TICKET => undef,
-               TARGET => undef,
-               TYPE => undef,
-                @_);
-
-
-    $self->Limit( FIELD => 'LinkedTo',
-                 BASE => undef,
-                 TARGET => ($args{'TARGET'} || $args{'TICKET'}),
-                 TYPE => $args{'TYPE'},
-                 DESCRIPTION => "Tickets ".$args{'TYPE'}." by ".($args{'TARGET'} || $args{'TICKET'})
-               );
+    my %args = (
+        TARGET   => undef,
+        TYPE     => undef,
+        OPERATOR => '=',
+        @_
+    );
+
+    $self->Limit(
+        FIELD       => 'LinkedTo',
+        BASE        => undef,
+        TARGET      => $args{'TARGET'},
+        TYPE        => $args{'TYPE'},
+        DESCRIPTION => $self->loc(
+            "Tickets [_1] by [_2]",
+            $self->loc( $args{'TYPE'} ),
+            $args{'TARGET'}
+        ),
+        OPERATOR    => $args{'OPERATOR'},
+    );
 }
 
 
 }
 
 
-# }}}
-
-# {{{ LimitLinkedFrom
 
 =head2 LimitLinkedFrom
 
 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
 
 =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
 
 
 BASE is the id or URI of the BASE of the link
-(BASE used to be 'TICKET'.  'TICKET' is deprecated, but will be treated as BASE
-
 
 =cut
 
 sub LimitLinkedFrom {
     my $self = shift;
 
 =cut
 
 sub LimitLinkedFrom {
     my $self = shift;
-    my %args = ( BASE => undef,
-                TICKET => undef,
-                TYPE => undef,
-                @_);
-
-    
-    $self->Limit( FIELD => 'LinkedTo',
-                 TARGET => undef,
-                 BASE => ($args{'BASE'} || $args{'TICKET'}),
-                 TYPE => $args{'TYPE'},
-                 DESCRIPTION => "Tickets " .($args{'BASE'} || $args{'TICKET'}) ." ".$args{'TYPE'}
-               );
+    my %args = (
+        BASE     => undef,
+        TYPE     => undef,
+        OPERATOR => '=',
+        @_
+    );
+
+    # translate RT2 From/To naming to RT3 TicketSQL naming
+    my %fromToMap = qw(DependsOn DependentOn
+        MemberOf  HasMember
+        RefersTo  ReferredToBy);
+
+    my $type = $args{'TYPE'};
+    $type = $fromToMap{$type} if exists( $fromToMap{$type} );
+
+    $self->Limit(
+        FIELD       => 'LinkedTo',
+        TARGET      => undef,
+        BASE        => $args{'BASE'},
+        TYPE        => $type,
+        DESCRIPTION => $self->loc(
+            "Tickets [_1] [_2]",
+            $self->loc( $args{'TYPE'} ),
+            $args{'BASE'},
+        ),
+        OPERATOR    => $args{'OPERATOR'},
+    );
 }
 
 
 }
 
 
-# }}}
-
-# {{{ LimitMemberOf 
 sub LimitMemberOf {
 sub LimitMemberOf {
-    my $self = shift;
+    my $self      = shift;
     my $ticket_id = shift;
     my $ticket_id = shift;
-    $self->LimitLinkedTo ( TARGET=> "$ticket_id",
-                          TYPE => 'MemberOf',
-                         );
-    
+    return $self->LimitLinkedTo(
+        @_,
+        TARGET => $ticket_id,
+        TYPE   => 'MemberOf',
+    );
 }
 }
-# }}}
 
 
-# {{{ LimitHasMember
+
 sub LimitHasMember {
 sub LimitHasMember {
-    my $self = shift;
-    my $ticket_id =shift;
-    $self->LimitLinkedFrom ( BASE => "$ticket_id",
-                            TYPE => 'MemberOf',
-                            );
-    
+    my $self      = shift;
+    my $ticket_id = shift;
+    return $self->LimitLinkedFrom(
+        @_,
+        BASE => "$ticket_id",
+        TYPE => 'HasMember',
+    );
+
 }
 }
-# }}}
 
 
-# {{{ LimitDependsOn
+
 
 sub LimitDependsOn {
 
 sub LimitDependsOn {
-    my $self = shift;
+    my $self      = shift;
     my $ticket_id = shift;
     my $ticket_id = shift;
-    $self->LimitLinkedTo ( TARGET => "$ticket_id",
-                           TYPE => 'DependsOn',
-                          );
-    
+    return $self->LimitLinkedTo(
+        @_,
+        TARGET => $ticket_id,
+        TYPE   => 'DependsOn',
+    );
+
 }
 
 }
 
-# }}}
 
 
-# {{{ LimitDependedOnBy
 
 sub LimitDependedOnBy {
 
 sub LimitDependedOnBy {
-    my $self = shift;
+    my $self      = shift;
     my $ticket_id = shift;
     my $ticket_id = shift;
-    $self->LimitLinkedFrom (  BASE => "$ticket_id",
-                               TYPE => 'DependsOn',
-                            );
-    
-}
+    return $self->LimitLinkedFrom(
+        @_,
+        BASE => $ticket_id,
+        TYPE => 'DependentOn',
+    );
 
 
-# }}}
+}
 
 
 
 
-# {{{ LimitRefersTo
 
 sub LimitRefersTo {
 
 sub LimitRefersTo {
-    my $self = shift;
+    my $self      = shift;
     my $ticket_id = shift;
     my $ticket_id = shift;
-    $self->LimitLinkedTo ( TARGET => "$ticket_id",
-                           TYPE => 'RefersTo',
-                          );
-    
+    return $self->LimitLinkedTo(
+        @_,
+        TARGET => $ticket_id,
+        TYPE   => 'RefersTo',
+    );
+
 }
 
 }
 
-# }}}
 
 
-# {{{ LimitReferredToBy
 
 sub LimitReferredToBy {
 
 sub LimitReferredToBy {
-    my $self = shift;
+    my $self      = shift;
     my $ticket_id = shift;
     my $ticket_id = shift;
-    $self->LimitLinkedFrom (  BASE=> "$ticket_id",
-                               TYPE => 'RefersTo',
-                            );
-    
+    return $self->LimitLinkedFrom(
+        @_,
+        BASE => $ticket_id,
+        TYPE => 'ReferredToBy',
+    );
 }
 
 }
 
-# }}}
 
 
-# }}}
 
 
-# {{{ limit based on ticket date attribtes
 
 
-# {{{ sub LimitDate
 
 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
 
 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
 
 
 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
 
 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
 
-OPERATOR is one of > or < 
+OPERATOR is one of > or <
 VALUE is a date and time in ISO format in GMT
 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
 
 VALUE is a date and time in ISO format in GMT
 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
 
@@ -764,64 +2990,69 @@ the need to pass in a FIELD argument.
 sub LimitDate {
     my $self = shift;
     my %args = (
 sub LimitDate {
     my $self = shift;
     my %args = (
-                  FIELD => undef,
-                 VALUE => $args{'VALUE'},
-                 OPERATOR => $args{'OPERATOR'},
+        FIELD    => undef,
+        VALUE    => undef,
+        OPERATOR => undef,
 
 
-                  @_);
+        @_
+    );
 
     #Set the description if we didn't get handed it above
 
     #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;
 
 sub LimitCreated {
     my $self = shift;
-    $self->LimitDate( FIELD => 'Created', @_);
+    $self->LimitDate( FIELD => 'Created', @_ );
 }
 }
+
 sub LimitDue {
     my $self = shift;
 sub LimitDue {
     my $self = shift;
-    $self->LimitDate( FIELD => 'Due', @_);
+    $self->LimitDate( FIELD => 'Due', @_ );
 
 }
 
 }
+
 sub LimitStarts {
     my $self = shift;
 sub LimitStarts {
     my $self = shift;
-    $self->LimitDate( FIELD => 'Starts', @_);
+    $self->LimitDate( FIELD => 'Starts', @_ );
 
 }
 
 }
+
 sub LimitStarted {
     my $self = shift;
 sub LimitStarted {
     my $self = shift;
-    $self->LimitDate( FIELD => 'Started', @_);
+    $self->LimitDate( FIELD => 'Started', @_ );
 }
 }
-sub LimitResolved { 
+
+sub LimitResolved {
     my $self = shift;
     my $self = shift;
-    $self->LimitDate( FIELD => 'Resolved', @_);
+    $self->LimitDate( FIELD => 'Resolved', @_ );
 }
 }
+
 sub LimitTold {
     my $self = shift;
 sub LimitTold {
     my $self = shift;
-    $self->LimitDate( FIELD => 'Told', @_);
+    $self->LimitDate( FIELD => 'Told', @_ );
 }
 }
+
 sub LimitLastUpdated {
     my $self = shift;
 sub LimitLastUpdated {
     my $self = shift;
-    $self->LimitDate( FIELD => 'LastUpdated', @_);
+    $self->LimitDate( FIELD => 'LastUpdated', @_ );
 }
 }
+
 #
 #
-# {{{ sub LimitTransactionDate
 
 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
 
 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
 
 
 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
 
 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
 
-OPERATOR is one of > or < 
+OPERATOR is one of > or <
 VALUE is a date and time in ISO format in GMT
 
 
 VALUE is a date and time in ISO format in GMT
 
 
@@ -830,94 +3061,111 @@ VALUE is a date and time in ISO format in GMT
 sub LimitTransactionDate {
     my $self = shift;
     my %args = (
 sub LimitTransactionDate {
     my $self = shift;
     my %args = (
-                  FIELD => 'TransactionDate',
-                 VALUE => $args{'VALUE'},
-                 OPERATOR => $args{'OPERATOR'},
+        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
 
     #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 LimitKeyword
 
 
-=head2 LimitKeyword 
+=head2 LimitCustomField
 
 Takes a paramhash of key/value pairs with the following keys:
 
 =over 4
 
 
 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
 
 =cut
 
 
 =back
 
 =cut
 
-sub LimitKeyword {
+sub LimitCustomField {
     my $self = shift;
     my $self = shift;
-    my %args = ( KEYWORD => undef,
-                 KEYWORDSELECT => undef,
-                OPERATOR => '=',
-                DESCRIPTION => undef,
-                FIELD => 'Keyword',
-                QUOTEVALUE => 1,
-                @_
-              );
-
-    use RT::KeywordSelect;
-    my $KeywordSelect = RT::KeywordSelect->new($self->CurrentUser);
-    $KeywordSelect->Load($args{KEYWORDSELECT});
-    
-
-    # Below, We're checking to see whether the keyword we're searching for
-    # is null or not.
-    # This could probably be rewritten to be easier to read and  understand
+    my %args = (
+        VALUE       => undef,
+        CUSTOMFIELD => undef,
+        OPERATOR    => '=',
+        DESCRIPTION => undef,
+        FIELD       => 'CustomFieldValue',
+        QUOTEVALUE  => 1,
+        @_
+    );
+
+    my $CF = RT::CustomField->new( $self->CurrentUser );
+    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 we are looking to compare with a null value.
-    if ($args{'OPERATOR'} =~ /is/i)  {
-       if ($args{'OPERATOR'} =~ /^is$/i) {
-           $args{'DESCRIPTION'} ||= "Keyword Selection ". $KeywordSelect->Name . " has no value";
-       }
-       elsif ($args{'OPERATOR'} =~ /^is not$/i) {
-           $args{'DESCRIPTION'} ||= "Keyword Selection ". $KeywordSelect->Name . " has a value";
-       }
-    }
-       # if we're not looking to compare with a null value
-    else {     
-        use RT::Keyword;
-       my $Keyword = RT::Keyword->new($self->CurrentUser);
-       $Keyword->Load($args{KEYWORD});
-       $args{'DESCRIPTION'} ||= "Keyword Selection " . $KeywordSelect->Name.  " $args{OPERATOR} ". $Keyword->Name;
+    if ( $args{'OPERATOR'} =~ /^is$/i ) {
+        $args{'DESCRIPTION'}
+            ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
     }
     }
-    
-    $args{SingleValued} = $KeywordSelect->Single();
-    
-    my $index = $self->_NextIndex;
-    %{$self->{'TicketRestrictions'}{$index}} = %args;
-    
+    elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
+        $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} );
+    }
+
+    if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
+        my $QueueObj = RT::Queue->new( $self->CurrentUser );
+        $QueueObj->Load( $args{'QUEUE'} );
+        $args{'QUEUE'} = $QueueObj->Id;
+    }
+    delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
+
+    my @rest;
+    @rest = ( ENTRYAGGREGATOR => 'AND' )
+        if ( $CF->Type eq 'SelectMultiple' );
+
+    $self->Limit(
+        VALUE => $args{VALUE},
+        FIELD => "CF"
+            .(defined $args{'QUEUE'}? ".$args{'QUEUE'}" : '' )
+            .".{" . $CF->Name . "}",
+        OPERATOR    => $args{OPERATOR},
+        CUSTOMFIELD => 1,
+        @rest,
+    );
+
     $self->{'RecalcTicketLimits'} = 1;
     $self->{'RecalcTicketLimits'} = 1;
-    return ($index);
 }
 
 }
 
-# }}}
 
 
-# {{{ sub _NextIndex
 
 =head2 _NextIndex
 
 
 =head2 _NextIndex
 
@@ -927,44 +3175,44 @@ Keep track of the counter for the array of restrictions
 
 sub _NextIndex {
     my $self = shift;
 
 sub _NextIndex {
     my $self = shift;
-    return ($self->{'restriction_index'}++);
+    return ( $self->{'restriction_index'}++ );
 }
 }
-# }}}
 
 
-# }}} 
 
 
-# {{{ Core bits to make this a DBIx::SearchBuilder object
 
 
-# {{{ sub _Init 
-sub _Init  {
+
+sub _Init {
     my $self = shift;
     my $self = shift;
-    $self->{'table'} = "Tickets";
-    $self->{'RecalcTicketLimits'} = 1;
+    $self->{'table'}                   = "Tickets";
+    $self->{'RecalcTicketLimits'}      = 1;
     $self->{'looking_at_effective_id'} = 0;
     $self->{'looking_at_effective_id'} = 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->SUPER::_Init(@_);
 
+    $self->_InitSQL;
+
 }
 }
-# }}}
 
 
-# {{{ sub NewItem 
-sub NewItem  {
-  my $self = shift;
-  return(RT::Ticket->new($self->CurrentUser));
 
 
+sub Count {
+    my $self = shift;
+    $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
+    return ( $self->SUPER::Count() );
 }
 }
-# }}}
 
 
-# {{{ sub Count
-sub Count {
-  my $self = shift;
-  $self->_ProcessRestrictions if ($self->{'RecalcTicketLimits'} == 1 );
-  return($self->SUPER::Count());
+
+sub CountAll {
+    my $self = shift;
+    $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
+    return ( $self->SUPER::CountAll() );
 }
 }
-# }}}
 
 
-# {{{ sub ItemsArrayRef
+
 
 =head2 ItemsArrayRef
 
 
 =head2 ItemsArrayRef
 
@@ -974,57 +3222,333 @@ Returns a reference to the set of all items found in this search
 
 sub ItemsArrayRef {
     my $self = shift;
 
 sub ItemsArrayRef {
     my $self = shift;
-    my @items;
-    
+
+    return $self->{'items_array'} if $self->{'items_array'};
+
     my $placeholder = $self->_ItemsCounter;
     $self->GotoFirstItem();
     my $placeholder = $self->_ItemsCounter;
     $self->GotoFirstItem();
-    while (my $item = $self->Next) { 
-       push (@items, $item);
+    while ( my $item = $self->Next ) {
+        push( @{ $self->{'items_array'} }, $item );
     }
     }
-    
     $self->GotoItem($placeholder);
     $self->GotoItem($placeholder);
-    return(\@items);
+    $self->{'items_array'}
+        = $self->ItemsOrderBy( $self->{'items_array'} );
+
+    return $self->{'items_array'};
 }
 }
-# }}}
 
 
-# {{{ sub Next 
+sub ItemsArrayRefWindow {
+    my $self = shift;
+    my $window = shift;
+
+    my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
+
+    $self->RowsPerPage( $window );
+    $self->FirstRow(1);
+    $self->GotoFirstItem;
+
+    my @res;
+    while ( my $item = $self->Next ) {
+        push @res, $item;
+    }
+
+    $self->RowsPerPage( $old[1] );
+    $self->FirstRow( $old[2] );
+    $self->GotoItem( $old[0] );
+
+    return \@res;
+}
+
+
 sub Next {
 sub Next {
-       my $self = shift;
-       
-       $self->_ProcessRestrictions if ($self->{'RecalcTicketLimits'} == 1 );
+    my $self = shift;
+
+    $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
+
+    my $Ticket = $self->SUPER::Next;
+    return $Ticket unless $Ticket;
+
+    if ( $Ticket->__Value('Status') eq 'deleted'
+        && !$self->{'allow_deleted_search'} )
+    {
+        return $self->Next;
+    }
+    elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
+        # if we found a ticket with this option enabled then
+        # all tickets we found are ACLed, cache this fact
+        my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
+        $RT::Principal::_ACL_CACHE->set( $key => 1 );
+        return $Ticket;
+    }
+    elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
+        # has rights
+        return $Ticket;
+    }
+    else {
+        # If the user doesn't have the right to show this ticket
+        return $self->Next;
+    }
+}
+
+sub _DoSearch {
+    my $self = shift;
+    $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
+    return $self->SUPER::_DoSearch( @_ );
+}
+
+sub _DoCount {
+    my $self = shift;
+    $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
+    return $self->SUPER::_DoCount( @_ );
+}
+
+sub _RolesCanSee {
+    my $self = shift;
+
+    my $cache_key = 'RolesHasRight;:;ShowTicket';
+    if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
+        return %$cached;
+    }
+
+    my $ACL = RT::ACL->new( RT->SystemUser );
+    $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
+    $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
+    my $principal_alias = $ACL->Join(
+        ALIAS1 => 'main',
+        FIELD1 => 'PrincipalId',
+        TABLE2 => 'Principals',
+        FIELD2 => 'id',
+    );
+    $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
+
+    my %res = ();
+    foreach my $ACE ( @{ $ACL->ItemsArrayRef } ) {
+        my $role = $ACE->__Value('PrincipalType');
+        my $type = $ACE->__Value('ObjectType');
+        if ( $type eq 'RT::System' ) {
+            $res{ $role } = 1;
+        }
+        elsif ( $type eq 'RT::Queue' ) {
+            next if $res{ $role } && !ref $res{ $role };
+            push @{ $res{ $role } ||= [] }, $ACE->__Value('ObjectId');
+        }
+        else {
+            $RT::Logger->error('ShowTicket right is granted on unsupported object');
+        }
+    }
+    $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
+    return %res;
+}
+
+sub _DirectlyCanSeeIn {
+    my $self = shift;
+    my $id = $self->CurrentUser->id;
+
+    my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
+    if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
+        return @$cached;
+    }
 
 
-       my $Ticket = $self->SUPER::Next();
-       if ((defined($Ticket)) and (ref($Ticket))) {
+    my $ACL = RT::ACL->new( RT->SystemUser );
+    $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
+    my $principal_alias = $ACL->Join(
+        ALIAS1 => 'main',
+        FIELD1 => 'PrincipalId',
+        TABLE2 => 'Principals',
+        FIELD2 => 'id',
+    );
+    $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
+    my $cgm_alias = $ACL->Join(
+        ALIAS1 => 'main',
+        FIELD1 => 'PrincipalId',
+        TABLE2 => 'CachedGroupMembers',
+        FIELD2 => 'GroupId',
+    );
+    $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
+    $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
+
+    my @res = ();
+    foreach my $ACE ( @{ $ACL->ItemsArrayRef } ) {
+        my $type = $ACE->__Value('ObjectType');
+        if ( $type eq 'RT::System' ) {
+            # If user is direct member of a group that has the right
+            # on the system then he can see any ticket
+            $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
+            return (-1);
+        }
+        elsif ( $type eq 'RT::Queue' ) {
+            push @res, $ACE->__Value('ObjectId');
+        }
+        else {
+            $RT::Logger->error('ShowTicket right is granted on unsupported object');
+        }
+    }
+    $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
+    return @res;
+}
 
 
-           #Make sure we _never_ show dead tickets
-           #TODO we should be doing this in the where clause.
-           #but you can't do multiple clauses on the same field just yet :/
+sub CurrentUserCanSee {
+    my $self = shift;
+    return if $self->{'_sql_current_user_can_see_applied'};
+
+    return $self->{'_sql_current_user_can_see_applied'} = 1
+        if $self->CurrentUser->UserObj->HasRight(
+            Right => 'SuperUser', Object => $RT::System
+        );
+
+    my $id = $self->CurrentUser->id;
+
+    # directly can see in all queues then we have nothing to do
+    my @direct_queues = $self->_DirectlyCanSeeIn;
+    return $self->{'_sql_current_user_can_see_applied'} = 1
+        if @direct_queues && $direct_queues[0] == -1;
+
+    my %roles = $self->_RolesCanSee;
+    {
+        my %skip = map { $_ => 1 } @direct_queues;
+        foreach my $role ( keys %roles ) {
+            next unless ref $roles{ $role };
+
+            my @queues = grep !$skip{$_}, @{ $roles{ $role } };
+            if ( @queues ) {
+                $roles{ $role } = \@queues;
+            } else {
+                delete $roles{ $role };
+            }
+        }
+    }
 
 
-           if ($Ticket->Status eq 'dead') {
-               return($self->Next());
-           }
-           elsif ($Ticket->CurrentUserHasRight('ShowTicket')) {
-               return($Ticket);
-           }
+# there is no global watchers, only queues and tickes, if at
+# some point we will add global roles then it's gonna blow
+# the idea here is that if the right is set globaly for a role
+# and user plays this role for a queue directly not a ticket
+# then we have to check in advance
+    if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
+
+        my $groups = RT::Groups->new( RT->SystemUser );
+        $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
+        foreach ( @tmp ) {
+            $groups->Limit( FIELD => 'Type', VALUE => $_ );
+        }
+        my $principal_alias = $groups->Join(
+            ALIAS1 => 'main',
+            FIELD1 => 'id',
+            TABLE2 => 'Principals',
+            FIELD2 => 'id',
+        );
+        $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
+        my $cgm_alias = $groups->Join(
+            ALIAS1 => 'main',
+            FIELD1 => 'id',
+            TABLE2 => 'CachedGroupMembers',
+            FIELD2 => 'GroupId',
+        );
+        $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
+        $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
+        while ( my $group = $groups->Next ) {
+            push @direct_queues, $group->Instance;
+        }
+    }
 
 
-           #If the user doesn't have the right to show this ticket
-           else {      
-               return($self->Next());
-           }
-       }
-       #if there never was any ticket
-       else {
-               return(undef);
-       }       
+    unless ( @direct_queues || keys %roles ) {
+        $self->SUPER::Limit(
+            SUBCLAUSE => 'ACL',
+            ALIAS => 'main',
+            FIELD => 'id',
+            VALUE => 0,
+            ENTRYAGGREGATOR => 'AND',
+        );
+        return $self->{'_sql_current_user_can_see_applied'} = 1;
+    }
 
 
+    {
+        my $join_roles = keys %roles;
+        $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
+        my ($role_group_alias, $cgm_alias);
+        if ( $join_roles ) {
+            $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
+            $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
+            $self->SUPER::Limit(
+                LEFTJOIN   => $cgm_alias,
+                FIELD      => 'MemberId',
+                OPERATOR   => '=',
+                VALUE      => $id,
+            );
+        }
+        my $limit_queues = sub {
+            my $ea = shift;
+            my @queues = @_;
+
+            return unless @queues;
+            if ( @queues == 1 ) {
+                $self->SUPER::Limit(
+                    SUBCLAUSE => 'ACL',
+                    ALIAS => 'main',
+                    FIELD => 'Queue',
+                    VALUE => $_[0],
+                    ENTRYAGGREGATOR => $ea,
+                );
+            } else {
+                $self->SUPER::_OpenParen('ACL');
+                foreach my $q ( @queues ) {
+                    $self->SUPER::Limit(
+                        SUBCLAUSE => 'ACL',
+                        ALIAS => 'main',
+                        FIELD => 'Queue',
+                        VALUE => $q,
+                        ENTRYAGGREGATOR => $ea,
+                    );
+                    $ea = 'OR';
+                }
+                $self->SUPER::_CloseParen('ACL');
+            }
+            return 1;
+        };
+
+        $self->SUPER::_OpenParen('ACL');
+        my $ea = 'AND';
+        $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
+        while ( my ($role, $queues) = each %roles ) {
+            $self->SUPER::_OpenParen('ACL');
+            if ( $role eq 'Owner' ) {
+                $self->SUPER::Limit(
+                    SUBCLAUSE => 'ACL',
+                    FIELD           => 'Owner',
+                    VALUE           => $id,
+                    ENTRYAGGREGATOR => $ea,
+                );
+            }
+            else {
+                $self->SUPER::Limit(
+                    SUBCLAUSE       => 'ACL',
+                    ALIAS           => $cgm_alias,
+                    FIELD           => 'MemberId',
+                    OPERATOR        => 'IS NOT',
+                    VALUE           => 'NULL',
+                    QUOTEVALUE      => 0,
+                    ENTRYAGGREGATOR => $ea,
+                );
+                $self->SUPER::Limit(
+                    SUBCLAUSE       => 'ACL',
+                    ALIAS           => $role_group_alias,
+                    FIELD           => 'Type',
+                    VALUE           => $role,
+                    ENTRYAGGREGATOR => 'AND',
+                );
+            }
+            $limit_queues->( 'AND', @$queues ) if ref $queues;
+            $ea = 'OR' if $ea eq 'AND';
+            $self->SUPER::_CloseParen('ACL');
+        }
+        $self->SUPER::_CloseParen('ACL');
+    }
+    return $self->{'_sql_current_user_can_see_applied'} = 1;
 }
 }
-# }}}
 
 
-# }}}
 
 
-# {{{ Deal with storing and restoring restrictions
 
 
-# {{{ sub LoadRestrictions
+
 
 =head2 LoadRestrictions
 
 
 =head2 LoadRestrictions
 
@@ -1033,32 +3557,29 @@ TODO It is not yet implemented
 
 =cut
 
 
 =cut
 
-# }}}
 
 
-# {{{ sub DescribeRestrictions
 
 =head2 DescribeRestrictions
 
 takes nothing.
 
 =head2 DescribeRestrictions
 
 takes nothing.
-Returns a hash keyed by restriction id. 
+Returns a hash keyed by restriction id.
 Each element of the hash is currently a one element hash that contains DESCRIPTION which
 is a description of the purpose of that TicketRestriction
 
 =cut
 
 Each element of the hash is currently a one element hash that contains DESCRIPTION which
 is a description of the purpose of that TicketRestriction
 
 =cut
 
-sub DescribeRestrictions  {
+sub DescribeRestrictions {
     my $self = shift;
     my $self = shift;
-    
-    my ($row, %listing);
-    
-    foreach $row (keys %{$self->{'TicketRestrictions'}}) {
-       $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
+
+    my %listing;
+
+    foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
+        $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
     }
     return (%listing);
 }
     }
     return (%listing);
 }
-# }}}
 
 
-# {{{ sub RestrictionValues
+
 
 =head2 RestrictionValues FIELD
 
 
 =head2 RestrictionValues FIELD
 
@@ -1068,36 +3589,32 @@ to.
 =cut
 
 sub RestrictionValues {
 =cut
 
 sub RestrictionValues {
-    my $self = shift;
+    my $self  = shift;
     my $field = 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'} };
 }
 
 }
 
-# }}}
 
 
-# {{{ sub ClearRestrictions
 
 =head2 ClearRestrictions
 
 Removes all restrictions irretrievably
 
 =cut
 
 =head2 ClearRestrictions
 
 Removes all restrictions irretrievably
 
 =cut
-  
+
 sub ClearRestrictions {
     my $self = shift;
     delete $self->{'TicketRestrictions'};
     $self->{'looking_at_effective_id'} = 0;
 sub ClearRestrictions {
     my $self = shift;
     delete $self->{'TicketRestrictions'};
     $self->{'looking_at_effective_id'} = 0;
-    $self->{'RecalcTicketLimits'} =1;
+    $self->{'looking_at_type'}         = 0;
+    $self->{'RecalcTicketLimits'}      = 1;
 }
 
 }
 
-# }}}
 
 
-# {{{ sub DeleteRestriction
 
 =head2 DeleteRestriction
 
 
 =head2 DeleteRestriction
 
@@ -1106,684 +3623,259 @@ Removes that restriction from the session's limits.
 
 =cut
 
 
 =cut
 
-
 sub DeleteRestriction {
     my $self = shift;
 sub DeleteRestriction {
     my $self = shift;
-    my $row = shift;
+    my $row  = shift;
     delete $self->{'TicketRestrictions'}{$row};
     delete $self->{'TicketRestrictions'}{$row};
-    
+
     $self->{'RecalcTicketLimits'} = 1;
     $self->{'RecalcTicketLimits'} = 1;
+
     #make the underlying easysearch object forget all its preconceptions
 }
 
     #make the underlying easysearch object forget all its preconceptions
 }
 
-# }}}
 
 
-# {{{ sub _ProcessRestrictions 
 
 
-sub _ProcessRestrictions {
+# Convert a set of oldstyle SB Restrictions to Clauses for RQL
+
+sub _RestrictionsToClauses {
     my $self = shift;
 
     my $self = shift;
 
-    #Need to clean the EasySearch slate because it makes things too sticky
-    $self->CleanSlate();
+    my %clause;
+    foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
+        my $restriction = $self->{'TicketRestrictions'}{$row};
 
 
-    #Blow away ticket aliases since we'll need to regenerate them for a new search
-    delete $self->{'TicketAliases'};
-    delete $self->{KeywordsAliases};
+        # 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.
 
 
-    my $row;
-    
-    foreach $row (keys %{$self->{'TicketRestrictions'}}) {
-        my $restriction = $self->{'TicketRestrictions'}{$row};
-       # {{{ if it's an int
-       
-       if ($TYPES{$restriction->{'FIELD'}} eq 'INT' ) {
-           if ($restriction->{'OPERATOR'} =~ /^(=|!=|>|<|>=|<=)$/) {
-               $self->SUPER::Limit( FIELD => $restriction->{'FIELD'},
-                             ENTRYAGGREGATOR => 'AND',
-                             OPERATOR => $restriction->{'OPERATOR'},
-                             VALUE => $restriction->{'VALUE'},
-                             );
-           }
-       }
-       # }}}
-       # {{{ if it's an enum
-       elsif ($TYPES{$restriction->{'FIELD'}} eq 'ENUM') {
-           
-           if ($restriction->{'OPERATOR'} eq '=') {
-               $self->SUPER::Limit( FIELD => $restriction->{'FIELD'},
-                             ENTRYAGGREGATOR => 'OR',
-                             OPERATOR => '=',
-                             VALUE => $restriction->{'VALUE'},
-                           );
-           }
-           elsif ($restriction->{'OPERATOR'} eq '!=') {
-               $self->SUPER::Limit( FIELD => $restriction->{'FIELD'},
-                             ENTRYAGGREGATOR => 'AND',
-                             OPERATOR => '!=',
-                             VALUE => $restriction->{'VALUE'},
-                           );
-           }
-           
-       }
-       # }}}
-       # {{{ if it's a date
-
-       elsif ($TYPES{$restriction->{'FIELD'}} eq 'DATE') {
-           $self->SUPER::Limit( FIELD => $restriction->{'FIELD'},
-                                ENTRYAGGREGATOR => 'AND',
-                                OPERATOR => $restriction->{'OPERATOR'},
-                                VALUE => $restriction->{'VALUE'},
-                              );
-       }
-       # }}}
-       # {{{ if it's a string
-
-       elsif ($TYPES{$restriction->{'FIELD'}} eq 'STRING') {
-           
-           if ($restriction->{'OPERATOR'} eq '=') {
-               $self->SUPER::Limit( FIELD => $restriction->{'FIELD'},
-                             ENTRYAGGREGATOR => 'OR',
-                             OPERATOR => '=',
-                             VALUE => $restriction->{'VALUE'},
-                             CASESENSITIVE => 0
-                           );
-           }
-           elsif ($restriction->{'OPERATOR'} eq '!=') {
-               $self->SUPER::Limit( FIELD => $restriction->{'FIELD'},
-                             ENTRYAGGREGATOR => 'AND',
-                             OPERATOR => '!=',
-                             VALUE => $restriction->{'VALUE'},
-                             CASESENSITIVE => 0
-                           );
-           }
-           elsif ($restriction->{'OPERATOR'} eq 'LIKE') {
-               $self->SUPER::Limit( FIELD => $restriction->{'FIELD'},
-                             ENTRYAGGREGATOR => 'AND',
-                             OPERATOR => 'LIKE',
-                             VALUE => $restriction->{'VALUE'},
-                             CASESENSITIVE => 0
-                           );
-           }
-           elsif ($restriction->{'OPERATOR'} eq 'NOT LIKE') {
-               $self->SUPER::Limit( FIELD => $restriction->{'FIELD'},
-                             ENTRYAGGREGATOR => 'AND',
-                             OPERATOR => 'NOT LIKE',
-                             VALUE => $restriction->{'VALUE'},
-                             CASESENSITIVE => 0
-                           );
-           }
-       }
-
-       # }}}
-       # {{{ if it's Transaction content that we're hunting for
-       elsif ($TYPES{$restriction->{'FIELD'}} eq 'TRANSFIELD') {
-
-           #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.
-           
-           unless (defined $self->{'TicketAliases'}{'TransFieldAlias'}) {
-               $self->{'TicketAliases'}{'TransFieldAlias'} = $self->NewAlias ('Transactions');
-           }
-           unless (defined $self->{'TicketAliases'}{'TransFieldAttachAlias'}){
-               $self->{'TicketAliases'}{'TransFieldAttachAlias'} = $self->NewAlias('Attachments');
-               
-           }
-           #Join transactions to attachments
-           $self->Join( ALIAS1 => $self->{'TicketAliases'}{'TransFieldAttachAlias'},  
-                        FIELD1 => 'TransactionId',
-                        ALIAS2 => $self->{'TicketAliases'}{'TransFieldAlias'}, FIELD2=> 'id');
-           
-           #Join transactions to tickets
-           $self->Join( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'},
-                        ALIAS2 =>$self->{'TicketAliases'}{'TransFieldAlias'}, FIELD2 => 'Ticket');
-           
-           #Search for the right field
-           $self->SUPER::Limit(ALIAS => $self->{'TicketAliases'}{'TransFieldAttachAlias'},
-                                 ENTRYAGGREGATOR => 'AND',
-                                 FIELD =>    $restriction->{'FIELD'},
-                                 OPERATOR => $restriction->{'OPERATOR'} ,
-                                 VALUE =>    $restriction->{'VALUE'},
-                                 CASESENSITIVE => 0
-                               );
-           
-
-       }
-
-       # }}}
-       # {{{ if it's a Transaction date that we're hunting for
-       elsif ($TYPES{$restriction->{'FIELD'}} eq 'TRANSDATE') {
-
-           #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.
-           
-           unless (defined $self->{'TicketAliases'}{'TransFieldAlias'}) {
-               $self->{'TicketAliases'}{'TransFieldAlias'} = $self->NewAlias ('Transactions');
-           }
-
-           #Join transactions to tickets
-           $self->Join( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'},
-                        ALIAS2 =>$self->{'TicketAliases'}{'TransFieldAlias'}, FIELD2 => 'Ticket');
-           
-           #Search for the right field
-           $self->SUPER::Limit(ALIAS => $self->{'TicketAliases'}{'TransFieldAlias'},
-                               ENTRYAGGREGATOR => 'AND',
-                               FIELD =>    'Created',
-                               OPERATOR => $restriction->{'OPERATOR'} ,
-                               VALUE =>    $restriction->{'VALUE'} );
-       }
-
-       # }}}
-       # {{{ if it's a relationship that we're hunting for
-       
-       # Takes FIELD: which is something like "LinkedTo"
-       # takes TARGET or BASE which is the TARGET or BASE id that we're searching for
-       # takes TYPE which is the type of link we're looking for.
-
-       elsif ($TYPES{$restriction->{'FIELD'}} eq 'LINKFIELD') {
-
-           
-           my $LinkAlias = $self->NewAlias ('Links');
-
-           
-           #Make sure we get the right type of link, if we're restricting it
-           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";
-               }       
-
-               $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";
-               }       
-
-
-               $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 it's a watcher that we're hunting for
-       elsif ($TYPES{$restriction->{'FIELD'}} eq 'WATCHERFIELD') {
-
-           my $Watch = $self->NewAlias('Watchers');
-
-           #Join watchers to users
-           my $User = $self->Join( TYPE => 'left',
-                                    ALIAS1 => $Watch, 
-                                    FIELD1 => 'Owner',
-                                    TABLE2 => 'Users', 
-                                    FIELD2 => 'id',
-                                  );
-
-           #Join Ticket to watchers
-           $self->Join( ALIAS1 => 'main', FIELD1 => 'id',
-                        ALIAS2 => $Watch, FIELD2 => 'Value');
-
-
-           #Make sure we're only talking about ticket watchers
-           $self->SUPER::Limit( ALIAS => $Watch,
-                                FIELD => 'Scope',
-                                VALUE => 'Ticket',
-                                OPERATOR => '=');
-
-
-           # Find email address watchers
-           $self->SUPER::Limit( SUBCLAUSE => 'WatcherEmailAddress',
-                                ALIAS => $Watch,
-                                FIELD => 'Email',
-                                ENTRYAGGREGATOR => 'OR',
-                                VALUE => $restriction->{'VALUE'},
-                                OPERATOR => $restriction->{'OPERATOR'},
-                                CASESENSITIVE => 0
-                       );
-
-
-
-           #Find user watchers
-           $self->SUPER::Limit(
-                               SUBCLAUSE => 'WatcherEmailAddress',
-                               ALIAS => $User,
-                               FIELD => 'EmailAddress',
-                               ENTRYAGGREGATOR => 'OR',
-                               VALUE => $restriction->{'VALUE'},
-                               OPERATOR => $restriction->{'OPERATOR'},
-                               CASESENSITIVE => 0
-                              );
-
-           
-           #If we only want a specific type of watchers, then limit it to that
-           if ($restriction->{'TYPE'}) {
-               $self->SUPER::Limit( ALIAS => $Watch,
-                                    FIELD => 'Type',
-                                    ENTRYAGGREGATOR => 'OR',
-                                    VALUE => $restriction->{'TYPE'},
-                                    OPERATOR => '=');
-           }
-       }
-
-       # }}}
-       # {{{ if it's a keyword
-       elsif ($TYPES{$restriction->{'FIELD'}} eq 'KEYWORDFIELD') {
-           my $null_columns_ok;
-
-            my $ObjKeywordsAlias;
-           $ObjKeywordsAlias = $self->{KeywordsAliases}{$restriction->{'KEYWORDSELECT'}}
-             if $restriction->{SingleValued};
-           unless (defined $ObjKeywordsAlias) {
-             $ObjKeywordsAlias = $self->Join(
-                                              TYPE => 'left',
-                                              ALIAS1 => 'main',
-                                              FIELD1 => 'id',
-                                              TABLE2 => 'ObjectKeywords',
-                                              FIELD2 => 'ObjectId'
-                                             );
-             if ($restriction->{'SingleValued'}) {
-               $self->{KeywordsAliases}{$restriction->{'KEYWORDSELECT'}} 
-                 = $ObjKeywordsAlias;
-             }
-           }
-
-         
-            $self->SUPER::Limit(
-                               ALIAS => $ObjKeywordsAlias,
-                               FIELD => 'Keyword',
-                               OPERATOR => $restriction->{'OPERATOR'},
-                               VALUE => $restriction->{'KEYWORD'},
-                               QUOTEVALUE => $restriction->{'QUOTEVALUE'},
-                               ENTRYAGGREGATOR => 'OR',
-                               );
-           
-            if  ( ($restriction->{'OPERATOR'} =~ /^IS$/i) or 
-                 ($restriction->{'OPERATOR'} eq '!=') ) {
-               
-               $null_columns_ok=1;
-
-           } 
-
-           #If we're trying to find tickets where the keyword isn't somethng, also check ones where it _IS_ null
-           if ( $restriction->{'OPERATOR'} eq '!=') {
-               $self->SUPER::Limit(
-                                   ALIAS => $ObjKeywordsAlias,
-                                   FIELD => 'Keyword',
-                                   OPERATOR => 'IS',
-                                   VALUE => 'NULL',
-                                   QUOTEVALUE => 0,
-                                   ENTRYAGGREGATOR => 'OR',
-                                  );
-             }
-
-
-            $self->SUPER::Limit(LEFTJOIN => $ObjKeywordsAlias,
-                               FIELD => 'KeywordSelect',
-                               VALUE => $restriction->{'KEYWORDSELECT'},
-                               ENTRYAGGREGATOR => 'OR');
+        # 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
 
 
-            $self->SUPER::Limit( ALIAS => $ObjKeywordsAlias,
-                                 FIELD => 'ObjectType',
-                                 VALUE => 'Ticket',
-                                 ENTRYAGGREGATOR => 'AND');
-           
-           if ($null_columns_ok) {
-                $self->SUPER::Limit(ALIAS => $ObjKeywordsAlias,
-                                    FIELD => 'ObjectType',
-                                   OPERATOR => 'IS',
-                                    VALUE => 'NULL',
-                                   QUOTEVALUE => 0,
-                                    ENTRYAGGREGATOR => 'OR');
-           }
-          
+        # One special case
+        # Rewrite LinkedTo meta field to the real field
+        if ( $field =~ /LinkedTo/ ) {
+            $realfield = $field = $restriction->{'TYPE'};
         }
         }
-        # }}}
 
 
-    
-     }
-
-     
-     # here, we make sure we don't get any tickets that have been merged  into other tickets
-     # (Ticket Id == Ticket EffectiveId
-     # note that we _really_ don't want to do this if we're already looking at the effectiveid
-     if ($self->_isLimited && (! $self->{'looking_at_effective_id'})) {
-        $self->SUPER::Limit( FIELD => 'EffectiveId', 
-              OPERATOR => '=',
-              QUOTEVALUE => 0,
-              VALUE => 'main.id');   #TODO, we shouldn't be hard coding the tablename to main.
-      } 
-    $self->{'RecalcTicketLimits'} = 0;
-}
+        # 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 $FIELD_METADATA{$realfield}
+                or $restriction->{CUSTOMFIELD} );
+
+        my $type = $FIELD_METADATA{$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;
+        }
 
 
-# {{{ Deal with displaying rows of the listing 
+        exists $clause{$realfield} or $clause{$realfield} = [];
 
 
-#
-#  Everything in this section is stub code for 2.2
-# It's not part of the API. It's not for your use
-# It's not for our use.
-#
+        # 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")
 
 
-# {{{ sub SetListingFormat
+        if (lc $ea eq 'none') {
+            $clause{$realfield} = [ $data ];
+        } else {
+            push @{ $clause{$realfield} }, $data;
+        }
+    }
+    return \%clause;
+}
 
 
-=head2 SetListingFormat
 
 
-Takes a single Format string as specified below. parses that format string and makes the various listing output
-things DTRT.
 
 
-=item Format strings
+=head2 _ProcessRestrictions PARAMHASH
 
 
-Format strings are made up of a chain of Elements delimited with vertical pipes (|).
-Elements of a Format string 
+# The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
+# but isn't quite generic enough to move into Tickets_SQL.
 
 
+=cut
 
 
-FormatString:    Element[::FormatString]
+sub _ProcessRestrictions {
+    my $self = shift;
 
 
-Element:         AttributeName[;HREF=<URL>][;TITLE=<TITLE>]
+    #Blow away ticket aliases since we'll need to regenerate them for
+    #a new search
+    delete $self->{'TicketAliases'};
+    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) if $sql;
+        }
+    }
 
 
-AttributeName    Id | Subject | Status | Owner | Priority | InitialPriority | TimeWorked | TimeLeft |
-  
-                 Keywords[;SELECT=<KeywordSelect>] | 
-       
-                <Created|Starts|Started|Contacted|Due|Resolved>Date<AsString|AsISO|AsAge>
+    $self->{'RecalcTicketLimits'} = 0;
 
 
+}
 
 
-=cut
+=head2 _BuildItemMap
 
 
+Build up a L</ItemMap> of first/last/next/prev items, so that we can
+display search nav quickly.
 
 
+=cut
 
 
+sub _BuildItemMap {
+    my $self = shift;
 
 
-#accept a format string
+    my $window = RT->Config->Get('TicketsItemMapSize');
 
 
+    $self->{'item_map'} = {};
 
 
+    my $items = $self->ItemsArrayRefWindow( $window );
+    return unless $items && @$items;
 
 
-sub SetListingFormat {
-    my $self = shift;
-    my $listing_format = shift;
-    
-    my ($element, $attribs);
-    my $i = 0;
-    foreach $element (split (/::/,$listing_format)) {
-       if ($element =~ /^(.*?);(.*)$/) {
-           $element = $1;
-           $attribs = $2;
-       }       
-       $self->{'format_string'}->[$i]->{'Element'} = $element;
-       foreach $attrib (split (/;/, $attribs)) {
-           my $value = "";
-           if ($attrib =~ /^(.*?)=(.*)$/) {
-               $attrib = $1;
-               $value = $2;
-           }   
-           $self->{'format_string'}->[$i]->{"$attrib"} = $val;
-           
-       }
-    
+    my $prev = 0;
+    $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
+    for ( my $i = 0; $i < @$items; $i++ ) {
+        my $item = $items->[$i];
+        my $id = $item->EffectiveId;
+        $self->{'item_map'}{$id}{'defined'} = 1;
+        $self->{'item_map'}{$id}{'prev'}    = $prev;
+        $self->{'item_map'}{$id}{'next'}    = $items->[$i+1]->EffectiveId
+            if $items->[$i+1];
+        $prev = $id;
     }
     }
-    return(1);
+    $self->{'item_map'}{'last'} = $prev
+        if !$window || @$items < $window;
 }
 
 }
 
-# }}}
+=head2 ItemMap
 
 
-# {{{ sub HeaderAsHTML
-sub HeaderAsHTML {
-    my $self = shift;
-    my $header = "";
-    my $col;
-    foreach $col ( @{[ $self->{'format_string'} ]}) {
-       $header .= "<TH>" . $self->_ColumnTitle($self->{'format_string'}->[$col]) . "</TH>";
-       
+Returns an a map of all items found by this search. The map is a hash
+of the form:
+
+    {
+        first => <first ticket id found>,
+        last => <last ticket id found or undef>,
+
+        <ticket id> => {
+            prev => <the ticket id found before>,
+            next => <the ticket id found after>,
+        },
+        <ticket id> => {
+            prev => ...,
+            next => ...,
+        },
     }
     }
-    return ($header);
-}
-# }}}
 
 
-# {{{ sub HeaderAsText
-#Print text header
-sub HeaderAsText {
-    my $self = shift;
-    my ($header);
-    
-    return ($header);
-}
-# }}}
+=cut
 
 
-# {{{ sub TicketAsHTMLRow
-#Print HTML row
-sub TicketAsHTMLRow {
+sub ItemMap {
     my $self = shift;
     my $self = shift;
-    my $Ticket = shift;
-    my ($row, $col);
-    foreach $col (@{[$self->{'format_string'}]}) {
-       $row .= "<TD>" . $self->_TicketColumnValue($ticket,$self->{'format_string'}->[$col]) . "</TD>";
-       
-    }
-    return ($row);
+    $self->_BuildItemMap unless $self->{'item_map'};
+    return $self->{'item_map'};
 }
 }
-# }}}
 
 
-# {{{ sub TicketAsTextRow
-#Print text row
-sub TicketAsTextRow {
-    my $self = shift;
-    my ($row);
 
 
-    #TODO implement
-    
-    return ($row);
-}
-# }}}
 
 
-# {{{ _ColumnTitle {
 
 
-sub _ColumnTitle {
+=head2 PrepForSerialization
+
+You don't want to serialize a big tickets object, as
+the {items} hash will be instantly invalid _and_ eat
+lots of space
+
+=cut
+
+sub PrepForSerialization {
     my $self = shift;
     my $self = shift;
-    
-    # Attrib is a hash 
-    my $attrib = shift;
-    
-    # return either attrib->{'TITLE'} or..
-    if ($attrib->{'TITLE'}) {
-       return($attrib->{'TITLE'});
-    }  
-    # failing that, Look up the title in a hash
-    else {
-       #TODO create $self->{'ColumnTitles'};
-       return ($self->{'ColumnTitles'}->{$attrib->{'Element'}});
-    }  
-    
+    delete $self->{'items'};
+    delete $self->{'items_array'};
+    $self->RedoSearch();
 }
 
 }
 
-# }}}
+=head1 FLAGS
+
+RT::Tickets supports several flags which alter search behavior:
+
 
 
-# {{{ _TicketColumnValue
-sub _TicketColumnValue {
+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
+
+
+
+=head2 NewItem
+
+Returns an empty new RT::Ticket item
+
+=cut
+
+sub NewItem {
     my $self = shift;
     my $self = shift;
-    my $Ticket = shift;
-    my $attrib = shift;
+    return(RT::Ticket->new($self->CurrentUser));
+}
+RT::Base->_ImportOverlays();
 
 
-    
-    my $out;
-
-  SWITCH: {
-       /^id/i && do {
-           $out = $Ticket->id;
-           last SWITCH; 
-       };
-       /^subj/i && do {
-           last SWITCH; 
-           $Ticket->Subject;
-                  };   
-       /^status/i && do {
-           last SWITCH; 
-           $Ticket->Status;
-       };
-       /^prio/i && do {
-           last SWITCH; 
-           $Ticket->Priority;
-       };
-       /^finalprio/i && do {
-           
-           last SWITCH; 
-           $Ticket->FinalPriority
-       };
-       /^initialprio/i && do {
-           
-           last SWITCH; 
-           $Ticket->InitialPriority;
-       };      
-       /^timel/i && do {
-           
-           last SWITCH; 
-           $Ticket->TimeWorked;
-       };
-       /^timew/i && do {
-           
-           last SWITCH; 
-           $Ticket->TimeLeft;
-       };
-       
-       /^(.*?)date(.*)$/i && do {
-           my $o = $1;
-           my $m = $2;
-           my ($obj);
-           #TODO: optimize
-           $obj = $Ticket->DueObj         if $o =~ /due/i;
-           $obj = $Ticket->CreatedObj     if $o =~ /created/i;
-           $obj = $Ticket->StartsObj      if $o =~ /starts/i;
-           $obj = $Ticket->StartedObj     if $o =~ /started/i;
-           $obj = $Ticket->ToldObj        if $o =~ /told/i;
-           $obj = $Ticket->LastUpdatedObj if $o =~ /lastu/i;
-           
-           $method = 'ISO' if $m =~ /iso/i;
-           
-           $method = 'AsString' if $m =~ /asstring/i;
-           $method = 'AgeAsString' if $m =~ /age/i;
-           last SWITCH;
-           $obj->$method();
-             
-       };
-         
-         /^watcher/i && do {
-             last SWITCH; 
-             $Ticket->WatchersAsString();
-         };    
-       
-       /^requestor/i && do {
-           last SWITCH; 
-           $Ticket->RequestorsAsString();
-       };      
-       /^cc/i && do {
-           last SWITCH; 
-           $Ticket->CCAsString();
-       };      
-       
-       
-       /^admincc/i && do {
-           last SWITCH; 
-           $Ticket->AdminCcAsString();
-       };
-       
-       /^keywords/i && do {
-           last SWITCH; 
-           #Limit it to the keyword select we're talking about, if we've got one.
-           my $objkeys =$Ticket->KeywordsObj($attrib->{'SELECT'});
-           $objkeys->KeywordRelativePathsAsString();
-       };
-       
-    }
-      
-}
-
-# }}}
-
-# }}}
-
-# {{{ POD
-=head2 notes
-"Enum" Things that get Is, IsNot
-
-
-"Int" Things that get Is LessThan and GreaterThan
-id
-InitialPriority
-FinalPriority
-Priority
-TimeLeft
-TimeWorked
-
-"Text" Things that get Is, Like
-Subject
-TransactionContent
-
-
-"Link" OPERATORs
-
-
-"Date" OPERATORs Is, Before, After
-
-  =cut
-# }}}
 1;
 1;