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

[5.3] [WIP] Add relationship subqueries for avg, max, min and sum #16815

Closed
wants to merge 16 commits into from
Closed
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
103 changes: 92 additions & 11 deletions src/Illuminate/Database/Eloquent/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -1069,49 +1069,130 @@ public function without($relations)
return $this;
}

/**
* Add subselect queries to average a column value of a relation.
*
* @param mixed $relation
* @param string $column
* @return $this
*/
public function withAvg($relation, $column)
{
return $this->withAggregate('avg', $relation, $column);
}

/**
* Add subselect queries to count the relations.
*
* @param mixed $relations
* @return $this
*/
public function withCount($relations)
{
$relations = is_array($relations) ? $relations : func_get_args();

return $this->withRaw($relations, new Expression('count(*)'), 'count');
}

/**
* Add subselect queries to get max column value of a relation.
*
* @param mixed $relation
* @param string $column
* @return $this
*/
public function withMax($relation, $column)
{
return $this->withAggregate('max', $relation, $column);
}

/**
* Add subselect queries to get min column value of a relation.
*
* @param mixed $relation
* @param string $column
* @return $this
*/
public function withMin($relation, $column)
{
return $this->withAggregate('min', $relation, $column);
}

/**
* Add subselect queries to sum a column value of a relation.
*
* @param mixed $relation
* @param string $column
* @return $this
*/
public function withSum($relation, $column)
{
return $this->withAggregate('sum', $relation, $column);
}

/**
* Add subselect queries to aggregate a column value of a relation.
*
* @param string $aggregate
* @param mixed $relation
* @param string $column
* @return $this
*/
protected function withAggregate($aggregate, $relation, $column)
{
return $this->withRaw($relation, new Expression($aggregate.'('.$this->query->getGrammar()->wrap($column).')'), $aggregate);
}

/**
* Add subselect queries to aggregate all rows or a column value of a relation.
*
* @param array|mixed $relations
* @param mixed $expression
* @param string $suffix
* @return $this
*/
public function withRaw($relations, $expression, $suffix = null)
{
if (is_null($this->query->columns)) {
$this->query->select(['*']);
}

$relations = is_array($relations) ? $relations : func_get_args();
$relations = is_array($relations) ? $relations : [$relations];

foreach ($this->parseWithRelations($relations) as $name => $constraints) {
// First we will determine if the name has been aliased using an "as" clause on the name
// and if it has we will extract the actual relationship name and the desired name of
// the resulting column. This allows multiple counts on the same relationship name.
$segments = explode(' ', $name);

unset($alias);

if (count($segments) == 3 && Str::lower($segments[1]) == 'as') {
list($name, $alias) = [$segments[0], $segments[2]];
}

$relation = $this->getHasRelationQuery($name);

// Here we will get the relationship count query and prepare to add it to the main query
// as a sub-select. First, we'll get the "has" query and use that to get the relation
// count query. We will normalize the relation name then append _count as the name.
$query = $relation->getRelationCountQuery(
$relation->getRelated()->newQuery(), $this
// Here we will get the relationship query and prepare to add it to the main query
// as a sub-select. First, we'll get the "has" query and use that to get the raw relation
// query.
$query = $relation->getRelationQuery(
$relation->getRelated()->newQuery(), $this, $expression
);

$query->callScope($constraints);

$query->mergeModelDefinedRelationConstraints($relation->getQuery());

// Finally we will add the proper result column alias to the query and run the subselect
// statement against the query builder. Then we will return the builder instance back
// to the developer for further constraint chaining that needs to take place on it.
$column = snake_case(isset($alias) ? $alias : $name).'_count';
// statement against the query builder. If the alias has not been set, we will normalize
// the relation name then append _$suffix as the name. Then we will return the builder
// instance back to the developer for further constraint chaining that needs to take
// place on it.
if (isset($alias)) {
$column = snake_case($alias);
unset($alias);
} else {
$column = snake_case($name.'_'.(is_null($suffix) ? 'aggregate' : $suffix));
}

$this->selectSub($query->toBase(), $column);
}
Expand Down
212 changes: 210 additions & 2 deletions tests/Database/DatabaseEloquentBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,58 @@ public function testDeleteOverride()
$this->assertEquals(['foo' => $builder], $builder->delete());
}

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

$builder = $model->withAvg('foo', 'bar');

$this->assertEquals('select *, (select avg("bar") 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_avg" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
}

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

$builder = $model->select('id')->withAvg('foo', 'bar');

$this->assertEquals('select "id", (select avg("bar") 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_avg" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
}

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

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

$this->assertEquals('select "id", (select avg("bar") 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_avg" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
$this->assertEquals(['qux', true], $builder->getBindings());
}

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

$builder = $model->where('bar', 'baz');
$builder->withAvg(['foo' => function ($q) {
$q->where('bam', '>', 'qux');
}], 'bar')->having('foo_avg', '>=', 1);

$this->assertEquals('select *, (select avg("bar") 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" > ?) as "foo_avg" from "eloquent_builder_test_model_parent_stubs" where "bar" = ? having "foo_avg" >= ?', $builder->toSql());
$this->assertEquals(['qux', 'baz', 1], $builder->getBindings());
}

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

$builder = $model->withAvg('foo as foo_bar', 'bar');

$this->assertEquals('select *, (select avg("bar") 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 testWithCount()
{
$model = new EloquentBuilderTestModelParentStub;
Expand Down Expand Up @@ -630,7 +682,7 @@ public function testWithCountAndRename()

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

$this->assertEquals('select *, (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_count" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
$this->assertEquals('select *, (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" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
}

public function testWithCountMultipleAndPartialRename()
Expand All @@ -639,7 +691,163 @@ public function testWithCountMultipleAndPartialRename()

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

$this->assertEquals('select *, (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_count", (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());
$this->assertEquals('select *, (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 testWithMax()
{
$model = new EloquentBuilderTestModelParentStub;

$builder = $model->withMax('foo', 'bar');

$this->assertEquals('select *, (select max("bar") 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_max" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
}

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

$builder = $model->select('id')->withMax('foo', 'bar');

$this->assertEquals('select "id", (select max("bar") 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_max" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
}

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

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

$this->assertEquals('select "id", (select max("bar") 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_max" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
$this->assertEquals(['qux', true], $builder->getBindings());
}

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

$builder = $model->where('bar', 'baz');
$builder->withMax(['foo' => function ($q) {
$q->where('bam', '>', 'qux');
}], 'bar')->having('foo_max', '>=', 1);

$this->assertEquals('select *, (select max("bar") 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" > ?) as "foo_max" from "eloquent_builder_test_model_parent_stubs" where "bar" = ? having "foo_max" >= ?', $builder->toSql());
$this->assertEquals(['qux', 'baz', 1], $builder->getBindings());
}

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

$builder = $model->withMax('foo as foo_bar', 'bar');

$this->assertEquals('select *, (select max("bar") 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 testWithMin()
{
$model = new EloquentBuilderTestModelParentStub;

$builder = $model->withMin('foo', 'bar');

$this->assertEquals('select *, (select min("bar") 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_min" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
}

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

$builder = $model->select('id')->withMin('foo', 'bar');

$this->assertEquals('select "id", (select min("bar") 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_min" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
}

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

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

$this->assertEquals('select "id", (select min("bar") 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_min" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
$this->assertEquals(['qux', true], $builder->getBindings());
}

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

$builder = $model->where('bar', 'baz');
$builder->withMin(['foo' => function ($q) {
$q->where('bam', '>', 'qux');
}], 'bar')->having('foo_min', '>=', 1);

$this->assertEquals('select *, (select min("bar") 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" > ?) as "foo_min" from "eloquent_builder_test_model_parent_stubs" where "bar" = ? having "foo_min" >= ?', $builder->toSql());
$this->assertEquals(['qux', 'baz', 1], $builder->getBindings());
}

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

$builder = $model->withMin('foo as foo_bar', 'bar');

$this->assertEquals('select *, (select min("bar") 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 testWithSum()
{
$model = new EloquentBuilderTestModelParentStub;

$builder = $model->withSum('foo', 'bar');

$this->assertEquals('select *, (select sum("bar") 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_sum" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
}

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

$builder = $model->select('id')->withSum('foo', 'bar');

$this->assertEquals('select "id", (select sum("bar") 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_sum" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
}

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

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

$this->assertEquals('select "id", (select sum("bar") 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_sum" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
$this->assertEquals(['qux', true], $builder->getBindings());
}

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

$builder = $model->where('bar', 'baz');
$builder->withSum(['foo' => function ($q) {
$q->where('bam', '>', 'qux');
}], 'bar')->having('foo_sum', '>=', 1);

$this->assertEquals('select *, (select sum("bar") 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" > ?) as "foo_sum" from "eloquent_builder_test_model_parent_stubs" where "bar" = ? having "foo_sum" >= ?', $builder->toSql());
$this->assertEquals(['qux', 'baz', 1], $builder->getBindings());
}

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

$builder = $model->withSum('foo as foo_bar', 'bar');

$this->assertEquals('select *, (select sum("bar") 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 testHasWithContraintsAndHavingInSubquery()
Expand Down