File Coverage

blib/lib/Search/QueryParser/SQL/Query.pm
Criterion Covered Total %
statement 15 197 7.6
branch 0 130 0.0
condition 0 22 0.0
subroutine 5 16 31.2
pod 6 6 100.0
total 26 371 7.0


line stmt bran cond sub pod time code
1             package Search::QueryParser::SQL::Query;
2 1     1   7 use strict;
  1         2  
  1         42  
3 1     1   6 use warnings;
  1         2  
  1         34  
4 1     1   6 use Carp;
  1         2  
  1         69  
5 1     1   6 use Data::Dump qw( dump );
  1         2  
  1         46  
6              
7 1     1   5 use overload '""' => 'stringify', 'fallback' => 1;
  1         2  
  1         9  
8              
9             our $VERSION = '0.010';
10              
11             my $debug = $ENV{PERL_DEBUG} || 0;
12              
13             =head1 NAME
14              
15             Search::QueryParser::SQL::Query - query object
16              
17             =head1 SYNOPSIS
18              
19             # see Search::QueryParser::SQL
20              
21             =head1 DESCRIPTION
22              
23             This class is primarily for unparsing Search::QueryParser
24             data structures into valid SQL.
25            
26             =head1 METHODS
27              
28             Only new or overridden method are documented here.
29              
30             =cut
31              
32             =head2 stringify
33              
34             Returns Query as a string suitable for plugging into a WHERE
35             clause.
36              
37             =cut
38              
39             sub stringify {
40 0     0 1   my $self = shift;
41 0           return $self->_unwind;
42             }
43              
44             my %op_map = (
45             '+' => 'AND',
46             '' => 'OR',
47             '-' => 'AND', # operator is munged
48             );
49              
50             =head2 dbi
51              
52             Like stringify(), but returns array ref of two items:
53             the SQL string and an array ref of values. The SQL string
54             uses the C placeholder as expected by the DBI API.
55              
56             =cut
57              
58             sub dbi {
59 0     0 1   my $self = shift;
60              
61             # set flag temporarily
62 0           $self->{opts}->{delims} = 1;
63              
64 0           my $sql = $self->_unwind;
65 0           my @values;
66 0           my $start = chr(2);
67 0           my $end = chr(3);
68 0           my $opstart = chr(5);
69 0           my $opend = chr(6);
70              
71             # do not need op delims at all
72 0           $sql =~ s/($opstart|$opend)//go;
73              
74 0           while ( $sql =~ s/$start(.+?)$end/\?/o ) {
75 0           push( @values, $1 );
76             }
77              
78 0           delete $self->{opts}->{delims};
79              
80 0           return [ $sql, \@values ];
81             }
82              
83             =head2 pairs
84              
85             Returns array ref of array refs of column/op/value pairs.
86             Note that the logical AND/OR connectors will not be present.
87              
88             =cut
89              
90             sub pairs {
91 0     0 1   my $self = shift;
92              
93 0           my @pairs;
94 0           my $vstart = chr(2);
95 0           my $vend = chr(3);
96 0           my $opstart = chr(5);
97 0           my $opend = chr(6);
98              
99             # set flag temporarily
100 0           $self->{opts}->{delims} = 1;
101 0           my $sql = $self->_unwind;
102              
103 0           while ( $sql =~ m/([\.\w]+)\ ?$opstart(.+?)$opend\ ?$vstart(.+?)$vend/go )
104             {
105 0           push( @pairs, [ $1, $2, $3 ] );
106             }
107              
108 0           delete $self->{opts}->{delims};
109              
110 0           return \@pairs;
111             }
112              
113             =head2 rdbo
114              
115             Returns array ref ready for passing to Rose::DB::Object::Querybuilder
116             build_select() method as the C argument.
117              
118             =cut
119              
120             sub rdbo {
121 0     0 1   my $self = shift;
122              
123 0 0         $debug and warn '=' x 80 . "\n";
124 0 0         $debug and warn "STRING: $self->{_string}\n";
125 0 0         $debug and warn "PARSER: " . dump( $self->{_parser} ) . "\n";
126              
127 0           my $q = $self->_orm;
128              
129 0 0         $debug and warn "rdbo q: " . dump $q;
130              
131 0 0         my $joiner = $self->{_implicit_AND} ? 'AND' : 'OR';
132 0 0         if ( defined $self->{'-'} ) {
133              
134             # no implicit OR with NOT queries
135 0           $joiner = 'AND';
136             }
137              
138 0 0         if ( scalar @$q > 2 ) {
139 0 0         $debug and warn "rdbo \$q > 2, joiner=$joiner";
140 0           return [ $joiner => $q ];
141             }
142             else {
143 0           return $q;
144             }
145             }
146              
147             =head2 dbic
148              
149             Returns array ref ready for passing to DBIx::Class as search query.
150             This is the SQL::Abstract format.
151              
152             =cut
153              
154             sub dbic {
155 0     0 1   my $self = shift;
156              
157 0 0         $debug and warn '=' x 80 . "\n";
158 0 0         $debug and warn "STRING: $self->{_string}\n";
159 0 0         $debug and warn "PARSER: " . dump( $self->{_parser} ) . "\n";
160              
161 0           $self->{opts}->{dbic} = 1;
162              
163 0           my $q = $self->_orm;
164              
165 0 0         $debug and warn "dbic q: " . dump $q;
166              
167 0           delete $self->{opts}->{dbic};
168              
169 0 0         my $joiner = $self->{_implicit_AND} ? '-and' : '-or';
170 0 0         if ( defined $self->{'-'} ) {
171              
172             # no implicit OR with NOT queries
173 0           $joiner = '-and';
174             }
175              
176 0 0         if ( scalar @$q > 2 ) {
177 0 0         $debug and warn "dbic \$q > 2, joiner=$joiner";
178 0           return [ $joiner => $q ];
179             }
180             else {
181 0           return $q;
182             }
183             }
184              
185             =head2 parser
186              
187             Returns the original parser object that generated the query.
188              
189             =cut
190              
191             sub parser {
192 0     0 1   shift->{_parser};
193             }
194              
195             sub _orm {
196 0     0     my $self = shift;
197 0   0       my $q = shift || $self;
198 0           my $query;
199 0 0         my $OR = $self->{opts}->{dbic} ? '-or' : 'OR';
200 0 0         my $AND = $self->{opts}->{dbic} ? '-and' : 'AND';
201 0           for my $prefix ( '+', '', '-' ) {
202 0 0 0       next unless ( defined $q->{$prefix} and @{ $q->{$prefix} } );
  0            
203              
204 0           my $joiner = $op_map{$prefix};
205              
206 0 0         $joiner = '-' . lc($joiner) if $self->{opts}->{dbic};
207              
208 0 0         $debug and warn "prefix '$prefix' ($joiner): " . dump $q->{$prefix};
209              
210 0           my @op_subq;
211              
212 0           for my $subq ( @{ $q->{$prefix} } ) {
  0            
213 0           my $q = $self->_orm_subq( $subq, $prefix );
214 0           my $items = scalar(@$q);
215              
216 0 0         $debug and warn "items $items $joiner : " . dump $q;
217 0 0         my $sub_joiner = $prefix eq '-' ? $AND : $OR;
218 0 0         push( @op_subq, ( $items > 2 ) ? ( $sub_joiner => $q ) : @$q );
219             }
220              
221 0 0         $debug and warn sprintf( "n subq == %d, joiner=%s, dump: %s\n",
222             scalar(@op_subq), $joiner, dump \@op_subq );
223              
224 0 0 0       if ( $self->{_parser}->{lower}
  0            
225             and grep { ref($_) eq 'ARRAY' } @op_subq )
226             {
227             # when 'lower' is on, items in the subq are arrayrefs, so count
228             # of items is different
229 0           push @$query, $joiner => \@op_subq;
230             }
231             else {
232              
233 0 0         push( @$query,
234             ( scalar(@op_subq) > 2 )
235             ? ( $joiner => \@op_subq )
236             : @op_subq );
237             }
238              
239             }
240 0           return $query;
241             }
242              
243             sub _orm_subq {
244 0     0     my $self = shift;
245 0           my $subQ = shift;
246 0           my $prefix = shift;
247 0   0       my $opts = $self->{opts} || {};
248              
249 0 0         return $self->_orm( $subQ->{value} )
250             if $subQ->{op} eq '()';
251              
252             # make sure we have a column
253             my @columns
254 0           = $subQ->{field}
255             ? ( $subQ->{field} )
256 0 0         : ( @{ $self->{_parser}->{default_column} } );
257              
258             # what value
259 0           my $value = $self->_doctor_value($subQ);
260              
261             # normalize operator
262 0           my $op = $subQ->{op};
263 0 0         if ( $op eq ':' ) {
264 0           $op = '=';
265             }
266 0 0         if ( $prefix eq '-' ) {
267 0           $op = '!' . $op;
268             }
269 0 0         if ( $value =~ m/\%/ ) {
270 0 0         $op = $prefix eq '-' ? '!~' : '~';
271             }
272              
273 0           my @buf;
274 0           for my $colname (@columns) {
275 0           my $column = $self->{_parser}->get_column($colname);
276              
277 0 0         $value =~ s/\%//g if $column->is_int;
278              
279 0           my @pair;
280              
281 0 0         if ( defined $column->orm_callback ) {
    0          
    0          
    0          
    0          
282 0           @pair = $column->orm_callback->( $column, $op, $value );
283             }
284              
285             # standard
286             elsif ( $op eq '=' ) {
287 0           @pair = ( $colname, $value );
288             }
289              
290             # negation
291             elsif ( $op eq '!=' ) {
292 0           @pair = ( $colname, { $op => $value } );
293             }
294              
295             # fuzzy
296             elsif ( $op eq '~' ) {
297 0           @pair = ( $colname, { $column->fuzzy_op => $value } );
298             }
299              
300             # not fuzzy
301             elsif ( $op eq '!~' ) {
302 0           @pair = ( $colname, { $column->fuzzy_not_op => $value } );
303             }
304             else {
305 0           croak
306             "unknown operator logic for column '$colname' op '$op' value '$value'";
307             }
308              
309             # if lower, then turn pair into a scalar ref literal
310 0 0 0       if ( !$column->is_int and $self->{_parser}->{lower} ) {
311 0           my $col = $pair[0];
312 0           my $val = $pair[1];
313 0           my $this_op = $op;
314 0 0         if ( ref $val ) {
315 0           ( $this_op, $val ) = each %$val;
316             }
317 0           @pair = ( [ \qq/lower($pair[0]) $this_op lower(?)/, $val ] );
318             }
319              
320 0           push @buf, @pair;
321             }
322              
323             #warn "buf: " . dump \@buf;
324              
325 0           return \@buf;
326              
327             }
328              
329             sub _unwind {
330 0     0     my $self = shift;
331 0   0       my $q = shift || $self;
332 0           my @subQ;
333 0           for my $prefix ( '+', '', '-' ) {
334 0           my @clause;
335 0           my $joiner = $op_map{$prefix};
336 0           for my $subq ( @{ $q->{$prefix} } ) {
  0            
337 0           push @clause, $self->_unwind_subQ( $subq, $prefix );
338             }
339 0 0         next if !@clause;
340              
341             #warn "$joiner clause: " . dump \@clause;
342              
343 0 0         push( @subQ,
344 0           join( " $joiner ", grep { defined && length } @clause ) );
345             }
346 0           return join( " AND ", @subQ );
347             }
348              
349             sub _doctor_value {
350 0     0     my ( $self, $subQ ) = @_;
351              
352 0           my $value = $subQ->{value};
353              
354 0 0         if ( $self->{_parser}->{fuzzify} ) {
    0          
355 0 0         $value .= '*' unless $value =~ m/[\*\%]/;
356             }
357             elsif ( $self->{_parser}->{fuzzify2} ) {
358 0 0         $value = "*$value*" unless $value =~ m/[\*\%]/;
359             }
360              
361             # normalize wildcard to sql variety
362 0           $value =~ s/\*/\%/g;
363              
364 0           return $value;
365             }
366              
367             sub _unwind_subQ {
368 0     0     my $self = shift;
369 0           my $subQ = shift;
370 0           my $prefix = shift;
371 0   0       my $opts = $self->{opts} || {};
372              
373 0 0         return "(" . $self->_unwind( $subQ->{value} ) . ")"
374             if $subQ->{op} eq '()';
375              
376             # optional
377 0           my $col_quote = $self->{_parser}->{quote_columns};
378 0           my $use_lower = $self->{_parser}->{lower};
379              
380             # make sure we have a column
381             my @columns
382 0           = $subQ->{field}
383             ? ( $subQ->{field} )
384 0 0         : ( @{ $self->{_parser}->{default_column} } );
385              
386             # what value
387 0           my $value = $self->_doctor_value($subQ);
388              
389             # normalize operator
390 0           my $op = $subQ->{op};
391 0 0         if ( $op eq ':' ) {
392 0           $op = '=';
393             }
394 0 0         if ( $prefix eq '-' ) {
395 0           $op = '!' . $op;
396             }
397 0 0         if ( $value =~ m/\%/ ) {
398 0 0         $op = $prefix eq '-' ? '!~' : '~';
399             }
400              
401 0           my @buf;
402 0           COLNAME: for my $colname (@columns) {
403 0           my $column = $self->{_parser}->get_column($colname);
404 0 0         $value =~ s/\%//g if $column->is_int;
405 0           my $this_op;
406              
407             # whether we quote depends on the field (column) type
408 0 0         my $quote = $column->is_int ? "" : "'";
409              
410 0           my $prefix = '';
411 0           my $suffix = '';
412 0 0 0       if ( !$column->is_int and $use_lower ) {
413 0           $prefix = 'lower(';
414 0           $suffix = ')';
415             }
416              
417             # fuzzy
418 0 0         if ( $op =~ m/\~/ ) {
419              
420             # negation
421 0 0         if ( $op eq '!~' ) {
422 0 0         if ( $column->is_int ) {
423 0           $this_op = $column->fuzzy_not_op;
424             }
425             else {
426 0           $this_op = ' ' . $column->fuzzy_not_op . ' ';
427             }
428             }
429              
430             # standard fuzzy
431             else {
432 0 0         if ( $column->is_int ) {
433 0           $this_op = $column->fuzzy_op;
434             }
435             else {
436 0           $this_op = ' ' . $column->fuzzy_op . ' ';
437             }
438             }
439             }
440             else {
441 0           $this_op = $op;
442             }
443              
444 0 0         if ( defined $column->callback ) {
445 0           push( @buf, $column->callback->( $column, $this_op, $value ) );
446 0           next COLNAME;
447             }
448              
449 0 0         if ( $opts->{delims} ) {
450 0           push(
451             @buf,
452             join( '',
453             $prefix, $col_quote, $colname, $col_quote, $suffix,
454             chr(5), $this_op, chr(6), $prefix, chr(2),
455             $value, chr(3), $suffix, )
456             );
457             }
458             else {
459 0           push(
460             @buf,
461             join( '',
462             $prefix, $col_quote, $colname, $col_quote,
463             $suffix, $this_op, $prefix, $quote,
464             $value, $quote, $suffix, )
465             );
466             }
467             }
468 0 0         my $joiner = $prefix eq '-' ? ' AND ' : ' OR ';
469             return
470 0 0         ( scalar(@buf) > 1 ? '(' : '' )
    0          
471             . join( $joiner, @buf )
472             . ( scalar(@buf) > 1 ? ')' : '' );
473              
474             }
475              
476             1;
477              
478             __END__