File Coverage

blib/lib/SQL/Interp.pm
Criterion Covered Total %
statement 190 212 89.6
branch 101 126 80.1
condition 14 17 82.3
subroutine 21 24 87.5
pod 3 5 60.0
total 329 384 85.6


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