File Coverage

blib/lib/Data/ObjectDriver/SQL.pm
Criterion Covered Total %
statement 233 267 87.2
branch 97 126 76.9
condition 44 75 58.6
subroutine 21 23 91.3
pod 15 15 100.0
total 410 506 81.0


line stmt bran cond sub pod time code
1             # $Id$
2              
3             package Data::ObjectDriver::SQL;
4 2     2   7930 use strict;
  2         3  
  2         65  
5 2     2   8 use warnings;
  2         2  
  2         89  
6 2     2   7 use Scalar::Util 'blessed';
  2         3  
  2         166  
7              
8 2     2   23 use base qw( Class::Accessor::Fast );
  2         4  
  2         3181  
9              
10             __PACKAGE__->mk_accessors(qw(
11             select distinct select_map select_map_reverse
12             from joins where bind limit offset group order
13             having where_values column_mutator index_hint
14             comment as
15             ));
16              
17             sub new {
18 53     53 1 687790 my $class = shift;
19 53         373 my $stmt = $class->SUPER::new(@_);
20 53         2517 $stmt->select([]);
21 53         2439 $stmt->distinct(0);
22 53         4746 $stmt->select_map({});
23 53         2125 $stmt->select_map_reverse({});
24 53         1750 $stmt->bind([]);
25 53         1549 $stmt->from([]);
26 53         1569 $stmt->where([]);
27 53         1636 $stmt->where_values({});
28 53         1584 $stmt->having([]);
29 53         1730 $stmt->joins([]);
30 53         1773 $stmt->index_hint({});
31 53         952 $stmt;
32             }
33              
34             sub add_select {
35 13     13 1 96 my $stmt = shift;
36 13         36 my($term, $col) = @_;
37 13         23 push @{ $stmt->select }, $term;
  13         322  
38 13 50 33     113 if (blessed($term) && $term->isa('Data::ObjectDriver::SQL')) {
39 0   0     0 my $alias = $col || $term->as || $term->as_sql;
40 0         0 $stmt->select_map->{$term} = $alias;
41 0         0 $stmt->select_map_reverse->{$alias} = $term;
42             } else {
43 13   66     47 $col ||= $term;
44 13         290 $stmt->select_map->{$term} = $col;
45 13         503 $stmt->select_map_reverse->{$col} = $term;
46             }
47             }
48              
49             sub add_join {
50 8     8 1 10360 my $stmt = shift;
51 8         26 my($table, $joins) = @_;
52 8 100       14 push @{ $stmt->joins }, {
  8         260  
53             table => $table,
54             joins => ref($joins) eq 'ARRAY' ? $joins : [ $joins ],
55             };
56             }
57              
58             sub add_index_hint {
59 1     1 1 3 my $stmt = shift;
60 1         2 my($table, $hint) = @_;
61             $stmt->index_hint->{$table} = {
62             type => $hint->{type} || 'USE',
63 1 50 50     29 list => ref($hint->{list}) eq 'ARRAY' ? $hint->{list} : [ $hint->{list} ],
64             };
65             }
66              
67             sub as_sql {
68 32     32 1 3343 my $stmt = shift;
69 32         60 my $sql = '';
70 32         57 my @bind_for_select;
71              
72 32 100       55 if (@{ $stmt->select }) {
  32         793  
73 14         122 $sql .= 'SELECT ';
74 14 100       325 $sql .= 'DISTINCT ' if $stmt->distinct;
75 14         322 my $select_map = $stmt->select_map;
76             $sql .= join(', ', map {
77 19         84 my $col = $_;
78 19         41 my $alias = $select_map->{$col};
79 19 50 33     61 if (blessed($col) && $col->isa('Data::ObjectDriver::SQL')) {
80 0         0 push @bind_for_select, @{ $col->{bind} };
  0         0  
81 0         0 @{ $col->{bind} } = ();
  0         0  
82 0         0 $col->as_subquery($alias);
83             } else {
84 19 50       42 if ($alias) {
85 19 100       419 /(?:^|\.)\Q$alias\E$/ ? $col : "$col $alias";
86             } else {
87 0         0 $col;
88             }
89             }
90 14         60 } @{ $stmt->select }) . "\n";
  14         252  
91             }
92 32         190 $sql .= 'FROM ';
93              
94             ## Add any explicit JOIN statements before the non-joined tables.
95 32         54 my %joined;
96 32 50       48 my @from = @{ $stmt->from || [] };
  32         850  
97 32 100 66     930 if ($stmt->joins && @{ $stmt->joins }) {
  32         3839  
98 8         59 my $initial_table_written = 0;
99 8         16 for my $j (@{ $stmt->joins }) {
  8         201  
100 11         69 my($table, $joins) = map { $j->{$_} } qw( table joins );
  22         66  
101 11         33 $table = $stmt->_add_index_hint($table); ## index hint handling
102 11 100       37 $sql .= $table unless $initial_table_written++;
103 11         41 $joined{$table}++;
104 11         17 for my $join (@{ $j->{joins} }) {
  11         31  
105 13         37 $joined{$join->{table}}++;
106             $sql .= ' ' .
107             uc($join->{type}) . ' JOIN ' . $join->{table} . ' ON ' .
108 13         55 $join->{condition};
109             }
110             }
111 8         22 @from = grep { ! $joined{ $_ } } @from;
  3         11  
112 8 100       23 $sql .= ', ' if @from;
113             }
114              
115 32         173 my @bind_for_from;
116              
117 32 100       78 if (@from) {
118             $sql .= join ', ', map {
119 25         58 my $from = $_;
  26         55  
120 26 50 33     84 if (blessed($from) && $from->isa('Data::ObjectDriver::SQL')) {
121 0         0 push @bind_for_from, @{$from->{bind}};
  0         0  
122 0         0 @{$from->{bind}} = ();
  0         0  
123 0         0 $from->as_subquery;
124             } else {
125 26         77 $stmt->_add_index_hint($from);
126             }
127             } @from;
128             }
129              
130 32         71 $sql .= "\n";
131 32         90 $sql .= $stmt->as_sql_where;
132              
133 32         295 $sql .= $stmt->as_aggregate('group');
134 32         86 $sql .= $stmt->as_sql_having;
135 32         339 $sql .= $stmt->as_aggregate('order');
136              
137 32         91 $sql .= $stmt->as_limit;
138 31         909 my $comment = $stmt->comment;
139 31 100 66     253 if ($comment && $comment =~ /([ 0-9a-zA-Z.:;()_#&,]+)/) {
140 3 50       18 $sql .= "-- $1" if $1;
141             }
142              
143 31         58 @{ $stmt->{bind} } = (@bind_for_select, @bind_for_from, @{ $stmt->{bind} });
  31         74  
  31         73  
144              
145 31         233 return $sql;
146             }
147              
148             sub as_subquery {
149 0     0 1 0 my ($stmt, $alias) = @_;
150 0         0 my $subquery = '(' . $stmt->as_sql . ')';
151 0   0     0 $alias ||= $stmt->as;
152 0 0       0 if ($alias) {
153 0         0 $subquery .= ' '. $alias;
154             }
155 0         0 $subquery;
156             }
157              
158             sub as_limit {
159 32     32 1 61 my $stmt = shift;
160 32 100       883 my $n = $stmt->limit or
161             return '';
162 4 100       54 die "Non-numerics in limit clause ($n)" if $n =~ /\D/;
163 3 100       72 return sprintf "LIMIT %d%s\n", $n,
164             ($stmt->offset ? " OFFSET " . int($stmt->offset) : "");
165             }
166              
167             sub as_aggregate {
168 64     64 1 110 my $stmt = shift;
169 64         123 my($set) = @_;
170              
171 64 100       1510 if (my $attribute = $stmt->$set()) {
172 10 100       77 my $elements = (ref($attribute) eq 'ARRAY') ? $attribute : [ $attribute ];
173             return uc($set) . ' BY '
174 10 100       35 . join(', ', map { $_->{column} . ($_->{desc} ? (' ' . $_->{desc}) : '') } @$elements)
  13         103  
175             . "\n";
176             }
177              
178 54         377 return '';
179             }
180              
181             sub as_sql_where {
182 68     68 1 570 my $stmt = shift;
183             $stmt->where && @{ $stmt->where } ?
184 68 100 66     1771 'WHERE ' . join(' AND ', @{ $stmt->where }) . "\n" :
  34         1108  
185             '';
186             }
187              
188             sub as_sql_having {
189 32     32 1 54 my $stmt = shift;
190             $stmt->having && @{ $stmt->having } ?
191 32 100 66     849 'HAVING ' . join(' AND ', @{ $stmt->having }) . "\n" :
  1         14  
192             '';
193             }
194              
195             sub as_escape {
196 6     6 1 19 my ($stmt, $escape_char) = @_;
197              
198             # escape_char can be ''(two quotes), or \\ for mysql and \ for others, but it doesn't accept any injections.
199 6 100 66     59 die 'escape_char length must be up to two characters' if defined($escape_char) && length($escape_char) > 2;
200              
201 5         50 return " ESCAPE '$escape_char'";
202             }
203              
204             sub add_where {
205 30     30 1 2012 my $stmt = shift;
206             ## xxx Need to support old range and transform behaviors.
207 30         2198 my($col, $val) = @_;
208 30 50       249 Carp::croak("Invalid/unsafe column name $col") unless $col =~ /^[\w\.]+$/;
209 30         144 my($term, $bind, $tcol) = $stmt->_mk_term($col, $val);
210 29         62 push @{ $stmt->{where} }, "($term)";
  29         116  
211 29         61 push @{ $stmt->{bind} }, @$bind;
  29         83  
212 29         752 $stmt->where_values->{$tcol} = $val;
213             }
214              
215             sub add_complex_where {
216 9     9 1 82 my $stmt = shift;
217 9         24 my ($terms) = @_;
218 9         36 my ($where, $bind) = $stmt->_parse_array_terms($terms);
219 9 100       36 if ($where) {
220 6         11 push @{ $stmt->{where} }, $where;
  6         18  
221 6         12 push @{ $stmt->{bind} }, @$bind;
  6         25  
222             }
223             }
224              
225             sub _parse_array_terms {
226 11     11   24 my $stmt = shift;
227 11         25 my ($term_list) = @_;
228              
229 11         21 my @out;
230 11         25 my $logic = 'AND';
231 11         21 my @bind;
232 11         34 foreach my $t ( @$term_list ) {
233 13 50       44 if (! ref $t ) {
234 0 0       0 $logic = $1 if uc($t) =~ m/^-?(OR|AND|OR_NOT|AND_NOT)$/;
235 0         0 $logic =~ s/_/ /;
236 0         0 next;
237             }
238 13         24 my $out;
239 13 100       54 if (ref $t eq 'HASH') {
    50          
240             # bag of terms to apply $logic with
241 11         22 my @out;
242 11         37 foreach my $t2 ( keys %$t ) {
243 10         39 my ($term, $bind, $col) = $stmt->_mk_term($t2, $t->{$t2});
244 10         243 $stmt->where_values->{$col} = $t->{$t2};
245 10 50       67 if ($term) {
246 10         31 push @out, "($term)";
247 10         32 push @bind, @$bind;
248             }
249             }
250 11 100       60 $out .= '(' . join(" AND ", @out) . ")" if @out;
251             }
252             elsif (ref $t eq 'ARRAY') {
253             # another array of terms to process!
254 2         13 my ($where, $bind) = $stmt->_parse_array_terms( $t );
255 2 50       12 if ($where) {
256 0         0 push @bind, @$bind;
257 0         0 $out = '(' . $where . ')';
258             }
259             }
260 13 100       59 push @out, (@out ? ' ' . $logic . ' ' : '') . $out if $out;
    100          
261             }
262 11         63 return (join("", @out), \@bind);
263             }
264              
265             sub has_where {
266 0     0 1 0 my $stmt = shift;
267 0         0 my($col, $val) = @_;
268              
269             # TODO: should check if the value is same with $val?
270 0         0 exists $stmt->where_values->{$col};
271             }
272              
273             sub add_having {
274 1     1 1 54 my $stmt = shift;
275 1         3 my($col, $val) = @_;
276             # Carp::croak("Invalid/unsafe column name $col") unless $col =~ /^[\w\.]+$/;
277              
278 1 50       12 if (my $orig = $stmt->select_map_reverse->{$col}) {
279 1 50 33     7 if (blessed($orig) && $orig->isa('Data::ObjectDriver::SQL')) {
280             # do nothins
281             } else {
282 1         2 $col = $orig;
283             }
284             }
285              
286 1         3 my($term, $bind) = $stmt->_mk_term($col, $val);
287 1         2 push @{ $stmt->{having} }, "($term)";
  1         2  
288 1         2 push @{ $stmt->{bind} }, @$bind;
  1         2  
289             }
290              
291             sub _mk_term {
292 62     62   146 my $stmt = shift;
293 62         151 my($col, $val) = @_;
294 62         124 my $term = '';
295 62         111 my (@bind, $m);
296 62 100       355 if (ref($val) eq 'ARRAY') {
    100          
    100          
    100          
297 11 100 100     88 if (ref $val->[0] or (($val->[0] || '') eq '-and')) {
      100        
298 9         22 my $logic = 'OR';
299 9         30 my @values = @$val;
300 9 100       34 if ($val->[0] eq '-and') {
301 5         11 $logic = 'AND';
302 5         14 shift @values;
303             }
304              
305 9         21 my @terms;
306 9         57 for my $v (@values) {
307 21         79 my($term, $bind) = $stmt->_mk_term($col, $v);
308 21         58 push @terms, "($term)";
309 21         57 push @bind, @$bind;
310             }
311 9         48 $term = join " $logic ", @terms;
312             } else {
313 2 50       55 $col = $m->($col) if $m = $stmt->column_mutator;
314 2         29 $term = $stmt->_mk_term_arrayref($col, 'IN', $val);
315 2         8 @bind = @$val;
316             }
317             } elsif (ref($val) eq 'HASH') {
318 20   66     239 my $c = $val->{column} || $col;
319 20 50       596 $c = $m->($c) if $m = $stmt->column_mutator;
320 20         186 my $op = uc $val->{op};
321 20 100 100     303 if (($op eq 'IN' or $op eq 'NOT IN') and ref $val->{value} eq 'ARRAY') {
    100 100        
    100 66        
      100        
      66        
322 2         7 $term = $stmt->_mk_term_arrayref($c, $op, $val->{value});
323 2         5 push @bind, @{$val->{value}};
  2         7  
324             } elsif (($op eq 'IN' or $op eq 'NOT IN') and ref $val->{value} eq 'REF') {
325 1         4 my @values = @{${$val->{value}}};
  1         2  
  1         6  
326 1         5 $term = "$c $op (" . (shift @values) . ")";
327 1         3 push @bind, @values;
328             } elsif ($op eq 'BETWEEN' and ref $val->{value} eq 'ARRAY') {
329 1 50       3 Carp::croak "USAGE: foo => {op => 'BETWEEN', value => [\$a, \$b]}" if @{$val->{value}} != 2;
  1         6  
330 1         4 $term = "$c $op ? AND ?";
331 1         3 push @bind, @{$val->{value}};
  1         5  
332             } else {
333 16         39 my $value = $val->{value};
334 16 100 33     82 if (ref $value eq 'SCALAR') {
    50          
335 1         9 $term = "$c $val->{op} " . $$value;
336             } elsif (blessed($value) && $value->isa('Data::ObjectDriver::SQL')) {
337 0         0 local $value->{as} = undef;
338 0         0 $term = "$c $val->{op} ". $value->as_subquery;
339 0         0 push @bind, @{$value->{bind}};
  0         0  
340             } else {
341 15         45 $term = "$c $val->{op} ?";
342 15 100 66     132 $term .= $stmt->as_escape($val->{escape}) if $val->{escape} && $op =~ /^(?:NOT\s+)?I?LIKE$/;
343 14         48 push @bind, $value;
344             }
345             }
346             } elsif (ref($val) eq 'SCALAR') {
347 9 50       257 $col = $m->($col) if $m = $stmt->column_mutator;
348 9         100 $term = "$col $$val";
349             } elsif (ref($val) eq 'REF') {
350 1 50       36 $col = $m->($col) if $m = $stmt->column_mutator;
351 1         10 my @values = @{$$val};
  1         5  
352 1         7 $term = "$col " . (shift @values);
353 1         4 push @bind, @values;
354             } else {
355 21 50       541 $col = $m->($col) if $m = $stmt->column_mutator;
356 21 100       150 if (defined $val) {
357 20         54 $term = "$col = ?";
358 20         49 push @bind, $val;
359             } else {
360 1         3 $term = "$col IS NULL";
361             }
362             }
363 61         296 ($term, \@bind, $col);
364             }
365              
366             sub _mk_term_arrayref {
367 4     4   16 my ($stmt, $col, $op, $val) = @_;
368 4 100       15 if (@$val) {
369 3         20 return "$col $op (".join(',', ('?') x scalar @$val).')';
370             } else {
371 1 50       5 if ($op eq 'IN') {
    0          
372 1         5 return '0 = 1';
373             } elsif ($op eq 'NOT IN') {
374 0         0 return '1 = 1';
375             }
376             }
377             }
378              
379             sub _add_index_hint {
380 37     37   66 my $stmt = shift;
381 37         73 my ($tbl_name) = @_;
382 37         915 my $hint = $stmt->index_hint->{$tbl_name};
383 37 100 66     430 return $tbl_name unless $hint && ref($hint) eq 'HASH';
384 3 50 33     9 if ($hint->{list} && @{ $hint->{list} }) {
  3         8  
385             return $tbl_name . ' ' . uc($hint->{type} || 'USE') . ' INDEX (' .
386 3   50     9 join (',', @{ $hint->{list} }) .
  3         11  
387             ')';
388             }
389 0           return $tbl_name;
390             }
391              
392             1;
393              
394             __END__