]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 574556: Refactor Search.pm so that we're not doing $$some_var everywhere.
authorMax Kanat-Alexander <mkanat@bugzilla.org>
Thu, 8 Jul 2010 18:23:59 +0000 (11:23 -0700)
committerMax Kanat-Alexander <mkanat@bugzilla.org>
Thu, 8 Jul 2010 18:23:59 +0000 (11:23 -0700)
Instead, we pass around a hashref and update the hashref. This patch also
includes some cleanup for bugs surrounding percentage_complete,
attachments.isobsolete, attachments.ispatch, and owner_idle_time.
r=mkanat, a=mkanat

Bugzilla/Bug.pm
Bugzilla/Group.pm
Bugzilla/Search.pm
template/en/default/global/code-error.html.tmpl
template/en/default/global/user-error.html.tmpl
xt/lib/Bugzilla/Test/Search/Constants.pm

index e32add2e1013c5146d3a8125a676fc42afb3a7cd..efee1482629a673259ac1b544316047c21a664d5 100644 (file)
@@ -1632,8 +1632,7 @@ sub _check_keywords {
     my %keywords;
     foreach my $keyword (@$keyword_array) {
         next unless $keyword;
-        my $obj = new Bugzilla::Keyword({ name => $keyword });
-        ThrowUserError("unknown_keyword", { keyword => $keyword }) if !$obj;
+        my $obj = Bugzilla::Keyword->check($keyword);
         $keywords{$obj->id} = $obj;
     }
     return [values %keywords];
index f047ef365e5a46afeb9cd9d7f6773afcf7b851fc..29b2a2f1c9092509437066670600c4041ef84fd2 100644 (file)
@@ -185,6 +185,16 @@ sub products {
 ####        Methods        ####
 ###############################
 
+sub check_members_are_visible {
+    my $self = shift;
+    my $user = Bugzilla->user;
+    return if !Bugzilla->params->{'usevisibilitygroups'};
+    my $is_visible = grep { $_->id == $_ } @{ $user->visible_groups_inherited };
+    if (!$is_visible) {
+        ThrowUserError('group_not_visible', { group => $self });
+    }
+}
+
 sub set_description { $_[0]->set('description', $_[1]); }
 sub set_is_active   { $_[0]->set('isactive', $_[1]);    }
 sub set_name        { $_[0]->set('name', $_[1]);        }
@@ -407,25 +417,6 @@ sub create {
     return $group;
 }
 
-sub ValidateGroupName {
-    my ($name, @users) = (@_);
-    my $dbh = Bugzilla->dbh;
-    my $query = "SELECT id FROM groups " .
-                "WHERE name = ?";
-    if (Bugzilla->params->{'usevisibilitygroups'}) {
-        my @visible = (-1);
-        foreach my $user (@users) {
-            $user && push @visible, @{$user->visible_groups_direct};
-        }
-        my $visible = join(', ', @visible);
-        $query .= " AND id IN($visible)";
-    }
-    my $sth = $dbh->prepare($query);
-    $sth->execute($name);
-    my ($ret) = $sth->fetchrow_array();
-    return $ret;
-}
-
 ###############################
 ###       Validators        ###
 ###############################
@@ -486,7 +477,6 @@ Bugzilla::Group - Bugzilla group class.
     my $icon_url     = $group->icon_url;
     my $is_active_bug_group = $group->is_active_bug_group;
 
-    my $group_id = Bugzilla::Group::ValidateGroupName('admin', @users);
     my @groups   = Bugzilla::Group->get_all;
 
 =head1 DESCRIPTION
@@ -506,24 +496,15 @@ normally does, this function also makes the new group be inherited
 by the C<admin> group. That is, the C<admin> group will automatically
 be a member of this group.
 
-=item C<ValidateGroupName($name, @users)>
-
- Description: ValidateGroupName checks to see if ANY of the users
-              in the provided list of user objects can see the
-              named group.
-
- Params:      $name - String with the group name.
-              @users - An array with Bugzilla::User objects.
-
- Returns:     It returns the group id if successful
-              and undef otherwise.
-
-=back
-
 =head1 METHODS
 
 =over
 
+=item C<check_members_are_visible>
+
+Throws an error if this group is not visible (according to 
+visibility groups) to the currently-logged-in user.
+
 =item C<check_remove>
 
 =over
index 89e2dfa61b93d683c7e40a84cd7ec162ea112165..7128b87512ca097cfd1eb67fc6691025345756eb 100644 (file)
@@ -60,20 +60,20 @@ use Storable qw(dclone);
 # If you specify a search type in the boolean charts, this describes
 # which operator maps to which internal function here.
 use constant OPERATORS => {
-    equals         => \&_equals,
-    notequals      => \&_notequals,
+    equals         => \&_simple_operator,
+    notequals      => \&_simple_operator,
     casesubstring  => \&_casesubstring,
     substring      => \&_substring,
     substr         => \&_substring,
     notsubstring   => \&_notsubstring,
     regexp         => \&_regexp,
     notregexp      => \&_notregexp,
-    lessthan       => \&_lessthan,
-    lessthaneq     => \&_lessthaneq,
+    lessthan       => \&_simple_operator,
+    lessthaneq     => \&_simple_operator,
     matches        => sub { ThrowUserError("search_content_without_matches"); },
     notmatches     => sub { ThrowUserError("search_content_without_matches"); },
-    greaterthan    => \&_greaterthan,
-    greaterthaneq  => \&_greaterthaneq,
+    greaterthan    => \&_simple_operator,
+    greaterthaneq  => \&_simple_operator,
     anyexact       => \&_anyexact,
     anywordssubstr => \&_anywordsubstr,
     allwordssubstr => \&_allwordssubstr,
@@ -85,7 +85,35 @@ use constant OPERATORS => {
     changedafter   => \&_changedbefore_changedafter,
     changedfrom    => \&_changedfrom_changedto,
     changedto      => \&_changedfrom_changedto,
-    changedby      => \&_changedby,    
+    changedby      => \&_changedby,
+};
+
+# Some operators are really just standard SQL operators, and are
+# all implemented by the _simple_operator function, which uses this
+# constant.
+use constant SIMPLE_OPERATORS => {
+    equals        => '=',
+    notequals     => '!=',
+    greaterthan   => '>',
+    greaterthaneq => '>=',
+    lessthan      => '<',
+    lessthaneq    => "<=",
+};
+
+# Most operators just reverse by removing or adding "not" from/to them.
+# However, some operators reverse in a different way, so those are listed
+# here.
+use constant OPERATOR_REVERSE => {
+    nowords        => 'anywords',
+    nowordssubstr  => 'anywordssubstr',
+    anywords       => 'nowords',
+    anywordssubstr => 'nowordssubstr',
+    lessthan       => 'greaterthaneq',
+    lessthaneq     => 'greaterthan',
+    greaterthan    => 'lessthaneq',
+    greaterthaneq  => 'lessthan',
+    # The following don't currently have reversals:
+    # casesubstring, anyexact, allwords, allwordssubstr
 };
 
 use constant OPERATOR_FIELD_OVERRIDE => {
@@ -95,7 +123,7 @@ use constant OPERATOR_FIELD_OVERRIDE => {
         _default => \&_attachments_submitter,
     },
     assigned_to => {
-        _non_changed => \&_assigned_to_reporter_nonchanged,
+        _non_changed => \&_contact_nonchanged,
     },
     cc => {
         _non_changed => \&_cc_nonchanged,
@@ -104,7 +132,7 @@ use constant OPERATOR_FIELD_OVERRIDE => {
         _default => \&_commenter,
     },
     reporter => {
-        _non_changed => \&_assigned_to_reporter_nonchanged,
+        _non_changed => \&_contact_nonchanged,
     },
     'requestees.login_name' => {
         _default => \&_requestees_login_name,
@@ -184,6 +212,7 @@ use constant OPERATOR_FIELD_OVERRIDE => {
         greaterthaneq => \&_owner_idle_time_greater_less,
         lessthan      => \&_owner_idle_time_greater_less,
         lessthaneq    => \&_owner_idle_time_greater_less,
+        _default      => \&_invalid_combination,
     },
     
     product => {
@@ -207,7 +236,7 @@ use constant OPERATOR_FIELD_OVERRIDE => {
     
     # Timetracking Fields
     percentage_complete => {
-        _default => \&_percentage_complete,
+        _non_changed => \&_percentage_complete,
     },
     work_time => {
         changedby     => \&_work_time_changedby,
@@ -929,7 +958,6 @@ sub init {
                  $col++) 
             {
                 my $field = $params->param("field$chart-$row-$col") || "noop";
-                my $original_field = $field; # Saved for search_description
                 my $operator = $params->param("type$chart-$row-$col") || "noop";
                 my $value = $params->param("value$chart-$row-$col");
                 $value = "" if !defined $value;
@@ -950,39 +978,40 @@ sub init {
                 my $quoted = $dbh->quote($value);
                 trick_taint($quoted);
 
-                my $term;
                 my $full_field = $field =~ /\./ ? $field : "bugs.$field";
-                $self->do_search_function({
-                    chartid  => \$chartid,
-                    sequence => \$sequence,
-                    f        => \$field,
-                    ff       => \$full_field,
-                    t        => \$operator,
-                    v        => \$value,
-                    q        => \$quoted,
-                    term     => \$term,
+                my %search_args = (
+                    chart_id   => $chartid,
+                    sequence   => $sequence,
+                    field      => $field,
+                    full_field => $full_field,
+                    operator   => $operator,
+                    value      => $value,
+                    quoted     => $quoted,
                     multi_fields => \@multi_select_fields,
-                    supptables   => \@supptables,
-                    wherepart    => \@wherepart,
+                    joins        => \@supptables,
+                    where        => \@wherepart,
                     having       => \@having,
-                    groupby      => \@groupby,
-                    chartfields  => \%chartfields,
+                    group_by     => \@groupby,
                     fields       => \@fields,
-                });
-
-                if ($term) {
+                    chart_fields => \%chartfields,
+                );
+                # This should add a "term" selement to %search_args.
+                $self->do_search_function(\%search_args);
+                
+                if ($search_args{term}) {
+                    # All the things here that don't get pulled out of
+                    # %search_args are their original values before
+                    # do_search_function modified them.
                     $self->search_description({
-                        field => $original_field, type => $operator,
-                        value => $value, term => $term,
+                        field => $field, type => $operator,
+                        value => $value, term => $search_args{term},
                     });
-                    push(@orlist, $term);
+                    push(@orlist, $search_args{term});
                 }
                 else {
                     # This field and this type don't work together.
-                    ThrowCodeError("field_type_mismatch",
-                                   { field => $params->param("field$chart-$row-$col"),
-                                     type => $params->param("type$chart-$row-$col"),
-                                   });
+                    ThrowUserrror("search_field_operator_invalid",
+                                   { field => $field, operator => $operator });
                 }
             }
             if (@orlist) {
@@ -1116,16 +1145,17 @@ sub init {
 # it into SQL, using the constants at the top of this file.
 sub do_search_function {
     my ($self, $args) = @_;
-    my ($field, $operator, $value) = @$args{qw(f t v)};
+    my ($field, $operator, $value) = @$args{qw(field operator value)};
     
-    my $actual_field = FIELD_MAP->{$$field} || $$field;
+    my $actual_field = FIELD_MAP->{$field} || $field;
+    $args->{field} = $actual_field;
     
     if (my $parse_func = SPECIAL_PARSING->{$actual_field}) {
-        $self->$parse_func(%$args);
+        $self->$parse_func($args);
         # Some parsing functions set $term, though most do not.
         # For the ones that set $term, we don't need to do any further
         # parsing.
-        return if ${ $args->{term} };
+        return if $args->{term};
     }
     
     my $override = OPERATOR_FIELD_OVERRIDE->{$actual_field};
@@ -1142,14 +1172,14 @@ sub do_search_function {
     }
     
     if ($override) {
-        my $search_func = $self->_pick_override_function($override, $$operator);
-        $self->$search_func(%$args) if $search_func;
+        my $search_func = $self->_pick_override_function($override, $operator);
+        $self->$search_func($args) if $search_func;
     }
 
     # Some search functions set $term, and some don't. For the ones that
     # don't (or for fields that don't have overrides) we now call the
     # direct operator function from OPERATORS.
-    if (!${ $args->{term} }) {
+    if (!defined $args->{term}) {
         $self->_do_operator_function($args);
     }
 }
@@ -1158,9 +1188,19 @@ sub do_search_function {
 # functions directly.
 sub _do_operator_function {
     my ($self, $func_args) = @_;
-    my $operator = $func_args->{t};
-    my $operator_func = OPERATORS->{$$operator};
-    $self->$operator_func(%$func_args);
+    my $operator = $func_args->{operator};
+    my $operator_func = OPERATORS->{$operator};
+    $self->$operator_func($func_args);
+}
+
+sub _reverse_operator {
+    my ($self, $operator) = @_;
+    my $reverse = OPERATOR_REVERSE->{$operator};
+    return $reverse if $reverse;
+    if ($operator =~ s/^not//) {
+        return $operator;
+    }
+    return "not$operator";
 }
 
 sub _pick_override_function {
@@ -1415,180 +1455,185 @@ sub translate_old_column {
 # Search Functions
 #####################################################################
 
+sub _invalid_combination {
+    my ($self, $args) = @_;
+    my ($field, $operator) = @$args{qw(field operator)};
+    ThrowUserError('search_field_operator_invalid',
+                   { field => $field, operator => $operator });
+}
+
 sub _contact_pronoun {
-    my $self = shift;
-    my %func_args = @_;
-    my ($value, $quoted) = @func_args{qw(v q)};
+    my ($self, $args) = @_;
+    my ($value, $quoted) = @$args{qw(value quoted)};
     my $user = $self->{'user'};
     
-    if ($$value =~ /^\%group/) {
-        $self->_contact_exact_group(%func_args);
+    if ($value =~ /^\%group/) {
+        $self->_contact_exact_group($args);
     }
-    elsif ($$value =~ /^(%\w+%)$/) {
-        $$value = pronoun($1, $user);
-        $$quoted = $$value;
+    elsif ($value =~ /^(%\w+%)$/) {
+        $args->{value} = pronoun($1, $user);
+        $args->{quoted} = $args->{value};
     }
-    
 }
 
 sub _contact_exact_group {
-    my $self = shift;
-    my %func_args = @_;
-    my ($chartid, $supptables, $f, $t, $v, $term) =
-        @func_args{qw(chartid supptables f t v term)};
-    my $user = $self->{'user'};
+    my ($self, $args) = @_;
+    my ($value, $operator, $field, $chart_id, $joins) =
+        @$args{qw(value operator field chart_id joins)};
+    my $dbh = Bugzilla->dbh;
     
-    $$v =~ /\%group\.([^%]+)%/;
-    my $group = $1;
-    my $groupid = Bugzilla::Group::ValidateGroupName( $group, ($user));
-    $groupid || ThrowUserError('invalid_group_name',{name => $group});
-    my @childgroups = @{Bugzilla::Group->flatten_group_membership($groupid)};
-    my $table = "user_group_map_$$chartid";
-    push (@$supptables, "LEFT JOIN user_group_map AS $table " .
-                        "ON $table.user_id = bugs.$$f " .
-                        "AND $table.group_id IN(" .
-                        join(',', @childgroups) . ") " .
-                        "AND $table.isbless = 0 " .
-                        "AND $table.grant_type IN(" .
-                        GRANT_DIRECT . "," . GRANT_REGEXP . ")"
-         );
-    if ($$t =~ /^not/) {
-        $$term = "$table.group_id IS NULL";
-    } else {
-        $$term = "$table.group_id IS NOT NULL";
+    $value =~ /\%group\.([^%]+)%/;
+    my $group = Bugzilla::Group->check($1);
+    $group->check_members_are_visible();
+    my $group_ids = Bugzilla::Group->flatten_group_membership($group->id);
+    my $table = "user_group_map_$chart_id";
+    my $join_sql =
+        "LEFT JOIN user_group_map AS $table"
+        .     " ON $table.user_id = bugs.$field"
+        .        " AND " . $dbh->sql_in("$table.group_id", $group_ids)
+        .        " AND $table.isbless = 0";
+    push(@$joins, $join_sql);
+    if ($operator =~ /^not/) {
+        $args->{term} = "$table.group_id IS NULL";
+    }
+    else {
+        $args->{term} = "$table.group_id IS NOT NULL";
     }
 }
 
-sub _assigned_to_reporter_nonchanged {
-    my $self = shift;
-    my %func_args = @_;
-    my ($f, $ff, $t, $term) =
-        @func_args{qw(f ff t term)};
+sub _contact_nonchanged {
+    my ($self, $args) = @_;
+    my $field = $args->{field};
     
-    $$ff = "profiles.login_name";
-    $self->_do_operator_function(\%func_args);
-    $$term = "bugs.$$f IN (SELECT userid FROM profiles WHERE $$term)";
+    $args->{full_field} = "profiles.login_name";
+    $self->_do_operator_function($args);
+    my $term = $args->{term};
+    $args->{term} = "bugs.$field IN (SELECT userid FROM profiles WHERE $term)";
 }
 
 sub _qa_contact_nonchanged {
-    my $self = shift;
-    my %func_args = @_;
-    my ($supptables, $f, $ff) =
-        @func_args{qw(supptables f ff)};
+    my ($self, $args) = @_;
+    my $joins = $args->{joins};
     
-    push(@$supptables, "LEFT JOIN profiles AS map_qa_contact " .
-                       "ON bugs.qa_contact = map_qa_contact.userid");
-    $$ff = "COALESCE(map_$$f.login_name,'')";
+    push(@$joins, "LEFT JOIN profiles AS map_qa_contact " .
+                         "ON bugs.qa_contact = map_qa_contact.userid");
+    $args->{full_field} = "COALESCE(map_qa_contact.login_name,'')";
 }
 
 sub _cc_pronoun {
-    my $self = shift;
-    my %func_args = @_;
-    my ($full_field, $value, $quoted) = @func_args{qw(ff v q)};
+    my ($self, $args) = @_;
+    my ($full_field, $value) = @$args{qw(full_field value)};
     my $user = $self->{'user'};
 
-    if ($$value =~ /\%group/) {
-        return $self->_cc_exact_group(%func_args);
+    if ($value =~ /\%group/) {
+        return $self->_cc_exact_group($args);
+    }
+    elsif ($value =~ /^(%\w+%)$/) {
+        $args->{value} = pronoun($1, $user);
+        $args->{quoted} = $args->{value};
+        $args->{full_field} = "profiles.userid";
     }
-    elsif ($$value =~ /^(%\w+%)$/) {
-        $$value = pronoun($1, $user);
-        $$quoted = $$value;
-        $$full_field = "profiles.userid";
-    }   
 }
 
 sub _cc_exact_group {
-    my $self = shift;
-    my %func_args = @_;
-    my ($chartid, $sequence, $supptables, $t, $v, $term) =
-        @func_args{qw(chartid sequence supptables t v term)};
+    my ($self, $args) = @_;
+    my ($chart_id, $sequence, $joins, $operator, $value) =
+        @$args{qw(chart_id sequence joins operator value)};
     my $user = $self->{'user'};
+    my $dbh = Bugzilla->dbh;
     
-    $$v =~ m/%group\.([^%]+)%/;
-    my $group = $1;
-    my $groupid = Bugzilla::Group::ValidateGroupName( $group, ($user));
-    $groupid || ThrowUserError('invalid_group_name',{name => $group});
-    my @childgroups = @{Bugzilla::Group->flatten_group_membership($groupid)};
-    my $chartseq = $$chartid;
-    if ($$chartid eq "") {
-        $chartseq = "CC$$sequence";
-        $$sequence++;
-    }
-    my $table = "user_group_map_$chartseq";
-    push(@$supptables, "LEFT JOIN cc AS cc_$chartseq " .
-                       "ON bugs.bug_id = cc_$chartseq.bug_id");
-    push(@$supptables, "LEFT JOIN user_group_map AS $table " .
-                        "ON $table.user_id = cc_$chartseq.who " .
-                        "AND $table.group_id IN(" .
-                        join(',', @childgroups) . ") " .
-                        "AND $table.isbless = 0 " .
-                        "AND $table.grant_type IN(" .
-                        GRANT_DIRECT . "," . GRANT_REGEXP . ")"
-         );
-    if ($$t =~ /^not/) {
-        $$term = "$table.group_id IS NULL";
-    } else {
-        $$term = "$table.group_id IS NOT NULL";
+    $value =~ m/%group\.([^%]+)%/;
+    my $group = Bugzilla::Group->check($1);
+    $group->check_members_are_visible();
+    my $all_groups = Bugzilla::Group->flatten_group_membership($group->id);
+
+    # This is for the email1, email2, email3 fields from query.cgi.
+    if ($chart_id eq "") {
+        $chart_id = "CC$$sequence";
+        $args->{sequence}++;
+    }
+    
+    my $group_table = "user_group_map_$chart_id";
+    my $cc_table = "cc_$chart_id";
+    push(@$joins, "LEFT JOIN cc AS $cc_table " .
+                         "ON bugs.bug_id = $cc_table.bug_id");
+    my $join_sql =
+        "LEFT JOIN user_group_map AS $group_table"
+        .     " ON $group_table.user_id = $cc_table.who"
+        .        " AND " . $dbh->sql_in("$group_table.group_id", $all_groups)
+        .        " AND $group_table.isbless = 0 ";
+    push(@$joins, $join_sql);
+    if ($operator =~ /^not/) {
+        $args->{term} = "$group_table.group_id IS NULL";
+    }
+    else {
+        $args->{term} = "$group_table.group_id IS NOT NULL";
     }
 }
 
 sub _cc_nonchanged {
-    my $self = shift;
-    my %func_args = @_;
-    my ($chartid, $sequence, $f, $ff, $t, $supptables, $term, $v) =
-        @func_args{qw(chartid sequence f ff t supptables term v)};
+    my ($self, $args) = @_;
+    my ($chart_id, $sequence, $field, $full_field, $operator, $joins, $value) =
+        @$args{qw(chart_id sequence field full_field operator joins value)};
 
-    my $chartseq = $$chartid;
-    if ($$chartid eq "") {
-        $chartseq = "CC$$sequence";
-        $$sequence++;
+    # This is for the email1, email2, email3 fields from query.cgi.
+    if ($chart_id eq "") {
+        $chart_id = "CC$$sequence";
+        $args->{sequence}++;
     }
-    if ($$ff eq 'bugs.cc') {
-        $$ff = "profiles.login_name";
+    
+    # $full_field might have been changed by one of the cc_pronoun
+    # functions, in which case we leave it alone.
+    if ($full_field eq 'bugs.cc') {
+        $args->{full_field} = "profiles.login_name";
     }
-    $self->_do_operator_function(\%func_args);
-    push(@$supptables, "LEFT JOIN cc AS cc_$chartseq " .
-                       "ON bugs.bug_id = cc_$chartseq.bug_id " .
-                       "AND cc_$chartseq.who IN" .
-                       "(SELECT userid FROM profiles WHERE $$term)"
-                       );
-    $$term = "cc_$chartseq.who IS NOT NULL";
+    
+    $self->_do_operator_function($args);
+    
+    my $term = $args->{term};
+    my $table = "cc_$chart_id";
+    my $join_sql =
+        "LEFT JOIN cc AS $table"
+        .     " ON bugs.bug_id = $table.bug_id"
+         .       " AND $table.who IN (SELECT userid FROM profiles WHERE $term)";
+    push(@$joins, $join_sql);
+    
+    $args->{term} = "$table.who IS NOT NULL";
 }
 
+# XXX This duplicates having Commenter as a search field.
 sub _long_desc_changedby {
-    my $self = shift;
-    my %func_args = @_;
-    my ($chartid, $supptables, $term, $v) =
-        @func_args{qw(chartid supptables term v)};
+    my ($self, $args) = @_;
+    my ($chart_id, $joins, $value) = @$args{qw(chart_id joins value)};
     
-    my $table = "longdescs_$$chartid";
-    push(@$supptables, "LEFT JOIN longdescs AS $table " .
-                       "ON $table.bug_id = bugs.bug_id");
-    my $id = login_to_id($$v, THROW_ERROR);
-    $$term = "$table.who = $id";
+    my $table = "longdescs_$chart_id";
+    push(@$joins, "LEFT JOIN longdescs AS $table " .
+                         "ON $table.bug_id = bugs.bug_id");
+    my $user_id = login_to_id($value, THROW_ERROR);
+    $args->{term} = "$table.who = $user_id";
 }
 
 sub _long_desc_changedbefore_after {
-    my $self = shift;
-    my %func_args = @_;
-    my ($chartid, $t, $v, $supptables, $term) =
-        @func_args{qw(chartid t v supptables term)};
+    my ($self, $args) = @_;
+    my ($chart_id, $operator, $value, $joins) =
+        @$args{qw(chart_id operator value joins)};
     my $dbh = Bugzilla->dbh;
     
-    my $operator = ($$t =~ /before/) ? '<' : '>';
-    my $table = "longdescs_$$chartid";
-    push(@$supptables, "LEFT JOIN longdescs AS $table " .
-                              "ON $table.bug_id = bugs.bug_id " .
-                                 "AND $table.bug_when $operator " .
-                                  $dbh->quote(SqlifyDate($$v)) );
-    $$term = "($table.bug_when IS NOT NULL)";
+    my $sql_operator = ($operator =~ /before/) ? '<' : '>';
+    my $table = "longdescs_$chart_id";
+    my $sql_date = $dbh->quote(SqlifyDate($value));
+    my $join_sql =
+        "LEFT JOIN longdescs AS $table "
+        .        " ON $table.bug_id = bugs.bug_id"
+        .           " AND $table.bug_when $sql_operator $sql_date";
+    push(@$joins, $join_sql);
+    $args->{term} = "$table.bug_when IS NOT NULL";
 }
 
 sub _content_matches {
-    my $self = shift;
-    my %func_args = @_;
-    my ($chartid, $supptables, $term, $groupby, $fields, $t, $v) =
-        @func_args{qw(chartid supptables term groupby fields t v)};
+    my ($self, $args) = @_;
+    my ($chart_id, $joins, $group_by, $fields, $operator, $value) =
+        @$args{qw(chart_id joins group_by fields operator value)};
     my $dbh = Bugzilla->dbh;
     
     # "content" is an alias for columns containing text for which we
@@ -1599,25 +1644,26 @@ sub _content_matches {
     # index searches.
 
     # Add the fulltext table to the query so we can search on it.
-    my $table = "bugs_fulltext_$$chartid";
+    my $table = "bugs_fulltext_$chart_id";
     my $comments_col = "comments";
     $comments_col = "comments_noprivate" unless $self->{'user'}->is_insider;
-    push(@$supptables, "LEFT JOIN bugs_fulltext AS $table " .
-                       "ON bugs.bug_id = $table.bug_id");
+    push(@$joins, "LEFT JOIN bugs_fulltext AS $table " .
+                         "ON bugs.bug_id = $table.bug_id");
     
     # Create search terms to add to the SELECT and WHERE clauses.
-    my ($term1, $rterm1) = $dbh->sql_fulltext_search("$table.$comments_col",
-                                                        $$v, 1);
-    my ($term2, $rterm2) = $dbh->sql_fulltext_search("$table.short_desc",
-                                                        $$v, 2);
+    my ($term1, $rterm1) =
+        $dbh->sql_fulltext_search("$table.$comments_col", $value, 1);
+    my ($term2, $rterm2) =
+        $dbh->sql_fulltext_search("$table.short_desc", $value, 2);
     $rterm1 = $term1 if !$rterm1;
     $rterm2 = $term2 if !$rterm2;
 
     # The term to use in the WHERE clause.
-    $$term = "$term1 > 0 OR $term2 > 0";
-    if ($$t =~ /not/i) {
-        $$term = "NOT($$term)";
+    my $term = "$term1 > 0 OR $term2 > 0";
+    if ($operator =~ /not/i) {
+        $term = "NOT($term)";
     }
+    $args->{term} = $term;
     
     # In order to sort by relevance (in case the user requests it),
     # we SELECT the relevance value so we can add it to the ORDER BY
@@ -1629,282 +1675,262 @@ sub _content_matches {
     my $current = COLUMNS->{'relevance'}->{name};
     $current = $current ? "$current + " : '';
     # For NOT searches, we just add 0 to the relevance.
-    my $select_term = $$t =~ /not/ ? 0 : "($current$rterm1 + $rterm2)";
+    my $select_term = $operator =~ /not/ ? 0 : "($current$rterm1 + $rterm2)";
     COLUMNS->{'relevance'}->{name} = $select_term;
 }
 
 sub _timestamp_translate {
-    my $self = shift;
-    my %func_args = @_;
-    my ($value, $quoted) = @func_args{qw(v q)};
+    my ($self, $args) = @_;
+    my $value = $args->{value};
     my $dbh = Bugzilla->dbh;
 
-    return if $$value !~ /^[\+\-]?\d+[hdwmy]$/i;
+    return if $value !~ /^[\+\-]?\d+[hdwmy]$/i;
     
-    $$value = SqlifyDate($$value);
-    $$quoted = $dbh->quote($$value);
+    $args->{value}  = SqlifyDate($value);
+    $args->{quoted} = $dbh->quote($args->{value});
 }
 
+# XXX This should probably be merged with cc_pronoun.
 sub _commenter_pronoun {
-    my $self = shift;
-    my %func_args = @_;
-    my ($full_field, $value, $quoted) = @func_args{qw(ff v q)};
+    my ($self, $args) = @_;
+    my $value = $args->{value};
     my $user = $self->{'user'};
 
-    if ($$value =~ /^(%\w+%)$/) {
-        $$value = pronoun($1, $user);
-        $$quoted = $$value;
-        $$full_field = "profiles.userid";
+    if ($value =~ /^(%\w+%)$/) {
+        $args->{value} = pronoun($1, $user);
+        $args->{quoted} = $args->{value};
+        $args->{full_field} = "profiles.userid";
     }
 }
 
 sub _commenter {
-    my $self = shift;
-    my %func_args = @_;
-    my ($chartid, $sequence, $supptables, $f, $ff, $t, $term) =
-        @func_args{qw(chartid sequence supptables f ff t term)};
+    my ($self, $args) = @_;
+    my ($chart_id, $sequence, $joins, $field, $full_field, $operator) =
+        @$args{qw(chart_id sequence joins field full_field operator)};
 
-    my $chartseq = $$chartid;
-    if ($$chartid eq "") {
-        $chartseq = "LD$$sequence";
-        $$sequence++;
+    if ($chart_id eq "") {
+        $chart_id = "LD$sequence";
+        $args->{sequence}++;
     }
-    my $table = "longdescs_$chartseq";
-    my $extra = $self->{'user'}->is_insider ? "" : "AND $table.isprivate < 1";
-    if ($$ff eq 'bugs.commenter') {
-        $$ff = "profiles.login_name";
+    my $table = "longdescs_$chart_id";
+    
+    my $extra = $self->{'user'}->is_insider ? "" : "AND $table.isprivate = 0";
+    # commenter_pronoun could have changed $full_field to something else,
+    # so we only set this if commenter_pronoun hasn't set it.
+    if ($full_field eq 'bugs.commenter') {
+        $args->{full_field} = "profiles.login_name";
     }
-    $self->_do_operator_function(\%func_args);
-    push(@$supptables, "LEFT JOIN longdescs AS $table " .
-                       "ON $table.bug_id = bugs.bug_id $extra " .
-                       "AND $table.who IN" .
-                       "(SELECT userid FROM profiles WHERE $$term)"
-                       );
-    $$term = "$table.who IS NOT NULL";
+    $self->_do_operator_function($args);
+    my $term = $args->{term};
+    my $join_sql =
+        "LEFT JOIN longdescs AS $table"
+        . " ON $table.bug_id = bugs.bug_id $extra"
+        .    " AND $table.who IN (SELECT userid FROM profiles WHERE $term)";
+    push(@$joins, $join_sql);
+    $args->{term} = "$table.who IS NOT NULL";
 }
 
 sub _long_desc {
-    my $self = shift;
-    my %func_args = @_;
-    my ($chartid, $supptables, $ff) =
-        @func_args{qw(chartid supptables ff)};
+    my ($self, $args) = @_;
+    my ($chart_id, $joins) = @$args{qw(chart_id joins)};
     
-    my $table = "longdescs_$$chartid";
-    my $extra = $self->{'user'}->is_insider ? "" : "AND $table.isprivate < 1";
-    push(@$supptables, "LEFT JOIN longdescs AS $table " .
-                       "ON $table.bug_id = bugs.bug_id $extra");
-    $$ff = "$table.thetext";
+    my $table = "longdescs_$chart_id";
+    my $extra = $self->{'user'}->is_insider ? "" : "AND $table.isprivate = 0";
+    push(@$joins, "LEFT JOIN longdescs AS $table " .
+                         "ON $table.bug_id = bugs.bug_id $extra");
+    $args->{full_field} = "$table.thetext";
 }
 
 sub _longdescs_isprivate {
-    my $self = shift;
-    my %func_args = @_;
-    my ($chartid, $supptables, $ff) =
-        @func_args{qw(chartid supptables ff)};
+    my ($self, $args) = @_;
+    my ($chart_id, $joins) = @$args{qw(chart_id joins)};
     
-    my $table = "longdescs_$$chartid";
-    my $extra = $self->{'user'}->is_insider ? "" : "AND $table.isprivate < 1";
-    push(@$supptables, "LEFT JOIN longdescs AS $table " .
-                      "ON $table.bug_id = bugs.bug_id $extra");
-    $$ff = "$table.isprivate";
+    my $table = "longdescs_$chart_id";
+    my $extra = $self->{'user'}->is_insider ? "" : "AND $table.isprivate = 0";
+    push(@$joins, "LEFT JOIN longdescs AS $table " .
+                         "ON $table.bug_id = bugs.bug_id $extra");
+    $args->{quoted} = $args->{value} ? 1 : 0;
+    $args->{full_field} = "$table.isprivate";
 }
 
 sub _work_time_changedby {
-    my $self = shift;
-    my %func_args = @_;
-    my ($chartid, $supptables, $v, $term) =
-        @func_args{qw(chartid supptables v term)};
+    my ($self, $args) = @_;
+    my ($chart_id, $joins, $value) = @$args{qw(chart_id joins value)};
     
-    my $table = "longdescs_$$chartid";
-    push(@$supptables, "LEFT JOIN longdescs AS $table " .
-                       "ON $table.bug_id = bugs.bug_id");
-    my $id = login_to_id($$v, THROW_ERROR);
-    $$term = "(($table.who = $id";
-    $$term .= ") AND ($table.work_time <> 0))";
+    my $table = "longdescs_$chart_id";
+    push(@$joins, "LEFT JOIN longdescs AS $table " .
+                         "ON $table.bug_id = bugs.bug_id");
+    my $user_id = login_to_id($value, THROW_ERROR);
+    $args->{term} = "$table.who = $user_id AND $table.work_time != 0";
 }
 
 sub _work_time_changedbefore_after {
-    my $self = shift;
-    my %func_args = @_;
-    my ($chartid, $t, $v, $supptables, $term) =
-        @func_args{qw(chartid t v supptables term)};
+    my ($self, $args) = @_;
+    my ($chart_id, $operator, $value, $joins) =
+        @$args{qw(chart_id operator value joins)};
     my $dbh = Bugzilla->dbh;
     
-    my $operator = ($$t =~ /before/) ? '<' : '>';
-    my $table = "longdescs_$$chartid";
-    push(@$supptables, "LEFT JOIN longdescs AS $table " .
-                              "ON $table.bug_id = bugs.bug_id " .
-                                 "AND $table.work_time <> 0 " .
-                                 "AND $table.bug_when $operator " .
-                                  $dbh->quote(SqlifyDate($$v)) );
-    $$term = "($table.bug_when IS NOT NULL)";
+    my $table = "longdescs_$chart_id";
+    my $sql_operator = ($operator =~ /before/) ? '<' : '>';
+    my $sql_date = $dbh->quote(SqlifyDate($value));
+    my $join_sql =
+        "LEFT JOIN longdescs AS $table"
+        .     " ON $table.bug_id = bugs.bug_id"
+        .        " AND $table.work_time != 0"
+        .        " AND $table.bug_when $sql_operator $sql_date";
+    push(@$joins, $join_sql);
+    
+    $args->{term} = "$table.bug_when IS NOT NULL";
 }
 
 sub _work_time {
-    my $self = shift;
-    my %func_args = @_;
-    my ($chartid, $supptables, $ff) =
-        @func_args{qw(chartid supptables ff)};
+    my ($self, $args) = @_;
+    my ($chart_id, $joins) = @$args{qw(chart_id joins)};
     
-    my $table = "longdescs_$$chartid";
-    push(@$supptables, "LEFT JOIN longdescs AS $table " .
-                      "ON $table.bug_id = bugs.bug_id");
-    $$ff = "$table.work_time";
+    my $table = "longdescs_$chart_id";
+    push(@$joins, "LEFT JOIN longdescs AS $table " .
+                         "ON $table.bug_id = bugs.bug_id");
+    $args->{full_field} = "$table.work_time";
 }
 
 sub _percentage_complete {
-    my $self = shift;
-    my %func_args = @_;
-    my ($t, $chartid, $supptables, $fields, $q, $v, $having, $groupby, $term) =
-        @func_args{qw(t chartid supptables fields q v having groupby term)};
-    my $dbh = Bugzilla->dbh;
-    
-    my $oper;
-    if ($$t eq "equals") {
-        $oper = "=";
-    } elsif ($$t eq "greaterthan") {
-        $oper = ">";
-    } elsif ($$t eq "greaterthaneq") {
-        $oper = ">=";
-    } elsif ($$t eq "lessthan") {
-        $oper = "<";
-    } elsif ($$t eq "lessthaneq") {
-        $oper = "<=";
-    } elsif ($$t eq "notequal") {
-        $oper = "<>";
-    } elsif ($$t eq "regexp") {
-        # This is just a dummy to help catch bugs- $oper won't be used
-        # since "regexp" is treated as a special case below.  But
-        # leaving $oper uninitialized seems risky...
-        $oper = "sql_regexp";
-    } elsif ($$t eq "notregexp") {
-        # This is just a dummy to help catch bugs- $oper won't be used
-        # since "notregexp" is treated as a special case below.  But
-        # leaving $oper uninitialized seems risky...
-        $oper = "sql_not_regexp";
-    } else {
-        $oper = "noop";
-    }
-    if ($oper ne "noop") {
-        my $table = "longdescs_$$chartid";
-        if (!grep($_ eq 'remaining_time', @$fields)) {
-            push(@$fields, "remaining_time");
-        }
-        push(@$supptables, "LEFT JOIN longdescs AS $table " .
-                           "ON $table.bug_id = bugs.bug_id");
-        my $expression = "(100 * ((SUM($table.work_time) *
-                                    COUNT(DISTINCT $table.bug_when) /
-                                    COUNT(bugs.bug_id)) /
-                                   ((SUM($table.work_time) *
-                                     COUNT(DISTINCT $table.bug_when) /
-                                     COUNT(bugs.bug_id)) +
-                                    bugs.remaining_time)))";
-        $$q = $dbh->quote($$v);
-        trick_taint($$q);
-        if ($$t eq "regexp") {
-            push(@$having, $dbh->sql_regexp($expression, $$q));
-        } elsif ($$t eq "notregexp") {
-            push(@$having, $dbh->sql_not_regexp($expression, $$q));
-        } else {
-            push(@$having, "$expression $oper " . $$q);
-        }
-        push(@$groupby, "bugs.remaining_time");
+    my ($self, $args) = @_;
+    my ($chart_id, $joins, $operator, $value, $having, $fields) =
+        @$args{qw(chart_id joins operator value having fields)};
+
+    my $table = "longdescs_$chart_id";
+
+    # We can't just use "percentage_complete" as the field, because
+    # (a) PostgreSQL doesn't accept it in the HAVING clause
+    # and (b) it wouldn't work in multiple chart rows, because it uses
+    # a fixed name for the table, "ldtime".
+    my $expression = COLUMNS->{percentage_complete}->{name};
+    $expression =~ s/\bldtime\b/$table/g;
+    $args->{full_field} = "($expression)";
+    push(@$joins, "LEFT JOIN longdescs AS $table " .
+                         "ON $table.bug_id = bugs.bug_id");
+
+    # We need remaining_time in @fields, otherwise we can't use
+    # it in the expression for creating percentage_complete.
+    if (!grep { $_ eq 'remaining_time' } @$fields) {
+        push(@$fields, 'remaining_time');
     }
-    $$term = "0=0";
+
+    $self->_do_operator_function($args);
+    push(@$having, $args->{term});
+   
+    # We put something into $args->{term} so that do_search_function
+    # stops processing.
+    $args->{term} = "0=0";
 }
 
 sub _bug_group_nonchanged {
-    my $self = shift;
-    my %func_args = @_;
-    my ($supptables, $chartid, $ff, $t, $term) =
-        @func_args{qw(supptables chartid ff t term)};
-    
-    push(@$supptables,
-            "LEFT JOIN bug_group_map AS bug_group_map_$$chartid " .
-            "ON bugs.bug_id = bug_group_map_$$chartid.bug_id");
-    $$ff = "groups_$$chartid.name";
-    $self->_do_operator_function(\%func_args);
-    push(@$supptables,
-            "LEFT JOIN groups AS groups_$$chartid " .
-            "ON groups_$$chartid.id = bug_group_map_$$chartid.group_id " .
-            "AND $$term");
-    $$term = "$$ff IS NOT NULL";
+    my ($self, $args) = @_;
+    my ($chart_id, $joins, $field) = @$args{qw(chart_id joins field)};
+    
+    my $map_table = "bug_group_map_$chart_id";
+    push(@$joins,
+        "LEFT JOIN bug_group_map AS $map_table " .
+               "ON bugs.bug_id = $map_table.bug_id");
+    
+    my $groups_table = "groups_$chart_id";
+    my $full_field = "$groups_table.name";
+    $args->{full_field} = $full_field;
+    $self->_do_operator_function($args);
+    my $term = $args->{term};
+    push(@$joins,
+        "LEFT JOIN groups AS $groups_table " .
+               "ON $groups_table.id = $map_table.group_id AND $term");
+    $args->{term} = "$full_field IS NOT NULL";
 }
 
 sub _attach_data_thedata {
-    my $self = shift;
-    my %func_args = @_;
-    my ($chartid, $supptables, $ff) =
-        @func_args{qw(chartid supptables ff)};
+    my ($self, $args) = @_;
+    my ($chart_id, $joins) = @$args{qw(chart_id joins)};
     
-    my $atable = "attachments_$$chartid";
-    my $dtable = "attachdata_$$chartid";
-    my $extra = $self->{'user'}->is_insider ? "" : "AND $atable.isprivate = 0";
-    push(@$supptables, "LEFT JOIN attachments AS $atable " .
-                       "ON bugs.bug_id = $atable.bug_id $extra");
-    push(@$supptables, "LEFT JOIN attach_data AS $dtable " .
-                       "ON $dtable.id = $atable.attach_id");
-    $$ff = "$dtable.thedata";
+    my $attach_table = "attachments_$chart_id";
+    my $data_table = "attachdata_$chart_id";
+    my $extra = $self->{'user'}->is_insider
+                ? "" : "AND $attach_table.isprivate = 0";
+    push(@$joins, "LEFT JOIN attachments AS $attach_table " .
+                         "ON bugs.bug_id = $attach_table.bug_id $extra");
+    push(@$joins, "LEFT JOIN attach_data AS $data_table " .
+                       "ON $data_table.id = $attach_table.attach_id");
+    $args->{full_field} = "$data_table.thedata";
 }
 
 sub _attachments_submitter {
-    my $self = shift;
-    my %func_args = @_;
-    my ($chartid, $supptables, $ff) =
-        @func_args{qw(chartid supptables ff)};
+    my ($self, $args) = @_;
+    my ($chart_id, $joins) = @$args{qw(chart_id joins)};
+    
+    my $attach_table = "attachment_submitter_$chart_id";
+    my $extra = $self->{'user'}->is_insider
+                ? "" : "AND $attach_table.isprivate = 0";
+    push(@$joins, "LEFT JOIN attachments AS $attach_table " .
+                         "ON bugs.bug_id = $attach_table.bug_id $extra");
     
-    my $atable = "map_attachment_submitter_$$chartid";
-    my $extra = $self->{'user'}->is_insider ? "" : "AND $atable.isprivate = 0";
-    push(@$supptables, "LEFT JOIN attachments AS $atable " .
-                       "ON bugs.bug_id = $atable.bug_id $extra");
-    push(@$supptables, "LEFT JOIN profiles AS attachers_$$chartid " .
-                       "ON $atable.submitter_id = attachers_$$chartid.userid");
-    $$ff = "attachers_$$chartid.login_name";
+    my $map_table = "map_attachment_submitter_$chart_id";
+    push(@$joins, "LEFT JOIN profiles AS $map_table " .
+                         "ON $attach_table.submitter_id = $map_table.userid");
+    $args->{full_field} = "$map_table.login_name";
 }
 
 sub _attachments {
-    my $self = shift;
-    my %func_args = @_;
-    my ($chartid, $supptables, $f, $ff, $t, $v, $q) =
-        @func_args{qw(chartid supptables f ff t v q)};
+    my ($self, $args) = @_;
+    my ($chart_id, $joins, $field, $operator, $value) =
+        @$args{qw(chart_id joins field operator value)};
     my $dbh = Bugzilla->dbh;
     
-    my $table = "attachments_$$chartid";
+    my $table = "attachments_$chart_id";
     my $extra = $self->{'user'}->is_insider ? "" : "AND $table.isprivate = 0";
-    push(@$supptables, "LEFT JOIN attachments AS $table " .
-                       "ON bugs.bug_id = $table.bug_id $extra");
-    $$f =~ m/^attachments\.(.*)$/;
-    my $field = $1;
-    if ($$t eq "changedby") {
-        $$v = login_to_id($$v, THROW_ERROR);
-        $$q = $dbh->quote($$v);
-        $field = "submitter_id";
-        $$t = "equals";
-    } elsif ($$t eq "changedbefore") {
-        $$v = SqlifyDate($$v);
-        $$q = $dbh->quote($$v);
-        $field = "creation_ts";
-        $$t = "lessthan";
-    } elsif ($$t eq "changedafter") {
-        $$v = SqlifyDate($$v);
-        $$q = $dbh->quote($$v);
-        $field = "creation_ts";
-        $$t = "greaterthan";
-    }
-    if ($field eq "ispatch" && $$v ne "0" && $$v ne "1") {
-        ThrowUserError("illegal_attachment_is_patch");
-    }
-    if ($field eq "isobsolete" && $$v ne "0" && $$v ne "1") {
-        ThrowUserError("illegal_is_obsolete");
-    }
-    $$ff = "$table.$field";
+    push(@$joins, "LEFT JOIN attachments AS $table " .
+                         "ON bugs.bug_id = $table.bug_id $extra");
+    
+    $field =~ /^attachments\.(.+)$/;
+    my $attach_field = $1;
+    # XXX This is not actually the correct method of searching for
+    # changes in attachment values--this just tells you who posted an
+    # attachment.
+    if ($operator eq "changedby") {
+        $args->{value} = login_to_id($value, THROW_ERROR);
+        $args->{quoted} = $args->{value};
+        $attach_field = "submitter_id";
+        $args->{operator} = "equals";
+    }
+    elsif ($operator eq 'changedbefore' or $operator eq 'changedafter') {
+        $args->{value} = SqlifyDate($value);
+        $args->{quoted} = $dbh->quote($args->{value});
+        $attach_field = "creation_ts";
+        $args->{operator} = $operator eq 'changedbefore' ? "lessthan"
+                                                         : "greaterthan";
+    }
+    
+    $args->{full_field} = "$table.$attach_field";
+}
+
+sub _join_flag_tables {
+    my ($self, $args) = @_;
+    my ($joins, $chart_id) = @$args{qw(joins chart_id)};
+    
+    my $attachments = "attachments_$chart_id";
+    my $extra = $self->{'user'}->is_insider
+                ? "" : "AND $attachments.isprivate = 0";
+    push(@$joins, "LEFT JOIN attachments AS $attachments " .
+                         "ON bugs.bug_id = $attachments.bug_id $extra");
+    my $flags = "flags_$chart_id";
+    # We join both the bugs and the attachments table in separately,
+    # and then the join code will later combine the terms.
+    push(@$joins, "LEFT JOIN flags AS $flags " . 
+                         "ON bugs.bug_id = $flags.bug_id ");
+    push(@$joins, "LEFT JOIN flags AS $flags " .
+                         "ON $flags.attach_id = $attachments.attach_id " .
+                             "OR $flags.attach_id IS NULL");
 }
 
 sub _flagtypes_name {
-    my $self = shift;
-    my %func_args = @_;
-    my ($t, $chartid, $supptables, $ff, $having, $term) =
-        @func_args{qw(t chartid supptables ff having term)};
+    my ($self, $args) = @_;
+    my ($chart_id, $operator, $joins, $field, $having) = 
+        @$args{qw(chart_id operator joins field having)};
     my $dbh = Bugzilla->dbh;
     
     # Matches bugs by flag name/status.
@@ -1914,31 +1940,25 @@ sub _flagtypes_name {
     
     # Don't do anything if this condition is about changes to flags,
     # as the generic change condition processors can handle those.
-    return if ($$t =~ m/^changed/);
+    return if $operator =~ /^changed/;
     
     # Add the flags and flagtypes tables to the query.  We do 
     # a left join here so bugs without any flags still match 
     # negative conditions (f.e. "flag isn't review+").
-    my $attachments = "attachments_$$chartid";
-    my $extra = $self->{'user'}->is_insider ? "" : "AND $attachments.isprivate = 0";
-    push(@$supptables, "LEFT JOIN attachments AS $attachments " .
-                       "ON bugs.bug_id = $attachments.bug_id $extra");
-    my $flags = "flags_$$chartid";
-    push(@$supptables, "LEFT JOIN flags AS $flags " . 
-                       "ON bugs.bug_id = $flags.bug_id ");
-    my $flagtypes = "flagtypes_$$chartid";
-    push(@$supptables, "LEFT JOIN flagtypes AS $flagtypes " . 
-                       "ON $flags.type_id = $flagtypes.id");
-    push(@$supptables, "LEFT JOIN flags AS $flags " .
-                       "ON $flags.attach_id = $attachments.attach_id " .
-                       "OR $flags.attach_id IS NULL");
+    $self->_join_flag_tables($args);
+    my $flags = "flags_$chart_id";
+    my $flagtypes = "flagtypes_$chart_id";
+    push(@$joins, "LEFT JOIN flagtypes AS $flagtypes " . 
+                         "ON $flags.type_id = $flagtypes.id");
     
     # Generate the condition by running the operator-specific
-    # function. Afterwards the condition resides in the global $term
+    # function. Afterwards the condition resides in the $args->{term}
     # variable.
-    $$ff = $dbh->sql_string_concat("${flagtypes}.name",
-                                   "$flags.status");
-    $self->_do_operator_function(\%func_args);
+    my $full_field = $dbh->sql_string_concat("$flagtypes.name",
+                                             "$flags.status");
+    $args->{full_field} = $full_field;
+    $self->_do_operator_function($args);
+    my $term = $args->{term};
     
     # If this is a negative condition (f.e. flag isn't "review+"),
     # we only want bugs where all flags match the condition, not 
@@ -1948,542 +1968,433 @@ sub _flagtypes_name {
     # of flags on each bug, then compare them in a HAVING clause.
     # If the numbers are the same, all flags match the condition,
     # so this bug should be included.
-    if ($$t =~ m/not/) {
+    if ($operator =~ /^not/) {
        push(@$having,
-            "SUM(CASE WHEN $$ff IS NOT NULL THEN 1 ELSE 0 END) = " .
-            "SUM(CASE WHEN $$term THEN 1 ELSE 0 END)");
-       $$term = "0=0";
+            "SUM(CASE WHEN $full_field IS NOT NULL THEN 1 ELSE 0 END) = " .
+            "SUM(CASE WHEN $term THEN 1 ELSE 0 END)");
+       $args->{term} = "0=0";
     }
 }
 
+# XXX These two functions can probably be joined (requestees and setters).
 sub _requestees_login_name {
-    my $self = shift;
-    my %func_args = @_;
-    my ($ff, $chartid, $supptables) = @func_args{qw(ff chartid supptables)};
+    my ($self, $args) = @_;
+    my ($chart_id, $joins) = @$args{qw(chart_id joins)};
     
-    my $attachments = "attachments_$$chartid";
-    my $extra = $self->{'user'}->is_insider ? "" : "AND $attachments.isprivate = 0";
-    push(@$supptables, "LEFT JOIN attachments AS $attachments " .
-                       "ON bugs.bug_id = $attachments.bug_id $extra");
-    my $flags = "flags_$$chartid";
-    push(@$supptables, "LEFT JOIN flags AS $flags " .
-                       "ON bugs.bug_id = $flags.bug_id ");
-    push(@$supptables, "LEFT JOIN profiles AS requestees_$$chartid " .
-                       "ON $flags.requestee_id = requestees_$$chartid.userid");
-    push(@$supptables, "LEFT JOIN flags AS $flags " .
-                       "ON $flags.attach_id = $attachments.attach_id " .
-                       "OR $flags.attach_id IS NULL");
+    $self->_join_flag_tables($args);
+    my $flags = "flags_$chart_id";
+    my $map_table = "map_flag_requestees_$chart_id";
+    push(@$joins, "LEFT JOIN profiles AS $map_table " .
+                         "ON $flags.requestee_id = $map_table.userid");
 
-    $$ff = "requestees_$$chartid.login_name";
+    $args->{full_field} = "$map_table.login_name";
 }
 
 sub _setters_login_name {
-    my $self = shift;
-    my %func_args = @_;
-    my ($ff, $chartid, $supptables) = @func_args{qw(ff chartid supptables)};
-
-    my $attachments = "attachments_$$chartid";
-    my $extra = $self->{'user'}->is_insider ? "" : "AND $attachments.isprivate = 0";
-    push(@$supptables, "LEFT JOIN attachments AS $attachments " .
-                       "ON bugs.bug_id = $attachments.bug_id $extra");
-    my $flags = "flags_$$chartid";
-    push(@$supptables, "LEFT JOIN flags AS $flags " .
-                       "ON bugs.bug_id = $flags.bug_id ");
-    push(@$supptables, "LEFT JOIN profiles AS setters_$$chartid " .
-                       "ON $flags.setter_id = setters_$$chartid.userid");
-    push(@$supptables, "LEFT JOIN flags AS $flags " .
-                       "ON $flags.attach_id = $attachments.attach_id " .
-                       "OR $flags.attach_id IS NULL");
+    my ($self, $args) = @_;
+    my ($chart_id, $joins) = @$args{qw(chart_id joins)};
+    
+    $self->_join_flag_tables($args);
+    my $flags = "flags_$chart_id";
+    my $map_table = "map_flag_setters_$chart_id";
+    push(@$joins, "LEFT JOIN profiles AS $map_table " .
+                         "ON $flags.setter_id = $map_table.userid");
 
-    $$ff = "setters_$$chartid.login_name";
+    $args->{full_field} = "$map_table.login_name";
 }
 
 sub _changedin_days_elapsed {
-    my $self = shift;
-    my %func_args = @_;
-    my ($ff) = @func_args{qw(ff)};
+    my ($self, $args) = @_;
     my $dbh = Bugzilla->dbh;
     
-    $$ff = "(" . $dbh->sql_to_days('NOW()') . " - " .
-                 $dbh->sql_to_days('bugs.delta_ts') . ")";
+    $args->{full_field} = "(" . $dbh->sql_to_days('NOW()') . " - " .
+                                $dbh->sql_to_days('bugs.delta_ts') . ")";
 }
 
 sub _component_nonchanged {
-    my $self = shift;
-    my %func_args = @_;
-    my ($ff, $t, $term) = @func_args{qw(ff t term)};
+    my ($self, $args) = @_;
     
-    $$ff = "components.name";
-    $self->_do_operator_function(\%func_args);
-    $$term = build_subselect("bugs.component_id",
-                             "components.id",
-                             "components",
-                             $$term);
+    $args->{full_field} = "components.name";
+    $self->_do_operator_function($args);
+    my $term = $args->{term};
+    $args->{term} = build_subselect("bugs.component_id",
+        "components.id", "components", $args->{term});
 }
+
 sub _product_nonchanged {
-    my $self = shift;
-    my %func_args = @_;
-    my ($ff, $t, $term) = @func_args{qw(ff t term)};
+    my ($self, $args) = @_;
     
     # Generate the restriction condition
-    $$ff = "products.name";
-    $self->_do_operator_function(\%func_args);
-    $$term = build_subselect("bugs.product_id",
-                             "products.id",
-                             "products",
-                             $$term);
+    $args->{full_field} = "products.name";
+    $self->_do_operator_function($args);
+    my $term = $args->{term};
+    $args->{term} = build_subselect("bugs.product_id",
+        "products.id", "products", $term);
 }
 
 sub _classification_nonchanged {
-    my $self = shift;
-    my %func_args = @_;
-    my ($chartid, $v, $ff, $t, $supptables, $term) =
-        @func_args{qw(chartid v ff t supptables term)};
+    my ($self, $args) = @_;
+    my $joins = $args->{joins};
     
     # Generate the restriction condition
-    push @$supptables, "INNER JOIN products AS map_products " .
-                      "ON bugs.product_id = map_products.id";
-    $$ff = "classifications.name";
-    $self->_do_operator_function(\%func_args);
-    $$term = build_subselect("map_products.classification_id",
-                             "classifications.id",
-                             "classifications",
-                              $$term);
+    push(@$joins, "INNER JOIN products AS map_products " .
+                          "ON bugs.product_id = map_products.id");
+    $args->{full_field} = "classifications.name";
+    $self->_do_operator_function($args);
+    my $term = $args->{term};
+    $args->{term} = build_subselect("map_products.classification_id",
+        "classifications.id", "classifications", $term);
 }
 
 sub _keywords_exact {
-    my $self = shift;
-    my %func_args = @_;
-    my ($chartid, $v, $ff, $f, $t, $term, $supptables) =
-        @func_args{qw(chartid v ff f t term supptables)};
-
-    my @list;
-    my $table = "keywords_$$chartid";
-    foreach my $value (split(/[\s,]+/, $$v)) {
-        if ($value eq '') {
-            next;
-        }
-        my $keyword = new Bugzilla::Keyword({name => $value});
-        if ($keyword) {
-            push(@list, "$table.keywordid = " . $keyword->id);
-        }
-        else {
-            ThrowUserError("unknown_keyword",
-                           { keyword => $$v });
-        }
-    }
-    my $haveawordterm;
-    if (@list) {
-        $haveawordterm = "(" . join(' OR ', @list) . ")";
-        if ($$t eq "anywords") {
-            $$term = $haveawordterm;
-        } elsif ($$t eq "allwords") {
-            $self->_allwords;
-            if ($$term && $haveawordterm) {
-                $$term = "(($$term) AND $haveawordterm)";
-            }
-        }
+    my ($self, $args) = @_;
+    my ($chart_id, $joins, $value, $operator) =
+        @$args{qw(chart_id joins value operator)};
+    my $dbh = Bugzilla->dbh;
+    
+    my @keyword_ids;
+    foreach my $value (split(/[\s,]+/, $value)) {
+        next if $value eq '';
+        my $keyword = Bugzilla::Keyword->check($value);
+        push(@keyword_ids, $keyword->id);
     }
-    if ($$term) {
-        push(@$supptables, "LEFT JOIN keywords AS $table " .
-                           "ON $table.bug_id = bugs.bug_id");
+    
+    # XXX We probably should instead throw an error here if there were
+    # just commas in the field.
+    if (!@keyword_ids) {
+        $args->{term} = "0=0";
+        return;
     }
-    else {
-        $self->_keywords_nonchanged(%func_args);
+    
+    # This is an optimization for anywords, since we already know
+    # the keyword id from having checked it above.
+    if ($operator eq 'anywords') {
+        my $table = "keywords_$chart_id";
+        $args->{term} = $dbh->sql_in("$table.keywordid", \@keyword_ids);
+        push(@$joins, "LEFT JOIN keywords AS $table"
+                      .     " ON $table.bug_id = bugs.bug_id");
+        return;
     }
+    
+    $self->_keywords_nonchanged($args);
 }
 
 sub _keywords_nonchanged {
-    my $self = shift;
-    my %func_args = @_;
-    my ($chartid, $v, $ff, $t, $term, $supptables) =
-        @func_args{qw(chartid v ff t term supptables)};
+    my ($self, $args) = @_;
+    my ($chart_id, $joins, $value, $operator) =
+        @$args{qw(chart_id joins value operator)};
 
-    my $k_table = "keywords_$$chartid";
-    my $kd_table = "keyworddefs_$$chartid";
+    my $k_table = "keywords_$chart_id";
+    my $kd_table = "keyworddefs_$chart_id";
     
-    push(@$supptables, "LEFT JOIN keywords AS $k_table " .
-                       "ON $k_table.bug_id = bugs.bug_id");
-    push(@$supptables, "LEFT JOIN keyworddefs AS $kd_table " .
-                       "ON $kd_table.id = $k_table.keywordid");
+    push(@$joins, "LEFT JOIN keywords AS $k_table " .
+                         "ON $k_table.bug_id = bugs.bug_id");
+    push(@$joins, "LEFT JOIN keyworddefs AS $kd_table " .
+                         "ON $kd_table.id = $k_table.keywordid");
     
-    $$ff = "$kd_table.name";
+    $args->{full_field} = "$kd_table.name";
 }
 
+# XXX This should be combined with blocked_nonchanged.
 sub _dependson_nonchanged {
-    my $self = shift;
-    my %func_args = @_;
-    my ($chartid, $ff, $f, $t, $term, $supptables) =
-        @func_args{qw(chartid ff f t term supptables)};
+    my ($self, $args) = @_;
+    my ($chart_id, $joins, $field, $operator) =
+        @$args{qw(chart_id joins field operator)};
     
-    my $table = "dependson_" . $$chartid;
-    $$ff = "$table.$$f";
-    $self->_do_operator_function(\%func_args);
-    push(@$supptables, "LEFT JOIN dependencies AS $table " .
-                       "ON $table.blocked = bugs.bug_id " .
-                       "AND ($$term)");
-    $$term = "$$ff IS NOT NULL";
+    my $table = "dependson_$chart_id";
+    my $full_field = "$table.$field";
+    $args->{full_field} = $full_field;
+    $self->_do_operator_function($args);
+    my $term = $args->{term};
+    push(@$joins, "LEFT JOIN dependencies AS $table " .
+                         "ON $table.blocked = bugs.bug_id AND ($term)");
+    $args->{term} = "$full_field IS NOT NULL";
 }
 
 sub _blocked_nonchanged {
-    my $self = shift;
-    my %func_args = @_;
-    my ($chartid, $ff, $f, $t, $term, $supptables) =
-        @func_args{qw(chartid ff f t term supptables)};
+    my ($self, $args) = @_;
+    my ($chart_id, $joins, $field, $operator) =
+        @$args{qw(chart_id joins field operator)};
 
-    my $table = "blocked_" . $$chartid;
-    $$ff = "$table.$$f";
-    $self->_do_operator_function(\%func_args);
-    push(@$supptables, "LEFT JOIN dependencies AS $table " .
-                       "ON $table.dependson = bugs.bug_id " .
-                       "AND ($$term)");
-    $$term = "$$ff IS NOT NULL";
+    my $table = "blocked_$chart_id";
+    my $full_field = "$table.$field";
+    $args->{full_field} = $full_field;
+    $self->_do_operator_function($args);
+    my $term = $args->{term};
+    push(@$joins, "LEFT JOIN dependencies AS $table " .
+                         "ON $table.dependson = bugs.bug_id AND ($term)");
+    $args->{term} = "$full_field IS NOT NULL";
 }
 
 sub _alias_nonchanged {
-    my $self = shift;
-    my %func_args = @_;
-    my ($ff, $t, $term) = @func_args{qw(ff t term)};
-    
-    $$ff = "COALESCE(bugs.alias, '')";
-    
-    $self->_do_operator_function(\%func_args);
+    my ($self, $args) = @_;
+    $args->{full_field} = "COALESCE(bugs.alias, '')";
+    $self->_do_operator_function($args);
 }
 
 sub _owner_idle_time_greater_less {
-    my $self = shift;
-    my %func_args = @_;
-    my ($chartid, $v, $supptables, $t, $wherepart, $term) =
-        @func_args{qw(chartid v supptables t wherepart term)};
+    my ($self, $args) = @_;
+    my ($chart_id, $joins, $value, $operator) =
+        @$args{qw(chart_id joins value operator)};
     my $dbh = Bugzilla->dbh;
     
-    my $table = "idle_" . $$chartid;
-    $$v =~ /^(\d+)\s*([hHdDwWmMyY])?$/;
-    my $quantity = $1 || 0;
-    my $unit = lc $2;
-    my $unitinterval = 'DAY';
-    if ($unit eq 'h') {
-        $unitinterval = 'HOUR';
-    } elsif ($unit eq 'w') {
-        $unitinterval = ' * 7 DAY';
-    } elsif ($unit eq 'm') {
-        $unitinterval = 'MONTH';
-    } elsif ($unit eq 'y') {
-        $unitinterval = 'YEAR';
-    }
-    my $cutoff = "NOW() - " .
-                 $dbh->sql_interval($quantity, $unitinterval);
+    my $table = "idle_$chart_id";
+    my $quoted = $dbh->quote(SqlifyDate($value));
+    
+    my $ld_table = "comment_$table";
+    my $comments_join =
+        "LEFT JOIN longdescs AS $ld_table"
+        .     " ON $ld_table.who = bugs.assigned_to"
+        .        " AND $ld_table.bug_id = bugs.bug_id"
+        .        " AND $ld_table.bug_when > $quoted";
+    push(@$joins, $comments_join);
+
+    my $act_table = "activity_$table";
     my $assigned_fieldid = get_field_id('assigned_to');
-    push(@$supptables, "LEFT JOIN longdescs AS comment_$table " .
-                       "ON comment_$table.who = bugs.assigned_to " .
-                       "AND comment_$table.bug_id = bugs.bug_id " .
-                       "AND comment_$table.bug_when > $cutoff");
-    push(@$supptables, "LEFT JOIN bugs_activity AS activity_$table " .
-                       "ON (activity_$table.who = bugs.assigned_to " .
-                       "OR activity_$table.fieldid = $assigned_fieldid) " .
-                       "AND activity_$table.bug_id = bugs.bug_id " .
-                       "AND activity_$table.bug_when > $cutoff");
-    if ($$t =~ /greater/) {
-        push(@$wherepart, "(comment_$table.who IS NULL " .
-                          "AND activity_$table.who IS NULL)");
+
+    # XXX Why are we joining using $assignedto_fieldid here? It shouldn't
+    #     matter when or if the assignee changed.
+    my $activity_join =
+        "LEFT JOIN bugs_activity AS $act_table"
+        .     " ON ( $act_table.who = bugs.assigned_to"
+        .         "  OR $act_table.fieldid = $assigned_fieldid )"
+        .        " AND $act_table.bug_id = bugs.bug_id"
+        .        " AND $act_table.bug_when > $quoted";
+    push(@$joins, $activity_join);
+    
+    if ($operator =~ /greater/) {
+        $args->{term} =
+            "$ld_table.who IS NULL AND $act_table.who IS NULL)";
     } else {
-        push(@$wherepart, "(comment_$table.who IS NOT NULL " .
-                          "OR activity_$table.who IS NOT NULL)");
+         $args->{term} =
+            "$ld_table.who IS NOT NULL OR $act_table.who IS NOT NULL";
     }
-    $$term = "0=0";
 }
 
 sub _multiselect_negative {
-    my $self = shift;
-    my %func_args = @_;
-    my ($f, $ff, $t, $term) = @func_args{qw(f ff t term)};
-    
-    my %map = (
-        notequals => 'equals',
-        notregexp => 'regexp',
-        notsubstring => 'substring',
-        nowords => 'anywords',
-        nowordssubstr => 'anywordssubstr',
-    );
+    my ($self, $args) = @_;
+    my ($field, $operator) = @$args{qw(field operator)};
 
     my $table;
-    if ($$f eq 'keywords') {
+    if ($field eq 'keywords') {
         $table = "keywords LEFT JOIN keyworddefs"
                  . " ON keywords.keywordid = keyworddefs.id";
-        $$ff = "keyworddefs.name";
+        $args->{full_field} = "keyworddefs.name";
     }
-    else {
-        $table = "bug_$$f";
-        $$ff = "$table.value";
+    else { 
+        $table = "bug_$field";
+        $args->{full_field} = "$table.value";
     }
-    
-    $$t = $map{$$t};
-    $self->_do_operator_function(\%func_args);
-    $$term = "bugs.bug_id NOT IN (SELECT bug_id FROM $table WHERE $$term)";
+    $args->{operator} = $self->_reverse_operator($operator);
+    $self->_do_operator_function($args);
+    my $term = $args->{term};
+    $args->{term} =
+        "bugs.bug_id NOT IN (SELECT bug_id FROM $table WHERE $term)";
 }
 
 sub _multiselect_multiple {
-    my $self = shift;
-    my %func_args = @_;
-    my ($f, $ff, $t, $v, $term) = @func_args{qw(f ff t v term)};
+    my ($self, $args) = @_;
+    my ($chart_id, $joins, $field, $operator, $value)
+        = @$args{qw(chart_id joins field operator value)};
     
-    my @terms;
-    my $table = "bug_$$f";
-    $$ff = "$table.value";
+    my $table = "bug_$field";
+    $args->{full_field} = "$table.value";
     
-    foreach my $word (split(/[\s,]+/, $$v)) {
-        $$v = $word;
-        $self->_do_operator_function(\%func_args);
-        push(@terms, "bugs.bug_id IN
-                      (SELECT bug_id FROM $table WHERE $$term)");
+    my @terms;
+    foreach my $word (split(/[\s,]+/, $value)) {
+        $args->{value} = $word;
+        $self->_do_operator_function($args);
+        my $term = $args->{term};
+        push(@terms, "bugs.bug_id IN (SELECT bug_id FROM $table WHERE $term)");
     }
     
-    if ($$t eq 'anyexact') {
-        $$term = "(" . join(" OR ", @terms) . ")";
+    if ($operator eq 'anyexact') {
+        $args->{term} = join(" OR ", @terms);
     }
     else {
-        $$term = "(" . join(" AND ", @terms) . ")";
+        $args->{term} = join(" AND ", @terms);
     }
 }
 
 sub _multiselect_nonchanged {
-    my $self = shift;
-    my %func_args = @_;
-    my ($chartid, $f, $ff, $t, $supptables) =
-        @func_args{qw(chartid f ff t supptables)};
-
-    my $table = $$f."_".$$chartid;
-    $$ff = "$table.value";
-    
-    $self->_do_operator_function(\%func_args);
-    push(@$supptables, "LEFT JOIN bug_$$f AS $table " .
-                       "ON $table.bug_id = bugs.bug_id ");
-}
+    my ($self, $args) = @_;
+    my ($chart_id, $joins, $field, $operator) =
+        @$args{qw(chart_id joins field operator)};
 
-sub _equals {
-    my $self = shift;
-    my %func_args = @_;
-    my ($ff, $q, $term) = @func_args{qw(ff q term)};
-    
-    $$term = "$$ff = $$q";
+    my $table = "${field}_$chart_id";
+    $args->{full_field} = "$table.value";
+    push(@$joins, "LEFT JOIN bug_$field AS $table " .
+                         "ON $table.bug_id = bugs.bug_id ");
 }
 
-sub _notequals {
-    my $self = shift;
-    my %func_args = @_;
-    my ($ff, $q, $term) = @func_args{qw(ff q term)};
-    
-    $$term = "$$ff != $$q";
+sub _simple_operator {
+    my ($self, $args) = @_;
+    my ($full_field, $quoted, $operator) =
+        @$args{qw(full_field quoted operator)};
+    my $sql_operator = SIMPLE_OPERATORS->{$operator};
+    $args->{term} = "$full_field $sql_operator $quoted";
 }
 
 sub _casesubstring {
-    my $self = shift;
-    my %func_args = @_;
-    my ($ff, $q, $term) = @func_args{qw(ff q term)};
+    my ($self, $args) = @_;
+    my ($full_field, $quoted) = @$args{qw(full_field quoted)};
     my $dbh = Bugzilla->dbh;
     
-    $$term = $dbh->sql_position($$q, $$ff) . " > 0";
+    $args->{term} = $dbh->sql_position($quoted, $full_field) . " > 0";
 }
 
 sub _substring {
-    my $self = shift;
-    my %func_args = @_;
-    my ($ff, $q, $term) = @func_args{qw(ff q term)};
+    my ($self, $args) = @_;
+    my ($full_field, $quoted) = @$args{qw(full_field quoted)};
     my $dbh = Bugzilla->dbh;
     
-    $$term = $dbh->sql_iposition($$q, $$ff) . " > 0";
+    # XXX This should probably be changed to just use LIKE
+    $args->{term} = $dbh->sql_iposition($quoted, $full_field) . " > 0";
 }
 
 sub _notsubstring {
-    my $self = shift;
-    my %func_args = @_;
-    my ($ff, $q, $term) = @func_args{qw(ff q term)};
+    my ($self, $args) = @_;
+    my ($full_field, $quoted) = @$args{qw(full_field quoted)};
     my $dbh = Bugzilla->dbh;
     
-    $$term = $dbh->sql_iposition($$q, $$ff) . " = 0";
+    # XXX This should probably be changed to just use NOT LIKE
+    $args->{term} = $dbh->sql_iposition($quoted, $full_field) . " = 0";
 }
 
 sub _regexp {
-    my $self = shift;
-    my %func_args = @_;
-    my ($ff, $q, $term) = @func_args{qw(ff q term)};
+    my ($self, $args) = @_;
+    my ($full_field, $quoted) = @$args{qw(full_field quoted)};
     my $dbh = Bugzilla->dbh;
     
-    $$term = $dbh->sql_regexp($$ff, $$q);
+    $args->{term} = $dbh->sql_regexp($full_field, $quoted);
 }
 
 sub _notregexp {
-    my $self = shift;
-    my %func_args = @_;
-    my ($ff, $q, $term) = @func_args{qw(ff q term)};
+    my ($self, $args) = @_;
+    my ($full_field, $quoted) = @$args{qw(full_field quoted)};
     my $dbh = Bugzilla->dbh;
     
-    $$term = $dbh->sql_not_regexp($$ff, $$q);
-}
-
-sub _lessthan {
-    my $self = shift;
-    my %func_args = @_;
-    my ($ff, $q, $term) = @func_args{qw(ff q term)};
-
-    $$term = "$$ff < $$q";
-}
-
-sub _lessthaneq {
-    my $self = shift;
-    my %func_args = @_;
-    my ($ff, $q, $term) = @func_args{qw(ff q term)};
-
-    $$term = "$$ff <= $$q";
-}
-
-sub _greaterthan {
-    my $self = shift;
-    my %func_args = @_;
-    my ($ff, $q, $term) = @func_args{qw(ff q term)};
-    
-    $$term = "$$ff > $$q";
-}
-
-sub _greaterthaneq {
-    my $self = shift;
-    my %func_args = @_;
-    my ($ff, $q, $term) = @func_args{qw(ff q term)};
-    
-    $$term = "$$ff >= $$q";
+    $args->{term} = $dbh->sql_not_regexp($full_field, $quoted);
 }
 
 sub _anyexact {
-    my $self = shift;
-    my %func_args = @_;
-    my ($f, $ff, $v, $q, $term) = @func_args{qw(f ff v q term)};
+    my ($self, $args) = @_;
+    my ($field, $value, $full_field) = @$args{qw(field value full_field)};
     my $dbh = Bugzilla->dbh;
     
     my @list;
-    foreach my $w (split(/,/, $$v)) {
-        if ($w eq "---" && $$f =~ /resolution/) {
-            $w = "";
+    foreach my $word (split(/,/, $value)) {
+        if ($word eq "---" && $field eq 'resolution') {
+            $word = "";
         }
-        $$q = $dbh->quote($w);
-        trick_taint($$q);
-        push(@list, $$q);
+        my $quoted_word = $dbh->quote($word);
+        trick_taint($quoted_word);
+        push(@list, $quoted_word);
     }
+    
     if (@list) {
-        $$term = $dbh->sql_in($$ff, \@list);
+        $args->{term} = $dbh->sql_in($full_field, \@list);
+    }
+    # XXX Perhaps if it's all commas, we should just throw an error.
+    else {
+        $args->{term} = "0=0";
     }
 }
 
 sub _anywordsubstr {
-    my $self = shift;
-    my %func_args = @_;
-    my ($ff, $v, $term) = @func_args{qw(ff v term)};
+    my ($self, $args) = @_;
+    my ($full_field, $value) = @$args{qw(full_field value)};
     
-    $$term = join(" OR ", @{GetByWordListSubstr($$ff, $$v)});
+    my $list = GetByWordListSubstr($full_field, $value);
+    $args->{term} = join(" OR ", @$list);
 }
 
 sub _allwordssubstr {
-    my $self = shift;
-    my %func_args = @_;
-    my ($ff, $v, $term) = @func_args{qw(ff v term)};
+    my ($self, $args) = @_;
+    my ($full_field, $value) = @$args{qw(full_field value)};
     
-    $$term = join(" AND ", @{GetByWordListSubstr($$ff, $$v)});
+    my $list = GetByWordListSubstr($full_field, $value);
+    $args->{term} = join(" AND ", @$list);
 }
 
 sub _nowordssubstr {
-    my $self = shift;
-    my %func_args = @_;
-    my ($ff, $v, $term) = @func_args{qw(ff v term)};
-    
-    my @list = @{GetByWordListSubstr($$ff, $$v)};
-    if (@list) {
-        $$term = "NOT (" . join(" OR ", @list) . ")";
-    }
+    my ($self, $args) = @_;
+    $self->_anywordsubstr($args);
+    my $term = $args->{term};
+    $args->{term} = "NOT($term)";
 }
 
 sub _anywords {
-    my $self = shift;
-    my %func_args = @_;
-    my ($ff, $v, $term) = @func_args{qw(ff v term)};
+    my ($self, $args) = @_;
+    my ($full_field, $value) = @$args{qw(full_field value)};
     
-    $$term = join(" OR ", @{GetByWordList($$ff, $$v)});
+    my $list = GetByWordList($full_field, $value);
+    $args->{term} = join(" OR ", @$list);
 }
 
 sub _allwords {
-    my $self = shift;
-    my %func_args = @_;
-    my ($ff, $v, $term) = @func_args{qw(ff v term)};
+    my ($self, $args) = @_;
+    my ($full_field, $value) = @$args{qw(full_field value)};
     
-    $$term = join(" AND ", @{GetByWordList($$ff, $$v)});
+    my $list = GetByWordList($full_field, $value);
+    $args->{term} = join(" AND ", @$list);
 }
 
 sub _nowords {
-    my $self = shift;
-    my %func_args = @_;
-    my ($ff, $v, $term) = @func_args{qw(ff v term)};
-    
-    my @list = @{GetByWordList($$ff, $$v)};
-    if (@list) {
-        $$term = "NOT (" . join(" OR ", @list) . ")";
-    }
+    my ($self, $args) = @_;
+    $self->_anywords($args);
+    my $term = $args->{term};
+    $args->{term} = "NOT($term)";
 }
 
 sub _changedbefore_changedafter {
-    my $self = shift;
-    my %func_args = @_;
-    my ($chartid, $f, $ff, $t, $v, $chartfields, $supptables, $term) =
-        @func_args{qw(chartid f ff t v chartfields supptables term)};
+    my ($self, $args) = @_;
+    my ($chart_id, $joins, $field, $operator, $value) =
+        @$args{qw(chart_id joins field operator value)};
     my $dbh = Bugzilla->dbh;
     
-    my $operator = ($$t =~ /before/) ? '<' : '>';
-    my $table = "act_$$chartid";
-    my $fieldid = $$chartfields{$$f};
-    if (!$fieldid) {
-        ThrowCodeError("invalid_field_name", {field => $$f});
-    }
-    push(@$supptables, "LEFT JOIN bugs_activity AS $table " .
-                      "ON $table.bug_id = bugs.bug_id " .
-                      "AND $table.fieldid = $fieldid " .
-                      "AND $table.bug_when $operator " . 
-                      $dbh->quote(SqlifyDate($$v)) );
-    $$term = "($table.bug_when IS NOT NULL)";
+    my $sql_operator = ($operator =~ /before/) ? '<' : '>';
+    my $table = "act_$chart_id";
+    my $field_id = get_field_id($field);
+    my $sql_date = $dbh->quote(SqlifyDate($value));
+    push(@$joins,
+        "LEFT JOIN bugs_activity AS $table"
+        .     " ON $table.bug_id = bugs.bug_id"
+        .         " AND $table.fieldid = $field_id"
+        .         " AND $table.bug_when $sql_operator $sql_date");
+    $args->{term} = "$table.bug_when IS NOT NULL";
 }
 
 sub _changedfrom_changedto {
-    my $self = shift;
-    my %func_args = @_;
-    my ($chartid, $chartfields, $f, $t, $v, $q, $supptables, $term) =
-        @func_args{qw(chartid chartfields f t v q supptables term)};
+    my ($self, $args) = @_;
+    my ($chart_id, $joins, $field, $operator, $quoted) =
+        @$args{qw(chart_id joins field operator quoted)};
     
-    my $operator = ($$t =~ /from/) ? 'removed' : 'added';
-    my $table = "act_$$chartid";
-    my $fieldid = $$chartfields{$$f};
-    if (!$fieldid) {
-        ThrowCodeError("invalid_field_name", {field => $$f});
-    }
-    push(@$supptables, "LEFT JOIN bugs_activity AS $table " .
-                      "ON $table.bug_id = bugs.bug_id " .
-                      "AND $table.fieldid = $fieldid " .
-                      "AND $table.$operator = $$q");
-    $$term = "($table.bug_when IS NOT NULL)";
+    my $column = ($operator =~ /from/) ? 'removed' : 'added';
+    my $table = "act_$chart_id";
+    my $field_id = get_field_id($field);
+    push(@$joins,
+        "LEFT JOIN bugs_activity AS $table"
+        .     " ON $table.bug_id = bugs.bug_id"
+        .        " AND $table.fieldid = $field_id"
+        .        " AND $table.$column = $quoted");
+    $args->{term} = "$table.bug_when IS NOT NULL";
 }
 
 sub _changedby {
-    my $self = shift;
-    my %func_args = @_;
-    my ($chartid, $chartfields, $f, $v, $supptables, $term) =
-        @func_args{qw(chartid chartfields f v supptables term)};
-    
-    my $table = "act_$$chartid";
-    my $fieldid = $$chartfields{$$f};
-    if (!$fieldid) {
-        ThrowCodeError("invalid_field_name", {field => $$f});
-    }
-    my $id = login_to_id($$v, THROW_ERROR);
-    push(@$supptables, "LEFT JOIN bugs_activity AS $table " .
-                      "ON $table.bug_id = bugs.bug_id " .
-                      "AND $table.fieldid = $fieldid " .
-                      "AND $table.who = $id");
-    $$term = "($table.bug_when IS NOT NULL)";
+    my ($self, $args) = @_;
+    my ($chart_id, $joins, $field, $operator, $value) =
+        @$args{qw(chart_id joins field operator value)};
+    
+    my $table = "act_$chart_id";
+    my $field_id = get_field_id($field);
+    my $user_id  = login_to_id($value, THROW_ERROR);
+    push(@$joins,
+        "LEFT JOIN bugs_activity AS $table"
+        .     " ON $table.bug_id = bugs.bug_id"
+        .        " AND $table.fieldid = $field_id"
+        .        " AND $table.who = $user_id");
+    $args->{term} = "$table.bug_when IS NOT NULL";
 }
 
 1;
index 43644f7035c092c5f05c28a8b9bfc7f73f783d8b..849008adb48ef1e3ae6520d2dd560cc73bbca18f 100644 (file)
     '[% field.description FILTER html %]' ([% field.name FILTER html %])
     is not a custom field.
 
-  [% ELSIF error == "field_type_mismatch" %]
-    Cannot seem to handle <code>[% field FILTER html %]</code>
-    and <code>[% type FILTER html %]</code> together.
-
   [% ELSIF error == "field_type_not_specified" %]
     [% title = "Field Type Not Specified" %]
     You must specify a type when creating a custom field.
index 490941807a7d74a3cf31c9f9aae051be2d40412b..3f7248c409775eb1c26185cd25e4f4e527845082 100644 (file)
     [% title = "Group not specified" %]
     No group was specified.
 
+  [% ELSIF error == "group_not_visible" %]
+    [% title = "Group Not Allowed" %]
+    You are not allowed to see members of the [% group.name FILTER html %]
+    group.
+
   [% ELSIF error == "system_group_not_deletable" %]
     [% title = "System Groups not deletable" %]
     <em>[% name FILTER html %]</em> is a system group.
     This group cannot be deleted.
-
   [% ELSIF error == "group_unknown" %]
     [% title = "Unknown Group" %]
     The group [% name FILTER html %] does not exist. Please specify
     [% IF class == "Bugzilla::User" %]
       Either you mis-typed the name or that user has not yet registered
       for a [% terms.Bugzilla %] account.
+    [% ELSIF class == "Bugzilla::Keyword" %]
+      The legal keyword names are <a href="describekeywords.cgi">listed
+      here</a>.
     [% END %]
 
   [% ELSIF error == "old_password_incorrect" %]
     and the "matches" search can only be used with the "content"
     field.
 
+  [% ELSIF error == "search_field_operator_invalid" %]
+    [% terms.Bugzilla %] does not support using the 
+    "[%+ field_descs.$field FILTER html %]" ([% field FILTER html %])
+    field with the "[% search_descs.$operator %]" ([% operator FILTER html %])
+    search type.
+
   [% ELSIF error == "series_already_exists" %]
     [% title = "Series Already Exists" %]
     [% docslinks = {'reporting.html' => 'Reporting'} %]
        I could not figure out what you wanted to do.
     [% END %]
 
-  [% ELSIF error == "unknown_keyword" %]
-    [% title = "Unknown Keyword" %]
-    <code>[% keyword FILTER html %]</code> is not a known keyword.
-    The legal keyword names are <a href="describekeywords.cgi">listed here</a>.
-
   [% ELSIF error == "unknown_tab" %]
     [% title = "Unknown Tab" %]
     <code>[% current_tab_name FILTER html %]</code> is not a legal tab name.
     field
   [% ELSIF class == "Bugzilla::Group" %]
     group
+  [% ELSIF class == "Bugzilla::Keyword" %]
+    keyword
   [% ELSIF class == "Bugzilla::Product" %]
     product
   [% ELSIF class == "Bugzilla::Search::Recent" %]
index 86e195ae0f16269ca18335f5e8896ed76c6b001e..7f35b371935646f6953ca09170a7adb2c80fb5ca 100644 (file)
@@ -179,22 +179,6 @@ use constant FIELD_SUBSTR_SIZE => {
 # See the KNOWN_BROKEN constant for a general description of these
 # "_BROKEN" constants.
 
-# Search.pm currently enforces "this must be a 0 or 1" in situations
-# where it should not, with two of the attachment booleans.
-use constant ATTACHMENT_BOOLEANS_SEARCH_BROKEN => (
-    'attachments.ispatch'    => { search => 1 },
-    'attachments.isobsolete' => { search => 1 },
-);
-
-# Sometimes the search for attachment booleans works, but then contains
-# the wrong results, because it does not contain bugs that fully lack
-# attachments.
-use constant ATTACHMENT_BOOLEANS_CONTAINS_BROKEN => (
-    'attachments.isobsolete'  => { contains => [5] },
-    'attachments.ispatch'     => { contains => [5] },
-    'attachments.isprivate'   => { contains => [5] },
-);
-
 # Certain fields fail all the "negative" search tests:
 #
 # Blocked and Dependson "notequals" only finds bugs that have
@@ -223,7 +207,9 @@ use constant ATTACHMENT_BOOLEANS_CONTAINS_BROKEN => (
 #
 # requestees.login_name doesn't find bugs that fully lack requestees.
 use constant NEGATIVE_BROKEN => (
-    ATTACHMENT_BOOLEANS_CONTAINS_BROKEN,
+    'attachments.isobsolete'  => { contains => [5] },
+    'attachments.ispatch'     => { contains => [5] },
+    'attachments.isprivate'   => { contains => [5] },
     'attach_data.thedata'     => { contains => [5] },
     'attachments.description' => { contains => [5] },
     'attachments.filename'    => { contains => [5] },
@@ -237,7 +223,6 @@ use constant NEGATIVE_BROKEN => (
     dependson    => { contains => [2,4,5] },
     longdesc     => { contains => [1] },
     'longdescs.isprivate'   => { contains => [1] },
-    percentage_complete     => { contains => [1] },
     'requestees.login_name' => { contains => [3,4,5] },
     'setters.login_name'    => { contains => [5] },
     work_time               => { contains => [1] },
@@ -271,16 +256,12 @@ use constant GREATERTHAN_BROKEN => (
 # allwordssubstr work_time only matches against a single comment,
 # instead of matching against all comments on a bug. Same is true
 # for the other longdesc fields, cc, keywords, and bug_group.
-#
-# percentage_complete just drops in 0=0 for the term.
 use constant ALLWORDS_BROKEN => (
-    ATTACHMENT_BOOLEANS_SEARCH_BROKEN,
     bug_group => { contains => [1] },
     cc        => { contains => [1] },
     keywords  => { contains => [1] },
     longdesc  => { contains => [1] },
     work_time => { contains => [1] },
-    percentage_complete => { contains => [2,3,4,5] },
 );
 
 # nowords and nowordssubstr have these broken tests in common.
@@ -306,7 +287,7 @@ use constant NOWORDS_BROKEN => (
 use constant CHANGED_BROKEN => (
     classification => { contains => [1] },
     commenter => { contains => [1] },
-    percentage_complete     => { contains => [2,3,4,5] },
+    percentage_complete     => { contains => [1] },
     'requestees.login_name' => { contains => [1] },
     'setters.login_name'    => { contains => [1] },
     delta_ts                => { contains => [1] },
@@ -346,28 +327,9 @@ use constant CHANGED_VALUE_BROKEN => (
 # while the other fails. In this case, we have a special override for
 # "operator-value", which uniquely identifies tests.
 use constant KNOWN_BROKEN => {
-    notequals => { NEGATIVE_BROKEN },
-    # percentage_complete substring matches every bug, regardless of
-    # its percentage_complete value.
-    substring => {
-        percentage_complete => { contains => [2,3,4,5] },
-    },
-    casesubstring => {
-        percentage_complete => { contains => [2,3,4,5] },
-    },
+    notequals    => { NEGATIVE_BROKEN },
     notsubstring => { NEGATIVE_BROKEN },
-
-    # Attachment noolean fields don't work with regexes, right now,
-    # because they throw an error that regexes are not valid booleans.
-    'regexp-^1-' => { ATTACHMENT_BOOLEANS_SEARCH_BROKEN },
-    # percentage_complete notregexp fails to match bugs that
-    # fully lack hours worked.
-    notregexp => {
-        NEGATIVE_BROKEN,
-        percentage_complete => { contains => [5] },
-    },
-    'notregexp-^1-' => { ATTACHMENT_BOOLEANS_SEARCH_BROKEN },
+    notregexp    => { NEGATIVE_BROKEN },
 
     # percentage_complete doesn't match bugs with 0 hours worked or remaining.
     #
@@ -376,19 +338,13 @@ use constant KNOWN_BROKEN => {
     # also broken in this way, but all our comments come from the same user.) 
     # Also, the attachments ones don't find bugs that have no attachments 
     # at all (which might be OK?).
-    #
-    # attachments.isprivate lessthan doesn't find bugs without attachments.
     lessthan   => {
-        ATTACHMENT_BOOLEANS_SEARCH_BROKEN,
-        'attachments.isprivate' => { contains => [5] },
         'longdescs.isprivate'   => { contains => [1] },
-        percentage_complete => { contains => [5] }, 
         work_time => { contains => [1,2,3,4] },
     },
     # The lessthaneq tests are broken for the same reasons, but they work
     # slightly differently so they have a different set of broken tests.
     lessthaneq => {
-        ATTACHMENT_BOOLEANS_CONTAINS_BROKEN,
         'longdescs.isprivate' => { contains => [1] },
         work_time => { contains => [2,3,4] },
     },
@@ -401,24 +357,18 @@ use constant KNOWN_BROKEN => {
         percentage_complete => { contains => [2] },
     },
 
-    # percentage_complete just throws 0=0 into the search term, returning
-    # all bugs.
+    # percentage_complete doesn't do a numeric comparison, so
+    # it doesn't find decimal values.
     anyexact => {
-        ATTACHMENT_BOOLEANS_SEARCH_BROKEN,
-        percentage_complete => { contains => [3,4,5] },
+        percentage_complete => { contains => [2] },
     },
     # bug_group anywordssubstr returns all our bugs. Not sure why.
     anywordssubstr => {
-        ATTACHMENT_BOOLEANS_SEARCH_BROKEN,
-        percentage_complete => { contains => [3,4,5] },
+        percentage_complete => { contains => [2] },
         bug_group => { contains => [3,4,5] },
     },
 
     'allwordssubstr-<1>' => { ALLWORDS_BROKEN },
-    'allwordssubstr-<1>,<2>' => {
-        ATTACHMENT_BOOLEANS_SEARCH_BROKEN,
-        percentage_complete => { contains => [1,2,3,4,5] },
-    },
     # flagtypes.name does not work here, probably because they all try to
     # match against a single flag.
     # Same for attach_data.thedata.
@@ -427,10 +377,6 @@ use constant KNOWN_BROKEN => {
         'attach_data.thedata' => { contains => [1] },
         'flagtypes.name' => { contains => [1] },
     },
-    'allwords-<1> <2>' => {
-        ATTACHMENT_BOOLEANS_SEARCH_BROKEN,
-        percentage_complete => { contains => [1,2,3,4,5] },
-    },
 
     nowordssubstr => { NOWORDS_BROKEN },
     # attach_data.thedata doesn't match properly with any of the plain
@@ -446,15 +392,13 @@ use constant KNOWN_BROKEN => {
     # attach_data doesn't work (perhaps because it's the entire
     # data, or some problem with the regex?).
     anywords => {
-        ATTACHMENT_BOOLEANS_SEARCH_BROKEN,
         'attach_data.thedata' => { contains => [1] },
         bug_group => { contains => [2,3,4,5] },
-        percentage_complete => { contains => [2,3,4,5] },
         work_time => { contains => [1] },
     },
     'anywords-<1> <2>' => {
         bug_group => { contains => [3,4,5] },
-        percentage_complete => { contains => [3,4,5] },
+        percentage_complete => { contains => [2] },
         'attach_data.thedata' => { contains => [1,2] },
         work_time => { contains => [1,2] },
     },
@@ -496,7 +440,7 @@ use constant KNOWN_BROKEN => {
         commenter   => { contains => [2,3,4] },
         creation_ts => { contains => [2,3,4] },
         delta_ts    => { contains => [2,3,4] },
-        percentage_complete     => { contains => [1,5] },
+        percentage_complete => { contains => [2,3,4] },
         'requestees.login_name' => { contains => [2,3,4] },
         'setters.login_name'    => { contains => [2,3,4] },
     },
@@ -552,11 +496,13 @@ use constant REGEX_OVERRIDE => {
     blocked   => { value => '^<1>$' },
     dependson => { value => '^<1>$' },
     bug_id    => { value => '^<1>$' },
-    'attachments.isprivate' => { value => '^1' },
-    cclist_accessible       => { value => '^1' },
-    reporter_accessible     => { value => '^1' },
-    everconfirmed           => { value => '^1' },
-    'longdescs.isprivate'   => { value => '^1' },
+    'attachments.isobsolete' => { value => '^1'},
+    'attachments.ispatch'    => { value => '^1'},
+    'attachments.isprivate'  => { value => '^1' },
+    cclist_accessible        => { value => '^1' },
+    reporter_accessible      => { value => '^1' },
+    everconfirmed            => { value => '^1' },
+    'longdescs.isprivate'    => { value => '^1' },
     creation_ts => { value => '^2037-01-01' },
     delta_ts    => { value => '^2037-01-01' },
     deadline    => { value => '^2037-02-01' },
@@ -734,11 +680,13 @@ use constant TESTS => {
               blocked   => { value => '<4-id>', contains => [1,2] },
               dependson => { value => '<3-id>', contains => [1,3] },
               bug_id    => { value => '<2-id>' },
-              'attachments.isprivate' => { value => 1, contains => [2,3,4,5] },
-              cclist_accessible       => { value => 1, contains => [2,3,4,5] },
-              reporter_accessible     => { value => 1, contains => [2,3,4,5] },
-              'longdescs.isprivate'   => { value => 1, contains => [2,3,4,5] },
-              everconfirmed           => { value => 1, contains => [2,3,4,5] },
+              'attachments.isprivate'  => { value => 1, contains => [2,3,4] },
+              'attachments.isobsolete' => { value => 1, contains => [2,3,4] },
+              'attachments.ispatch'    => { value => 1, contains => [2,3,4] },
+              cclist_accessible        => { value => 1, contains => [2,3,4,5] },
+              reporter_accessible      => { value => 1, contains => [2,3,4,5] },
+              'longdescs.isprivate'    => { value => 1, contains => [2,3,4,5] },
+              everconfirmed            => { value => 1, contains => [2,3,4,5] },
               creation_ts => { value => '2037-01-02', contains => [1,5] },
               delta_ts    => { value => '2037-01-02', contains => [1,5] },
               deadline    => { value => '2037-02-02' },
@@ -755,9 +703,9 @@ use constant TESTS => {
     lessthaneq => [
         { contains => [1], value => '<1>',
           override => {
-              'attachments.ispatch'    => { value => 0, contains => [2,3,4,5] },
-              'attachments.isobsolete' => { value => 0, contains => [2,3,4,5] },
-              'attachments.isprivate'  => { value => 0, contains => [2,3,4,5] },
+              'attachments.isobsolete' => { value => 0, contains => [2,3,4] },
+              'attachments.ispatch'    => { value => 0, contains => [2,3,4] },
+              'attachments.isprivate'  => { value => 0, contains => [2,3,4] },
               cclist_accessible        => { value => 0, contains => [2,3,4,5] },
               reporter_accessible      => { value => 0, contains => [2,3,4,5] },
               'longdescs.isprivate'    => { value => 0, contains => [2,3,4,5] },
@@ -768,6 +716,7 @@ use constant TESTS => {
               delta_ts       => { contains => [1,5] },
               remaining_time => { contains => [1,5] },
               longdesc       => { contains => [1,5] },
+              percentage_complete => { contains => [1,5] },
               work_time => { value => 1, contains => [1,5] },
               LESSTHAN_OVERRIDE,
           },
@@ -936,13 +885,7 @@ use constant TESTS => {
 # operator_ok overrides the "brokenness" of certain operators, so that they
 # are always OK for that field/operator combination.
 use constant INJECTION_BROKEN_FIELD => {
-    'attachments.isobsolete' => { search => 1 },
-    'attachments.ispatch'    => { search => 1 },
-    owner_idle_time => {
-        sql_error => qr/bugs\.owner_idle_time.+where clause/,
-        operator_ok => [qw(changedfrom changedto greaterthan greaterthaneq
-                           lessthan lessthaneq)]
-    },
+    owner_idle_time => { search => 1 },
     keywords => {
         search => 1,
         operator_ok => [qw(allwordssubstr anywordssubstr casesubstring
@@ -957,9 +900,9 @@ use constant INJECTION_BROKEN_FIELD => {
 # search => 1 means the Bugzilla::Search creation fails, but
 # field_ok contains fields that it does actually succeed for.
 use constant INJECTION_BROKEN_OPERATOR => {
-    changedafter  => { search => 1, field_ok => ['percentage_complete'] },
-    changedbefore => { search => 1, field_ok => ['percentage_complete'] },
-    changedby     => { search => 1, field_ok => ['percentage_complete'] },
+    changedafter  => { search => 1 },
+    changedbefore => { search => 1 },
+    changedby     => { search => 1 },
 };
 
 # Tests run by Bugzilla::Test::Search::InjectionTest.