Skip to content

Commit

Permalink
Added WindowExpression with support for empty OVER () clause. Added W…
Browse files Browse the repository at this point in the history
…indowInterface to share window expression building between AggregateExpression and WindowExpression
  • Loading branch information
othercorey committed Jan 17, 2020
1 parent eaa604a commit 58d935e
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 0 deletions.
63 changes: 63 additions & 0 deletions src/Database/Expression/AggregateExpression.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
*/
namespace Cake\Database\Expression;

use Cake\Database\ValueBinder;
use Closure;

/**
* This represents a SQL aggregate function expression in a SQL statement.
* Calls can be constructed by passing the name of the function and a list of params.
Expand All @@ -24,4 +27,64 @@
*/
class AggregateExpression extends FunctionExpression
{
/**
* @var \Cake\Database\Expression\WindowExpression
*/
protected $window;

/**
* Adds an empty `OVER()` window expression.
*
* If the window expression for this aggregate is already
* initialized, this does nothing.
*
* @return $this
*/
public function over()
{
if ($this->window === null) {
$this->window = new WindowExpression();
}

return $this;
}

/**
* @inheritDoc
*/
public function sql(ValueBinder $generator): string
{
$sql = parent::sql($generator);
if ($this->window !== null) {
$sql .= ' ' . $this->window->sql($generator);
}

return $sql;
}

/**
* @inheritDoc
*/
public function traverse(Closure $visitor)
{
parent::traverse($visitor);
if ($this->window !== null) {
$this->window->traverse($visitor);
}

return $this;
}

/**
* @inheritDoc
*/
public function count(): int
{
$count = parent::count();
if ($this->window !== null) {
$count = $count + 1;
}

return $count;
}
}
43 changes: 43 additions & 0 deletions src/Database/Expression/WindowExpression.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);

/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.1.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;

use Cake\Database\ExpressionInterface;
use Cake\Database\ValueBinder;
use Closure;

/**
* This represents a SQL window expression used by aggregate and window functions.
*/
class WindowExpression implements ExpressionInterface
{
/**
* @inheritDoc
*/
public function sql(ValueBinder $generator): string
{
return 'OVER ()';
}

/**
* @inheritDoc
*/
public function traverse(Closure $visitor)
{
return $this;
}
}
110 changes: 110 additions & 0 deletions src/Database/Expression/WindowInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);

/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.1.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Expression;

/**
* This defines the functions used for building window expressions.
*/
interface WindowInterface
{
/**
* Adds one or more partition expressions to the window.
*
* @param (\Cake\Database\ExpressionInterface|string)[]|\Cake\Database\ExpressionInterface|string $partitions Partition expressions
* @return $this
*/
public function partition($partitions);

/**
* Adds one or more order clauses to the window.
*
* @param (\Cake\Database\ExpressionInterface|string)[]|\Cake\Database\ExpressionInterface|string $fields Order expressions
* @return $this
*/
public function order($fields);

/**
* Adds a range frame clause to the window. Only one frame clause can be
* specified per window.
*
* `$start` assumes `PRECEDING`, and `$end` assumes `FOLLOWING. Both can be
* overriden by passing an array with the order as the key. The SQL standard
* for ordering must be followed.
*
* ```
* // this is the same as '1 FOLLOWING`
* $window->range(['following' => 1]);
* ```
*
* The SQL keywords `UNBOUNDED` and `CURRENT ROW` can be used directly or
* easier to read substitutes `null` and `0` instead.
*
* ```
* // this is the same as 'CURRENT ROW'
* $window->range(0);
*
* // this is the same as 'UNBOUNDED PRECEDING'
* $window->range(null)
* ```
*
* @param array|int|string|null $start Frame start
* @param array|int|string|null $end Frame end
* If not passed in, only frame start SQL will be generated.
* @return $this
*/
public function range($start, $end = 0);

/**
* Adds a rows frame clause to the window. Only one frame clause can be
* specified per window.
*
* See `range()` for details on `$start` and `$end` format.
*
* @param array|int|string|null $start Frame start
* @param array|int|string|null $end Frame end
* If not passed in, only frame start SQL will be generated.
* @return $this
*/
public function rows($start, $end = 0);

/**
* Adds a groups frame clause to the window. Only one frame clause can be
* specified per window.
*
* See `range()` for details on `$start` and `$end` format.
*
* @param array|int|string|null $start Frame start
* @param array|int|string|null $end Frame end
* If not passed in, only frame start SQL will be generated.
* @return $this
*/
public function groups($start, $end = 0);

/**
* Adds a frame exclusion to the window.
*
* Known exclusion keywords are:
* - CURRENT ROW
* - GROUP
* - TIES
* - NO OTHERS
*
* @param string $exclusion Frame exclusion
* @return $this
*/
public function exclude(string $exclusion);
}
12 changes: 12 additions & 0 deletions tests/TestCase/Database/Expression/AggregateExpressionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
namespace Cake\Test\TestCase\Database\Expression;

use Cake\Database\Expression\AggregateExpression;
use Cake\Database\ValueBinder;

/**
* Tests FunctionExpression class
Expand All @@ -26,4 +27,15 @@ class AggregateExpressionTest extends FunctionExpressionTest
* @var string The expression class to test with
*/
protected $expressionClass = AggregateExpression::class;

/**
* Tests annotating an aggregate with an empty window expression
*
* @return void
*/
public function testEmptyWindow()
{
$f = (new AggregateExpression('MyFunction'))->over();
$this->assertSame('MyFunction() OVER ()', $f->sql(new ValueBinder()));
}
}

0 comments on commit 58d935e

Please sign in to comment.