Fix condition collapser corrupting -X operators
Peter Rabbitson [Wed, 3 Sep 2014 10:44:46 +0000 (12:44 +0200)]
This is (fingercross) the complete fix for RT#98161

lib/DBIx/Class/Storage/DBIHacks.pm
t/search/stack_cond.t
t/sqlmaker/dbihacks_internals.t

index e172299..da09d12 100644 (file)
@@ -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 {
index a68f692..d43f274 100644 (file)
@@ -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,
index 1ad550a..ced331f 100644 (file)
@@ -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%' } ],