Merge branch 'master' into topic/constructor_rewrite
Peter Rabbitson [Wed, 17 Apr 2013 07:34:50 +0000 (09:34 +0200)]
Add some extra code to enforce the assumption that any bind type constant
is accessible in _dbi_attrs_for_bind, or in other words that all necessary
DBDs are already loaded (concept originally introduced in ad7c50fc)

Without this the combination of 9930caaf7e (do not recalculate bind attrs
on dbh_do retry) and a2f228547 (do not wrap iterators in dbh_do) can result
in _dbi_attrs_for_bind being called before DBI/DBD::* has been loaded at all

1  2 
Changes
lib/DBIx/Class.pm
lib/DBIx/Class/ResultSet.pm
lib/DBIx/Class/Row.pm
lib/DBIx/Class/Storage/DBI.pm
t/60core.t
t/sqlmaker/limit_dialects/torture.t

diff --combined Changes
+++ b/Changes
@@@ -1,28 -1,26 +1,51 @@@
  Revision history for DBIx::Class
  
+     * Fixes
++        - Fix _dbi_attrs_for_bind() being called befor DBI has been loaded
++          (regression in 0.08210)
+         - Fix update/delete operations on resultsets *joining* the updated
+           table failing on MySQL. Resolves oversights in the fixes for
+           RT#81378 and RT#81897
+         - Stop Sybase ASE storage from generating invalid SQL in subselects
+           when a limit without offset is encountered
+ 0.08210 2013-04-04 15:30 (UTC)
+     * New Features / Changes
+         - Officially deprecate the 'cols' and 'include_columns' resultset
+           attributes
+         - Remove ::Storage::DBI::sth() deprecated in 0.08191
+     * Fixes
+         - Work around a *critical* bug with potential for data loss in
+           DBD::SQLite - RT#79576
+         - Audit and correct potential bugs associated with braindead reuse
+           of $1 on unsuccessful matches
+         - Fix incorrect warning/exception originator reported by carp*() and
+           throw_exception()
 +0.08242-TRIAL (EXPERIMENTAL BETA RELEASE) 2013-03-10 14:44 (UTC)
 +    * New Features / Changes
 +        - Prefetch with limit on right-side ordered resultsets now works
 +          correctly (via aggregated grouping)
 +        - Changing the result_class of a ResultSet in progress is now
 +          explicitly forbidden. The behavior was undefined before, and
 +          would result in wildly differing outcomes depending on $rs
 +          attributes.
 +        - Scale back validation of the 'as' attribute - in the field
 +          there are legitimate-ish uses of a inflating into an apparently
 +          invalid relationship graph
 +        - Warn in case of iterative collapse being upgraded to an eager
 +          cursor slurp
 +        - No longer order the insides of a complex prefetch subquery,
 +          unless required to satisfy a limit
 +
 +    * Fixes
 +        - Properly consider unselected order_by criteria during complex
 +          subqueried prefetch
 +        - Properly support "MySQL-style" left-side group_by with prefetch
 +        - Fix $grouped_rs->get_column($col)->func($func) producing incorrect
 +          SQL (RT#81127)
 +
  0.08209 2013-03-01 12:56 (UTC)
      * New Features / Changes
          - Debugging aid - warn on invalid result objects created by what
            tarball contents (implicitly fixes RT#83084)
          - Added strict and warnings tests for all lib and test files
  
 +0.08241-TRIAL (EXPERIMENTAL BETA RELEASE) 2013-02-20 11:37 (UTC)
 +    * New Features / Changes
 +        - Revert to passing the original (pre-0.08240) arguments to
 +          inflate_result() and remove the warning about ResultClass
 +          inheritance.
 +        - Optimize the generated rowparsers even more - no user-visible
 +          changes.
 +        - Emit a warning on incorrect use of nullable columns within a
 +          primary key
 +
 +0.08240-TRIAL (EXPERIMENTAL BETA RELEASE) 2013-02-14 05:56 (UTC)
 +    * New Features / Changes
 +        - Rewrite from scratch the result constructor codepath - many bugfixes
 +          and performance improvements (the current codebase is now capable of
 +          outperforming both DBIx::DataModel and Rose::DB::Object on some
 +          workloads). Some notable benefits:
 +          - Multiple has_many prefetch
 +          - Partial prefetch - you now can select only columns you are
 +            interested in, while preserving the collapse functionality
 +            (collapse is now exposed as a first-class API attribute)
 +          - Prefetch of resultsets with arbitrary order
 +            (RT#54949, RT#74024, RT#74584)
 +          - Prefetch no longer inserts right-side table order_by clauses
 +            (massively helps the deficient MySQL optimizer)
 +        - Massively optimize codepath around ->cursor(), over 10x speedup
 +          on some iterating workloads.
 +
 +    * Fixes
 +        - Fix open cursors silently resetting when inherited across a fork
 +          or a thread
 +        - Fix duplicated selected columns when calling 'count' when a same
 +          aggregate function is used more than once in a 'having' clause
 +          (RT#83305)
 +
 +    * Misc
 +        - Fixup our distbuilding process to stop creating world-writable
 +          tarball contents (implicitly fixes RT#83084)
 +        - Added strict and warnings tests for all lib and test files
 +
  0.08206 2013-02-08
      * Fixes
          - Fix dbh_do() failing to properly reconnect (regression in 0.08205)
diff --combined lib/DBIx/Class.pm
@@@ -11,7 -11,7 +11,7 @@@ our $VERSION
  # $VERSION declaration must stay up here, ahead of any other package
  # declarations, as to not confuse various modules attempting to determine
  # this ones version, whether that be s.c.o. or Module::Metadata, etc
 -$VERSION = '0.08210';
 +$VERSION = '0.08242';
  
  $VERSION = eval $VERSION if $VERSION =~ /_/; # numify for warning-free dev releases
  
@@@ -95,6 -95,10 +95,10 @@@ sub _attr_cache 
  
  1;
  
+ __END__
+ =encoding UTF-8
  =head1 NAME
  
  DBIx::Class - Extensible and flexible object <-> relational mapper.
@@@ -131,41 -135,11 +135,11 @@@ list below is sorted by "fastest respon
  
  =back
  
- =head1 HOW TO CONTRIBUTE
- Contributions are always welcome, in all usable forms (we especially
- welcome documentation improvements). The delivery methods include git-
- or unified-diff formatted patches, GitHub pull requests, or plain bug
- reports either via RT or the Mailing list. Contributors are generally
- granted full access to the official repository after their first patch
- passes successful review.
- =for comment
- FIXME: Getty, frew and jnap need to get off their asses and finish the contrib section so we can link it here ;)
- This project is maintained in a git repository. The code and related tools are
- accessible at the following locations:
- =over
- =item * Official repo: L<git://git.shadowcat.co.uk/dbsrgits/DBIx-Class.git>
- =item * Official gitweb: L<http://git.shadowcat.co.uk/gitweb/gitweb.cgi?p=dbsrgits/DBIx-Class.git>
- =item * GitHub mirror: L<https://github.com/dbsrgits/DBIx-Class>
- =item * Authorized committers: L<ssh://dbsrgits@git.shadowcat.co.uk/DBIx-Class.git>
- =item * Travis-CI log: L<https://travis-ci.org/dbsrgits/dbix-class/builds>
- =for html
- <br>&#x21AA; Stable branch CI status: <img src="https://secure.travis-ci.org/dbsrgits/dbix-class.png?branch=master"></img>
- =back
  =head1 SYNOPSIS
  
- Create a schema class called MyApp/Schema.pm:
+ =head2 Schema classes preparation
+ Create a schema class called F<MyApp/Schema.pm>:
  
    package MyApp::Schema;
    use base qw/DBIx::Class::Schema/;
    1;
  
  Create a result class to represent artists, who have many CDs, in
- MyApp/Schema/Result/Artist.pm:
+ F<MyApp/Schema/Result/Artist.pm>:
  
  See L<DBIx::Class::ResultSource> for docs on defining result classes.
  
    1;
  
  A result class to represent a CD, which belongs to an artist, in
- MyApp/Schema/Result/CD.pm:
+ F<MyApp/Schema/Result/CD.pm>:
  
    package MyApp::Schema::Result::CD;
    use base qw/DBIx::Class::Core/;
  
    1;
  
+ =head2 API usage
  Then you can use these classes in your application's code:
  
    # Connect to your database.
@@@ -271,7 -247,8 +247,8 @@@ that allows abstract encapsulation of d
  representing queries in your code as perl-ish as possible while still
  providing access to as many of the capabilities of the database as possible,
  including retrieving related records from multiple tables in a single query,
- JOIN, LEFT JOIN, COUNT, DISTINCT, GROUP BY, ORDER BY and HAVING support.
+ C<JOIN>, C<LEFT JOIN>, C<COUNT>, C<DISTINCT>, C<GROUP BY>, C<ORDER BY> and
+ C<HAVING> support.
  
  DBIx::Class can handle multi-column primary and foreign keys, complex
  queries and database-level paging, and does its best to only query the
@@@ -284,8 -261,8 +261,8 @@@ and thread-safe out of the box (althoug
  L<your DBD may not be|DBI/Threads and Thread Safety>).
  
  This project is still under rapid development, so large new features may be
- marked EXPERIMENTAL - such APIs are still usable but may have edge bugs.
- Failing test cases are *always* welcome and point releases are put out rapidly
+ marked B<experimental> - such APIs are still usable but may have edge bugs.
+ Failing test cases are I<always> welcome and point releases are put out rapidly
  as bugs are found and fixed.
  
  We do our best to maintain full backwards compatibility for published
@@@ -297,6 -274,38 +274,38 @@@ The test suite is quite substantial, an
  are generally made to CPAN before the branch for the next release is
  merged back to trunk for a major release.
  
+ =head1 HOW TO CONTRIBUTE
+ Contributions are always welcome, in all usable forms (we especially
+ welcome documentation improvements). The delivery methods include git-
+ or unified-diff formatted patches, GitHub pull requests, or plain bug
+ reports either via RT or the Mailing list. Contributors are generally
+ granted full access to the official repository after their first patch
+ passes successful review.
+ =for comment
+ FIXME: Getty, frew and jnap need to get off their asses and finish the contrib section so we can link it here ;)
+ This project is maintained in a git repository. The code and related tools are
+ accessible at the following locations:
+ =over
+ =item * Official repo: L<git://git.shadowcat.co.uk/dbsrgits/DBIx-Class.git>
+ =item * Official gitweb: L<http://git.shadowcat.co.uk/gitweb/gitweb.cgi?p=dbsrgits/DBIx-Class.git>
+ =item * GitHub mirror: L<https://github.com/dbsrgits/DBIx-Class>
+ =item * Authorized committers: L<ssh://dbsrgits@git.shadowcat.co.uk/DBIx-Class.git>
+ =item * Travis-CI log: L<https://travis-ci.org/dbsrgits/dbix-class/builds>
+ =for html
+ &#x21AA; Stable branch CI status: <img src="https://secure.travis-ci.org/dbsrgits/dbix-class.png?branch=master"></img>
+ =back
  =head1 AUTHOR
  
  mst: Matt S. Trout <mst@shadowcatsystems.co.uk>
@@@ -362,8 -371,12 +371,12 @@@ clkao: CL Ka
  
  da5id: David Jack Olrik <djo@cpan.org>
  
+ dariusj: Darius Jokilehto <dariusjokilehto@yahoo.co.uk>
  davewood: David Schmidt <davewood@gmx.at>
  
+ daxim: Lars Dɪᴇᴄᴋᴏᴡ 迪拉斯 <daxim@cpan.org>
  debolaz: Anders Nor Berle <berle@cpan.org>
  
  dew: Dan Thomas <dan@godders.org>
@@@ -563,5 -576,3 +576,3 @@@ as listed above
  
  This library is free software and may be distributed under the same terms
  as perl itself.
- =cut
@@@ -141,15 -141,11 +141,15 @@@ another
  
  =head3 Resolving conditions and attributes
  
 -When a resultset is chained from another resultset, conditions and
 -attributes with the same keys need resolving.
 +When a resultset is chained from another resultset (ie:
 +C<my $new_rs = $old_rs->search(\%extra_cond, \%attrs)>), conditions
 +and attributes with the same keys need resolving.
  
 -L</join>, L</prefetch>, L</+select>, L</+as> attributes are merged
 -into the existing ones from the original resultset.
 +If any of L</columns>, L</select>, L</as> are present, they reset the
 +original selection, and start the selection "clean".
 +
 +The L</join>, L</prefetch>, L</+columns>, L</+select>, L</+as> attributes
 +are merged into the existing ones from the original resultset.
  
  The L</where> and L</having> attributes, and any search conditions, are
  merged with an SQL C<AND> to the existing condition from the original
@@@ -443,6 -439,7 +443,7 @@@ sub search_rs 
  
      # older deprecated name, use only if {columns} is not there
      if (my $c = delete $new_attrs->{cols}) {
+       carp_unique( "Resultset attribute 'cols' is deprecated, use 'columns' instead" );
        if ($new_attrs->{columns}) {
          carp "Resultset specifies both the 'columns' and the legacy 'cols' attributes - ignoring 'cols'";
        }
@@@ -489,8 -486,12 +490,12 @@@ sub _normalize_selection 
    my ($self, $attrs) = @_;
  
    # legacy syntax
-   $attrs->{'+columns'} = $self->_merge_attr($attrs->{'+columns'}, delete $attrs->{include_columns})
-     if exists $attrs->{include_columns};
+   if ( exists $attrs->{include_columns} ) {
+     carp_unique( "Resultset attribute 'include_columns' is deprecated, use '+columns' instead" );
+     $attrs->{'+columns'} = $self->_merge_attr(
+       $attrs->{'+columns'}, delete $attrs->{include_columns}
+     );
+   }
  
    # columns are always placed first, however
  
@@@ -846,7 -847,7 +851,7 @@@ sub find 
  
    # Run the query, passing the result_class since it should propagate for find
    my $rs = $self->search ($final_cond, {result_class => $self->result_class, %$attrs});
 -  if (keys %{$rs->_resolved_attrs->{collapse}}) {
 +  if ($rs->_resolved_attrs->{collapse}) {
      my $row = $rs->next;
      carp "Query returned more than one row" if $rs->next;
      return $row;
@@@ -1056,9 -1057,11 +1061,9 @@@ sub single 
  
    my $attrs = { %{$self->_resolved_attrs} };
  
 -  if (keys %{$attrs->{collapse}}) {
 -    $self->throw_exception(
 -      'single() can not be used on resultsets prefetching has_many. Use find( \%cond ) or next() instead'
 -    );
 -  }
 +  $self->throw_exception(
 +    'single() can not be used on resultsets prefetching has_many. Use find( \%cond ) or next() instead'
 +  ) if $attrs->{collapse};
  
    if ($where) {
      if (defined $attrs->{where}) {
      }
    }
  
 -  my @data = $self->result_source->storage->select_single(
 +  my $data = [ $self->result_source->storage->select_single(
      $attrs->{from}, $attrs->{select},
      $attrs->{where}, $attrs
 -  );
 -
 -  return (@data ? ($self->_construct_object(@data))[0] : undef);
 +  )];
 +  return undef unless @$data;
 +  $self->{_stashed_rows} = [ $data ];
 +  $self->_construct_results->[0];
  }
  
  
@@@ -1235,232 -1237,161 +1240,232 @@@ first record from the resultset
  
  sub next {
    my ($self) = @_;
 +
    if (my $cache = $self->get_cache) {
      $self->{all_cache_position} ||= 0;
      return $cache->[$self->{all_cache_position}++];
    }
 +
    if ($self->{attrs}{cache}) {
      delete $self->{pager};
      $self->{all_cache_position} = 1;
      return ($self->all)[0];
    }
 -  if ($self->{stashed_objects}) {
 -    my $obj = shift(@{$self->{stashed_objects}});
 -    delete $self->{stashed_objects} unless @{$self->{stashed_objects}};
 -    return $obj;
 -  }
 -  my @row = (
 -    exists $self->{stashed_row}
 -      ? @{delete $self->{stashed_row}}
 -      : $self->cursor->next
 -  );
 -  return undef unless (@row);
 -  my ($row, @more) = $self->_construct_object(@row);
 -  $self->{stashed_objects} = \@more if @more;
 -  return $row;
 -}
  
 -sub _construct_object {
 -  my ($self, @row) = @_;
 +  return shift(@{$self->{_stashed_results}}) if @{ $self->{_stashed_results}||[] };
 +
 +  $self->{_stashed_results} = $self->_construct_results
 +    or return undef;
  
 -  my $info = $self->_collapse_result($self->{_attrs}{as}, \@row)
 -    or return ();
 -  my @new = $self->result_class->inflate_result($self->result_source, @$info);
 -  @new = $self->{_attrs}{record_filter}->(@new)
 -    if exists $self->{_attrs}{record_filter};
 -  return @new;
 +  return shift @{$self->{_stashed_results}};
  }
  
 -sub _collapse_result {
 -  my ($self, $as_proto, $row) = @_;
 +# Constructs as many results as it can in one pass while respecting
 +# cursor laziness. Several modes of operation:
 +#
 +# * Always builds everything present in @{$self->{_stashed_rows}}
 +# * If called with $fetch_all true - pulls everything off the cursor and
 +#   builds all result structures (or objects) in one pass
 +# * If $self->_resolved_attrs->{collapse} is true, checks the order_by
 +#   and if the resultset is ordered properly by the left side:
 +#   * Fetches stuff off the cursor until the "master object" changes,
 +#     and saves the last extra row (if any) in @{$self->{_stashed_rows}}
 +#   OR
 +#   * Just fetches, and collapses/constructs everything as if $fetch_all
 +#     was requested (there is no other way to collapse except for an
 +#     eager cursor)
 +# * If no collapse is requested - just get the next row, construct and
 +#   return
 +sub _construct_results {
 +  my ($self, $fetch_all) = @_;
  
 -  my @copy = @$row;
 +  my $rsrc = $self->result_source;
 +  my $attrs = $self->_resolved_attrs;
  
 -  # 'foo'         => [ undef, 'foo' ]
 -  # 'foo.bar'     => [ 'foo', 'bar' ]
 -  # 'foo.bar.baz' => [ 'foo.bar', 'baz' ]
 +  if (
 +    ! $fetch_all
 +      and
 +    ! $attrs->{order_by}
 +      and
 +    $attrs->{collapse}
 +      and
 +    my @pcols = $rsrc->primary_columns
 +  ) {
 +    # default order for collapsing unless the user asked for something
 +    $attrs->{order_by} = [ map { join '.', $attrs->{alias}, $_} @pcols ];
 +    $attrs->{_ordered_for_collapse} = 1;
 +    $attrs->{_order_is_artificial} = 1;
 +  }
  
 -  my @construct_as = map { [ (/^(?:(.*)\.)?([^.]+)$/) ] } @$as_proto;
 +  my $cursor = $self->cursor;
  
 -  my %collapse = %{$self->{_attrs}{collapse}||{}};
 +  # this will be used as both initial raw-row collector AND as a RV of
 +  # _construct_results. Not regrowing the array twice matters a lot...
 +  # a surprising amount actually
 +  my $rows = delete $self->{_stashed_rows};
  
 -  my @pri_index;
 +  my $did_fetch_all = $fetch_all;
  
 -  # if we're doing collapsing (has_many prefetch) we need to grab records
 -  # until the PK changes, so fill @pri_index. if not, we leave it empty so
 -  # we know we don't have to bother.
 +  if ($fetch_all) {
 +    # FIXME SUBOPTIMAL - we can do better, cursor->next/all (well diff. methods) should return a ref
 +    $rows = [ ($rows ? @$rows : ()), $cursor->all ];
 +  }
 +  elsif( $attrs->{collapse} ) {
  
 -  # the reason for not using the collapse stuff directly is because if you
 -  # had for e.g. two artists in a row with no cds, the collapse info for
 -  # both would be NULL (undef) so you'd lose the second artist
 +    $attrs->{_ordered_for_collapse} = (!$attrs->{order_by}) ? 0 : do {
 +      my $st = $rsrc->schema->storage;
 +      my @ord_cols = map
 +        { $_->[0] }
 +        ( $st->_extract_order_criteria($attrs->{order_by}) )
 +      ;
  
 -  # store just the index so we can check the array positions from the row
 -  # without having to contruct the full hash
 +      my $colinfos = $st->_resolve_column_info($attrs->{from}, \@ord_cols);
  
 -  if (keys %collapse) {
 -    my %pri = map { ($_ => 1) } $self->result_source->_pri_cols;
 -    foreach my $i (0 .. $#construct_as) {
 -      next if defined($construct_as[$i][0]); # only self table
 -      if (delete $pri{$construct_as[$i][1]}) {
 -        push(@pri_index, $i);
 +      for (0 .. $#ord_cols) {
 +        if (
 +          ! $colinfos->{$ord_cols[$_]}
 +            or
 +          $colinfos->{$ord_cols[$_]}{-result_source} != $rsrc
 +        ) {
 +          splice @ord_cols, $_;
 +          last;
 +        }
 +      }
 +
 +      # 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 tsable manner
 +      (@ord_cols and $rsrc->_identifying_column_set({ map
 +        { $colinfos->{$_}{-colname} => $colinfos->{$_} }
 +        @ord_cols
 +      })) ? 1 : 0;
 +    } unless defined $attrs->{_ordered_for_collapse};
 +
 +    if (! $attrs->{_ordered_for_collapse}) {
 +      $did_fetch_all = 1;
 +
 +      # instead of looping over ->next, use ->all in stealth mode
 +      # *without* calling a ->reset afterwards
 +      # FIXME ENCAPSULATION - encapsulation breach, cursor method additions pending
 +      if (! $cursor->{_done}) {
 +        $rows = [ ($rows ? @$rows : ()), $cursor->all ];
 +        $cursor->{_done} = 1;
        }
      }
    }
  
 -  # no need to do an if, it'll be empty if @pri_index is empty anyway
 +  if (! $did_fetch_all and ! @{$rows||[]} ) {
 +    # FIXME SUBOPTIMAL - we can do better, cursor->next/all (well diff. methods) should return a ref
 +    if (scalar (my @r = $cursor->next) ) {
 +      $rows = [ \@r ];
 +    }
 +  }
  
 -  my %pri_vals = map { ($_ => $copy[$_]) } @pri_index;
 +  return undef unless @{$rows||[]};
  
 -  my @const_rows;
 +  my @extra_collapser_args;
 +  if ($attrs->{collapse} and ! $did_fetch_all ) {
  
 -  do { # no need to check anything at the front, we always want the first row
 +    @extra_collapser_args = (
 +      # FIXME SUBOPTIMAL - we can do better, cursor->next/all (well diff. methods) should return a ref
 +      sub { my @r = $cursor->next or return; \@r }, # how the collapser gets more rows
 +      ($self->{_stashed_rows} = []),                # where does it stuff excess
 +    );
 +  }
  
 -    my %const;
 +  # hotspot - skip the setter
 +  my $res_class = $self->_result_class;
  
 -    foreach my $this_as (@construct_as) {
 -      $const{$this_as->[0]||''}{$this_as->[1]} = shift(@copy);
 -    }
 +  my $inflator_cref = $self->{_result_inflator}{cref} ||= do {
 +    $res_class->can ('inflate_result')
 +      or $self->throw_exception("Inflator $res_class does not provide an inflate_result() method");
 +  };
  
 -    push(@const_rows, \%const);
 +  my $infmap = $attrs->{as};
  
 -  } until ( # no pri_index => no collapse => drop straight out
 -      !@pri_index
 -    or
 -      do { # get another row, stash it, drop out if different PK
  
 -        @copy = $self->cursor->next;
 -        $self->{stashed_row} = \@copy;
 +  $self->{_result_inflator}{is_core_row} = ( (
 +    $inflator_cref
 +      ==
 +    ( \&DBIx::Class::Row::inflate_result || die "No ::Row::inflate_result() - can't happen" )
 +  ) ? 1 : 0 ) unless defined $self->{_result_inflator}{is_core_row};
  
 -        # last thing in do block, counts as true if anything doesn't match
 +  $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')
 +    )
 +  ) ? 1 : 0 ) unless defined $self->{_result_inflator}{is_hri};
  
 -        # check xor defined first for NULL vs. NOT NULL then if one is
 -        # defined the other must be so check string equality
  
 -        grep {
 -          (defined $pri_vals{$_} ^ defined $copy[$_])
 -          || (defined $pri_vals{$_} && ($pri_vals{$_} ne $copy[$_]))
 -        } @pri_index;
 +  if (! $attrs->{_related_results_construction}) {
 +    # construct a much simpler array->hash folder for the one-table cases right here
 +    if ($self->{_result_inflator}{is_hri}) {
 +      for my $r (@$rows) {
 +        $r = { map { $infmap->[$_] => $r->[$_] } 0..$#$infmap };
        }
 -  );
 -
 -  my $alias = $self->{attrs}{alias};
 -  my $info = [];
 -
 -  my %collapse_pos;
 -
 -  my @const_keys;
 -
 -  foreach my $const (@const_rows) {
 -    scalar @const_keys or do {
 -      @const_keys = sort { length($a) <=> length($b) } keys %$const;
 -    };
 -    foreach my $key (@const_keys) {
 -      if (length $key) {
 -        my $target = $info;
 -        my @parts = split(/\./, $key);
 -        my $cur = '';
 -        my $data = $const->{$key};
 -        foreach my $p (@parts) {
 -          $target = $target->[1]->{$p} ||= [];
 -          $cur .= ".${p}";
 -          if ($cur eq ".${key}" && (my @ckey = @{$collapse{$cur}||[]})) {
 -            # collapsing at this point and on final part
 -            my $pos = $collapse_pos{$cur};
 -            CK: foreach my $ck (@ckey) {
 -              if (!defined $pos->{$ck} || $pos->{$ck} ne $data->{$ck}) {
 -                $collapse_pos{$cur} = $data;
 -                delete @collapse_pos{ # clear all positioning for sub-entries
 -                  grep { m/^\Q${cur}.\E/ } keys %collapse_pos
 -                };
 -                push(@$target, []);
 -                last CK;
 -              }
 -            }
 -          }
 -          if (exists $collapse{$cur}) {
 -            $target = $target->[-1];
 -          }
 -        }
 -        $target->[0] = $data;
 -      } else {
 -        $info->[0] = $const->{$key};
 +    }
 +    # FIXME SUBOPTIMAL this is a very very very hot spot
 +    # while rather optimal we can *still* do much better, by
 +    # building a smarter Row::inflate_result(), and
 +    # switch to feeding it data via a much leaner interface
 +    #
 +    # crude unscientific benchmarking indicated the shortcut eval is not worth it for
 +    # this particular resultset size
 +    elsif (@$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 )
 +      );
 +    }
 +  }
 +  # Special-case multi-object HRI (we always prune, and there is no $inflator_cref pass)
 +  elsif ($self->{_result_inflator}{is_hri}) {
 +    ( $self->{_row_parser}{hri} ||= $rsrc->_mk_row_parser({
 +      eval => 1,
 +      inflate_map => $infmap,
 +      selection => $attrs->{select},
 +      collapse => $attrs->{collapse},
 +      premultiplied => $attrs->{_main_source_premultiplied},
 +      hri_style => 1,
 +      prune_null_branches => 1,
 +    }) )->($rows, @extra_collapser_args);
 +  }
 +  # Regular multi-object
 +  else {
 +    my $parser_type = $self->{_result_inflator}{is_core_row} ? 'classic_pruning' : 'classic_nonpruning';
 +
 +    ( $self->{_row_parser}{$parser_type} ||= $rsrc->_mk_row_parser({
 +      eval => 1,
 +      inflate_map => $infmap,
 +      selection => $attrs->{select},
 +      collapse => $attrs->{collapse},
 +      premultiplied => $attrs->{_main_source_premultiplied},
 +      prune_null_branches => $self->{_result_inflator}{is_core_row},
 +    }) )->($rows, @extra_collapser_args);
 +
 +    $_ = $inflator_cref->($res_class, $rsrc, @$_) for @$rows;
    }
  
 -  return $info;
 +  # The @$rows check seems odd at first - why wouldn't we want to warn
 +  # regardless? The issue is things like find() etc, where the user
 +  # *knows* only one result will come back. In these cases the ->all
 +  # is not a pessimization, but rather something we actually want
 +  carp_unique(
 +    'Unable to properly collapse has_many results in iterator mode due '
 +  . 'to order criteria - performed an eager cursor slurp underneath. '
 +  . 'Consider using ->all() instead'
 +  ) if ( ! $fetch_all and @$rows > 1 );
 +
 +  return $rows;
  }
  
  =head2 result_source
@@@ -1500,22 -1431,14 +1505,22 @@@ in the original source class will not r
  sub result_class {
    my ($self, $result_class) = @_;
    if ($result_class) {
 -    unless (ref $result_class) { # don't fire this for an object
 -      $self->ensure_class_loaded($result_class);
 +
 +    # don't fire this for an object
 +    $self->ensure_class_loaded($result_class)
 +      unless ref($result_class);
 +
 +    if ($self->get_cache) {
 +      carp_unique('Changing the result_class of a ResultSet instance with cached results is a noop - the cache contents will not be altered');
 +    }
 +    # FIXME ENCAPSULATION - encapsulation breach, cursor method additions pending
 +    elsif ($self->{cursor} && $self->{cursor}{_pos}) {
 +      $self->throw_exception('Changing the result_class of a ResultSet instance with an active cursor is not supported');
      }
 +
      $self->_result_class($result_class);
 -    # THIS LINE WOULD BE A BUG - this accessor specifically exists to
 -    # permit the user to set result class on one result set only; it only
 -    # chains if provided to search()
 -    #$self->{attrs}{result_class} = $result_class if ref $self;
 +
 +    delete $self->{_result_inflator};
    }
    $self->_result_class;
  }
@@@ -1545,7 -1468,8 +1550,7 @@@ sub count 
  
    # this is a little optimization - it is faster to do the limit
    # adjustments in software, instead of a subquery
 -  my $rows = delete $attrs->{rows};
 -  my $offset = delete $attrs->{offset};
 +  my ($rows, $offset) = delete @{$attrs}{qw/rows offset/};
  
    my $crs;
    if ($self->_has_resolved_attr (qw/collapse group_by/)) {
@@@ -1611,11 -1535,12 +1616,11 @@@ sub _count_rs 
  
    my $tmp_attrs = { %$attrs };
    # take off any limits, record_filter is cdbi, and no point of ordering nor locking a count
 -  delete @{$tmp_attrs}{qw/rows offset order_by record_filter for/};
 +  delete @{$tmp_attrs}{qw/rows offset order_by _related_results_construction record_filter for/};
  
    # overwrite the selector (supplied by the storage)
    $tmp_attrs->{select} = $rsrc->storage->_count_select ($rsrc, $attrs);
    $tmp_attrs->{as} = 'count';
 -  delete @{$tmp_attrs}{qw/columns/};
  
    my $tmp_rs = $rsrc->resultset_class->new($rsrc, $tmp_attrs)->get_column ('count');
  
@@@ -1633,11 -1558,11 +1638,11 @@@ sub _count_subq_rs 
  
    my $sub_attrs = { %$attrs };
    # extra selectors do not go in the subquery and there is no point of ordering it, nor locking it
 -  delete @{$sub_attrs}{qw/collapse columns as select _prefetch_selector_range order_by for/};
 +  delete @{$sub_attrs}{qw/collapse columns as select _related_results_construction order_by for/};
  
    # if we multi-prefetch we group_by something unique, as this is what we would
    # get out of the rs via ->next/->all. We *DO WANT* to clobber old group_by regardless
 -  if ( keys %{$attrs->{collapse}}  ) {
 +  if ( $attrs->{collapse}  ) {
      $sub_attrs->{group_by} = [ map { "$attrs->{alias}.$_" } @{
        $rsrc->_identifying_column_set || $self->throw_exception(
          'Unable to construct a unique group_by criteria properly collapsing the '
@@@ -1758,22 -1683,33 +1763,22 @@@ Returns all elements in the resultset
  sub all {
    my $self = shift;
    if(@_) {
 -      $self->throw_exception("all() doesn't take any arguments, you probably wanted ->search(...)->all()");
 +    $self->throw_exception("all() doesn't take any arguments, you probably wanted ->search(...)->all()");
    }
  
 -  return @{ $self->get_cache } if $self->get_cache;
 -
 -  my @obj;
 -
 -  if (keys %{$self->_resolved_attrs->{collapse}}) {
 -    # Using $self->cursor->all is really just an optimisation.
 -    # If we're collapsing has_many prefetches it probably makes
 -    # very little difference, and this is cleaner than hacking
 -    # _construct_object to survive the approach
 -    $self->cursor->reset;
 -    my @row = $self->cursor->next;
 -    while (@row) {
 -      push(@obj, $self->_construct_object(@row));
 -      @row = (exists $self->{stashed_row}
 -               ? @{delete $self->{stashed_row}}
 -               : $self->cursor->next);
 -    }
 -  } else {
 -    @obj = map { $self->_construct_object(@$_) } $self->cursor->all;
 +  delete @{$self}{qw/_stashed_rows _stashed_results/};
 +
 +  if (my $c = $self->get_cache) {
 +    return @$c;
    }
  
 -  $self->set_cache(\@obj) if $self->{attrs}{cache};
 +  $self->cursor->reset;
  
 -  return @obj;
 +  my $objs = $self->_construct_results('fetch_all') || [];
 +
 +  $self->set_cache($objs) if $self->{attrs}{cache};
 +
 +  return @$objs;
  }
  
  =head2 reset
@@@ -1794,8 -1730,6 +1799,8 @@@ another query
  
  sub reset {
    my ($self) = @_;
 +
 +  delete @{$self}{qw/_stashed_rows _stashed_results/};
    $self->{all_cache_position} = 0;
    $self->cursor->reset;
    return $self;
@@@ -1836,7 -1770,7 +1841,7 @@@ sub _rs_update_delete 
    my $attrs = { %{$self->_resolved_attrs} };
  
    my $join_classifications;
 -  my $existing_group_by = delete $attrs->{group_by};
 +  my ($existing_group_by) = delete @{$attrs}{qw(group_by _grouped_by_distinct)};
  
    # do we need a subquery for any reason?
    my $needs_subq = (
      );
  
      # make a new $rs selecting only the PKs (that's all we really need for the subq)
 -    delete $attrs->{$_} for qw/collapse _collapse_order_by select _prefetch_selector_range as/;
 +    delete $attrs->{$_} for qw/select as collapse _related_results_construction/;
      $attrs->{columns} = [ map { "$attrs->{alias}.$_" } @$idcols ];
      $attrs->{group_by} = \ '';  # FIXME - this is an evil hack, it causes the optimiser to kick in and throw away the LEFT joins
      my $subrs = (ref $self)->new($rsrc, $attrs);
@@@ -2333,7 -2267,7 +2338,7 @@@ sub pager 
    # throw away the paging flags and re-run the count (possibly
    # with a subselect) to get the real total count
    my $count_attrs = { %$attrs };
 -  delete $count_attrs->{$_} for qw/rows offset page pager/;
 +  delete @{$count_attrs}{qw/rows offset page pager/};
  
    my $total_rs = (ref $self)->new($self->result_source, $count_attrs);
  
@@@ -2606,16 -2540,9 +2611,9 @@@ sub as_query 
  
    my $attrs = { %{ $self->_resolved_attrs } };
  
-   # For future use:
-   #
-   # in list ctx:
-   # my ($sql, \@bind, \%dbi_bind_attrs) = _select_args_to_query (...)
-   # $sql also has no wrapping parenthesis in list ctx
-   #
-   my $sqlbind = $self->result_source->storage
-     ->_select_args_to_query ($attrs->{from}, $attrs->{select}, $attrs->{where}, $attrs);
-   return $sqlbind;
+   $self->result_source->storage->_select_args_to_query (
+     $attrs->{from}, $attrs->{select}, $attrs->{where}, $attrs
+   );
  }
  
  =head2 find_or_new
@@@ -3087,6 -3014,7 +3085,6 @@@ Returns a related resultset for the sup
  sub related_resultset {
    my ($self, $rel) = @_;
  
 -  $self->{related_resultsets} ||= {};
    return $self->{related_resultsets}{$rel} ||= do {
      my $rsrc = $self->result_source;
      my $rel_info = $rsrc->relationship_info($rel);
      #XXX - temp fix for result_class bug. There likely is a more elegant fix -groditi
      delete @{$attrs}{qw(result_class alias)};
  
 -    my $new_cache;
 +    my $related_cache;
  
      if (my $cache = $self->get_cache) {
 -      if ($cache->[0] && $cache->[0]->related_resultset($rel)->get_cache) {
 -        $new_cache = [ map { @{$_->related_resultset($rel)->get_cache} }
 -                        @$cache ];
 -      }
 +      $related_cache = [ map
 +        { @{$_->related_resultset($rel)->get_cache||[]} }
 +        @$cache
 +      ];
      }
  
      my $rel_source = $rsrc->related_source($rel);
                         where => $attrs->{where},
                     });
      };
 -    $new->set_cache($new_cache) if $new_cache;
 +    $new->set_cache($related_cache) if $related_cache;
      $new;
    };
  }
@@@ -3282,7 -3210,7 +3280,7 @@@ sub _chain_relationship 
    # ->_resolve_join as otherwise they get lost - captainL
    my $join = $self->_merge_joinpref_attr( $attrs->{join}, $attrs->{prefetch} );
  
 -  delete @{$attrs}{qw/join prefetch collapse group_by distinct select as columns +select +as +columns/};
 +  delete @{$attrs}{qw/join prefetch collapse group_by distinct _grouped_by_distinct select as columns +select +as +columns/};
  
    my $seen = { %{ (delete $attrs->{seen_join}) || {} } };
  
@@@ -3412,10 -3340,14 +3410,10 @@@ sub _resolved_attrs 
      if $attrs->{select};
  
    # assume all unqualified selectors to apply to the current alias (legacy stuff)
 -  for (@sel) {
 -    $_ = (ref $_ or $_ =~ /\./) ? $_ : "$alias.$_";
 -  }
 +  $_ = (ref $_ or $_ =~ /\./) ? $_ : "$alias.$_" for @sel;
  
 -  # disqualify all $alias.col as-bits (collapser mandated)
 -  for (@as) {
 -    $_ = ($_ =~ /^\Q$alias.\E(.+)$/) ? $1 : $_;
 -  }
 +  # disqualify all $alias.col as-bits (inflate-map mandated)
 +  $_ = ($_ =~ /^\Q$alias.\E(.+)$/) ? $1 : $_ for @as;
  
    # de-duplicate the result (remove *identical* select/as pairs)
    # and also die on duplicate {as} pointing to different {select}s
        carp_unique ("Useless use of distinct on a grouped resultset ('distinct' is ignored when a 'group_by' is present)");
      }
      else {
 +      $attrs->{_grouped_by_distinct} = 1;
        # distinct affects only the main selection part, not what prefetch may
        # add below.
        $attrs->{group_by} = $source->storage->_group_over_selection (
      }
    }
  
 -  $attrs->{collapse} ||= {};
 -  if ($attrs->{prefetch}) {
 +  # generate selections based on the prefetch helper
 +  my $prefetch;
 +  $prefetch = $self->_merge_joinpref_attr( {}, delete $attrs->{prefetch} )
 +    if defined $attrs->{prefetch};
 +
 +  if ($prefetch) {
  
      $self->throw_exception("Unable to prefetch, resultset contains an unnamed selector $attrs->{_dark_selector}{string}")
        if $attrs->{_dark_selector};
  
 -    my $prefetch = $self->_merge_joinpref_attr( {}, delete $attrs->{prefetch} );
 -
 -    my $prefetch_ordering = [];
 +    $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
        }
      }
  
 -    my @prefetch =
 -      $source->_resolve_prefetch( $prefetch, $alias, $join_map, $prefetch_ordering, $attrs->{collapse} );
 -
 -    # we need to somehow mark which columns came from prefetch
 -    if (@prefetch) {
 -      my $sel_end = $#{$attrs->{select}};
 -      $attrs->{_prefetch_selector_range} = [ $sel_end + 1, $sel_end + @prefetch ];
 -    }
 +    my @prefetch = $source->_resolve_prefetch( $prefetch, $alias, $join_map );
  
      push @{ $attrs->{select} }, (map { $_->[0] } @prefetch);
      push @{ $attrs->{as} }, (map { $_->[1] } @prefetch);
 +  }
  
 -    push( @{$attrs->{order_by}}, @$prefetch_ordering );
 -    $attrs->{_collapse_order_by} = \@$prefetch_ordering;
 +  if ( List::Util::first { $_ =~ /\./ } @{$attrs->{as}} ) {
 +    $attrs->{_related_results_construction} = 1;
 +  }
 +  else {
 +    $attrs->{collapse} = 0;
 +  }
 +
 +  # run through the resulting joinstructure (starting from our current slot)
 +  # and unset collapse if proven unnesessary
 +  #
 +  # also while we are at it find out if the current root source has
 +  # been premultiplied by previous related_source chaining
 +  #
 +  # this allows to predict whether a root object with all other relation
 +  # data set to NULL is in fact unique
 +  if ($attrs->{collapse}) {
 +
 +    if (ref $attrs->{from} eq 'ARRAY') {
 +
 +      if (@{$attrs->{from}} <= 1) {
 +        # no joins - no collapse
 +        $attrs->{collapse} = 0;
 +      }
 +      else {
 +        # find where our table-spec starts
 +        my @fromlist = @{$attrs->{from}};
 +        while (@fromlist) {
 +          my $t = shift @fromlist;
 +
 +          my $is_multi;
 +          # me vs join from-spec distinction - a ref means non-root
 +          if (ref $t eq 'ARRAY') {
 +            $t = $t->[0];
 +            $is_multi ||= ! $t->{-is_single};
 +          }
 +          last if ($t->{-alias} && $t->{-alias} eq $alias);
 +          $attrs->{_main_source_premultiplied} ||= $is_multi;
 +        }
 +
 +        # no non-singles remaining, nor any premultiplication - nothing to collapse
 +        if (
 +          ! $attrs->{_main_source_premultiplied}
 +            and
 +          ! List::Util::first { ! $_->[0]{-is_single} } @fromlist
 +        ) {
 +          $attrs->{collapse} = 0;
 +        }
 +      }
 +    }
 +
 +    else {
 +      # if we can not analyze the from - err on the side of safety
 +      $attrs->{_main_source_premultiplied} = 1;
 +    }
    }
  
    # if both page and offset are specified, produce a combined offset
@@@ -3722,7 -3605,7 +3720,7 @@@ sub _merge_joinpref_attr 
      $seen_keys->{$import_key} = 1; # don't merge the same key twice
    }
  
 -  return $orig;
 +  return @$orig ? $orig : ();
  }
  
  {
@@@ -3818,8 -3701,7 +3816,8 @@@ sub STORABLE_freeze 
    my $to_serialize = { %$self };
  
    # A cursor in progress can't be serialized (and would make little sense anyway)
 -  delete $to_serialize->{cursor};
 +  # the parser can be regenerated (and can't be serialized)
 +  delete @{$to_serialize}{qw/cursor _row_parser _result_inflator/};
  
    # nor is it sensical to store a not-yet-fired-count pager
    if ($to_serialize->{pager} and ref $to_serialize->{pager}{total_entries} eq 'CODE') {
@@@ -3856,10 -3738,6 +3854,10 @@@ sub throw_exception 
    }
  }
  
 +1;
 +
 +__END__
 +
  # XXX: FIXME: Attributes docs need clearing up
  
  =head1 ATTRIBUTES
@@@ -3909,7 -3787,7 +3907,7 @@@ syntax as outlined above
  
  =over 4
  
 -=item Value: \@columns
 +=item Value: \@columns | \%columns | $column
  
  =back
  
@@@ -3919,7 -3797,7 +3917,7 @@@ case the key is the C<as> value, and th
  expression). Adds C<me.> 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.)
+ earlier versions of DBIC, but this is deprecated.)
  
  Essentially C<columns> does the same as L</select> and L</as>.
  
@@@ -3938,10 -3816,10 +3936,10 @@@ is the same a
  
  =back
  
- Indicates additional columns to be selected from storage. Works the same
- as L</columns> but adds columns to the selection. (You may also use the
- C<include_columns> attribute, as in earlier versions of DBIC). For
- example:-
+ Indicates additional columns to be selected from storage. Works the same as
+ L</columns> but adds columns to the selection. (You may also use the
+ C<include_columns> attribute, as in earlier versions of DBIC, but this is
+ deprecated). For example:-
  
    $schema->resultset('CD')->search(undef, {
      '+columns' => ['artist.name'],
@@@ -4011,6 -3889,14 +4009,6 @@@ an explicit list
  
  =back
  
 -=head2 +as
 -
 -=over 4
 -
 -Indicates additional column names for those added via L</+select>. See L</as>.
 -
 -=back
 -
  =head2 as
  
  =over 4
@@@ -4053,14 -3939,6 +4051,14 @@@ use C<get_column> instead
  You can create your own accessors if required - see
  L<DBIx::Class::Manual::Cookbook> for details.
  
 +=head2 +as
 +
 +=over 4
 +
 +Indicates additional column names for those added via L</+select>. See L</as>.
 +
 +=back
 +
  =head2 join
  
  =over 4
@@@ -4124,7 -4002,7 +4122,7 @@@ similarly for a third time). For e.g
  will return a set of all artists that have both a cd with title 'Down
  to Earth' and a cd with title 'Popular'.
  
 -If you want to fetch related objects from other tables as well, see C<prefetch>
 +If you want to fetch related objects from other tables as well, see L</prefetch>
  below.
  
   NOTE: An internal join-chain pruner will discard certain joins while
  
  For more help on using joins with search, see L<DBIx::Class::Manual::Joining>.
  
 -=head2 prefetch
 +=head2 collapse
  
  =over 4
  
 -=item Value: ($rel_name | \@rel_names | \%rel_names)
 +=item Value: (0 | 1)
  
  =back
  
 -Contains one or more relationships that should be fetched along with
 -the main query (when they are accessed afterwards the data will
 -already be available, without extra queries to the database).  This is
 -useful for when you know you will need the related objects, because it
 -saves at least one query:
 -
 -  my $rs = $schema->resultset('Tag')->search(
 -    undef,
 -    {
 -      prefetch => {
 -        cd => 'artist'
 -      }
 -    }
 -  );
 -
 -The initial search results in SQL like the following:
 -
 -  SELECT tag.*, cd.*, artist.* FROM tag
 -  JOIN cd ON tag.cd = cd.cdid
 -  JOIN artist ON cd.artist = artist.artistid
 -
 -L<DBIx::Class> has no need to go back to the database when we access the
 -C<cd> or C<artist> relationships, which saves us two SQL statements in this
 -case.
 -
 -Simple prefetches will be joined automatically, so there is no need
 -for a C<join> attribute in the above search.
 -
 -L</prefetch> can be used with the any of the relationship types and
 -multiple prefetches can be specified together. Below is a more complex
 -example that prefetches a CD's artist, its liner notes (if present),
 -the cover image, the tracks on that cd, and the guests on those
 -tracks.
 -
 - # Assuming:
 - My::Schema::CD->belongs_to( artist      => 'My::Schema::Artist'     );
 - My::Schema::CD->might_have( liner_note  => 'My::Schema::LinerNotes' );
 - My::Schema::CD->has_one(    cover_image => 'My::Schema::Artwork'    );
 - My::Schema::CD->has_many(   tracks      => 'My::Schema::Track'      );
 -
 - My::Schema::Artist->belongs_to( record_label => 'My::Schema::RecordLabel' );
 -
 - My::Schema::Track->has_many( guests => 'My::Schema::Guest' );
 -
 -
 - my $rs = $schema->resultset('CD')->search(
 -   undef,
 -   {
 -     prefetch => [
 -       { artist => 'record_label'},  # belongs_to => belongs_to
 -       'liner_note',                 # might_have
 -       'cover_image',                # has_one
 -       { tracks => 'guests' },       # has_many => has_many
 -     ]
 -   }
 - );
 -
 -This will produce SQL like the following:
 -
 - SELECT cd.*, artist.*, record_label.*, liner_note.*, cover_image.*,
 -        tracks.*, guests.*
 -   FROM cd me
 -   JOIN artist artist
 -     ON artist.artistid = me.artistid
 -   JOIN record_label record_label
 -     ON record_label.labelid = artist.labelid
 -   LEFT JOIN track tracks
 -     ON tracks.cdid = me.cdid
 -   LEFT JOIN guest guests
 -     ON guests.trackid = track.trackid
 -   LEFT JOIN liner_notes liner_note
 -     ON liner_note.cdid = me.cdid
 -   JOIN cd_artwork cover_image
 -     ON cover_image.cdid = me.cdid
 - ORDER BY tracks.cd
 -
 -Now the C<artist>, C<record_label>, C<liner_note>, C<cover_image>,
 -C<tracks>, and C<guests> of the CD will all be available through the
 -relationship accessors without the need for additional queries to the
 -database.
 -
 -However, there is one caveat to be observed: it can be dangerous to
 -prefetch more than one L<has_many|DBIx::Class::Relationship/has_many>
 -relationship on a given level. e.g.:
 -
 - my $rs = $schema->resultset('CD')->search(
 -   undef,
 -   {
 -     prefetch => [
 -       'tracks',                         # has_many
 -       { cd_to_producer => 'producer' }, # has_many => belongs_to (i.e. m2m)
 -     ]
 -   }
 - );
 -
 -The collapser currently can't identify duplicate tuples for multiple
 -L<has_many|DBIx::Class::Relationship/has_many> relationships and as a
 -result the second L<has_many|DBIx::Class::Relationship/has_many>
 -relation could contain redundant objects.
 +When set to a true value, indicates that any rows fetched from joined has_many
 +relationships are to be aggregated into the corresponding "parent" object. For
 +example, the resultset:
  
 -=head3 Using L</prefetch> with L</join>
 +  my $rs = $schema->resultset('CD')->search({}, {
 +    '+columns' => [ qw/ tracks.title tracks.position / ],
 +    join => 'tracks',
 +    collapse => 1,
 +  });
  
 -L</prefetch> implies a L</join> with the equivalent argument, and is
 -properly merged with any existing L</join> specification. So the
 -following:
 +While executing the following query:
  
 -  my $rs = $schema->resultset('CD')->search(
 -   {'record_label.name' => 'Music Product Ltd.'},
 -   {
 -     join     => {artist => 'record_label'},
 -     prefetch => 'artist',
 -   }
 - );
 +  SELECT me.*, tracks.title, tracks.position
 +    FROM cd me
 +    LEFT JOIN track tracks
 +      ON tracks.cdid = me.cdid
  
 -... will work, searching on the record label's name, but only
 -prefetching the C<artist>.
 +Will return only as many objects as there are rows in the CD source, even
 +though the result of the query may span many rows. Each of these CD objects
 +will in turn have multiple "Track" objects hidden behind the has_many
 +generated accessor C<tracks>. Without C<< collapse => 1 >>, the return values
 +of this resultset would be as many CD objects as there are tracks (a "Cartesian
 +product"), with each CD object containing exactly one of all fetched Track data.
  
 -=head3 Using L</prefetch> with L</select> / L</+select> / L</as> / L</+as>
 +When a collapse is requested on a non-ordered resultset, an order by some
 +unique part of the main source (the left-most table) is inserted automatically.
 +This is done so that the resultset is allowed to be "lazy" - calling
 +L<< $rs->next|/next >> will fetch only as many rows as it needs to build the next
 +object with all of its related data.
  
 -L</prefetch> implies a L</+select>/L</+as> with the fields of the
 -prefetched relations.  So given:
 +If an L</order_by> is already declared, and orders the resultset in a way that
 +makes collapsing as described above impossible (e.g. C<< ORDER BY
 +has_many_rel.column >> or C<ORDER BY RANDOM()>), DBIC will automatically
 +switch to "eager" mode and slurp the entire resultset before consturcting the
 +first object returned by L</next>.
  
 -  my $rs = $schema->resultset('CD')->search(
 -   undef,
 -   {
 -     select   => ['cd.title'],
 -     as       => ['cd_title'],
 -     prefetch => 'artist',
 -   }
 - );
 +Setting this attribute on a resultset that does not join any has_many
 +relations is a no-op.
  
 -The L</select> becomes: C<'cd.title', 'artist.*'> and the L</as>
 -becomes: C<'cd_title', 'artist.*'>.
 -
 -=head3 CAVEATS
 +For a more in-depth discussion, see L</PREFETCHING>.
  
 -Prefetch does a lot of deep magic. As such, it may not behave exactly
 -as you might expect.
 +=head2 prefetch
  
  =over 4
  
 -=item *
 -
 -Prefetch uses the L</cache> to populate the prefetched relationships. This
 -may or may not be what you want.
 +=item Value: ($rel_name | \@rel_names | \%rel_names)
  
 -=item *
 +=back
  
 -If you specify a condition on a prefetched relationship, ONLY those
 -rows that match the prefetched condition will be fetched into that relationship.
 -This means that adding prefetch to a search() B<may alter> what is returned by
 -traversing a relationship. So, if you have C<< Artist->has_many(CDs) >> and you do
 +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:
  
 -  my $artist_rs = $schema->resultset('Artist')->search({
 -      'cds.year' => 2008,
 -  }, {
 -      join => 'cds',
 +  my $rs = $schema->resultset('Artist')->search({}, {
 +    prefetch => { cds => ['genre', 'tracks' ] },
    });
  
 -  my $count = $artist_rs->first->cds->count;
 +and
  
 -  my $artist_rs_prefetch = $artist_rs->search( {}, { prefetch => 'cds' } );
 +  my $rs = $schema->resultset('Artist')->search({}, {
 +    join => { cds => ['genre', 'tracks' ] },
 +    collapse => 1,
 +    '+columns' => [
 +      (map
 +        { +{ "cds.$_" => "cds.$_" } }
 +        $schema->source('Artist')->related_source('cds')->columns
 +      ),
 +      (map
 +        { +{ "cds.genre.$_" => "genre.$_" } }
 +        $schema->source('Artist')->related_source('cds')->related_source('genre')->columns
 +      ),
 +      (map
 +        { +{ "cds.tracks.$_" => "tracks.$_" } }
 +        $schema->source('Artist')->related_source('cds')->related_source('tracks')->columns
 +      ),
 +    ],
 +  });
  
 -  my $prefetch_count = $artist_rs_prefetch->first->cds->count;
 +Both producing the following SQL:
 +
 +  SELECT  me.artistid, me.name, me.rank, me.charfield,
 +          cds.cdid, cds.artist, cds.title, cds.year, cds.genreid, cds.single_track,
 +          genre.genreid, genre.name,
 +          tracks.trackid, tracks.cd, tracks.position, tracks.title, tracks.last_updated_on, tracks.last_updated_at
 +    FROM artist me
 +    LEFT JOIN cd cds
 +      ON cds.artist = me.artistid
 +    LEFT JOIN genre genre
 +      ON genre.genreid = cds.genreid
 +    LEFT JOIN track tracks
 +      ON tracks.cd = cds.cdid
 +  ORDER BY me.artistid
 +
 +While L</prefetch> implies a L</join>, it is ok to mix the two together, as
 +the arguments are properly merged and generally do the right thing. For
 +example, you may want to do the following:
 +
 +  my $artists_and_cds_without_genre = $schema->resultset('Artist')->search(
 +    { 'genre.genreid' => undef },
 +    {
 +      join => { cds => 'genre' },
 +      prefetch => 'cds',
 +    }
 +  );
  
 -  cmp_ok( $count, '==', $prefetch_count, "Counts should be the same" );
 +Which generates the following SQL:
  
 -that cmp_ok() may or may not pass depending on the datasets involved. This
 -behavior may or may not survive the 0.09 transition.
 +  SELECT  me.artistid, me.name, me.rank, me.charfield,
 +          cds.cdid, cds.artist, cds.title, cds.year, cds.genreid, cds.single_track
 +    FROM artist me
 +    LEFT JOIN cd cds
 +      ON cds.artist = me.artistid
 +    LEFT JOIN genre genre
 +      ON genre.genreid = cds.genreid
 +  WHERE genre.genreid IS NULL
 +  ORDER BY me.artistid
  
 -=back
 +For a more in-depth discussion, see L</PREFETCHING>.
  
  =head2 alias
  
@@@ -4439,131 -4369,6 +4437,131 @@@ Set to 'update' for a SELECT ... FOR UP
  ... FOR SHARED. If \$scalar is passed, this is taken directly and embedded in the
  query.
  
 +=head1 PREFETCHING
 +
 +DBIx::Class supports arbitrary related data prefetching from multiple related
 +sources. Any combination of relationship types and column sets are supported.
 +If L<collapsing|/collapse> is requested, there is an additional requirement of
 +selecting enough data to make every individual object uniquely identifiable.
 +
 +Here are some more involved examples, based on the following relationship map:
 +
 +  # Assuming:
 +  My::Schema::CD->belongs_to( artist      => 'My::Schema::Artist'     );
 +  My::Schema::CD->might_have( liner_note  => 'My::Schema::LinerNotes' );
 +  My::Schema::CD->has_many(   tracks      => 'My::Schema::Track'      );
 +
 +  My::Schema::Artist->belongs_to( record_label => 'My::Schema::RecordLabel' );
 +
 +  My::Schema::Track->has_many( guests => 'My::Schema::Guest' );
 +
 +
 +
 +  my $rs = $schema->resultset('Tag')->search(
 +    undef,
 +    {
 +      prefetch => {
 +        cd => 'artist'
 +      }
 +    }
 +  );
 +
 +The initial search results in SQL like the following:
 +
 +  SELECT tag.*, cd.*, artist.* FROM tag
 +  JOIN cd ON tag.cd = cd.cdid
 +  JOIN artist ON cd.artist = artist.artistid
 +
 +L<DBIx::Class> has no need to go back to the database when we access the
 +C<cd> or C<artist> relationships, which saves us two SQL statements in this
 +case.
 +
 +Simple prefetches will be joined automatically, so there is no need
 +for a C<join> attribute in the above search.
 +
 +The L</prefetch> attribute can be used with any of the relationship types
 +and multiple prefetches can be specified together. Below is a more complex
 +example that prefetches a CD's artist, its liner notes (if present),
 +the cover image, the tracks on that CD, and the guests on those
 +tracks.
 +
 +  my $rs = $schema->resultset('CD')->search(
 +    undef,
 +    {
 +      prefetch => [
 +        { artist => 'record_label'},  # belongs_to => belongs_to
 +        'liner_note',                 # might_have
 +        'cover_image',                # has_one
 +        { tracks => 'guests' },       # has_many => has_many
 +      ]
 +    }
 +  );
 +
 +This will produce SQL like the following:
 +
 +  SELECT cd.*, artist.*, record_label.*, liner_note.*, cover_image.*,
 +         tracks.*, guests.*
 +    FROM cd me
 +    JOIN artist artist
 +      ON artist.artistid = me.artistid
 +    JOIN record_label record_label
 +      ON record_label.labelid = artist.labelid
 +    LEFT JOIN track tracks
 +      ON tracks.cdid = me.cdid
 +    LEFT JOIN guest guests
 +      ON guests.trackid = track.trackid
 +    LEFT JOIN liner_notes liner_note
 +      ON liner_note.cdid = me.cdid
 +    JOIN cd_artwork cover_image
 +      ON cover_image.cdid = me.cdid
 +  ORDER BY tracks.cd
 +
 +Now the C<artist>, C<record_label>, C<liner_note>, C<cover_image>,
 +C<tracks>, and C<guests> of the CD will all be available through the
 +relationship accessors without the need for additional queries to the
 +database.
 +
 +=head3 CAVEATS
 +
 +Prefetch does a lot of deep magic. As such, it may not behave exactly
 +as you might expect.
 +
 +=over 4
 +
 +=item *
 +
 +Prefetch uses the L</cache> to populate the prefetched relationships. This
 +may or may not be what you want.
 +
 +=item *
 +
 +If you specify a condition on a prefetched relationship, ONLY those
 +rows that match the prefetched condition will be fetched into that relationship.
 +This means that adding prefetch to a search() B<may alter> what is returned by
 +traversing a relationship. So, if you have C<< Artist->has_many(CDs) >> and you do
 +
 +  my $artist_rs = $schema->resultset('Artist')->search({
 +      'cds.year' => 2008,
 +  }, {
 +      join => 'cds',
 +  });
 +
 +  my $count = $artist_rs->first->cds->count;
 +
 +  my $artist_rs_prefetch = $artist_rs->search( {}, { prefetch => 'cds' } );
 +
 +  my $prefetch_count = $artist_rs_prefetch->first->cds->count;
 +
 +  cmp_ok( $count, '==', $prefetch_count, "Counts should be the same" );
 +
 +That cmp_ok() may or may not pass depending on the datasets involved. In other
 +words the C<WHERE> condition would apply to the entire dataset, just like
 +it would in regular SQL. If you want to add a condition only to the "right side"
 +of a C<LEFT JOIN> - consider declaring and using a L<relationship with a custom
 +condition|DBIx::Class::Relationship::Base/condition>
 +
 +=back
 +
  =head1 DBIC BIND VALUES
  
  Because DBIC may need more information to bind values than just the column name
@@@ -4620,3 -4425,6 +4618,3 @@@ See L<AUTHOR|DBIx::Class/AUTHOR> and L<
  
  You may distribute this code under the same terms as Perl itself.
  
 -=cut
 -
 -1;
diff --combined lib/DBIx/Class/Row.pm
@@@ -22,8 -22,6 +22,8 @@@ BEGIN 
  
  use namespace::clean;
  
 +__PACKAGE__->mk_group_accessors ( simple => [ in_storage => '_in_storage' ] );
 +
  =head1 NAME
  
  DBIx::Class::Row - Basic row methods
@@@ -134,16 -132,16 +134,16 @@@ sub __new_related_find_or_new_helper 
    my $proc_data = { $new_rel_obj->get_columns };
  
    if ($self->__their_pk_needs_us($relname)) {
-     MULTICREATE_DEBUG and warn "MC $self constructing $relname via new_result";
+     MULTICREATE_DEBUG and print STDERR "MC $self constructing $relname via new_result\n";
      return $new_rel_obj;
    }
    elsif ($rsrc->_pk_depends_on($relname, $proc_data )) {
      if (! keys %$proc_data) {
        # there is nothing to search for - blind create
-       MULTICREATE_DEBUG and warn "MC $self constructing default-insert $relname";
+       MULTICREATE_DEBUG and print STDERR "MC $self constructing default-insert $relname\n";
      }
      else {
-       MULTICREATE_DEBUG and warn "MC $self constructing $relname via find_or_new";
+       MULTICREATE_DEBUG and print STDERR "MC $self constructing $relname via find_or_new\n";
        # this is not *really* find or new, as we don't want to double-new the
        # data (thus potentially double encoding or whatever)
        my $exists = $rel_rs->find ($proc_data);
@@@ -178,7 -176,7 +178,7 @@@ sub new 
    my ($class, $attrs) = @_;
    $class = ref $class if ref $class;
  
 -  my $new = bless { _column_data => {} }, $class;
 +  my $new = bless { _column_data => {}, _in_storage => 0 }, $class;
  
    if ($attrs) {
      $new->throw_exception("attrs must be a hashref")
              $new->{_rel_in_storage}{$key} = 1;
              $new->set_from_related($key, $rel_obj);
            } else {
-             MULTICREATE_DEBUG and warn "MC $new uninserted $key $rel_obj\n";
+             MULTICREATE_DEBUG and print STDERR "MC $new uninserted $key $rel_obj\n";
            }
  
            $related->{$key} = $rel_obj;
                $rel_obj->throw_exception ('A multi relationship can not be pre-existing when doing multicreate. Something went wrong');
              } else {
                MULTICREATE_DEBUG and
-                 warn "MC $new uninserted $key $rel_obj (${\($idx+1)} of $total)\n";
+                 print STDERR "MC $new uninserted $key $rel_obj (${\($idx+1)} of $total)\n";
              }
              push(@objects, $rel_obj);
            }
              $new->{_rel_in_storage}{$key} = 1;
            }
            else {
-             MULTICREATE_DEBUG and warn "MC $new uninserted $key $rel_obj";
+             MULTICREATE_DEBUG and print STDERR "MC $new uninserted $key $rel_obj\n";
            }
            $inflated->{$key} = $rel_obj;
            next;
@@@ -363,7 -361,7 +363,7 @@@ sub insert 
        # The guard will save us if we blow out of this scope via die
        $rollback_guard ||= $storage->txn_scope_guard;
  
-       MULTICREATE_DEBUG and warn "MC $self pre-reconstructing $relname $rel_obj\n";
+       MULTICREATE_DEBUG and print STDERR "MC $self pre-reconstructing $relname $rel_obj\n";
  
        my $them = { %{$rel_obj->{_relationship_data} || {} }, $rel_obj->get_columns };
        my $existing;
  
    MULTICREATE_DEBUG and do {
      no warnings 'uninitialized';
-     warn "MC $self inserting (".join(', ', $self->get_columns).")\n";
+     print STDERR "MC $self inserting (".join(', ', $self->get_columns).")\n";
    };
  
    # perform the insert - the storage will return everything it is asked to
          $obj->set_from_related($_, $self) for keys %$reverse;
          if ($self->__their_pk_needs_us($relname)) {
            if (exists $self->{_ignore_at_insert}{$relname}) {
-             MULTICREATE_DEBUG and warn "MC $self skipping post-insert on $relname";
+             MULTICREATE_DEBUG and print STDERR "MC $self skipping post-insert on $relname\n";
            }
            else {
-             MULTICREATE_DEBUG and warn "MC $self inserting $relname $obj";
+             MULTICREATE_DEBUG and print STDERR "MC $self inserting $relname $obj\n";
              $obj->insert;
            }
          } else {
-           MULTICREATE_DEBUG and warn "MC $self post-inserting $obj";
+           MULTICREATE_DEBUG and print STDERR "MC $self post-inserting $obj\n";
            $obj->insert();
          }
        }
@@@ -482,6 -480,13 +482,6 @@@ are used
  Creating a result object using L<DBIx::Class::ResultSet/new_result>, or
  calling L</delete> on one, sets it to false.
  
 -=cut
 -
 -sub in_storage {
 -  my ($self, $val) = @_;
 -  $self->{_in_storage} = $val if @_ > 1;
 -  return $self->{_in_storage} ? 1 : 0;
 -}
  
  =head2 update
  
@@@ -614,7 -619,7 +614,7 @@@ sub delete 
      );
  
      delete $self->{_column_data_in_storage};
 -    $self->in_storage(undef);
 +    $self->in_storage(0);
    }
    else {
      my $rsrc = try { $self->result_source_instance }
@@@ -768,7 -773,6 +768,7 @@@ Marks a column as having been changed r
  really changed.
  
  =cut
 +
  sub make_column_dirty {
    my ($self, $column) = @_;
  
@@@ -1177,54 -1181,76 +1177,54 @@@ L<DBIx::Class::ResultSet>, see L<DBIx::
  sub inflate_result {
    my ($class, $source, $me, $prefetch) = @_;
  
 -  $source = $source->resolve
 -    if $source->isa('DBIx::Class::ResultSourceHandle');
 -
    my $new = bless
      { _column_data => $me, _result_source => $source },
      ref $class || $class
    ;
  
 -  foreach my $pre (keys %{$prefetch||{}}) {
 -
 -    my (@pre_vals, $is_multi);
 -    if (ref $prefetch->{$pre}[0] eq 'ARRAY') {
 -      $is_multi = 1;
 -      @pre_vals = @{$prefetch->{$pre}};
 -    }
 -    else {
 -      @pre_vals = $prefetch->{$pre};
 -    }
 +  if ($prefetch) {
 +    for my $pre ( keys %$prefetch ) {
  
 -    my $pre_source = try {
 -      $source->related_source($pre)
 -    }
 -    catch {
 -      $class->throw_exception(sprintf
 -
 -        "Can't inflate manual prefetch into non-existent relationship '%s' from '%s', "
 -      . "check the inflation specification (columns/as) ending in '%s.%s'.",
 -
 -        $pre,
 -        $source->source_name,
 -        $pre,
 -        (keys %{$pre_vals[0][0]})[0] || 'something.something...',
 -      );
 -    };
 +      my @pre_objects;
 +      if (
 +        @{$prefetch->{$pre}||[]}
 +          and
 +        ref($prefetch->{$pre}) ne $DBIx::Class::ResultSource::RowParser::Util::null_branch_class
 +      ) {
 +        my $pre_source = try {
 +          $source->related_source($pre)
 +        } catch {
 +          my $err = sprintf
 +            "Inflation into non-existent relationship '%s' of '%s' requested",
 +            $pre,
 +            $source->source_name,
 +          ;
 +          if (my ($colname) = sort { length($a) <=> length ($b) } keys %{$prefetch->{$pre}[0] || {}} ) {
 +            $err .= sprintf ", check the inflation specification (columns/as) ending in '...%s.%s'",
 +            $pre,
 +            $colname,
 +          }
  
 -    my $accessor = $source->relationship_info($pre)->{attrs}{accessor}
 -      or $class->throw_exception("No accessor type declared for prefetched $pre");
 +          $source->throw_exception($err);
 +        };
  
 -    if (! $is_multi and $accessor eq 'multi') {
 -      $class->throw_exception("Manual prefetch (via select/columns) not supported with accessor 'multi'");
 -    }
 +        @pre_objects = map {
 +          $pre_source->result_class->inflate_result( $pre_source, @$_ )
 +        } ( ref $prefetch->{$pre}[0] eq 'ARRAY' ?  @{$prefetch->{$pre}} : $prefetch->{$pre} );
 +      }
  
 -    my @pre_objects;
 -    for my $me_pref (@pre_vals) {
 -
 -        # FIXME - this should not be necessary
 -        # the collapser currently *could* return bogus elements with all
 -        # columns set to undef
 -        my $has_def;
 -        for (values %{$me_pref->[0]}) {
 -          if (defined $_) {
 -            $has_def++;
 -            last;
 -          }
 -        }
 -        next unless $has_def;
 +      my $accessor = $source->relationship_info($pre)->{attrs}{accessor}
 +        or $class->throw_exception("No accessor type declared for prefetched relationship '$pre'");
  
 -        push @pre_objects, $pre_source->result_class->inflate_result(
 -          $pre_source, @$me_pref
 -        );
 -    }
 +      if ($accessor eq 'single') {
 +        $new->{_relationship_data}{$pre} = $pre_objects[0];
 +      }
 +      elsif ($accessor eq 'filter') {
 +        $new->{_inflated_column}{$pre} = $pre_objects[0];
 +      }
  
 -    if ($accessor eq 'single') {
 -      $new->{_relationship_data}{$pre} = $pre_objects[0];
 -    }
 -    elsif ($accessor eq 'filter') {
 -      $new->{_inflated_column}{$pre} = $pre_objects[0];
 +      $new->related_resultset($pre)->set_cache(\@pre_objects);
      }
 -
 -    $new->related_resultset($pre)->set_cache(\@pre_objects);
    }
  
    $new->in_storage (1);
@@@ -176,6 -176,7 +176,6 @@@ sub new 
    $new->_sql_maker_opts({});
    $new->_dbh_details({});
    $new->{_in_do_block} = 0;
 -  $new->{_dbh_gen} = 0;
  
    # read below to see what this does
    $new->_arm_global_destructor;
      # soon as possible (DBIC will reconnect only on demand from within
      # the thread)
      my @instances = grep { defined $_ } values %seek_and_destroy;
 +    %seek_and_destroy = ();
 +
      for (@instances) {
 -      $_->{_dbh_gen}++;  # so that existing cursors will drop as well
        $_->_dbh(undef);
  
        $_->transaction_depth(0);
        $_->savepoints([]);
 -    }
  
 -    # properly renumber all existing refs
 -    %seek_and_destroy = ();
 -    $_->_arm_global_destructor for @instances;
 +      # properly renumber existing refs
 +      $_->_arm_global_destructor
 +    }
    }
  }
  
@@@ -251,6 -252,7 +251,6 @@@ sub _verify_pid 
    my $pid = $self->_conn_pid;
    if( defined $pid and $pid != $$ and my $dbh = $self->_dbh ) {
      $dbh->{InactiveDestroy} = 1;
 -    $self->{_dbh_gen}++;
      $self->_dbh(undef);
      $self->transaction_depth(0);
      $self->savepoints([]);
@@@ -833,6 -835,7 +833,6 @@@ sub disconnect 
      %{ $self->_dbh->{CachedKids} } = ();
      $self->_dbh->disconnect;
      $self->_dbh(undef);
 -    $self->{_dbh_gen}++;
    }
  }
  
@@@ -1703,22 -1706,63 +1703,68 @@@ sub _execute 
  
    my ($sql, $bind) = $self->_prep_for_execute($op, $ident, \@args);
  
-   shift->dbh_do(    # retry over disconnects
-     '_dbh_execute',
 -  shift->dbh_do( _dbh_execute =>     # retry over disconnects
++  # not even a PID check - we do not care about the state of the _dbh.
++  # All we need is to get the appropriate drivers loaded if they aren't
++  # already so that the assumption in ad7c50fc26e holds
++  $self->_populate_dbh unless $self->_dbh;
++
++  $self->dbh_do( _dbh_execute =>     # retry over disconnects
      $sql,
      $bind,
-     $ident,
+     $self->_dbi_attrs_for_bind($ident, $bind),
    );
  }
  
  sub _dbh_execute {
-   my ($self, undef, $sql, $bind, $ident) = @_;
+   my ($self, $dbh, $sql, $bind, $bind_attrs) = @_;
  
    $self->_query_start( $sql, $bind );
  
-   my $bind_attrs = $self->_dbi_attrs_for_bind($ident, $bind);
+   my $sth = $self->_bind_sth_params(
+     $self->_prepare_sth($dbh, $sql),
+     $bind,
+     $bind_attrs,
+   );
+   # Can this fail without throwing an exception anyways???
+   my $rv = $sth->execute();
+   $self->throw_exception(
+     $sth->errstr || $sth->err || 'Unknown error: execute() returned false, but error flags were not set...'
+   ) if !$rv;
+   $self->_query_end( $sql, $bind );
+   return (wantarray ? ($rv, $sth, @$bind) : $rv);
+ }
+ sub _prepare_sth {
+   my ($self, $dbh, $sql) = @_;
+   # 3 is the if_active parameter which avoids active sth re-use
+   my $sth = $self->disable_sth_caching
+     ? $dbh->prepare($sql)
+     : $dbh->prepare_cached($sql, {}, 3);
+   # XXX You would think RaiseError would make this impossible,
+   #  but apparently that's not true :(
+   $self->throw_exception(
+     $dbh->errstr
+       ||
+     sprintf( "\$dbh->prepare() of '%s' through %s failed *silently* without "
+             .'an exception and/or setting $dbh->errstr',
+       length ($sql) > 20
+         ? substr($sql, 0, 20) . '...'
+         : $sql
+       ,
+       'DBD::' . $dbh->{Driver}{Name},
+     )
+   ) if !$sth;
+   $sth;
+ }
  
-   my $sth = $self->_sth($sql);
+ sub _bind_sth_params {
+   my ($self, $sth, $bind, $bind_attrs) = @_;
  
    for my $i (0 .. $#$bind) {
      if (ref $bind->[$i][1] eq 'SCALAR') {  # any scalarrefs are assumed to be bind_inouts
        );
      }
      else {
+       # FIXME SUBOPTIMAL - most likely this is not necessary at all
+       # confirm with dbi-dev whether explicit stringification is needed
+       my $v = ( length ref $bind->[$i][1] and overload::Method($bind->[$i][1], '""') )
+         ? "$bind->[$i][1]"
+         : $bind->[$i][1]
+       ;
        $sth->bind_param(
          $i + 1,
-         (ref $bind->[$i][1] and overload::Method($bind->[$i][1], '""'))
-           ? "$bind->[$i][1]"
-           : $bind->[$i][1]
-         ,
+         $v,
          $bind_attrs->[$i],
        );
      }
    }
  
-   # Can this fail without throwing an exception anyways???
-   my $rv = $sth->execute();
-   $self->throw_exception(
-     $sth->errstr || $sth->err || 'Unknown error: execute() returned false, but error flags were not set...'
-   ) if !$rv;
-   $self->_query_end( $sql, $bind );
-   return (wantarray ? ($rv, $sth, @$bind) : $rv);
+   $sth;
  }
  
  sub _prefetch_autovalues {
@@@ -1886,14 -1925,15 +1927,15 @@@ sub insert_bulk 
  
    my @col_range = (0..$#$cols);
  
-   # FIXME - perhaps this is not even needed? does DBI stringify?
+   # FIXME SUBOPTIMAL - most likely this is not necessary at all
+   # confirm with dbi-dev whether explicit stringification is needed
    #
    # forcibly stringify whatever is stringifiable
    # ResultSet::populate() hands us a copy - safe to mangle
    for my $r (0 .. $#$data) {
      for my $c (0 .. $#{$data->[$r]}) {
        $data->[$r][$c] = "$data->[$r][$c]"
-         if ( ref $data->[$r][$c] and overload::Method($data->[$r][$c], '""') );
+         if ( length ref $data->[$r][$c] and overload::Method($data->[$r][$c], '""') );
      }
    }
  
    my $guard = $self->txn_scope_guard;
  
    $self->_query_start( $sql, @$proto_bind ? [[undef => '__BULK_INSERT__' ]] : () );
-   my $sth = $self->_sth($sql);
+   my $sth = $self->_prepare_sth($self->_dbh, $sql);
    my $rv = do {
      if (@$proto_bind) {
        # proto bind contains the information on which pieces of $data to pull
@@@ -2243,13 -2283,11 +2285,11 @@@ sub _select_args_to_query 
      $self->_select_args(@_);
  
    # my ($sql, $prepared_bind) = $self->_gen_sql_bind($op, $ident, [ $select, $cond, $rs_attrs, $rows, $offset ]);
-   my ($sql, $prepared_bind) = $self->_gen_sql_bind($op, $ident, \@args);
-   $prepared_bind ||= [];
+   my ($sql, $bind) = $self->_gen_sql_bind($op, $ident, \@args);
  
-   return wantarray
-     ? ($sql, $prepared_bind)
-     : \[ "($sql)", @$prepared_bind ]
-   ;
+   # reuse the bind arrayref
+   unshift @{$bind}, "($sql)";
+   \$bind;
  }
  
  sub _select_args {
      $attrs->{rows} = $sql_maker->__max_int;
    }
  
 -  my @limit;
 +  my ($complex_prefetch, @limit);
  
 -  # see if we need to tear the prefetch apart otherwise delegate the limiting to the
 -  # storage, unless software limit was requested
 +  # see if we will need to tear the prefetch apart to satisfy group_by == select
 +  # this is *extremely tricky* to get right
 +  #
 +  # Follows heavy but necessary analyzis of the group_by - if it refers to any
 +  # sort of non-root column assume the user knows what they are doing and do
 +  # not try to be clever
    if (
 -    #limited has_many
 -    ( $attrs->{rows} && keys %{$attrs->{collapse}} )
 -       ||
 -    # grouped prefetch (to satisfy group_by == select)
 -    ( $attrs->{group_by}
 -        &&
 -      @{$attrs->{group_by}}
 -        &&
 -      $attrs->{_prefetch_selector_range}
 -    )
 +    $attrs->{_related_results_construction}
 +      and
 +    $attrs->{group_by}
 +      and
 +    @{$attrs->{group_by}}
 +      and
 +    my $grp_aliases = try {
 +      $self->_resolve_aliastypes_from_select_args( $attrs->{from}, undef, undef, { group_by => $attrs->{group_by} } )
 +    }
    ) {
 -    ($ident, $select, $where, $attrs)
 -      = $self->_adjust_select_args_for_complex_prefetch ($ident, $select, $where, $attrs);
 +    $complex_prefetch = ! defined first { $_ ne $rs_alias } keys %{ $grp_aliases->{grouping} || {} };
 +  }
 +
 +  $complex_prefetch ||= ( $attrs->{rows} && $attrs->{collapse} );
 +
 +  if ($complex_prefetch) {
 +    ($ident, $select, $where, $attrs) =
 +      $self->_adjust_select_args_for_complex_prefetch ($ident, $select, $where, $attrs);
    }
    elsif (! $attrs->{software_limit} ) {
      push @limit, (
  
    # try to simplify the joinmap further (prune unreferenced type-single joins)
    if (
 +    ! $complex_prefetch
 +      and
      ref $ident
        and
      reftype $ident eq 'ARRAY'
@@@ -2395,42 -2422,6 +2435,6 @@@ see L<DBIx::Class::SQLMaker::LimitDiale
  
  =cut
  
- sub _dbh_sth {
-   my ($self, $dbh, $sql) = @_;
-   # 3 is the if_active parameter which avoids active sth re-use
-   my $sth = $self->disable_sth_caching
-     ? $dbh->prepare($sql)
-     : $dbh->prepare_cached($sql, {}, 3);
-   # XXX You would think RaiseError would make this impossible,
-   #  but apparently that's not true :(
-   $self->throw_exception(
-     $dbh->errstr
-       ||
-     sprintf( "\$dbh->prepare() of '%s' through %s failed *silently* without "
-             .'an exception and/or setting $dbh->errstr',
-       length ($sql) > 20
-         ? substr($sql, 0, 20) . '...'
-         : $sql
-       ,
-       'DBD::' . $dbh->{Driver}{Name},
-     )
-   ) if !$sth;
-   $sth;
- }
- sub sth {
-   carp_unique 'sth was mistakenly marked/documented as public, stop calling it (will be removed before DBIC v0.09)';
-   shift->_sth(@_);
- }
- sub _sth {
-   my ($self, $sql) = @_;
-   $self->dbh_do('_dbh_sth', $sql);  # retry over disconnects
- }
  sub _dbh_columns_info_for {
    my ($self, $dbh, $table) = @_;
  
@@@ -2658,8 -2649,7 +2662,7 @@@ $version in the name with "$preversion-
  See L<SQL::Translator/METHODS> for a list of values for C<\%sqlt_args>.
  The most common value for this would be C<< { add_drop_table => 1 } >>
  to have the SQL produced include a C<DROP TABLE> statement for each table
- created. For quoting purposes supply C<quote_table_names> and
- C<quote_field_names>.
+ created. For quoting purposes supply C<quote_identifiers>.
  
  If no arguments are passed, then the following default values are assumed:
  
diff --combined t/60core.t
@@@ -173,7 -173,7 +173,7 @@@ is_deeply( \@cd, [qw/cdid artist title 
  $cd = $schema->resultset("CD")->search({ title => 'Spoonful of bees' }, { columns => ['title'] })->next;
  is($cd->title, 'Spoonful of bees', 'subset of columns returned correctly');
  
- $cd = $schema->resultset("CD")->search(undef, { include_columns => [ { name => 'artist.name' } ], join => [ 'artist' ] })->find(1);
+ $cd = $schema->resultset("CD")->search(undef, { '+columns' => [ { name => 'artist.name' } ], join => [ 'artist' ] })->find(1);
  
  is($cd->title, 'Spoonful of bees', 'Correct CD returned with include');
  is($cd->get_column('name'), 'Caterwauler McCrae', 'Additional column returned');
@@@ -253,13 -253,11 +253,13 @@@ is ($collapsed_or_rs->all, 4, 'Collapse
  is ($collapsed_or_rs->count, 4, 'Collapsed search count with OR ok');
  
  # make sure sure distinct on a grouped rs is warned about
 -my $cd_rs = $schema->resultset ('CD')
 -              ->search ({}, { distinct => 1, group_by => 'title' });
 -warnings_exist (sub {
 -  $cd_rs->next;
 -}, qr/Useless use of distinct/, 'UUoD warning');
 +{
 +  my $cd_rs = $schema->resultset ('CD')
 +                ->search ({}, { distinct => 1, group_by => 'title' });
 +  warnings_exist (sub {
 +    $cd_rs->next;
 +  }, qr/Useless use of distinct/, 'UUoD warning');
 +}
  
  {
    my $tcount = $schema->resultset('Track')->search(
@@@ -300,16 -298,10 +300,18 @@@ is($or_rs->next->cdid, $rel_rs->next->c
  $or_rs->reset;
  $rel_rs->reset;
  
 +# at this point there should be no active statements
 +# (finish() was called everywhere, either explicitly via
 +# reset() or on DESTROY)
 +for (keys %{$schema->storage->dbh->{CachedKids}}) {
 +  fail("Unreachable cached statement still active: $_")
 +    if $schema->storage->dbh->{CachedKids}{$_}->FETCH('Active');
 +}
 +
  my $tag = $schema->resultset('Tag')->search(
-                [ { 'me.tag' => 'Blue' } ], { cols=>[qw/tagid/] } )->next;
+   [ { 'me.tag' => 'Blue' } ],
+   { columns => 'tagid' }
+ )->next;
  
  ok($tag->has_column_loaded('tagid'), 'Has tagid loaded');
  ok(!$tag->has_column_loaded('tag'), 'Has not tag loaded');
@@@ -39,7 -39,7 +39,7 @@@ my $tests = 
            JOIN owners owner
              ON owner.id = me.owner
          WHERE source != ? AND me.title = ? AND source = ?
 -        GROUP BY avg(me.id / ?)
 +        GROUP BY AVG(me.id / ?), MAX(owner.id)
          HAVING ?
          ORDER BY ? / ?, ?
          LIMIT ?
@@@ -65,6 -65,7 +65,6 @@@
            ) me
            LEFT JOIN books books
              ON books.owner = me.id
 -        ORDER BY books.owner
        )',
        [
          [ { sqlt_datatype => 'integer' } => 3 ],
@@@ -81,7 -82,7 +81,7 @@@
            JOIN owners owner
              ON owner.id = me.owner
          WHERE source != ? AND me.title = ? AND source = ?
 -        GROUP BY avg(me.id / ?)
 +        GROUP BY AVG(me.id / ?), MAX(owner.id)
          HAVING ?
          ORDER BY ? / ?, ?
          LIMIT ?, ?
            ) me
            LEFT JOIN books books
              ON books.owner = me.id
 -        ORDER BY books.owner
        )',
        [
          [ { sqlt_datatype => 'integer' } => 1 ],
            JOIN owners owner
              ON owner.id = me.owner
          WHERE source != ? AND me.title = ? AND source = ?
 -        GROUP BY avg(me.id / ?)
 +        GROUP BY AVG(me.id / ?), MAX(owner.id)
          HAVING ?
          ORDER BY ? / ?, ?
        )',
            ) me
            LEFT JOIN books books
              ON books.owner = me.id
 -        ORDER BY books.owner
        )',
        [
          [ { sqlt_datatype => 'integer' } => 1 ],
            JOIN owners owner
              ON owner.id = me.owner
          WHERE source != ? AND me.title = ? AND source = ?
 -        GROUP BY avg(me.id / ?)
 +        GROUP BY AVG(me.id / ?), MAX(owner.id)
          HAVING ?
          ORDER BY ? / ?, ?
        )',
            ) me
            LEFT JOIN books books
              ON books.owner = me.id
 -        ORDER BY books.owner
        )',
        [
          [ { sqlt_datatype => 'integer' } => 3 ],
                  JOIN owners owner
                    ON owner.id = me.owner
                WHERE source != ? AND me.title = ? AND source = ?
 -              GROUP BY avg(me.id / ?)
 +              GROUP BY AVG(me.id / ?), MAX(owner.id)
                HAVING ?
              ) me
        ) me
                  JOIN owners owner
                    ON owner.id = me.owner
                WHERE source != ? AND me.title = ? AND source = ?
 -              GROUP BY avg(me.id / ?)
 +              GROUP BY AVG(me.id / ?), MAX(owner.id)
                HAVING ?
              ) me
        ) me
              ) me
              LEFT JOIN books books
                ON books.owner = me.id
 -          ORDER BY books.owner
          )',
          [
            [ { sqlt_datatype => 'integer' } => 2 ],
                JOIN owners owner
                  ON owner.id = me.owner
              WHERE source != ? AND me.title = ? AND source = ?
 -            GROUP BY avg(me.id / ?)
 +            GROUP BY AVG(me.id / ?), MAX(owner.id)
              HAVING ?
              %s
            ) me
                      JOIN owners owner
                        ON owner.id = me.owner
                    WHERE source != ? AND me.title = ? AND source = ?
 -                  GROUP BY avg(me.id / ?)
 +                  GROUP BY AVG(me.id / ?), MAX(owner.id)
                    HAVING ?
                  ) me
              ) me
                      JOIN owners owner
                        ON owner.id = me.owner
                    WHERE source != ? AND me.title = ? AND source = ?
 -                  GROUP BY avg(me.id / ?)
 +                  GROUP BY AVG(me.id / ?), MAX(owner.id)
                    HAVING ?
                    ORDER BY ? / ?, ?
                  ) me
              ) me
              LEFT JOIN books books
                ON books.owner = me.id
 -          ORDER BY books.owner
          )',
          [
            [ { sqlt_datatype => 'integer' } => 2 ],
            JOIN owners owner
              ON owner.id = me.owner
          WHERE source != ? AND me.title = ? AND source = ?
 -        GROUP BY avg(me.id / ?)
 +        GROUP BY AVG(me.id / ?), MAX(owner.id)
          HAVING ?
          FETCH FIRST 4 ROWS ONLY
        )',
                JOIN owners owner
                  ON owner.id = me.owner
              WHERE source != ? AND me.title = ? AND source = ?
 -            GROUP BY avg(me.id / ?)
 +            GROUP BY AVG(me.id / ?), MAX(owner.id)
              HAVING ?
              ORDER BY me.id
              FETCH FIRST 7 ROWS ONLY
            JOIN owners owner
              ON owner.id = me.owner
          WHERE source != ? AND me.title = ? AND source = ?
 -        GROUP BY avg(me.id / ?)
 +        GROUP BY AVG(me.id / ?), MAX(owner.id)
          HAVING ?
          ORDER BY ? / ?, ?
          FETCH FIRST 4 ROWS ONLY
                    JOIN owners owner
                      ON owner.id = me.owner
                  WHERE source != ? AND me.title = ? AND source = ?
 -                GROUP BY avg(me.id / ?)
 +                GROUP BY AVG(me.id / ?), MAX(owner.id)
                  HAVING ?
                  ORDER BY ? / ?, ?
                  FETCH FIRST 7 ROWS ONLY
            ) me
            LEFT JOIN books books
              ON books.owner = me.id
 -        ORDER BY books.owner
        )',
        [],
      ],
            JOIN owners owner
              ON owner.id = me.owner
          WHERE source != ? AND me.title = ? AND source = ?
 -        GROUP BY avg(me.id / ?)
 +        GROUP BY AVG(me.id / ?), MAX(owner.id)
          HAVING ?
        )',
        [
                JOIN owners owner
                  ON owner.id = me.owner
              WHERE source != ? AND me.title = ? AND source = ?
 -            GROUP BY avg(me.id / ?)
 +            GROUP BY AVG(me.id / ?), MAX(owner.id)
              HAVING ?
              ORDER BY me.id
            ) me
            JOIN owners owner
              ON owner.id = me.owner
          WHERE source != ? AND me.title = ? AND source = ?
 -        GROUP BY avg(me.id / ?)
 +        GROUP BY AVG(me.id / ?), MAX(owner.id)
          HAVING ?
          ORDER BY ? / ?, ?
        )',
                    JOIN owners owner
                      ON owner.id = me.owner
                  WHERE source != ? AND me.title = ? AND source = ?
 -                GROUP BY avg(me.id / ?)
 +                GROUP BY AVG(me.id / ?), MAX(owner.id)
                  HAVING ?
                  ORDER BY ? / ?, ?
                ) me
            ) me
            LEFT JOIN books books
              ON books.owner = me.id
 -        ORDER BY books.owner
        )',
        [],
      ],
    },
  
-   RowCountOrGenericSubQ => {
-     limit => [
-       '(
-         SET ROWCOUNT 4
-         SELECT me.id, owner.id, owner.name, ? * ?, ?
-           FROM books me
-           JOIN owners owner
-             ON owner.id = me.owner
-         WHERE source != ? AND me.title = ? AND source = ?
-         GROUP BY AVG(me.id / ?), MAX(owner.id)
-         HAVING ?
-         ORDER BY me.id
-         SET ROWCOUNT 0
-       )',
-       [
-         @select_bind,
-         @where_bind,
-         @group_bind,
-         @having_bind,
-       ],
-     ],
-     limit_offset => [
-       '(
-         SELECT me.id, owner__id, owner__name, bar, baz
-           FROM (
-             SELECT me.id, owner.id AS owner__id, owner.name AS owner__name, ? * ? AS bar, ? AS baz
-               FROM books me
-               JOIN owners owner
-                 ON owner.id = me.owner
-             WHERE source != ? AND me.title = ? AND source = ?
-             GROUP BY AVG(me.id / ?), MAX(owner.id)
-             HAVING ?
-           ) me
-         WHERE (
-           SELECT COUNT( * )
-             FROM books rownum__emulation
-           WHERE rownum__emulation.id < me.id
-         ) BETWEEN ? AND ?
-         ORDER BY me.id
-       )',
-       [
-         @select_bind,
-         @where_bind,
-         @group_bind,
-         @having_bind,
-         [ { sqlt_datatype => 'integer' } => 3 ],
-         [ { sqlt_datatype => 'integer' } => 6 ],
-       ],
-     ],
-   },
    GenericSubQ => {
      limit => [
        '(
                JOIN owners owner
                  ON owner.id = me.owner
              WHERE source != ? AND me.title = ? AND source = ?
 -            GROUP BY avg( me.id / ? )
 +            GROUP BY AVG(me.id / ?), MAX(owner.id)
              HAVING ?
            ) me
          WHERE (
                JOIN owners owner
                  ON owner.id = me.owner
              WHERE source != ? AND me.title = ? AND source = ?
 -            GROUP BY avg( me.id / ? )
 +            GROUP BY AVG(me.id / ?), MAX(owner.id)
              HAVING ?
            ) me
          WHERE (
            ) me
            LEFT JOIN books books
              ON books.owner = me.id
 -        ORDER BY me.id, books.owner
 +        ORDER BY me.id
        )',
        [
          [ { sqlt_datatype => 'integer' } => 1 ],
@@@ -779,7 -736,7 +728,7 @@@ for my $limtype (sort keys %$tests) 
      join => 'owner',  # single-rel manual prefetch
      rows => 4,
      '+columns' => { bar => \['? * ?', [ $attr => 11 ], [ $attr => 12 ]], baz => \[ '?', [ $attr => 13 ]] },
 -    group_by => \[ 'avg(me.id / ?)', [ $attr => 21 ] ],
 +    group_by => \[ 'AVG(me.id / ?), MAX(owner.id)', [ $attr => 21 ] ],
      having => \[ '?', [ $attr => 31 ] ],
      ($limtype =~ /GenericSubQ/ ? ( order_by => 'me.id' ) : () ),  # needs a simple-column stable order to be happy
    });