Update clases test+functionality
[dbsrgits/SQL-Abstract-2.0-ish.git] / lib / SQL / Abstract / AST / v1.pm
CommitLineData
14774be0 1use MooseX::Declare;
2
3class SQL::Abstract::AST::v1 extends SQL::Abstract {
4
5 use Carp qw/croak/;
6 use Data::Dump qw/pp/;
7
8 use Moose::Util::TypeConstraints;
cbcfedc1 9 use MooseX::Types::Moose qw/ArrayRef Str Int Ref HashRef/;
14774be0 10 use MooseX::AttributeHelpers;
bad761ba 11 use SQL::Abstract::Types qw/AST/;
ef0d6124 12 use Devel::PartialDump qw/dump/;
14774be0 13
14 clean;
15
0c371882 16 # set things that are valid in where clauses
ef0d6124 17 override _build_expr_dispatch_table {
0bf8a8c4 18 return {
19 %{super()},
1b85673a 20 in => $self->can('_in'),
21 not_in => $self->can('_in'),
1f4bd99c 22 between => $self->can('_between'),
23 not_between => $self->can('_between'),
a464be15 24 and => $self->can('_recurse_where'),
25 or => $self->can('_recurse_where'),
1b85673a 26 map { +"$_" => $self->can("_$_") } qw/
0c371882 27 value
627dcb62 28 identifier
0c371882 29 true
30 false
e7996b3a 31 expr
0c371882 32 /
0bf8a8c4 33 };
14774be0 34 }
35
bad761ba 36 method _select(AST $ast) {
747f7c21 37 # Default to requiring columns and from.
38 # DB specific ones (i.e. mysql/Pg) can not require the FROM part with a bit
39 # of refactoring
40
41 for (qw/columns tablespec/) {
42 confess "'$_' is required in select AST with " . dump ($ast)
43 unless exists $ast->{$_};
4ee32f41 44 }
45
46 # Check that columns is a -list
747f7c21 47 confess "'columns' should be an array ref, not " . dump($ast->{columns})
48 unless is_ArrayRef($ast->{columns});
49
64c32031 50 my $cols = $self->_list({-type => 'list', args => $ast->{columns} });
4ee32f41 51
52 my @output = (
747f7c21 53 SELECT => $cols
4ee32f41 54 );
55
747f7c21 56 push @output, FROM => $self->dispatch($ast->{tablespec})
57 if exists $ast->{tablespec};
58
924d940e 59 if (exists $ast->{where}) {
60 my $sub_ast = $ast->{where};
61
62 confess "$_ option is not an AST: " . dump($sub_ast)
63 unless is_AST($sub_ast);
64
65 push @output, "WHERE", $self->_expr($sub_ast);
66 }
67
68 for (qw/group_by having order_by/) {
4ee32f41 69 if (exists $ast->{$_}) {
70 my $sub_ast = $ast->{$_};
e68f980b 71
924d940e 72 confess "$_ option is not an AST or an ArrayRef: " . dump($sub_ast)
73 unless is_AST($sub_ast) || is_ArrayRef($sub_ast);;
4ee32f41 74
e68f980b 75 my $meth = "__$_";
76 push @output, $self->$meth($sub_ast);
4ee32f41 77 }
78 }
79
80 return join(' ', @output);
14774be0 81 }
82
d4656fcf 83 method _update(AST $ast) {
84
85 for (qw/columns values tablespec/) {
86 confess "'$_' is required in update AST with " . dump ($ast)
87 unless exists $ast->{$_};
88 }
fc20481d 89
90 my $table = $ast->{tablespec};
91 confess 'update: tablespec must be an ident or an alias in ' . dump($ast)
92 unless $table->{-type} =~ /^identifier|alias$/;
93
94 my @output = (
95 'UPDATE',
96 $self->dispatch($table),
97 'SET'
98 );
99
100 confess 'update: number of columns doesn\'t match values: ' . dump($ast)
101 if @{$ast->{columns}} != @{$ast->{values}};
102
103 $DB::single = 1;
104 my $list = {
105 -type => 'list',
106 args => [ map {
107 { -type => 'expr',
108 op => '==', # This should really be '=' but hmmmmmmmm
109 args => [
110 $ast->{columns}[$_],
111 $ast->{values}[$_]
112 ]
113 }
114 } 0..$#{$ast->{columns}} ]
115 };
116
117 push @output, $self->dispatch($list);
118
119 return join(' ', @output);
d4656fcf 120
121 }
122
64c32031 123 method _join(HashRef $ast) {
d0ad3a92 124
125 # TODO: Validate join type
126 my $type = $ast->{join_type} || "";
14774be0 127
f7dc4536 128 my @output = $self->dispatch($ast->{lhs});
d0ad3a92 129
130 push @output, uc $type if $type;
f7dc4536 131 push @output, "JOIN", $self->dispatch($ast->{rhs});
64c32031 132
d0ad3a92 133 push @output,
134 exists $ast->{on}
135 ? ('ON', '(' . $self->_expr( $ast->{on} ) . ')' )
924d940e 136 : ('USING', '(' .$self->dispatch($ast->{using}
137 || croak "No 'on' or 'uinsg' clause passed to join cluase: " .
138 dump($ast)
139 ) .
d0ad3a92 140 ')' );
64c32031 141
d0ad3a92 142 return join(" ", @output);
64c32031 143
14774be0 144 }
145
924d940e 146 method _ordering(AST $ast) {
147
148 my $output = $self->_expr($ast->{expr});
149
150 $output .= " " . uc $1
151 if $ast->{direction} &&
152 ( $ast->{direction} =~ /^(asc|desc)$/i
153 || confess "Unknown ordering direction " . dump($ast)
154 );
14774be0 155
924d940e 156 return $output;
14774be0 157 }
158
627dcb62 159 method _identifier(AST $ast) {
160 my @names = @{$ast->{elements}};
14774be0 161
627dcb62 162 my $sep = $self->ident_separator;
4ee32f41 163 my $quote = $self->is_quoting
164 ? $self->quote_chars
165 : [ '' ];
166
167 my $join = $quote->[-1] . $sep . $quote->[0];
14774be0 168
4ee32f41 169 # We dont want to quote * in [qw/me */]: `me`.* is the desired output there
170 # This means you can't have a field called `*`. I am willing to accept this
171 # situation, cos thats a really stupid thing to want.
172 my $post;
173 $post = pop @names if $names[-1] eq '*';
14774be0 174
8b398780 175 my $ret;
176 $ret = $quote->[0] .
177 join( $join, @names ) .
178 $quote->[-1]
179 if @names;
180
181 $ret = $ret
182 ? $ret . $sep . $post
183 : $post
184 if defined $post;
185
4ee32f41 186
4ee32f41 187 return $ret;
14774be0 188 }
189
14774be0 190
7a56723e 191 method _list(AST $ast) {
e6a33ce3 192 return "" unless $ast->{args};
193
924d940e 194 my @items = is_ArrayRef($ast->{args})
195 ? @{$ast->{args}}
196 : $ast->{args};
14774be0 197
198 return join(
199 $self->list_separator,
200 map { $self->dispatch($_) } @items);
201 }
202
747f7c21 203 # TODO: I think i want to parameterized AST type to get better validation
7a56723e 204 method _alias(AST $ast) {
205
4ee32f41 206 # TODO: Maybe we want qq{ AS "$as"} here
7a56723e 207 return $self->dispatch($ast->{ident}) . " AS " . $ast->{as};
14774be0 208
209 }
210
bad761ba 211 method _value(AST $ast) {
14774be0 212
1b85673a 213 $self->add_bind($ast->{value});
14774be0 214 return "?";
215 }
216
e68f980b 217 # Not dispatchable to.
924d940e 218 method __having($args) {
219 return "HAVING " . $self->_list({-type => 'list', args => $args});
220 }
221
222 method __group_by($args) {
223 return "GROUP BY " . $self->_list({-type => 'list', args => $args});
e68f980b 224 }
225
924d940e 226 method __order_by($args) {
227 return "ORDER BY " . $self->_list({-type => 'list', args => $args});
228 }
229
230
ef0d6124 231 # Perhaps badly named. handles 'and' and 'or' clauses
bad761ba 232 method _recurse_where(AST $ast) {
14774be0 233
a464be15 234 my $op = $ast->{op};
14774be0 235
a464be15 236 my $OP = uc $op;
237 my $prio = $SQL::Abstract::PRIO{$op};
14774be0 238
ef0d6124 239 my $dispatch_table = $self->expr_dispatch_table;
0bf8a8c4 240
14774be0 241 my @output;
a464be15 242 foreach ( @{$ast->{args}} ) {
bad761ba 243 croak "invalid component in where clause: $_" unless is_AST($_);
14774be0 244
9d7d0694 245 if ($_->{-type} eq 'expr' && $_->{op} =~ /^(and|or)$/) {
14774be0 246 my $sub_prio = $SQL::Abstract::PRIO{$1};
247
39f7dc30 248 if ($sub_prio == $prio) {
249 # When the element below has same priority, i.e. 'or' as a child of
250 # 'or', dont produce extra brackets
14774be0 251 push @output, $self->_recurse_where($_);
252 } else {
253 push @output, '(' . $self->_recurse_where($_) . ')';
254 }
255 } else {
ef0d6124 256 push @output, $self->_expr($_);
14774be0 257 }
258 }
259
260 return join(" $OP ", @output);
261 }
262
bad761ba 263 method _expr(AST $ast) {
1b85673a 264 my $op = $ast->{-type};
0c371882 265
ef0d6124 266 $op = $ast->{op} if $op eq 'expr';
267
268 if (my $code = $self->lookup_expr_dispatch($op)) {
0c371882 269
270 return $code->($self, $ast);
271
272 }
ef0d6124 273 croak "'$op' is not a valid AST type in an expression with " . dump($ast)
274 if $ast->{-type} ne 'expr';
0c371882 275
e6a33ce3 276 # This is an attempt to do some form of validation on function names. This
277 # might end up being a bad thing.
278 croak "'$op' is not a valid operator in an expression with " . dump($ast)
279 if $op =~ /\W/;
280
281 return $self->_generic_function_op($ast);
1b85673a 282
1b85673a 283 }
0c371882 284
bad761ba 285 method _binop(AST $ast) {
1b85673a 286 my ($lhs, $rhs) = @{$ast->{args}};
287 my $op = $ast->{op};
0bf8a8c4 288
ec376489 289 # IS NOT? NULL
290 if ($rhs->{-type} eq 'value' && !defined $rhs->{value} &&
291 ($op eq '==' || $op eq '!='))
292 {
293 return $self->_expr($lhs) .
294 ($op eq '==' ? " IS " : " IS NOT ") .
295 "NULL";
296 }
297
ef0d6124 298 join (' ', $self->_expr($lhs),
0bf8a8c4 299 $self->binop_mapping($op) || croak("Unknown binary operator $op"),
ef0d6124 300 $self->_expr($rhs)
14774be0 301 );
302 }
303
e6a33ce3 304 method _generic_function_op(AST $ast) {
305 my $op = $ast->{op};
306
307 return "$op(" . $self->_list($ast) . ")";
308 }
309
bad761ba 310 method _in(AST $ast) {
a464be15 311
9d7d0694 312 my ($field,@values) = @{$ast->{args}};
a464be15 313
9d7d0694 314 my $not = ($ast->{op} =~ /^not_/) ? " NOT" : "";
0bf8a8c4 315
9d7d0694 316 return $self->_false unless @values;
14774be0 317
ef0d6124 318 return $self->_expr($field) .
9d7d0694 319 $not .
14774be0 320 " IN (" .
9d7d0694 321 join(", ", map { $self->dispatch($_) } @values ) .
14774be0 322 ")";
323 }
324
1f4bd99c 325 method _between(AST $ast) {
326
327 my ($field,@values) = @{$ast->{args}};
328
329 my $not = ($ast->{op} =~ /^not_/) ? " NOT" : "";
330 croak "between requires 3 arguments: " . dump($ast)
331 unless @values == 2;
332
39f7dc30 333 # The brackets are to work round an issue with SQL::A::Test
334 return "(" .
335 $self->_expr($field) .
1f4bd99c 336 $not .
337 " BETWEEN " .
39f7dc30 338 join(" AND ", map { $self->dispatch($_) } @values ) .
339 ")";
1f4bd99c 340 }
341
44cfd1f6 342 # 'constants' that are portable across DBs
343 method _false($ast?) { "0 = 1" }
344 method _true($ast?) { "1 = 1" }
345
14774be0 346}