File Coverage

lib/UR/Context/LoadingIterator.pm
Criterion Covered Total %
statement 147 208 70.6
branch 81 120 67.5
condition 53 72 73.6
subroutine 11 13 84.6
pod n/a
total 292 413 70.7


line stmt bran cond sub pod time code
1             package UR::Context::LoadingIterator;
2              
3 266     266   1049 use strict;
  266         380  
  266         6907  
4 266     266   921 use warnings;
  266         351  
  266         5744  
5              
6 266     266   909 use UR::Context;
  266         362  
  266         1280  
7              
8 266     266   5671 use List::MoreUtils qw(any);
  266         388  
  266         2416  
9              
10             our $VERSION = "0.46"; # UR $VERSION;
11              
12             # A helper package for UR::Context to handling queries which require loading
13             # data from outside the current context. It is responsible for collating
14             # cached objects and incoming objects. When create_iterator() is used in
15             # application code, this is the iterator that gets returned
16             #
17             # These are normal Perl objects, not UR objects, so they get regular
18             # refcounting and scoping
19              
20             our @CARP_NOT = qw( UR::Context );
21              
22             # A boolean flag used in the loading iterator to control whether we need to
23             # inject loaded objects into other loading iterators' cached lists
24             my $is_multiple_loading_iterators = 0;
25              
26             my %all_loading_iterators;
27              
28              
29             # The set of objects returned by an iterator is initially determined when the
30             # iterator is created, but the final determination of membership happens when
31             # the object is about to be returned from the iterator's next() method.
32             # In practice, this means that an object matches the BoolExpr at iterator
33             # creation, and no longer matches when that object is about to be returned,
34             # it will not be returned.
35             #
36             # If an object does not match the bx when the iterator is created, it will
37             # not be returned even if it later changes to match before the iterator is
38             # exhausted.
39             #
40             # If an object changes so that it's sort order changes after the iterator is
41             # created but before it is returned by the iterator, the object will be
42             # returned in the order it had at iterator creation time.
43              
44             # Finally, the LoadingIterator will throw an exception if an object matches
45             # the BoolExpr at iterator creation time, but is deleted when next() is about
46             # to return it (ie. isa UR::DeletedRef). Since DeletedRef's die any time you
47             # try to use them, the object sorters can't sort them. Instead, we'll just
48             # punt and throw an exception ourselves if we come across one.
49             #
50             # This seems like the least suprising thing to do, but there are other solutions:
51             # 1) just plain don't return the deleted object
52             # 2) use signal_change to register a callback which will remove objects being deleted
53             # from all the in-process iterator @$cached lists (accomplishes the same as #1).
54             # For completeness, this may imply that other signal_change callbacks would remove
55             # objects that no longer match rules for in-process iterators, and that means that
56             # next() returns things true at the time next() is called, not when the iterator
57             # is created.
58             # 3) Put in some additional infrastructure so we can pull out the ID of a deleted
59             # object. That lets us call $next_object->id at the end of the closure, and return these
60             # deleted objects back to the user. Problem being that the user then can't really
61             # do anything with them. But it would be consistent about returning _all_ objects
62             # that matched the rule at iterator creation time
63             # 4) Like #3, but just always return the deleted object before any underlying_context
64             # object, and then don't try to get its ID at the end if the iterator if it's deleted
65              
66              
67              
68             sub _create {
69 2023     2023   3884 my($class, $cached, $context, $normalized_rule, $data_source, $this_get_serial ) = @_;
70              
71 2023         5613 my $limit = $normalized_rule->template->limit;
72 2023         4770 my $offset = $normalized_rule->template->offset;
73 2023         3068 my $db_results_should_be_complete = 1;
74              
75 2023 100 100     14164 if (($offset or defined($limit))
    100 100        
      66        
76             and ( ! $data_source->does_support_limit_offset($normalized_rule)
77 40     40   62 or any { $_->__changes__ } @$cached
78             )
79             ) {
80             # If there are any cached objects, then the asked-for offset may not necessarily
81             # be the offset that applies to data in the database. And since the offset is not
82             # meaningful, neither is the limit. Consider a query matching these objects with
83             # limit => 2, offset => 1
84             # In memory: 1 2
85             # In DB : 3 4 5 6
86             # The result should be (2, 3). If we kept the -offset in the DB's SQL, we would
87             # have missed object 3.
88             # Similarly, if any DB rows exist as objects with changed data, then rows returnd
89             # from the DB might not be included in the results, and the supplied -limit could
90             # keep us from reading rows that should be returned
91 14         89 my %filters = $normalized_rule->params_list;
92 14         43 delete @filters{'-limit', '-offset'};
93 14         43 $normalized_rule = UR::BoolExpr->resolve_normalized($normalized_rule->subject_class_name, %filters);
94 14         46 $db_results_should_be_complete = 0;
95              
96             } elsif ($offset) {
97             # Also apply the offset to the list of cached objects.
98 5 100       19 if ($offset > @$cached) {
99 2         5 @$cached = ();
100             } else {
101 3         8 splice(@$cached, 0, $offset);
102             }
103 5         8 undef($offset); # Now don't have to deal with offset below in the iterator
104             }
105              
106 2023         8596 my $underlying_context_iterator = $context->_create_import_iterator_for_underlying_context(
107             $normalized_rule, $data_source, $this_get_serial);
108              
109 2000         6295 my $is_monitor_query = $context->monitor_query;
110              
111             # These are captured by the closure...
112 2000         2624 my($last_loaded_id, $next_obj_current_context, $next_obj_underlying_context);
113              
114 2000         5642 my $object_sorter = $normalized_rule->template->sorter();
115              
116 2000         5352 my $bx_subject_class = $normalized_rule->subject_class_name;
117              
118             # Collection of object IDs that were read from the DB query. These objects are for-sure
119             # not deleted, even though a cached object for it might have been turned into a ghost or
120             # had its properties changed
121 2000         2632 my %db_seen_ids_that_are_not_deleted;
122              
123             # Collection of object IDs that were read from the cached object list and haven't been
124             # seen in the lsit of results from the database (yet). It could be missing from the DB
125             # results because that row has been deleted, because the DB row still exists but has been
126             # changed since we loaded it and now doesn't match the BoolExp, or because we're sorting
127             # results by something other than just ID, that sorted property has been changed in the DB
128             # and we haven't come across this row yet but will before.
129             #
130             # The short story is that if there is anything in this hash when the underlying context iterator
131             # is exhausted, then the ID-ed object is really deleted, and should be an exception
132             my %changed_objects_that_might_be_db_deleted;
133              
134 2000         2307 my $underlying_context_objects_count = 0;
135 2000         2260 my $cached_objects_count = 0;
136              
137             # knowing if an object's changed properties are one of the rule's order-by
138             # properties helps later on in the loading process of detecting deleted DB rows
139 2000         2283 my %order_by_properties;
140 2000 100       4130 if ($normalized_rule->template->order_by) {
141 96         140 %order_by_properties = map { $_ => 1 } @{ $normalized_rule->template->order_by };
  96         346  
  96         232  
142             }
143             my $change_is_order_by_property = sub {
144 41     41   190 foreach my $prop_name ( shift->_changed_property_names ) {
145 43 100       259 return 1 if exists($order_by_properties{$prop_name});
146             }
147 22         91 return;
148 2000         8991 };
149 2000         5088 my %bx_filter_properties = map { $_ => 1 } $normalized_rule->template->_property_names;
  2995         7599  
150             my $change_is_bx_filter_property = sub {
151 6     6   15 foreach my $prop_name ( shift->_changed_property_names ) {
152 6 50       46 return 1 if exists($bx_filter_properties{$prop_name});
153             }
154 0         0 return;
155 2000         6382 };
156              
157 2000         2291 my $me_loading_iterator_as_string; # See note below the closure definition
158             my $loading_iterator = sub {
159              
160 105297 100 100 105297   175660 return if (defined($limit) and !$limit);
161 105279         63435 my $next_object;
162              
163             PICK_NEXT_OBJECT_FOR_LOADING:
164 105279         140660 while (! defined($next_object)) {
165 105424 100 100     297563 if ($underlying_context_iterator && ! defined($next_obj_underlying_context)) {
166 105221         160927 ($next_obj_underlying_context) = $underlying_context_iterator->(1);
167              
168 105208 50 33     177552 $underlying_context_objects_count++ if ($is_monitor_query and defined($next_obj_underlying_context));
169              
170 105208 100       142078 if (defined($next_obj_underlying_context)) {
171 103248 100 100     377583 if ($next_obj_underlying_context->isa('UR::DeletedRef')) {
    100          
172             # This object is deleted in the current context and not yet committed
173             # skip it and pick again
174 1         124 $next_obj_underlying_context = undef;
175 1         3 redo PICK_NEXT_OBJECT_FOR_LOADING;
176             } elsif ($next_obj_underlying_context->__changes__
177             and
178             $change_is_order_by_property->($next_obj_underlying_context)
179             ) {
180 8 100       28 unless (delete $changed_objects_that_might_be_db_deleted{$next_obj_underlying_context->id}) {
181 6         17 $db_seen_ids_that_are_not_deleted{$next_obj_underlying_context->id} = 1;
182             }
183 8         16 $next_obj_underlying_context = undef;
184 8         15 redo PICK_NEXT_OBJECT_FOR_LOADING;
185             }
186             }
187             }
188              
189 105402 100       145355 unless (defined $next_obj_current_context) {
190 105219         111409 ($next_obj_current_context) = shift @$cached;
191 105219 50 33     159789 $cached_objects_count++ if ($is_monitor_query and $next_obj_current_context);
192             }
193 105402 100 100     157760 if (defined($next_obj_current_context) and $next_obj_current_context->isa('UR::DeletedRef')) {
194 1         11 my $obj_to_complain_about = $next_obj_current_context;
195             # undef it in case the user traps the exception, next time we'll pull another off the list
196 1         1 $next_obj_current_context = undef;
197 1         73 Carp::croak("Attempt to fetch an object which matched $normalized_rule when the iterator was created, "
198             . "but was deleted in the meantime:\n"
199             . Data::Dumper::Dumper($obj_to_complain_about) );
200             }
201              
202 105401 100 66     281865 if (!defined($next_obj_underlying_context)) {
    50          
203              
204 2113 50       3967 if ($is_monitor_query) {
205 0         0 $context->_log_query_for_rule($bx_subject_class,
206             $normalized_rule,
207             "QUERY: loaded $underlying_context_objects_count object(s) total from underlying context.");
208             }
209 2113         2299 $underlying_context_iterator = undef;
210              
211             # Anything left in this hash when the DB iterator is exhausted are object we expected to
212             # see by now and must be deleted. If any of these object have changes then
213             # the __merge below will throw an exception
214 2113 100       59249 if ($db_results_should_be_complete) {
215 2110         5292 foreach my $problem_obj (values(%changed_objects_that_might_be_db_deleted)) {
216 2         8 $context->__merge_db_data_with_existing_object($bx_subject_class, $problem_obj, undef, []);
217             }
218             }
219              
220             }
221             elsif (defined($last_loaded_id)
222             and
223             $last_loaded_id eq $next_obj_underlying_context->id)
224             {
225             # during a get() with -hints or is_many+is_optional (ie. something with an
226             # outer join), it's possible that the join can produce the same main object
227             # as it's chewing through the (possibly) multiple objects joined to it.
228             # Since the objects will be returned sorted by their IDs, we only have to
229             # remember the last one we saw
230             # FIXME - is this still true now that the underlying context iterator and/or
231             # object fabricator hold off on returning any objects until all the related
232             # joined data bas been loaded?
233 0         0 $next_obj_underlying_context = undef;
234 0         0 redo PICK_NEXT_OBJECT_FOR_LOADING;
235             }
236              
237             # decide which pending object to return next
238             # both the cached list and the list from the database are sorted separately but with
239             # equivalent algorithms (we hope).
240             #
241             # we're collating these into one return stream here
242              
243 105399         104557 my $comparison_result = undef;
244 105399 100 100     274196 if (defined($next_obj_underlying_context) && defined($next_obj_current_context)) {
245 919         2306 $comparison_result = $object_sorter->($next_obj_underlying_context, $next_obj_current_context);
246             }
247              
248 105399         68714 my $next_obj_underlying_context_id;
249 105399 100       195078 $next_obj_underlying_context_id = $next_obj_underlying_context->id if (defined $next_obj_underlying_context);
250 105399         84496 my $next_obj_current_context_id;
251 105399 100       127773 $next_obj_current_context_id = $next_obj_current_context->id if (defined $next_obj_current_context);
252              
253             # This if() section is for when the in-memory and DB iterators return the same
254             # object at the same time.
255 105399 100 100     506567 if (
    100 100        
    100 100        
    50 66        
      66        
      66        
      33        
256             defined($next_obj_underlying_context)
257             and defined($next_obj_current_context)
258             and $comparison_result == 0 # $next_obj_underlying_context->id eq $next_obj_current_context->id
259             ) {
260             # Both objects sort the same. Since the ID properties are always last in the sort order list,
261             # this means both objects must be the same object.
262 684 50       1217 $context->_log_query_for_rule($bx_subject_class, $normalized_rule, "QUERY: loaded object was already cached") if ($is_monitor_query);
263 684         672 $next_object = $next_obj_current_context;
264 684         636 $next_obj_current_context = undef;
265 684         657 $next_obj_underlying_context = undef;
266             }
267              
268             # This if() section is for when the DB iterator's object sorts first
269             elsif (
270             defined($next_obj_underlying_context)
271             and (
272             (!defined($next_obj_current_context))
273             or
274             ($comparison_result < 0) # ($next_obj_underlying_context->id le $next_obj_current_context->id)
275             )
276             ) {
277             # db object sorts first
278             # If we deleted it from memory the DB would not have given it back.
279             # So it either failed to match the BX now, or one of the order-by parameters changed
280 102553 100       128785 if ($next_obj_underlying_context->__changes__) {
281            
282             # See if one of the changes is an order-by property
283 3 50       8 if ($change_is_order_by_property->($next_obj_underlying_context)) {
    50          
284             # If the object has changes, and one of the changes is one of the
285             # order-by properties, then the object will:
286             # 1) Already have appeared as $next_obj_current_context.
287             # it will be in $changed_objects_that_might_be_db_deleted - remove it from that list
288             # 2) Will appear later as $next_obj_current_context.
289             # Mark here that it's not deleted
290 0 0       0 unless (delete $changed_objects_that_might_be_db_deleted{$next_obj_underlying_context_id}) {
291 0         0 $db_seen_ids_that_are_not_deleted{$next_obj_underlying_context_id} = 1;
292             }
293             } elsif ($change_is_bx_filter_property->($next_obj_underlying_context)) {
294             # If the object has any changes, then it will appear in the cached object list in
295             # $next_object_current_context at the appropriate time. For the case where the
296             # object no longer matches the BoolExpr, then the appropriate time is never.
297             # Discard this object from the DB and pick again
298 3         4 $next_obj_underlying_context = undef;
299 3         7 redo PICK_NEXT_OBJECT_FOR_LOADING;
300             } else {
301             # some other kind of change?
302 0         0 $next_object = $next_obj_underlying_context;
303 0         0 $next_obj_underlying_context = undef;
304 0         0 next PICK_NEXT_OBJECT_FOR_LOADING;
305             }
306             } else {
307             # If the object has no changes, it must be something newly brought into the system.
308 102550         68596 $next_object = $next_obj_underlying_context;
309 102550         70134 $next_obj_underlying_context = undef;
310 102550         99456 next PICK_NEXT_OBJECT_FOR_LOADING;
311             }
312             }
313              
314             # This if() section is for when the in-memory iterator's object sorts first
315             elsif (
316             defined($next_obj_current_context)
317             and (
318             (!defined($next_obj_underlying_context))
319             or
320             ($comparison_result > 0) # ($next_obj_underlying_context->id ge $next_obj_current_context->id)
321             )
322             ) {
323             # The cached object sorts first
324             # Either it was changed in memory, in the DB or both
325             # In addition, the change could have been to an order-by property, one of the
326             # properties in the BoolExpr, or both
327              
328 209 100 100     2219 if (! $next_obj_current_context->isa('UR::Object::Set') # Sets aren't really from the underlying context
329             and
330             $context->object_exists_in_underlying_context($next_obj_current_context)
331             ) {
332 84 100       184 if ($next_obj_current_context->__changes__) {
333 14 100       34 if ($change_is_order_by_property->($next_obj_current_context)) {
    50          
334              
335             # This object is expected to exist in the underlying context, has changes, and at
336             # least one of those changes is to an order-by property
337             #
338             # if it's in %db_seen_ids_that_are_not_deleted, then it was seen earlier
339             # from the DB, and can now be removed from that hash.
340 11 100       32 unless (delete $db_seen_ids_that_are_not_deleted{$next_obj_current_context_id}) {
341             # If not in that list, then add it to the list of things we might see later
342             # in the DB iterator. If we don't see it by the end if the iterator, it
343             # must have been deleted from the DB. At that time, we'll throw an exception.
344             # It's later than we'd like, since the caller has already gotten ahold of the
345             # object, but better late than never. The alternative is to do an id-only
346             # query right now, but that would be inefficient.
347             #
348             # We could avoid storing this if we could verify that the db_committed/db_saved_uncommitted
349             # values did NOT match the BoolExpr, but this will suffice for now.
350 8         17 $changed_objects_that_might_be_db_deleted{$next_obj_current_context_id} = $next_obj_current_context;
351             }
352             # In any case, return the cached object.
353 11         15 $next_object = $next_obj_current_context;
354 11         16 $next_obj_current_context = undef;
355 11         20 next PICK_NEXT_OBJECT_FOR_LOADING;
356             }
357             elsif ($change_is_bx_filter_property->($next_obj_current_context)) {
358             # The change was that the object originally did not the filter, but since being
359             # loaded it's been changed so it now matches the filter. The DB iterator isn't
360             # returning the object since the DB's copy doesn't match the filter.
361 3         3 delete $db_seen_ids_that_are_not_deleted{$next_obj_current_context_id};
362 3         3 $next_object = $next_obj_current_context;
363 3         1 $next_obj_current_context = undef;
364 3         6 next PICK_NEXT_OBJECT_FOR_LOADING;
365             }
366             else {
367             # The change is not an order-by property. This object must have been deleted
368             # from the DB. The call to __merge below will throw an exception
369 0         0 $context->__merge_db_data_with_existing_object($bx_subject_class, $next_obj_current_context, undef, []);
370 0         0 $next_obj_current_context = undef;
371 0         0 redo PICK_NEXT_OBJECT_FOR_LOADING;
372             }
373              
374             } else {
375             # This cached object has no changes, so the database must have changed.
376             # It could be deleted, no longer match the BoolExpr, or have changes in an order-by property
377              
378 70 50       242 if (delete $db_seen_ids_that_are_not_deleted{$next_obj_current_context_id}) {
    100          
379             # We saw this already on the DB iterator. It's not deleted. Go ahead and return it
380 0         0 $next_object = $next_obj_current_context;
381 0         0 $next_obj_current_context = undef;
382 0         0 next PICK_NEXT_OBJECT_FOR_LOADING;
383              
384             }
385             elsif ($normalized_rule->is_id_only) {
386             # If the query is id-only, and we didn't see the DB object at the same time, then
387             # the DB row must have been deleted. Changing the PK columns in the DB are logically
388             # the same as deleting the old object and creating/defineing a new one in UR.
389             #
390             # The __merge will delete the cached object, then pick again
391 21         98 $context->__merge_db_data_with_existing_object($bx_subject_class, $next_obj_current_context, undef, []);
392 21         28 $next_obj_current_context = undef;
393 21         44 redo PICK_NEXT_OBJECT_FOR_LOADING;
394              
395             } else {
396             # Force an ID-only query to the underying context
397 49         172 my $requery_obj = $context->reload($bx_subject_class, id => $next_obj_current_context_id);
398 49 100       102 if ($requery_obj) {
399             # In any case, the DB iterator will pull it up at the appropriate time,
400             # and since the object has no changes, it will be returned to the caller then.
401             # Discard this in-memory object and pick again
402 28         27 $next_obj_current_context = undef;
403 28         48 redo PICK_NEXT_OBJECT_FOR_LOADING;
404             } else {
405             # We've now confirmed that the object in the DB is really gone
406             # NOTE: the reload() has already performed the __merge (implying deletion)
407             # in the above branch "elsif ($normalized_rule->is_id_only)" so we don't need
408             # to __merge/delete it here
409 21         23 $next_obj_current_context = undef;
410 21         49 redo PICK_NEXT_OBJECT_FOR_LOADING;
411             }
412             }
413             }
414             } else {
415             # The object does not exist in the underlying context. It must be
416             # a newly created object.
417 125         178 $next_object = $next_obj_current_context;
418 125         151 $next_obj_current_context = undef;
419 125         244 next PICK_NEXT_OBJECT_FOR_LOADING;
420             }
421              
422             } elsif (!defined($next_obj_current_context)
423             and
424             !defined($next_obj_underlying_context)
425             ) {
426             # Both iterators are exhausted. Bail out
427 1953         2361 $next_object = undef;
428 1953         2233 $last_loaded_id = undef;
429 1953         3260 last PICK_NEXT_OBJECT_FOR_LOADING;
430              
431             } else {
432             # Couldn't decide which to pick next? Something has gone horribly wrong.
433             # We're using other vars to hold the objects and setting
434             # $next_obj_current_context/$next_obj_underlying_context to undef so if
435             # the caller is trapping exceptions, this iterator will pick new objects next time
436 0         0 my $current_problem_obj = $next_obj_current_context;
437 0         0 my $underlying_problem_obj = $next_obj_underlying_context;
438 0         0 $next_obj_current_context = undef;
439 0         0 $next_obj_underlying_context = undef;
440 0         0 $next_object = undef;
441 0         0 Carp::croak("Loading iterator internal error. Could not pick a next object for loading.\n"
442             . "Next object underlying context: " . Data::Dumper::Dumper($underlying_problem_obj)
443             . "\nNext object current context: ". Data::Dumper::Dumper($current_problem_obj));
444            
445             }
446              
447 684 50       1426 return unless defined $next_object;
448              
449             # end while ! $next_object
450             } continue {
451 103373 100 66     384405 if (defined($next_object) and defined($offset) and $offset) {
      100        
452 63         58 $offset--;
453 63         93 $next_object = undef;
454             }
455             }
456              
457 105263 100       189940 $last_loaded_id = $next_object->id if (defined $next_object);
458              
459 105263 100       136030 $limit-- if defined $limit;
460              
461 105263         184486 return $next_object;
462 2000         14111 }; # end of the closure
463              
464 2000         4896 bless $loading_iterator, $class;
465 2000         11280 Sub::Name::subname($class . '__loading_iterator_closure__', $loading_iterator);
466              
467             # Inside the closure, it needs to know its own address, but without holding a real reference
468             # to itself - otherwise the closure would never go out of scope, the destructor would never
469             # get called, and the list of outstanding loaders would never get pruned. This way, the closure
470             # holds a reference to the string version of its address, which is the only thing it really
471             # needed anyway
472 2000         4961 $me_loading_iterator_as_string = $loading_iterator . '';
473              
474 2000         8084 $all_loading_iterators{$me_loading_iterator_as_string} =
475             [ $me_loading_iterator_as_string,
476             $normalized_rule,
477             $object_sorter,
478             $cached,
479             \$underlying_context_objects_count,
480             \$cached_objects_count,
481             $context,
482             ];
483              
484 2000 100       5535 $is_multiple_loading_iterators = 1 if (keys(%all_loading_iterators) > 1);
485              
486 2000         8691 return $loading_iterator;
487             } # end _create()
488              
489              
490              
491             sub DESTROY {
492 2000     2000   9143 my $self = shift;
493              
494 2000         4293 my $iter_data = $all_loading_iterators{$self};
495 2000 50       5493 if ($iter_data->[0] eq $self) {
496             # that's me!
497              
498             # Items in the listref are: $loading_iterator_string, $rule, $object_sorter, $cached,
499             # \$underlying_context_objects_count, \$cached_objects_count, $context
500              
501 2000         2784 my $context = $iter_data->[6];
502 2000 50 33     8516 if ($context and $context->monitor_query) {
503 0         0 my $rule = $iter_data->[1];
504 0         0 my $count = ${$iter_data->[4]} + ${$iter_data->[5]};
  0         0  
  0         0  
505 0         0 $context->_log_query_for_rule($rule->subject_class_name, $rule, "QUERY: Query complete after returning $count object(s) for rule $rule.");
506 0         0 $context->_log_done_elapsed_time_for_rule($rule);
507             }
508 2000         4233 delete $all_loading_iterators{$self};
509 2000 100       68697 $is_multiple_loading_iterators = 0 if (keys(%all_loading_iterators) < 2);
510              
511             } else {
512 0         0 Carp::carp('A loading iterator went out of scope, but could not be found in the registered list of iterators');
513             }
514             }
515              
516              
517             # Used by the loading itertor to inject a newly loaded object into another
518             # loading iterator's @$cached list. This is to handle the case where the user creates
519             # an iterator which will load objects from the DB. Before all the data from that
520             # iterator is read, another get() or iterator is created that covers (some of) the same
521             # objects which get pulled into the object cache, and the second request is run to
522             # completion. Since the underlying context iterator has been changed to never return
523             # objects currently cached, the first iterator would have incorrectly skipped ome objects that
524             # were not loaded when the first iterator was created, but later got loaded by the second.
525             sub _inject_object_into_other_loading_iterators {
526 0     0   0 my($self, $new_object, $iterator_to_skip) = @_;
527              
528             ITERATOR:
529 0         0 foreach my $iter_name ( keys %all_loading_iterators ) {
530 0 0       0 next if $iter_name eq $iterator_to_skip; # That's me! Don't insert into our own @$cached this way
531             my($loading_iterator, $rule, $object_sorter, $cached)
532 0         0 = @{$all_loading_iterators{$iter_name}};
  0         0  
533 0 0       0 if ($rule->evaluate($new_object)) {
534              
535 0         0 my $cached_list_len = @$cached;
536 0         0 for(my $i = 0; $i < $cached_list_len; $i++) {
537 0         0 my $cached_object = $cached->[$i];
538 0 0       0 next if $cached_object->isa('UR::DeletedRef');
539              
540 0         0 my $comparison = $object_sorter->($new_object, $cached_object);
541              
542 0 0       0 if ($comparison < 0) {
    0          
543             # The new object sorts sooner than this one. Insert it into the list
544 0         0 splice(@$cached, $i, 0, $new_object);
545 0         0 next ITERATOR;
546             } elsif ($comparison == 0) {
547             # This object is already in the list
548 0         0 next ITERATOR;
549             }
550             }
551              
552             # It must go at the end...
553 0         0 push @$cached, $new_object;
554             }
555             } # end foreach
556             }
557              
558              
559             # Reverse of _inject_object_into_other_loading_iterators(). Used when one iterator detects that
560             # a previously loaded object no longer exists in the underlying context/datasource
561             sub _remove_object_from_other_loading_iterators {
562 21     21   31 my($self, $disappearing_object, $iterator_to_skip) = @_;
563              
564             ITERATOR:
565 21         57 foreach my $iter_name ( keys %all_loading_iterators ) {
566 42 50 33     102 next if(! defined $iterator_to_skip or ($iter_name eq $iterator_to_skip)); # That's me! Don't remove into our own @$cached this way
567             my($loading_iterator, $rule, $object_sorter, $cached)
568 0           = @{$all_loading_iterators{$iter_name}};
  0            
569 0 0 0       next if (defined($iterator_to_skip)
570             and $loading_iterator eq $iterator_to_skip); # That's me! Don't insert into our own @$cached this way
571 0 0         if ($rule->evaluate($disappearing_object)) {
572              
573 0           my $cached_list_len = @$cached;
574 0           for(my $i = 0; $i < $cached_list_len; $i++) {
575 0           my $cached_object = $cached->[$i];
576 0 0         next if $cached_object->isa('UR::DeletedRef');
577              
578 0           my $comparison = $object_sorter->($disappearing_object, $cached_object);
579              
580 0 0         if ($comparison == 0) {
    0          
581             # That's the one, remove it from the list
582 0           splice(@$cached, $i, 1);
583 0           next ITERATOR;
584             } elsif ($comparison < 0) {
585             # past the point where we expect to find this object
586 0           next ITERATOR;
587             }
588             }
589             }
590             } # end foreach
591             }
592              
593              
594             # Returns true if any of the object's changed properites are keys
595             # in the passed-in hashref. Used by the Loading Iterator to find out if
596             # a change is one of the order-by properties of a bx
597             sub _changed_property_in_hash {
598 0     0     my($self,$object,$hash) = @_;
599              
600 0           foreach my $prop_name ( $object->_changed_property_names ) {
601 0 0         return 1 if (exists $hash->{$prop_name});
602             }
603 0           return;
604             }
605             1;
606              
607