From: Peter Rabbitson Date: Mon, 21 Sep 2009 11:34:26 +0000 (+0000) Subject: Merge 'trunk' into 'prefetch' X-Git-Tag: v0.08240~39 X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=commitdiff_plain;h=8e84e727440f2d5f9f6fdc5271aa48c094ab0df6;hp=-c;p=dbsrgits%2FDBIx-Class.git Merge 'trunk' into 'prefetch' 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 --- 8e84e727440f2d5f9f6fdc5271aa48c094ab0df6 diff --combined lib/DBIx/Class/ResultSet.pm index 851e269,45e838f..ac349bb --- a/lib/DBIx/Class/ResultSet.pm +++ b/lib/DBIx/Class/ResultSet.pm @@@ -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 instead. + If you want objects to be saved immediately, use L + instead. - B: C 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 - will cause L to attempt to search for a row with a value of - I. + B: Take care when using C 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, even when set to C. =cut @@@ -2321,6 -2284,19 +2327,19 @@@ Cresultset. 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 + or L depending on how early in the + L 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: C 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 - will cause L to attempt to search for a row with a value of - I. + B: Take care when using C 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, even when set to C. See also L and L. For information on how to declare unique constraints, see L. @@@ -2437,11 -2413,11 +2456,11 @@@ If the C is specified as C and L. For information on how to declare unique constraints, see L. - B: C 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 will cause L to attempt to - search for a row with a value of I. + B: Take care when using C 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, even when set to C. =cut @@@ -2498,7 -2474,13 +2517,13 @@@ For example $cd->insert; } - See also L, L and L. + B: Take care when using C 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, even when set to C. + + See also L, L and L. =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 { @@@ -2883,7 -2876,7 +2919,7 @@@ 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} || {}; @@@ -2922,7 -2915,12 +2958,12 @@@ # 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_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. - =head2 from - - =over 4 - - =item Value: \@from_clause - - =back - - The C attribute gives you manual control over the C clause of SQL - statements generated by L, allowing you to express custom C - clauses. - - NOTE: Use this on your own risk. This allows you to shoot off your foot! - - C will usually do what you need and it is strongly recommended that you - avoid using C unless you cannot achieve the desired result using C. - And we really do mean "cannot", not just tried and failed. Attempting to use - this because you're having problems with C 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 - - - [ - { => }, - [ - { => , -join_type => 'inner|left|right' }, - [], # nested JOIN (optional) - { => , ... (more conditions) }, - ], - # More of the above [ ] may follow for additional joins - ] - - - JOIN - - [JOIN ...] - ON = - - - 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 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: - - $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 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 diff --combined lib/DBIx/Class/ResultSource.pm index 233e97f,fa08fae..cce1d77 --- a/lib/DBIx/Class/ResultSource.pm +++ b/lib/DBIx/Class/ResultSource.pm @@@ -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') @@@ -1206,46 -1207,56 +1207,56 @@@ $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; @@@ -1446,7 -1476,7 +1464,7 @@@ $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}} ); @@@ -1464,7 -1494,19 +1482,7 @@@ "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 Lschema) { $self->schema->throw_exception(@_); - } else { - croak(@_); + } + else { + DBIx::Class::Exception->throw(@_); } } diff --combined t/prefetch/grouped.t index baa8fea,7f97943..25e0e34 --- a/t/prefetch/grouped.t +++ b/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 ( ?, ?, ?, ?, ? ) ) )', @@@ -166,7 -166,7 +166,7 @@@ 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 ) @@@ -217,7 -217,7 +217,7 @@@ $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 @@@ -271,4 -271,62 +271,62 @@@ ); } + { + 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;