File Coverage

lib/UR/Object/Join.pm
Criterion Covered Total %
statement 200 226 88.5
branch 65 90 72.2
condition 30 48 62.5
subroutine 14 14 100.0
pod 0 3 0.0
total 309 381 81.1


line stmt bran cond sub pod time code
1             package UR::Object::Join;
2 266     266   2699 use strict;
  266         329  
  266         7786  
3 266     266   956 use warnings;
  266         316  
  266         7146  
4 266     266   871 use UR;
  266         297  
  266         2532  
5             our $VERSION = "0.46"; # UR $VERSION;
6              
7             our @CARP_NOT = qw( UR::Object::Property );
8              
9             class UR::Object::Join {
10             #is => 'UR::Value',
11             id_by => [
12             id => { is => 'Text' },
13             ],
14             has_optional_transient => [
15             source_class => { is => 'Text' },
16             source_property_names => { is => 'Text' },
17            
18             foreign_class => { is => 'Text' },
19             foreign_property_names => { is => 'Text' },
20            
21             source_name_for_foreign => { is => 'Text' },
22             foreign_name_for_source => { is => 'Text' },
23            
24             is_optional => { is => 'Boolean' },
25              
26             is_many => { is => 'Boolean' },
27              
28             sub_group_label => { is => 'Text' },
29              
30             where => { is => 'Text' },
31             ],
32             doc => "join metadata used internally by the ::QueryBuilder"
33             };
34              
35             our %resolve_chain;
36              
37             # When a Join is unloaded, we need to remove from the cache any join chain
38             # using this join
39             sub unload {
40 4     4 0 7 my $self = shift;
41 4         13 my $id_to_remove = $self->id;
42              
43 4         11 foreach my $joins_for_class ( values %resolve_chain ) {
44 8         21 foreach my $property_chain ( keys %$joins_for_class ) {
45             # need to skip over DeletedRefs that may already be in the list
46 8 50       8 if (grep { $_->isa('UR::Object::Join') and ($_->id eq $id_to_remove) }
  10 100       39  
47 8         21 @{$joins_for_class->{$property_chain}}
48             ) {
49             # This unloaded join is in the list - nuke the whole list
50 6         13 delete $joins_for_class->{$property_chain};
51             }
52             }
53             }
54 4         27 $self->SUPER::unload(@_);
55             }
56              
57             sub resolve_chain {
58 841     841 0 1247 my ($class, $class_name, $property_chain) = @_;
59              
60             my $join_chain =
61             $resolve_chain{$class_name}{$property_chain}
62 841   66     2691 ||= do {
63 351         995 my $class_meta = $class_name->__meta__;
64 351         936 my @pmeta = $class_meta->property_meta_for_name($property_chain);
65 351         401 my @joins;
66 351         549 for my $pmeta (@pmeta) {
67 349         1479 push @joins, $class->_resolve_chain_for_property_meta($pmeta);
68             }
69 351         1085 \@joins;
70             };
71              
72 841         2613 return @$join_chain;
73             }
74              
75             sub _resolve_chain_for_property_meta {
76 349     349   443 my ($class, $pmeta) = @_;
77 349 100 66     952 if ($pmeta->via or $pmeta->to) {
78 105         600 return $class->_resolve_via_to($pmeta);
79             }
80             else {
81 244         793 my $foreign_class = $pmeta->_data_type_as_class_name;
82 244 50 33     1171 unless (defined($foreign_class) and $foreign_class->can('get')) {
83 0         0 return;
84             }
85 244 100 100     2747 if ($pmeta->id_by or $foreign_class->isa("UR::Value")) {
    100          
86 191         858 return $class->_resolve_forward($pmeta);
87             }
88             elsif (my $reverse_as = $pmeta->reverse_as) {
89 52         222 return $class->_resolve_reverse($pmeta);
90             }
91             else {
92             # TODO: handle hard-references to objects here maybe?
93 1         4 $pmeta->error_message("Property '" . $pmeta->property_name . "' of class " . $pmeta->class_name
94             . " has no 'id_by' or 'reverse_as' property metadata");
95 1         7 return;
96             }
97             }
98             }
99              
100             sub _get_or_define {
101 294     294   442 my $class = shift;
102 294         1870 my %p = @_;
103 294         632 my $id = delete $p{id};
104 294         404 delete $p{__get_serial};
105 294         335 delete $p{db_committed};
106 294         333 delete $p{_change_count};
107 294         317 delete $p{__defined};
108 294         1097 my $self = $class->get(id => $id);
109 294 100       728 unless ($self) {
110 286         1072 $self = $class->__define__($id);
111 286         895 for my $k (keys %p) {
112 2575         5899 $self->$k($p{$k});
113 266     266   1242 no warnings;
  266         371  
  266         340658  
114 2575 50       4646 unless ($self->{$k} eq $p{$k}) {
115 0         0 Carp::confess(Data::Dumper::Dumper($self, \%p));
116             }
117             }
118             }
119 294 50       760 unless ($self) {
120 0         0 Carp::confess("Failed to create join???");
121             }
122 294         996 return $self;
123             }
124              
125             sub _resolve_via_to {
126 105     105   172 my ($class, $pmeta) = @_;
127              
128 105         289 my $class_name = $pmeta->class_name;
129 105         448 my $class_meta = UR::Object::Type->get(class_name => $class_name);
130              
131 105         177 my @joins;
132 105         360 my $via = $pmeta->via;
133              
134 105         363 my $to = $pmeta->to;
135 105 50 33     597 if ($via and not $to) {
136 0         0 $to = $pmeta->property_name;
137             }
138              
139 105         160 my $via_meta;
140 105 50       259 if ($via) {
141 105 100       314 if ($via eq '__self__') {
142 2         8 my $to_meta = $class_meta->property_meta_for_name($to);
143 2 50       5 unless ($to_meta) {
144 0         0 my $property_name = $pmeta->property_name;
145 0         0 Carp::croak "Can't resolve joins for property '$property_name' of $class_name: No property metadata 'to' property '$to'";
146             }
147 2         12 return $to_meta->_resolve_join_chain();
148             }
149 103         350 $via_meta = $class_meta->property_meta_for_name($via);
150 103 50       350 unless ($via_meta) {
151 0 0       0 return if $class_name->can($via); # It's via a method, not an actual property
152              
153 0         0 my $property_name = $pmeta->property_name;
154 0         0 Carp::croak "Can't resolve joins for property '$property_name' of $class_name: No property metadata for via property '$via'";
155             }
156              
157 103 50 66     297 if ($via_meta->to and ($via_meta->to eq '-filter')) {
158 0         0 return $via_meta->_resolve_join_chain();
159             }
160              
161 103 50       340 unless ($via_meta->data_type) {
162 0         0 my $property_name = $pmeta->property_name;
163 0         0 my $class_name = $pmeta->class_name;
164 0         0 Carp::croak "Can't resolve joins for property '$property_name' of $class_name: No data type for via property '$via'";
165             }
166 103         544 push @joins, $via_meta->_resolve_join_chain();
167            
168 103 100       419 if (my $where = $pmeta->where) {
169 25         49 my $join = pop @joins;
170 25 50 33     155 unless ($join and $join->{foreign_class}) {
171 0         0 my $property_name = $pmeta->property_name;
172 0         0 my $class_name = $pmeta->class_name;
173 0         0 Carp::croak("Can't resolve joins for property '$property_name' of $class_name: Couldn't determine foreign class for via property '$via'\n"
174             . "join data so far: ". Data::Dumper::Dumper($join, \@joins));
175             }
176 25         116 my $where_rule = UR::BoolExpr->resolve($join->{foreign_class}, @$where);
177 25         53 my $id = $join->{id};
178 25         96 $id .= ' ' . $where_rule->id;
179 25         240 my %join_data = %$join;
180 25         185 push @joins, $class->_get_or_define(%join_data, id => $id, where => $where, sub_group_label => $pmeta->property_name);
181             }
182             }
183             else {
184 0         0 $via_meta = $pmeta;
185             }
186              
187 103 100 66     913 if ($to and $to ne '__self__' and $to ne '-filter') {
      66        
188 98         156 my $to_class_meta = eval { $via_meta->data_type->__meta__ };
  98         346  
189 98 50       305 unless ($to_class_meta) {
190 0         0 Carp::croak("Can't get class metadata for " . $via_meta->data_type
191             . " while resolving property '" . $pmeta->property_name . "' in class " . $pmeta->class_name . "\n"
192             . "Is the data_type for property '" . $via_meta->property_name . "' in class "
193             . $via_meta->class_name . " correct?");
194             }
195              
196 98         485 my $to_meta = $to_class_meta->property_meta_for_name($to);
197 98 50       272 unless ($to_meta) {
198 0         0 my $property_name = $pmeta->property_name;
199 0         0 my $class_name = $pmeta->class_name;
200 0         0 Carp::croak "Can't resolve property '$property_name' of $class_name: No '$to' property found on " . $via_meta->data_type;
201             }
202              
203 98         390 push @joins, $to_meta->_resolve_join_chain();
204             }
205            
206 103 50       439 if (my $return_class_name = $pmeta->_convert_data_type_for_source_class_to_final_class($pmeta->data_type, $pmeta->class_name)) {
207 103         356 my $final_class_name = $joins[-1]->foreign_class;
208 103 100       339 if ($return_class_name ne $final_class_name) {
209 69 100 100     696 if ($return_class_name->isa($final_class_name)) {
    100          
    50          
210             # the property is a subclass of the one involved in the final join
211             # this happens when there is a via/where/to where say "to" goes-to any "Animal" but this overall property is known to be a "Dog".
212 3         5 my $general_join = pop @joins;
213             my $specific_join = UR::Object::Join->_get_or_define(
214             source_class => $general_join->{'source_class'},
215             source_property_names => $general_join->{'source_property_names'},
216             foreign_class => $return_class_name, # more specific
217             foreign_property_names => $general_join->{'foreign_property_names'}, # presume the borrow took you into a subclass and these still work
218             is_optional => $general_join->{'is_optional'},
219 3         18 id => $general_join->{id} . ' isa ' . $return_class_name
220             );
221 3         6 push @joins, $specific_join;
222             }
223             elsif ($return_class_name eq 'UR::Value::SloppyPrimitive' or $final_class_name eq 'UR::Value::SloppyPrimitive') {
224             # backward-compatible layer for before there were primitive types
225             }
226             elsif ($final_class_name->isa($return_class_name)) {
227 0         0 Carp::carp("Joins for property '" . $pmeta->property_name . "' of class " . $pmeta->class_name
228             . " is declared as data type $return_class_name while its joins connect to a more specific data type $final_class_name!");
229             }
230             else {
231             #Carp::carp("Discrepant join for property '" . $pmeta->property_name . "' of class " . $pmeta->class_name
232             # . ". Its data type ($return_class_name) does not match the join from property '"
233             # . join("','", @{$joins[-1]->{source_property_names}}) . "' of class " . $joins[-1]->{source_class}
234             # . " with type $final_class_name");
235             }
236             }
237             }
238              
239 103         3282 return @joins;
240             }
241              
242             # code below uses these to convert objects using hash slices
243             my @old = qw/source_class source_property_names foreign_class foreign_property_names source_name_for_foreign foreign_name_for_source is_optional is_many sub_group_label/;
244             my @new = qw/foreign_class foreign_property_names source_class source_property_names foreign_name_for_source source_name_for_foreign is_optional is_many sub_group_label/;
245              
246             sub _resolve_forward {
247 191     191   323 my ($class, $pmeta) = @_;
248              
249 191         517 my $foreign_class = $pmeta->_data_type_as_class_name;
250 191 50 33     876 unless (defined($foreign_class) and $foreign_class->can('get')) {
251             #Carp::cluck("No metadata?!");
252 0         0 return;
253             }
254              
255 191         1408 my $source_class = $pmeta->class_name;
256 191         565 my $class_meta = UR::Object::Type->get(class_name => $pmeta->class_name);
257 191         308 my @joins;
258 191         651 my $where = $pmeta->where;
259 191         802 my $foreign_class_meta = $foreign_class->__meta__;
260 191         515 my $property_name = $pmeta->property_name;
261              
262 191         527 my $id = $source_class . '::' . $property_name;
263 191 100       615 if ($where) {
264 1         3 my $where_rule = UR::BoolExpr->resolve($foreign_class, @$where);
265 1         5 $id .= ' ' . $where_rule->id;
266             }
267              
268             #####
269            
270             # direct reference (or primitive, which is a direct ref to a value obj)
271 191         273 my (@source_property_names, @source_property_types,
272             @foreign_property_names, @foreign_property_types,
273             $source_name_for_foreign, $foreign_name_for_source);
274              
275 191 100       1145 if ($foreign_class->isa("UR::Value")) {
    50          
276 82 100       264 if (my $id_by = $pmeta->id_by) {
277 1 50       5 my @id_by = ref($id_by) eq 'ARRAY' ? @$id_by : ($id_by);
278 1         2 foreach my $id_by_name ( @id_by ) {
279 1         3 my $id_by_property = $class_meta->property_meta_for_name($id_by_name);
280 1         5 push @joins, $id_by_property->_resolve_join_chain();
281             }
282             }
283              
284 82         195 @source_property_names = ($property_name);
285 82         189 @foreign_property_names = ('id');
286              
287 82         145 $source_name_for_foreign = ($property_name);
288             }
289             elsif (my $id_by = $pmeta->id_by) {
290 109         501 my @pairs = $pmeta->get_property_name_pairs_for_join;
291 109         252 @source_property_names = map { $_->[0] } @pairs;
  110         338  
292 109         223 @foreign_property_names = map { $_->[1] } @pairs;
  110         255  
293              
294 109 50       490 if (ref($id_by) eq 'ARRAY') {
295             # satisfying the id_by requires joins of its own
296             # sms: why is this only done on multi-value fks?
297 109         234 foreach my $id_by_property_name ( @$id_by ) {
298 110         381 my $id_by_property = $class_meta->property_meta_for_name($id_by_property_name);
299 110 100 66     621 next unless ($id_by_property and $id_by_property->is_delegated);
300            
301 5         23 push @joins, $id_by_property->_resolve_join_chain();
302 5         12 $source_class = $joins[-1]->{'foreign_class'};
303 5         11 @source_property_names = @{$joins[-1]->{'foreign_property_names'}};
  5         22  
304             }
305             }
306              
307 109         311 $source_name_for_foreign = $pmeta->property_name;
308 109         408 my @reverse = $foreign_class_meta->properties(reverse_as => $source_name_for_foreign, data_type => $pmeta->class_name);
309 109         190 my $reverse;
310 109 100       456 if (@reverse > 1) {
311 5         12 my @reduced = grep { not $_->where } @reverse;
  10         21  
312 5 50       24 if (@reduced != 1) {
313 0         0 Carp::confess("Ambiguous results finding reversal for $property_name!" . Data::Dumper::Dumper(\@reverse));
314             }
315 5         11 $reverse = $reduced[0];
316             }
317             else {
318 104         176 $reverse = $reverse[0];
319             }
320 109 100       320 if ($reverse) {
321 58         182 $foreign_name_for_source = $reverse->property_name;
322             }
323             }
324              
325             # the foreign class might NOT have a reverse_as, but
326             # this records what to reverse in this case.
327 191   66     962 $foreign_name_for_source ||= '<' . $source_class . '::' . $source_name_for_foreign;
328              
329 191   66     866 push @joins, $class->_get_or_define(
330             id => $id,
331              
332             source_class => $source_class,
333             source_property_names => \@source_property_names,
334            
335             foreign_class => $foreign_class,
336             foreign_property_names => \@foreign_property_names,
337            
338             source_name_for_foreign => $source_name_for_foreign,
339             foreign_name_for_source => $foreign_name_for_source,
340            
341             is_optional => ($pmeta->is_optional or $pmeta->is_many),
342              
343             is_many => $pmeta->is_many,
344              
345             where => $where,
346             );
347              
348 191         940 return @joins;
349             }
350              
351             sub _resolve_reverse {
352 52     52   98 my ($class, $pmeta) = @_;
353              
354 52         179 my $foreign_class = $pmeta->_data_type_as_class_name;
355              
356 52 50 33     280 unless (defined($foreign_class) and $foreign_class->can('get')) {
357             #Carp::cluck("No metadata?!");
358 0         0 return;
359             }
360              
361 52         465 my $source_class = $pmeta->class_name;
362 52         168 my $class_meta = UR::Object::Type->get(class_name => $pmeta->class_name);
363 52         90 my @joins;
364 52         207 my $where = $pmeta->where;
365 52         151 my $property_name = $pmeta->property_name;
366              
367 52         145 my $id = $source_class . '::' . $property_name;
368 52 100       159 if ($where) {
369 2         10 my $where_rule = UR::BoolExpr->resolve($foreign_class, @$where);
370 2         9 $id .= ' ' . $where_rule->id;
371             }
372              
373             #####
374            
375 52         147 my $reverse_as = $pmeta->reverse_as;
376              
377 52         232 my $foreign_class_meta = $foreign_class->__meta__;
378 52         442 my $foreign_property_via = $foreign_class_meta->property_meta_for_name($reverse_as);
379 52 50       162 unless ($foreign_property_via) {
380 0         0 Carp::confess("No property '$reverse_as' in class $foreign_class, needed to resolve property '" .
381             $pmeta->property_name . "' of class " . $pmeta->class_name);
382             }
383              
384 52         236 my @join_data = map { { %$_ } } $foreign_property_via->_resolve_join_chain();
  54         505  
385 52         109 my $prev_where = $where;
386 52         113 for (@join_data) {
387 54         447 @$_{@new} = @$_{@old};
388              
389 54         117 my $next_where = $_->{where};
390 54         105 $_->{where} = $prev_where;
391              
392 266     266   1437 no warnings qw(uninitialized); #source_name_for_foreign can be undefined at the end of the chain
  266         367  
  266         12932  
393 54         166 my $id = $_->{source_class} . '::' . $_->{source_name_for_foreign};
394 266     266   1035 use warnings qw(uninitialized);
  266         338  
  266         68313  
395 54 100       151 if ($prev_where) {
396 3         14 my $where_rule = UR::BoolExpr->resolve($foreign_class, @$where);
397 3         14 $id .= ' ' . $where_rule->id;
398              
399             }
400 54         109 $_->{id} = $id;
401              
402 54   100     195 $_->{is_optional} = ($pmeta->is_optional || $pmeta->is_many);
403              
404 54         99 $_->{is_many} = $pmeta->{is_many};
405              
406 54         161 $_->{sub_group_label} = $pmeta->property_name;
407              
408 54         121 $prev_where = $next_where;
409             }
410 52         94 @join_data = reverse @join_data;
411 52 50       168 if ($prev_where) {
412             # Having a where clause in the last join is only a problem if testing
413             # the where condition needs more joins. But if it did, then those additional
414             # joins would have already been in the list, right?
415             #Carp::confess("final join needs placement! " . Data::Dumper::Dumper($prev_where));
416             }
417              
418 52         106 for my $join_data (@join_data) {
419 54         281 push @joins, $class->_get_or_define(%$join_data);
420             }
421              
422 52         371 return @joins;
423             }
424              
425              
426             # Return true if the foreign-end of the join includes all the ID properties of
427             # the foreign class. Used by the ObjectFabricator when it is determining whether or
428             # not to include more rules in the all_params_loaded hash for delegations
429             sub destination_is_all_id_properties {
430 123     123 0 176 my $self = shift;
431              
432 123         448 my $foreign_class_meta = $self->{'foreign_class'}->__meta__;
433 123         175 my %join_properties = map { $_ => 1 } @{$self->{'foreign_property_names'}};
  123         448  
  123         307  
434 123         192 my $join_has_all_id_props = 1;
435 123         782 foreach my $foreign_id_meta ( $foreign_class_meta->all_id_property_metas ) {
436 390 100       765 next if $foreign_id_meta->class_name eq 'UR::Object'; # Skip the manufactured 'id' property
437 144 100       426 next if (delete $join_properties{ $foreign_id_meta->property_name });
438 97         158 $join_has_all_id_props = 0;
439             }
440 123         569 return $join_has_all_id_props;
441             }
442              
443              
444             1;