File Coverage

blib/lib/Class/ReluctantORM/FetchDeep.pm
Criterion Covered Total %
statement 39 267 14.6
branch 0 114 0.0
condition 0 50 0.0
subroutine 13 21 61.9
pod 3 3 100.0
total 55 455 12.0


line stmt bran cond sub pod time code
1             =begin devnotes
2              
3             As the fetch_deep code is fairly complex, I thought best to
4             pull it out of the CRO main module and place it here.
5              
6             This is a mix-in module - so set package to be CRO.
7              
8             =cut
9              
10             package Class::ReluctantORM;
11              
12              
13 1     1   5 use strict;
  1         4  
  1         36  
14 1     1   6 use warnings;
  1         3  
  1         33  
15 1     1   6 use Scalar::Util qw(blessed refaddr);
  1         2  
  1         67  
16 1     1   6 use Class::ReluctantORM::Exception;
  1         2  
  1         21  
17 1     1   5 use Class::ReluctantORM::Utilities qw(check_args);
  1         2  
  1         60  
18              
19             our $DEBUG_FD ||= 0;
20 1     1   7 use Class::ReluctantORM::SQL::Aliases;
  1         2  
  1         84  
21              
22 1     1   6 use Class::ReluctantORM::SQL::From;
  1         2  
  1         21  
23 1     1   6 use Class::ReluctantORM::SQL::Table;
  1         3  
  1         10  
24 1     1   27 use Class::ReluctantORM::SQL::Column;
  1         2  
  1         8  
25 1     1   25 use Class::ReluctantORM::SQL::Param;
  1         3  
  1         7  
26 1     1   26 use Class::ReluctantORM::SQL::Where;
  1         2  
  1         22  
27 1     1   6 use Class::ReluctantORM::SQL::Expression::Criterion;
  1         1  
  1         9  
28 1     1   21 use Class::ReluctantORM::SQL::OrderBy;
  1         2  
  1         2556  
29              
30             =begin devnotes
31              
32             fetch_deep and search_Deep are really just frontends
33             for __deep_query.
34              
35             =cut
36              
37             sub fetch_deep {
38 0     0 1   my $class = shift;
39 0 0         if (@_ % 2) { Class::ReluctantORM::Exception::Param::ExpectedHash->croak(); }
  0            
40 0           my %args = @_;
41              
42 0           return $class->__deep_query(%args, fatal => 1);
43             }
44              
45             sub search_deep {
46 0     0 1   my $class = shift;
47 0 0         if (@_ % 2) { Class::ReluctantORM::Exception::Param::ExpectedHash->croak(); }
  0            
48 0           my %args = @_;
49 0           return $class->__deep_query(%args, fatal => 0);
50             }
51              
52             sub fetch_deep_overlay {
53 0     0 1   my $inv = shift;
54 0           my $plural_mode = !ref($inv);
55 0 0         my $class = ref($inv) ? ref($inv) : $inv;
56 0           my %args;
57 0 0         if ($plural_mode) {
58 0           %args = check_args(args => \@_, required => [qw(with objects)]);
59             } else {
60 0           %args = check_args(args => \@_, required => [qw(with)]);
61             }
62              
63             # With cannot be empty for an overlay
64 0 0         unless (keys %{$args{with}}) {
  0            
65 0           Class::ReluctantORM::Exception::Param::BadValue->croak
66             (
67             param => 'with',
68             value => '{}',
69             error => "'with' cannot be empty for an overlay fetch",
70             );
71             }
72              
73             # Check that 'with' arg with a dummy 'where'
74             {
75 0           my %checked_args = $inv->__dq_check_args(with => $args{with}, where => Where->new());
  0            
76 0           $args{with} = $checked_args{with};
77             }
78             # If in plural mode, objects param must be an array ref
79 0 0 0       if ($plural_mode && ref($args{objects}) ne 'ARRAY') {
80 0           Class::ReluctantORM::Exception::Param::ExpectedArrayRef->croak
81             (
82             param => 'objects',
83             value => $args{objects},
84             );
85             }
86 0 0 0       if ($plural_mode && !@{$args{objects}}) {
  0            
87             # Zero objects to act on, just return.
88 0           return;
89             }
90              
91 0 0         my @parent_objects = ($plural_mode) ? @{$args{objects}} : ($inv);
  0            
92              
93             # Ok, start building the actual where clause
94              
95             # Assume single-column PKs
96 0 0         if ($inv->primary_key_column_count() > 1) {
97             # TODO
98 0           Class::ReluctantORM::Exception::NotImplemented->croak
99             (
100             "Multiple-column primary keys are not supported when using fetch_deep_overlay. Need perl-side KEY_COMPOSITOR support."
101             );
102             }
103              
104             # we want to match primary keys in the objects against the base table PKs
105             # two ways to do this: as a $pk_column IN (pkv1, pkv2, ....) (faster, but vulnerable to SQL injection since we can't do placeholders)
106             # or as $pk_column IN = pkv1 OR $pk_column IN = pkv2 ... (dead slow but safe)
107              
108             # We'll check to see if all the PK values look like integers. If so, we'll do an IN. If not, we'll do ORs.
109 0           my $must_use_OR_approach = grep { $_->id !~ /^\d+$/ } @parent_objects;
  0            
110              
111 0           my %fetch_args;
112 0 0         if ($must_use_OR_approach) {
113 0           my $where = Where->new();
114 0           my $pk_column = Column->new(
115             column => $class->first_primary_key_column(),
116             table => $class->table_name,
117             );
118 0           foreach my $obj (@parent_objects) {
119 0           $where = $where->or(Criterion->new('=', $pk_column, Param->new($obj->id)));
120             }
121 0           $fetch_args{where} = $where;
122             } else {
123             # Ugggh, apparently we haven't implemented 'IN'. Lame.
124 0           $fetch_args{where} =
125             $class->table_name . '.' .
126             $class->first_primary_key_column() .
127 0           ' IN (' . join(',', map { $_->id } @parent_objects) . ')';
128 0           $fetch_args{parse_where} = 0;
129             }
130 0           $fetch_args{with} = $args{with};
131              
132             # Ok, run fetch.
133 0           my @new_copies_of_parents = $class->__deep_query(%fetch_args);
134              
135             # Overlay merge.
136 0           my %parent_index = map { $_->id => $_ } @parent_objects;
  0            
137 0           foreach my $copy (@new_copies_of_parents) {
138 0           my $id = $copy->id;
139 0           my $original = $parent_index{$id};
140 0 0         next if refaddr($copy) eq refaddr($original); # Registry has already merged them
141             # Loop over top-level relations in the with
142 0           foreach my $relname (grep { $_ !~ /^__/} keys %{$args{with}}) {
  0            
  0            
143 0           my $rel = $class->relationships($relname);
144 0           my ($lower, $upper) = ($rel->lower_multiplicity, $rel->upper_multiplicity);
145 0 0 0       if (defined($lower) && defined($upper) && $lower == 0 && $upper == 0) {
    0 0        
      0        
      0        
146             # Has-Lazy or similar
147 0           $original->$relname($copy->$relname);
148 0           $original->_mark_field_clean($relname);
149             } elsif (defined($upper) && $upper == 1) {
150             # Has-one, or similar
151 0           $original->$relname($copy->$relname);
152 0           $original->_mark_field_clean($relname);
153             } else {
154             # has-many, HMM, or similar
155 0           $original->$relname->_set_contents($copy->$relname->all());
156             }
157             }
158 0           $original->capture_origin();
159             }
160             }
161              
162              
163             =begin devnotes
164              
165             =head2 @results = __deep_query(%deep_args, fatal => 0|1)
166              
167             __deep_query does the hard work. The fatal flag says whether to
168             throw an exception if there are no results.
169              
170             Helper subs are named __dq_* .
171              
172             =cut
173              
174             sub __deep_query {
175 0     0     my ($class, %orig_args) = @_;
176              
177             # Normalize args
178 0 0         if ($DEBUG_FD > 2) { print STDERR __FILE__ . ':' . __LINE__ . " - __deep_query orig args: :" . Dumper(\%orig_args); }
  0            
179 0           my %args = $class->__dq_check_args(%orig_args);
180 0 0         if ($DEBUG_FD > 2) { print STDERR __FILE__ . ':' . __LINE__ . " - __deep_query scrubbed args: :" . Dumper(\%args); }
  0            
181              
182             # Build SQL
183 0           my $sql = $class->__dq_build_sql(%args);
184 0 0         if ($DEBUG_FD > 2) { print STDERR __FILE__ . ':' . __LINE__ . " - __deep_query SQL object:" . Dumper($sql); }
  0            
185              
186             # Execute SQL Query
187 0           my $driver = $class->driver();
188 0           my @results = $driver->execute_fetch_deep($sql, $args{with});
189 0 0 0       if ($args{fatal} && !@results) {
190 0           Class::ReluctantORM::Exception::Data::NotFound->croak();
191             }
192 0           $class->_apply_deep_filter_args($args{filter_info}, \@results);
193 0 0         return wantarray ? @results : $results[0];
194             }
195              
196             sub __dq_check_args {
197 0     0     my $class = shift;
198 0           my %orig_args = @_;
199 0           my %args;
200              
201 0 0         if ($orig_args{with}) {
202 0 0 0       unless (ref($orig_args{with}) && ref($orig_args{with}) eq 'HASH') {
203 0           Class::ReluctantORM::Exception::Param::ExpectedHashRef->croak
204             (param => 'with', frames => 3, );
205             }
206 0           $args{with} = $class->__dq_normalize_with($orig_args{with});
207 0           delete $orig_args{with};
208              
209             # Go ahead and build From here - we'll need it to process the WHERE
210 0           my $from = From->_new_from_with($class, $args{with});
211 0           $args{from} = $from;
212             } else {
213 0           my $t = Table->new($class);
214 0           $args{from} = From->new($t);
215 0           $args{with} = { __upper_table => $t };
216             }
217              
218 0           $args{fatal} = $orig_args{fatal};
219 0           delete $orig_args{fatal};
220              
221             # Boost order by to be an Order By object if it isn't already
222 0           $args{order_by} = $orig_args{order_by};
223 0 0         if ($args{order_by}) {
224 0 0 0       if (ref($args{order_by}) && !$args{order_by}->isa('Class::ReluctantORM::SQL::OrderBy')) {
225 0           Class::ReluctantORM::Exception::Param::WrongType->croak(
226             error => 'order_by must be either a string or an OrderBy object',
227             param => 'order_by',
228             expected => 'Class::ReluctantORM::SQL::OrderBy',
229             frames => 3,
230             );
231             }
232 0 0         unless (ref($args{order_by})) {
233 0           $args{order_by} = $class->driver->parse_order_by($args{order_by});
234             }
235             }
236 0           delete $orig_args{order_by};
237              
238             # Pagination args
239 0 0         if (defined $orig_args{limit}) {
240 0 0         unless ($args{order_by}) {
241 0           Class::ReluctantORM::Exception::Param::Missing->croak(
242             error => 'order_by is required if limit is provided',
243             param => 'order_by',
244             frames => 3
245             );
246             }
247 0 0         unless ($orig_args{limit} =~ /-?\d+/) {
248 0           Class::ReluctantORM::Exception::Param::BadValue->croak(
249             error => 'limit must be an integer',
250             param => 'limit',
251             value => $orig_args{limit},
252             frames => 3
253             );
254             }
255 0 0         if ($orig_args{limit} < 1) {
256 0           Class::ReluctantORM::Exception::Param->croak(
257             error => 'when limit is provided, it must be a positive integer',
258             param => 'limit',
259             value => $orig_args{limit},
260             frames => 3
261             );
262             }
263 0           $args{limit} = $orig_args{limit};
264 0           delete $orig_args{limit};
265             }
266 0 0         if (defined $orig_args{offset}) {
267 0 0         unless ($args{limit}) {
268 0           Class::ReluctantORM::Exception::Param::Missing->croak(
269             error => 'limit is required if offset is provided',
270             param => 'limit',
271             frames => 3
272             );
273             }
274 0 0         unless ($orig_args{offset} =~ /-?\d+/) {
275 0           Class::ReluctantORM::Exception::Param::BadValue->croak(
276             error => 'offset must be an integer',
277             param => 'offset',
278             value => $orig_args{offset},
279             frames => 3
280             );
281             }
282 0 0         if ($orig_args{offset} < 0) {
283 0           Class::ReluctantORM::Exception::Param::BadValue->croak(
284             error => 'when offset is provided, it must be a non-negative integer',
285             param => 'offset',
286             value => $orig_args{offset},
287             frames => 3
288             );
289             }
290 0           $args{offset} = $orig_args{offset};
291 0           delete $orig_args{offset};
292             }
293              
294             # Filter args
295 0           $args{filter_info} = $class->_extract_deep_filter_args(\%orig_args);
296              
297             # At this point, only other permissible args are a
298             # field, where, or execargs
299 0 0         if ($orig_args{where}) {
300 0 0 0       if (blessed($orig_args{where}) && $orig_args{where}->isa(Where)) {
301 0           $args{where_obj} = $orig_args{where};
302 0           delete $orig_args{where};
303 0           delete $orig_args{parse_where}; # Spurious in this case, but just ignore it if present
304             } else {
305             # Respect parse options
306 0 0         if ($orig_args{no_re_alias_where}) {
307 0           $orig_args{parse_where} = 0;
308             }
309 0 0         my $should_attempt_parse =
310             defined($orig_args{parse_where}) ?
311             $orig_args{parse_where} :
312             $class->get_global_option('parse_where');
313 0           delete $orig_args{parse_where};
314              
315 0           my $parse_failures_are_fatal = $class->get_global_option('parse_where_hard');
316              
317 0           my $raw_where = $orig_args{where};
318 0           my $where_obj;
319              
320 0 0         if ($should_attempt_parse) {
321 0           my $driver = $class->driver();
322              
323 0 0         if ($parse_failures_are_fatal) {
324 0           $where_obj = $driver->parse_where($raw_where);
325             } else {
326             # Ignore exception (was asked to!)
327 0           eval { $where_obj = $driver->parse_where($raw_where); };
  0            
328             }
329             }
330              
331 0 0         if ($where_obj) {
332 0 0         $where_obj->bind_params(@{$orig_args{execargs} || []});
  0            
333 0           $args{where_obj} = $where_obj;
334             } else {
335 0           $args{raw_where} = $raw_where;
336 0   0       $args{raw_where_execargs} = $orig_args{execargs} || [];
337 0   0       $args{no_re_alias_where} = $orig_args{no_re_alias_where} || 0;
338 0           delete $orig_args{no_re_alias_where};
339             }
340             }
341 0           delete $orig_args{where};
342 0           delete $orig_args{execargs};
343             } else {
344 0           delete $orig_args{parse_where}; # Spurious in this case, but just ignore it if present
345 0           delete $orig_args{no_re_alias_where}; # Spurious in this case, but just ignore it if present
346              
347             # Must have exactly one key left, and it must be a field or column
348 0 0         if ((keys %orig_args) < 1) {
    0          
349 0           Class::ReluctantORM::Exception::Param::Missing->croak(error => 'Must provide either where clause or exactly one field or column argument', param => 'where');
350             } elsif ((keys %orig_args) > 1) {
351 0           Class::ReluctantORM::Exception::Param::Spurious->croak(error => 'Must provide either where clause or exactly one field or column argument', param => (join ',', keys %orig_args));
352             }
353 0           my $field = (keys %orig_args)[0];
354 0           my ($col_name) = $class->__to_column_name($field);
355 0 0         unless ($col_name) {
356 0           Class::ReluctantORM::Exception::Param::Spurious->croak(error => "$field is not a field or column", param => $field);
357             }
358              
359 0           my $col = Column->new(
360             column => $col_name,
361             table => $args{with}{__upper_table},
362             );
363 0           my $param = Param->new();
364 0           $param->bind_value($orig_args{$field});
365              
366 0           my $crit = Criterion->new('=', $col, $param);
367 0           my $where = Where->new($crit);
368              
369 0           $args{where_obj} = $where;
370 0           delete $orig_args{$field};
371             }
372              
373 0 0         if (%orig_args) {
374             # Should have nothing left
375 0           Class::ReluctantORM::Exception::Param::Spurious->croak(error => "Ended up with leftover options to search/fetch_deep", param => join ',', keys %orig_args);
376             }
377              
378 0           return %args;
379             }
380              
381              
382             =begin devnotes
383              
384             This forces the with clause to be in 0.4 syntax.
385              
386             So....
387             with => {
388             pirates => {
389             booties => {}
390             }
391             }
392             becomes
393             with => {
394             pirates => {
395             with => {
396             booties => {}
397             }
398             }
399             }
400              
401              
402             =cut
403              
404             sub __dq_normalize_with {
405 0     0     my $class = shift;
406 0   0       my $with = shift || {};
407              
408             # These were all introduced in 0.4
409 0           my %zero_four_attrs = map { $_ => 1 } qw(join_type join_on where execargs with class relationship);
  0            
410              
411 0           my %relations = %{$class->relationships};
  0            
412 0 0         if ($DEBUG_FD > 2) { print STDERR __FILE__ . ':' . __LINE__ . " - have relationships for $class:\n" . Dumper([keys %relations]); }
  0            
413 0           foreach my $rel_name (keys %$with) {
414             # Permit __upper_table annotation
415 0 0         next if ($rel_name eq '__upper_table');
416              
417             # Each key must be the name of a relation
418 0 0         unless (exists $relations{$rel_name}) {
419 0           Class::ReluctantORM::Exception::Param::BadValue->croak(error => "$rel_name is not a relationship of $class", param => 'with', frames => 3);
420             }
421              
422             # Boost into 0.4 mode if needed
423 0           my $is_zero_four_mode = 1;
424 0           my $any_zero_four_mode = 0;
425 0 0         for my $attr (keys %{$with->{$rel_name} || {}}) {
  0            
426 0   0       $is_zero_four_mode &&= exists $zero_four_attrs{$attr};
427 0   0       $any_zero_four_mode ||= exists $zero_four_attrs{$attr};
428             }
429 0 0 0       if ($any_zero_four_mode && !$is_zero_four_mode) {
430 0           Class::ReluctantORM::Exception::Param::BadValue->croak
431             (
432             error => "The 'with' argument under '$rel_name' contains a mix of advanced CRO with options and unrecognized other keys (which might be typos, or might be relationships). Mixing 'simple with' and 'advanced with' syntax is not permitted - either fix your typo, or move the relation down a level under an additional 'with'. See perldoc Class::ReluctantORM::Manual::Prefetching. ",
433             param => 'with',
434 0           value => join(',', keys %{$with->{$rel_name}}),
435             frames => 3,
436             );
437             }
438 0 0         unless ($is_zero_four_mode) {
439             # OK, boost to 0.4 mode
440 0   0       $with->{$rel_name} = { with => ($with->{$rel_name} || {} )};
441             }
442              
443             # Set relationship hint
444 0           $with->{$rel_name}->{relationship} = $relations{$rel_name};
445              
446             # Recurse
447 0           my $linked_class = $relations{$rel_name}->linked_class();
448             # HasLazy, possibly others in the future do not have a linked class
449 0 0         if ($linked_class) {
  0 0          
450 0           $with->{$rel_name}{with} = $linked_class->__dq_normalize_with($with->{$rel_name}{with});
451             } elsif (keys %{$with->{$rel_name}{with}}) {
452 0           Class::ReluctantORM::Exception::Param::BadValue->croak(error => "$rel_name is a " . $relations{$rel_name}->type . " which cannot have children in the with tree, but you provided " . join(',', keys %{$with->{$rel_name}{with}}), param => 'with', frames => 3);
  0            
453             }
454              
455             }
456              
457 0           return $with;
458             }
459              
460             =begin devnotes
461              
462             At this point, %args should have
463             where => a Class::ReluctantORM::SQL::Where object, with unreconciled columns and tables
464             - OR -
465             raw_where => an unparsed SQL string
466             raw_where_execargs => execargs or an empty arrayref
467             order_by => an OrderBy object or undef
468             limit => an int or undef
469             offset => an int or undef
470             with => version 0.4+ syntax with relationships included
471              
472             Should return a fully reconciled Class::ReluctantORM::SQL object.
473              
474             =cut
475              
476             sub __dq_build_sql {
477 0     0     my $self = shift;
478 0           my %args = @_;
479              
480 0           my $sql = Class::ReluctantORM::SQL->new('SELECT');
481 0   0       my $class = ref($self) || $self;
482             # Should have a FROM, created when we ran dq_check_args
483 0           $sql->from($args{from});
484              
485              
486             # Most output columns will be added when reconcile() is called
487             # But we have to be sure that we add any extra columns requested by relationships
488             # (Since zero-join relations won't appear in the FROM clause, reconcile won't know about it)
489 0           my @rels = __get_rels_from_with($args{with});
490 0           foreach my $rel (__get_rels_from_with($args{with})) {
491 0           foreach my $col ($rel->additional_output_sql_columns) {
492 0           $sql->add_output($col);
493             }
494             }
495              
496             # Set where clause
497 0 0         if ($args{raw_where}) {
498 0           $sql->raw_where($args{raw_where});
499 0           $sql->_raw_where_execargs($args{raw_where_execargs});
500 0 0         if (defined $args{no_re_alias_where}) {
501 0           $sql->_raw_where_pristine($args{no_re_alias_where});
502             }
503             } else {
504 0           $sql->where($args{where_obj});
505             }
506              
507              
508             # Set order_by
509 0           $sql->order_by($args{order_by});
510              
511             # Set limit and offset
512 0 0         if (defined $args{limit}) {
513 0           $sql->limit($args{limit});
514 0 0         if (defined $args{offset}) {
515 0           $sql->offset($args{offset});
516             }
517             }
518              
519 0           return $sql;
520             }
521              
522             sub __get_rels_from_with {
523 0     0     my $with = shift;
524 0 0         return () unless $with;
525              
526 0           my @rels;
527              
528 0           foreach my $rel_name (keys %{$with}) {
  0            
529 0 0         next if $rel_name eq '__upper_table';
530 0           push @rels, $with->{$rel_name}->{relationship};
531 0           push @rels, __get_rels_from_with($with->{$rel_name}->{with})
532             }
533 0           return @rels;
534             }
535              
536             1;
537