From: Peter Rabbitson Date: Wed, 3 Sep 2014 10:44:46 +0000 (+0200) Subject: Fix condition collapser corrupting -X operators X-Git-Tag: v0.082800~69 X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=commitdiff_plain;h=135ac69d;p=dbsrgits%2FDBIx-Class.git Fix condition collapser corrupting -X operators This is (fingercross) the complete fix for RT#98161 --- diff --git a/lib/DBIx/Class/Storage/DBIHacks.pm b/lib/DBIx/Class/Storage/DBIHacks.pm index e172299..da09d12 100644 --- a/lib/DBIx/Class/Storage/DBIHacks.pm +++ b/lib/DBIx/Class/Storage/DBIHacks.pm @@ -986,6 +986,8 @@ sub _extract_colinfo_of_stable_main_source_order_by_portion { sub _collapse_cond { my ($self, $where, $where_is_anded_array) = @_; + my $fin; + if (! $where) { return; } @@ -1018,25 +1020,31 @@ sub _collapse_cond { 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}; @@ -1044,39 +1052,8 @@ sub _collapse_cond { } } } - - # 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; - } - } - - # 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; - } - else { - $fin->{$col} = [ -and => map { $val_bag->{$_} } sort keys %$val_bag ]; - } - } - - return $fin; } elsif (ref $where eq 'ARRAY') { - # we are always at top-level here, it is safe to dump empty *standalone* pieces my $fin_idx; @@ -1092,20 +1069,23 @@ sub _collapse_cond { my $sub_elt = $self->_collapse_cond({ $logic_mod => $where->[$i] }) or next; - $fin_idx->{ serialize $sub_elt } = $sub_elt; + $fin_idx->{ "SER_" . serialize $sub_elt } = $sub_elt; } elsif (! length ref $where->[$i] ) { - $fin_idx->{"$where->[$i]_$i"} = $self->_collapse_cond({ @{$where}[$i, $i+1] }) || next; + my $sub_elt = $self->_collapse_cond({ @{$where}[$i, $i+1] }) + or next; + + $fin_idx->{ "COL_$where->[$i]_" . serialize $sub_elt } = $sub_elt; $i++; } else { - $fin_idx->{ serialize $where->[$i] } = $self->_collapse_cond( $where->[$i] ) || next; + $fin_idx->{ "SER_" . serialize $where->[$i] } = $self->_collapse_cond( $where->[$i] ) || next; } } return unless $fin_idx; - return ( keys %$fin_idx == 1 ) ? (values %$fin_idx)[0] : { + $fin = ( keys %$fin_idx == 1 ) ? (values %$fin_idx)[0] : { -or => [ map { ref $fin_idx->{$_} eq 'HASH' ? %{$fin_idx->{$_}} : $fin_idx->{$_} } sort keys %$fin_idx @@ -1114,10 +1094,48 @@ sub _collapse_cond { } else { # not a hash not an array - return { '' => $where }; + $fin = { '' => $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]} + }; + } + } + + # 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 ) + : ( ! 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 { diff --git a/t/search/stack_cond.t b/t/search/stack_cond.t index a68f692..d43f274 100644 --- a/t/search/stack_cond.t +++ b/t/search/stack_cond.t @@ -35,8 +35,10 @@ for my $c ( my $bare_cond = is_literal_value($c->{cond}) ? { '=', $c->{cond} } : $c->{cond}; my @query_steps = ( - # this is a monkey-wrench, always there + # these are monkey-wrenches, always there { title => { '!=', [ -and => \'bar' ] }, year => { '!=', [ -and => 'bar' ] } }, + { -or => [ genreid => undef, genreid => { '!=' => \42 } ] }, + { -or => [ genreid => undef, genreid => { '!=' => \42 } ] }, { title => $bare_cond, year => { '=', $c->{cond} } }, { -and => [ year => $bare_cond, { title => { '=', $c->{cond} } } ] }, @@ -69,7 +71,18 @@ for my $c ( "( SELECT me.title FROM cd me - WHERE title != bar AND title $c->{sql} AND year != ? AND year $c->{sql} + WHERE + ( genreid != 42 OR genreid IS NULL ) + AND + ( genreid != 42 OR genreid IS NULL ) + AND + title != bar + AND + title $c->{sql} + AND + year != ? + AND + year $c->{sql} )", \@bind, 'Double condition correctly collapsed for steps' . Dumper \@query_steps, diff --git a/t/sqlmaker/dbihacks_internals.t b/t/sqlmaker/dbihacks_internals.t index 1ad550a..ced331f 100644 --- a/t/sqlmaker/dbihacks_internals.t +++ b/t/sqlmaker/dbihacks_internals.t @@ -117,6 +117,105 @@ for my $t ( rank => undef, }, }, + (map { { + where => $_, + sql => 'WHERE (rank = 13 OR charfield IS NULL OR artistid = ?) AND (artistid = ? OR charfield IS NULL OR rank != 42)', + collapsed_sql => 'WHERE (artistid = ? OR charfield IS NULL OR rank = 13) AND (artistid = ? OR charfield IS NULL OR rank != 42)', + cc_result => { -and => [ + { -or => [ artistid => 1, charfield => undef, rank => { '=' => \13 } ] }, + { -or => [ artistid => 1, charfield => undef, rank => { '!=' => \42 } ] }, + ] }, + efcc_result => {}, + efcc_n_result => {}, + } } ( + { -and => [ + -or => [ rank => { '=' => \13 }, charfield => { '=' => undef }, artistid => 1 ], + -or => { artistid => { '=' => 1 }, charfield => undef, rank => { '!=' => \42 } }, + ] }, + + { + -OR => [ rank => { '=' => \13 }, charfield => { '=' => undef }, artistid => 1 ], + -or => { artistid => { '=' => 1 }, charfield => undef, rank => { '!=' => \42 } }, + }, + ) ), + { + where => { -or => [ rank => { '=' => \13 }, charfield => { '=' => undef }, artistid => { '=' => 1 }, genreid => { '=' => \['?', 2] } ] }, + sql => 'WHERE rank = 13 OR charfield IS NULL OR artistid = ? OR genreid = ?', + collapsed_sql => 'WHERE artistid = ? OR charfield IS NULL OR genreid = ? OR rank = 13', + cc_result => { -or => [ artistid => 1, charfield => undef, genreid => { '=' => \['?', 2] }, rank => { '=' => \13 } ] }, + efcc_result => {}, + efcc_n_result => {}, + }, + { + where => { -and => [ + -or => [ rank => { '=' => \13 }, charfield => { '=' => undef }, artistid => 1 ], + -or => { artistid => { '=' => 1 }, charfield => undef, rank => { '=' => \13 } }, + ] }, + cc_result => { -and => [ + { -or => [ artistid => 1, charfield => undef, rank => { '=' => \13 } ] }, + { -or => [ artistid => 1, charfield => undef, rank => { '=' => \13 } ] }, + ] }, + sql => 'WHERE (rank = 13 OR charfield IS NULL OR artistid = ?) AND (artistid = ? OR charfield IS NULL OR rank = 13)', + collapsed_sql => 'WHERE (artistid = ? OR charfield IS NULL OR rank = 13) AND (artistid = ? OR charfield IS NULL OR rank = 13)', + efcc_result => {}, + efcc_n_result => {}, + }, + { + where => { -and => [ + -or => [ rank => { '=' => \13 }, charfield => { '=' => undef }, artistid => 1 ], + -or => { artistid => { '=' => 1 }, charfield => undef, rank => { '!=' => \42 } }, + -and => [ foo => { '=' => \1 }, bar => 2 ], + -and => [ foo => 3, bar => { '=' => \4 } ], + -exists => \'(SELECT 1)', + -exists => \'(SELECT 2)', + -not => { foo => 69 }, + -not => { foo => 42 }, + ]}, + sql => 'WHERE + ( rank = 13 OR charfield IS NULL OR artistid = ? ) + AND ( artistid = ? OR charfield IS NULL OR rank != 42 ) + AND foo = 1 + AND bar = ? + AND foo = ? + AND bar = 4 + AND (EXISTS (SELECT 1)) + AND (EXISTS (SELECT 2)) + AND NOT foo = ? + AND NOT foo = ? + ', + collapsed_sql => 'WHERE + ( artistid = ? OR charfield IS NULL OR rank = 13 ) + AND ( artistid = ? OR charfield IS NULL OR rank != 42 ) + AND (EXISTS (SELECT 1)) + AND (EXISTS (SELECT 2)) + AND NOT foo = ? + AND NOT foo = ? + AND bar = 4 + AND bar = ? + AND foo = 1 + AND foo = ? + ', + cc_result => { + -and => [ + { -or => [ artistid => 1, charfield => undef, rank => { '=' => \13 } ] }, + { -or => [ artistid => 1, charfield => undef, rank => { '!=' => \42 } ] }, + { -exists => \'(SELECT 1)' }, + { -exists => \'(SELECT 2)' }, + { -not => { foo => 69 } }, + { -not => { foo => 42 } }, + ], + foo => [ -and => { '=' => \1 }, 3 ], + bar => [ -and => { '=' => \4 }, 2 ], + }, + efcc_result => { + foo => UNRESOLVABLE_CONDITION, + bar => UNRESOLVABLE_CONDITION, + }, + efcc_n_result => { + foo => UNRESOLVABLE_CONDITION, + bar => UNRESOLVABLE_CONDITION, + }, + }, { where => { -and => [ [ '_macro.to' => { -like => '%correct%' }, '_wc_macros.to' => { -like => '%correct%' } ],