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 withColumn() to QueriesRelationships to support min, max, sum and… #34965

Merged
merged 1 commit into from
Oct 27, 2020
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
89 changes: 76 additions & 13 deletions src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php
Original file line number Diff line number Diff line change
Expand Up @@ -348,12 +348,15 @@ public function orWhereDoesntHaveMorph($relation, $types, Closure $callback = nu
}

/**
* Add subselect queries to count the relations.
* Add subselect queries to include the given relation's column.
* Also, apply the SQL function on that column if provided.
*
* @param mixed $relations
* @param mixed $relation
* @param string $column
* @param string $function
* @return $this
*/
public function withCount($relations)
public function withColumn($relations, $column, $function = null)
{
if (empty($relations)) {
return $this;
Expand All @@ -363,12 +366,12 @@ public function withCount($relations)
$this->query->select([$this->query->from.'.*']);
}

$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.
// the resulting column.
$segments = explode(' ', $name);

unset($alias);
Expand All @@ -379,12 +382,13 @@ public function withCount($relations)

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

// Here we will get the relationship count query and prepare to add it to the main query
// Here we will get the relationship sub-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->getRelationExistenceCountQuery(
$relation->getRelated()->newQuery(), $this
);
// sub-query.
// We will normalize the relation name then append _$column as the name if the $alias was not given.
$query = $relation->getRelationExistenceQuery(
$relation->getRelated()->newQuery(), $this, new Expression($function ? "$function($column)" : $column)
)->setBindings([], 'select');

$query->callScope($constraints);

Expand All @@ -400,17 +404,76 @@ public function withCount($relations)
$query->bindings['select'] = [];
}

// Finally we will add the proper result column alias to the query and run the subselect
// Finally we will make the proper 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 = $alias ?? Str::snake($name.'_count');
$alias = $alias ?? Str::snake(preg_replace('/[^[:alnum:][:space:]_]/u', '', "$name $function $column"));

$this->selectSub($query, $column);
$this->selectSub($query->limit(1), $alias);
}

return $this;
}

/**
* Add subselect queries to count the relations.
*
* @param mixed $relations
* @return $this
*/
public function withCount($relations)
{
return $this->withColumn(is_array($relations) ? $relations : func_get_args(), '*', 'count');
}

/**
* Add subselect queries to include the max of the relation's column.
*
* @param string $relation
* @param string $column
* @return $this
*/
public function withMax($relation, $column)
{
return $this->withColumn($relation, $column, 'max');
}

/**
* Add subselect queries to include the min of the relation's column.
*
* @param string $relation
* @param string $column
* @return $this
*/
public function withMin($relation, $column)
{
return $this->withColumn($relation, $column, 'min');
}

/**
* Add subselect queries to include the sum of the relation's column.
*
* @param string $relation
* @param string $column
* @return $this
*/
public function withSum($relation, $column)
{
return $this->withColumn($relation, $column, 'sum');
}

/**
* Add subselect queries to include the average of the relation's column.
*
* @param string $relation
* @param string $column
* @return $this
*/
public function withAvg($relation, $column)
{
return $this->withColumn($relation, $column, 'avg');
}

/**
* Add the "has" condition where clause to the query.
*
Expand Down
16 changes: 8 additions & 8 deletions tests/Database/DatabaseEloquentBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -775,7 +775,7 @@ public function testWithCount()

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

$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_count" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
$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" limit 1) as "foo_count" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
}

public function testWithCountAndSelect()
Expand All @@ -784,7 +784,7 @@ public function testWithCountAndSelect()

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

$this->assertSame('select "id", (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->assertSame('select "id", (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" limit 1) as "foo_count" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
}

public function testWithCountAndMergedWheres()
Expand All @@ -795,7 +795,7 @@ public function testWithCountAndMergedWheres()
$q->where('bam', '>', 'qux');
}]);

$this->assertSame('select "id", (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" and "bam" > ? and "active" = ?) as "active_foo_count" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
$this->assertSame('select "id", (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" and "bam" > ? and "active" = ? limit 1) as "active_foo_count" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
$this->assertEquals(['qux', true], $builder->getBindings());
}

Expand All @@ -813,7 +813,7 @@ public function testWithCountAndGlobalScope()
//
});

$this->assertSame('select "id", (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->assertSame('select "id", (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" limit 1) as "foo_count" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
}

public function testWithCountAndConstraintsAndHaving()
Expand All @@ -825,7 +825,7 @@ public function testWithCountAndConstraintsAndHaving()
$q->where('bam', '>', 'qux');
}])->having('foo_count', '>=', 1);

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

Expand All @@ -835,7 +835,7 @@ public function testWithCountAndRename()

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

$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" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
$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" limit 1) as "foo_bar" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
}

public function testWithCountMultipleAndPartialRename()
Expand All @@ -844,7 +844,7 @@ public function testWithCountMultipleAndPartialRename()

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

$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());
$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" limit 1) 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" limit 1) as "foo_count" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
}

public function testHasWithConstraintsAndHavingInSubquery()
Expand Down Expand Up @@ -912,7 +912,7 @@ public function testWithCountAndConstraintsWithBindingInSelectSub()
$q->selectSub($model->newQuery()->where('bam', '=', 3)->selectRaw('count(0)'), 'bam_3_count');
}]);

$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_count" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
$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" limit 1) as "foo_count" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql());
$this->assertSame([], $builder->getBindings());
}

Expand Down
2 changes: 1 addition & 1 deletion tests/Integration/Database/EloquentWithCountTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public function testSortingScopes()

$result = Model1::withCount('twos')->toSql();

$this->assertSame('select "one".*, (select count(*) from "two" where "one"."id" = "two"."one_id") as "twos_count" from "one"', $result);
$this->assertSame('select "one".*, (select count(*) from "two" where "one"."id" = "two"."one_id" limit 1) as "twos_count" from "one"', $result);
}
}

Expand Down