File Coverage

blib/lib/SQL/TwoWay.pm
Criterion Covered Total %
statement 95 103 92.2
branch 26 34 76.4
condition 6 9 66.6
subroutine 16 17 94.1
pod 0 5 0.0
total 143 168 85.1


line stmt bran cond sub pod time code
1             package SQL::TwoWay;
2 4     4   75723 use strict;
  4         11  
  4         180  
3 4     4   23 use warnings FATAL => 'recursion';
  4         7  
  4         171  
4 4     4   173 use 5.010001; # Named capture
  4         16  
  4         227  
5             our $VERSION = "0.05";
6 4     4   37 use Carp ();
  4         8  
  4         102  
7 4     4   21 use Scalar::Util qw(reftype);
  4         8  
  4         531  
8              
9 4     4   4151 use parent qw(Exporter);
  4         1356  
  4         19  
10              
11             our @EXPORT = qw(two_way_sql);
12              
13             our ($TOKEN_STR2ID, $TOKEN_ID2STR);
14             BEGIN {
15 4     4   443 $TOKEN_STR2ID = +{
16             VARIABLE => 1,
17             SQL => 2,
18             IF => 3,
19             ELSE => 4,
20             END_ => 5,
21             };
22 4         146 $TOKEN_ID2STR = +{ reverse %$TOKEN_STR2ID };
23             }
24 4     4   21 use constant $TOKEN_STR2ID;
  4         6  
  4         5152  
25              
26             sub token2str {
27 0     0 0 0 $TOKEN_ID2STR->{+shift}
28             }
29              
30             sub two_way_sql {
31 14     14 0 35203 my ($sql, $params) = @_;
32              
33 14         40 my $tokens = tokenize_two_way_sql($sql);
34 14         30 my $ast = parse_two_way_sql($tokens);
35 14         30 my ($generated_sql, @binds) = process_two_way_sql($ast, $params);
36 14         60 return ($generated_sql, @binds);
37             }
38              
39             sub process_two_way_sql {
40 18     18 0 22 my ($ast, $params) = @_;
41 18         18 my ($sql, @binds);
42 18         24 for my $node (@$ast) {
43 32 100       95 if ($node->[0] eq IF) {
    100          
    50          
44 4         7 my $name = $node->[1];
45 4 50       10 unless (exists $params->{$name}) {
46 0         0 Carp::croak("Unknown parameter for IF stmt: $name");
47             }
48 4 100       7 if ($params->{$name}) {
49 2         8 my ($is, @ib) = process_two_way_sql($node->[2], $params);
50 2         3 $sql .= $is;
51 2         4 push @binds, @ib;
52             } else {
53 2         6 my ($is, @ib) = process_two_way_sql($node->[3], $params);
54 2         3 $sql .= $is;
55 2         6 push @binds, @ib;
56             }
57             } elsif ($node->[0] eq VARIABLE) {
58 11         17 my $name = $node->[1];
59 11 50       27 unless (exists $params->{$name}) {
60 0         0 Carp::croak("Unknown parameter: $name");
61             }
62              
63 11 100       42 if (reftype($params->{$name}) eq 'ARRAY') {
64 2         4 $sql .= '('. join(',', ('?')x@{$params->{$name}}) .')';
  2         10  
65 2         3 push @binds, @{$params->{$name}};
  2         8  
66             } else {
67 9         9 $sql .= '?';
68 9         22 push @binds, $params->{$name};
69             }
70             } elsif ($node->[0] eq SQL) {
71 17         64 $sql .= $node->[1];
72             } else {
73 0         0 Carp::croak("Unknown node: " . token2str($node->[0]));
74             }
75             }
76 18         54 return ($sql, @binds);
77             }
78              
79             sub parse_two_way_sql {
80 18     18 0 36 my ($tokens) = @_;
81 18         22 my @ast;
82 18         45 while (@$tokens > 0) {
83 34         63 push @ast, _parse_stmt($tokens);
84             }
85 18         34 return \@ast;
86             }
87              
88             sub _parse_statements {
89 10     10   13 my ($tokens) = @_;
90              
91 10         11 my @stmts;
92 10   66     76 while (@$tokens && (
      33        
93             $tokens->[0]->[0] == SQL
94             || $tokens->[0]->[0] == VARIABLE
95             || $tokens->[0]->[0] == IF
96             )) {
97 15         32 push @stmts, _parse_stmt($tokens);
98             }
99 10         21 return \@stmts;
100             }
101              
102             sub _parse_stmt {
103 49     49   52 my ($tokens) = @_;
104              
105 49 100 100     194 if ($tokens->[0]->[0] eq SQL || $tokens->[0]->[0] eq VARIABLE) {
    50          
106 42         54 my $token = shift @$tokens;
107             return [
108 42         275 $token->[0],
109             $token->[1]
110             ];
111             } elsif ($tokens->[0]->[0] eq IF) {
112 7         16 return _parse_if_stmt($tokens);
113             } else {
114 0         0 Carp::croak("Unexpected token: " . token2str($tokens->[0]->[0]));
115             }
116             }
117              
118             sub _parse_if_stmt {
119 7     7   12 my ($tokens) = @_;
120              
121             # IF
122 7         9 my $if = shift @$tokens;
123              
124             # Parse statements
125 7         16 my $if_block = _parse_statements($tokens);
126              
127             # ELSE block
128 7         14 my $else_block = [];
129 7 100       18 if ($tokens->[0]->[0] eq ELSE) {
130 3         6 shift @$tokens; # remove ELSE
131 3         7 $else_block = _parse_statements($tokens);
132             }
133              
134             # And, there is END_
135 7 50       20 unless ($tokens->[0]->[0] eq END_) {
136 0         0 Carp::croak("Unexpected EOF in IF statement");
137             }
138 7         8 shift @$tokens; # remove END_
139              
140             return [
141 7         36 IF, $if->[1], $if_block, $else_block
142             ];
143             }
144              
145             sub tokenize_two_way_sql {
146 25     25 0 3850 my $sql = shift;
147              
148 25         33 my @ret;
149 25         43 my $NUMERIC_LITERAL = "-? [0-9.]+";
150 25         31 my $STRING_LITERAL = q{ (?:
151             "
152             (?:
153             \\\\"
154             | ""
155             | [^"]
156             )*
157             "
158             |
159             '
160             (?:
161             \\\\'
162             | ''
163             | [^']
164             )*
165             '
166             ) };
167 25         80 my $LITERAL = "(?: $STRING_LITERAL | $NUMERIC_LITERAL )";
168 25         34 my $SINGLE_SLASH = '/ (?! \*)';
169 25         1012 $sql =~ s!
170             # Variable /* $var */3
171             (
172             /\* \s+ \$ (? [A-Za-z0-9_-]+) \s+ \*/
173             (?:
174             # (3,2,4)
175             $LITERAL | \(
176             (?: \s* $LITERAL \s* , \s* )*
177             $LITERAL
178             \)
179             )
180             )
181             |
182             (?:
183             /\* \s+ IF \s+ \$ (? [A-Za-z0-9_-]+) \s+ \*/
184             )
185             |
186             (?
187             /\* \s+ ELSE \s+ \*/
188             )
189             |
190             (?
191             /\* \s+ END \s+ \*/
192             )
193             |
194             # Normal SQL strings
195             (? [^/]+ )
196             |
197             # Single slash character
198             (? $SINGLE_SLASH )
199             !
200 4 100   4   3906 if (defined $+{variable}) {
  4 100       2645  
  4 100       1062  
  87 100       1022  
    50          
    0          
201 20         98 push @ret, [VARIABLE, $+{variable}]
202             } elsif (defined $+{ifcond}) {
203 12         63 push @ret, [IF, $+{ifcond}]
204             } elsif (defined $+{else}) {
205 4         13 push @ret, [ELSE]
206             } elsif (defined $+{end}) {
207 12         31 push @ret, [END_]
208             } elsif (defined $+{sql1}) {
209 39         211 push @ret, [SQL, $+{sql1}]
210             } elsif (defined $+{sql2}) {
211 0         0 push @ret, [SQL, $+{sql2}]
212             } else {
213 0         0 Carp::croak("Invalid sql: $sql");
214             }
215 87         491 ''
216             !gex;
217              
218 25         143 return \@ret;
219             }
220              
221             1;
222             __END__