File Coverage

blib/lib/Data/ObjectDriver/SQL.pm
Criterion Covered Total %
statement 209 220 95.0
branch 77 98 78.5
condition 37 51 72.5
subroutine 19 20 95.0
pod 13 13 100.0
total 355 402 88.3


line stmt bran cond sub pod time code
1             # $Id$
2              
3             package Data::ObjectDriver::SQL;
4 2     2   2092 use strict;
  2         19  
  2         80  
5 2     2   15 use warnings;
  2         5  
  2         96  
6              
7 2     2   14 use base qw( Class::Accessor::Fast );
  2         5  
  2         3698  
8              
9             __PACKAGE__->mk_accessors(qw(
10             select distinct select_map select_map_reverse
11             from joins where bind limit offset group order
12             having where_values column_mutator index_hint
13             comment
14             ));
15              
16             sub new {
17 36     36 1 32694 my $class = shift;
18 36         133 my $stmt = $class->SUPER::new(@_);
19 36         1021 $stmt->select([]);
20 36         805 $stmt->distinct(0);
21 36         780 $stmt->select_map({});
22 36         771 $stmt->select_map_reverse({});
23 36         772 $stmt->bind([]);
24 36         777 $stmt->from([]);
25 36         755 $stmt->where([]);
26 36         799 $stmt->where_values({});
27 36         829 $stmt->having([]);
28 36         764 $stmt->joins([]);
29 36         768 $stmt->index_hint({});
30 36         386 $stmt;
31             }
32              
33             sub add_select {
34 9     9 1 50 my $stmt = shift;
35 9         22 my($term, $col) = @_;
36 9   66     28 $col ||= $term;
37 9         14 push @{ $stmt->select }, $term;
  9         153  
38 9         190 $stmt->select_map->{$term} = $col;
39 9         175 $stmt->select_map_reverse->{$col} = $term;
40             }
41              
42             sub add_join {
43 8     8 1 14 my $stmt = shift;
44 8         18 my($table, $joins) = @_;
45 8 100       11 push @{ $stmt->joins }, {
  8         143  
46             table => $table,
47             joins => ref($joins) eq 'ARRAY' ? $joins : [ $joins ],
48             };
49             }
50              
51             sub add_index_hint {
52 1     1 1 3 my $stmt = shift;
53 1         3 my($table, $hint) = @_;
54             $stmt->index_hint->{$table} = {
55             type => $hint->{type} || 'USE',
56 1 50 50     33 list => ref($hint->{list}) eq 'ARRAY' ? $hint->{list} : [ $hint->{list} ],
57             };
58             }
59              
60             sub as_sql {
61 30     30 1 1148 my $stmt = shift;
62 30         51 my $sql = '';
63 30 100       45 if (@{ $stmt->select }) {
  30         502  
64 12         72 $sql .= 'SELECT ';
65 12 100       194 $sql .= 'DISTINCT ' if $stmt->distinct;
66             $sql .= join(', ', map {
67 15         289 my $alias = $stmt->select_map->{$_};
68 15 100 66     323 $alias && /(?:^|\.)\Q$alias\E$/ ? $_ : "$_ $alias";
69 12         65 } @{ $stmt->select }) . "\n";
  12         193  
70             }
71 30         137 $sql .= 'FROM ';
72              
73             ## Add any explicit JOIN statements before the non-joined tables.
74 30         48 my %joined;
75 30 50       45 my @from = @{ $stmt->from || [] };
  30         508  
76 30 100 66     655 if ($stmt->joins && @{ $stmt->joins }) {
  30         587  
77 8         50 my $initial_table_written = 0;
78 8         10 for my $j (@{ $stmt->joins }) {
  8         131  
79 11         46 my($table, $joins) = map { $j->{$_} } qw( table joins );
  22         50  
80 11         26 $table = $stmt->_add_index_hint($table); ## index hint handling
81 11 100       33 $sql .= $table unless $initial_table_written++;
82 11         25 $joined{$table}++;
83 11         18 for my $join (@{ $j->{joins} }) {
  11         24  
84 13         29 $joined{$join->{table}}++;
85             $sql .= ' ' .
86             uc($join->{type}) . ' JOIN ' . $join->{table} . ' ON ' .
87 13         48 $join->{condition};
88             }
89             }
90 8         14 @from = grep { ! $joined{ $_ } } @from;
  3         9  
91 8 100       21 $sql .= ', ' if @from;
92             }
93              
94 30 100       168 if (@from) {
95 23         46 $sql .= join ', ', map { $stmt->_add_index_hint($_) } @from;
  24         73  
96             }
97              
98 30         56 $sql .= "\n";
99 30         61 $sql .= $stmt->as_sql_where;
100              
101 30         210 $sql .= $stmt->as_aggregate('group');
102 30         66 $sql .= $stmt->as_sql_having;
103 30         193 $sql .= $stmt->as_aggregate('order');
104              
105 30         64 $sql .= $stmt->as_limit;
106 29         641 my $comment = $stmt->comment;
107 29 100 66     165 if ($comment && $comment =~ /([ 0-9a-zA-Z.:;()_#&,]+)/) {
108 3 50       14 $sql .= "-- $1" if $1;
109             }
110 29         143 return $sql;
111             }
112              
113             sub as_limit {
114 30     30 1 46 my $stmt = shift;
115 30 100       537 my $n = $stmt->limit or
116             return '';
117 4 100       41 die "Non-numerics in limit clause ($n)" if $n =~ /\D/;
118 3 100       54 return sprintf "LIMIT %d%s\n", $n,
119             ($stmt->offset ? " OFFSET " . int($stmt->offset) : "");
120             }
121              
122             sub as_aggregate {
123 60     60 1 88 my $stmt = shift;
124 60         98 my($set) = @_;
125              
126 60 100       1054 if (my $attribute = $stmt->$set()) {
127 10 100       67 my $elements = (ref($attribute) eq 'ARRAY') ? $attribute : [ $attribute ];
128             return uc($set) . ' BY '
129 10 100       30 . join(', ', map { $_->{column} . ($_->{desc} ? (' ' . $_->{desc}) : '') } @$elements)
  13         64  
130             . "\n";
131             }
132              
133 50         271 return '';
134             }
135              
136             sub as_sql_where {
137 52     52 1 230 my $stmt = shift;
138             $stmt->where && @{ $stmt->where } ?
139 52 100 66     901 'WHERE ' . join(' AND ', @{ $stmt->where }) . "\n" :
  23         474  
140             '';
141             }
142              
143             sub as_sql_having {
144 30     30 1 47 my $stmt = shift;
145             $stmt->having && @{ $stmt->having } ?
146 30 100 66     511 'HAVING ' . join(' AND ', @{ $stmt->having }) . "\n" :
  1         21  
147             '';
148             }
149              
150             sub add_where {
151 22     22 1 656 my $stmt = shift;
152             ## xxx Need to support old range and transform behaviors.
153 22         51 my($col, $val) = @_;
154 22 50       100 Carp::croak("Invalid/unsafe column name $col") unless $col =~ /^[\w\.]+$/;
155 22         74 my($term, $bind, $tcol) = $stmt->_mk_term($col, $val);
156 22         36 push @{ $stmt->{where} }, "($term)";
  22         63  
157 22         35 push @{ $stmt->{bind} }, @$bind;
  22         41  
158 22         392 $stmt->where_values->{$tcol} = $val;
159             }
160              
161             sub add_complex_where {
162 2     2 1 20 my $stmt = shift;
163 2         4 my ($terms) = @_;
164 2         6 my ($where, $bind) = $stmt->_parse_array_terms($terms);
165 2         5 push @{ $stmt->{where} }, $where;
  2         5  
166 2         5 push @{ $stmt->{bind} }, @$bind;
  2         8  
167             }
168              
169             sub _parse_array_terms {
170 2     2   4 my $stmt = shift;
171 2         4 my ($term_list) = @_;
172              
173 2         4 my @out;
174 2         4 my $logic = 'AND';
175 2         4 my @bind;
176 2         5 foreach my $t ( @$term_list ) {
177 4 50       12 if (! ref $t ) {
178 0 0       0 $logic = $1 if uc($t) =~ m/^-?(OR|AND|OR_NOT|AND_NOT)$/;
179 0         0 $logic =~ s/_/ /;
180 0         0 next;
181             }
182 4         7 my $out;
183 4 50       11 if (ref $t eq 'HASH') {
    0          
184             # bag of terms to apply $logic with
185 4         6 my @out;
186 4         11 foreach my $t2 ( keys %$t ) {
187 5         16 my ($term, $bind, $col) = $stmt->_mk_term($t2, $t->{$t2});
188 5         110 $stmt->where_values->{$col} = $t->{$t2};
189 5         31 push @out, "($term)";
190 5         14 push @bind, @$bind;
191             }
192 4         16 $out .= '(' . join(" AND ", @out) . ")";
193             }
194             elsif (ref $t eq 'ARRAY') {
195             # another array of terms to process!
196 0         0 my ($where, $bind) = $stmt->_parse_array_terms( $t );
197 0         0 push @bind, @$bind;
198 0         0 $out = '(' . $where . ')';
199             }
200 4 100       15 push @out, (@out ? ' ' . $logic . ' ' : '') . $out;
201             }
202 2         10 return (join("", @out), \@bind);
203             }
204              
205             sub has_where {
206 0     0 1 0 my $stmt = shift;
207 0         0 my($col, $val) = @_;
208              
209             # TODO: should check if the value is same with $val?
210 0         0 exists $stmt->where_values->{$col};
211             }
212              
213             sub add_having {
214 1     1 1 81 my $stmt = shift;
215 1         3 my($col, $val) = @_;
216             # Carp::croak("Invalid/unsafe column name $col") unless $col =~ /^[\w\.]+$/;
217              
218 1 50       18 if (my $orig = $stmt->select_map_reverse->{$col}) {
219 1         7 $col = $orig;
220             }
221              
222 1         4 my($term, $bind) = $stmt->_mk_term($col, $val);
223 1         4 push @{ $stmt->{having} }, "($term)";
  1         4  
224 1         2 push @{ $stmt->{bind} }, @$bind;
  1         3  
225             }
226              
227             sub _mk_term {
228 43     43   67 my $stmt = shift;
229 43         72 my($col, $val) = @_;
230 43         63 my $term = '';
231 43         68 my (@bind, $m);
232 43 100       148 if (ref($val) eq 'ARRAY') {
    100          
    100          
    100          
233 8 100 100     46 if (ref $val->[0] or (($val->[0] || '') eq '-and')) {
      100        
234 6         11 my $logic = 'OR';
235 6         17 my @values = @$val;
236 6 100       20 if ($val->[0] eq '-and') {
237 4         6 $logic = 'AND';
238 4         5 shift @values;
239             }
240              
241 6         11 my @terms;
242 6         15 for my $v (@values) {
243 15         37 my($term, $bind) = $stmt->_mk_term($col, $v);
244 15         39 push @terms, "($term)";
245 15         38 push @bind, @$bind;
246             }
247 6         26 $term = join " $logic ", @terms;
248             } else {
249 2 50       39 $col = $m->($col) if $m = $stmt->column_mutator;
250 2         19 $term = $stmt->_mk_term_arrayref($col, 'IN', $val);
251 2         5 @bind = @$val;
252             }
253             } elsif (ref($val) eq 'HASH') {
254 14   66     58 my $c = $val->{column} || $col;
255 14 50       259 $c = $m->($c) if $m = $stmt->column_mutator;
256 14         94 my $op = uc $val->{op};
257 14 100 100     129 if (($op eq 'IN' or $op eq 'NOT IN') and ref $val->{value} eq 'ARRAY') {
    100 100        
    100 66        
      100        
      66        
258 2         7 $term = $stmt->_mk_term_arrayref($c, $op, $val->{value});
259 2         3 push @bind, @{$val->{value}};
  2         7  
260             } elsif (($op eq 'IN' or $op eq 'NOT IN') and ref $val->{value} eq 'REF') {
261 1         2 my @values = @{${$val->{value}}};
  1         2  
  1         3  
262 1         5 $term = "$c $op (" . (shift @values) . ")";
263 1         3 push @bind, @values;
264             } elsif ($op eq 'BETWEEN' and ref $val->{value} eq 'ARRAY') {
265 1 50       2 Carp::croak "USAGE: foo => {op => 'BETWEEN', value => [\$a, \$b]}" if @{$val->{value}} != 2;
  1         5  
266 1         5 $term = "$c $op ? AND ?";
267 1         2 push @bind, @{$val->{value}};
  1         3  
268             } else {
269 10 100       28 if (ref $val->{value} eq 'SCALAR') {
270 1         3 $term = "$c $val->{op} " . ${$val->{value}};
  1         4  
271             } else {
272 9         41 $term = "$c $val->{op} ?";
273 9         32 push @bind, $val->{value};
274             }
275             }
276             } elsif (ref($val) eq 'SCALAR') {
277 1 50       20 $col = $m->($col) if $m = $stmt->column_mutator;
278 1         10 $term = "$col $$val";
279             } elsif (ref($val) eq 'REF') {
280 1 50       20 $col = $m->($col) if $m = $stmt->column_mutator;
281 1         9 my @values = @{$$val};
  1         4  
282 1         4 $term = "$col " . (shift @values);
283 1         2 push @bind, @values;
284             } else {
285 19 50       346 $col = $m->($col) if $m = $stmt->column_mutator;
286 19 100       116 if (defined $val) {
287 18         36 $term = "$col = ?";
288 18         37 push @bind, $val;
289             } else {
290 1         3 $term = "$col IS NULL";
291             }
292             }
293 43         137 ($term, \@bind, $col);
294             }
295              
296             sub _mk_term_arrayref {
297 4     4   12 my ($stmt, $col, $op, $val) = @_;
298 4 100       11 if (@$val) {
299 3         17 return "$col $op (".join(',', ('?') x scalar @$val).')';
300             } else {
301 1 50       5 if ($op eq 'IN') {
    0          
302 1         4 return '0 = 1';
303             } elsif ($op eq 'NOT IN') {
304 0         0 return '1 = 1';
305             }
306             }
307             }
308              
309             sub _add_index_hint {
310 35     35   57 my $stmt = shift;
311 35         59 my ($tbl_name) = @_;
312 35         589 my $hint = $stmt->index_hint->{$tbl_name};
313 35 100 66     266 return $tbl_name unless $hint && ref($hint) eq 'HASH';
314 3 50 33     14 if ($hint->{list} && @{ $hint->{list} }) {
  3         10  
315             return $tbl_name . ' ' . uc($hint->{type} || 'USE') . ' INDEX (' .
316 3   50     12 join (',', @{ $hint->{list} }) .
  3         13  
317             ')';
318             }
319 0           return $tbl_name;
320             }
321              
322             1;
323              
324             __END__