File Coverage

lib/UR/BoolExpr.pm
Criterion Covered Total %
statement 480 605 79.3
branch 185 276 67.0
condition 54 80 67.5
subroutine 45 49 91.8
pod 12 25 48.0
total 776 1035 74.9


line stmt bran cond sub pod time code
1             package UR::BoolExpr;
2 266     266   1024 use warnings;
  266         1990  
  266         7938  
3 266     266   924 use strict;
  266         338  
  266         9308  
4              
5 266     266   3063 use List::MoreUtils qw(uniq);
  266         297  
  266         5909  
6 266     266   110841 use List::Util qw(first);
  266         348  
  266         19582  
7 266     266   1031 use Scalar::Util qw(blessed);
  266         333  
  266         12700  
8             require UR;
9              
10 266     266   1005 use Carp;
  266         357  
  266         23271  
11             our @CARP_NOT = ('UR::Context');
12              
13             our $VERSION = "0.46"; # UR $VERSION;;
14              
15             # readable stringification
16 266     266   4431 use overload ('""' => '__display_name__');
  266         4016  
  266         6140  
17 266     266   28163 use overload ('==' => sub { $_[0] . '' eq $_[1] . '' } );
  266     33   353  
  266         2971  
  33         77  
18 266     266   20817 use overload ('eq' => sub { $_[0] . '' eq $_[1] . '' } );
  266     1   371  
  266         2992  
  1         2065  
19              
20             UR::Object::Type->define(
21             class_name => 'UR::BoolExpr',
22             composite_id_separator => $UR::BoolExpr::Util::id_sep,
23             id_by => [
24             template_id => { type => 'Blob' },
25             value_id => { type => 'Blob' },
26             ],
27             has => [
28             template => { is => 'UR::BoolExpr::Template', id_by => 'template_id' },
29             subject_class_name => { via => 'template' },
30             logic_type => { via => 'template' },
31             logic_detail => { via => 'template' },
32              
33             num_values => { via => 'template' },
34             is_normalized => { via => 'template' },
35             is_id_only => { via => 'template' },
36             has_meta_options => { via => 'template' },
37             ],
38             is_transactional => 0,
39             );
40              
41              
42             # for performance
43             sub UR::BoolExpr::Type::resolve_composite_id_from_ordered_values {
44 557792     557792   418352 shift;
45 557792         1156259 return join($UR::BoolExpr::Util::id_sep,@_);
46             }
47              
48             # only respect the first delimiter instead of splitting
49             sub UR::BoolExpr::Type::resolve_ordered_values_from_composite_id {
50 347619     347619   316136 my ($self,$id) = @_;
51 347619         396634 my $pos = index($id,$UR::BoolExpr::Util::id_sep);
52 347619         839051 return (substr($id,0,$pos), substr($id,$pos+1));
53             }
54              
55             sub template {
56 2993309     2993309 0 2079730 my $self = $_[0];
57 2993309   66     5407864 return $self->{template} ||= $self->__template;
58             }
59              
60             sub flatten {
61 13     13 1 421 my $self = shift;
62 13 50       44 return $self->{flatten} if exists $self->{flatten};
63 13         21 my $flat = $self->template->_flatten_bx($self);
64 13         26 $self->{flatten} = $flat;
65 13 100       79 Scalar::Util::weaken($self->{flatten}) if $self == $flat;
66 13         27 return $flat;
67             }
68              
69             sub reframe {
70 20     20 1 53 my $self = shift;
71 20         31 my $in_terms_of = shift;
72 20 50       66 return $self->{reframe}{$in_terms_of} if $self->{reframe}{$in_terms_of};
73 20         41 my $reframe = $self->template->_reframe_bx($self, $in_terms_of);
74 20         41 $self->{reframe}{$in_terms_of} = $reframe;
75 20 50       156 Scalar::Util::weaken($self->{reframe}{$in_terms_of}) if $self == $reframe;
76 20         55 return $reframe;
77             }
78              
79              
80             # override the UR/system display name
81             # this is used in stringification overload
82             sub __display_name__ {
83 522     522   5814 my $self = shift;
84 522         872 my %b = $self->_params_list;
85 522         2106 my $s = Data::Dumper->new([\%b])->Terse(1)->Indent(0)->Useqq(1)->Sortkeys(1)->Dump;
86 522         32380 $s =~ s/\n/ /gs;
87 522         1950 $s =~ s/^\s*{//;
88 522         1363 $s =~ s/\}\s*$//;
89 522         2323 $s =~ s/\"(\w+)\" \=\> / $1 => /g;
90 522         1328 return __PACKAGE__ . '=(' . $self->subject_class_name . ':' . $s . ')';
91             }
92              
93             # The primary function: evaluate a subject object as matching the rule or not.
94             sub evaluate {
95 103880     103880 1 158478 my $self = shift;
96 103880         80854 my $subject = shift;
97 103880         128426 my $template = $self->template;
98 103880         133247 my @values = $self->values;
99 103880         218320 return $template->evaluate_subject_and_values($subject,@values);
100             }
101              
102             # Behind the id properties:
103             sub template_and_values {
104 347619     347619 1 273046 my $self = shift;
105 347619         522892 my ($template_id, $value_id) = UR::BoolExpr::Type->resolve_ordered_values_from_composite_id($self->id);
106 347619         696846 return (UR::BoolExpr::Template->get($template_id), UR::BoolExpr::Util::value_id_to_values($value_id));
107             }
108              
109             # Returns true if the rule represents a subset of the things the other
110             # rule would match. It returns undef if the answer is not known, such as
111             # when one of the values is a list and we didn't go to the trouble of
112             # searching the list for a matching value
113             sub is_subset_of {
114 247     247 1 350 my($self, $other_rule) = @_;
115              
116 247 50 33     1717 return 0 unless (ref($other_rule) and $self->isa(ref $other_rule));
117              
118 247         502 my $my_template = $self->template;
119 247         402 my $other_template = $other_rule->template;
120              
121 247 50 33     1597 unless ($my_template->isa("UR::BoolExpr::Template::And")
122             and $other_template->isa("UR::BoolExpr::Template::And")) {
123 0         0 Carp::confess("This method currently works only on ::And expressions. Update to handle ::Or, ::PropertyComparison, and templates of mismatched class!");
124             }
125 247 100       662 return unless ($my_template->is_subset_of($other_template));
126              
127 226         285 my $values_match = 1;
128 226         511 foreach my $prop ( $other_template->_property_names ) {
129 292   50     676 my $my_operator = $my_template->operator_for($prop) || '=';
130 292   50     581 my $other_operator = $other_template->operator_for($prop) || '=';
131              
132 292         694 my $my_value = $self->value_for($prop);
133 292         481 my $other_value = $other_rule->value_for($prop);
134              
135             # If either is a list of values, return undef
136 292 50 33     1233 return undef if (ref($my_value) || ref($other_value));
137              
138 266     266   180318 no warnings 'uninitialized';
  266         440  
  266         302530  
139 292 100       716 $values_match = undef if ($my_value ne $other_value);
140             }
141              
142 226         1065 return $values_match;
143             }
144              
145             sub values {
146 856288     856288 1 689507 my $self = shift;
147 856288 100       1249618 if ($self->{values}) {
148 354875         257753 return @{ $self->{values}}
  354875         661585  
149             }
150 501413         822726 my $value_id = $self->value_id;
151 501413 50 33     1577769 return unless defined($value_id) and length($value_id);
152 501413         368525 my @values;
153 501413         884960 @values = UR::BoolExpr::Util::value_id_to_values($value_id);
154 501413 100       889291 if (my $hard_refs = $self->{hard_refs}) {
155 64         182 for my $n (keys %$hard_refs) {
156 80         247 $values[$n] = $hard_refs->{$n};
157             }
158             }
159 501413         581325 $self->{values} = \@values;
160 501413         1044065 return @values;
161             }
162              
163             sub value_for_id {
164 279936     279936 1 215597 my $self = shift;
165 279936         314144 my $t = $self->template;
166 279936         464865 my $position = $t->id_position;
167 279936 100       405145 return unless defined $position;
168 279934         400618 return $self->value_for_position($position);
169             }
170              
171             sub specifies_value_for {
172 1058     1058 1 35446 my $self = shift;
173 1058         1762 my $rule_template = $self->template;
174 1058         2670 return $rule_template->specifies_value_for(@_);
175             }
176              
177             sub value_for {
178 2226     2226 1 86551 my $self = shift;
179 2226         2177 my $property_name = shift;
180              
181             # TODO: refactor to be more efficient
182 2226         3260 my $template = $self->template;
183 2226         3652 my $h = $self->legacy_params_hash;
184 2226         2100 my $v;
185 2226 100       3527 if (exists $h->{$property_name}) {
186             # normal case
187 1745         1907 $v = $h->{$property_name};
188 1745         4013 my $tmpl_pos = $template->value_position_for_property_name($property_name);
189 1745 100       5654 if (exists $self->{'hard_refs'}->{$tmpl_pos}) {
    100          
190 47         85 $v = $self->{'hard_refs'}->{$tmpl_pos}; # It was stored during resolve() as a hard ref
191             }
192             elsif ($self->_value_is_old_style_operator_and_value($v)) {
193 209         262 $v = $v->{'value'}; # It was old style operator/value hash
194             }
195             } else {
196             # No value found under that name... try decomposing the id
197 481 100       1180 return if $property_name eq 'id';
198 252         611 my $id_value = $self->value_for('id');
199 252         665 my $class_meta = $self->subject_class_name->__meta__();
200 252         1252 my @id_property_values = $class_meta->get_composite_id_decomposer->($id_value);
201              
202 252         739 my @id_property_names = $class_meta->id_property_names;
203 252         620 for (my $i = 0; $i < @id_property_names; $i++) {
204 450 100       1132 if ($id_property_names[$i] eq $property_name) {
205 96         97 $v = $id_property_values[$i];
206 96         166 last;
207             }
208             }
209             }
210 1997         7039 return $v;
211             }
212              
213             sub value_for_position {
214 279934     279934 0 258896 my ($self, $pos) = @_;
215 279934         387786 return ($self->values)[$pos];
216             }
217              
218             sub operator_for {
219 272     272 1 6665 my $self = shift;
220 272         470 my $t = $self->template;
221 272         649 return $t->operator_for(@_);
222             }
223              
224             sub underlying_rules {
225 52     52 0 422 my $self = shift;
226 52 100       133 unless (exists $self->{'_underlying_rules'}) {
227 1         3 my @values = $self->values;
228 1         2 $self->{'_underlying_rules'} = [ $self->template->get_underlying_rules_for_values(@values) ];
229             }
230 52         36 return @{ $self->{'_underlying_rules'} };
  52         139  
231             }
232              
233             # De-compose the rule back into its original form.
234             sub params_list {
235             # This is the reverse of the bulk of resolve.
236             # It returns the params in list form, directly coercable into a hash if necessary.
237             # $r = UR::BoolExpr->resolve($c1,@p1);
238             # ($c2, @p2) = ($r->subject_class_name, $r->params_list);
239 131612     131612 0 112513 my $self = shift;
240 131612         157831 my $template = $self->template;
241 131612         201651 my @values_sorted = $self->values;
242 131612         348262 return $template->params_list_for_values(@values_sorted);
243             }
244              
245             # TODO: replace these with the logical set operations
246             # FIXME: the name is confusing b/c it doesn't mutate the object, it returns a different object
247             sub add_filter {
248 655     655 0 757 my $self = shift;
249 655         1624 return __PACKAGE__->resolve($self->subject_class_name, $self->params_list, @_);
250             }
251              
252             # TODO: replace these with the logical set operations
253             # FIXME: the name is confusing b/c it doesn't mutate the object, it returns a different object
254             sub remove_filter {
255 602     602 0 631 my $self = shift;
256 602         593 my $property_name = shift;
257 602         959 my @params_list = $self->params_list;
258 602         679 my @new_params_list;
259 602         1397 for (my $n=0; $n<=$#params_list; $n+=2) {
260 945         939 my $key = $params_list[$n];
261 945 100       9615 if ($key =~ /^$property_name\b/) {
262 571         1395 next;
263             }
264 374         566 my $value = $params_list[$n+1];
265 374         927 push @new_params_list, $key, $value;
266             }
267 602         1532 return __PACKAGE__->resolve($self->subject_class_name, @new_params_list);
268             }
269              
270             # as above, doesn't mutate, just returns a different bx
271             sub sub_classify {
272 0     0 0 0 my ($self,$subclass_name) = @_;
273 0         0 my ($t,@v) = $self->template_and_values();
274 0         0 return $t->sub_classify($subclass_name)->get_rule_for_values(@v);
275             }
276              
277             # flyweight constructor
278             # like regular UR::Value objects, but kept separate from the cache but kept
279             # out of the regular transaction cache so they alwasy vaporize when derefed
280             sub get {
281 743715     743715 1 622626 my $rule_id = pop;
282 743715 100       1399332 unless (exists $UR::Object::rules->{$rule_id}) {
283 634825         634475 my $pos = index($rule_id,$UR::BoolExpr::Util::id_sep);
284 634825         1119022 my ($template_id,$value_id) = (substr($rule_id,0,$pos), substr($rule_id,$pos+1));
285 634825         1424778 my $rule = { id => $rule_id, template_id => $template_id, value_id => $value_id };
286 634825         677325 bless ($rule, "UR::BoolExpr");
287 634825         992372 $UR::Object::rules->{$rule_id} = $rule;
288 634825         1523613 Scalar::Util::weaken($UR::Object::rules->{$rule_id});
289 634825         1025951 return $rule;
290             }
291 108890         158737 return $UR::Object::rules->{$rule_id};
292             }
293              
294             # because these are weakened
295             sub DESTROY {
296 634787     634787   3027340 delete $UR::Object::rules->{$_[0]->{id}};
297             }
298              
299             sub flatten_hard_refs {
300 344092     344092 0 270912 my $self = $_[0];
301 344092 100       713648 return $self if not $self->{hard_refs};
302              
303 179         457 my $subject_class_name = $self->subject_class_name;
304 179         497 my $meta = $subject_class_name->__meta__;
305 179         457 my %params = $self->_params_list;
306 179         256 my $changes = 0;
307 179         422 for my $key (keys %params) {
308 183         260 my $value = $params{$key};
309 183 100 100     1050 if (ref($value) and Scalar::Util::blessed($value) and $value->isa("UR::Object")) {
      66        
310 8         46 my ($property_name, $op) = ($key =~ /^(\S+)\s*(.*)/);
311              
312 8         81 my $pmeta = $meta->property($property_name);
313 8         36 my $final_pmeta = $pmeta->final_property_meta();
314              
315 8         19 my @possible_data_types = uniq grep { $_ } map { $_->data_type } ($pmeta, $final_pmeta);
  16         59  
  16         30  
316 8 50       30 unless (@possible_data_types) {
317             # this might not be possible at runtime
318 0         0 croak sprintf 'unable to determine data type for property: %s', $property_name;
319             }
320              
321 8     8   58 my $data_type = first { $value->isa($_) } @possible_data_types;
  8         32  
322 8 50       56 unless ($data_type) {
323 0         0 croak sprintf 'value type, %s, is incompatible with: %s', $value->class, join(', ', @possible_data_types);
324             }
325              
326 8         8 my $value2 = do {
327 8         10 local $@;
328 8         15 eval {
329 8         24 $data_type->get($value->id)
330             };
331             };
332 8 50       25 unless ($value2) {
333 0         0 croak sprintf 'unable to retrieve a %s by value ID: %s', $data_type, $value->id;
334             }
335              
336 8 100       31 unless ($value2 eq $value) {
337 1         5 croak sprintf 'retrieved duplicate %s with ID, %s,', $data_type, $value->id;
338             }
339              
340             # safe to re-represent as .id
341 7         16 my $new_key = $property_name . '.id';
342 7 50       21 $new_key .= ' ' . $op if $op;
343 7         12 delete $params{$key};
344 7         22 $params{$new_key} = $value->id;
345 7         21 $changes++;
346             }
347             }
348 178 100       369 if ($changes) {
349 7         30 return $self->resolve_normalized($subject_class_name, %params);
350             } else {
351 171         443 return $self;
352             }
353             }
354              
355             sub resolve_normalized {
356 63633     63633 0 58101 my $class = shift;
357 63633         116825 my ($unnormalized_rule, @extra) = $class->resolve(@_);
358 63633         111051 my $normalized_rule = $unnormalized_rule->normalize();
359 63633 50       103222 return if !defined(wantarray);
360 63633 100       96756 return ($normalized_rule,@extra) if wantarray;
361 62665 50       96495 if (@extra) {
362 266     266   1383 no warnings;
  266         380  
  266         280337  
363 0         0 my $rule_class = $normalized_rule->subject_class_name;
364 0         0 Carp::confess("Extra params for class $rule_class found: @extra\n");
365             }
366 62665         119278 return $normalized_rule;
367             }
368              
369             sub resolve_for_template_id_and_values {
370 0     0 0 0 my ($class,$template_id, @values) = @_;
371 0         0 my $value_id = UR::BoolExpr::Util::values_to_value_id(@values);
372 0         0 my $rule_id = $class->__meta__->resolve_composite_id_from_ordered_values($template_id,$value_id);
373 0         0 $class->get($rule_id);
374             }
375              
376              
377             # Return true if it's a hashref that specifies the old-style operator/value
378             # like property => { operator => '=', value => 1 }
379             # FYI, the new way to do this is:
380             # 'property =' => 1
381             sub _value_is_old_style_operator_and_value {
382 2852     2852   3468 my($class,$value) = @_;
383              
384             return (ref($value) eq 'HASH')
385             &&
386             (exists($value->{'operator'}))
387             &&
388             (exists($value->{'value'}))
389             &&
390             ( (keys(%$value) == 2)
391             ||
392             ((keys(%$value) == 3)
393 2852   33     15215 && exists($value->{'escape'}))
394             );
395             }
396              
397              
398             my $resolve_depth;
399             sub resolve {
400 190580     190580 0 165643 $resolve_depth++;
401 190580 50       278757 Carp::confess("Deep recursion in UR::BoolExpr::resolve()!") if $resolve_depth > 10;
402              
403             # handle the case in which we've already processed the params into a boolexpr
404 190580 100 100     416761 if ( @_ == 3 and ref($_[2]) and ref($_[2])->isa("UR::BoolExpr") ) {
      100        
405 5992         5976 $resolve_depth--;
406 5992         11825 return $_[2];
407             }
408              
409 184588         158953 my $class = shift;
410 184588         142039 my $subject_class = shift;
411 184588 50       253199 Carp::confess("Can't resolve BoolExpr: expected subject class as arg 2, got '$subject_class'") if not $subject_class;
412             # support for legacy passing of hashref instead of object or list
413             # TODO: eliminate the need for this
414 184588         143112 my @in_params;
415 184588 100 100     1112377 if ($subject_class->isa('UR::Value::PerlReference') and $subject_class eq 'UR::Value::' . ref($_[0])) {
    100          
416 1         2 @in_params = @_;
417             }
418             elsif (ref($_[0]) eq "HASH") {
419 2         4 @in_params = %{$_[0]};
  2         11  
420             }
421             else {
422 184585         289267 @in_params = @_;
423             }
424              
425 184588 100 100     628261 if (defined($in_params[0]) and $in_params[0] eq '-or') {
426 31         49 shift @in_params;
427 31         41 my @sub_queries = @{ shift @in_params };
  31         59  
428              
429 31         33 my @meta_params;
430 31         114 for (my $i = 0; $i < @in_params; $i += 2 ) {
431 3 50       15 if ($in_params[$i] =~ m/^-/) {
432 3         13 push @meta_params, $in_params[$i], $in_params[$i+1];
433             }
434             }
435              
436 31         176 my $bx = UR::BoolExpr::Template::Or->_compose(
437             $subject_class,
438             \@sub_queries,
439             \@meta_params,
440             );
441              
442 31         40 $resolve_depth--;
443 31         73 return $bx;
444             }
445              
446 184557 100       436075 if (@in_params == 1) {
    50          
447 6194         11614 unshift @in_params, "id";
448             }
449             elsif (@in_params % 2 == 1) {
450 0         0 Carp::carp("Odd number of params while creating $class: (",join(',',@in_params),")");
451             }
452              
453             # split the params into keys and values
454             # where an operator is on the right-side, it is moved into the key
455 184557         181909 my $count = @in_params;
456 184557         160057 my (@keys,@values,@constant_values,$key,$value,$property_name,$operator,@hard_refs);
457 184557         319248 for(my $n = 0; $n < $count;) {
458 217869         221380 $key = $in_params[$n++];
459 217869         183820 $value = $in_params[$n++];
460              
461 217869 50       312842 unless (defined $key) {
462 0         0 Carp::croak("Can't resolve BoolExpr: undef is an invalid key/property name. Args were: ".join(', ',@in_params));
463             }
464              
465 217869 100       439658 if (UR::BoolExpr::Util::is_meta_param($key)) {
466             # these are keys whose values live in the rule template
467 650         810 push @keys, $key;
468 650         791 push @constant_values, $value;
469 650         1246 next;
470             }
471              
472 217219 100       417723 if ($key =~ m/^(_id_only|_param_key|_unique|__get_serial|_change_count)$/) {
473             # skip the pair: legacy/internal cruft
474 93         142 next;
475             }
476              
477 217126         263696 my $pos = index($key,' ');
478 217126 100       260254 if ($pos != -1) {
479             # the key is "propname op"
480 790         1158 $property_name = substr($key,0,$pos);
481 790         1199 $operator = substr($key,$pos+1);
482 790 50       1721 if (substr($operator,0,1) eq ' ') {
483 0         0 $operator =~ s/^\s+//;
484             }
485             }
486             else {
487             # the key is "propname"
488 216336         171766 $property_name = $key;
489 216336         175922 $operator = '';
490             }
491              
492 217126 100       335142 if (my $ref = ref($value)) {
493 11080 100 100     41102 if ( (not $operator) and ($ref eq "HASH")) {
494 1154 100       2816 if ( $class->_value_is_old_style_operator_and_value($value)) {
495             # the key => { operator => $o, value => $v } syntax
496             # cannot be used with a value type of HASH
497             $operator = defined($value->{operator})
498             ? lc($value->{operator})
499 871 100       2288 : '';
500 871 50       1699 if (exists $value->{escape}) {
501             $operator .= "-" . $value->{escape}
502 0         0 }
503 871         1504 $key .= " " . $operator;
504 871         1069 $value = $value->{value};
505 871         1008 $ref = ref($value);
506             }
507             else {
508             # the HASH is a value for the specified param
509 283         366 push @hard_refs, scalar(@values), $value;
510             }
511             }
512              
513 11080 100       18850 if ($ref eq "ARRAY") {
514 1805 100       3511 if (not $operator) {
    50          
515             # key => [] is the same as "key in" => []
516 1397         1467 $operator = 'in';
517 1397         1877 $key .= ' in';
518             }
519             elsif ($operator eq 'not') {
520             # "key not" => [] is the same as "key not in"
521 0         0 $operator .= ' in';
522 0         0 $key .= ' in';
523             }
524              
525 1805         2643 foreach my $val (@$value) {
526 2724 100       4584 if (ref($val)) {
527             # when there are any refs in the arrayref
528             # we must keep the arrayerf contents
529             # to reconstruct effectively
530 54         81 push @hard_refs, scalar(@values), $value;
531 54         84 last;
532             }
533             }
534              
535             } # done handling ARRAY value
536              
537             } # done handling ref values
538              
539 217126         207227 push @keys, $key;
540 217126         395482 push @values, $value;
541             }
542              
543             # the above uses no class metadata
544              
545             # this next section uses class metadata
546             # it should be moved into the normalization layer
547              
548 184557         141016 my $subject_class_meta;
549 184557         129266 my $exception = do {
550 184557         149269 local $@;
551 184557         185221 $subject_class_meta = eval { $subject_class->__meta__ };
  184557         459488  
552 184557         262415 $@;
553             };
554 184557 50       272698 if ($exception) {
555 0         0 Carp::croak("Can't get class metadata for $subject_class. Is it a valid class name?\nErrors were: $exception");
556             }
557 184557 50       322939 unless ($subject_class_meta) {
558 0         0 Carp::croak("No class metadata for $subject_class?!");
559             }
560              
561             my $subject_class_props =
562             $subject_class_meta->{'cache'}{'UR::BoolExpr::resolve'} ||=
563 184557   100     416570 { map {$_, 1} ( $subject_class_meta->all_property_type_names) };
  53079         64298  
564              
565 184557         232630 my($kn, $vn, $cn, $complex_values) = (0,0,0,0);
566 184557         149952 my ($op,@extra,@xadd_keys,@xadd_values,@xremove_keys,@xremove_values,@extra_key_pos,@extra_value_pos,
567             @swap_key_pos,@swap_key_value);
568              
569 184557         201359 for my $value (@values) {
570 217275         197299 $key = $keys[$kn++];
571 217275 100       319468 if (UR::BoolExpr::Util::is_meta_param($key)) {
572 149         173 $cn++;
573 149         169 redo;
574             }
575             else {
576 217126         162365 $vn++;
577             }
578              
579 217126         236832 my $pos = index($key,' ');
580 217126 100       244525 if ($pos != -1) {
581             # "propname op"
582 3054         3788 $property_name = substr($key,0,$pos);
583 3054         3876 $operator = substr($key,$pos+1);
584 3054 100       6070 if (substr($operator,0,1) eq ' ') {
585 4         14 $operator =~ s/^\s+//;
586             }
587             }
588             else {
589             # "propname"
590 214072         164484 $property_name = $key;
591 214072         172911 $operator = '';
592             }
593              
594             # account for the case where this parameter does
595             # not match an actual property
596 217126         162062 my $base_property_name = $property_name;
597 217126         365031 $base_property_name =~ s/[.-].+//;
598 217126 100       380057 if (!exists $subject_class_props->{$base_property_name}) {
599 230 50       606 if (substr($property_name,0,1) eq '_') {
600 0         0 warn "ignoring $property_name in $subject_class bx construction!"
601             }
602             else {
603 230         373 push @extra_key_pos, $kn-1;
604 230         283 push @extra_value_pos, $vn-1;
605 230         381 next;
606             }
607             }
608              
609 216896         186366 my $ref = ref($value);
610 216896 100       367086 if($ref) {
611 10404         9800 $complex_values = 1;
612 10404 100 100     53057 if ($ref eq "ARRAY" and $operator ne 'between' and $operator ne 'not between') {
    100 100        
    100          
613 1558         1466 my $data_type;
614             my $is_many;
615 1558 50       2458 if ($UR::initialized) {
616 1558         4609 my $property_meta = $subject_class_meta->property_meta_for_name($property_name);
617 1558 50       2934 unless (defined $property_meta) {
618 0         0 push @extra_key_pos, $kn-1;
619 0         0 push @extra_value_pos, $vn-1;
620 0         0 next;
621             }
622 1558         4255 $data_type = $property_meta->data_type;
623 1558         3331 $is_many = $property_meta->is_many;
624             }
625             else {
626 0 0       0 if (exists $subject_class_meta->{has}{$property_name}) {
627 0         0 $data_type = $subject_class_meta->{has}{$property_name}{data_type};
628 0         0 $is_many = $subject_class_meta->{has}{$property_name}{is_many};
629             }
630             }
631 1558   100     3108 $data_type ||= '';
632              
633 1558 100       3831 if ($data_type eq 'ARRAY') {
    100          
634             # ensure we re-constitute the original array not a copy
635 219         281 push @hard_refs, $vn-1, $value;
636 219         232 push @swap_key_pos, $vn-1;
637 219         314 push @swap_key_value, $property_name;
638             }
639             elsif (not $is_many) {
640 266     266   1392 no warnings;
  266         353  
  266         607468  
641              
642             # sort and replace
643             # note that in perl5.10 and above strings like "inf*" have a numeric value
644             # causing this kind of sorting to do surprising things. Hopefully looks_like_number()
645             # does the right thing with these.
646             #
647             # undef/null sorts at the end
648 831 50   831   1368 my $sorter = sub { if (! defined($a)) { return 1 }
  0         0  
649 831 100       1189 if (! defined($b)) { return -1}
  1         3  
650 955         3521 return $a cmp $b; };
  830         1653  
651 955         2948 $value = [ sort $sorter @$value ];
652              
653             # Remove duplicates from the list
654 955         1073 my $last = $value;
655 955         2408 for (my $i = 0; $i < @$value;) {
656 1480 100       2508 if ($last eq $value->[$i]) {
657 19         52 splice(@$value, $i, 1);
658             }
659             else {
660 1461         5325 $last = $value->[$i++];
661             }
662             }
663             # push @swap_key_pos, $vn-1;
664             # push @swap_key_value, $property_name;
665             }
666             else {
667             # disable: break 47, enable: break 62
668             #push @swap_key_pos, $vn-1;
669             #push @swap_key_value, $property_name;
670             }
671             }
672             elsif (blessed($value)) {
673 8289         22971 my $property_meta = $subject_class_meta->property_meta_for_name($property_name);
674 8289 50       16208 unless ($property_meta) {
675 0         0 for my $class_name ($subject_class_meta->ancestry_class_names) {
676 0         0 my $class_object = $class_name->__meta__;
677 0         0 $property_meta = $subject_class_meta->property_meta_for_name($property_name);
678 0 0       0 last if $property_meta;
679             }
680 0 0       0 unless ($property_meta) {
681 0         0 Carp::croak("No property metadata for $subject_class property '$property_name'");
682             }
683             }
684              
685 8289 100 100     21805 if ($property_meta->id_by or $property_meta->reverse_as) {
    100          
    50          
686 8173         15088 my $property_meta = $subject_class_meta->property_meta_for_name($property_name);
687 8173 50       14394 unless ($property_meta) {
688 0         0 Carp::croak("No property metadata for $subject_class property '$property_name'");
689             }
690              
691 8173         23174 my @joins = $property_meta->get_property_name_pairs_for_join();
692 8173         10947 for my $join (@joins) {
693             # does this really work for >1 joins?
694 19690         22490 my ($my_method, $their_method) = @$join;
695 19690         16854 push @xadd_keys, $my_method;
696 19690         49710 push @xadd_values, $value->$their_method;
697             }
698             # TODO: this may need to be moved into the above get_property_name_pairs_for_join(),
699             # but the exact syntax for expressing that this is part of the join is unclear.
700 8173 100       18341 if (my $id_class_by = $property_meta->id_class_by) {
701 11         12 push @xadd_keys, $id_class_by;
702 11         13 push @xadd_values, ref($value);
703             }
704 8173         10272 push @xremove_keys, $kn-1;
705 8173         16754 push @xremove_values, $vn-1;
706             }
707             elsif ($property_meta->is_valid_storage_for_value($value)) {
708 84         873 push @hard_refs, $vn-1, $value;
709             }
710             elsif ($value->can($property_name)) {
711             # TODO: stop suporting foo_id => $foo, since you can do foo=>$foo, and foo_id=>$foo->id
712             # Carp::cluck("using $property_name => \$obj to get $property_name => \$obj->$property_name is deprecated...");
713 32         277 $value = $value->$property_name;
714             }
715             else {
716 0 0       0 $operator = 'eq' unless $operator;
717 0         0 $DB::single = 1;
718 0         0 print $value->isa($property_meta->_data_type_as_class_name),"\n";
719 0         0 print $value->isa($property_meta->_data_type_as_class_name),"\n";
720 0         0 Carp::croak("Invalid data type in rule. A value of type " . ref($value) . " cannot be used in class $subject_class property '$property_name' with operator $operator!");
721             }
722             # end of handling a value which is an arrayref
723             }
724             elsif ($ref ne 'HASH') {
725             # other reference, code, etc.
726 274         530 push @hard_refs, $vn-1, $value;
727             }
728             }
729             }
730 184556         164603 push @keys, @xadd_keys;
731 184556         143873 push @values, @xadd_values;
732              
733 184556 100       268974 if (@swap_key_pos) {
734 89         174 @keys[@swap_key_pos] = @swap_key_value;
735             }
736              
737 184556 100       262142 if (@extra_key_pos) {
738 184         246 push @xremove_keys, @extra_key_pos;
739 184         272 push @xremove_values, @extra_value_pos;
740 184         581 for (my $n = 0; $n < @extra_key_pos; $n++) {
741 230         660 push @extra, $keys[$extra_key_pos[$n]], $values[$extra_value_pos[$n]];
742             }
743             }
744              
745 184556 100       257690 if (@xremove_keys) {
746 4518         5279 my $write_key_idx = 0;
747 4518         13048 for (my($read_key_idx, $xremove_key_idx) = (0,0);
748             $read_key_idx < @keys;
749             $read_key_idx++
750             ) {
751 29823 100 100     60512 if ($xremove_key_idx < @xremove_keys
752             and
753             $read_key_idx == $xremove_keys[$xremove_key_idx]
754             ) {
755 8403         6938 $xremove_key_idx++;
756 8403         13707 next;
757             }
758 21420         31896 $keys[$write_key_idx++] = $keys[$read_key_idx];
759             }
760 4518         14320 $#keys = $write_key_idx-1;
761             }
762              
763 184556 100       252820 if (@xremove_values) {
764 4518 100       9159 if (@hard_refs) {
765             # shift the numbers down to account for positional removals
766 5         16 for (my $n = 0; $n < @hard_refs; $n+=2) {
767 5         6 my $ref_pos = $hard_refs[$n];
768 5         8 for my $rem_pos (@xremove_values) {
769 5 50       13 if ($rem_pos < $ref_pos) {
    0          
770 5         7 $hard_refs[$n] -= 1;
771             #print "$n from $ref_pos to $hard_refs[$n]\n";
772 5         13 $ref_pos = $hard_refs[$n];
773             }
774             elsif ($rem_pos == $ref_pos) {
775 0         0 $hard_refs[$n] = '';
776 0         0 $hard_refs[$n+1] = undef;
777             }
778             }
779             }
780             }
781              
782 4518         5194 my $write_value_idx = 0;
783 4518         11863 for(my($read_value_idx, $xremove_value_idx) = (0,0);
784             $read_value_idx < @values;
785             $read_value_idx++
786             ) {
787 29815 100 100     57403 if ($xremove_value_idx < @xremove_values
788             and
789             $read_value_idx == $xremove_values[$xremove_value_idx]
790             ) {
791 8403         6870 $xremove_value_idx++;
792 8403         12063 next;
793             }
794 21412         32805 $values[$write_value_idx++] = $values[$read_value_idx];
795             }
796 4518         9357 $#values = $write_value_idx-1;
797             }
798              
799 184556         149498 my $template;
800 184556 100       229733 if (@constant_values) {
801 589         2684 $template = UR::BoolExpr::Template::And->_fast_construct(
802             $subject_class,
803             \@keys,
804             \@constant_values,
805             );
806             }
807             else {
808 183967   66     931088 $template = $subject_class_meta->{cache}{"UR::BoolExpr::resolve"}{"template for class and keys without constant values"}{"$subject_class @keys"}
809             ||= UR::BoolExpr::Template::And->_fast_construct(
810             $subject_class,
811             \@keys,
812             \@constant_values,
813             );
814             }
815              
816 184555         361842 my $value_id = UR::BoolExpr::Util::values_to_value_id(@values);
817              
818 184555         342025 my $rule_id = join($UR::BoolExpr::Util::id_sep,$template->{id},$value_id);
819              
820 184555         337627 my $rule = __PACKAGE__->get($rule_id); # flyweight constructor
821              
822 184555         254013 $rule->{template} = $template;
823 184555         223659 $rule->{values} = \@values;
824              
825 184555         174688 $vn = 0;
826 184555         135489 $cn = 0;
827 184555         135018 my @list;
828 184555         182496 for my $key (@keys) {
829 229072         239196 push @list, $key;
830 229072 100       349428 if (UR::BoolExpr::Util::is_meta_param($key)) {
831 650         1107 push @list, $constant_values[$cn++];
832             }
833             else {
834 228422         326931 push @list, $values[$vn++];
835             }
836             }
837 184555         211251 $rule->{_params_list} = \@list;
838              
839 184555 100       292579 if (@hard_refs) {
840 570         1647 $rule->{hard_refs} = { @hard_refs };
841 570         898 delete $rule->{hard_refs}{''};
842             }
843              
844 184555         159428 $resolve_depth--;
845 184555 100 66     246844 if (wantarray) {
    100          
846 178404         641453 return ($rule, @extra);
847             }
848             elsif (@extra && defined wantarray) {
849 1 50       4 Carp::confess("Unknown parameters in rule for $subject_class: " . join(",", map { defined($_) ? "'$_'" : "(undef)" } @extra));
  2         229  
850             }
851             else {
852 6150         26677 return $rule;
853             }
854             }
855              
856             sub _params_list {
857 5443   66 5443   12131 my $list = $_[0]->{_params_list} ||= do {
858 79         104 my $self = $_[0];
859 79         159 my $template = $self->template;
860 79 100       423 $self->values unless $self->{values};
861 79         94 my @list;
862             # are method calls really too expensive here?
863 79         132 my $template_class = ref($template);
864 79 100       207 if ($template_class eq 'UR::BoolExpr::Template::And') {
    50          
    0          
865 54         132 my ($k,$v,$c) = ($template->{_keys}, $self->{values}, $template->{_constant_values});
866 54         71 my $vn = 0;
867 54         77 my $cn = 0;
868 54         114 for my $key (@$k) {
869 123         151 push @list, $key;
870 123 100       202 if (UR::BoolExpr::Util::is_meta_param($key)) {
871 18         42 push @list, $c->[$cn++];
872             }
873             else {
874 105         198 push @list, $v->[$vn++];
875             }
876             }
877             }
878             elsif ($template_class eq 'UR::BoolExpr::Template::Or') {
879 25         28 my @sublist;
880 25         76 my @u = $self->underlying_rules();
881 25         43 for my $u (@u) {
882 62         121 my @p = $u->_params_list;
883 62         98 push @sublist, \@p;
884             }
885 25         76 @list = (-or => \@sublist);
886             }
887             elsif ($template_class->isa("UR::BoolExpr::PropertyComparison")) {
888 0         0 @list = ($template->logic_detail => [@{$self->{values}}]);
  0         0  
889             }
890 79         192 \@list;
891             };
892 5443         14551 return @$list;
893             }
894              
895             sub normalize {
896 517777     517777 1 411049 my $self = shift;
897              
898 517777         628208 my $rule_template = $self->template;
899              
900 517777 100       827080 if ($rule_template->{is_normalized}) {
901 179096         253317 return $self;
902             }
903 338681         481021 my @unnormalized_values = $self->values();
904              
905 338681         771633 my $normalized = $rule_template->get_normalized_rule_for_values(@unnormalized_values);
906 338681 50       512749 return unless defined $normalized;
907              
908 338681 100       544918 if (my $special = $self->{hard_refs}) {
909 64         378 $normalized->{hard_refs} = $rule_template->_normalize_non_ur_values_hash($special);
910             }
911 338681         557185 return $normalized;
912             }
913              
914             # a handful of places still use this
915             sub legacy_params_hash {
916 61139     61139 0 55425 my $self = shift;
917              
918             # See if we have one already.
919 61139         63408 my $params_array = $self->{legacy_params_array};
920 61139 100       99311 return { @$params_array } if $params_array;
921              
922             # Make one by starting with the one on the rule template
923 58422         74273 my $rule_template = $self->template;
924 58422         49721 my $params = { %{$rule_template->legacy_params_hash}, $self->params_list };
  58422         118409  
925              
926             # If the template has a _param_key, fill it in.
927 58422 100       137881 if (exists $params->{_param_key}) {
928 3357         8296 $params->{_param_key} = $self->id;
929             }
930              
931             # This was cached above and will return immediately on the next call.
932             # Note: the caller should copy this reference before making changes.
933 58422         201785 $self->{legacy_params_array} = [ %$params ];
934 58422         121619 return $params;
935             }
936              
937              
938             my $LOADED_BXPARSE = 0;
939             sub resolve_for_string {
940 125     125 0 5803 my ($class, $subject_class_name, $filter_string, $usage_hints_string, $order_string, $page_string) = @_;
941              
942 125 100       272 unless ($LOADED_BXPARSE) {
943 4         7 my $exception = do {
944 4         4 local $@;
945 4         6 eval { require UR::BoolExpr::BxParser };
  4         2560  
946 4         288 $@;
947             };
948 4 50       19 if ($exception) {
949 0         0 Carp::croak("resolve_for_string() can't load UR::BoolExpr::BxParser: $exception");
950             }
951 4         8 $LOADED_BXPARSE=1;
952             }
953              
954             #$DB::single=1;
955             #my $tree = UR::BoolExpr::BxParser::parse($filter_string, tokdebug => 1, yydebug => 7);
956 125         321 my($tree, $remaining_strref) = UR::BoolExpr::BxParser::parse($filter_string);
957 114 50       236 unless ($tree) {
958 0         0 Carp::croak("resolve_for_string() couldn't parse string \"$filter_string\"");
959             }
960              
961 114 100       218 push @$tree, '-hints', [split(',',$usage_hints_string) ] if ($usage_hints_string);
962 114 100       216 push @$tree, '-order_by', [split(',',$order_string) ] if ($order_string);
963 114 50       162 push @$tree, '-page', [split(',',$page_string) ] if ($page_string);
964              
965 114         98 my ($bx, @extra);
966 114 100       157 if(wantarray) {
967 6         26 ($bx, @extra) = UR::BoolExpr->resolve($subject_class_name, @$tree);
968             } else {
969 108         325 $bx = UR::BoolExpr->resolve($subject_class_name, @$tree);
970             }
971 114 50       299 unless ($bx) {
972 0         0 Carp::croak("Can't create BoolExpr on $subject_class_name from params generated from string "
973             . $filter_string . " which parsed as:\n"
974             . Data::Dumper::Dumper($tree));
975             }
976 114 50       247 if ($$remaining_strref) {
977 0         0 Carp::croak("Trailing input after the parsable end of the filter string: '". $$remaining_strref."'");
978             }
979 114 100       181 if(wantarray) {
980 6         32 return ($bx, @extra);
981             } else {
982 108         360 return $bx;
983             }
984             }
985              
986             sub _resolve_from_filter_array {
987 0     0     my $class = shift;
988              
989 0           my $subject_class_name = shift;
990 0           my $filters = shift;
991 0           my $usage_hints = shift;
992 0           my $order = shift;
993 0           my $page = shift;
994              
995 0           my @rule_filters;
996              
997             my @keys;
998 0           my @values;
999              
1000 0           for my $fdata (@$filters) {
1001 0           my $rule_filter;
1002              
1003             # rule component
1004 0           my $key = $fdata->[0];
1005 0           my $value;
1006              
1007             # process the operator
1008 0 0         if ($fdata->[1] =~ /^!?(:|@|between|in)$/i) {
    0          
1009              
1010 0           my @list_parts;
1011             my @range_parts;
1012              
1013 0 0         if ($fdata->[1] eq "@") {
1014             # file path
1015 0           my $fh = IO::File->new($fdata->[2]);
1016 0 0         unless ($fh) {
1017 0           die "Failed to open file $fdata->[2]: $!\n";
1018             }
1019 0           @list_parts = $fh->getlines;
1020 0           chomp @list_parts;
1021 0           $fh->close;
1022             }
1023             else {
1024 0           @list_parts = split(/\//,$fdata->[2]);
1025 0           @range_parts = split(/-/,$fdata->[2]);
1026             }
1027              
1028 0 0         if (@list_parts > 1) {
    0          
1029 0 0         my $op = ($fdata->[1] =~ /^!/ ? 'not in' : 'in');
1030             # rule component
1031 0 0         if (substr($key, -3, 3) ne ' in') {
1032 0           $key = join(' ', $key, $op);
1033             }
1034 0           $value = \@list_parts;
1035 0           $rule_filter = [$fdata->[0],$op,\@list_parts];
1036             }
1037             elsif (@range_parts >= 2) {
1038 0 0         if (@range_parts > 2) {
    0          
1039 0 0         if (@range_parts % 2) {
1040 0           die "The \":\" operator expects a range sparated by a single dash: @range_parts ." . "\n";
1041             }
1042             else {
1043 0           my $half = (@range_parts)/2;
1044 0           $a = join("-",@range_parts[0..($half-1)]);
1045 0           $b = join("-",@range_parts[$half..$#range_parts]);
1046             }
1047             }
1048             elsif (@range_parts == 2) {
1049 0           ($a,$b) = @range_parts;
1050             }
1051             else {
1052 0           die 'The ":" operator expects a range sparated by a dash.' . "\n";
1053             }
1054              
1055 0           $key = $fdata->[0] . " between";
1056 0           $value = [$a, $b];
1057 0           $rule_filter = [$fdata->[0], "between", [$a, $b] ];
1058             }
1059             else {
1060 0           die 'The ":" operator expects a range sparated by a dash, or a slash-separated list.' . "\n";
1061             }
1062              
1063             }
1064             # this accounts for cases where value is null
1065             elsif (length($fdata->[2])==0) {
1066 0 0         if ($fdata->[1] eq "=") {
1067 0           $key = $fdata->[0];
1068 0           $value = undef;
1069 0           $rule_filter = [ $fdata->[0], "=", undef ];
1070             }
1071             else {
1072 0           $key = $fdata->[0] . " !=";
1073 0           $value = undef;
1074 0           $rule_filter = [ $fdata->[0], "!=", undef ];
1075             }
1076             }
1077             else {
1078 0   0       $key = $fdata->[0] . ($fdata->[1] and $fdata->[1] ne '='? ' ' . $fdata->[1] : '');
1079 0           $value = $fdata->[2];
1080 0           $rule_filter = [ @$fdata ];
1081             }
1082              
1083 0           push @keys, $key;
1084 0           push @values, $value;
1085             }
1086 0 0 0       if ($usage_hints or $order or $page) {
      0        
1087             # todo: incorporate hints in a smarter way
1088 0           my %p;
1089 0           for my $key (@keys) {
1090 0           $p{$key} = shift @values;
1091             }
1092 0 0         return $class->resolve(
    0          
    0          
1093             $subject_class_name,
1094             %p,
1095             ($usage_hints ? (-hints => $usage_hints) : () ),
1096             ($order ? (-order => $order) : () ),
1097             ($page ? (-page => $page) : () ),
1098             );
1099             }
1100             else {
1101 0           return UR::BoolExpr->_resolve_from_subject_class_name_keys_and_values(
1102             subject_class_name => $subject_class_name,
1103             keys => \@keys,
1104             values=> \@values,
1105             );
1106             }
1107              
1108             }
1109              
1110             sub _resolve_from_subject_class_name_keys_and_values {
1111 0     0     my $class = shift;
1112              
1113 0           my %params = @_;
1114 0           my $subject_class_name = $params{subject_class_name};
1115 0 0         my @values = @{ $params{values} || [] };
  0            
1116 0 0         my @constant_values = @{ $params{constant_values} || [] };
  0            
1117 0 0         my @keys = @{ $params{keys} || [] };
  0            
1118 0 0         die "unexpected params: " . Data::Dumper::Dumper(\%params) if %params;
1119              
1120 0           my $value_id = UR::BoolExpr::Util::values_to_value_id(@values);
1121 0           my $constant_value_id = UR::BoolExpr::Util::values_to_value_id(@constant_values);
1122              
1123 0           my $template_id = $subject_class_name . '/And/' . join(",",@keys) . "/" . $constant_value_id;
1124 0           my $rule_id = join($UR::BoolExpr::Util::id_sep,$template_id,$value_id);
1125              
1126 0           my $rule = __PACKAGE__->get($rule_id);
1127              
1128 0           $rule->{values} = \@values;
1129              
1130 0           return $rule;
1131             }
1132              
1133             1;
1134              
1135             =pod
1136              
1137             =head1 NAME
1138              
1139             UR::BoolExpr - a "where clause" for objects
1140              
1141             =head1 SYNOPSIS
1142              
1143             my $o = Acme::Employee->create(
1144             ssn => '123-45-6789',
1145             name => 'Pat Jones',
1146             status => 'active',
1147             start_date => UR::Context->current->now,
1148             payroll_category => 'hourly',
1149             boss => $other_employee,
1150             );
1151              
1152             my $bx = Acme::Employee->define_boolexpr(
1153             'payroll_category' => 'hourly',
1154             'status' => ['active','terminated'],
1155             'name like' => '%Jones',
1156             'ssn matches' => '\d{3}-\d{2}-\d{4}',
1157             'start_date between' => ['2009-01-01','2009-02-01'],
1158             'boss.name in' => ['Cletus Titus', 'Mitzy Mayhem'],
1159             );
1160              
1161             $bx->evaluate($o); # true
1162              
1163             $bx->specifies_value_for('payroll_category') # true
1164              
1165             $bx->value_for('payroll_cagtegory') # 'hourly'
1166              
1167             $o->payroll_category('salary');
1168             $bx->evaluate($o); # false
1169              
1170             # these could take either a boolean expression, or a list of params
1171             # from which it will generate one on-the-fly
1172             my $set = Acme::Employee->define_set($bx); # same as listing all of the params
1173             my @matches = Acme::Employee->get($bx); # same as above, but returns the members
1174              
1175             my $bx2 = $bx->reframe('boss');
1176             #'employees.payroll_category' => 'hourly',
1177             #'employees.status' => ['active','terminated'],
1178             #'employees.name like' => '%Jones',
1179             #'employees.ssn matches' => '\d{3}-\d{2}-\d{4}',
1180             #'employees.start_date between' => ['2009-01-01','2009-02-01'],
1181             #'name in' => ['Cletus Titus', 'Mitzy Mayhem'],
1182              
1183             my $bx3 = $bx->flatten();
1184             # any indirection in the params takes the form a.b.c at the lowest level
1185             # also 'payroll_category' might become 'pay_history.category', and 'pay_history.is_current' => 1 is added to the list
1186             # if this parameter has that as a custom filter
1187              
1188              
1189             =head1 DESCRIPTION
1190              
1191             A UR::BoolExpr object captures a set of match criteria for some class of object.
1192              
1193             Calls to get(), create(), and define_set() all use this internally to objectify
1194             their parameters. If given a boolean expression object directly they will use it.
1195             Otherwise they will construct one from the parameters given.
1196              
1197             They have a 1:1 correspondence within the WHERE clause in an SQL statement where
1198             RDBMS persistence is used. They also imply the FROM clause in these cases,
1199             since the query properties control which joins must be included to return
1200             the matching object set.
1201              
1202             =head1 REFLECTION
1203              
1204             The data used to create the boolean expression can be re-extracted:
1205              
1206             my $c = $r->subject_class_name;
1207             # $c eq "GSC::Clone"
1208              
1209             my @p = $r->params_list;
1210             # @p = four items
1211              
1212             my %p = $r->params_list;
1213             # %p = two key value pairs
1214              
1215             =head1 TEMPLATE SUBCLASSES
1216              
1217             The template behind the expression can be of type ::Or, ::And or ::PropertyComparison.
1218             These classes handle all of the operating logic for the expressions.
1219              
1220             Each of those classes incapsulates 0..n of the next type in the list. All templates
1221             simplify to this level. See L for details.
1222              
1223             =head1 CONSTRUCTOR
1224              
1225             =over 4
1226              
1227             my $bx = UR::BoolExpr->resolve('Some::Class', property_1 => 'value_1', ... property_n => 'value_n');
1228             my $bx1 = Some::Class->define_boolexpr(property_1 => value_1, ... property_n => 'value_n');
1229             my $bx2 = Some::Class->define_boolexpr('property_1 >' => 12345);
1230             my $bx3 = UR::BoolExpr->resolve_for_string(
1231             'Some::Class',
1232             'property_1 = value_1 and ( property_2 < value_2 or property_3 = value_3 )',
1233             );
1234              
1235             Returns a UR::BoolExpr object that can be used to perform tests on the given class and
1236             properties. The default comparison for each property is equality. The third example shows
1237             using greater-than operator for property_1. The last example shows constructing a
1238             UR::BoolExpr from a string containing properties, operators and values joined with
1239             'and' and 'or', with parentheses indicating precedence.
1240              
1241             =back
1242              
1243             C can parse simple and complicated expressions. A simple expression
1244             is a property name followed by an operator followed by a value. The property name can be
1245             a series of properties joined by dots (.) to indicate traversal of multiple layers of
1246             indirect properties. Values that include spaces, characters that look like operators,
1247             commas, or other special characters should be enclosed in quotes.
1248              
1249             The parser understands all the same operators the underlying C method understands:
1250             =, <, >, <=, >=, "like", "between" and "in". Operators may be prefixed by a bang (!) or the
1251             word "not" to negate the operator. The "like" operator understands the SQL wildcards % and _.
1252             Values for the "between" operator should be separated by a minus (-). Values for the "in"
1253             operator should begin with a left bracket, end with a right bracket, and have commas between
1254             them. For example:
1255             name_property in [Bob,Fred,Joe]
1256              
1257             Simple expressions may be joined together with the words "and" and "or" to form a more
1258             complicated expression. "and" has higher precedence than "or", and parentheses can
1259             surround sub-expressions to indicate the requested precedence. For example:
1260             ((prop1 = foo or prop2 = 1) and (prop2 > 10 or prop3 like 'Yo%')) or prop4 in [1,2,3]
1261              
1262             In general, whitespace is insignificant. The strings "prop1 = 1" is parsed the same as
1263             "prop1=1". Spaces inside quoted value strings are preserved. For backward compatibility
1264             with the deprecated string parser, bare words that appear after the operators =,<,>,<=
1265             and >= which are separated by one or more spaces is treated as if it had quotes around
1266             the list of words starting with the first character of the first word and ending with
1267             the last character of the last word, meaning that spaces at the start and end of the
1268             list are trimmed.
1269              
1270             Specific ordering may be requested by putting an "order by" clause at the end, and is the
1271             same as using a -order argument to resolve():
1272             score > 10 order by name,score.
1273              
1274             Likewise, grouping and Set construction is indicated with a "group by" clause:
1275             score > 10 group by color
1276              
1277             =head1 METHODS
1278              
1279             =over 4
1280              
1281             =item evaluate
1282              
1283             $bx->evaluate($object)
1284              
1285             Returns true if the given object satisfies the BoolExpr
1286              
1287              
1288             =item template_and_values
1289              
1290             ($template, @values) = $bx->template_and_values();
1291              
1292             Returns the UR::BoolExpr::Template and list of the values for the given BoolExpr
1293              
1294             =item is_subset_of
1295              
1296             $bx->is_subset_of($other_bx)
1297              
1298             Returns true if the set of objects that matches this BoolExpr is a subset of
1299             the set of objects that matches $other_bx. In practice this means:
1300              
1301             * The subject class of $bx isa the subject class of $other_bx
1302             * all the properties from $bx also appear in $other_bx
1303             * the operators and values for $bx's properties match $other_bx
1304              
1305             =item values
1306              
1307             @values = $bx->values
1308              
1309             Return a list of the values from $bx. The values will be in the same order
1310             the BoolExpr was created from
1311              
1312             =item value_for_id
1313              
1314             $id = $bx->value_for_id
1315              
1316             If $bx's properties include all the ID properties of its subject class,
1317             C returns that value. Otherwise, it returns the empty list.
1318             If the subject class has more than one ID property, this returns the value
1319             of the composite ID.
1320              
1321             =item specifies_value_for
1322              
1323             $bx->specifies_value_for('property_name');
1324              
1325             Returns true if the filter list of $bx includes the given property name
1326              
1327             =item value_for
1328              
1329             my $value = $bx->value_for('property_name');
1330              
1331             Return the value for the given property
1332              
1333             =item operator_for
1334              
1335             my $operator = $bx->operator_for('property_name');
1336              
1337             Return a string for the operator of the given property. A value of '' (the
1338             empty string) means equality ("="). Other possible values include '<', '>',
1339             '<=', '>=', 'between', 'true', 'false', 'in', 'not <', 'not >', etc.
1340              
1341             =item normalize
1342              
1343             $bx2 = $bx->normalize;
1344              
1345             A boolean expression can be changed in incidental ways and still be equivalent.
1346             This method converts the expression into a normalized form so that it can be
1347             compared to other normalized expressions without incidental differences
1348             affecting the comparison.
1349              
1350             =item flatten
1351              
1352             $bx2 = $bx->flatten();
1353              
1354             Transforms a boolean expression into a functional equivalent where
1355             indirect properties are turned into property chains.
1356              
1357             For instance, in a class with
1358              
1359             a => { is => "A", id_by => "a_id" },
1360             b => { via => "a", to => "bb" },
1361             c => { via => "b", to => "cc" },
1362              
1363             An expression of:
1364              
1365             c => 1234
1366              
1367             Becomes:
1368              
1369             a.bb.cc => 1234
1370              
1371             In cases where one of the indirect properties includes a "where" clause,
1372             the flattened expression would have an additional value for each element:
1373              
1374             a => { is => "A", id_by => "a_id" },
1375             b => { via => "a", to => "bb" },
1376             c => { via => "b", where ["xx" => 5678], to => "cc" },
1377              
1378             An expression of:
1379              
1380             c => 1234
1381              
1382             Becomes:
1383              
1384             a.bb.cc => 1234
1385             a.bb.xx => 5678
1386              
1387              
1388              
1389             =item reframe
1390              
1391             $bx = Acme::Order->define_boolexpr(status => 'active');
1392             $bx2 = $bx->reframe('customer');
1393              
1394             The above will turn a query for orders which are active into a query for
1395             customers with active orders, presuming an Acme::Order has a property called
1396             "customer" with a defined relationship to another class.
1397              
1398             =back
1399              
1400             =head1 INTERNAL STRUCTURE
1401              
1402             A boolean expression (or "rule") has an "id", which completely describes the rule in stringified form,
1403             and a method called evaluate($o) which tests the rule on a given object.
1404              
1405             The id is composed of two parts:
1406             - A template_id.
1407             - A value_id.
1408              
1409             Nearly all real work delegates to the template to avoid duplication of cached details.
1410              
1411             The template_id embeds several other properties, for which the rule delegates to it:
1412             - subject_class_name, objects of which the rule can be applied-to
1413             - subclass_name, the subclass of rule (property comparison, and, or "or")
1414             - the body of the rule either key-op-val, or a list of other rules
1415              
1416             For example, the rule GSC::Clone name=x,chromosome>y:
1417             - the template_id embeds:
1418             subject_class_name = GSC::Clone
1419             subclass_name = UR::BoolExpr::And
1420             and the key-op pairs in sorted order: "chromosome>,name="
1421             - the value_id embeds the x,y values in a special format
1422              
1423             =head1 EXAMPLES
1424              
1425              
1426             my $bool = $x->evaluate($obj);
1427              
1428             my $t = GSC::Clone->template_for_params(
1429             "status =",
1430             "chromosome []",
1431             "clone_name like",
1432             "clone_size between"
1433             );
1434              
1435             my @results = $t->get_matching_objects(
1436             "active",
1437             [2,4,7],
1438             "Foo%",
1439             [100000,200000]
1440             );
1441              
1442             my $r = $t->get_rule($v1,$v2,$v3);
1443              
1444             my $t = $r->template;
1445              
1446             my @results = $t->get_matching_objects($v1,$v2,$v3);
1447             my @results = $r->get_matching_objects();
1448              
1449             @r = $r->underlying_rules();
1450             for (@r) {
1451             print $r->evaluate($c1);
1452             }
1453              
1454             my $rt = $r->template();
1455             my @rt = $rt->get_underlying_rule_templates();
1456              
1457             $r = $rt->get_rule_for_values(@v);
1458              
1459             $r = UR::BoolExpr->resolve_for_string(
1460             'My::Class',
1461             'name=Bob and (score=10 or score < 5)',
1462             );
1463              
1464             =head1 SEE ALSO
1465              
1466             UR(3), UR::Object(3), UR::Object::Set(3), UR::BoolExpr::Template(3)
1467              
1468             =cut