Commit | Line | Data |
01dd4e4f |
1 | package SQL::Abstract::Tree; |
2 | |
3 | use strict; |
4 | use warnings; |
b3b79607 |
5 | no warnings 'qw'; |
01dd4e4f |
6 | use Carp; |
7 | |
0769ac0e |
8 | use Hash::Merge qw//; |
9 | |
10 | use base 'Class::Accessor::Grouped'; |
11 | |
12 | __PACKAGE__->mk_group_accessors( simple => $_ ) for qw( |
13 | newline indent_string indent_amount colormap indentmap fill_in_placeholders |
14 | placeholder_surround |
15 | ); |
2fed0b4b |
16 | |
bc482085 |
17 | my $merger = Hash::Merge->new; |
18 | |
19 | $merger->specify_behavior({ |
2fed0b4b |
20 | SCALAR => { |
21 | SCALAR => sub { $_[1] }, |
22 | ARRAY => sub { [ $_[0], @{$_[1]} ] }, |
23 | HASH => sub { $_[1] }, |
24 | }, |
25 | ARRAY => { |
26 | SCALAR => sub { $_[1] }, |
27 | ARRAY => sub { $_[1] }, |
28 | HASH => sub { $_[1] }, |
29 | }, |
30 | HASH => { |
31 | SCALAR => sub { $_[1] }, |
32 | ARRAY => sub { [ values %{$_[0]}, @{$_[1]} ] }, |
33 | HASH => sub { Hash::Merge::_merge_hashes( $_[0], $_[1] ) }, |
34 | }, |
0769ac0e |
35 | }, 'SQLA::Tree Behavior' ); |
1536de15 |
36 | |
0769ac0e |
37 | my $op_look_ahead = '(?: (?= [\s\)\(\;] ) | \z)'; |
b3b79607 |
38 | my $op_look_behind = '(?: (?<= [\,\s\)\(] ) | \A )'; |
39 | |
0769ac0e |
40 | my $quote_left = qr/[\`\'\"\[]/; |
41 | my $quote_right = qr/[\`\'\"\]]/; |
01dd4e4f |
42 | |
4e914a7c |
43 | my $placeholder_re = qr/(?: \? | \$\d+ )/x; |
44 | |
01dd4e4f |
45 | # These SQL keywords always signal end of the current expression (except inside |
46 | # of a parenthesized subexpression). |
0769ac0e |
47 | # Format: A list of strings that will be compiled to extended syntax ie. |
01dd4e4f |
48 | # /.../x) regexes, without capturing parentheses. They will be automatically |
0769ac0e |
49 | # anchored to op boundaries (excluding quotes) to match the whole token. |
50 | my @expression_start_keywords = ( |
01dd4e4f |
51 | 'SELECT', |
7853a177 |
52 | 'UPDATE', |
53 | 'INSERT \s+ INTO', |
54 | 'DELETE \s+ FROM', |
3d910890 |
55 | 'FROM', |
7853a177 |
56 | 'SET', |
01dd4e4f |
57 | '(?: |
58 | (?: |
0769ac0e |
59 | (?: (?: LEFT | RIGHT | FULL ) \s+ )? |
60 | (?: (?: CROSS | INNER | OUTER ) \s+ )? |
01dd4e4f |
61 | )? |
62 | JOIN |
63 | )', |
64 | 'ON', |
65 | 'WHERE', |
efc991a0 |
66 | '(?: DEFAULT \s+ )? VALUES', |
01dd4e4f |
67 | 'EXISTS', |
68 | 'GROUP \s+ BY', |
69 | 'HAVING', |
70 | 'ORDER \s+ BY', |
c0eaa9fd |
71 | 'SKIP', |
72 | 'FIRST', |
01dd4e4f |
73 | 'LIMIT', |
74 | 'OFFSET', |
75 | 'FOR', |
76 | 'UNION', |
77 | 'INTERSECT', |
78 | 'EXCEPT', |
820bb1f5 |
79 | 'BEGIN \s+ WORK', |
80 | 'COMMIT', |
81 | 'ROLLBACK \s+ TO \s+ SAVEPOINT', |
82 | 'ROLLBACK', |
83 | 'SAVEPOINT', |
84 | 'RELEASE \s+ SAVEPOINT', |
01dd4e4f |
85 | 'RETURNING', |
8d0dd7dc |
86 | 'ROW_NUMBER \s* \( \s* \) \s+ OVER', |
01dd4e4f |
87 | ); |
88 | |
b3b79607 |
89 | my $expr_start_re = join ("\n\t|\n", @expression_start_keywords ); |
90 | $expr_start_re = qr/ $op_look_behind (?i: $expr_start_re ) $op_look_ahead /x; |
0769ac0e |
91 | |
01dd4e4f |
92 | # These are binary operator keywords always a single LHS and RHS |
93 | # * AND/OR are handled separately as they are N-ary |
94 | # * so is NOT as being unary |
95 | # * BETWEEN without paranthesis around the ANDed arguments (which |
96 | # makes it a non-binary op) is detected and accomodated in |
97 | # _recurse_parse() |
01dd4e4f |
98 | |
0769ac0e |
99 | # this will be included in the $binary_op_re, the distinction is interesting during |
100 | # testing as one is tighter than the other, plus mathops have different look |
101 | # ahead/behind (e.g. "x"="y" ) |
102 | my @math_op_keywords = (qw/ < > != <> = <= >= /); |
103 | my $math_re = join ("\n\t|\n", map |
104 | { "(?: (?<= [\\w\\s] | $quote_right ) | \\A )" . quotemeta ($_) . "(?: (?= [\\w\\s] | $quote_left ) | \\z )" } |
105 | @math_op_keywords |
01dd4e4f |
106 | ); |
b7b0f832 |
107 | $math_re = qr/$math_re/x; |
0769ac0e |
108 | |
109 | sub _math_op_re { $math_re } |
110 | |
111 | |
112 | my $binary_op_re = '(?: NOT \s+)? (?:' . join ('|', qw/IN BETWEEN R?LIKE/) . ')'; |
b3b79607 |
113 | $binary_op_re = join "\n\t|\n", |
114 | "$op_look_behind (?i: $binary_op_re ) $op_look_ahead", |
115 | $math_re, |
116 | $op_look_behind . 'IS (?:\s+ NOT)?' . "(?= \\s+ NULL \\b | $op_look_ahead )", |
117 | ; |
b7b0f832 |
118 | $binary_op_re = qr/$binary_op_re/x; |
0769ac0e |
119 | |
120 | sub _binary_op_re { $binary_op_re } |
121 | |
b3b79607 |
122 | my $all_known_re = join("\n\t|\n", |
123 | $expr_start_re, |
0769ac0e |
124 | $binary_op_re, |
125 | "$op_look_behind (?i: AND|OR|NOT ) $op_look_ahead", |
b3b79607 |
126 | (map { quotemeta $_ } qw/, ( ) */), |
4e914a7c |
127 | $placeholder_re, |
0769ac0e |
128 | ); |
01dd4e4f |
129 | |
b3b79607 |
130 | $all_known_re = qr/$all_known_re/x; |
131 | |
132 | #this one *is* capturing for the split below |
133 | # splits on whitespace if all else fails |
134 | my $tokenizer_re = qr/ \s* ( $all_known_re ) \s* | \s+ /x; |
135 | |
136 | # Parser states for _recurse_parse() |
137 | use constant PARSE_TOP_LEVEL => 0; |
138 | use constant PARSE_IN_EXPR => 1; |
139 | use constant PARSE_IN_PARENS => 2; |
140 | use constant PARSE_IN_FUNC => 3; |
141 | use constant PARSE_RHS => 4; |
142 | |
143 | my $expr_term_re = qr/ ^ (?: $expr_start_re | \) ) $/x; |
144 | my $rhs_term_re = qr/ ^ (?: $expr_term_re | $binary_op_re | (?i: AND | OR | NOT | \, ) ) $/x; |
4e914a7c |
145 | my $func_start_re = qr/^ (?: \* | $placeholder_re | \( ) $/x; |
01dd4e4f |
146 | |
7e5600e9 |
147 | my %indents = ( |
7853a177 |
148 | select => 0, |
149 | update => 0, |
150 | 'insert into' => 0, |
151 | 'delete from' => 0, |
3d910890 |
152 | from => 1, |
91916220 |
153 | where => 0, |
7853a177 |
154 | join => 1, |
155 | 'left join' => 1, |
156 | on => 2, |
2867f4f5 |
157 | having => 0, |
91916220 |
158 | 'group by' => 0, |
159 | 'order by' => 0, |
7853a177 |
160 | set => 1, |
161 | into => 1, |
91916220 |
162 | values => 1, |
c0eaa9fd |
163 | limit => 1, |
164 | offset => 1, |
165 | skip => 1, |
166 | first => 1, |
7e5600e9 |
167 | ); |
168 | |
75c3a063 |
169 | my %profiles = ( |
170 | console => { |
84c65032 |
171 | fill_in_placeholders => 1, |
9d11f0d4 |
172 | placeholder_surround => ['?/', ''], |
1536de15 |
173 | indent_string => ' ', |
75c3a063 |
174 | indent_amount => 2, |
1536de15 |
175 | newline => "\n", |
3be357b0 |
176 | colormap => {}, |
6d388c84 |
177 | indentmap => \%indents, |
aafbf833 |
178 | |
179 | eval { require Term::ANSIColor } |
180 | ? do { |
181 | my $c = \&Term::ANSIColor::color; |
6d388c84 |
182 | |
183 | my $red = [$c->('red') , $c->('reset')]; |
184 | my $cyan = [$c->('cyan') , $c->('reset')]; |
185 | my $green = [$c->('green') , $c->('reset')]; |
186 | my $yellow = [$c->('yellow') , $c->('reset')]; |
187 | my $blue = [$c->('blue') , $c->('reset')]; |
188 | my $magenta = [$c->('magenta'), $c->('reset')]; |
189 | my $b_o_w = [$c->('black on_white'), $c->('reset')]; |
aafbf833 |
190 | ( |
09931431 |
191 | placeholder_surround => [q(') . $c->('black on_magenta'), $c->('reset') . q(')], |
aafbf833 |
192 | colormap => { |
6d388c84 |
193 | 'begin work' => $b_o_w, |
194 | commit => $b_o_w, |
195 | rollback => $b_o_w, |
196 | savepoint => $b_o_w, |
197 | 'rollback to savepoint' => $b_o_w, |
198 | 'release savepoint' => $b_o_w, |
199 | |
200 | select => $red, |
201 | 'insert into' => $red, |
202 | update => $red, |
203 | 'delete from' => $red, |
204 | |
205 | set => $cyan, |
206 | from => $cyan, |
207 | |
208 | where => $green, |
209 | values => $yellow, |
210 | |
211 | join => $magenta, |
212 | 'left join' => $magenta, |
213 | on => $blue, |
214 | |
215 | 'group by' => $yellow, |
2867f4f5 |
216 | having => $yellow, |
6d388c84 |
217 | 'order by' => $yellow, |
218 | |
219 | skip => $green, |
220 | first => $green, |
221 | limit => $green, |
222 | offset => $green, |
aafbf833 |
223 | } |
224 | ); |
225 | } : (), |
3be357b0 |
226 | }, |
227 | console_monochrome => { |
84c65032 |
228 | fill_in_placeholders => 1, |
9d11f0d4 |
229 | placeholder_surround => ['?/', ''], |
3be357b0 |
230 | indent_string => ' ', |
231 | indent_amount => 2, |
232 | newline => "\n", |
233 | colormap => {}, |
6d388c84 |
234 | indentmap => \%indents, |
7e5600e9 |
235 | }, |
236 | html => { |
84c65032 |
237 | fill_in_placeholders => 1, |
9d11f0d4 |
238 | placeholder_surround => ['<span class="placeholder">', '</span>'], |
7e5600e9 |
239 | indent_string => ' ', |
240 | indent_amount => 2, |
241 | newline => "<br />\n", |
242 | colormap => { |
7853a177 |
243 | select => ['<span class="select">' , '</span>'], |
244 | 'insert into' => ['<span class="insert-into">' , '</span>'], |
245 | update => ['<span class="select">' , '</span>'], |
246 | 'delete from' => ['<span class="delete-from">' , '</span>'], |
c0eaa9fd |
247 | |
248 | set => ['<span class="set">', '</span>'], |
7853a177 |
249 | from => ['<span class="from">' , '</span>'], |
c0eaa9fd |
250 | |
251 | where => ['<span class="where">' , '</span>'], |
252 | values => ['<span class="values">', '</span>'], |
253 | |
7853a177 |
254 | join => ['<span class="join">' , '</span>'], |
c0eaa9fd |
255 | 'left join' => ['<span class="left-join">','</span>'], |
7853a177 |
256 | on => ['<span class="on">' , '</span>'], |
c0eaa9fd |
257 | |
7853a177 |
258 | 'group by' => ['<span class="group-by">', '</span>'], |
2867f4f5 |
259 | having => ['<span class="having">', '</span>'], |
7853a177 |
260 | 'order by' => ['<span class="order-by">', '</span>'], |
c0eaa9fd |
261 | |
262 | skip => ['<span class="skip">', '</span>'], |
263 | first => ['<span class="first">', '</span>'], |
264 | limit => ['<span class="limit">', '</span>'], |
265 | offset => ['<span class="offset">', '</span>'], |
820bb1f5 |
266 | |
267 | 'begin work' => ['<span class="begin-work">', '</span>'], |
268 | commit => ['<span class="commit">', '</span>'], |
269 | rollback => ['<span class="rollback">', '</span>'], |
270 | savepoint => ['<span class="savepoint">', '</span>'], |
271 | 'rollback to savepoint' => ['<span class="rollback-to-savepoint">', '</span>'], |
272 | 'release savepoint' => ['<span class="release-savepoint">', '</span>'], |
1536de15 |
273 | }, |
6d388c84 |
274 | indentmap => \%indents, |
75c3a063 |
275 | }, |
276 | none => { |
1536de15 |
277 | colormap => {}, |
278 | indentmap => {}, |
75c3a063 |
279 | }, |
280 | ); |
281 | |
282 | sub new { |
2fed0b4b |
283 | my $class = shift; |
284 | my $args = shift || {}; |
75c3a063 |
285 | |
286 | my $profile = delete $args->{profile} || 'none'; |
1c33db5d |
287 | |
288 | die "No such profile '$profile'!" unless exists $profiles{$profile}; |
289 | |
bc482085 |
290 | my $data = $merger->merge( $profiles{$profile}, $args ); |
75c3a063 |
291 | |
292 | bless $data, $class |
293 | } |
d695b0ad |
294 | |
01dd4e4f |
295 | sub parse { |
d695b0ad |
296 | my ($self, $s) = @_; |
01dd4e4f |
297 | |
298 | # tokenize string, and remove all optional whitespace |
299 | my $tokens = []; |
300 | foreach my $token (split $tokenizer_re, $s) { |
b3b79607 |
301 | push @$tokens, $token if ( |
302 | defined $token |
303 | and |
304 | length $token |
09931431 |
305 | and |
b3b79607 |
306 | $token =~ /\S/ |
307 | ); |
01dd4e4f |
308 | } |
b3b79607 |
309 | $self->_recurse_parse($tokens, PARSE_TOP_LEVEL); |
01dd4e4f |
310 | } |
311 | |
312 | sub _recurse_parse { |
d695b0ad |
313 | my ($self, $tokens, $state) = @_; |
01dd4e4f |
314 | |
315 | my $left; |
316 | while (1) { # left-associative parsing |
317 | |
318 | my $lookahead = $tokens->[0]; |
319 | if ( not defined($lookahead) |
320 | or |
321 | ($state == PARSE_IN_PARENS && $lookahead eq ')') |
322 | or |
b3b79607 |
323 | ($state == PARSE_IN_EXPR && $lookahead =~ $expr_term_re ) |
0769ac0e |
324 | or |
b3b79607 |
325 | ($state == PARSE_RHS && $lookahead =~ $rhs_term_re ) |
01dd4e4f |
326 | or |
b3b79607 |
327 | ($state == PARSE_IN_FUNC && $lookahead !~ $func_start_re) # if there are multiple values - the parenthesis will switch the $state |
01dd4e4f |
328 | ) { |
0769ac0e |
329 | return $left||(); |
01dd4e4f |
330 | } |
331 | |
332 | my $token = shift @$tokens; |
333 | |
334 | # nested expression in () |
335 | if ($token eq '(' ) { |
d695b0ad |
336 | my $right = $self->_recurse_parse($tokens, PARSE_IN_PARENS); |
337 | $token = shift @$tokens or croak "missing closing ')' around block " . $self->unparse($right); |
338 | $token eq ')' or croak "unexpected token '$token' terminating block " . $self->unparse($right); |
01dd4e4f |
339 | |
0769ac0e |
340 | $left = $left ? [$left, [PAREN => [$right||()] ]] |
341 | : [PAREN => [$right||()] ]; |
01dd4e4f |
342 | } |
b3b79607 |
343 | # AND/OR and LIST (,) |
344 | elsif ($token =~ /^ (?: OR | AND | \, ) $/xi ) { |
345 | my $op = ($token eq ',') ? 'LIST' : uc $token; |
346 | |
d695b0ad |
347 | my $right = $self->_recurse_parse($tokens, PARSE_IN_EXPR); |
01dd4e4f |
348 | |
349 | # Merge chunks if logic matches |
350 | if (ref $right and $op eq $right->[0]) { |
b3b79607 |
351 | $left = [ (shift @$right ), [$left||(), map { @$_ } @$right] ]; |
01dd4e4f |
352 | } |
353 | else { |
b3b79607 |
354 | $left = [$op => [ $left||(), $right||() ]]; |
01dd4e4f |
355 | } |
356 | } |
357 | # binary operator keywords |
a1e204f4 |
358 | elsif ( $token =~ /^ $binary_op_re $ /x ) { |
01dd4e4f |
359 | my $op = uc $token; |
d695b0ad |
360 | my $right = $self->_recurse_parse($tokens, PARSE_RHS); |
01dd4e4f |
361 | |
362 | # A between with a simple LITERAL for a 1st RHS argument needs a |
363 | # rerun of the search to (hopefully) find the proper AND construct |
364 | if ($op eq 'BETWEEN' and $right->[0] eq 'LITERAL') { |
365 | unshift @$tokens, $right->[1][0]; |
d695b0ad |
366 | $right = $self->_recurse_parse($tokens, PARSE_IN_EXPR); |
01dd4e4f |
367 | } |
368 | |
369 | $left = [$op => [$left, $right] ]; |
370 | } |
371 | # expression terminator keywords (as they start a new expression) |
b3b79607 |
372 | elsif ( $token =~ / ^ $expr_start_re $ /x ) { |
01dd4e4f |
373 | my $op = uc $token; |
d695b0ad |
374 | my $right = $self->_recurse_parse($tokens, PARSE_IN_EXPR); |
efc991a0 |
375 | $left = $left ? [ $left, [$op => [$right||()] ]] |
376 | : [ $op => [$right||()] ]; |
01dd4e4f |
377 | } |
0769ac0e |
378 | # NOT |
379 | elsif ( $token =~ /^ NOT $/ix ) { |
01dd4e4f |
380 | my $op = uc $token; |
d695b0ad |
381 | my $right = $self->_recurse_parse ($tokens, PARSE_RHS); |
01dd4e4f |
382 | $left = $left ? [ @$left, [$op => [$right] ]] |
383 | : [ $op => [$right] ]; |
384 | |
385 | } |
4e914a7c |
386 | elsif ( $token =~ $placeholder_re) { |
387 | $left = $left ? [ $left, [ PLACEHOLDER => [ $token ] ] ] |
388 | : [ PLACEHOLDER => [ $token ] ]; |
389 | } |
b3b79607 |
390 | # we're now in "unknown token" land - start eating tokens until |
391 | # we see something familiar |
01dd4e4f |
392 | else { |
b3b79607 |
393 | my $right; |
394 | |
395 | # check if the current token is an unknown op-start |
396 | if (@$tokens and $tokens->[0] =~ $func_start_re) { |
397 | $right = [ $token => [ $self->_recurse_parse($tokens, PARSE_IN_FUNC) || () ] ]; |
398 | } |
399 | else { |
400 | $right = [ LITERAL => [ $token ] ]; |
401 | } |
402 | |
403 | $left = $left ? [ $left, $right ] |
404 | : $right; |
01dd4e4f |
405 | } |
406 | } |
407 | } |
408 | |
d695b0ad |
409 | sub format_keyword { |
410 | my ($self, $keyword) = @_; |
411 | |
1536de15 |
412 | if (my $around = $self->colormap->{lc $keyword}) { |
d695b0ad |
413 | $keyword = "$around->[0]$keyword$around->[1]"; |
414 | } |
415 | |
416 | return $keyword |
417 | } |
418 | |
728f26a2 |
419 | my %starters = ( |
420 | select => 1, |
421 | update => 1, |
422 | 'insert into' => 1, |
423 | 'delete from' => 1, |
424 | ); |
425 | |
f2ab166a |
426 | sub pad_keyword { |
a24cc3a0 |
427 | my ($self, $keyword, $depth) = @_; |
e171c446 |
428 | |
429 | my $before = ''; |
1536de15 |
430 | if (defined $self->indentmap->{lc $keyword}) { |
431 | $before = $self->newline . $self->indent($depth + $self->indentmap->{lc $keyword}); |
a24cc3a0 |
432 | } |
728f26a2 |
433 | $before = '' if $depth == 0 and defined $starters{lc $keyword}; |
e4570c8e |
434 | return [$before, '']; |
a24cc3a0 |
435 | } |
436 | |
1536de15 |
437 | sub indent { ($_[0]->indent_string||'') x ( ( $_[0]->indent_amount || 0 ) * $_[1] ) } |
a24cc3a0 |
438 | |
a97eb57c |
439 | sub _is_key { |
440 | my ($self, $tree) = @_; |
0569a14f |
441 | $tree = $tree->[0] while ref $tree; |
442 | |
a97eb57c |
443 | defined $tree && defined $self->indentmap->{lc $tree}; |
0569a14f |
444 | } |
445 | |
9d11f0d4 |
446 | sub fill_in_placeholder { |
fb272e73 |
447 | my ($self, $bindargs) = @_; |
448 | |
449 | if ($self->fill_in_placeholders) { |
ad46269d |
450 | my $val = shift @{$bindargs} || ''; |
9d11f0d4 |
451 | my ($left, $right) = @{$self->placeholder_surround}; |
fb272e73 |
452 | $val =~ s/\\/\\\\/g; |
453 | $val =~ s/'/\\'/g; |
ad46269d |
454 | return qq($left$val$right) |
fb272e73 |
455 | } |
456 | return '?' |
457 | } |
458 | |
3a247d23 |
459 | # FIXME - terrible name for a user facing API |
01dd4e4f |
460 | sub unparse { |
3a247d23 |
461 | my ($self, $tree, $bindargs) = @_; |
462 | $self->_unparse($tree, [@{$bindargs||[]}], 0); |
463 | } |
a24cc3a0 |
464 | |
3a247d23 |
465 | sub _unparse { |
466 | my ($self, $tree, $bindargs, $depth) = @_; |
01dd4e4f |
467 | |
0769ac0e |
468 | if (not $tree or not @$tree) { |
01dd4e4f |
469 | return ''; |
470 | } |
a24cc3a0 |
471 | |
0769ac0e |
472 | my ($car, $cdr) = @{$tree}[0,1]; |
473 | |
474 | if (! defined $car or (! ref $car and ! defined $cdr) ) { |
475 | require Data::Dumper; |
476 | Carp::confess( sprintf ( "Internal error - malformed branch at depth $depth:\n%s", |
477 | Data::Dumper::Dumper($tree) |
478 | ) ); |
479 | } |
a24cc3a0 |
480 | |
481 | if (ref $car) { |
3a247d23 |
482 | return join (' ', map $self->_unparse($_, $bindargs, $depth), @$tree); |
01dd4e4f |
483 | } |
a24cc3a0 |
484 | elsif ($car eq 'LITERAL') { |
485 | return $cdr->[0]; |
01dd4e4f |
486 | } |
4e914a7c |
487 | elsif ($car eq 'PLACEHOLDER') { |
488 | return $self->fill_in_placeholder($bindargs); |
489 | } |
a24cc3a0 |
490 | elsif ($car eq 'PAREN') { |
e4570c8e |
491 | return sprintf ('(%s)', |
492 | join (' ', map { $self->_unparse($_, $bindargs, $depth + 2) } @{$cdr} ) |
493 | . |
494 | ($self->_is_key($cdr) |
495 | ? ( $self->newline||'' ) . $self->indent($depth + 1) |
496 | : '' |
497 | ) |
498 | ); |
01dd4e4f |
499 | } |
0769ac0e |
500 | elsif ($car eq 'AND' or $car eq 'OR' or $car =~ / ^ $binary_op_re $ /x ) { |
3a247d23 |
501 | return join (" $car ", map $self->_unparse($_, $bindargs, $depth), @{$cdr}); |
01dd4e4f |
502 | } |
b3b79607 |
503 | elsif ($car eq 'LIST' ) { |
3a247d23 |
504 | return join (', ', map $self->_unparse($_, $bindargs, $depth), @{$cdr}); |
b3b79607 |
505 | } |
01dd4e4f |
506 | else { |
f2ab166a |
507 | my ($l, $r) = @{$self->pad_keyword($car, $depth)}; |
3a247d23 |
508 | return sprintf "$l%s %s$r", $self->format_keyword($car), $self->_unparse($cdr, $bindargs, $depth); |
01dd4e4f |
509 | } |
510 | } |
511 | |
fb272e73 |
512 | sub format { my $self = shift; $self->unparse($self->parse($_[0]), $_[1]) } |
01dd4e4f |
513 | |
514 | 1; |
515 | |
3be357b0 |
516 | =pod |
517 | |
518 | =head1 SYNOPSIS |
519 | |
520 | my $sqla_tree = SQL::Abstract::Tree->new({ profile => 'console' }); |
521 | |
522 | print $sqla_tree->format('SELECT * FROM foo WHERE foo.a > 2'); |
523 | |
524 | # SELECT * |
525 | # FROM foo |
526 | # WHERE foo.a > 2 |
527 | |
6b1bf9f8 |
528 | =head1 METHODS |
529 | |
530 | =head2 new |
531 | |
532 | my $sqla_tree = SQL::Abstract::Tree->new({ profile => 'console' }); |
533 | |
c22f502d |
534 | $args = { |
535 | profile => 'console', # predefined profile to use (default: 'none') |
536 | fill_in_placeholders => 1, # true for placeholder population |
9d11f0d4 |
537 | placeholder_surround => # The strings that will be wrapped around |
538 | [GREEN, RESET], # populated placeholders if the above is set |
c22f502d |
539 | indent_string => ' ', # the string used when indenting |
540 | indent_amount => 2, # how many of above string to use for a single |
541 | # indent level |
542 | newline => "\n", # string for newline |
543 | colormap => { |
544 | select => [RED, RESET], # a pair of strings defining what to surround |
545 | # the keyword with for colorization |
546 | # ... |
547 | }, |
548 | indentmap => { |
549 | select => 0, # A zero means that the keyword will start on |
550 | # a new line |
551 | from => 1, # Any other positive integer means that after |
552 | on => 2, # said newline it will get that many indents |
553 | # ... |
554 | }, |
555 | } |
556 | |
557 | Returns a new SQL::Abstract::Tree object. All arguments are optional. |
558 | |
559 | =head3 profiles |
560 | |
561 | There are four predefined profiles, C<none>, C<console>, C<console_monochrome>, |
562 | and C<html>. Typically a user will probably just use C<console> or |
563 | C<console_monochrome>, but if something about a profile bothers you, merely |
564 | use the profile and override the parts that you don't like. |
565 | |
6b1bf9f8 |
566 | =head2 format |
567 | |
c22f502d |
568 | $sqlat->format('SELECT * FROM bar WHERE x = ?', [1]) |
569 | |
570 | Takes C<$sql> and C<\@bindargs>. |
6b1bf9f8 |
571 | |
1a3cc911 |
572 | Returns a formatting string based on the string passed in |
ee4227a7 |
573 | |
574 | =head2 parse |
575 | |
576 | $sqlat->parse('SELECT * FROM bar WHERE x = ?') |
577 | |
578 | Returns a "tree" representing passed in SQL. Please do not depend on the |
579 | structure of the returned tree. It may be stable at some point, but not yet. |
580 | |
581 | =head2 unparse |
582 | |
583 | $sqlat->parse($tree_structure, \@bindargs) |
584 | |
585 | Transform "tree" into SQL, applying various transforms on the way. |
586 | |
587 | =head2 format_keyword |
588 | |
589 | $sqlat->format_keyword('SELECT') |
590 | |
591 | Currently this just takes a keyword and puts the C<colormap> stuff around it. |
592 | Later on it may do more and allow for coderef based transforms. |
593 | |
f2ab166a |
594 | =head2 pad_keyword |
ee4227a7 |
595 | |
f2ab166a |
596 | my ($before, $after) = @{$sqlat->pad_keyword('SELECT')}; |
ee4227a7 |
597 | |
598 | Returns whitespace to be inserted around a keyword. |
9d11f0d4 |
599 | |
600 | =head2 fill_in_placeholder |
601 | |
602 | my $value = $sqlat->fill_in_placeholder(\@bindargs) |
603 | |
604 | Removes last arg from passed arrayref and returns it, surrounded with |
605 | the values in placeholder_surround, and then surrounded with single quotes. |
f2ab166a |
606 | |
607 | =head2 indent |
608 | |
609 | Returns as many indent strings as indent amounts times the first argument. |
610 | |
611 | =head1 ACCESSORS |
612 | |
613 | =head2 colormap |
614 | |
615 | See L</new> |
616 | |
617 | =head2 fill_in_placeholders |
618 | |
619 | See L</new> |
620 | |
621 | =head2 indent_amount |
622 | |
623 | See L</new> |
624 | |
625 | =head2 indent_string |
626 | |
627 | See L</new> |
628 | |
629 | =head2 indentmap |
630 | |
631 | See L</new> |
632 | |
633 | =head2 newline |
634 | |
635 | See L</new> |
636 | |
637 | =head2 placeholder_surround |
638 | |
639 | See L</new> |
640 | |