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   123001 use 5.006; use strict; use warnings;
  1     1   3  
  1     1   4  
  1         2  
  1         16  
  1         4  
  1         2  
  1         41  
2              
3             package SQL::Interpol;
4              
5             our $VERSION = '1.104';
6              
7 1     1   862 use Exporter::Tidy all => [ qw( sql_interp sql ) ];
  1         13  
  1         5  
8              
9 23     23 1 16793 sub sql { bless [ @_ ], __PACKAGE__ }
10              
11             sub sql_interp {
12 64     64 1 34817 my $p = SQL::Interpol::Parser->new;
13 64         377 my $sql = $p->parse( @_ );
14 58         910 my $bind = $p->bind;
15 58         737 return ( $sql, @$bind );
16             }
17              
18              
19             package SQL::Interpol::Parser;
20              
21             our $VERSION = '1.104';
22              
23 1     1   453 use Object::Tiny::Lvalue qw( alias_id bind );
  1         322  
  1         3  
24              
25 1     1   174 use Carp ();
  1         2  
  1         27  
26              
27             my $IDENT = '[a-zA-Z_][a-zA-Z0-9_\$\.]*';
28 1     1   4 use constant VALID => { ARRAY => 1, SCALAR => 1, 'SQL::Interpol' => 1, '' => 1 };
  1         1  
  1         1039  
29              
30 6     6   556 sub _error { Carp::croak 'SQL::Interpol error: ', @_ }
31              
32             sub new {
33 64     64   97 my $class = shift;
34 64         165 $class->SUPER::new( alias_id => 0, bind => [] );
35             }
36              
37             sub parse {
38 77     77   89 my $self = shift;
39              
40 77         106 my $sql = '';
41 77         1160 my $bind = $self->bind;
42              
43 77         268 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         277 };
48              
49 77         168 while ( @_ ) {
50 164         198 $item = shift @_;
51 164         212 my $type = ref $item;
52 164         161 my $append;
53              
54 164 100       291 if ( 'SQL::Interpol' eq $type ) {
55 23         54 unshift @_, @$item;
56 23         48 next;
57             }
58              
59 141 100       724 if ( not $type ) {
    100          
    100          
    100          
    100          
    50          
60 73         91 $prev = $append = $item;
61             }
62             elsif ( $sql =~ s/(\s*$IDENT\s+(NOT\s+)?IN)\s*$//oi ) {
63             my @value
64 8 50 33     27 = '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     24 my $list = @value && join ', ', $self->bind_or_parse_values( @value );
69 8 100       47 $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       32 : 'HASH' eq $type ? do {
    100          
    100          
76 3         14 my @key = sort keys %$item;
77 3         6 my $list = join ', ', @key;
78 3         6 $append = "($list) ";
79 3         6 @$item{ @key };
80             }
81             : $error->();
82 10 100       25 my $list = @value ? join ', ', $self->bind_or_parse_values( @value ) : '';
83 10         46 $append .= "VALUES($list)";
84             }
85             elsif ( 'SCALAR' eq $type ) {
86 14         20 push @$bind, $$item;
87 14         16 $append = '?';
88             }
89             elsif ( 'HASH' eq $type ) { # e.g. WHERE {x = 3, y = 4}
90 10 100       42 if ( $sql =~ /\b(?:ON\s+DUPLICATE\s+KEY\s+UPDATE|SET)\s*$/i ) {
    100          
91 2 50       6 _error 'Hash has zero elements.' if not keys %$item;
92 2         9 my @k = sort keys %$item;
93 2         7 my @v = $self->bind_or_parse_values( @$item{ @k } );
94 2         24 $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         23 my $expr = $_;
  10         13  
102 10         13 my $eval = $item->{ $expr };
103             ( not defined $eval ) ? "$expr IS NULL"
104 5         30 : 'ARRAY' ne ref $eval ? map { "$expr=$_" } $self->bind_or_parse_values( $eval )
105 10 100       27 : do {
    100          
106 3 100       10 @$eval ? do {
107 2         6 my $list = join ', ', $self->bind_or_parse_values( @$eval );
108 2         12 "$expr IN ($list)";
109             } : '1=0';
110             }
111             } sort keys %$item;
112 6 100       17 $cond = "($cond)" if keys %$item > 1;
113 6         10 $append = $cond;
114             }
115             }
116             elsif ( 'ARRAY' eq $type ) { # result set
117 26 100       53 _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     112 my $do_alias = $sql =~ /(?:\bFROM|JOIN)\s*$/i && ( $_[0] || '' ) !~ /\s*AS\b/i;
121              
122 25         37 my $row0 = $item->[0];
123 25         34 my $type0 = ref $row0;
124              
125 25 100       42 if ( 'ARRAY' eq $type0 ) {
    50          
126 15 100       27 _error 'table reference has zero columns' if not @$row0; # improve?
127             $append = join ' UNION ALL ', map {
128 13         20 'SELECT ' . join ', ', $self->bind_or_parse_values( @$_ );
  15         40  
129             } @$item;
130             }
131             elsif ( 'HASH' eq $type0 ) {
132 10 100       26 _error 'table reference has zero columns' if not keys %$row0; # improve?
133 8         23 my @k = sort keys %$row0;
134             $append = join ' UNION ALL ', do {
135 8         17 my @v = $self->bind_or_parse_values( @$row0{ @k } );
136 8         73 'SELECT ' . join ', ', map "$v[$_] AS $k[$_]", 0 .. $#k;
137             }, map {
138 8         15 'SELECT ' . join ', ', $self->bind_or_parse_values( @$_{ @k } );
  2         9  
139             } @$item[ 1 .. $#$item ];
140             }
141 0         0 else { $error->() }
142              
143 21         99 $append = "($append)";
144 21 100       235 $append .= ' AS tbl' . $self->alias_id++ if $do_alias;
145             }
146 0         0 else { $error->() }
147              
148 135 50       265 next if not defined $append;
149 135 100 100     412 $sql .= ' ' if $sql =~ /\S/ and $append !~ /\A\s/;
150 135         285 $sql .= $append;
151             }
152              
153 71         288 return $sql;
154             }
155              
156             # interpolate values from aggregate variable (hashref or arrayref)
157             sub bind_or_parse_values {
158 48     48   58 my $self = shift;
159             map {
160 48         74 my $type = ref;
  74         184  
161 74 100       124 _error "unrecognized $type value in aggregate" unless VALID->{ $type };
162 73 100       133 $type ? $self->parse( $_ ) : ( '?', push @{ $self->bind }, $_ )[0];
  60         911  
163             } @_;
164             }
165              
166             undef *VALID;
167              
168             1;
169              
170             __END__