diff --git a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php index 3d2f45f2af37..18bebd365eb7 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php @@ -201,6 +201,117 @@ public function withCount($relations) return $this; } + /** + * Add subselect queries to sum the relations. + * + * @param mixed $relations + * @return $this + */ + public function withSum($relations) + { + $relations = is_array($relations) ? $relations : func_get_args(); + + return $this->withAggregate($relations, 'SUM'); + } + + /** + * Add subselect queries to max the relations. + * + * @param mixed $relations + * @return $this + */ + public function withMax($relations) + { + $relations = is_array($relations) ? $relations : func_get_args(); + + return $this->withAggregate($relations, 'MAX'); + } + + /** + * Add subselect queries to min the relations. + * + * @param mixed $relations + * @return $this + */ + public function withMin($relations) + { + $relations = is_array($relations) ? $relations : func_get_args(); + + return $this->withAggregate($relations, 'MIN'); + } + + /** + * Add subselect queries to min the relations. + * + * @param mixed $relations + * @return $this + */ + public function withAvg($relations) + { + $relations = is_array($relations) ? $relations : func_get_args(); + + return $this->withAggregate($relations, 'AVG'); + } + + /** + * use the MySQL aggregate functions including AVG COUNT, SUM, MAX and MIN. + * + * @param array $relations + * @param string $function + * @return $this + */ + public function withAggregate($relations, $function = 'COUNT') + { + if (is_null($this->query->columns)) { + $this->query->select([$this->query->from.'.*']); + } + + // set to lower + $function = Str::lower($function); + + 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]]; + } + + // set the default column as * or primary key + $column = ($function == 'count') ? '*' : $this->model->getKeyName(); + + if (strpos($name, '|') !== false) { + list($name, $column) = explode('|', $name); + } + + $relation = $this->getRelationWithoutConstraints($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->getRelationExistenceAggregateQuery( + $relation->getRelated()->newQuery(), $this, $function, $column + ); + + $query->callScope($constraints); + + $query->mergeConstraintsFrom($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).'_'.$function; + + $this->selectSub($query->toBase(), $column); + } + + return $this; + } + /** * Add the "has" condition where clause to the query. * diff --git a/src/Illuminate/Database/Eloquent/Relations/Relation.php b/src/Illuminate/Database/Eloquent/Relations/Relation.php index acbfbede1aa1..80f7300591ad 100755 --- a/src/Illuminate/Database/Eloquent/Relations/Relation.php +++ b/src/Illuminate/Database/Eloquent/Relations/Relation.php @@ -177,6 +177,22 @@ public function getRelationExistenceCountQuery(Builder $query, Builder $parentQu ); } + /** + * Add the constraints for a relationship aggregate query. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parentQuery + * @param string $type + * @param string $column + * @return \Illuminate\Database\Eloquent\Builder + */ + public function getRelationExistenceAggregateQuery(Builder $query, Builder $parentQuery, $type, $column) + { + return $this->getRelationExistenceQuery( + $query, $parentQuery, new Expression($type.'('.$this->query->getQuery()->getGrammar()->wrap($column).')') + ); + } + /** * Add the constraints for an internal relationship existence query. * diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index deeab8e67ba8..f922290f6db6 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -696,6 +696,198 @@ public function testWithCountMultipleAndPartialRename() $this->assertEquals('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 testWithSum() + { + $model = new EloquentBuilderTestModelParentStub; + + $builder = $model->withSum('foo'); + + $this->assertEquals('select "eloquent_builder_test_model_parent_stubs".*, (select sum("id") 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'); + + $this->assertEquals('select "id", (select sum("id") 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'); + }]); + + $this->assertEquals('select "id", (select sum("id") 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 testWithSumAndRename() + { + $model = new EloquentBuilderTestModelParentStub; + + $builder = $model->withSum('foo as foo_bar'); + + $this->assertEquals('select "eloquent_builder_test_model_parent_stubs".*, (select sum("id") 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_sum" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql()); + } + + public function testWithSumMultipleAndPartialRename() + { + $model = new EloquentBuilderTestModelParentStub; + + $builder = $model->withSum(['foo as foo_bar', 'foo']); + + $this->assertEquals('select "eloquent_builder_test_model_parent_stubs".*, (select sum("id") 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_sum", (select sum("id") 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 testWithMax() + { + $model = new EloquentBuilderTestModelParentStub; + + $builder = $model->withMax('foo'); + + $this->assertEquals('select "eloquent_builder_test_model_parent_stubs".*, (select max("id") 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'); + + $this->assertEquals('select "id", (select max("id") 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'); + }]); + + $this->assertEquals('select "id", (select max("id") 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 testWithMaxAndRename() + { + $model = new EloquentBuilderTestModelParentStub; + + $builder = $model->withMax('foo as foo_bar'); + + $this->assertEquals('select "eloquent_builder_test_model_parent_stubs".*, (select max("id") 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_max" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql()); + } + + public function testWithMaxMultipleAndPartialRename() + { + $model = new EloquentBuilderTestModelParentStub; + + $builder = $model->withMax(['foo as foo_bar', 'foo']); + + $this->assertEquals('select "eloquent_builder_test_model_parent_stubs".*, (select max("id") 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_max", (select max("id") 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 testWithMin() + { + $model = new EloquentBuilderTestModelParentStub; + + $builder = $model->withMin('foo'); + + $this->assertEquals('select "eloquent_builder_test_model_parent_stubs".*, (select min("id") 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'); + + $this->assertEquals('select "id", (select min("id") 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'); + }]); + + $this->assertEquals('select "id", (select min("id") 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 testWithMinAndRename() + { + $model = new EloquentBuilderTestModelParentStub; + + $builder = $model->withMin('foo as foo_bar'); + + $this->assertEquals('select "eloquent_builder_test_model_parent_stubs".*, (select min("id") 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_min" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql()); + } + + public function testWithMinMultipleAndPartialRename() + { + $model = new EloquentBuilderTestModelParentStub; + + $builder = $model->withMin(['foo as foo_bar', 'foo']); + + $this->assertEquals('select "eloquent_builder_test_model_parent_stubs".*, (select min("id") 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_min", (select min("id") 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 testWithAvg() + { + $model = new EloquentBuilderTestModelParentStub; + + $builder = $model->withAvg('foo'); + + $this->assertEquals('select "eloquent_builder_test_model_parent_stubs".*, (select avg("id") 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'); + + $this->assertEquals('select "id", (select avg("id") 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'); + }]); + + $this->assertEquals('select "id", (select avg("id") 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 testWithAvgAndRename() + { + $model = new EloquentBuilderTestModelParentStub; + + $builder = $model->withAvg('foo as foo_bar'); + + $this->assertEquals('select "eloquent_builder_test_model_parent_stubs".*, (select avg("id") 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_avg" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql()); + } + + public function testWithAvgMultipleAndPartialRename() + { + $model = new EloquentBuilderTestModelParentStub; + + $builder = $model->withAvg(['foo as foo_bar', 'foo']); + + $this->assertEquals('select "eloquent_builder_test_model_parent_stubs".*, (select avg("id") 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_avg", (select avg("id") 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 testHasWithContraintsAndHavingInSubquery() { $model = new EloquentBuilderTestModelParentStub;