File Coverage

blib/lib/DBR/Interface/Where.pm
Criterion Covered Total %
statement 133 147 90.4
branch 56 94 59.5
condition 8 14 57.1
subroutine 13 14 92.8
pod 0 5 0.0
total 210 274 76.6


line stmt bran cond sub pod time code
1             package DBR::Interface::Where;
2              
3 18     18   107 use strict;
  18         39  
  18         688  
4 18     18   100 use Carp;
  18         38  
  18         1287  
5 18     18   108 use DBR::Query::Part;
  18         44  
  18         464  
6 18     18   16680 use Clone;
  18         108107  
  18         1269  
7 18     18   189 use Digest::MD5 qw(md5_base64);
  18         41  
  18         1010  
8 18     18   10553 use DBR::Misc::General;
  18         52  
  18         52032  
9              
10             sub new {
11 31     31 0 210 my( $package ) = shift;
12 31         188 my %params = @_;
13              
14 31         75 my $self = {};
15              
16 31 50       178 $self->{session} = $params{session} or croak "session is required";
17 31 50       228 $self->{instance} = $params{instance} or croak "instance is required";
18 31 50       456 $self->{table} = $params{primary_table} or croak "primary_table is required";
19              
20 31 50       140 croak('primary_table object must be specified') unless ref($self->{table}) eq 'DBR::Config::Table';
21              
22 31         125 bless( $self, $package );
23              
24 31         250 $self->{tables} = [$self->{table}];
25 31         87 $self->{aliascount} = 0;
26              
27 31         244 return( $self );
28             }
29              
30 12     12 0 89 sub tables { shift->{tables} }
31              
32             sub _andify{
33 64     64   101 my $self = shift;
34 64 100       319 return $_[0] if (@_ == 1);
35 25         209 return DBR::Query::Part::And->new( @_ );
36             }
37              
38             # fast way to discern the difference between one where clause
39             # and another without actually doing the work of assembling everything
40             sub digest{
41 150010     150010 0 878718 my $self = shift;
42 150010         201859 md5_base64( join ( "\0|", map {_expandstr($_)} @{ shift() } ) );
  600020         1514568  
  150010         278759  
43             }
44             sub digest_clear{
45 0     0 0 0 my $self = shift;
46 0         0 join ( "\0|", map {_expandstr($_)} @{ shift() } );
  0         0  
  0         0  
47             }
48             sub build{
49 47     47 0 265 my $self = shift;
50 47         77 my @input = @{shift()}; # Make a shallow copy
  47         186  
51 47 50       276 scalar (@input) || croak "input is required";
52              
53 47         177 my $pendgroup = { table => $self->{table} }; # prime the pump.
54              
55 47         92 my @andparts = (); # Storage for finished query part objects
56 47         72 my $pendct;
57 47         142 while (@input){ # Iterate over key/value pairs
58 88         170 my $next = shift @input;
59 88 100       288 if(ref($next) eq 'DBR::_LOP'){ # Logical OPerator
60 22         99 my $op = $next->operator;
61 22 50 66     114 scalar(@andparts) || $pendct || croak('Cannot use an operator without a preceeding comparison');
62              
63 22 100       152 if ($op eq 'And'){
    50          
64 11 100       43 if( $next->only_contains_and ){
65             # This is an optomisation to prevent unnecessary recusion,
66             # and to avoid duplication of subqueries when possible.
67             # Because: A and ( B and C ) is equivelant to A and B and C...
68             # We are able to collapse the contents of the AND into the current context,
69             # provided the sequence is maintained. Thus unshift, not push
70 8         23 unshift @input, @{$next->value};
  8         31  
71             }else{
72             # We have to recurse to handle this situation properly
73             # A AND (B OR C) is not equivelant to A AND B OR C
74 3         16 push @andparts, $self->build( $next->value );
75             }
76             } elsif ( $op eq 'Or' ){
77 11 100       40 if($pendct){
78 9         33 push @andparts, $self->_reljoin( $pendgroup ); # Everything before me (pending)...
79             }
80 11         36 my $A = $self->_andify( @andparts );
81 11         38 my $B = $self->build( $next->value ); # Compared to everything inside
82              
83 11         74 @andparts = ( DBR::Query::Part::Or->new( $A, $B ) ); # Russian dolls... Get in mahh belly
84              
85 11         43 $pendgroup = { table => $self->{table} }; # Reset
86 11         40 $pendct = 0; # Reset
87             }else{
88 0         0 confess "Sanity error. Invalid operator."
89             }
90              
91 22         71 next;
92             }
93              
94 66         105 my $rawval = shift @input;
95 66         107 $pendct++;
96 66         230 $self->_process_comparison($next, $rawval, $pendgroup); # add it to the hopper
97              
98             }
99              
100 47 50       140 scalar(@input) and croak('Odd number of arguments in where parameters'); # I hate leftovers
101              
102 47         180 push @andparts, $self->_reljoin( $pendgroup );
103              
104 47 100       208 return wantarray?(@andparts):$self->_andify(@andparts); # don't wrap it in an and if we want an array
105             }
106              
107              
108             # Process ONE comparison.
109             # Walk the relation.relation.relation.field chain and set up the heirarchical hash structure for reljoin.
110             sub _process_comparison{
111 66     66   96 my $self = shift;
112              
113 66         129 my $key = shift;
114 66         87 my $rawval = shift;
115 66         90 my $ref = shift;
116              
117 66         351 $key =~ /^\s+|\s+$/g; # trim junk
118 66         334 my @parts = split(/\s*\.\s*/,$key); # Break down each key into parts
119              
120 66         121 my $tablect;
121              
122 66         207 my $cur_table = $self->{table}; # Start
123              
124 66         231 while ( my $part = shift @parts ){
125 78 100       209 my $last = (scalar(@parts) == 0)?1:0;
126              
127 78 100       181 if($last){ # The last part should always be a field
128 66 50       247 croak ('Duplicate field ' .$part ) if $ref->{fields}->{$part};
129              
130 66 50       363 my $field = $cur_table->get_field( $part ) or croak("invalid field $part");
131 66 50       345 my $value = $field->makevalue( $rawval ) or croak("failed to build value object for $part");
132              
133 66 50       406 my $out = DBR::Query::Part::Compare->new( field => $field, value => $value ) or confess('failed to create compare object');
134 66         297 my $conn = $self->{instance}->connect;
135              
136 66         399 $ref->{fields}->{$part} = $out;
137              
138             }else{
139             #test for relation?
140 12   100     91 $ref = $ref->{kids}->{$part} ||= {}; # step deeper into the tree
141              
142 12 100       41 if( $ref->{been_here} ){ # Dejavu - merge any common paths together
143              
144 3         12 $cur_table = $ref->{table}; # next!
145              
146             }else{
147              
148 9 50       54 my $relation = $cur_table->get_relation($part) or croak("invalid relationship $part");
149 9 50       37 my $maptable = $relation->maptable or confess("failed to get maptable");
150              
151             # Any to_one relationship results in a join. we'll need some table aliases for later.
152             # Do them now so everything is in sync. I originally assigned the alias in _reljoin,
153             # but it didn't always alias the fields that needed to be aliased due to the order of execution.
154 9 50 33     39 if( $relation->is_same_schema && $relation->is_to_one ){
155 9 50       43 croak ('No more than 25 tables allowed in a join') if $self->{aliascount} > 24;
156              
157 9 100       54 $cur_table ->alias() || $cur_table ->alias( chr(97 + $self->{aliascount}++) ); # might be doing this one again
158 9         44 $maptable ->alias( chr(97 + $self->{aliascount}++) );
159             }
160              
161 9         28 $ref->{relation} = $relation;
162 9         20 $ref->{prevtable} = $cur_table;
163 9         19 $ref->{table} = $maptable;
164 9         18 $ref->{been_here} = 1;
165              
166 9         61 $cur_table = $maptable; # next!
167             }
168             }
169              
170             };
171             }
172             sub _reljoin{
173 65     65   105 my $self = shift;
174 65         169 my $ref = shift;
175 65   66     527 my $tables = shift || $self->{tables}; # Allow override of table list for subqueries
176              
177 65 50       290 confess ('ref must be hash') unless ref($ref) eq 'HASH';
178              
179 65         86 my @and;
180              
181 65 100       270 if($ref->{kids}){
182 9         15 foreach my $key (sort keys %{$ref->{kids}}){ # sort for consistent sql ordering
  9         50  
183 9         75 my $kid = $ref->{kids}->{ $key };
184 9         18 my $relation = $kid->{relation};
185              
186             # it's important we use the same table objects to preserve aliases
187              
188 9 50       32 my $table = $kid->{table} or confess("failed to get table");
189 9 50       39 my $prevtable = $kid->{prevtable} or confess("failed to get prev_table");
190              
191 9 50       47 my $field = $relation->mapfield or confess('Failed to fetch field');
192 9 50       46 my $prevfield = $relation->field or confess('Failed to fetch prevfield');
193              
194 9         34 my $prevalias = $prevtable ->alias();
195 9         28 my $alias = $table ->alias();
196              
197 9 50       70 $prevfield ->table_alias( $prevalias ) if $prevalias;
198 9 50       41 $field ->table_alias( $alias ) if $alias;
199              
200 9 50 33     29 if ($relation->is_same_schema && $relation->is_to_one) { # Do a join
201              
202 9 50       25 $prevalias or die('Sanity error: prevtable alias is required');
203 9 50       23 $alias or die('Sanity error: table alias is required');
204              
205 9         24 push @$tables, $table;
206              
207 9 50       72 my $where = $self->_reljoin( $kid, $tables ) or confess('_reljoin failed');
208 9         20 push @and, $where;
209              
210 9 50       56 my $join = DBR::Query::Part::Join->new($field,$prevfield) or confess('failed to create join object');
211 9         37 push @and, $join;
212              
213             }else{ # if it's a to_many relationship ( or cross schema ), then subqery
214 0         0 my @tables = $table;
215 0 0       0 my $where = $self->_reljoin( $kid, \@tables ) or confess('_reljoin failed');
216              
217 0         0 my $instance = $self->{instance};
218 0 0       0 unless ( $relation->is_same_schema ){
219 0 0       0 $instance = $table->schema->get_instance( $instance->class ) or return $self->_error('Failed to retrieve db instance for subquery table');
220             }
221              
222 0 0       0 my $query = DBR::Query::Select->new(
223             instance => $instance,
224             session => $self->{session},
225             fields => [$field],
226             tables => \@tables,
227             where => $where,
228             ) or confess('failed to create query object');
229              
230 0         0 my $runflag = ! $relation->is_same_schema;
231 0 0       0 my $subquery = DBR::Query::Part::Subquery->new($prevfield, $query, $runflag) or confess ('failed to create subquery object');
232 0         0 push @and, $subquery;
233             }
234              
235             }
236             }
237              
238             # It's important that fields are evaluated after all relationships are processed for this node
239 65 100       197 if($ref->{fields}){
240 48         301 my $alias = $ref->{table}->alias;
241              
242 48         101 foreach my $key (sort keys %{$ref->{fields}}){
  48         571  
243 66         138 my $compare = $ref->{fields}->{ $key };
244 66 100       189 $compare->field->table_alias( $alias ) if $alias;
245 66         212 push @and, $compare;
246             }
247             }
248              
249 65 100       301 return wantarray?(@and):$self->_andify(@and); # don't wrap it in an and if we want an array
250             }
251              
252             1;