simple literals get detected and turned into nodes
[dbsrgits/SQL-Abstract.git] / lib / SQL / Abstract / Converter.pm
CommitLineData
a82e41dc 1package SQL::Abstract::Converter;
2
3use Carp ();
4use List::Util ();
5use Scalar::Util ();
6use Data::Query::Constants qw(
7 DQ_IDENTIFIER DQ_OPERATOR DQ_VALUE DQ_LITERAL DQ_JOIN DQ_SELECT DQ_ORDER
8 DQ_WHERE DQ_DELETE DQ_UPDATE DQ_INSERT
9);
10use Data::Query::ExprHelpers qw(perl_scalar_value);
11use Moo;
12
13has renderer_will_quote => (
14 is => 'ro'
15);
16
17has lower_case => (
18 is => 'ro'
19);
20
21has default_logic => (
22 is => 'ro', coerce => sub { uc($_[0]) }, default => sub { 'OR' }
23);
24
25has bind_meta => (
26 is => 'ro', default => sub { 1 }
27);
28
29has cmp => (is => 'ro', default => sub { '=' });
30
31has sqltrue => (is => 'ro', default => sub { '1=1' });
32has sqlfalse => (is => 'ro', default => sub { '0=1' });
33
34has special_ops => (is => 'ro', default => sub { [] });
35
36# XXX documented but I don't current fail any tests not using it
37has unary_ops => (is => 'ro', default => sub { [] });
38
39has injection_guard => (
40 is => 'ro',
41 default => sub {
42 qr/
43 \;
44 |
45 ^ \s* go \s
46 /xmi;
47 }
48);
49
50has identifier_sep => (
51 is => 'ro', default => sub { '.' },
52);
53
54has always_quote => (is => 'ro', default => sub { 1 });
55
56has convert => (is => 'ro');
57
58has array_datatypes => (is => 'ro');
59
60sub _literal_to_dq {
61 my ($self, $literal) = @_;
62 my @bind;
63 ($literal, @bind) = @$literal if ref($literal) eq 'ARRAY';
64 +{
65 type => DQ_LITERAL,
66 subtype => 'SQL',
67 literal => $literal,
68 (@bind ? (values => [ $self->_bind_to_dq(@bind) ]) : ()),
69 };
70}
71
72sub _bind_to_dq {
73 my ($self, @bind) = @_;
74 return unless @bind;
75 $self->bind_meta
76 ? do {
77 $self->_assert_bindval_matches_bindtype(@bind);
78 map perl_scalar_value(reverse @$_), @bind
79 }
80 : map perl_scalar_value($_), @bind
81}
82
83sub _value_to_dq {
84 my ($self, $value) = @_;
85 $self->_maybe_convert_dq(perl_scalar_value($value, our $Cur_Col_Meta));
86}
87
88sub _ident_to_dq {
89 my ($self, $ident) = @_;
90 $self->_assert_pass_injection_guard($ident)
91 unless $self->renderer_will_quote;
92 $self->_maybe_convert_dq({
93 type => DQ_IDENTIFIER,
94 elements => [ split /\Q${\$self->identifier_sep}/, $ident ],
95 });
96}
97
98sub _maybe_convert_dq {
99 my ($self, $dq) = @_;
100 if (my $c = $self->{where_convert}) {
101 +{
102 type => DQ_OPERATOR,
103 operator => { 'SQL.Naive' => 'apply' },
104 args => [
105 { type => DQ_IDENTIFIER, elements => [ $self->_sqlcase($c) ] },
106 $dq
107 ]
108 };
109 } else {
110 $dq;
111 }
112}
113
114sub _op_to_dq {
115 my ($self, $op, @args) = @_;
116 $self->_assert_pass_injection_guard($op);
117 +{
118 type => DQ_OPERATOR,
119 operator => { 'SQL.Naive' => $op },
120 args => \@args
121 };
122}
123
124sub _assert_pass_injection_guard {
125 if ($_[1] =~ $_[0]->{injection_guard}) {
126 my $class = ref $_[0];
127 die "Possible SQL injection attempt '$_[1]'. If this is indeed a part of the "
128 . "desired SQL use literal SQL ( \'...' or \[ '...' ] ) or supply your own "
129 . "{injection_guard} attribute to ${class}->new()"
130 }
131}
132
133sub _insert_to_dq {
134 my ($self, $table, $data, $options) = @_;
135 my (@names, @values);
136 if (ref($data) eq 'HASH') {
137 @names = sort keys %$data;
138 foreach my $k (@names) {
139 local our $Cur_Col_Meta = $k;
140 push @values, $self->_mutation_rhs_to_dq($data->{$k});
141 }
142 } elsif (ref($data) eq 'ARRAY') {
143 local our $Cur_Col_Meta;
144 @values = map $self->_mutation_rhs_to_dq($_), @$data;
145 } else {
146 die "Not handled yet";
147 }
148 my $returning;
149 if (my $r_source = $options->{returning}) {
150 $returning = [
151 map +(ref($_) ? $self->_expr_to_dq($_) : $self->_ident_to_dq($_)),
152 (ref($r_source) eq 'ARRAY' ? @$r_source : $r_source),
153 ];
154 }
155 +{
156 type => DQ_INSERT,
157 target => $self->_table_to_dq($table),
158 (@names ? (names => [ map $self->_ident_to_dq($_), @names ]) : ()),
159 values => [ \@values ],
160 ($returning ? (returning => $returning) : ()),
161 };
162}
163
164sub _mutation_rhs_to_dq {
165 my ($self, $v) = @_;
166 if (ref($v) eq 'ARRAY') {
167 if ($self->{array_datatypes}) {
168 return $self->_value_to_dq($v);
169 }
170 $v = \do { my $x = $v };
171 }
172 if (ref($v) eq 'HASH') {
173 my ($op, $arg, @rest) = %$v;
174
175 die 'Operator calls in update/insert must be in the form { -op => $arg }'
176 if (@rest or not $op =~ /^\-(.+)/);
177 }
178 return $self->_expr_to_dq($v);
179}
180
181sub _update_to_dq {
182 my ($self, $table, $data, $where) = @_;
183
184 die "Unsupported data type specified to \$sql->update"
185 unless ref $data eq 'HASH';
186
187 my @set;
188
189 foreach my $k (sort keys %$data) {
190 my $v = $data->{$k};
191 local our $Cur_Col_Meta = $k;
192 push @set, [ $self->_ident_to_dq($k), $self->_mutation_rhs_to_dq($v) ];
193 }
194
195 return +{
196 type => DQ_UPDATE,
197 target => $self->_table_to_dq($table),
198 set => \@set,
199 where => $self->_where_to_dq($where),
200 };
201}
202
203sub _source_to_dq {
204 my ($self, $table, undef, $where) = @_;
205
206 my $source_dq = $self->_table_to_dq($table);
207
208 if (my $where_dq = $self->_where_to_dq($where)) {
209 $source_dq = {
210 type => DQ_WHERE,
211 from => $source_dq,
212 where => $where_dq,
213 };
214 }
215
216 $source_dq;
217}
218
219sub _select_to_dq {
220 my $self = shift;
221 my ($table, $fields, $where, $order) = @_;
222
223 my $source_dq = $self->_source_to_dq(@_);
224
225 my $ordered_dq = do {
226 if ($order) {
227 $self->_order_by_to_dq($order, undef, $source_dq);
228 } else {
229 $source_dq
230 }
231 };
232
87af4204 233 return $self->_select_select_to_dq($fields, $ordered_dq);
a82e41dc 234}
235
87af4204 236sub _select_select_to_dq {
a82e41dc 237 my ($self, $fields, $from_dq) = @_;
238
239 $fields ||= '*';
240
241 return +{
242 type => DQ_SELECT,
87af4204 243 select => $self->_select_field_list_to_dq($fields),
a82e41dc 244 from => $from_dq,
245 };
246}
247
248sub _select_field_list_to_dq {
249 my ($self, $fields) = @_;
87af4204 250 [ map $self->_select_field_to_dq($_),
251 ref($fields) eq 'ARRAY' ? @$fields : $fields ];
a82e41dc 252}
253
254sub _select_field_to_dq {
255 my ($self, $field) = @_;
87af4204 256 if (my $ref = ref($field)) {
257 if ($ref eq 'REF' and ref($$field) eq 'HASH') {
258 return $$field;
259 } else {
260 return $self->_literal_to_dq($$field);
261 }
262 }
263 return $self->_ident_to_dq($field)
a82e41dc 264}
265
266sub _delete_to_dq {
267 my ($self, $table, $where) = @_;
268 +{
269 type => DQ_DELETE,
270 target => $self->_table_to_dq($table),
271 where => $self->_where_to_dq($where),
272 }
273}
274
275sub _where_to_dq {
276 my ($self, $where, $logic) = @_;
277
278 return undef unless defined($where);
279
e177c256 280 # if we're given a simple string assume it's a literal
281 return $self->_literal_to_dq($where) if !ref($where);
282
a82e41dc 283 # turn the convert misfeature on - only used in WHERE clauses
284 local $self->{where_convert} = $self->convert;
285
286 return $self->_expr_to_dq($where, $logic);
287}
288
289sub _expr_to_dq {
290 my ($self, $where, $logic) = @_;
291
292 if (ref($where) eq 'ARRAY') {
293 return $self->_expr_to_dq_ARRAYREF($where, $logic);
294 } elsif (ref($where) eq 'HASH') {
295 return $self->_expr_to_dq_HASHREF($where, $logic);
296 } elsif (
297 ref($where) eq 'SCALAR'
298 or (ref($where) eq 'REF' and ref($$where) eq 'ARRAY')
299 ) {
300 return $self->_literal_to_dq($$where);
62d17764 301 } elsif (ref($where) eq 'REF' and ref($$where) eq 'HASH') {
302 return $$where;
a82e41dc 303 } elsif (!ref($where) or Scalar::Util::blessed($where)) {
304 return $self->_value_to_dq($where);
305 }
306 die "Can't handle $where";
307}
308
309sub _expr_to_dq_ARRAYREF {
310 my ($self, $where, $logic) = @_;
311
312 $logic = uc($logic || $self->default_logic || 'OR');
313 $logic eq 'AND' or $logic eq 'OR' or die "unknown logic: $logic";
314
315 return unless @$where;
316
317 my ($first, @rest) = @$where;
318
319 return $self->_expr_to_dq($first) unless @rest;
320
321 my $first_dq = do {
322 if (!ref($first)) {
323 $self->_where_hashpair_to_dq($first => shift(@rest));
324 } else {
325 $self->_expr_to_dq($first);
326 }
327 };
328
329 return $self->_expr_to_dq_ARRAYREF(\@rest, $logic) unless $first_dq;
330
331 $self->_op_to_dq(
332 $logic, $first_dq, $self->_expr_to_dq_ARRAYREF(\@rest, $logic)
333 );
334}
335
336sub _expr_to_dq_HASHREF {
337 my ($self, $where, $logic) = @_;
338
339 $logic = uc($logic) if $logic;
340
341 my @dq = map {
342 $self->_where_hashpair_to_dq($_ => $where->{$_}, $logic)
343 } sort keys %$where;
344
345 return $dq[0] unless @dq > 1;
346
347 my $final = pop(@dq);
348
349 foreach my $dq (reverse @dq) {
350 $final = $self->_op_to_dq($logic||'AND', $dq, $final);
351 }
352
353 return $final;
354}
355
356sub _where_to_dq_SCALAR {
357 shift->_value_to_dq(@_);
358}
359
360sub _apply_to_dq {
361 my ($self, $op, $v) = @_;
362 my @args = map $self->_expr_to_dq($_), (ref($v) eq 'ARRAY' ? @$v : $v);
363
364 # Ok. Welcome to stupid compat code land. An SQLA expr that would in the
365 # absence of this piece of crazy render to:
366 #
367 # A( B( C( x ) ) )
368 #
369 # such as
370 #
371 # { -a => { -b => { -c => $x } } }
372 #
373 # actually needs to render to:
374 #
375 # A( B( C x ) )
376 #
377 # because SQL sucks, and databases are hateful, and SQLA is Just That DWIM.
378 #
379 # However, we don't want to catch 'A(x)' and turn it into 'A x'
380 #
381 # So the way we deal with this is to go through all our arguments, and
382 # then if the argument is -also- an apply, i.e. at least 'B', we check
383 # its arguments - and if there's only one of them, and that isn't an apply,
384 # then we convert to the bareword form. The end result should be:
385 #
386 # A( x ) -> A( x )
387 # A( B( x ) ) -> A( B x )
388 # A( B( C( x ) ) ) -> A( B( C x ) )
389 # A( B( x + y ) ) -> A( B( x + y ) )
390 # A( B( x, y ) ) -> A( B( x, y ) )
391 #
392 # If this turns out not to be quite right, please add additional tests
393 # to either 01generate.t or 02where.t *and* update this comment.
394
395 foreach my $arg (@args) {
396 if (
397 $arg->{type} eq DQ_OPERATOR and $arg->{operator}{'SQL.Naive'} eq 'apply'
398 and @{$arg->{args}} == 2 and $arg->{args}[1]{type} ne DQ_OPERATOR
399 ) {
400 $arg->{operator}{'SQL.Naive'} = (shift @{$arg->{args}})->{elements}->[0];
401 }
402 }
403 $self->_assert_pass_injection_guard($op);
404 return $self->_op_to_dq(
405 apply => $self->_ident_to_dq($op), @args
406 );
407}
408
409sub _where_hashpair_to_dq {
410 my ($self, $k, $v, $logic) = @_;
411
412 if ($k =~ /^-(.*)/s) {
413 my $op = uc($1);
414 if ($op eq 'AND' or $op eq 'OR') {
415 return $self->_expr_to_dq($v, $op);
416 } elsif ($op eq 'NEST') {
417 return $self->_expr_to_dq($v);
418 } elsif ($op eq 'NOT') {
419 return $self->_op_to_dq(NOT => $self->_expr_to_dq($v));
420 } elsif ($op eq 'BOOL') {
421 return ref($v) ? $self->_expr_to_dq($v) : $self->_ident_to_dq($v);
422 } elsif ($op eq 'NOT_BOOL') {
423 return $self->_op_to_dq(
424 NOT => ref($v) ? $self->_expr_to_dq($v) : $self->_ident_to_dq($v)
425 );
426 } elsif ($op eq 'IDENT') {
427 return $self->_ident_to_dq($v);
428 } elsif ($op eq 'VALUE') {
429 return $self->_value_to_dq($v);
430 } elsif ($op =~ /^(?:AND|OR|NEST)_?\d+/) {
431 die "Use of [and|or|nest]_N modifiers is no longer supported";
432 } else {
433 return $self->_apply_to_dq($op, $v);
434 }
435 } else {
436 local our $Cur_Col_Meta = $k;
437 if (ref($v) eq 'ARRAY') {
438 if (!@$v) {
439 return $self->_literal_to_dq($self->{sqlfalse});
440 } elsif (defined($v->[0]) && $v->[0] =~ /-(and|or)/i) {
441 return $self->_expr_to_dq_ARRAYREF([
442 map +{ $k => $_ }, @{$v}[1..$#$v]
443 ], uc($1));
444 }
445 return $self->_expr_to_dq_ARRAYREF([
446 map +{ $k => $_ }, @$v
447 ], $logic);
448 } elsif (ref($v) eq 'SCALAR' or (ref($v) eq 'REF' and ref($$v) eq 'ARRAY')) {
449 return +{
450 type => DQ_LITERAL,
451 subtype => 'SQL',
452 parts => [ $self->_ident_to_dq($k), $self->_literal_to_dq($$v) ]
453 };
454 }
455 my ($op, $rhs) = do {
456 if (ref($v) eq 'HASH') {
457 if (keys %$v > 1) {
458 return $self->_expr_to_dq_ARRAYREF([
459 map +{ $k => { $_ => $v->{$_} } }, sort keys %$v
460 ], $logic||'AND');
461 }
462 my ($op, $value) = %$v;
463 s/^-//, s/_/ /g for $op;
464 if ($op =~ /^(and|or)$/i) {
465 return $self->_expr_to_dq({ $k => $value }, $op);
466 } elsif (
467 my $special_op = List::Util::first {$op =~ $_->{regex}}
468 @{$self->{special_ops}}
469 ) {
470 return $self->_literal_to_dq(
471 [ $special_op->{handler}->($k, $op, $value) ]
472 );;
473 } elsif ($op =~ /^(?:AND|OR|NEST)_?\d+$/i) {
474 die "Use of [and|or|nest]_N modifiers is no longer supported";
475 }
476 (uc($op), $value);
477 } else {
478 ($self->{cmp}, $v);
479 }
480 };
481 if ($op eq 'BETWEEN' or $op eq 'IN' or $op eq 'NOT IN' or $op eq 'NOT BETWEEN') {
482 if (ref($rhs) ne 'ARRAY') {
483 if ($op =~ /IN$/) {
484 # have to add parens if none present because -in => \"SELECT ..."
485 # got documented. mst hates everything.
486 if (ref($rhs) eq 'SCALAR') {
487 my $x = $$rhs;
488 1 while ($x =~ s/\A\s*\((.*)\)\s*\Z/$1/s);
489 $rhs = \$x;
490 } else {
491 my ($x, @rest) = @{$$rhs};
492 1 while ($x =~ s/\A\s*\((.*)\)\s*\Z/$1/s);
493 $rhs = \[ $x, @rest ];
494 }
495 }
496 return $self->_op_to_dq(
497 $op, $self->_ident_to_dq($k), $self->_literal_to_dq($$rhs)
498 );
499 }
500 return $self->_literal_to_dq($self->{sqlfalse}) unless @$rhs;
501 return $self->_op_to_dq(
502 $op, $self->_ident_to_dq($k), map $self->_expr_to_dq($_), @$rhs
503 )
504 } elsif ($op =~ s/^NOT (?!LIKE)//) {
505 return $self->_where_hashpair_to_dq(-not => { $k => { $op => $rhs } });
506 } elsif ($op eq 'IDENT') {
507 return $self->_op_to_dq(
508 $self->{cmp}, $self->_ident_to_dq($k), $self->_ident_to_dq($rhs)
509 );
510 } elsif ($op eq 'VALUE') {
511 return $self->_op_to_dq(
512 $self->{cmp}, $self->_ident_to_dq($k), $self->_value_to_dq($rhs)
513 );
514 } elsif (!defined($rhs)) {
515 my $null_op = do {
516 if ($op eq '=' or $op eq 'LIKE') {
517 'IS NULL'
518 } elsif ($op eq '!=') {
519 'IS NOT NULL'
520 } else {
521 die "Can't do undef -> NULL transform for operator ${op}";
522 }
523 };
524 return $self->_op_to_dq($null_op, $self->_ident_to_dq($k));
525 }
526 if (ref($rhs) eq 'ARRAY') {
527 if (!@$rhs) {
528 return $self->_literal_to_dq(
529 $op eq '!=' ? $self->{sqltrue} : $self->{sqlfalse}
530 );
531 } elsif (defined($rhs->[0]) and $rhs->[0] =~ /^-(and|or)$/i) {
532 return $self->_expr_to_dq_ARRAYREF([
533 map +{ $k => { $op => $_ } }, @{$rhs}[1..$#$rhs]
534 ], uc($1));
535 } elsif ($op =~ /^-(?:AND|OR|NEST)_?\d+/) {
536 die "Use of [and|or|nest]_N modifiers is no longer supported";
537 }
538 return $self->_expr_to_dq_ARRAYREF([
539 map +{ $k => { $op => $_ } }, @$rhs
540 ]);
541 }
542 return $self->_op_to_dq(
543 $op, $self->_ident_to_dq($k), $self->_expr_to_dq($rhs)
544 );
545 }
546}
547
548sub _order_by_to_dq {
549 my ($self, $arg, $dir, $from) = @_;
550
551 return unless $arg;
552
553 my $dq = {
554 type => DQ_ORDER,
3d82b6c9 555 (defined($dir) ? (reverse => !!($dir =~ /desc/i)) : ()),
a82e41dc 556 ($from ? (from => $from) : ()),
557 };
558
559 if (!ref($arg)) {
560 $dq->{by} = $self->_ident_to_dq($arg);
561 } elsif (ref($arg) eq 'ARRAY') {
562 return unless @$arg;
563 local our $Order_Inner unless our $Order_Recursing;
564 local $Order_Recursing = 1;
565 my ($outer, $inner);
566 foreach my $member (@$arg) {
567 local $Order_Inner;
568 my $next = $self->_order_by_to_dq($member, $dir, $from);
569 $outer ||= $next;
570 $inner->{from} = $next if $inner;
571 $inner = $Order_Inner || $next;
572 }
573 $Order_Inner = $inner;
574 return $outer;
575 } elsif (ref($arg) eq 'REF' and ref($$arg) eq 'ARRAY') {
576 $dq->{by} = $self->_literal_to_dq($$arg);
577 } elsif (ref($arg) eq 'SCALAR') {
daf6f830 578 if (my ($ident, $dir) = $$arg =~ /^(\w+)(?:\s+(desc|asc))?$/i) {
3d82b6c9 579 $dq->{by} = $self->_ident_to_dq($ident);
580 $dq->{reverse} = 1 if $dir and lc($dir) eq 'desc';
581 } else {
582 $dq->{by} = $self->_literal_to_dq($$arg);
583 }
a82e41dc 584 } elsif (ref($arg) eq 'HASH') {
585 my ($key, $val, @rest) = %$arg;
586
587 return unless $key;
588
589 if (@rest or not $key =~ /^-(desc|asc)/i) {
590 die "hash passed to _order_by must have exactly one key (-desc or -asc)";
591 }
592 my $dir = uc $1;
593 return $self->_order_by_to_dq($val, $dir, $from);
594 } else {
595 die "Can't handle $arg in _order_by_to_dq";
596 }
597 return $dq;
598}
599
600sub _table_to_dq {
601 my ($self, $from) = @_;
602 if (ref($from) eq 'ARRAY') {
603 die "Empty FROM list" unless my @f = @$from;
604 my $dq = $self->_table_to_dq(shift @f);
605 while (my $x = shift @f) {
606 $dq = {
607 type => DQ_JOIN,
608 join => [ $dq, $self->_table_to_dq($x) ]
609 };
610 }
611 $dq;
612 } elsif (ref($from) eq 'SCALAR' or (ref($from) eq 'REF')) {
613 $self->_literal_to_dq($$from);
614 } else {
615 $self->_ident_to_dq($from);
616 }
617}
618
619# And bindtype
620sub _bindtype (@) {
621 #my ($self, $col, @vals) = @_;
622
623 #LDNOTE : changed original implementation below because it did not make
624 # sense when bindtype eq 'columns' and @vals > 1.
625# return $self->{bindtype} eq 'columns' ? [ $col, @vals ] : @vals;
626
627 # called often - tighten code
628 return $_[0]->bind_meta
629 ? map {[$_[1], $_]} @_[2 .. $#_]
630 : @_[2 .. $#_]
631 ;
632}
633
634# Dies if any element of @bind is not in [colname => value] format
635# if bindtype is 'columns'.
636sub _assert_bindval_matches_bindtype {
637# my ($self, @bind) = @_;
638 my $self = shift;
639 if ($self->bind_meta) {
640 for (@_) {
641 if (!defined $_ || ref($_) ne 'ARRAY' || @$_ != 2) {
642 die "bindtype 'columns' selected, you need to pass: [column_name => bind_value]"
643 }
644 }
645 }
646}
647
648# Fix SQL case, if so requested
649sub _sqlcase {
650 return $_[0]->lower_case ? $_[1] : uc($_[1]);
651}
652
6531;