use Scalar::Util 'blessed';
use DBIx::Class::_Util qw(UNRESOLVABLE_CONDITION serialize);
use SQL::Abstract qw(is_plain_value is_literal_value);
+use DBIx::Class::Carp;
use namespace::clean;
#
# generate sql chunks
my $to_scan = {
restricting => [
- $sql_maker->_recurse_where ($attrs->{where}),
+ ($sql_maker->_recurse_where ($attrs->{where}))[0],
$sql_maker->_parse_rs_attrs ({ having => $attrs->{having} }),
],
grouping => [
),
],
selecting => [
- map { ($sql_maker->_recurse_fields($_))[0] } @{$attrs->{select}},
+ # kill all selectors which look like a proper subquery
+ # this is a sucky heuristic *BUT* - if we get it wrong the query will simply
+ # fail to run, so we are relatively safe
+ grep
+ { $_ !~ / \A \s* \( \s* SELECT \s+ .+? \s+ FROM \s+ .+? \) \s* \z /xsi }
+ map
+ { ($sql_maker->_recurse_fields($_))[0] }
+ @{$attrs->{select}}
],
ordering => [
map { $_->[0] } $self->_extract_order_criteria ($attrs->{order_by}, $sql_maker),
],
};
- # throw away empty chunks and all 2-value arrayrefs: the thinking is that these are
- # bind value specs left in by the sloppy renderer above. It is ok to do this
- # at this point, since we are going to end up rewriting this crap anyway
- for my $v (values %$to_scan) {
- my @nv;
- for (@$v) {
- next if (
- ! defined $_
- or
- (
- ref $_ eq 'ARRAY'
- and
- ( @$_ == 0 or @$_ == 2 )
- )
- );
+ # throw away empty-string chunks, and make sure no binds snuck in
+ # note that we operate over @{$to_scan->{$type}}, hence the
+ # semi-mindbending ... map ... for values ...
+ ( $_ = [ map {
- if (ref $_) {
- require Data::Dumper::Concise;
- $self->throw_exception("Unexpected ref in scan-plan: " . Data::Dumper::Concise::Dumper($v) );
- }
+ (not $_) ? ()
+ : (length ref $_) ? (require Data::Dumper::Concise && $self->throw_exception(
+ "Unexpected ref in scan-plan: " . Data::Dumper::Concise::Dumper($_)
+ ))
+ : $_
- push @nv, $_;
- }
+ } @$_ ] ) for values %$to_scan;
- $v = \@nv;
- }
+ # throw away empty to-scan's
+ (
+ @{$to_scan->{$_}}
+ or
+ delete $to_scan->{$_}
+ ) for keys %$to_scan;
- # kill all selectors which look like a proper subquery
- # this is a sucky heuristic *BUT* - if we get it wrong the query will simply
- # fail to run, so we are relatively safe
- $to_scan->{selecting} = [ grep {
- $_ !~ / \A \s* \( \s* SELECT \s+ .+? \s+ FROM \s+ .+? \) \s* \z /xsi
- } @{ $to_scan->{selecting} || [] } ];
# first see if we have any exact matches (qualified or unqualified)
for my $type (keys %$to_scan) {
sub _collapse_cond {
my ($self, $where, $where_is_anded_array) = @_;
+ my $fin;
+
if (! $where) {
return;
}
my $chunk = shift @pieces;
if (ref $chunk eq 'HASH') {
- push @pairs, map { $_ => $chunk->{$_} } sort keys %$chunk;
+ for (sort keys %$chunk) {
+
+ # Match SQLA 1.79 behavior
+ if ($_ eq '') {
+ is_literal_value($chunk->{$_})
+ ? carp 'Hash-pairs consisting of an empty string with a literal are deprecated, use -and => [ $literal ] instead'
+ : $self->throw_exception("Supplying an empty left hand side argument is not supported in hash-pairs")
+ ;
+ }
+
+ push @pairs, $_ => $chunk->{$_};
+ }
}
elsif (ref $chunk eq 'ARRAY') {
push @pairs, -or => $chunk
if @$chunk;
}
elsif ( ! length ref $chunk) {
+
+ # Match SQLA 1.79 behavior
+ $self->throw_exception("Supplying an empty left hand side argument is not supported in array-pairs")
+ if $where_is_anded_array and (! defined $chunk or $chunk eq '');
+
push @pairs, $chunk, shift @pieces;
}
else {
or return;
# Consolidate various @conds back into something more compact
- my $fin;
-
for my $c (@conds) {
if (ref $c ne 'HASH') {
push @{$fin->{-and}}, $c;
}
else {
for my $col (sort keys %$c) {
- if (exists $fin->{$col}) {
- my ($l, $r) = ($fin->{$col}, $c->{$col});
- (ref $_ ne 'ARRAY' or !@$_) and $_ = [ -and => $_ ] for ($l, $r);
-
- if (@$l and @$r and $l->[0] eq $r->[0] and $l->[0] =~ /^\-and$/i) {
- $fin->{$col} = [ -and => map { @$_[1..$#$_] } ($l, $r) ];
- }
- else {
- $fin->{$col} = [ -and => $fin->{$col}, $c->{$col} ];
- }
+ # consolidate all -and nodes
+ if ($col =~ /^\-and$/i) {
+ push @{$fin->{-and}},
+ ref $c->{$col} eq 'ARRAY' ? @{$c->{$col}}
+ : ref $c->{$col} eq 'HASH' ? %{$c->{$col}}
+ : { $col => $c->{$col} }
+ ;
+ }
+ elsif ($col =~ /^\-/) {
+ push @{$fin->{-and}}, { $col => $c->{$col} };
+ }
+ elsif (exists $fin->{$col}) {
+ $fin->{$col} = [ -and => map {
+ (ref $_ eq 'ARRAY' and ($_->[0]||'') =~ /^\-and$/i )
+ ? @{$_}[1..$#$_]
+ : $_
+ ;
+ } ($fin->{$col}, $c->{$col}) ];
}
else {
$fin->{$col} = $c->{$col};
}
}
}
+ }
+ elsif (ref $where eq 'ARRAY') {
+ # we are always at top-level here, it is safe to dump empty *standalone* pieces
+ my $fin_idx;
- # unroll single-element -and nodes
- if ( ref $fin->{-and} eq 'ARRAY' and @{$fin->{-and}} == 1 ) {
- my $piece = (delete $fin->{-and})->[0];
- if (ref $piece eq 'ARRAY') {
- $fin->{-or} = $fin->{-or} ? [ $piece, $fin->{-or} ] : $piece;
- }
- elsif (! exists $fin->{''}) {
- $fin->{''} = $piece;
+ for (my $i = 0; $i <= $#$where; $i++ ) {
+
+ # Match SQLA 1.79 behavior
+ $self->throw_exception(
+ "Supplying an empty left hand side argument is not supported in array-pairs"
+ ) if (! defined $where->[$i] or ! length $where->[$i]);
+
+ my $logic_mod = lc ( ($where->[$i] =~ /^(\-(?:and|or))$/i)[0] || '' );
+
+ if ($logic_mod) {
+ $i++;
+ $self->throw_exception("Unsupported top-level op/arg pair: [ $logic_mod => $where->[$i] ]")
+ unless ref $where->[$i] eq 'HASH' or ref $where->[$i] eq 'ARRAY';
+
+ my $sub_elt = $self->_collapse_cond({ $logic_mod => $where->[$i] })
+ or next;
+
+ my @keys = keys %$sub_elt;
+ if ( @keys == 1 and $keys[0] !~ /^\-/ ) {
+ $fin_idx->{ "COL_$keys[0]_" . serialize $sub_elt } = $sub_elt;
+ }
+ else {
+ $fin_idx->{ "SER_" . serialize $sub_elt } = $sub_elt;
+ }
}
- }
+ elsif (! length ref $where->[$i] ) {
+ my $sub_elt = $self->_collapse_cond({ @{$where}[$i, $i+1] })
+ or next;
- # compress same-column conds found in $fin
- for my $col ( keys %$fin ) {
- next unless ref $fin->{$col} eq 'ARRAY' and ($fin->{$col}[0]||'') eq '-and';
- my $val_bag = { map {
- (! defined $_ ) ? ( UNDEF => undef )
- : ( ! ref $_ or is_plain_value $_ ) ? ( "VAL_$_" => $_ )
- : ( ( 'SER_' . serialize $_ ) => $_ )
- } @{$fin->{$col}}[1 .. $#{$fin->{$col}}] };
-
- if (keys %$val_bag == 1 ) {
- ($fin->{$col}) = values %$val_bag;
+ $fin_idx->{ "COL_$where->[$i]_" . serialize $sub_elt } = $sub_elt;
+ $i++;
}
else {
- $fin->{$col} = [ -and => map { $val_bag->{$_} } sort keys %$val_bag ];
+ $fin_idx->{ "SER_" . serialize $where->[$i] } = $self->_collapse_cond( $where->[$i] ) || next;
}
}
- return $fin;
- }
- elsif (ref $where eq 'ARRAY') {
- my @w = @$where;
+ if (! $fin_idx) {
+ return;
+ }
+ elsif ( keys %$fin_idx == 1 ) {
+ $fin = (values %$fin_idx)[0];
+ }
+ else {
+ my @or;
+
+ # at this point everything is at most one level deep - unroll if needed
+ for (sort keys %$fin_idx) {
+ if ( ref $fin_idx->{$_} eq 'HASH' and keys %{$fin_idx->{$_}} == 1 ) {
+ my ($l, $r) = %{$fin_idx->{$_}};
+
+ if (
+ ref $r eq 'ARRAY'
+ and
+ (
+ ( @$r == 1 and $l =~ /^\-and$/i )
+ or
+ $l =~ /^\-or$/i
+ )
+ ) {
+ push @or, @$r
+ }
- while ( @w and (
- (ref $w[0] eq 'ARRAY' and ! @{$w[0]} )
- or
- (ref $w[0] eq 'HASH' and ! keys %{$w[0]})
- )) { shift @w };
+ elsif (
+ ref $r eq 'HASH'
+ and
+ keys %$r == 1
+ and
+ $l =~ /^\-(?:and|or)$/i
+ ) {
+ push @or, %$r;
+ }
- return unless @w;
+ else {
+ push @or, $l, $r;
+ }
+ }
+ else {
+ push @or, $fin_idx->{$_};
+ }
+ }
- if ( @w == 1 ) {
- return ( length ref $w[0] )
- ? $self->_collapse_cond($w[0])
- : { $w[0] => undef }
- ;
+ $fin->{-or} = \@or;
}
- elsif ( @w == 2 and ! length ref $w[0]) {
- if ( ( $w[0]||'' ) =~ /^\-and$/i ) {
- return (ref $w[1] eq 'HASH' or ref $w[1] eq 'ARRAY')
- ? $self->_collapse_cond($w[1], (ref $w[1] eq 'ARRAY') )
- : $self->throw_exception("Unsupported top-level op/arg pair: [ $w[0] => $w[1] ]")
- ;
- }
- else {
- return $self->_collapse_cond({ @w });
- }
+ }
+ else {
+ # not a hash not an array
+ $fin = { -and => [ $where ] };
+ }
+
+ # unroll single-element -and's
+ while (
+ $fin->{-and}
+ and
+ @{$fin->{-and}} < 2
+ ) {
+ my $and = delete $fin->{-and};
+ last if @$and == 0;
+
+ # at this point we have @$and == 1
+ if (
+ ref $and->[0] eq 'HASH'
+ and
+ ! grep { exists $fin->{$_} } keys %{$and->[0]}
+ ) {
+ $fin = {
+ %$fin, %{$and->[0]}
+ };
}
else {
- return { -or => \@w };
+ $fin->{-and} = $and;
+ last;
}
}
- else {
- # not a hash not an array
- return { '' => $where };
+
+ # compress same-column conds found in $fin
+ for my $col ( grep { $_ !~ /^\-/ } keys %$fin ) {
+ next unless ref $fin->{$col} eq 'ARRAY' and ($fin->{$col}[0]||'') =~ /^\-and$/i;
+ my $val_bag = { map {
+ (! defined $_ ) ? ( UNDEF => undef )
+ : ( ! length ref $_ or is_plain_value $_ ) ? ( "VAL_$_" => $_ )
+ : ( ( 'SER_' . serialize $_ ) => $_ )
+ } @{$fin->{$col}}[1 .. $#{$fin->{$col}}] };
+
+ if (keys %$val_bag == 1 ) {
+ ($fin->{$col}) = values %$val_bag;
+ }
+ else {
+ $fin->{$col} = [ -and => map { $val_bag->{$_} } sort keys %$val_bag ];
+ }
}
- die 'should not get here';
+ return keys %$fin ? $fin : ();
}
sub _collapse_cond_unroll_pairs {
if (ref $rhs eq 'HASH' and ! keys %$rhs) {
# FIXME - SQLA seems to be doing... nothing...?
}
+ # normalize top level -ident, for saner extract_fixed_condition_columns code
elsif (ref $rhs eq 'HASH' and keys %$rhs == 1 and exists $rhs->{-ident}) {
push @conds, { $lhs => { '=', $rhs } };
}
push @conds, { $lhs => $rhs->{-value} };
}
elsif (ref $rhs eq 'HASH' and keys %$rhs == 1 and exists $rhs->{'='}) {
- if( is_literal_value $rhs->{'='}) {
+ if ( length ref $rhs->{'='} and is_literal_value $rhs->{'='} ) {
push @conds, { $lhs => $rhs };
}
else {
my ($l, $r) = %$p;
- push @conds, ( ! length ref $r or is_plain_value($r) )
+ push @conds, (
+ ! length ref $r
+ or
+ # the unroller recursion may return a '=' prepended value already
+ ref $r eq 'HASH' and keys %$rhs == 1 and exists $rhs->{'='}
+ or
+ is_plain_value($r)
+ )
? { $l => $r }
: { $l => { '=' => $r } }
;
push @conds, { $lhs => $rhs };
}
}
+ # unroll func + { -value => ... }
+ elsif (
+ ref $rhs eq 'HASH'
+ and
+ ( my ($subop) = keys %$rhs ) == 1
+ and
+ length ref ((values %$rhs)[0])
+ and
+ my $vref = is_plain_value( (values %$rhs)[0] )
+ ) {
+ push @conds, { $lhs => { $subop => $$vref } }
+ }
else {
push @conds, { $lhs => $rhs };
}
}
}
# do not need to check for plain values - _collapse_cond did it for us
- elsif(length ref $v->{'='} and is_literal_value($v->{'='}) ) {
+ elsif(
+ length ref $v->{'='}
+ and
+ (
+ ( ref $v->{'='} eq 'HASH' and keys %{$v->{'='}} == 1 and exists $v->{'='}{-ident} )
+ or
+ is_literal_value($v->{'='})
+ )
+ ) {
$vals->{ 'SER_' . serialize $v->{'='} } = $v->{'='};
}
}