X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FResultSource%2FRowParser.pm;h=141037bfce2441a2c07798652dbcf9c86919325a;hb=fcf32d04540e2c67625641b0bc004111a7d90252;hp=1d1e781dbef9769979015559a8c2f49d93268e7d;hpb=82f0e0aa75e07eeb3395c5dd2854073708450b96;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/ResultSource/RowParser.pm b/lib/DBIx/Class/ResultSource/RowParser.pm index 1d1e781..141037b 100644 --- a/lib/DBIx/Class/ResultSource/RowParser.pm +++ b/lib/DBIx/Class/ResultSource/RowParser.pm @@ -5,7 +5,7 @@ use strict; use warnings; use Try::Tiny; -use List::Util 'first'; +use List::Util qw(first max); use B 'perlstring'; use namespace::clean; @@ -87,13 +87,12 @@ sub _resolve_collapse { $rel_cols->{$1}{$2} = 1; } else { - $my_cols->{$_} = {}; # important for ||= below + $my_cols->{$_} = {}; # important for ||='s below } } my $relinfo; - # run through relationships, collect metadata, inject non-left fk-bridges from - # *INNER-JOINED* children (if any) + # run through relationships, collect metadata for my $rel (keys %$rel_cols) { my $rel_src = __get_related_source($self, $rel, $rel_cols->{$rel}); @@ -103,6 +102,7 @@ sub _resolve_collapse { $relinfo->{$rel}{is_inner} = ( $inf->{attrs}{join_type} || '' ) !~ /^left/i; $relinfo->{$rel}{rsrc} = $rel_src; + # FIME - need to use _resolve_cond here instead my $cond = $inf->{cond}; if ( @@ -110,25 +110,28 @@ sub _resolve_collapse { and keys %$cond and - ! first { $_ !~ /^foreign\./ } (keys %$cond) + ! defined first { $_ !~ /^foreign\./ } (keys %$cond) and - ! first { $_ !~ /^self\./ } (values %$cond) + ! defined first { $_ !~ /^self\./ } (values %$cond) ) { for my $f (keys %$cond) { my $s = $cond->{$f}; $_ =~ s/^ (?: foreign | self ) \.//x for ($f, $s); $relinfo->{$rel}{fk_map}{$s} = $f; - - # need to know source from *our* pov, hence $rel. - $my_cols->{$s} ||= { via_fk => "$rel.$f" } if ( - defined $rel_cols->{$rel}{$f} # in fact selected - and - $relinfo->{$rel}{is_inner} - ); } } } + # inject non-left fk-bridges from *INNER-JOINED* children (if any) + for my $rel (grep { $relinfo->{$_}{is_inner} } keys %$relinfo) { + my $ri = $relinfo->{$rel}; + for (keys %{$ri->{fk_map}} ) { + # need to know source from *our* pov, hence $rel.col + $my_cols->{$_} ||= { via_fk => "$rel.$ri->{fk_map}{$_}" } + if defined $rel_cols->{$rel}{$ri->{fk_map}{$_}} # in fact selected + } + } + # if the parent is already defined, assume all of its related FKs are selected # (even if they in fact are NOT in the select list). Keep a record of what we # assumed, and if any such phantom-column becomes part of our own collapser, @@ -136,14 +139,11 @@ sub _resolve_collapse { # the parent (whatever it may be) my $assumed_from_parent; unless ($args->{_parent_info}{underdefined}) { - $assumed_from_parent->{columns} = { map - # only add to the list if we do not already select said columns - { ! exists $my_cols->{$_} ? ( $_ => 1 ) : () } - values %{$args->{_parent_info}{rel_condition} || {}} - }; - - $my_cols->{$_} = { via_collapse => $args->{_parent_info}{collapse_on} } - for keys %{$assumed_from_parent->{columns}}; + for my $col ( values %{$args->{_parent_info}{rel_condition} || {}} ) { + next if exists $my_cols->{$col}; + $my_cols->{$col} = { via_collapse => $args->{_parent_info}{collapse_on_idcols} }; + $assumed_from_parent->{columns}{$col}++; + } } # get colinfo for everything @@ -154,8 +154,18 @@ sub _resolve_collapse { my $collapse_map; - # try to resolve based on our columns (plus already inserted FK bridges) + # first try to reuse the parent's collapser (i.e. reuse collapser over 1:1) + # (makes for a leaner coderef later) + unless ($collapse_map->{-idcols_current_node}) { + $collapse_map->{-idcols_current_node} = $args->{_parent_info}{collapse_on_idcols} + if $args->{_parent_info}{collapser_reusable}; + } + + + # Still dont know how to collapse - try to resolve based on our columns (plus already inserted FK bridges) if ( + ! $collapse_map->{-idcols_current_node} + and $my_cols and my $idset = $self->_identifying_column_set ({map { $_ => $my_cols->{$_}{colinfo} } keys %$my_cols}) @@ -164,8 +174,9 @@ sub _resolve_collapse { # and fix stuff up if this is the case my @reduced_set = grep { ! $assumed_from_parent->{columns}{$_} } @$idset; - $collapse_map->{-node_id} = __unique_numlist( - (@reduced_set != @$idset) ? @{$args->{_parent_info}{collapse_on}} : (), + $collapse_map->{-idcols_current_node} = [ __unique_numlist( + @{ $args->{_parent_info}{collapse_on_idcols}||[] }, + (map { my $fqc = join ('.', @@ -177,13 +188,13 @@ sub _resolve_collapse { } @reduced_set ), - ); + )]; } # Stil don't know how to collapse - keep descending down 1:1 chains - if # a related non-LEFT 1:1 is resolvable - its condition will collapse us # too - unless ($collapse_map->{-node_id}) { + unless ($collapse_map->{-idcols_current_node}) { my @candidates; for my $rel (keys %$relinfo) { @@ -194,7 +205,7 @@ sub _resolve_collapse { _rel_chain => [ @{$args->{_rel_chain}}, $rel ], _parent_info => { underdefined => 1 }, }, $common_args)) { - push @candidates, $rel_collapse->{-node_id}; + push @candidates, $rel_collapse->{-idcols_current_node}; } } @@ -202,25 +213,94 @@ sub _resolve_collapse { # FIXME - maybe need to implement a data type order as well (i.e. prefer several ints # to a single varchar) if (@candidates) { - ($collapse_map->{-node_id}) = sort { scalar @$a <=> scalar @$b } (@candidates); + ($collapse_map->{-idcols_current_node}) = sort { scalar @$a <=> scalar @$b } (@candidates); } } - # Still dont know how to collapse - see if the parent passed us anything - # (i.e. reuse collapser over 1:1) - unless ($collapse_map->{-node_id}) { - $collapse_map->{-node_id} = $args->{_parent_info}{collapse_on} - if $args->{_parent_info}{collapser_reusable}; + # Stil don't know how to collapse, and we are the root node. Last ditch + # effort in case we are *NOT* premultiplied. + # Run through *each multi* all the way down, left or not, and all + # *left* singles (a single may become a multi underneath) . When everything + # gets back see if all the rels link to us definitively. If this is the + # case we are good - either one of them will define us, or if all are NULLs + # we know we are "unique" due to the "non-premultiplied" check + if ( + ! $collapse_map->{-idcols_current_node} + and + ! $args->{premultiplied} + and + $common_args->{_node_idx} == 1 + ) { + my (@collapse_sets, $uncollapsible_chain); + + for my $rel (keys %$relinfo) { + + # we already looked at these higher up + next if ($relinfo->{$rel}{is_single} && $relinfo->{$rel}{is_inner}); + + if (my $clps = $relinfo->{$rel}{rsrc}->_resolve_collapse ({ + as => $rel_cols->{$rel}, + _rel_chain => [ @{$args->{_rel_chain}}, $rel ], + _parent_info => { underdefined => 1 }, + }, $common_args) ) { + + # for singles use the idcols wholesale (either there or not) + if ($relinfo->{$rel}{is_single}) { + push @collapse_sets, $clps->{-idcols_current_node}; + } + elsif (! $relinfo->{$rel}{fk_map}) { + $uncollapsible_chain = 1; + last; + } + else { + my $defined_cols_parent_side; + + for my $fq_col ( grep { /^$rel\.[^\.]+$/ } keys %{$args->{as}} ) { + my ($col) = $fq_col =~ /([^\.]+)$/; + + $defined_cols_parent_side->{$_} = $args->{as}{$fq_col} for grep + { $relinfo->{$rel}{fk_map}{$_} eq $col } + keys %{$relinfo->{$rel}{fk_map}} + ; + } + + if (my $set = $self->_identifying_column_set([ keys %$defined_cols_parent_side ]) ) { + push @collapse_sets, [ sort map { $defined_cols_parent_side->{$_} } @$set ]; + } + else { + $uncollapsible_chain = 1; + last; + } + } + } + else { + $uncollapsible_chain = 1; + last; + } + } + + unless ($uncollapsible_chain) { + # if we got here - we are good to go, but the construction is tricky + # since our children will want to include our collapse criteria - we + # don't give them anything (safe, since they are all collapsible on their own) + # in addition we record the individual collapse posibilities + # of all left children node collapsers, and merge them in the rowparser + # coderef later + $collapse_map->{-idcols_current_node} = []; + $collapse_map->{-root_node_idcol_variants} = [ sort { + (scalar @$a) <=> (scalar @$b) or max(@$a) <=> max(@$b) + } @collapse_sets ]; + } } # stop descending into children if we were called by a parent for first-pass # and don't despair if nothing was found (there may be other parallel branches # to dive into) if ($args->{_parent_info}{underdefined}) { - return $collapse_map->{-node_id} ? $collapse_map : undef + return $collapse_map->{-idcols_current_node} ? $collapse_map : undef } # nothing down the chain resolved - can't calculate a collapse-map - elsif (! $collapse_map->{-node_id}) { + elsif (! $collapse_map->{-idcols_current_node}) { $self->throw_exception ( sprintf "Unable to calculate a definitive collapse column set for %s%s: fetch more unique non-nullable columns", $self->source_name, @@ -237,14 +317,16 @@ sub _resolve_collapse { $collapse_map->{-is_optional} = 1 if $args->{_parent_info}{is_optional}; $collapse_map->{-node_index} = $common_args->{_node_idx}++; - my (@id_sets, $multis_in_chain); + + my @id_sets; for my $rel (sort keys %$relinfo) { $collapse_map->{$rel} = $relinfo->{$rel}{rsrc}->_resolve_collapse ({ as => { map { $_ => 1 } ( keys %{$rel_cols->{$rel}} ) }, _rel_chain => [ @{$args->{_rel_chain}}, $rel], _parent_info => { - collapse_on => [ @{$collapse_map->{-node_id}} ], + # shallow copy + collapse_on_idcols => [ @{$collapse_map->{-idcols_current_node}} ], rel_condition => $relinfo->{$rel}{fk_map}, @@ -252,16 +334,25 @@ sub _resolve_collapse { # if this is a 1:1 our own collapser can be used as a collapse-map # (regardless of left or not) - collapser_reusable => $relinfo->{$rel}{is_single}, + collapser_reusable => @{$collapse_map->{-idcols_current_node}} && $relinfo->{$rel}{is_single}, }, }, $common_args ); $collapse_map->{$rel}{-is_single} = 1 if $relinfo->{$rel}{is_single}; $collapse_map->{$rel}{-is_optional} ||= 1 unless $relinfo->{$rel}{is_inner}; - push @id_sets, @{ $collapse_map->{$rel}{-branch_id} }; + push @id_sets, ( map { @$_ } ( + $collapse_map->{$rel}{-idcols_current_node}, + $collapse_map->{$rel}{-idcols_extra_from_children} || (), + )); } - $collapse_map->{-branch_id} = __unique_numlist( @id_sets, @{$collapse_map->{-node_id}} ); + if (@id_sets) { + my $cur_nodeid_hash = { map { $_ => 1 } @{$collapse_map->{-idcols_current_node}} }; + $collapse_map->{-idcols_extra_from_children} = [ grep + { ! $cur_nodeid_hash->{$_} } + __unique_numlist( @id_sets ) + ]; + } return $collapse_map; } @@ -275,7 +366,7 @@ sub _resolve_collapse { # For an example of this coderef in action (and to see its guts) look at # t/resultset/rowparser_internals.t # -# This is a huge performance win, as we call the same code for # every row +# This is a huge performance win, as we call the same code for every row # returned from the db, thus avoiding repeated method lookups when traversing # relationships # @@ -322,8 +413,8 @@ sub _mk_row_parser { # the collapsing parser is more complicated - it needs to keep a lot of state # else { - my $collapse_map = $self->_resolve_collapse ({ + premultiplied => $args->{premultiplied}, # FIXME # only consider real columns (not functions) during collapse resolution # this check shouldn't really be here, as fucktards are not supposed to @@ -338,20 +429,48 @@ sub _mk_row_parser { } }); - my $top_branch_idx_list = join (', ', @{$collapse_map->{-branch_id}}); - - my $top_node_id_path = join ('', map - { "{'\xFF__IDVALPOS__${_}__\xFF'}" } - @{$collapse_map->{-node_id}} + my @all_idcols = sort { $a <=> $b } map { @$_ } ( + $collapse_map->{-idcols_current_node}, + $collapse_map->{-idcols_extra_from_children} || (), ); + my ($top_node_id_path, $top_node_id_cacher, @path_variants); + if (scalar @{$collapse_map->{-idcols_current_node}}) { + $top_node_id_path = join ('', map + { "{'\xFF__IDVALPOS__${_}__\xFF'}" } + @{$collapse_map->{-idcols_current_node}} + ); + } + elsif( my @variants = @{$collapse_map->{-root_node_idcol_variants}} ) { + my @path_parts; + + for (@variants) { + + push @path_variants, sprintf "(join qq(\xFF), '', %s, '')", + ( join ', ', map { "'\xFF__VALPOS__${_}__\xFF'" } @$_ ) + ; + + push @path_parts, sprintf "( %s && %s)", + ( join ' && ', map { "( defined '\xFF__VALPOS__${_}__\xFF' )" } @$_ ), + $path_variants[-1]; + ; + } + + $top_node_id_cacher = sprintf '$cur_row_ids[%d] = (%s);', + $all_idcols[-1] + 1, + "\n" . join( "\n or\n", @path_parts, qq{"\0\$rows_pos\0"} ); + $top_node_id_path = sprintf '{$cur_row_ids[%d]}', $all_idcols[-1] + 1; + } + else { + $self->throw_exception('Unexpected collapse map contents'); + } + my $rel_assemblers = __visit_infmap_collapse ( - $inflate_index, $collapse_map + $inflate_index, { %$collapse_map, -custom_node_id => $top_node_id_path }, ); - $parser_src = sprintf (<<'EOS', $top_branch_idx_list, $top_node_id_path, $rel_assemblers); -### BEGIN STRING EVAL - + $parser_src = sprintf (<<'EOS', join(', ', @all_idcols), $top_node_id_path, $top_node_id_cacher||'', $rel_assemblers); +### BEGIN LITERAL STRING EVAL my ($rows_pos, $result_pos, $cur_row, @cur_row_ids, @collapse_idx, $is_new_res) = (0,0); # this loop is a bit arcane - the rationale is that the passed in @@ -366,21 +485,28 @@ sub _mk_row_parser { ($_[1] and $_[1]->()) ) { - $cur_row_ids[$_] = defined $cur_row->[$_] ? $cur_row->[$_] : "\xFF\xFFN\xFFU\xFFL\xFFL\xFF\xFF" - for (%1$s); # the top branch_id includes all id values + # 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 + $cur_row_ids[$_] = defined $cur_row->[$_] ? $cur_row->[$_] : "\0NULL\xFF$rows_pos\xFF$_\0" + for (%1$s); + + # maybe(!) cache the top node id calculation + %3$s $is_new_res = ! $collapse_idx[1]%2$s and ( $_[1] and $result_pos and (unshift @{$_[2]}, $cur_row) and last ); - %3$s + %4$s $_[0][$result_pos++] = $collapse_idx[1]%2$s if $is_new_res; } splice @{$_[0]}, $result_pos; # truncate the passed in array for cases of collapsing ->all() -### END STRING EVAL +### END LITERAL STRING EVAL EOS # !!! note - different var than the one above @@ -460,9 +586,9 @@ sub __visit_infmap_collapse { } } - my $sequenced_node_id = join ('', map + my $sequenced_node_id = $collapse_map->{-custom_node_id} || join ('', map { "{'\xFF__IDVALPOS__${_}__\xFF'}" } - @{$collapse_map->{-node_id}} + @{$collapse_map->{-idcols_current_node}} ); my $me_struct = keys %$my_cols @@ -501,12 +627,12 @@ sub __visit_infmap_collapse { # DISABLEPRUNE #my $known_defined = { %{ $parent_info->{known_defined} || {} } }; - #$known_defined->{$_}++ for @{$collapse_map->{-node_id}}; - + #$known_defined->{$_}++ for @{$collapse_map->{-idcols_current_node}}; for my $rel (sort keys %$rel_cols) { - push @src, sprintf( '%s[1]{%s} ||= [];', $node_idx_ref, perlstring($rel) ) - unless $collapse_map->{$rel}{-is_single}; +# push @src, sprintf( +# '%s[1]{%s} ||= [];', $node_idx_ref, perlstring($rel) +# ) unless $collapse_map->{$rel}{-is_single}; push @src, __visit_infmap_collapse($rel_cols->{$rel}, $collapse_map->{$rel}, { node_idx => $collapse_map->{-node_index}, @@ -521,7 +647,7 @@ sub __visit_infmap_collapse { # { "(! defined '\xFF__IDVALPOS__${_}__\xFF')" } # sort { $a <=> $b } grep # { ! $known_defined->{$_} } - # @{$collapse_map->{$rel}{-node_id}} + # @{$collapse_map->{$rel}{-idcols_current_node}} #) { # $src[-1] = sprintf( '(%s) or %s', # join (' || ', @null_checks ), @@ -535,7 +661,7 @@ sub __visit_infmap_collapse { # adding a dep on MoreUtils *just* for this is retarded sub __unique_numlist { - [ sort { $a <=> $b } keys %{ {map { $_ => 1 } @_ }} ] + sort { $a <=> $b } keys %{ {map { $_ => 1 } @_ }} } # This error must be thrown from two distinct codepaths, joining them is