Add an explicit deduplication of identical condition in cond normalizer
Peter Rabbitson [Sat, 7 Nov 2015 10:49:37 +0000 (11:49 +0100)]
In order to make everything work consistently add a "lax serializer" based
on Data::Dumper, as Storable is sensitive to IV vs PVIV differences.

While at it tighten up the serialize/dump env in DBIC::_Util

lib/DBIx/Class/SQLMaker/Util.pm
lib/DBIx/Class/_Util.pm
t/52leaks.t
t/search/stack_cond.t
xt/extra/internals/sqla_condition_parsers.t

index e538843..f029e24 100644 (file)
@@ -13,7 +13,71 @@ our @EXPORT_OK = qw(
 use DBIx::Class::Carp;
 use Carp 'croak';
 use SQL::Abstract qw( is_literal_value is_plain_value );
-use DBIx::Class::_Util qw( UNRESOLVABLE_CONDITION serialize dump_value );
+use DBIx::Class::_Util qw( UNRESOLVABLE_CONDITION dump_value modver_gt_or_eq );
+
+# Can not use DBIx::Class::_Util::serialize as it is based on
+# Storable and leaks through differences between PVIV and an identical IV
+# Since SQLA itself is lossy in this regard (it does not make proper copies
+# for efficiency) one could end up in a situation where semantically
+# identical values aren't treated as such
+my $dd_obj;
+sub lax_serialize ($) {
+  my $dump_str = (
+    $dd_obj
+      ||=
+    do {
+      require Data::Dumper;
+
+      # Warnings without this on early loads under -w
+      # Why? Because fuck me, that's why :/
+      local $Data::Dumper::Indent = 0
+        unless defined $Data::Dumper::Indent;
+
+      # Make sure each option is spelled out with a value, so that
+      # global environment changes can not override any of these
+      # between two serialization calls
+      #
+      my $d = Data::Dumper->new([])
+        ->Indent('0')
+        ->Purity(0)
+        ->Pad('')
+        ->Useqq(0)
+        ->Terse(1)
+        ->Freezer('')
+        ->Toaster('')
+        ->Deepcopy(0)
+        ->Quotekeys(0)
+        ->Bless('bless')
+        ->Pair(' => ')
+        ->Maxdepth(0)
+        ->Useperl(0)
+        ->Sortkeys(1)
+        ->Deparse(0)
+      ;
+
+      # FIXME - this is kinda ridiculous - there ought to be a
+      # Data::Dumper->new_with_defaults or somesuch...
+      #
+      if( modver_gt_or_eq ( 'Data::Dumper', '2.136' ) ) {
+        $d->Sparseseen(1);
+
+        if( modver_gt_or_eq ( 'Data::Dumper', '2.153' ) ) {
+          $d->Maxrecurse(1000);
+
+          if( modver_gt_or_eq ( 'Data::Dumper', '2.160' ) ) {
+            $d->Trailingcomma(0);
+          }
+        }
+      }
+
+      $d;
+    }
+  )->Values([$_[0]])->Dump;
+
+  $dd_obj->Reset->Values([]);
+
+  $dump_str;
+}
 
 
 # Attempts to flatten a passed in SQLA condition as much as possible towards
@@ -81,7 +145,7 @@ sub normalize_sqla_condition {
         push @{$fin->{-and}}, $c;
       }
       else {
-        for my $col (sort keys %$c) {
+        for my $col (keys %$c) {
 
           # consolidate all -and nodes
           if ($col =~ /^\-and$/i) {
@@ -108,6 +172,17 @@ sub normalize_sqla_condition {
         }
       }
     }
+
+    # a deduplication (and sort) pass on all individual -and/-or members
+    for my $op (qw( -and -or )) {
+      if( @{ $fin->{$op} || [] } > 1 ) {
+        my $seen_chunks = { map {
+          lax_serialize($_) => $_
+        } @{$fin->{$op}} };
+
+        $fin->{$op} = [ @{$seen_chunks}{ sort keys %$seen_chunks } ];
+      }
+    }
   }
   elsif (ref $where eq 'ARRAY') {
     # we are always at top-level here, it is safe to dump empty *standalone* pieces
@@ -132,21 +207,21 @@ sub normalize_sqla_condition {
 
         my @keys = keys %$sub_elt;
         if ( @keys == 1 and $keys[0] !~ /^\-/ ) {
-          $fin_idx->{ "COL_$keys[0]_" . serialize $sub_elt } = $sub_elt;
+          $fin_idx->{ "COL_$keys[0]_" . lax_serialize $sub_elt } = $sub_elt;
         }
         else {
-          $fin_idx->{ "SER_" . serialize $sub_elt } = $sub_elt;
+          $fin_idx->{ "SER_" . lax_serialize $sub_elt } = $sub_elt;
         }
       }
       elsif (! length ref $where->[$i] ) {
         my $sub_elt = normalize_sqla_condition({ @{$where}[$i, $i+1] })
           or next;
 
-        $fin_idx->{ "COL_$where->[$i]_" . serialize $sub_elt } = $sub_elt;
+        $fin_idx->{ "COL_$where->[$i]_" . lax_serialize $sub_elt } = $sub_elt;
         $i++;
       }
       else {
-        $fin_idx->{ "SER_" . serialize $where->[$i] } = normalize_sqla_condition( $where->[$i] ) || next;
+        $fin_idx->{ "SER_" . lax_serialize $where->[$i] } = normalize_sqla_condition( $where->[$i] ) || next;
       }
     }
 
@@ -234,7 +309,7 @@ sub normalize_sqla_condition {
     my $val_bag = { map {
       (! defined $_ )                          ? ( UNDEF => undef )
     : ( ! length ref $_ or is_plain_value $_ ) ? ( "VAL_$_" => $_ )
-    : ( ( 'SER_' . serialize $_ ) => $_ )
+    : ( ( 'SER_' . lax_serialize $_ ) => $_ )
     } @{$fin->{$col}}[1 .. $#{$fin->{$col}}] };
 
     if (keys %$val_bag == 1 ) {
@@ -414,7 +489,7 @@ sub extract_equality_conditions {
           is_literal_value($v->{'='})
         )
        ) {
-        $vals->{ 'SER_' . serialize $v->{'='} } = $v->{'='};
+        $vals->{ 'SER_' . lax_serialize $v->{'='} } = $v->{'='};
       }
     }
     elsif (
@@ -431,7 +506,7 @@ sub extract_equality_conditions {
         $vals->{
           ! defined $subval->{$c}                                        ? 'UNDEF'
         : ( ! length ref $subval->{$c} or is_plain_value $subval->{$c} ) ? "VAL_$subval->{$c}"
-        : ( 'SER_' . serialize $subval->{$c} )
+        : ( 'SER_' . lax_serialize $subval->{$c} )
         } = $subval->{$c};
       }
     }
index ac3a937..7d4a407 100644 (file)
@@ -352,7 +352,19 @@ sub set_subname ($$) {
 }
 
 sub serialize ($) {
+  # stable hash order
   local $Storable::canonical = 1;
+
+  # explicitly false - there is nothing sensible that can come out of
+  # an attempt at CODE serialization
+  local $Storable::Deparse;
+
+  # take no chances
+  local $Storable::forgive_me;
+
+  # FIXME
+  # A number of codepaths *expect* this to be Storable.pm-based so that
+  # the STORABLE_freeze hooks in the metadata subtree get executed properly
   nfreeze($_[0]);
 }
 
@@ -388,9 +400,20 @@ sub dump_value ($) {
         ->Deparse(1)
       ;
 
-      $d->Sparseseen(1) if modver_gt_or_eq (
-        'Data::Dumper', '2.136'
-      );
+      # FIXME - this is kinda ridiculous - there ought to be a
+      # Data::Dumper->new_with_defaults or somesuch...
+      #
+      if( modver_gt_or_eq ( 'Data::Dumper', '2.136' ) ) {
+        $d->Sparseseen(1);
+
+        if( modver_gt_or_eq ( 'Data::Dumper', '2.153' ) ) {
+          $d->Maxrecurse(1000);
+
+          if( modver_gt_or_eq ( 'Data::Dumper', '2.160' ) ) {
+            $d->Trailingcomma(1);
+          }
+        }
+      }
 
       $d;
     }
@@ -723,11 +746,10 @@ sub modver_gt_or_eq ($$) {
   croak "Nonsensical minimum version supplied"
     if ! defined $ver or $ver !~ $ver_rx;
 
-  no strict 'refs';
-  my $ver_cache = ${"${mod}::__DBIC_MODULE_VERSION_CHECKS__"} ||= ( $mod->VERSION
-    ? {}
-    : croak "$mod does not seem to provide a version (perhaps it never loaded)"
-  );
+  my $ver_cache = do {
+    no strict 'refs';
+    ${"${mod}::__DBIC_MODULE_VERSION_CHECKS__"} ||= {}
+  };
 
   ! defined $ver_cache->{$ver}
     and
@@ -736,6 +758,18 @@ sub modver_gt_or_eq ($$) {
     local $SIG{__WARN__} = sigwarn_silencer( qr/\Qisn't numeric in subroutine entry/ )
       if SPURIOUS_VERSION_CHECK_WARNINGS;
 
+    # prevent captures by potential __WARN__ hooks or the like:
+    # there is nothing of value that can be happening here, and
+    # leaving a hook in-place can only serve to fail some test
+    local $SIG{__WARN__} if (
+      ! SPURIOUS_VERSION_CHECK_WARNINGS
+        and
+      $SIG{__WARN__}
+    );
+
+    croak "$mod does not seem to provide a version (perhaps it never loaded)"
+      unless $mod->VERSION;
+
     local $SIG{__DIE__} if $SIG{__DIE__};
     local $@;
     eval { $mod->VERSION($ver) } ? 1 : 0;
index ae96a21..bd159a7 100644 (file)
@@ -461,6 +461,15 @@ for my $addr (keys %$weak_registry) {
     delete $weak_registry->{$addr}
       unless $cleared->{bheos_pptiehinthashfieldhash}++;
   }
+  elsif (
+    $names =~ /^Data::Dumper/m
+      and
+    $weak_registry->{$addr}{stacktrace} =~ /\bDBIx::Class::SQLMaker::Util::lax_serialize\b/
+  ) {
+    # only clear one object of a specific behavior - more would indicate trouble
+    delete $weak_registry->{$addr}
+      unless $cleared->{dd_lax_serializer}++;
+  }
   elsif ($names =~ /^DateTime::TimeZone::UTC/m) {
     # DT is going through a refactor it seems - let it leak zones for now
     delete $weak_registry->{$addr};
index 497b698..6989c6f 100644 (file)
@@ -72,9 +72,7 @@ for my $c (
       SELECT me.title
         FROM cd me
       WHERE
-        ( genreid != 42 OR genreid IS NULL )
-          AND
-        ( genreid != 42 OR genreid IS NULL )
+        ( genreid IS NULL OR genreid != 42 )
           AND
         title != bar
           AND
@@ -85,7 +83,7 @@ for my $c (
         year $c->{sql}
     )",
     \@bind,
-    'Double condition correctly collapsed for steps' . dump_value \@query_steps,
+    'Double condition correctly collapsed for steps:' . join( '', map { "\n\t" . dump_value($_) } @query_steps ),
   );
 }
 
index 14a2c31..98a76b0 100644 (file)
@@ -34,6 +34,9 @@ my $num = bless( \do { my $foo = 69 }, 'DBICTest::SillyInt' );
 is($num, 69, 'test overloaded object is "sane"');
 is("$num", 69, 'test overloaded object is "sane"');
 
+my $AttUQoLtUaE = 42;
+my $PVIVmaker = $AttUQoLtUaE . '';
+
 my @tests = (
   {
     where => { artistid => 1, charfield => undef },
@@ -89,9 +92,9 @@ my @tests = (
   },
   {
     where => { -and => [ \'foo=bar',  [ { artistid => { '=', $num } } ], { name => 'Caterwauler McCrae'}, \'buzz=bozz' ] },
-    normalized => { -and => [ \'foo=bar', \'buzz=bozz' ], name => 'Caterwauler McCrae', artistid => $num },
+    normalized => { -and => [ \'buzz=bozz', \'foo=bar' ], name => 'Caterwauler McCrae', artistid => $num },
     sql =>            'WHERE foo=bar AND artistid = ? AND name = ? AND buzz=bozz',
-    normalized_sql => 'WHERE foo=bar AND buzz=bozz AND artistid = ? AND name = ?',
+    normalized_sql => 'WHERE buzz=bozz AND foo=bar AND artistid = ? AND name = ?',
     equality_extract => { name => 'Caterwauler McCrae', artistid => $num },
   },
   {
@@ -110,9 +113,8 @@ my @tests = (
   },
   {
     where => { artistid => { '=' => [ 1 ], }, charfield => { '=' => [ -AND => \'1', \['?',2] ] }, rank => { '=' => [ -OR => $num, $num ] } },
-    normalized => { artistid => 1, charfield => [-and => { '=' => \['?',2] }, { '=' => \'1' } ], rank => { '=' => [$num, $num] } },
+    normalized => { artistid => 1, charfield => [-and => { '=' => \'1' }, { '=' => \['?',2] } ], rank => { '=' => [$num, $num] } },
     sql =>            'WHERE artistid = ? AND charfield = 1 AND charfield = ? AND ( rank = ? OR rank = ? )',
-    normalized_sql => 'WHERE artistid = ? AND charfield = ? AND charfield = 1 AND ( rank = ? OR rank = ? )',
     equality_extract => { artistid => 1, charfield => UNRESOLVABLE_CONDITION },
   },
   {
@@ -135,10 +137,10 @@ my @tests = (
   (map { {
     where => $_,
     sql =>            'WHERE (rank = 13 OR charfield IS NULL OR artistid = ?) AND (artistid = ? OR charfield IS NULL OR rank != 42)',
-    normalized_sql => 'WHERE (artistid = ? OR charfield IS NULL OR rank = 13) AND (artistid = ? OR charfield IS NULL OR rank != 42)',
+    normalized_sql => 'WHERE (artistid = ? OR charfield IS NULL OR rank != 42) AND (artistid = ? OR charfield IS NULL OR rank = 13)',
     normalized => { -and => [
-      { -or => [ artistid => 1, charfield => undef, rank => { '=' => \13 } ] },
       { -or => [ artistid => 1, charfield => undef, rank => { '!=' => \42 } ] },
+      { -or => [ artistid => 1, charfield => undef, rank => { '=' => \13 } ] },
     ] },
     equality_extract => {},
     equality_considering_nulls_extract => {},
@@ -146,7 +148,7 @@ my @tests = (
 
     { -and => [
       -or => [ rank => { '=' => \13 }, charfield => { '=' => undef }, artistid => 1 ],
-      -or => { artistid => { '=' => 1 }, charfield => undef, rank => { '!=' => \42 } },
+      -or => { artistid => { '=' => 1 }, charfield => undef, rank => { '!=' => \$AttUQoLtUaE } },
     ] },
 
     {
@@ -182,20 +184,34 @@ my @tests = (
   },
   {
     where => { -and => [
-      -or => [ rank => { '=' => \13 }, charfield => { '=' => undef }, artistid => 1 ],
-      -or => { artistid => { '=' => 1 }, charfield => undef, rank => { '=' => \13 } },
-    ] },
-    normalized => { -and => [
-      { -or => [ artistid => 1, charfield => undef, rank => { '=' => \13 } ] },
-      { -or => [ artistid => 1, charfield => undef, rank => { '=' => \13 } ] },
+      -or => [ rank => { '=' => \$AttUQoLtUaE }, charfield => { '=' => undef }, artistid => 1 ],
+      -or => { artistid => { '=' => 1 }, charfield => undef, rank => { '=' => \42 } },
     ] },
-    sql =>            'WHERE (rank = 13 OR charfield IS NULL OR artistid = ?) AND (artistid = ? OR charfield IS NULL OR rank = 13)',
-    normalized_sql => 'WHERE (artistid = ? OR charfield IS NULL OR rank = 13) AND (artistid = ? OR charfield IS NULL OR rank = 13)',
+    normalized => {
+      -or => [ artistid => 1, charfield => undef, rank => { '=' => \42 } ],
+    },
+    sql =>            'WHERE (rank = 42 OR charfield IS NULL OR artistid = ?) AND (artistid = ? OR charfield IS NULL OR rank = 42)',
+    normalized_sql => 'WHERE artistid = ? OR charfield IS NULL OR rank = 42',
     equality_extract => {},
     equality_considering_nulls_extract => {},
   },
   {
     where => { -and => [
+      { -or => [ \42 ] },
+      { -and => [
+        { -or => [ \$AttUQoLtUaE ] },
+        { -or => [ \13 ] },
+      ] },
+    ] },
+    normalized => {
+      -and => [ \13, \42 ],
+    },
+    sql             => 'WHERE 42 AND 42 AND 13',
+    normalized_sql  => 'WHERE 13 AND 42',
+    equality_extract => {},
+  },
+  {
+    where => { -and => [
       -or => [ rank => { '=' => \13 }, charfield => { '=' => undef }, artistid => 1 ],
       -or => { artistid => { '=' => 1 }, charfield => undef, rank => { '!=' => \42 } },
       -and => [ foo => { '=' => \1 }, bar => 2 ],
@@ -218,12 +234,12 @@ my @tests = (
       AND NOT foo = ?
     ',
     normalized_sql => 'WHERE
-          ( artistid = ? OR charfield IS NULL OR rank = 13 )
-      AND ( artistid = ? OR charfield IS NULL OR rank != 42 )
-      AND (EXISTS (SELECT 1))
+          (EXISTS (SELECT 1))
       AND (EXISTS (SELECT 2))
       AND NOT foo = ?
       AND NOT foo = ?
+      AND ( artistid = ? OR charfield IS NULL OR rank != 42 )
+      AND ( artistid = ? OR charfield IS NULL OR rank = 13 )
       AND bar = 4
       AND bar = ?
       AND foo = 1
@@ -231,12 +247,12 @@ my @tests = (
     ',
     normalized => {
       -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 } },
+        { -not => { foo => 69 } },
+        { -or => [ artistid => 1, charfield => undef, rank => { '!=' => \42 } ] },
+        { -or => [ artistid => 1, charfield => undef, rank => { '=' => \13 } ] },
       ],
       foo => [ -and => { '=' => \1 }, 3 ],
       bar => [ -and => { '=' => \4 }, 2 ],
@@ -418,13 +434,14 @@ my @tests = (
         \'baz = ber',
       ],
     },
-    sql => 'WHERE foo = bar AND baz = ber',
     normalized => {
       -and => [
-        \'foo = bar',
         \'baz = ber',
+        \'foo = bar',
       ],
     },
+    sql            => 'WHERE foo = bar AND baz = ber',
+    normalized_sql => 'WHERE baz = ber AND foo = bar',
     equality_extract => {},
   },
   {
@@ -435,14 +452,15 @@ my @tests = (
         x => { -ident => 'y' },
       ],
     },
-    sql => 'WHERE foo = bar AND baz = ber AND x = y',
     normalized => {
       -and => [
-        \'foo = bar',
         \'baz = ber',
+        \'foo = bar',
       ],
       x => { '=' => { -ident => 'y' } }
     },
+    sql            => 'WHERE foo = bar AND baz = ber AND x = y',
+    normalized_sql => 'WHERE baz = ber AND foo = bar AND x = y',
     equality_extract => { x => { -ident => 'y' } },
   },
 );