Make sure unaliased selectors and prefetch coexist peacefully
Peter Rabbitson [Wed, 5 Jan 2011 14:52:09 +0000 (15:52 +0100)]
Changes
lib/DBIx/Class/ResultSet.pm
lib/DBIx/Class/Storage/DBI.pm
lib/DBIx/Class/Storage/DBIHacks.pm
t/prefetch/correlated.t
t/prefetch/with_limit.t
t/search/select_chains_unbalanced.t

diff --git a/Changes b/Changes
index eaee66e..96478b4 100644 (file)
--- a/Changes
+++ b/Changes
@@ -1,6 +1,7 @@
 Revision history for DBIx::Class
 
     * Fixes
+        - Unaliased "dark" selectors no longer throw off prefetch
         - Fix proper composition of bind values across all possible
           SQL areas ( group_by => \[ ... ] now works properly )
 
index 733c82d..554c93d 100644 (file)
@@ -1436,7 +1436,7 @@ sub _count_subq_rs {
 
   my $sub_attrs = { %$attrs };
   # extra selectors do not go in the subquery and there is no point of ordering it, nor locking it
-  delete @{$sub_attrs}{qw/collapse columns as select _prefetch_select _trailing_select order_by for/};
+  delete @{$sub_attrs}{qw/collapse columns as select _prefetch_selector_range _trailing_select order_by for/};
 
   # if we multi-prefetch we group_by primary keys only as this is what we would
   # get out of the rs via ->next/->all. We *DO WANT* to clobber old group_by regardless
@@ -1650,7 +1650,7 @@ sub _rs_update_delete {
     my $attrs = $self->_resolved_attrs_copy;
 
 
-    delete $attrs->{$_} for qw/collapse _collapse_order_by select _prefetch_select as/;
+    delete $attrs->{$_} for qw/collapse _collapse_order_by select _prefetch_selector_range as/;
     $attrs->{columns} = [ map { "$attrs->{alias}.$_" } ($self->result_source->_pri_cols) ];
 
     if ($needs_group_by_subq) {
@@ -3243,9 +3243,6 @@ sub _resolved_attrs {
   push @sel, @{ ref $attrs->{select} eq 'ARRAY' ? $attrs->{select} : [ $attrs->{select} ] }
     if $attrs->{select};
 
-  push @sel, @{$attrs->{_trailing_select}}
-    if $attrs->{_trailing_select};
-
   # assume all unqualified selectors to apply to the current alias (legacy stuff)
   for (@sel) {
     $_ = (ref $_ or $_ =~ /\./) ? $_ : "$alias.$_";
@@ -3331,8 +3328,13 @@ sub _resolved_attrs {
       carp ("Useless use of distinct on a grouped resultset ('distinct' is ignored when a 'group_by' is present)");
     }
     else {
+      # distinct affects only the main selection part, not what prefetch may
+      # add below. However trailing is not yet a part of the selection as
+      # prefetch must insert before it
       $attrs->{group_by} = $source->storage->_group_over_selection (
-        @{$attrs}{qw/from select order_by/}
+        $attrs->{from},
+        [ @{$attrs->{select}||[]}, @{$attrs->{_trailing_select}||[]} ],
+        $attrs->{order_by},
       );
     }
   }
@@ -3368,15 +3370,22 @@ sub _resolved_attrs {
       $source->_resolve_prefetch( $prefetch, $alias, $join_map, $prefetch_ordering, $attrs->{collapse} );
 
     # we need to somehow mark which columns came from prefetch
-    $attrs->{_prefetch_select} = [ map { $_->[0] } @prefetch ];
+    if (@prefetch) {
+      my $sel_end = $#{$attrs->{select}};
+      $attrs->{_prefetch_selector_range} = [ $sel_end + 1, $sel_end + @prefetch ];
+    }
 
-    push @{ $attrs->{select} }, @{$attrs->{_prefetch_select}};
+    push @{ $attrs->{select} }, (map { $_->[0] } @prefetch);
     push @{ $attrs->{as} }, (map { $_->[1] } @prefetch);
 
     push( @{$attrs->{order_by}}, @$prefetch_ordering );
     $attrs->{_collapse_order_by} = \@$prefetch_ordering;
   }
 
+
+  push @sel, @{$attrs->{_trailing_select}}
+    if $attrs->{_trailing_select};
+
   # if both page and offset are specified, produce a combined offset
   # even though it doesn't make much sense, this is what pre 081xx has
   # been doing
index d86a763..e67d36b 100644 (file)
@@ -2087,9 +2087,7 @@ sub _select_args {
         &&
       @{$attrs->{group_by}}
         &&
-      $attrs->{_prefetch_select}
-        &&
-      @{$attrs->{_prefetch_select}}
+      $attrs->{_prefetch_selector_range}
     )
   ) {
     ($ident, $select, $where, $attrs)
index 864cbf5..cbf4626 100644 (file)
@@ -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' ) {
@@ -197,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};
 
@@ -205,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
@@ -362,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, $_;
index e4e747d..7e7690d 100644 (file)
@@ -19,10 +19,6 @@ my $cd_data = { map {
   },
 } ( $cdrs->all ) };
 
-my $queries = 0;
-$schema->storage->debugcb(sub { $queries++; });
-$schema->storage->debug(1);
-
 my $c_rs = $cdrs->search ({}, {
   prefetch => 'tracks',
   '+columns' => { sibling_count => $cdrs->search(
@@ -53,13 +49,21 @@ is_same_sql_bind(
     ORDER BY tracks.cd
   )',
   [
+
+    # subselect
     [ 'siblings.cdid' => 'bogus condition' ],
     [ 'me.artist' => 2 ],
+
+    # outher WHERE
     [ 'me.artist' => 2 ],
   ],
   'Expected SQL on correlated realiased subquery'
 );
 
+my $queries = 0;
+$schema->storage->debugcb(sub { $queries++; });
+$schema->storage->debug(1);
+
 is_deeply (
   { map
     { $_->cdid => {
@@ -77,4 +81,65 @@ is ($queries, 1, 'Only 1 query fired to retrieve everything');
 $schema->storage->debug($orig_debug);
 $schema->storage->debugcb(undef);
 
+# try to unbalance the select
+
+# first add a lone non-as-ed select
+# it should be reordered to appear at the end without throwing prefetch/bind off
+$c_rs = $c_rs->search({}, { '+select' => \[ 'me.cdid + ?', [ __add => 1 ] ] });
+
+# now add an unbalanced select/as pair
+$c_rs = $c_rs->search ({}, {
+  '+select' => $cdrs->search(
+    { 'siblings.artist' => { -ident => 'me.artist' } },
+    { alias => 'siblings', columns => [
+      { first_year => { min => 'year' }},
+      { last_year => { max => 'year' }},
+    ]},
+  )->as_query,
+  '+as' => [qw/active_from active_to/],
+});
+
+
+is_same_sql_bind(
+  $c_rs->as_query,
+  '(
+    SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track,
+           (SELECT COUNT( * )
+              FROM cd siblings
+            WHERE siblings.artist = me.artist
+              AND siblings.cdid != me.cdid
+              AND siblings.cdid != ?
+              AND me.artist != ?
+           ),
+           (SELECT MIN( year ), MAX( year )
+              FROM cd siblings
+            WHERE siblings.artist = me.artist
+              AND me.artist != ?
+           ),
+           tracks.trackid, tracks.cd, tracks.position, tracks.title, tracks.last_updated_on, tracks.last_updated_at,
+           me.cdid + ?
+      FROM cd me
+      LEFT JOIN track tracks
+        ON tracks.cd = me.cdid
+    WHERE me.artist != ?
+    ORDER BY tracks.cd
+  )',
+  [
+
+    # first subselect
+    [ 'siblings.cdid' => 'bogus condition' ],
+    [ 'me.artist' => 2 ],
+
+    # second subselect
+    [ 'me.artist' => 2 ],
+
+    # the addition
+    [ __add => 1 ],
+
+    # outher WHERE
+    [ 'me.artist' => 2 ],
+  ],
+  'Expected SQL on correlated realiased subquery'
+);
+
 done_testing;
index 4acdcab..f6729b1 100644 (file)
@@ -31,6 +31,62 @@ my $use_prefetch = $no_prefetch->search(
   }
 );
 
+# add a floating +select to make sure it does nto throw things off
+# we also expect it to appear in both selectors, as we can not know
+# for sure which part of the query it applies to (may be order_by,
+# maybe something else)
+#
+# we use a reference to the same array in bind vals, because
+# is_deeply picks up this difference too (not sure if bug or
+# feature)
+my $bind_one = [ __add => 1 ];
+$use_prefetch = $use_prefetch->search({}, {
+  '+select' => \[ 'me.artistid + ?', $bind_one ],
+});
+
+is_same_sql_bind (
+  $use_prefetch->as_query,
+  '(
+    SELECT  me.artistid, me.name,
+            cds.cdid, cds.artist, cds.title, cds.year, cds.genreid, cds.single_track,
+            me.artistid + ?
+      FROM (
+        SELECT me.artistid, me.name,
+               me.artistid + ?
+          FROM artist me
+          LEFT JOIN cd cds
+            ON cds.artist = me.artistid
+          LEFT JOIN cd_artwork artwork
+            ON artwork.cd_id = cds.cdid
+          LEFT JOIN track tracks
+            ON tracks.cd = cds.cdid
+        WHERE   artwork.cd_id IS NULL
+             OR tracks.title != ?
+        GROUP BY me.artistid, me.name, me.artistid + ?
+        ORDER BY name DESC LIMIT 3
+      ) me
+      LEFT JOIN cd cds
+        ON cds.artist = me.artistid
+      LEFT JOIN cd_artwork artwork
+        ON artwork.cd_id = cds.cdid
+      LEFT JOIN track tracks
+        ON tracks.cd = cds.cdid
+    WHERE artwork.cd_id IS NULL
+       OR tracks.title != ?
+    GROUP BY me.artistid, me.name, cds.cdid, cds.artist, cds.title, cds.year, cds.genreid, cds.single_track, me.artistid + ?
+    ORDER BY name DESC, cds.artist, cds.year ASC
+  )',
+  [
+    $bind_one,  # outer select
+    $bind_one,  # inner select
+    [ 'tracks.title' => 'blah-blah-1234568' ], # inner where
+    $bind_one,  # inner group_by
+    [ 'tracks.title' => 'blah-blah-1234568' ], # outer where
+    $bind_one,  # outer group_by
+  ],
+  'Expected SQL on complex limited prefetch'
+);
+
 is($no_prefetch->count, $use_prefetch->count, '$no_prefetch->count == $use_prefetch->count');
 is(
   scalar ($no_prefetch->all),
index d0facb8..d602605 100644 (file)
@@ -34,33 +34,30 @@ my @chain = (
     => [qw/cdid title foo bar/],
 
   {
-    '+select'   => [ 'genreid', $multicol_rs->as_query ],
-    '+as'       => [qw/genreid name rank/],
+    '+select'   => \'unaliased randomness',
   } => 'SELECT
           me.cdid,
           me.title,
           DISTINCT(foo, bar),
-          me.genreid,
-          (SELECT me.name, me.rank FROM artist me WHERE ( artistid 1 ))
+          unaliased randomness
         FROM cd me'
-    => [qw/cdid title foo bar genreid name rank/],
-
+    => [qw/cdid title foo bar/],
   {
-    '+select'   => { count => 'me.cdid', -as => 'cnt' },  # lack of 'as' infers from '-as'
-    '+columns'  => { len => { length => 'me.title' } },
+    '+select'   => [ 'genreid', $multicol_rs->as_query ],
+    '+as'       => [qw/genreid name rank/],
   } => 'SELECT
           me.cdid,
           me.title,
-          LENGTH( me.title ),
-          COUNT( me.cdid ) AS cnt,
           DISTINCT(foo, bar),
           me.genreid,
-          (SELECT me.name, me.rank FROM artist me WHERE ( artistid 1 ))
+          (SELECT me.name, me.rank FROM artist me WHERE ( artistid 1 )),
+          unaliased randomness
         FROM cd me'
-    => [qw/cdid title len cnt foo bar genreid name rank/],
+    => [qw/cdid title foo bar genreid name rank/],
 
   {
-    '+select'   => \'unaliased randomness',
+    '+select'   => { count => 'me.cdid', -as => 'cnt' },  # lack of 'as' infers from '-as'
+    '+columns'  => { len => { length => 'me.title' } },
   } => 'SELECT
           me.cdid,
           me.title,
@@ -73,6 +70,7 @@ my @chain = (
         FROM cd me'
     => [qw/cdid title len cnt foo bar genreid name rank/],
 
+
 );
 
 my $rs = $schema->resultset('CD');
@@ -95,7 +93,7 @@ while (@chain) {
   is_deeply(
     $rs->_resolved_attrs->{as},
     $as,
-    'Correct dbic-side aliasing',
+    "Correct dbic-side aliasing for test $testno",
   );
 
   $testno++;