File Coverage

blib/lib/SQL/Interpol.pm
Criterion Covered Total %
statement 96 100 96.0
branch 60 68 88.2
condition 9 12 75.0
subroutine 13 14 92.8
pod 2 2 100.0
total 180 196 91.8


line stmt bran cond sub pod time code
1 1     1   45377 use 5.006;
  1         4  
2 1     1   6 use strict;
  1         2  
  1         23  
3 1     1   6 use warnings;
  1         2  
  1         55  
4              
5             package SQL::Interpol;
6             $SQL::Interpol::VERSION = '1.103';
7             # ABSTRACT: interpolate Perl variables into SQL statements
8              
9 1     1   1419 use Exporter::Tidy all => [ qw( sql_interp sql ) ];
  1         11  
  1         7  
10              
11 23     23 1 16994 sub sql { bless [ @_ ], __PACKAGE__ }
12              
13             sub sql_interp {
14 64     64 1 35534 my $p = SQL::Interpol::Parser->new;
15 64         470 my $sql = $p->parse( @_ );
16 58         1316 my $bind = $p->bind;
17 58         1032 return ( $sql, @$bind );
18             }
19              
20              
21             package SQL::Interpol::Parser;
22             $SQL::Interpol::Parser::VERSION = '1.103';
23 1     1   854 use Object::Tiny::Lvalue qw( alias_id bind );
  1         325  
  1         6  
24              
25 1     1   186 use Carp ();
  1         2  
  1         36  
26              
27             my $IDENT = '[a-zA-Z_][a-zA-Z0-9_\$\.]*';
28 1     1   5 use constant VALID => { ARRAY => 1, SCALAR => 1, 'SQL::Interpol' => 1, '' => 1 };
  1         8  
  1         1371  
29              
30 6     6   786 sub _error { Carp::croak 'SQL::Interpol error: ', @_ }
31              
32             sub new {
33 64     64   115 my $class = shift;
34 64         216 $class->SUPER::new( alias_id => 0, bind => [] );
35             }
36              
37             sub parse {
38 77     77   107 my $self = shift;
39              
40 77         110 my $sql = '';
41 77         1801 my $bind = $self->bind;
42              
43 77         276 my ( $item, $prev );
44             my $error = sub {
45 0 0   0   0 my $where = defined $prev ? " following '$prev'" : '';
46 0         0 _error "Unrecognized element '$item'$where";
47 77         299 };
48              
49 77         225 while ( @_ ) {
50 164         235 $item = shift @_;
51 164         245 my $type = ref $item;
52 164         162 my $append;
53              
54 164 100       333 if ( 'SQL::Interpol' eq $type ) {
55 23         50 unshift @_, @$item;
56 23         82 next;
57             }
58              
59 141 100       1041 if ( not $type ) {
    100          
    100          
    100          
    100          
    50          
60 73         118 $prev = $append = $item;
61             }
62             elsif ( $sql =~ s/(\s*$IDENT\s+(NOT\s+)?IN)\s*$//oi ) {
63             my @value
64 8 50 33     36 = 'SCALAR' eq $type ? $$item
    100          
    100          
65             : 'ARRAY' eq $type ? @$item
66             : 'REF' eq $type && 'ARRAY' eq ref $$item ? @$$item
67             : $error->();
68 8   66     27 my $list = @value && join ', ', $self->bind_or_parse_values( @value );
69 8 100       55 $append = @value ? "$1 ($list)" : $2 ? '1=1' : '1=0';
    100          
70             }
71             elsif ( $sql =~ /\b(REPLACE|INSERT)[\w\s]*\sINTO\s*$IDENT\s*$/oi ) {
72             my @value
73             = 'SCALAR' eq $type ? $$item
74             : 'ARRAY' eq $type ? @$item
75 10 50       38 : 'HASH' eq $type ? do {
    100          
    100          
76 3         17 my @key = sort keys %$item;
77 3         8 my $list = join ', ', @key;
78 3         6 $append = "($list) ";
79 3         10 @$item{ @key };
80             }
81             : $error->();
82 10 100       34 my $list = @value ? join ', ', $self->bind_or_parse_values( @value ) : '';
83 10         62 $append .= "VALUES($list)";
84             }
85             elsif ( 'SCALAR' eq $type ) {
86 14         21 push @$bind, $$item;
87 14         21 $append = '?';
88             }
89             elsif ( 'HASH' eq $type ) { # e.g. WHERE {x = 3, y = 4}
90 10 100       53 if ( $sql =~ /\b(?:ON\s+DUPLICATE\s+KEY\s+UPDATE|SET)\s*$/i ) {
    100          
91 2 50       8 _error 'Hash has zero elements.' if not keys %$item;
92 2         9 my @k = sort keys %$item;
93 2         8 my @v = $self->bind_or_parse_values( @$item{ @k } );
94 2         30 $append = join ', ', map "$k[$_]=$v[$_]", 0 .. $#k;
95             }
96             elsif ( not keys %$item ) {
97 1         3 $append = '1=1';
98             }
99             else {
100             my $cond = join ' AND ', map {
101 7         22 my $expr = $_;
  10         17  
102 10         15 my $eval = $item->{ $expr };
103             ( not defined $eval ) ? "$expr IS NULL"
104 5         35 : 'ARRAY' ne ref $eval ? map { "$expr=$_" } $self->bind_or_parse_values( $eval )
105 10 100       40 : do {
    100          
106 3 100       11 @$eval ? do {
107 2         6 my $list = join ', ', $self->bind_or_parse_values( @$eval );
108 2         13 "$expr IN ($list)";
109             } : '1=0';
110             }
111             } sort keys %$item;
112 6 100       23 $cond = "($cond)" if keys %$item > 1;
113 6         10 $append = $cond;
114             }
115             }
116             elsif ( 'ARRAY' eq $type ) { # result set
117 26 100       57 _error 'table reference has zero rows' if not @$item; # improve?
118              
119             # e.g. [[1,2],[3,4]] or [{a=>1,b=>2},{a=>3,b=>4}].
120 25   100     155 my $do_alias = $sql =~ /(?:\bFROM|JOIN)\s*$/i && ( $_[0] || '' ) !~ /\s*AS\b/i;
121              
122 25         40 my $row0 = $item->[0];
123 25         33 my $type0 = ref $row0;
124              
125 25 100       52 if ( 'ARRAY' eq $type0 ) {
    50          
126 15 100       34 _error 'table reference has zero columns' if not @$row0; # improve?
127             $append = join ' UNION ALL ', map {
128 13         22 'SELECT ' . join ', ', $self->bind_or_parse_values( @$_ );
  15         48  
129             } @$item;
130             }
131             elsif ( 'HASH' eq $type0 ) {
132 10 100       31 _error 'table reference has zero columns' if not keys %$row0; # improve?
133 8         28 my @k = sort keys %$row0;
134             $append = join ' UNION ALL ', do {
135 8         21 my @v = $self->bind_or_parse_values( @$row0{ @k } );
136 8         85 'SELECT ' . join ', ', map "$v[$_] AS $k[$_]", 0 .. $#k;
137             }, map {
138 8         12 'SELECT ' . join ', ', $self->bind_or_parse_values( @$_{ @k } );
  2         7  
139             } @$item[ 1 .. $#$item ];
140             }
141 0         0 else { $error->() }
142              
143 21         118 $append = "($append)";
144 21 100       340 $append .= ' AS tbl' . $self->alias_id++ if $do_alias;
145             }
146 0         0 else { $error->() }
147              
148 135 50       322 next if not defined $append;
149 135 100 100     587 $sql .= ' ' if $sql =~ /\S/ and $append !~ /\A\s/;
150 135         382 $sql .= $append;
151             }
152              
153 71         357 return $sql;
154             }
155              
156             # interpolate values from aggregate variable (hashref or arrayref)
157             sub bind_or_parse_values {
158 48     48   66 my $self = shift;
159             map {
160 48         73 my $type = ref;
  74         198  
161 74 100       191 _error "unrecognized $type value in aggregate" unless VALID->{ $type };
162 73 100       149 $type ? $self->parse( $_ ) : ( '?', push @{ $self->bind }, $_ )[0];
  60         1461  
163             } @_;
164             }
165              
166             undef *VALID;
167              
168             1;
169              
170             __END__