Browse Source

5.x: add a comment to a query instance (#16958)

5.x: add a comment to a query instance
Kevin Pfeifer 3 years ago
parent
commit
5cc27a16e9

+ 1 - 0
src/Database/PostgresCompiler.php

@@ -48,6 +48,7 @@ class PostgresCompiler extends QueryCompiler
         'limit' => ' LIMIT %s',
         'offset' => ' OFFSET %s',
         'epilog' => ' %s',
+        'comment' => '/* %s */ ',
     ];
 
     /**

+ 22 - 0
src/Database/Query.php

@@ -98,6 +98,7 @@ abstract class Query implements ExpressionInterface, Stringable
      * @var array<string, mixed>
      */
     protected array $_parts = [
+        'comment' => null,
         'delete' => true,
         'update' => [],
         'set' => [],
@@ -1453,6 +1454,27 @@ abstract class Query implements ExpressionInterface, Stringable
     }
 
     /**
+     * A string or expression that will be appended to the generated query as a comment
+     *
+     * ### Examples:
+     * ```
+     * $query->select('id')->where(['author_id' => 1])->comment('Filter for admin user');
+     * ```
+     *
+     * Comment content is raw SQL and not suitable for use with user supplied data.
+     *
+     * @param string|null $expression The comment to be added
+     * @return $this
+     */
+    public function comment(?string $expression = null)
+    {
+        $this->_dirty();
+        $this->_parts['comment'] = $expression;
+
+        return $this;
+    }
+
+    /**
      * Returns the type of this query (select, insert, update, delete)
      *
      * @return string

+ 1 - 0
src/Database/Query/DeleteQuery.php

@@ -36,6 +36,7 @@ class DeleteQuery extends Query
      * @var array<string, mixed>
      */
     protected array $_parts = [
+        'comment' => null,
         'with' => [],
         'delete' => true,
         'modifier' => [],

+ 1 - 0
src/Database/Query/InsertQuery.php

@@ -39,6 +39,7 @@ class InsertQuery extends Query
      * @var array<string, mixed>
      */
     protected array $_parts = [
+        'comment' => null,
         'with' => [],
         'insert' => [],
         'modifier' => [],

+ 1 - 0
src/Database/Query/SelectQuery.php

@@ -51,6 +51,7 @@ class SelectQuery extends Query implements IteratorAggregate
      * @var array<string, mixed>
      */
     protected array $_parts = [
+        'comment' => null,
         'modifier' => [],
         'with' => [],
         'select' => [],

+ 1 - 0
src/Database/Query/UpdateQuery.php

@@ -39,6 +39,7 @@ class UpdateQuery extends Query
      * @var array<string, mixed>
      */
     protected array $_parts = [
+        'comment' => null,
         'with' => [],
         'update' => [],
         'modifier' => [],

+ 5 - 4
src/Database/QueryCompiler.php

@@ -43,6 +43,7 @@ class QueryCompiler
         'limit' => ' LIMIT %s',
         'offset' => ' OFFSET %s',
         'epilog' => ' %s',
+        'comment' => '/* %s */ ',
     ];
 
     /**
@@ -51,7 +52,7 @@ class QueryCompiler
      * @var array<string>
      */
     protected array $_selectParts = [
-        'with', 'select', 'from', 'join', 'where', 'group', 'having', 'window', 'order',
+        'comment', 'with', 'select', 'from', 'join', 'where', 'group', 'having', 'window', 'order',
         'limit', 'offset', 'union', 'epilog',
     ];
 
@@ -60,21 +61,21 @@ class QueryCompiler
      *
      * @var array<string>
      */
-    protected array $_updateParts = ['with', 'update', 'set', 'where', 'epilog'];
+    protected array $_updateParts = ['comment', 'with', 'update', 'set', 'where', 'epilog'];
 
     /**
      * The list of query clauses to traverse for generating a DELETE statement
      *
      * @var array<string>
      */
-    protected array $_deleteParts = ['with', 'delete', 'modifier', 'from', 'where', 'epilog'];
+    protected array $_deleteParts = ['comment', 'with', 'delete', 'modifier', 'from', 'where', 'epilog'];
 
     /**
      * The list of query clauses to traverse for generating an INSERT statement
      *
      * @var array<string>
      */
-    protected array $_insertParts = ['with', 'insert', 'values', 'epilog'];
+    protected array $_insertParts = ['comment', 'with', 'insert', 'values', 'epilog'];
 
     /**
      * Indicate whether this query dialect supports ordered unions.

+ 2 - 1
src/Database/SqlserverCompiler.php

@@ -46,6 +46,7 @@ class SqlserverCompiler extends QueryCompiler
         'order' => ' %s',
         'offset' => ' OFFSET %s ROWS',
         'epilog' => ' %s',
+        'comment' => '/* %s */ ',
     ];
 
     /**
@@ -54,7 +55,7 @@ class SqlserverCompiler extends QueryCompiler
      * @var array<string>
      */
     protected array $_selectParts = [
-        'with', 'select', 'from', 'join', 'where', 'group', 'having', 'window', 'order',
+        'comment', 'with', 'select', 'from', 'join', 'where', 'group', 'having', 'window', 'order',
         'offset', 'limit', 'union', 'epilog',
     ];
 

+ 211 - 0
tests/TestCase/Database/QueryCompilerTest.php

@@ -0,0 +1,211 @@
+<?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
+ * Redistributions of files must retain the above copyright notice
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ * @since         5.0.0
+ * @license       https://opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Test\TestCase\Database;
+
+use Cake\Database\Connection;
+use Cake\Database\Driver\Sqlserver;
+use Cake\Database\Query;
+use Cake\Database\QueryCompiler;
+use Cake\Database\ValueBinder;
+use Cake\Datasource\ConnectionInterface;
+use Cake\Datasource\ConnectionManager;
+use Cake\TestSuite\TestCase;
+
+/**
+ * Tests Query class
+ */
+class QueryCompilerTest extends TestCase
+{
+    use QueryAssertsTrait;
+
+    protected array $fixtures = [
+        'core.Articles',
+    ];
+
+    protected Connection|ConnectionInterface $connection;
+
+    protected QueryCompiler $compiler;
+
+    protected ValueBinder $binder;
+
+    public function setUp(): void
+    {
+        parent::setUp();
+        $this->connection = ConnectionManager::get('test');
+        $this->compiler = $this->connection->getDriver()->newCompiler();
+        $this->binder = new ValueBinder();
+    }
+
+    public function tearDown(): void
+    {
+        parent::tearDown();
+        unset($this->compiler);
+        unset($this->binder);
+    }
+
+    protected function newQuery(string $type): Query
+    {
+        return match ($type) {
+            Query::TYPE_SELECT => new Query\SelectQuery($this->connection),
+            Query::TYPE_INSERT => new Query\InsertQuery($this->connection),
+            Query::TYPE_UPDATE => new Query\UpdateQuery($this->connection),
+            Query::TYPE_DELETE => new Query\DeleteQuery($this->connection),
+        };
+    }
+
+    public function testSelectFrom(): void
+    {
+        /** @var \Cake\Database\Query\SelectQuery $query */
+        $query = $this->newQuery(Query::TYPE_SELECT);
+        $query = $query->select('*')
+            ->from('articles');
+        $result = $this->compiler->compile($query, $this->binder);
+        $this->assertSame('SELECT * FROM articles', $result);
+
+        $result = $query->all();
+        $this->assertCount(3, $result);
+    }
+
+    public function testSelectWhere(): void
+    {
+        /** @var \Cake\Database\Query\SelectQuery $query */
+        $query = $this->newQuery(Query::TYPE_SELECT);
+        $query = $query->select('*')
+            ->from('articles')
+            ->where(['author_id' => 1]);
+        $result = $this->compiler->compile($query, $this->binder);
+        $this->assertSame('SELECT * FROM articles WHERE author_id = :c0', $result);
+
+        $result = $query->all();
+        $this->assertCount(2, $result);
+    }
+
+    public function testSelectWithComment(): void
+    {
+        /** @var \Cake\Database\Query\SelectQuery $query */
+        $query = $this->newQuery(Query::TYPE_SELECT);
+        $query = $query->select('*')
+            ->from('articles')
+            ->comment('This is a test');
+        $result = $this->compiler->compile($query, $this->binder);
+        $this->assertSame('/* This is a test */ SELECT * FROM articles', $result);
+
+        $result = $query->all();
+        $this->assertCount(3, $result);
+    }
+
+    public function testInsert(): void
+    {
+        /** @var \Cake\Database\Query\InsertQuery $query */
+        $query = $this->newQuery(Query::TYPE_INSERT);
+        $query = $query->insert(['title'])
+            ->into('articles')
+            ->values(['title' => 'A new article']);
+        $result = $this->compiler->compile($query, $this->binder);
+
+        if ($this->connection->getDriver() instanceof Sqlserver) {
+            $this->assertSame('INSERT INTO articles (title) OUTPUT INSERTED.* VALUES (:c0)', $result);
+        } else {
+            $this->assertSame('INSERT INTO articles (title) VALUES (:c0)', $result);
+        }
+
+        $result = $query->execute();
+        $this->assertInstanceOf('Cake\Database\StatementInterface', $result);
+        $result->closeCursor();
+    }
+
+    public function testInsertWithComment(): void
+    {
+        /** @var \Cake\Database\Query\InsertQuery $query */
+        $query = $this->newQuery(Query::TYPE_INSERT);
+        $query = $query->insert(['title'])
+            ->into('articles')
+            ->values(['title' => 'A new article'])
+            ->comment('This is a test');
+        $result = $this->compiler->compile($query, $this->binder);
+
+        if ($this->connection->getDriver() instanceof Sqlserver) {
+            $this->assertSame('/* This is a test */ INSERT INTO articles (title) OUTPUT INSERTED.* VALUES (:c0)', $result);
+        } else {
+            $this->assertSame('/* This is a test */ INSERT INTO articles (title) VALUES (:c0)', $result);
+        }
+
+        $result = $query->execute();
+        $this->assertInstanceOf('Cake\Database\StatementInterface', $result);
+        $result->closeCursor();
+    }
+
+    public function testUpdate(): void
+    {
+        /** @var \Cake\Database\Query\UpdateQuery $query */
+        $query = $this->newQuery(Query::TYPE_UPDATE);
+        $query = $query->update('articles')
+            ->set('title', 'mark')
+            ->where(['id' => 1]);
+        $result = $this->compiler->compile($query, $this->binder);
+        $this->assertSame('UPDATE articles SET title = :c0 WHERE id = :c1', $result);
+
+        $result = $query->execute();
+        $this->assertInstanceOf('Cake\Database\StatementInterface', $result);
+        $result->closeCursor();
+    }
+
+    public function testUpdateWithComment(): void
+    {
+        /** @var \Cake\Database\Query\UpdateQuery $query */
+        $query = $this->newQuery(Query::TYPE_UPDATE);
+        $query = $query->update('articles')
+            ->set('title', 'mark')
+            ->where(['id' => 1])
+            ->comment('This is a test');
+        $result = $this->compiler->compile($query, $this->binder);
+        $this->assertSame('/* This is a test */ UPDATE articles SET title = :c0 WHERE id = :c1', $result);
+
+        $result = $query->execute();
+        $this->assertInstanceOf('Cake\Database\StatementInterface', $result);
+        $result->closeCursor();
+    }
+
+    public function testDelete(): void
+    {
+        /** @var \Cake\Database\Query\DeleteQuery $query */
+        $query = $this->newQuery(Query::TYPE_DELETE);
+        $query = $query->delete()
+            ->from('articles')
+            ->where(['id !=' => 1]);
+        $result = $this->compiler->compile($query, $this->binder);
+        $this->assertSame('DELETE FROM articles WHERE id != :c0', $result);
+
+        $result = $query->execute();
+        $this->assertInstanceOf('Cake\Database\StatementInterface', $result);
+        $result->closeCursor();
+    }
+
+    public function testDeleteWithComment(): void
+    {
+        /** @var \Cake\Database\Query\DeleteQuery $query */
+        $query = $this->newQuery(Query::TYPE_DELETE);
+        $query = $query->delete()
+            ->from('articles')
+            ->where(['id !=' => 1])
+            ->comment('This is a test');
+        $result = $this->compiler->compile($query, $this->binder);
+        $this->assertSame('/* This is a test */ DELETE FROM articles WHERE id != :c0', $result);
+
+        $result = $query->execute();
+        $this->assertInstanceOf('Cake\Database\StatementInterface', $result);
+        $result->closeCursor();
+    }
+}

+ 7 - 9
tests/TestCase/Database/QueryTest.php

@@ -2,17 +2,15 @@
 declare(strict_types=1);
 
 /**
- * cakephp(tm) : rapid development framework (https://cakephp.org)
- * copyright (c) cake software foundation, inc. (https://cakefoundation.org)
+ * 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.
+ * Licensed under The MIT License
+ * 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
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  * @since         3.0.0
- * @license       https://opensource.org/licenses/mit-license.php mit license
+ * @license       https://opensource.org/licenses/mit-license.php MIT License
  */
 namespace Cake\Test\TestCase\Database;
 
@@ -313,7 +311,7 @@ class QueryTest extends TestCase
     public function testClauseUndefined(): void
     {
         $this->expectException(InvalidArgumentException::class);
-        $this->expectExceptionMessage('The `nope` clause is not defined. Valid clauses are: `delete`, `update`');
+        $this->expectExceptionMessage('The `nope` clause is not defined. Valid clauses are: `comment`, `delete`, `update`');
 
         $this->assertEmpty($this->query->clause('where'));
         $this->query->clause('nope');