Skip to content

Commit

Permalink
Merge 1254666 into ba9ab1b
Browse files Browse the repository at this point in the history
  • Loading branch information
mzealey committed Dec 29, 2018
2 parents ba9ab1b + 1254666 commit 17fa86b
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 0 deletions.
96 changes: 96 additions & 0 deletions lib/DBIx/Class/Helper/ResultSet/Exists.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package DBIx::Class::Helper::ResultSet::Exists;

# ABSTRACT: Allow EXISTS/NOT EXISTS subqueries with DBIx::Class

use strict;
use warnings;

use parent 'DBIx::Class::ResultSet';

sub _handle_exists {
my ( $self, $exists, $exists_subq, $join ) = @_;

die "Calling ->exists without a join query doesn't make any sense" unless $join && %$join;

die "You need to specify an alias on your exists subquery to allow joining with the main query" unless $exists_subq->{attrs}{alias} ne $self->{attrs}{alias};

my %constraints;

# We could have used something like
# ResultSource->_resolve_relationship_condition but this would not be so
# flexible as we may want to be doing the exists against joined tables'
# relationships in the main query
while ( my ( $self, $foreign ) = each %$join ) {
$constraints{$self} = { -ident => $foreign };
}

# Don't fetch all the columns - just fetch a 1...
$exists_subq = $exists_subq->search_rs(
\%constraints,
{
select => \'1',
}
);

return $self->search_rs({ $exists => $exists_subq->as_query });
}

sub exists { shift->_handle_exists( -exists => @_ ) }
sub not_exists { shift->_handle_exists( -not_exists => @_ ) }

1;

=pod
=head1 DESCRIPTION
Generate (NOT) EXISTS clauses in DBIx::Class syntax.
JOIN allows you to select a set of rows in one table based on parameters in a
second table, however if it is a one-to-many join then you will get duplicates
of some rows if you query like that.
In situations like these, you can use something like:
column => { -in => $other_table->get_column( ... )->as_query }
however method does not work if you are trying to do it on a composite key
field.
The correct SQL way is to use EXISTS ( subquery ) where subquery returns a true
or false value and can reference out to the surrounding tables, however
DBIx::Class doesn't have any natural support for this. Thats where this module
comes in.
=head1 METHODS
=head2 exists
$rs = $rs->exists(
$dbic->resultset('User')->search( user_criteria, { alias => 'exists_query' } ),
{ username => 'me.username' }
);
Generates something like:
WHERE
...
AND EXISTS (
SELECT 1
FROM user AS exists_query
WHERE
user_criteria
AND exists_query.username = me.username
)
You must pass an alias option to the exists subquery so that the join condition
can reference the main query, otherwise they will both be called 'me' by
default. You also need to specify a join condition otherwise it makes no sense
to do an EXISTS query.
=head2 not_exists
Like C<exists> but generates NOT EXISTS ( subquery ).
=cut

33 changes: 33 additions & 0 deletions t/ResultSet/Exists.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!perl

use strict;
use warnings;

use lib 't/lib';
use Test::More;
use Test::Deep;
use Test::Fatal 'exception';

use TestSchema;
my $schema = TestSchema->deploy_or_connect();
$schema->prepopulate;

my $rs2 = $schema->resultset('Foo')->search({ id => { '>=' => 3 } });
my $rs3 = $schema->resultset('Foo')->search({ id => [ 1, 3 ] }, { alias => 'rs3' });

cmp_deeply [ sort map $_->id, $rs2->exists($rs3, { id => 'me.id' })->all ], [3],
'exists returns correct values';

cmp_deeply [ sort map $_->id, $rs2->not_exists($rs3, { id => 'me.id' })->all ], [4,5],
'not_exists returns correct values';

like exception { $rs2->exists($rs2)->all } => qr/without a join query doesn't make any sense/,
'non-existent join query should throw exception';

like exception { $rs2->exists($rs2, {})->all } => qr/without a join query doesn't make any sense/,
'empty join query should throw exception';

like exception { $rs2->exists($rs2, { id => 'me.id' })->all } => qr/specify an alias/,
'exists with same alias throws exception';

done_testing;
1 change: 1 addition & 0 deletions t/lib/TestSchema/ResultSet/Foo.pm
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use parent 'TestSchema::ResultSet';
__PACKAGE__->load_components(qw{
Helper::ResultSet::Bare
Helper::ResultSet::RemoveColumns
Helper::ResultSet::Exists
Helper::ResultSet::Union
Helper::ResultSet::Random
Helper::ResultSet::ResultClassDWIM
Expand Down

0 comments on commit 17fa86b

Please sign in to comment.