Cut down amount of calls to _resolve_aliastypes_from_select_args
[dbsrgits/DBIx-Class-Historic.git] / lib / DBIx / Class / Storage / DBIHacks.pm
index 225ab01..34b358c 100644 (file)
@@ -24,7 +24,7 @@ use namespace::clean;
 #
 sub _prune_unused_joins {
   my $self = shift;
-  my ($from, $select, $where, $attrs, $ignore_multiplication) = @_;
+  my ($from, $select, $where, $attrs) = @_;
 
   return $from unless $self->_use_join_optimizer;
 
@@ -34,13 +34,13 @@ sub _prune_unused_joins {
 
   my $aliastypes = $self->_resolve_aliastypes_from_select_args(@_);
 
-  # don't care
-  delete $aliastypes->{joining};
+  my $orig_joins = delete $aliastypes->{joining};
+  my $orig_multiplying = $aliastypes->{multiplying};
 
   # a grouped set will not be affected by amount of rows. Thus any
   # {multiplying} joins can go
   delete $aliastypes->{multiplying}
-    if $ignore_multiplication or $attrs->{group_by};
+    if $attrs->{_force_prune_multiplying_joins} or $attrs->{group_by};
 
   my @newfrom = $from->[0]; # FROM head is always present
 
@@ -56,13 +56,17 @@ sub _prune_unused_joins {
 
   for my $j (@{$from}[1..$#$from]) {
     push @newfrom, $j if (
-      (! $j->[0]{-alias}) # legacy crap
+      (! defined $j->[0]{-alias}) # legacy crap
         ||
       $need_joins{$j->[0]{-alias}}
     );
   }
 
-  return \@newfrom;
+  return ( \@newfrom, {
+    multiplying => { map { $need_joins{$_} ? ($_  => $orig_multiplying->{$_}) : () } keys %$orig_multiplying },
+    %$aliastypes,
+    joining => { map { $_ => $orig_joins->{$_} } keys %need_joins },
+  } );
 }
 
 #
@@ -146,7 +150,7 @@ sub _adjust_select_args_for_complex_prefetch {
   # 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
@@ -175,16 +179,16 @@ sub _adjust_select_args_for_complex_prefetch {
     local $self->{_use_join_optimizer} = 1;
 
     # throw away multijoins since we def. do not care about those inside the subquery
-    my $inner_from = $self->_prune_unused_joins ($from, $inner_select, $where, $inner_attrs, 'ignore_multiplication');
-
-    my $inner_aliastypes =
-      $self->_resolve_aliastypes_from_select_args( $inner_from, $inner_select, $where, $inner_attrs );
+    my ($inner_from, $inner_aliastypes) = $self->_prune_unused_joins ($from, $inner_select, $where, {
+      %$inner_attrs, _force_prune_multiplying_joins => 1
+    });
 
     # uh-oh a multiplier (which is not us) left in, this is a problem
     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}}
     ) {
@@ -196,10 +200,13 @@ sub _adjust_select_args_for_complex_prefetch {
           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}
-        );
+        ($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 '
@@ -218,21 +225,11 @@ sub _adjust_select_args_for_complex_prefetch {
         # 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',
@@ -264,14 +261,13 @@ sub _adjust_select_args_for_complex_prefetch {
           # skip ourselves
           next if $chunk =~ $own_re;
 
-          my $is_desc = $chunk =~ s/\sDESC$//i;
-          $chunk =~ s/\sASC$//i;
+          ($chunk, my $is_desc) = $sql_maker->_split_order_chunk($chunk);
 
           # maybe our own unqualified column
-          my ($ord_bit) = ($lquote and $sep)
-            ? $chunk =~ /^ $lquote ([^$sep]+) $rquote $/x
-            : $chunk
-          ;
+          my $ord_bit = (
+            $lquote and $sep and $chunk =~ /^ $lquote ([^$sep]+) $rquote $/x
+          ) ? $1 : $chunk;
+
           next if (
             $ord_bit
               and
@@ -295,9 +291,11 @@ sub _adjust_select_args_for_complex_prefetch {
 
         # 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,
+        });
       }
     }
 
@@ -331,7 +329,7 @@ sub _adjust_select_args_for_complex_prefetch {
 
   # 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, [
@@ -351,7 +349,7 @@ sub _adjust_select_args_for_complex_prefetch {
     };
   }
 
-  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
@@ -382,11 +380,12 @@ sub _adjust_select_args_for_complex_prefetch {
   }
 
   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 '
@@ -444,7 +443,7 @@ sub _resolve_aliastypes_from_select_args {
     );
   }
 
-  # get a column to source/alias map (including unqualified ones)
+  # get a column to source/alias map (including unambiguous unqualified ones)
   my $colinfo = $self->_resolve_column_info ($from);
 
   # set up a botched SQLA
@@ -501,7 +500,17 @@ sub _resolve_aliastypes_from_select_args {
   # throw away empty chunks
   $_ = [ map { $_ || () } @$_ ] for values %$to_scan;
 
-  # first loop through all fully qualified columns and get the corresponding
+  # first see if we have any exact matches (qualified or unqualified)
+  for my $type (keys %$to_scan) {
+    for my $piece (@{$to_scan->{$type}}) {
+      if ($colinfo->{$piece} and my $alias = $colinfo->{$piece}{-source_alias}) {
+        $aliases_by_type->{$type}{$alias} ||= { -parents => $alias_list->{$alias}{-join_path}||[] };
+        $aliases_by_type->{$type}{$alias}{-seen_columns}{$colinfo->{$piece}{-fq_colname}} = $piece;
+      }
+    }
+  }
+
+  # now loop through all fully qualified columns and get the corresponding
   # alias (should work even if they are in scalarrefs)
   for my $alias (keys %$alias_list) {
     my $al_re = qr/
@@ -530,7 +539,7 @@ sub _resolve_aliastypes_from_select_args {
 
     for my $type (keys %$to_scan) {
       for my $piece (@{$to_scan->{$type}}) {
-        if (my @matches = $piece =~ /$col_re/g) {
+        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.$_"} = $_
@@ -559,43 +568,49 @@ sub _resolve_aliastypes_from_select_args {
 
 # 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}++;
   }
 
@@ -609,14 +624,12 @@ sub _resolve_ident_sources {
   my ($self, $ident) = @_;
 
   my $alias2source = {};
-  my $rs_alias;
 
   # the reason this is so contrived is that $ident may be a {from}
   # structure, specifying multiple tables to join
   if ( blessed $ident && $ident->isa("DBIx::Class::ResultSource") ) {
     # this is compat mode for insert/update/delete which do not deal with aliases
     $alias2source->{me} = $ident;
-    $rs_alias = 'me';
   }
   elsif (ref $ident eq 'ARRAY') {
 
@@ -624,7 +637,6 @@ sub _resolve_ident_sources {
       my $tabinfo;
       if (ref $_ eq 'HASH') {
         $tabinfo = $_;
-        $rs_alias = $tabinfo->{-alias};
       }
       if (ref $_ eq 'ARRAY' and ref $_->[0] eq 'HASH') {
         $tabinfo = $_->[0];
@@ -635,7 +647,7 @@ sub _resolve_ident_sources {
     }
   }
 
-  return ($alias2source, $rs_alias);
+  return $alias2source;
 }
 
 # Takes $ident, \@column_names
@@ -647,7 +659,7 @@ sub _resolve_ident_sources {
 # for all sources
 sub _resolve_column_info {
   my ($self, $ident, $colnames) = @_;
-  my ($alias2src, $root_alias) = $self->_resolve_ident_sources($ident);
+  my $alias2src = $self->_resolve_ident_sources($ident);
 
   my (%seen_cols, @auto_colnames);
 
@@ -786,7 +798,7 @@ sub _extract_order_criteria {
     my @chunks;
     for ($sql_maker->_order_by_chunks ($order_by) ) {
       my $chunk = ref $_ ? [ @$_ ] : [ $_ ];
-      $chunk->[0] =~ s/\s+ (?: ASC|DESC ) \s* $//ix;
+      ($chunk->[0]) = $sql_maker->_split_order_chunk($chunk->[0]);
 
       # order criteria may have come back pre-quoted (literals and whatnot)
       # this is fragile, but the best we can currently do
@@ -851,7 +863,8 @@ sub _main_source_order_by_portion_is_stable {
   ;
   return unless @ord_cols;
 
-  my $colinfos = $self->_resolve_column_info($main_rsrc, \@ord_cols);
+  my $colinfos = $self->_resolve_column_info($main_rsrc);
+
   for (0 .. $#ord_cols) {
     if (
       ! $colinfos->{$ord_cols[$_]}
@@ -866,25 +879,43 @@ sub _main_source_order_by_portion_is_stable {
   # 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)
-  ) : {};
+  # 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
+  #
+  # RV of _identifying_column_set contains unqualified names only
+  my $unqualified_idset = $main_rsrc->_identifying_column_set({
+    ( $where ? %{
+      $self->_resolve_column_info(
+        $main_rsrc, $self->_extract_fixed_condition_columns($where)
+      )
+    } : () ),
+    %$order_portion_ci
+  }) or return;
+
+  my $ret_info;
+  my %unqualified_idcols_from_order = map {
+    $order_portion_ci->{$_} ? ( $_ => $order_portion_ci->{$_} ) : ()
+  } @$unqualified_idset;
+
+  # extra optimization - cut the order_by at the end of the identifying set
+  # (just in case the user was stupid and overlooked the obvious)
+  for my $i (0 .. $#ord_cols) {
+    my $col = $ord_cols[$i];
+    my $unqualified_colname = $order_portion_ci->{$col}{-colname};
+    $ret_info->{$col} = { %{$order_portion_ci->{$col}}, -idx_in_order_subset => $i };
+    delete $unqualified_idcols_from_order{$ret_info->{$col}{-colname}};
+
+    # we didn't reach the end of the identifying portion yet
+    return $ret_info unless keys %unqualified_idcols_from_order;
+  }
 
-  return (
-    $main_rsrc->_identifying_column_set({ %$where, %$order_portion_ci })
-      ? $order_portion_ci
-      : undef
-  );
+  die 'How did we get here...';
 }
 
 # returns an arrayref of column names which *definitely* have som