From 6aab22375b182b5dcfc3218ba29f27ecc6c78bf9 Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Sun, 29 Nov 2020 00:17:47 +0000
Subject: [PATCH] Issue #3137883 by mondrake, Pooja Ganjage, andypost, paulocs,
 anmolgoyal74, daffie, alexpott, catch: Deprecate passing a StatementInterface
 object to Connection::query

---
 core/lib/Drupal/Core/Database/Connection.php  | 21 +++++----
 .../Core/Database/Driver/pgsql/Insert.php     | 43 ++++++++++++-------
 .../Core/Database/Driver/pgsql/Update.php     |  5 ++-
 .../Core/Database/Driver/pgsql/Upsert.php     |  2 +-
 .../Core/Database/ConnectionTest.php          | 31 +++++++++++++
 5 files changed, 75 insertions(+), 27 deletions(-)

diff --git a/core/lib/Drupal/Core/Database/Connection.php b/core/lib/Drupal/Core/Database/Connection.php
index 8d86059aeacc..ff172b5a6c44 100644
--- a/core/lib/Drupal/Core/Database/Connection.php
+++ b/core/lib/Drupal/Core/Database/Connection.php
@@ -749,11 +749,12 @@ protected function filterComment($comment = '') {
    * statements.
    *
    * @param string|\Drupal\Core\Database\StatementInterface|\PDOStatement $query
-   *   The query to execute. In most cases this will be a string containing
-   *   an SQL query with placeholders. An already-prepared instance of
-   *   StatementInterface may also be passed in order to allow calling
-   *   code to manually bind variables to a query. If a
-   *   StatementInterface is passed, the $args array will be ignored.
+   *   The query to execute. This is a string containing an SQL query with
+   *   placeholders.
+   *   (deprecated) An already-prepared instance of StatementInterface or of
+   *   \PDOStatement may also be passed in order to allow calling code to
+   *   manually bind variables to a query. In such cases, the content of the
+   *   $args array will be ignored.
    *   It is extremely rare that module code will need to pass a statement
    *   object to this method. It is used primarily for database drivers for
    *   databases that require special LOB field handling.
@@ -792,14 +793,16 @@ public function query($query, array $args = [], $options = []) {
     assert(!isset($options['target']), 'Passing "target" option to query() has no effect. See https://www.drupal.org/node/2993033');
 
     try {
-      // We allow either a pre-bound statement object or a literal string.
-      // In either case, we want to end up with an executed statement object,
-      // which we pass to PDOStatement::execute.
+      // We allow either a pre-bound statement object (deprecated) or a literal
+      // string. In either case, we want to end up with an executed statement
+      // object, which we pass to PDOStatement::execute.
       if ($query instanceof StatementInterface) {
+        @trigger_error('Passing a StatementInterface object as a $query argument to ' . __METHOD__ . ' is deprecated in drupal:9.2.0 and is removed in drupal:10.0.0. Call the execute method from the StatementInterface object directly instead. See https://www.drupal.org/node/3154439', E_USER_DEPRECATED);
         $stmt = $query;
         $stmt->execute(NULL, $options);
       }
       elseif ($query instanceof \PDOStatement) {
+        @trigger_error('Passing a \\PDOStatement object as a $query argument to ' . __METHOD__ . ' is deprecated in drupal:9.2.0 and is removed in drupal:10.0.0. Call the execute method from the StatementInterface object directly instead. See https://www.drupal.org/node/3154439', E_USER_DEPRECATED);
         $stmt = $query;
         $stmt->execute();
       }
@@ -878,6 +881,8 @@ protected function handleQueryException(\PDOException $e, $query, array $args =
       // Wrap the exception in another exception, because PHP does not allow
       // overriding Exception::getMessage(). Its message is the extra database
       // debug information.
+      // @todo in Drupal 10, remove checking if $query is a statement object.
+      // @see https://www.drupal.org/node/3154439
       if ($query instanceof StatementInterface) {
         $query_string = $query->getQueryString();
       }
diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Insert.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Insert.php
index bbab92babfac..c7b67f9fa00f 100644
--- a/core/lib/Drupal/Core/Database/Driver/pgsql/Insert.php
+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Insert.php
@@ -3,6 +3,8 @@
 namespace Drupal\Core\Database\Driver\pgsql;
 
 use Drupal\Core\Database\Database;
+use Drupal\Core\Database\DatabaseExceptionWrapper;
+use Drupal\Core\Database\IntegrityConstraintViolationException;
 use Drupal\Core\Database\Query\Insert as QueryInsert;
 
 // cSpell:ignore nextval setval
@@ -84,18 +86,10 @@ public function execute() {
       }
     }
 
-    // PostgreSQL requires the table name to be specified explicitly
-    // when requesting the last insert ID, so we pass that in via
-    // the options array.
-    $options = $this->queryOptions;
-
-    if (!empty($table_information->sequences)) {
-      $options['sequence_name'] = $table_information->sequences[0];
-    }
-    // If there are no sequences then we can't get a last insert id.
-    elseif ($options['return'] == Database::RETURN_INSERT_ID) {
-      $options['return'] = Database::RETURN_NULL;
-    }
+    // PostgreSQL requires the table name to be specified explicitly when
+    // requesting the last insert ID. If there are no sequences, then we can't
+    // get a last insert id.
+    $sequence_name = $table_information->sequences[0] ?? NULL;
 
     // Create a savepoint so we can rollback a failed query. This is so we can
     // mimic MySQL and SQLite transactions which don't fail if a single query
@@ -103,14 +97,31 @@ public function execute() {
     // example, \Drupal\Core\Cache\DatabaseBackend.
     $this->connection->addSavepoint();
     try {
+      $stmt->execute(NULL, $this->queryOptions);
       // Only use the returned last_insert_id if it is not already set.
-      if (!empty($last_insert_id)) {
-        $this->connection->query($stmt, [], $options);
+      // PostgreSQL requires the table name to be specified explicitly when
+      // requesting the last insert ID.
+      if (!isset($last_insert_id)) {
+        if ($this->queryOptions['return'] === Database::RETURN_INSERT_ID && $sequence_name !== NULL) {
+          // cspell:ignore currval
+          $last_insert_id = $this->connection->query("SELECT currval('$sequence_name')")->fetchField();
+        }
+        else {
+          $last_insert_id = NULL;
+        }
+      }
+      $this->connection->releaseSavepoint();
+    }
+    catch (\PDOException $e) {
+      $this->connection->rollbackSavepoint();
+      $message = $e->getMessage() . ": " . $stmt->getQueryString();
+      // Match all SQLSTATE 23xxx errors.
+      if (substr($e->getCode(), -6, -3) == '23') {
+        throw new IntegrityConstraintViolationException($message, $e->getCode(), $e);
       }
       else {
-        $last_insert_id = $this->connection->query($stmt, [], $options);
+        throw new DatabaseExceptionWrapper($message, 0, $e->getCode());
       }
-      $this->connection->releaseSavepoint();
     }
     catch (\Exception $e) {
       $this->connection->rollbackSavepoint();
diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Update.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Update.php
index 7e49310175a3..8a3d918c9096 100644
--- a/core/lib/Drupal/Core/Database/Driver/pgsql/Update.php
+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Update.php
@@ -75,9 +75,10 @@ public function execute() {
 
     $this->connection->addSavepoint();
     try {
-      $result = $this->connection->query($stmt, [], $options);
+      $stmt->execute(NULL, $options);
       $this->connection->releaseSavepoint();
-      return $result;
+      $stmt->allowRowCount = TRUE;
+      return $stmt->rowCount();
     }
     catch (\Exception $e) {
       $this->connection->rollbackSavepoint();
diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Upsert.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Upsert.php
index 4aa51e588589..c5ffacdddd78 100644
--- a/core/lib/Drupal/Core/Database/Driver/pgsql/Upsert.php
+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Upsert.php
@@ -80,7 +80,7 @@ public function execute() {
     // example, \Drupal\Core\Cache\DatabaseBackend.
     $this->connection->addSavepoint();
     try {
-      $this->connection->query($stmt, [], $options);
+      $stmt->execute(NULL, $options);
       $this->connection->releaseSavepoint();
     }
     catch (\Exception $e) {
diff --git a/core/tests/Drupal/KernelTests/Core/Database/ConnectionTest.php b/core/tests/Drupal/KernelTests/Core/Database/ConnectionTest.php
index c92ed677642d..ff840e19b47d 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/ConnectionTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/ConnectionTest.php
@@ -6,6 +6,7 @@
 use Drupal\Core\Database\Database;
 use Drupal\Core\Database\DatabaseExceptionWrapper;
 use Drupal\Core\Database\Query\Condition;
+use Drupal\Core\Database\StatementWrapper;
 
 /**
  * Tests of the core database system.
@@ -134,6 +135,36 @@ public function testTransactionsOptionDeprecation() {
     $this->assertTrue($foo_connection->supportsTransactions());
   }
 
+  /**
+   * Tests the deprecation of passing a statement object to ::query.
+   *
+   * @group legacy
+   */
+  public function testStatementQueryDeprecation(): void {
+    $this->expectDeprecation('Passing a StatementInterface object as a $query argument to Drupal\Core\Database\Connection::query is deprecated in drupal:9.2.0 and is removed in drupal:10.0.0. Call the execute method from the StatementInterface object directly instead. See https://www.drupal.org/node/3154439');
+    $db = Database::getConnection();
+    $stmt = $db->prepareStatement('SELECT * FROM {test}', []);
+    $this->assertNotNull($db->query($stmt));
+  }
+
+  /**
+   * Tests the deprecation of passing a PDOStatement object to ::query.
+   *
+   * @group legacy
+   */
+  public function testPDOStatementQueryDeprecation(): void {
+    $db = Database::getConnection();
+    $stmt = $db->prepareStatement('SELECT * FROM {test}', []);
+    if (!$stmt instanceof StatementWrapper) {
+      $this->markTestSkipped("This test only runs for db drivers using StatementWrapper.");
+    }
+    if (!$stmt->getClientStatement() instanceof \PDOStatement) {
+      $this->markTestSkipped("This test only runs for PDO-based db drivers.");
+    }
+    $this->expectDeprecation('Passing a \\PDOStatement object as a $query argument to Drupal\Core\Database\Connection::query is deprecated in drupal:9.2.0 and is removed in drupal:10.0.0. Call the execute method from the StatementInterface object directly instead. See https://www.drupal.org/node/3154439');
+    $this->assertNotNull($db->query($stmt->getClientStatement()));
+  }
+
   /**
    * Ensure that you cannot execute multiple statements on MySQL.
    */
-- 
GitLab