File Coverage

blib/lib/DBIx/Class/SQLMaker/Util.pm
Criterion Covered Total %
statement 184 191 96.3
branch 144 160 90.0
condition 107 121 88.4
subroutine 11 11 100.0
pod 0 3 0.0
total 446 486 91.7


line stmt bran cond sub pod time code
1             package #hide from PAUSE
2             DBIx::Class::SQLMaker::Util;
3              
4 313     313   1988 use strict;
  313         669  
  313         8321  
5 313     313   1613 use warnings;
  313         628  
  313         8127  
6              
7 313     313   1637 use base 'Exporter';
  313         849  
  313         34145  
8             our @EXPORT_OK = qw(
9             normalize_sqla_condition
10             extract_equality_conditions
11             );
12              
13 313     313   2151 use DBIx::Class::Carp;
  313         1024  
  313         2007  
14 313     313   2040 use Carp 'croak';
  313         709  
  313         16355  
15 313     313   163771 use SQL::Abstract qw( is_literal_value is_plain_value );
  313         2993161  
  313         27300  
16 313     313   3273 use DBIx::Class::_Util qw( UNRESOLVABLE_CONDITION dump_value modver_gt_or_eq );
  313         790  
  313         789590  
17              
18             # Can not use DBIx::Class::_Util::serialize as it is based on
19             # Storable and leaks through differences between PVIV and an identical IV
20             # Since SQLA itself is lossy in this regard (it does not make proper copies
21             # for efficiency) one could end up in a situation where semantically
22             # identical values aren't treated as such
23             my $dd_obj;
24             sub lax_serialize ($) {
25             my $dump_str = (
26             $dd_obj
27             ||=
28 13075   66 13075 0 111645 do {
29 105         62056 require Data::Dumper;
30              
31             # Warnings without this on early loads under -w
32             # Why? Because fuck me, that's why :/
33 105 100       649833 local $Data::Dumper::Indent = 0
34             unless defined $Data::Dumper::Indent;
35              
36             # Make sure each option is spelled out with a value, so that
37             # global environment changes can not override any of these
38             # between two serialization calls
39             #
40 105         1197 my $d = Data::Dumper->new([])
41             ->Indent('0')
42             ->Purity(0)
43             ->Pad('')
44             ->Useqq(0)
45             ->Terse(1)
46             ->Freezer('')
47             ->Toaster('')
48             ->Deepcopy(0)
49             ->Quotekeys(0)
50             ->Bless('bless')
51             ->Pair(' => ')
52             ->Maxdepth(0)
53             ->Useperl(0)
54             ->Sortkeys(1)
55             ->Deparse(0)
56             ;
57              
58             # FIXME - this is kinda ridiculous - there ought to be a
59             # Data::Dumper->new_with_defaults or somesuch...
60             #
61 105 50       14223 if( modver_gt_or_eq ( 'Data::Dumper', '2.136' ) ) {
62 105         686 $d->Sparseseen(1);
63              
64 105 50       1068 if( modver_gt_or_eq ( 'Data::Dumper', '2.153' ) ) {
65 105         824 $d->Maxrecurse(1000);
66              
67 105 50       939 if( modver_gt_or_eq ( 'Data::Dumper', '2.160' ) ) {
68 105         625 $d->Trailingcomma(0);
69             }
70             }
71             }
72              
73 105         3138 $d;
74             }
75             )->Values([$_[0]])->Dump;
76              
77 13075         441210 $dd_obj->Reset->Values([]);
78              
79 13075         217873 $dump_str;
80             }
81              
82              
83             # Attempts to flatten a passed in SQLA condition as much as possible towards
84             # a plain hashref, *without* altering its semantics.
85             #
86             # FIXME - while relatively robust, this is still imperfect, one of the first
87             # things to tackle when we get access to a formalized AST. Note that this code
88             # is covered by a *ridiculous* amount of tests, so starting with porting this
89             # code would be a rather good exercise
90             sub normalize_sqla_condition {
91 47562     47562 0 179972 my ($where, $where_is_anded_array) = @_;
92              
93 47562         71958 my $fin;
94              
95 47562 100 100     219881 if (! $where) {
    100          
    100          
96 272         544 return;
97             }
98             elsif ($where_is_anded_array or ref $where eq 'HASH') {
99              
100 42096         71056 my @pairs;
101              
102 42096 100       111289 my @pieces = $where_is_anded_array ? @$where : $where;
103 42096         99675 while (@pieces) {
104 46689         84126 my $chunk = shift @pieces;
105              
106 46689 100       113633 if (ref $chunk eq 'HASH') {
    100          
    100          
107 45321         156055 for (sort keys %$chunk) {
108              
109             # Match SQLA 1.79 behavior
110 49997 100       111912 unless( length $_ ) {
111 128 100       525 is_literal_value($chunk->{$_})
112             ? carp 'Hash-pairs consisting of an empty string with a literal are deprecated, use -and => [ $literal ] instead'
113             : croak 'Supplying an empty left hand side argument is not supported in hash-pairs'
114             ;
115             }
116              
117 49929         181264 push @pairs, $_ => $chunk->{$_};
118             }
119             }
120             elsif (ref $chunk eq 'ARRAY') {
121 511 100       2441 push @pairs, -or => $chunk
122             if @$chunk;
123             }
124             elsif ( ! length ref $chunk) {
125              
126             # Match SQLA 1.79 behavior
127 454 100 100     2667 croak("Supplying an empty left hand side argument is not supported in array-pairs")
      66        
128             if $where_is_anded_array and (! defined $chunk or ! length $chunk);
129              
130 444         1227 push @pairs, $chunk, shift @pieces;
131             }
132             else {
133 403         1149 push @pairs, '', $chunk;
134             }
135             }
136              
137 42018 100       94547 return unless @pairs;
138              
139 41688 100       112501 my @conds = _normalize_cond_unroll_pairs(\@pairs)
140             or return;
141              
142             # Consolidate various @conds back into something more compact
143 41120         90170 for my $c (@conds) {
144 50662 50       119016 if (ref $c ne 'HASH') {
145 0         0 push @{$fin->{-and}}, $c;
  0         0  
146             }
147             else {
148 50662         134409 for my $col (keys %$c) {
149              
150             # consolidate all -and nodes
151 56636 100       223530 if ($col =~ /^\-and$/i) {
    100          
    100          
152 998         3221 push @{$fin->{-and}},
153 998         2693 ref $c->{$col} eq 'ARRAY' ? @{$c->{$col}}
154 0         0 : ref $c->{$col} eq 'HASH' ? %{$c->{$col}}
155 998 0       1506 : { $col => $c->{$col} }
    50          
156             ;
157             }
158             elsif ($col =~ /^\-/) {
159 2028         2915 push @{$fin->{-and}}, { $col => $c->{$col} };
  2028         7327  
160             }
161             elsif (exists $fin->{$col}) {
162             $fin->{$col} = [ -and => map {
163             (ref $_ eq 'ARRAY' and ($_->[0]||'') =~ /^\-and$/i )
164 3160 100 100     15528 ? @{$_}[1..$#$_]
  89         342  
165             : $_
166             ;
167 1580         5224 } ($fin->{$col}, $c->{$col}) ];
168             }
169             else {
170 52030         147921 $fin->{$col} = $c->{$col};
171             }
172             }
173             }
174             }
175              
176             # a deduplication (and sort) pass on all individual -and/-or members
177 41120         78272 for my $op (qw( -and -or )) {
178 82240 100       116663 if( @{ $fin->{$op} || [] } > 1 ) {
  82240 100       378714  
179             my $seen_chunks = { map {
180 796         1328 lax_serialize($_) => $_
181 315         455 } @{$fin->{$op}} };
  315         586  
182              
183 315         1134 $fin->{$op} = [ @{$seen_chunks}{ sort keys %$seen_chunks } ];
  315         1202  
184             }
185             }
186             }
187             elsif (ref $where eq 'ARRAY') {
188             # we are always at top-level here, it is safe to dump empty *standalone* pieces
189 4498         7501 my $fin_idx;
190              
191 4498         15085 for (my $i = 0; $i <= $#$where; $i++ ) {
192              
193             # Match SQLA 1.79 behavior
194 7505 100 100     33521 croak(
195             "Supplying an empty left hand side argument is not supported in array-pairs"
196             ) if (! defined $where->[$i] or ! length $where->[$i]);
197              
198 7349   100     36385 my $logic_mod = lc ( ($where->[$i] =~ /^(\-(?:and|or))$/i)[0] || '' );
199              
200 7349 100       20294 if ($logic_mod) {
    100          
201 1175         1733 $i++;
202 1175 50 66     4512 croak("Unsupported top-level op/arg pair: [ $logic_mod => $where->[$i] ]")
203             unless ref $where->[$i] eq 'HASH' or ref $where->[$i] eq 'ARRAY';
204              
205 1175 100       4029 my $sub_elt = normalize_sqla_condition({ $logic_mod => $where->[$i] })
206             or next;
207              
208 922         2454 my @keys = keys %$sub_elt;
209 922 100 100     4412 if ( @keys == 1 and $keys[0] !~ /^\-/ ) {
210 369         1472 $fin_idx->{ "COL_$keys[0]_" . lax_serialize $sub_elt } = $sub_elt;
211             }
212             else {
213 553         1456 $fin_idx->{ "SER_" . lax_serialize $sub_elt } = $sub_elt;
214             }
215             }
216             elsif (! length ref $where->[$i] ) {
217 4013 100       6530 my $sub_elt = normalize_sqla_condition({ @{$where}[$i, $i+1] })
  4013         12796  
218             or next;
219              
220 4012         15063 $fin_idx->{ "COL_$where->[$i]_" . lax_serialize $sub_elt } = $sub_elt;
221 4012         12815 $i++;
222             }
223             else {
224 2161   100     7268 $fin_idx->{ "SER_" . lax_serialize $where->[$i] } = normalize_sqla_condition( $where->[$i] ) || next;
225             }
226             }
227              
228 4164 100       16805 if (! $fin_idx) {
    100          
229 580         1842 return;
230             }
231             elsif ( keys %$fin_idx == 1 ) {
232 2041         7399 $fin = (values %$fin_idx)[0];
233             }
234             else {
235 1543         2560 my @or;
236              
237             # at this point everything is at most one level deep - unroll if needed
238 1543         5865 for (sort keys %$fin_idx) {
239 4376 100 66     10544 if ( ref $fin_idx->{$_} eq 'HASH' and keys %{$fin_idx->{$_}} == 1 ) {
  4376         11351  
240 4333         5864 my ($l, $r) = %{$fin_idx->{$_}};
  4333         9239  
241              
242 4333 100 66     16733 if (
    50 66        
      66        
      66        
243             ref $r eq 'ARRAY'
244             and
245             (
246             ( @$r == 1 and $l =~ /^\-and$/i )
247             or
248             $l =~ /^\-or$/i
249             )
250             ) {
251 136         388 push @or, @$r
252             }
253              
254             elsif (
255             ref $r eq 'HASH'
256             and
257             keys %$r == 1
258             and
259             $l =~ /^\-(?:and|or)$/i
260             ) {
261 0         0 push @or, %$r;
262             }
263              
264             else {
265 4197         9544 push @or, $l, $r;
266             }
267             }
268             else {
269 43         112 push @or, $fin_idx->{$_};
270             }
271             }
272              
273 1543         6519 $fin->{-or} = \@or;
274             }
275             }
276             else {
277             # not a hash not an array
278 696         2198 $fin = { -and => [ $where ] };
279             }
280              
281             # unroll single-element -and's
282 45400   100     134167 while (
283             $fin->{-and}
284             and
285 3605         10484 @{$fin->{-and}} < 2
286             ) {
287 3269         6189 my $and = delete $fin->{-and};
288 3269 50       6512 last if @$and == 0;
289              
290             # at this point we have @$and == 1
291 3269 100 66     8429 if (
292             ref $and->[0] eq 'HASH'
293             and
294 1832         6399 ! grep { exists $fin->{$_} } keys %{$and->[0]}
  1832         5054  
295             ) {
296             $fin = {
297 1832         3539 %$fin, %{$and->[0]}
  1832         7723  
298             };
299             }
300             else {
301 1437         2365 $fin->{-and} = $and;
302 1437         2422 last;
303             }
304             }
305              
306             # compress same-column conds found in $fin
307 45400         127576 for my $col ( grep { $_ !~ /^\-/ } keys %$fin ) {
  59538         194245  
308 54064 100 100     183584 next unless ref $fin->{$col} eq 'ARRAY' and ($fin->{$col}[0]||'') =~ /^\-and$/i;
      100        
309             my $val_bag = { map {
310 5477 100 66     31518 (! defined $_ ) ? ( UNDEF => undef )
    100          
311             : ( ! length ref $_ or is_plain_value $_ ) ? ( "VAL_$_" => $_ )
312             : ( ( 'SER_' . lax_serialize $_ ) => $_ )
313 2671         6838 } @{$fin->{$col}}[1 .. $#{$fin->{$col}}] };
  2671         6907  
  2671         7267  
314              
315 2671 100       11109 if (keys %$val_bag == 1 ) {
316 348         1850 ($fin->{$col}) = values %$val_bag;
317             }
318             else {
319 2323         9029 $fin->{$col} = [ -and => map { $val_bag->{$_} } sort keys %$val_bag ];
  4728         17122  
320             }
321             }
322              
323 45400 50       224614 return keys %$fin ? $fin : ();
324             }
325              
326             sub _normalize_cond_unroll_pairs {
327 44603     44603   70635 my $pairs = shift;
328              
329 44603         63830 my @conds;
330              
331 44603         101557 while (@$pairs) {
332 54333         134189 my ($lhs, $rhs) = splice @$pairs, 0, 2;
333              
334 54333 100       251651 if (! length $lhs) {
    100          
    100          
335 463         1185 push @conds, normalize_sqla_condition($rhs);
336             }
337             elsif ( $lhs =~ /^\-and$/i ) {
338 5569         24750 push @conds, normalize_sqla_condition($rhs, (ref $rhs eq 'ARRAY'));
339             }
340             elsif ( $lhs =~ /^\-or$/i ) {
341             push @conds, normalize_sqla_condition(
342 2660 100       10174 (ref $rhs eq 'HASH') ? [ map { $_ => $rhs->{$_} } sort keys %$rhs ] : $rhs
  595         1960  
343             );
344             }
345             else {
346 45641 100 100     495079 if (ref $rhs eq 'HASH' and ! keys %$rhs) {
    100 100        
    100 66        
    100 100        
    100 100        
    100 100        
      100        
      66        
      100        
      100        
      100        
347             # FIXME - SQLA seems to be doing... nothing...?
348             }
349             # normalize top level -ident, for saner extract_equality_conditions() code
350             elsif (ref $rhs eq 'HASH' and keys %$rhs == 1 and exists $rhs->{-ident}) {
351 11292         60308 push @conds, { $lhs => { '=', $rhs } };
352             }
353             elsif (ref $rhs eq 'HASH' and keys %$rhs == 1 and exists $rhs->{-value} and is_plain_value $rhs->{-value}) {
354 33         265 push @conds, { $lhs => $rhs->{-value} };
355             }
356             elsif (ref $rhs eq 'HASH' and keys %$rhs == 1 and exists $rhs->{'='}) {
357 4183 100 100     22047 if ( length ref $rhs->{'='} and is_literal_value $rhs->{'='} ) {
358 1268         13250 push @conds, { $lhs => $rhs };
359             }
360             else {
361 2915         22418 for my $p (_normalize_cond_unroll_pairs([ $lhs => $rhs->{'='} ])) {
362              
363             # extra sanity check
364 2920 50       10981 if (keys %$p > 1) {
365 0         0 local $Data::Dumper::Deepcopy = 1;
366 0         0 croak(
367             "Internal error: unexpected collapse unroll:"
368             . dump_value { in => { $lhs => $rhs }, out => $p }
369             );
370             }
371              
372 2920         9618 my ($l, $r) = %$p;
373              
374             push @conds, (
375             ! length ref $r
376             or
377             # the unroller recursion may return a '=' prepended value already
378 2920 100 100     29276 ref $r eq 'HASH' and keys %$rhs == 1 and exists $rhs->{'='}
379             or
380             is_plain_value($r)
381             )
382             ? { $l => $r }
383             : { $l => { '=' => $r } }
384             ;
385             }
386             }
387             }
388             elsif (ref $rhs eq 'ARRAY') {
389             # some of these conditionals encounter multi-values - roll them out using
390             # an unshift, which will cause extra looping in the while{} above
391 328 100 100     1705 if (! @$rhs ) {
    100          
    100          
392 66         219 push @conds, { $lhs => [] };
393             }
394             elsif ( ($rhs->[0]||'') =~ /^\-(?:and|or)$/i ) {
395 118 50       310 croak("Value modifier not followed by any values: $lhs => [ $rhs->[0] ] ")
396             if @$rhs == 1;
397              
398 118 100       398 if( $rhs->[0] =~ /^\-and$/i ) {
    100          
399 108         278 unshift @$pairs, map { $lhs => $_ } @{$rhs}[1..$#$rhs];
  245         758  
  108         277  
400             }
401             # if not an AND then it's an OR
402             elsif(@$rhs == 2) {
403 5         21 unshift @$pairs, $lhs => $rhs->[1];
404             }
405             else {
406 5         13 push @conds, { $lhs => [ @{$rhs}[1..$#$rhs] ] };
  5         21  
407             }
408             }
409             elsif (@$rhs == 1) {
410 15         114 unshift @$pairs, $lhs => $rhs->[0];
411             }
412             else {
413 129         645 push @conds, { $lhs => $rhs };
414             }
415             }
416             # unroll func + { -value => ... }
417             elsif (
418             ref $rhs eq 'HASH'
419             and
420             ( my ($subop) = keys %$rhs ) == 1
421             and
422             length ref ((values %$rhs)[0])
423             and
424             my $vref = is_plain_value( (values %$rhs)[0] )
425             ) {
426 25 100       357 push @conds, (
427             (length ref $$vref)
428             ? { $lhs => $rhs }
429             : { $lhs => { $subop => $$vref } }
430             );
431             }
432             else {
433 29774         126736 push @conds, { $lhs => $rhs };
434             }
435             }
436             }
437              
438 44265         150055 return @conds;
439             }
440              
441             # Analyzes a given condition and attempts to extract all columns
442             # with a definitive fixed-condition criteria. Returns a hashref
443             # of k/v pairs suitable to be passed to set_columns(), with a
444             # MAJOR CAVEAT - multi-value (contradictory) equalities are still
445             # represented as a reference to the UNRESOVABLE_CONDITION constant
446             # The reason we do this is that some codepaths only care about the
447             # codition being stable, as opposed to actually making sense
448             #
449             # The normal mode is used to figure out if a resultset is constrained
450             # to a column which is part of a unique constraint, which in turn
451             # allows us to better predict how ordering will behave etc.
452             #
453             # With the optional "consider_nulls" boolean argument, the function
454             # is instead used to infer inambiguous values from conditions
455             # (e.g. the inheritance of resultset conditions on new_result)
456             #
457             sub extract_equality_conditions {
458 8256     8256 0 3777546 my ($where, $consider_nulls) = @_;
459 8256         23110 my $where_hash = normalize_sqla_condition($where);
460              
461 8256         18160 my $res = {};
462 8256         15672 my ($c, $v);
463 8256         20763 for $c (keys %$where_hash) {
464 10108         16248 my $vals;
465              
466 10108 100 100     59714 if (!defined ($v = $where_hash->{$c}) ) {
    100 100        
    100 100        
    100 100        
467 322 100       1436 $vals->{UNDEF} = $v if $consider_nulls
468             }
469             elsif (
470             ref $v eq 'HASH'
471             and
472             keys %$v == 1
473             ) {
474 1557 100 100     11254 if (exists $v->{-value}) {
    100 66        
475 12 50       31 if (defined $v->{-value}) {
    0          
476             $vals->{"VAL_$v->{-value}"} = $v->{-value}
477 12         49 }
478             elsif( $consider_nulls ) {
479 0         0 $vals->{UNDEF} = $v->{-value};
480             }
481             }
482             # do not need to check for plain values - normalize_sqla_condition did it for us
483             elsif(
484             length ref $v->{'='}
485             and
486             (
487             ( ref $v->{'='} eq 'HASH' and keys %{$v->{'='}} == 1 and exists $v->{'='}{-ident} )
488             or
489             is_literal_value($v->{'='})
490             )
491             ) {
492 1409         8414 $vals->{ 'SER_' . lax_serialize $v->{'='} } = $v->{'='};
493             }
494             }
495             elsif (
496             ! length ref $v
497             or
498             is_plain_value ($v)
499             ) {
500 6643         27187 $vals->{"VAL_$v"} = $v;
501             }
502             elsif (ref $v eq 'ARRAY' and ($v->[0]||'') eq '-and') {
503 87         982 for ( @{$v}[1..$#$v] ) {
  87         210  
504 198         526 my $subval = extract_equality_conditions({ $c => $_ }, 'consider nulls'); # always fish nulls out on recursion
505 198 100       507 next unless exists $subval->{$c}; # didn't find anything
506             $vals->{
507             ! defined $subval->{$c} ? 'UNDEF'
508             : ( ! length ref $subval->{$c} or is_plain_value $subval->{$c} ) ? "VAL_$subval->{$c}"
509             : ( 'SER_' . lax_serialize $subval->{$c} )
510 147 100 66     751 } = $subval->{$c};
    100          
511             }
512             }
513              
514 10108 100       52232 if (keys %$vals == 1) {
    100          
515             ($res->{$c}) = (values %$vals)
516 8176 100 100     54191 unless !$consider_nulls and exists $vals->{UNDEF};
517             }
518             elsif (keys %$vals > 1) {
519 60         198 $res->{$c} = UNRESOLVABLE_CONDITION;
520             }
521             }
522              
523 8256         36457 $res;
524             }
525              
526             1;