X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FStorage%2FDBI.pm;h=1a1aeec2e97f285f68652cc47147dd2cad040dea;hb=9041a97a57cd2831d20e3b25772b29f9af3d8ef9;hp=0b4c4b73b1d1a36a7c8778cea96ca5c3d0101538;hpb=4913997492ff5a1db89762fc4713512e0ffc142e;p=dbsrgits%2FDBIx-Class-Historic.git diff --git a/lib/DBIx/Class/Storage/DBI.pm b/lib/DBIx/Class/Storage/DBI.pm index 0b4c4b7..1a1aeec 100644 --- a/lib/DBIx/Class/Storage/DBI.pm +++ b/lib/DBIx/Class/Storage/DBI.pm @@ -927,6 +927,22 @@ sub _fix_bind_params { } @bind; } +sub _flatten_bind_params { + my ($self, @bind) = @_; + + ### Turn @bind from something like this: + ### ( [ "artist", 1 ], [ "cdid", 1, 3 ] ) + ### to this: + ### ( 1, 1, 3 ) + return + map { + if ( defined( $_ && $_->[1] ) ) { + @{$_}[ 1 .. $#$_ ]; + } + else { undef; } + } @bind; +} + sub _query_start { my ( $self, $sql, @bind ) = @_; @@ -1191,13 +1207,25 @@ sub _per_row_update_delete { sub _select { my $self = shift; + + # localization is neccessary as + # 1) there is no infrastructure to pass this around (easy to do, but will wait) + # 2) _select_args sets it and _prep_for_execute consumes it my $sql_maker = $self->sql_maker; + local $sql_maker->{for}; + return $self->_execute($self->_select_args(@_)); } sub _select_args_to_query { my $self = shift; + # localization is neccessary as + # 1) there is no infrastructure to pass this around (easy to do, but will wait) + # 2) _select_args sets it and _prep_for_execute consumes it + my $sql_maker = $self->sql_maker; + local $sql_maker->{for}; + # my ($op, $bind, $ident, $bind_attrs, $select, $cond, $order, $rows, $offset) # = $self->_select_args($ident, $select, $cond, $attrs); my ($op, $bind, $ident, $bind_attrs, @args) = @@ -1205,28 +1233,22 @@ sub _select_args_to_query { # my ($sql, $prepared_bind) = $self->_prep_for_execute($op, $bind, $ident, [ $select, $cond, $order, $rows, $offset ]); my ($sql, $prepared_bind) = $self->_prep_for_execute($op, $bind, $ident, \@args); + $prepared_bind ||= []; - return \[ "($sql)", @{ $prepared_bind || [] }]; + return wantarray + ? ($sql, $prepared_bind, $bind_attrs) + : \[ "($sql)", @$prepared_bind ] + ; } sub _select_args { - my ($self, $ident, $select, $condition, $attrs) = @_; + my ($self, $ident, $select, $where, $attrs) = @_; - my $for = delete $attrs->{for}; my $sql_maker = $self->sql_maker; - - local $sql_maker->{for} = $for; - - my $order = { map - { $attrs->{$_} ? ( $_ => $attrs->{$_} ) : () } - (qw/order_by group_by having _virtual_order_by/ ) - }; - - - my $bind_attrs = {}; - my $alias2source = $self->_resolve_ident_sources ($ident); + # calculate bind_attrs before possible $ident mangling + my $bind_attrs = {}; for my $alias (keys %$alias2source) { my $bindtypes = $self->source_bind_attributes ($alias2source->{$alias}) || {}; for my $col (keys %$bindtypes) { @@ -1239,15 +1261,7 @@ sub _select_args { } } - # This would be the point to deflate anything found in $condition - # (and leave $attrs->{bind} intact). Problem is - inflators historically - # expect a row object. And all we have is a resultsource (it is trivial - # to extract deflator coderefs via $alias2source above). - # - # I don't see a way forward other than changing the way deflators are - # invoked, and that's just bad... - - my @args = ('select', $attrs->{bind}, $ident, $bind_attrs, $select, $condition, $order); + my @limit; if ($attrs->{software_limit} || $sql_maker->_default_limit_syntax eq "GenericSubQ") { $attrs->{software_limit} = 1; @@ -1257,9 +1271,165 @@ sub _select_args { # MySQL actually recommends this approach. I cringe. $attrs->{rows} = 2**48 if not defined $attrs->{rows} and defined $attrs->{offset}; - push @args, $attrs->{rows}, $attrs->{offset}; + + if ($attrs->{rows} && keys %{$attrs->{collapse}}) { + ($ident, $select, $where, $attrs) + = $self->_adjust_select_args_for_limited_prefetch ($ident, $select, $where, $attrs); + } + else { + push @limit, $attrs->{rows}, $attrs->{offset}; + } } - return @args; + +### + # This would be the point to deflate anything found in $where + # (and leave $attrs->{bind} intact). Problem is - inflators historically + # expect a row object. And all we have is a resultsource (it is trivial + # to extract deflator coderefs via $alias2source above). + # + # I don't see a way forward other than changing the way deflators are + # invoked, and that's just bad... +### + + my $order = { map + { $attrs->{$_} ? ( $_ => $attrs->{$_} ) : () } + (qw/order_by group_by having _virtual_order_by/ ) + }; + + + $sql_maker->{for} = delete $attrs->{for}; + + return ('select', $attrs->{bind}, $ident, $bind_attrs, $select, $where, $order, @limit); +} + +sub _adjust_select_args_for_limited_prefetch { + my ($self, $from, $select, $where, $attrs) = @_; + + if ($attrs->{group_by} and @{$attrs->{group_by}}) { + $self->throw_exception ('Prefetch with limit (rows/offset) is not supported on resultsets with a group_by attribute'); + } + + $self->throw_exception ('Prefetch with limit (rows/offset) is not supported on resultsets with a custom from attribute') + if (ref $from ne 'ARRAY'); + + # separate attributes + my $sub_attrs = { %$attrs }; + delete $attrs->{$_} for qw/where bind rows offset/; + delete $sub_attrs->{$_} for qw/for collapse select order_by/; + + my $alias = $attrs->{alias}; + + # create subquery select list + my $sub_select = [ grep { $_ =~ /^$alias\./ } @{$attrs->{select}} ]; + + # bring over all non-collapse-induced order_by into the inner query (if any) + # the outer one will have to keep them all + if (my $ord_cnt = @{$attrs->{order_by}} - @{$attrs->{_collapse_order_by}} ) { + $sub_attrs->{order_by} = [ + @{$attrs->{order_by}}[ 0 .. ($#{$attrs->{order_by}} - $ord_cnt - 1) ] + ]; + } + + + # mangle the head of the {from} + my $self_ident = shift @$from; + + my %join_info = map { $_->[0]{-alias} => $_->[0] } (@$from); + + my (%inner_joins); + + # decide which parts of the join will remain on the inside + # + # this is not a very viable optimisation, but it was written + # before I realised this, so might as well remain. We can throw + # away _any_ branches of the join tree that are: + # 1) not mentioned in the condition/order + # 2) left-join leaves (or left-join leaf chains) + # Most of the join ocnditions will not satisfy this, but for real + # complex queries some might, and we might make some RDBMS happy. + # + # + # since we do not have introspectable SQLA, we fall back to ugly + # scanning of raw SQL for WHERE, and for pieces of ORDER BY + # in order to determine what goes into %inner_joins + # It may not be very efficient, but it's a reasonable stop-gap + { + # produce stuff unquoted, so it can be scanned + my $sql_maker = $self->sql_maker; + local $sql_maker->{quote_char}; + + my @order_by = (map + { ref $_ ? $_->[0] : $_ } + $sql_maker->_order_by_chunks ($sub_attrs->{order_by}) + ); + + my $where_sql = $sql_maker->where ($where); + + # sort needed joins + for my $alias (keys %join_info) { + + # any table alias found on a column name in where or order_by + # gets included in %inner_joins + # Also any parent joins that are needed to reach this particular alias + for my $piece ($where_sql, @order_by ) { + if ($piece =~ /\b$alias\./) { + $inner_joins{$alias} = 1; + } + } + } + } + + # scan for non-leaf/non-left joins and mark as needed + # also mark all ancestor joins that are needed to reach this particular alias + # (e.g. join => { cds => 'tracks' } - tracks will bring cds too ) + # + # traverse by the size of the -join_path i.e. reverse depth first + for my $alias (sort { @{$join_info{$b}{-join_path}} <=> @{$join_info{$a}{-join_path}} } (keys %join_info) ) { + + my $j = $join_info{$alias}; + $inner_joins{$alias} = 1 if (! $j->{-join_type} || ($j->{-join_type} !~ /^left$/i) ); + + if ($inner_joins{$alias}) { + $inner_joins{$_} = 1 for (@{$j->{-join_path}}); + } + } + + # construct the inner $from for the subquery + my $inner_from = [ $self_ident ]; + if (keys %inner_joins) { + for my $j (@$from) { + push @$inner_from, $j if $inner_joins{$j->[0]{-alias}}; + } + + # if a multi-type join was needed in the subquery ("multi" is indicated by + # presence in {collapse}) - add a group_by to simulate the collapse in the subq + for my $alias (keys %inner_joins) { + + # the dot comes from some weirdness in collapse + # remove after the rewrite + if ($attrs->{collapse}{".$alias"}) { + $sub_attrs->{group_by} = $sub_select; + last; + } + } + } + + # generate the subquery + my $subq = $self->_select_args_to_query ( + $inner_from, + $sub_select, + $where, + $sub_attrs + ); + + # put it back in $from + unshift @$from, { $alias => $subq }; + + # 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 + # + # OTOH it can be seen as a plus: (notes that this query would make a DBA cry ;) + return ($from, $select, $where, $attrs); } sub _resolve_ident_sources { @@ -1284,21 +1454,28 @@ sub _resolve_ident_sources { $tabinfo = $_->[0]; } - $alias2source->{$tabinfo->{-alias}} = $tabinfo->{-result_source} - if ($tabinfo->{-result_source}); + $alias2source->{$tabinfo->{-alias}} = $tabinfo->{-source_handle}->resolve + if ($tabinfo->{-source_handle}); } } return $alias2source; } -sub count { +sub _copy_attributes_for_count { my ($self, $source, $attrs) = @_; + my %attrs = %$attrs; + + # take off any column specs, any pagers, record_filter is cdbi, and no point of ordering a count + delete @attrs{qw/select as rows offset page order_by record_filter/}; + + return \%attrs; +} - my $tmp_attrs = { %$attrs }; +sub count { + my ($self, $source, $attrs) = @_; - # take off any pagers, record_filter is cdbi, and no point of ordering a count - delete $tmp_attrs->{$_} for (qw/select as rows offset page order_by record_filter/); + my $tmp_attrs = $self->_copy_attributes_for_count($source, $attrs); # overwrite the selector $tmp_attrs->{select} = { count => '*' }; @@ -1322,7 +1499,7 @@ sub count_grouped { my $sub_attrs = { %$attrs }; # these can not go in the subquery, and there is no point of ordering it - delete $sub_attrs->{$_} for qw/prefetch collapse select as order_by/; + delete $sub_attrs->{$_} for qw/collapse select as order_by/; # if we prefetch, we group_by primary keys only as this is what we would get out of the rs via ->next/->all # simply deleting group_by suffices, as the code below will re-fill it @@ -1339,7 +1516,7 @@ sub count_grouped { }]; # the subquery replaces this - delete $attrs->{$_} for qw/where bind prefetch collapse group_by having having_bind rows offset page pager/; + delete $attrs->{$_} for qw/where bind collapse group_by having having_bind rows offset/; return $self->count ($source, $attrs); }