Step up the error reporting on unexpected NULLs during collapse
Peter Rabbitson [Fri, 25 Mar 2016 13:28:42 +0000 (14:28 +0100)]
The collapser became so complex that it is practically impossible to debug
when things go sideways. Expand the rudimentary "just check root identifier"
to a comprehensive check of all identifier groups, at the expense of some
performance. This builds on top of the {nullchecks} metadata collected in
5ff6d603, generating maximally unrolled definedness checks which cause an
early return with no results and flagged unexpected-null-positions

The commit itself is relatively straightforward, though the meager changeset
in lib/ is misleading: the damned thing took almost 8 days to fully think
through and implement with several detours for bugfixing :/

Changes
lib/DBIx/Class/ResultSet.pm
lib/DBIx/Class/ResultSource/RowParser.pm
lib/DBIx/Class/ResultSource/RowParser/Util.pm
t/resultset/misled_rowparser.t [new file with mode: 0644]
t/resultset/rowparser_internals.t

diff --git a/Changes b/Changes
index 2714c0f..9954895 100644 (file)
--- a/Changes
+++ b/Changes
@@ -11,6 +11,9 @@ Revision history for DBIx::Class
           an underlying search_rs(), as by design these arguments would be
           used only on the first call to ->related_resultset(), and ignored
           afterwards. Instead an exception (detailing the fix) is thrown.
+        - Increased checking for the correctness of the is_nullable attribute
+          within the prefetch result parser may highlight previously unknown
+          mismatches between your codebase and data source
         - Calling the set_* many-to-many helper with a list (instead of an
           arrayref) now emits a deprecation warning
 
index 4565031..cc5b398 100644 (file)
@@ -1425,7 +1425,7 @@ sub _construct_results {
 
     # $args and $attrs to _mk_row_parser are separated to delineate what is
     # core collapser stuff and what is dbic $rs specific
-    @{$self->{_row_parser}{$parser_type}}{qw(cref nullcheck)} = $rsrc->_mk_row_parser({
+    $self->{_row_parser}{$parser_type}{cref} = $rsrc->_mk_row_parser({
       eval => 1,
       inflate_map => $infmap,
       collapse => $attrs->{collapse},
@@ -1434,49 +1434,9 @@ sub _construct_results {
       prune_null_branches => $self->{_result_inflator}{is_hri} || $self->{_result_inflator}{is_core_row},
     }, $attrs) unless $self->{_row_parser}{$parser_type}{cref};
 
-    # column_info metadata historically hasn't been too reliable.
-    # We need to start fixing this somehow (the collapse resolver
-    # can't work without it). Add an explicit check for the *main*
-    # result, hopefully this will gradually weed out such errors
-    #
-    # FIXME - this is a temporary kludge that reduces performance
-    # It is however necessary for the time being
-    my ($unrolled_non_null_cols_to_check, $err);
-
-    if (my $check_non_null_cols = $self->{_row_parser}{$parser_type}{nullcheck} ) {
-
-      $err =
-        'Collapse aborted due to invalid ResultSource metadata - the following '
-      . 'selections are declared non-nullable but NULLs were retrieved: '
-      ;
-
-      my @violating_idx;
-      COL: for my $i (@$check_non_null_cols) {
-        ! defined $_->[$i] and push @violating_idx, $i and next COL for @$rows;
-      }
-
-      $self->throw_exception( $err . join (', ', map { "'$infmap->[$_]'" } @violating_idx ) )
-        if @violating_idx;
-
-      $unrolled_non_null_cols_to_check = join (',', @$check_non_null_cols);
-
-      utf8::upgrade($unrolled_non_null_cols_to_check)
-        if DBIx::Class::_ENV_::STRESSTEST_UTF8_UPGRADE_GENERATED_COLLAPSER_SOURCE;
-    }
-
-    my $next_cref =
-      ($did_fetch_all or ! $attrs->{collapse})  ? undef
-    : defined $unrolled_non_null_cols_to_check  ? eval sprintf <<'EOS', $unrolled_non_null_cols_to_check
-sub {
-  # FIXME SUBOPTIMAL - we can do better, cursor->next/all (well diff. methods) should return a ref
-  my @r = $cursor->next or return;
-  if (my @violating_idx = grep { ! defined $r[$_] } (%s) ) {
-    $self->throw_exception( $err . join (', ', map { "'$infmap->[$_]'" } @violating_idx ) )
-  }
-  \@r
-}
-EOS
-    : sub {
+    my $next_cref = ($did_fetch_all or ! $attrs->{collapse})
+      ? undef
+      : sub {
         # FIXME SUBOPTIMAL - we can do better, cursor->next/all (well diff. methods) should return a ref
         my @r = $cursor->next or return;
         \@r
@@ -1487,8 +1447,23 @@ EOS
       $rows,
       $next_cref,
       ( $self->{_stashed_rows} = [] ),
+      ( my $null_violations = {} ),
     );
 
+    $self->throw_exception(
+      'Collapse aborted - the following columns are declared (or defaulted to) '
+    . 'non-nullable within DBIC but NULLs were retrieved from storage: '
+    . join( ', ', map { "'$infmap->[$_]'" } sort { $a <=> $b } keys %$null_violations )
+    . ' within data row ' . dump_value({
+      map {
+        $infmap->[$_] =>
+          ( ! defined $self->{_stashed_rows}[0][$_] or length $self->{_stashed_rows}[0][$_] < 50 )
+            ? $self->{_stashed_rows}[0][$_]
+            : substr( $self->{_stashed_rows}[0][$_], 0, 50 ) . '...'
+      } 0 .. $#{$self->{_stashed_rows}[0]}
+    })
+    ) if keys %$null_violations;
+
     # simple in-place substitution, does not regrow $rows
     if ($self->{_result_inflator}{is_core_row}) {
       $_ = $inflator_cref->($res_class, $rsrc, @$_) for @$rows
index 93d7ef9..4683e15 100644 (file)
@@ -122,8 +122,6 @@ sub _mk_row_parser {
     },
   );
 
-  my $check_null_columns;
-
   my $src = (! $args->{collapse} ) ? assemble_simple_parser(\%common) : do {
     my $collapse_map = $self->_resolve_collapse ({
       # FIXME
@@ -141,9 +139,6 @@ sub _mk_row_parser {
       premultiplied => $args->{premultiplied},
     });
 
-    $check_null_columns = $collapse_map->{-identifying_columns}
-      if @{$collapse_map->{-identifying_columns}};
-
     assemble_collapsing_parser({
       %common,
       collapse_map => $collapse_map,
@@ -155,7 +150,6 @@ sub _mk_row_parser {
 
   return (
     $args->{eval} ? ( eval "sub $src" || die $@ ) : $src,
-    $check_null_columns,
   );
 }
 
index 7192e1b..0409c1a 100644 (file)
@@ -177,7 +177,65 @@ sub assemble_collapsing_parser {
     )
   ;
 
-  my $parser_src = sprintf (<<'EOS', $row_id_defs, $top_node_key_assembler, $top_node_key, join( "\n", @$data_assemblers ) );
+  my $null_checks = '';
+
+  for my $c ( sort { $a <=> $b } keys %{$stats->{nullchecks}{mandatory}} ) {
+    $null_checks .= sprintf <<'EOS', $c
+( defined( $cur_row_data->[%1$s] ) or $_[3]->{%1$s} = 1 ),
+
+EOS
+  }
+
+  for my $set ( @{ $stats->{nullchecks}{from_first_encounter} || [] } ) {
+    my @sub_checks;
+
+    for my $i (0 .. $#$set - 1) {
+
+      push @sub_checks, sprintf
+        '( not defined $cur_row_data->[%1$s] ) ? ( %2$s or ( $_[3]->{%1$s} = 1 ) )',
+        $set->[$i],
+        join( ' and ', map
+          { "( not defined \$cur_row_data->[$set->[$_]] )" }
+          ( $i+1 .. $#$set )
+        ),
+      ;
+    }
+
+    $null_checks .= "(\n @{[ join qq(\n: ), @sub_checks, '()' ]} \n),\n";
+  }
+
+  for my $set ( @{ $stats->{nullchecks}{all_or_nothing} || [] } ) {
+
+    $null_checks .= sprintf "(\n( %s )\n  or\n(\n%s\n)\n),\n",
+      join ( ' and ', map
+        { "( not defined \$cur_row_data->[$_] )" }
+        sort { $a <=> $b } keys %$set
+      ),
+      join ( ",\n", map
+        { "( defined(\$cur_row_data->[$_]) or \$_[3]->{$_} = 1 )" }
+        sort { $a <=> $b } keys %$set
+      ),
+    ;
+  }
+
+  # If any of the above generators produced something, we need to add the
+  # final "if seen any violations - croak" part
+  # Do not throw from within the string eval itself as it does not have
+  # the necessary metadata to construct a nice exception text. As a bonus
+  # we get to entirely avoid https://github.com/Test-More/Test2/issues/16
+  # and https://rt.perl.org/Public/Bug/Display.html?id=127774
+
+  $null_checks .= <<'EOS' if $null_checks;
+
+( keys %{$_[3]} and (
+    ( @{$_[2]} = $cur_row_data ),
+    ( $result_pos = 0 ),
+    last
+) ),
+EOS
+
+
+  my $parser_src = sprintf (<<'EOS', $null_checks, $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, %%cur_row_ids );
@@ -210,13 +268,24 @@ sub assemble_collapsing_parser {
     ( $_[1] and $_[1]->() )
   ) ) {
 
-    # the undef checks may or may not be there
-    # depending on whether we prune or not
+    # column_info metadata historically hasn't been too reliable.
+    # We need to start fixing this somehow (the collapse resolver
+    # can't work without it). Add explicit checks for several cases
+    # of "unexpected NULL", based on the metadata returned by
+    # __visit_infmap_collapse
     #
+    # FIXME - this is a temporary kludge that reduces performance
+    # It is however necessary for the time being, until way into the
+    # future when the extra errors clear out all invalid metadata
+%s
+
     # 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
+    #
+    # the undef checks may or may not be there depending on whether
+    # we prune or not
 %s
 
     # in the case of an underdefined root - calculate the virtual id (otherwise no code at all)
diff --git a/t/resultset/misled_rowparser.t b/t/resultset/misled_rowparser.t
new file mode 100644 (file)
index 0000000..2c76aed
--- /dev/null
@@ -0,0 +1,63 @@
+BEGIN { do "./t/lib/ANFANG.pm" or die ( $@ || $! ) }
+
+use strict;
+use warnings;
+
+use Test::More;
+use Test::Exception;
+
+use DBICTest;
+my $schema = DBICTest->init_schema();
+
+# The nullchecks metadata for this collapse resolution is:
+#
+# mandatory => { 0 => 1 }
+# from_first_encounter => [ [ 1, 2, 3 ] ]
+# all_or_nothing => [ { 1 => 1, 2 => 1 } ]
+#
+my $rs = $schema->resultset('Artist')->search({}, {
+  collapse => 1,
+  join => { cds => 'tracks' },
+  columns => [qw(
+    me.artistid
+    cds.artist
+    cds.title
+  ),
+  { 'cds.tracks.title' => 'tracks.title' },
+  ],
+});
+
+my @cases = (
+  "'artistid'"
+    => [ undef, 0, 0, undef ],
+
+  "'artistid', 'cds.title'"
+    => [ undef, 0, undef, undef ],
+
+  "'artistid', 'cds.artist'"
+    => [ undef, undef, 0, undef ],
+
+  "'cds.artist'"
+    => [ 0, undef, 0, 0 ],
+
+  "'cds.title'"
+    => [ 0, 0, undef, 0 ],
+
+  # petrhaps need to report cds.title here as well, but that'll complicate checks even more...
+  "'cds.artist'"
+    => [ 0, undef, undef, 0 ],
+);
+
+while (@cases) {
+  my ($err, $cursor) = splice @cases, 0, 2;
+
+  $rs->{_stashed_rows} = [ $cursor ];
+
+  throws_ok
+    { $rs->next }
+    qr/\Qthe following columns are declared (or defaulted to) non-nullable within DBIC but NULLs were retrieved from storage: $err within data row/,
+    "Correct exception on non-nullable-yet-NULL $err"
+  ;
+}
+
+done_testing;
index d83a685..2c593e7 100644 (file)
@@ -259,6 +259,40 @@ is_same_src (
       ( $_[1] and $_[1]->() )
     ) ) {
 
+
+      # NULL checks
+      # mandatory => { 4 => 1, 5 => 1 }
+      # from_first_encounter => [ [ 1, 3, 0 ] ]
+      #
+      ( defined( $cur_row_data->[4] ) or $_[3]->{4} = 1 ),
+
+      ( defined( $cur_row_data->[5] ) or $_[3]->{5} = 1 ),
+
+      (
+        ( not defined $cur_row_data->[1] )
+        ? (
+            ( not defined $cur_row_data->[3] )
+              and
+            ( not defined $cur_row_data->[0] )
+              or
+            ( $_[3]->{1} = 1 )
+          )
+      : ( not defined $cur_row_data->[3] )
+        ? (
+            ( not defined $cur_row_data->[0] )
+              or
+            ( $_[3]->{3} = 1 )
+        )
+      : ()
+      ),
+
+      ( keys %{$_[3]} and (
+        ( @{$_[2]} = $cur_row_data ),
+        ( $result_pos = 0 ),
+        last
+      ) ),
+
+
       ( @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" ),
@@ -333,6 +367,40 @@ is_same_src (
       ( $_[1] and $_[1]->() )
     ) ) {
 
+
+      # NULL checks
+      # mandatory => { 4 => 1, 5 => 1 }
+      # from_first_encounter => [ [ 1, 3, 0 ] ]
+      #
+      ( defined( $cur_row_data->[4] ) or $_[3]->{4} = 1 ),
+
+      ( defined( $cur_row_data->[5] ) or $_[3]->{5} = 1 ),
+
+      (
+        ( not defined $cur_row_data->[1] )
+        ? (
+          ( not defined $cur_row_data->[3] )
+            and
+          ( not defined $cur_row_data->[0] )
+            or
+          ( $_[3]->{1} = 1 )
+        )
+      : ( not defined $cur_row_data->[3] )
+        ? (
+          ( not defined $cur_row_data->[0] )
+            or
+          ( $_[3]->{3} = 1 )
+        )
+      : ()
+      ),
+
+      ( keys %{$_[3]} and (
+        ( @{$_[2]} = $cur_row_data ),
+        ( $result_pos = 0 ),
+        last
+      ) ),
+
+
       ( @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]
@@ -464,6 +532,48 @@ is_same_src (
       ( $_[1] and $_[1]->() )
     ) ) {
 
+
+      # NULL checks
+      # mandatory => { 1 => 1 }
+      # from_first_encounter => [ [6, 8], [5, 10, 0] ],
+      #
+      ( defined( $cur_row_data->[1] ) or $_[3]->{1} = 1 ),
+
+      (
+        ( not defined $cur_row_data->[6] )
+        ? (
+          ( not defined $cur_row_data->[8] )
+            or
+          ( $_[3]->{6} = 1 )
+        )
+      : ()
+      ),
+
+      (
+        ( not defined $cur_row_data->[5] )
+        ? (
+          ( not defined $cur_row_data->[10] )
+            and
+          ( not defined $cur_row_data->[0] )
+            or
+          ( $_[3]->{5} = 1 )
+          )
+      : ( not defined $cur_row_data->[10] )
+        ? (
+          ( not defined $cur_row_data->[0] )
+            or
+          ( $_[3]->{10} = 1 )
+        )
+      : ()
+      ),
+
+      ( keys %{$_[3]} and (
+        ( @{$_[2]} = $cur_row_data ),
+        ( $result_pos = 0 ),
+        last
+      ) ),
+
+
       ( @cur_row_ids{0, 1, 5, 6, 8, 10} = (
         $cur_row_data->[0] // "\0NULL\xFF$rows_pos\xFF0\0",
         $cur_row_data->[1],
@@ -549,6 +659,48 @@ is_same_src (
       ( $_[1] and $_[1]->() )
     ) ) {
 
+
+      # NULL checks
+      # mandatory => { 1 => 1 }
+      # from_first_encounter => [ [6, 8], [5, 10, 0] ],
+      #
+      ( defined( $cur_row_data->[1] ) or $_[3]->{1} = 1 ),
+
+      (
+        ( not defined $cur_row_data->[6] )
+        ? (
+          ( not defined $cur_row_data->[8] )
+            or
+          ( $_[3]->{6} = 1 )
+        )
+      : ()
+      ),
+
+      (
+        ( not defined $cur_row_data->[5] )
+        ? (
+          ( not defined $cur_row_data->[10] )
+            and
+          ( not defined $cur_row_data->[0] )
+            or
+          ( $_[3]->{5} = 1 )
+          )
+      : ( not defined $cur_row_data->[10] )
+        ? (
+          ( not defined $cur_row_data->[0] )
+            or
+          ( $_[3]->{10} = 1 )
+        )
+      : ()
+      ),
+
+      ( keys %{$_[3]} and (
+        ( @{$_[2]} = $cur_row_data ),
+        ( $result_pos = 0 ),
+        last
+      ) ),
+
+
       ( @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]
@@ -680,6 +832,49 @@ is_same_src (
       ( $_[1] and $_[1]->() )
     ) ) {
 
+
+      # NULL checks
+      #
+      # from_first_encounter => [ [0, 4, 8] ]
+      # all_or_nothing => [ { 2 => 1, 3 => 1 } ]
+      (
+        ( not defined $cur_row_data->[0] )
+        ? (
+          ( not defined $cur_row_data->[4] )
+            and
+          ( not defined $cur_row_data->[8] )
+            or
+          ( $_[3]->{0} = 1 )
+          )
+      : ( not defined $cur_row_data->[4] )
+        ? (
+          ( not defined $cur_row_data->[8] )
+            or
+          ( $_[3]->{4} = 1 )
+        )
+      : ()
+      ),
+
+      (
+        (
+          ( not defined $cur_row_data->[2] )
+            and
+          ( not defined $cur_row_data->[3] )
+        )
+          or
+        (
+          ( defined($cur_row_data->[2]) or $_[3]->{2} = 1 ),
+          ( defined($cur_row_data->[3]) or $_[3]->{3} = 1 ),
+        )
+      ),
+
+      ( keys %{$_[3]} and (
+        ( @{$_[2]} = $cur_row_data ),
+        ( $result_pos = 0 ),
+        last
+      ) ),
+
+
       ( @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",
@@ -766,6 +961,49 @@ is_same_src (
       ( $_[1] and $_[1]->() )
     ) ) {
 
+
+      # NULL checks
+      #
+      # from_first_encounter => [ [0, 4, 8] ]
+      # all_or_nothing => [ { 2 => 1, 3 => 1 } ]
+      (
+        ( not defined $cur_row_data->[0] )
+        ? (
+          ( not defined $cur_row_data->[4] )
+            and
+          ( not defined $cur_row_data->[8] )
+            or
+          ( $_[3]->{0} = 1 )
+          )
+      : ( not defined $cur_row_data->[4] )
+        ? (
+          ( not defined $cur_row_data->[8] )
+            or
+          ( $_[3]->{4} = 1 )
+        )
+      : ()
+      ),
+
+      (
+        (
+          ( not defined $cur_row_data->[2] )
+            and
+          ( not defined $cur_row_data->[3] )
+        )
+          or
+        (
+          ( defined($cur_row_data->[2]) or $_[3]->{2} = 1 ),
+          ( defined($cur_row_data->[3]) or $_[3]->{3} = 1 ),
+        )
+      ),
+
+      ( keys %{$_[3]} and (
+        ( @{$_[2]} = $cur_row_data ),
+        ( $result_pos = 0 ),
+        last
+      ) ),
+
+
       # do not care about nullability here
       ( @cur_row_ids{( 0, 2, 3, 4, 8 )} = @{$cur_row_data}[( 0, 2, 3, 4, 8 )] ),
 
@@ -911,6 +1149,66 @@ is_same_src (
       ( $_[1] and $_[1]->() )
     ) ) {
 
+      # NULL checks
+      #
+      # from_first_encounter => [ [6, 4, 8], [6, 0, 9] ]
+      # all_or_nothing => [ { 2 => 1, 3 => 1 } ]
+      (
+        ( not defined $cur_row_data->[6] )
+        ? (
+          ( not defined $cur_row_data->[4] )
+            and
+          ( not defined $cur_row_data->[8] )
+            or
+          ( $_[3]->{6} = 1 )
+          )
+      : ( not defined $cur_row_data->[4] )
+        ? (
+          ( not defined $cur_row_data->[8] )
+            or
+          ( $_[3]->{4} = 1 )
+          )
+      : ()
+      ),
+
+      (
+        ( not defined $cur_row_data->[6] )
+        ? (
+          ( not defined $cur_row_data->[0] )
+            and
+          ( not defined $cur_row_data->[9] )
+            or
+          ( $_[3]->{6} = 1 )
+          )
+      : ( not defined $cur_row_data->[0] )
+        ? (
+          ( not defined $cur_row_data->[9] )
+            or
+          ( $_[3]->{0} = 1 )
+          )
+      : ()
+      ),
+
+      (
+        (
+          ( not defined $cur_row_data->[2] )
+            and
+          ( not defined $cur_row_data->[3] )
+        )
+          or
+        (
+          ( defined($cur_row_data->[2]) or $_[3]->{2} = 1 ),
+          ( defined($cur_row_data->[3]) or $_[3]->{3} = 1 ),
+        )
+      ),
+
+      ( keys %{$_[3]} and (
+        ( @{$_[2]} = $cur_row_data ),
+        ( $result_pos = 0 ),
+        last
+      ) ),
+
+
       # do not care about nullability here
       ( @cur_row_ids{( 0, 2, 3, 4, 6, 8, 9 )} = @{$cur_row_data}[( 0, 2, 3, 4, 6, 8, 9 )] ),
 
@@ -1037,6 +1335,110 @@ is_same_src (
   'Non-premultiplied implicit collapse with missing join columns',
 );
 
+is_same_src (
+  ($schema->source('Artist')->_mk_row_parser({
+    inflate_map => [qw( artistid cds.artist cds.title cds.tracks.title )],
+    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
+          ( ($rows_pos = -1), undef )
+        )
+      )
+        or
+      ( $_[1] and $_[1]->() )
+    ) ) {
+
+      # NULL checks
+      #
+      # mandatory => { 0 => 1 }
+      # from_first_encounter => [ [1, 2, 3] ]
+      # all_or_nothing => [ { 1 => 1, 2 => 1 } ]
+
+      ( defined( $cur_row_data->[0] ) or $_[3]->{0} = 1 ),
+
+      (
+        ( not defined $cur_row_data->[1] )
+        ? (
+          ( not defined $cur_row_data->[2] )
+            and
+          ( not defined $cur_row_data->[3] )
+            or
+          $_[3]->{1} = 1
+          )
+      : ( not defined $cur_row_data->[2] )
+        ? (
+          ( not defined $cur_row_data->[3] )
+            or
+          $_[3]->{2} = 1
+          )
+      : ()
+      ),
+
+      (
+        (
+          ( not defined $cur_row_data->[1] )
+            and
+          ( not defined $cur_row_data->[2] )
+        )
+          or
+        (
+          ( defined($cur_row_data->[1]) or $_[3]->{1} = 1 ),
+          ( defined($cur_row_data->[2]) or $_[3]->{2} = 1 ),
+        )
+      ),
+
+      ( keys %{$_[3]} and (
+        ( @{$_[2]} = $cur_row_data ),
+        ( $result_pos = 0 ),
+        last
+      ) ),
+
+
+      ( @cur_row_ids{( 0, 1, 2, 3 )} = @{$cur_row_data}[ 0, 1, 2, 3 ] ),
+
+      ( $_[1] and $result_pos and ! $collapse_idx[0]{$cur_row_ids{0}} and (unshift @{$_[2]}, $cur_row_data) and last ),
+
+      ( $collapse_idx[0]{ $cur_row_ids{0} }
+          //= $_[0][$result_pos++] = [ { "artistid" => $cur_row_data->[0] } ]
+      ),
+
+      ( ( ! defined $cur_row_data->[1] ) ? $collapse_idx[0]{ $cur_row_ids{0} }[1]{"cds"} = [] : do {
+
+        (
+          ! $collapse_idx[1]{ $cur_row_ids{0} }{ $cur_row_ids{1} }{ $cur_row_ids{2} }
+            and
+          push @{$collapse_idx[0]{ $cur_row_ids{0} }[1]{"cds"}},
+            $collapse_idx[1]{ $cur_row_ids{0} }{ $cur_row_ids{1} }{ $cur_row_ids{2} }
+              = [ { "artist" => $cur_row_data->[1], "title" => $cur_row_data->[2] } ]
+        ),
+
+        ( ( ! defined $cur_row_data->[3] ) ? $collapse_idx[1]{ $cur_row_ids{0} }{ $cur_row_ids{1} }{ $cur_row_ids{2} }[1]{"tracks"} = [] : do {
+          (
+            ! $collapse_idx[2]{ $cur_row_ids{0} }{ $cur_row_ids{1} }{ $cur_row_ids{2} }{ $cur_row_ids{3} }
+              and
+            push @{$collapse_idx[1]{ $cur_row_ids{0} }{ $cur_row_ids{1} }{ $cur_row_ids{2} }[1]{"tracks"}},
+              $collapse_idx[2]{ $cur_row_ids{0} }{ $cur_row_ids{1} }{ $cur_row_ids{2} }{ $cur_row_ids{3} }
+                = [ { "title" => $cur_row_data->[3] } ]
+          ),
+        } ),
+      } ),
+    }
+
+    $#{$_[0]} = $result_pos - 1
+  ',
+  'A rolled out version of inflate map of misled_rowparser.t'
+);
+
 done_testing;
 
 my $deparser;