- Fix update/delete operations on resultsets *joining* the updated
table failing on MySQL. Resolves oversights in the fixes for
RT#81378 and RT#81897
+ - Stop erroneously considering order_by criteria from a join under
+ distinct => 1 (the distinct should apply to the main source only)
- Even more robust behavior of GenericSubQuery limit dialect
- Stop Sybase ASE storage from generating invalid SQL in subselects
when a limit without offset is encountered
$attrs->{_grouped_by_distinct} = 1;
# distinct affects only the main selection part, not what prefetch may
# add below.
- $attrs->{group_by} = $source->storage->_group_over_selection (
- $attrs->{from},
- $attrs->{select},
- $attrs->{order_by},
- );
+ $attrs->{group_by} = $source->storage->_group_over_selection($attrs);
}
}
$attrs->{rows} = $sql_maker->__max_int;
}
- my ($complex_prefetch, @limit);
-
# see if we will need to tear the prefetch apart to satisfy group_by == select
- # this is *extremely tricky* to get right
+ # this is *extremely tricky* to get right, I am still not sure I did
#
- # Follows heavy but necessary analyzis of the group_by - if it refers to any
- # sort of non-root column assume the user knows what they are doing and do
- # not try to be clever
- if (
- $attrs->{_related_results_construction}
+ my ($prefetch_needs_subquery, @limit_args);
+
+ if ( $attrs->{_grouped_by_distinct} and $attrs->{collapse} ) {
+ # we already know there is a valid group_by and we know it is intended
+ # to be based *only* on the main result columns
+ # short circuit the group_by parsing below
+ $prefetch_needs_subquery = 1;
+ }
+ elsif (
+ # The rationale is that even if we do *not* have collapse, we still
+ # need to wrap the core grouped select/group_by in a subquery
+ # so that databases that care about group_by/select equivalence
+ # are happy (this includes MySQL in strict_mode)
+ # If any of the other joined tables are referenced in the group_by
+ # however - the user is on their own
+ ( $prefetch_needs_subquery or $attrs->{_related_results_construction} )
and
$attrs->{group_by}
and
@{$attrs->{group_by}}
and
- my $grp_aliases = try {
+ my $grp_aliases = try { # try{} because $attrs->{from} may be unreadable
$self->_resolve_aliastypes_from_select_args( $attrs->{from}, undef, undef, { group_by => $attrs->{group_by} } )
}
) {
- $complex_prefetch = ! defined first { $_ ne $attrs->{alias} } keys %{ $grp_aliases->{grouping} || {} };
+ # no aliases other than our own in group_by
+ # if there are - do not allow subquery even if limit is present
+ $prefetch_needs_subquery = ! scalar grep { $_ ne $attrs->{alias} } keys %{ $grp_aliases->{grouping} || {} };
+ }
+ elsif ( $attrs->{rows} && $attrs->{collapse} ) {
+ # active collapse with a limit - that one is a no-brainer unless
+ # overruled by a group_by above
+ $prefetch_needs_subquery = 1;
}
- $complex_prefetch ||= ( $attrs->{rows} && $attrs->{collapse} );
-
- if ($complex_prefetch) {
+ if ($prefetch_needs_subquery) {
($ident, $select, $where, $attrs) =
$self->_adjust_select_args_for_complex_prefetch ($ident, $select, $where, $attrs);
}
elsif (! $attrs->{software_limit} ) {
- push @limit, (
+ push @limit_args, (
$attrs->{rows} || (),
$attrs->{offset} || (),
);
# try to simplify the joinmap further (prune unreferenced type-single joins)
if (
- ! $complex_prefetch
+ ! $prefetch_needs_subquery # already pruned
and
ref $ident
and
# invoked, and that's just bad...
###
- return ('select', $ident, $select, $where, $attrs, @limit);
+ return ('select', $ident, $select, $where, $attrs, @limit_args);
}
# Returns a counting SELECT for a simple count
# 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
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}}
) {
) {
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 '
# 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
# 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,
+ });
}
}
# 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, [
};
}
- 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
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 '
# 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}++;
}
use DBIx::Class::SQLMaker::LimitDialects;
my $ROWS = DBIx::Class::SQLMaker::LimitDialects->__rows_bindtype;
+my $OFFSET = DBIx::Class::SQLMaker::LimitDialects->__offset_bindtype;
my $schema = DBICTest->init_schema();
my $sdebug = $schema->storage->debug;
);
}
+# make sure distinct applies to the CD part only, not to the order_by part
{
- my $rs = $schema->resultset('CD')->search({},
- {
- '+select' => [{ count => 'tags.tag' }],
- '+as' => ['test_count'],
- prefetch => ['tags'],
- distinct => 1,
- order_by => {'-asc' => 'tags.tag'},
- rows => 1
- }
+ my $rs = $schema->resultset('CD')->search({}, {
+ columns => [qw( cdid title )],
+ '+select' => [{ count => 'tags.tag' }],
+ '+as' => ['test_count'],
+ prefetch => ['tags'],
+ distinct => 1,
+ order_by => {'-desc' => 'tags.tag'},
+ offset => 1,
+ rows => 3,
+ });
+
+ is_same_sql_bind($rs->as_query,
+ '(
+ SELECT me.cdid, me.title, me.test_count,
+ tags.tagid, tags.cd, tags.tag
+ FROM (
+ SELECT me.cdid, me.title,
+ COUNT( tags.tag ) AS test_count
+ FROM cd me
+ LEFT JOIN tags tags
+ ON tags.cd = me.cdid
+ GROUP BY me.cdid, me.title
+ ORDER BY MAX( tags.tag ) DESC
+ LIMIT ?
+ OFFSET ?
+ ) me
+ LEFT JOIN tags tags
+ ON tags.cd = me.cdid
+ ORDER BY tags.tag DESC
+ )',
+ [ [$ROWS => 3], [$OFFSET => 1] ],
+ 'Expected limited prefetch with distinct SQL',
+ );
+
+ my $expected_hri = [
+ { cdid => 4, test_count => 2, title => "Generic Manufactured Singles", tags => [
+ { cd => 4, tag => "Shiny", tagid => 9 },
+ { cd => 4, tag => "Cheesy", tagid => 6 },
+ ]},
+ {
+ cdid => 5, test_count => 2, title => "Come Be Depressed With Us", tags => [
+ { cd => 5, tag => "Cheesy", tagid => 7 },
+ { cd => 5, tag => "Blue", tagid => 4 },
+ ]},
+ {
+ cdid => 1, test_count => 1, title => "Spoonful of bees", tags => [
+ { cd => 1, tag => "Blue", tagid => 1 },
+ ]},
+ ];
+
+ is_deeply (
+ $rs->all_hri,
+ $expected_hri,
+ 'HRI dump of limited prefetch with distinct as expected'
+ );
+
+ # pre-multiplied main source also should work
+ $rs = $schema->resultset('CD')->search_related('artist')->search_related('cds', {}, {
+ columns => [qw( cdid title )],
+ '+select' => [{ count => 'tags.tag' }],
+ '+as' => ['test_count'],
+ prefetch => ['tags'],
+ distinct => 1,
+ order_by => {'-desc' => 'tags.tag'},
+ offset => 1,
+ rows => 3,
+ });
+
+ is_same_sql_bind($rs->as_query,
+ '(
+ SELECT cds.cdid, cds.title, cds.test_count,
+ tags.tagid, tags.cd, tags.tag
+ FROM cd me
+ JOIN artist artist
+ ON artist.artistid = me.artist
+ JOIN (
+ SELECT cds.cdid, cds.title,
+ COUNT( tags.tag ) AS test_count,
+ cds.artist
+ FROM cd me
+ JOIN artist artist
+ ON artist.artistid = me.artist
+ JOIN cd cds
+ ON cds.artist = artist.artistid
+ LEFT JOIN tags tags
+ ON tags.cd = cds.cdid
+ GROUP BY cds.cdid, cds.title, cds.artist
+ ORDER BY MAX( tags.tag ) DESC
+ LIMIT ?
+ OFFSET ?
+ ) cds
+ ON cds.artist = artist.artistid
+ LEFT JOIN tags tags
+ ON tags.cd = cds.cdid
+ ORDER BY tags.tag DESC
+ )',
+ [ [$ROWS => 3], [$OFFSET => 1] ],
+ 'Expected limited prefetch with distinct SQL on premultiplied head',
+ );
+
+ # Tag counts are multiplied by the cd->artist->cds multiplication
+ # I would *almost* call this "expected" without wraping an as_subselect_rs
+ {
+ local $TODO = 'Not sure if we can stop the count/group of premultiplication abstraction leak';
+ is_deeply (
+ $rs->all_hri,
+ $expected_hri,
+ 'HRI dump of limited prefetch with distinct as expected on premultiplid head'
);
- is_same_sql_bind($rs->as_query, q{
- (SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track, me.test_count, tags.tagid, tags.cd, tags.tag
- FROM (SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track, COUNT( tags.tag ) AS test_count
- FROM cd me LEFT JOIN tags tags ON tags.cd = me.cdid
- GROUP BY me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track, tags.tag
- ORDER BY tags.tag ASC LIMIT ?)
- me
- LEFT JOIN tags tags ON tags.cd = me.cdid
- ORDER BY tags.tag ASC
- )
- }, [[$ROWS => 1]]);
+ }
}
done_testing;