diff options
Diffstat (limited to 'Bugzilla/Search.pm')
-rw-r--r-- | Bugzilla/Search.pm | 423 |
1 files changed, 379 insertions, 44 deletions
diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm index d67df03dd..8097d5fb8 100644 --- a/Bugzilla/Search.pm +++ b/Bugzilla/Search.pm @@ -5,10 +5,13 @@ # This Source Code Form is "Incompatible With Secondary Licenses", as # defined by the Mozilla Public License, v. 2.0. +package Bugzilla::Search; + +use 5.10.1; use strict; +use warnings; -package Bugzilla::Search; -use base qw(Exporter); +use parent qw(Exporter); @Bugzilla::Search::EXPORT = qw( IsValidQueryType split_order_term @@ -107,6 +110,7 @@ use Time::HiRes qw(gettimeofday tv_interval); # When doing searches, NULL datetimes are treated as this date. use constant EMPTY_DATETIME => '1970-01-01 00:00:00'; +use constant EMPTY_DATE => '1970-01-01'; # This is the regex for real numbers from Regexp::Common, modified to be # more readable. @@ -157,6 +161,8 @@ use constant OPERATORS => { changedfrom => \&_changedfrom_changedto, changedto => \&_changedfrom_changedto, changedby => \&_changedby, + isempty => \&_isempty, + isnotempty => \&_isnotempty, }; # Some operators are really just standard SQL operators, and are @@ -183,6 +189,8 @@ use constant OPERATOR_REVERSE => { lessthaneq => 'greaterthan', greaterthan => 'lessthaneq', greaterthaneq => 'lessthan', + isempty => 'isnotempty', + isnotempty => 'isempty', # The following don't currently have reversals: # casesubstring, anyexact, allwords, allwordssubstr }; @@ -198,6 +206,12 @@ use constant NON_NUMERIC_OPERATORS => qw( notregexp ); +# These operators ignore the entered value +use constant NO_VALUE_OPERATORS => qw( + isempty + isnotempty +); + use constant MULTI_SELECT_OVERRIDE => { notequals => \&_multiselect_negative, notregexp => \&_multiselect_negative, @@ -251,7 +265,7 @@ use constant OPERATOR_FIELD_OVERRIDE => { }, # General Bug Fields - alias => { _non_changed => \&_nullable }, + alias => { _non_changed => \&_alias_nonchanged }, 'attach_data.thedata' => MULTI_SELECT_OVERRIDE, # We check all attachment fields against this. attachments => MULTI_SELECT_OVERRIDE, @@ -303,7 +317,8 @@ use constant OPERATOR_FIELD_OVERRIDE => { _non_changed => \&_product_nonchanged, }, tag => MULTI_SELECT_OVERRIDE, - + comment_tag => MULTI_SELECT_OVERRIDE, + # Timetracking Fields deadline => { _non_changed => \&_deadline }, percentage_complete => { @@ -315,11 +330,16 @@ use constant OPERATOR_FIELD_OVERRIDE => { changedafter => \&_work_time_changedbefore_after, _default => \&_work_time, }, + last_visit_ts => { + _non_changed => \&_last_visit_ts, + _default => \&_last_visit_ts_invalid_operator, + }, # Custom Fields FIELD_TYPE_FREETEXT, { _non_changed => \&_nullable }, FIELD_TYPE_BUG_ID, { _non_changed => \&_nullable_int }, FIELD_TYPE_DATETIME, { _non_changed => \&_nullable_datetime }, + FIELD_TYPE_DATE, { _non_changed => \&_nullable_date }, FIELD_TYPE_TEXTAREA, { _non_changed => \&_nullable }, FIELD_TYPE_MULTI_SELECT, MULTI_SELECT_OVERRIDE, FIELD_TYPE_BUG_URLS, MULTI_SELECT_OVERRIDE, @@ -340,13 +360,19 @@ sub SPECIAL_PARSING { 'requestees.login_name' => \&_contact_pronoun, # Date Fields that accept the 1d, 1w, 1m, 1y, etc. format. - creation_ts => \&_timestamp_translate, - deadline => \&_timestamp_translate, - delta_ts => \&_timestamp_translate, + creation_ts => \&_datetime_translate, + deadline => \&_date_translate, + delta_ts => \&_datetime_translate, + + # last_visit field that accept both a 1d, 1w, 1m, 1y format and the + # %last_changed% pronoun. + last_visit_ts => \&_last_visit_datetime, }; foreach my $field (Bugzilla->active_custom_fields) { if ($field->type == FIELD_TYPE_DATETIME) { - $map->{$field->name} = \&_timestamp_translate; + $map->{$field->name} = \&_datetime_translate; + } elsif ($field->type == FIELD_TYPE_DATE) { + $map->{$field->name} = \&_date_translate; } } return $map; @@ -391,6 +417,7 @@ use constant FIELD_MAP => { bugidtype => 'bug_id_type', changedin => 'days_elapsed', long_desc => 'longdesc', + tags => 'tag', }; # Some fields are not sorted on themselves, but on other fields. @@ -429,6 +456,10 @@ sub COLUMN_JOINS { . ' FROM longdescs GROUP BY bug_id)', join => 'INNER', }, + alias => { + table => 'bugs_aliases', + as => 'map_alias', + }, assigned_to => { from => 'assigned_to', to => 'userid', @@ -484,6 +515,14 @@ sub COLUMN_JOINS { to => 'id', }, }, + blocked => { + table => 'dependencies', + to => 'dependson', + }, + dependson => { + table => 'dependencies', + to => 'blocked', + }, 'longdescs.count' => { table => 'longdescs', join => 'INNER', @@ -498,7 +537,14 @@ sub COLUMN_JOINS { from => 'map_bug_tag.tag_id', to => 'id', }, - } + }, + last_visit_ts => { + as => 'bug_user_last_visit', + table => 'bug_user_last_visit', + extra => ['bug_user_last_visit.user_id = ' . $user->id], + from => 'bug_id', + to => 'bug_id', + }, }; return $joins; }; @@ -544,6 +590,7 @@ sub COLUMNS { # like "bugs.bug_id". my $total_time = "(map_actual_time.total + bugs.remaining_time)"; my %special_sql = ( + alias => $dbh->sql_group_concat('DISTINCT map_alias.alias'), deadline => $dbh->sql_date_format('bugs.deadline', '%Y-%m-%d'), actual_time => 'map_actual_time.total', @@ -558,13 +605,18 @@ sub COLUMNS { . " END)", 'flagtypes.name' => $dbh->sql_group_concat('DISTINCT ' - . $dbh->sql_string_concat('map_flagtypes.name', 'map_flags.status')), + . $dbh->sql_string_concat('map_flagtypes.name', 'map_flags.status'), + undef, undef, 'map_flagtypes.sortkey, map_flagtypes.name'), 'keywords' => $dbh->sql_group_concat('DISTINCT map_keyworddefs.name'), + + blocked => $dbh->sql_group_concat('DISTINCT map_blocked.blocked'), + dependson => $dbh->sql_group_concat('DISTINCT map_dependson.dependson'), 'longdescs.count' => 'COUNT(DISTINCT map_longdescs_count.comment_id)', tag => $dbh->sql_group_concat('DISTINCT map_tag.name'), + last_visit_ts => 'bug_user_last_visit.last_visit_ts', ); # Backward-compatibility for old field names. Goes new_name => old_name. @@ -635,12 +687,7 @@ sub REPORT_COLUMNS { # or simply don't work with the current reporting system. my @no_report_columns = qw(bug_id alias short_short_desc opendate changeddate - flagtypes.name keywords relevance); - - # Multi-select fields are not currently supported. - my @multi_selects = @{Bugzilla->fields( - { obsolete => 0, type => FIELD_TYPE_MULTI_SELECT })}; - push(@no_report_columns, map { $_->name } @multi_selects); + flagtypes.name relevance); # If you're not a time-tracker, you can't use time-tracking # columns. @@ -658,7 +705,10 @@ sub REPORT_COLUMNS { # is here because it *always* goes into the GROUP BY as the first item, # so it should be skipped when determining extra GROUP BY columns. use constant GROUP_BY_SKIP => qw( + alias + blocked bug_id + dependson flagtypes.name keywords longdescs.count @@ -712,7 +762,7 @@ sub data { my @orig_fields = $self->_input_columns; my $all_in_bugs_table = 1; foreach my $field (@orig_fields) { - next if $self->COLUMNS->{$field}->{name} =~ /^bugs\.\w+$/; + next if ($self->COLUMNS->{$field}->{name} // $field) =~ /^bugs\.\w+$/; $self->{fields} = ['bug_id']; $all_in_bugs_table = 0; last; @@ -964,10 +1014,16 @@ sub _sql_select { my ($self) = @_; my @sql_fields; foreach my $column ($self->_display_columns) { - my $alias = $column; - # Aliases cannot contain dots in them. We convert them to underscores. - $alias =~ s/\./_/g; - my $sql = $self->COLUMNS->{$column}->{name} . " AS $alias"; + my $sql = $self->COLUMNS->{$column}->{name} // ''; + if ($sql) { + my $alias = $column; + # Aliases cannot contain dots in them. We convert them to underscores. + $alias =~ tr/./_/; + $sql .= " AS $alias"; + } + else { + $sql = $column; + } push(@sql_fields, $sql); } return @sql_fields; @@ -1210,9 +1266,12 @@ sub _standard_joins { push(@joins, $security_join); if ($user->id) { - $security_join->{extra} = - ["NOT (" . $user->groups_in_sql('security_map.group_id') . ")"]; - + # See also _standard_joins for the other half of the below statement + if (!Bugzilla->params->{'or_groups'}) { + $security_join->{extra} = + ["NOT (" . $user->groups_in_sql('security_map.group_id') . ")"]; + } + my $security_cc_join = { table => 'cc', as => 'security_cc', @@ -1286,10 +1345,17 @@ sub _standard_where { # until their group controls are set. So if a bug has a NULL creation_ts, # it shouldn't show up in searches at all. my @where = ('bugs.creation_ts IS NOT NULL'); - - my $security_term = 'security_map.group_id IS NULL'; my $user = $self->_user; + my $security_term = ''; + # See also _standard_joins for the other half of the below statement + if (Bugzilla->params->{'or_groups'}) { + $security_term .= " (security_map.group_id IS NULL OR security_map.group_id IN (" . $user->groups_as_string . "))"; + } + else { + $security_term = 'security_map.group_id IS NULL'; + } + if ($user->id) { my $userid = $user->id; # This indentation makes the resulting SQL more readable. @@ -1334,7 +1400,7 @@ sub _sql_group_by { my @extra_group_by; foreach my $column ($self->_select_columns) { next if $self->_skip_group_by->{$column}; - my $sql = $self->COLUMNS->{$column}->{name}; + my $sql = $self->COLUMNS->{$column}->{name} // $column; push(@extra_group_by, $sql); } @@ -1536,9 +1602,8 @@ sub _special_parse_chfield { sub _special_parse_deadline { my ($self) = @_; - return if !$self->_user->is_timetracker; my $params = $self->_params; - + my $clause = new Bugzilla::Search::Clause(); if (my $from = $params->{'deadlinefrom'}) { $clause->add('deadline', 'greaterthaneq', $from); @@ -1680,6 +1745,8 @@ sub _boolean_charts { my $field = $params->{"field$identifier"}; my $operator = $params->{"type$identifier"}; my $value = $params->{"value$identifier"}; + # no-value operators ignore the value, however a value needs to be set + $value = ' ' if $operator && grep { $_ eq $operator } NO_VALUE_OPERATORS; $or_clause->add($field, $operator, $value); } $and_clause->add($or_clause); @@ -1726,6 +1793,8 @@ sub _custom_search { my $operator = $params->{"o$id"}; my $value = $params->{"v$id"}; + # no-value operators ignore the value, however a value needs to be set + $value = ' ' if $operator && grep { $_ eq $operator } NO_VALUE_OPERATORS; my $condition = condition($field, $operator, $value); $condition->negate($params->{"n$id"}); $current_clause->add($condition); @@ -1755,20 +1824,30 @@ sub _handle_chart { my ($field, $operator, $value) = $condition->fov; return if (!defined $field or !defined $operator or !defined $value); $field = FIELD_MAP->{$field} || $field; - - my $string_value; + + my ($string_value, $orig_value); + state $is_mysql = $dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0; + if (ref $value eq 'ARRAY') { # Trim input and ignore blank values. @$value = map { trim($_) } @$value; @$value = grep { defined $_ and $_ ne '' } @$value; return if !@$value; + $orig_value = join(',', @$value); + if ($field eq 'longdesc' && $is_mysql) { + @$value = map { _convert_unicode_characters($_) } @$value; + } $string_value = join(',', @$value); } else { return if $value eq ''; + $orig_value = $value; + if ($field eq 'longdesc' && $is_mysql) { + $value = _convert_unicode_characters($value); + } $string_value = $value; } - + $self->_chart_fields->{$field} or ThrowCodeError("invalid_field_name", { field => $field }); trick_taint($field); @@ -1812,7 +1891,7 @@ sub _handle_chart { # do_search_function modified them. $self->search_description({ field => $field, type => $operator, - value => $string_value, term => $search_args{term}, + value => $orig_value, term => $search_args{term}, }); foreach my $join (@{ $search_args{joins} }) { @@ -1823,6 +1902,18 @@ sub _handle_chart { $condition->translated(\%search_args); } +# XXX - This is a hack for MySQL which doesn't understand Unicode characters +# above U+FFFF, see Bugzilla::Comment::_check_thetext(). This hack can go away +# once we require MySQL 5.5.3 and use utf8mb4. +sub _convert_unicode_characters { + my $string = shift; + + # Perl 5.13.8 and older complain about non-characters. + no warnings 'utf8'; + $string =~ s/([\x{10000}-\x{10FFFF}])/"\x{FDD0}[" . uc(sprintf('U+%04x', ord($1))) . "]\x{FDD1}"/eg; + return $string; +} + ################################## # do_search_function And Helpers # ################################## @@ -2075,22 +2166,44 @@ sub _word_terms { ##################################### sub _timestamp_translate { - my ($self, $args) = @_; + my ($self, $ignore_time, $args) = @_; my $value = $args->{value}; my $dbh = Bugzilla->dbh; return if $value !~ /^(?:[\+\-]?\d+[hdwmy]s?|now)$/i; - # By default, the time is appended to the date, which we don't want - # for deadlines. $value = SqlifyDate($value); - if ($args->{field} eq 'deadline') { + # By default, the time is appended to the date, which we don't always want. + if ($ignore_time) { ($value) = split(/\s/, $value); } $args->{value} = $value; $args->{quoted} = $dbh->quote($value); } +sub _datetime_translate { + return shift->_timestamp_translate(0, @_); +} + +sub _last_visit_datetime { + my ($self, $args) = @_; + my $value = $args->{value}; + + $self->_datetime_translate($args); + if ($value eq $args->{value}) { + # Failed to translate a datetime. let's try the pronoun expando. + if ($value eq '%last_changed%') { + $self->_add_extra_column('changeddate'); + $args->{value} = $args->{quoted} = 'bugs.delta_ts'; + } + } +} + + +sub _date_translate { + return shift->_timestamp_translate(1, @_); +} + sub SqlifyDate { my ($str) = @_; my $fmt = "%Y-%m-%d %H:%M:%S"; @@ -2178,7 +2291,8 @@ sub pronoun { if ($noun eq "%qacontact%") { return "COALESCE(bugs.qa_contact,0)"; } - return 0; + + ThrowUserError('illegal_pronoun', { pronoun => $noun }); } sub _contact_pronoun { @@ -2354,7 +2468,7 @@ sub _user_nonchanged { # For negative operators, the system we're using here # only works properly if we reverse the operator and check IS NULL # in the WHERE. - my $is_negative = $operator =~ /^no/ ? 1 : 0; + my $is_negative = $operator =~ /^(?:no|isempty)/ ? 1 : 0; if ($is_negative) { $args->{operator} = $self->_reverse_operator($operator); } @@ -2442,6 +2556,11 @@ sub _long_desc_nonchanged { my ($self, $args) = @_; my ($chart_id, $operator, $value, $joins, $bugs_table) = @$args{qw(chart_id operator value joins bugs_table)}; + + if ($operator =~ /^is(not)?empty$/) { + $args->{term} = $self->_multiselect_isempty($args, $operator eq 'isnotempty'); + return; + } my $dbh = Bugzilla->dbh; my $table = "longdescs_$chart_id"; @@ -2583,6 +2702,21 @@ sub _percentage_complete { $self->_add_extra_column('actual_time'); } +sub _last_visit_ts { + my ($self, $args) = @_; + + $args->{full_field} = $self->COLUMNS->{last_visit_ts}->{name}; + $self->_add_extra_column('last_visit_ts'); +} + +sub _last_visit_ts_invalid_operator { + my ($self, $args) = @_; + + ThrowUserError('search_field_operator_invalid', + { field => $args->{field}, + operator => $args->{operator} }); +} + sub _days_elapsed { my ($self, $args) = @_; my $dbh = Bugzilla->dbh; @@ -2612,6 +2746,15 @@ sub _product_nonchanged { "products.id", "products", $term); } +sub _alias_nonchanged { + my ($self, $args) = @_; + + $args->{full_field} = "bugs_aliases.alias"; + $self->_do_operator_function($args); + $args->{term} = build_subselect("bugs.bug_id", + "bugs_aliases.bug_id", "bugs_aliases", $args->{term}); +} + sub _classification_nonchanged { my ($self, $args) = @_; my $joins = $args->{joins}; @@ -2646,6 +2789,13 @@ sub _nullable_datetime { $args->{full_field} = "COALESCE($field, $empty)"; } +sub _nullable_date { + my ($self, $args) = @_; + my $field = $args->{full_field}; + my $empty = Bugzilla->dbh->quote(EMPTY_DATE); + $args->{full_field} = "COALESCE($field, $empty)"; +} + sub _deadline { my ($self, $args) = @_; my $field = $args->{full_field}; @@ -2734,6 +2884,12 @@ sub _flagtypes_nonchanged { my ($self, $args) = @_; my ($chart_id, $operator, $value, $joins, $bugs_table, $condition) = @$args{qw(chart_id operator value joins bugs_table condition)}; + + if ($operator =~ /^is(not)?empty$/) { + $args->{term} = $self->_multiselect_isempty($args, $operator eq 'isnotempty'); + return; + } + my $dbh = Bugzilla->dbh; # For 'not' operators, we need to negate the whole term. @@ -2838,10 +2994,12 @@ sub _multiselect_table { return "attachments INNER JOIN attach_data " . " ON attachments.attach_id = attach_data.id" } - elsif ($field eq 'flagtypes.name') { - $args->{full_field} = $dbh->sql_string_concat("flagtypes.name", - "flags.status"); - return "flags INNER JOIN flagtypes ON flags.type_id = flagtypes.id"; + elsif ($field eq 'comment_tag') { + $args->{_extra_where} = " AND longdescs.isprivate = 0" + if !$self->_user->is_insider; + $args->{full_field} = 'longdescs_tags.tag'; + return "longdescs INNER JOIN longdescs_tags". + " ON longdescs.comment_id = longdescs_tags.comment_id"; } my $table = "bug_$field"; $args->{full_field} = "bug_$field.value"; @@ -2850,6 +3008,11 @@ sub _multiselect_table { sub _multiselect_term { my ($self, $args, $not) = @_; + my ($operator) = $args->{operator}; + my $value = $args->{value} || ''; + # 'empty' operators require special handling + return $self->_multiselect_isempty($args, $not) + if ($operator =~ /^is(not)?empty$/ || $value eq '---'); my $table = $self->_multiselect_table($args); $self->_do_operator_function($args); my $term = $args->{term}; @@ -2858,6 +3021,125 @@ sub _multiselect_term { return build_subselect("$args->{bugs_table}.bug_id", $select, $table, $term, $not); } +# We can't use the normal operator_functions to build isempty queries which +# join to different tables. +sub _multiselect_isempty { + my ($self, $args, $not) = @_; + my ($field, $operator, $joins, $chart_id) = @$args{qw(field operator joins chart_id)}; + my $dbh = Bugzilla->dbh; + $operator = $self->_reverse_operator($operator) if $not; + $not = $operator eq 'isnotempty' ? 'NOT' : ''; + + if ($field eq 'keywords') { + push @$joins, { + table => 'keywords', + as => "keywords_$chart_id", + from => 'bug_id', + to => 'bug_id', + }; + return "keywords_$chart_id.bug_id IS $not NULL"; + } + elsif ($field eq 'bug_group') { + push @$joins, { + table => 'bug_group_map', + as => "bug_group_map_$chart_id", + from => 'bug_id', + to => 'bug_id', + }; + return "bug_group_map_$chart_id.bug_id IS $not NULL"; + } + elsif ($field eq 'flagtypes.name') { + push @$joins, { + table => 'flags', + as => "flags_$chart_id", + from => 'bug_id', + to => 'bug_id', + }; + return "flags_$chart_id.bug_id IS $not NULL"; + } + elsif ($field eq 'blocked' or $field eq 'dependson') { + my $to = $field eq 'blocked' ? 'dependson' : 'blocked'; + push @$joins, { + table => 'dependencies', + as => "dependencies_$chart_id", + from => 'bug_id', + to => $to, + }; + return "dependencies_$chart_id.$to IS $not NULL"; + } + elsif ($field eq 'longdesc') { + my @extra = ( "longdescs_$chart_id.type != " . CMT_HAS_DUPE ); + push @extra, "longdescs_$chart_id.isprivate = 0" + unless $self->_user->is_insider; + push @$joins, { + table => 'longdescs', + as => "longdescs_$chart_id", + from => 'bug_id', + to => 'bug_id', + extra => \@extra, + }; + return $not + ? "longdescs_$chart_id.thetext != ''" + : "longdescs_$chart_id.thetext = ''"; + } + elsif ($field eq 'longdescs.isprivate') { + ThrowUserError('search_field_operator_invalid', { field => $field, + operator => $operator }); + } + elsif ($field =~ /^attachments\.(.+)/) { + my $sub_field = $1; + if ($sub_field eq 'description' || $sub_field eq 'filename' || $sub_field eq 'mimetype') { + # can't be null/empty + return $not ? '1=1' : '1=2'; + } else { + # all other fields which get here are boolean + ThrowUserError('search_field_operator_invalid', { field => $field, + operator => $operator }); + } + } + elsif ($field eq 'attach_data.thedata') { + push @$joins, { + table => 'attachments', + as => "attachments_$chart_id", + from => 'bug_id', + to => 'bug_id', + extra => [ $self->_user->is_insider ? '' : "attachments_$chart_id.isprivate = 0" ], + }; + push @$joins, { + table => 'attach_data', + as => "attach_data_$chart_id", + from => "attachments_$chart_id.attach_id", + to => 'id', + }; + return "attach_data_$chart_id.thedata IS $not NULL"; + } + elsif ($field eq 'tag') { + push @$joins, { + table => 'bug_tag', + as => "bug_tag_$chart_id", + from => 'bug_id', + to => 'bug_id', + }; + push @$joins, { + table => 'tag', + as => "tag_$chart_id", + from => "bug_tag_$chart_id.tag_id", + to => 'id', + extra => [ "tag_$chart_id.user_id = " . ($self->_sharer_id || $self->_user->id) ], + }; + return "tag_$chart_id.id IS $not NULL"; + } + elsif ($self->_multi_select_fields->{$field}) { + push @$joins, { + table => "bug_$field", + as => "bug_${field}_$chart_id", + from => 'bug_id', + to => 'bug_id', + }; + return "bug_${field}_$chart_id.bug_id IS $not NULL"; + } +} + ############################### # Standard Operator Functions # ############################### @@ -3074,6 +3356,27 @@ sub _changed_security_check { } } +sub _isempty { + my ($self, $args) = @_; + my $full_field = $args->{full_field}; + $args->{term} = "$full_field IS NULL OR $full_field = " . $self->_empty_value($args->{field}); +} + +sub _isnotempty { + my ($self, $args) = @_; + my $full_field = $args->{full_field}; + $args->{term} = "$full_field IS NOT NULL AND $full_field != " . $self->_empty_value($args->{field}); +} + +sub _empty_value { + my ($self, $field) = @_; + my $field_obj = $self->_chart_fields->{$field}; + return "0" if $field_obj->type == FIELD_TYPE_BUG_ID; + return Bugzilla->dbh->quote(EMPTY_DATETIME) if $field_obj->type == FIELD_TYPE_DATETIME; + return Bugzilla->dbh->quote(EMPTY_DATE) if $field_obj->type == FIELD_TYPE_DATE; + return "''"; +} + ###################### # Public Subroutines # ###################### @@ -3177,7 +3480,7 @@ value for this field. At least one search criteria must be defined if the =item C<sharer> -When a saved search is shared by a user, this is his user ID. +When a saved search is shared by a user, this is their user ID. =item C<user> @@ -3228,3 +3531,35 @@ two hashes if two SQL queries have been executed sequentially to get all the required data. =back + +=head1 B<Methods in need of POD> + +=over + +=item invalid_order_columns + +=item COLUMN_JOINS + +=item split_order_term + +=item SqlifyDate + +=item REPORT_COLUMNS + +=item pronoun + +=item COLUMNS + +=item order + +=item search_description + +=item IsValidQueryType + +=item build_subselect + +=item do_search_function + +=item boolean_charts_to_custom_search + +=back |