File Coverage

lib/UR/Object/Index.pm
Criterion Covered Total %
statement 255 304 83.8
branch 107 140 76.4
condition 60 98 61.2
subroutine 20 24 83.3
pod 1 6 16.6
total 443 572 77.4


line stmt bran cond sub pod time code
1             # Index for cached objects.
2              
3             package UR::Object::Index;
4             our $VERSION = "0.46"; # UR $VERSION;;
5 266     266   1111 use base qw(UR::Object);
  266         342  
  266         24756  
6              
7 266     266   1068 use strict;
  266         364  
  266         4366  
8 266     266   868 use warnings;
  266         302  
  266         6861  
9             require UR;
10              
11 266     266   958 use List::MoreUtils;
  266         358  
  266         2329  
12              
13             # wrapper for one of the ID properties to make it less ugly
14              
15             sub indexed_property_names
16             {
17 201819     201819 0 153427 my $self = shift;
18 201819 100       293305 unless (exists $self->{indexed_property_names}) {
19 266     266   72490 no warnings;
  266         369  
  266         83949  
20 1070         4903 $self->{indexed_property_names} = [ split(/,/,$self->{indexed_property_string}) ];
21             }
22 201819         129611 return @{$self->{indexed_property_names}};
  201819         425849  
23             }
24              
25             sub indexed_property_numericness {
26 64728     64728 0 57226 my $self = shift;
27 64728 100       116411 unless (exists $self->{indexed_property_numericness}) {
28 1070         2821 my $class_meta = $self->indexed_class_name->__meta__;
29             my @is_numeric = map {
30 1070         2481 my @props = $class_meta->_concrete_property_meta_for_class_and_name($_);
  2206         7005  
31 2206 100       7571 @props == 1
32             ? $props[0]->is_numeric
33             : 0 # multiple ID properties are treated as a string
34             }
35             $self->indexed_property_names;
36              
37 1070         2899 $self->{indexed_property_numericness} = \@is_numeric;
38             }
39 64728         57145 return @{ $self->{indexed_property_numericness} };
  64728         125789  
40             }
41              
42              
43             # the only non-id property has an accessor...
44              
45             sub data_tree
46             {
47 0 0   0 0 0 if (@_ > 1)
48             {
49 0         0 my $old = $_[0]->{data_tree};
50 0         0 my $new = $_[1];
51 0 0       0 if ($old ne $new)
52             {
53 0         0 $_[0]->{data_tree} = $new;
54 0         0 $_[0]->__signal_change__('data_tree', $old, $new);
55             }
56 0         0 return $new;
57             }
58 0         0 return $_[0]->{data_tree};
59             }
60              
61             # override create to initilize the index
62              
63             sub create {
64 1070     1070 1 1541 my $class = shift;
65            
66             # NOTE: This is called from one location in UR::Context and relies
67             # on all properties including the ID being specifically defined.
68            
69 1070         4629 my $self = $UR::Context::current->_construct_object($class, @_);
70 1070 50       3730 return unless $self;
71 1070   50     5324 $self->{data_tree} ||= {};
72            
73 1070         3392 $self->_build_data_tree;
74 1070         4321 $self->_setup_change_subscription;
75            
76 1070         4729 $self->__signal_change__("create");
77 1070         3231 return $self;
78             }
79              
80             # this does a lookup as efficiently as possible
81              
82             sub get_objects_matching
83             {
84 64728     64728 0 58932 my $self = shift;
85 64728         89475 my @values = @_;
86              
87             # The hash access below generates warnings
88             # where undef is a value. Ignore these.
89 266     266   1191 no warnings 'uninitialized';
  266         442  
  266         270263  
90              
91 64728         95348 my @hr = ($self->{data_tree});
92 64728         118776 my @is_numeric = $self->indexed_property_numericness;
93              
94 64728         343673 my $iter = List::MoreUtils::each_array(@values, @is_numeric);
95 64728         253389 while(my($value, $is_numeric) = $iter->())
96             {
97 114225         111748 my $value_ref = ref($value);
98 114225 100       203291 if($value_ref eq "HASH")
    100          
    50          
99             {
100             # property => { operator => "not like", value => "H~_WGS%", escape "~" }
101 16272 50       32984 if (my $op = $value->{operator})
102             {
103 16272         25646 $op = lc($op);
104 16272         16384 my $not = 0;
105 16272 100       54115 if ($op =~ m/^(!|not\s*)(.*)/) {
106 208         279 $not = 1;
107 208         447 $op = $2;
108             }
109              
110 16272         14243 my $result;
111              
112 16272 100 100     211569 if ($op eq '=' and !$not) {
    100 66        
    50 66        
    100 100        
    100 100        
    100 66        
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    50          
113 7         19 @hr = grep { $_ } map { $_->{$value->{'value'}} } @hr;
  7         40  
  7         23  
114             }
115             elsif ($op eq 'like')
116             {
117 111         209 my $comparison_value = $value->{value};
118 111         156 my $escape = $value->{escape};
119            
120 111         888 my $regex =
121             UR::BoolExpr::Template::PropertyComparison::Like->
122             comparison_value_and_escape_character_to_regex(
123             $comparison_value,
124             $escape
125             );
126            
127 111         133 my @thr;
128 111 100       257 if ($not)
129             {
130             # Get the values using the regular or negative match op.
131 40         63 foreach my $h (@hr) {
132 40         112 foreach my $k (sort keys %$h) {
133 56 100       131 next if $k eq ''; # an earlier undef value got saved as an empty string here
134 24 100       85 if($k !~ /$regex/) {
135 18         29 push @thr, $h->{$k};
136             }
137             }
138             }
139             }
140             else
141             {
142             # Standard positive match
143 71         127 for my $h (@hr) {
144 71         309 for my $k (sort keys %$h) {
145 240 100       372 next if $k eq ''; # an earlier undef value got saved as an empty string here
146 208 100       563 if ($k =~ /$regex/) {
147 111         182 push @thr, $h->{$k};
148             }
149             }
150             }
151             }
152 111         406 @hr = grep { $_ } @thr;
  129         296  
153             }
154             elsif ($op eq 'in' and !$not)
155             {
156 0         0 $value = $value->{value};
157 0 0       0 my $has_null = ( (grep { length($_) == 0 } @$value) ? 1 : 0);
  0         0  
158 0 0       0 if ($has_null) {
159 0         0 @hr = grep { $_ } map { @$_{@$value} } @hr;
  0         0  
  0         0  
160             } else {
161 0         0 my @value = grep { length($_) > 0 } @$value;
  0         0  
162 0         0 @hr = grep { $_ } map { @$_{@value} } @hr;
  0         0  
  0         0  
163             }
164             }
165             elsif ($op eq 'in' and $not)
166             {
167 30         48 $value = $value->{value};
168            
169             # make a hash if we got an array as a value
170             #die ">@$value<" if ref($value) eq "ARRAY";
171 30 50       109 $value = { map { $_ => 1 } @$value } if ref($value) eq "ARRAY";
  39         96  
172            
173             # if there is a single null, the not in clause will be false
174 30 100       68 if ($value->{""}) {
175 8         36 @hr = ();
176             }
177             else {
178             # return everything NOT in the hash
179 22         30 my @thr;
180 22         36 for my $h (@hr) {
181 22         54 for my $k (sort keys %$h) {
182 38 100       71 next unless length($k);
183 22 100       38 unless ($value->{$k}) {
184 5         8 push @thr, $h->{$k};
185             }
186             }
187             }
188 22         95 @hr = grep { $_ } @thr;
  5         13  
189             }
190              
191             } elsif ($op eq 'isa') {
192 32         32 my @thr;
193 32         46 foreach my $h ( @hr ) {
194 44         127 foreach my $k ( keys %$h) {
195 475 100 50     2226 if ($k->isa($value->{value}) xor $not) {
196 34         52 push @thr, $h->{$k};
197             }
198             }
199             }
200 32         74 @hr = grep { $_ } @thr;
  34         109  
201              
202             } elsif ($op eq 'true' or $op eq 'false') {
203 58   33     415 $not = (( $op eq 'true' && $not) or ($op eq 'false' && !$not));
204 58         77 my @thr;
205 58         96 foreach my $h ( @hr ) {
206 51         109 foreach my $k ( keys %$h ) {
207 82 100 75     320 if ($k xor $not) {
208 43         73 push @thr, $h->{$k};
209             }
210             }
211             }
212 58         139 @hr = grep { $_ } @thr;
  43         176  
213              
214             } elsif ($not and ($op eq '=' or !$op)) {
215 48         46 my @thr;
216 48         74 foreach my $h (@hr) {
217 48         161 foreach my $k (sort keys %$h) {
218             # An empty string for $k means the object's value was loaded as NULL
219             # and we want things like 0 != NULL to be true to match the SQL that
220             # gets generated for the same rule
221             my $t = ($k eq '')
222             ||
223             ($is_numeric
224             ? $k != $value->{value}
225 131   66     281 : $k ne $value->{value});
226 131 100       191 if ($t) {
227 107         130 push @thr, $h->{$k};
228             }
229             }
230             }
231 48         62 @hr = grep { $_ } @thr;
  107         244  
232             } elsif($op eq '>') {
233 110         128 my @thr;
234 110         163 foreach my $h (@hr) {
235 608         1140 foreach my $k (keys %$h) {
236 1650 100       1944 next if $k eq ''; # an earlier undef value got saved as an empty string here
237             my $t = $is_numeric
238             ? $k > $value->{value}
239 1610 100       1881 : $k gt $value->{value};
240 1610 100 100     3890 if ($t xor $not) {
241 1113         1276 push @thr, $h->{$k};
242             }
243             }
244             }
245 110         315 @hr = grep { $_ } @thr;
  1113         948  
246             } elsif($op eq '<') {
247 101         133 my @thr;
248 101         162 foreach my $h (@hr) {
249 365         633 foreach my $k (keys %$h) {
250 913 100       1076 next if $k eq ''; # an earlier undef value got saved as an empty string here
251             my $t = $is_numeric
252             ? $k < $value->{value}
253 873 100       1073 : $k lt $value->{value};
254 873 100 100     2214 if ($t xor $not) {
255 646         760 push @thr, $h->{$k};
256             }
257             }
258             }
259 101         277 @hr = grep { $_ } @thr;
  646         674  
260             } elsif($op eq '>=') {
261 15568         15664 my @thr;
262 15568         22464 foreach my $h (@hr) {
263 15302         36288 foreach my $k (keys %$h) {
264 15398 100       30263 next if $k eq ''; # an earlier undef value got saved as an empty string here
265             my $t = $is_numeric
266             ? $k >= $value->{value}
267 15358 50       33788 : $k ge $value->{value};
268 15358 100 100     61885 if ($t xor $not) {
269 15292         34662 push @thr, $h->{$k};
270             }
271             }
272             }
273 15568         20432 @hr = grep { $_ } @thr;
  15292         60864  
274             } elsif($op eq '<=') {
275 84         96 my @thr;
276 84         132 foreach my $h (@hr) {
277 108         404 foreach my $k (keys %$h) {
278 865 100       1032 next if $k eq ''; # an earlier undef value got saved as an empty string here
279             my $t = $is_numeric
280             ? $k <= $value->{value}
281 825 100       968 : $k le $value->{value};
282 825 100 100     2166 if ($t xor $not) {
283 176         233 push @thr, $h->{$k};
284             }
285             }
286             }
287 84         230 @hr = grep { $_ } @thr;
  176         309  
288             } elsif($op eq 'ne') {
289 2         3 my @thr;
290 2         4 foreach my $h (@hr) {
291 2         13 foreach my $k (sort keys %$h) {
292 17 50       23 next if $k eq ''; # an earlier undef value got saved as an empty string here
293 17 100 50     44 if($k ne $value->{value} xor $not) {
294 15         19 push @thr, $h->{$k};
295             }
296             }
297             }
298 2         3 @hr = grep { $_ } @thr;
  15         19  
299             } elsif($op eq '<>') {
300 0         0 my @thr;
301 0         0 foreach my $h (@hr) {
302 0         0 foreach my $k (sort keys %$h) {
303 0 0 0     0 if((length($k) and length($value->{value}) and $k ne $value->{value}) xor $not) {
      0        
      0        
304 0         0 push @thr, $h->{$k};
305             }
306             }
307             }
308 0         0 @hr = grep { $_ } @thr;
  0         0  
309             } elsif($op eq 'between') {
310 121         113 my @thr;
311 121         123 my ($min,$max) = @{ $value->{value} };
  121         253  
312 121         204 foreach my $h (@hr) {
313 121         334 foreach my $k (sort keys %$h) {
314 219 100       408 next if $k eq '';
315 131 100 66     348 my $t = $is_numeric
      100        
316             ? ( $k >= $min and $k <= $max )
317             : ( $k ge $min and $k le $max );
318 131 100 100     397 if ($t xor $not) {
319 64         96 push @thr, $h->{$k};
320             }
321             }
322             }
323 121         402 @hr = grep { $_ } @thr;
  64         161  
324             } else {
325 266     266   1487 use Data::Dumper;
  266         358  
  266         109990  
326 0         0 Carp::confess("Unknown operator in key-value pair used in index lookup for index " . Dumper($value));
327             }
328             }
329             else
330             {
331 0         0 Carp::confess("No operator specified in hashref value!" . Dumper($value));
332             }
333             }
334             elsif (not $value_ref)
335             {
336             # property => value
337 97821         101498 @hr = grep { $_ } map { $_->{$value} } @hr;
  96771         312728  
  96771         167654  
338             }
339             elsif ($value_ref eq "ARRAY")
340             {
341             # property => [ v1, v2, v3]
342 132         231 @hr = grep { $_ } map { @$_{@$value} } @hr;
  264         558  
  129         437  
343             }
344             }
345 64728         184134 return (map { values(%$_) } @hr);
  41097         335522  
346             }
347              
348              
349             # private methods
350              
351             sub _build_data_tree
352             {
353 1070     1070   1419 my $self = $_[0];
354            
355 1070         3094 my @indexed_property_names = $self->indexed_property_names;
356 1070         1783 my $hr_base = $self->{data_tree};
357            
358             # _remove_object in bulk.
359 1070         1880 %$hr_base = ();
360 1070         3685 my $indexed_class_name = $self->indexed_class_name;
361            
362 1070 50       2056 if (my @bad_properties =
363 2206         14098 grep { not $indexed_class_name->can($_) }
364             @indexed_property_names
365             ) {
366 0         0 Carp::confess(
367             "Attempt to index $indexed_class_name by properties which "
368             . "do not function: @bad_properties"
369             );
370             }
371            
372             # _add_object in bulk.
373 1070         14981 for my $object ($UR::Context::current->all_objects_loaded($indexed_class_name)) {
374 15551         9547 my(@values, $hr);
375 15551 50       18714 if (@indexed_property_names) {
376 15551 100       12258 @values = map { my $val = $object->$_; defined $val ? $val : undef } @indexed_property_names;
  26388         47129  
  26388         38611  
377 15551 50       20297 @values = (undef) unless(@values);
378             }
379 15551         10124 $hr = $hr_base;
380 15551         12266 for my $value (@values)
381             {
382 266     266   1291 no warnings 'uninitialized'; # in case $value is undef
  266         377  
  266         35243  
383 26388   100     38008 $hr->{$value} ||= {};
384 26388         21963 $hr = $hr->{$value};
385             }
386 15551         24723 my $obj_id = $object->id;
387 15551         21857 $hr->{$obj_id} = $object;
388 15551 100       37320 if (Scalar::Util::isweak($UR::Context::all_objects_loaded->{$indexed_class_name}->{$obj_id})) {
389 2         7 Scalar::Util::weaken($hr->{$obj_id});
390             }
391             }
392             }
393              
394             # FIXME maybe objects in an index should always be weakend?
395             sub weaken_reference_for_object {
396 401     401 0 274 my $self = shift;
397 401         269 my $object = shift;
398 401         229 my $overrides = shift; # FIXME copied from _remove_object - what's this for?
399              
400 266     266   1139 no warnings;
  266         378  
  266         99336  
401 401         384 my @indexed_property_names = $self->indexed_property_names;
402             my @values =
403             map
404             {
405 401         358 ($overrides && exists($overrides->{$_}))
406             ?
407 564 50 33     1334 $overrides->{$_}
408             :
409             $object->$_
410             }
411             @indexed_property_names;
412              
413 401         367 my $hr = $self->{data_tree};
414 401         222 my $value;
415 401         307 for $value (@values)
416             {
417 564         564 $hr = $hr->{$value};
418 564 50       746 return unless $hr;
419             }
420 401         538 Scalar::Util::weaken($hr->{$object->id});
421             }
422            
423            
424             sub _setup_change_subscription
425             {
426            
427 1070     1070   1593 my $self = shift;
428            
429            
430 1070         2972 my $indexed_class_name = $self->indexed_class_name;
431 1070         2405 my @indexed_property_names = $self->indexed_property_names;
432            
433 1070         1346 if (1) {
434             # This is a new indexing strategy which pays at index creation time instead of use.
435            
436 1070         3439 my @properties_to_watch = (@indexed_property_names, qw/create delete load unload/);
437             #print "making index $self->{id}\n";
438 1070         1362 for my $class ($indexed_class_name, @{ $UR::Object::Type::_init_subclasses_loaded{$indexed_class_name} }) {
  1070         2367  
439 4244         4004 for my $property (@properties_to_watch) {
440 23402   100     58236 my $index_list = $UR::Object::Index::all_by_class_name_and_property_name{$class}{$property} ||= [];
441             #print " adding to $class\n";
442 23402         23711 push @$index_list, $self;
443             }
444             }
445            
446 1070         2415 return 1;
447             }
448            
449             # This will be ignored for now.
450             # If the __signal_change__/subscription system is improved, it may be better to go back?
451            
452 0         0 my %properties_to_watch = map { $_ => 1 } (@indexed_property_names, qw/create delete load unload/);
  0         0  
453            
454             $self->{_get_change_subscription} = $indexed_class_name->create_subscription(
455             callback =>
456             sub
457             {
458 0     0   0 my ($changed_object, $changed_property, $old_value, $new_value) = @_;
459            
460             #print "got change $changed_property for $indexed_class_name: $changed_object->{id}: @_\n";
461            
462             # ensure we don't track changes for subclasses
463             #return() unless ref($changed_object) eq $indexed_class_name;
464            
465             # ensure we only add/remove for selected method calls
466 0 0       0 return() unless $properties_to_watch{$_[1]};
467            
468             #print "changing @_\n";
469            
470 0 0 0     0 $self->_remove_object(
      0        
471             $changed_object,
472             { $changed_property => $old_value }
473             ) if ($changed_property ne 'create'
474             and $changed_property ne 'load'
475             and $changed_property ne '__define__');
476            
477 0 0 0     0 $self->_add_object($changed_object) if ($changed_property ne 'delete' and $changed_property ne 'unload');
478             },
479 0         0 note => "index monitor " . $self->id,
480             priority => 0,
481             );
482             }
483              
484             sub _get_change_subscription
485             {
486             # accessor for the change subscription
487 0 0   0   0 $_[0]->{_get_change_subscription} = $_[1] if (@_ > 1);
488 0         0 return $_[0]->{_get_change_subscription};
489             }
490              
491             sub _remove_object($$)
492             {
493 266     266   1299 no warnings;
  266         358  
  266         30612  
494            
495 1275     1275   1444 my ($self, $object, $overrides) = @_;
496 1275         2259 my @indexed_property_names = $self->indexed_property_names;
497             my @values =
498             map
499             {
500 1275         1656 ($overrides && exists($overrides->{$_}))
501             ?
502 1627 100 33     6328 $overrides->{$_}
503             :
504             $object->$_
505             }
506             @indexed_property_names;
507            
508 1275         1504 my $hr = $self->{data_tree};
509 1275         1008 my $value;
510 1275         1331 for $value (@values)
511             {
512 1627         2544 $hr = $hr->{$value};
513             }
514 1275         2185 delete $hr->{$object->id};
515             }
516              
517             sub _add_object($$)
518             {
519             # We get warnings when undef converts into an empty string.
520             # For efficiency, we turn warnings off in this method.
521 266     266   1104 no warnings;
  266         354  
  266         55262  
522            
523 196933     196933   156409 my ($self, $object) = @_;
524 196933         245387 my @indexed_property_names = $self->indexed_property_names;
525 196933         223038 my @values = map { $object->$_ } @indexed_property_names;
  485400         830693  
526 196933         196676 my $hr = $self->{data_tree};
527 196933         126142 my $value;
528 196933         168841 for $value (@values)
529             {
530 485400   100     1067507 $hr->{$value} ||= {};
531 485400         457216 $hr = $hr->{$value};
532             }
533 196933         297356 $hr->{$object->id} = $object;
534            
535             # This is the exact formula used elsewhere. TODO: refactor, base on class meta
536 196933 50 33     597096 if ($UR::Context::light_cache and substr($self->indexed_class_name,0,5) ne 'App::') {
537 0           Scalar::Util::weaken($hr->{$object->id});
538             }
539             }
540              
541             sub _all_objects_indexed {
542 0     0     my $self = shift;
543              
544 0           my @object_hashes = ( $self->{data_tree} );
545              
546             # Recurse one level deep for each indexed property name
547             # and collect the hashes at that level
548 0           foreach ( $self->indexed_property_names ) {
549 0           my @new_object_hashes;
550 0           while (my $hr = shift @object_hashes) {
551 0           push @new_object_hashes, values(%$hr);
552             }
553 0           @object_hashes = @new_object_hashes;
554             }
555              
556             # The final level's values are all the objects
557 0           return map { values %$_ } @object_hashes;
  0            
558             }
559              
560             1;
561              
562             =pod
563              
564             =head1 NAME
565              
566             UR::Object::Index - Indexing system for retrieving objects by non-id properties
567              
568             =head1 DESCRIPTION
569              
570             This class implements an indexing system for objects to retrieve them quickly
571             by properties other than their ID properties. Their existence and use is
572             managed by the Context as needed, and end-users should never need to interact
573             with UR::Object::Index instances.
574              
575             Internally, they are a container for objects of the same class and a set of
576             properties used to look them up. Each time a get() is performed on a new set
577             of non-id properties, a new Index is created to handle the request for
578             objects which may already exist in the object cache,
579              
580             The data_tree inside the Index is a multi-level hash. The levels are in the
581             same order as the properties in the get request. At each level, the hash
582             keys are the values that target property has. For that level and key, all the
583             objects inside have the same value for that property. A get() by three non-id
584             properties will have a 3-level hash.
585              
586             =cut