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   1763 use strict;
  2         9  
  2         58  
5 2     2   9 use warnings;
  2         4  
  2         54  
6              
7 2     2   9 use base qw( Class::Accessor::Fast );
  2         4  
  2         3250  
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 33254 my $class = shift;
18 36         125 my $stmt = $class->SUPER::new(@_);
19 36         1030 $stmt->select([]);
20 36         842 $stmt->distinct(0);
21 36         834 $stmt->select_map({});
22 36         768 $stmt->select_map_reverse({});
23 36         803 $stmt->bind([]);
24 36         761 $stmt->from([]);
25 36         756 $stmt->where([]);
26 36         744 $stmt->where_values({});
27 36         801 $stmt->having([]);
28 36         750 $stmt->joins([]);
29 36         790 $stmt->index_hint({});
30 36         361 $stmt;
31             }
32              
33             sub add_select {
34 9     9 1 50 my $stmt = shift;
35 9         21 my($term, $col) = @_;
36 9   66     22 $col ||= $term;
37 9         15 push @{ $stmt->select }, $term;
  9         154  
38 9         182 $stmt->select_map->{$term} = $col;
39 9         175 $stmt->select_map_reverse->{$col} = $term;
40             }
41              
42             sub add_join {
43 8     8 1 20 my $stmt = shift;
44 8         20 my($table, $joins) = @_;
45 8 100       10 push @{ $stmt->joins }, {
  8         152  
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         4 my($table, $hint) = @_;
54             $stmt->index_hint->{$table} = {
55             type => $hint->{type} || 'USE',
56 1 50 50     64 list => ref($hint->{list}) eq 'ARRAY' ? $hint->{list} : [ $hint->{list} ],
57             };
58             }
59              
60             sub as_sql {
61 30     30 1 1175 my $stmt = shift;
62 30         54 my $sql = '';
63 30 100       39 if (@{ $stmt->select }) {
  30         547  
64 12         74 $sql .= 'SELECT ';
65 12 100       196 $sql .= 'DISTINCT ' if $stmt->distinct;
66             $sql .= join(', ', map {
67 15         289 my $alias = $stmt->select_map->{$_};
68 15 100 66     296 $alias && /(?:^|\.)\Q$alias\E$/ ? $_ : "$_ $alias";
69 12         63 } @{ $stmt->select }) . "\n";
  12         194  
70             }
71 30         136 $sql .= 'FROM ';
72              
73             ## Add any explicit JOIN statements before the non-joined tables.
74 30         53 my %joined;
75 30 50       39 my @from = @{ $stmt->from || [] };
  30         512  
76 30 100 66     619 if ($stmt->joins && @{ $stmt->joins }) {
  30         578  
77 8         47 my $initial_table_written = 0;
78 8         12 for my $j (@{ $stmt->joins }) {
  8         132  
79 11         49 my($table, $joins) = map { $j->{$_} } qw( table joins );
  22         48  
80 11         27 $table = $stmt->_add_index_hint($table); ## index hint handling
81 11 100       35 $sql .= $table unless $initial_table_written++;
82 11         24 $joined{$table}++;
83 11         14 for my $join (@{ $j->{joins} }) {
  11         27  
84 13         38 $joined{$join->{table}}++;
85             $sql .= ' ' .
86             uc($join->{type}) . ' JOIN ' . $join->{table} . ' ON ' .
87 13         50 $join->{condition};
88             }
89             }
90 8         18 @from = grep { ! $joined{ $_ } } @from;
  3         9  
91 8 100       19 $sql .= ', ' if @from;
92             }
93              
94 30 100       160 if (@from) {
95 23         40 $sql .= join ', ', map { $stmt->_add_index_hint($_) } @from;
  24         65  
96             }
97              
98 30         55 $sql .= "\n";
99 30         63 $sql .= $stmt->as_sql_where;
100              
101 30         218 $sql .= $stmt->as_aggregate('group');
102 30         60 $sql .= $stmt->as_sql_having;
103 30         199 $sql .= $stmt->as_aggregate('order');
104              
105 30         60 $sql .= $stmt->as_limit;
106 29         622 my $comment = $stmt->comment;
107 29 100 66     163 if ($comment && $comment =~ /([ 0-9a-zA-Z.:;()_#&,]+)/) {
108 3 50       13 $sql .= "-- $1" if $1;
109             }
110 29         161 return $sql;
111             }
112              
113             sub as_limit {
114 30     30 1 40 my $stmt = shift;
115 30 100       489 my $n = $stmt->limit or
116             return '';
117 4 100       40 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 90 my $stmt = shift;
124 60         109 my($set) = @_;
125              
126 60 100       1023 if (my $attribute = $stmt->$set()) {
127 10 100       64 my $elements = (ref($attribute) eq 'ARRAY') ? $attribute : [ $attribute ];
128             return uc($set) . ' BY '
129 10 100       29 . join(', ', map { $_->{column} . ($_->{desc} ? (' ' . $_->{desc}) : '') } @$elements)
  13         67  
130             . "\n";
131             }
132              
133 50         273 return '';
134             }
135              
136             sub as_sql_where {
137 52     52 1 218 my $stmt = shift;
138             $stmt->where && @{ $stmt->where } ?
139 52 100 66     874 'WHERE ' . join(' AND ', @{ $stmt->where }) . "\n" :
  23         461  
140             '';
141             }
142              
143             sub as_sql_having {
144 30     30 1 59 my $stmt = shift;
145             $stmt->having && @{ $stmt->having } ?
146 30 100 66     494 'HAVING ' . join(' AND ', @{ $stmt->having }) . "\n" :
  1         22  
147             '';
148             }
149              
150             sub add_where {
151 22     22 1 673 my $stmt = shift;
152             ## xxx Need to support old range and transform behaviors.
153 22         55 my($col, $val) = @_;
154 22 50       96 Carp::croak("Invalid/unsafe column name $col") unless $col =~ /^[\w\.]+$/;
155 22         57 my($term, $bind, $tcol) = $stmt->_mk_term($col, $val);
156 22         41 push @{ $stmt->{where} }, "($term)";
  22         61  
157 22         34 push @{ $stmt->{bind} }, @$bind;
  22         46  
158 22         412 $stmt->where_values->{$tcol} = $val;
159             }
160              
161             sub add_complex_where {
162 2     2 1 22 my $stmt = shift;
163 2         5 my ($terms) = @_;
164 2         6 my ($where, $bind) = $stmt->_parse_array_terms($terms);
165 2         4 push @{ $stmt->{where} }, $where;
  2         6  
166 2         4 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         3 my @out;
174 2         3 my $logic = 'AND';
175 2         4 my @bind;
176 2         6 foreach my $t ( @$term_list ) {
177 4 50       13 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         4 my $out;
183 4 50       10 if (ref $t eq 'HASH') {
    0          
184             # bag of terms to apply $logic with
185 4         6 my @out;
186 4         13 foreach my $t2 ( keys %$t ) {
187 5         14 my ($term, $bind, $col) = $stmt->_mk_term($t2, $t->{$t2});
188 5         90 $stmt->where_values->{$col} = $t->{$t2};
189 5         29 push @out, "($term)";
190 5         14 push @bind, @$bind;
191             }
192 4         15 $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 80 my $stmt = shift;
215 1         4 my($col, $val) = @_;
216             # Carp::croak("Invalid/unsafe column name $col") unless $col =~ /^[\w\.]+$/;
217              
218 1 50       20 if (my $orig = $stmt->select_map_reverse->{$col}) {
219 1         9 $col = $orig;
220             }
221              
222 1         3 my($term, $bind) = $stmt->_mk_term($col, $val);
223 1         2 push @{ $stmt->{having} }, "($term)";
  1         4  
224 1         2 push @{ $stmt->{bind} }, @$bind;
  1         5  
225             }
226              
227             sub _mk_term {
228 43     43   77 my $stmt = shift;
229 43         81 my($col, $val) = @_;
230 43         63 my $term = '';
231 43         65 my (@bind, $m);
232 43 100       136 if (ref($val) eq 'ARRAY') {
    100          
    100          
    100          
233 8 100 100     44 if (ref $val->[0] or (($val->[0] || '') eq '-and')) {
      100        
234 6         9 my $logic = 'OR';
235 6         16 my @values = @$val;
236 6 100       17 if ($val->[0] eq '-and') {
237 4         7 $logic = 'AND';
238 4         7 shift @values;
239             }
240              
241 6         12 my @terms;
242 6         13 for my $v (@values) {
243 15         39 my($term, $bind) = $stmt->_mk_term($col, $v);
244 15         43 push @terms, "($term)";
245 15         38 push @bind, @$bind;
246             }
247 6         23 $term = join " $logic ", @terms;
248             } else {
249 2 50       38 $col = $m->($col) if $m = $stmt->column_mutator;
250 2         17 $term = $stmt->_mk_term_arrayref($col, 'IN', $val);
251 2         6 @bind = @$val;
252             }
253             } elsif (ref($val) eq 'HASH') {
254 14   66     54 my $c = $val->{column} || $col;
255 14 50       273 $c = $m->($c) if $m = $stmt->column_mutator;
256 14         119 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         23 $term = $stmt->_mk_term_arrayref($c, $op, $val->{value});
259 2         4 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         4  
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       2 Carp::croak "USAGE: foo => {op => 'BETWEEN', value => [\$a, \$b]}" if @{$val->{value}} != 2;
  1         6  
266 1         4 $term = "$c $op ? AND ?";
267 1         3 push @bind, @{$val->{value}};
  1         2  
268             } else {
269 10 100       25 if (ref $val->{value} eq 'SCALAR') {
270 1         6 $term = "$c $val->{op} " . ${$val->{value}};
  1         4  
271             } else {
272 9         41 $term = "$c $val->{op} ?";
273 9         30 push @bind, $val->{value};
274             }
275             }
276             } elsif (ref($val) eq 'SCALAR') {
277 1 50       19 $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         8 my @values = @{$$val};
  1         4  
282 1         4 $term = "$col " . (shift @values);
283 1         3 push @bind, @values;
284             } else {
285 19 50       349 $col = $m->($col) if $m = $stmt->column_mutator;
286 19 100       117 if (defined $val) {
287 18         30 $term = "$col = ?";
288 18         38 push @bind, $val;
289             } else {
290 1         4 $term = "$col IS NULL";
291             }
292             }
293 43         170 ($term, \@bind, $col);
294             }
295              
296             sub _mk_term_arrayref {
297 4     4   12 my ($stmt, $col, $op, $val) = @_;
298 4 100       12 if (@$val) {
299 3         17 return "$col $op (".join(',', ('?') x scalar @$val).')';
300             } else {
301 1 50       7 if ($op eq 'IN') {
    0          
302 1         5 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   51 my $stmt = shift;
311 35         67 my ($tbl_name) = @_;
312 35         581 my $hint = $stmt->index_hint->{$tbl_name};
313 35 100 66     258 return $tbl_name unless $hint && ref($hint) eq 'HASH';
314 3 50 33     11 if ($hint->{list} && @{ $hint->{list} }) {
  3         10  
315             return $tbl_name . ' ' . uc($hint->{type} || 'USE') . ' INDEX (' .
316 3   50     12 join (',', @{ $hint->{list} }) .
  3         12  
317             ')';
318             }
319 0           return $tbl_name;
320             }
321              
322             1;
323              
324             __END__