File Coverage

blib/lib/SQL/Abstract/FromQuery.pm
Criterion Covered Total %
statement 164 165 99.3
branch 35 44 79.5
condition 14 18 77.7
subroutine 36 36 100.0
pod 2 14 14.2
total 251 277 90.6


line stmt bran cond sub pod time code
1             package SQL::Abstract::FromQuery;
2            
3 9     9   253054 use strict;
  9         43  
  9         186  
4 6     6   27 use warnings;
  6         12  
  6         171  
5 6     6   29 use Scalar::Util qw/refaddr reftype blessed/;
  6         11  
  6         297  
6 6     6   3307 use List::MoreUtils qw/mesh/;
  6         78037  
  6         41  
7 6     6   7882 use Module::Load qw/load/;
  6         3947  
  6         42  
8 6         528 use Params::Validate qw/validate SCALAR SCALARREF CODEREF ARRAYREF HASHREF
9 6     6   3617 UNDEF BOOLEAN/;
  6         50894  
10 6     6   2604 use UNIVERSAL::DOES qw/does/;
  6         2885  
  6         325  
11 6     6   38 use Digest::MD5 qw/md5_base64/;
  6         13  
  6         252  
12 6     6   32 use mro 'c3';
  6         10  
  6         42  
13            
14 6     6   2896 use namespace::clean;
  6         53020  
  6         36  
15            
16             our $VERSION = '0.11';
17            
18             # root grammar (will be inherited by subclasses)
19             my $root_grammar = do {
20 6     6   9094 use Regexp::Grammars 1.048;
  6         139196  
  6         43  
21             qr{
22             #
23            
24            
25            
26            
27             \A (?:
28            
29             |
30             |
31             |
32             )
33             (?: \Z | )
34            
35            
36             (*COMMIT) (?: | )
37            
38            
39             (*COMMIT) (?: | )
40            
41            
42             <[value]>+ % , # list of scalars separated by ','
43            
44            
45             BETWEEN (*COMMIT) (?: AND | )
46            
47            
48             \<= | \< | \>= | \>
49            
50            
51             \<\> | -(?!\d) | != | !
52            
53            
54            
55             |
56             |
57             |
58             |
59             |
60             # | # removed from "standard" value because it might
61             # interfere with other codes like gender M/F
62            
63            
64             NULL
65            
66            
67             T?
68            
69            
70             \.\.
71             | --
72            
73            
74             :(?::)?
75            
76            
77             //
78            
79            
80             Y(?:ES)? (?{ $MATCH = 1 })
81             | T(?:RUE)? (?{ $MATCH = 1 })
82             | N(?:O)? (?{ $MATCH = 0 })
83             | F(?:ALSE)? (?{ $MATCH = 0 })
84            
85            
86            
87             |
88            
89            
90             '(.*?)' (*COMMIT) (?{ $MATCH = $CAPTURE })
91             | "(.*?)" (*COMMIT) (?{ $MATCH = $CAPTURE })
92            
93            
94             [^\s,]+(?:\s+[^\s,]+)*?
95            
96             }xms;
97             };
98            
99            
100            
101             #======================================================================
102             # CLASS METHODS
103             #======================================================================
104             sub sub_grammar {
105 2     2 0 5 my $class = shift;
106 2         6 return; # subclasses that refine the root grammar should override this method
107             }
108            
109             my %params_for_new = (
110             -components => {type => ARRAYREF, optional => 1 },
111             -fields => {type => HASHREF, default => {} },
112             );
113            
114             sub new {
115 7     7 1 31321 my $class = shift;
116 7         21 my $self = {};
117 7         174 my %args = validate(@_, \%params_for_new);
118            
119             # load optional components
120 7 100       48 if ($args{-components}) {
121             # deactivate strict refs because we'll be playing with symbol tables
122 6     6   60 no strict 'refs';
  6         13  
  6         8444  
123            
124 5         11 my @components;
125 5         12 foreach my $component (@{$args{-components}}) {
  5         15  
126 6 50       29 $component =~ s/^\+//
127             or $component = __PACKAGE__ . "::$component";
128 6         31 load $component;
129 6         256 push @components, $component;
130 6         50 my @sub_grammar = $component->sub_grammar;
131 6 100       23 push @{$self->{grammar_ISA}}, @sub_grammar if @sub_grammar;
  4         20  
132             }
133            
134             # a new anonymous class will inherit from all components
135 5         51 $class .= "::_ANON_::" . md5_base64(join ",", @components);
136 5 50       13 unless (@{$class . "::ISA"}) {
  5         57  
137             # dynamically create that class and use 'c3' inheritance in it
138 5         10 push @{$class . "::ISA"}, @components;
  5         49  
139 5         34 mro::set_mro($class, 'c3');
140             }
141             }
142            
143             # use root grammar if no derived grammar was installed by components
144 7   100     46 $self->{grammar_ISA} ||= [ 'SQL::Abstract::FromQuery' ];
145            
146             # setup fields info
147 7         13 foreach my $type (keys %{$args{-fields}}) {
  7         32  
148 4 100       17 if ($type eq 'IGNORE') {
149 1 50       4 ref $args{-fields}{IGNORE} eq 'Regexp'
150             or die "IGNORE should be associated with a qr/.../ regular expression";
151 1         4 $self->{IGNORE} = $args{-fields}{IGNORE};
152             }
153             else {
154 3         10 my $fields_aref = $args{-fields}{$type};
155 3 50       19 does($fields_aref, 'ARRAY')
156             or die "list of fields for type $type should be an arrayref";
157 3         100 $self->{field}{$_} = $type foreach @$fields_aref;
158             }
159             }
160            
161             # return the blessed object
162 7         58 bless $self, $class;
163             }
164            
165             sub _error_handler {
166 71     71   108 my $class = shift;
167             return 'INCORRECT INPUT', sub {
168 85     85   504 my ($error, $rule, $context) = @_;
169             my $msg = {
170             negated_values => 'Expected a value after negation',
171             op_and_value => 'Expected a value after comparison operator',
172             between => 'Expected min and max after "BETWEEN"',
173             standard => 'Unexpected input after initial value',
174 85         274 }->{$rule};
175 85   33     196 $msg //= "Could not parse rule '$rule'";
176 85 100       200 $msg .= " ('$context')" if $context;
177 85         185 return $msg;
178 71         337 };
179             }
180            
181            
182             #======================================================================
183             # INSTANCE METHODS
184             #======================================================================
185            
186             sub _grammar {
187 9     9   31 my ($self, $rule) = @_;
188            
189 9         19 my $extends = join "", map {"\n"} @{$self->{grammar_ISA}};
  9         55  
  9         28  
190 9         34 my $grammar = "<$rule>\n$extends";
191            
192             # compile this grammar. NOTE : since Regexp::Grammars uses a very
193             # special form of operator overloading, we must go through an eval
194             # so that qr/../ receives a string without variable interpolation;
195             # do {use Regexp::Grammars; qr{$grammar}x;} would seem logical but won't work.
196 9         18 local $@;
197 6 50   6   48 my $compiled_grammar = eval "use Regexp::Grammars; qr{$grammar}x"
  6         11  
  6         32  
  9         783  
198             or die "INVALID GRAMMAR: $@";
199            
200 9         74 return $compiled_grammar;
201             }
202            
203             sub parse {
204 74     74 1 56620 my ($self, $data) = @_;
205 74         136 my $class = ref $self;
206            
207             # if $data is an object with ->param() method, transform into plain hashref
208 74 100 66     436 $data = $self->_flatten_into_hashref($data) if blessed $data
209             && $data->can('param');
210            
211             # set error translator for grammars
212 74         191 my ($err_msg, $err_translator) = $self->_error_handler;
213 74         207 my $tmp = Regexp::Grammars::set_error_translator($err_translator);
214            
215             # parse each field within $data
216 74         1085 my %result;
217             my %errors;
218             FIELD:
219 74         192 foreach my $field (keys %$data) {
220             # ignore fields in exclusion list or fields without any data
221 89 100 100     509 !$self->{IGNORE} or $field !~ $self->{IGNORE} or next FIELD;
222 87 50       217 my $val = $data->{$field} or next FIELD;
223            
224             # decide which grammar to apply
225 87   100     272 my $rule = $self->{field}{$field} || 'standard';
226 87   66     254 my $grammar = $self->{grammar}{$rule} ||= $self->_grammar($rule);
227            
228             # invoke grammar on field content
229 87 100       242 if ($val =~ $grammar->with_actions($self)) {
230 75         309 $result{$field} = $/{$rule};
231             }
232             else {
233 12         387 $errors{$field} = [@!];
234             }
235             }
236            
237             # report errors, if any
238 74 100       212 SQL::Abstract::FromQuery::_Exception->throw($err_msg, %errors) if %errors;
239            
240 66         194 return $self->finalize(\%result);
241             }
242            
243            
244             sub finalize { # subclasses may override this to modify the result
245 66     66 0 148 my ($self, $result) = @_;
246 66         248 return $result;
247             }
248            
249            
250             sub _flatten_into_hashref {
251 34     34   57 my ($self, $data) = @_;
252 34         44 my %h;
253 34         81 foreach my $field ($data->param()) {
254 34         516 my @vals = $data->param($field);
255 34         601 my $val = join ",", @vals; # TOO simple-minded - should make it more abstract
256 34         85 $h{$field} = $val;
257             }
258 34         67 return \%h;
259             }
260            
261             #======================================================================
262             # ACTIONS HOOKED TO THE GRAMMAR
263             #======================================================================
264            
265             sub negated_values {
266 9     9 0 264 my ($self, $h) = @_;
267 9         16 my $vals = $h->{values};
268 9 100       21 if (ref $vals) {
269 5 50       19 ref $vals eq 'HASH' or die 'unexpected reference in negation';
270 5         17 my ($op, $val, @others) = %$vals;
271 5 50       33 not @others or die 'unexpected hash size in negation';
272 5 100       18 if ($op eq '-in') {return {-not_in => $val} }
  2 50       8  
273 3         10 elsif ($op eq '=' ) {return {'<>' => $val} }
274 0         0 else {die "unexpected operator '$op' in negation"}
275             }
276             else {
277 4         11 return {'<>' => $vals};
278             }
279             }
280            
281             sub null {
282 7     7 0 209 my ($self, $h) = @_;
283 7         18 return {'=' => undef};
284             # Note: unfortunately, we can't return just undef at this stage,
285             # because Regexp::Grammars would interpret it as a parse failure.
286             }
287            
288             sub op_and_value {
289 7     7 0 196 my ($self, $h) = @_;
290 7         21 return {$h->{compare} => $h->{value}};
291             }
292            
293             sub between {
294 13     13 0 330 my ($self, $h) = @_;
295 13         45 return {-between => [$h->{min}, $h->{max}]};
296             }
297            
298             sub values {
299 63     63 0 1636 my ($self, $h) = @_;
300 63         88 my $n_values = @{$h->{value}};
  63         93  
301             return $n_values > 1 ? {-in => $h->{value}}
302 63 100       186 : $h->{value}[0];
303             }
304            
305             sub datetime {
306 5     5 0 119 my ($self, $h) = @_;
307 5         18 return join "T", $h->{date}, $h->{time};
308             }
309            
310             sub date {
311 27     27 0 760 my ($self, $h) = @_;
312 27 100       87 $h->{year} += 2000 if length($h->{year}) < 3;
313 27         38 return sprintf "%04d-%02d-%02d", @{$h}{qw/year month day/};
  27         141  
314             }
315            
316             sub time {
317 8     8 0 202 my ($self, $h) = @_;
318 8   100     27 $h->{seconds} ||= 0;
319 8         14 return sprintf "%02d:%02d:%02d", @{$h}{qw/hour minutes seconds/};
  8         42  
320             }
321            
322             sub regexp {
323 2     2 0 65 my ($self, $h) = @_;
324            
325             # There is no standard SQL syntax for regexp matching. So here we
326             # just return a datastructure for the SQL::Abstract instance, assuming
327             # that this instance will have a 'special operator' implementing regexp
328             # behaviour. If the special operator is not present, an error will be
329             # generated
330 2         9 return {-regexp => [$h->{regexp}, $h->{flags}]};
331            
332             # die "this grammar has no regexp() method; need a DBMS-specific component "
333             # . "to implement this behaviour";
334             }
335            
336            
337             sub string {
338 99     99 0 2222 my ($self, $s) = @_;
339            
340             # if the string contains any '*', substitute by '%' and make it a
341             # "-like" operator
342 99         208 my $is_pattern = $s =~ tr/*/%/;
343             # NOTE : a reentrant =~ s/../../ would segfault, but tr/../../ is OK
344            
345 99 100       246 return $is_pattern ? {-like => $s} : $s;
346             }
347            
348            
349             #======================================================================
350             # PRIVATE CLASS FOR REPORTING PARSE EXCEPTIONS
351             #======================================================================
352            
353             package
354             SQL::Abstract::FromQuery::_Exception;
355 6     6   72 use strict;
  6         14  
  6         174  
356 6     6   33 use warnings;
  6         22  
  6         1018  
357            
358             use overload
359             '""' => sub {
360 14     14   2299 my $self = shift;
361 14         29 my $msg = $self->{err_msg};
362 14         19 for my $field (sort keys %{$self->{errors}}) {
  14         56  
363 18         32 my $field_errors = $self->{errors}{$field};
364 18         67 $msg .= "\n$field : " . join ", ", @$field_errors;
365             }
366            
367 14         83 return $msg;
368             },
369 6         84 fallback => 1,
370 6     6   43 ;
  6         15  
371            
372             sub throw {
373 8     8   27 my ($class, $err_msg, %errors) = @_;
374 8         25 my $self = bless {err_msg => $err_msg, errors => \%errors}, $class;
375 8         48 die $self;
376             }
377            
378            
379             #======================================================================
380             1; # End of SQL::Abstract::FromQuery
381             #======================================================================
382            
383             __END__