File Coverage

blib/lib/SQL/Interp.pm
Criterion Covered Total %
statement 194 215 90.2
branch 103 128 80.4
condition 19 23 82.6
subroutine 22 25 88.0
pod 3 5 60.0
total 341 396 86.1


line stmt bran cond sub pod time code
1             package SQL::Interp;
2              
3             our $VERSION = '1.27';
4              
5 4     4   139937 use strict;
  4         24  
  4         121  
6 4     4   19 use warnings;
  4         7  
  4         98  
7 4     4   18 use Carp;
  4         6  
  4         698  
8              
9              
10             # Custom import magic to support Sub::Exporter '-as' style function renaming.
11             # (for backwards compatibility with older versions without adding an additional dependency)
12             sub import {
13 6     6   8564 my $pkg = shift;
14 6         15 my $caller = caller;
15 6         21 my %export = qw/sql_interp 1 sql_interp_strict 1 sql_type 1 sql 1/;
16 6         25 while(my $fn = shift) {
17 27 100       56 if($fn eq ':all') {
18 5         25 push @_, keys %export;
19 5         17 next;
20             }
21 22 50       46 croak "Symbol '$fn' is not exported by $pkg" if !$export{$fn};
22 22         26 my $as = $fn;
23 22 100       46 if(ref $_[0] eq 'HASH') {
24 2         4 my $arg = shift;
25 2 50       5 $as = $arg->{'-as'} if defined $arg->{'-as'};
26             }
27 4     4   31 no strict 'refs';
  4         7  
  4         9494  
28 22         28 *{$caller.'::'.$as} = *{$pkg.'::'.$fn};
  22         1598  
  22         54  
29             }
30             }
31              
32              
33             # whether TRACE_SQL is enabled
34             my $trace_sql_enabled = $ENV{TRACE_SQL} || 0;
35              
36             # regexes
37             my $id_match = qr/(?:[a-zA-Z_][a-zA-Z0-9_\$\.]*|"[^"]+"|`[^`]+`)/;
38              
39              
40             # next ID to use for table alias
41             # [local to sql_interp functions]
42             my $alias_id = 0;
43              
44             # current index in interpolation list
45             # [local to sql_interp functions]
46             my $idx = 0;
47              
48             # current interpolation list
49             # [local to sql_interp functions]
50             my $items_ref = undef;
51              
52             # whether typed sql_type() ever used (if so,
53             # format of @bind result is more complicated)
54             # [local to sql_interp functions]
55             my $is_var_used = 0;
56              
57             # bind elements in interpolation
58             # [local to sql_interp functions]
59             my @bind;
60              
61             # only used by DBIx::Interp, so not further documented here.
62             # Doesn't do anything, but may accept options to influence sql_interp()'s behavior in the future.
63             sub new {
64 2     2 0 89 bless {}, shift;
65             }
66              
67              
68             # note: sql_interp is not reentrant.
69             sub sql_interp {
70 164     164 1 50889 my @items = @_;
71              
72             # clear call state
73 164         263 $alias_id = 0;
74 164         218 $idx = 0;
75 164         300 $items_ref = undef;
76 164         214 $is_var_used = 0;
77 164         306 @bind = ();
78              
79             # Legacy: We may be called with an object as first argument; it's unused so throw it away
80 164 100 66     940 shift @items if UNIVERSAL::isa($items[0], 'SQL::Interp') || UNIVERSAL::isa($items[0], 'DBI::db');
81              
82 164         271 $items_ref = \@items;
83              
84             # interpolate!
85 164         330 my $sql = _sql_interp(@items);
86              
87             # convert bind values to complex format (if needed)
88 159 100       322 if ($is_var_used) {
89 12         24 for my $val (@bind) {
90 36         48 my $valcopy = $val;
91 36 100       85 ! ref $val and $val = [$val, sql_type(\$valcopy)];
92             }
93             }
94              
95             $trace_sql_enabled
96 159 50       279 and print STDERR "DEBUG:interp[sql=$sql,bind="
97             . join(':', @bind) . "]\n";
98              
99 159         481 return ($sql, @bind);
100             }
101              
102             # Prevent accidental SQL injection holes
103             # By enforcing the rule that two non-references cannot be used
104             # in a row. If you really mean that, concatanate the strings instead.
105             sub sql_interp_strict {
106 1     1 0 138 my @items = @_;
107              
108 1         3 my $adjacent_string_cnt = 0;
109 1         3 for my $item (@items) {
110             # If we have a reference, reset the counter and move to the next element.
111 2 50       6 if (ref $item) {
112 0         0 $adjacent_string_cnt = 0;
113             }
114             else {
115 2         4 $adjacent_string_cnt++;
116 2 100       7 if ($adjacent_string_cnt == 2) {
117 1         171 croak "failed sql_interp_strict check. Refactor to concatenate adjacent strings in sql_interp array";
118             }
119             }
120              
121             }
122              
123 0         0 return sql_interp(@_);
124             }
125              
126             # helper called by sql_interp()
127             # @items - interpolation list
128             sub _sql_interp {
129 252     252   433 my (@items) = @_;
130              
131 252         335 my $sql = '';
132              
133 252         412 foreach my $item (@items) {
134 446         528 my $varobj;
135 446         589 my $bind_size = @bind;
136 446 100       871 if (ref $item eq 'SQL::Interp::Variable') {
137 18 100 66     67 unless (keys %$item == 1 && defined($item->{value})) {
138 16         26 $varobj = $item;
139 16         19 $is_var_used = 1;
140             }
141 18         33 $item = $item->{value};
142             }
143              
144 446 100       926 if (ref $item eq 'SQL::Interp::SQL') {
    100          
145 50         132 my ($sql2, @bind2) = _sql_interp(@$item);
146 50 100       132 $sql .= ' ' if $sql ne '';
147 50         75 $sql .= $sql2;
148 50         78 push @bind, @bind2;
149             }
150             elsif (ref $item) {
151 191 100 66     2440 if ($sql =~ /\b(NOT\s+)?IN\s*$/si) {
    100 100        
    100          
    100          
    100          
    100          
    100          
    50          
152 30   100     151 my $not = quotemeta($1 || '');
153              
154 30 100       71 $item = [ $$item ] if ref $item eq 'SCALAR';
155              
156             # allow double references
157 30 100       70 $item = $$item if ref $item eq 'REF' ;
158              
159 30 50       57 if (ref $item eq 'ARRAY') {
160 30 100       59 if (@$item == 0) {
161 12 100       22 my $dummy_expr = $not ? '1=1' : '1=0';
162 12 50       301 $sql =~ s/$id_match\s+${not}IN\s*$/$dummy_expr/si or croak 'ASSERT';
163             }
164             else {
165             $sql .= " (" . join(', ', map {
166 18         36 _sql_interp_data($_);
  37         68  
167             } @$item) . ")";
168             }
169             }
170             else {
171 0         0 _error_item($idx, \@items);
172             }
173             }
174             elsif ($sql =~ /\bARRAY\s*$/si) {
175 8 100       21 $item = [ $$item ] if ref $item eq 'SCALAR';
176              
177             # allow double references
178 8 100       19 $item = $$item if ref $item eq 'REF' ;
179              
180 8 50       14 if (ref $item eq 'ARRAY') {
181             $sql .= '[' . join(', ', map {
182 8         20 _sql_interp_data($_);
  10         19  
183             } @$item) . ']';
184             }
185             else {
186 0         0 _error_item($idx, \@items);
187             }
188             }
189             elsif ($sql =~ /\b(?:ON\s+DUPLICATE\s+KEY\s+UPDATE|SET)\s*$/si && ref $item eq 'HASH') {
190 4 50       13 _error('Hash has zero elements.') if keys %$item == 0;
191             $sql .= " " . join(', ', map {
192 4         18 my $key = $_;
  10         15  
193 10         19 my $val = $item->{$key};
194 10         17 "$key=" .
195             _sql_interp_data($val);
196             } (sort keys %$item));
197             }
198             elsif ($sql =~ /\b(REPLACE|INSERT)[\w\s]*\sINTO\s*$id_match\s*$/si) {
199 26 100       85 $item = [ $$item ] if ref $item eq 'SCALAR';
200 26 100       60 if (ref $item eq 'ARRAY') {
    50          
201             $sql .= " VALUES(" . join(', ', map {
202 18         39 _sql_interp_data($_);
  24         37  
203             } @$item) . ")";
204             }
205             elsif (ref $item eq 'HASH') {
206 8         36 my @keyseq = sort keys %$item;
207             $sql .=
208             " (" . join(', ', @keyseq) . ")" .
209             " VALUES(" . join(', ', map {
210 8         35 _sql_interp_data($item->{$_});
  14         28  
211             } @keyseq) . ")";
212             }
213 0         0 else { _error_item($idx, \@items); }
214             }
215             elsif ($sql =~ /(?:\bFROM|JOIN)\s*$/si && $sql !~ /DISTINCT\s+FROM\s*$/) {
216             # table reference
217              
218             # get alias for table
219 33         65 my $table_alias = undef; # alias given to table
220 33         52 my $next_item = $items[$idx + 1];
221 33 100 66     103 if(defined $next_item && ref $next_item eq '' &&
      100        
222             $next_item =~ /\s*AS\b/is)
223             {
224 2         5 $table_alias = undef; # provided by client
225             }
226             else {
227 31         57 $table_alias = 'tbl' . $alias_id++;
228             }
229              
230 33 50       73 $sql .= ' ' unless $sql eq '';
231 33         99 $sql .= _sql_interp_resultset($item);
232 30 100       85 $sql .= " AS $table_alias" if defined $table_alias;
233             }
234             elsif (ref $item eq 'SCALAR') {
235 54         131 push @bind, $$item;
236 54         99 $sql .= ' ?';
237             }
238             elsif (ref $item eq 'HASH') { # e.g. WHERE {x = 3, y = 4}
239 22 100       66 if (keys %$item == 0) {
240 2         6 $sql .= ' 1=1';
241             }
242             else {
243             my $s = join ' AND ', map {
244 20         69 my $key = $_;
  34         54  
245 34         55 my $val = $item->{$key};
246 34 100       78 if (! defined $val) {
    100          
247 2         7 "$key IS NULL";
248             }
249             elsif (ref $val eq 'ARRAY') {
250 8         18 _sql_interp_list($key, $val);
251             }
252             else {
253 24         48 "$key=" .
254             _sql_interp_data($val);
255             }
256             } (sort keys %$item);
257 20 100       64 $s = "($s)" if keys %$item > 1;
258 20         37 $s = " $s";
259 20         44 $sql .= $s;
260             }
261             }
262             elsif (ref $item eq 'ARRAY') { # result set
263 14 50       36 $sql .= ' ' unless $sql eq '';
264 14         26 $sql .= _sql_interp_resultset($item);
265             }
266 0         0 else { _error_item($idx, \@items); }
267             }
268             else {
269 205 100 100     1028 $sql .= ' ' unless $sql =~ /(^|\s)$/ || $item =~ /^\s/; # style
270 205         446 $sql .= $item;
271             }
272              
273             # attach $varobj to any bind values it generates
274 441 100       785 if ($varobj) {
275 16         29 my $num_pushed = @bind - $bind_size;
276 16         43 for my $val (@bind[-$num_pushed..-1]) {
277 20         49 $val = [$val, $varobj];
278             }
279             }
280 441         736 $idx++;
281             }
282              
283 247         553 return $sql;
284             }
285              
286             # sql_interp helper function.
287             # Interpolate data element in aggregate variable (hashref or arrayref).
288             # $ele - raw input element from aggregate.
289             # returns $sql
290             sub _sql_interp_data {
291 203     203   320 my ($ele) = @_;
292 203 100       328 if (ref $ele) { # e.g. sql()
293 38         66 my ($sql2, @bind2) = _sql_interp($ele);
294 38         54 push @bind, @bind2;
295 38 50       76 $is_var_used = 1 if ref $bind2[0];
296 38         145 return $sql2;
297             }
298             else {
299 165         266 push @bind, $ele;
300 165         506 return '?';
301             }
302             }
303              
304             # sql_interp helper function to interpolate "key IN list",
305             # assuming context ("WHERE", {key => $list, ...}).
306             sub _sql_interp_list {
307 8     8   13 my ($key, $list) = @_;
308 8 100       18 if (@$list == 0) {
309 2         9 return "1=0";
310             }
311             else {
312 6         6 my @sqle;
313 6         13 for my $ele (@$list) {
314 12         18 my $sqle
315             = _sql_interp_data($ele);
316 12         23 push @sqle, $sqle;
317             }
318 6         19 my $sql2 = $key . " IN (" . join(', ', @sqle) . ")";
319 6         22 return $sql2;
320             }
321             }
322             # sql_interp helper function to interpolate result set,
323             # e.g. [[1,2],[3,4]] or [{a=>1,b=>2},{a=>3,b=>4}].
324             sub _sql_interp_resultset {
325 47     47   72 my($item) = @_;
326 47         70 my $sql = '';
327 47 50       86 if (ref $item eq 'ARRAY') {
328 47 100       87 _error("table reference has zero rows") # improve?
329             if @$item == 0;
330 46         67 my $sql2 = '';
331 46 100       108 if(ref $item->[0] eq 'ARRAY') {
    50          
332             _error("table reference has zero columns") # improve?
333 28 100       37 if @{ $item->[0] } == 0;
  28         63  
334 26         48 for my $row ( @$item ) {
335 30         44 my $is_first_row = ($sql2 eq '');
336 30 100       56 $sql2 .= ' UNION ALL ' unless $is_first_row;
337             $sql2 .=
338             "SELECT " .
339             join(', ', map {
340 30         55 _sql_interp_data($_)
  40         64  
341             } @$row);
342             }
343             }
344             elsif(ref $item->[0] eq 'HASH') {
345             _error("table reference has zero columns") # improve?
346 18 100       20 if keys %{ $item->[0] } == 0;
  18         54  
347 16         26 my $first_row = $item->[0];
348 16         29 for my $row ( @$item ) {
349 20         28 my $is_first_row = ($sql2 eq '');
350 20 100       38 $sql2 .= ' UNION ALL ' unless $is_first_row;
351             $sql2 .=
352             "SELECT " .
353             join(', ', map {
354 20         62 my($key, $val) = ($_, $row->{$_});
  32         60  
355 32         50 my $sql3 = _sql_interp_data($val);
356 32 100       82 $sql3 .= " AS $key" if $is_first_row;
357 32         95 $sql3;
358             } (sort keys %$first_row));
359             }
360             }
361             else {
362 0         0 _error_item($idx, $items_ref);
363             }
364 42 50       90 $sql .= ' ' unless $sql eq '';
365 42         85 $sql .= "($sql2)";
366             }
367 0         0 else { _error_item($idx, $items_ref); }
368 42         83 return $sql;
369             }
370              
371             sub sql {
372 23     23 1 2954 return SQL::Interp::SQL->new(@_);
373             }
374              
375             sub sql_type {
376 29     29 1 2031 return SQL::Interp::Variable->new(@_);
377             }
378              
379             # helper function to throw error
380             sub _error_item {
381 0     0   0 my ($idx, $items_ref) = @_;
382 0 0       0 my $prev = $idx > 0 ? $items_ref->[$idx-1] : undef;
383 0 0       0 my $prev_text = defined($prev) ? " following '$prev'" : "";
384 0         0 my $cur = $items_ref->[$idx];
385 0         0 _error("SQL::Interp error: Unrecognized "
386             . "'$cur'$prev_text in interpolation list.");
387 0         0 return;
388             }
389              
390             sub _error {
391 5     5   635 croak "SQL::Interp error: $_[0]";
392             }
393              
394             1;
395              
396             package SQL::Interp::Variable;
397 4     4   34 use strict;
  4         14  
  4         128  
398 4     4   24 use Carp;
  4         7  
  4         681  
399              
400             sub new {
401 29     29   60 my ($class, $value, %params) = @_;
402 29 50       63 SQL::Interp::_error(
403             "Value '$value' in sql_type constructor is not a reference")
404             if ! ref $value;
405 29         76 my $self = bless {value => $value, %params}, $class;
406 29         101 return $self;
407             }
408              
409             1;
410              
411              
412             package SQL::Interp::SQL;
413 4     4   31 use strict;
  4         8  
  4         111  
414 4     4   23 use Carp;
  4         7  
  4         316  
415 4     4   3907 use overload '.' => \&concat, '""' => \&stringify;
  4         3093  
  4         31  
416              
417             sub new {
418 24     24   58 my ($class, @list) = @_;
419              
420 24         39 my $self = \@list;
421 24         40 bless $self, $class;
422 24         146 return $self;
423             }
424              
425             # Concatenate SQL object with another expression.
426             # An SQL object can be concatenated with another SQL object,
427             # variable reference, or an SQL string.
428             sub concat {
429 0     0     my ($a, $b, $inverted) = @_;
430              
431 0 0         my @params = ( @$a, ref $b eq __PACKAGE__ ? @$b : $b );
432 0 0         @params = reverse @params if $inverted;
433 0           my $o = SQL::Interp::SQL->new(@params);
434 0           return $o;
435             }
436              
437             sub stringify {
438 0     0     my ($a) = @_;
439 0           return $a;
440             }
441              
442             1;
443              
444             __END__