X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FStorage%2FDBIHacks.pm;h=34b358c255af5d6e444b151301cdef1ddafee43f;hb=a6ef93cbf8182ed257e5a5e877835694a23b4e74;hp=225ab012e003eb7b28be6b254aaddbe67917418c;hpb=e6977bbbc3dfb119cc11ee7e235ca58dfef7e7e6;p=dbsrgits%2FDBIx-Class-Historic.git diff --git a/lib/DBIx/Class/Storage/DBIHacks.pm b/lib/DBIx/Class/Storage/DBIHacks.pm index 225ab01..34b358c 100644 --- a/lib/DBIx/Class/Storage/DBIHacks.pm +++ b/lib/DBIx/Class/Storage/DBIHacks.pm @@ -24,7 +24,7 @@ use namespace::clean; # sub _prune_unused_joins { my $self = shift; - my ($from, $select, $where, $attrs, $ignore_multiplication) = @_; + my ($from, $select, $where, $attrs) = @_; return $from unless $self->_use_join_optimizer; @@ -34,13 +34,13 @@ sub _prune_unused_joins { my $aliastypes = $self->_resolve_aliastypes_from_select_args(@_); - # don't care - delete $aliastypes->{joining}; + my $orig_joins = delete $aliastypes->{joining}; + my $orig_multiplying = $aliastypes->{multiplying}; # a grouped set will not be affected by amount of rows. Thus any # {multiplying} joins can go delete $aliastypes->{multiplying} - if $ignore_multiplication or $attrs->{group_by}; + if $attrs->{_force_prune_multiplying_joins} or $attrs->{group_by}; my @newfrom = $from->[0]; # FROM head is always present @@ -56,13 +56,17 @@ sub _prune_unused_joins { for my $j (@{$from}[1..$#$from]) { push @newfrom, $j if ( - (! $j->[0]{-alias}) # legacy crap + (! defined $j->[0]{-alias}) # legacy crap || $need_joins{$j->[0]{-alias}} ); } - return \@newfrom; + return ( \@newfrom, { + multiplying => { map { $need_joins{$_} ? ($_ => $orig_multiplying->{$_}) : () } keys %$orig_multiplying }, + %$aliastypes, + joining => { map { $_ => $orig_joins->{$_} } keys %need_joins }, + } ); } # @@ -146,7 +150,7 @@ sub _adjust_select_args_for_complex_prefetch { # We can not just fetch everything because a potential has_many restricting # join collapse *will not work* on heavy data types. my $connecting_aliastypes = $self->_resolve_aliastypes_from_select_args( - [grep { ref($_) eq 'ARRAY' or ref($_) eq 'HASH' } @{$from}[$root_node_offset .. $#$from]], + $from, [], $where, $inner_attrs @@ -175,16 +179,16 @@ sub _adjust_select_args_for_complex_prefetch { local $self->{_use_join_optimizer} = 1; # throw away multijoins since we def. do not care about those inside the subquery - my $inner_from = $self->_prune_unused_joins ($from, $inner_select, $where, $inner_attrs, 'ignore_multiplication'); - - my $inner_aliastypes = - $self->_resolve_aliastypes_from_select_args( $inner_from, $inner_select, $where, $inner_attrs ); + my ($inner_from, $inner_aliastypes) = $self->_prune_unused_joins ($from, $inner_select, $where, { + %$inner_attrs, _force_prune_multiplying_joins => 1 + }); # uh-oh a multiplier (which is not us) left in, this is a problem if ( $inner_aliastypes->{multiplying} and - !$inner_aliastypes->{grouping} # if there are groups - assume user knows wtf they are up to + # if there are user-supplied groups - assume user knows wtf they are up to + ( ! $inner_aliastypes->{grouping} or $inner_attrs->{_grouped_by_distinct} ) and my @multipliers = grep { $_ ne $root_alias } keys %{$inner_aliastypes->{multiplying}} ) { @@ -196,10 +200,13 @@ sub _adjust_select_args_for_complex_prefetch { or ! first { $inner_aliastypes->{ordering}{$_} } @multipliers ) { + my $unprocessed_order_chunks; - ($inner_attrs->{group_by}, $unprocessed_order_chunks) = $self->_group_over_selection ( - $inner_from, $inner_select, $inner_attrs->{order_by} - ); + ($inner_attrs->{group_by}, $unprocessed_order_chunks) = $self->_group_over_selection ({ + %$inner_attrs, + from => $inner_from, + select => $inner_select, + }); $self->throw_exception ( 'A required group_by clause could not be constructed automatically due to a complex ' @@ -218,21 +225,11 @@ sub _adjust_select_args_for_complex_prefetch { # for DESC, and group_by the root columns. The end result should be # exactly what we expect - # FIXME REMOVE LATER - (just a sanity check) - if (defined ( my $impostor = first - { $_ ne $root_alias } - keys %{ $inner_aliastypes->{selecting} } - ) ) { - $self->throw_exception(sprintf - 'Unexpected inner selection during complex prefetch (%s)...', - join ', ', keys %{ $inner_aliastypes->{joining}{$impostor}{-seen_columns} || {} } - ); - } - # supplement the main selection with pks if not already there, # as they will have to be a part of the group_by to colapse # things properly my $cur_sel = { map { $_ => 1 } @$inner_select }; + my @pks = map { "$root_alias.$_" } $root_node->{-rsrc}->primary_columns or $self->throw_exception( sprintf 'Unable to perform complex limited prefetch off %s without declared primary key', @@ -264,14 +261,13 @@ sub _adjust_select_args_for_complex_prefetch { # skip ourselves next if $chunk =~ $own_re; - my $is_desc = $chunk =~ s/\sDESC$//i; - $chunk =~ s/\sASC$//i; + ($chunk, my $is_desc) = $sql_maker->_split_order_chunk($chunk); # maybe our own unqualified column - my ($ord_bit) = ($lquote and $sep) - ? $chunk =~ /^ $lquote ([^$sep]+) $rquote $/x - : $chunk - ; + my $ord_bit = ( + $lquote and $sep and $chunk =~ /^ $lquote ([^$sep]+) $rquote $/x + ) ? $1 : $chunk; + next if ( $ord_bit and @@ -295,9 +291,11 @@ sub _adjust_select_args_for_complex_prefetch { # do not care about leftovers here - it will be all the functions # we just created - ($inner_attrs->{group_by}) = $self->_group_over_selection ( - $inner_from, $inner_select, $inner_attrs->{order_by} - ); + ($inner_attrs->{group_by}) = $self->_group_over_selection ({ + %$inner_attrs, + from => $inner_from, + select => $inner_select, + }); } } @@ -331,7 +329,7 @@ sub _adjust_select_args_for_complex_prefetch { # we may not be the head if ($root_node_offset) { - # first generate the outer_from, up to the substitution point + # first generate the outer_from, up and including the substitution point @outer_from = splice @$from, 0, $root_node_offset; push @outer_from, [ @@ -351,7 +349,7 @@ sub _adjust_select_args_for_complex_prefetch { }; } - shift @$from; # it's replaced in @outer_from already + shift @$from; # what we just replaced above # scan the *remaining* from spec against different attributes, and see which joins are needed # in what role @@ -382,11 +380,12 @@ sub _adjust_select_args_for_complex_prefetch { } if ( $need_outer_group_by and $attrs->{_grouped_by_distinct} ) { - my $unprocessed_order_chunks; - ($outer_attrs->{group_by}, $unprocessed_order_chunks) = $self->_group_over_selection ( - \@outer_from, $outer_select, $outer_attrs->{order_by} - ); + ($outer_attrs->{group_by}, $unprocessed_order_chunks) = $self->_group_over_selection ({ + %$outer_attrs, + from => \@outer_from, + select => $outer_select, + }); $self->throw_exception ( 'A required group_by clause could not be constructed automatically due to a complex ' @@ -444,7 +443,7 @@ sub _resolve_aliastypes_from_select_args { ); } - # get a column to source/alias map (including unqualified ones) + # get a column to source/alias map (including unambiguous unqualified ones) my $colinfo = $self->_resolve_column_info ($from); # set up a botched SQLA @@ -501,7 +500,17 @@ sub _resolve_aliastypes_from_select_args { # throw away empty chunks $_ = [ map { $_ || () } @$_ ] for values %$to_scan; - # first loop through all fully qualified columns and get the corresponding + # first see if we have any exact matches (qualified or unqualified) + for my $type (keys %$to_scan) { + for my $piece (@{$to_scan->{$type}}) { + if ($colinfo->{$piece} and my $alias = $colinfo->{$piece}{-source_alias}) { + $aliases_by_type->{$type}{$alias} ||= { -parents => $alias_list->{$alias}{-join_path}||[] }; + $aliases_by_type->{$type}{$alias}{-seen_columns}{$colinfo->{$piece}{-fq_colname}} = $piece; + } + } + } + + # now loop through all fully qualified columns and get the corresponding # alias (should work even if they are in scalarrefs) for my $alias (keys %$alias_list) { my $al_re = qr/ @@ -530,7 +539,7 @@ sub _resolve_aliastypes_from_select_args { for my $type (keys %$to_scan) { for my $piece (@{$to_scan->{$type}}) { - if (my @matches = $piece =~ /$col_re/g) { + if ( my @matches = $piece =~ /$col_re/g) { my $alias = $colinfo->{$col}{-source_alias}; $aliases_by_type->{$type}{$alias} ||= { -parents => $alias_list->{$alias}{-join_path}||[] }; $aliases_by_type->{$type}{$alias}{-seen_columns}{"$alias.$_"} = $_ @@ -559,43 +568,49 @@ sub _resolve_aliastypes_from_select_args { # This is the engine behind { distinct => 1 } sub _group_over_selection { - my ($self, $from, $select, $order_by) = @_; + my ($self, $attrs) = @_; - my $rs_column_list = $self->_resolve_column_info ($from); + my $colinfos = $self->_resolve_column_info ($attrs->{from}); my (@group_by, %group_index); # the logic is: if it is a { func => val } we assume an aggregate, # otherwise if \'...' or \[...] we assume the user knows what is # going on thus group over it - for (@$select) { + for (@{$attrs->{select}}) { if (! ref($_) or ref ($_) ne 'HASH' ) { push @group_by, $_; $group_index{$_}++; - if ($rs_column_list->{$_} and $_ !~ /\./ ) { + if ($colinfos->{$_} and $_ !~ /\./ ) { # add a fully qualified version as well - $group_index{"$rs_column_list->{$_}{-source_alias}.$_"}++; + $group_index{"$colinfos->{$_}{-source_alias}.$_"}++; } } } - # add any order_by parts that are not already present in the group_by + # add any order_by parts *from the main source* that are not already + # present in the group_by # we need to be careful not to add any named functions/aggregates # i.e. order_by => [ ... { count => 'foo' } ... ] my @leftovers; - for ($self->_extract_order_criteria($order_by)) { + for ($self->_extract_order_criteria($attrs->{order_by})) { # only consider real columns (for functions the user got to do an explicit group_by) if (@$_ != 1) { push @leftovers, $_; next; } my $chunk = $_->[0]; - my $colinfo = $rs_column_list->{$chunk} or do { + + if ( + !$colinfos->{$chunk} + or + $colinfos->{$chunk}{-source_alias} ne $attrs->{alias} + ) { push @leftovers, $_; next; - }; + } - $chunk = "$colinfo->{-source_alias}.$chunk" if $chunk !~ /\./; + $chunk = $colinfos->{$chunk}{-fq_colname}; push @group_by, $chunk unless $group_index{$chunk}++; } @@ -609,14 +624,12 @@ sub _resolve_ident_sources { my ($self, $ident) = @_; my $alias2source = {}; - my $rs_alias; # the reason this is so contrived is that $ident may be a {from} # structure, specifying multiple tables to join if ( blessed $ident && $ident->isa("DBIx::Class::ResultSource") ) { # this is compat mode for insert/update/delete which do not deal with aliases $alias2source->{me} = $ident; - $rs_alias = 'me'; } elsif (ref $ident eq 'ARRAY') { @@ -624,7 +637,6 @@ sub _resolve_ident_sources { my $tabinfo; if (ref $_ eq 'HASH') { $tabinfo = $_; - $rs_alias = $tabinfo->{-alias}; } if (ref $_ eq 'ARRAY' and ref $_->[0] eq 'HASH') { $tabinfo = $_->[0]; @@ -635,7 +647,7 @@ sub _resolve_ident_sources { } } - return ($alias2source, $rs_alias); + return $alias2source; } # Takes $ident, \@column_names @@ -647,7 +659,7 @@ sub _resolve_ident_sources { # for all sources sub _resolve_column_info { my ($self, $ident, $colnames) = @_; - my ($alias2src, $root_alias) = $self->_resolve_ident_sources($ident); + my $alias2src = $self->_resolve_ident_sources($ident); my (%seen_cols, @auto_colnames); @@ -786,7 +798,7 @@ sub _extract_order_criteria { my @chunks; for ($sql_maker->_order_by_chunks ($order_by) ) { my $chunk = ref $_ ? [ @$_ ] : [ $_ ]; - $chunk->[0] =~ s/\s+ (?: ASC|DESC ) \s* $//ix; + ($chunk->[0]) = $sql_maker->_split_order_chunk($chunk->[0]); # order criteria may have come back pre-quoted (literals and whatnot) # this is fragile, but the best we can currently do @@ -851,7 +863,8 @@ sub _main_source_order_by_portion_is_stable { ; return unless @ord_cols; - my $colinfos = $self->_resolve_column_info($main_rsrc, \@ord_cols); + my $colinfos = $self->_resolve_column_info($main_rsrc); + for (0 .. $#ord_cols) { if ( ! $colinfos->{$ord_cols[$_]} @@ -866,25 +879,43 @@ sub _main_source_order_by_portion_is_stable { # we just truncated it above return unless @ord_cols; - # since all we check here are the start of the order_by belonging to the - # top level $rsrc, a present identifying set will mean that the resultset - # is ordered by its leftmost table in a stable manner - # - # single source - safely use both qualified and unqualified name my $order_portion_ci = { map { $colinfos->{$_}{-colname} => $colinfos->{$_}, $colinfos->{$_}{-fq_colname} => $colinfos->{$_}, } @ord_cols }; - $where = $where ? $self->_resolve_column_info( - $main_rsrc, $self->_extract_fixed_condition_columns($where) - ) : {}; + # since all we check here are the start of the order_by belonging to the + # top level $rsrc, a present identifying set will mean that the resultset + # is ordered by its leftmost table in a stable manner + # + # RV of _identifying_column_set contains unqualified names only + my $unqualified_idset = $main_rsrc->_identifying_column_set({ + ( $where ? %{ + $self->_resolve_column_info( + $main_rsrc, $self->_extract_fixed_condition_columns($where) + ) + } : () ), + %$order_portion_ci + }) or return; + + my $ret_info; + my %unqualified_idcols_from_order = map { + $order_portion_ci->{$_} ? ( $_ => $order_portion_ci->{$_} ) : () + } @$unqualified_idset; + + # extra optimization - cut the order_by at the end of the identifying set + # (just in case the user was stupid and overlooked the obvious) + for my $i (0 .. $#ord_cols) { + my $col = $ord_cols[$i]; + my $unqualified_colname = $order_portion_ci->{$col}{-colname}; + $ret_info->{$col} = { %{$order_portion_ci->{$col}}, -idx_in_order_subset => $i }; + delete $unqualified_idcols_from_order{$ret_info->{$col}{-colname}}; + + # we didn't reach the end of the identifying portion yet + return $ret_info unless keys %unqualified_idcols_from_order; + } - return ( - $main_rsrc->_identifying_column_set({ %$where, %$order_portion_ci }) - ? $order_portion_ci - : undef - ); + die 'How did we get here...'; } # returns an arrayref of column names which *definitely* have som