File Coverage

blib/lib/Tree/Predicate.pm
Criterion Covered Total %
statement 73 74 98.6
branch 20 24 83.3
condition 4 5 80.0
subroutine 15 15 100.0
pod 7 7 100.0
total 119 125 95.2


line stmt bran cond sub pod time code
1             package Tree::Predicate;
2              
3 3     3   42140 use warnings;
  3         7  
  3         82  
4 3     3   15 use strict;
  3         4  
  3         97  
5              
6 3     3   15 use base 'Exporter';
  3         19  
  3         275  
7              
8 3     3   3334 use Storable qw(dclone);
  3         11815  
  3         331  
9              
10             our @EXPORT_OK = qw(AND OR NOT);
11             our %EXPORT_TAGS = (logical => [qw(AND OR NOT)]);
12              
13 3     3   24 use constant SPLIT_LIMIT => 50;
  3         3  
  3         2861  
14              
15             =head1 NAME
16              
17             Tree::Predicate - a balanced, splittable tree for SQL predicates
18              
19             =head1 VERSION
20              
21             Version 0.03
22              
23             =cut
24              
25             our $VERSION = '0.03';
26              
27              
28             =head1 SYNOPSIS
29              
30             Tree::Predicate allows the composition of a tree of SQL predicates that
31             can then be "split" into UNION-able predicats that do not contain an OR.
32              
33             use Tree::Predicate qw(:logical);
34            
35             my $left_branch = OR('a', 'b');
36             my $right_branch = OR('c', 'd');
37             my $tree = AND($left_branch, $right_branch);
38            
39             print $tree->as_string; # ((a OR b) AND (c OR d))
40            
41             my @trees = $tree->split;
42             # four trees
43             # (a AND c)
44             # (a AND d)
45             # (b AND c)
46             # (b AND d)
47            
48             $tree->negate;
49             print $tree->as_string; # ((NOT(a) AND NOT(b)) OR (NOT(c) AND NOT(d)))
50              
51             =head1 EXPORT
52              
53             AND/OR/NOT may be individually imported, or they may be collectively
54             imported with :logical.
55              
56             =head1 FUNCTIONS
57              
58             =head2 as_string
59              
60             expresses the tree as a string suitable for including in SQL
61              
62             =cut
63              
64             sub as_string {
65 24     24 1 6122 my $self = shift;
66              
67 24         57 '(' . join(" $self->{OP} ", map { $_->as_string } @{$self->{OPERANDS}}) . ')';
  52         131  
  24         54  
68             }
69              
70             =head2 negate
71              
72             negates the tree
73              
74             =cut
75              
76             # negating means to change the node form AND/OR to OR/AND and to negate
77             # the children
78             sub negate {
79 11     11 1 16 my $self = shift;
80              
81 11 100       37 $self->{OP} = $self->{OP} eq 'AND' ? 'OR' : 'AND';
82 11         14 for (@{$self->{OPERANDS}}) {
  11         29  
83 22         119 $_->negate;
84             }
85             }
86              
87             =head2 operands
88              
89             returns a list (or reference) of the tree's operands, for whatever
90             you might want that
91              
92             =cut
93              
94             sub operands {
95 193     193 1 211 my $self = shift;
96              
97 193 50       274 wantarray ? @{$self->{OPERANDS}} : $self->{OPERANDS};
  193         994  
98             }
99              
100             =head2 split
101              
102             returns a list of subtrees that can be used in a UNION statement to
103             produce a logically equivalent query.
104              
105             dies if number of children exceeds SPLIT_LIMIT
106              
107             =cut
108              
109             sub split {
110 15     15 1 877 my $self = shift;
111              
112 15         19 my @results;
113 15 100       68 if ($self->{OP} eq 'AND') {
    50          
114 4         7 my @children;
115 4         9 for (@{$self->{OPERANDS}}) {
  4         13  
116 11         913 my $child = dclone $_;
117 11         35 push @children, [$child->split];
118             }
119 4         35 @results = _produce_combinations(@children);
120             } elsif ($self->{OP} eq 'OR') {
121 11         17 push @results, $_->split for (@{$self->{OPERANDS}});
  11         51  
122             } else {
123 0         0 die "unknown operand $self->{OP}";
124             }
125 14 50       43 die "too many children" if @results > SPLIT_LIMIT;
126 14         100 @results;
127             }
128              
129             =head2 AND/OR/NOT
130              
131             constructors for trees
132              
133             =cut
134              
135 313     313 1 810 sub AND { __PACKAGE__->_new_AND(@_); }
136 15     15 1 954 sub OR { __PACKAGE__->_new_OR(@_); }
137              
138             sub NOT {
139 2   50 2 1 11 my $operand = shift || die "operand required";
140 2 50       7 die "too many operands" if @_;
141              
142 2 100       13 if (UNIVERSAL::isa($operand, __PACKAGE__)) {
143 1         4 $operand->negate;
144 1         4 $operand;
145             } else {
146 1         9 require Tree::Predicate::Leaf;
147 1         6 Tree::Predicate::Leaf->new($operand, negated => 1);
148             }
149             }
150              
151             # internal constructors and mutators. Invited guests only!
152              
153             sub _new {
154 6     6   10 my $op = shift;
155            
156             return sub {
157 328     328   424 my $class = shift;
158            
159 328         347 my @operands;
160 328         547 for (@_) {
161 663 100       2008 if (UNIVERSAL::isa($_, __PACKAGE__)) {
162 620 100 100     2216 if (defined($_->{OP}) && $_->{OP} eq $op) {
163 193         373 push @operands, $_->operands;
164             } else {
165 427         864 push @operands, $_;
166             }
167             } else {
168 43         1081 require Tree::Predicate::Leaf;
169 43         149 push @operands, Tree::Predicate::Leaf->new($_);
170             }
171             }
172 328 100       766 return $operands[0] if @operands == 1;
173            
174 325         1003 my $self = {
175             OP => $op,
176             OPERANDS => \@operands,
177             };
178 325         1548 bless $self, $class;
179 6         29 };
180             }
181              
182             *_new_AND = _new('AND');
183             *_new_OR = _new('OR');
184              
185             sub _produce_combinations {
186 58     58   72 my $aryref = shift;
187            
188 58         61 my @combinations;
189 58 100       96 if (@_) {
190 19         34 for my $term (@$aryref) {
191 302         17202 push @combinations,
192 54         111 map { AND(dclone $term, $_) } _produce_combinations(@_);
193             }
194 19 100       259 die "too many children" if @combinations > SPLIT_LIMIT;
195 18         96 @combinations;
196             } else {
197 39         52 map { dclone $_ } @$aryref;
  113         4326  
198             }
199             }
200              
201             =head1 AUTHOR
202              
203             David Marshall, C<< >>
204              
205             =head1 BUGS
206              
207             Please report any bugs or feature requests to C, or through
208             the web interface at L. I will be notified, and then you'll
209             automatically be notified of progress on your bug as I make changes.
210              
211             =head1 SUPPORT
212              
213             You can find documentation for this module with the perldoc command.
214              
215             perldoc Tree::Predicate
216              
217              
218             You can also look for information at:
219              
220             =over 4
221              
222             =item * RT: CPAN's request tracker
223              
224             L
225              
226             =item * AnnoCPAN: Annotated CPAN documentation
227              
228             L
229              
230             =item * CPAN Ratings
231              
232             L
233              
234             =item * Search CPAN
235              
236             L
237              
238             =back
239              
240              
241             =head1 ACKNOWLEDGEMENTS
242              
243              
244             =head1 COPYRIGHT & LICENSE
245              
246             Copyright 2009 Yahoo! Inc., all rights reserved.
247              
248             This program is free software; you can redistribute it and/or modify it
249             under the same terms as Perl itself.
250              
251              
252             =cut
253              
254             1; # End of Tree::Predicate