File Coverage

blib/lib/Class/ReluctantORM/SQL/From.pm
Criterion Covered Total %
statement 39 188 20.7
branch 0 38 0.0
condition 0 14 0.0
subroutine 13 31 41.9
pod 8 8 100.0
total 60 279 21.5


line stmt bran cond sub pod time code
1             package Class::ReluctantORM::SQL::From;
2              
3             =head1 NAME
4              
5             Class::ReluctantORM::SQL::From - Represent SQL FROM clauses
6              
7             =head1 SYNOPSIS
8              
9             # Save yourself some typing
10             use Class::ReluctantORM::SQL::Aliases;
11              
12             # Make a From clause using an existing Relation
13             my $table = Table->new(...);
14             my $from1 = From->new($table);
15             my $join = Join->new(...);
16             my $from2 = From->new($join);
17              
18             # Examine the From clause
19             $rel = $from->root_relation();
20              
21             # List all referenced tables
22             @tables = $from->tables();
23              
24             # List all available (not referenced) columns
25             @columns = $from->columns();
26              
27              
28             =head1 DESCRIPTION
29              
30             Represent a SQL FROM clause, including join information.
31              
32             =cut
33              
34 1     1   6 use strict;
  1         4  
  1         34  
35 1     1   6 use warnings;
  1         1  
  1         29  
36              
37 1     1   5 use Data::Dumper;
  1         2  
  1         58  
38 1     1   5 use Scalar::Util qw(blessed);
  1         8  
  1         46  
39 1     1   5 use Class::ReluctantORM::Exception;
  1         2  
  1         32  
40 1     1   5 use Class::ReluctantORM::Utilities qw(check_args);
  1         3  
  1         61  
41              
42             our $DEBUG ||= 0;
43              
44 1     1   5 use Class::ReluctantORM::SQL::Aliases;
  1         3  
  1         124  
45              
46 1     1   6 use Class::ReluctantORM::SQL::Expression::Criterion;
  1         2  
  1         10  
47 1     1   27 use Class::ReluctantORM::SQL::Column;
  1         3  
  1         12  
48 1     1   682 use Class::ReluctantORM::SQL::Param;
  1         3  
  1         10  
49 1     1   730 use Class::ReluctantORM::SQL::Table;
  1         4  
  1         11  
50 1     1   787 use Class::ReluctantORM::SQL::From::Join;
  1         4  
  1         11  
51 1     1   44 use Class::ReluctantORM::SQL::From::Relation;
  1         2  
  1         10  
52              
53              
54             =head1 CONSTRUCTORS
55              
56             =cut
57              
58             =head2 $from = From->new($rel);
59              
60             Creates a new FROM clause using the given Class::ReluctantORM::SQL::From::Relation as the base.
61              
62             =cut
63              
64             sub new {
65 0     0 1   my $class = shift;
66 0           my $rel = shift;
67 0 0 0       unless (blessed($rel) && $rel->isa(Relation)) { Class::ReluctantORM::Exception::Param::WrongType->croak(expected => Relation, value => $rel); }
  0            
68 0 0         if (@_) { Class::ReluctantORM::Exception::Param::Spurious->croak(); }
  0            
69              
70 0           my $self = bless { root => $rel }, $class;
71 0           return $self;
72             }
73              
74             =begin devnotes
75              
76             =head2 $from = From->_new_from_with($with);
77              
78             Creates a new FROM clause by parsing the given fully populated 'with' structure
79             (as provided by fetch_deep).
80              
81             =cut
82              
83             sub _new_from_with {
84 0     0     my $class = shift;
85 0           my $tb_class = shift;
86 0           my $with = shift;
87 0           my $counter = 0;
88              
89 0 0         if ($DEBUG > 2) { print STDERR __PACKAGE__ . ':' . __LINE__ . " - in _new_From_with, have with:\n" . Dumper($with); }
  0            
90              
91             # Prime the pump with the base table
92 0           my $rel = Table->new($tb_class);
93 0           $rel->alias('t0');
94 0           my $self = $class->new($rel);
95 0           $with->{__upper_table} = $rel;
96              
97 0 0         if (__is_empty($with)) {
98 0           return $self;
99             } else {
100 0           $self->{alias_counter} = 1; # zero was used for base table
101 0           $self->{root} = $self->__build_rel_from_with_recursor($rel, $with);
102 0           return $self;
103             }
104              
105             }
106              
107             sub __is_empty {
108 0     0     my $href = shift;
109             # Ignore the special __upper_table key
110 0           my @keys = grep { $_ ne '__upper_table' } keys %$href;
  0            
111 0           return !@keys;
112             }
113              
114             sub __build_rel_from_with_recursor {
115 0     0     my $self = shift;
116 0           my $lhs = shift;
117 0   0       my $with = shift || {};
118              
119 0 0         if ($DEBUG > 2) { print STDERR __PACKAGE__ . ':' . __LINE__ . " - in _brfw_recursor, have with:\n" . Dumper($with); }
  0            
120              
121             # Base case: with is empty
122 0 0         if (__is_empty($with)) { return $lhs; }
  0            
123              
124             # Loop over relationships, boosting as we go
125 0           foreach my $relname (keys %$with) {
126 0 0         next if ($relname eq '__upper_table');
127 0           my $opts = $with->{$relname};
128 0           my $relationship = $opts->{relationship};
129              
130 0   0       $opts->{join_type} ||= $relationship->join_type();
131 0 0         if ($DEBUG > 2) {
132 0           print STDERR __PACKAGE__ . ':' . __LINE__ . " - working on relationship $relname\n";
133 0           print STDERR __PACKAGE__ . ':' . __LINE__ . " - have relclass " . ref($relationship) . "\n";
134 0           print STDERR __PACKAGE__ . ':' . __LINE__ . " - have rel join depth " . $relationship->join_depth . "\n";
135             }
136              
137 0 0         if ($relationship->join_depth == 0) {
    0          
    0          
138             # Do nothing - no join
139             } elsif ($relationship->join_depth == 1) {
140 0           $lhs = $self->__perform_single_join($lhs, $opts);
141             } elsif ($relationship->join_depth == 2) {
142 0           $lhs = $self->__perform_double_join($lhs, $opts);
143             }
144             }
145              
146 0           return $lhs;
147              
148             }
149              
150             sub __perform_single_join {
151 0     0     my $self = shift;
152 0           my $lhs = shift;
153 0           my $opts = shift;
154 0           my $relationship = $opts->{relationship};
155              
156             # Init the new right-hand table
157 0           my $right_table = $relationship->remote_sql_table();
158 0           $right_table->alias('t' . $self->{alias_counter}++);
159              
160 0           my $join_type = __normalize_join_type($opts->{join_type});
161              
162             # Find the nearest table that matches the one being linked against
163 0           my $left_table = $lhs->_find_latest_table($relationship->local_sql_table);
164              
165             # Build join condition
166 0           my $crit = __normalize_single_join_criterion($left_table, $right_table, $opts);
167              
168             # Boost left-hand side to be a join while recursing into the right side
169 0           $opts->{with}->{__upper_table} = $right_table;
170 0           $lhs = Join->new(
171             $join_type,
172             $lhs,
173             $self->__build_rel_from_with_recursor($right_table, $opts->{with}),
174             $crit,
175             );
176 0           $lhs->relationship($relationship);
177 0           return $lhs;
178             }
179              
180             sub __perform_double_join {
181 0     0     my $self = shift;
182 0           my $lhs = shift;
183 0           my $opts = shift;
184 0           my $relationship = $opts->{relationship};
185              
186             # Init the new right-hand table and mapping table
187 0           my $right_table = $relationship->remote_sql_table();
188 0           $right_table->alias('t' . $self->{alias_counter}++);
189              
190 0           my $join_table = $relationship->join_sql_table();
191 0           $join_table->alias('t' . $self->{alias_counter}++);
192              
193 0   0       my $first_join_type = __normalize_join_type($opts->{join_type} || 'LEFT OUTER');
194 0           my $second_join_type = 'INNER';
195              
196 0           my $second_crit = __make_join_criterion_on_keys(
197             [$relationship->join_remote_key_sql_columns],
198             $join_table,
199             [$relationship->remote_key_sql_columns],
200             $right_table,
201             );
202 0           $opts->{with}->{__upper_table} = $right_table;
203 0           my $second_join = Join->new(
204             $second_join_type,
205             $join_table,
206             $self->__build_rel_from_with_recursor($right_table, $opts->{with}),
207             $second_crit,
208             );
209              
210              
211             # Find the nearest table that matches the one being linked against
212 0           my $left_table = $lhs->_find_latest_table($relationship->local_sql_table);
213              
214             # Build first join condition
215 0           my $first_crit = __normalize_double_join_criterion($left_table, $second_join, $opts);
216              
217             # Double-Boost left-hand side to be a join while recursing into the right side
218 0           $lhs = Join->new(
219             $first_join_type,
220             $lhs,
221             $second_join,
222             $first_crit,
223             );
224 0           $lhs->relationship($relationship);
225 0           return $lhs;
226              
227             }
228              
229             sub __normalize_join_type {
230 0     0     my $type = shift;
231 0   0       $type = uc($type) || 'INNER';
232 0 0         if ($type =~ /RIGHT/) {
233 0           Class::ReluctantORM::Exception::Param::BadValue->croak(
234             error => 'Right joins not permitted',
235             param => 'join_type',
236             frames => 4,
237             );
238             }
239 0 0         if ($type =~ /OUTER/) {
    0          
    0          
240 0           $type = 'LEFT OUTER';
241             } elsif ($type =~ /CROSS/) {
242 0           $type = 'CROSS';
243             } elsif ($type =~ /NATURAL/) {
244 0           $type = 'INNER'; # keys auto-detected if unspecified
245             } else {
246 0           $type = 'INNER';
247             }
248              
249 0           return $type;
250             }
251              
252             sub __normalize_single_join_criterion {
253 0     0     my $left_table = shift;
254 0           my $right_table = shift;
255 0           my $join_opts = shift;
256 0           my $rel = $join_opts->{relationship};
257              
258 0 0         if ($join_opts->{join_on}) {
259 0           my $driver = $rel->linked_class->driver();
260 0           my $where = $driver->parse_where($join_opts->{join_on});
261 0           return $where->root_criterion;
262             } else {
263 0           return __make_join_criterion_on_keys([$rel->local_key_sql_columns], $left_table, [$rel->remote_key_sql_columns], $right_table);
264             }
265             }
266              
267             sub __normalize_double_join_criterion {
268 0     0     my $left_table = shift;
269 0           my $lower_rel = shift;
270 0           my $join_opts = shift;
271 0           my $rel = $join_opts->{relationship};
272              
273 0 0         if ($join_opts->{join_on}) {
274 0           my $driver = $rel->linked_class->driver();
275 0           my $where = $driver->parse_where($join_opts->{join_on});
276 0           return $where->root_criterion;
277             } else {
278 0           my $join_table = $lower_rel->_find_latest_table($rel->join_sql_table);
279 0           return __make_join_criterion_on_keys([$rel->local_key_sql_columns], $left_table, [$rel->join_local_key_sql_columns], $join_table);
280             }
281             }
282              
283             sub __make_join_criterion_on_keys {
284 0     0     my $left_cols = shift;
285 0           my $left_table = shift;
286 0           my $right_cols = shift;
287 0           my $right_table = shift;
288              
289 0           for my $col (@$left_cols) { $col->table($left_table); }
  0            
290 0           for my $col (@$right_cols) { $col->table($right_table); }
  0            
291              
292 0           my $first_right = shift @$right_cols;
293 0           my $first_left = shift @$left_cols;
294              
295 0           my $root = Criterion->new('=', $first_left, $first_right);
296              
297 0           foreach my $i (0..(@$left_cols - 1)) {
298 0           $root = Criterion->new('AND', $root, Criterion->new('=', $left_cols->[$i], $right_cols->[$i]));
299             }
300              
301 0           return $root;
302             }
303              
304              
305             =head1 ACCESSORS
306              
307             =cut
308              
309             =head2 @columns = $from->columns();
310              
311             Returns a list of all available columns. Will fail if not
312             all of the relations know their columns.
313              
314             =cut
315              
316 0     0 1   sub columns { return shift->root_relation->columns(); }
317              
318              
319             =head2 @relationships = $from->relationships();
320              
321             Returns a list of all known relationships in the FROM clause.
322              
323             =cut
324              
325             sub relationships {
326 0     0 1   my $self = shift;
327 0           my $rel = $self->root_relation();
328 0           my @raw = $self->__rels_recursor($rel);
329             # Remove undefs - hmm's have second joins without relationships
330 0           return grep { defined($_) } @raw;
  0            
331             }
332              
333             sub __rels_recursor {
334 0     0     my $self = shift;
335 0           my $rel = shift;
336 0 0         if ($rel->is_leaf_relation) {
    0          
337 0           return ();
338             } elsif ($rel->is_join) {
339 0           return ($rel->relationship(), $self->__rels_recursor($rel->left_relation), $self->__rels_recursor($rel->right_relation));
340             } else {
341 0           Class::ReluctantORM::Exception::Call::NotImplemented->croak();
342             }
343             }
344              
345             =head2 @joins = $from->joins()
346              
347             Returns a list of any Joins present in the From clause.
348              
349             =cut
350              
351             sub joins {
352 0     0 1   my $from = shift;
353 0           return $from->root_relation->joins();
354             }
355              
356             =head2 $str = $from->pretty_print();
357              
358             Returns a human-readable string representation of the clause. Not appropriate for use for feeding to a prepare() statement.
359              
360             =cut
361              
362             sub pretty_print {
363 0     0 1   my $self = shift;
364 0           my %args = @_;
365 0   0       my $prefix = $args{prefix} || '';
366 0           my $str = $prefix . "FROM\n";
367 0           $str .= $self->root_relation->pretty_print(prefix => $prefix . ' ');
368 0           return $str;
369             }
370              
371              
372             =head2 $rel = $from->root_relation();
373              
374             Reads the relation that forms the root of the FROM tree, as a Class::ReluctantORM::SQL::From::Relation.
375              
376             =cut
377              
378 0     0 1   sub root_relation { return shift->{root}; }
379              
380             =head2 @tables = $from->tables(%opts);
381              
382             Returns a list of all referenced tables. If a table is refered to more
383             than once (due to self-joins), it will be present more than once, but their
384             aliases will differ.
385              
386             Supported options:
387              
388             =over
389              
390             =item exclude_subqueries
391              
392             Optional boolean, default false. If true, tables mentioned only in subqueries will not be included.
393              
394             =back
395              
396              
397             =cut
398              
399             sub tables {
400 0     0 1   my $from = shift;
401 0           my %opts = check_args(args => \@_, optional => [qw(exclude_subqueries)]);
402 0           return $from->root_relation->tables(%opts);
403             }
404              
405             =head2 $clone = $join->clone();
406              
407             Makes a deep copy of the Join object. All SQL objects are clone()'d, but annotations (such as Relationships) are not.
408              
409             =cut
410              
411             sub clone {
412 0     0 1   my $self = shift;
413 0           my $class = ref $self;
414 0           my $other = $class->new($self->{root}->clone());
415 0           return $other;
416              
417             }
418              
419              
420             =head1 AUTHOR
421              
422             Clinton Wolfe January 2009
423              
424             =cut
425              
426             1;
427