From: Peter Rabbitson Date: Thu, 7 May 2009 17:28:29 +0000 (+0000) Subject: Merge 'trunk' into 'count_distinct' X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=commitdiff_plain;h=038af71cf6f6c77cb1380378d4887a7da03fe2b2;hp=e90b8592bad60e552f3a013254f9a4a68acbe138;p=dbsrgits%2FDBIx-Class-Historic.git Merge 'trunk' into 'count_distinct' r6164@Thesaurus (orig r6163): ribasushi | 2009-05-07 19:09:01 +0200 r6115@Thesaurus (orig r6114): plu | 2009-05-03 10:39:16 +0200 new branch to fix $rs->update and $rs->delete using the new as_query method r6116@Thesaurus (orig r6115): plu | 2009-05-03 10:52:07 +0200 Methods update/delete on resultset use now new as_query method to updated/delete properly on joined/prefetched resultset using a subquery. Therefore some tests have been added and some have been changed as well as the warnings around $rs->update/delete have been removed. Cheers! r6117@Thesaurus (orig r6116): plu | 2009-05-03 11:13:48 +0200 Using "is" instead of "cmp_ok" r6160@Thesaurus (orig r6159): ribasushi | 2009-05-07 11:58:14 +0200 Back out skip_parens support in as_query r6161@Thesaurus (orig r6160): ribasushi | 2009-05-07 19:00:48 +0200 This test is completely borked, needs a rewrite r6162@Thesaurus (orig r6161): ribasushi | 2009-05-07 19:07:19 +0200 Temporary fix or the IN ( ( ... ) ) problem until we get proper SQLA AST (needs SQLA released with commit 6158 to work) r6165@Thesaurus (orig r6164): ribasushi | 2009-05-07 19:11:46 +0200 Changes, remove merged branch r6169@Thesaurus (orig r6168): ribasushi | 2009-05-07 19:24:54 +0200 Bump SQLA dependency so -in/-between workarounds overload properly --- diff --git a/lib/DBIx/Class/ResultSet.pm b/lib/DBIx/Class/ResultSet.pm index e231107..7611945 100644 --- a/lib/DBIx/Class/ResultSet.pm +++ b/lib/DBIx/Class/ResultSet.pm @@ -307,7 +307,7 @@ sub search_rs { my $new_attrs = { %{$our_attrs}, %{$attrs} }; # merge new attrs into inherited - foreach my $key (qw/join prefetch +select +as/) { + foreach my $key (qw/join prefetch +select +as bind/) { next unless exists $attrs->{$key}; $new_attrs->{$key} = $self->_merge_attr($our_attrs->{$key}, $attrs->{$key}); } @@ -1151,12 +1151,6 @@ Performs an SQL C with the same query as the resultset was built with to find the number of elements. If passed arguments, does a search on the resultset and counts the results of that. -Note: When using C with C, L emulates C -using C. Some databases (notably SQLite) do -not support C with multiple columns. If you are using such a -database, you should only use columns from the main table in your C -clause. - =cut sub count { @@ -1177,32 +1171,21 @@ sub count { sub _count { # Separated out so pager can get the full count my $self = shift; - my $select = { count => '*' }; - my $attrs = { %{$self->_resolved_attrs} }; - if (my $group_by = delete $attrs->{group_by}) { - delete $attrs->{having}; - my @distinct = (ref $group_by ? @$group_by : ($group_by)); - # todo: try CONCAT for multi-column pk - my @pk = $self->result_source->primary_columns; - if (@pk == 1) { - my $alias = $attrs->{alias}; - foreach my $column (@distinct) { - if ($column =~ qr/^(?:\Q${alias}.\E)?$pk[0]$/) { - @distinct = ($column); - last; - } - } - } - $select = { count => { distinct => \@distinct } }; + if (my $group_by = $attrs->{group_by}) { + delete $attrs->{order_by}; + + $attrs->{select} = $group_by; + $attrs->{from} = [ { 'mesub' => (ref $self)->new($self->result_source, $attrs)->cursor->as_query } ]; + delete $attrs->{where}; } - $attrs->{select} = $select; + $attrs->{select} = { count => '*' }; $attrs->{as} = [qw/count/]; - # offset, order by and page are not needed to count. record_filter is cdbi - delete $attrs->{$_} for qw/rows offset order_by page pager record_filter/; + # offset, order by, group by, where and page are not needed to count. record_filter is cdbi + delete $attrs->{$_} for qw/rows offset order_by group_by page pager record_filter/; my $tmp_rs = (ref $self)->new($self->result_source, $attrs); my ($count) = $tmp_rs->cursor->next; diff --git a/lib/DBIx/Class/Storage/DBI.pm b/lib/DBIx/Class/Storage/DBI.pm index 7987c40..a1bb464 100644 --- a/lib/DBIx/Class/Storage/DBI.pm +++ b/lib/DBIx/Class/Storage/DBI.pm @@ -149,6 +149,9 @@ sub _find_syntax { sub select { my ($self, $table, $fields, $where, $order, @rest) = @_; + local $self->{having_bind} = []; + local $self->{from_bind} = []; + if (ref $table eq 'SCALAR') { $table = $$table; } @@ -160,8 +163,7 @@ sub select { @rest = (-1) unless defined $rest[0]; die "LIMIT 0 Does Not Compute" if $rest[0] == 0; # and anyway, SQL::Abstract::Limit will cause a barf if we don't first - local $self->{having_bind} = []; - my ($sql, @ret) = $self->SUPER::select( + my ($sql, @where_bind) = $self->SUPER::select( $table, $self->_recurse_fields($fields), $where, $order, @rest ); $sql .= @@ -173,7 +175,7 @@ sub select { ) : '' ; - return wantarray ? ($sql, @ret, @{$self->{having_bind}}) : $sql; + return wantarray ? ($sql, @{$self->{from_bind}}, @where_bind, @{$self->{having_bind}}) : $sql; } sub insert { @@ -227,7 +229,7 @@ sub _recurse_fields { } # Is the second check absolutely necessary? elsif ( $ref eq 'REF' and ref($$fields) eq 'ARRAY' ) { - return $self->_bind_to_sql( $fields ); + return $self->_fold_sqlbind( $fields ); } else { Carp::croak($ref . qq{ unexpected in _recurse_fields()}) @@ -320,19 +322,18 @@ sub _recurse_from { return join('', @sqlf); } -sub _bind_to_sql { - my $self = shift; - my $arr = shift; - my $sql = shift @$$arr; - $sql =~ s/\?/$self->_quote((shift @$$arr)->[1])/eg; - return $sql +sub _fold_sqlbind { + my ($self, $sqlbind) = @_; + my $sql = shift @$$sqlbind; + push @{$self->{from_bind}}, @$$sqlbind; + return $sql; } sub _make_as { my ($self, $from) = @_; - return join(' ', map { (ref $_ eq 'SCALAR' ? $$_ - : ref $_ eq 'REF' ? $self->_bind_to_sql($_) - : $self->_quote($_)) + return join(' ', map { (ref $_ eq 'SCALAR' ? $$_ + : ref $_ eq 'REF' ? $self->_fold_sqlbind($_) + : $self->_quote($_)) } reverse each %{$self->_skip_options($from)}); } diff --git a/lib/DBIx/Class/Storage/DBI/MultiDistinctEmulation.pm b/lib/DBIx/Class/Storage/DBI/MultiDistinctEmulation.pm deleted file mode 100644 index 7ab7846..0000000 --- a/lib/DBIx/Class/Storage/DBI/MultiDistinctEmulation.pm +++ /dev/null @@ -1,51 +0,0 @@ -package DBIx::Class::Storage::DBI::MultiDistinctEmulation; - -use strict; -use warnings; - -use base qw/DBIx::Class::Storage::DBI/; - -sub _select { - my ($self, $ident, $select, $condition, $attrs) = @_; - - # hack to make count distincts with multiple columns work in SQLite and Oracle - if (ref $select eq 'ARRAY') { - @{$select} = map {$self->replace_distincts($_)} @{$select}; - } else { - $select = $self->replace_distincts($select); - } - - return $self->next::method($ident, $select, $condition, $attrs); -} - -sub replace_distincts { - my ($self, $select) = @_; - - $select->{count}->{distinct} = join("||", @{$select->{count}->{distinct}}) - if (ref $select eq 'HASH' && $select->{count} && ref $select->{count} eq 'HASH' && - $select->{count}->{distinct} && ref $select->{count}->{distinct} eq 'ARRAY'); - - return $select; -} - -1; - -=head1 NAME - -DBIx::Class::Storage::DBI::MultiDistinctEmulation - Some databases can't handle count distincts with multiple cols. They should use base on this. - -=head1 SYNOPSIS - -=head1 DESCRIPTION - -This class allows count distincts with multiple columns for retarded databases (Oracle and SQLite) - -=head1 AUTHORS - -Luke Saunders - -=head1 LICENSE - -You may distribute this code under the same terms as Perl itself. - -=cut diff --git a/lib/DBIx/Class/Storage/DBI/Oracle/Generic.pm b/lib/DBIx/Class/Storage/DBI/Oracle/Generic.pm index 2e9a8c1..e3d08d0 100644 --- a/lib/DBIx/Class/Storage/DBI/Oracle/Generic.pm +++ b/lib/DBIx/Class/Storage/DBI/Oracle/Generic.pm @@ -26,7 +26,7 @@ This class implements autoincrements for Oracle. use Carp::Clan qw/^DBIx::Class/; -use base qw/DBIx::Class::Storage::DBI::MultiDistinctEmulation/; +use base qw/DBIx::Class::Storage::DBI/; # __PACKAGE__->load_components(qw/PK::Auto/); diff --git a/lib/DBIx/Class/Storage/DBI/SQLite.pm b/lib/DBIx/Class/Storage/DBI/SQLite.pm index a4aeff6..5e125c9 100644 --- a/lib/DBIx/Class/Storage/DBI/SQLite.pm +++ b/lib/DBIx/Class/Storage/DBI/SQLite.pm @@ -6,7 +6,7 @@ use POSIX 'strftime'; use File::Copy; use File::Spec; -use base qw/DBIx::Class::Storage::DBI::MultiDistinctEmulation/; +use base qw/DBIx::Class::Storage::DBI/; sub _dbh_last_insert_id { my ($self, $dbh, $source, $col) = @_; diff --git a/t/03podcoverage.t b/t/03podcoverage.t index 175542b..b82d4f7 100644 --- a/t/03podcoverage.t +++ b/t/03podcoverage.t @@ -99,7 +99,6 @@ my $exceptions = { 'DBIx::Class::Storage::DBI' => { skip => 1 }, 'DBIx::Class::Storage::DBI::DB2' => { skip => 1 }, 'DBIx::Class::Storage::DBI::MSSQL' => { skip => 1 }, - 'DBIx::Class::Storage::DBI::MultiDistinctEmulation' => { skip => 1 }, 'DBIx::Class::Storage::DBI::ODBC400' => { skip => 1 }, 'DBIx::Class::Storage::DBI::ODBC::DB2_400_SQL' => { skip => 1 }, 'DBIx::Class::Storage::DBI::Oracle' => { skip => 1 }, diff --git a/t/47bind_attribute.t b/t/47bind_attribute.t index 3bc1935..be662a2 100644 --- a/t/47bind_attribute.t +++ b/t/47bind_attribute.t @@ -3,7 +3,9 @@ use warnings; use Test::More; use lib qw(t/lib); -use DBICTest; +use DBIC::SqlMakerTest; + +use_ok('DBICTest'); my $schema = DBICTest->init_schema; @@ -11,11 +13,9 @@ BEGIN { eval "use DBD::SQLite"; plan $@ ? ( skip_all => 'needs DBD::SQLite for testing' ) - : ( tests => 7 ); + : ( tests => 9 ); } -### $schema->storage->debug(1); - my $where_bind = { where => \'name like ?', bind => [ 'Cat%' ], @@ -55,10 +55,10 @@ my $new_source = $source->new($source); $new_source->source_name('Complex'); $new_source->name(\<<''); -( select a.*, cd.cdid as cdid, cd.title as title, cd.year as year - from artist a - join cd on cd.artist=a.artistid - where cd.year=?) +( SELECT a.*, cd.cdid AS cdid, cd.title AS title, cd.year AS year + FROM artist a + JOIN cd ON cd.artist = a.artistid + WHERE cd.year = ?) $schema->register_extra_source('Complex' => $new_source); @@ -72,11 +72,22 @@ $rs = $schema->resultset('Complex')->search({}, { bind => [ 1999 ] }) ->search({ 'artistid' => 1 }); is ( $rs->count, 1, '...cookbook (bind first) + chained search' ); -TODO: { - # not sure what causes an uninit warning here, please remove when the TODO starts to pass, - # so the real reason for the warning can be found and fixed - local $SIG{__WARN__} = sub { warn @_ unless $_[0] =~ /uninitialized/ }; +{ + $rs = $schema->resultset('Complex')->search({}, { bind => [ 1999 ] })->search({}, { where => \"title LIKE ?", bind => [ 'Spoon%' ] }); + my ($sql, @bind) = @${$rs->as_query}; + is_same_sql_bind( + $sql, \@bind, + "(SELECT me.artistid, me.name, me.rank, me.charfield FROM (SELECT a.*, cd.cdid AS cdid, cd.title AS title, cd.year AS year FROM artist a JOIN cd ON cd.artist = a.artistid WHERE cd.year = ?) WHERE title LIKE ?)", + [ + [ '!!dummy' => '1999' ], + [ '!!dummy' => 'Spoon%' ] + ], + 'got correct SQL' +); +} + +TODO: { local $TODO = 'bind args order needs fixing (semifor)'; $rs = $schema->resultset('Complex')->search({}, { bind => [ 1999 ] }) ->search({ 'artistid' => 1 }, { diff --git a/t/60core.t b/t/60core.t index 011675b..de290de 100644 --- a/t/60core.t +++ b/t/60core.t @@ -8,7 +8,7 @@ use DBICTest; my $schema = DBICTest->init_schema(); -plan tests => 95; +plan tests => 96; eval { require DateTime::Format::MySQL }; my $NO_DTFM = $@ ? 1 : 0; @@ -28,7 +28,7 @@ if( $schema->storage->dbh->get_info(17) eq 'SQLite' && my @art = $schema->resultset("Artist")->search({ }, { order_by => 'name DESC'}); -cmp_ok(@art, '==', 3, "Three artists returned"); +is(@art, 3, "Three artists returned"); my $art = $art[0]; @@ -39,7 +39,7 @@ $art->name('We Are In Rehab'); is($art->name, 'We Are In Rehab', "Accessor update ok"); my %dirty = $art->get_dirty_columns(); -cmp_ok(scalar(keys(%dirty)), '==', 1, '1 dirty column'); +is(scalar(keys(%dirty)), 1, '1 dirty column'); ok(grep($_ eq 'name', keys(%dirty)), 'name is dirty'); is($art->get_column("name"), 'We Are In Rehab', 'And via get_column'); @@ -47,7 +47,7 @@ is($art->get_column("name"), 'We Are In Rehab', 'And via get_column'); ok($art->update, 'Update run'); my %not_dirty = $art->get_dirty_columns(); -cmp_ok(scalar(keys(%not_dirty)), '==', 0, 'Nothing is dirty'); +is(scalar(keys(%not_dirty)), 0, 'Nothing is dirty'); eval { my $ret = $art->make_column_dirty('name2'); @@ -55,7 +55,7 @@ eval { ok(defined($@), 'Failed to make non-existent column dirty'); $art->make_column_dirty('name'); my %fake_dirty = $art->get_dirty_columns(); -cmp_ok(scalar(keys(%fake_dirty)), '==', 1, '1 fake dirty column'); +is(scalar(keys(%fake_dirty)), 1, '1 fake dirty column'); ok(grep($_ eq 'name', keys(%fake_dirty)), 'name is fake dirty'); my $record_jp = $schema->resultset("Artist")->search(undef, { join => 'cds' })->search(undef, { prefetch => 'cds' })->next; @@ -68,15 +68,15 @@ ok($record_fn, "funny join is okay"); @art = $schema->resultset("Artist")->search({ name => 'We Are In Rehab' }); -cmp_ok(@art, '==', 1, "Changed artist returned by search"); +is(@art, 1, "Changed artist returned by search"); -cmp_ok($art[0]->artistid, '==', 3,'Correct artist too'); +is($art[0]->artistid, 3,'Correct artist too'); lives_ok (sub { $art->delete }, 'Cascading delete on Ordered has_many works' ); # real test in ordered.t @art = $schema->resultset("Artist")->search({ }); -cmp_ok(@art, '==', 2, 'And then there were two'); +is(@art, 2, 'And then there were two'); ok(!$art->in_storage, "It knows it's dead"); @@ -90,15 +90,15 @@ ok($art->in_storage, "Re-created"); @art = $schema->resultset("Artist")->search({ }); -cmp_ok(@art, '==', 3, 'And now there are three again'); +is(@art, 3, 'And now there are three again'); my $new = $schema->resultset("Artist")->create({ artistid => 4 }); -cmp_ok($new->artistid, '==', 4, 'Create produced record ok'); +is($new->artistid, 4, 'Create produced record ok'); @art = $schema->resultset("Artist")->search({ }); -cmp_ok(@art, '==', 4, "Oh my god! There's four of them!"); +is(@art, 4, "Oh my god! There's four of them!"); $new->set_column('name' => 'Man With A Fork'); @@ -152,7 +152,7 @@ is($schema->resultset("Artist")->count, 4, 'count ok'); my $cd = $schema->resultset("CD")->find(1); my %cols = $cd->get_columns; -cmp_ok(keys %cols, '==', 6, 'get_columns number of columns ok'); +is(keys %cols, 6, 'get_columns number of columns ok'); is($cols{title}, 'Spoonful of bees', 'get_columns values ok'); @@ -235,31 +235,40 @@ my $search = [ { 'tags.tag' => 'Cheesy' }, { 'tags.tag' => 'Blue' } ]; my( $or_rs ) = $schema->resultset("CD")->search_rs($search, { join => 'tags', order_by => 'cdid' }); -cmp_ok($or_rs->count, '==', 5, 'Search with OR ok'); +is($or_rs->count, 5, 'Search with OR ok'); my $distinct_rs = $schema->resultset("CD")->search($search, { join => 'tags', distinct => 1 }); -cmp_ok($distinct_rs->all, '==', 4, 'DISTINCT search with OR ok'); +is($distinct_rs->all, 4, 'DISTINCT search with OR ok'); SKIP: { skip "SQLite < 3.2.6 doesn't understand COUNT(DISTINCT())", 2 if $is_broken_sqlite; - my $tcount = $schema->resultset("Track")->search( + my $tcount = $schema->resultset('Track')->search( {}, - { - select => {count => {distinct => ['position', 'title']}}, - as => ['count'] + { + select => [ qw/position title/ ], + distinct => 1, } ); - cmp_ok($tcount->next->get_column('count'), '==', 13, 'multiple column COUNT DISTINCT ok'); + is($tcount->count, 13, 'multiple column COUNT DISTINCT ok'); - $tcount = $schema->resultset("Track")->search( + $tcount = $schema->resultset('Track')->search( {}, - { - columns => {count => {count => {distinct => ['position', 'title']}}}, + { + columns => [ qw/position title/ ], + distinct => 1, } ); - cmp_ok($tcount->next->get_column('count'), '==', 13, 'multiple column COUNT DISTINCT using column syntax ok'); + is($tcount->count, 13, 'multiple column COUNT DISTINCT ok'); + + $tcount = $schema->resultset('Track')->search( + {}, + { + group_by => [ qw/position title/ ] + } + ); + is($tcount->count, 13, 'multiple column COUNT DISTINCT using column syntax ok'); } my $tag_rs = $schema->resultset('Tag')->search( @@ -267,17 +276,17 @@ my $tag_rs = $schema->resultset('Tag')->search( my $rel_rs = $tag_rs->search_related('cd'); -cmp_ok($rel_rs->count, '==', 5, 'Related search ok'); +is($rel_rs->count, 5, 'Related search ok'); -cmp_ok($or_rs->next->cdid, '==', $rel_rs->next->cdid, 'Related object ok'); +is($or_rs->next->cdid, $rel_rs->next->cdid, 'Related object ok'); $or_rs->reset; $rel_rs->reset; my $tag = $schema->resultset('Tag')->search( [ { 'me.tag' => 'Blue' } ], { cols=>[qw/tagid/] } )->next; -cmp_ok($tag->has_column_loaded('tagid'), '==', 1, 'Has tagid loaded'); -cmp_ok($tag->has_column_loaded('tag'), '==', 0, 'Has not tag loaded'); +ok($tag->has_column_loaded('tagid'), 'Has tagid loaded'); +ok(!$tag->has_column_loaded('tag'), 'Has not tag loaded'); ok($schema->storage(), 'Storage available'); @@ -309,7 +318,7 @@ ok($schema->storage(), 'Storage available'); ok($schema->source('SourceNameArtists'), 'SourceNameArtists result source exists'); my @artsn = $schema->resultset('SourceNameArtists')->search({}, { order_by => 'name DESC' }); - cmp_ok(@artsn, '==', 4, "Four artists returned"); + is(@artsn, 4, "Four artists returned"); # make sure subclasses that don't set source_name are ok ok($schema->source('ArtistSubclass'), 'ArtistSubclass exists'); @@ -323,8 +332,8 @@ lives_ok (sub { my $newlink = $newbook->link}, "stringify to false value doesn't { my $art_del = $schema->resultset("Artist")->find({ artistid => 1 }); lives_ok (sub { $art_del->delete }, 'Cascading delete on Ordered has_many works' ); # real test in ordered.t - cmp_ok( $schema->resultset("CD")->search({artist => 1}), '==', 0, 'Cascading through has_many top level.'); - cmp_ok( $schema->resultset("CD_to_Producer")->search({cd => 1}), '==', 0, 'Cascading through has_many children.'); + is( $schema->resultset("CD")->search({artist => 1}), 0, 'Cascading through has_many top level.'); + is( $schema->resultset("CD_to_Producer")->search({cd => 1}), 0, 'Cascading through has_many children.'); } # test column_info diff --git a/t/73oracle.t b/t/73oracle.t index 51cc932..c5fe45a 100644 --- a/t/73oracle.t +++ b/t/73oracle.t @@ -39,7 +39,7 @@ plan skip_all => 'Set $ENV{DBICTEST_ORA_DSN}, _USER and _PASS to run this test. ' as well as following sequences: \'pkid1_seq\', \'pkid2_seq\' and \'nonpkid_seq\'' unless ($dsn && $user && $pass); -plan tests => 24; +plan tests => 26; DBICTest::Schema->load_classes('ArtistFQN'); my $schema = DBICTest::Schema->connect($dsn, $user, $pass); @@ -106,15 +106,32 @@ is($tjoin->next->title, 'Track1', "ambiguous column ok"); # check count distinct with multiple columns my $other_track = $schema->resultset('Track')->create({ trackid => 2, cd => 1, position => 1, title => 'Track2' }); + my $tcount = $schema->resultset('Track')->search( - {}, - { - select => [{count => {distinct => ['position', 'title']}}], - as => ['count'] - } - ); + {}, + { + select => [ qw/position title/ ], + distinct => 1, + } +); +is($tcount->count, 2, 'multiple column COUNT DISTINCT ok'); + +$tcount = $schema->resultset('Track')->search( + {}, + { + columns => [ qw/position title/ ], + distinct => 1, + } +); +is($tcount->count, 2, 'multiple column COUNT DISTINCT ok'); -is($tcount->next->get_column('count'), 2, "multiple column select distinct ok"); +$tcount = $schema->resultset('Track')->search( + {}, + { + group_by => [ qw/position title/ ] + } +); +is($tcount->count, 2, 'multiple column COUNT DISTINCT using column syntax ok'); # test LIMIT support for (1..6) { diff --git a/t/count/count_distinct.t b/t/count/count_distinct.t new file mode 100644 index 0000000..d4dd422 --- /dev/null +++ b/t/count/count_distinct.t @@ -0,0 +1,57 @@ +use strict; +use warnings; + +use Test::More; + +use lib qw(t/lib); + +use DBICTest; +use DBIC::SqlMakerTest; + +my $schema = DBICTest->init_schema(); + +eval "use DBD::SQLite"; +plan skip_all => 'needs DBD::SQLite for testing' if $@; +plan tests => 13; + +my $in_rs = $schema->resultset('Tag')->search({ tag => [ 'Blue', 'Shiny' ] }); +my $rs; + +$rs = $schema->resultset('Tag')->search({ tag => 'Blue' }); +is($rs->count, 4, 'Count without DISTINCT'); + +$rs = $schema->resultset('Tag')->search({ tag => [ 'Blue', 'Shiny' ] }, { group_by => 'tag' }); +is($rs->count, 2, 'Count with single column group_by'); + +$rs = $schema->resultset('Tag')->search({ tag => 'Blue' }, { group_by => [ qw/tag cd/ ]}); +is($rs->count, 4, 'Count with multiple column group_by'); + +$rs = $schema->resultset('Tag')->search({ tag => 'Blue' }, { distinct => 1 }); +is($rs->count, 4, 'Count with single column distinct'); + +$rs = $schema->resultset('Tag')->search({ tag => { -in => $in_rs->get_column('tag')->as_query } }); +is($rs->count, 4, 'Count with IN subquery'); + +$rs = $schema->resultset('Tag')->search({ tag => { -in => $in_rs->get_column('tag')->as_query } }, { group_by => 'tag' }); +is($rs->count, 1, 'Count with IN subquery with outside group_by'); + +$rs = $schema->resultset('Tag')->search({ tag => { -in => $in_rs->get_column('tag')->as_query } }, { distinct => 1 }); +is($rs->count, 4, 'Count with IN subquery with outside distinct'); + +$rs = $schema->resultset('Tag')->search({ tag => { -in => $in_rs->get_column('tag')->as_query } }, { distinct => 1, select => 'tag' }), +is($rs->count, 1, 'Count with IN subquery with outside distinct on a single column'); + +$rs = $schema->resultset('Tag')->search({ tag => { -in => $in_rs->search({}, { group_by => 'tag' })->get_column('tag')->as_query } }); +is($rs->count, 4, 'Count with IN subquery with single group_by'); + +$rs = $schema->resultset('Tag')->search({ tag => { -in => $in_rs->search({}, { group_by => [ qw/tag cd/ ] })->get_column('tag')->as_query } }); +is($rs->count, 4, 'Count with IN subquery with multiple group_by'); + +$rs = $schema->resultset('Tag')->search({ tag => \"= 'Blue'" }); +is($rs->count, 4, 'Count without DISTINCT, using literal SQL'); + +$rs = $schema->resultset('Tag')->search({ tag => \" IN ('Blue', 'Shiny')" }, { group_by => 'tag' }); +is($rs->count, 2, 'Count with literal SQL and single group_by'); + +$rs = $schema->resultset('Tag')->search({ tag => \" IN ('Blue', 'Shiny')" }, { group_by => [ qw/tag cd/ ] }); +is($rs->count, 6, 'Count with literal SQL and multiple group_by'); diff --git a/t/count/count_joined.t b/t/count/count_joined.t new file mode 100644 index 0000000..992d23b --- /dev/null +++ b/t/count/count_joined.t @@ -0,0 +1,18 @@ +use strict; +use warnings; + +use Test::More; + +use lib qw(t/lib); + +use DBICTest; + +plan tests => 1; + +my $schema = DBICTest->init_schema(); + +TODO: { + local $TODO = 'Needs -paren fixes in SQLA before it can work'; + my $cds = $schema->resultset("CD")->search({ cdid => 1 }, { join => { cd_to_producer => 'producer' } }); + is($cds->count, 1, "extra joins do not explode single entity count"); +} diff --git a/t/from_subquery.t b/t/from_subquery.t new file mode 100644 index 0000000..5dc91d0 --- /dev/null +++ b/t/from_subquery.t @@ -0,0 +1,192 @@ +use strict; +use warnings FATAL => 'all'; + +use Test::More; + +BEGIN { + eval "use SQL::Abstract 1.49"; + plan $@ + ? ( skip_all => "Needs SQLA 1.49+" ) + : ( tests => 8 ); +} + +use lib qw(t/lib); +use DBICTest; +use DBIC::SqlMakerTest; + +my $schema = DBICTest->init_schema(); +my $art_rs = $schema->resultset('Artist'); +my $cdrs = $schema->resultset('CD'); + +{ + my $cdrs2 = $cdrs->search({ + artist_id => { 'in' => $art_rs->search({}, { rows => 1 })->get_column( 'id' )->as_query }, + }); + + my $arr = $cdrs2->as_query; + my ($query, @bind) = @{$$arr}; + is_same_sql_bind( + $query, \@bind, + "(SELECT me.cdid,me.artist,me.title,me.year,me.genreid,me.single_track FROM cd me WHERE artist_id IN ( SELECT id FROM artist me LIMIT 1 ))", + [], + ); +} + +{ + my $rs = $art_rs->search( + {}, + { + 'select' => [ + $cdrs->search({}, { rows => 1 })->get_column('id')->as_query, + ], + }, + ); + + my $arr = $rs->as_query; + my ($query, @bind) = @{$$arr}; + is_same_sql_bind( + $query, \@bind, + "(SELECT (SELECT id FROM cd me LIMIT 1) FROM artist me)", + [], + ); +} + +{ + my $rs = $art_rs->search( + {}, + { + '+select' => [ + $cdrs->search({}, { rows => 1 })->get_column('id')->as_query, + ], + }, + ); + + my $arr = $rs->as_query; + my ($query, @bind) = @{$$arr}; + is_same_sql_bind( + $query, \@bind, + "(SELECT me.artistid, me.name, me.rank, me.charfield, (SELECT id FROM cd me LIMIT 1) FROM artist me)", + [], + ); +} + +# simple from +{ + my $rs = $cdrs->search( + {}, + { + alias => 'cd2', + from => [ + { cd2 => $cdrs->search({ id => { '>' => 20 } })->as_query }, + ], + }, + ); + + my $arr = $rs->as_query; + my ($query, @bind) = @{$$arr}; + is_same_sql_bind( + $query, \@bind, + "(SELECT cd2.cdid, cd2.artist, cd2.title, cd2.year, cd2.genreid, cd2.single_track FROM (SELECT me.cdid,me.artist,me.title,me.year,me.genreid,me.single_track FROM cd me WHERE ( id > ? ) ) cd2)", + [ + [ 'id', 20 ] + ], + ); +} + +# nested from +{ + my $art_rs2 = $schema->resultset('Artist')->search({}, + { + from => [ { 'me' => 'artist' }, + [ { 'cds' => $cdrs->search({},{ 'select' => [\'me.artist as cds_artist' ]})->as_query }, + { 'me.artistid' => 'cds_artist' } ] ] + }); + + my $arr = $art_rs2->as_query; + my ($query, @bind) = @{$$arr}; + is_same_sql_bind( + $query, \@bind, + "(SELECT me.artistid, me.name, me.rank, me.charfield FROM artist me JOIN (SELECT me.artist as cds_artist FROM cd me) cds ON me.artistid = cds_artist)", + [] + ); + + +} + +# nested subquery in from +{ + my $rs = $cdrs->search( + {}, + { + alias => 'cd2', + from => [ + { cd2 => $cdrs->search( + { id => { '>' => 20 } }, + { + alias => 'cd3', + from => [ + { cd3 => $cdrs->search( { id => { '<' => 40 } } )->as_query } + ], + }, )->as_query }, + ], + }, + ); + + my $arr = $rs->as_query; + my ($query, @bind) = @{$$arr}; + is_same_sql_bind( + $query, \@bind, + "(SELECT cd2.cdid, cd2.artist, cd2.title, cd2.year, cd2.genreid, cd2.single_track + FROM + (SELECT cd3.cdid,cd3.artist,cd3.title,cd3.year,cd3.genreid,cd3.single_track + FROM + (SELECT me.cdid,me.artist,me.title,me.year,me.genreid,me.single_track + FROM cd me WHERE ( id < ? ) ) cd3 + WHERE ( id > ? ) ) cd2)", + [ + [ 'id', 40 ], + [ 'id', 20 ] + ], + ); + +} + +{ + my $rs = $cdrs->search({ + year => { + '=' => $cdrs->search( + { artistid => { '=' => \'me.artistid' } }, + { alias => 'inner' } + )->get_column('year')->max_rs->as_query, + }, + }); + my $arr = $rs->as_query; + my ($query, @bind) = @{$$arr}; + is_same_sql_bind( + $query, \@bind, + "(SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track FROM cd me WHERE year = (SELECT MAX(inner.year) FROM cd inner WHERE artistid = me.artistid))", + [], + ); +} + +{ + my $rs = $cdrs->search( + {}, + { + alias => 'cd2', + from => [ + { cd2 => $cdrs->search({ title => 'Thriller' })->as_query }, + ], + }, + ); + + my $arr = $rs->as_query; + my ($query, @bind) = @{$$arr}; + is_same_sql_bind( + $query, \@bind, + "(SELECT cd2.cdid, cd2.artist, cd2.title, cd2.year, cd2.genreid, cd2.single_track FROM (SELECT me.cdid,me.artist,me.title,me.year,me.genreid,me.single_track FROM cd me WHERE ( title = ? ) ) cd2)", + [ [ 'title', 'Thriller' ] ], + ); +} + +__END__ diff --git a/t/resultset/as_query.t b/t/resultset/as_query.t index c496085..f849f7a 100644 --- a/t/resultset/as_query.t +++ b/t/resultset/as_query.t @@ -7,7 +7,7 @@ use Data::Dumper; use Test::More; -plan ( tests => 4 ); +plan ( tests => 5 ); use lib qw(t/lib); use DBICTest; @@ -65,3 +65,13 @@ my $rscol = $art_rs->get_column( 'charfield' ); [ [ rank => 2 ], [ name => 'Billy Joel' ] ], ); } + +TODO: { + local $TODO = 'Needs -paren fixes in SQLA before it can work'; + my $rs = $schema->resultset("CD")->search( + { 'artist.name' => 'Caterwauler McCrae' }, + { join => [qw/artist/]} + ); + my $subsel_rs = $schema->resultset("CD")->search( { cdid => { IN => $rs->get_column('cdid')->as_query } } ); + cmp_ok($subsel_rs->count, '==', $rs->count, 'Subselect on PK got the same row count'); +} diff --git a/t/search/subquery.t b/t/search/subquery.t index b18bfa6..2d96605 100644 --- a/t/search/subquery.t +++ b/t/search/subquery.t @@ -7,7 +7,7 @@ use Data::Dumper; use Test::More; -plan ( tests => 7 ); +plan ( tests => 8 ); use lib qw(t/lib); use DBICTest; @@ -85,8 +85,10 @@ my $cdrs = $schema->resultset('CD'); my ($query, @bind) = @{$$arr}; is_same_sql_bind( $query, \@bind, - "( SELECT cd2.cdid, cd2.artist, cd2.title, cd2.year, cd2.genreid, cd2.single_track FROM (SELECT me.cdid,me.artist,me.title,me.year,me.genreid,me.single_track FROM cd me WHERE id > 20) cd2 )", - [], + "( SELECT cd2.cdid, cd2.artist, cd2.title, cd2.year, cd2.genreid, cd2.single_track FROM (SELECT me.cdid,me.artist,me.title,me.year,me.genreid,me.single_track FROM cd me WHERE id > ?) cd2 )", + [ + [ 'id', 20 ] + ], ); } @@ -137,10 +139,13 @@ my $cdrs = $schema->resultset('CD'); (SELECT cd3.cdid,cd3.artist,cd3.title,cd3.year,cd3.genreid,cd3.single_track FROM (SELECT me.cdid,me.artist,me.title,me.year,me.genreid,me.single_track - FROM cd me WHERE id < 40) cd3 - WHERE id > 20) cd2 + FROM cd me WHERE id < ?) cd3 + WHERE id > ?) cd2 )", - [], + [ + [ 'id', 40 ], + [ 'id', 20 ] + ], ); } @@ -163,4 +168,28 @@ my $cdrs = $schema->resultset('CD'); ); } +{ + my $rs = $cdrs->search( + {}, + { + alias => 'cd2', + from => [ + { cd2 => $cdrs->search({ title => 'Thriller' })->as_query }, + ], + }, + ); + + my $arr = $rs->as_query; + my ($query, @bind) = @{$$arr}; + is_same_sql_bind( + $query, \@bind, + "(SELECT cd2.cdid, cd2.artist, cd2.title, cd2.year, cd2.genreid, cd2.single_track FROM (SELECT me.cdid,me.artist,me.title,me.year,me.genreid,me.single_track FROM cd me WHERE title = ?) cd2)", + [ + [ 'title', + 'Thriller' + ] + ], + ); +} + __END__