From: Peter Rabbitson Date: Fri, 25 Mar 2016 13:28:42 +0000 (+0100) Subject: Step up the error reporting on unexpected NULLs during collapse X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?p=dbsrgits%2FDBIx-Class.git;a=commitdiff_plain;h=b3a400a044a5e4a768e26d450e3cce289481ee7a Step up the error reporting on unexpected NULLs during collapse 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 :/ --- diff --git a/Changes b/Changes index 2714c0f..9954895 100644 --- 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 diff --git a/lib/DBIx/Class/ResultSet.pm b/lib/DBIx/Class/ResultSet.pm index 4565031..cc5b398 100644 --- a/lib/DBIx/Class/ResultSet.pm +++ b/lib/DBIx/Class/ResultSet.pm @@ -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 diff --git a/lib/DBIx/Class/ResultSource/RowParser.pm b/lib/DBIx/Class/ResultSource/RowParser.pm index 93d7ef9..4683e15 100644 --- a/lib/DBIx/Class/ResultSource/RowParser.pm +++ b/lib/DBIx/Class/ResultSource/RowParser.pm @@ -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, ); } diff --git a/lib/DBIx/Class/ResultSource/RowParser/Util.pm b/lib/DBIx/Class/ResultSource/RowParser/Util.pm index 7192e1b..0409c1a 100644 --- a/lib/DBIx/Class/ResultSource/RowParser/Util.pm +++ b/lib/DBIx/Class/ResultSource/RowParser/Util.pm @@ -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 index 0000000..2c76aed --- /dev/null +++ b/t/resultset/misled_rowparser.t @@ -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; diff --git a/t/resultset/rowparser_internals.t b/t/resultset/rowparser_internals.t index d83a685..2c593e7 100644 --- a/t/resultset/rowparser_internals.t +++ b/t/resultset/rowparser_internals.t @@ -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;