File Coverage

blib/lib/Search/QueryParser/SQL.pm
Criterion Covered Total %
statement 24 94 25.5
branch 0 46 0.0
condition 0 24 0.0
subroutine 8 13 61.5
pod 4 4 100.0
total 36 181 19.8


line stmt bran cond sub pod time code
1             package Search::QueryParser::SQL;
2 1     1   26105 use warnings;
  1         2  
  1         33  
3 1     1   5 use strict;
  1         2  
  1         33  
4 1     1   5 use Carp;
  1         5  
  1         86  
5 1     1   5 use base qw( Search::QueryParser );
  1         2  
  1         1321  
6 1     1   16586 use Data::Dump qw( dump );
  1         15760  
  1         100  
7 1     1   827 use Search::QueryParser::SQL::Query;
  1         8  
  1         77  
8 1     1   1807 use Search::QueryParser::SQL::Column;
  1         3  
  1         11  
9 1     1   43 use Scalar::Util qw( blessed );
  1         2  
  1         1728  
10              
11             our $VERSION = '0.010';
12              
13             my $debug = $ENV{PERL_DEBUG} || 0;
14              
15             =head1 NAME
16              
17             Search::QueryParser::SQL - turn free-text queries into SQL WHERE clauses
18              
19             =head1 SYNOPSIS
20              
21             use Search::QueryParser::SQL;
22             my $parser = Search::QueryParser::SQL->new(
23             columns => [qw( first_name last_name email )]
24             );
25            
26             my $query = $parser->parse('joe smith', 1); # 1 for explicit AND
27             print $query;
28             # prints:
29             # (first_name='joe' OR last_name='joe' OR email='joe') AND \
30             # (first_name='smith' OR last_name='smith' OR email='smith')
31            
32             # for the DBI
33             my $query = $parser->parse('foo');
34             print $query->dbi->[0];
35             # prints
36             # (first_name=? OR last_name=? OR email=?)
37            
38             # wildcard support
39             my $query = $parser->parse('foo*');
40             print $query;
41             # prints
42             # (first_name ILIKE 'foo%' OR last_name ILIKE 'foo%' OR email ILIKE 'foo%')
43              
44              
45             =head1 DESCRIPTION
46              
47             Search::QueryParser::SQL is a subclass of Search::QueryParser.
48             Chiefly it extends the unparse() method to stringify free-text
49             search queries as valid SQL WHERE clauses.
50              
51             The idea is to allow you to treat your database like a free-text
52             search index, when it really isn't.
53            
54             =head1 METHODS
55              
56             Only new or overridden method are documented here.
57              
58             =cut
59              
60             =head2 new( I )
61              
62             Returns a new Parser. In addition to the I documented
63             in Search::QueryParser, this new() method supports additional
64             I:
65              
66             =over
67              
68             =item columns
69              
70             B
71              
72             May be a hash or array ref of column names. If a hash ref,
73             the keys should be column names and the values either the column type
74             (e.g., int, varchar, etc.) or a hashref of attributes used to
75             instantiate a Search::QueryParser::SQL::Column object.
76              
77             The values are used for determining correct quoting in strings
78             and for operator selection with wildcards.
79             If passed as an array ref, all column arguments will be
80             treated like 'char'.
81              
82             See Search::QueryParser::SQL::Column for more information.
83              
84             =item default_column
85              
86             I
87              
88             The column name or names to be used when no explicit column name is
89             used in a query string. If not present, defaults to I.
90              
91             =item quote_columns
92              
93             I
94              
95             The default behaviour is to not quote column names, but some SQL
96             dialects expect column names to be quoted (escaped).
97              
98             Set this arg to a quote value. Example:
99              
100             my $parser = Search::QueryParser::SQL->new(
101             columns => [qw( foo bar )],
102             quote_columns => '`'
103             );
104             # query will look like `foo` and `bar`
105              
106             =item fuzzify
107              
108             I
109              
110             Treat all query keywords as if they had wildcards attached to the end.
111             E.g., C would be treated like C.
112              
113             =item fuzzify2
114              
115             I
116              
117             Like fuzzify but prepend wildcards as well. E.g., C would be treated
118             like C<*foo*>.
119              
120             =item strict
121              
122             I
123              
124             Croak if any of the column names in I are not among the supplied
125             column names in I.
126              
127             =item like
128              
129             I
130              
131             The SQL operator to use for wildcard query strings. The default is
132             C.
133              
134             =item lower
135              
136             I
137              
138             Wrap the C function around column names for case-insensitive comparison.
139              
140             =item column_class
141              
142             I
143              
144             The name of the class to bless Column objects into. Default is
145             C.
146              
147             =back
148              
149             =cut
150              
151             sub new {
152 0     0 1   my $self = shift->SUPER::new(
153             @_,
154              
155             # add the dot for table.column
156             'rxField' => qr/[\.\w]+/,
157              
158             # make and/or/not case insensitive
159             'rxAnd' => qr/AND|ET|UND|E/i,
160             'rxOr' => qr/OR|OU|ODER|O/i,
161             'rxNot' => qr/NOT|PAS|NICHT|NON/i,
162             );
163 0 0         my $args = ref $_[0] eq 'HASH' ? $_[0] : {@_};
164 0   0       $self->{quote_columns} = delete $args->{quote_columns} || '';
165 0   0       $self->{fuzzify} = delete $args->{fuzzify} || 0;
166 0   0       $self->{fuzzify2} = delete $args->{fuzzify2} || 0;
167 0   0       $self->{strict} = delete $args->{strict} || 0;
168 0   0       $self->{like} = delete $args->{like} || 'ILIKE';
169 0   0       $self->{lower} = delete $args->{lower} || 0;
170 0   0       $self->{column_class} = delete $args->{column_class}
171             || 'Search::QueryParser::SQL::Column';
172              
173 0 0         my $cols = delete $args->{columns} or croak "columns required";
174 0           $self->_set_columns($cols);
175              
176             $self->{default_column} = delete $args->{default_column}
177 0   0       || [ sort keys %{ $self->{columns} } ];
178              
179 0 0         if ( !ref( $self->{default_column} ) ) {
180 0           $self->{default_column} = [ $self->{default_column} ];
181             }
182              
183 0 0         dump $self if $debug;
184              
185 0           return $self;
186             }
187              
188             sub _set_columns {
189 0     0     my $self = shift;
190 0 0         my $cols = shift or croak "columns required";
191 0           my %columns;
192 0           my $colclass = $self->{column_class};
193              
194 0           my $reftype = ref($cols);
195 0 0 0       if ( !$reftype or ( $reftype ne 'ARRAY' and $reftype ne 'HASH' ) ) {
      0        
196 0           croak "columns must be an ARRAY or HASH ref";
197             }
198              
199             # convert simple array to hash
200 0 0         if ( $reftype eq 'ARRAY' ) {
    0          
201 0           %columns = map {
202 0           $_ => $colclass->new(
203             type => 'char',
204             name => $_,
205             fuzzy_op => $self->{like},
206             fuzzy_not_op => 'NOT ' . $self->{like},
207             )
208             } @$cols;
209             }
210             elsif ( $reftype eq 'HASH' ) {
211 0           for my $name ( keys %$cols ) {
212 0           my $val = $cols->{$name};
213 0           my $obj;
214 0 0         if ( blessed($val) ) {
    0          
    0          
215 0           $obj = $val;
216             }
217             elsif ( ref($val) eq 'HASH' ) {
218 0           $obj = $colclass->new($val);
219             }
220             elsif ( !ref $val ) {
221 0           $obj = $colclass->new( name => $name, type => $val );
222 0 0         $obj->fuzzy_op( $self->{like} ) if !$obj->is_int;
223 0 0         $obj->fuzzy_not_op( 'NOT ' . $self->{like} ) if !$obj->is_int;
224             }
225             else {
226 0           croak
227             "column value for $name must be a column type, hashref or Column object";
228             }
229 0           $columns{$name} = $obj;
230             }
231             }
232              
233             # normalize everything
234 0           for my $name ( keys %columns ) {
235 0           my $column = $columns{$name};
236              
237             # set the alias as if it were a real column.
238 0 0         if ( defined $column->alias ) {
239             my @aliases
240 0           = ref $column->alias
241 0 0         ? @{ $column->alias }
242             : ( $column->alias );
243 0           for my $al (@aliases) {
244 0           $columns{$al} = $column;
245             }
246             }
247              
248             # shortcut for lookup
249 0           $self->{_is_int}->{$name} = $column->is_int;
250             }
251              
252 0           $self->{columns} = \%columns;
253 0           return $self->{columns};
254             }
255              
256             =head2 parse( I [, I] )
257              
258             Acts like parse() method in Search::QueryParser, but
259             returns a Search::QueryParser::SQL::Query object.
260              
261             If a second, true, value is passed as I,
262             the query is assumed to "AND" terms together. The default
263             is to "OR" them together.
264              
265             =cut
266              
267             sub parse {
268 0     0 1   my $self = shift;
269 0 0         my $query = $self->SUPER::parse(@_)
270             or croak "query parse failed: " . $self->err;
271              
272 0 0         if ( $self->{strict} ) {
273 0           for my $key ( keys %$query ) {
274 0 0         next unless defined $query->{$key};
275 0           for my $subq ( @{ $query->{$key} } ) {
  0            
276 0 0         next unless $subq->{field};
277 0 0         unless ( exists $self->{columns}->{ $subq->{field} } ) {
278 0           croak "invalid column name: $subq->{field}";
279             }
280             }
281             }
282             }
283              
284 0           $query->{_parser} = $self;
285 0           $query->{_string} = $_[0];
286 0   0       $query->{_implicit_AND} = $_[1] || 0;
287              
288             #dump $query;
289 0           return bless( $query, 'Search::QueryParser::SQL::Query' );
290             }
291              
292             =head2 columns
293              
294             Get/set the column descriptions, which is a hashref of
295             Search::QueryParser::SQL::Column objects keyed by the column name.
296              
297             =cut
298              
299             sub columns {
300 0     0 1   my $self = shift;
301 0 0         if (@_) {
302 0           $self->_set_columns(shift);
303             }
304 0           return $self->{columns};
305             }
306              
307             =head2 get_column( I )
308              
309             Returns the Column object for I or croaks if it has not been defined.
310              
311             =cut
312              
313             sub get_column {
314 0     0 1   my $self = shift;
315 0 0         my $name = shift or croak "column name required";
316 0 0         if ( !exists $self->{columns}->{$name} ) {
317 0           croak "column $name not defined";
318             }
319 0           return $self->{columns}->{$name};
320             }
321              
322             1;
323              
324             __END__