File Coverage

blib/lib/Class/ReluctantORM/SQL/Where.pm
Criterion Covered Total %
statement 45 145 31.0
branch 0 36 0.0
condition 0 17 0.0
subroutine 15 32 46.8
pod 12 12 100.0
total 72 242 29.7


line stmt bran cond sub pod time code
1             package Class::ReluctantORM::SQL::Where;
2              
3             =head1 NAME
4              
5             Class::ReluctantORM::SQL::Where - Represent SQL WHERE clauses
6              
7             =head1 SYNOPSIS
8              
9             # Save yourself some typing
10             use Class::ReluctantORM::SQL::Aliases;
11              
12             # This creates an "always true" where clause
13             my $where = Where->new();
14              
15             # Build criteria using Criterion objects
16             my $crit = Criterion->new(
17             '=',
18             Column->new(
19             column => $column_name,
20             table => $sql_table,
21             ),
22             Param->new(),
23             );
24              
25             # You can make a new where clause....
26             my $where2 = Where->new($crit);
27              
28             # Or add to an existing one
29             $where->and($crit);
30             $where->or($crit);
31              
32             # You can also make a where clause directly from a SQL string
33             # by using your Driver
34             my $driver = Ship->driver();
35             my $parsed_where = $driver->parse_where(q(name LIKE '%Beard' AND leg_count < ?));
36              
37             # Interrogate a SQL::Where for info
38             my @params = $where->params();
39             my @tables = $where->tables();
40             my @columns = $where->columns();
41              
42             # Walk the tree - see Class::ReluctantORM::SQL::Where::Criterion for details
43             my $crit = $where->root_criterion;
44             while ($crit) {
45             ...
46             }
47              
48             # Attach a where clause to a SQL object
49             $sql->where($where);
50              
51             =head1 DESCRIPTION
52              
53             Represent a SQL where clause abstractly.
54              
55             TODO DOCS
56              
57             =head1 USAGE
58              
59             Generally, you construct a Where object in one of two ways:
60              
61             =over
62              
63             =item parse it from a SQL string
64              
65             =item build it using SQL::Expression::Criterion objects
66              
67             =back
68              
69             =cut
70              
71 1     1   5 use strict;
  1         3  
  1         24  
72 1     1   5 use warnings;
  1         2  
  1         18  
73              
74 1     1   1115 use SQL::Statement; # Extended by
  1         21458  
  1         26  
75 1     1   8 use Data::Dumper;
  1         3  
  1         47  
76              
77 1     1   5 use Class::ReluctantORM::Exception;
  1         2  
  1         24  
78 1     1   5 use Scalar::Util qw(blessed);
  1         3  
  1         50  
79              
80             our $DEBUG = 0;
81              
82 1     1   5 use Class::ReluctantORM::Utilities qw(check_args);
  1         1  
  1         38  
83              
84 1     1   6 use Class::ReluctantORM::SQL::Aliases;
  1         2  
  1         118  
85              
86 1     1   14 use Class::ReluctantORM::SQL::Expression::Criterion;
  1         2  
  1         24  
87 1     1   31 use Class::ReluctantORM::SQL::Expression::Literal;
  1         2  
  1         11  
88 1     1   21 use Class::ReluctantORM::SQL::Expression::FunctionCall;
  1         3  
  1         11  
89 1     1   31 use Class::ReluctantORM::SQL::Column;
  1         2  
  1         13  
90 1     1   21 use Class::ReluctantORM::SQL::Param;
  1         1  
  1         12  
91 1     1   25 use Class::ReluctantORM::SQL::Table;
  1         2  
  1         13  
92 1     1   20 use Class::ReluctantORM::SQL::Parser;
  1         2  
  1         1249  
93              
94             =head1 CONSTRUCTORS
95              
96             =cut
97              
98             =head2 $where = SQL::Where->new();
99              
100             =head2 $where = SQL::Where->new($crit);
101              
102             Creates a new Where object.
103              
104             In the first form, creates an "always true" where clause. You can then safely add constraints using and() and or().
105              
106             In the second form, creates a where clause whose root criterion will be $crit, a SQL::Where::Criterion.
107              
108             =cut
109              
110             sub new {
111 0     0 1   my $class = shift;
112 0           my $root = shift;
113              
114 0 0         if ($root) {
115 0 0 0       unless (blessed($root) && $root->isa(Criterion())) {
116 0           Class::ReluctantORM::Exception::Param::WrongType->croak(expected => Criterion());
117             }
118             }
119              
120 0           my $self = bless {}, $class;
121              
122 0           $self->{orig_str} = '';
123              
124 0   0       $self->{root} = $root || Criterion->new_tautology();
125              
126 0           return $self;
127             }
128              
129              
130             =head1 CRITERIA-BUILDING METHODS
131              
132             =cut
133              
134             =head2 $where->and($crit);
135              
136             Adds the given SQL::Where::Criterion, ANDing it against the root-level criterion and
137             setting the new root criterion from the resulting operation.
138              
139             In other words, given 'a=b', if you then call and() with a criteria equivalent to 'c=d',
140             you will get '(a=b) AND (c=d)', and the new root criterion will be the AND operation.
141             This may then be repeated with and('e=f'), giving '((a=b) AND (c=d)) AND (e=f)'.
142              
143             =cut
144              
145             sub and {
146 0     0 1   my $self = shift;
147 0           $self->__and_or('AND', @_);
148             }
149              
150             =head2 $where->or($crit);
151              
152             Adds the given SQL::Where::Criterion, ORing it against the root-level criterion and
153             setting the new root criterion from the resulting operation.
154              
155             See and() for examples.
156              
157             =cut
158              
159             sub or {
160 0     0 1   my $self = shift;
161 0           $self->__and_or('OR', @_);
162             }
163              
164             sub __and_or {
165 0     0     my $self = shift;
166 0           my $op = shift;
167 0 0         if (@_ > 1) { Class::ReluctantORM::Exception::Param::Spurious->croak(frames => 2); }
  0            
168 0           my $crit = shift;
169 0 0 0       unless (blessed($crit) && $crit->isa(Criterion())) {
170 0           Class::ReluctantORM::Exception::Param::WrongType->croak(
171             param => 'criterion',
172             value => $crit,
173             expected => Criterion(),
174             frames => 2,
175             );
176             }
177              
178 0           $self->{root} = Criterion->new(
179             $op,
180             $self->{root},
181             $crit,
182             );
183              
184             }
185              
186              
187             =head1 MUTATORS
188              
189             =cut
190              
191             =head2 $w->bind_params($val1, $val2,...);
192              
193             Binds the given values to the parameters in the where clause.
194              
195             =cut
196              
197             sub bind_params {
198 0     0 1   my $self = shift;
199 0           my @vals = @_;
200 0           my @params = $self->params();
201 0 0         if (@vals < @params) {
    0          
202 0           Class::ReluctantORM::Exception::Param::Missing->croak('The number of values must match the number of parameters in the where clause.');
203             } elsif (@vals > @params) {
204 0           Class::ReluctantORM::Exception::Param::Spurious->croak('The number of values must match the number of parameters in the where clause.');
205             }
206 0           for my $i (0..(@params - 1)) {
207 0           $params[$i]->bind_value($vals[$i]);
208             }
209             }
210              
211              
212             =head1 ACCESSORS
213              
214             =cut
215              
216             =head2 @columns = $where->columns();
217              
218             Returns the current list of SQL::Column objects referenced in the Where.
219              
220             =cut
221              
222             sub columns {
223 0     0 1   my $self = shift;
224 0           my @columns = ();
225             my $walker = sub {
226 0     0     my $leaf = shift;
227 0 0         if ($leaf->is_column) {
228 0           push @columns, $leaf;
229             }
230 0           };
231 0           $self->walk_leaves($walker);
232 0           return @columns;
233             }
234              
235              
236             =head2 $table = $where->find_table($name_or_alias);
237              
238             Checks to see if a given table name or alias has been used in the
239             where clause, and if so, returns the corresonding Table object.
240              
241             =cut
242              
243             sub find_table {
244 0     0 1   my $self = shift;
245 0           my $table_or_alias = shift;
246 0           my @tables = $self->tables();
247              
248 0           my @results = grep { $_->table eq $table_or_alias } @tables;
  0            
249 0 0         if (@results > 1) {
250 0           Class::ReluctantORM::Exception::SQL::AmbiguousReference->croak(
251             error => "'$table_or_alias' appears to refer to more than one table.",
252             referent => $table_or_alias,
253             statement => $self->{orig_str},
254             );
255             }
256 0 0         if (@results == 1) {
257 0           return $results[0];
258             }
259              
260 0           @results = grep { $_->alias eq $table_or_alias } @tables;
  0            
261 0 0         if (@results > 1) {
262 0           Class::ReluctantORM::Exception::SQL::AmbiguousReference->croak(
263             error => "'$table_or_alias' appears to refer to more than one table.",
264             referent => $table_or_alias,
265             statement => $self->{orig_str},
266             );
267             }
268 0 0         if (@results == 1) {
269 0           return $results[0];
270             }
271 0           return undef;
272              
273             }
274              
275              
276             =head2 @params = $where->params();
277              
278             Returns the current list of SQL::Param objects embedded in the Where.
279             DBI placeholders get turned into Params.
280              
281             =cut
282              
283             sub params {
284 0     0 1   my $where = shift;
285 0           return $where->__params_recursor($where->root_criterion);
286             }
287              
288             sub __params_recursor {
289 0     0     my $where = shift;
290 0           my $expr = shift;
291 0 0         if ($expr->is_leaf_expression()) {
292 0 0         if ($expr->is_param()) {
    0          
293 0           return $expr;
294             } elsif ($expr->is_subquery()) {
295 0           my $select = $expr->statement();
296 0           return ($select->params());
297             } else {
298 0           return ();
299             }
300             } else {
301 0           return (map { $where->__params_recursor($_) } $expr->child_expressions());
  0            
302             }
303             }
304              
305              
306             =head2 $str = $where->pretty_print();
307              
308             Returns a human-readable string representation of the clause. Not appropriate for use for feeding to a prepare() statement.
309              
310             =cut
311              
312             sub pretty_print {
313 0     0 1   my $self = shift;
314 0           my %args = @_;
315 0   0       my $prefix = $args{prefix} || '';
316 0           my $str = $prefix . "WHERE\n";
317 0           $str .= $prefix . $self->root_criterion->pretty_print(one_line => 1, prefix => $prefix . ' ') . "\n";
318 0           return $str;
319             }
320              
321             =head2 $crit = $where->root_criterion();
322              
323             Returns the root Criterion of the where clause.
324              
325             =cut
326              
327             sub root_criterion {
328 0     0 1   my $where = shift;
329 0 0         if (@_) {
330 0           Class::ReluctantORM::Exception::Call::NotMutator->croak();
331             }
332 0           return $where->{root};
333             }
334              
335             =head2 @tables = $where->tables(%opts);
336              
337             Returns the current list of SQL::Table objects referenced by the columns in criteria in the Where, as well as in subqueries.
338              
339             Supported options:
340              
341             =over
342              
343             =item exclude_subqueries
344              
345             Optional boolean, default false. If true, tables mentioned only in subqueries will not be included.
346              
347             =back
348              
349             =cut
350              
351             sub tables {
352 0     0 1   my $self = shift;
353 0           my %opts = check_args(args => \@_, optional => [qw(exclude_subqueries)]);
354              
355 0           my @tables = ();
356             my $walker = sub {
357 0     0     my $expr = shift;
358 0 0 0       if ($expr->is_subquery() && !$opts{exclude_subqueries}) {
    0 0        
359 0           push @tables, ($expr->statement->tables());
360             } elsif ($expr->is_column() && defined $expr->table) {
361 0           push @tables, $expr->table();
362             }
363 0           };
364 0           $self->walk_leaves($walker);
365 0           return @tables;
366             }
367              
368             =head2 $where->walk_leaves($code_ref)
369              
370             Traverses the Where tree, and executes the coderef on each leaf node.
371             The coderef is passed the leaf as the one argument. The leaf is guarenteed
372             to be a subclass of Class::ReluctantORM::SQL::Expression.
373              
374             =cut
375              
376             sub walk_leaves {
377 0     0 1   my $self = shift;
378 0           my $code = shift;
379 0           return $self->__walk_leaves_recursor($self->{root}, $code);
380             }
381              
382             sub __walk_leaves_recursor {
383 0     0     my $self = shift;
384 0           my $node = shift;
385 0           my $code = shift;
386              
387 0 0         if ($node->is_leaf_expression) {
388 0           $code->($node);
389             } else {
390 0           foreach my $child ($node->child_expressions) {
391 0           $self->__walk_leaves_recursor($child, $code);
392             }
393              
394             }
395             }
396              
397             =head2 $clone = $w->clone()
398              
399             Creates a new Where whose root criterion is a clone of the original's root.
400              
401             =cut
402              
403              
404             sub clone {
405 0     0 1   my $self = shift;
406 0           my $class = ref $self;
407 0           my $other = $class->new();
408              
409 0           $other->{root} = $self->{root}->clone();
410 0           return $other;
411              
412             }
413              
414              
415             1;
416