Skip to content

Commit

Permalink
Added named window expression support
Browse files Browse the repository at this point in the history
  • Loading branch information
othercorey committed May 3, 2020
1 parent 06027e7 commit 1a1afa5
Show file tree
Hide file tree
Showing 8 changed files with 280 additions and 50 deletions.
17 changes: 11 additions & 6 deletions src/Database/Expression/AggregateExpression.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,18 @@ class AggregateExpression extends FunctionExpression implements WindowInterface
protected $window;

/**
* Adds an empty `OVER()` window expression.
* Adds an empty `OVER()` window expression or a named window epression.
*
* If the window expression for this aggregate is already
* initialized, this does nothing.
* Does nothing if the window expression for this aggregate is already
* initialized
*
* @param string|null $name Window name
* @return $this
*/
public function over()
public function over(?string $name = null)
{
if ($this->window === null) {
$this->window = new WindowExpression();
$this->window = new WindowExpression($name);
}

return $this;
Expand Down Expand Up @@ -176,7 +177,11 @@ public function sql(ValueBinder $generator): string
{
$sql = parent::sql($generator);
if ($this->window !== null) {
$sql .= ' ' . $this->window->sql($generator);
if ($this->window->isEmpty() && $this->window->getName()) {
$sql .= ' OVER ' . $this->window->getName();
} else {
$sql .= ' OVER (' . $this->window->sql($generator) . ')';
}
}

return $sql;
Expand Down
59 changes: 57 additions & 2 deletions src/Database/Expression/WindowExpression.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
*/
class WindowExpression implements ExpressionInterface, WindowInterface
{
/**
* @var string|null
*/
protected $name;

/**
* @var \Cake\Database\ExpressionInterface[]
*/
Expand All @@ -46,6 +51,49 @@ class WindowExpression implements ExpressionInterface, WindowInterface
*/
protected $exclusion;

/**
* @param string|null $name Window name
*/
public function __construct(?string $name = null)
{
$this->name = $name;
}

/**
* Return whether window expression is empty.
*
* An expression is empty if it has no partition, order or frame clauses.
*
* @return bool
*/
public function isEmpty(): bool
{
return !$this->partitions && !$this->frame && !$this->order;
}

/**
* Return named window used in this expresion if set.
*
* @return string|null
*/
public function getName(): ?string
{
return $this->name;
}

/**
* Sets the window name.
*
* @param string $name Window name
* @return $this
*/
public function setName(string $name)
{
$this->name = $name;

return $this;
}

/**
* @inheritDoc
*/
Expand Down Expand Up @@ -192,6 +240,10 @@ public function excludeTies()
*/
public function sql(ValueBinder $generator): string
{
if ($this->isEmpty()) {
return '';
}

$partitionSql = '';
if ($this->partitions) {
$expressions = [];
Expand All @@ -202,7 +254,7 @@ public function sql(ValueBinder $generator): string
$partitionSql = 'PARTITION BY ' . implode(', ', $expressions);
}

$orderSql = $this->order ? $orderSql = $this->order->sql($generator) : '';
$orderSql = $this->order ? $this->order->sql($generator) : '';

$frameSql = '';
if ($this->frame) {
Expand Down Expand Up @@ -234,8 +286,11 @@ public function sql(ValueBinder $generator): string
}
}

$name = $this->name ? $this->name . ' ' : '';

return sprintf(
'OVER (%s%s%s%s%s)',
'%s%s%s%s%s%s',
$name,
$partitionSql,
$partitionSql && $orderSql ? ' ' : '',
$orderSql,
Expand Down
28 changes: 28 additions & 0 deletions src/Database/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Cake\Database\Expression\OrderClauseExpression;
use Cake\Database\Expression\QueryExpression;
use Cake\Database\Expression\ValuesExpression;
use Cake\Database\Expression\WindowExpression;
use Cake\Database\Statement\CallbackStatement;
use Closure;
use InvalidArgumentException;
Expand Down Expand Up @@ -85,6 +86,7 @@ class Query implements ExpressionInterface, IteratorAggregate
'where' => null,
'group' => [],
'having' => null,
'window' => [],
'order' => null,
'limit' => null,
'offset' => null,
Expand Down Expand Up @@ -1328,6 +1330,32 @@ public function andHaving($conditions, $types = [])
return $this;
}

/**
* Adds a named window expression.
*
* Users are responsible for adding windows in the order each database requires.
*
* @param string $name Window name
* @param \Cake\Database\Expression\WindowExpression|\Closure $window Window expression
* @param bool $overwrite Clear all previous query window expressions
* @return $this
*/
public function window(string $name, $window, bool $overwrite = false)
{
if ($overwrite) {
$this->_parts['window'] = [];
}

if ($window instanceof Closure) {
$window = $window(new WindowExpression(), $this);
}

$this->_parts['window'][] = ['name' => $name, 'window' => $window];
$this->_dirty();

return $this;
}

/**
* Set the page of results you want.
*
Expand Down
25 changes: 24 additions & 1 deletion src/Database/QueryCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class QueryCompiler
* @var array
*/
protected $_selectParts = [
'select', 'from', 'join', 'where', 'group', 'having', 'order', 'limit',
'select', 'from', 'join', 'where', 'group', 'having', 'window', 'order', 'limit',
'offset', 'union', 'epilog',
];

Expand Down Expand Up @@ -270,6 +270,29 @@ protected function _buildJoinPart(array $parts, Query $query, ValueBinder $gener
return $joins;
}

/**
* Helper function to build the string representation of a window clause.
*
* @param array $parts List of windows to be transformed to string
* @param \Cake\Database\Query $query The query that is being compiled
* @param \Cake\Database\ValueBinder $generator the placeholder generator to be used in expressions
* @return string
*/
protected function _buildWindowPart(array $parts, Query $query, ValueBinder $generator): string
{
$windows = [];
foreach ($parts as $window) {
$expr = $window['window'];
if ($expr->isEmpty() && $expr->getName()) {
$windows[] = $window['name'] . ' AS (' . $expr->getName() . ')';
} else {
$windows[] = $window['name'] . ' AS (' . $expr->sql($generator) . ')';
}
}

return ' WINDOW ' . implode(', ', $windows);
}

/**
* Helper function to generate SQL for SET expressions.
*
Expand Down
2 changes: 1 addition & 1 deletion src/Database/SqlserverCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class SqlserverCompiler extends QueryCompiler
* @inheritDoc
*/
protected $_selectParts = [
'select', 'from', 'join', 'where', 'group', 'having', 'order', 'offset',
'select', 'from', 'join', 'where', 'group', 'having', 'window', 'order', 'offset',
'limit', 'union', 'epilog',
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ public function testEmptyWindow()
{
$f = (new AggregateExpression('MyFunction'))->over();
$this->assertSame('MyFunction() OVER ()', $f->sql(new ValueBinder()));

$f = (new AggregateExpression('MyFunction'))->over('name');
$this->assertEqualsSql(
'MyFunction() OVER name',
$f->sql(new ValueBinder())
);
}

/**
Expand Down

0 comments on commit 1a1afa5

Please sign in to comment.