File Coverage

blib/lib/Parse/SQL.pm
Criterion Covered Total %
statement 114 140 81.4
branch 22 30 73.3
condition 0 6 0.0
subroutine 46 51 90.2
pod 15 15 100.0
total 197 242 81.4


line stmt bran cond sub pod time code
1             package Parse::SQL;
2             # ABSTRACT: Parse SQL statements using Parser::MGC
3 2     2   20197 use strict;
  2         4  
  2         78  
4 2     2   10 use warnings FATAL => 'all';
  2         4  
  2         93  
5 2     2   1831 use parent qw(Parser::MGC);
  2         649  
  2         13  
6              
7             our $VERSION = '0.002';
8              
9 2     2   44470 use constant DEBUG => 0;
  2         5  
  2         130  
10              
11 2         3565 use constant KEYWORDS => (
12             'select', 'insert', 'update', 'truncate', 'delete', 'from', 'where', 'group',
13             'by', 'limit', 'not', 'in', 'like', 'order', 'having', 'create', 'table',
14             'drop'
15 2     2   9 );
  2         4  
16              
17             =head1 NAME
18              
19             Parse::SQL - simple SQL parser
20              
21             =head1 VERSION
22              
23             version 0.002
24              
25             =head1 SYNOPSIS
26              
27             use strict;
28             use warnings;
29             use Parse::SQL;
30             use Data::Dumper;
31             my $parser = Parse::SQL->new;
32             print Dumper($parser->from_string('select id from tbl x inner join tbl y on x.idx = y.idx where x.idx < 14'));
33              
34             =head1 DESCRIPTION
35              
36             Warning: This is a preview release, and the entire API is subject to change several times over the next
37             few releases.
38              
39             Please check the L section before reading any further!
40              
41             =head1 METHODS
42              
43             =cut
44              
45             =head2 parse
46              
47             Main parsing method.
48              
49             =cut
50              
51             sub parse {
52 11     11 1 15218 my $self = shift;
53              
54 11     11   76 $self->list_of(';', sub { $self->parse_statement });
  11         554  
55             }
56              
57             =head2 parse_select
58              
59             Specific handling for C
60              
61             =cut
62              
63             sub parse_select {
64 11     11 1 15 my $self = shift;
65 11         22 $self->where_am_i;
66 11         33 $self->token_kw('select');
67             }
68              
69             =head2 token_lvalue
70              
71             Parse an 'lvalue'.
72              
73             =cut
74              
75             sub token_lvalue {
76 12     12 1 14 my $self = shift;
77 12         15 $self->where_am_i;
78             $self->any_of(
79             sub {
80 12 100   12   99 join '', @{ $self->maybe(sub { [$self->token_ident, $self->expect('.')] }) || [] }, $self->token_ident;
  12         47  
  12         98  
81             },
82 0     0   0 sub { $self->token_int },
83 0     0   0 sub { $self->token_float },
84 0     0   0 sub { $self->token_string },
85 12         88 );
86             }
87              
88             =head2 token_rvalue
89              
90             Parse an 'rvalue'.
91              
92             =cut
93              
94             sub token_rvalue {
95 12     12 1 547 my $self = shift;
96 12         18 $self->where_am_i;
97             $self->any_of(
98             sub {
99 12 100   12   120 join '', @{ $self->maybe(sub { [$self->token_ident, $self->expect('.')] }) || [] }, $self->token_ident;
  12         40  
  12         103  
100             },
101 5     5   662 sub { $self->token_int },
102 5     5   540 sub { $self->token_float },
103 5     5   523 sub { $self->token_string },
104 12         86 );
105             }
106              
107             =head2 token_operator
108              
109             Parse binary operators.
110              
111             =cut
112              
113             sub token_operator {
114 12     12 1 1807 my $self = shift;
115 12         21 $self->where_am_i;
116             $self->any_of(
117 12     12   119 sub { $self->expect('=') },
118 5     5   314 sub { $self->expect('!=') },
119 5     5   307 sub { $self->expect('<=') },
120 5     5   309 sub { $self->expect('>=') },
121 5     5   322 sub { $self->expect('<') },
122 5     5   302 sub { $self->expect('>') },
123 5     5   284 sub { $self->expect('<>') },
124 5     5   301 sub { $self->expect('is') },
125 5     5   324 sub { $self->expect('in') },
126 5     5   277 sub { $self->expect('like') },
127 12         136 );
128             }
129              
130             {
131             my $ANY_KEYWORD_RE = qr(@{[ join '|', KEYWORDS ]});
132              
133             =head2 check_keyword
134              
135             Match a keyword.
136              
137             =cut
138              
139             sub check_keyword {
140 18     18 1 15 my $self = shift;
141 18         24 $self->where_am_i;
142 18         34 $self->skip_ws;
143 18 100       336 if($self->{str} =~ m/\G($ANY_KEYWORD_RE)/gc) {
144 2         11 $self->fail( "Had keyword $1" );
145 0         0 return 1;
146             }
147 16         40 return 0;
148             }
149             }
150              
151             =head2 token_alias
152              
153             =cut
154              
155             sub token_alias {
156 18     18 1 19 my $self = shift;
157 18         29 $self->where_am_i;
158 18 50       29 return if $self->check_keyword;
159 16         40 $self->token_ident;
160             }
161              
162             =head2 parse_join
163              
164             =cut
165              
166             sub parse_join {
167 11     11 1 14 my $self = shift;
168 11         20 $self->where_am_i;
169             $self->sequence_of(sub {
170 14     14   1624 $self->skip_ws;
171 14         156 $self->where_am_i('join seq');
172             [
173 9 50       1054 @{ $self->maybe(sub {
174 14         116 $self->where_am_i('find join kw');
175             $self->any_of(
176 14         127 sub { [ $self->expect('full'), $self->expect('outer') ] },
177 12         761 sub { [ $self->token_kw(qw(inner left right full cross hash)) ] },
178 5         458 sub { [ $self->expect('left'), $self->expect('outer') ] },
179 5         326 sub { [ $self->expect('right'), $self->expect('outer') ] },
180             )
181 14 100       59 }) || [] },
  14         89  
182             $self->expect('join'),
183             @{ $self->parse_table_or_query || [] },
184 14         15 @{ $self->maybe(sub {
185             [ $self->expect('on'),
186             $self->any_of(
187             sub {
188 7         391 $self->where_am_i;
189             [
190 7         15 $self->token_lvalue,
191             $self->token_operator,
192             $self->token_rvalue
193             ]
194             },
195             sub {
196 0         0 $self->where_am_i;
197             [
198 0         0 $self->token_rvalue
199             ]
200             }
201 9         87 ) ]
202 9 100       470 }) || [] } ]
203 11         54 });
204             }
205              
206             =head2 parse_statement
207              
208             =cut
209              
210             sub parse_statement {
211 11     11 1 18 my $self = shift;
212 11         24 $self->where_am_i;
213              
214             return [
215             # Query type
216             $self->any_of(
217 11     11   115 sub { $self->parse_select }
218             ),
219             $self->parse_fields,
220             @{ $self->maybe(sub {
221 11     11   111 $self->parse_from;
222 11 100       1182 }) || []},
223             @{ $self->maybe(sub {
224 11     11   151 $self->parse_join;
225 11 50       733 }) || []},
226 11         54 @{ $self->maybe(sub {
227 11     11   101 $self->parse_where;
228 11 100       1009 }) || []},
229             ];
230             }
231              
232             =head2 token_keyword
233              
234             =cut
235              
236             sub token_keyword {
237 0     0 1 0 my $self = shift;
238 0     0   0 $self->any_of(map { my $k = $_; sub { $self->expect($k) } } KEYWORDS);
  0         0  
  0         0  
  0         0  
239             }
240              
241             =head2 parse_from
242              
243             =cut
244              
245             sub parse_from {
246 11     11 1 12 my $self = shift;
247 11         20 $self->where_am_i;
248             return [
249 9 50       428 $self->expect('from'),
250 11         28 @{ $self->parse_table_or_query || [] },
251             ];
252             }
253              
254             =head2 parse_table_or_query
255              
256             =cut
257              
258             sub parse_table_or_query {
259 18     18 1 22 my $self = shift;
260             [
261             $self->any_of(
262 18 50   18   69 @{ $self->maybe(sub { [$self->token_ident, $self->expect('.')] }) || [] },
  18         165  
263 18     18   2185 sub { $self->token_ident },
264             ),
265 18 50   18   933 @{ $self->maybe(sub { [$self->expect('as')] }) || [] },
  18         209  
266 18 100   18   19 @{ $self->maybe(sub { [$self->token_alias ] }) || [] },
  18         1076  
  18         150  
267             ];
268             }
269              
270             =head2 parse_where
271              
272             =cut
273              
274             sub parse_where {
275 11     11 1 13 my $self = shift;
276 11         17 $self->where_am_i;
277             [$self->expect('where'), $self->sequence_of(
278             sub {
279             $self->any_of(
280             sub {
281             [
282 5         47 $self->token_lvalue,
283             $self->token_operator,
284             $self->token_rvalue
285             ]
286             },
287             sub {
288             [
289 0         0 $self->token_rvalue
290             ]
291             }
292 5     5   380 );
293             }
294 11         23 )];
295             }
296              
297             =head2 parse_fields
298              
299             =cut
300              
301             sub parse_fields {
302 11     11 1 1108 my $self = shift;
303 11         21 $self->where_am_i;
304              
305             # Fields
306             $self->list_of(',', sub { $self->any_of(
307 17         153 sub { $self->where_am_i('int field'); $self->token_int },
  17         45  
308 13         1226 sub { $self->where_am_i('ident field'); $self->token_ident },
  13         26  
309 0         0 sub { $self->where_am_i('string field'); $self->token_string },
  0         0  
310 0         0 sub { $self->where_am_i('nested fields'); $self->scope_of( "(", \&parse, ")" ) }
  0         0  
311 11     17   55 )});
  17         1144  
312             }
313              
314             =head2 where_am_i
315              
316             Debug function reporting on the current position in the parsed string.
317              
318             =cut
319              
320             sub where_am_i {
321 203     203 1 231 return unless DEBUG;
322 0           my $self = shift;
323 0   0       my $note = shift || (caller(1))[3];
324 0           my ( $lineno, $col, $text ) = $self->where;
325 0           my $len = length($text);
326 0           my $target_pos = $col;
327 0   0       $target_pos++ while $target_pos < length($text) && substr($text, $target_pos, 1) =~ /^\s/;
328 0           $target_pos++;
329 0 0         substr $text, ($target_pos >= length($text) ? length($text) : $target_pos), 0, "\033[01;00m";
330 0           substr $text, $col, 0, "\033[01;32m";
331 0           printf("%-80.80s %d,%d %d %s\n", $text, $col, $lineno, $len, $note);
332             }
333              
334             1;
335              
336             __END__