Merge 'trunk' into 'prefetch'
Peter Rabbitson [Mon, 21 Sep 2009 11:34:26 +0000 (11:34 +0000)]
r7673@Thesaurus (orig r7662):  ribasushi | 2009-09-15 09:43:46 +0200
Warn when distinct is used with group_by
r7674@Thesaurus (orig r7663):  rbuels | 2009-09-15 22:45:32 +0200
doc patch, clarified warning about using find_or_create() and friends on tables with auto-increment or similar columns
r7675@Thesaurus (orig r7664):  rbuels | 2009-09-15 22:55:15 +0200
another doc clarification regarding auto-inc columns with find_or_create() and such functions
r7683@Thesaurus (orig r7672):  ribasushi | 2009-09-17 13:54:44 +0200
Fix left-join chaining
r7694@Thesaurus (orig r7683):  ribasushi | 2009-09-18 12:36:42 +0200
 r6389@Thesaurus (orig r6388):  caelum | 2009-05-23 22:48:06 +0200
 recreating Sybase branch
 r6395@Thesaurus (orig r6394):  caelum | 2009-05-24 01:47:32 +0200
 try not to fuck mssql with the sybase crap
 r6488@Thesaurus (orig r6487):  caelum | 2009-06-03 17:31:24 +0200
 resolve conflict
 r6490@Thesaurus (orig r6489):  caelum | 2009-06-03 18:25:36 +0200
 add missing files to sybase branch
 r6492@Thesaurus (orig r6491):  caelum | 2009-06-04 01:51:39 +0200
 fix Sybase DT stuff and storage bases
 r6493@Thesaurus (orig r6492):  caelum | 2009-06-04 02:10:45 +0200
 fix base for mssql (can't be a sybase anymore)
 r6494@Thesaurus (orig r6493):  caelum | 2009-06-04 02:20:37 +0200
 test sybase SMALLDATETIME inflation
 r6495@Thesaurus (orig r6494):  caelum | 2009-06-04 04:52:31 +0200
 update Sybase docs
 r6501@Thesaurus (orig r6500):  caelum | 2009-06-04 14:50:49 +0200
 sybase limit count without offset now works
 r6504@Thesaurus (orig r6503):  caelum | 2009-06-04 18:03:01 +0200
 use TOP for sybase limit count thanks to refactored count
 r6505@Thesaurus (orig r6504):  caelum | 2009-06-04 18:41:54 +0200
 back to counting rows for Sybase LIMIT counts
 r6506@Thesaurus (orig r6505):  caelum | 2009-06-04 19:07:48 +0200
 minor sybase count fix
 r6512@Thesaurus (orig r6511):  caelum | 2009-06-05 01:02:48 +0200
 test sybase group_by count, works
 r6513@Thesaurus (orig r6512):  caelum | 2009-06-05 01:28:18 +0200
 set date format on _rebless correctly
 r6516@Thesaurus (orig r6515):  caelum | 2009-06-05 02:24:46 +0200
 manually merged in sybase_noquote branch
 r6518@Thesaurus (orig r6517):  caelum | 2009-06-05 06:34:25 +0200
 shit doesn't work yet
 r6520@Thesaurus (orig r6519):  caelum | 2009-06-05 16:55:41 +0200
 update sybase types which shouldn't be quoted
 r6525@Thesaurus (orig r6524):  caelum | 2009-06-06 04:40:51 +0200
 tweaks to sybase types
 r6527@Thesaurus (orig r6526):  caelum | 2009-06-06 05:36:03 +0200
 temporary sybase noquote hack
 r6595@Thesaurus (orig r6594):  caelum | 2009-06-10 13:46:37 +0200
 Sybase::NoBindVars now correctly quotes
 r6596@Thesaurus (orig r6595):  caelum | 2009-06-10 14:04:19 +0200
 cache rsrc in NoBindVars, use name_sep
 r6597@Thesaurus (orig r6596):  caelum | 2009-06-10 14:35:52 +0200
 Sybase count by first pk, if available
 r6599@Thesaurus (orig r6598):  caelum | 2009-06-10 15:00:42 +0200
 cache rsrc in NoBindVars correctly
 r6600@Thesaurus (orig r6599):  caelum | 2009-06-10 15:27:41 +0200
 handle unknown rsrc in NoBindVars and Sybase::NoBindVars
 r6605@Thesaurus (orig r6604):  caelum | 2009-06-10 18:17:31 +0200
 cache rsrc properly in NoBindVars, return undef if no rsrc
 r6658@Thesaurus (orig r6657):  caelum | 2009-06-13 05:57:40 +0200
 switch to DateTime::Format::Sybase
 r6700@Thesaurus (orig r6699):  caelum | 2009-06-17 16:25:28 +0200
 rename and document dt setup method, will be an on_connect_call at later merge point
 r6701@Thesaurus (orig r6700):  caelum | 2009-06-17 16:30:08 +0200
 more dt docs reorg
 r6715@Thesaurus (orig r6714):  caelum | 2009-06-19 01:28:17 +0200
 todo tests for text/image columns in sybase
 r6716@Thesaurus (orig r6715):  caelum | 2009-06-19 01:46:56 +0200
 added connect_call_blob_setup for Sybase
 r6724@Thesaurus (orig r6723):  caelum | 2009-06-19 17:12:20 +0200
 cleanups
 r6771@Thesaurus (orig r6770):  caelum | 2009-06-23 16:42:32 +0200
 minor changes
 r6788@Thesaurus (orig r6787):  caelum | 2009-06-25 05:31:06 +0200
 fixup POD, comment out count
 r6811@Thesaurus (orig r6810):  caelum | 2009-06-28 02:14:56 +0200
 prototype blob implementation
 r6857@Thesaurus (orig r6856):  caelum | 2009-06-29 23:45:19 +0200
 branch pushed, removing
 r6868@Thesaurus (orig r6867):  caelum | 2009-06-30 03:39:51 +0200
 merge on_connect_call updates
 r6877@Thesaurus (orig r6876):  caelum | 2009-06-30 12:46:43 +0200
 code cleanups
 r6957@Thesaurus (orig r6956):  caelum | 2009-07-03 02:32:48 +0200
 minor changes
 r6959@Thesaurus (orig r6958):  caelum | 2009-07-03 05:04:12 +0200
 fix sybase mro
 r7001@Thesaurus (orig r7000):  caelum | 2009-07-07 13:34:23 +0200
 fix sybase rebless to NoBindVars
 r7021@Thesaurus (orig r7020):  caelum | 2009-07-10 12:52:13 +0200
 fix NoBindVars
 r7053@Thesaurus (orig r7052):  caelum | 2009-07-15 01:39:02 +0200
 set maxConnect in DSN and add docs
 r7065@Thesaurus (orig r7064):  caelum | 2009-07-17 09:39:54 +0200
 make insertion of blobs into tables with identity columns work, other minor fixes
 r7070@Thesaurus (orig r7069):  caelum | 2009-07-17 23:30:13 +0200
 some compatibility updated for older DBD::Sybase versions, some initial work on _select_args for blobs
 r7072@Thesaurus (orig r7071):  caelum | 2009-07-19 23:57:11 +0200
 mangling _select_args turned out to be unnecessary
 r7073@Thesaurus (orig r7072):  caelum | 2009-07-20 01:02:19 +0200
 minor cleanups
 r7074@Thesaurus (orig r7073):  caelum | 2009-07-20 15:47:48 +0200
 blob update now works
 r7076@Thesaurus (orig r7075):  caelum | 2009-07-20 19:06:46 +0200
 change the (incorrect) version check to a check for FreeTDS
 r7077@Thesaurus (orig r7076):  caelum | 2009-07-20 19:13:25 +0200
 better check for FreeTDS thanks to arcanez
 r7089@Thesaurus (orig r7086):  caelum | 2009-07-22 07:09:21 +0200
 minor cleanups
 r7091@Thesaurus (orig r7088):  caelum | 2009-07-22 17:05:37 +0200
 remove unnecessary test Result class
 r7092@Thesaurus (orig r7089):  caelum | 2009-07-23 00:47:14 +0200
 fix doc for how to check for FreeTDS
 r7095@Thesaurus (orig r7092):  caelum | 2009-07-23 14:35:53 +0200
 doc tweak
 r7115@Thesaurus (orig r7112):  caelum | 2009-07-24 09:58:24 +0200
 add support for IDENTITY_INSERT
 r7117@Thesaurus (orig r7114):  caelum | 2009-07-24 16:19:08 +0200
 savepoint support
 r7120@Thesaurus (orig r7117):  caelum | 2009-07-24 20:35:37 +0200
 fix race condition in last_insert_id with placeholders
 r7121@Thesaurus (orig r7118):  caelum | 2009-07-24 21:22:25 +0200
 code cleanup
 r7124@Thesaurus (orig r7121):  caelum | 2009-07-25 16:19:58 +0200
 use _resolve_column_info in NoBindVars
 r7125@Thesaurus (orig r7122):  caelum | 2009-07-25 21:23:49 +0200
 make insert work as a nested transaction too
 r7126@Thesaurus (orig r7123):  caelum | 2009-07-25 22:52:17 +0200
 add money type support
 r7128@Thesaurus (orig r7125):  caelum | 2009-07-27 03:48:35 +0200
 better FreeTDS support
 r7130@Thesaurus (orig r7127):  caelum | 2009-07-28 06:23:54 +0200
 minor refactoring, cleanups, doc updates
 r7131@Thesaurus (orig r7128):  caelum | 2009-07-28 09:32:45 +0200
 forgot to set mro in dbi::cursor
 r7141@Thesaurus (orig r7138):  caelum | 2009-07-30 10:21:20 +0200
 better test for "smalldatetime" in Sybase
 r7146@Thesaurus (orig r7143):  caelum | 2009-07-30 15:37:18 +0200
 update sqlite test schema
 r7207@Thesaurus (orig r7204):  caelum | 2009-08-04 23:40:16 +0200
 update Changes
 r7222@Thesaurus (orig r7219):  caelum | 2009-08-05 11:02:26 +0200
 fix a couple minor issues after pull from trunk
 r7260@Thesaurus (orig r7257):  caelum | 2009-08-07 14:45:18 +0200
 add note about where to get Schema::Loader
 r7273@Thesaurus (orig r7270):  ribasushi | 2009-08-09 01:19:49 +0200
 Changes and minor code rewrap
 r7285@Thesaurus (orig r7282):  ribasushi | 2009-08-10 08:08:06 +0200
 pesky whitespace
 r7286@Thesaurus (orig r7283):  ribasushi | 2009-08-10 08:11:46 +0200
 privatize dormant method - it may be useful for sybase at *some* point
 r7287@Thesaurus (orig r7284):  ribasushi | 2009-08-10 08:19:55 +0200
 Whoops
 r7289@Thesaurus (orig r7286):  caelum | 2009-08-10 08:44:51 +0200
 document placeholders_with_type_conversion_supported and add a redispatch to reblessed storage in DBI::update
 r7290@Thesaurus (orig r7287):  caelum | 2009-08-10 10:07:45 +0200
 fix and test redispatch to reblessed storage insert/update
 r7292@Thesaurus (orig r7289):  caelum | 2009-08-10 10:32:37 +0200
 rename get_connected_schema to get_schema in sybase test
 r7345@Thesaurus (orig r7342):  ribasushi | 2009-08-18 22:45:06 +0200
 Fix Changes
 r7367@Thesaurus (orig r7364):  ribasushi | 2009-08-23 10:00:34 +0200
 Minaor speedup
 r7368@Thesaurus (orig r7365):  ribasushi | 2009-08-23 10:01:10 +0200
 Generalize and hide placeholder support check
 r7369@Thesaurus (orig r7366):  ribasushi | 2009-08-23 10:04:26 +0200
 Rename the common sybase driver
 r7373@Thesaurus (orig r7370):  caelum | 2009-08-24 13:21:51 +0200
 make insert only use a txn if needed, add connect_call_unsafe_insert
 r7374@Thesaurus (orig r7371):  caelum | 2009-08-24 14:42:57 +0200
 add test for IDENTITY_INSERT
 r7378@Thesaurus (orig r7375):  caelum | 2009-08-24 15:51:48 +0200
 use debugobj->callback instead of local *_query_start in test to capture query
 r7379@Thesaurus (orig r7376):  caelum | 2009-08-24 17:19:46 +0200
 remove duplicate oracle method and fix an mssql method call
 r7417@Thesaurus (orig r7414):  caelum | 2009-08-29 07:23:45 +0200
 update link to Schema::Loader branch
 r7427@Thesaurus (orig r7424):  caelum | 2009-08-29 09:31:41 +0200
 switch to ::DBI::AutoCast
 r7428@Thesaurus (orig r7425):  ribasushi | 2009-08-29 13:36:22 +0200
 Cleanup:
 Added commented method signatures for easier debugging
 privatize transform_unbound_value as _prep_bind_value
 Remove \@_ splice's in lieu of of simple shifts
 Exposed TYPE_MAPPING used by native_data_type via our
 Removed use of txn_do - internal code uses the scope guard
 Renamed some variables, whitespace cleanup, the works
 r7429@Thesaurus (orig r7426):  ribasushi | 2009-08-29 13:40:48 +0200
 Varname was absolutely correct
 r7430@Thesaurus (orig r7427):  caelum | 2009-08-29 14:09:13 +0200
 minor changes for tests to pass again
 r7431@Thesaurus (orig r7428):  caelum | 2009-08-29 21:08:51 +0200
 fix inserts with active cursors
 r7432@Thesaurus (orig r7429):  caelum | 2009-08-29 22:53:02 +0200
 remove extra connection
 r7434@Thesaurus (orig r7431):  caelum | 2009-08-30 00:02:20 +0200
 test correlated subquery
 r7442@Thesaurus (orig r7439):  ribasushi | 2009-08-30 09:07:00 +0200
 Put the ocmment back
 r7443@Thesaurus (orig r7440):  ribasushi | 2009-08-30 09:15:41 +0200
 Change should_quote_value to interpolate_unquoted to make it harder to stop quoting by accident (it's easier to return a undef by accident than a 1)
 r7446@Thesaurus (orig r7443):  caelum | 2009-08-30 18:19:46 +0200
 added txn_scope_guards for blob operations
 r7447@Thesaurus (orig r7444):  ribasushi | 2009-08-30 18:56:43 +0200
 Rename insert_txn to unsafe_insert
 r7512@Thesaurus (orig r7509):  ribasushi | 2009-09-03 20:24:14 +0200
 Minor cleanups
 r7575@Thesaurus (orig r7572):  caelum | 2009-09-05 07:23:57 +0200
 pending review by mpeppler
 r7593@Thesaurus (orig r7590):  ribasushi | 2009-09-07 09:10:05 +0200
 Release 0.08111 tag
 r7594@Thesaurus (orig r7591):  ribasushi | 2009-09-07 09:14:33 +0200
 Whoops this should not have committed
 r7602@Thesaurus (orig r7599):  caelum | 2009-09-07 21:31:38 +0200
 fix _insert_dbh code to only connect when needed, doc update
 r7607@Thesaurus (orig r7604):  caelum | 2009-09-09 02:15:54 +0200
 remove unsafe_insert
 r7608@Thesaurus (orig r7605):  ribasushi | 2009-09-09 09:14:20 +0200
 Localisation ain't free, we don't do it unless we have to
 r7609@Thesaurus (orig r7606):  ribasushi | 2009-09-09 09:40:29 +0200
 Much simpler
 r7610@Thesaurus (orig r7607):  ribasushi | 2009-09-09 10:38:41 +0200
 Reduce amount of perl-golf :)
 r7611@Thesaurus (orig r7608):  ribasushi | 2009-09-09 10:41:15 +0200
 This should not have worked - I guess we lack tests?
 r7614@Thesaurus (orig r7611):  caelum | 2009-09-09 12:08:36 +0200
 test multi-row blob update
 r7619@Thesaurus (orig r7616):  caelum | 2009-09-09 18:01:15 +0200
 remove Sub::Name hack for method dispatch, pass $next instead
 r7620@Thesaurus (orig r7617):  caelum | 2009-09-10 02:16:03 +0200
 do blob update over _insert_dbh
 r7661@Thesaurus (orig r7650):  caelum | 2009-09-13 10:27:44 +0200
 change _insert_dbh to _insert_storage
 r7663@Thesaurus (orig r7652):  caelum | 2009-09-13 11:52:20 +0200
 make sure _init doesn't loop, steal insert_bulk from mssql, add some insert_bulk tests
 r7664@Thesaurus (orig r7653):  caelum | 2009-09-13 13:27:51 +0200
 allow subclassing of methods proxied to _writer_storage
 r7666@Thesaurus (orig r7655):  caelum | 2009-09-14 15:09:21 +0200
 sybase bulk API support stuff (no blobs yet, coming soon...)
 r7667@Thesaurus (orig r7656):  caelum | 2009-09-14 15:33:14 +0200
 add another test for sybase bulk stuff (passes)
 r7668@Thesaurus (orig r7657):  caelum | 2009-09-14 15:44:06 +0200
 minor change (fix inverted boolean for warning)
 r7669@Thesaurus (orig r7658):  caelum | 2009-09-14 15:48:52 +0200
 remove @args from DBI::sth, use full arg list
 r7676@Thesaurus (orig r7665):  caelum | 2009-09-16 15:06:35 +0200
 use execute_array for insert_bulk, test insert_bulk with blobs, clean up blob tests a bit
 r7680@Thesaurus (orig r7669):  ribasushi | 2009-09-16 19:36:19 +0200
 Remove branched changes
 r7682@Thesaurus (orig r7671):  caelum | 2009-09-17 03:03:34 +0200
 I'll rewrite this bit tomorrow to be less retarded
 r7684@Thesaurus (orig r7673):  caelum | 2009-09-18 04:03:15 +0200
 fix yesterday's stuff, identity_update works, blob updates are better
 r7686@Thesaurus (orig r7675):  caelum | 2009-09-18 04:22:38 +0200
 column no longer necessary in test
 r7688@Thesaurus (orig r7677):  caelum | 2009-09-18 08:33:14 +0200
 fix freetds
 r7691@Thesaurus (orig r7680):  ribasushi | 2009-09-18 12:25:42 +0200
  r7678@Thesaurus (orig r7667):  ribasushi | 2009-09-16 19:31:14 +0200
  New subbranch
  r7679@Thesaurus (orig r7668):  ribasushi | 2009-09-16 19:34:29 +0200
  Caelum's work so far
  r7690@Thesaurus (orig r7679):  caelum | 2009-09-18 11:10:16 +0200
  support for blobs in insert_bulk fallback

 r7692@Thesaurus (orig r7681):  ribasushi | 2009-09-18 12:28:09 +0200
 Rollback all bulk insert code before merge

r7699@Thesaurus (orig r7688):  ribasushi | 2009-09-18 14:12:05 +0200
Cleanup exception handling
r7700@Thesaurus (orig r7689):  ribasushi | 2009-09-18 14:22:02 +0200
duh
r7701@Thesaurus (orig r7690):  ribasushi | 2009-09-18 14:25:06 +0200
Minor cleanup of RSC with has_many joins
r7702@Thesaurus (orig r7691):  ribasushi | 2009-09-18 14:32:15 +0200
Changes and dev notes in makefile
r7705@Thesaurus (orig r7694):  ribasushi | 2009-09-18 14:52:26 +0200
Nothing says the grouping column can not be nullable
r7706@Thesaurus (orig r7695):  ribasushi | 2009-09-18 14:53:33 +0200
Changes
r7707@Thesaurus (orig r7696):  ribasushi | 2009-09-18 20:09:04 +0200
This code belogs in Storage::DBI
r7708@Thesaurus (orig r7697):  ribasushi | 2009-09-18 20:38:26 +0200
Clear up some legacy cruft and straighten inheritance
r7710@Thesaurus (orig r7699):  ribasushi | 2009-09-21 00:25:20 +0200
Backout sybase changes
r7713@Thesaurus (orig r7702):  ribasushi | 2009-09-21 00:46:32 +0200
Missed a part of the revert
r7720@Thesaurus (orig r7709):  ribasushi | 2009-09-21 02:49:11 +0200
Oops
r7721@Thesaurus (orig r7710):  ribasushi | 2009-09-21 11:02:14 +0200
Changes
r7722@Thesaurus (orig r7711):  ribasushi | 2009-09-21 12:49:30 +0200
Undocument the from attribute (the description was mostly outdated anyway)
r7723@Thesaurus (orig r7712):  ribasushi | 2009-09-21 12:58:58 +0200
Release 0.08112

1  2 
lib/DBIx/Class/ResultSet.pm
lib/DBIx/Class/ResultSource.pm
t/prefetch/grouped.t

@@@ -7,6 -7,7 +7,7 @@@ use overloa
          'bool'   => "_bool",
          fallback => 1;
  use Carp::Clan qw/^DBIx::Class/;
+ use DBIx::Class::Exception;
  use Data::Page;
  use Storable;
  use DBIx::Class::ResultSetColumn;
@@@ -570,12 -571,16 +571,16 @@@ sub _unique_queries 
    my $where = $self->_collapse_cond($self->{attrs}{where} || {});
    my $num_where = scalar keys %$where;
  
-   my @unique_queries;
+   my (@unique_queries, %seen_column_combinations);
    foreach my $name (@constraint_names) {
-     my @unique_cols = $self->result_source->unique_constraint_columns($name);
-     my $unique_query = $self->_build_unique_query($query, \@unique_cols);
+     my @constraint_cols = $self->result_source->unique_constraint_columns($name);
  
-     my $num_cols = scalar @unique_cols;
+     my $constraint_sig = join "\x00", sort @constraint_cols;
+     next if $seen_column_combinations{$constraint_sig}++;
+     my $unique_query = $self->_build_unique_query($query, \@constraint_cols);
+     my $num_cols = scalar @constraint_cols;
      my $num_query = scalar keys %$unique_query;
  
      my $total = $num_query + $num_where;
@@@ -966,183 -971,140 +971,183 @@@ sub _construct_object 
    return @new;
  }
  
 -sub _collapse_result {
 -  my ($self, $as_proto, $row) = @_;
 -
 -  # if the first row that ever came in is totally empty - this means we got
 -  # hit by a smooth^Wempty left-joined resultset. Just noop in that case
 -  # instead of producing a {}
 -  #
 -  my $has_def;
 -  for (@$row) {
 -    if (defined $_) {
 -      $has_def++;
 -      last;
 +# _unflatten_result takes a row hashref which looks like this:
 +# $VAR1 = {
 +#   'cd.artist.artistid' => '1',
 +#   'cd.artist' => '1',
 +#   'cd_id' => '1',
 +#   'cd.genreid' => undef,
 +#   'cd.year' => '1999',
 +#   'cd.title' => 'Spoonful of bees',
 +#   'cd.single_track' => undef,
 +#   'cd.artist.name' => 'Caterwauler McCrae',
 +#   'cd.artist.rank' => '13',
 +#   'cd.artist.charfield' => undef,
 +#   'cd.cdid' => '1'
 +# };
 +
 +# and generates the following structure:
 +
 +# $VAR1 = [
 +#   {
 +#     'cd_id' => '1'
 +#   },
 +#   {
 +#     'cd' => [
 +#       {
 +#         'single_track' => undef,
 +#         'cdid' => '1',
 +#         'artist' => '1',
 +#         'title' => 'Spoonful of bees',
 +#         'year' => '1999',
 +#         'genreid' => undef
 +#       },
 +#       {
 +#         'artist' => [
 +#           {
 +#             'artistid' => '1',
 +#             'charfield' => undef,
 +#             'name' => 'Caterwauler McCrae',
 +#             'rank' => '13'
 +#           }
 +#         ]
 +#       }
 +#     ]
 +#   }
 +# ];  
 +
 +# It returns one row object which consists of an arrayref with two
 +# elements. The first contains the plain column data, the second 
 +# contains the data of relationships. Those are row arrayrefs, themselves.
 +
 +# it's a recursive function. It needs to request the relationship_info
 +# to decide whether to put the data of a relationship in a hashref
 +# (i.e. belongs_to) or an arrayref (i.e. has_many).
 +
 +sub _unflatten_result {
 +    my ( $self, $row ) = @_;
 +
 +    my $columns = {};
 +    my $rels    = {};
 +
 +    foreach my $column ( sort keys %$row ) {
 +        if ( $column =~ /^(.*?)\.(.*)$/ ) {
 +            $rels->{$1} ||= {};
 +            $rels->{$1}->{$2} = $row->{$column};
 +        }
 +        else {
 +            $columns->{$column} = $row->{$column};
 +        }
      }
 -  }
 -  return undef unless $has_def;
 -
 -  my @copy = @$row;
 -
 -  # 'foo'         => [ undef, 'foo' ]
 -  # 'foo.bar'     => [ 'foo', 'bar' ]
 -  # 'foo.bar.baz' => [ 'foo.bar', 'baz' ]
 -
 -  my @construct_as = map { [ (/^(?:(.*)\.)?([^.]+)$/) ] } @$as_proto;
 -
 -  my %collapse = %{$self->{_attrs}{collapse}||{}};
 -
 -  my @pri_index;
 -
 -  # 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.
  
 -  # 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
 -
 -  # store just the index so we can check the array positions from the row
 -  # without having to contruct the full hash
 -
 -  if (keys %collapse) {
 -    my %pri = map { ($_ => 1) } $self->result_source->primary_columns;
 -    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);
 -      }
 -      last unless keys %pri; # short circuit (Johnny Five Is Alive!)
 +    foreach my $rel ( sort keys %$rels ) {
 +        my $rel_info = $self->result_source->relationship_info($rel);
 +        $rels->{$rel} =
 +          $self->related_resultset($rel)->_unflatten_result( $rels->{$rel} );
 +        $rels->{$rel} = [ $rels->{$rel} ]
 +          if ( $rel_info->{attrs}->{accessor} eq 'multi' );
      }
 -  }
 -
 -  # no need to do an if, it'll be empty if @pri_index is empty anyway
  
 -  my %pri_vals = map { ($_ => $copy[$_]) } @pri_index;
 -
 -  my @const_rows;
 +    return keys %$rels ? [ $columns, $rels ] : [$columns];
 +}
  
 -  do { # no need to check anything at the front, we always want the first row
 +# two arguments: $as_proto is an arrayref of column names,
 +# $row_ref is an arrayref of the data. If none of the row data
 +# is defined we return undef (that's copied from the old
 +# _collapse_result). Next we decide whether we need to collapse
 +# the resultset (i.e. we prefetch something) or not. $collapse
 +# indicates that. The do-while loop will run once if we do not need
 +# to collapse the result and will run as long as _merge_result returns
 +# a true value. It will return undef if the current added row does not
 +# match the previous row. A bit of stashing and cursor magic is
 +# required so that the cursor is not mixed up.
  
 -    my %const;
 +# "$rows" is a bit misleading. In the end, there should only be one
 +# element in this arrayref. 
  
 -    foreach my $this_as (@construct_as) {
 -      $const{$this_as->[0]||''}{$this_as->[1]} = shift(@copy);
 +sub _collapse_result {
 +    my ( $self, $as_proto, $row_ref ) = @_;
 +    my $has_def;
 +    for (@$row_ref) {
 +        if ( defined $_ ) {
 +            $has_def++;
 +            last;
 +        }
      }
 +    return undef unless $has_def;
 +
 +    my $collapse = keys %{ $self->{_attrs}{collapse} || {} };
 +    my $rows     = [];
 +    my @row      = @$row_ref;
 +    do {
 +        my $i = 0;
 +        my $row = { map { $_ => $row[ $i++ ] } @$as_proto };
 +        $row = $self->_unflatten_result($row);
 +        unless ( scalar @$rows ) {
 +            push( @$rows, $row );
 +        }
 +        $collapse = undef unless ( $self->_merge_result( $rows, $row ) );
 +      } while (
 +        $collapse
 +        && do { @row = $self->cursor->next; $self->{stashed_row} = \@row if @row; }
 +      );
  
 -    push(@const_rows, \%const);
 -
 -  } 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;
 -
 -        # last thing in do block, counts as true if anything doesn't match
 -
 -        # check xor defined first for NULL vs. NOT NULL then if one is
 -        # defined the other must be so check string equality
 +    return $rows->[0];
  
 -        grep {
 -          (defined $pri_vals{$_} ^ defined $copy[$_])
 -          || (defined $pri_vals{$_} && ($pri_vals{$_} ne $copy[$_]))
 -        } @pri_index;
 -      }
 -  );
 +}
  
 -  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;
 -              }
 +# _merge_result accepts an arrayref of rows objects (again, an arrayref of two elements)
 +# and a row object which should be merged into the first object.
 +# First we try to find out whether $row is already in $rows. If this is the case
 +# we try to merge them by iteration through their relationship data. We call
 +# _merge_result again on them, so they get merged.
 +
 +# If we don't find the $row in $rows, we append it to $rows and return undef.
 +# _merge_result returns 1 otherwise (i.e. $row has been found in $rows).
 +
 +sub _merge_result {
 +    my ( $self, $rows, $row ) = @_;
 +    my ( $columns, $rels ) = @$row;
 +    my $found = undef;
 +    foreach my $seen (@$rows) {
 +        my $match = 1;
 +        foreach my $column ( keys %$columns ) {
 +            if (   defined $seen->[0]->{$column} ^ defined $columns->{$column}
 +                or defined $columns->{$column}
 +                && $seen->[0]->{$column} ne $columns->{$column} )
 +            {
 +
 +                $match = 0;
 +                last;
              }
 -          }
 -          if (exists $collapse{$cur}) {
 -            $target = $target->[-1];
 -          }
          }
 -        $target->[0] = $data;
 -      } else {
 -        $info->[0] = $const->{$key};
 -      }
 +        if ($match) {
 +            $found = $seen;
 +            last;
 +        }
      }
 -  }
 +    if ($found) {
 +        foreach my $rel ( keys %$rels ) {
 +            my $old_rows = $found->[1]->{$rel};
 +            $self->_merge_result(
 +                ref $found->[1]->{$rel}->[0] eq 'HASH' ? [ $found->[1]->{$rel} ]
 +                : $found->[1]->{$rel},
 +                ref $rels->{$rel}->[0] eq 'HASH' ? [ $rels->{$rel}->[0], $rels->{$rel}->[1] ]
 +                : $rels->{$rel}->[0]
 +            );
 +
 +        }
  
 -  return $info;
 +    }
 +    else {
 +        push( @$rows, $row );
 +        return undef;
 +    }
 +
 +    return 1;
  }
  
 +
  =head2 result_source
  
  =over 4
@@@ -2235,13 -2197,14 +2240,14 @@@ You most likely want this method when l
  a unique constraint that is not the primary key, or looking for
  related rows.
  
- If you want objects to be saved immediately, use L</find_or_create> instead.
+ If you want objects to be saved immediately, use L</find_or_create>
+ instead.
  
- B<Note>: C<find_or_new> is probably not what you want when creating a
- new row in a table that uses primary keys supplied by the
- database. Passing in a primary key column with a value of I<undef>
- will cause L</find> to attempt to search for a row with a value of
- I<NULL>.
+ B<Note>: Take care when using C<find_or_new> with a table having
+ columns with default values that you intend to be automatically
+ supplied by the database (e.g. an auto_increment primary key column).
+ In normal usage, the value of such columns should NOT be included at
+ all in the call to C<find_or_new>, even when set to C<undef>.
  
  =cut
  
@@@ -2321,6 -2284,19 +2327,19 @@@ C<belongs_to>resultset. Note Hashref
      }
    });
  
+ =over
+ =item WARNING
+ When subclassing ResultSet never attempt to override this method. Since
+ it is a simple shortcut for C<< $self->new_result($attrs)->insert >>, a
+ lot of the internals simply never call it, so your override will be
+ bypassed more often than not. Override either L<new|DBIx::Class::Row/new>
+ or L<insert|DBIx::Class::Row/insert> depending on how early in the
+ L</create> process you need to intervene.
+ =back
  =cut
  
  sub create {
@@@ -2370,11 -2346,11 +2389,11 @@@ condition. Another process could creat
  the find has completed and before the create has started. To avoid
  this problem, use find_or_create() inside a transaction.
  
- B<Note>: C<find_or_create> is probably not what you want when creating
- a new row in a table that uses primary keys supplied by the
- database. Passing in a primary key column with a value of I<undef>
- will cause L</find> to attempt to search for a row with a value of
- I<NULL>.
+ B<Note>: Take care when using C<find_or_create> with a table having
+ columns with default values that you intend to be automatically
+ supplied by the database (e.g. an auto_increment primary key column).
+ In normal usage, the value of such columns should NOT be included at
+ all in the call to C<find_or_create>, even when set to C<undef>.
  
  See also L</find> and L</update_or_create>. For information on how to declare
  unique constraints, see L<DBIx::Class::ResultSource/add_unique_constraint>.
@@@ -2437,11 -2413,11 +2456,11 @@@ If the C<key> is specified as C<primary
  See also L</find> and L</find_or_create>. For information on how to declare
  unique constraints, see L<DBIx::Class::ResultSource/add_unique_constraint>.
  
- B<Note>: C<update_or_create> is probably not what you want when
- looking for a row in a table that uses primary keys supplied by the
- database, unless you actually have a key value. Passing in a primary
- key column with a value of I<undef> will cause L</find> to attempt to
- search for a row with a value of I<NULL>.
+ B<Note>: Take care when using C<update_or_create> with a table having
+ columns with default values that you intend to be automatically
+ supplied by the database (e.g. an auto_increment primary key column).
+ In normal usage, the value of such columns should NOT be included at
+ all in the call to C<update_or_create>, even when set to C<undef>.
  
  =cut
  
@@@ -2498,7 -2474,13 +2517,13 @@@ For example
        $cd->insert;
    }
  
- See also L</find>, L</find_or_create> and L<find_or_new>.
+ B<Note>: Take care when using C<update_or_new> with a table having
+ columns with default values that you intend to be automatically
+ supplied by the database (e.g. an auto_increment primary key column).
+ In normal usage, the value of such columns should NOT be included at
+ all in the call to C<update_or_new>, even when set to C<undef>.
+ See also L</find>, L</find_or_create> and L</find_or_new>.
  
  =cut
  
@@@ -2808,24 -2790,35 +2833,35 @@@ sub _resolved_attrs 
  
    # build columns (as long as select isn't set) into a set of as/select hashes
    unless ( $attrs->{select} ) {
-       @colbits = map {
-           ( ref($_) eq 'HASH' )
-               ? $_
-               : {
-                   (
-                     /^\Q${alias}.\E(.+)$/
-                       ? "$1"
-                       : "$_"
-                   )
-                 =>
-                   (
-                     /\./
-                       ? "$_"
-                       : "${alias}.$_"
-                   )
-             }
-       } ( ref($attrs->{columns}) eq 'ARRAY' ) ? @{ delete $attrs->{columns}} : (delete $attrs->{columns} || $source->columns );
+     my @cols = ( ref($attrs->{columns}) eq 'ARRAY' )
+       ? @{ delete $attrs->{columns}}
+       : (
+           ( delete $attrs->{columns} )
+             ||
+           $source->columns
+         )
+     ;
+     @colbits = map {
+       ( ref($_) eq 'HASH' )
+       ? $_
+       : {
+           (
+             /^\Q${alias}.\E(.+)$/
+               ? "$1"
+               : "$_"
+           )
+             =>
+           (
+             /\./
+               ? "$_"
+               : "${alias}.$_"
+           )
+         }
+     } @cols;
    }
    # add the additional columns on
    foreach ( 'include_columns', '+columns' ) {
        push @colbits, map {
  
    if ( $attrs->{join} || $attrs->{prefetch} ) {
  
-     $self->throw_exception ('join/prefetch can not be used with a literal scalarref {from}')
+     $self->throw_exception ('join/prefetch can not be used with a custom {from}')
        if ref $attrs->{from} ne 'ARRAY';
  
      my $join = delete $attrs->{join} || {};
    # generate the distinct induced group_by early, as prefetch will be carried via a
    # subquery (since a group_by is present)
    if (delete $attrs->{distinct}) {
-     $attrs->{group_by} ||= [ grep { !ref($_) || (ref($_) ne 'HASH') } @{$attrs->{select}} ];
+     if ($attrs->{group_by}) {
+       carp ("Useless use of distinct on a grouped resultset ('distinct' is ignored when a 'group_by' is present)");
+     }
+     else {
+       $attrs->{group_by} = [ grep { !ref($_) || (ref($_) ne 'HASH') } @{$attrs->{select}} ];
+     }
    }
  
    $attrs->{collapse} ||= {};
@@@ -3029,6 -3027,13 +3070,13 @@@ sub _rollout_hash 
  sub _calculate_score {
    my ($self, $a, $b) = @_;
  
+   if (defined $a xor defined $b) {
+     return 0;
+   }
+   elsif (not defined $a) {
+     return 1;
+   }
    if (ref $b eq 'HASH') {
      my ($b_key) = keys %{$b};
      if (ref $a eq 'HASH') {
@@@ -3110,12 -3115,13 +3158,13 @@@ See L<DBIx::Class::Schema/throw_excepti
  
  sub throw_exception {
    my $self=shift;
    if (ref $self && $self->_source_handle->schema) {
      $self->_source_handle->schema->throw_exception(@_)
-   } else {
-     croak(@_);
    }
+   else {
+     DBIx::Class::Exception->throw(@_);
+   }
  }
  
  # XXX: FIXME: Attributes docs need clearing up
@@@ -3534,7 -3540,8 +3583,8 @@@ done
  
  =back
  
- Set to 1 to group by all columns.
+ Set to 1 to group by all columns. If the resultset already has a group_by
+ attribute, this setting is ignored and an appropriate warning is issued.
  
  =head2 where
  
@@@ -3568,177 -3575,6 +3618,6 @@@ By default, searches are not cached
  For more examples of using these attributes, see
  L<DBIx::Class::Manual::Cookbook>.
  
- =head2 from
- =over 4
- =item Value: \@from_clause
- =back
- The C<from> attribute gives you manual control over the C<FROM> clause of SQL
- statements generated by L<DBIx::Class>, allowing you to express custom C<JOIN>
- clauses.
- NOTE: Use this on your own risk.  This allows you to shoot off your foot!
- C<join> will usually do what you need and it is strongly recommended that you
- avoid using C<from> unless you cannot achieve the desired result using C<join>.
- And we really do mean "cannot", not just tried and failed. Attempting to use
- this because you're having problems with C<join> is like trying to use x86
- ASM because you've got a syntax error in your C. Trust us on this.
- Now, if you're still really, really sure you need to use this (and if you're
- not 100% sure, ask the mailing list first), here's an explanation of how this
- works.
- The syntax is as follows -
-   [
-     { <alias1> => <table1> },
-     [
-       { <alias2> => <table2>, -join_type => 'inner|left|right' },
-       [], # nested JOIN (optional)
-       { <table1.column1> => <table2.column2>, ... (more conditions) },
-     ],
-     # More of the above [ ] may follow for additional joins
-   ]
-   <table1> <alias1>
-   JOIN
-     <table2> <alias2>
-     [JOIN ...]
-   ON <table1.column1> = <table2.column2>
-   <more joins may follow>
- An easy way to follow the examples below is to remember the following:
-     Anything inside "[]" is a JOIN
-     Anything inside "{}" is a condition for the enclosing JOIN
- The following examples utilize a "person" table in a family tree application.
- In order to express parent->child relationships, this table is self-joined:
-     # Person->belongs_to('father' => 'Person');
-     # Person->belongs_to('mother' => 'Person');
- C<from> can be used to nest joins. Here we return all children with a father,
- then search against all mothers of those children:
-   $rs = $schema->resultset('Person')->search(
-       undef,
-       {
-           alias => 'mother', # alias columns in accordance with "from"
-           from => [
-               { mother => 'person' },
-               [
-                   [
-                       { child => 'person' },
-                       [
-                           { father => 'person' },
-                           { 'father.person_id' => 'child.father_id' }
-                       ]
-                   ],
-                   { 'mother.person_id' => 'child.mother_id' }
-               ],
-           ]
-       },
-   );
-   # Equivalent SQL:
-   # SELECT mother.* FROM person mother
-   # JOIN (
-   #   person child
-   #   JOIN person father
-   #   ON ( father.person_id = child.father_id )
-   # )
-   # ON ( mother.person_id = child.mother_id )
- The type of any join can be controlled manually. To search against only people
- with a father in the person table, we could explicitly use C<INNER JOIN>:
-     $rs = $schema->resultset('Person')->search(
-         undef,
-         {
-             alias => 'child', # alias columns in accordance with "from"
-             from => [
-                 { child => 'person' },
-                 [
-                     { father => 'person', -join_type => 'inner' },
-                     { 'father.id' => 'child.father_id' }
-                 ],
-             ]
-         },
-     );
-     # Equivalent SQL:
-     # SELECT child.* FROM person child
-     # INNER JOIN person father ON child.father_id = father.id
- You can select from a subquery by passing a resultset to from as follows.
-     $schema->resultset('Artist')->search( 
-         undef, 
-         {   alias => 'artist2',
-             from  => [ { artist2 => $artist_rs->as_query } ],
-         } );
-     # and you'll get sql like this..
-     # SELECT artist2.artistid, artist2.name, artist2.rank, artist2.charfield FROM 
-     #   ( SELECT me.artistid, me.name, me.rank, me.charfield FROM artists me ) artist2
- If you need to express really complex joins, you
- can supply literal SQL to C<from> via a scalar reference. In this case
- the contents of the scalar will replace the table name associated with the
- resultsource.
- WARNING: This technique might very well not work as expected on chained
- searches - you have been warned.
-     # Assuming the Event resultsource is defined as:
-         MySchema::Event->add_columns (
-             sequence => {
-                 data_type => 'INT',
-                 is_auto_increment => 1,
-             },
-             location => {
-                 data_type => 'INT',
-             },
-             type => {
-                 data_type => 'INT',
-             },
-         );
-         MySchema::Event->set_primary_key ('sequence');
-     # This will get back the latest event for every location. The column
-     # selector is still provided by DBIC, all we do is add a JOIN/WHERE
-     # combo to limit the resultset
-     $rs = $schema->resultset('Event');
-     $table = $rs->result_source->name;
-     $latest = $rs->search (
-         undef,
-         { from => \ "
-             (SELECT e1.* FROM $table e1
-                 JOIN $table e2
-                     ON e1.location = e2.location
-                     AND e1.sequence < e2.sequence
-                 WHERE e2.sequence is NULL
-             ) me",
-         },
-     );
-     # Equivalent SQL (with the DBIC chunks added):
-     SELECT me.sequence, me.location, me.type FROM
-        (SELECT e1.* FROM events e1
-            JOIN events e2
-                ON e1.location = e2.location
-                AND e1.sequence < e2.sequence
-            WHERE e2.sequence is NULL
-        ) me;
  =head2 for
  
  =over 4
@@@ -5,8 -5,9 +5,9 @@@ use warnings
  
  use DBIx::Class::ResultSet;
  use DBIx::Class::ResultSourceHandle;
+ use DBIx::Class::Exception;
  use Carp::Clan qw/^DBIx::Class/;
- use Storable;
  
  use base qw/DBIx::Class/;
  
@@@ -1195,7 -1196,7 +1196,7 @@@ sub resolve_join 
  
  # Returns the {from} structure used to express JOIN conditions
  sub _resolve_join {
-   my ($self, $join, $alias, $seen, $jpath, $force_left) = @_;
+   my ($self, $join, $alias, $seen, $jpath, $parent_force_left) = @_;
  
    # we need a supplied one, because we do in-place modifications, no returns
    $self->throw_exception ('You must supply a seen hashref as the 3rd argument to _resolve_join')
  
    $jpath = [@$jpath];
  
-   if (ref $join eq 'ARRAY') {
+   if (not defined $join) {
+     return ();
+   }
+   elsif (ref $join eq 'ARRAY') {
      return
        map {
-         $self->_resolve_join($_, $alias, $seen, $jpath, $force_left);
+         $self->_resolve_join($_, $alias, $seen, $jpath, $parent_force_left);
        } @$join;
-   } elsif (ref $join eq 'HASH') {
-     return
-       map {
-         my $as = ($seen->{$_} ? join ('_', $_, $seen->{$_} + 1) : $_);  # the actual seen value will be incremented below
-         local $force_left->{force} = $force_left->{force};
-         (
-           $self->_resolve_join($_, $alias, $seen, [@$jpath], $force_left),
-           $self->related_source($_)->_resolve_join(
-             $join->{$_}, $as, $seen, [@$jpath, $_], $force_left
-           )
-         );
-       } keys %$join;
-   } elsif (ref $join) {
-     $self->throw_exception("No idea how to resolve join reftype ".ref $join);
-   } else {
+   }
+   elsif (ref $join eq 'HASH') {
  
-     return() unless defined $join;
+     my @ret;
+     for my $rel (keys %$join) {
  
+       my $rel_info = $self->relationship_info($rel)
+         or $self->throw_exception("No such relationship ${rel}");
+       my $force_left = $parent_force_left;
+       $force_left ||= lc($rel_info->{attrs}{join_type}||'') eq 'left';
+       # the actual seen value will be incremented by the recursion
+       my $as = ($seen->{$rel} ? join ('_', $rel, $seen->{$rel} + 1) : $rel);
+       push @ret, (
+         $self->_resolve_join($rel, $alias, $seen, [@$jpath], $force_left),
+         $self->related_source($rel)->_resolve_join(
+           $join->{$rel}, $as, $seen, [@$jpath, $rel], $force_left
+         )
+       );
+     }
+     return @ret;
+   }
+   elsif (ref $join) {
+     $self->throw_exception("No idea how to resolve join reftype ".ref $join);
+   }
+   else {
      my $count = ++$seen->{$join};
      my $as = ($count > 1 ? "${join}_${count}" : $join);
  
-     my $rel_info = $self->relationship_info($join);
-     $self->throw_exception("No such relationship ${join}") unless $rel_info;
-     my $type;
-     if ($force_left) {
-       $type = 'left';
-     } else {
-       $type = $rel_info->{attrs}{join_type} || '';
-       $force_left = 1 if lc($type) eq 'left';
-     }
+     my $rel_info = $self->relationship_info($join)
+       or $self->throw_exception("No such relationship ${join}");
  
      my $rel_src = $self->related_source($join);
      return [ { $as => $rel_src->from,
                 -source_handle => $rel_src->handle,
-                -join_type => $type,
+                -join_type => $parent_force_left
+                   ? 'left'
+                   : $rel_info->{attrs}{join_type}
+                 ,
                 -join_path => [@$jpath, $join],
                 -alias => $as,
                 -relation_chain_depth => $seen->{-relation_chain_depth} || 0,
@@@ -1322,10 -1333,14 +1333,14 @@@ sub _resolve_condition 
          #warn "$self $k $for $v";
          unless ($for->has_column_loaded($v)) {
            if ($for->in_storage) {
-             $self->throw_exception(
-               "Column ${v} not loaded or not passed to new() prior to insert()"
-                 ." on ${for} trying to resolve relationship (maybe you forgot "
-                   ."to call ->discard_changes to get defaults from the db)"
+             $self->throw_exception(sprintf
+               'Unable to resolve relationship from %s to %s: column %s.%s not '
+             . 'loaded from storage (or not passed to new() prior to insert()). '
+             . 'Maybe you forgot to call ->discard_changes to get defaults from the db.',
+               $for->result_source->source_name,
+               $as,
+               $as, $v,
              );
            }
            return $UNRESOLVABLE_CONDITION;
@@@ -1392,7 -1407,19 +1407,7 @@@ sub resolve_prefetch 
          "Can't prefetch has_many ${pre} (join cond too complex)")
          unless ref($rel_info->{cond}) eq 'HASH';
        my $dots = @{[$as_prefix =~ m/\./g]} + 1; # +1 to match the ".${as_prefix}"
 -      if (my ($fail) = grep { @{[$_ =~ m/\./g]} == $dots }
 -                         keys %{$collapse}) {
 -        my ($last) = ($fail =~ /([^\.]+)$/);
 -        carp (
 -          "Prefetching multiple has_many rels ${last} and ${pre} "
 -          .(length($as_prefix)
 -            ? "at the same level (${as_prefix}) "
 -            : "at top level "
 -          )
 -          . 'will explode the number of row objects retrievable via ->next or ->all. '
 -          . 'Use at your own risk.'
 -        );
 -      }
 +
        #my @col = map { (/^self\.(.+)$/ ? ("${as_prefix}.$1") : ()); }
        #              values %{$rel_info->{cond}};
        $collapse->{".${as_prefix}${pre}"} = [ $rel_source->primary_columns ];
@@@ -1423,7 -1450,10 +1438,10 @@@ sub _resolve_prefetch 
    my ($self, $pre, $alias, $alias_map, $order, $collapse, $pref_path) = @_;
    $pref_path ||= [];
  
-   if( ref $pre eq 'ARRAY' ) {
+   if (not defined $pre) {
+     return ();
+   }
+   elsif( ref $pre eq 'ARRAY' ) {
      return
        map { $self->_resolve_prefetch( $_, $alias, $alias_map, $order, $collapse, [ @$pref_path ] ) }
          @$pre;
      $p = $p->{$_} for (@$pref_path, $pre);
  
      $self->throw_exception (
-       "Unable to resolve prefetch $pre - join alias map does not contain an entry for path: "
+       "Unable to resolve prefetch '$pre' - join alias map does not contain an entry for path: "
        . join (' -> ', @$pref_path, $pre)
      ) if (ref $p->{-join_aliases} ne 'ARRAY' or not @{$p->{-join_aliases}} );
  
          "Can't prefetch has_many ${pre} (join cond too complex)")
          unless ref($rel_info->{cond}) eq 'HASH';
        my $dots = @{[$as_prefix =~ m/\./g]} + 1; # +1 to match the ".${as_prefix}"
 -      if (my ($fail) = grep { @{[$_ =~ m/\./g]} == $dots }
 -                         keys %{$collapse}) {
 -        my ($last) = ($fail =~ /([^\.]+)$/);
 -        carp (
 -          "Prefetching multiple has_many rels ${last} and ${pre} "
 -          .(length($as_prefix)
 -            ? "at the same level (${as_prefix}) "
 -            : "at top level "
 -          )
 -          . 'will explode the number of row objects retrievable via ->next or ->all. '
 -          . 'Use at your own risk.'
 -        );
 -      }
 +
        #my @col = map { (/^self\.(.+)$/ ? ("${as_prefix}.$1") : ()); }
        #              values %{$rel_info->{cond}};
        $collapse->{".${as_prefix}${pre}"} = [ $rel_source->primary_columns ];
@@@ -1551,10 -1593,12 +1569,12 @@@ See L<DBIx::Class::Schema/"throw_except
  
  sub throw_exception {
    my $self = shift;
    if (defined $self->schema) {
      $self->schema->throw_exception(@_);
-   } else {
-     croak(@_);
+   }
+   else {
+     DBIx::Class::Exception->throw(@_);
    }
  }
  
diff --combined t/prefetch/grouped.t
@@@ -87,12 -87,12 +87,12 @@@ for ($cd_rs->all) 
      '(
        SELECT me.cd, me.track_count, cd.cdid, cd.artist, cd.title, cd.year, cd.genreid, cd.single_track
          FROM (
 -          SELECT me.cd, COUNT (me.trackid) AS track_count,
 +          SELECT me.cd, COUNT (me.trackid) AS track_count
              FROM track me
              JOIN cd cd ON cd.cdid = me.cd
            WHERE ( me.cd IN ( ?, ?, ?, ?, ? ) )
            GROUP BY me.cd
 -          ) as me
 +          ) me
          JOIN cd cd ON cd.cdid = me.cd
        WHERE ( me.cd IN ( ?, ?, ?, ?, ? ) )
      )',
                tracks.trackid, tracks.cd, tracks.position, tracks.title, tracks.last_updated_on, tracks.last_updated_at, tracks.small_dt,
                liner_notes.liner_id, liner_notes.notes
          FROM (
 -          SELECT me.cdid, COUNT( tracks.trackid ) AS track_count, MAX( tracks.trackid ) AS maxtr,
 +          SELECT me.cdid, COUNT( tracks.trackid ) AS track_count, MAX( tracks.trackid ) AS maxtr
              FROM cd me
              LEFT JOIN track tracks ON tracks.cd = me.cdid
            WHERE ( me.cdid IS NOT NULL )
      $rs->as_query,
      '(
        SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track,
 -             tags.tagid, tags.cd, tags.tag 
 +             tags.tagid, tags.cd, tags.tag
          FROM (
            SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track
              FROM cd me
    );
  }
  
+ {
+     my $cd_rs = $schema->resultset('CD')->search({}, {
+             distinct => 1,
+             join     => [qw/ tracks /],
+             prefetch => [qw/ artist /],
+         });
+     is($cd_rs->count, 5, 'complex prefetch + non-prefetching has_many join count correct');
+     is($cd_rs->all, 5, 'complex prefetch + non-prefetching has_many join number of objects correct');
+     # make sure join tracks was thrown out
+     is_same_sql_bind (
+       $cd_rs->as_query,
+       '(
+         SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track,
+                artist.artistid, artist.name, artist.rank, artist.charfield
+           FROM (
+             SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track
+               FROM cd me
+               JOIN artist artist ON artist.artistid = me.artist
+             GROUP BY me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track
+           ) me
+           JOIN artist artist ON artist.artistid = me.artist
+       )',
+       [],
+     );
+     # try the same as above, but add a condition so the tracks join can not be thrown away
+     my $cd_rs2 = $cd_rs->search ({ 'tracks.title' => { '!=' => 'ugabuganoexist' } });
+     is($cd_rs2->count, 5, 'complex prefetch + non-prefetching restricted has_many join count correct');
+     is($cd_rs2->all, 5, 'complex prefetch + non-prefetching restricted has_many join number of objects correct');
+     # the outer group_by seems like a necessary evil, if someone can figure out how to take it away
+     # without breaking compat - be my guest
+     is_same_sql_bind (
+       $cd_rs2->as_query,
+       '(
+         SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track,
+                artist.artistid, artist.name, artist.rank, artist.charfield
+           FROM (
+             SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track
+               FROM cd me
+               LEFT JOIN track tracks ON tracks.cd = me.cdid
+               JOIN artist artist ON artist.artistid = me.artist
+             WHERE ( tracks.title != ? )
+             GROUP BY me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track
+           ) me
+           LEFT JOIN track tracks ON tracks.cd = me.cdid
+           JOIN artist artist ON artist.artistid = me.artist
+         WHERE ( tracks.title != ? )
+         GROUP BY me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track,
+                  artist.artistid, artist.name, artist.rank, artist.charfield
+       )',
+       [ map { [ 'tracks.title' => 'ugabuganoexist' ] } (1 .. 2) ],
+     );
+ }
  done_testing;