Sponsoring The Perl Toolchain Summit 2025: Help make this important event another success Learn more

package UR::Context;
# Methods related to the import iterator (part of the loading process).
#
# They are broken out here for readability purposes. The methods still live
# in the UR::Context namespace.
use strict;
our $VERSION = "0.47"; # UR $VERSION;
# A wrapper around the method of the same name in UR::DataSource::* to iterate over the
# possible data sources involved in a query. The easy case (a query against a single data source)
# will return the $primary_template data structure. If the query involves more than one data source,
# then this method also returns a list containing triples (@addl_loading_info) where each member is:
# 1) The secondary data source name
# 2) a listref of delegated properties joining the primary class to the secondary class
# 3) a rule template applicable against the secondary data source
sub _resolve_query_plan_for_ds_and_bxt {
my($self,$primary_data_source,$rule_template) = @_;
my $primary_query_plan = $primary_data_source->_resolve_query_plan($rule_template);
unless ($primary_query_plan->{'joins_across_data_sources'}) {
# Common, easy case
return $primary_query_plan;
}
my @addl_loading_info;
foreach my $secondary_data_source_id ( keys %{$primary_query_plan->{'joins_across_data_sources'}} ) {
my $this_ds_delegations = $primary_query_plan->{'joins_across_data_sources'}->{$secondary_data_source_id};
my %seen_properties;
foreach my $delegated_property ( @$this_ds_delegations ) {
my $delegated_property_name = $delegated_property->property_name;
next if ($seen_properties{$delegated_property_name}++);
my $operator = $rule_template->operator_for($delegated_property_name);
$operator ||= '='; # FIXME - shouldn't the template return this for us?
my @secondary_params = ($delegated_property->to . ' ' . $operator);
my $class_meta = UR::Object::Type->get($delegated_property->class_name);
my $relation_property = $class_meta->property_meta_for_name($delegated_property->via);
my $secondary_class = $relation_property->data_type;
# we can also add in any properties in the property's joins that also appear in the rule
my @property_pairs = $relation_property->get_property_name_pairs_for_join();
foreach my $pair ( @property_pairs ) {
my($primary_property, $secondary_property) = @$pair;
next if ($seen_properties{$primary_property}++);
next unless ($rule_template->specifies_value_for($primary_property));
my $operator = $rule_template->operator_for($primary_property);
$operator ||= '=';
push @secondary_params, "$secondary_property $operator";
}
my $secondary_rule_template = UR::BoolExpr::Template->resolve($secondary_class, @secondary_params);
# FIXME there should be a way to collect all the requests for the same datasource together...
# FIXME - currently in the process of switching to object-based instead of class-based data sources
# For now, data sources are still singleton objects, so this get() will work. When we're fully on
# regular-object-based data sources, then it'll probably change to UR::DataSource->get($secondary_data_source_id);
my $secondary_data_source = UR::DataSource->get($secondary_data_source_id) || $secondary_data_source_id->get();
push @addl_loading_info,
$secondary_data_source,
[$delegated_property],
$secondary_rule_template;
}
}
return ($primary_query_plan, @addl_loading_info);
}
# Used by _create_secondary_loading_comparators to convert a rule against the primary data source
# to a rule that can be used against a secondary data source
# FIXME this might be made simpler be leaning on infer_property_value_from_rule()?
sub _create_secondary_rule_from_primary {
my($self,$primary_rule, $delegated_properties, $secondary_rule_template) = @_;
my @secondary_values;
my %seen_properties; # FIXME - we've already been over this list in _resolve_query_plan_for_ds_and_bxt()...
# FIXME - is there ever a case where @$delegated_properties will be more than one item?
foreach my $property ( @$delegated_properties ) {
my $value = $primary_rule->value_for($property->property_name);
my $secondary_property_name = $property->to;
my $pos = $secondary_rule_template->value_position_for_property_name($secondary_property_name);
$secondary_values[$pos] = $value;
$seen_properties{$property->property_name}++;
my $class_meta = $property->class_meta;
my $via_property = $class_meta->property_meta_for_name($property->via);
my @pairs = $via_property->get_property_name_pairs_for_join();
foreach my $pair ( @pairs ) {
my($primary_property_name, $secondary_property_name) = @$pair;
next if ($seen_properties{$primary_property_name}++);
$value = $primary_rule->value_for($primary_property_name);
next unless $value;
$pos = $secondary_rule_template->value_position_for_property_name($secondary_property_name);
$secondary_values[$pos] = $value;
}
}
my $secondary_rule = $secondary_rule_template->get_rule_for_values(@secondary_values);
return $secondary_rule;
}
# Since we'll be appending more "columns" of data to the listrefs returned by
# the primary datasource's query, we need to apply fixups to the column positions
# to all the secondary loading templates
# The column_position and object_num offsets needed for the next call of this method
# are returned
sub _fixup_secondary_loading_template_column_positions {
my($self,$primary_loading_templates, $secondary_loading_templates, $column_position_offset, $object_num_offset) = @_;
if (! defined($column_position_offset) or ! defined($object_num_offset)) {
$column_position_offset = 0;
foreach my $tmpl ( @{$primary_loading_templates} ) {
$column_position_offset += scalar(@{$tmpl->{'column_positions'}});
}
$object_num_offset = scalar(@{$primary_loading_templates});
}
my $this_template_column_count;
foreach my $tmpl ( @$secondary_loading_templates ) {
foreach ( @{$tmpl->{'column_positions'}} ) {
$_ += $column_position_offset;
}
foreach ( @{$tmpl->{'id_column_positions'}} ) {
$_ += $column_position_offset;
}
$tmpl->{'object_num'} += $object_num_offset;
$this_template_column_count += scalar(@{$tmpl->{'column_positions'}});
}
return ($column_position_offset + $this_template_column_count,
$object_num_offset + scalar(@$secondary_loading_templates) );
}
# For queries that have to hit multiple data sources, this method creates two lists of
# closures. The first is a list of object fabricators, where the loading templates
# have been given fixups to the column positions (see _fixup_secondary_loading_template_column_positions())
# The second is a list of closures for each data source (the @addl_loading_info stuff
# from _resolve_query_plan_for_ds_and_bxt) that's able to compare the row loaded from the
# primary data source and see if it joins to a row from this secondary datasource's database
sub _create_secondary_loading_closures {
my($self, $primary_template, $rule, @addl_loading_info) = @_;
my $loading_templates = $primary_template->{'loading_templates'};
# Make a mapping of property name to column positions returned by the primary query
my %primary_query_column_positions;
foreach my $tmpl ( @$loading_templates ) {
my $property_name_count = scalar(@{$tmpl->{'property_names'}});
for (my $i = 0; $i < $property_name_count; $i++) {
my $property_name = $tmpl->{'property_names'}->[$i];
my $pos = $tmpl->{'column_positions'}->[$i];
$primary_query_column_positions{$property_name} = $pos;
}
}
my @secondary_object_importers;
my @addl_join_comparators;
# used to shift the apparent column position of the secondary loading template info
my ($column_position_offset,$object_num_offset);
while (@addl_loading_info) {
my $secondary_data_source = shift @addl_loading_info;
my $this_ds_delegations = shift @addl_loading_info;
my $secondary_rule_template = shift @addl_loading_info;
my $secondary_rule = $self->_create_secondary_rule_from_primary (
$rule,
$this_ds_delegations,
$secondary_rule_template,
);
$secondary_data_source = $secondary_data_source->resolve_data_sources_for_rule($secondary_rule);
my $secondary_template = $self->_resolve_query_plan_for_ds_and_bxt($secondary_data_source,$secondary_rule_template);
# sets of triples where the first in the triple is the column index in the
# $secondary_db_row (in the join_comparator closure below), the second is the
# index in the $next_db_row. And the last is a flag indicating if we should
# perform a numeric comparison. This way we can preserve the order the comparisons
# should be done in
my @join_comparison_info;
foreach my $property ( @$this_ds_delegations ) {
# first, map column names in the joined class to column names in the primary class
my %foreign_property_name_map;
my @this_property_joins = $property->_resolve_join_chain();
foreach my $join ( @this_property_joins ) {
last if ($join->{foreign_class}->isa('UR::Value') and $join eq $this_property_joins[-1]);
my @source_names = @{$join->{'source_property_names'}};
my @foreign_names = @{$join->{'foreign_property_names'}};
@foreign_property_name_map{@foreign_names} = @source_names;
}
# Now, find out which numbered column in the result query maps to those names
my $secondary_loading_templates = $secondary_template->{'loading_templates'};
foreach my $tmpl ( @$secondary_loading_templates ) {
my $property_name_count = scalar(@{$tmpl->{'property_names'}});
for (my $i = 0; $i < $property_name_count; $i++) {
my $property_name = $tmpl->{'property_names'}->[$i];
if ($foreign_property_name_map{$property_name}) {
# This is the one we're interested in... Where does it come from in the primary query?
my $column_position = $tmpl->{'column_positions'}->[$i];
# What are the types involved?
my $primary_query_column_name = $foreign_property_name_map{$property_name};
my $primary_property_class_meta = $primary_template->{'class_name'}->__meta__;
my $primary_property_meta = $primary_property_class_meta->property_meta_for_name($primary_query_column_name);
unless ($primary_property_meta) {
Carp::croak("Can't resolve property metadata for property '$primary_query_column_name' of class ".$primary_template->{'class_name'});
}
my $secondary_class_meta = $secondary_template->{'class_name'}->__meta__;
my $secondary_property_meta = $secondary_class_meta->property_meta_for_name($property_name);
unless ($secondary_property_meta) {
Carp::croak("Can't resolve property metadata for property '$property_name' of class ".$secondary_template->{'class_name'});
}
my $comparison_type;
if ($primary_property_meta->is_numeric && $secondary_property_meta->is_numeric) {
$comparison_type = 1;
}
my $comparison_position;
if (exists $primary_query_column_positions{$primary_query_column_name} ) {
$comparison_position = $primary_query_column_positions{$primary_query_column_name};
} else {
# This isn't a real column we can get from the data source. Maybe it's
# in the constant_property_names of the primary_loading_template?
unless (grep { $_ eq $primary_query_column_name}
@{$loading_templates->[0]->{'constant_property_names'}}) {
die sprintf("Can't resolve datasource comparison to join %s::%s to %s:%s",
$primary_template->{'class_name'}, $primary_query_column_name,
$secondary_template->{'class_name'}, $property_name);
}
my $comparison_value = $rule->value_for($primary_query_column_name);
unless (defined $comparison_value) {
$comparison_value = $self->infer_property_value_from_rule($primary_query_column_name, $rule);
}
$comparison_position = \$comparison_value;
}
push @join_comparison_info, $column_position,
$comparison_position,
$comparison_type;
}
}
}
}
my $secondary_db_iterator = $secondary_data_source->create_iterator_closure_for_rule($secondary_rule);
my $secondary_db_row;
# For this closure, pass in the row we just loaded from the primary DB query.
# This one will return the data from this secondary DB's row if the passed-in
# row successfully joins to this secondary db iterator. It returns an empty list
# if there were no matches, and returns false if there is no more data from the query
my $join_comparator = sub {
my $next_db_row = shift; # From the primary DB
READ_DB_ROW:
while(1) {
return unless ($secondary_db_iterator);
unless ($secondary_db_row) {
($secondary_db_row) = $secondary_db_iterator->();
unless($secondary_db_row) {
# No more data to load
$secondary_db_iterator = undef;
return;
}
}
for (my $i = 0; $i < @join_comparison_info; $i += 3) {
my $secondary_column = $join_comparison_info[$i];
my $primary_column = $join_comparison_info[$i+1];
my $is_numeric = $join_comparison_info[$i+2];
my $comparison;
if (ref $primary_column) {
# This was one of those constant value items
if ($is_numeric) {
$comparison = $secondary_db_row->[$secondary_column] <=> $$primary_column;
} else {
$comparison = $secondary_db_row->[$secondary_column] cmp $$primary_column;
}
} else {
if ($join_comparison_info[$i+2]) {
$comparison = $secondary_db_row->[$secondary_column] <=> $next_db_row->[$primary_column];
} else {
$comparison = $secondary_db_row->[$secondary_column] cmp $next_db_row->[$primary_column];
}
}
if ($comparison < 0) {
# less than, get the next row from the secondary DB
$secondary_db_row = undef;
redo READ_DB_ROW;
} elsif ($comparison == 0) {
# This one was the same, keep looking at the others
} else {
# greater-than, there's no match for this primary DB row
return 0;
}
}
# All the joined columns compared equal, return the data
return $secondary_db_row;
}
};
Sub::Name::subname('UR::Context::__join_comparator(closure)__', $join_comparator);
push @addl_join_comparators, $join_comparator;
# And for the object importer/fabricator, here's where we need to shift the column order numbers
# over, because these closures will be called after all the db iterators' rows are concatenated
# together. We also need to make a copy of the loading_templates list so as to not mess up the
# class' notion of where the columns are
# FIXME - it seems wasteful that we need to re-created this each time. Look into some way of using
# the original copy that lives in $primary_template->{'loading_templates'}? Somewhere else?
my @secondary_loading_templates;
foreach my $tmpl ( @{$secondary_template->{'loading_templates'}} ) {
my %copy;
foreach my $key ( keys %$tmpl ) {
my $value_to_copy = $tmpl->{$key};
if (ref($value_to_copy) eq 'ARRAY') {
$copy{$key} = [ @$value_to_copy ];
} elsif (ref($value_to_copy) eq 'HASH') {
$copy{$key} = { %$value_to_copy };
} else {
$copy{$key} = $value_to_copy;
}
}
push @secondary_loading_templates, \%copy;
}
($column_position_offset,$object_num_offset) =
$self->_fixup_secondary_loading_template_column_positions($primary_template->{'loading_templates'},
\@secondary_loading_templates,
$column_position_offset,$object_num_offset);
#my($secondary_rule_template,@secondary_values) = $secondary_rule->get_template_and_values();
my @secondary_values = $secondary_rule->values();
foreach my $secondary_loading_template ( @secondary_loading_templates ) {
my $secondary_object_importer = UR::Context::ObjectFabricator->create_for_loading_template(
$self,
$secondary_loading_template,
$secondary_template,
$secondary_rule,
$secondary_rule_template,
\@secondary_values,
$secondary_data_source
);
next unless $secondary_object_importer;
push @secondary_object_importers, $secondary_object_importer;
}
}
return (\@secondary_object_importers, \@addl_join_comparators);
}
# This returns an iterator that is used to bring objects in from an underlying
# context into this context.
sub _create_import_iterator_for_underlying_context {
my ($self, $rule, $dsx, $this_get_serial) = @_;
# TODO: instead of taking a data source, resolve this internally.
# The underlying context itself should be responsible for its data sources.
# Make an iterator for the primary data source.
# Primary here meaning the one for the class we're explicitly requesting.
# We may need to join to other data sources to complete the query.
my ($db_iterator)
= $dsx->create_iterator_closure_for_rule($rule);
my ($rule_template, @values) = $rule->template_and_values();
my ($query_plan,@addl_loading_info) = $self->_resolve_query_plan_for_ds_and_bxt($dsx,$rule_template);
my $class_name = $query_plan->{class_name};
my $group_by = $rule_template->group_by;
my $order_by = $rule_template->order_by;
my $aggregate = $rule_template->aggregate;
if (my $sub_typing_property) {
# When the rule has a property specified which indicates a specific sub-type, catch this and re-call
# this method recursively with the specific subclass name.
my ($rule_template, @values) = $rule->template_and_values();
my $rule_template_specifies_value_for_subtype = $query_plan->{rule_template_specifies_value_for_subtype};
my $class_table_name = $query_plan->{class_table_name};
warn "Implement me carefully";
if ($rule_template_specifies_value_for_subtype) {
my $sub_classification_meta_class_name = $query_plan->{sub_classification_meta_class_name};
my $value = $rule->value_for($sub_typing_property);
my $type_obj = $sub_classification_meta_class_name->get($value);
if ($type_obj) {
my $subclass_name = $type_obj->subclass_name($class_name);
if ($subclass_name and $subclass_name ne $class_name) {
#$rule = $subclass_name->define_boolexpr($rule->params_list, $sub_typing_property => $value);
$rule = UR::BoolExpr->resolve_normalized($subclass_name, $rule->params_list, $sub_typing_property => $value);
return $self->_create_import_iterator_for_underlying_context($rule,$dsx,$this_get_serial);
}
}
else {
die "No $value for $class_name?\n";
}
}
elsif (not $class_table_name) {
die "No longer supported!";
my $rule = UR::BoolExpr->resolve(
$class_name,
$rule_template->get_rule_for_values(@values)->params_list,
);
return $self->_create_import_iterator_for_underlying_context($rule,$dsx,$this_get_serial)
}
else {
# continue normally
# the logic below will handle sub-classifying each returned entity
}
}
my $loading_templates = $query_plan->{loading_templates};
my $sub_typing_property = $query_plan->{sub_typing_property};
my $next_db_row;
my $rows = 0; # number of rows the query returned
my $recursion_desc = $query_plan->{recursion_desc};
my($rule_template_without_recursion_desc, $rule_template_id_without_recursion);
my($rule_without_recursion_desc, $rule_id_without_recursion);
# These get set if you're doing a -recurse query, and the underlying data source doesn't support recursion
my($by_hand_recursive_rule_template,$by_hand_recursive_source_property,@by_hand_recursive_source_values,$by_hand_recursing_iterator);
if ($recursion_desc) {
$rule_template_without_recursion_desc = $query_plan->{rule_template_without_recursion_desc};
$rule_template_id_without_recursion = $rule_template_without_recursion_desc->id;
$rule_without_recursion_desc = $rule_template_without_recursion_desc->get_rule_for_values(@values);
$rule_id_without_recursion = $rule_without_recursion_desc->id;
if ($query_plan->{'recurse_resolution_by_iteration'}) {
# The data source does not support a recursive query. Accomplish the same thing by
# recursing back into _create_import_iterator_for_underlying_context for each level
my $this;
($this,$by_hand_recursive_source_property) = @$recursion_desc;
my @extra;
$by_hand_recursive_rule_template = UR::BoolExpr::Template->resolve($class_name, "$this in");
$by_hand_recursive_rule_template->recursion_desc($recursion_desc);
if (!$by_hand_recursive_rule_template or @extra) {
Carp::croak("Can't resolve recursive query: Class $class_name cannot filter by one or more properties: "
. join(', ', @extra));
}
}
}
my $rule_id = $rule->id;
my $rule_template_id = $rule_template->id;
my $needs_further_boolexpr_evaluation_after_loading = $query_plan->{'needs_further_boolexpr_evaluation_after_loading'};
my %subordinate_iterator_for_class;
# TODO: move the creation of the fabricators into the query plan object initializer.
# instead of making just one import iterator, we make one per loading template
# we then have our primary iterator use these to fabricate objects for each db row
my @object_fabricators;
if ($group_by) {
# returning sets for each sub-group instead of instance objects...
my $division_point = scalar(@$group_by)-1;
my $subset_template = $rule_template->_template_for_grouped_subsets();
my $set_class = $class_name . '::Set';
my @aggregate_properties = ($aggregate ? @$aggregate : ());
unshift(@aggregate_properties, 'count') unless (grep { $_ eq 'count' } @aggregate_properties);
my $fab_subref = sub {
my $row = $_[0];
my @group_values = @$row[0..$division_point];
my $ss_rule = $subset_template->get_rule_for_values(@values, @group_values);
my $set = $set_class->get($ss_rule->id);
unless ($set) {
Carp::croak("Failed to fabricate $set_class for rule $ss_rule");
}
my $aggregates = $set->{__aggregates} ||= {};
@$aggregates{@aggregate_properties} = @$row[$division_point+1..$#$row];
return $set;
};
my $object_fabricator = UR::Context::ObjectFabricator->_create(
fabricator => $fab_subref,
context => $self,
);
unshift @object_fabricators, $object_fabricator;
}
else {
# regular instances
for my $loading_template (@$loading_templates) {
my $object_fabricator =
UR::Context::ObjectFabricator->create_for_loading_template(
$self,
$loading_template,
$query_plan,
$rule,
$rule_template,
\@values,
$dsx,
);
next unless $object_fabricator;
unshift @object_fabricators, $object_fabricator;
}
}
# For joins across data sources, we need to create importers/fabricators for those
# classes, as well as callbacks used to perform the equivalent of an SQL join in
# UR-space
my @addl_join_comparators;
if (@addl_loading_info) {
if ($group_by) {
Carp::croak("cross-datasource group-by is not supported yet");
}
my($addl_object_fabricators, $addl_join_comparators) =
$self->_create_secondary_loading_closures( $query_plan,
$rule,
@addl_loading_info
);
unshift @object_fabricators, @$addl_object_fabricators;
push @addl_join_comparators, @$addl_join_comparators;
}
# To avoid calling the useless method 'fabricate' on a fabricator object for each object of each resultset row
my @object_fabricator_closures = map { $_->fabricator } @object_fabricators;
# Insert the key into all_objects_are_loaded to indicate that when we're done loading, we'll
# have everything
if ($query_plan->{'rule_matches_all'} and not $group_by) {
$class_name->all_objects_are_loaded(undef);
}
#my $is_monitor_query = $self->monitor_query();
# Make the iterator we'll return.
my $next_object_to_return;
my @object_ids_from_fabricators;
my $underlying_context_iterator = sub {
return undef unless $db_iterator;
my $primary_object_for_next_db_row;
LOAD_AN_OBJECT:
until (defined $primary_object_for_next_db_row) { # note that we return directly when the db is out of data
my ($next_db_row);
($next_db_row) = $db_iterator->() if ($db_iterator);
if (! $next_db_row and $by_hand_recursive_rule_template and @by_hand_recursive_source_values) {
# DB is out of results for this query, we need to handle recursion here in the context
# and there are values to recurse on
unless ($by_hand_recursing_iterator) {
# Do a new get() on the data source to recursively get more data
my $recurse_rule = $by_hand_recursive_rule_template->get_rule_for_values(\@by_hand_recursive_source_values);
$by_hand_recursing_iterator = $self->_create_import_iterator_for_underlying_context($recurse_rule,$dsx,$this_get_serial);
}
my $retval = $next_object_to_return;
$next_object_to_return = $by_hand_recursing_iterator->();
unless ($next_object_to_return) {
$by_hand_recursing_iterator = undef;
$by_hand_recursive_rule_template = undef;
}
return $retval;
}
unless ($next_db_row) {
$db_iterator = undef;
if ($rows == 0) {
# if we got no data at all from the sql then we give a status
# message about it and we update all_params_loaded to indicate
# that this set of parameters yielded 0 objects
my $rule_template_is_id_only = $query_plan->{rule_template_is_id_only};
if ($rule_template_is_id_only) {
my $id = $rule->value_for_id;
$UR::Context::all_objects_loaded->{$class_name}->{$id} = undef;
}
else {
$UR::Context::all_params_loaded->{$rule_template_id}->{$rule_id} = 0;
}
}
if ( $query_plan->{rule_matches_all} ) {
# No parameters. We loaded the whole class.
# Doing a load w/o a specific ID w/o custom SQL loads the whole class.
# Set a flag so that certain optimizations can be made, such as
# short-circuiting future loads of this class.
#
# If the key still exists in the all_objects_are_loaded hash, then
# we can set it to true. This is needed in the case where the user
# gets an iterator for all the objects of some class, but unloads
# one or more of the instances (be calling unload or through the
# cache pruner) before the iterator completes. If so, _abandon_object()
# will have removed the key from the hash
if (exists($UR::Context::all_objects_are_loaded->{$class_name})) {
$class_name->all_objects_are_loaded(1);
}
}
if ($recursion_desc) {
my @results = $class_name->is_loaded($rule_without_recursion_desc);
$UR::Context::all_params_loaded->{$rule_template_id_without_recursion}{$rule_id_without_recursion} = scalar(@results);
for my $object (@results) {
$object->{__load}->{$rule_template_id_without_recursion}->{$rule_id_without_recursion}++;
}
}
# Apply changes to all_params_loaded that each importer has collected
foreach (@object_fabricators) {
$_->finalize if $_;
}
# If the SQL for the subclassed items was constructed properly, then each
# of these iterators should be at the end, too. Call them one more time
# so they'll finalize their object fabricators.
foreach my $class ( keys %subordinate_iterator_for_class ) {
my $obj = $subordinate_iterator_for_class{$class}->();
if ($obj) {
# The last time this happened, it was because a get() was done on an abstract
# base class with only 'id' as a param. When the subclassified rule was
# turned into SQL in UR::DataSource::QueryPlan()
# it removed that one 'id' filter, since it assumed any class with more than
# one ID property (usually classes have a named whatever_id property, and an alias 'id'
# property) will have a rule that covered both ID properties
Carp::carp("Leftover objects in subordinate iterator for $class. This shouldn't happen, but it's not fatal...");
while ($obj = $subordinate_iterator_for_class{$class}->()) {1;}
}
}
my $retval = $next_object_to_return;
$next_object_to_return = undef;
return $retval;
}
# we count rows processed mainly for more concise sanity checking
$rows++;
# For multi-datasource queries, does this row successfully join with all the other datasources?
#
# Normally, the policy is for the data source query to return (possibly) more than what you
# asked for, and then we'd cache everything that may have been loaded. In this case, we're
# making the choice not to. Reason being that a join across databases is likely to involve
# a lot of objects, and we don't want to be stuffing our object cache with a lot of things
# we're not interested in. FIXME - in order for this to be true, then we could never query
# these secondary data sources against, say, a calculated property because we're never turning
# them into objects. FIXME - fix this by setting the $needs_further_boolexpr_evaluation_after_loading
# flag maybe?
my @secondary_data;
foreach my $callback (@addl_join_comparators) {
# FIXME - (no, not another one...) There's no mechanism for duplicating SQL join's
# behavior where if a row from a table joins to 2 rows in the secondary table, the
# first table's data will be in the result set twice.
my $secondary_db_row = $callback->($next_db_row);
unless (defined $secondary_db_row) {
# That data source has no more data, so there can be no more joins even if the
# primary data source has more data left to read
$db_iterator = undef;
$primary_object_for_next_db_row = undef;
last LOAD_AN_OBJECT;
}
unless ($secondary_db_row) {
# It returned 0
# didn't join (but there is still more data we can read later)... throw this row out.
$primary_object_for_next_db_row = undef;
redo LOAD_AN_OBJECT;
}
# $next_db_row is a read-only value from DBI, so we need to track our additional
# data seperately and smash them together before the object importer is called
push(@secondary_data, @$secondary_db_row);
}
# get one or more objects from this row of results
my $re_iterate = 0;
my @imported;
for (my $i = 0; $i < @object_fabricator_closures; $i++) {
my $object_fabricator = $object_fabricator_closures[$i];
# The usual case is that the query is just against one data source, and so the importer
# callback is just given the row returned from the DB query. For multiple data sources,
# we need to smash together the primary and all the secondary lists
my $imported_object;
#my $object_creation_time;
#if ($is_monitor_query) {
# $object_creation_time = Time::HiRes::time();
#}
if (@secondary_data) {
$imported_object = $object_fabricator->([@$next_db_row, @secondary_data]);
} else {
$imported_object = $object_fabricator->($next_db_row);
}
#if ($is_monitor_query) {
# $self->_log_query_for_rule($class_name, $rule, sprintf("QUERY: object fabricator took %.4f s",Time::HiRes::time() - $object_creation_time));
#}
if ($imported_object and not ref($imported_object)) {
# object requires sub-classsification in a way which involves different db data.
$re_iterate = 1;
}
push @imported, $imported_object;
# If the object ID for fabricator slot $i changes, then we can apply the
# all_params_loaded changes from iterators 0 .. $i-1 because we know we've
# loaded all the hangoff data related to the previous object
# remember that the last fabricator in the list is for the primary object
if (defined $imported_object and ref($imported_object)) {
if (!defined $object_ids_from_fabricators[$i]) {
$object_ids_from_fabricators[$i] = $imported_object->id;
} elsif ($object_ids_from_fabricators[$i] ne $imported_object->id) {
for (my $j = 0; $j < $i; $j++) {
$object_fabricators[$j]->apply_all_params_loaded;
}
$object_ids_from_fabricators[$i] = $imported_object->id;
}
}
}
$primary_object_for_next_db_row = $imported[-1];
# The object importer will return undef for an object if no object
# got created for that $next_db_row, and will return a string if the object
# needs to be subclassed before being returned. Don't put serial numbers on
# these
map { $_->{'__get_serial'} = $this_get_serial }
grep { defined && ref }
@imported;
if ($re_iterate and defined($primary_object_for_next_db_row) and ! ref($primary_object_for_next_db_row)) {
# It is possible that one or more objects go into subclasses which require more
# data than is on the results row. For each subclass (or set of subclasses),
# we make a more specific, subordinate iterator to delegate-to.
my $subclass_name = $primary_object_for_next_db_row;
my $subclass_meta = UR::Object::Type->get(class_name => $subclass_name);
my $table_subclass = $subclass_meta->most_specific_subclass_with_table();
my $sub_iterator = $subordinate_iterator_for_class{$table_subclass};
unless ($sub_iterator) {
#print "parallel iteration for loading $subclass_name under $class_name!\n";
my $sub_classified_rule_template = $rule_template->sub_classify($subclass_name);
my $sub_classified_rule = $sub_classified_rule_template->get_normalized_rule_for_values(@values);
$sub_iterator
= $subordinate_iterator_for_class{$table_subclass}
= $self->_create_import_iterator_for_underlying_context($sub_classified_rule,$dsx,$this_get_serial);
}
($primary_object_for_next_db_row) = $sub_iterator->();
if (! defined $primary_object_for_next_db_row) {
# the newly subclassed object
redo LOAD_AN_OBJECT;
}
} # end of handling a possible subordinate iterator delegate
unless (defined $primary_object_for_next_db_row) {
#if (!$primary_object_for_next_db_row or $rule->evaluate($primary_object_for_next_db_row)) {
redo LOAD_AN_OBJECT;
}
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)) ) {
$primary_object_for_next_db_row = undef;
redo LOAD_AN_OBJECT;
}
if ($by_hand_recursive_source_property) {
my @values = grep { defined } $primary_object_for_next_db_row->$by_hand_recursive_source_property;
push @by_hand_recursive_source_values, @values;
}
if (! defined($next_object_to_return)
or (Scalar::Util::refaddr($next_object_to_return) == Scalar::Util::refaddr($primary_object_for_next_db_row))
) {
# The first time through the iterator, we need to buffer the object until
# $primary_object_for_next_db_row is something different.
$next_object_to_return = $primary_object_for_next_db_row;
$primary_object_for_next_db_row = undef;
redo LOAD_AN_OBJECT;
}
} # end of loop until we have a defined object to return
#foreach my $object_fabricator ( @object_fabricators ) {
# # Don't apply all_params_loaded for primary fab until it's all done
# next if ($object_fabricator eq $object_fabricators[-1]);
# $object_fabricator->apply_all_params_loaded;
#}
my $retval = $next_object_to_return;
$next_object_to_return = $primary_object_for_next_db_row;
return $retval;
};
Sub::Name::subname('UR::Context::__underlying_context_iterator(closure)__', $underlying_context_iterator);
return $underlying_context_iterator;
}
1;