| 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 |  |  |  |  |  |  |  |