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