Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.x] Add withExists method to QueriesRelationships #37302

Merged
merged 1 commit into from
May 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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