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.28';
4              
5 4     4   555126 use strict;
  4         11  
  4         167  
6 4     4   36 use warnings;
  4         10  
  4         245  
7 4     4   24 use Carp;
  4         8  
  4         970  
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   13885 my $pkg = shift;
14 6         17 my $caller = caller;
15 6         52 my %export = qw/sql_interp 1 sql_interp_strict 1 sql_type 1 sql 1/;
16 6         27 while(my $fn = shift) {
17 27 100       79 if($fn eq ':all') {
18 5         24 push @_, keys %export;
19 5         21 next;
20             }
21 22 50       49 croak "Symbol '$fn' is not exported by $pkg" if !$export{$fn};
22 22         45 my $as = $fn;
23 22 100       49 if(ref $_[0] eq 'HASH') {
24 2         10 my $arg = shift;
25 2 50       7 $as = $arg->{'-as'} if defined $arg->{'-as'};
26             }
27 4     4   27 no strict 'refs';
  4         10  
  4         13782  
28 22         29 *{$caller.'::'.$as} = *{$pkg.'::'.$fn};
  22         2297  
  22         64  
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 165039 bless {}, shift;
65             }
66              
67              
68             # note: sql_interp is not reentrant.
69             sub sql_interp {
70 164     164 1 52150 my @items = @_;
71              
72             # clear call state
73 164         207 $alias_id = 0;
74 164         163 $idx = 0;
75 164         340 $items_ref = undef;
76 164         187 $is_var_used = 0;
77 164         226 @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     971 shift @items if UNIVERSAL::isa($items[0], 'SQL::Interp') || UNIVERSAL::isa($items[0], 'DBI::db');
81              
82 164         230 $items_ref = \@items;
83              
84             # interpolate!
85 164         282 my $sql = _sql_interp(@items);
86              
87             # convert bind values to complex format (if needed)
88 159 100       267 if ($is_var_used) {
89 12         18 for my $val (@bind) {
90 36         49 my $valcopy = $val;
91 36 100       98 ! ref $val and $val = [$val, sql_type(\$valcopy)];
92             }
93             }
94              
95             $trace_sql_enabled
96 159 50       238 and print STDERR "DEBUG:interp[sql=$sql,bind="
97             . join(':', @bind) . "]\n";
98              
99 159         441 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 189896 my @items = @_;
107              
108 1         2 my $adjacent_string_cnt = 0;
109 1         2 for my $item (@items) {
110             # If we have a reference, reset the counter and move to the next element.
111 2 50       5 if (ref $item) {
112 0         0 $adjacent_string_cnt = 0;
113             }
114             else {
115 2         3 $adjacent_string_cnt++;
116 2 100       5 if ($adjacent_string_cnt == 2) {
117 1         143 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   356 my (@items) = @_;
130              
131 252         303 my $sql = '';
132              
133 252         358 foreach my $item (@items) {
134 446         407 my $varobj;
135 446         506 my $bind_size = @bind;
136 446 100       727 if (ref $item eq 'SQL::Interp::Variable') {
137 18 100 66     89 unless (keys %$item == 1 && defined($item->{value})) {
138 16         30 $varobj = $item;
139 16         19 $is_var_used = 1;
140             }
141 18         30 $item = $item->{value};
142             }
143              
144 446 100       769 if (ref $item eq 'SQL::Interp::SQL') {
    100          
145 50         119 my ($sql2, @bind2) = _sql_interp(@$item);
146 50 100       86 $sql .= ' ' if $sql ne '';
147 50         50 $sql .= $sql2;
148 50         59 push @bind, @bind2;
149             }
150             elsif (ref $item) {
151 191 100 66     3077 if ($sql =~ /\b(NOT\s+)?IN\s*$/si) {
    100 100        
    100          
    100          
    100          
    100          
    100          
    50          
152 30   100     180 my $not = quotemeta($1 || '');
153              
154 30 100       72 $item = [ $$item ] if ref $item eq 'SCALAR';
155              
156             # allow double references
157 30 100       46 $item = $$item if ref $item eq 'REF' ;
158              
159 30 50       52 if (ref $item eq 'ARRAY') {
160 30 100       48 if (@$item == 0) {
161 12 100       18 my $dummy_expr = $not ? '1=1' : '1=0';
162 12 50       388 $sql =~ s/$id_match\s+${not}IN\s*$/$dummy_expr/si or croak 'ASSERT';
163             }
164             else {
165             $sql .= " (" . join(', ', map {
166 18         33 _sql_interp_data($_);
  37         66  
167             } @$item) . ")";
168             }
169             }
170             else {
171 0         0 _error_item($idx, \@items);
172             }
173             }
174             elsif ($sql =~ /\bARRAY\s*$/si) {
175 8 100       19 $item = [ $$item ] if ref $item eq 'SCALAR';
176              
177             # allow double references
178 8 100       13 $item = $$item if ref $item eq 'REF' ;
179              
180 8 50       14 if (ref $item eq 'ARRAY') {
181             $sql .= '[' . join(', ', map {
182 8         40 _sql_interp_data($_);
  10         14  
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       12 _error('Hash has zero elements.') if keys %$item == 0;
191             $sql .= " " . join(', ', map {
192 4         19 my $key = $_;
  10         11  
193 10         13 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       47 $item = [ $$item ] if ref $item eq 'SCALAR';
200 26 100       48 if (ref $item eq 'ARRAY') {
    50          
201             $sql .= " VALUES(" . join(', ', map {
202 18         28 _sql_interp_data($_);
  24         38  
203             } @$item) . ")";
204             }
205             elsif (ref $item eq 'HASH') {
206 8         30 my @keyseq = sort keys %$item;
207             $sql .=
208             " (" . join(', ', @keyseq) . ")" .
209             " VALUES(" . join(', ', map {
210 8         22 _sql_interp_data($item->{$_});
  14         21  
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         44 my $table_alias = undef; # alias given to table
220 33         47 my $next_item = $items[$idx + 1];
221 33 100 66     83 if(defined $next_item && ref $next_item eq '' &&
      100        
222             $next_item =~ /\s*AS\b/is)
223             {
224 2         2 $table_alias = undef; # provided by client
225             }
226             else {
227 31         64 $table_alias = 'tbl' . $alias_id++;
228             }
229              
230 33 50       58 $sql .= ' ' unless $sql eq '';
231 33         76 $sql .= _sql_interp_resultset($item);
232 30 100       65 $sql .= " AS $table_alias" if defined $table_alias;
233             }
234             elsif (ref $item eq 'SCALAR') {
235 54         91 push @bind, $$item;
236 54         80 $sql .= ' ?';
237             }
238             elsif (ref $item eq 'HASH') { # e.g. WHERE {x = 3, y = 4}
239 22 100       49 if (keys %$item == 0) {
240 2         34 $sql .= ' 1=1';
241             }
242             else {
243             my $s = join ' AND ', map {
244 20         62 my $key = $_;
  34         40  
245 34         86 my $val = $item->{$key};
246 34 100       67 if (! defined $val) {
    100          
247 2         6 "$key IS NULL";
248             }
249             elsif (ref $val eq 'ARRAY') {
250 8         13 _sql_interp_list($key, $val);
251             }
252             else {
253 24         47 "$key=" .
254             _sql_interp_data($val);
255             }
256             } (sort keys %$item);
257 20 100       56 $s = "($s)" if keys %$item > 1;
258 20         27 $s = " $s";
259 20         32 $sql .= $s;
260             }
261             }
262             elsif (ref $item eq 'ARRAY') { # result set
263 14 50       21 $sql .= ' ' unless $sql eq '';
264 14         24 $sql .= _sql_interp_resultset($item);
265             }
266 0         0 else { _error_item($idx, \@items); }
267             }
268             else {
269 205 100 100     963 $sql .= ' ' unless $sql =~ /(^|\s)$/ || $item =~ /^\s/; # style
270 205         323 $sql .= $item;
271             }
272              
273             # attach $varobj to any bind values it generates
274 441 100       589 if ($varobj) {
275 16         47 my $num_pushed = @bind - $bind_size;
276 16         44 for my $val (@bind[-$num_pushed..-1]) {
277 20         48 $val = [$val, $varobj];
278             }
279             }
280 441         590 $idx++;
281             }
282              
283 247         445 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   265 my ($ele) = @_;
292 203 100       335 if (ref $ele) { # e.g. sql()
293 38         61 my ($sql2, @bind2) = _sql_interp($ele);
294 38         43 push @bind, @bind2;
295 38 50       62 $is_var_used = 1 if ref $bind2[0];
296 38         116 return $sql2;
297             }
298             else {
299 165         212 push @bind, $ele;
300 165         459 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   11 my ($key, $list) = @_;
308 8 100       36 if (@$list == 0) {
309 2         6 return "1=0";
310             }
311             else {
312 6         7 my @sqle;
313 6         8 for my $ele (@$list) {
314 12         10 my $sqle
315             = _sql_interp_data($ele);
316 12         17 push @sqle, $sqle;
317             }
318 6         14 my $sql2 = $key . " IN (" . join(', ', @sqle) . ")";
319 6         18 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   52 my($item) = @_;
326 47         48 my $sql = '';
327 47 50       61 if (ref $item eq 'ARRAY') {
328 47 100       102 _error("table reference has zero rows") # improve?
329             if @$item == 0;
330 46         51 my $sql2 = '';
331 46 100       73 if(ref $item->[0] eq 'ARRAY') {
    50          
332             _error("table reference has zero columns") # improve?
333 28 100       23 if @{ $item->[0] } == 0;
  28         53  
334 26         35 for my $row ( @$item ) {
335 30         34 my $is_first_row = ($sql2 eq '');
336 30 100       41 $sql2 .= ' UNION ALL ' unless $is_first_row;
337             $sql2 .=
338             "SELECT " .
339             join(', ', map {
340 30         39 _sql_interp_data($_)
  40         48  
341             } @$row);
342             }
343             }
344             elsif(ref $item->[0] eq 'HASH') {
345             _error("table reference has zero columns") # improve?
346 18 100       16 if keys %{ $item->[0] } == 0;
  18         41  
347 16         18 my $first_row = $item->[0];
348 16         17 for my $row ( @$item ) {
349 20         25 my $is_first_row = ($sql2 eq '');
350 20 100       27 $sql2 .= ' UNION ALL ' unless $is_first_row;
351             $sql2 .=
352             "SELECT " .
353             join(', ', map {
354 20         48 my($key, $val) = ($_, $row->{$_});
  32         45  
355 32         35 my $sql3 = _sql_interp_data($val);
356 32 100       47 $sql3 .= " AS $key" if $is_first_row;
357 32         71 $sql3;
358             } (sort keys %$first_row));
359             }
360             }
361             else {
362 0         0 _error_item($idx, $items_ref);
363             }
364 42 50       62 $sql .= ' ' unless $sql eq '';
365 42         78 $sql .= "($sql2)";
366             }
367 0         0 else { _error_item($idx, $items_ref); }
368 42         57 return $sql;
369             }
370              
371             sub sql {
372 23     23 1 2326 return SQL::Interp::SQL->new(@_);
373             }
374              
375             sub sql_type {
376 29     29 1 4304 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   746 croak "SQL::Interp error: $_[0]";
392             }
393              
394             1;
395              
396             package SQL::Interp::Variable;
397 4     4   36 use strict;
  4         11  
  4         143  
398 4     4   21 use Carp;
  4         7  
  4         734  
399              
400             sub new {
401 29     29   51 my ($class, $value, %params) = @_;
402 29 50       55 SQL::Interp::_error(
403             "Value '$value' in sql_type constructor is not a reference")
404             if ! ref $value;
405 29         68 my $self = bless {value => $value, %params}, $class;
406 29         124 return $self;
407             }
408              
409             1;
410              
411              
412             package SQL::Interp::SQL;
413 4     4   29 use strict;
  4         7  
  4         182  
414 4     4   31 use Carp;
  4         6  
  4         418  
415 4     4   2316 use overload '.' => \&concat, '""' => \&stringify;
  4         6480  
  4         41  
416              
417             sub new {
418 24     24   65 my ($class, @list) = @_;
419              
420 24         40 my $self = \@list;
421 24         35 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__