Centralize specification of expected Result class base in the codebase
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / ResultSet.pm
index 6f39723..bf7e88f 100644 (file)
@@ -2,18 +2,18 @@ package DBIx::Class::ResultSet;
 
 use strict;
 use warnings;
-use base qw/DBIx::Class/;
+
+use base 'DBIx::Class';
+
 use DBIx::Class::Carp;
 use DBIx::Class::ResultSetColumn;
-use Scalar::Util qw/blessed weaken reftype/;
+use DBIx::Class::ResultClass::HashRefInflator;
+use Scalar::Util qw( blessed reftype );
 use DBIx::Class::_Util qw(
-  fail_on_internal_wantarray is_plain_value is_literal_value
+  dbic_internal_try dbic_internal_catch dump_value emit_loud_diag
+  fail_on_internal_wantarray fail_on_internal_call UNRESOLVABLE_CONDITION
 );
-use Try::Tiny;
-use Data::Compare (); # no imports!!! guard against insane architecture
-
-# not importing first() as it will clash with our own method
-use List::Util ();
+use DBIx::Class::SQLMaker::Util qw( normalize_sqla_condition extract_equality_conditions );
 
 BEGIN {
   # De-duplication in _merge_attr() is disabled, but left in for reference
@@ -59,7 +59,7 @@ just stores all the conditions needed to create the query.
 
 A basic ResultSet representing the data of an entire table is returned
 by calling C<resultset> on a L<DBIx::Class::Schema> and passing in a
-L<Source|DBIx::Class::Manual::Glossary/Source> name.
+L<Source|DBIx::Class::Manual::Glossary/ResultSource> name.
 
   my $users_rs = $schema->resultset('User');
 
@@ -301,14 +301,18 @@ creation B<will not work>. See also warning pertaining to L</create>.
 
 sub new {
   my $class = shift;
-  return $class->new_result(@_) if ref $class;
+
+  if (ref $class) {
+    DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_INDIRECT_CALLS and fail_on_internal_call;
+    return $class->new_result(@_);
+  }
 
   my ($source, $attrs) = @_;
   $source = $source->resolve
     if $source->isa('DBIx::Class::ResultSourceHandle');
 
   $attrs = { %{$attrs||{}} };
-  delete @{$attrs}{qw(_last_sqlmaker_alias_map _related_results_construction)};
+  delete @{$attrs}{qw(_last_sqlmaker_alias_map _simple_passthrough_construction)};
 
   if ($attrs->{page}) {
     $attrs->{rows} ||= 10;
@@ -389,7 +393,7 @@ sub search {
   my $rs = $self->search_rs( @_ );
 
   if (wantarray) {
-    DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_WANTARRAY and my $sog = fail_on_internal_wantarray($rs);
+    DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_WANTARRAY and my $sog = fail_on_internal_wantarray;
     return $rs->all;
   }
   elsif (defined wantarray) {
@@ -438,7 +442,7 @@ sub search_rs {
     $call_cond = shift;
   }
   # fish out attrs in the ($condref, $attr) case
-  elsif (@_ == 2 and ( ! defined $_[0] or (ref $_[0]) ne '') ) {
+  elsif (@_ == 2 and ( ! defined $_[0] or length ref $_[0] ) ) {
     ($call_cond, $call_attrs) = @_;
   }
   elsif (@_ % 2) {
@@ -452,7 +456,7 @@ sub search_rs {
     for my $i (0 .. $#_) {
       next if $i % 2;
       $self->throw_exception ('All keys in condition key/value pairs must be plain scalars')
-        if (! defined $_[$i] or ref $_[$i] ne '');
+        if (! defined $_[$i] or length ref $_[$i] );
     }
 
     $call_cond = { @_ };
@@ -461,7 +465,7 @@ sub search_rs {
   # see if we can keep the cache (no $rs changes)
   my $cache;
   my %safe = (alias => 1, cache => 1);
-  if ( ! List::Util::first { !$safe{$_} } keys %$call_attrs and (
+  if ( ! grep { !$safe{$_} } keys %$call_attrs and (
     ! defined $call_cond
       or
     ref $call_cond eq 'HASH' && ! keys %$call_cond
@@ -485,9 +489,8 @@ sub search_rs {
     my @selector_attrs = qw/select as columns cols +select +as +columns include_columns/;
 
     # reset the current selector list if new selectors are supplied
-    if (List::Util::first { exists $call_attrs->{$_} } qw/columns cols select as/) {
-      delete @{$old_attrs}{(@selector_attrs, '_dark_selector')};
-    }
+    delete @{$old_attrs}{(@selector_attrs, '_dark_selector')}
+      if grep { exists $call_attrs->{$_} } qw(columns cols select as);
 
     # Normalize the new selector list (operates on the passed-in attr structure)
     # Need to do it on every chain instead of only once on _resolved_attrs, in
@@ -549,7 +552,6 @@ sub search_rs {
   return $rs;
 }
 
-my $dark_sel_dumper;
 sub _normalize_selection {
   my ($self, $attrs) = @_;
 
@@ -614,11 +616,10 @@ sub _normalize_selection {
           else {
             $attrs->{_dark_selector} = {
               plus_stage => $pref,
-              string => ($dark_sel_dumper ||= do {
-                  require Data::Dumper::Concise;
-                  Data::Dumper::Concise::DumperObject()->Indent(0);
-                })->Values([$_])->Dump
-              ,
+              string => do {
+                local $Data::Dumper::Indent = 0;
+                dump_value $_;
+              },
             };
             last SELECTOR;
           }
@@ -652,26 +653,15 @@ sub _stack_cond {
     (ref $_ eq 'HASH' and ! keys %$_)
   ) and $_ = undef for ($left, $right);
 
-  # either on of the two undef or both undef
-  if ( ( (defined $left) xor (defined $right) ) or ! defined $left ) {
-    return defined $left ? $left : $right;
-  }
-
-  my $cond = $self->result_source->schema->storage->_collapse_cond({ -and => [$left, $right] });
+  return(
+    # either one of the two undef
+    ( (defined $left) xor (defined $right) )  ? ( defined $left ? $left : $right )
 
-  for my $c (grep { ref $cond->{$_} eq 'ARRAY' and ($cond->{$_}[0]||'') eq '-and' } keys %$cond) {
+    # both undef
+  : ( ! defined $left )                       ? undef
 
-    my @vals = sort @{$cond->{$c}}[ 1..$#{$cond->{$c}} ];
-    my @fin = shift @vals;
-
-    for my $v (@vals) {
-      push @fin, $v unless Data::Compare::Compare( $fin[-1], $v );
-    }
-
-    $cond->{$c} = (@fin == 1) ? $fin[0] : [-and => @fin ];
-  }
-
-  $cond;
+                                              : { -and => [$left, $right] }
+  );
 }
 
 =head2 search_literal
@@ -784,7 +774,7 @@ See also L</find_or_create> and L</update_or_create>.
 
 sub find {
   my $self = shift;
-  my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {});
+  my $attrs = (@_ > 1 && ref $_[-1] eq 'HASH' ? pop(@_) : {});
 
   my $rsrc = $self->result_source;
 
@@ -817,21 +807,19 @@ sub find {
     . "corresponding to the columns of the specified unique constraint '$constraint_name'"
     ) unless @c_cols == @_;
 
-    $call_cond = {};
     @{$call_cond}{@c_cols} = @_;
   }
 
-  my %related;
+  # process relationship data if any
   for my $key (keys %$call_cond) {
     if (
-      my $keyref = ref($call_cond->{$key})
+      length ref($call_cond->{$key})
         and
       my $relinfo = $rsrc->relationship_info($key)
+        and
+      # implicitly skip has_many's (likely MC)
+      ( ref( my $val = delete $call_cond->{$key} ) ne 'ARRAY' )
     ) {
-      my $val = delete $call_cond->{$key};
-
-      next if $keyref eq 'ARRAY'; # has_many for multi_create
-
       my ($rel_cond, $crosstable) = $rsrc->_resolve_condition(
         $relinfo->{cond}, $val, $key, $key
       );
@@ -839,22 +827,21 @@ sub find {
       $self->throw_exception("Complex condition via relationship '$key' is unsupported in find()")
          if $crosstable or ref($rel_cond) ne 'HASH';
 
-      # supplement
-      @related{keys %$rel_cond} = values %$rel_cond;
+      # supplement condition
+      # relationship conditions take precedence (?)
+      @{$call_cond}{keys %$rel_cond} = values %$rel_cond;
     }
   }
 
-  # relationship conditions take precedence (?)
-  @{$call_cond}{keys %related} = values %related;
-
   my $alias = exists $attrs->{alias} ? $attrs->{alias} : $self->{attrs}{alias};
   my $final_cond;
   if (defined $constraint_name) {
     $final_cond = $self->_qualify_cond_columns (
 
-      $self->_build_unique_cond (
-        $constraint_name,
-        $call_cond,
+      $rsrc->_minimal_valueset_satisfying_constraint(
+        constraint_name => $constraint_name,
+        values => ($self->_merge_with_rscond($call_cond))[0],
+        carp_on_nulls => 1,
       ),
 
       $alias,
@@ -869,23 +856,42 @@ sub find {
     # relationship
   }
   else {
+    my (@unique_queries, %seen_column_combinations, $ci, @fc_exceptions);
+
     # no key was specified - fall down to heuristics mode:
     # run through all unique queries registered on the resultset, and
     # 'OR' all qualifying queries together
-    my (@unique_queries, %seen_column_combinations);
-    for my $c_name ($rsrc->unique_constraint_names) {
+    #
+    # always start from 'primary' if it exists at all
+    for my $c_name ( sort {
+        $a eq 'primary' ? -1
+      : $b eq 'primary' ? 1
+      : $a cmp $b
+    } $rsrc->unique_constraint_names) {
+
       next if $seen_column_combinations{
         join "\x00", sort $rsrc->unique_constraint_columns($c_name)
       }++;
 
-      push @unique_queries, try {
-        $self->_build_unique_cond ($c_name, $call_cond, 'croak_on_nulls')
-      } || ();
+      dbic_internal_try {
+        push @unique_queries, $self->_qualify_cond_columns(
+          $rsrc->_minimal_valueset_satisfying_constraint(
+            constraint_name => $c_name,
+            values => ($self->_merge_with_rscond($call_cond))[0],
+            columns_info => ($ci ||= $rsrc->columns_info),
+          ),
+          $alias
+        );
+      }
+      dbic_internal_catch {
+        push @fc_exceptions, $_ if $_ =~ /\bFilterColumn\b/;
+      };
     }
 
-    $final_cond = @unique_queries
-      ? [ map { $self->_qualify_cond_columns($_, $alias) } @unique_queries ]
-      : $self->_non_unique_find_fallback ($call_cond, $attrs)
+    $final_cond =
+        @unique_queries   ? \@unique_queries
+      : @fc_exceptions    ? $self->throw_exception(join "; ", map { $_ =~ /(.*) at .+ line \d+$/s } @fc_exceptions )
+      :                     $self->_non_unique_find_fallback ($call_cond, $attrs)
     ;
   }
 
@@ -938,51 +944,20 @@ sub _qualify_cond_columns {
 }
 
 sub _build_unique_cond {
-  my ($self, $constraint_name, $extra_cond, $croak_on_null) = @_;
-
-  my @c_cols = $self->result_source->unique_constraint_columns($constraint_name);
-
-  # combination may fail if $self->{cond} is non-trivial
-  my ($final_cond) = try {
-    $self->_merge_with_rscond ($extra_cond)
-  } catch {
-    +{ %$extra_cond }
-  };
-
-  # trim out everything not in $columns
-  $final_cond = { map {
-    exists $final_cond->{$_}
-      ? ( $_ => $final_cond->{$_} )
-      : ()
-  } @c_cols };
-
-  if (my @missing = grep
-    { ! ($croak_on_null ? defined $final_cond->{$_} : exists $final_cond->{$_}) }
-    (@c_cols)
-  ) {
-    $self->throw_exception( sprintf ( "Unable to satisfy requested constraint '%s', no values for column(s): %s",
-      $constraint_name,
-      join (', ', map { "'$_'" } @missing),
-    ) );
-  }
-
-  if (
-    !$croak_on_null
-      and
-    !$ENV{DBIC_NULLABLE_KEY_NOWARN}
-      and
-    my @undefs = sort grep { ! defined $final_cond->{$_} } (keys %$final_cond)
-  ) {
-    carp_unique ( sprintf (
-      "NULL/undef values supplied for requested unique constraint '%s' (NULL "
-    . 'values in column(s): %s). This is almost certainly not what you wanted, '
-    . 'though you can set DBIC_NULLABLE_KEY_NOWARN to disable this warning.',
-      $constraint_name,
-      join (', ', map { "'$_'" } @undefs),
-    ));
-  }
-
-  return $final_cond;
+  carp_unique sprintf
+    '_build_unique_cond is a private method, and moreover is about to go '
+  . 'away. Please contact the development team at %s if you believe you '
+  . 'have a genuine use for this method, in order to discuss alternatives.',
+    DBIx::Class::_ENV_::HELP_URL,
+  ;
+
+  my ($self, $constraint_name, $cond, $croak_on_null) = @_;
+
+  $self->result_source->_minimal_valueset_satisfying_constraint(
+    constraint_name => $constraint_name,
+    values => $cond,
+    carp_on_nulls => !$croak_on_null
+  );
 }
 
 =head2 search_related
@@ -1009,7 +984,8 @@ See also L</search_related_rs>.
 
 =cut
 
-sub search_related {
+sub search_related :DBIC_method_is_indirect_sugar {
+  DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_INDIRECT_CALLS and fail_on_internal_call;
   return shift->related_resultset(shift)->search(@_);
 }
 
@@ -1020,7 +996,8 @@ it guarantees a resultset, even in list context.
 
 =cut
 
-sub search_related_rs {
+sub search_related_rs :DBIC_method_is_indirect_sugar {
+  DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_INDIRECT_CALLS and fail_on_internal_call;
   return shift->related_resultset(shift)->search_rs(@_);
 }
 
@@ -1044,7 +1021,7 @@ sub cursor {
 
   return $self->{cursor} ||= do {
     my $attrs = $self->_resolved_attrs;
-    $self->result_source->storage->select(
+    $self->result_source->schema->storage->select(
       $attrs->{from}, $attrs->{select}, $attrs->{where}, $attrs
     );
   };
@@ -1117,7 +1094,7 @@ sub single {
     }
   }
 
-  my $data = [ $self->result_source->storage->select_single(
+  my $data = [ $self->result_source->schema->storage->select_single(
     $attrs->{from}, $attrs->{select},
     $attrs->{where}, $attrs
   )];
@@ -1144,9 +1121,7 @@ Returns a L<DBIx::Class::ResultSetColumn> instance for a column of the ResultSet
 =cut
 
 sub get_column {
-  my ($self, $column) = @_;
-  my $new = DBIx::Class::ResultSetColumn->new($self, $column);
-  return $new;
+  DBIx::Class::ResultSetColumn->new(@_);
 }
 
 =head2 search_like
@@ -1168,7 +1143,7 @@ You most likely want to use L</search> with specific operators.
 
 For more information, see L<DBIx::Class::Manual::Cookbook>.
 
-This method is deprecated and will be removed in 0.09. Use L</search()>
+This method is deprecated and will be removed in 0.09. Use L<search()|/search>
 instead. An example conversion is:
 
   ->search_like({ foo => 'bar' });
@@ -1186,7 +1161,7 @@ sub search_like {
    .' Instead use ->search({ x => { -like => "y%" } })'
    .' (note the outer pair of {}s - they are important!)'
   );
-  my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {});
+  my $attrs = (@_ > 1 && ref $_[-1] eq 'HASH' ? pop(@_) : {});
   my $query = ref $_[0] eq 'HASH' ? { %{shift()} }: {@_};
   $query->{$_} = { 'like' => $query->{$_} } for keys %$query;
   return $class->search($query, { %$attrs });
@@ -1403,16 +1378,12 @@ sub _construct_results {
   $self->{_result_inflator}{is_hri} = ( (
     ! $self->{_result_inflator}{is_core_row}
       and
-    $inflator_cref == (
-      require DBIx::Class::ResultClass::HashRefInflator
-        &&
-      DBIx::Class::ResultClass::HashRefInflator->can('inflate_result')
-    )
+    $inflator_cref == \&DBIx::Class::ResultClass::HashRefInflator::inflate_result
   ) ? 1 : 0 ) unless defined $self->{_result_inflator}{is_hri};
 
 
-  if (! $attrs->{_related_results_construction}) {
-    # construct a much simpler array->hash folder for the one-table cases right here
+  if ($attrs->{_simple_passthrough_construction}) {
+    # construct a much simpler array->hash folder for the one-table HRI cases right here
     if ($self->{_result_inflator}{is_hri}) {
       for my $r (@$rows) {
         $r = { map { $infmap->[$_] => $r->[$_] } 0..$#$infmap };
@@ -1425,16 +1396,20 @@ sub _construct_results {
     #
     # crude unscientific benchmarking indicated the shortcut eval is not worth it for
     # this particular resultset size
-    elsif (@$rows < 60) {
+    elsif ( $self->{_result_inflator}{is_core_row} and @$rows < 60 ) {
       for my $r (@$rows) {
         $r = $inflator_cref->($res_class, $rsrc, { map { $infmap->[$_] => $r->[$_] } (0..$#$infmap) } );
       }
     }
     else {
       eval sprintf (
-        '$_ = $inflator_cref->($res_class, $rsrc, { %s }) for @$rows',
-        join (', ', map { "\$infmap->[$_] => \$_->[$_]" } 0..$#$infmap )
-      );
+        ( $self->{_result_inflator}{is_core_row}
+          ? '$_ = $inflator_cref->($res_class, $rsrc, { %s }) for @$rows'
+          # a custom inflator may be a multiplier/reductor - put it in direct list ctx
+          : '@$rows = map { $inflator_cref->($res_class, $rsrc, { %s } ) } @$rows'
+        ),
+        ( join (', ', map { "\$infmap->[$_] => \$_->[$_]" } 0..$#$infmap ) )
+      ) . '; 1' or die;
     }
   }
   else {
@@ -1444,57 +1419,30 @@ sub _construct_results {
       :                                           'classic_nonpruning'
     ;
 
-    # $args and $attrs to _mk_row_parser are separated to delineate what is
-    # core collapser stuff and what is dbic $rs specific
-    @{$self->{_row_parser}{$parser_type}}{qw(cref nullcheck)} = $rsrc->_mk_row_parser({
-      eval => 1,
-      inflate_map => $infmap,
-      collapse => $attrs->{collapse},
-      premultiplied => $attrs->{_main_source_premultiplied},
-      hri_style => $self->{_result_inflator}{is_hri},
-      prune_null_branches => $self->{_result_inflator}{is_hri} || $self->{_result_inflator}{is_core_row},
-    }, $attrs) unless $self->{_row_parser}{$parser_type}{cref};
-
-    # column_info metadata historically hasn't been too reliable.
-    # We need to start fixing this somehow (the collapse resolver
-    # can't work without it). Add an explicit check for the *main*
-    # result, hopefully this will gradually weed out such errors
-    #
-    # FIXME - this is a temporary kludge that reduces performance
-    # It is however necessary for the time being
-    my ($unrolled_non_null_cols_to_check, $err);
+    unless( $self->{_row_parser}{$parser_type}{cref} ) {
 
-    if (my $check_non_null_cols = $self->{_row_parser}{$parser_type}{nullcheck} ) {
+      # $args and $attrs to _mk_row_parser are separated to delineate what is
+      # core collapser stuff and what is dbic $rs specific
+      $self->{_row_parser}{$parser_type}{src} = $rsrc->_mk_row_parser({
+        inflate_map => $infmap,
+        collapse => $attrs->{collapse},
+        premultiplied => $attrs->{_main_source_premultiplied},
+        hri_style => $self->{_result_inflator}{is_hri},
+        prune_null_branches => $self->{_result_inflator}{is_hri} || $self->{_result_inflator}{is_core_row},
+      }, $attrs);
 
-      $err =
-        'Collapse aborted due to invalid ResultSource metadata - the following '
-      . 'selections are declared non-nullable but NULLs were retrieved: '
-      ;
+      $self->{_row_parser}{$parser_type}{cref} = do {
+        package # hide form PAUSE
+          DBIx::Class::__GENERATED_ROW_PARSER__;
 
-      my @violating_idx;
-      COL: for my $i (@$check_non_null_cols) {
-        ! defined $_->[$i] and push @violating_idx, $i and next COL for @$rows;
-      }
-
-      $self->throw_exception( $err . join (', ', map { "'$infmap->[$_]'" } @violating_idx ) )
-        if @violating_idx;
-
-      $unrolled_non_null_cols_to_check = join (',', @$check_non_null_cols);
+        eval $self->{_row_parser}{$parser_type}{src};
+      } || die $@;
     }
 
-    my $next_cref =
-      ($did_fetch_all or ! $attrs->{collapse})  ? undef
-    : defined $unrolled_non_null_cols_to_check  ? eval sprintf <<'EOS', $unrolled_non_null_cols_to_check
-sub {
-  # FIXME SUBOPTIMAL - we can do better, cursor->next/all (well diff. methods) should return a ref
-  my @r = $cursor->next or return;
-  if (my @violating_idx = grep { ! defined $r[$_] } (%s) ) {
-    $self->throw_exception( $err . join (', ', map { "'$infmap->[$_]'" } @violating_idx ) )
-  }
-  \@r
-}
-EOS
-    : sub {
+    # this needs to close over the *current* cursor, hence why it is not cached above
+    my $next_cref = ($did_fetch_all or ! $attrs->{collapse})
+      ? undef
+      : sub {
         # FIXME SUBOPTIMAL - we can do better, cursor->next/all (well diff. methods) should return a ref
         my @r = $cursor->next or return;
         \@r
@@ -1503,13 +1451,34 @@ EOS
 
     $self->{_row_parser}{$parser_type}{cref}->(
       $rows,
-      $next_cref ? ( $next_cref, $self->{_stashed_rows} = [] ) : (),
+      $next_cref,
+      ( $self->{_stashed_rows} = [] ),
+      ( my $null_violations = {} ),
     );
 
-    # Special-case multi-object HRI - there is no $inflator_cref pass
-    unless ($self->{_result_inflator}{is_hri}) {
+    $self->throw_exception(
+      'Collapse aborted - the following columns are declared (or defaulted to) '
+    . 'non-nullable within DBIC but NULLs were retrieved from storage: '
+    . join( ', ', map { "'$infmap->[$_]'" } sort { $a <=> $b } keys %$null_violations )
+    . ' within data row ' . dump_value({
+      map {
+        $infmap->[$_] =>
+          ( ! defined $self->{_stashed_rows}[0][$_] or length $self->{_stashed_rows}[0][$_] < 50 )
+            ? $self->{_stashed_rows}[0][$_]
+            : substr( $self->{_stashed_rows}[0][$_], 0, 50 ) . '...'
+      } 0 .. $#{$self->{_stashed_rows}[0]}
+    })
+    ) if keys %$null_violations;
+
+    # simple in-place substitution, does not regrow $rows
+    if ($self->{_result_inflator}{is_core_row}) {
       $_ = $inflator_cref->($res_class, $rsrc, @$_) for @$rows
     }
+    # Special-case multi-object HRI - there is no $inflator_cref pass at all
+    elsif ( ! $self->{_result_inflator}{is_hri} ) {
+      # the inflator may be a multiplier/reductor - put it in list ctx
+      @$rows = map { $inflator_cref->($res_class, $rsrc, @$_) } @$rows;
+    }
   }
 
   # The @$rows check seems odd at first - why wouldn't we want to warn
@@ -1554,8 +1523,8 @@ L<"table"|DBIx::Class::Manual::Glossary/"ResultSource"> class.
 
 Note that changing the result_class will also remove any components
 that were originally loaded in the source class via
-L<DBIx::Class::ResultSource/load_components>. Any overloaded methods
-in the original source class will not run.
+L<load_components|Class::C3::Componentised/load_components( @comps )>.
+Any overloaded methods in the original source class will not run.
 
 =cut
 
@@ -1677,7 +1646,7 @@ sub _count_rs {
   # overwrite the selector (supplied by the storage)
   $rsrc->resultset_class->new($rsrc, {
     %$tmp_attrs,
-    select => $rsrc->storage->_count_select ($rsrc, $attrs),
+    select => $rsrc->schema->storage->_count_select ($rsrc, $attrs),
     as => 'count',
   })->get_column ('count');
 }
@@ -1708,7 +1677,7 @@ sub _count_subq_rs {
   # Calculate subquery selector
   if (my $g = $sub_attrs->{group_by}) {
 
-    my $sql_maker = $rsrc->storage->sql_maker;
+    my $sql_maker = $rsrc->schema->storage->sql_maker;
 
     # necessary as the group_by may refer to aliased functions
     my $sel_index;
@@ -1775,7 +1744,7 @@ sub _count_subq_rs {
   return $rsrc->resultset_class
                ->new ($rsrc, $sub_attrs)
                 ->as_subselect_rs
-                 ->search ({}, { columns => { count => $rsrc->storage->_count_select ($rsrc, $attrs) } })
+                 ->search ({}, { columns => { count => $rsrc->schema->storage->_count_select ($rsrc, $attrs) } })
                   ->get_column ('count');
 }
 
@@ -1798,7 +1767,10 @@ with the passed arguments, then L</count>.
 
 =cut
 
-sub count_literal { shift->search_literal(@_)->count; }
+sub count_literal :DBIC_method_is_indirect_sugar {
+  DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_INDIRECT_CALLS and fail_on_internal_call;
+  shift->search_literal(@_)->count
+}
 
 =head2 all
 
@@ -1875,7 +1847,8 @@ an object for the first result (or C<undef> if the resultset is empty).
 
 =cut
 
-sub first {
+sub first :DBIC_method_is_indirect_sugar {
+  DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_INDIRECT_CALLS and fail_on_internal_call;
   return $_[0]->reset->next;
 }
 
@@ -1915,7 +1888,7 @@ sub _rs_update_delete {
       $storage->_prune_unused_joins ($attrs);
 
     # any non-pruneable non-local restricting joins imply subq
-    $needs_subq = defined List::Util::first { $_ ne $attrs->{alias} } keys %{ $join_classifications->{restricting} || {} };
+    $needs_subq = grep { $_ ne $attrs->{alias} } keys %{ $join_classifications->{restricting} || {} };
   }
 
   # check if the head is composite (by now all joins are thrown out unless $needs_subq)
@@ -1932,7 +1905,7 @@ sub _rs_update_delete {
     # a condition containing 'me' or other table prefixes will not work
     # at all. Tell SQLMaker to dequalify idents via a gross hack.
     $cond = do {
-      my $sqla = $rsrc->storage->sql_maker;
+      my $sqla = $rsrc->schema->storage->sql_maker;
       local $sqla->{_dequalify_idents} = 1;
       \[ $sqla->_recurse_where($self->{cond}) ];
     };
@@ -2230,127 +2203,320 @@ case there are obviously no benefits to using this method over L</create>.
 sub populate {
   my $self = shift;
 
-  # cruft placed in standalone method
-  my $data = $self->_normalize_populate_args(@_);
+  # this is naive and just a quick check
+  # the types will need to be checked more thoroughly when the
+  # multi-source populate gets added
+  my $data = (
+    ref $_[0] eq 'ARRAY'
+      and
+    ( @{$_[0]} or return )
+      and
+    ( ref $_[0][0] eq 'HASH' or ref $_[0][0] eq 'ARRAY' )
+      and
+    $_[0]
+  ) or $self->throw_exception('Populate expects an arrayref of hashrefs or arrayref of arrayrefs');
 
-  return unless @$data;
+  # FIXME - no cref handling
+  # At this point assume either hashes or arrays
+
+  my $rsrc = $self->result_source;
 
   if(defined wantarray) {
-    my @created = map { $self->create($_) } @$data;
-    return wantarray ? @created : \@created;
-  }
-  else {
-    my $first = $data->[0];
+    my (@results, $guard);
 
-    # if a column is a registered relationship, and is a non-blessed hash/array, consider
-    # it relationship data
-    my (@rels, @columns);
-    my $rsrc = $self->result_source;
-    my $rels = { map { $_ => $rsrc->relationship_info($_) } $rsrc->relationships };
-    for (keys %$first) {
-      my $ref = ref $first->{$_};
-      $rels->{$_} && ($ref eq 'ARRAY' or $ref eq 'HASH')
-        ? push @rels, $_
-        : push @columns, $_
+    if (ref $data->[0] eq 'ARRAY') {
+      # column names only, nothing to do
+      return if @$data == 1;
+
+      $guard = $rsrc->schema->storage->txn_scope_guard
+        if @$data > 2;
+
+      @results = map
+        { my $vals = $_; $self->new_result({ map { $data->[0][$_] => $vals->[$_] } 0..$#{$data->[0]} })->insert }
+        @{$data}[1 .. $#$data]
       ;
     }
+    else {
+
+      $guard = $rsrc->schema->storage->txn_scope_guard
+        if @$data > 1;
+
+      @results = map { $self->new_result($_)->insert } @$data;
+    }
+
+    $guard->commit if $guard;
+    return wantarray ? @results : \@results;
+  }
+
+  # we have to deal with *possibly incomplete* related data
+  # this means we have to walk the data structure twice
+  # whether we want this or not
+  # jnap, I hate you ;)
+  my $rel_info = { map { $_ => $rsrc->relationship_info($_) } $rsrc->relationships };
+
+  my ($colinfo, $colnames, $slices_with_rels);
+  my $data_start = 0;
+
+  DATA_SLICE:
+  for my $i (0 .. $#$data) {
+
+    my $current_slice_seen_rel_infos;
+
+### Determine/Supplement collists
+### BEWARE - This is a hot piece of code, a lot of weird idioms were used
+    if( ref $data->[$i] eq 'ARRAY' ) {
 
-    my @pks = $rsrc->primary_columns;
+      # positional(!) explicit column list
+      if ($i == 0) {
+        # column names only, nothing to do
+        return if @$data == 1;
 
-    ## do the belongs_to relationships
-    foreach my $index (0..$#$data) {
+        $colinfo->{$data->[0][$_]} = { pos => $_, name => $data->[0][$_] } and push @$colnames, $data->[0][$_]
+          for 0 .. $#{$data->[0]};
 
-      # delegate to create() for any dataset without primary keys with specified relationships
-      if (grep { !defined $data->[$index]->{$_} } @pks ) {
-        for my $r (@rels) {
-          if (grep { ref $data->[$index]{$r} eq $_ } qw/HASH ARRAY/) {  # a related set must be a HASH or AoH
-            my @ret = $self->populate($data);
-            return;
+        $data_start = 1;
+
+        next DATA_SLICE;
+      }
+      else {
+        for (values %$colinfo) {
+          if ($_->{is_rel} ||= (
+            $rel_info->{$_->{name}}
+              and
+            (
+              ref $data->[$i][$_->{pos}] eq 'ARRAY'
+                or
+              ref $data->[$i][$_->{pos}] eq 'HASH'
+                or
+              (
+                defined blessed $data->[$i][$_->{pos}]
+                  and
+                $data->[$i][$_->{pos}]->isa(
+                  $DBIx::Class::ResultSource::__expected_result_class_isa
+                    ||
+                  emit_loud_diag(
+                    confess => 1,
+                    msg => 'Global $DBIx::Class::ResultSource::__expected_result_class_isa unexpectedly unset...'
+                  )
+                )
+              )
+            )
+              and
+            1
+          )) {
+
+            # moar sanity check... sigh
+            for ( ref $data->[$i][$_->{pos}] eq 'ARRAY' ? @{$data->[$i][$_->{pos}]} : $data->[$i][$_->{pos}] ) {
+              if (
+                defined blessed $_
+                  and
+                $_->isa(
+                  $DBIx::Class::ResultSource::__expected_result_class_isa
+                    ||
+                  emit_loud_diag(
+                    confess => 1,
+                    msg => 'Global $DBIx::Class::ResultSource::__expected_result_class_isa unexpectedly unset...'
+                  )
+                )
+              ) {
+                carp_unique("Fast-path populate() with supplied related objects is not possible - falling back to regular create()");
+                return my $throwaway = $self->populate(@_);
+              }
+            }
+
+            push @$current_slice_seen_rel_infos, $rel_info->{$_->{name}};
           }
         }
       }
 
-      foreach my $rel (@rels) {
-        next unless ref $data->[$index]->{$rel} eq "HASH";
-        my $result = $self->related_resultset($rel)->create($data->[$index]->{$rel});
-        my (undef, $reverse_relinfo) = %{$rsrc->reverse_relationship_info($rel)};
-        my $related = $result->result_source->_resolve_condition(
-          $reverse_relinfo->{cond},
-          $self,
-          $result,
-          $rel,
-        );
-
-        delete $data->[$index]->{$rel};
-        $data->[$index] = {%{$data->[$index]}, %$related};
+     if ($current_slice_seen_rel_infos) {
+        push @$slices_with_rels, { map { $colnames->[$_] => $data->[$i][$_] } 0 .. $#$colnames };
 
-        push @columns, keys %$related if $index == 0;
+        # this is needed further down to decide whether or not to fallback to create()
+        $colinfo->{$colnames->[$_]}{seen_null} ||= ! defined $data->[$i][$_]
+          for 0 .. $#$colnames;
       }
     }
+    elsif( ref $data->[$i] eq 'HASH' ) {
 
-    ## inherit the data locked in the conditions of the resultset
-    my ($rs_data) = $self->_merge_with_rscond({});
-    delete @{$rs_data}{@columns};
+      for ( sort keys %{$data->[$i]} ) {
 
-    ## do bulk insert on current row
-    $rsrc->storage->insert_bulk(
-      $rsrc,
-      [@columns, keys %$rs_data],
-      [ map { [ @$_{@columns}, values %$rs_data ] } @$data ],
-    );
-
-    ## do the has_many relationships
-    foreach my $item (@$data) {
+        $colinfo->{$_} ||= do {
 
-      my $main_row;
+          $self->throw_exception("Column '$_' must be present in supplied explicit column list")
+            if $data_start; # it will be 0 on AoH, 1 on AoA
 
-      foreach my $rel (@rels) {
-        next unless ref $item->{$rel} eq "ARRAY" && @{ $item->{$rel} };
+          push @$colnames, $_;
 
-        $main_row ||= $self->new_result({map { $_ => $item->{$_} } @pks});
+          # RV
+          { pos => $#$colnames, name => $_ }
+        };
 
-        my $child = $main_row->$rel;
+        if ($colinfo->{$_}{is_rel} ||= (
+          $rel_info->{$_}
+            and
+          (
+            ref $data->[$i]{$_} eq 'ARRAY'
+              or
+            ref $data->[$i]{$_} eq 'HASH'
+              or
+            (
+              defined blessed $data->[$i]{$_}
+                and
+              $data->[$i]{$_}->isa(
+                $DBIx::Class::ResultSource::__expected_result_class_isa
+                  ||
+                emit_loud_diag(
+                  confess => 1,
+                  msg => 'Global $DBIx::Class::ResultSource::__expected_result_class_isa unexpectedly unset...'
+                )
+              )
+            )
+          )
+            and
+          1
+        )) {
+
+          # moar sanity check... sigh
+          for ( ref $data->[$i]{$_} eq 'ARRAY' ? @{$data->[$i]{$_}} : $data->[$i]{$_} ) {
+            if (
+              defined blessed $_
+                and
+              $_->isa(
+                $DBIx::Class::ResultSource::__expected_result_class_isa
+                  ||
+                emit_loud_diag(
+                  confess => 1,
+                  msg => 'Global $DBIx::Class::ResultSource::__expected_result_class_isa unexpectedly unset...'
+                )
+              )
+            ) {
+              carp_unique("Fast-path populate() with supplied related objects is not possible - falling back to regular create()");
+              return my $throwaway = $self->populate(@_);
+            }
+          }
 
-        my $related = $child->result_source->_resolve_condition(
-          $rels->{$rel}{cond},
-          $child,
-          $main_row,
-          $rel,
-        );
+          push @$current_slice_seen_rel_infos, $rel_info->{$_};
+        }
+      }
 
-        my @rows_to_add = ref $item->{$rel} eq 'ARRAY' ? @{$item->{$rel}} : ($item->{$rel});
-        my @populate = map { {%$_, %$related} } @rows_to_add;
+      if ($current_slice_seen_rel_infos) {
+        push @$slices_with_rels, $data->[$i];
 
-        $child->populate( \@populate );
+        # this is needed further down to decide whether or not to fallback to create()
+        $colinfo->{$_}{seen_null} ||= ! defined $data->[$i]{$_}
+          for keys %{$data->[$i]};
       }
     }
+    else {
+      $self->throw_exception('Unexpected populate() data structure member type: ' . ref $data->[$i] );
+    }
+
+    if ( grep
+      { $_->{attrs}{is_depends_on} }
+      @{ $current_slice_seen_rel_infos || [] }
+    ) {
+      carp_unique("Fast-path populate() of belongs_to relationship data is not possible - falling back to regular create()");
+      return my $throwaway = $self->populate(@_);
+    }
   }
-}
 
+  if( $slices_with_rels ) {
 
-# populate() arguments went over several incarnations
-# What we ultimately support is AoH
-sub _normalize_populate_args {
-  my ($self, $arg) = @_;
+    # need to exclude the rel "columns"
+    $colnames = [ grep { ! $colinfo->{$_}{is_rel} } @$colnames ];
 
-  if (ref $arg eq 'ARRAY') {
-    if (!@$arg) {
-      return [];
-    }
-    elsif (ref $arg->[0] eq 'HASH') {
-      return $arg;
+    # extra sanity check - ensure the main source is in fact identifiable
+    # the localizing of nullability is insane, but oh well... the use-case is legit
+    my $ci = $rsrc->columns_info($colnames);
+
+    $ci->{$_} = { %{$ci->{$_}}, is_nullable => 0 }
+      for grep { ! $colinfo->{$_}{seen_null} } keys %$ci;
+
+    unless( $rsrc->_identifying_column_set($ci) ) {
+      carp_unique("Fast-path populate() of non-uniquely identifiable rows with related data is not possible - falling back to regular create()");
+      return my $throwaway = $self->populate(@_);
     }
-    elsif (ref $arg->[0] eq 'ARRAY') {
-      my @ret;
-      my @colnames = @{$arg->[0]};
-      foreach my $values (@{$arg}[1 .. $#$arg]) {
-        push @ret, { map { $colnames[$_] => $values->[$_] } (0 .. $#colnames) };
+  }
+
+### inherit the data locked in the conditions of the resultset
+  my ($rs_data) = $self->_merge_with_rscond({});
+  delete @{$rs_data}{@$colnames};  # passed-in stuff takes precedence
+
+  # if anything left - decompose rs_data
+  my $rs_data_vals;
+  if (keys %$rs_data) {
+     push @$rs_data_vals, $rs_data->{$_}
+      for sort keys %$rs_data;
+  }
+
+### start work
+  my $guard;
+  $guard = $rsrc->schema->storage->txn_scope_guard
+    if $slices_with_rels;
+
+### main source data
+  # FIXME - need to switch entirely to a coderef-based thing,
+  # so that large sets aren't copied several times... I think
+  $rsrc->schema->storage->_insert_bulk(
+    $rsrc,
+    [ @$colnames, sort keys %$rs_data ],
+    [ map {
+      ref $data->[$_] eq 'ARRAY'
+      ? (
+          $slices_with_rels ? [ @{$data->[$_]}[0..$#$colnames], @{$rs_data_vals||[]} ]  # the collist changed
+        : $rs_data_vals     ? [ @{$data->[$_]}, @$rs_data_vals ]
+        :                     $data->[$_]
+      )
+      : [ @{$data->[$_]}{@$colnames}, @{$rs_data_vals||[]} ]
+    } $data_start .. $#$data ],
+  );
+
+### do the children relationships
+  if ( $slices_with_rels ) {
+    my @rels = grep { $colinfo->{$_}{is_rel} } keys %$colinfo
+      or die 'wtf... please report a bug with DBIC_TRACE=1 output (stacktrace)';
+
+    for my $sl (@$slices_with_rels) {
+
+      my ($main_proto, $main_proto_rs);
+      for my $rel (@rels) {
+        next unless defined $sl->{$rel};
+
+        $main_proto ||= {
+          %$rs_data,
+          (map { $_ => $sl->{$_} } @$colnames),
+        };
+
+        unless (defined $colinfo->{$rel}{rs}) {
+
+          $colinfo->{$rel}{rs} = $rsrc->related_source($rel)->resultset;
+
+          $colinfo->{$rel}{fk_map} = { reverse %{ $rsrc->_resolve_relationship_condition(
+            rel_name => $rel,
+            self_alias => "\xFE", # irrelevant
+            foreign_alias => "\xFF", # irrelevant
+          )->{identity_map} || {} } };
+
+        }
+
+        $colinfo->{$rel}{rs}->search({ map # only so that we inherit them values properly, no actual search
+          {
+            $_ => { '=' =>
+              ( $main_proto_rs ||= $rsrc->resultset->search($main_proto) )
+                ->get_column( $colinfo->{$rel}{fk_map}{$_} )
+                 ->as_query
+            }
+          }
+          keys %{$colinfo->{$rel}{fk_map}}
+        })->populate( ref $sl->{$rel} eq 'ARRAY' ? $sl->{$rel} : [ $sl->{$rel} ] );
+
+        1;
       }
-      return \@ret;
     }
   }
 
-  $self->throw_exception('Populate expects an arrayref of hashrefs or arrayref of arrayrefs');
+  $guard->commit if $guard;
 }
 
 =head2 pager
@@ -2443,11 +2609,8 @@ Passes the hashref of input on to L<DBIx::Class::Row/new>.
 sub new_result {
   my ($self, $values) = @_;
 
-  $self->throw_exception( "new_result takes only one argument - a hashref of values" )
-    if @_ > 2;
-
-  $self->throw_exception( "new_result expects a hashref" )
-    unless (ref $values eq 'HASH');
+  $self->throw_exception( "Result object instantiation requires a single hashref argument" )
+    if @_ > 2 or ref $values ne 'HASH';
 
   my ($merged_cond, $cols_from_relations) = $self->_merge_with_rscond($values);
 
@@ -2485,51 +2648,33 @@ sub new_result {
 sub _merge_with_rscond {
   my ($self, $data) = @_;
 
-  my (%new_data, @cols_from_relations);
+  my ($implied_data, @cols_from_relations);
 
   my $alias = $self->{attrs}{alias};
 
   if (! defined $self->{cond}) {
     # just massage $data below
   }
-  elsif ($self->{cond} eq $DBIx::Class::ResultSource::UNRESOLVABLE_CONDITION) {
-    %new_data = %{ $self->{attrs}{related_objects} || {} };  # nothing might have been inserted yet
-    @cols_from_relations = keys %new_data;
-  }
-  elsif (ref $self->{cond} ne 'HASH') {
-    $self->throw_exception(
-      "Can't abstract implicit construct, resultset condition not a hash"
-    );
+  elsif ($self->{cond} eq UNRESOLVABLE_CONDITION) {
+    $implied_data = $self->{attrs}{related_objects};  # nothing might have been inserted yet
+    @cols_from_relations = keys %{ $implied_data || {} };
   }
   else {
-    if ($self->{cond}) {
-      my $implied = $self->_remove_alias(
-        $self->result_source->schema->storage->_collapse_cond($self->{cond}),
-        $alias,
-      );
-
-      for my $c (keys %$implied) {
-        my $v = $implied->{$c};
-        if ( ! length ref $v or is_plain_value($v) ) {
-          $new_data{$c} = $v;
-        }
-        elsif (
-          ref $v eq 'HASH' and keys %$v == 1 and exists $v->{'='} and is_literal_value($v->{'='})
-        ) {
-          $new_data{$c} = $v->{'='};
-        }
-      }
-    }
+    my $eqs = extract_equality_conditions( $self->{cond}, 'consider_nulls' );
+    $implied_data = { map {
+      ( ($eqs->{$_}||'') eq UNRESOLVABLE_CONDITION ) ? () : ( $_ => $eqs->{$_} )
+    } keys %$eqs };
   }
 
-  # precedence must be given to passed values over values inherited from
-  # the cond, so the order here is important.
-  %new_data = (
-    %new_data,
-    %{ $self->_remove_alias($data, $alias) },
+  return (
+    { map
+      { %{ $self->_remove_alias($_, $alias) } }
+      # precedence must be given to passed values over values inherited from
+      # the cond, so the order here is important.
+      ( $implied_data||(), $data)
+    },
+    \@cols_from_relations
   );
-
-  return (\%new_data, \@cols_from_relations);
 }
 
 # _has_resolved_attr
@@ -2629,7 +2774,7 @@ sub as_query {
 
   my $attrs = { %{ $self->_resolved_attrs } };
 
-  my $aq = $self->result_source->storage->_select_args_to_query (
+  my $aq = $self->result_source->schema->storage->_select_args_to_query (
     $attrs->{from}, $attrs->{select}, $attrs->{where}, $attrs
   );
 
@@ -2675,7 +2820,7 @@ all in the call to C<find_or_new>, even when set to C<undef>.
 
 sub find_or_new {
   my $self     = shift;
-  my $attrs    = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {});
+  my $attrs    = (@_ > 1 && ref $_[-1] eq 'HASH' ? pop(@_) : {});
   my $hash     = ref $_[0] eq 'HASH' ? shift : {@_};
   if (keys %$hash and my $row = $self->find($hash, $attrs) ) {
     return $row;
@@ -2764,11 +2909,10 @@ L</new>.
 
 =cut
 
-sub create {
-  my ($self, $col_data) = @_;
-  $self->throw_exception( "create needs a hashref" )
-    unless ref $col_data eq 'HASH';
-  return $self->new_result($col_data)->insert;
+sub create :DBIC_method_is_indirect_sugar {
+  #my ($self, $col_data) = @_;
+  DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_INDIRECT_CALLS and fail_on_internal_call;
+  return shift->new_result(shift)->insert;
 }
 
 =head2 find_or_create
@@ -2845,12 +2989,12 @@ database!
 
 sub find_or_create {
   my $self     = shift;
-  my $attrs    = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {});
+  my $attrs    = (@_ > 1 && ref $_[-1] eq 'HASH' ? pop(@_) : {});
   my $hash     = ref $_[0] eq 'HASH' ? shift : {@_};
   if (keys %$hash and my $row = $self->find($hash, $attrs) ) {
     return $row;
   }
-  return $self->create($hash);
+  return $self->new_result($hash)->insert;
 }
 
 =head2 update_or_create
@@ -2911,7 +3055,7 @@ database!
 
 sub update_or_create {
   my $self = shift;
-  my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {});
+  my $attrs = (@_ > 1 && ref $_[-1] eq 'HASH' ? pop(@_) : {});
   my $cond = ref $_[0] eq 'HASH' ? shift : {@_};
 
   my $row = $self->find($cond, $attrs);
@@ -2920,7 +3064,7 @@ sub update_or_create {
     return $row;
   }
 
-  return $self->create($cond);
+  return $self->new_result($cond)->insert;
 }
 
 =head2 update_or_new
@@ -2974,7 +3118,7 @@ See also L</find>, L</find_or_create> and L</find_or_new>.
 
 sub update_or_new {
     my $self  = shift;
-    my $attrs = ( @_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {} );
+    my $attrs = ( @_ > 1 && ref $_[-1] eq 'HASH' ? pop(@_) : {} );
     my $cond  = ref $_[0] eq 'HASH' ? shift : {@_};
 
     my $row = $self->find( $cond, $attrs );
@@ -3083,7 +3227,7 @@ sub is_paged {
 
 sub is_ordered {
   my ($self) = @_;
-  return scalar $self->result_source->storage->_extract_order_criteria($self->{attrs}{order_by});
+  return scalar $self->result_source->schema->storage->_extract_order_criteria($self->{attrs}{order_by});
 }
 
 =head2 related_resultset
@@ -3103,10 +3247,16 @@ Returns a related resultset for the supplied relationship name.
 =cut
 
 sub related_resultset {
-  my ($self, $rel) = @_;
+  $_[0]->throw_exception(
+    'Extra arguments to $rs->related_resultset() were always quietly '
+  . 'discarded without consideration, you need to switch to '
+  . '...->related_resultset( $relname )->search_rs( $search, $args ) instead.'
+  ) if @_ > 2;
 
-  return $self->{related_resultsets}{$rel}
-    if defined $self->{related_resultsets}{$rel};
+  return $_[0]->{related_resultsets}{$_[1]}
+    if defined $_[0]->{related_resultsets}{$_[1]};
+
+  my ($self, $rel) = @_;
 
   return $self->{related_resultsets}{$rel} = do {
     my $rsrc = $self->result_source;
@@ -3119,22 +3269,25 @@ sub related_resultset {
 
     my $attrs = $self->_chain_relationship($rel);
 
-    my $join_count = $attrs->{seen_join}{$rel};
+    my $storage = $rsrc->schema->storage;
 
-    my $alias = $self->result_source->storage
-        ->relname_to_table_alias($rel, $join_count);
+    # Previously this atribute was deleted (instead of being set as it is now)
+    # Doing so seems to be harmless in all available test permutations
+    # See also 01d59a6a6 and mst's comment below
+    #
+    $attrs->{alias} = $storage->relname_to_table_alias(
+      $rel,
+      $attrs->{seen_join}{$rel}
+    );
 
     # since this is search_related, and we already slid the select window inwards
     # (the select/as attrs were deleted in the beginning), we need to flip all
     # left joins to inner, so we get the expected results
     # read the comment on top of the actual function to see what this does
-    $attrs->{from} = $rsrc->schema->storage->_inner_join_to_node ($attrs->{from}, $alias);
-
+    $attrs->{from} = $storage->_inner_join_to_node( $attrs->{from}, $attrs->{alias} );
 
     #XXX - temp fix for result_class bug. There likely is a more elegant fix -groditi
-    delete @{$attrs}{qw(result_class alias)};
-
-    my $rel_source = $rsrc->related_source($rel);
+    delete $attrs->{result_class};
 
     my $new = do {
 
@@ -3143,16 +3296,19 @@ sub related_resultset {
       # source you need to know what alias it's -going- to have for things
       # to work sanely (e.g. RestrictWithObject wants to be able to add
       # extra query restrictions, and these may need to be $alias.)
-
-      my $rel_attrs = $rel_source->resultset_attributes;
-      local $rel_attrs->{alias} = $alias;
-
-      $rel_source->resultset
-                 ->search_rs(
-                     undef, {
-                       %$attrs,
-                       where => $attrs->{where},
-                   });
+      #                                       -- mst ~ 2007 (01d59a6a6)
+      #
+      # FIXME - this seems to be no longer neccessary (perhaps due to the
+      # advances in relcond resolution. Testing DBIC::S::RWO and its only
+      # dependent (as of Jun 2015 ) does not yield any difference with or
+      # without this line. Nevertheless keep it as is for now, to minimize
+      # churn, there is enough potential for breakage in 0.0829xx as it is
+      #                                       -- ribasushi Jun 2015
+      #
+      my $rel_source = $rsrc->related_source($rel);
+      local $rel_source->resultset_attributes->{alias} = $attrs->{alias};
+
+      $rel_source->resultset->search_rs( undef, $attrs );
     };
 
     if (my $cache = $self->get_cache) {
@@ -3203,6 +3359,9 @@ source alias of the current result set:
     });
   }
 
+The alias of L<newly created resultsets|/search> can be altered by the
+L<alias attribute|/alias>.
+
 =cut
 
 sub current_source_alias {
@@ -3399,9 +3558,25 @@ sub _resolved_attrs {
   $self->throw_exception("Specifying distinct => 1 in conjunction with collapse => 1 is unsupported")
     if $attrs->{collapse} and $attrs->{distinct};
 
+
+  # Sanity check the paging attributes
+  # SQLMaker does it too, but in case of a software_limit we'll never get there
+  if (defined $attrs->{offset}) {
+    $self->throw_exception('A supplied offset attribute must be a non-negative integer')
+      if ( $attrs->{offset} =~ /[^0-9]/ or $attrs->{offset} < 0 );
+  }
+  if (defined $attrs->{rows}) {
+    $self->throw_exception("The rows attribute must be a positive integer if present")
+      if ( $attrs->{rows} =~ /[^0-9]/ or $attrs->{rows} <= 0 );
+  }
+
+  # normalize where condition
+  $attrs->{where} = normalize_sqla_condition( $attrs->{where} )
+    if $attrs->{where};
+
   # default selection list
   $attrs->{columns} = [ $source->columns ]
-    unless List::Util::first { exists $attrs->{$_} } qw/columns cols select as/;
+    unless grep { exists $attrs->{$_} } qw/columns cols select as/;
 
   # merge selectors together
   for (qw/columns select as/) {
@@ -3497,62 +3672,35 @@ sub _resolved_attrs {
       ];
   }
 
-  if ( defined $attrs->{order_by} ) {
-    $attrs->{order_by} = (
-      ref( $attrs->{order_by} ) eq 'ARRAY'
-      ? [ @{ $attrs->{order_by} } ]
-      : [ $attrs->{order_by} || () ]
-    );
-  }
 
-  if ($attrs->{group_by} and ref $attrs->{group_by} ne 'ARRAY') {
-    $attrs->{group_by} = [ $attrs->{group_by} ];
-  }
+  for my $attr (qw(order_by group_by)) {
 
+    if ( defined $attrs->{$attr} ) {
+      $attrs->{$attr} = (
+        ref( $attrs->{$attr} ) eq 'ARRAY'
+        ? [ @{ $attrs->{$attr} } ]
+        : [ $attrs->{$attr} || () ]
+      );
 
-  # generate selections based on the prefetch helper
-  my ($prefetch, @prefetch_select, @prefetch_as);
-  $prefetch = $self->_merge_joinpref_attr( {}, delete $attrs->{prefetch} )
-    if defined $attrs->{prefetch};
+      delete $attrs->{$attr} unless @{$attrs->{$attr}};
+    }
+  }
 
-  if ($prefetch) {
-
-    $self->throw_exception("Unable to prefetch, resultset contains an unnamed selector $attrs->{_dark_selector}{string}")
-      if $attrs->{_dark_selector};
 
+  # set collapse default based on presence of prefetch
+  my $prefetch;
+  if (
+    defined $attrs->{prefetch}
+      and
+    $prefetch = $self->_merge_joinpref_attr( {}, delete $attrs->{prefetch} )
+  ) {
     $self->throw_exception("Specifying prefetch in conjunction with an explicit collapse => 0 is unsupported")
       if defined $attrs->{collapse} and ! $attrs->{collapse};
 
     $attrs->{collapse} = 1;
-
-    # this is a separate structure (we don't look in {from} directly)
-    # as the resolver needs to shift things off the lists to work
-    # properly (identical-prefetches on different branches)
-    my $join_map = {};
-    if (ref $attrs->{from} eq 'ARRAY') {
-
-      my $start_depth = $attrs->{seen_join}{-relation_chain_depth} || 0;
-
-      for my $j ( @{$attrs->{from}}[1 .. $#{$attrs->{from}} ] ) {
-        next unless $j->[0]{-alias};
-        next unless $j->[0]{-join_path};
-        next if ($j->[0]{-relation_chain_depth} || 0) < $start_depth;
-
-        my @jpath = map { keys %$_ } @{$j->[0]{-join_path}};
-
-        my $p = $join_map;
-        $p = $p->{$_} ||= {} for @jpath[ ($start_depth/2) .. $#jpath]; #only even depths are actual jpath boundaries
-        push @{$p->{-join_aliases} }, $j->[0]{-alias};
-      }
-    }
-
-    my @prefetch = $source->_resolve_prefetch( $prefetch, $alias, $join_map );
-
-    # save these for after distinct resolution
-    @prefetch_select = map { $_->[0] } @prefetch;
-    @prefetch_as = map { $_->[1] } @prefetch;
   }
 
+
   # run through the resulting joinstructure (starting from our current slot)
   # and unset collapse if proven unnecessary
   #
@@ -3589,7 +3737,7 @@ sub _resolved_attrs {
         if (
           ! $attrs->{_main_source_premultiplied}
             and
-          ! List::Util::first { ! $_->[0]{-is_single} } @fromlist
+          ! grep { ! $_->[0]{-is_single} } @fromlist
         ) {
           $attrs->{collapse} = 0;
         }
@@ -3602,6 +3750,7 @@ sub _resolved_attrs {
     }
   }
 
+
   # generate the distinct induced group_by before injecting the prefetched select/as parts
   if (delete $attrs->{distinct}) {
     if ($attrs->{group_by}) {
@@ -3610,7 +3759,7 @@ sub _resolved_attrs {
     else {
       $attrs->{_grouped_by_distinct} = 1;
       # distinct affects only the main selection part, not what prefetch may add below
-      ($attrs->{group_by}, my $new_order) = $source->storage->_group_over_selection($attrs);
+      ($attrs->{group_by}, my $new_order) = $source->schema->storage->_group_over_selection($attrs);
 
       # FIXME possibly ignore a rewritten order_by (may turn out to be an issue)
       # The thinking is: if we are collapsing the subquerying prefetch engine will
@@ -3621,15 +3770,46 @@ sub _resolved_attrs {
     }
   }
 
-  # inject prefetch-bound selection (if any)
-  push @{$attrs->{select}}, @prefetch_select;
-  push @{$attrs->{as}}, @prefetch_as;
 
-  # whether we can get away with the dumbest (possibly DBI-internal) collapser
-  if ( List::Util::first { $_ =~ /\./ } @{$attrs->{as}} ) {
-    $attrs->{_related_results_construction} = 1;
+  # generate selections based on the prefetch helper
+  if ($prefetch) {
+
+    $self->throw_exception("Unable to prefetch, resultset contains an unnamed selector $attrs->{_dark_selector}{string}")
+      if $attrs->{_dark_selector};
+
+    # this is a separate structure (we don't look in {from} directly)
+    # as the resolver needs to shift things off the lists to work
+    # properly (identical-prefetches on different branches)
+    my $joined_node_aliases_map = {};
+    if (ref $attrs->{from} eq 'ARRAY') {
+
+      my $start_depth = $attrs->{seen_join}{-relation_chain_depth} || 0;
+
+      for my $j ( @{$attrs->{from}}[1 .. $#{$attrs->{from}} ] ) {
+        next unless $j->[0]{-alias};
+        next unless $j->[0]{-join_path};
+        next if ($j->[0]{-relation_chain_depth} || 0) < $start_depth;
+
+        my @jpath = map { keys %$_ } @{$j->[0]{-join_path}};
+
+        my $p = $joined_node_aliases_map;
+        $p = $p->{$_} ||= {} for @jpath[ ($start_depth/2) .. $#jpath]; #only even depths are actual jpath boundaries
+        push @{$p->{-join_aliases} }, $j->[0]{-alias};
+      }
+    }
+
+    ( push @{$attrs->{select}}, $_->[0] ) and ( push @{$attrs->{as}}, $_->[1] )
+      for $source->_resolve_selection_from_prefetch( $prefetch, $joined_node_aliases_map );
   }
 
+
+  $attrs->{_simple_passthrough_construction} = !(
+    $attrs->{collapse}
+      or
+    grep { $_ =~ /\./ } @{$attrs->{as}}
+  );
+
+
   # 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
@@ -3695,8 +3875,10 @@ sub _calculate_score {
 
   if (ref $b eq 'HASH') {
     my ($b_key) = keys %{$b};
+    $b_key = '' if ! defined $b_key;
     if (ref $a eq 'HASH') {
       my ($a_key) = keys %{$a};
+      $a_key = '' if ! defined $a_key;
       if ($a_key eq $b_key) {
         return (1 + $self->_calculate_score( $a->{$a_key}, $b->{$b_key} ));
       } else {
@@ -3785,7 +3967,7 @@ sub _merge_joinpref_attr {
           },
           ARRAY => sub {
             return $_[1] if !defined $_[0];
-            return $_[1] if __HM_DEDUP and List::Util::first { $_ eq $_[0] } @{$_[1]};
+            return $_[1] if __HM_DEDUP and grep { $_ eq $_[0] } @{$_[1]};
             return [$_[0], @{$_[1]}]
           },
           HASH  => sub {
@@ -3798,7 +3980,7 @@ sub _merge_joinpref_attr {
         ARRAY => {
           SCALAR => sub {
             return $_[0] if !defined $_[1];
-            return $_[0] if __HM_DEDUP and List::Util::first { $_ eq $_[1] } @{$_[0]};
+            return $_[0] if __HM_DEDUP and grep { $_ eq $_[1] } @{$_[0]};
             return [@{$_[0]}, $_[1]]
           },
           ARRAY => sub {
@@ -3811,7 +3993,7 @@ sub _merge_joinpref_attr {
           HASH => sub {
             return [ $_[1] ] if ! @{$_[0]};
             return $_[0] if !keys %{$_[1]};
-            return $_[0] if __HM_DEDUP and List::Util::first { $_ eq $_[1] } @{$_[0]};
+            return $_[0] if __HM_DEDUP and grep { $_ eq $_[1] } @{$_[0]};
             return [ @{$_[0]}, $_[1] ];
           },
         },
@@ -3826,7 +4008,7 @@ sub _merge_joinpref_attr {
             return [] if !keys %{$_[0]} and !@{$_[1]};
             return [ $_[0] ] if !@{$_[1]};
             return $_[1] if !keys %{$_[0]};
-            return $_[1] if __HM_DEDUP and List::Util::first { $_ eq $_[0] } @{$_[1]};
+            return $_[1] if __HM_DEDUP and grep { $_ eq $_[0] } @{$_[1]};
             return [ $_[0], @{$_[1]} ];
           },
           HASH => sub {
@@ -3948,7 +4130,7 @@ syntax as outlined above.
 Shortcut to request a particular set of columns to be retrieved. Each
 column spec may be a string (a table column name), or a hash (in which
 case the key is the C<as> value, and the value is used as the C<select>
-expression). Adds C<me.> onto the start of any column without a C<.> in
+expression). Adds the L</current_source_alias> onto the start of any column without a C<.> in
 it and sets C<select> from that, then auto-populates C<as> from
 C<select> as normal. (You may also use the C<cols> attribute, as in
 earlier versions of DBIC, but this is deprecated)
@@ -3963,19 +4145,34 @@ is the same as
     as     => [qw(some_column dbic_slot)]
 
 If you want to individually retrieve related columns (in essence perform
-manual prefetch) you have to make sure to specify the correct inflation slot
+manual L</prefetch>) you have to make sure to specify the correct inflation slot
 chain such that it matches existing relationships:
 
     my $rs = $schema->resultset('Artist')->search({}, {
         # required to tell DBIC to collapse has_many relationships
         collapse => 1,
-        join     => { cds => 'tracks'},
+        join     => { cds => 'tracks' },
         '+columns'  => {
           'cds.cdid'         => 'cds.cdid',
           'cds.tracks.title' => 'tracks.title',
         },
     });
 
+Like elsewhere, literal SQL or literal values can be included by using a
+scalar reference or a literal bind value, and these values will be available
+in the result with C<get_column> (see also
+L<SQL::Abstract/Literal SQL and value type operators>):
+
+    # equivalent SQL: SELECT 1, 'a string', IF(my_column,?,?) ...
+    # bind values: $true_value, $false_value
+    columns => [
+        {
+            foo => \1,
+            bar => \q{'a string'},
+            baz => \[ 'IF(my_column,?,?)', $true_value, $false_value ],
+        }
+    ]
+
 =head2 +columns
 
 B<NOTE:> You B<MUST> explicitly quote C<'+columns'> when using this attribute.
@@ -4028,10 +4225,11 @@ names:
 
 B<NOTE:> You will almost always need a corresponding L</as> attribute when you
 use L</select>, to instruct DBIx::Class how to store the result of the column.
-Also note that the L</as> attribute has nothing to do with the SQL-side 'AS'
-identifier aliasing. You can however alias a function, so you can use it in
-e.g. an C<ORDER BY> clause. This is done via the C<-as> B<select function
-attribute> supplied as shown in the example above.
+
+Also note that the L</as> attribute has B<nothing to do> with the SQL-side
+C<AS> identifier aliasing. You B<can> alias a function (so you can use it e.g.
+in an C<ORDER BY> clause), however this is done via the C<-as> B<select
+function attribute> supplied as shown in the example above.
 
 =head2 +select
 
@@ -4061,8 +4259,10 @@ Indicates DBIC-side names for object inflation. That is L</as> indicates the
 slot name in which the column value will be stored within the
 L<Row|DBIx::Class::Row> object. The value will then be accessible via this
 identifier by the C<get_column> method (or via the object accessor B<if one
-with the same name already exists>) as shown below. The L</as> attribute has
-B<nothing to do> with the SQL-side C<AS>. See L</select> for details.
+with the same name already exists>) as shown below.
+
+The L</as> attribute has B<nothing to do> with the SQL-side identifier
+aliasing C<AS>. See L</select> for details.
 
   $rs = $schema->resultset('Employee')->search(undef, {
     select => [
@@ -4238,8 +4438,10 @@ For a more in-depth discussion, see L</PREFETCHING>.
 
 This attribute is a shorthand for specifying a L</join> spec, adding all
 columns from the joined related sources as L</+columns> and setting
-L</collapse> to a true value. For example, the following two queries are
-equivalent:
+L</collapse> to a true value. It can be thought of as a rough B<superset>
+of the L</join> attribute.
+
+For example, the following two queries are equivalent:
 
   my $rs = $schema->resultset('Artist')->search({}, {
     prefetch => { cds => ['genre', 'tracks' ] },
@@ -4416,15 +4618,20 @@ A arrayref of columns to group by. Can include columns of joined tables.
 
 =back
 
-HAVING is a select statement attribute that is applied between GROUP BY and
-ORDER BY. It is applied to the after the grouping calculations have been
-done.
+The HAVING operator specifies a B<secondary> condition applied to the set
+after the grouping calculations have been done. In other words it is a
+constraint just like L</where> (and accepting the same
+L<SQL::Abstract syntax|SQL::Abstract/WHERE CLAUSES>) applied to the data
+as it exists after GROUP BY has taken place. Specifying L</having> without
+L</group_by> is a logical mistake, and a fatal error on most RDBMS engines.
+
+E.g.
 
   having => { 'count_employee' => { '>=', 100 } }
 
 or with an in-place function in which case literal SQL is required:
 
-  having => \[ 'count(employee) >= ?', [ count => 100 ] ]
+  having => \[ 'count(employee) >= ?', 100 ]
 
 =head2 distinct
 
@@ -4448,19 +4655,14 @@ setting is ignored and an appropriate warning is issued.
 
 =head2 where
 
-=over 4
-
-Adds to the WHERE clause.
+Adds extra conditions to the resultset, combined with the preexisting C<WHERE>
+conditions, same as the B<first> argument to the L<search operator|/search>
 
   # only return rows WHERE deleted IS NULL for all searches
   __PACKAGE__->resultset_attributes({ where => { deleted => undef } });
 
-Can be overridden by passing C<< { where => undef } >> as an attribute
-to a resultset.
-
-For more complicated where clauses see L<SQL::Abstract/WHERE CLAUSES>.
-
-=back
+Note that the above example is
+L<strongly discouraged|DBIx::Class::ResultSource/resultset_attributes>.
 
 =head2 cache
 
@@ -4473,7 +4675,7 @@ revisit rows in your ResultSet:
     ... do stuff ...
   }
 
-  $rs->first; # without cache, this would issue a query
+  $resultset->first; # without cache, this would issue a query
 
 By default, searches are not cached.
 
@@ -4666,11 +4868,15 @@ supported:
   [ undef,   $val ] === [ {}, $val ]
   $val              === [ {}, $val ]
 
-=head1 AUTHOR AND CONTRIBUTORS
+=head1 FURTHER QUESTIONS?
 
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
+=cut