* Add enclair_db option to UserDB.pm. Allows logging of enclair password
[interchange.git] / lib / Vend / DbSearch.pm
1 # Vend::DbSearch - Search indexes with Interchange
2 #
3 # $Id: DbSearch.pm,v 2.27 2008-09-06 05:22:56 mheins Exp $
4 #
5 # Adapted for use with Interchange from Search::TextSearch
6 #
7 # Copyright (C) 2002-2007 Interchange Development Group
8 # Copyright (C) 1996-2002 Red Hat, Inc.
9 #
10 # This program is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
14 #
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 # GNU General Public License for more details.
19 #
20 # You should have received a copy of the GNU General Public
21 # License along with this program; if not, write to the Free
22 # Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
23 # MA  02110-1301  USA.
24
25 package Vend::DbSearch;
26 require Vend::Search;
27
28 @ISA = qw(Vend::Search);
29
30 $VERSION = substr(q$Revision: 2.27 $, 10);
31
32 use Search::Dict;
33 use strict;
34 no warnings qw(uninitialized numeric);
35
36 sub array {
37         my ($s, $opt) = @_;
38         $s->{mv_one_sql_table} = 1;
39         $s->{mv_list_only} = 1; # makes perform_search only return results array
40         return Vend::Scan::perform_search($opt, undef, $s);
41 }
42
43 sub hash {
44         my ($s, $opt) = @_;
45         $s->{mv_return_reference} = 'HASH';
46         $s->{mv_one_sql_table} = 1;
47         $s->{mv_list_only} = 1; # makes perform_search only return results array
48         return Vend::Scan::perform_search($opt, undef, $s);
49 }
50
51 sub list {
52         my ($s, $opt) = @_;
53         $s->{mv_return_reference} = 'LIST';
54         $s->{mv_one_sql_table} = 1;
55         $s->{mv_list_only} = 1; # makes perform_search only return results array
56         return Vend::Scan::perform_search($opt, undef, $s);
57 }
58
59 my %Default = (
60         matches                 => 0,
61         mv_head_skip            => 0,
62         mv_index_delim          => "\t",
63         mv_matchlimit           => 50,
64         mv_min_string           => 1,
65         verbatim_columns        => 1,
66 );
67
68 sub init {
69         my ($s, $options) = @_;
70
71         # autovivify references of nested data structures we use below, since they
72         # don't yet exist at daemon startup time before configuration is done
73         $Vend::Cfg->{ProductFiles}[0] or 1;
74         $::Variable->{MV_DEFAULT_SEARCH_TABLE} or 1;
75
76         @{$s}{keys %Default} = (values %Default);
77         $s->{mv_all_chars}              = [1];
78         
79         ### This is a bit of a misnomer, for really it is the base table
80         ### that we will use if no base=table param is specified
81         $s->{mv_base_directory}     = $Vend::Cfg->{ProductFiles}[0];
82         $s->{mv_begin_string}       = [];
83         $s->{mv_case}               = [];
84         $s->{mv_column_op}          = [];
85         $s->{mv_negate}             = [];
86         $s->{mv_numeric}            = [];
87         $s->{mv_orsearch}           = [];
88         $s->{mv_search_field}       = [];
89         $s->{mv_search_group}       = [];
90         $s->{mv_searchspec}         = [];
91         $s->{mv_sort_option}        = [];
92         $s->{mv_substring_match}    = [];
93
94         for(keys %$options) {
95                 $s->{$_} = $options->{$_};
96         }
97         $s->{mv_search_file}        =   [ @{
98                                                                                 $::Variable->{MV_DEFAULT_SEARCH_TABLE}
99                                                                                 ||      $Vend::Cfg->{ProductFiles}
100                                                                                 } ]
101                 unless ref($s->{mv_search_file}) and scalar(@{$s->{mv_search_file}});
102
103         return;
104 }
105
106 sub new {
107         my ($class, %options) = @_;
108         my $s = new Vend::Search;
109         bless $s, $class;
110 #::logDebug("mv_search_file initted=" . ::uneval($options{mv_search_file}));
111         $s->init(\%options);
112 #::logDebug("mv_search_file now=" . ::uneval($s->{mv_search_file}));
113         return $s;
114 }
115
116 sub search {
117         my($s,%options) = @_;
118
119         my(@out);
120         my($limit_sub,$return_sub,$delayed_return);
121         my($f,$key,$val);
122         my($searchfile,@searchfiles);
123         my(@specs);
124         my(@pats);
125
126         while (($key,$val) = each %options) {
127                 $s->{$key} = $val;
128         }
129
130         $s->{mv_return_delim} = $s->{mv_index_delim}
131                 unless defined $s->{mv_return_delim};
132
133         @searchfiles = @{$s->{mv_search_file}};
134
135         for(@searchfiles) {
136                 s:.*/::;
137                 s/\..*//;
138         }
139         my $dbref = $s->{table} || undef;
140
141         if( ! $dbref ) {
142                 $s->{dbref} = $dbref = Vend::Data::database_exists_ref($searchfiles[0]);
143         }
144         if(! $dbref) {
145                 return $s->search_error(
146                         "search file '$searchfiles[0]' is not a valid database reference."
147                         );
148         }
149         $s->{dbref} = $dbref;
150
151         my (@fn) = $dbref->columns();
152
153 #::logDebug("specs=" . ::uneval($s->{mv_searchspec}));
154         @specs = @{$s->{mv_searchspec}};
155
156
157         if(ref $s->{mv_like_field} and ref $s->{mv_like_spec}) {
158                 my $ary = [];
159                 for(my $i = 0; $i < @{$s->{mv_like_field}}; $i++) {
160                         my $col = $s->{mv_like_field}[$i];
161                         next unless length($col);
162                         my $val = $s->{mv_like_spec}[$i];
163                         length($val) or next;
164                         next unless defined $dbref->test_column($col);
165                         $val = $dbref->quote("$val%");
166                         if(
167                                 ! $dbref->config('UPPER_COMPARE')
168                                         or 
169                                 $s->{mv_case_sensitive} and $s->{mv_case_sensitive}[0]
170                                 )
171                         {
172                                 push @$ary, "$col like $val";
173                         }
174                         else {
175                                 $val = uc $val;
176                                 push @$ary, "UPPER($col) like $val";
177                         }
178                 }
179                 if(@$ary) {
180                         $s->{eq_specs_sql} = [] if ! $s->{eq_specs_sql};
181                         push @{$s->{eq_specs_sql}}, @$ary;
182                 }
183         }
184
185         # pass mv_min_string check if a valid pair of mv_like_field
186         # and mv_like_spec has been specified
187         
188         my $min_string = $s->{mv_min_string};
189
190         if ($s->{eq_specs_sql}) {
191                 $s->{mv_min_string} = 0;
192         }
193         
194         @pats = $s->spec_check(@specs);
195
196         $s->{mv_min_string} = $min_string;
197         
198 #::logDebug("specs now=" . ::uneval(\@pats));
199
200         if ($s->{mv_search_error}) {
201                 return $s;
202         }
203         
204         if ($s->{mv_coordinate}) {
205                 undef $f;
206         }
207         elsif ($s->{mv_return_all}) {
208                 $f = sub {1};
209         }
210         elsif ($s->{mv_orsearch}[0]) {
211                 eval {$f = $s->create_search_or(
212                                                                         $s->get_scalar(
213                                                                                         qw/mv_case mv_substring_match mv_negate/
214                                                                                         ),
215                                                                                 @pats                                   )};
216         }
217         else  { 
218                 eval {$f = $s->create_search_and(
219                                                                         $s->get_scalar(
220                                                                                         qw/mv_case mv_substring_match mv_negate/
221                                                                                         ),
222                                                                                 @pats                                   )};
223         }
224
225         $@  and  return $s->search_error("Function creation: $@");
226
227         my $qual;
228         if($s->{eq_specs_sql}) {
229                 $qual = ' WHERE ';
230                 my $joiner = $s->{mv_orsearch}[0] ? ' OR ' : ' AND ';
231                 $qual .= join $joiner, @{$s->{eq_specs_sql}};
232         }
233
234         $s->save_specs();
235
236         # set max_matches based on the lower of the pragma & the search parameter
237         # (so end-users can further restrict the size of the result set, but not increase it)
238         my $max_matches;
239         {
240                 no warnings 'uninitialized';
241                 $max_matches = $::Pragma->{max_matches};
242                 undef $max_matches if $max_matches < 1;
243                 my $search_mm = $s->{mv_max_matches};
244                 $max_matches = $search_mm
245                         if $search_mm > 0 and (!$max_matches or $search_mm < $max_matches);
246         }
247
248         SEARCHFILE: for $searchfile (@searchfiles) {
249                 my $lqual = $qual || '';
250                 $searchfile =~ s/\..*//;
251                 my $db;
252                 if (! $s->{mv_one_sql_table} ) {
253                         $db = Vend::Data::database_exists_ref($searchfile)
254                                 or ::logError(
255                                                         "Attempt to search non-existent database %s",
256                                                         $searchfile,
257                                                 ), next;
258                         
259                         $dbref = $s->{dbref} = $db->ref();
260                         $dbref->reset();
261                         @fn = $dbref->columns();
262                 }
263
264                 if(! $s->{mv_no_hide} and my $hf = $dbref->config('HIDE_FIELD')) {
265 #::logDebug("found hide_field $hf");
266                         my $ss = $dbref->quote(1, $hf);
267                         $lqual =~ s/^\s*WHERE\s+/ WHERE $hf <> $ss AND /
268                                 or $lqual = " WHERE $hf <> $ss";
269 #::logDebug("lqual now '$lqual'");
270                 }
271                 $s->hash_fields(\@fn);
272                 my $prospect;
273                 eval {
274                         ($limit_sub, $prospect) = $s->get_limit($f);
275                 };
276
277                 $@  and  return $s->search_error("Limit subroutine creation: $@");
278
279                 $f = $prospect if $prospect;
280
281                 eval {($return_sub, $delayed_return) = $s->get_return()};
282
283                 $@  and  return $s->search_error("Return subroutine creation: $@");
284
285                 if(! defined $f and defined $limit_sub) {
286 #::logDebug("no f, limit, dbref=$dbref");
287                         local($_);
288                         my $ref;
289                         while($ref = $dbref->each_nokey($lqual) ) {
290                                 next unless $limit_sub->($ref);
291                                 push @out, $return_sub->($ref);
292                                 last SEARCHFILE if $max_matches and @out >= $max_matches;
293                         }
294                 }
295                 elsif(defined $limit_sub) {
296 #::logDebug("f and limit, dbref=$dbref");
297                         local($_);
298                         my $ref;
299                         while($ref = $dbref->each_nokey($lqual) ) {
300                                 $_ = join "\t", @$ref;
301                                 next unless &$f();
302                                 next unless $limit_sub->($ref);
303                                 push @out, $return_sub->($ref);
304                                 last SEARCHFILE if $max_matches and @out >= $max_matches;
305                         }
306                 }
307                 elsif (!defined $f) {
308                         return $s->search_error('No search definition');
309                 }
310                 else {
311 #::logDebug("f and no limit, dbref=$dbref");
312                         local($_);
313                         my $ref;
314                         while($ref = $dbref->each_nokey($lqual) ) {
315 #::logDebug("f and no limit, ref=$ref");
316                                 $_ = join "\t", @$ref;
317                                 next unless &$f();
318                                 push @out, $return_sub->($ref);
319                                 last SEARCHFILE if $max_matches and @out >= $max_matches;
320                         }
321                 }
322                 $s->restore_specs();
323         }
324
325         # Search the results and return
326         if($s->{mv_next_search}) {
327                 @out = $s->search_reference(\@out);
328 #::logDebug("did next_search: " . ::uneval(\@out));
329         }
330
331         $s->{matches} = scalar(@out);
332
333 #::logDebug("before delayed return: self=" . ::Vend::Util::uneval_it({%$s}));
334
335         if($delayed_return and $s->{matches} > 0) {
336                 $s->hash_fields($s->{mv_field_names}, qw/mv_sort_field/);
337                 $s->sort_search_return(\@out);
338                 $delayed_return = $s->get_return(1);
339                 @out = map { $delayed_return->($_) } @out;
340         }
341 #::logDebug("after delayed return: self=" . ::Vend::Util::uneval({%$s}));
342
343         if($s->{mv_unique}) {
344                 my %seen;
345                 @out = grep ! $seen{$_->[0]}++, @out;
346         }
347
348         splice @out, $max_matches if $max_matches;
349
350         $s->{matches} = scalar(@out);
351
352         if ($s->{matches} > $s->{mv_matchlimit} and $s->{mv_matchlimit} > 0) {
353                 $s->save_more(\@out)
354                         or ::logError("Error saving matches: $!");
355                 if ($s->{mv_first_match}) {
356                         splice(@out,0,$s->{mv_first_match}) if $s->{mv_first_match};
357                         $s->{mv_next_pointer} = $s->{mv_first_match} + $s->{mv_matchlimit};
358                         $s->{mv_next_pointer} = 0
359                                 if $s->{mv_next_pointer} > $s->{matches};
360                 }
361                 elsif ($s->{mv_start_match}) {
362                         my $comp = $s->{mv_start_match};
363                         my $i = -1;
364                         my $found;
365                         for(@out) {
366                                 $i++;
367                                 next unless $_->[0] eq $comp;
368                                 $found = $i;
369                                 last;
370                         }
371                         if(! $found and $s->{mv_numeric}[0]) {
372                                 for(@out) {
373                                         $i++;
374                                         next unless $_->[0] >= $comp;
375                                         $found = $i;
376                                         last;
377                                 }
378                         }
379                         elsif (! $found) {
380                                 for(@out) {
381                                         $i++;
382                                         next unless $_->[0] ge $comp;
383                                         $found = $i;
384                                         last;
385                                 }
386                         }
387                         if($found) {
388                                 splice(@out,0,$found);
389                                 $s->{mv_first_match} = $found;
390                                 $s->{mv_next_pointer} = $found + $s->{mv_matchlimit};
391                                 $s->{mv_next_pointer} = 0
392                                         if $s->{mv_next_pointer} > $s->{matches};
393                         }
394                 }
395                 $#out = $s->{mv_matchlimit} - 1;
396         }
397 #::logDebug("after hash fields: self=" . ::Vend::Util::uneval_it({%$s}));
398
399         if(! $s->{mv_return_reference}) {
400                 $s->{mv_results} = \@out;
401         }
402         elsif($s->{mv_return_reference} eq 'LIST') {
403                 @out = map { join $s->{mv_return_delim}, @$_ } @out;
404                 $s->{mv_results} = join $s->{mv_record_delim}, @out;
405         }
406         else {
407                 my @names;
408                 @names = @{ $s->{mv_field_names} }[ @{$s->{mv_return_fields}} ];
409                 $names[0] eq '0' and $names[0] = 'code';
410                 my @ary;
411                 for (@out) {
412                         my $h = {};
413                         @{ $h } {@names} = @$_;
414                         push @ary, $h;
415                 }
416                 $s->{mv_results} = \@ary;
417         }
418         return $s;
419 }
420
421 # Unfortunate hack need for Safe searches
422 *create_search_and  = \&Vend::Search::create_search_and;
423 *create_search_or   = \&Vend::Search::create_search_or;
424 *dump_options       = \&Vend::Search::dump_options;
425 *escape             = \&Vend::Search::escape;
426 *get_limit          = \&Vend::Search::get_limit;
427 *get_return         = \&Vend::Search::get_return;
428 *get_scalar         = \&Vend::Search::get_scalar;
429 *hash_fields        = \&Vend::Search::hash_fields;
430 *map_ops            = \&Vend::Search::map_ops;
431 *more_matches       = \&Vend::Search::more_matches;
432 *range_check        = \&Vend::Search::range_check;
433 *restore_specs      = \&Vend::Search::restore_specs;
434 *save_context       = \&Vend::Search::save_context;
435 *save_more          = \&Vend::Search::save_more;
436 *save_specs         = \&Vend::Search::save_specs;
437 *saved_params       = \&Vend::Search::saved_params;
438 *search_error       = \&Vend::Search::search_error;
439 *sort_search_return = \&Vend::Search::sort_search_return;
440 *spec_check         = \&Vend::Search::spec_check;
441 *splice_specs       = \&Vend::Search::splice_specs;
442
443 1;
444 __END__