Consider where condition when determining leftmost order stability
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / Storage / DBIHacks.pm
index 75a8fb2..10101f8 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;
 }
 
@@ -712,6 +798,58 @@ sub _order_by_is_stable {
   return undef;
 }
 
+# this is almost identical to the above, except it accepts only
+# a single rsrc, and will succeed only if the first portion of the order
+# by is stable.
+# returns that portion as a colinfo hashref on success
+sub _main_source_order_by_portion_is_stable {
+  my ($self, $main_rsrc, $order_by, $where) = @_;
+
+  die "Huh... I expect a blessed result_source..."
+    if ref($main_rsrc) eq 'ARRAY';
+
+  my @ord_cols = map
+    { $_->[0] }
+    ( $self->_extract_order_criteria($order_by) )
+  ;
+  return unless @ord_cols;
+
+  my $colinfos = $self->_resolve_column_info($main_rsrc, \@ord_cols);
+  for (0 .. $#ord_cols) {
+    if (
+      ! $colinfos->{$ord_cols[$_]}
+        or
+      $colinfos->{$ord_cols[$_]}{-result_source} != $main_rsrc
+    ) {
+      $#ord_cols =  $_ - 1;
+      last;
+    }
+  }
+
+  # we just truncated it above
+  return unless @ord_cols;
+
+  # since all we check here are the start of the order_by belonging to the
+  # top level $rsrc, a present identifying set will mean that the resultset
+  # is ordered by its leftmost table in a stable manner
+  #
+  # single source - safely use both qualified and unqualified name
+  my $order_portion_ci = { map {
+    $colinfos->{$_}{-colname} => $colinfos->{$_},
+    $colinfos->{$_}{-fq_colname} => $colinfos->{$_},
+  } @ord_cols };
+
+  $where = $where ? $self->_resolve_column_info(
+    $main_rsrc, $self->_extract_fixed_condition_columns($where)
+  ) : {};
+
+  return (
+    $main_rsrc->_identifying_column_set({ %$where, %$order_portion_ci })
+      ? $order_portion_ci
+      : undef
+  );
+}
+
 # returns an arrayref of column names which *definitely* have som
 # sort of non-nullable equality requested in the given condition
 # specification. This is used to figure out if a resultset is