X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FSQL%2FAbstract.pm;h=b7dab9931673f04ad023366287d4a0b90890e6cd;hb=f5def2aa230884f9f244faf05bb30e7001c3a91f;hp=7d62ac80f6c3d9d4fbabb3315dc460a929023c5d;hpb=3c34bdb2026293b097847618f20706aa9533169e;p=dbsrgits%2FSQL-Abstract.git diff --git a/lib/SQL/Abstract.pm b/lib/SQL/Abstract.pm index 7d62ac8..b7dab99 100644 --- a/lib/SQL/Abstract.pm +++ b/lib/SQL/Abstract.pm @@ -184,12 +184,49 @@ sub new { ^ \s* go \s /xmi; + $opt{expand_unary} = {}; + + $opt{expand} = { + -not => '_expand_not', + -bool => '_expand_bool', + -and => '_expand_op_andor', + -or => '_expand_op_andor', + -nest => '_expand_nest', + }; + + $opt{expand_op} = { + 'between' => '_expand_between', + 'not between' => '_expand_between', + 'in' => '_expand_in', + 'not in' => '_expand_in', + 'nest' => '_expand_nest', + (map +($_ => '_expand_op_andor'), + qw(and or)), + }; + + # placeholder for _expand_unop system + { + my %unops = (-ident => '_expand_ident', -value => '_expand_value'); + foreach my $name (keys %unops) { + $opt{expand}{$name} = $unops{$name}; + my ($op) = $name =~ /^-(.*)$/; + $opt{expand_op}{$op} = sub { + my ($self, $op, $arg, $k) = @_; + return +{ -op => [ + $self->{cmp}, + $self->_expand_ident(-ident => $k), + $self->_expand_expr({ '-'.$op => $arg }), + ] }; + }; + } + } + $opt{render} = { (map +("-$_", "_render_$_"), qw(op func bind ident literal list)), %{$opt{render}||{}} }; - $opt{expand_unary} = {}; + $opt{render_op} = our $RENDER_OP; return bless \%opt, $class; } @@ -390,7 +427,7 @@ sub _expand_update_set_values { map { my ($k, $set) = @$_; $set = { -bind => $_ } unless defined $set; - +{ -op => [ '=', { -ident => $k }, $set ] }; + +{ -op => [ '=', $self->_expand_ident(-ident => $k), $set ] }; } map { my $k = $_; @@ -506,7 +543,7 @@ sub where { sub expand_expr { my ($self, $expr, $default_scalar_to) = @_; local our $Default_Scalar_To = $default_scalar_to if $default_scalar_to; - $self->_expand_expr($expr, undef); + $self->_expand_expr($expr); } sub render_aqt { @@ -519,72 +556,48 @@ sub render_aqt { die "notreached: $k"; } +sub render_expr { + my ($self, $expr) = @_; + $self->render_aqt($self->expand_expr($expr)); +} + sub _expand_expr { - my ($self, $expr, $logic, $default_scalar_to) = @_; - local our $Default_Scalar_To = $default_scalar_to if $default_scalar_to; + my ($self, $expr) = @_; our $Expand_Depth ||= 0; local $Expand_Depth = $Expand_Depth + 1; return undef unless defined($expr); if (ref($expr) eq 'HASH') { - if (keys %$expr > 1) { - $logic ||= 'and'; - return +{ -op => [ - $logic, - map $self->_expand_expr_hashpair($_ => $expr->{$_}, $logic), - sort keys %$expr - ] }; + return undef unless my $kc = keys %$expr; + if ($kc > 1) { + return $self->_expand_op_andor(-and => $expr); } - return undef unless keys %$expr; - return $self->_expand_expr_hashpair(%$expr, $logic); + my ($key, $value) = %$expr; + if ($key =~ /^-/ and $key =~ s/ [_\s]? \d+ $//x ) { + belch 'Use of [and|or|nest]_N modifiers is deprecated and will be removed in SQLA v2.0. ' + . "You probably wanted ...-and => [ $key => COND1, $key => COND2 ... ]"; + } + if (my $exp = $self->{expand}{$key}) { + return $self->$exp($key, $value); + } + return $self->_expand_expr_hashpair($key, $value); } if (ref($expr) eq 'ARRAY') { - my $logic = lc($logic || $self->{logic}); - $logic eq 'and' or $logic eq 'or' or puke "unknown logic: $logic"; - - my @expr = grep { - (ref($_) eq 'ARRAY' and @$_) - or (ref($_) eq 'HASH' and %$_) - or 1 - } @$expr; - - my @res; - - while (my ($el) = splice @expr, 0, 1) { - puke "Supplying an empty left hand side argument is not supported in array-pairs" - unless defined($el) and length($el); - my $elref = ref($el); - if (!$elref) { - local $Expand_Depth = 0; - push(@res, grep defined, $self->_expand_expr({ $el, shift(@expr) })); - } elsif ($elref eq 'ARRAY') { - push(@res, grep defined, $self->_expand_expr($el)) if @$el; - } elsif (my $l = is_literal_value($el)) { - push @res, { -literal => $l }; - } elsif ($elref eq 'HASH') { - local $Expand_Depth = 0; - push @res, grep defined, $self->_expand_expr($el) if %$el; - } else { - die "notreached"; - } - } - return { -op => [ $logic, @res ] }; + my $logic = '-'.lc($self->{logic}); + return $self->_expand_op_andor($logic, $expr); } if (my $literal = is_literal_value($expr)) { return +{ -literal => $literal }; } if (!ref($expr) or Scalar::Util::blessed($expr)) { - if (my $d = $Default_Scalar_To) { + if (my $d = our $Default_Scalar_To) { return $self->_expand_expr({ $d => $expr }); } - if (my $m = our $Cur_Col_Meta) { - return +{ -bind => [ $m, $expr ] }; - } - return +{ -bind => [ undef, $expr ] }; + return $self->_expand_value(-value => $expr); } die "notreached"; } sub _expand_expr_hashpair { - my ($self, $k, $v, $logic) = @_; + my ($self, $k, $v) = @_; unless (defined($k) and length($k)) { if (defined($k) and my $literal = is_literal_value($v)) { belch 'Hash-pairs consisting of an empty string with a literal are deprecated, and will be removed in 2.0: use -and => [ $literal ] instead'; @@ -593,91 +606,16 @@ sub _expand_expr_hashpair { puke "Supplying an empty left hand side argument is not supported"; } if ($k =~ /^-/) { - $self->_assert_pass_injection_guard($k =~ /^-(.*)$/s); - if ($k =~ s/ [_\s]? \d+ $//x ) { - belch 'Use of [and|or|nest]_N modifiers is deprecated and will be removed in SQLA v2.0. ' - . "You probably wanted ...-and => [ $k => COND1, $k => COND2 ... ]"; - } - if ($k eq '-nest') { - # DBIx::Class requires a nest warning to be emitted once but the private - # method it overrode to do so no longer exists - if ($self->{is_dbic_sqlmaker}) { - unless (our $Nest_Warned) { - belch( - "-nest in search conditions is deprecated, you most probably wanted:\n" - .q|{..., -and => [ \%cond0, \@cond1, \'cond2', \[ 'cond3', [ col => bind ] ], etc. ], ... }| - ); - $Nest_Warned = 1; - } - } - return $self->_expand_expr($v); - } - if ($k eq '-bool') { - if (ref($v)) { - return $self->_expand_expr($v); - } - puke "-bool => undef not supported" unless defined($v); - return { -ident => $v }; - } - if ($k eq '-not') { - return { -op => [ 'not', $self->_expand_expr($v) ] }; - } - if (my ($rest) = $k =~/^-not[_ ](.*)$/) { - return +{ -op => [ - 'not', - $self->_expand_expr_hashpair("-${rest}", $v, $logic) - ] }; - } - if (my ($logic) = $k =~ /^-(and|or)$/i) { - if (ref($v) eq 'HASH') { - return $self->_expand_expr($v, $logic); - } - if (ref($v) eq 'ARRAY') { - return $self->_expand_expr($v, $logic); - } - } - { - my $op = $k; - $op =~ s/^-// if length($op) > 1; - - # top level special ops are illegal in general - # note that, arguably, if it makes no sense at top level, it also - # makes no sense on the other side of an = sign or similar but DBIC - # gets disappointingly upset if I disallow it - if ( - (our $Expand_Depth) == 1 - and List::Util::first { $op =~ $_->{regex} } @{$self->{special_ops}} - ) { - puke "Illegal use of top-level '-$op'" - } - if (my $us = List::Util::first { $op =~ $_->{regex} } @{$self->{unary_ops}}) { - return { -op => [ $op, $v ] }; - } - } - if ($k eq '-value') { - return +{ -bind => [ our $Cur_Col_Meta, $v ] }; - } - if (my $custom = $self->{expand_unary}{$k}) { - return $self->$custom($v); - } - if ($self->{render}{$k}) { - return { $k => $v }; - } - if ( - ref($v) eq 'HASH' - and keys %$v == 1 - and (keys %$v)[0] =~ /^-/ - ) { - my ($func) = $k =~ /^-(.*)$/; - if (List::Util::first { $func =~ $_->{regex} } @{$self->{special_ops}}) { - return +{ -op => [ $func, $self->_expand_expr($v) ] }; - } - return +{ -func => [ $func, $self->_expand_expr($v) ] }; - } - if (!ref($v) or is_literal_value($v)) { - return +{ -op => [ $k =~ /^-(.*)$/, $self->_expand_expr($v) ] }; - } + return $self->_expand_expr_hashpair_op($k, $v); } + return $self->_expand_expr_hashpair_ident($k, $v); +} + +sub _expand_expr_hashpair_ident { + my ($self, $k, $v) = @_; + + # undef needs to be re-sent with cmp to achieve IS/IS NOT NULL + if ( !defined($v) or ( @@ -686,127 +624,60 @@ sub _expand_expr_hashpair { and not defined $v->{-value} ) ) { - return $self->_expand_expr_hashpair($k => { $self->{cmp} => undef }); + return $self->_expand_expr({ $k => { $self->{cmp} => undef } }); } + + my $ik = $self->_expand_ident(-ident => $k); + + # scalars and objects get expanded as whatever requested or values + if (!ref($v) or Scalar::Util::blessed($v)) { my $d = our $Default_Scalar_To; - return +{ - -op => [ - $self->{cmp}, - { -ident => $k }, - ($d ? { $d => $v } : { -bind => [ $k, $v ] }) - ] - }; + local our $Cur_Col_Meta = $k; + return $self->_expand_expr_hashpair_ident( + $k, + ($d + ? $self->_expand_expr($d => $v) + : { -value => $v } + ) + ); } if (ref($v) eq 'HASH') { if (keys %$v > 1) { - return { -op => [ - 'and', - map $self->_expand_expr_hashpair($k => { $_ => $v->{$_} }), + return $self->_expand_op_andor(-and => [ + map +{ $_ => $v->{$_} }, sort keys %$v - ] }; + ], $k); } return undef unless keys %$v; my ($vk, $vv) = %$v; - $vk =~ s/^-//; - $vk = lc($vk); - $self->_assert_pass_injection_guard($vk); - if ($vk =~ s/ [_\s]? \d+ $//x ) { - belch 'Use of [and|or|nest]_N modifiers is deprecated and will be removed in SQLA v2.0. ' - . "You probably wanted ...-and => [ -$vk => COND1, -$vk => COND2 ... ]"; + my $op = join ' ', split '_', (map lc, $vk =~ /^-?(.*)$/)[0]; + $self->_assert_pass_injection_guard($op); + if ($op =~ s/ [_\s]? \d+ $//x ) { + return $self->_expand_expr($k, $v); } - if ($vk =~ /^(?:not[ _])?between$/) { + if (my $x = $self->{expand_op}{$op}) { local our $Cur_Col_Meta = $k; - my @rhs = map $self->_expand_expr($_), - ref($vv) eq 'ARRAY' ? @$vv : $vv; - unless ( - (@rhs == 1 and ref($rhs[0]) eq 'HASH' and $rhs[0]->{-literal}) - or - (@rhs == 2 and defined($rhs[0]) and defined($rhs[1])) - ) { - puke "Operator '${\uc($vk)}' requires either an arrayref with two defined values or expressions, or a single literal scalarref/arrayref-ref"; - } - return +{ -op => [ - join(' ', split '_', $vk), - { -ident => $k }, - @rhs - ] } - } - if ($vk =~ /^(?:not[ _])?in$/) { - if (my $literal = is_literal_value($vv)) { - my ($sql, @bind) = @$literal; - my $opened_sql = $self->_open_outer_paren($sql); - return +{ -op => [ - $vk, { -ident => $k }, - [ { -literal => [ $opened_sql, @bind ] } ] - ] }; - } - my $undef_err = - 'SQL::Abstract before v1.75 used to generate incorrect SQL when the ' - . "-${\uc($vk)} operator was given an undef-containing list: !!!AUDIT YOUR CODE " - . 'AND DATA!!! (the upcoming Data::Query-based version of SQL::Abstract ' - . 'will emit the logically correct SQL instead of raising this exception)' - ; - puke("Argument passed to the '${\uc($vk)}' operator can not be undefined") - if !defined($vv); - my @rhs = map $self->_expand_expr($_), - map { ref($_) ? $_ : { -bind => [ $k, $_ ] } } - map { defined($_) ? $_: puke($undef_err) } - (ref($vv) eq 'ARRAY' ? @$vv : $vv); - return $self->${\($vk =~ /^not/ ? 'sqltrue' : 'sqlfalse')} unless @rhs; - - return +{ -op => [ - join(' ', split '_', $vk), - { -ident => $k }, - \@rhs - ] }; + return $self->$x($op, $vv, $k); } - if ($vk eq 'ident') { - if (! defined $vv or (ref($vv) and ref($vv) eq 'ARRAY')) { - puke "-$vk requires a single plain scalar argument (a quotable identifier) or an arrayref of identifier parts"; - } - return +{ -op => [ - $self->{cmp}, - { -ident => $k }, - { -ident => $vv } - ] }; - } - if ($vk eq 'value') { - return $self->_expand_expr_hashpair($k, undef) unless defined($vv); - return +{ -op => [ - $self->{cmp}, - { -ident => $k }, - { -bind => [ $k, $vv ] } - ] }; - } - if ($vk =~ /^is(?:[ _]not)?$/) { - puke "$vk can only take undef as argument" + if ($op =~ /^is(?: not)?$/) { + puke "$op can only take undef as argument" if defined($vv) and not ( ref($vv) eq 'HASH' and exists($vv->{-value}) and !defined($vv->{-value}) ); - $vk =~ s/_/ /g; - return +{ -op => [ $vk.' null', { -ident => $k } ] }; - } - if ($vk =~ /^(and|or)$/) { - if (ref($vv) eq 'HASH') { - return +{ -op => [ - $vk, - map $self->_expand_expr_hashpair($k, { $_ => $vv->{$_} }), - sort keys %$vv - ] }; - } + return +{ -op => [ $op.' null', $ik ] }; } - if (my $us = List::Util::first { $vk =~ $_->{regex} } @{$self->{special_ops}}) { - return { -op => [ $vk, { -ident => $k }, $vv ] }; + if (my $us = List::Util::first { $op =~ $_->{regex} } @{$self->{special_ops}}) { + return { -op => [ $op, $ik, $vv ] }; } - if (my $us = List::Util::first { $vk =~ $_->{regex} } @{$self->{unary_ops}}) { + if (my $us = List::Util::first { $op =~ $_->{regex} } @{$self->{unary_ops}}) { return { -op => [ $self->{cmp}, - { -ident => $k }, - { -op => [ $vk, $vv ] } + $ik, + { -op => [ $op, $vv ] } ] }; } if (ref($vv) eq 'ARRAY') { @@ -816,12 +687,11 @@ sub _expand_expr_hashpair { : (-or => @$vv) ); if ( - $vk =~ $self->{inequality_op} - or join(' ', split '_', $vk) =~ $self->{not_like_op} + $op =~ $self->{inequality_op} + or $op =~ $self->{not_like_op} ) { if (lc($logic) eq '-or' and @values > 1) { - my $op = uc join ' ', split '_', $vk; - belch "A multi-element arrayref as an argument to the inequality op '$op' " + belch "A multi-element arrayref as an argument to the inequality op '${\uc($op)}' " . 'is technically equivalent to an always-true 1=1 (you probably wanted ' . "to say ...{ \$inequality_op => [ -and => \@values ] }... instead)" ; @@ -829,7 +699,6 @@ sub _expand_expr_hashpair { } unless (@values) { # try to DWIM on equality operators - my $op = join ' ', split '_', $vk; return $op =~ $self->{equality_op} ? $self->sqlfalse : $op =~ $self->{like_op} ? belch("Supplying an empty arrayref to '@{[ uc $op]}' is deprecated") && $self->sqlfalse @@ -837,11 +706,10 @@ sub _expand_expr_hashpair { : $op =~ $self->{not_like_op} ? belch("Supplying an empty arrayref to '@{[ uc $op]}' is deprecated") && $self->sqltrue : puke "operator '$op' applied on an empty array (field '$k')"; } - return +{ -op => [ - $logic =~ /^-(.*)$/, - map $self->_expand_expr_hashpair($k => { $vk => $_ }), + return $self->_expand_op_andor($logic => [ + map +{ $vk => $_ }, @values - ] }; + ], $k); } if ( !defined($vv) @@ -851,7 +719,6 @@ sub _expand_expr_hashpair { and not defined $vv->{-value} ) ) { - my $op = join ' ', split '_', $vk; my $is = $op =~ /^not$/i ? 'is not' # legacy : $op =~ $self->{equality_op} ? 'is' @@ -859,27 +726,26 @@ sub _expand_expr_hashpair { : $op =~ $self->{inequality_op} ? 'is not' : $op =~ $self->{not_like_op} ? belch("Supplying an undefined argument to '@{[ uc $op]}' is deprecated") && 'is not' : puke "unexpected operator '$op' with undef operand"; - return +{ -op => [ $is.' null', { -ident => $k } ] }; + return +{ -op => [ $is.' null', $ik ] }; } local our $Cur_Col_Meta = $k; return +{ -op => [ - $vk, - { -ident => $k }, - $self->_expand_expr($vv) + $op, + $ik, + $self->_expand_expr($vv) ] }; } if (ref($v) eq 'ARRAY') { return $self->sqlfalse unless @$v; $self->_debug("ARRAY($k) means distribute over elements"); - my $this_logic = ( - $v->[0] =~ /^-((?:and|or))$/i - ? ($v = [ @{$v}[1..$#$v] ], $1) - : ($self->{logic} || 'or') + my $logic = lc( + $v->[0] =~ /^-(and|or)$/i + ? shift(@{$v = [ @$v ]}) + : '-'.lc($self->{logic} || 'OR') + ); + return $self->_expand_op_andor( + $logic => $v, $k ); - return +{ -op => [ - $this_logic, - map $self->_expand_expr({ $k => $_ }, $this_logic), @$v - ] }; } if (my $literal = is_literal_value($v)) { unless (length $k) { @@ -897,6 +763,217 @@ sub _expand_expr_hashpair { die "notreached"; } +sub _expand_expr_hashpair_op { + my ($self, $k, $v) = @_; + + my $op = $k; + $op =~ s/^-// if length($op) > 1; + $self->_assert_pass_injection_guard($op); + + # Ops prefixed with -not_ get converted + + if (my ($rest) = $op =~/^not[_ ](.*)$/) { + return +{ -op => [ + 'not', + $self->_expand_expr({ "-${rest}", $v }) + ] }; + } + + # the old special op system requires illegality for top-level use + + if ( + (our $Expand_Depth) == 1 + and List::Util::first { $op =~ $_->{regex} } @{$self->{special_ops}} + ) { + puke "Illegal use of top-level '-$op'" + } + + # the old unary op system means we should touch nothing and let it work + + if (my $us = List::Util::first { $op =~ $_->{regex} } @{$self->{unary_ops}}) { + return { -op => [ $op, $v ] }; + } + + # an explicit node type is currently assumed to be expanded (this is almost + # certainly wrong and there should be expansion anyway) + + if ($self->{render}{$k}) { + return { $k => $v }; + } + + # hashref RHS values get expanded and used as op/func args + + if ( + ref($v) eq 'HASH' + and keys %$v == 1 + and (keys %$v)[0] =~ /^-/ + ) { + my ($func) = $k =~ /^-(.*)$/; + if (List::Util::first { $func =~ $_->{regex} } @{$self->{special_ops}}) { + return +{ -op => [ $func, $self->_expand_expr($v) ] }; + } + return +{ -func => [ $func, $self->_expand_expr($v) ] }; + } + + # scalars and literals get simply expanded + + if (!ref($v) or is_literal_value($v)) { + return +{ -op => [ $op, $self->_expand_expr($v) ] }; + } + + die "notreached"; +} + +sub _expand_ident { + my ($self, $op, $body) = @_; + unless (defined($body) or (ref($body) and ref($body) eq 'ARRAY')) { + puke "$op requires a single plain scalar argument (a quotable identifier) or an arrayref of identifier parts"; + } + my @parts = map split(/\Q${\($self->{name_sep}||'.')}\E/, $_), + ref($body) ? @$body : $body; + return { -ident => $parts[-1] } if $self->{_dequalify_idents}; + unless ($self->{quote_char}) { + $self->_assert_pass_injection_guard($_) for @parts; + } + return +{ -ident => \@parts }; +} + +sub _expand_value { + +{ -bind => [ our $Cur_Col_Meta, $_[2] ] }; +} + +sub _expand_not { + +{ -op => [ 'not', $_[0]->_expand_expr($_[2]) ] }; +} + +sub _expand_bool { + my ($self, undef, $v) = @_; + if (ref($v)) { + return $self->_expand_expr($v); + } + puke "-bool => undef not supported" unless defined($v); + return $self->_expand_ident(-ident => $v); +} + +sub _expand_op_andor { + my ($self, $logic, $v, $k) = @_; + if (defined $k) { + $v = [ map +{ $k, $_ }, + (ref($v) eq 'HASH') + ? (map +{ $_ => $v->{$_} }, sort keys %$v) + : @$v, + ]; + } + my ($logop) = $logic =~ /^-?(.*)$/; + if (ref($v) eq 'HASH') { + return +{ -op => [ + $logop, + map $self->_expand_expr({ $_ => $v->{$_} }), + sort keys %$v + ] }; + } + if (ref($v) eq 'ARRAY') { + $logop eq 'and' or $logop eq 'or' or puke "unknown logic: $logop"; + + my @expr = grep { + (ref($_) eq 'ARRAY' and @$_) + or (ref($_) eq 'HASH' and %$_) + or 1 + } @$v; + + my @res; + + while (my ($el) = splice @expr, 0, 1) { + puke "Supplying an empty left hand side argument is not supported in array-pairs" + unless defined($el) and length($el); + my $elref = ref($el); + if (!$elref) { + local our $Expand_Depth = 0; + push(@res, grep defined, $self->_expand_expr({ $el, shift(@expr) })); + } elsif ($elref eq 'ARRAY') { + push(@res, grep defined, $self->_expand_expr($el)) if @$el; + } elsif (my $l = is_literal_value($el)) { + push @res, { -literal => $l }; + } elsif ($elref eq 'HASH') { + local our $Expand_Depth = 0; + push @res, grep defined, $self->_expand_expr($el) if %$el; + } else { + die "notreached"; + } + } + # ??? + # return $res[0] if @res == 1; + return { -op => [ $logop, @res ] }; + } + die "notreached"; +} + +sub _expand_between { + my ($self, $op, $vv, $k) = @_; + local our $Cur_Col_Meta = $k; + my @rhs = map $self->_expand_expr($_), + ref($vv) eq 'ARRAY' ? @$vv : $vv; + unless ( + (@rhs == 1 and ref($rhs[0]) eq 'HASH' and $rhs[0]->{-literal}) + or + (@rhs == 2 and defined($rhs[0]) and defined($rhs[1])) + ) { + puke "Operator '${\uc($op)}' requires either an arrayref with two defined values or expressions, or a single literal scalarref/arrayref-ref"; + } + return +{ -op => [ + $op, + $self->_expand_ident(-ident => $k), + @rhs + ] } +} + +sub _expand_in { + my ($self, $op, $vv, $k) = @_; + if (my $literal = is_literal_value($vv)) { + my ($sql, @bind) = @$literal; + my $opened_sql = $self->_open_outer_paren($sql); + return +{ -op => [ + $op, $self->_expand_ident(-ident => $k), + [ { -literal => [ $opened_sql, @bind ] } ] + ] }; + } + my $undef_err = + 'SQL::Abstract before v1.75 used to generate incorrect SQL when the ' + . "-${\uc($op)} operator was given an undef-containing list: !!!AUDIT YOUR CODE " + . 'AND DATA!!! (the upcoming Data::Query-based version of SQL::Abstract ' + . 'will emit the logically correct SQL instead of raising this exception)' + ; + puke("Argument passed to the '${\uc($op)}' operator can not be undefined") + if !defined($vv); + my @rhs = map $self->_expand_expr($_), + map { ref($_) ? $_ : { -bind => [ $k, $_ ] } } + map { defined($_) ? $_: puke($undef_err) } + (ref($vv) eq 'ARRAY' ? @$vv : $vv); + return $self->${\($op =~ /^not/ ? 'sqltrue' : 'sqlfalse')} unless @rhs; + + return +{ -op => [ + $op, + $self->_expand_ident(-ident => $k), + \@rhs + ] }; +} + +sub _expand_nest { + my ($self, $op, $v) = @_; + # DBIx::Class requires a nest warning to be emitted once but the private + # method it overrode to do so no longer exists + if ($self->{is_dbic_sqlmaker}) { + unless (our $Nest_Warned) { + belch( + "-nest in search conditions is deprecated, you most probably wanted:\n" + .q|{..., -and => [ \%cond0, \@cond1, \'cond2', \[ 'cond3', [ col => bind ] ], etc. ], ... }| + ); + $Nest_Warned = 1; + } + } + return $self->_expand_expr($v); +} + sub _recurse_where { my ($self, $where, $logic) = @_; @@ -926,92 +1003,117 @@ sub _render_ident { return $self->_convert($self->_quote($ident)); } -my %unop_postfix = map +($_ => 1), - 'is null', 'is not null', - 'asc', 'desc', -; - -my %special = ( - (map +($_ => do { - my $op = $_; - sub { - my ($self, $args) = @_; - my ($left, $low, $high) = @$args; - my ($rhsql, @rhbind) = do { - if (@$args == 2) { - puke "Single arg to between must be a literal" - unless $low->{-literal}; - @{$low->{-literal}} - } else { - my ($l, $h) = map [ $self->render_aqt($_) ], $low, $high; - (join(' ', $l->[0], $self->_sqlcase('and'), $h->[0]), - @{$l}[1..$#$l], @{$h}[1..$#$h]) - } - }; - my ($lhsql, @lhbind) = $self->render_aqt($left); - return ( - join(' ', '(', $lhsql, $self->_sqlcase($op), $rhsql, ')'), - @lhbind, @rhbind - ); - } +sub _render_list { + my ($self, $list) = @_; + my @parts = grep length($_->[0]), map [ $self->render_aqt($_) ], @$list; + return join(', ', map $_->[0], @parts), map @{$_}[1..$#$_], @parts; +} + +sub _render_func { + my ($self, $rest) = @_; + my ($func, @args) = @$rest; + my @arg_sql; + my @bind = map { + my @x = @$_; + push @arg_sql, shift @x; + @x + } map [ $self->render_aqt($_) ], @args; + return ($self->_sqlcase($func).'('.join(', ', @arg_sql).')', @bind); +} + +sub _render_bind { + my ($self, $bind) = @_; + return ($self->_convert('?'), $self->_bindtype(@$bind)); +} + +sub _render_literal { + my ($self, $literal) = @_; + $self->_assert_bindval_matches_bindtype(@{$literal}[1..$#$literal]); + return @$literal; +} + +our $RENDER_OP = { + (map +($_ => sub { + my ($self, $op, $args) = @_; + my ($left, $low, $high) = @$args; + my ($rhsql, @rhbind) = do { + if (@$args == 2) { + puke "Single arg to between must be a literal" + unless $low->{-literal}; + @{$low->{-literal}} + } else { + my ($l, $h) = map [ $self->render_aqt($_) ], $low, $high; + (join(' ', $l->[0], $self->_sqlcase('and'), $h->[0]), + @{$l}[1..$#$l], @{$h}[1..$#$h]) + } + }; + my ($lhsql, @lhbind) = $self->render_aqt($left); + return ( + join(' ', '(', $lhsql, $self->_sqlcase($op), $rhsql, ')'), + @lhbind, @rhbind + ); }), 'between', 'not between'), - (map +($_ => do { - my $op = $_; - sub { - my ($self, $args) = @_; - my ($lhs, $rhs) = @$args; - my @in_bind; - my @in_sql = map { - my ($sql, @bind) = $self->render_aqt($_); - push @in_bind, @bind; - $sql; - } @$rhs; - my ($lhsql, @lbind) = $self->render_aqt($lhs); - return ( - $lhsql.' '.$self->_sqlcase($op).' ( ' - .join(', ', @in_sql) - .' )', - @lbind, @in_bind - ); - } + (map +($_ => sub { + my ($self, $op, $args) = @_; + my ($lhs, $rhs) = @$args; + my @in_bind; + my @in_sql = map { + my ($sql, @bind) = $self->render_aqt($_); + push @in_bind, @bind; + $sql; + } @$rhs; + my ($lhsql, @lbind) = $self->render_aqt($lhs); + return ( + $lhsql.' '.$self->_sqlcase($op).' ( ' + .join(', ', @in_sql) + .' )', + @lbind, @in_bind + ); }), 'in', 'not in'), -); + (map +($_ => '_render_unop_postfix'), + 'is null', 'is not null', 'asc', 'desc', + ), + (not => '_render_op_not'), + (map +($_ => sub { + my ($self, $op, $args) = @_; + my @parts = grep length($_->[0]), map [ $self->render_aqt($_) ], @$args; + return '' unless @parts; + return @{$parts[0]} if @parts == 1; + my ($final_sql) = join( + ' '.$self->_sqlcase($op).' ', + map $_->[0], @parts + ); + return ( + '('.$final_sql.')', + map @{$_}[1..$#$_], @parts + ); + }), qw(and or)), +}; sub _render_op { my ($self, $v) = @_; my ($op, @args) = @$v; - $op =~ s/^-// if length($op) > 1; - $op = lc($op); - if (my $h = $special{$op}) { - return $self->$h(\@args); + if (my $r = $self->{render_op}{$op}) { + return $self->$r($op, \@args); } my $us = List::Util::first { $op =~ $_->{regex} } @{$self->{special_ops}}; if ($us and @args > 1) { puke "Special op '${op}' requires first value to be identifier" - unless my ($k) = map $_->{-ident}, grep ref($_) eq 'HASH', $args[0]; + unless my ($ident) = map $_->{-ident}, grep ref($_) eq 'HASH', $args[0]; + my $k = join(($self->{name_sep}||'.'), @$ident); local our $Expand_Depth = 1; return $self->${\($us->{handler})}($k, $op, $args[1]); } if (my $us = List::Util::first { $op =~ $_->{regex} } @{$self->{unary_ops}}) { return $self->${\($us->{handler})}($op, $args[0]); } - my $final_op = $op =~ /^(?:is|not)_/ ? join(' ', split '_', $op) : $op; - if (@args == 1 and $op !~ /^(and|or)$/) { - my ($expr_sql, @bind) = $self->render_aqt($args[0]); - my $op_sql = $self->_sqlcase($final_op); - my $final_sql = ( - $unop_postfix{lc($final_op)} - ? "${expr_sql} ${op_sql}" - : "${op_sql} ${expr_sql}" - ); - return (($op eq 'not' || $us ? '('.$final_sql.')' : $final_sql), @bind); + if (@args == 1) { + return $self->_render_unop_prefix($op, \@args); } else { my @parts = grep length($_->[0]), map [ $self->render_aqt($_) ], @args; return '' unless @parts; - my $is_andor = !!($op =~ /^(and|or)$/); - return @{$parts[0]} if $is_andor and @parts == 1; - my ($final_sql) = map +($is_andor ? "( ${_} )" : $_), join( - ' '.$self->_sqlcase($final_op).' ', + my ($final_sql) = join( + ' '.$self->_sqlcase($op).' ', map $_->[0], @parts ); return ( @@ -1022,33 +1124,24 @@ sub _render_op { die "unhandled"; } -sub _render_list { - my ($self, $list) = @_; - my @parts = grep length($_->[0]), map [ $self->render_aqt($_) ], @$list; - return join(', ', map $_->[0], @parts), map @{$_}[1..$#$_], @parts; -} - -sub _render_func { - my ($self, $rest) = @_; - my ($func, @args) = @$rest; - my @arg_sql; - my @bind = map { - my @x = @$_; - push @arg_sql, shift @x; - @x - } map [ $self->render_aqt($_) ], @args; - return ($self->_sqlcase($func).'('.join(', ', @arg_sql).')', @bind); +sub _render_op_not { + my ($self, $op, $v) = @_; + my ($sql, @bind) = $self->_render_unop_prefix($op, $v); + return "(${sql})", @bind; } -sub _render_bind { - my ($self, $bind) = @_; - return ($self->_convert('?'), $self->_bindtype(@$bind)); +sub _render_unop_prefix { + my ($self, $op, $v) = @_; + my ($expr_sql, @bind) = $self->render_aqt($v->[0]); + my $op_sql = $self->_sqlcase($op); + return ("${op_sql} ${expr_sql}", @bind); } -sub _render_literal { - my ($self, $literal) = @_; - $self->_assert_bindval_matches_bindtype(@{$literal}[1..$#$literal]); - return @$literal; +sub _render_unop_postfix { + my ($self, $op, $v) = @_; + my ($expr_sql, @bind) = $self->render_aqt($v->[0]); + my $op_sql = $self->_sqlcase($op); + return ($expr_sql.' '.$op_sql, @bind); } # Some databases (SQLite) treat col IN (1, 2) different from @@ -1103,16 +1196,15 @@ sub _expand_order_by { puke "ordering direction hash passed to order by must have exactly one key (-asc or -desc)"; } } - my @exp = map +(defined($dir) ? { -op => [ $dir => $_ ] } : $_), + my @exp = map +( + defined($dir) ? { -op => [ $dir =~ /^-?(.*)$/ ,=> $_ ] } : $_ + ), map $self->expand_expr($_, -ident), map ref($_) eq 'ARRAY' ? @$_ : $_, @to_expand; return (@exp > 1 ? { -list => \@exp } : $exp[0]); }; - local @{$self->{expand_unary}}{qw(-asc -desc)} = ( - sub { shift->$expander(asc => @_) }, - sub { shift->$expander(desc => @_) }, - ); + local @{$self->{expand}}{qw(-asc -desc)} = (($expander) x 2); return $self->$expander(undef, $arg); }