File Coverage

blib/lib/UR/Object/Set.pm
Criterion Covered Total %
statement 190 208 91.3
branch 46 68 67.6
condition 18 29 62.0
subroutine 37 40 92.5
pod 0 6 0.0
total 291 351 82.9


line stmt bran cond sub pod time code
1             package UR::Object::Set;
2              
3 6     6   341 use strict;
  6         9  
  6         188  
4 6     6   21 use warnings;
  6         8  
  6         168  
5 6     6   23 use UR;
  6         7  
  6         35  
6 6     6   26 use List::MoreUtils qw(any);
  6         6  
  6         63  
7             our $VERSION = "0.46"; # UR $VERSION;
8              
9             our @CARP_NOT = qw( UR::Object::Type );
10              
11 6     6   3312 use overload ('""' => '__display_name__');
  6         7  
  6         54  
12 6     6   573 use overload ('==' => sub { $_[0] . '' eq $_[1] . '' } );
  6     0   8  
  6         35  
  0         0  
13 6     6   343 use overload ('eq' => sub { $_[0] . '' eq $_[1] . '' } );
  6     3   5  
  6         35  
  3         9  
14 6     6   312 use overload ('!=' => sub { $_[0] . '' ne $_[1] . '' } );
  6     0   8  
  6         27  
  0         0  
15 6     6   297 use overload ('ne' => sub { $_[0] . '' ne $_[1] . '' } );
  6     1   7  
  6         25  
  1         88  
16 6     6   331 use overload ('cmp' => sub { $_[0]->id cmp $_[1]->id } );
  6     0   7  
  6         32  
  0         0  
17              
18             class UR::Object::Set {
19             is => 'UR::Value',
20             is_abstract => 1,
21             has => [
22             rule => { is => 'UR::BoolExpr', id_by => 'id' },
23             rule_display => { is => 'Text', via => 'rule', to => '__display_name__'},
24             member_class_name => { is => 'Text', via => 'rule', to => 'subject_class_name' },
25             members => { is => 'UR::Object', is_many => 1, is_calculated => 1 }
26             ],
27             doc => 'an unordered group of distinct UR::Objects'
28             };
29              
30             # override the UR/system display name
31             # this is used in stringification overload
32             sub __display_name__ {
33 625     625   1159 my $self = shift;
34 625         1505 my %b = $self->rule->_params_list;
35 625         2328 my $s = Data::Dumper->new([\%b])->Terse(1)->Indent(0)->Useqq(1)->Dump;
36 625         38026 $s =~ s/\n/ /gs;
37 625         2206 $s =~ s/^\s*{//;
38 625         1499 $s =~ s/\}\s*$//;
39 625         2821 $s =~ s/\"(\w+)\" \=\> / $1 => /g;
40 625         3256 return '(' . ref($self) . ' ' . $s . ')';
41             }
42              
43             # When a set comes into existance, set up a subscription to monitor changes
44             # to the set's members
45             UR::Object::Set->create_subscription(
46             method => 'load',
47             note => 'set creation monitor',
48             callback => sub {
49             my $set = shift;
50             my $rule = $set->rule;
51             my %set_defining_attributes = map { $_ => 1 } $rule->template->_property_names();
52             my $deps = $set->{__aggregate_deps} ||= {};
53              
54             $set->member_class_name->create_subscription(
55             note => 'set monitor '.$set->id,
56             priority => 0,
57             callback => sub {
58             return unless exists($set->{__aggregates}); # nothing cached yet
59              
60             my ($member, $attr_name, $before, $after) = @_;
61             # load/unload won't affect aggregate values
62             return if ($attr_name eq 'load' or $attr_name eq 'unload');
63              
64             # If a set-defining attribute changes, or an object matching
65             # the set is created or deleted, then the set membership has
66             # possibly changed. Invalidate the whole aggregate cache.
67             if (exists($set_defining_attributes{$attr_name})
68             ||
69             ( ($attr_name eq 'create' or $attr_name eq 'delete')
70             &&
71             $rule->evaluate($member)
72             )
73             ) {
74             $set->__invalidate_cache__;
75             # A later call to _members_have_changes() would miss the case
76             # where a member becomes deleted or a member-defining attribute
77             # changes
78             $set->{__members_have_changes} = 1;
79              
80             }
81             # if the changed attribute is a dependancy for a cached aggregation
82             # value, and it's a set member...
83             elsif ((my $dependant_aggregates = $deps->{$attr_name})
84             &&
85             $rule->evaluate($member)
86             ) {
87             # remove the cached aggregates that depend on this attribute
88             delete @{$set->{__aggregates}}{@$dependant_aggregates};
89             # remove the dependancy records
90             delete @$deps{@$dependant_aggregates};
91             delete $deps->{$attr_name}
92             }
93             }
94             );
95             }
96             );
97              
98             # When a transaction rolls back, it doesn't trigger subscriptions for the
99             # member objects as they get changed back to their original values.
100             # The safe thing is to set wipe out all Sets' aggregate caches :(
101             # It would be helpful if sets had a db_committed like other objects
102             # and we could just revert their values back to their db_committed values
103             UR::Context::Transaction->create_subscription(
104             method => 'rollback',
105             note => 'rollback set cache invalidator',
106             callback => sub {
107             delete(@$_{'__aggregates','__aggregate_deps','__members_have_changes'}) foreach UR::Object::Set->is_loaded();
108             }
109             );
110              
111             UR::Context->create_subscription(
112             method => 'commit',
113             callback => sub {
114             my $worked = shift;
115             return unless $worked; # skip if the commit failed
116             delete $_->{__members_have_changes} foreach UR::Object::Set->is_loaded();
117             }
118             );
119              
120              
121             sub get_with_special_parameters {
122 2     2 0 287 Carp::cluck("Getting sets by directly properties of their members method will be removed shortly because of ambiguity on the meaning of 'id'. Please update the code which calls this.");
123 2         4 my $class = shift;
124 2         3 my $bx = shift;
125 2         4 my @params = @_;
126 2         4 my $member_class = $class;
127 2         10 $member_class =~ s/::Set$//;
128 2         7 return $member_class->define_set($bx->params_list, @params);
129             }
130              
131             sub members {
132 38     38 0 4831 my $self = shift;
133 38         113 my $rule = $self->rule;
134 38         91 while (@_) {
135 0         0 $rule = $rule->add_filter(shift, shift);
136             }
137 38         116 return $self->member_class_name->get($rule);
138             }
139              
140             sub member_iterator {
141 5     5 0 9 my $self = shift;
142 5         26 my $rule = $self->rule;
143 5         16 while (@_) {
144 0         0 $rule = $rule->add_filter(shift, shift);
145             }
146 5         20 return $self->member_class_name->create_iterator($rule);
147             }
148              
149             sub _members_have_changes {
150 52     52   79 my $self = shift;
151 52 100       166 return 1 if $self->{__members_have_changes};
152              
153 46         79 my @property_names = @_;
154 46         109 my $rule = $self->rule;
155 46 100   87   238 return any { $rule->evaluate($_) && $_->__changes__(@property_names) } $self->member_class_name->is_loaded;
  87         183  
156             }
157              
158             sub subset {
159 3     3 0 2411 my $self = shift;
160 3         13 my $member_class_name = $self->member_class_name;
161 3         12 my $bx = UR::BoolExpr->resolve($member_class_name,@_);
162 3         9 my $subset = $self->class->get($bx->id);
163 3         11 return $subset;
164             }
165              
166             sub group_by {
167 3     3 0 803 my $self = shift;
168 3         7 my @group_by = @_;
169 3         11 my $grouping_rule = $self->rule->add_filter(-group_by => \@group_by);
170 3         16 my @groups = UR::Context->current->get_objects_for_class_and_rule(
171             $self->member_class_name,
172             $grouping_rule,
173             undef, #$load,
174             0, #$return_closure,
175             );
176 3         16 return $self->context_return(@groups);
177             }
178              
179              
180             sub __invalidate_cache__ {
181 5     5   9 my $self = shift;
182 5 50       9 if (@_) {
183 0         0 my $aggregate = shift;
184 0         0 delete $self->{__aggregates}->{$aggregate};
185             } else {
186 5         18 delete @$self{'__aggregates','__aggregate_deps'};
187             }
188             }
189              
190             sub __aggregate__ {
191 67     67   77 my $self = shift;
192 67         72 my $aggr = shift;
193              
194 67         102 my $f = $aggr->{f};
195 67         79 my $aggr_properties = $aggr->{properties};
196              
197 67 50       137 Carp::croak("$f is a group operation, and is not writable") if @_;
198              
199 67         201 my $subject_class_meta = $self->rule->subject_class_name->__meta__;
200              
201 67 100 66     180 my $not_ds_expressable = grep { $_->is_calculated or $_->is_transient or $_->is_constant }
202 67 100       232 map { $_->final_property_meta or $_ }
203 67 50       183 map { $subject_class_meta->property_meta_for_name($_) || () }
  67         236  
204             $self->rule->template->_property_names;
205              
206 67         173 my($cache, $deps) = @$self{'__aggregates','__aggregate_deps'};
207              
208             # If there are no member-class objects with changes, we can just interrogate the DB
209 67 100       146 if (! exists($cache->{$f})) {
210 50 100 100     199 if ($not_ds_expressable or $self->_members_have_changes(@$aggr_properties)) {
211 26         33 my $fname;
212             my @fargs;
213 26 100       121 if ($f =~ /^(\w+)\((.*)\)$/) {
214 21         40 $fname = $1;
215 21 50       81 @fargs = ($2 ? split(',',$2) : ());
216             }
217             else {
218 5         11 $fname = $f;
219 5         10 @fargs = ();
220             }
221 26         55 my $local_method = '__aggregate_' . $fname . '__';
222 26         74 $self->{__aggregates}->{$f} = $self->$local_method(@fargs);
223              
224             } else {
225 24         85 my $rule = $self->rule->add_filter(-aggregate => [$f])->add_filter(-group_by => []);
226 24         68 UR::Context->current->get_objects_for_class_and_rule(
227             $self->member_class_name,
228             $rule,
229             1, # load
230             0, # return_closure
231             );
232              
233             }
234             # keep 2-way mapping of dependances...
235             # First, keep a list of properties this aggregate cached value depends on
236 50         250 $deps->{$f} = $aggr_properties;
237             # And add this aggregate to the lists these properties are dependancies for
238 50         98 foreach ( @$aggr_properties ) {
239 35   100     122 $deps->{$_} ||= [];
240 35         30 push @{$deps->{$_}}, $f;
  35         97  
241             }
242             }
243 67         742 return $self->{__aggregates}->{$f};
244             }
245              
246             sub __aggregate_count__ {
247 5     5   8 my $self = shift;
248 5         21 my @members = $self->members;
249 5         25 return scalar(@members);
250             }
251              
252             sub __aggregate_min__ {
253 9     9   11 my $self = shift;
254 9         12 my $p = shift;
255 9         12 my $min = undef;
256 6     6   7324 no warnings;
  6         7  
  6         609  
257 9         41 for my $member ($self->members) {
258 21         57 my $v = $member->$p;
259 21 50       37 next unless defined $v;
260 21 100 66     109 $min = $v if (!defined($min) || ($v < $min) || ($v lt $min));
      100        
261             }
262 9         38 return $min;
263             }
264              
265             sub __aggregate_max__ {
266 5     5   8 my $self = shift;
267 5         8 my $p = shift;
268 5         5 my $max = undef;
269 6     6   34 no warnings;
  6         9  
  6         481  
270 5         13 for my $member ($self->members) {
271 11         27 my $v = $member->$p;
272 11 50       18 next unless defined $v;
273 11 50 66     43 $max = $v if (!defined($max) || ($v > $max) || ($v gt $max));
      33        
274             }
275 5         16 return $max;
276             }
277              
278             sub __aggregate_sum__ {
279 7     7   12 my $self = shift;
280 7         10 my $p = shift;
281 7         11 my $sum = undef;
282 6     6   21 no warnings;
  6         6  
  6         3794  
283 7         14 for my $member ($self->members) {
284 16         39 my $v = $member->$p;
285 16 50       29 next unless defined $v;
286 16         18 $sum += $v;
287             }
288 7         19 return $sum;
289             }
290              
291             sub __related_set__ {
292 6     6   10 my $self = $_[0];
293 6         10 my $property_name = $_[1];
294 6         21 my $bx1 = $self->rule;
295 6         18 my $bx2 = $bx1->reframe($property_name);
296 6         15 return $bx2->subject_class_name->define_set($bx2);
297             }
298              
299             require Class::AutoloadCAN;
300             Class::AutoloadCAN->import();
301              
302             sub CAN {
303 211     211 0 41578 my ($class,$method,$self) = @_;
304            
305 211 100       648 if ($method =~ /^__aggregate_(.*)__/) {
306             # prevent circularity issues since this actually calls ->can();
307 59         122 return;
308             }
309              
310              
311 152         220 my $member_class_name = $class;
312 152         514 $member_class_name =~ s/::Set$//g;
313 152 50       335 return unless $member_class_name;
314              
315 152         225 my $is_class_method = !ref($self);
316 152         493 my $member_method_closure = $member_class_name->can($method);
317 152 100 100     5933 if ($is_class_method && $member_method_closure) {
318             # We should only get here if the Set class has not implemented the method.
319             # In which case we will delegate to the member class.
320             return sub {
321 1     1   14 my $self = shift;
322 1         5 return $member_method_closure->($member_class_name, @_);
323 1         10 };
324             }
325              
326 151 100       290 if ($member_method_closure) {
327 25         80 my $member_class_meta = $member_class_name->__meta__;
328 25         80 my $member_property_meta = $member_class_meta->property_meta_for_name($method);
329            
330             # regular property access
331 25 100       62 if ($member_property_meta) {
332             return sub {
333 23     23   143 my $self = shift;
334 23 50       56 if (@_) {
335 0         0 Carp::croak("Cannot use method $method as a mutator: Set properties are not mutable");
336             }
337 23         75 my $rule = $self->rule;
338 23 100       63 if ($rule->specifies_value_for($method)) {
339 20         68 return $rule->value_for($method);
340             }
341             else {
342 3         13 my @members = $self->members;
343 3         9 my @values = map { $_->$method } @members;
  3         12  
344 3 50       52 return @values if wantarray;
345 0 0       0 return if not defined wantarray;
346 0 0 0     0 Carp::confess("Multiple matches for $class method '$method' called in scalar context. The set has ".scalar(@values)." values to return") if @values > 1 and not wantarray;
347 0         0 return $values[0];
348             }
349 23         137 };
350             }
351              
352             # set relaying with $s->foo_set->bar_set->baz_set;
353 2 100       13 if (my ($property_name) = ($method =~ /^(.*)_set$/)) {
354             return sub {
355 1     1   23 shift->__related_set__($property_name, @_)
356             }
357 1         8 }
358              
359             # other method
360             return sub {
361 1     1   10 my $self = shift;
362 1 50       4 if (@_) {
363 1         247 Carp::croak("Cannot use method $method as a mutator: Set properties are not mutable");
364             }
365 0         0 my @members = $self->members;
366 0         0 my @values = map { $_->$method } @members;
  0         0  
367 0 0       0 return @values if wantarray;
368 0 0       0 return if not defined wantarray;
369 0 0 0     0 Carp::confess("Multiple matches for $class method '$method' called in scalar context. The set has ".scalar(@values)." values to return") if @values > 1 and not wantarray;
370 0         0 return $values[0];
371 1         6 };
372              
373             }
374             else {
375             # a possible aggregation function
376             # see if the method ___aggregate__ uses exists, and if so, delegate to __aggregate__
377             # TODO: delegate these to aggregation function modules instead of having them in this module
378 126         269 my $aggregator = '__aggregate_' . $method . '__';
379 126 100       289 if ($self->can($aggregator)) {
380             return sub {
381 67     67   457 my $self = shift;
382 67         96 my $f = $method;
383 67         106 my @aggr_properties = @_;
384 67 100       174 if (@aggr_properties) {
385 47         101 $f .= '(' . join(',',@aggr_properties) . ')';
386             }
387 67         322 return $self->__aggregate__({ f => $f, properties => \@aggr_properties });
388 67         718 };
389             }
390            
391             # set relaying with $s->foo_set->bar_set->baz_set;
392 59 100       1540 if (my ($property_name) = ($method =~ /^(.*)_set$/)) {
393             return sub {
394 4     4   40 shift->__related_set__($property_name, @_)
395             }
396 4         21 }
397             }
398 55         93 return;
399             }
400              
401             1;
402