Skip to content

Commit

Permalink
Add withExists method to QueriesRelationships (#37302)
Browse files Browse the repository at this point in the history
  • Loading branch information
bastien-phi authored May 14, 2021
1 parent 5d0d6cf commit 11387ec
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 6 deletions.
32 changes: 26 additions & 6 deletions src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php
Original file line number Diff line number Diff line change
Expand Up @@ -387,9 +387,11 @@ public function withAggregate($relations, $column, $function = null)
? "{$relation->getRelationCountHash(false)}.$column"
: $column;

$expression = sprintf('%s(%s)', $function, $this->getQuery()->getGrammar()->wrap(
$wrappedColumn = $this->getQuery()->getGrammar()->wrap(
$column === '*' ? $column : $relation->getRelated()->qualifyColumn($hashedColumn)
));
);

$expression = $function === 'exists' ? $wrappedColumn : sprintf('%s(%s)', $function, $wrappedColumn);
} else {
$expression = $column;
}
Expand Down Expand Up @@ -423,10 +425,17 @@ public function withAggregate($relations, $column, $function = null)
preg_replace('/[^[:alnum:][:space:]_]/u', '', "$name $function $column")
);

$this->selectSub(
$function ? $query : $query->limit(1),
$alias
);
if ($function === 'exists') {
$this->selectRaw(
sprintf('exists(%s) as %s', $query->toSql(), $this->getQuery()->grammar->wrap($alias)),
$query->getBindings()
)->withCasts([$alias => 'bool']);
} else {
$this->selectSub(
$function ? $query : $query->limit(1),
$alias
);
}
}

return $this;
Expand Down Expand Up @@ -491,6 +500,17 @@ public function withAvg($relation, $column)
return $this->withAggregate($relation, $column, 'avg');
}

/**
* Add subselect queries to include the existence of related models.
*
* @param string|array $relation
* @return $this
*/
public function withExists($relation)
{
return $this->withAggregate($relation, '*', 'exists');
}

/**
* Add the "has" condition where clause to the query.
*
Expand Down
102 changes: 102 additions & 0 deletions tests/Database/DatabaseEloquentBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -992,6 +992,95 @@ public function testWithCountMultipleAndPartialRename()
$this->assertSame('select "eloquent_builder_test_model_parent_stubs".*, (select count(*) from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id") as "foo_bar", (select count(*) from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id") as "foo_count" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
}

public function testWithExists()
{
$model = new EloquentBuilderTestModelParentStub;

$builder = $model->withExists('foo');

$this->assertSame('select "eloquent_builder_test_model_parent_stubs".*, exists(select * from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id") as "foo_exists" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
}

public function testWithExistsAndSelect()
{
$model = new EloquentBuilderTestModelParentStub;

$builder = $model->select('id')->withExists('foo');

$this->assertSame('select "id", exists(select * from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id") as "foo_exists" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
}

public function testWithExistsAndMergedWheres()
{
$model = new EloquentBuilderTestModelParentStub;

$builder = $model->select('id')->withExists(['activeFoo' => function ($q) {
$q->where('bam', '>', 'qux');
}]);

$this->assertSame('select "id", exists(select * from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id" and "bam" > ? and "active" = ?) as "active_foo_exists" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
$this->assertEquals(['qux', true], $builder->getBindings());
}

public function testWithExistsAndGlobalScope()
{
$model = new EloquentBuilderTestModelParentStub;
EloquentBuilderTestModelCloseRelatedStub::addGlobalScope('withExists', function ($query) {
return $query->addSelect('id');
});

$builder = $model->select('id')->withExists(['foo']);

// Remove the global scope so it doesn't interfere with any other tests
EloquentBuilderTestModelCloseRelatedStub::addGlobalScope('withExists', function ($query) {
//
});

$this->assertSame('select "id", exists(select * from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id") as "foo_exists" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
}

public function testWithExistsOnBelongsToMany()
{
$model = new EloquentBuilderTestModelParentStub;

$builder = $model->withExists('roles');

$this->assertSame('select "eloquent_builder_test_model_parent_stubs".*, exists(select * from "eloquent_builder_test_model_far_related_stubs" inner join "user_role" on "eloquent_builder_test_model_far_related_stubs"."id" = "user_role"."related_id" where "eloquent_builder_test_model_parent_stubs"."id" = "user_role"."self_id") as "roles_exists" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
}

public function testWithExistsOnSelfRelated()
{
$model = new EloquentBuilderTestModelSelfRelatedStub;

$sql = $model->withExists('childFoos')->toSql();

// alias has a dynamic hash, so replace with a static string for comparison
$alias = 'self_alias_hash';
$aliasRegex = '/\b(laravel_reserved_\d)(\b|$)/i';

$sql = preg_replace($aliasRegex, $alias, $sql);

$this->assertSame('select "self_related_stubs".*, exists(select * from "self_related_stubs" as "self_alias_hash" where "self_related_stubs"."id" = "self_alias_hash"."parent_id") as "child_foos_exists" from "self_related_stubs"', $sql);
}

public function testWithExistsAndRename()
{
$model = new EloquentBuilderTestModelParentStub;

$builder = $model->withExists('foo as foo_bar');

$this->assertSame('select "eloquent_builder_test_model_parent_stubs".*, exists(select * from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id") as "foo_bar" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
}

public function testWithExistsMultipleAndPartialRename()
{
$model = new EloquentBuilderTestModelParentStub;

$builder = $model->withExists(['foo as foo_bar', 'foo']);

$this->assertSame('select "eloquent_builder_test_model_parent_stubs".*, exists(select * from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id") as "foo_bar", exists(select * from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id") as "foo_exists" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
}

public function testHasWithConstraintsAndHavingInSubquery()
{
$model = new EloquentBuilderTestModelParentStub;
Expand Down Expand Up @@ -1061,6 +1150,19 @@ public function testWithCountAndConstraintsWithBindingInSelectSub()
$this->assertSame([], $builder->getBindings());
}

public function testWithExistsAndConstraintsWithBindingInSelectSub()
{
$model = new EloquentBuilderTestModelParentStub;

$builder = $model->newQuery();
$builder->withExists(['foo' => function ($q) use ($model) {
$q->selectSub($model->newQuery()->where('bam', '=', 3)->selectRaw('count(0)'), 'bam_3_count');
}]);

$this->assertSame('select "eloquent_builder_test_model_parent_stubs".*, exists(select * from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id") as "foo_exists" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
$this->assertSame([], $builder->getBindings());
}

public function testHasNestedWithConstraints()
{
$model = new EloquentBuilderTestModelParentStub;
Expand Down

0 comments on commit 11387ec

Please sign in to comment.