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.0] Refactor how aggregates are calculated #6985

Closed
wants to merge 1 commit 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
57 changes: 42 additions & 15 deletions src/Illuminate/Database/Query/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class Builder {
*/
protected $bindings = array(
'select' => [],
'from' => [],
'join' => [],
'where' => [],
'having' => [],
Expand Down Expand Up @@ -301,8 +302,15 @@ public function distinct()
* @param string $table
* @return $this
*/
public function from($table)
public function from($table, $as = null)
{
if ($table instanceof Builder)
{
$this->setBindings($table->getBindings(), 'from');

$table = new Expression('('.$table->toSql().') as '.$this->grammar->wrap($as ?: 'sub'));
}

$this->from = $table;

return $this;
Expand Down Expand Up @@ -888,7 +896,7 @@ public function orWhereNotNull($column)
{
return $this->whereNotNull($column, 'or');
}

/**
* Add a "where date" statement to the query.
*
Expand All @@ -902,7 +910,7 @@ public function whereDate($column, $operator, $value, $boolean = 'and')
{
return $this->addDateBasedWhere('Date', $column, $operator, $value, $boolean);
}

/**
* Add a "where day" statement to the query.
*
Expand Down Expand Up @@ -1645,18 +1653,7 @@ public function avg($column)
*/
public function aggregate($function, $columns = array('*'))
{
$this->aggregate = compact('function', 'columns');

$previousColumns = $this->columns;

$results = $this->get($columns);

// Once we have executed the query, we will reset the aggregate property so
// that more select queries can be executed against the database without
// the aggregate value getting in the way when the grammar builds it.
$this->aggregate = null;

$this->columns = $previousColumns;
$results = $this->aggregateQuery($function, $columns)->get();

if (isset($results[0]))
{
Expand All @@ -1666,6 +1663,36 @@ public function aggregate($function, $columns = array('*'))
}
}

/**
* Create a query to compute an aggregate value.
*
* @param string $function
* @param array $columns
* @return mixed
*/
protected function aggregateQuery($function, $columns = array('*'))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The additional method is for clarity. Moreover, we could chose to make it public such that it can be used from the tests and then calling toSql to check the query using PHPUnit asserts instead of relying on Mockery to intercept the calls to the database.

{
$previousColumns = $this->columns;

// If the developer specified a column to calculate the aggregate for, we will
// make sure it is also selected from within the subquery otherwise the data
// would not be available when the aggregate is actually being calculated.
if ($columns != array('*'))
{
$this->addSelect($columns);

$this->columns = array_unique($this->columns);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the column was already selected, we need to filter it out here. This is tested in testAggregateResetFollowedBySelectGet

}

$query = $this->newQuery()->from($this);

$query->aggregate = compact('function', 'columns');

$this->columns = $previousColumns;

return $query;
}

/**
* Insert a new record into the database.
*
Expand Down
54 changes: 41 additions & 13 deletions tests/Database/DatabaseQueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@ public function testBasicTableWrappingProtectsQuotationMarks()
$this->assertEquals('select * from "some""table"', $builder->toSql());
}


public function testAliasWrappingAsWholeConstant()
{
$builder = $this->getBuilder();
$builder->select('x.y as foo.bar')->from('baz');
$this->assertEquals('select "x"."y" as "foo.bar" from "baz"', $builder->toSql());
}


public function testAddingSelects()
{
$builder = $this->getBuilder();
Expand All @@ -73,7 +75,6 @@ public function testBasicSelectDistinct()
}



public function testBasicAlias()
{
$builder = $this->getBuilder();
Expand Down Expand Up @@ -624,6 +625,7 @@ public function testComplexJoin()
$this->assertEquals(array('foo', 'bar'), $builder->getBindings());
}


public function testJoinWhereNull()
{
$builder = $this->getBuilder();
Expand All @@ -634,6 +636,7 @@ public function testJoinWhereNull()
$this->assertEquals('select * from "users" inner join "contacts" on "users"."id" = "contacts"."id" and "contacts"."deleted_at" is null', $builder->toSql());
}


public function testRawExpressionsInSelect()
{
$builder = $this->getBuilder();
Expand Down Expand Up @@ -721,31 +724,31 @@ public function testPluckMethodReturnsSingleColumn()
public function testAggregateFunctions()
{
$builder = $this->getBuilder();
$builder->getConnection()->shouldReceive('select')->once()->with('select count(*) as aggregate from "users"', array())->andReturn(array(array('aggregate' => 1)));
$builder->getConnection()->shouldReceive('select')->once()->with('select count(*) as aggregate from (select * from "users") as "sub"', array())->andReturn(array(array('aggregate' => 1)));
$builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function($builder, $results) { return $results; });
$results = $builder->from('users')->count();
$this->assertEquals(1, $results);

$builder = $this->getBuilder();
$builder->getConnection()->shouldReceive('select')->once()->with('select count(*) as aggregate from "users" limit 1', array())->andReturn(array(array('aggregate' => 1)));
$builder->getConnection()->shouldReceive('select')->once()->with('select count(*) as aggregate from (select * from "users" limit 1) as "sub"', array())->andReturn(array(array('aggregate' => 1)));
$builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function($builder, $results) { return $results; });
$results = $builder->from('users')->exists();
$this->assertTrue($results);

$builder = $this->getBuilder();
$builder->getConnection()->shouldReceive('select')->once()->with('select max("id") as aggregate from "users"', array())->andReturn(array(array('aggregate' => 1)));
$builder->getConnection()->shouldReceive('select')->once()->with('select max("id") as aggregate from (select "id" from "users") as "sub"', array())->andReturn(array(array('aggregate' => 1)));
$builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function($builder, $results) { return $results; });
$results = $builder->from('users')->max('id');
$this->assertEquals(1, $results);

$builder = $this->getBuilder();
$builder->getConnection()->shouldReceive('select')->once()->with('select min("id") as aggregate from "users"', array())->andReturn(array(array('aggregate' => 1)));
$builder->getConnection()->shouldReceive('select')->once()->with('select min("id") as aggregate from (select "id" from "users") as "sub"', array())->andReturn(array(array('aggregate' => 1)));
$builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function($builder, $results) { return $results; });
$results = $builder->from('users')->min('id');
$this->assertEquals(1, $results);

$builder = $this->getBuilder();
$builder->getConnection()->shouldReceive('select')->once()->with('select sum("id") as aggregate from "users"', array())->andReturn(array(array('aggregate' => 1)));
$builder->getConnection()->shouldReceive('select')->once()->with('select sum("id") as aggregate from (select "id" from "users") as "sub"', array())->andReturn(array(array('aggregate' => 1)));
$builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function($builder, $results) { return $results; });
$results = $builder->from('users')->sum('id');
$this->assertEquals(1, $results);
Expand All @@ -755,8 +758,8 @@ public function testAggregateFunctions()
public function testAggregateResetFollowedByGet()
{
$builder = $this->getBuilder();
$builder->getConnection()->shouldReceive('select')->once()->with('select count(*) as aggregate from "users"', array())->andReturn(array(array('aggregate' => 1)));
$builder->getConnection()->shouldReceive('select')->once()->with('select sum("id") as aggregate from "users"', array())->andReturn(array(array('aggregate' => 2)));
$builder->getConnection()->shouldReceive('select')->once()->with('select count(*) as aggregate from (select "column1", "column2" from "users") as "sub"', array())->andReturn(array(array('aggregate' => 1)));
$builder->getConnection()->shouldReceive('select')->once()->with('select sum("id") as aggregate from (select "column1", "column2", "id" from "users") as "sub"', array())->andReturn(array(array('aggregate' => 2)));
$builder->getConnection()->shouldReceive('select')->once()->with('select "column1", "column2" from "users"', array())->andReturn(array(array('column1' => 'foo', 'column2' => 'bar')));
$builder->getProcessor()->shouldReceive('processSelect')->andReturnUsing(function($builder, $results) { return $results; });
$builder->from('users')->select('column1', 'column2');
Expand All @@ -772,21 +775,21 @@ public function testAggregateResetFollowedByGet()
public function testAggregateResetFollowedBySelectGet()
{
$builder = $this->getBuilder();
$builder->getConnection()->shouldReceive('select')->once()->with('select count("column1") as aggregate from "users"', array())->andReturn(array(array('aggregate' => 1)));
$builder->getConnection()->shouldReceive('select')->once()->with('select "column2", "column3" from "users"', array())->andReturn(array(array('column2' => 'foo', 'column3' => 'bar')));
$builder->getConnection()->shouldReceive('select')->once()->with('select count("column1") as aggregate from (select "column2", "column3", "column1" from "users") as "sub"', array())->andReturn(array(array('aggregate' => 1)));
$builder->getConnection()->shouldReceive('select')->once()->with('select "column2", "column3", "column1" from "users"', array())->andReturn(array(array('column2' => 'foo', 'column3' => 'bar')));
$builder->getProcessor()->shouldReceive('processSelect')->andReturnUsing(function($builder, $results) { return $results; });
$builder->from('users');
$builder->from('users')->select('column2', 'column3', 'column1');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the select to be before the aggregate call, to verify the behaviour when the query itself selects a specific set of columns. We now also select column1 to verify that it is not added again.

$count = $builder->count('column1');
$this->assertEquals(1, $count);
$result = $builder->select('column2', 'column3')->get();
$result = $builder->get();
$this->assertEquals(array(array('column2' => 'foo', 'column3' => 'bar')), $result);
}


public function testAggregateResetFollowedByGetWithColumns()
{
$builder = $this->getBuilder();
$builder->getConnection()->shouldReceive('select')->once()->with('select count("column1") as aggregate from "users"', array())->andReturn(array(array('aggregate' => 1)));
$builder->getConnection()->shouldReceive('select')->once()->with('select count("column1") as aggregate from (select "column1" from "users") as "sub"', array())->andReturn(array(array('aggregate' => 1)));
$builder->getConnection()->shouldReceive('select')->once()->with('select "column2", "column3" from "users"', array())->andReturn(array(array('column2' => 'foo', 'column3' => 'bar')));
$builder->getProcessor()->shouldReceive('processSelect')->andReturnUsing(function($builder, $results) { return $results; });
$builder->from('users');
Expand All @@ -797,6 +800,16 @@ public function testAggregateResetFollowedByGetWithColumns()
}


public function testAggregateWithDistict()
{
$builder = $this->getBuilder();
$builder->getConnection()->shouldReceive('select')->once()->with('select count(*) as aggregate from (select distinct * from "users") as "sub"', array())->andReturn(array(array('aggregate' => 1)));
$builder->getProcessor()->shouldReceive('processSelect')->once()->andReturnUsing(function($builder, $results) { return $results; });
$results = $builder->from('users')->distinct()->count();
$this->assertEquals(1, $results);
}


public function testInsertMethod()
{
$builder = $this->getBuilder();
Expand Down Expand Up @@ -1184,6 +1197,21 @@ public function testSubSelect()
}


public function testSelectFromQuery()
{
$expectedSql = 'select "foo", "bar" from (select "baz" from "two" where "subkey" = ?) as "sub" where "key" = ?';
$expectedBindings = ['subval', 'val'];

$table = $this->getPostgresBuilder();
$table->from('two')->select('baz')->where('subkey', '=', 'subval');

$builder = $this->getPostgresBuilder();
$builder->from($table, 'sub')->select(['foo', 'bar'])->where('key', '=', 'val');
$this->assertEquals($expectedSql, $builder->toSql());
$this->assertEquals($expectedBindings, $builder->getBindings());
}


protected function getBuilder()
{
$grammar = new Illuminate\Database\Query\Grammars\Grammar;
Expand Down