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
 
@@ -8,7 +65,7 @@
 =head1 SYNOPSIS
 
   use RT::Tickets;
-  my $tickets = new RT::Tickets($CurrentUser);
+  my $tickets = RT::Tickets->new($CurrentUser);
 
 =head1 DESCRIPTION
 
 
 =head1 METHODS
 
-=begin testing
-
-ok (require RT::TestHarness);
-ok (require RT::Tickets);
-
-=end testing
 
 =cut
 
 package RT::Tickets;
-use RT::EasySearch;
+
+use strict;
+use warnings;
+
+
 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
 
@@ -85,140 +250,2177 @@ Returns the list of fields that lists of tickets can easily be sorted by
 
 =cut
 
-
 sub SortFields {
-       my $self = shift;
-       return(@SORTFIELDS);
+    my $self = shift;
+    return (@SORTFIELDS);
 }
 
 
-# }}}
+# BEGIN SQL STUFF *********************************
 
-# {{{ 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
-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 %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
-# {{{ 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
-# {{{ 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 =).
-VALUE is a queue id. 
+VALUE is a queue id or Name.
+
 
 =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
 
-    $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
 
@@ -226,22 +2428,51 @@ Takes a paramhash with the fields OPERATOR and VALUE.
 OPERATOR is one of = or !=.
 VALUE is a status.
 
+RT adds Status != 'deleted' until object has
+allow_deleted_search internal property set.
+$tickets->{'allow_deleted_search'} = 1;
+$tickets->LimitStatus( VALUE => 'deleted' );
+
 =cut
 
 sub LimitStatus {
     my $self = shift;
-    my %args = ( OPERATOR => '=',
-                  @_);
-    $self->Limit (FIELD => 'Status',
-                 VALUE => $args{'VALUE'},
-                 OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => '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
 
@@ -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.
 
+
+
 =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
 
@@ -282,21 +2515,19 @@ VALUE is a string to search for in the subject of the ticket.
 sub LimitSubject {
     my $self = shift;
     my %args = (@_);
-    $self->Limit (FIELD => 'Subject',
-                 VALUE => $args{'VALUE'},
-                 OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => '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 > < = !=
 
-# {{{ sub LimitId
 
 =head2 LimitId
 
@@ -308,47 +2539,50 @@ VALUE is a ticket Id to search for
 
 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 !=.
-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 = (@_);
-    $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 !=.
-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
@@ -356,38 +2590,40 @@ VALUE is a value to match the ticket\'s initial priority against
 sub LimitInitialPriority {
     my $self = shift;
     my %args = (@_);
-    $self->Limit (FIELD => 'InitialPriority',
-                 VALUE => $args{'VALUE'},
-                 OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => '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 !=.
-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 = (@_);
-    $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
 
@@ -400,16 +2636,17 @@ VALUE is a value to match the ticket's TimeWorked attribute
 sub LimitTimeWorked {
     my $self = shift;
     my %args = (@_);
-    $self->Limit (FIELD => 'TimeWorked',
-                 VALUE => $args{'VALUE'},
-                 OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => '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
 
@@ -422,20 +2659,19 @@ VALUE is a value to match the ticket's TimeLeft attribute
 sub LimitTimeLeft {
     my $self = shift;
     my %args = (@_);
-    $self->Limit (FIELD => 'TimeLeft',
-                 VALUE => $args{'VALUE'},
-                 OPERATOR => $args{'OPERATOR'},
-                 DESCRIPTION => '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
 
@@ -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
-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 = (@_);
-    $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.
-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
 
-
-sub LimitRequestor {
+sub LimitContentType {
     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
 
-sub LimitCc {
+sub LimitOwner {
     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
-  
-sub LimitAdminCc {
+
+sub LimitWatcher {
     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
-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 used to be 'TICKET'.  'TICKET' is deprecated, but will be treated as TARGET
 
 =cut
 
 sub LimitLinkedTo {
     my $self = shift;
-    my %args = ( 
-               TICKET => undef,
-               TARGET => undef,
-               TYPE => undef,
-                @_);
-
-
-    $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
-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 used to be 'TICKET'.  'TICKET' is deprecated, but will be treated as BASE
-
 
 =cut
 
 sub LimitLinkedFrom {
     my $self = shift;
-    my %args = ( BASE => undef,
-                TICKET => undef,
-                TYPE => undef,
-                @_);
-
-    
-    $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 {
-    my $self = shift;
+    my $self      = shift;
     my $ticket_id = shift;
-    $self->LimitLinkedTo ( TARGET=> "$ticket_id",
-                          TYPE => 'MemberOf',
-                         );
-    
+    return $self->LimitLinkedTo(
+        @_,
+        TARGET => $ticket_id,
+        TYPE   => 'MemberOf',
+    );
 }
-# }}}
 
-# {{{ 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 {
-    my $self = shift;
+    my $self      = shift;
     my $ticket_id = shift;
-    $self->LimitLinkedTo ( TARGET => "$ticket_id",
-                           TYPE => 'DependsOn',
-                          );
-    
+    return $self->LimitLinkedTo(
+        @_,
+        TARGET => $ticket_id,
+        TYPE   => 'DependsOn',
+    );
+
 }
 
-# }}}
 
-# {{{ LimitDependedOnBy
 
 sub LimitDependedOnBy {
-    my $self = shift;
+    my $self      = shift;
     my $ticket_id = shift;
-    $self->LimitLinkedFrom (  BASE => "$ticket_id",
-                               TYPE => 'DependsOn',
-                            );
-    
-}
+    return $self->LimitLinkedFrom(
+        @_,
+        BASE => $ticket_id,
+        TYPE => 'DependentOn',
+    );
 
-# }}}
+}
 
 
-# {{{ LimitRefersTo
 
 sub LimitRefersTo {
-    my $self = shift;
+    my $self      = shift;
     my $ticket_id = shift;
-    $self->LimitLinkedTo ( TARGET => "$ticket_id",
-                           TYPE => 'RefersTo',
-                          );
-    
+    return $self->LimitLinkedTo(
+        @_,
+        TARGET => $ticket_id,
+        TYPE   => 'RefersTo',
+    );
+
 }
 
-# }}}
 
-# {{{ LimitReferredToBy
 
 sub LimitReferredToBy {
-    my $self = shift;
+    my $self      = 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.
 
-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
 
@@ -764,64 +2990,69 @@ the need to pass in a FIELD argument.
 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
-    unless ($args{'DESCRIPTION'} ) {
-       $args{'DESCRIPTION'} = $args{'FIELD'} . " " .$args{'OPERATOR'}. " ". $args{'VALUE'} . " GMT"
+    unless ( $args{'DESCRIPTION'} ) {
+        $args{'DESCRIPTION'} = $args{'FIELD'} . " "
+            . $args{'OPERATOR'} . " "
+            . $args{'VALUE'} . " GMT";
     }
 
-    $self->Limit (%args);
+    $self->Limit(%args);
 
 }
 
-# }}}
-
-
-
 
 sub LimitCreated {
     my $self = shift;
-    $self->LimitDate( FIELD => 'Created', @_);
+    $self->LimitDate( FIELD => 'Created', @_ );
 }
+
 sub LimitDue {
     my $self = shift;
-    $self->LimitDate( FIELD => 'Due', @_);
+    $self->LimitDate( FIELD => 'Due', @_ );
 
 }
+
 sub LimitStarts {
     my $self = shift;
-    $self->LimitDate( FIELD => 'Starts', @_);
+    $self->LimitDate( FIELD => 'Starts', @_ );
 
 }
+
 sub LimitStarted {
     my $self = shift;
-    $self->LimitDate( FIELD => 'Started', @_);
+    $self->LimitDate( FIELD => 'Started', @_ );
 }
-sub LimitResolved { 
+
+sub LimitResolved {
     my $self = shift;
-    $self->LimitDate( FIELD => 'Resolved', @_);
+    $self->LimitDate( FIELD => 'Resolved', @_ );
 }
+
 sub LimitTold {
     my $self = shift;
-    $self->LimitDate( FIELD => 'Told', @_);
+    $self->LimitDate( FIELD => 'Told', @_ );
 }
+
 sub LimitLastUpdated {
     my $self = shift;
-    $self->LimitDate( FIELD => 'LastUpdated', @_);
+    $self->LimitDate( FIELD => 'LastUpdated', @_ );
 }
+
 #
-# {{{ sub LimitTransactionDate
 
 =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
 
 
@@ -830,94 +3061,111 @@ VALUE is a date and time in ISO format in GMT
 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
-    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
 
-=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
 
-sub LimitKeyword {
+sub LimitCustomField {
     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 ($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;
-    return ($index);
 }
 
-# }}}
 
-# {{{ sub _NextIndex
 
 =head2 _NextIndex
 
@@ -927,44 +3175,44 @@ Keep track of the counter for the array of restrictions
 
 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;
-    $self->{'table'} = "Tickets";
-    $self->{'RecalcTicketLimits'} = 1;
+    $self->{'table'}                   = "Tickets";
+    $self->{'RecalcTicketLimits'}      = 1;
     $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->_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
 
@@ -974,57 +3222,333 @@ Returns a reference to the set of all items found in this search
 
 sub ItemsArrayRef {
     my $self = shift;
-    my @items;
-    
+
+    return $self->{'items_array'} if $self->{'items_array'};
+
     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);
-    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 {
-       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
 
@@ -1033,32 +3557,29 @@ TODO It is not yet implemented
 
 =cut
 
-# }}}
 
-# {{{ sub DescribeRestrictions
 
 =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
 
-sub DescribeRestrictions  {
+sub DescribeRestrictions {
     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);
 }
-# }}}
 
-# {{{ sub RestrictionValues
+
 
 =head2 RestrictionValues FIELD
 
@@ -1068,36 +3589,32 @@ to.
 =cut
 
 sub RestrictionValues {
-    my $self = shift;
+    my $self  = shift;
     my $field = shift;
-    map $self->{'TicketRestrictions'}{$_}{'VALUE'},
-      grep {
-             $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
-             && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
-           }
-        keys %{$self->{'TicketRestrictions'}};
+    map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
+               $self->{'TicketRestrictions'}{$_}{'FIELD'}    eq $field
+            && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
+        }
+        keys %{ $self->{'TicketRestrictions'} };
 }
 
-# }}}
 
-# {{{ sub ClearRestrictions
 
 =head2 ClearRestrictions
 
 Removes all restrictions irretrievably
 
 =cut
-  
+
 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
 
@@ -1106,684 +3623,259 @@ Removes that restriction from the session's limits.
 
 =cut
 
-
 sub DeleteRestriction {
     my $self = shift;
-    my $row = shift;
+    my $row  = shift;
     delete $self->{'TicketRestrictions'}{$row};
-    
+
     $self->{'RecalcTicketLimits'} = 1;
+
     #make the underlying easysearch object forget all its preconceptions
 }
 
-# }}}
 
-# {{{ sub _ProcessRestrictions 
 
-sub _ProcessRestrictions {
+# Convert a set of oldstyle SB Restrictions to Clauses for RQL
+
+sub _RestrictionsToClauses {
     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 $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;
-    
-    # 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 $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;