File Coverage

blib/lib/SQL/Interpol.pm
Criterion Covered Total %
statement 98 102 96.0
branch 60 68 88.2
condition 9 12 75.0
subroutine 13 14 92.8
pod 2 2 100.0
total 182 198 91.9


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