diff --git a/core/lib/Drupal/Core/Database/Connection.php b/core/lib/Drupal/Core/Database/Connection.php index 8d86059aeacc5dfb4e9508f179bb8e47d088737a..ff172b5a6c445eef081c4ae2ee460b1328423da3 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 bbab92babfac6455ebf2bdab57596da840a1ef2a..c7b67f9fa00f353b6cf940039665876e40706ae4 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 7e49310175a34b4ad6f7bf4edc07b5fc5ca9c5a4..8a3d918c9096a92427f600dfb19b75970c492377 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 4aa51e588589c11f5eec9ed410df7cbcdddc63ed..c5ffacdddd78fd3363760be37be714168f09d236 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 c92ed677642de30a6623464d9c05b8e394cb3e39..ff840e19b47d65413fa1cab9fec3159a58191faf 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. */