File Coverage

lib/UR/Context/ImportIterator.pm
Criterion Covered Total %
statement 332 379 87.6
branch 102 140 72.8
condition 38 54 70.3
subroutine 10 10 100.0
pod n/a
total 482 583 82.6


line stmt bran cond sub pod time code
1             package UR::Context;
2              
3             # Methods related to the import iterator (part of the loading process).
4             #
5             # They are broken out here for readability purposes. The methods still live
6             # in the UR::Context namespace.
7              
8 266     266   998 use strict;
  266         337  
  266         6661  
9 266     266   906 use warnings;
  266         303  
  266         739412  
10              
11             our $VERSION = "0.46"; # UR $VERSION;
12              
13              
14             # A wrapper around the method of the same name in UR::DataSource::* to iterate over the
15             # possible data sources involved in a query. The easy case (a query against a single data source)
16             # will return the $primary_template data structure. If the query involves more than one data source,
17             # then this method also returns a list containing triples (@addl_loading_info) where each member is:
18             # 1) The secondary data source name
19             # 2) a listref of delegated properties joining the primary class to the secondary class
20             # 3) a rule template applicable against the secondary data source
21             sub _resolve_query_plan_for_ds_and_bxt {
22 2045     2045   3207 my($self,$primary_data_source,$rule_template) = @_;
23              
24 2045         7023 my $primary_query_plan = $primary_data_source->_resolve_query_plan($rule_template);
25              
26 2045 100       6093 unless ($primary_query_plan->{'joins_across_data_sources'}) {
27             # Common, easy case
28 2031         4589 return $primary_query_plan;
29             }
30              
31 14         18 my @addl_loading_info;
32 14         21 foreach my $secondary_data_source_id ( keys %{$primary_query_plan->{'joins_across_data_sources'}} ) {
  14         50  
33 14         25 my $this_ds_delegations = $primary_query_plan->{'joins_across_data_sources'}->{$secondary_data_source_id};
34              
35 14         25 my %seen_properties;
36 14         27 foreach my $delegated_property ( @$this_ds_delegations ) {
37 14         41 my $delegated_property_name = $delegated_property->property_name;
38 14 50       51 next if ($seen_properties{$delegated_property_name}++);
39              
40 14         59 my $operator = $rule_template->operator_for($delegated_property_name);
41 14   50     36 $operator ||= '='; # FIXME - shouldn't the template return this for us?
42 14         42 my @secondary_params = ($delegated_property->to . ' ' . $operator);
43              
44 14         64 my $class_meta = UR::Object::Type->get($delegated_property->class_name);
45 14         102 my $relation_property = $class_meta->property_meta_for_name($delegated_property->via);
46              
47 14         44 my $secondary_class = $relation_property->data_type;
48              
49             # we can also add in any properties in the property's joins that also appear in the rule
50 14         49 my @property_pairs = $relation_property->get_property_name_pairs_for_join();
51 14         28 foreach my $pair ( @property_pairs ) {
52 14         29 my($primary_property, $secondary_property) = @$pair;
53 14 50       49 next if ($seen_properties{$primary_property}++);
54 14 50       53 next unless ($rule_template->specifies_value_for($primary_property));
55              
56 0         0 my $operator = $rule_template->operator_for($primary_property);
57 0   0     0 $operator ||= '=';
58 0         0 push @secondary_params, "$secondary_property $operator";
59             }
60              
61 14         57 my $secondary_rule_template = UR::BoolExpr::Template->resolve($secondary_class, @secondary_params);
62              
63             # FIXME there should be a way to collect all the requests for the same datasource together...
64             # FIXME - currently in the process of switching to object-based instead of class-based data sources
65             # For now, data sources are still singleton objects, so this get() will work. When we're fully on
66             # regular-object-based data sources, then it'll probably change to UR::DataSource->get($secondary_data_source_id);
67 14   33     52 my $secondary_data_source = UR::DataSource->get($secondary_data_source_id) || $secondary_data_source_id->get();
68 14         66 push @addl_loading_info,
69             $secondary_data_source,
70             [$delegated_property],
71             $secondary_rule_template;
72             }
73             }
74              
75 14         42 return ($primary_query_plan, @addl_loading_info);
76             }
77              
78              
79             # Used by _create_secondary_loading_comparators to convert a rule against the primary data source
80             # to a rule that can be used against a secondary data source
81             # FIXME this might be made simpler be leaning on infer_property_value_from_rule()?
82             sub _create_secondary_rule_from_primary {
83 14     14   41 my($self,$primary_rule, $delegated_properties, $secondary_rule_template) = @_;
84              
85 14         15 my @secondary_values;
86             my %seen_properties; # FIXME - we've already been over this list in _resolve_query_plan_for_ds_and_bxt()...
87             # FIXME - is there ever a case where @$delegated_properties will be more than one item?
88 14         27 foreach my $property ( @$delegated_properties ) {
89 14         52 my $value = $primary_rule->value_for($property->property_name);
90              
91 14         37 my $secondary_property_name = $property->to;
92 14         38 my $pos = $secondary_rule_template->value_position_for_property_name($secondary_property_name);
93 14         30 $secondary_values[$pos] = $value;
94 14         39 $seen_properties{$property->property_name}++;
95              
96 14         53 my $class_meta = $property->class_meta;
97 14         39 my $via_property = $class_meta->property_meta_for_name($property->via);
98 14         55 my @pairs = $via_property->get_property_name_pairs_for_join();
99 14         25 foreach my $pair ( @pairs ) {
100 14         28 my($primary_property_name, $secondary_property_name) = @$pair;
101              
102 14 50       39 next if ($seen_properties{$primary_property_name}++);
103 14         39 $value = $primary_rule->value_for($primary_property_name);
104 14 50       52 next unless $value;
105              
106 0         0 $pos = $secondary_rule_template->value_position_for_property_name($secondary_property_name);
107 0         0 $secondary_values[$pos] = $value;
108             }
109             }
110              
111 14         61 my $secondary_rule = $secondary_rule_template->get_rule_for_values(@secondary_values);
112              
113 14         34 return $secondary_rule;
114             }
115              
116              
117             # Since we'll be appending more "columns" of data to the listrefs returned by
118             # the primary datasource's query, we need to apply fixups to the column positions
119             # to all the secondary loading templates
120             # The column_position and object_num offsets needed for the next call of this method
121             # are returned
122             sub _fixup_secondary_loading_template_column_positions {
123 14     14   25 my($self,$primary_loading_templates, $secondary_loading_templates, $column_position_offset, $object_num_offset) = @_;
124              
125 14 50 33     50 if (! defined($column_position_offset) or ! defined($object_num_offset)) {
126 14         16 $column_position_offset = 0;
127 14         14 foreach my $tmpl ( @{$primary_loading_templates} ) {
  14         32  
128 14         14 $column_position_offset += scalar(@{$tmpl->{'column_positions'}});
  14         35  
129             }
130 14         12 $object_num_offset = scalar(@{$primary_loading_templates});
  14         27  
131             }
132              
133 14         16 my $this_template_column_count;
134 14         23 foreach my $tmpl ( @$secondary_loading_templates ) {
135 14         17 foreach ( @{$tmpl->{'column_positions'}} ) {
  14         27  
136 32         66 $_ += $column_position_offset;
137             }
138 14         18 foreach ( @{$tmpl->{'id_column_positions'}} ) {
  14         25  
139 14         19 $_ += $column_position_offset;
140             }
141 14         22 $tmpl->{'object_num'} += $object_num_offset;
142              
143 14         13 $this_template_column_count += scalar(@{$tmpl->{'column_positions'}});
  14         29  
144             }
145              
146              
147 14         32 return ($column_position_offset + $this_template_column_count,
148             $object_num_offset + scalar(@$secondary_loading_templates) );
149             }
150              
151             # For queries that have to hit multiple data sources, this method creates two lists of
152             # closures. The first is a list of object fabricators, where the loading templates
153             # have been given fixups to the column positions (see _fixup_secondary_loading_template_column_positions())
154             # The second is a list of closures for each data source (the @addl_loading_info stuff
155             # from _resolve_query_plan_for_ds_and_bxt) that's able to compare the row loaded from the
156             # primary data source and see if it joins to a row from this secondary datasource's database
157             sub _create_secondary_loading_closures {
158 14     14   33 my($self, $primary_template, $rule, @addl_loading_info) = @_;
159              
160 14         27 my $loading_templates = $primary_template->{'loading_templates'};
161              
162             # Make a mapping of property name to column positions returned by the primary query
163 14         23 my %primary_query_column_positions;
164 14         28 foreach my $tmpl ( @$loading_templates ) {
165 14         20 my $property_name_count = scalar(@{$tmpl->{'property_names'}});
  14         26  
166 14         48 for (my $i = 0; $i < $property_name_count; $i++) {
167 50         53 my $property_name = $tmpl->{'property_names'}->[$i];
168 50         48 my $pos = $tmpl->{'column_positions'}->[$i];
169 50         110 $primary_query_column_positions{$property_name} = $pos;
170             }
171             }
172              
173 14         17 my @secondary_object_importers;
174             my @addl_join_comparators;
175              
176             # used to shift the apparent column position of the secondary loading template info
177 0         0 my ($column_position_offset,$object_num_offset);
178              
179 14         36 while (@addl_loading_info) {
180 14         24 my $secondary_data_source = shift @addl_loading_info;
181 14         30 my $this_ds_delegations = shift @addl_loading_info;
182 14         24 my $secondary_rule_template = shift @addl_loading_info;
183              
184 14         62 my $secondary_rule = $self->_create_secondary_rule_from_primary (
185             $rule,
186             $this_ds_delegations,
187             $secondary_rule_template,
188             );
189 14         54 $secondary_data_source = $secondary_data_source->resolve_data_sources_for_rule($secondary_rule);
190 14         38 my $secondary_template = $self->_resolve_query_plan_for_ds_and_bxt($secondary_data_source,$secondary_rule_template);
191              
192             # sets of triples where the first in the triple is the column index in the
193             # $secondary_db_row (in the join_comparator closure below), the second is the
194             # index in the $next_db_row. And the last is a flag indicating if we should
195             # perform a numeric comparison. This way we can preserve the order the comparisons
196             # should be done in
197 14         24 my @join_comparison_info;
198 14         33 foreach my $property ( @$this_ds_delegations ) {
199             # first, map column names in the joined class to column names in the primary class
200 14         19 my %foreign_property_name_map;
201 14         56 my @this_property_joins = $property->_resolve_join_chain();
202 14         33 foreach my $join ( @this_property_joins ) {
203 28 100 66     237 last if ($join->{foreign_class}->isa('UR::Value') and $join eq $this_property_joins[-1]);
204 14         20 my @source_names = @{$join->{'source_property_names'}};
  14         37  
205 14         43 my @foreign_names = @{$join->{'foreign_property_names'}};
  14         35  
206 14         39 @foreign_property_name_map{@foreign_names} = @source_names;
207             }
208              
209             # Now, find out which numbered column in the result query maps to those names
210 14         27 my $secondary_loading_templates = $secondary_template->{'loading_templates'};
211 14         31 foreach my $tmpl ( @$secondary_loading_templates ) {
212 14         18 my $property_name_count = scalar(@{$tmpl->{'property_names'}});
  14         30  
213 14         46 for (my $i = 0; $i < $property_name_count; $i++) {
214 32         44 my $property_name = $tmpl->{'property_names'}->[$i];
215 32 100       99 if ($foreign_property_name_map{$property_name}) {
216             # This is the one we're interested in... Where does it come from in the primary query?
217 17         27 my $column_position = $tmpl->{'column_positions'}->[$i];
218             # What are the types involved?
219 17         27 my $primary_query_column_name = $foreign_property_name_map{$property_name};
220 17         71 my $primary_property_class_meta = $primary_template->{'class_name'}->__meta__;
221 17         57 my $primary_property_meta = $primary_property_class_meta->property_meta_for_name($primary_query_column_name);
222 17 50       42 unless ($primary_property_meta) {
223 0         0 Carp::croak("Can't resolve property metadata for property '$primary_query_column_name' of class ".$primary_template->{'class_name'});
224             }
225              
226 17         56 my $secondary_class_meta = $secondary_template->{'class_name'}->__meta__;
227 17         50 my $secondary_property_meta = $secondary_class_meta->property_meta_for_name($property_name);
228 17 50       43 unless ($secondary_property_meta) {
229 0         0 Carp::croak("Can't resolve property metadata for property '$property_name' of class ".$secondary_template->{'class_name'});
230             }
231              
232 17         23 my $comparison_type;
233 17 100 100     53 if ($primary_property_meta->is_numeric && $secondary_property_meta->is_numeric) {
234 11         14 $comparison_type = 1;
235             }
236              
237 17         29 my $comparison_position;
238 17 50       46 if (exists $primary_query_column_positions{$primary_query_column_name} ) {
239 17         22 $comparison_position = $primary_query_column_positions{$primary_query_column_name};
240              
241             } else {
242             # This isn't a real column we can get from the data source. Maybe it's
243             # in the constant_property_names of the primary_loading_template?
244 0 0       0 unless (grep { $_ eq $primary_query_column_name}
  0         0  
245 0         0 @{$loading_templates->[0]->{'constant_property_names'}}) {
246             die sprintf("Can't resolve datasource comparison to join %s::%s to %s:%s",
247             $primary_template->{'class_name'}, $primary_query_column_name,
248 0         0 $secondary_template->{'class_name'}, $property_name);
249             }
250 0         0 my $comparison_value = $rule->value_for($primary_query_column_name);
251 0 0       0 unless (defined $comparison_value) {
252 0         0 $comparison_value = $self->infer_property_value_from_rule($primary_query_column_name, $rule);
253             }
254 0         0 $comparison_position = \$comparison_value;
255             }
256 17         69 push @join_comparison_info, $column_position,
257             $comparison_position,
258             $comparison_type;
259              
260              
261             }
262             }
263             }
264             }
265 14         68 my $secondary_db_iterator = $secondary_data_source->create_iterator_closure_for_rule($secondary_rule);
266              
267 14         25 my $secondary_db_row;
268             # For this closure, pass in the row we just loaded from the primary DB query.
269             # This one will return the data from this secondary DB's row if the passed-in
270             # row successfully joins to this secondary db iterator. It returns an empty list
271             # if there were no matches, and returns false if there is no more data from the query
272             my $join_comparator = sub {
273 32     32   40 my $next_db_row = shift; # From the primary DB
274             READ_DB_ROW:
275 32         30 while(1) {
276 39 50       72 return unless ($secondary_db_iterator);
277 39 100       55 unless ($secondary_db_row) {
278 21         43 ($secondary_db_row) = $secondary_db_iterator->();
279 21 100       51 unless($secondary_db_row) {
280             # No more data to load
281 8         12 $secondary_db_iterator = undef;
282 8         35 return;
283             }
284             }
285              
286 31         106 for (my $i = 0; $i < @join_comparison_info; $i += 3) {
287 34         37 my $secondary_column = $join_comparison_info[$i];
288 34         40 my $primary_column = $join_comparison_info[$i+1];
289 34         34 my $is_numeric = $join_comparison_info[$i+2];
290              
291 34         28 my $comparison;
292 34 50       54 if (ref $primary_column) {
293             # This was one of those constant value items
294 0 0       0 if ($is_numeric) {
295 0         0 $comparison = $secondary_db_row->[$secondary_column] <=> $$primary_column;
296             } else {
297 0         0 $comparison = $secondary_db_row->[$secondary_column] cmp $$primary_column;
298             }
299             } else {
300 34 100       66 if ($join_comparison_info[$i+2]) {
301 28         52 $comparison = $secondary_db_row->[$secondary_column] <=> $next_db_row->[$primary_column];
302             } else {
303 6         10 $comparison = $secondary_db_row->[$secondary_column] cmp $next_db_row->[$primary_column];
304             }
305             }
306 34 100       96 if ($comparison < 0) {
    100          
307             # less than, get the next row from the secondary DB
308 7         10 $secondary_db_row = undef;
309 7         11 redo READ_DB_ROW;
310             } elsif ($comparison == 0) {
311             # This one was the same, keep looking at the others
312             } else {
313             # greater-than, there's no match for this primary DB row
314 11         23 return 0;
315             }
316             }
317             # All the joined columns compared equal, return the data
318 13         22 return $secondary_db_row;
319             }
320 14         71 };
321 14         92 Sub::Name::subname('UR::Context::__join_comparator(closure)__', $join_comparator);
322 14         26 push @addl_join_comparators, $join_comparator;
323              
324              
325             # And for the object importer/fabricator, here's where we need to shift the column order numbers
326             # over, because these closures will be called after all the db iterators' rows are concatenated
327             # together. We also need to make a copy of the loading_templates list so as to not mess up the
328             # class' notion of where the columns are
329             # FIXME - it seems wasteful that we need to re-created this each time. Look into some way of using
330             # the original copy that lives in $primary_template->{'loading_templates'}? Somewhere else?
331 14         21 my @secondary_loading_templates;
332 14         18 foreach my $tmpl ( @{$secondary_template->{'loading_templates'}} ) {
  14         43  
333 14         18 my %copy;
334 14         64 foreach my $key ( keys %$tmpl ) {
335 126         104 my $value_to_copy = $tmpl->{$key};
336 126 100       190 if (ref($value_to_copy) eq 'ARRAY') {
    50          
337 56         111 $copy{$key} = [ @$value_to_copy ];
338             } elsif (ref($value_to_copy) eq 'HASH') {
339 0         0 $copy{$key} = { %$value_to_copy };
340             } else {
341 70         109 $copy{$key} = $value_to_copy;
342             }
343             }
344 14         37 push @secondary_loading_templates, \%copy;
345             }
346             ($column_position_offset,$object_num_offset) =
347 14         87 $self->_fixup_secondary_loading_template_column_positions($primary_template->{'loading_templates'},
348             \@secondary_loading_templates,
349             $column_position_offset,$object_num_offset);
350              
351             #my($secondary_rule_template,@secondary_values) = $secondary_rule->get_template_and_values();
352 14         49 my @secondary_values = $secondary_rule->values();
353 14         27 foreach my $secondary_loading_template ( @secondary_loading_templates ) {
354 14         82 my $secondary_object_importer = UR::Context::ObjectFabricator->create_for_loading_template(
355             $self,
356             $secondary_loading_template,
357             $secondary_template,
358             $secondary_rule,
359             $secondary_rule_template,
360             \@secondary_values,
361             $secondary_data_source
362             );
363 14 50       41 next unless $secondary_object_importer;
364 14         63 push @secondary_object_importers, $secondary_object_importer;
365             }
366              
367              
368             }
369              
370 14         54 return (\@secondary_object_importers, \@addl_join_comparators);
371             }
372              
373              
374             # This returns an iterator that is used to bring objects in from an underlying
375             # context into this context.
376             sub _create_import_iterator_for_underlying_context {
377 2054     2054   3270 my ($self, $rule, $dsx, $this_get_serial) = @_;
378              
379             # TODO: instead of taking a data source, resolve this internally.
380             # The underlying context itself should be responsible for its data sources.
381              
382             # Make an iterator for the primary data source.
383             # Primary here meaning the one for the class we're explicitly requesting.
384             # We may need to join to other data sources to complete the query.
385 2054         10430 my ($db_iterator)
386             = $dsx->create_iterator_closure_for_rule($rule);
387              
388 2031         7329 my ($rule_template, @values) = $rule->template_and_values();
389 2031         8801 my ($query_plan,@addl_loading_info) = $self->_resolve_query_plan_for_ds_and_bxt($dsx,$rule_template);
390 2031         3840 my $class_name = $query_plan->{class_name};
391              
392 2031         4788 my $group_by = $rule_template->group_by;
393 2031         5112 my $order_by = $rule_template->order_by;
394 2031         5833 my $aggregate = $rule_template->aggregate;
395              
396 2031 50       4405 if (my $sub_typing_property) {
397             # When the rule has a property specified which indicates a specific sub-type, catch this and re-call
398             # this method recursively with the specific subclass name.
399 0         0 my ($rule_template, @values) = $rule->template_and_values();
400 0         0 my $rule_template_specifies_value_for_subtype = $query_plan->{rule_template_specifies_value_for_subtype};
401 0         0 my $class_table_name = $query_plan->{class_table_name};
402              
403 0         0 warn "Implement me carefully";
404              
405 0 0       0 if ($rule_template_specifies_value_for_subtype) {
    0          
406 0         0 my $sub_classification_meta_class_name = $query_plan->{sub_classification_meta_class_name};
407 0         0 my $value = $rule->value_for($sub_typing_property);
408 0         0 my $type_obj = $sub_classification_meta_class_name->get($value);
409 0 0       0 if ($type_obj) {
410 0         0 my $subclass_name = $type_obj->subclass_name($class_name);
411 0 0 0     0 if ($subclass_name and $subclass_name ne $class_name) {
412             #$rule = $subclass_name->define_boolexpr($rule->params_list, $sub_typing_property => $value);
413 0         0 $rule = UR::BoolExpr->resolve_normalized($subclass_name, $rule->params_list, $sub_typing_property => $value);
414 0         0 return $self->_create_import_iterator_for_underlying_context($rule,$dsx,$this_get_serial);
415             }
416             }
417             else {
418 0         0 die "No $value for $class_name?\n";
419             }
420             }
421             elsif (not $class_table_name) {
422 0         0 die "No longer supported!";
423 0         0 my $rule = UR::BoolExpr->resolve(
424             $class_name,
425             $rule_template->get_rule_for_values(@values)->params_list,
426             );
427 0         0 return $self->_create_import_iterator_for_underlying_context($rule,$dsx,$this_get_serial)
428             }
429             else {
430             # continue normally
431             # the logic below will handle sub-classifying each returned entity
432             }
433             }
434              
435              
436 2031         3648 my $loading_templates = $query_plan->{loading_templates};
437 2031         2818 my $sub_typing_property = $query_plan->{sub_typing_property};
438 2031         2271 my $next_db_row;
439 2031         2480 my $rows = 0; # number of rows the query returned
440              
441 2031         2610 my $recursion_desc = $query_plan->{recursion_desc};
442 2031         2560 my($rule_template_without_recursion_desc, $rule_template_id_without_recursion);
443 0         0 my($rule_without_recursion_desc, $rule_id_without_recursion);
444             # These get set if you're doing a -recurse query, and the underlying data source doesn't support recursion
445 0         0 my($by_hand_recursive_rule_template,$by_hand_recursive_source_property,@by_hand_recursive_source_values,$by_hand_recursing_iterator);
446 2031 100       4297 if ($recursion_desc) {
447 23         42 $rule_template_without_recursion_desc = $query_plan->{rule_template_without_recursion_desc};
448 23         57 $rule_template_id_without_recursion = $rule_template_without_recursion_desc->id;
449 23         55 $rule_without_recursion_desc = $rule_template_without_recursion_desc->get_rule_for_values(@values);
450 23         46 $rule_id_without_recursion = $rule_without_recursion_desc->id;
451              
452 23 50       60 if ($query_plan->{'recurse_resolution_by_iteration'}) {
453             # The data source does not support a recursive query. Accomplish the same thing by
454             # recursing back into _create_import_iterator_for_underlying_context for each level
455 23         24 my $this;
456 23         49 ($this,$by_hand_recursive_source_property) = @$recursion_desc;
457              
458 23         22 my @extra;
459 23         93 $by_hand_recursive_rule_template = UR::BoolExpr::Template->resolve($class_name, "$this in");
460 23         70 $by_hand_recursive_rule_template->recursion_desc($recursion_desc);
461 23 50 33     44 if (!$by_hand_recursive_rule_template or @extra) {
462 0         0 Carp::croak("Can't resolve recursive query: Class $class_name cannot filter by one or more properties: "
463             . join(', ', @extra));
464             }
465             }
466             }
467              
468 2031         5270 my $rule_id = $rule->id;
469 2031         4291 my $rule_template_id = $rule_template->id;
470              
471 2031         3086 my $needs_further_boolexpr_evaluation_after_loading = $query_plan->{'needs_further_boolexpr_evaluation_after_loading'};
472              
473 2031         2446 my %subordinate_iterator_for_class;
474              
475             # TODO: move the creation of the fabricators into the query plan object initializer.
476             # instead of making just one import iterator, we make one per loading template
477             # we then have our primary iterator use these to fabricate objects for each db row
478             my @object_fabricators;
479 2031 100       4174 if ($group_by) {
480             # returning sets for each sub-group instead of instance objects...
481 27         46 my $division_point = scalar(@$group_by)-1;
482 27         146 my $subset_template = $rule_template->_template_for_grouped_subsets();
483 27         53 my $set_class = $class_name . '::Set';
484 27 100       79 my @aggregate_properties = ($aggregate ? @$aggregate : ());
485 27 100       53 unshift(@aggregate_properties, 'count') unless (grep { $_ eq 'count' } @aggregate_properties);
  24         93  
486              
487             my $fab_subref = sub {
488 32     32   38 my $row = $_[0];
489 32         95 my @group_values = @$row[0..$division_point];
490 32         149 my $ss_rule = $subset_template->get_rule_for_values(@values, @group_values);
491 32         75 my $set = $set_class->get($ss_rule->id);
492 32 50       122 unless ($set) {
493 0         0 Carp::croak("Failed to fabricate $set_class for rule $ss_rule");
494             }
495 32   100     160 my $aggregates = $set->{__aggregates} ||= {};
496 32         142 @$aggregates{@aggregate_properties} = @$row[$division_point+1..$#$row];
497 32         59 return $set;
498 27         167 };
499              
500 27         186 my $object_fabricator = UR::Context::ObjectFabricator->_create(
501             fabricator => $fab_subref,
502             context => $self,
503             );
504 27         67 unshift @object_fabricators, $object_fabricator;
505             }
506             else {
507             # regular instances
508 2004         3786 for my $loading_template (@$loading_templates) {
509 2164         12312 my $object_fabricator =
510             UR::Context::ObjectFabricator->create_for_loading_template(
511             $self,
512             $loading_template,
513             $query_plan,
514             $rule,
515             $rule_template,
516             \@values,
517             $dsx,
518             );
519 2164 50       5036 next unless $object_fabricator;
520 2164         5562 unshift @object_fabricators, $object_fabricator;
521             }
522             }
523              
524             # For joins across data sources, we need to create importers/fabricators for those
525             # classes, as well as callbacks used to perform the equivalent of an SQL join in
526             # UR-space
527 2031         2372 my @addl_join_comparators;
528 2031 100       4579 if (@addl_loading_info) {
529 14 50       75 if ($group_by) {
530 0         0 Carp::croak("cross-datasource group-by is not supported yet");
531             }
532 14         79 my($addl_object_fabricators, $addl_join_comparators) =
533             $self->_create_secondary_loading_closures( $query_plan,
534             $rule,
535             @addl_loading_info
536             );
537              
538 14         35 unshift @object_fabricators, @$addl_object_fabricators;
539 14         32 push @addl_join_comparators, @$addl_join_comparators;
540             }
541              
542             # To avoid calling the useless method 'fabricate' on a fabricator object for each object of each resultset row
543 2031         3521 my @object_fabricator_closures = map { $_->fabricator } @object_fabricators;
  2205         6865  
544              
545             # Insert the key into all_objects_are_loaded to indicate that when we're done loading, we'll
546             # have everything
547 2031 100 100     7271 if ($query_plan->{'rule_matches_all'} and not $group_by) {
548 151         736 $class_name->all_objects_are_loaded(undef);
549             }
550              
551             #my $is_monitor_query = $self->monitor_query();
552              
553             # Make the iterator we'll return.
554 2031         2583 my $next_object_to_return;
555             my @object_ids_from_fabricators;
556             my $underlying_context_iterator = sub {
557 105324 100   105324   130752 return undef unless $db_iterator;
558              
559 104149         63313 my $primary_object_for_next_db_row;
560              
561             LOAD_AN_OBJECT:
562 104149         120399 until (defined $primary_object_for_next_db_row) { # note that we return directly when the db is out of data
563              
564 105693         67084 my ($next_db_row);
565 105693 50       262598 ($next_db_row) = $db_iterator->() if ($db_iterator);
566              
567 105693 100 100     190464 if (! $next_db_row and $by_hand_recursive_rule_template and @by_hand_recursive_source_values) {
      100        
568             # DB is out of results for this query, we need to handle recursion here in the context
569             # and there are values to recurse on
570 22 100       47 unless ($by_hand_recursing_iterator) {
571             # Do a new get() on the data source to recursively get more data
572 10         43 my $recurse_rule = $by_hand_recursive_rule_template->get_rule_for_values(\@by_hand_recursive_source_values);
573 10         40 $by_hand_recursing_iterator = $self->_create_import_iterator_for_underlying_context($recurse_rule,$dsx,$this_get_serial);
574             }
575 22         31 my $retval = $next_object_to_return;
576 22         34 $next_object_to_return = $by_hand_recursing_iterator->();
577 22 100       49 unless ($next_object_to_return) {
578 10         13 $by_hand_recursing_iterator = undef;
579 10         274 $by_hand_recursive_rule_template = undef;
580             }
581 22         74 return $retval;
582             }
583              
584 105671 100       132584 unless ($next_db_row) {
585 1978         2586 $db_iterator = undef;
586              
587 1978 100       24463 if ($rows == 0) {
588             # if we got no data at all from the sql then we give a status
589             # message about it and we update all_params_loaded to indicate
590             # that this set of parameters yielded 0 objects
591              
592 796         1725 my $rule_template_is_id_only = $query_plan->{rule_template_is_id_only};
593 796 100       1888 if ($rule_template_is_id_only) {
594 93         401 my $id = $rule->value_for_id;
595 93         352 $UR::Context::all_objects_loaded->{$class_name}->{$id} = undef;
596             }
597             else {
598 703         2941 $UR::Context::all_params_loaded->{$rule_template_id}->{$rule_id} = 0;
599             }
600             }
601              
602 1978 100       5657 if ( $query_plan->{rule_matches_all} ) {
603             # No parameters. We loaded the whole class.
604             # Doing a load w/o a specific ID w/o custom SQL loads the whole class.
605             # Set a flag so that certain optimizations can be made, such as
606             # short-circuiting future loads of this class.
607             #
608             # If the key still exists in the all_objects_are_loaded hash, then
609             # we can set it to true. This is needed in the case where the user
610             # gets an iterator for all the objects of some class, but unloads
611             # one or more of the instances (be calling unload or through the
612             # cache pruner) before the iterator completes. If so, _abandon_object()
613             # will have removed the key from the hash
614 151 100       440 if (exists($UR::Context::all_objects_are_loaded->{$class_name})) {
615 139         537 $class_name->all_objects_are_loaded(1);
616             }
617             }
618              
619 1978 100       4409 if ($recursion_desc) {
620 23         87 my @results = $class_name->is_loaded($rule_without_recursion_desc);
621 23         60 $UR::Context::all_params_loaded->{$rule_template_id_without_recursion}{$rule_id_without_recursion} = scalar(@results);
622 23         46 for my $object (@results) {
623 19         42 $object->{__load}->{$rule_template_id_without_recursion}->{$rule_id_without_recursion}++;
624             }
625             }
626              
627             # Apply changes to all_params_loaded that each importer has collected
628 1978         3836 foreach (@object_fabricators) {
629 2144 50       9864 $_->finalize if $_;
630             }
631              
632             # If the SQL for the subclassed items was constructed properly, then each
633             # of these iterators should be at the end, too. Call them one more time
634             # so they'll finalize their object fabricators.
635 1978         4688 foreach my $class ( keys %subordinate_iterator_for_class ) {
636 21         69 my $obj = $subordinate_iterator_for_class{$class}->();
637 21 50       71 if ($obj) {
638             # The last time this happened, it was because a get() was done on an abstract
639             # base class with only 'id' as a param. When the subclassified rule was
640             # turned into SQL in UR::DataSource::QueryPlan()
641             # it removed that one 'id' filter, since it assummed any class with more than
642             # one ID property (usually classes have a named whatever_id property, and an alias 'id'
643             # property) will have a rule that covered both ID properties
644 0         0 Carp::carp("Leftover objects in subordinate iterator for $class. This shouldn't happen, but it's not fatal...");
645 0         0 while ($obj = $subordinate_iterator_for_class{$class}->()) {1;}
  0         0  
646             }
647             }
648              
649 1978         2450 my $retval = $next_object_to_return;
650 1978         2119 $next_object_to_return = undef;
651 1978         4989 return $retval;
652             }
653              
654             # we count rows processed mainly for more concise sanity checking
655 103693         80959 $rows++;
656             # For multi-datasource queries, does this row successfully join with all the other datasources?
657             #
658             # Normally, the policy is for the data source query to return (possibly) more than what you
659             # asked for, and then we'd cache everything that may have been loaded. In this case, we're
660             # making the choice not to. Reason being that a join across databases is likely to involve
661             # a lot of objects, and we don't want to be stuffing our object cache with a lot of things
662             # we're not interested in. FIXME - in order for this to be true, then we could never query
663             # these secondary data sources against, say, a calculated property because we're never turning
664             # them into objects. FIXME - fix this by setting the $needs_further_boolexpr_evaluation_after_loading
665             # flag maybe?
666 103693         72100 my @secondary_data;
667 103693         115980 foreach my $callback (@addl_join_comparators) {
668             # FIXME - (no, not another one...) There's no mechanism for duplicating SQL join's
669             # behavior where if a row from a table joins to 2 rows in the secondary table, the
670             # first table's data will be in the result set twice.
671 32         53 my $secondary_db_row = $callback->($next_db_row);
672 32 100       55 unless (defined $secondary_db_row) {
673             # That data source has no more data, so there can be no more joins even if the
674             # primary data source has more data left to read
675 8         10 $db_iterator = undef;
676 8         49 $primary_object_for_next_db_row = undef;
677 8         17 last LOAD_AN_OBJECT;
678             }
679 24 100       40 unless ($secondary_db_row) {
680             # It returned 0
681             # didn't join (but there is still more data we can read later)... throw this row out.
682 11         11 $primary_object_for_next_db_row = undef;
683 11         22 redo LOAD_AN_OBJECT;
684             }
685             # $next_db_row is a read-only value from DBI, so we need to track our additional
686             # data seperately and smash them together before the object importer is called
687 13         32 push(@secondary_data, @$secondary_db_row);
688             }
689              
690             # get one or more objects from this row of results
691 103674         81148 my $re_iterate = 0;
692 103674         65532 my @imported;
693 103674         159971 for (my $i = 0; $i < @object_fabricator_closures; $i++) {
694 104126         90193 my $object_fabricator = $object_fabricator_closures[$i];
695              
696             # The usual case is that the query is just against one data source, and so the importer
697             # callback is just given the row returned from the DB query. For multiple data sources,
698             # we need to smash together the primary and all the secondary lists
699 104126         72177 my $imported_object;
700              
701             #my $object_creation_time;
702             #if ($is_monitor_query) {
703             # $object_creation_time = Time::HiRes::time();
704             #}
705              
706 104126 100       117352 if (@secondary_data) {
707 26         107 $imported_object = $object_fabricator->([@$next_db_row, @secondary_data]);
708             } else {
709 104100         178594 $imported_object = $object_fabricator->($next_db_row);
710             }
711              
712             #if ($is_monitor_query) {
713             # $self->_log_query_for_rule($class_name, $rule, sprintf("QUERY: object fabricator took %.4f s",Time::HiRes::time() - $object_creation_time));
714             #}
715              
716 104113 100 100     296893 if ($imported_object and not ref($imported_object)) {
717             # object requires sub-classsification in a way which involves different db data.
718 60         73 $re_iterate = 1;
719             }
720 104113         102842 push @imported, $imported_object;
721              
722             # If the object ID for fabricator slot $i changes, then we can apply the
723             # all_params_loaded changes from iterators 0 .. $i-1 because we know we've
724             # loaded all the hangoff data related to the previous object
725             # remember that the last fabricator in the list is for the primary object
726 104113 100 100     275076 if (defined $imported_object and ref($imported_object)) {
727 103911 100       230032 if (!defined $object_ids_from_fabricators[$i]) {
    100          
728 1312         3422 $object_ids_from_fabricators[$i] = $imported_object->id;
729             } elsif ($object_ids_from_fabricators[$i] ne $imported_object->id) {
730 102370         145468 for (my $j = 0; $j < $i; $j++) {
731 85         251 $object_fabricators[$j]->apply_all_params_loaded;
732             }
733 102370         132089 $object_ids_from_fabricators[$i] = $imported_object->id;
734             }
735             }
736             }
737              
738 103661         89895 $primary_object_for_next_db_row = $imported[-1];
739              
740             # The object importer will return undef for an object if no object
741             # got created for that $next_db_row, and will return a string if the object
742             # needs to be subclassed before being returned. Don't put serial numbers on
743             # these
744 103911         141035 map { $_->{'__get_serial'} = $this_get_serial }
745 103661 100       100434 grep { defined && ref }
  104113         291298  
746             @imported;
747              
748 103661 50 66     168077 if ($re_iterate and defined($primary_object_for_next_db_row) and ! ref($primary_object_for_next_db_row)) {
      66        
749             # It is possible that one or more objects go into subclasses which require more
750             # data than is on the results row. For each subclass (or set of subclasses),
751             # we make a more specific, subordinate iterator to delegate-to.
752              
753 60         86 my $subclass_name = $primary_object_for_next_db_row;
754              
755 60         194 my $subclass_meta = UR::Object::Type->get(class_name => $subclass_name);
756 60         283 my $table_subclass = $subclass_meta->most_specific_subclass_with_table();
757 60         89 my $sub_iterator = $subordinate_iterator_for_class{$table_subclass};
758 60 100       121 unless ($sub_iterator) {
759             #print "parallel iteration for loading $subclass_name under $class_name!\n";
760 21         150 my $sub_classified_rule_template = $rule_template->sub_classify($subclass_name);
761 21         83 my $sub_classified_rule = $sub_classified_rule_template->get_normalized_rule_for_values(@values);
762             $sub_iterator
763 21         100 = $subordinate_iterator_for_class{$table_subclass}
764             = $self->_create_import_iterator_for_underlying_context($sub_classified_rule,$dsx,$this_get_serial);
765             }
766 60         128 ($primary_object_for_next_db_row) = $sub_iterator->();
767 60 100       141 if (! defined $primary_object_for_next_db_row) {
768             # the newly subclassed object
769 13         43 redo LOAD_AN_OBJECT;
770             }
771              
772             } # end of handling a possible subordinate iterator delegate
773              
774 103648 100       131997 unless (defined $primary_object_for_next_db_row) {
775             #if (!$primary_object_for_next_db_row or $rule->evaluate($primary_object_for_next_db_row)) {
776 121         197 redo LOAD_AN_OBJECT;
777             }
778              
779 103527 50 100     309187 if ( !$group_by and (ref($primary_object_for_next_db_row) ne $class_name) and (not $primary_object_for_next_db_row->isa($class_name)) ) {
      66        
780 0         0 $primary_object_for_next_db_row = undef;
781 0         0 redo LOAD_AN_OBJECT;
782             }
783              
784 103527 100       119656 if ($by_hand_recursive_source_property) {
785 17         62 my @values = grep { defined } $primary_object_for_next_db_row->$by_hand_recursive_source_property;
  17         43  
786 17         25 push @by_hand_recursive_source_values, @values;
787             }
788              
789 103527 100 100     511077 if (! defined($next_object_to_return)
790             or (Scalar::Util::refaddr($next_object_to_return) == Scalar::Util::refaddr($primary_object_for_next_db_row))
791             ) {
792             # The first time through the iterator, we need to buffer the object until
793             # $primary_object_for_next_db_row is something different.
794 1399         1411 $next_object_to_return = $primary_object_for_next_db_row;
795 1399         1700 $primary_object_for_next_db_row = undef;
796 1399         2683 redo LOAD_AN_OBJECT;
797             }
798              
799              
800             } # end of loop until we have a defined object to return
801              
802             #foreach my $object_fabricator ( @object_fabricators ) {
803             # # Don't apply all_params_loaded for primary fab until it's all done
804             # next if ($object_fabricator eq $object_fabricators[-1]);
805             # $object_fabricator->apply_all_params_loaded;
806             #}
807              
808 102136         84558 my $retval = $next_object_to_return;
809 102136         68545 $next_object_to_return = $primary_object_for_next_db_row;
810 102136         165153 return $retval;
811 2031         17371 };
812              
813 2031         10837 Sub::Name::subname('UR::Context::__underlying_context_iterator(closure)__', $underlying_context_iterator);
814 2031         9629 return $underlying_context_iterator;
815             }
816              
817              
818              
819             1;