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   1774 use strict;
  2         10  
  2         62  
5 2     2   11 use warnings;
  2         3  
  2         57  
6              
7 2     2   10 use base qw( Class::Accessor::Fast );
  2         3  
  2         3309  
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 34243 my $class = shift;
18 36         156 my $stmt = $class->SUPER::new(@_);
19 36         1079 $stmt->select([]);
20 36         848 $stmt->distinct(0);
21 36         772 $stmt->select_map({});
22 36         815 $stmt->select_map_reverse({});
23 36         800 $stmt->bind([]);
24 36         770 $stmt->from([]);
25 36         758 $stmt->where([]);
26 36         791 $stmt->where_values({});
27 36         803 $stmt->having([]);
28 36         767 $stmt->joins([]);
29 36         812 $stmt->index_hint({});
30 36         360 $stmt;
31             }
32              
33             sub add_select {
34 9     9 1 51 my $stmt = shift;
35 9         21 my($term, $col) = @_;
36 9   66     26 $col ||= $term;
37 9         12 push @{ $stmt->select }, $term;
  9         151  
38 9         184 $stmt->select_map->{$term} = $col;
39 9         173 $stmt->select_map_reverse->{$col} = $term;
40             }
41              
42             sub add_join {
43 8     8 1 16 my $stmt = shift;
44 8         21 my($table, $joins) = @_;
45 8 100       11 push @{ $stmt->joins }, {
  8         196  
46             table => $table,
47             joins => ref($joins) eq 'ARRAY' ? $joins : [ $joins ],
48             };
49             }
50              
51             sub add_index_hint {
52 1     1 1 4 my $stmt = shift;
53 1         4 my($table, $hint) = @_;
54             $stmt->index_hint->{$table} = {
55             type => $hint->{type} || 'USE',
56 1 50 50     32 list => ref($hint->{list}) eq 'ARRAY' ? $hint->{list} : [ $hint->{list} ],
57             };
58             }
59              
60             sub as_sql {
61 30     30 1 1176 my $stmt = shift;
62 30         51 my $sql = '';
63 30 100       41 if (@{ $stmt->select }) {
  30         509  
64 12         69 $sql .= 'SELECT ';
65 12 100       232 $sql .= 'DISTINCT ' if $stmt->distinct;
66             $sql .= join(', ', map {
67 15         289 my $alias = $stmt->select_map->{$_};
68 15 100 66     301 $alias && /(?:^|\.)\Q$alias\E$/ ? $_ : "$_ $alias";
69 12         68 } @{ $stmt->select }) . "\n";
  12         191  
70             }
71 30         135 $sql .= 'FROM ';
72              
73             ## Add any explicit JOIN statements before the non-joined tables.
74 30         45 my %joined;
75 30 50       49 my @from = @{ $stmt->from || [] };
  30         504  
76 30 100 66     687 if ($stmt->joins && @{ $stmt->joins }) {
  30         593  
77 8         47 my $initial_table_written = 0;
78 8         13 for my $j (@{ $stmt->joins }) {
  8         129  
79 11         50 my($table, $joins) = map { $j->{$_} } qw( table joins );
  22         52  
80 11         25 $table = $stmt->_add_index_hint($table); ## index hint handling
81 11 100       32 $sql .= $table unless $initial_table_written++;
82 11         25 $joined{$table}++;
83 11         17 for my $join (@{ $j->{joins} }) {
  11         25  
84 13         30 $joined{$join->{table}}++;
85             $sql .= ' ' .
86             uc($join->{type}) . ' JOIN ' . $join->{table} . ' ON ' .
87 13         52 $join->{condition};
88             }
89             }
90 8         19 @from = grep { ! $joined{ $_ } } @from;
  3         10  
91 8 100       26 $sql .= ', ' if @from;
92             }
93              
94 30 100       162 if (@from) {
95 23         58 $sql .= join ', ', map { $stmt->_add_index_hint($_) } @from;
  24         69  
96             }
97              
98 30         58 $sql .= "\n";
99 30         62 $sql .= $stmt->as_sql_where;
100              
101 30         209 $sql .= $stmt->as_aggregate('group');
102 30         63 $sql .= $stmt->as_sql_having;
103 30         192 $sql .= $stmt->as_aggregate('order');
104              
105 30         94 $sql .= $stmt->as_limit;
106 29         632 my $comment = $stmt->comment;
107 29 100 66     164 if ($comment && $comment =~ /([ 0-9a-zA-Z.:;()_#&,]+)/) {
108 3 50       13 $sql .= "-- $1" if $1;
109             }
110 29         149 return $sql;
111             }
112              
113             sub as_limit {
114 30     30 1 47 my $stmt = shift;
115 30 100       504 my $n = $stmt->limit or
116             return '';
117 4 100       43 die "Non-numerics in limit clause ($n)" if $n =~ /\D/;
118 3 100       55 return sprintf "LIMIT %d%s\n", $n,
119             ($stmt->offset ? " OFFSET " . int($stmt->offset) : "");
120             }
121              
122             sub as_aggregate {
123 60     60 1 101 my $stmt = shift;
124 60         97 my($set) = @_;
125              
126 60 100       1033 if (my $attribute = $stmt->$set()) {
127 10 100       66 my $elements = (ref($attribute) eq 'ARRAY') ? $attribute : [ $attribute ];
128             return uc($set) . ' BY '
129 10 100       30 . join(', ', map { $_->{column} . ($_->{desc} ? (' ' . $_->{desc}) : '') } @$elements)
  13         66  
130             . "\n";
131             }
132              
133 50         272 return '';
134             }
135              
136             sub as_sql_where {
137 52     52 1 226 my $stmt = shift;
138             $stmt->where && @{ $stmt->where } ?
139 52 100 66     883 'WHERE ' . join(' AND ', @{ $stmt->where }) . "\n" :
  23         464  
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     491 'HAVING ' . join(' AND ', @{ $stmt->having }) . "\n" :
  1         22  
147             '';
148             }
149              
150             sub add_where {
151 22     22 1 687 my $stmt = shift;
152             ## xxx Need to support old range and transform behaviors.
153 22         66 my($col, $val) = @_;
154 22 50       101 Carp::croak("Invalid/unsafe column name $col") unless $col =~ /^[\w\.]+$/;
155 22         61 my($term, $bind, $tcol) = $stmt->_mk_term($col, $val);
156 22         39 push @{ $stmt->{where} }, "($term)";
  22         62  
157 22         29 push @{ $stmt->{bind} }, @$bind;
  22         45  
158 22         425 $stmt->where_values->{$tcol} = $val;
159             }
160              
161             sub add_complex_where {
162 2     2 1 23 my $stmt = shift;
163 2         6 my ($terms) = @_;
164 2         7 my ($where, $bind) = $stmt->_parse_array_terms($terms);
165 2         5 push @{ $stmt->{where} }, $where;
  2         6  
166 2         4 push @{ $stmt->{bind} }, @$bind;
  2         7  
167             }
168              
169             sub _parse_array_terms {
170 2     2   3 my $stmt = shift;
171 2         5 my ($term_list) = @_;
172              
173 2         3 my @out;
174 2         5 my $logic = 'AND';
175 2         2 my @bind;
176 2         6 foreach my $t ( @$term_list ) {
177 4 50       14 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         5 my $out;
183 4 50       11 if (ref $t eq 'HASH') {
    0          
184             # bag of terms to apply $logic with
185 4         5 my @out;
186 4         12 foreach my $t2 ( keys %$t ) {
187 5         13 my ($term, $bind, $col) = $stmt->_mk_term($t2, $t->{$t2});
188 5         90 $stmt->where_values->{$col} = $t->{$t2};
189 5         31 push @out, "($term)";
190 5         13 push @bind, @$bind;
191             }
192 4         12 $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         9 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 75 my $stmt = shift;
215 1         4 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         6 $col = $orig;
220             }
221              
222 1         4 my($term, $bind) = $stmt->_mk_term($col, $val);
223 1         3 push @{ $stmt->{having} }, "($term)";
  1         4  
224 1         1 push @{ $stmt->{bind} }, @$bind;
  1         4  
225             }
226              
227             sub _mk_term {
228 43     43   65 my $stmt = shift;
229 43         76 my($col, $val) = @_;
230 43         59 my $term = '';
231 43         64 my (@bind, $m);
232 43 100       159 if (ref($val) eq 'ARRAY') {
    100          
    100          
    100          
233 8 100 100     47 if (ref $val->[0] or (($val->[0] || '') eq '-and')) {
      100        
234 6         12 my $logic = 'OR';
235 6         15 my @values = @$val;
236 6 100       21 if ($val->[0] eq '-and') {
237 4         6 $logic = 'AND';
238 4         7 shift @values;
239             }
240              
241 6         12 my @terms;
242 6         12 for my $v (@values) {
243 15         39 my($term, $bind) = $stmt->_mk_term($col, $v);
244 15         40 push @terms, "($term)";
245 15         39 push @bind, @$bind;
246             }
247 6         25 $term = join " $logic ", @terms;
248             } else {
249 2 50       40 $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     65 my $c = $val->{column} || $col;
255 14 50       261 $c = $m->($c) if $m = $stmt->column_mutator;
256 14         107 my $op = uc $val->{op};
257 14 100 100     125 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         5 push @bind, @{$val->{value}};
  2         5  
260             } elsif (($op eq 'IN' or $op eq 'NOT IN') and ref $val->{value} eq 'REF') {
261 1         3 my @values = @{${$val->{value}}};
  1         2  
  1         6  
262 1         6 $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       3 Carp::croak "USAGE: foo => {op => 'BETWEEN', value => [\$a, \$b]}" if @{$val->{value}} != 2;
  1         5  
266 1         4 $term = "$c $op ? AND ?";
267 1         2 push @bind, @{$val->{value}};
  1         4  
268             } else {
269 10 100       25 if (ref $val->{value} eq 'SCALAR') {
270 1         4 $term = "$c $val->{op} " . ${$val->{value}};
  1         3  
271             } else {
272 9         41 $term = "$c $val->{op} ?";
273 9         33 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       19 $col = $m->($col) if $m = $stmt->column_mutator;
281 1         10 my @values = @{$$val};
  1         4  
282 1         4 $term = "$col " . (shift @values);
283 1         4 push @bind, @values;
284             } else {
285 19 50       344 $col = $m->($col) if $m = $stmt->column_mutator;
286 19 100       116 if (defined $val) {
287 18         33 $term = "$col = ?";
288 18         38 push @bind, $val;
289             } else {
290 1         2 $term = "$col IS NULL";
291             }
292             }
293 43         157 ($term, \@bind, $col);
294             }
295              
296             sub _mk_term_arrayref {
297 4     4   13 my ($stmt, $col, $op, $val) = @_;
298 4 100       11 if (@$val) {
299 3         20 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   48 my $stmt = shift;
311 35         65 my ($tbl_name) = @_;
312 35         595 my $hint = $stmt->index_hint->{$tbl_name};
313 35 100 66     252 return $tbl_name unless $hint && ref($hint) eq 'HASH';
314 3 50 33     16 if ($hint->{list} && @{ $hint->{list} }) {
  3         11  
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__