X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FStorage%2FDBIHacks.pm;h=defcecda315c3ee269bb02c09a69cb0e97e80363;hb=61f031bfb3718ddbf69a3fa2c219917eabf244e2;hp=f5a03c92a7d1c2afb045dad4e01e932e0111f949;hpb=0a3441ee8e0e747cfa05eff02df0d918ed5d6acb;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/Storage/DBIHacks.pm b/lib/DBIx/Class/Storage/DBIHacks.pm index f5a03c9..defcecd 100644 --- a/lib/DBIx/Class/Storage/DBIHacks.pm +++ b/lib/DBIx/Class/Storage/DBIHacks.pm @@ -60,7 +60,7 @@ sub _adjust_select_args_for_complex_prefetch { my ($self, $from, $select, $where, $attrs) = @_; $self->throw_exception ('Nothing to prefetch... how did we get here?!') - if not @{$attrs->{_prefetch_select}}; + if not @{$attrs->{_prefetch_selector_range}}; $self->throw_exception ('Complex prefetches are not supported on resultsets with a custom from attribute') if (ref $from ne 'ARRAY' || ref $from->[0] ne 'HASH' || ref $from->[1] ne 'ARRAY'); @@ -71,7 +71,7 @@ sub _adjust_select_args_for_complex_prefetch { delete $outer_attrs->{$_} for qw/where bind rows offset group_by having/; my $inner_attrs = { %$attrs }; - delete $inner_attrs->{$_} for qw/for collapse _prefetch_select _collapse_order_by select as/; + delete $inner_attrs->{$_} for qw/for collapse _prefetch_selector_range _collapse_order_by select as/; # bring over all non-collapse-induced order_by into the inner query (if any) @@ -88,7 +88,9 @@ sub _adjust_select_args_for_complex_prefetch { # on the outside we substitute any function for its alias my $outer_select = [ @$select ]; my $inner_select = []; - for my $i (0 .. ( @$outer_select - @{$outer_attrs->{_prefetch_select}} - 1) ) { + + my ($p_start, $p_end) = @{$outer_attrs->{_prefetch_selector_range}}; + for my $i (0 .. $p_start - 1, $p_end + 1 .. $#$outer_select) { my $sel = $outer_select->[$i]; if (ref $sel eq 'HASH' ) { @@ -115,16 +117,30 @@ sub _adjust_select_args_for_complex_prefetch { group_by => ['dummy'], %$inner_attrs, }); - # if a multi-type join was needed in the subquery - add a group_by to simulate the - # collapse in the subq + my $inner_aliastypes = + $self->_resolve_aliastypes_from_select_args( $inner_from, $inner_select, $where, $inner_attrs ); + + # if a multi-type non-selecting (only restricting) join was needed in the subquery + # add a group_by to simulate the collapse in the subq if ( ! $inner_attrs->{group_by} and - first { ! $_->[0]{-is_single} } (@{$inner_from}[1 .. $#$inner_from]) + first { + $inner_aliastypes->{restricting}{$_} + and + ! $inner_aliastypes->{selecting}{$_} + } ( keys %{$inner_aliastypes->{multiplying}||{}} ) ) { - $inner_attrs->{group_by} = $self->_group_over_selection ( + my $unprocessed_order_chunks; + ($inner_attrs->{group_by}, $unprocessed_order_chunks) = $self->_group_over_selection ( $inner_from, $inner_select, $inner_attrs->{order_by} ); + + $self->throw_exception ( + 'A required group_by clause could not be constructed automatically due to a complex ' + . 'order_by criteria. Either order_by columns only (no functions) or construct a suitable ' + . 'group_by by hand' + ) if $unprocessed_order_chunks; } # we already optimized $inner_from above @@ -140,7 +156,7 @@ sub _adjust_select_args_for_complex_prefetch { +{ -alias => $attrs->{alias}, - -source_handle => $inner_from->[0]{-source_handle}, + -rsrc => $inner_from->[0]{-rsrc}, $attrs->{alias} => $subq, }; }; @@ -183,6 +199,7 @@ sub _adjust_select_args_for_complex_prefetch { # also throw in a group_by if restricting to guard against # cross-join explosions # + my $need_outer_group_by; while (my $j = shift @$from) { my $alias = $j->[0]{-alias}; @@ -191,13 +208,28 @@ sub _adjust_select_args_for_complex_prefetch { } elsif ($outer_aliastypes->{restricting}{$alias}) { push @outer_from, $j; - $outer_attrs->{group_by} ||= $outer_select unless $j->[0]{-is_single}; + $need_outer_group_by ||= ! $j->[0]{-is_single}; } } # demote the outer_from head $outer_from[0] = $outer_from[0][0]; + if ($need_outer_group_by and ! $outer_attrs->{group_by}) { + + my $unprocessed_order_chunks; + ($outer_attrs->{group_by}, $unprocessed_order_chunks) = $self->_group_over_selection ( + \@outer_from, $outer_select, $outer_attrs->{order_by} + ); + + $self->throw_exception ( + 'A required group_by clause could not be constructed automatically due to a complex ' + . 'order_by criteria. Either order_by columns only (no functions) or construct a suitable ' + . 'group_by by hand' + ) if $unprocessed_order_chunks; + + } + # This is totally horrific - the $where ends up in both the inner and outer query # Unfortunately not much can be done until SQLA2 introspection arrives, and even # then if where conditions apply to the *right* side of the prefetch, you may have @@ -247,7 +279,11 @@ sub _resolve_aliastypes_from_select_args { # set up a botched SQLA my $sql_maker = $self->sql_maker; - local $sql_maker->{having_bind}; # these are throw away results + # these are throw away results, do not pollute the bind stack + local $sql_maker->{select_bind}; + local $sql_maker->{where_bind}; + local $sql_maker->{group_bind}; + local $sql_maker->{having_bind}; # we can't scan properly without any quoting (\b doesn't cut it # everywhere), so unless there is proper quoting set - use our @@ -259,7 +295,9 @@ sub _resolve_aliastypes_from_select_args { local $sql_maker->{name_sep} = $sql_maker->{name_sep}; unless (defined $sql_maker->{quote_char} and length $sql_maker->{quote_char}) { - $sql_maker->{quote_char} = "\x00"; + $sql_maker->{quote_char} = ["\x00", "\xFF"]; + # if we don't unset it we screw up retarded but unfortunately working + # 'MAX(foo.bar)' => { '>', 3 } $sql_maker->{name_sep} = ''; } @@ -274,8 +312,8 @@ sub _resolve_aliastypes_from_select_args { }), ], selecting => [ - $self->_extract_order_columns ($attrs->{order_by}, $sql_maker), $sql_maker->_recurse_fields ($select), + ( map { $_->[0] } $self->_extract_order_criteria ($attrs->{order_by}, $sql_maker) ), ], }; @@ -334,6 +372,7 @@ sub _resolve_aliastypes_from_select_args { return $aliases_by_type; } +# This is the engine behind { distinct => 1 } sub _group_over_selection { my ($self, $from, $select, $order_by) = @_; @@ -341,6 +380,9 @@ sub _group_over_selection { 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) { if (! ref($_) or ref ($_) ne 'HASH' ) { push @group_by, $_; @@ -354,17 +396,28 @@ sub _group_over_selection { # add any order_by parts that are not already present in the group_by # we need to be careful not to add any named functions/aggregates - # i.e. select => [ ... { count => 'foo', -as 'foocount' } ... ] - for my $chunk ($self->_extract_order_columns($order_by)) { + # i.e. order_by => [ ... { count => 'foo' } ... ] + my @leftovers; + for ($self->_extract_order_criteria($order_by)) { # only consider real columns (for functions the user got to do an explicit group_by) - my $colinfo = $rs_column_list->{$chunk} - or next; + if (@$_ != 1) { + push @leftovers, $_; + next; + } + my $chunk = $_->[0]; + my $colinfo = $rs_column_list->{$chunk} or do { + push @leftovers, $_; + next; + }; $chunk = "$colinfo->{-source_alias}.$chunk" if $chunk !~ /\./; push @group_by, $chunk unless $group_index{$chunk}++; } - return \@group_by; + return wantarray + ? (\@group_by, (@leftovers ? \@leftovers : undef) ) + : \@group_by + ; } sub _resolve_ident_sources { @@ -392,8 +445,8 @@ sub _resolve_ident_sources { $tabinfo = $_->[0]; } - $alias2source->{$tabinfo->{-alias}} = $tabinfo->{-source_handle}->resolve - if ($tabinfo->{-source_handle}); + $alias2source->{$tabinfo->{-alias}} = $tabinfo->{-rsrc} + if ($tabinfo->{-rsrc}); } } @@ -411,7 +464,7 @@ sub _resolve_column_info { my ($self, $ident, $colnames) = @_; my ($alias2src, $root_alias) = $self->_resolve_ident_sources($ident); - my (%return, %seen_cols, @auto_colnames); + my (%seen_cols, @auto_colnames); # compile a global list of column names, to be able to properly # disambiguate unqualified column names (if at all possible) @@ -428,25 +481,23 @@ sub _resolve_column_info { grep { @{$seen_cols{$_}} == 1 } (keys %seen_cols), ]; - COLUMN: + my (%return, $colinfos); foreach my $col (@$colnames) { - my ($alias, $colname) = $col =~ m/^ (?: ([^\.]+) \. )? (.+) $/x; + my ($source_alias, $colname) = $col =~ m/^ (?: ([^\.]+) \. )? (.+) $/x; - unless ($alias) { - # see if the column was seen exactly once (so we know which rsrc it came from) - if ($seen_cols{$colname} and @{$seen_cols{$colname}} == 1) { - $alias = $seen_cols{$colname}[0]; - } - else { - next COLUMN; - } - } + # if the column was seen exactly once - we know which rsrc it came from + $source_alias ||= $seen_cols{$colname}[0] + if ($seen_cols{$colname} and @{$seen_cols{$colname}} == 1); - my $rsrc = $alias2src->{$alias}; - $return{$col} = $rsrc && { - %{$rsrc->column_info($colname)}, + next unless $source_alias; + + my $rsrc = $alias2src->{$source_alias} + or next; + + $return{$col} = { + %{ ( $colinfos->{$source_alias} ||= $rsrc->columns_info )->{$colname} }, -result_source => $rsrc, - -source_alias => $alias, + -source_alias => $source_alias, }; } @@ -528,6 +579,26 @@ sub _inner_join_to_node { # at all. What this code tries to do (badly) is introspect the condition # and remove all column qualifiers. If it bails out early (returns undef) # the calling code should try another approach (e.g. a subquery) + +sub _strip_cond_qualifiers_from_array { + my ($self, $where) = @_; + my @cond; + for (my $i = 0; $i < @$where; $i++) { + my $entry = $where->[$i]; + my $hash; + my $ref = ref $entry; + if ($ref eq 'HASH' or $ref eq 'ARRAY') { + $hash = $self->_strip_cond_qualifiers($entry); + } + elsif (! $ref) { + $entry =~ /([^.]+)$/; + $hash->{$1} = $where->[++$i]; + } + push @cond, $hash; + } + return \@cond; +} + sub _strip_cond_qualifiers { my ($self, $where) = @_; @@ -537,37 +608,12 @@ sub _strip_cond_qualifiers { return $cond unless $where; if (ref $where eq 'ARRAY') { - $cond = [ - map { - my %hash; - foreach my $key (keys %{$_}) { - $key =~ /([^.]+)$/; - $hash{$1} = $_->{$key}; - } - \%hash; - } @$where - ]; + $cond = $self->_strip_cond_qualifiers_from_array($where); } elsif (ref $where eq 'HASH') { if ( (keys %$where) == 1 && ( (keys %{$where})[0] eq '-and' )) { - $cond->{-and} = []; - my @cond = @{$where->{-and}}; - for (my $i = 0; $i < @cond; $i++) { - my $entry = $cond[$i]; - my $hash; - my $ref = ref $entry; - if ($ref eq 'HASH' or $ref eq 'ARRAY') { - $hash = $self->_strip_cond_qualifiers($entry); - } - elsif (! $ref) { - $entry =~ /([^.]+)$/; - $hash->{$1} = $cond[++$i]; - } - else { - $self->throw_exception ("_strip_cond_qualifiers() is unable to handle a condition reftype $ref"); - } - push @{$cond->{-and}}, $hash; - } + $cond->{-and} = + $self->_strip_cond_qualifiers_from_array($where->{-and}); } else { foreach my $key (keys %$where) { @@ -588,7 +634,7 @@ sub _strip_cond_qualifiers { return $cond; } -sub _extract_order_columns { +sub _extract_order_criteria { my ($self, $order_by, $sql_maker) = @_; my $parser = sub { @@ -598,8 +644,9 @@ sub _extract_order_columns { unless wantarray; my @chunks; - for my $chunk (map { ref $_ ? @$_ : $_ } ($sql_maker->_order_by_chunks ($order_by) ) ) { - $chunk =~ s/\s+ (?: ASC|DESC ) \s* $//ix; + for ($sql_maker->_order_by_chunks ($order_by) ) { + my $chunk = ref $_ ? $_ : [ $_ ]; + $chunk->[0] =~ s/\s+ (?: ASC|DESC ) \s* $//ix; push @chunks, $chunk; }