File Coverage

blib/lib/DBIx/Skinny/SQL.pm
Criterion Covered Total %
statement 202 213 94.8
branch 78 100 78.0
condition 34 57 59.6
subroutine 20 22 90.9
pod 0 16 0.0
total 334 408 81.8


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