Rewrite the collapsing rowparser pigeonholing logic
Peter Rabbitson [Thu, 26 Feb 2015 12:51:49 +0000 (13:51 +0100)]
Instead of dynamically determining whether to use the data as a key directly,
always compose a list of copied 'row ids' and use that in all operations.

This is mainly done to stop dualvar-ing of numeric variables returned by the
engine, thus confusing JSON and similar serializers down the road

lib/DBIx/Class/ResultSource/RowParser/Util.pm
t/resultset/rowparser_internals.t

index 732bb50..fcfd100 100644 (file)
@@ -114,10 +114,7 @@ sub __visit_infmap_simple {
 sub assemble_collapsing_parser {
   my $args = shift;
 
-  # it may get unset further down
-  my $no_rowid_container = $args->{prune_null_branches};
-
-  my ($top_node_key, $top_node_key_assembler);
+  my ($top_node_key, $top_node_key_assembler, $variant_idcols);
 
   if (scalar @{$args->{collapse_map}{-identifying_columns}}) {
     $top_node_key = join ('', map
@@ -130,7 +127,7 @@ sub assemble_collapsing_parser {
     my @path_parts = map { sprintf
       "( ( defined '\xFF__VALPOS__%d__\xFF' ) && (join qq(\xFF), '', %s, '') )",
       $_->[0],  # checking just first is enough - one ID defined, all defined
-      ( join ', ', map { "'\xFF__VALPOS__${_}__\xFF'" } @$_ ),
+      ( join ', ', map { ++$variant_idcols->{$_} and "'\xFF__IDVALPOS__${_}__\xFF'" } @$_ ),
     } @variants;
 
     my $virtual_column_idx = (scalar keys %{$args->{val_index}} ) + 1;
@@ -146,8 +143,6 @@ sub assemble_collapsing_parser {
       %{$args->{collapse_map}},
       -custom_node_key => $top_node_key,
     };
-
-    $no_rowid_container = 0;
   }
   else {
     die('Unexpected collapse map contents');
@@ -155,23 +150,30 @@ sub assemble_collapsing_parser {
 
   my ($data_assemblers, $stats) = __visit_infmap_collapse ($args);
 
-  my @idcol_args = $no_rowid_container ? ('', '') : (
-    ', %cur_row_ids', # only declare the variable if we'll use it
-    join ("\n", map {
-      my $quoted_null_val = qq( "\0NULL\xFF\${rows_pos}\xFF${_}\0" );
-      qq(\$cur_row_ids{$_} = ) . (
-        # in case we prune - we will never hit these undefs
-        $args->{prune_null_branches} ? qq( \$cur_row_data->[$_]; )
-        : HAS_DOR                    ? qq( \$cur_row_data->[$_] // $quoted_null_val; )
-        :                              qq( defined(\$cur_row_data->[$_]) ? \$cur_row_data->[$_] : $quoted_null_val; )
-      )
-    } sort { $a <=> $b } keys %{ $stats->{idcols_seen} } ),
-  );
-
-  my $parser_src = sprintf (<<'EOS', @idcol_args, $top_node_key_assembler||'', $top_node_key, join( "\n", @{$data_assemblers||[]} ) );
+  # variants do not necessarily overlap with true idcols
+  my @row_ids = sort { $a <=> $b } keys %{ {
+    %{ $variant_idcols || {} },
+    %{ $stats->{idcols_seen} },
+  } };
+
+  my $row_id_defs = sprintf "\@cur_row_ids{( %s )} = ( \n%s \n );",
+    join (', ', @row_ids ),
+    # in case we prune - we will never hit undefs/NULLs as pigeon-hole-criteria
+    ( $args->{prune_null_branches}
+      ? sprintf( '@{$cur_row_data}[( %s )]', join ', ', @row_ids )
+      : join (",\n", map {
+        my $quoted_null_val = qq( "\0NULL\xFF\${rows_pos}\xFF${_}\0" );
+        HAS_DOR
+          ? qq! ( \$cur_row_data->[$_] // $quoted_null_val ) !
+          : qq! ( defined(\$cur_row_data->[$_]) ? \$cur_row_data->[$_] : $quoted_null_val ) !
+      } @row_ids)
+    )
+  ;
+
+  my $parser_src = sprintf (<<'EOS', $row_id_defs, $top_node_key_assembler||'', $top_node_key, join( "\n", @{$data_assemblers||[]} ) );
 ### BEGIN LITERAL STRING EVAL
   my $rows_pos = 0;
-  my ($result_pos, @collapse_idx, $cur_row_data %1$s);
+  my ($result_pos, @collapse_idx, $cur_row_data, %%cur_row_ids );
 
   # this loop is a bit arcane - the rationale is that the passed in
   # $_[0] will either have only one row (->next) or will have all
@@ -185,27 +187,26 @@ sub assemble_collapsing_parser {
     ( $_[1] and $rows_pos = -1 and $_[1]->() )
   ) ) {
 
-    # this code exists only when we are using a cur_row_ids
-    # furthermore the undef checks may or may not be there
+    # the undef checks may or may not be there
     # depending on whether we prune or not
     #
     # due to left joins some of the ids may be NULL/undef, and
     # won't play well when used as hash lookups
     # we also need to differentiate NULLs on per-row/per-col basis
     # (otherwise folding of optional 1:1s will be greatly confused
-%2$s
+%1$s
 
     # in the case of an underdefined root - calculate the virtual id (otherwise no code at all)
-%3$s
+%2$s
 
     # if we were supplied a coderef - we are collapsing lazily (the set
     # is ordered properly)
     # as long as we have a result already and the next result is new we
     # return the pre-read data and bail
-$_[1] and $result_pos and ! $collapse_idx[0]%4$s and (unshift @{$_[2]}, $cur_row_data) and last;
+$_[1] and $result_pos and ! $collapse_idx[0]%3$s and (unshift @{$_[2]}, $cur_row_data) and last;
 
     # the rel assemblers
-%5$s
+%4$s
 
   }
 
@@ -219,7 +220,7 @@ EOS
   $parser_src =~ s/
     \' \xFF__IDVALPOS__(\d+)__\xFF \'
   /
-    $no_rowid_container ? "\$cur_row_data->[$1]" : "\$cur_row_ids{$1}"
+    "\$cur_row_ids{$1}"
   /gex;
 
   __wrap_in_strictured_scope($parser_src);
index d1b4f07..69b2e64 100644 (file)
@@ -257,11 +257,13 @@ is_same_src (
       ( $_[1] and $rows_pos = -1 and $_[1]->() )
     ) ) {
 
-      $cur_row_ids{0} = $cur_row_data->[0] // "\0NULL\xFF$rows_pos\xFF0\0";
-      $cur_row_ids{1} = $cur_row_data->[1] // "\0NULL\xFF$rows_pos\xFF1\0";
-      $cur_row_ids{3} = $cur_row_data->[3] // "\0NULL\xFF$rows_pos\xFF3\0";
-      $cur_row_ids{4} = $cur_row_data->[4] // "\0NULL\xFF$rows_pos\xFF4\0";
-      $cur_row_ids{5} = $cur_row_data->[5] // "\0NULL\xFF$rows_pos\xFF5\0";
+      @cur_row_ids{0,1,3,4,5} = (
+        ( $cur_row_data->[0] // "\0NULL\xFF$rows_pos\xFF0\0" ),
+        ( $cur_row_data->[1] // "\0NULL\xFF$rows_pos\xFF1\0" ),
+        ( $cur_row_data->[3] // "\0NULL\xFF$rows_pos\xFF3\0" ),
+        ( $cur_row_data->[4] // "\0NULL\xFF$rows_pos\xFF4\0" ),
+        ( $cur_row_data->[5] // "\0NULL\xFF$rows_pos\xFF5\0" ),
+      );
 
       # a present cref in $_[1] implies lazy prefetch, implies a supplied stash in $_[2]
       $_[1] and $result_pos and ! $collapse_idx[0]{$cur_row_ids{4}}{$cur_row_ids{5}} and (unshift @{$_[2]}, $cur_row_data) and last;
@@ -309,7 +311,7 @@ is_same_src (
     prune_null_branches => 1,
   }))[0],
   ' my $rows_pos = 0;
-    my ($result_pos, @collapse_idx, $cur_row_data);
+    my ($result_pos, @collapse_idx, $cur_row_data, %cur_row_ids);
 
     while ($cur_row_data = (
       ( $rows_pos >= 0 and $_[0][$rows_pos++] )
@@ -317,38 +319,40 @@ is_same_src (
       ( $_[1] and $rows_pos = -1 and $_[1]->() )
     ) ) {
 
+      @cur_row_ids{0, 1, 3, 4, 5} = @{$cur_row_data}[0, 1, 3, 4, 5];
+
       # a present cref in $_[1] implies lazy prefetch, implies a supplied stash in $_[2]
-      $_[1] and $result_pos and ! $collapse_idx[0]{$cur_row_data->[4]}{$cur_row_data->[5]} and (unshift @{$_[2]}, $cur_row_data) and last;
+      $_[1] and $result_pos and ! $collapse_idx[0]{$cur_row_ids{4}}{$cur_row_ids{5}} and (unshift @{$_[2]}, $cur_row_data) and last;
 
       # the rowdata itself for root node
-      $collapse_idx[0]{$cur_row_data->[4]}{$cur_row_data->[5]} //= $_[0][$result_pos++] = { artist => $cur_row_data->[5], title => $cur_row_data->[4], year => $cur_row_data->[2] };
+      $collapse_idx[0]{$cur_row_ids{4}}{$cur_row_ids{5}} //= $_[0][$result_pos++] = { artist => $cur_row_data->[5], title => $cur_row_data->[4], year => $cur_row_data->[2] };
 
       # prefetch data of single_track (placed in root)
-      (! defined($cur_row_data->[1]) ) ? $collapse_idx[0]{$cur_row_data->[4]}{$cur_row_data->[5]}{single_track} = undef : do {
-        $collapse_idx[0]{$cur_row_data->[4]}{$cur_row_data->[5]}{single_track} //= $collapse_idx[1]{$cur_row_data->[1]}{$cur_row_data->[4]}{$cur_row_data->[5]};
+      (! defined($cur_row_data->[1]) ) ? $collapse_idx[0]{$cur_row_ids{4}}{$cur_row_ids{5}}{single_track} = undef : do {
+        $collapse_idx[0]{$cur_row_ids{4}}{$cur_row_ids{5}}{single_track} //= $collapse_idx[1]{$cur_row_ids{1}}{$cur_row_ids{4}}{$cur_row_ids{5}};
 
         # prefetch data of cd (placed in single_track)
-        $collapse_idx[1]{$cur_row_data->[1]}{$cur_row_data->[4]}{$cur_row_data->[5]}{cd} //= $collapse_idx[2]{$cur_row_data->[1]}{$cur_row_data->[4]}{$cur_row_data->[5]};
+        $collapse_idx[1]{$cur_row_ids{1}}{$cur_row_ids{4}}{$cur_row_ids{5}}{cd} //= $collapse_idx[2]{$cur_row_ids{1}}{$cur_row_ids{4}}{$cur_row_ids{5}};
 
         # prefetch data of artist ( placed in single_track->cd)
-        $collapse_idx[2]{$cur_row_data->[1]}{$cur_row_data->[4]}{$cur_row_data->[5]}{artist} //= $collapse_idx[3]{$cur_row_data->[1]}{$cur_row_data->[4]}{$cur_row_data->[5]} = { artistid => $cur_row_data->[1] };
+        $collapse_idx[2]{$cur_row_ids{1}}{$cur_row_ids{4}}{$cur_row_ids{5}}{artist} //= $collapse_idx[3]{$cur_row_ids{1}}{$cur_row_ids{4}}{$cur_row_ids{5}} = { artistid => $cur_row_data->[1] };
 
         # prefetch data of cds (if available)
-        (! defined $cur_row_data->[3] ) ? $collapse_idx[3]{$cur_row_data->[1]}{$cur_row_data->[4]}{$cur_row_data->[5]}{cds} = [] : do {
+        (! defined $cur_row_data->[3] ) ? $collapse_idx[3]{$cur_row_ids{1}}{$cur_row_ids{4}}{$cur_row_ids{5}}{cds} = [] : do {
 
-          (! $collapse_idx[4]{$cur_row_data->[1]}{$cur_row_data->[3]}{$cur_row_data->[4]}{$cur_row_data->[5]} )
+          (! $collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{3}}{$cur_row_ids{4}}{$cur_row_ids{5}} )
             and
-          push @{$collapse_idx[3]{$cur_row_data->[1]}{$cur_row_data->[4]}{$cur_row_data->[5]}{cds}}, (
-            $collapse_idx[4]{$cur_row_data->[1]}{$cur_row_data->[3]}{$cur_row_data->[4]}{$cur_row_data->[5]} = { cdid => $cur_row_data->[3] }
+          push @{$collapse_idx[3]{$cur_row_ids{1}}{$cur_row_ids{4}}{$cur_row_ids{5}}{cds}}, (
+            $collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{3}}{$cur_row_ids{4}}{$cur_row_ids{5}} = { cdid => $cur_row_data->[3] }
           );
 
           # prefetch data of tracks (if available)
-          ( ! defined $cur_row_data->[0] ) ? $collapse_idx[4]{$cur_row_data->[1]}{$cur_row_data->[3]}{$cur_row_data->[4]}{$cur_row_data->[5]}{tracks} = [] : do {
+          ( ! defined $cur_row_data->[0] ) ? $collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{3}}{$cur_row_ids{4}}{$cur_row_ids{5}}{tracks} = [] : do {
 
-            (! $collapse_idx[5]{$cur_row_data->[0]}{$cur_row_data->[1]}{$cur_row_data->[3]}{$cur_row_data->[4]}{$cur_row_data->[5]} )
+            (! $collapse_idx[5]{$cur_row_ids{0}}{$cur_row_ids{1}}{$cur_row_ids{3}}{$cur_row_ids{4}}{$cur_row_ids{5}} )
               and
-            push @{$collapse_idx[4]{$cur_row_data->[1]}{$cur_row_data->[3]}{$cur_row_data->[4]}{$cur_row_data->[5]}{tracks}}, (
-              $collapse_idx[5]{$cur_row_data->[0]}{$cur_row_data->[1]}{$cur_row_data->[3]}{$cur_row_data->[4]}{$cur_row_data->[5]} = { title => $cur_row_data->[0] }
+            push @{$collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{3}}{$cur_row_ids{4}}{$cur_row_ids{5}}{tracks}}, (
+              $collapse_idx[5]{$cur_row_ids{0}}{$cur_row_ids{1}}{$cur_row_ids{3}}{$cur_row_ids{4}}{$cur_row_ids{5}} = { title => $cur_row_data->[0] }
             );
           };
         };
@@ -434,12 +438,14 @@ is_same_src (
       ( $_[1] and $rows_pos = -1 and $_[1]->() )
     ) ) {
 
-      $cur_row_ids{0} = $cur_row_data->[0] // "\0NULL\xFF$rows_pos\xFF0\0";
-      $cur_row_ids{1} = $cur_row_data->[1] // "\0NULL\xFF$rows_pos\xFF1\0";
-      $cur_row_ids{5} = $cur_row_data->[5] // "\0NULL\xFF$rows_pos\xFF5\0";
-      $cur_row_ids{6} = $cur_row_data->[6] // "\0NULL\xFF$rows_pos\xFF6\0";
-      $cur_row_ids{8} = $cur_row_data->[8] // "\0NULL\xFF$rows_pos\xFF8\0";
-      $cur_row_ids{10} = $cur_row_data->[10] // "\0NULL\xFF$rows_pos\xFF10\0";
+      @cur_row_ids{0, 1, 5, 6, 8, 10} = (
+        $cur_row_data->[0] // "\0NULL\xFF$rows_pos\xFF0\0",
+        $cur_row_data->[1] // "\0NULL\xFF$rows_pos\xFF1\0",
+        $cur_row_data->[5] // "\0NULL\xFF$rows_pos\xFF5\0",
+        $cur_row_data->[6] // "\0NULL\xFF$rows_pos\xFF6\0",
+        $cur_row_data->[8] // "\0NULL\xFF$rows_pos\xFF8\0",
+        $cur_row_data->[10] // "\0NULL\xFF$rows_pos\xFF10\0",
+      );
 
       # a present cref in $_[1] implies lazy prefetch, implies a supplied stash in $_[2]
       $_[1] and $result_pos and ! $collapse_idx[0]{$cur_row_ids{1}} and (unshift @{$_[2]}, $cur_row_data) and last;
@@ -493,7 +499,7 @@ is_same_src (
     prune_null_branches => 1,
   }))[0],
   ' my $rows_pos = 0;
-    my ($result_pos, @collapse_idx, $cur_row_data);
+    my ($result_pos, @collapse_idx, $cur_row_data, %cur_row_ids);
 
     while ($cur_row_data = (
       ( $rows_pos >= 0 and $_[0][$rows_pos++] )
@@ -501,48 +507,50 @@ is_same_src (
       ( $_[1] and $rows_pos = -1 and $_[1]->() )
     ) ) {
 
+      @cur_row_ids{( 0, 1, 5, 6, 8, 10 )} = @{$cur_row_data}[( 0, 1, 5, 6, 8, 10 )];
+
       # a present cref in $_[1] implies lazy prefetch, implies a supplied stash in $_[2]
-      $_[1] and $result_pos and ! $collapse_idx[0]{$cur_row_data->[1]} and (unshift @{$_[2]}, $cur_row_data) and last;
+      $_[1] and $result_pos and ! $collapse_idx[0]{$cur_row_ids{1}} and (unshift @{$_[2]}, $cur_row_data) and last;
 
-      $collapse_idx[0]{$cur_row_data->[1]} //= $_[0][$result_pos++] = [{ genreid => $cur_row_data->[4], latest_cd => $cur_row_data->[7], year => $cur_row_data->[3] }];
+      $collapse_idx[0]{$cur_row_ids{1}} //= $_[0][$result_pos++] = [{ genreid => $cur_row_data->[4], latest_cd => $cur_row_data->[7], year => $cur_row_data->[3] }];
 
-      $collapse_idx[0]{$cur_row_data->[1]}[1]{existing_single_track} //= $collapse_idx[1]{$cur_row_data->[1]} = [];
-      $collapse_idx[1]{$cur_row_data->[1]}[1]{cd} //= $collapse_idx[2]{$cur_row_data->[1]} = [];
-      $collapse_idx[2]{$cur_row_data->[1]}[1]{artist} //= $collapse_idx[3]{$cur_row_data->[1]} = [{ artistid => $cur_row_data->[1] }];
+      $collapse_idx[0]{$cur_row_ids{1}}[1]{existing_single_track} //= $collapse_idx[1]{$cur_row_ids{1}} = [];
+      $collapse_idx[1]{$cur_row_ids{1}}[1]{cd} //= $collapse_idx[2]{$cur_row_ids{1}} = [];
+      $collapse_idx[2]{$cur_row_ids{1}}[1]{artist} //= $collapse_idx[3]{$cur_row_ids{1}} = [{ artistid => $cur_row_data->[1] }];
 
-      (! defined($cur_row_data->[6])) ? $collapse_idx[3]{$cur_row_data->[1]}[1]{cds} = [] : do {
-        (! $collapse_idx[4]{$cur_row_data->[1]}{$cur_row_data->[6]} )
+      (! defined($cur_row_data->[6])) ? $collapse_idx[3]{$cur_row_ids{1}}[1]{cds} = [] : do {
+        (! $collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{6}} )
           and
-        push @{ $collapse_idx[3]{$cur_row_data->[1]}[1]{cds} }, (
-          $collapse_idx[4]{$cur_row_data->[1]}{$cur_row_data->[6]} = [{ cdid => $cur_row_data->[6], genreid => $cur_row_data->[9], year => $cur_row_data->[2] }]
+        push @{ $collapse_idx[3]{$cur_row_ids{1}}[1]{cds} }, (
+          $collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{6}} = [{ cdid => $cur_row_data->[6], genreid => $cur_row_data->[9], year => $cur_row_data->[2] }]
         );
 
-        (! defined($cur_row_data->[8]) ) ? $collapse_idx[4]{$cur_row_data->[1]}{$cur_row_data->[6]}[1]{tracks} = [] : do {
+        (! defined($cur_row_data->[8]) ) ? $collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{6}}[1]{tracks} = [] : do {
 
-          (! $collapse_idx[5]{$cur_row_data->[1]}{$cur_row_data->[6]}{$cur_row_data->[8]} )
+          (! $collapse_idx[5]{$cur_row_ids{1}}{$cur_row_ids{6}}{$cur_row_ids{8}} )
             and
-          push @{ $collapse_idx[4]{$cur_row_data->[1]}{$cur_row_data->[6]}[1]{tracks} }, (
-            $collapse_idx[5]{$cur_row_data->[1]}{$cur_row_data->[6]}{$cur_row_data->[8]} = [{ title => $cur_row_data->[8] }]
+          push @{ $collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{6}}[1]{tracks} }, (
+            $collapse_idx[5]{$cur_row_ids{1}}{$cur_row_ids{6}}{$cur_row_ids{8}} = [{ title => $cur_row_data->[8] }]
           );
         };
       };
 
-      (! defined($cur_row_data->[5]) ) ? $collapse_idx[0]{$cur_row_data->[1]}[1]{tracks} = [] : do {
+      (! defined($cur_row_data->[5]) ) ? $collapse_idx[0]{$cur_row_ids{1}}[1]{tracks} = [] : do {
 
-        (! $collapse_idx[6]{$cur_row_data->[1]}{$cur_row_data->[5]} )
+        (! $collapse_idx[6]{$cur_row_ids{1}}{$cur_row_ids{5}} )
           and
-        push @{ $collapse_idx[0]{$cur_row_data->[1]}[1]{tracks} }, (
-          $collapse_idx[6]{$cur_row_data->[1]}{$cur_row_data->[5]} = [{ title => $cur_row_data->[5] }]
+        push @{ $collapse_idx[0]{$cur_row_ids{1}}[1]{tracks} }, (
+          $collapse_idx[6]{$cur_row_ids{1}}{$cur_row_ids{5}} = [{ title => $cur_row_data->[5] }]
         );
 
-        (! defined($cur_row_data->[10]) ) ? $collapse_idx[6]{$cur_row_data->[1]}{$cur_row_data->[5]}[1]{lyrics} = [] : do {
+        (! defined($cur_row_data->[10]) ) ? $collapse_idx[6]{$cur_row_ids{1}}{$cur_row_ids{5}}[1]{lyrics} = [] : do {
 
-          $collapse_idx[6]{$cur_row_data->[1]}{$cur_row_data->[5]}[1]{lyrics} //= $collapse_idx[7]{$cur_row_data->[1]}{$cur_row_data->[5]}{$cur_row_data->[10]} = [];
+          $collapse_idx[6]{$cur_row_ids{1}}{$cur_row_ids{5}}[1]{lyrics} //= $collapse_idx[7]{$cur_row_ids{1}}{$cur_row_ids{5}}{$cur_row_ids{10}} = [];
 
-          (! $collapse_idx[8]{$cur_row_data->[0]}{$cur_row_data->[1]}{$cur_row_data->[5]}{$cur_row_data->[10]} )
+          (! $collapse_idx[8]{$cur_row_ids{0}}{$cur_row_ids{1}}{$cur_row_ids{5}}{$cur_row_ids{10}} )
             and
-          push @{ $collapse_idx[7]{$cur_row_data->[1]}{$cur_row_data->[5]}{$cur_row_data->[10]}[1]{existing_lyric_versions} }, (
-            $collapse_idx[8]{$cur_row_data->[0]}{$cur_row_data->[1]}{$cur_row_data->[5]}{$cur_row_data->[10]} = [{ lyric_id => $cur_row_data->[10], text => $cur_row_data->[0] }]
+          push @{ $collapse_idx[7]{$cur_row_ids{1}}{$cur_row_ids{5}}{$cur_row_ids{10}}[1]{existing_lyric_versions} }, (
+            $collapse_idx[8]{$cur_row_ids{0}}{$cur_row_ids{1}}{$cur_row_ids{5}}{$cur_row_ids{10}} = [{ lyric_id => $cur_row_data->[10], text => $cur_row_data->[0] }]
           );
         };
       };
@@ -615,17 +623,19 @@ is_same_src (
       ( $_[1] and $rows_pos = -1 and $_[1]->() )
     ) ) {
 
-      $cur_row_ids{0} = $cur_row_data->[0] // "\0NULL\xFF$rows_pos\xFF0\0";
-      $cur_row_ids{2} = $cur_row_data->[2] // "\0NULL\xFF$rows_pos\xFF2\0";
-      $cur_row_ids{3} = $cur_row_data->[3] // "\0NULL\xFF$rows_pos\xFF3\0";
-      $cur_row_ids{4} = $cur_row_data->[4] // "\0NULL\xFF$rows_pos\xFF4\0";
-      $cur_row_ids{8} = $cur_row_data->[8] // "\0NULL\xFF$rows_pos\xFF8\0";
+      @cur_row_ids{( 0, 2, 3, 4, 8 )} = (
+        $cur_row_data->[0] // "\0NULL\xFF$rows_pos\xFF0\0",
+        $cur_row_data->[2] // "\0NULL\xFF$rows_pos\xFF2\0",
+        $cur_row_data->[3] // "\0NULL\xFF$rows_pos\xFF3\0",
+        $cur_row_data->[4] // "\0NULL\xFF$rows_pos\xFF4\0",
+        $cur_row_data->[8] // "\0NULL\xFF$rows_pos\xFF8\0",
+      );
 
       # cache expensive set of ops in a non-existent rowid slot
       $cur_row_ids{10} = (
-        ( ( defined $cur_row_data->[0] ) && (join "\xFF", q{}, $cur_row_data->[0], q{} ))
+        ( ( defined $cur_row_data->[0] ) && (join "\xFF", q{}, $cur_row_ids{0}, q{} ))
           or
-        ( ( defined $cur_row_data->[2] ) && (join "\xFF", q{}, $cur_row_data->[2], q{} ))
+        ( ( defined $cur_row_data->[2] ) && (join "\xFF", q{}, $cur_row_ids{2}, q{} ))
           or
         "\0$rows_pos\0"
       );
@@ -686,17 +696,13 @@ is_same_src (
     ) ) {
 
       # do not care about nullability here
-      $cur_row_ids{0} = $cur_row_data->[0];
-      $cur_row_ids{2} = $cur_row_data->[2];
-      $cur_row_ids{3} = $cur_row_data->[3];
-      $cur_row_ids{4} = $cur_row_data->[4];
-      $cur_row_ids{8} = $cur_row_data->[8];
+      @cur_row_ids{( 0, 2, 3, 4, 8 )} = @{$cur_row_data}[( 0, 2, 3, 4, 8 )];
 
       # cache expensive set of ops in a non-existent rowid slot
       $cur_row_ids{10} = (
-        ( ( defined $cur_row_data->[0] ) && (join "\xFF", q{}, $cur_row_data->[0], q{} ))
+        ( ( defined $cur_row_data->[0] ) && (join "\xFF", q{}, $cur_row_ids{0}, q{} ))
           or
-        ( ( defined $cur_row_data->[2] ) && (join "\xFF", q{}, $cur_row_data->[2], q{} ))
+        ( ( defined $cur_row_data->[2] ) && (join "\xFF", q{}, $cur_row_ids{2}, q{} ))
           or
         "\0$rows_pos\0"
       );
@@ -747,6 +753,46 @@ is_same_src (
   'Multiple has_many on multiple branches with underdefined root, HRI-direct torture test',
 );
 
+is_same_src (
+  ($schema->source ('Owners')->_mk_row_parser({
+    inflate_map => [qw( books.title books.owner )],
+    collapse => 1,
+    prune_null_branches => 1,
+  }))[0],
+  ' my $rows_pos = 0;
+    my ($result_pos, @collapse_idx, $cur_row_data, %cur_row_ids );
+
+    while ($cur_row_data = (
+      ( $rows_pos >= 0 and $_[0][$rows_pos++] )
+        or
+      ( $_[1] and $rows_pos = -1 and $_[1]->() )
+    ) ) {
+
+      @cur_row_ids{0,1} = @{$cur_row_data}[0,1];
+
+      $cur_row_ids{3} = (
+        ( ( defined $cur_row_data->[1] ) && (join "\xFF", q{}, $cur_row_ids{1}, q{} ))
+          or
+        "\0${rows_pos}\0"
+      );
+
+      $_[1] and $result_pos and ! $collapse_idx[0]{$cur_row_ids{3}} and (unshift @{$_[2]}, $cur_row_data) and last;
+
+      # empty data for the root node
+      $collapse_idx[0]{$cur_row_ids{3}} //= $_[0][$result_pos++] = [];
+
+      ( ! defined $cur_row_data->[0] ) ? $collapse_idx[0]{$cur_row_ids{3}}[1]{"books"} = [] : do {
+        push @{$collapse_idx[0]{$cur_row_ids{3}}[1]{books}},
+          $collapse_idx[1]{$cur_row_ids{0}} = [ { owner => $cur_row_data->[1], title => $cur_row_data->[0] } ]
+        unless $collapse_idx[1]{$cur_row_ids{0}}
+      };
+    }
+
+    $#{$_[0]} = $result_pos - 1; # truncate the passed in array to where we filled it with results
+  ',
+  'Non-premultiplied implicit collapse with missing join columns',
+);
+
 done_testing;
 
 my $deparser;