File Coverage

blib/lib/SQL/Interpolate.pm
Criterion Covered Total %
statement 192 205 93.6
branch 77 102 75.4
condition 9 12 75.0
subroutine 29 32 90.6
pod 5 6 83.3
total 312 357 87.3


line stmt bran cond sub pod time code
1             package SQL::Interpolate;
2              
3             our $VERSION = '0.32';
4              
5 4     4   51366 use strict;
  4         9  
  4         163  
6 4     4   21 use warnings;
  4         8  
  4         137  
7 4     4   22 use Carp;
  4         17  
  4         345  
8 4     4   22 use base 'Exporter';
  4         6  
  4         10937  
9              
10             our @EXPORT;
11             our %EXPORT_TAGS = (all => [qw(
12             make_sql_interp
13             sql_interp
14             sql_var
15             sql
16             sql_literal
17             )]); # note: sql_literal depreciated
18             our @EXPORT_OK = @{ $EXPORT_TAGS{all} };
19              
20             # whether TRACE_SQL is enabled
21             my $trace_sql_enabled = 0;
22              
23             # whether TRACE_FILTER is enabled
24             my $trace_filter_enabled = 0;
25              
26             # whether macros are enabled
27             my $macros_enabled = 0;
28              
29             sub import {
30 11     11   83443 my $class = shift;
31 11         28 my @params = @_;
32              
33             # process any special "use" parameters
34 11         19 my $filter_enabled = 0; # whether filtering enabled
35 11         18 my $is_wrapped = 0; # whether module wrapped
36             # (e.g. by DBIx::Interpolate)
37             my %action_for = (
38 2     2   19 FILTER => sub { $filter_enabled = shift @params; },
39 1     1   2 TRACE_SQL => sub { $trace_sql_enabled = shift @params;
40 1 50       5 print STDERR "TRACE_SQL enabled\n"
41             if $trace_sql_enabled; },
42 1     1   2 TRACE_FILTER => sub { $trace_filter_enabled = shift @params;
43 1 50       7 print STDERR "TRACE_FILTER enabled\n"
44             if $trace_filter_enabled; },
45 0     0   0 __WRAP => sub { $is_wrapped = shift @params; }
46 11         170 );
47 11         33 @_ = ($class); # unprocessed params
48 11         46 while (my $item = shift @params) {
49 11         22 my $action = $action_for{$item};
50 11 100       27 if ($action) { $action->(); }
  4         11  
51 7         30 else { push @_, $item; }
52             }
53              
54             # handle exports
55 11 50       29 my $level = $is_wrapped ? 2 : 1;
56 11         1246 __PACKAGE__->export_to_level($level, @_);
57              
58             # handle source filtering (if enabled)
59 11 100       39 if ($filter_enabled) {
60 1         1282 require SQL::Interpolate::Filter;
61 1         22 goto &SQL::Interpolate::Filter::import; # @_
62             }
63              
64 10         373 return;
65             }
66              
67             sub new {
68 2     2 1 16 my $class = shift;
69              
70             # process special params.
71 2         5 my $dbh;
72 2         6 my $filters = [];
73 2         12 while (ref $_[0] ne '') {
74 1 50       13 if (UNIVERSAL::isa($_[0], 'DBI::db')) {
    50          
75 0         0 $dbh = shift;
76             }
77             elsif (UNIVERSAL::isa($_[0], 'SQL::Interpolate::SQLFilter')) {
78 1         5 push @$filters, shift;
79             }
80             }
81 2         8 my %params = @_;
82              
83             # build indicies on $filters for quick access
84 2         5 my $filters_hash = {}; # filter class name --> [filter]
85 2         4 my $text_filters = []; # filter
86 2         7 my $inits = []; # filter
87 2         5 my $text_fragment_filters = []; # filter
88 2         40 for my $filter (@$filters) {
89 1         2 push @{$filters_hash->{ref $filter}}, $filter;
  1         5  
90 1 50       15 push @$text_filters, $filter
91             if $filter->can("filter_text");
92 1 50       5 push @$inits, $filter
93             if $filter->can("init");
94 1 50       6 push @$text_fragment_filters, $filter
95             if $filter->can("filter_text_fragment");
96             }
97              
98             # build object
99 2         20 my $self = bless {
100             dbh => $dbh,
101             filters => $filters,
102             filters_hash => $filters_hash,
103             text_filters => $text_filters,
104             inits => $inits,
105             text_fragment_filters => $text_fragment_filters
106             }, $class;
107 2         8 return $self;
108             }
109              
110             sub sql_interp {
111 238     238 1 50309 my @items = @_;
112              
113             # extract state item (if any)
114 238         303 my $state;
115             my $interp;
116 238 100       1426 if (UNIVERSAL::isa($items[0], 'SQL::Interpolate')) {
    50          
117 126         206 $state = $interp = $items[0];
118             }
119             elsif (UNIVERSAL::isa($items[0], 'DBI::db')) {
120 0         0 $state = $items[0];
121             }
122              
123             # process macros (if enabled)
124 238 50       405 if ($macros_enabled) {
125 238 100       532 if ($interp) {
126 126         148 for my $initer (@{$interp->{inits}}) { $initer->init(); }
  126         333  
  16         47  
127             }
128 238         829 @items = SQL::Interpolate::Macro::sql_flatten(@items);
129             }
130             else {
131 0 0       0 shift @items if $state;
132             }
133              
134             # interpolate!
135 238         420 my $varobj_used = 0; # whether typed sql_var() ever used (if so,
136             # format of @bind result is more complicated)
137 238         616 my ($sql, @bind) = _sql_interp($state, \$varobj_used, @items);
138              
139             # convert bind values to complex format (if needed)
140 236 100       559 if ($varobj_used) {
141 36         60 for my $val (@bind) {
142 64         76 my $valcopy = $val;
143 64 100       174 ! ref $val and $val = [$val, sql_var(\$valcopy)];
144             }
145             }
146              
147             # process text filters (if any)
148 236 100       487 if ($interp) {
149 126         145 for my $text_filter (@{$interp->{text_filters}}) {
  126         300  
150 16         45 $sql = $text_filter->filter_text($sql);
151             }
152             }
153              
154             $trace_sql_enabled
155 236 50       520 and print STDERR "DEBUG:interp[sql=$sql,bind="
156             . join(':', @bind) . "]\n";
157              
158 236         947 return ($sql, @bind);
159             }
160              
161             # helper called by sql_interp()
162             # $state - SQL::Interpolate derived object, DBI handle, or undef
163             # $varobj_used_ref - reference to Boolean indicator of complex
164             # bind format [out]
165             # @items - interpolation list (no macros)
166             sub _sql_interp {
167 238     238   431 my ($state, $varobj_used_ref, @items) = @_;
168              
169 238         302 my $sql = '';
170 238         248 my @bind;
171 238         777 my $id_match = qr/[a-zA-Z_\.]+/;
172 238         334 my $idx = 0;
173              
174 238         387 foreach my $item (@items) {
175 522         503 my $varobj;
176 522         627 my $bind_size = @bind;
177 522 100       1085 if (ref $item eq 'SQL::Interpolate::Variable') {
178 24 100 66     84 unless (keys %$item == 1 && defined($item->{value})) {
179 20         26 $varobj = $item;
180 20         28 $$varobj_used_ref = 1;
181             }
182 24         42 $item = $item->{value};
183             }
184              
185 522 50       1162 if (ref $item eq 'SQL::Interpolate::SQL') {
    100          
186 0         0 my ($sql2, @bind2) = _sql_interp($state, $varobj_used_ref, @$item);
187 0         0 $sql .= " $sql2";
188 0         0 push @bind, @bind2;
189             }
190             elsif (ref $item) {
191 198 100 66     2065 if ($sql =~ /\bIN\s*$/si) {
    100          
    100          
    100          
    100          
192 25 100       62 $item = [ $$item ] if ref $item eq 'SCALAR';
193 25 100       59 if (ref $item eq 'ARRAY') {
194 24 100       38 if (@$item == 0) {
195 8 50       102 $sql =~ s/$id_match\s+IN\s*$/1=0/si or croak 'ASSERT';
196             }
197             else {
198 28         66 $sql .= " (" . join(', ', map {
199 16         24 _sql_interp_data($state, \@bind,
200             $varobj_used_ref, $_);
201             } @$item) . ")";
202             }
203             }
204             else {
205 1         4 _error_item($idx, \@items);
206             }
207             }
208             elsif ($sql =~ /\bSET\s*$/si && ref $item eq 'HASH') {
209 8 50       20 _error('Hash has zero elements.') if keys %$item == 0;
210 20         134 $sql .= " " . join(', ', map {
211 8         19 my $key = $_;
212 20         32 my $val = $item->{$key};
213 20         45 "$key=" .
214             _sql_interp_data($state, \@bind,
215             $varobj_used_ref, $val);
216             } keys %$item);
217             }
218             elsif ($sql =~ /\bINSERT[\w\s]*\sINTO\s*$id_match\s*$/si)
219             {
220 40 100       105 $item = [ $$item ] if ref $item eq 'SCALAR';
221 40 100       92 if (ref $item eq 'ARRAY') {
    50          
222 36         82 $sql .= " VALUES(" . join(', ', map {
223 24         53 _sql_interp_data($state, \@bind,
224             $varobj_used_ref, $_);
225             } @$item) . ")";
226             }
227             elsif (ref $item eq 'HASH') {
228 28         60 $sql .=
229             " (" . join(', ', keys %$item) . ")" .
230             " VALUES(" . join(', ', map {
231 16         70 _sql_interp_data($state, \@bind,
232             $varobj_used_ref, $_);
233             } values %$item) . ")";
234             }
235 0         0 else { _error_item($idx, \@items); }
236             }
237             elsif (ref $item eq 'SCALAR') {
238 88         154 push @bind, $$item;
239 88         147 $sql .= ' ?';
240             }
241             elsif (ref $item eq 'HASH') { # e.g. WHERE {x = 3, y = 4}
242 36 100       88 if (keys %$item == 0) {
243 4         8 $sql .= ' 1=1';
244             }
245             else {
246 52         76 my $s = join ' AND ', map {
247 32         80 my $key = $_;
248 52         73 my $val = $item->{$key};
249 52 100       108 if (ref $val eq 'ARRAY') {
250 16         46 _in_list($state, \@bind, $varobj_used_ref,
251             $key, $val);
252             }
253             else {
254 36         92 "$key=" .
255             _sql_interp_data($state, \@bind,
256             $varobj_used_ref, $val);
257             }
258             } keys %$item;
259 32 100       112 $s = "($s)" if keys %$item > 1;
260 32         52 $s = " $s";
261 32         75 $sql .= $s;
262             }
263             }
264 1         28 else { _error_item($idx, \@items); }
265             }
266             else {
267 324 100 100     1474 $sql .= ' ' unless $sql =~ /(^|\s)$/ || $item =~ /^\s/; # style
268 324         553 $sql .= $item;
269             }
270              
271             # attach $varobj to any bind values it generates
272 520 100       930 if ($varobj) {
273 20         31 my $num_pushed = @bind - $bind_size;
274 20         55 for my $val (@bind[-$num_pushed..-1]) {
275 20         71 $val = [$val, $varobj];
276             }
277             }
278 520         1036 $idx++;
279             }
280              
281 236         1026 return ($sql, @bind);
282             }
283              
284             # sql_interp helper function.
285             # Interpolate data element in aggregate variable (hashref or arrayref).
286             # $state - (may be undef)
287             # $bindref - \@bind (is modified--appended to)
288             # $varobj_usedref - \$varobj_used (is modified)
289             # $ele - raw input element from aggregate.
290             # returns $sql
291             sub _sql_interp_data {
292 172     172   288 my ($state, $bindref, $varobj_usedref, $ele) = @_;
293 172 100       319 if (ref $ele) {
294 56   66     243 my ($sql2, @bind2) = sql_interp($state || (), $ele);
295 56         129 push @$bindref, @bind2;
296 56 100       117 $$varobj_usedref = 1 if ref $bind2[0];
297 56         270 return $sql2;
298             }
299             else {
300 116         187 push @$bindref, $ele;
301 116         441 return '?';
302             }
303             }
304              
305             # sql_interp helper function to interpolate "key IN list",
306             # assuming context ("WHERE", {key => $list, ...}).
307             sub _in_list {
308 16     16   30 my ($state, $bindref, $varobj_usedref, $key, $list) = @_;
309 16 100       29 if (@$list == 0) {
310 4         19 return "1=0";
311             }
312             else {
313 12         15 my @sqle;
314 12         24 for my $ele (@$list) {
315 24         64 my $sqle
316             = _sql_interp_data($state, $bindref, $varobj_usedref, $ele);
317 24         54 push @sqle, $sqle;
318             }
319 12         39 my $sql2 = $key . " IN (" . join(', ', @sqle) . ")";
320 12         49 return $sql2;
321             }
322             }
323              
324             sub sql {
325 22     22 1 3591 return SQL::Interpolate::SQL->new(@_);
326             }
327              
328             sub make_sql_interp {
329 2     2 1 13 my (@params) = @_;
330             my $interp = sub {
331 82     82   77324 return sql_interp(@params, @_);
332 2         10 };
333 2         7 return $interp;
334             }
335              
336             sub sql_var {
337 38     38 1 1711 return SQL::Interpolate::Variable->new(@_);
338             }
339              
340             # helper function to throw error
341             sub _error_item {
342 2     2   5 my ($idx, $items_ref) = @_;
343 2 50       8 my $prev = $idx > 0 ? $items_ref->[$idx-1] : undef;
344 2 50       8 my $prev_text = defined($prev) ? " following '$prev'" : "";
345 2         4 my $cur = $items_ref->[$idx];
346 2         12 _error("SQL::Interpolate error: Unrecognized "
347             . "'$cur'$prev_text in interpolation list.");
348 0         0 return;
349             }
350              
351             sub _error {
352 2     2   413 croak "SQL::Interpolate error: $_[0]";
353             }
354              
355             # This shall only be called by SQL::Interpolate::Macro.
356             sub _enable_macros {
357 4 50   4   25 scalar(caller()) eq 'SQL::Interpolate::Macro' or die 'ASSERT';
358 4         7 $macros_enabled = 1; # enable macros
359 4         4882 return;
360             }
361              
362             # This shall only be called by DBIx::Interpolate.
363             sub _use_params {
364 0 0   0   0 scalar(caller()) eq 'DBIx::Interpolate' or die 'ASSERT';
365              
366             # supported use parameters.
367 0         0 return qw(FILTER TRACE_SQL TRACE_FILTER);
368             }
369              
370             # depreciated
371             sub sql_literal {
372 0     0 0 0 print STDERR
373             "SQL::Interpolate - WARNING: "
374             . "sql_literal() is depreciated. use sql() instead.\n";
375 0         0 return sql(@_);
376             }
377              
378             1;
379              
380             package SQL::Interpolate::Variable;
381 4     4   39 use strict;
  4         9  
  4         194  
382 4     4   24 use Carp;
  4         8  
  4         629  
383              
384             sub new {
385 38     38   74 my ($class, $value, %params) = @_;
386 38 50       82 SQL::Interpolate::_error(
387             "Value '$value' in sql_var constructor is not a reference")
388             if ! ref $value;
389 38         142 my $self = bless {value => $value, %params}, $class;
390 38         149 return $self;
391             }
392              
393             1;
394              
395              
396             package SQL::Interpolate::SQL;
397 4     4   20 use strict;
  4         9  
  4         118  
398 4     4   19 use Carp;
  4         7  
  4         349  
399 4     4   3636 use overload '.' => \&concat, '""' => \&stringify;
  4         2693  
  4         42  
400              
401             sub new {
402 60     60   135 my ($class, @list) = @_;
403              
404 60         93 my $self = \@list;
405 60         151 bless $self, $class;
406 60         294 return $self;
407             }
408              
409             # Concatenate SQL object with another expression.
410             # An SQL object can be concatenated with another SQL object,
411             # variable reference, or an SQL string.
412             # This is particularly useful to SQL::Interpolate::Filter.
413             sub concat {
414 3     3   7 my ($a, $b, $inverted) = @_;
415              
416 3 50       12 my @params = ( @$a, ref $b eq __PACKAGE__ ? @$b : $b );
417 3 50       9 @params = reverse @params if $inverted;
418 3         11 my $o = SQL::Interpolate::SQL->new(@params);
419 3         13 return $o;
420             }
421              
422             sub stringify {
423 77     77   157 my ($a) = @_;
424 77         427 return $a;
425             }
426              
427             1;
428              
429             __END__