Radically rethink complex prefetch - make most useful cases just work (tm)
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / Storage / DBIHacks.pm
index 75a8fb2..f136f52 100644 (file)
@@ -72,18 +72,17 @@ sub _prune_unused_joins {
 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_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');
 
+  my $root_alias = $attrs->{alias};
+
   # generate inner/outer attribute lists, remove stuff that doesn't apply
   my $outer_attrs = { %$attrs };
-  delete $outer_attrs->{$_} for qw/where bind rows offset group_by having/;
+  delete $outer_attrs->{$_} for qw/where bind rows offset group_by _grouped_by_distinct having/;
 
   my $inner_attrs = { %$attrs };
-  delete $inner_attrs->{$_} for qw/for collapse _prefetch_selector_range select as/;
+  delete $inner_attrs->{$_} for qw/from for collapse select as _related_results_construction/;
 
   # there is no point of ordering the insides if there is no limit
   delete $inner_attrs->{order_by} if (
@@ -107,7 +106,7 @@ sub _adjust_select_args_for_complex_prefetch {
           : next
     ;
 
-    if ( ($h->{-alias}||'') eq $attrs->{alias} and $h->{-rsrc} ) {
+    if ( ($h->{-alias}||'') eq $root_alias and $h->{-rsrc} ) {
       $root_node = $h;
       $root_node_offset = $i;
       last;
@@ -121,13 +120,16 @@ sub _adjust_select_args_for_complex_prefetch {
   my $colinfo = $self->_resolve_column_info($from);
   my $selected_root_columns;
 
-  my ($p_start, $p_end) = @{$outer_attrs->{_prefetch_selector_range}};
-  for my $i (0 .. $p_start - 1, $p_end + 1 .. $#$outer_select) {
+  for my $i (0 .. $#$outer_select) {
     my $sel = $outer_select->[$i];
 
+    next if (
+      $colinfo->{$sel} and $colinfo->{$sel}{-source_alias} ne $root_alias
+    );
+
     if (ref $sel eq 'HASH' ) {
       $sel->{-as} ||= $attrs->{as}[$i];
-      $outer_select->[$i] = join ('.', $attrs->{alias}, ($sel->{-as} || "inner_column_$i") );
+      $outer_select->[$i] = join ('.', $root_alias, ($sel->{-as} || "inner_column_$i") );
     }
     elsif (! ref $sel and my $ci = $colinfo->{$sel}) {
       $selected_root_columns->{$ci->{-colname}} = 1;
@@ -153,7 +155,7 @@ sub _adjust_select_args_for_complex_prefetch {
   for (sort map { keys %{$_->{-seen_columns}||{}} } map { values %$_ } values %$connecting_aliastypes) {
     my $ci = $colinfo->{$_} or next;
     if (
-      $ci->{-source_alias} eq $attrs->{alias}
+      $ci->{-source_alias} eq $root_alias
         and
       ! $selected_root_columns->{$ci->{-colname}}++
     ) {
@@ -178,27 +180,104 @@ sub _adjust_select_args_for_complex_prefetch {
     my $inner_aliastypes =
       $self->_resolve_aliastypes_from_select_args( $inner_from, $inner_select, $where, $inner_attrs );
 
-    # we need to simulate collapse in the subq if a multiplying join is pulled
-    # by being a non-selecting restrictor
+    # uh-oh a multiplier (which is not us) left in, this is a problem
     if (
-      ! $inner_attrs->{group_by}
+      $inner_aliastypes->{multiplying}
+        and
+      !$inner_aliastypes->{grouping}  # if there are groups - assume user knows wtf they are up to
         and
-      first {
-        $inner_aliastypes->{restricting}{$_}
-          and
-        ! $inner_aliastypes->{selecting}{$_}
-      } ( keys %{$inner_aliastypes->{multiplying}||{}} )
+      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}
-      );
-
-      $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;
+
+      # if none of the multipliers came from an order_by (guaranteed to have been combined
+      # with a limit) - easy - just slap a group_by to simulate a collape and be on our way
+      if (
+        ! $inner_aliastypes->{ordering}
+          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}
+        );
+
+        $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;
+      }
+      else {
+        # We need to order by external columns and group at the same time
+        # so we can calculate the proper limit
+        # This doesn't really make sense in SQL, however from DBICs point
+        # of view is rather valid (order the leftmost objects by whatever
+        # criteria and get the offset/rows many). There is a way around
+        # this however in SQL - we simply tae the direction of each piece
+        # of the foreign order and convert them to MIN(X) for ASC or MAX(X)
+        # 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',
+            $root_node->{-rsrc}->source_name,
+          );
+        for my $col (@pks) {
+          push @$inner_select, $col
+            unless $cur_sel->{$col}++;
+        }
+
+        # wrap any part of the order_by that "responds" to an ordering alias
+        # into a MIN/MAX
+        # FIXME - this code is a joke, will need to be completely rewritten in
+        # the DQ branch. But I need to push a POC here, otherwise the
+        # pesky tests won't pass
+        my $sql_maker = $self->sql_maker;
+        my ($lquote, $rquote, $sep) = map { quotemeta $_ } ($sql_maker->_quote_chars, $sql_maker->name_sep);
+        my $own_re = qr/ $lquote \Q$root_alias\E $rquote $sep | \b \Q$root_alias\E $sep /x;
+        my @order = @{$attrs->{order_by}};
+        my @order_chunks = map { ref $_ eq 'ARRAY' ? $_ : [ $_ ] } $sql_maker->_order_by_chunks (\@order);
+        $self->throw_exception ('Order By parsing failed...') if @order != @order_chunks;
+        for my $i (0 .. $#order) {
+          # skip ourselves, and anything that looks like a literal
+          next if $order_chunks[$i][0] =~ $own_re;
+          next if (ref $order[$i] and ref $order[$i] ne 'HASH');
+
+          my $is_desc = $order_chunks[$i][0] =~ s/\sDESC$//i;
+          $order_chunks[$i][0] =~ s/\sASC$//i;
+
+          $order[$i] = \[
+            sprintf(
+              '%s(%s)%s',
+              ($is_desc ? 'MAX' : 'MIN'),
+              $order_chunks[$i][0],
+              ($is_desc ? ' DESC' : ''),
+            ),
+            @ {$order_chunks[$i]} [ 1 .. $#{$order_chunks[$i]} ]
+          ];
+        }
+
+        $inner_attrs->{order_by} = \@order;
+        ($inner_attrs->{group_by}) = $self->_group_over_selection (
+          $inner_from, $inner_select, $inner_attrs->{order_by}
+        );
+      }
     }
 
     # we already optimized $inner_from above
@@ -236,18 +315,18 @@ sub _adjust_select_args_for_complex_prefetch {
 
     push @outer_from, [
       {
-        -alias => $attrs->{alias},
+        -alias => $root_alias,
         -rsrc => $root_node->{-rsrc},
-        $attrs->{alias} => $inner_subq,
+        $root_alias => $inner_subq,
       },
       @{$from->[0]}[1 .. $#{$from->[0]}],
     ];
   }
   else {
     @outer_from = {
-      -alias => $attrs->{alias},
+      -alias => $root_alias,
       -rsrc => $root_node->{-rsrc},
-      $attrs->{alias} => $inner_subq,
+      $root_alias => $inner_subq,
     };
   }
 
@@ -259,9 +338,9 @@ sub _adjust_select_args_for_complex_prefetch {
     $self->_resolve_aliastypes_from_select_args( $from, $outer_select, $where, $outer_attrs );
 
   # unroll parents
-  my ($outer_select_chain, $outer_restrict_chain) = map { +{
-    map { $_ => 1 } map { values %$_} map { @{$_->{-parents}} } values %{ $outer_aliastypes->{$_} }
-  } } qw/selecting restricting/;
+  my ($outer_select_chain, @outer_nonselecting_chains) = map { +{
+    map { $_ => 1 } map { values %$_} map { @{$_->{-parents}} } values %{ $outer_aliastypes->{$_} || {} }
+  } } qw/selecting restricting grouping ordering/;
 
   # see what's left - throw away if not selecting/restricting
   # also throw in a group_by if a non-selecting multiplier,
@@ -275,13 +354,13 @@ sub _adjust_select_args_for_complex_prefetch {
     ) {
       push @outer_from, $j
     }
-    elsif ($outer_restrict_chain->{$alias}) {
+    elsif (first { $_->{$alias} } @outer_nonselecting_chains ) {
       push @outer_from, $j;
       $need_outer_group_by ||= $outer_aliastypes->{multiplying}{$alias} ? 1 : 0;
     }
   }
 
-  if ($need_outer_group_by and ! $outer_attrs->{group_by}) {
+  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 (
@@ -379,9 +458,10 @@ sub _resolve_aliastypes_from_select_args {
   my $to_scan = {
     restricting => [
       $sql_maker->_recurse_where ($where),
-      $sql_maker->_parse_rs_attrs ({
-        map { $_ => $attrs->{$_} } (qw/group_by having/)
-      }),
+      $sql_maker->_parse_rs_attrs ({ having => $attrs->{having} }),
+    ],
+    grouping => [
+      $sql_maker->_parse_rs_attrs ({ group_by => $attrs->{group_by} }),
     ],
     joining => [
       $sql_maker->_recurse_from (
@@ -391,7 +471,9 @@ sub _resolve_aliastypes_from_select_args {
     ],
     selecting => [
       $sql_maker->_recurse_fields ($select),
-      ( map { $_->[0] } $self->_extract_order_criteria ($attrs->{order_by}, $sql_maker) ),
+    ],
+    ordering => [
+      map { $_->[0] } $self->_extract_order_criteria ($attrs->{order_by}, $sql_maker),
     ],
   };
 
@@ -411,7 +493,7 @@ sub _resolve_aliastypes_from_select_args {
       for my $piece (@{$to_scan->{$type}}) {
         if (my @matches = $piece =~ /$al_re/g) {
           $aliases_by_type->{$type}{$alias} ||= { -parents => $alias_list->{$alias}{-join_path}||[] };
-          $aliases_by_type->{$type}{$alias}{-seen_columns}{"$alias.$_"} = 1
+          $aliases_by_type->{$type}{$alias}{-seen_columns}{"$alias.$_"} = "$alias.$_"
             for grep { defined $_ } @matches;
         }
       }
@@ -430,7 +512,7 @@ sub _resolve_aliastypes_from_select_args {
         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.$_"} = 1
+          $aliases_by_type->{$type}{$alias}{-seen_columns}{"$alias.$_"} = $_
             for grep { defined $_ } @matches;
         }
       }
@@ -447,6 +529,10 @@ sub _resolve_aliastypes_from_select_args {
     );
   }
 
+  for (keys %$aliases_by_type) {
+    delete $aliases_by_type->{$_} unless keys %{$aliases_by_type->{$_}};
+  }
+
   return $aliases_by_type;
 }