Browse Source

Added WindowExpression with support for empty OVER () clause. Added WindowInterface to share window expression building between AggregateExpression and WindowExpression

Corey Taylor 6 years ago
parent
commit
58d935eae7

+ 63 - 0
src/Database/Expression/AggregateExpression.php

@@ -16,6 +16,9 @@ declare(strict_types=1);
  */
 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.
@@ -24,4 +27,64 @@ namespace Cake\Database\Expression;
  */
 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 - 0
src/Database/Expression/WindowExpression.php

@@ -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 - 0
src/Database/Expression/WindowInterface.php

@@ -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 - 0
tests/TestCase/Database/Expression/AggregateExpressionTest.php

@@ -16,6 +16,7 @@ declare(strict_types=1);
 namespace Cake\Test\TestCase\Database\Expression;
 
 use Cake\Database\Expression\AggregateExpression;
+use Cake\Database\ValueBinder;
 
 /**
  * Tests FunctionExpression class
@@ -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()));
+    }
 }