diff --git a/core/includes/install.inc b/core/includes/install.inc index da4a89049f5..f85499042f8 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -626,10 +626,15 @@ function drupal_install_system($install_state) { // When the database driver is provided by a module, then install that module. // This module must be installed before any other module, as it must be able // to override any call to hook_schema() or any "backend_overridable" service. + // In edge cases, a driver module may extend from another driver module (for + // instance, a module to provide backward compatibility with a database + // version no longer supported by core). In order for the extended classes to + // be autoloadable, the extending module should list the extended module in + // its dependencies, and here the dependencies will be installed as well. if ($provider !== 'core') { $autoload = $connection->getConnectionOptions()['autoload'] ?? ''; if (($pos = strpos($autoload, 'src/Driver/Database/')) !== FALSE) { - $kernel->getContainer()->get('module_installer')->install([$provider], FALSE); + $kernel->getContainer()->get('module_installer')->install([$provider], TRUE); } } diff --git a/core/lib/Drupal/Core/Config/DatabaseStorage.php b/core/lib/Drupal/Core/Config/DatabaseStorage.php index 607fdde5fb7..14290691e1b 100644 --- a/core/lib/Drupal/Core/Config/DatabaseStorage.php +++ b/core/lib/Drupal/Core/Config/DatabaseStorage.php @@ -143,6 +143,8 @@ class DatabaseStorage implements StorageInterface { * @return bool */ protected function doWrite($name, $data) { + // @todo Remove the 'return' option in Drupal 11. + // @see https://www.drupal.org/project/drupal/issues/3256524 $options = ['return' => Database::RETURN_AFFECTED] + $this->options; return (bool) $this->connection->merge($this->table, $options) ->keys(['collection', 'name'], [$this->collection, $name]) @@ -218,6 +220,8 @@ class DatabaseStorage implements StorageInterface { * @todo Ignore replica targets for data manipulation operations. */ public function delete($name) { + // @todo Remove the 'return' option in Drupal 11. + // @see https://www.drupal.org/project/drupal/issues/3256524 $options = ['return' => Database::RETURN_AFFECTED] + $this->options; return (bool) $this->connection->delete($this->table, $options) ->condition('collection', $this->collection) @@ -231,6 +235,8 @@ class DatabaseStorage implements StorageInterface { * @throws PDOException */ public function rename($name, $new_name) { + // @todo Remove the 'return' option in Drupal 11. + // @see https://www.drupal.org/project/drupal/issues/3256524 $options = ['return' => Database::RETURN_AFFECTED] + $this->options; return (bool) $this->connection->update($this->table, $options) ->fields(['name' => $new_name]) @@ -280,6 +286,8 @@ class DatabaseStorage implements StorageInterface { */ public function deleteAll($prefix = '') { try { + // @todo Remove the 'return' option in Drupal 11. + // @see https://www.drupal.org/project/drupal/issues/3256524 $options = ['return' => Database::RETURN_AFFECTED] + $this->options; return (bool) $this->connection->delete($this->table, $options) ->condition('name', $prefix . '%', 'LIKE') diff --git a/core/lib/Drupal/Core/Database/Connection.php b/core/lib/Drupal/Core/Database/Connection.php index 67c601d3db0..cea7fc4028e 100644 --- a/core/lib/Drupal/Core/Database/Connection.php +++ b/core/lib/Drupal/Core/Database/Connection.php @@ -367,12 +367,12 @@ abstract class Connection { * class. If a string is specified, each record will be fetched into a new * object of that class. The behavior of all other values is defined by PDO. * See http://php.net/manual/pdostatement.fetch.php - * - return: Depending on the type of query, different return values may be - * meaningful. This directive instructs the system which type of return - * value is desired. The system will generally set the correct value - * automatically, so it is extremely rare that a module developer will ever - * need to specify this value. Setting it incorrectly will likely lead to - * unpredictable results or fatal errors. Legal values include: + * - return: (deprecated) Depending on the type of query, different return + * values may be meaningful. This directive instructs the system which type + * of return value is desired. The system will generally set the correct + * value automatically, so it is extremely rare that a module developer will + * ever need to specify this value. Setting it incorrectly will likely lead + * to unpredictable results or fatal errors. Legal values include: * - Database::RETURN_STATEMENT: Return the prepared statement object for * the query. This is usually only meaningful for SELECT queries, where * the statement object is how one accesses the result set returned by the @@ -414,7 +414,6 @@ abstract class Connection { protected function defaultOptions() { return [ 'fetch' => \PDO::FETCH_OBJ, - 'return' => Database::RETURN_STATEMENT, 'allow_delimiter_in_query' => FALSE, 'allow_square_brackets' => FALSE, 'pdo' => [], @@ -616,6 +615,10 @@ abstract class Connection { * @throws \Drupal\Core\Database\DatabaseExceptionWrapper */ public function prepareStatement(string $query, array $options, bool $allow_row_count = FALSE): StatementInterface { + if (isset($options['return'])) { + @trigger_error('Passing "return" option to ' . __METHOD__ . '() is deprecated in drupal:9.4.0 and is removed in drupal:11.0.0. For data manipulation operations, use dynamic queries instead. See https://www.drupal.org/node/3185520', E_USER_DEPRECATED); + } + try { $query = $this->preprocessStatement($query, $options); @@ -914,6 +917,11 @@ abstract class Connection { public function query($query, array $args = [], $options = []) { // Use default values if not already set. $options += $this->defaultOptions(); + + if (isset($options['return'])) { + @trigger_error('Passing "return" option to ' . __METHOD__ . '() is deprecated in drupal:9.4.0 and is removed in drupal:11.0.0. For data manipulation operations, use dynamic queries instead. See https://www.drupal.org/node/3185520', E_USER_DEPRECATED); + } + assert(!isset($options['target']), 'Passing "target" option to query() has no effect. See https://www.drupal.org/node/2993033'); // We allow either a pre-bound statement object (deprecated) or a literal @@ -946,7 +954,7 @@ abstract class Connection { // Depending on the type of query we may need to return a different value. // See DatabaseConnection::defaultOptions() for a description of each // value. - switch ($options['return']) { + switch ($options['return'] ?? Database::RETURN_STATEMENT) { case Database::RETURN_STATEMENT: return $stmt; @@ -1234,6 +1242,40 @@ abstract class Connection { return new $class($this, $table, $options); } + /** + * Returns the ID of the last inserted row or sequence value. + * + * This method should normally be used only within database driver code. + * + * This is a proxy to invoke lastInsertId() from the wrapped connection. + * If a sequence name is not specified for the name parameter, this returns a + * string representing the row ID of the last row that was inserted into the + * database. + * If a sequence name is specified for the name parameter, this returns a + * string representing the last value retrieved from the specified sequence + * object. + * + * @param string|null $name + * (Optional) Name of the sequence object from which the ID should be + * returned. + * + * @return string + * The value returned by the wrapped connection. + * + * @throws \Drupal\Core\Database\DatabaseExceptionWrapper + * In case of failure. + * + * @see \PDO::lastInsertId + * + * @internal + */ + public function lastInsertId(?string $name = NULL): string { + if (($last_insert_id = $this->connection->lastInsertId($name)) === FALSE) { + throw new DatabaseExceptionWrapper("Could not determine last insert id" . $name === NULL ? '' : " for sequence $name"); + } + return $last_insert_id; + } + /** * Prepares and returns a MERGE query object. * diff --git a/core/lib/Drupal/Core/Database/Database.php b/core/lib/Drupal/Core/Database/Database.php index 2394c5eff6e..79b681dc883 100644 --- a/core/lib/Drupal/Core/Database/Database.php +++ b/core/lib/Drupal/Core/Database/Database.php @@ -19,21 +19,41 @@ abstract class Database { * * This is used for queries that have no reasonable return value anyway, such * as INSERT statements to a table without a serial primary key. + * + * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. There is no + * replacement. + * + * @see https://www.drupal.org/node/3185520 */ const RETURN_NULL = 0; /** * Flag to indicate a query call should return the prepared statement. + * + * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. There is no + * replacement. + * + * @see https://www.drupal.org/node/3185520 */ const RETURN_STATEMENT = 1; /** * Flag to indicate a query call should return the number of affected rows. + * + * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. There is no + * replacement. + * + * @see https://www.drupal.org/node/3185520 */ const RETURN_AFFECTED = 2; /** * Flag to indicate a query call should return the "last insert id". + * + * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. There is no + * replacement. + * + * @see https://www.drupal.org/node/3185520 */ const RETURN_INSERT_ID = 3; diff --git a/core/lib/Drupal/Core/Database/Query/Delete.php b/core/lib/Drupal/Core/Database/Query/Delete.php index 2fb0b91449d..864d0648dd2 100644 --- a/core/lib/Drupal/Core/Database/Query/Delete.php +++ b/core/lib/Drupal/Core/Database/Query/Delete.php @@ -32,6 +32,8 @@ class Delete extends Query implements ConditionInterface { * Array of database options. */ public function __construct(Connection $connection, $table, array $options = []) { + // @todo Remove $options['return'] in Drupal 11. + // @see https://www.drupal.org/project/drupal/issues/3256524 $options['return'] = Database::RETURN_AFFECTED; parent::__construct($connection, $options); $this->table = $table; diff --git a/core/lib/Drupal/Core/Database/Query/Insert.php b/core/lib/Drupal/Core/Database/Query/Insert.php index 011d4423d1e..af4e32820f0 100644 --- a/core/lib/Drupal/Core/Database/Query/Insert.php +++ b/core/lib/Drupal/Core/Database/Query/Insert.php @@ -31,6 +31,8 @@ class Insert extends Query implements \Countable { * Array of database options. */ public function __construct($connection, $table, array $options = []) { + // @todo Remove $options['return'] in Drupal 11. + // @see https://www.drupal.org/project/drupal/issues/3256524 if (!isset($options['return'])) { $options['return'] = Database::RETURN_INSERT_ID; } @@ -82,11 +84,12 @@ class Insert extends Query implements \Countable { // we wrap it in a transaction so that it is atomic where possible. On many // databases, such as SQLite, this is also a notable performance boost. $transaction = $this->connection->startTransaction(); + $stmt = $this->connection->prepareStatement((string) $this, $this->queryOptions); try { - $sql = (string) $this; foreach ($this->insertValues as $insert_values) { - $last_insert_id = $this->connection->query($sql, $insert_values, $this->queryOptions); + $stmt->execute($insert_values, $this->queryOptions); + $last_insert_id = $this->connection->lastInsertId(); } } catch (\Exception $e) { diff --git a/core/lib/Drupal/Core/Database/Query/Merge.php b/core/lib/Drupal/Core/Database/Query/Merge.php index 90a9abab1e1..fffca6ce9f7 100644 --- a/core/lib/Drupal/Core/Database/Query/Merge.php +++ b/core/lib/Drupal/Core/Database/Query/Merge.php @@ -134,6 +134,8 @@ class Merge extends Query implements ConditionInterface { * Array of database options. */ public function __construct(Connection $connection, $table, array $options = []) { + // @todo Remove $options['return'] in Drupal 11. + // @see https://www.drupal.org/project/drupal/issues/3256524 $options['return'] = Database::RETURN_AFFECTED; parent::__construct($connection, $options); $this->table = $table; diff --git a/core/lib/Drupal/Core/Database/Query/Select.php b/core/lib/Drupal/Core/Database/Query/Select.php index 5da38481662..ac7bdd4e6f9 100644 --- a/core/lib/Drupal/Core/Database/Query/Select.php +++ b/core/lib/Drupal/Core/Database/Query/Select.php @@ -131,6 +131,8 @@ class Select extends Query implements SelectInterface { * Array of query options. */ public function __construct(Connection $connection, $table, $alias = NULL, $options = []) { + // @todo Remove $options['return'] in Drupal 11. + // @see https://www.drupal.org/project/drupal/issues/3256524 $options['return'] = Database::RETURN_STATEMENT; parent::__construct($connection, $options); $conjunction = $options['conjunction'] ?? 'AND'; diff --git a/core/lib/Drupal/Core/Database/Query/Truncate.php b/core/lib/Drupal/Core/Database/Query/Truncate.php index 2c0adcb3101..66b39644b50 100644 --- a/core/lib/Drupal/Core/Database/Query/Truncate.php +++ b/core/lib/Drupal/Core/Database/Query/Truncate.php @@ -28,6 +28,8 @@ class Truncate extends Query { * Array of database options. */ public function __construct(Connection $connection, $table, array $options = []) { + // @todo Remove $options['return'] in Drupal 11. + // @see https://www.drupal.org/project/drupal/issues/3256524 $options['return'] = Database::RETURN_AFFECTED; parent::__construct($connection, $options); $this->table = $table; diff --git a/core/lib/Drupal/Core/Database/Query/Update.php b/core/lib/Drupal/Core/Database/Query/Update.php index 88af918647e..1311083e102 100644 --- a/core/lib/Drupal/Core/Database/Query/Update.php +++ b/core/lib/Drupal/Core/Database/Query/Update.php @@ -61,6 +61,8 @@ class Update extends Query implements ConditionInterface { * Array of database options. */ public function __construct(Connection $connection, $table, array $options = []) { + // @todo Remove $options['return'] in Drupal 11. + // @see https://www.drupal.org/project/drupal/issues/3256524 $options['return'] = Database::RETURN_AFFECTED; parent::__construct($connection, $options); $this->table = $table; diff --git a/core/lib/Drupal/Core/Database/Query/Upsert.php b/core/lib/Drupal/Core/Database/Query/Upsert.php index 524b145b55a..00c26cddbb6 100644 --- a/core/lib/Drupal/Core/Database/Query/Upsert.php +++ b/core/lib/Drupal/Core/Database/Query/Upsert.php @@ -35,6 +35,8 @@ abstract class Upsert extends Query implements \Countable { * (optional) An array of database options. */ public function __construct(Connection $connection, $table, array $options = []) { + // @todo Remove $options['return'] in Drupal 11. + // @see https://www.drupal.org/project/drupal/issues/3256524 $options['return'] = Database::RETURN_AFFECTED; parent::__construct($connection, $options); $this->table = $table; diff --git a/core/lib/Drupal/Core/Database/RowCountException.php b/core/lib/Drupal/Core/Database/RowCountException.php index cd4131045fc..83b6d65e721 100644 --- a/core/lib/Drupal/Core/Database/RowCountException.php +++ b/core/lib/Drupal/Core/Database/RowCountException.php @@ -9,7 +9,7 @@ class RowCountException extends \RuntimeException implements DatabaseException { public function __construct($message = '', $code = 0, \Exception $previous = NULL) { if (empty($message)) { - $message = "rowCount() is supported for DELETE, INSERT, or UPDATE statements performed with structured query builders only, since they would not be portable across database engines otherwise. If the query builders are not sufficient, set the 'return' option to Database::RETURN_AFFECTED to get the number of affected rows."; + $message = "rowCount() is supported for DELETE, INSERT, or UPDATE statements performed with structured query builders only, since they would not be portable across database engines otherwise. If the query builders are not sufficient, use a prepareStatement() with an \$allow_row_count argument set to TRUE, execute() the Statement and get the number of affected rows via rowCount()."; } parent::__construct($message, $code, $previous); } diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php index a21f2646987..289d2a99345 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php @@ -932,6 +932,8 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt } } else { + // @todo Remove the 'return' option in Drupal 11. + // @see https://www.drupal.org/project/drupal/issues/3256524 $insert_id = $this->database ->insert($this->baseTable, ['return' => Database::RETURN_INSERT_ID]) ->fields((array) $record) @@ -1135,6 +1137,8 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt $entity->preSaveRevision($this, $record); if ($entity->isNewRevision()) { + // @todo Remove the 'return' option in Drupal 11. + // @see https://www.drupal.org/project/drupal/issues/3256524 $insert_id = $this->database ->insert($this->revisionTable, ['return' => Database::RETURN_INSERT_ID]) ->fields((array) $record) diff --git a/core/lib/Drupal/Core/Menu/MenuTreeStorage.php b/core/lib/Drupal/Core/Menu/MenuTreeStorage.php index b343a2f36fe..b0d8787b4af 100644 --- a/core/lib/Drupal/Core/Menu/MenuTreeStorage.php +++ b/core/lib/Drupal/Core/Menu/MenuTreeStorage.php @@ -314,6 +314,8 @@ class MenuTreeStorage implements MenuTreeStorageInterface { try { if (!$original) { // Generate a new mlid. + // @todo Remove the 'return' option in Drupal 11. + // @see https://www.drupal.org/project/drupal/issues/3256524 $options = ['return' => Database::RETURN_INSERT_ID] + $this->options; $link['mlid'] = $this->connection->insert($this->table, $options) ->fields(['id' => $link['id'], 'menu_name' => $link['menu_name']]) diff --git a/core/modules/mysql/src/Driver/Database/mysql/Connection.php b/core/modules/mysql/src/Driver/Database/mysql/Connection.php index 4eb7700bf65..b2de80be4f4 100644 --- a/core/modules/mysql/src/Driver/Database/mysql/Connection.php +++ b/core/modules/mysql/src/Driver/Database/mysql/Connection.php @@ -343,7 +343,8 @@ class Connection extends DatabaseConnection { } public function nextId($existing_id = 0) { - $new_id = $this->query('INSERT INTO {sequences} () VALUES ()', [], ['return' => Database::RETURN_INSERT_ID]); + $this->query('INSERT INTO {sequences} () VALUES ()'); + $new_id = $this->lastInsertId(); // This should only happen after an import or similar event. if ($existing_id >= $new_id) { // If we INSERT a value manually into the sequences table, on the next @@ -354,7 +355,8 @@ class Connection extends DatabaseConnection { // UPDATE in such a way that the UPDATE does not do anything. This way, // duplicate keys do not generate errors but everything else does. $this->query('INSERT INTO {sequences} (value) VALUES (:value) ON DUPLICATE KEY UPDATE value = value', [':value' => $existing_id]); - $new_id = $this->query('INSERT INTO {sequences} () VALUES ()', [], ['return' => Database::RETURN_INSERT_ID]); + $this->query('INSERT INTO {sequences} () VALUES ()'); + $new_id = $this->lastInsertId(); } $this->needsCleanup = TRUE; return $new_id; diff --git a/core/modules/mysql/src/Driver/Database/mysql/Delete.php b/core/modules/mysql/src/Driver/Database/mysql/Delete.php new file mode 100644 index 00000000000..764a99b133c --- /dev/null +++ b/core/modules/mysql/src/Driver/Database/mysql/Delete.php @@ -0,0 +1,22 @@ +queryOptions['return']); + } + +} diff --git a/core/modules/mysql/src/Driver/Database/mysql/Insert.php b/core/modules/mysql/src/Driver/Database/mysql/Insert.php index 61698521039..e14e6d0d280 100644 --- a/core/modules/mysql/src/Driver/Database/mysql/Insert.php +++ b/core/modules/mysql/src/Driver/Database/mysql/Insert.php @@ -9,6 +9,16 @@ use Drupal\Core\Database\Query\Insert as QueryInsert; */ class Insert extends QueryInsert { + /** + * {@inheritdoc} + */ + public function __construct(Connection $connection, string $table, array $options = []) { + // @todo Remove the __construct in Drupal 11. + // @see https://www.drupal.org/project/drupal/issues/3256524 + parent::__construct($connection, $table, $options); + unset($this->queryOptions['return']); + } + public function execute() { if (!$this->preExecute()) { return NULL; @@ -29,7 +39,14 @@ class Insert extends QueryInsert { $values = $this->fromQuery->getArguments(); } - $last_insert_id = $this->connection->query((string) $this, $values, $this->queryOptions); + $stmt = $this->connection->prepareStatement((string) $this, $this->queryOptions); + try { + $stmt->execute($values, $this->queryOptions); + $last_insert_id = $this->connection->lastInsertId(); + } + catch (\Exception $e) { + $this->connection->exceptionHandler()->handleExecutionException($e, $stmt, $values, $this->queryOptions); + } // Re-initialize the values array so that we can re-use this query. $this->insertValues = []; diff --git a/core/modules/mysql/src/Driver/Database/mysql/Merge.php b/core/modules/mysql/src/Driver/Database/mysql/Merge.php new file mode 100644 index 00000000000..f26090032ba --- /dev/null +++ b/core/modules/mysql/src/Driver/Database/mysql/Merge.php @@ -0,0 +1,22 @@ +queryOptions['return']); + } + +} diff --git a/core/modules/mysql/src/Driver/Database/mysql/Select.php b/core/modules/mysql/src/Driver/Database/mysql/Select.php new file mode 100644 index 00000000000..534cf38ac24 --- /dev/null +++ b/core/modules/mysql/src/Driver/Database/mysql/Select.php @@ -0,0 +1,22 @@ +queryOptions['return']); + } + +} diff --git a/core/modules/mysql/src/Driver/Database/mysql/Truncate.php b/core/modules/mysql/src/Driver/Database/mysql/Truncate.php new file mode 100644 index 00000000000..6682318f85e --- /dev/null +++ b/core/modules/mysql/src/Driver/Database/mysql/Truncate.php @@ -0,0 +1,22 @@ +queryOptions['return']); + } + +} diff --git a/core/modules/mysql/src/Driver/Database/mysql/Update.php b/core/modules/mysql/src/Driver/Database/mysql/Update.php new file mode 100644 index 00000000000..e0995d9c881 --- /dev/null +++ b/core/modules/mysql/src/Driver/Database/mysql/Update.php @@ -0,0 +1,22 @@ +queryOptions['return']); + } + +} diff --git a/core/modules/mysql/src/Driver/Database/mysql/Upsert.php b/core/modules/mysql/src/Driver/Database/mysql/Upsert.php index 0e5f7d3b50e..5b2a5929d09 100644 --- a/core/modules/mysql/src/Driver/Database/mysql/Upsert.php +++ b/core/modules/mysql/src/Driver/Database/mysql/Upsert.php @@ -9,6 +9,16 @@ use Drupal\Core\Database\Query\Upsert as QueryUpsert; */ class Upsert extends QueryUpsert { + /** + * {@inheritdoc} + */ + public function __construct(Connection $connection, string $table, array $options = []) { + // @todo Remove the __construct in Drupal 11. + // @see https://www.drupal.org/project/drupal/issues/3256524 + parent::__construct($connection, $table, $options); + unset($this->queryOptions['return']); + } + /** * {@inheritdoc} */ diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Delete.php b/core/modules/pgsql/src/Driver/Database/pgsql/Delete.php index 9585a9c4bc5..78f7908956a 100644 --- a/core/modules/pgsql/src/Driver/Database/pgsql/Delete.php +++ b/core/modules/pgsql/src/Driver/Database/pgsql/Delete.php @@ -9,6 +9,16 @@ use Drupal\Core\Database\Query\Delete as QueryDelete; */ class Delete extends QueryDelete { + /** + * {@inheritdoc} + */ + public function __construct(Connection $connection, string $table, array $options = []) { + // @todo Remove the __construct in Drupal 11. + // @see https://www.drupal.org/project/drupal/issues/3256524 + parent::__construct($connection, $table, $options); + unset($this->queryOptions['return']); + } + /** * {@inheritdoc} */ diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Insert.php b/core/modules/pgsql/src/Driver/Database/pgsql/Insert.php index 1b53274729d..3caea783d8f 100644 --- a/core/modules/pgsql/src/Driver/Database/pgsql/Insert.php +++ b/core/modules/pgsql/src/Driver/Database/pgsql/Insert.php @@ -3,7 +3,6 @@ namespace Drupal\pgsql\Driver\Database\pgsql; use Drupal\Core\Database\DatabaseExceptionWrapper; -use Drupal\Core\Database\IntegrityConstraintViolationException; use Drupal\Core\Database\Query\Insert as QueryInsert; // cSpell:ignore nextval setval @@ -18,6 +17,16 @@ use Drupal\Core\Database\Query\Insert as QueryInsert; */ class Insert extends QueryInsert { + /** + * {@inheritdoc} + */ + public function __construct(Connection $connection, string $table, array $options = []) { + // @todo Remove the __construct in Drupal 11. + // @see https://www.drupal.org/project/drupal/issues/3256524 + parent::__construct($connection, $table, $options); + unset($this->queryOptions['return']); + } + public function execute() { if (!$this->preExecute()) { return NULL; @@ -92,20 +101,9 @@ class Insert extends QueryInsert { } $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 { - throw new DatabaseExceptionWrapper($message, 0, $e->getCode()); - } - } catch (\Exception $e) { $this->connection->rollbackSavepoint(); - throw $e; + $this->connection->exceptionHandler()->handleExecutionException($e, $stmt, [], $this->queryOptions); } // Re-initialize the values array so that we can re-use this query. diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Merge.php b/core/modules/pgsql/src/Driver/Database/pgsql/Merge.php new file mode 100644 index 00000000000..11fa6386866 --- /dev/null +++ b/core/modules/pgsql/src/Driver/Database/pgsql/Merge.php @@ -0,0 +1,22 @@ +queryOptions['return']); + } + +} diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Select.php b/core/modules/pgsql/src/Driver/Database/pgsql/Select.php index 959b6092d92..dbd787027b8 100644 --- a/core/modules/pgsql/src/Driver/Database/pgsql/Select.php +++ b/core/modules/pgsql/src/Driver/Database/pgsql/Select.php @@ -14,6 +14,16 @@ use Drupal\Core\Database\Query\Select as QuerySelect; */ class Select extends QuerySelect { + /** + * {@inheritdoc} + */ + public function __construct(Connection $connection, $table, $alias = NULL, array $options = []) { + // @todo Remove the __construct in Drupal 11. + // @see https://www.drupal.org/project/drupal/issues/3256524 + parent::__construct($connection, $table, $alias, $options); + unset($this->queryOptions['return']); + } + public function orderRandom() { $alias = $this->addExpression('RANDOM()', 'random_field'); $this->orderBy($alias); diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Truncate.php b/core/modules/pgsql/src/Driver/Database/pgsql/Truncate.php index 18115e0a7d0..102cceae4b5 100644 --- a/core/modules/pgsql/src/Driver/Database/pgsql/Truncate.php +++ b/core/modules/pgsql/src/Driver/Database/pgsql/Truncate.php @@ -9,6 +9,16 @@ use Drupal\Core\Database\Query\Truncate as QueryTruncate; */ class Truncate extends QueryTruncate { + /** + * {@inheritdoc} + */ + public function __construct(Connection $connection, string $table, array $options = []) { + // @todo Remove the __construct in Drupal 11. + // @see https://www.drupal.org/project/drupal/issues/3256524 + parent::__construct($connection, $table, $options); + unset($this->queryOptions['return']); + } + /** * {@inheritdoc} */ diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Update.php b/core/modules/pgsql/src/Driver/Database/pgsql/Update.php index d3f2ebf6431..c680097284a 100644 --- a/core/modules/pgsql/src/Driver/Database/pgsql/Update.php +++ b/core/modules/pgsql/src/Driver/Database/pgsql/Update.php @@ -10,6 +10,16 @@ use Drupal\Core\Database\Query\SelectInterface; */ class Update extends QueryUpdate { + /** + * {@inheritdoc} + */ + public function __construct(Connection $connection, string $table, array $options = []) { + // @todo Remove the __construct in Drupal 11. + // @see https://www.drupal.org/project/drupal/issues/3256524 + parent::__construct($connection, $table, $options); + unset($this->queryOptions['return']); + } + public function execute() { $max_placeholder = 0; $blobs = []; diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Upsert.php b/core/modules/pgsql/src/Driver/Database/pgsql/Upsert.php index 35823a270b1..e738bf25a0a 100644 --- a/core/modules/pgsql/src/Driver/Database/pgsql/Upsert.php +++ b/core/modules/pgsql/src/Driver/Database/pgsql/Upsert.php @@ -11,6 +11,16 @@ use Drupal\Core\Database\Query\Upsert as QueryUpsert; */ class Upsert extends QueryUpsert { + /** + * {@inheritdoc} + */ + public function __construct(Connection $connection, string $table, array $options = []) { + // @todo Remove the __construct in Drupal 11. + // @see https://www.drupal.org/project/drupal/issues/3256524 + parent::__construct($connection, $table, $options); + unset($this->queryOptions['return']); + } + /** * {@inheritdoc} */ diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php b/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php index 5a1ab3b9d42..17fd2567d57 100644 --- a/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php +++ b/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php @@ -435,6 +435,10 @@ class Connection extends DatabaseConnection { * {@inheritdoc} */ public function prepareStatement(string $query, array $options, bool $allow_row_count = FALSE): StatementInterface { + if (isset($options['return'])) { + @trigger_error('Passing "return" option to ' . __METHOD__ . '() is deprecated in drupal:9.4.0 and is removed in drupal:11.0.0. For data manipulation operations, use dynamic queries instead. See https://www.drupal.org/node/3185520', E_USER_DEPRECATED); + } + try { $query = $this->preprocessStatement($query, $options); $statement = new Statement($this->connection, $this, $query, $options['pdo'] ?? [], $allow_row_count); diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Delete.php b/core/modules/sqlite/src/Driver/Database/sqlite/Delete.php new file mode 100644 index 00000000000..bdbe0138e7d --- /dev/null +++ b/core/modules/sqlite/src/Driver/Database/sqlite/Delete.php @@ -0,0 +1,22 @@ +queryOptions['return']); + } + +} diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Insert.php b/core/modules/sqlite/src/Driver/Database/sqlite/Insert.php index d1cc245e273..229fd03656a 100644 --- a/core/modules/sqlite/src/Driver/Database/sqlite/Insert.php +++ b/core/modules/sqlite/src/Driver/Database/sqlite/Insert.php @@ -13,16 +13,61 @@ use Drupal\Core\Database\Query\Insert as QueryInsert; */ class Insert extends QueryInsert { + /** + * {@inheritdoc} + */ + public function __construct(Connection $connection, string $table, array $options = []) { + // @todo Remove the __construct in Drupal 11. + // @see https://www.drupal.org/project/drupal/issues/3256524 + parent::__construct($connection, $table, $options); + unset($this->queryOptions['return']); + } + public function execute() { if (!$this->preExecute()) { return NULL; } - if (count($this->insertFields) || !empty($this->fromQuery)) { - return parent::execute(); + + // If we're selecting from a SelectQuery, finish building the query and + // pass it back, as any remaining options are irrelevant. + if (!empty($this->fromQuery)) { + // The SelectQuery may contain arguments, load and pass them through. + return $this->connection->query((string) $this, $this->fromQuery->getArguments(), $this->queryOptions); + } + + // We wrap the insert in a transaction so that it is atomic where possible. + // In SQLite, this is also a notable performance boost. + $transaction = $this->connection->startTransaction(); + + if (count($this->insertFields)) { + // Each insert happens in its own query. + $stmt = $this->connection->prepareStatement((string) $this, $this->queryOptions); + foreach ($this->insertValues as $insert_values) { + try { + $stmt->execute($insert_values, $this->queryOptions); + } + catch (\Exception $e) { + // One of the INSERTs failed, rollback the whole batch. + $transaction->rollBack(); + $this->connection->exceptionHandler()->handleExecutionException($e, $stmt, $insert_values, $this->queryOptions); + } + } + // Re-initialize the values array so that we can re-use this query. + $this->insertValues = []; } else { - return $this->connection->query('INSERT INTO {' . $this->table . '} DEFAULT VALUES', [], $this->queryOptions); + $stmt = $this->connection->prepareStatement("INSERT INTO {{$this->table}} DEFAULT VALUES", $this->queryOptions); + try { + $stmt->execute(NULL, $this->queryOptions); + } + catch (\Exception $e) { + $transaction->rollBack(); + $this->connection->exceptionHandler()->handleExecutionException($e, $stmt, [], $this->queryOptions); + } } + + // Transaction commits here when $transaction looses scope. + return $this->connection->lastInsertId(); } public function __toString() { diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Merge.php b/core/modules/sqlite/src/Driver/Database/sqlite/Merge.php new file mode 100644 index 00000000000..377720784f1 --- /dev/null +++ b/core/modules/sqlite/src/Driver/Database/sqlite/Merge.php @@ -0,0 +1,22 @@ +queryOptions['return']); + } + +} diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Select.php b/core/modules/sqlite/src/Driver/Database/sqlite/Select.php index 5ee521af8b2..fbd928065cf 100644 --- a/core/modules/sqlite/src/Driver/Database/sqlite/Select.php +++ b/core/modules/sqlite/src/Driver/Database/sqlite/Select.php @@ -9,6 +9,16 @@ use Drupal\Core\Database\Query\Select as QuerySelect; */ class Select extends QuerySelect { + /** + * {@inheritdoc} + */ + public function __construct(Connection $connection, $table, $alias = NULL, array $options = []) { + // @todo Remove the __construct in Drupal 11. + // @see https://www.drupal.org/project/drupal/issues/3256524 + parent::__construct($connection, $table, $alias, $options); + unset($this->queryOptions['return']); + } + public function forUpdate($set = TRUE) { // SQLite does not support FOR UPDATE so nothing to do. return $this; diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Truncate.php b/core/modules/sqlite/src/Driver/Database/sqlite/Truncate.php index f1535fb0196..137e395b03f 100644 --- a/core/modules/sqlite/src/Driver/Database/sqlite/Truncate.php +++ b/core/modules/sqlite/src/Driver/Database/sqlite/Truncate.php @@ -12,6 +12,16 @@ use Drupal\Core\Database\Query\Truncate as QueryTruncate; */ class Truncate extends QueryTruncate { + /** + * {@inheritdoc} + */ + public function __construct(Connection $connection, string $table, array $options = []) { + // @todo Remove the __construct in Drupal 11. + // @see https://www.drupal.org/project/drupal/issues/3256524 + parent::__construct($connection, $table, $options); + unset($this->queryOptions['return']); + } + public function __toString() { // Create a sanitized comment string to prepend to the query. $comments = $this->connection->makeComment($this->comments); diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Update.php b/core/modules/sqlite/src/Driver/Database/sqlite/Update.php new file mode 100644 index 00000000000..d101ed384f7 --- /dev/null +++ b/core/modules/sqlite/src/Driver/Database/sqlite/Update.php @@ -0,0 +1,22 @@ +queryOptions['return']); + } + +} diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Upsert.php b/core/modules/sqlite/src/Driver/Database/sqlite/Upsert.php index 59974272531..9e42fcf4baa 100644 --- a/core/modules/sqlite/src/Driver/Database/sqlite/Upsert.php +++ b/core/modules/sqlite/src/Driver/Database/sqlite/Upsert.php @@ -11,6 +11,16 @@ use Drupal\Core\Database\Query\Upsert as QueryUpsert; */ class Upsert extends QueryUpsert { + /** + * {@inheritdoc} + */ + public function __construct(Connection $connection, string $table, array $options = []) { + // @todo Remove the __construct in Drupal 11. + // @see https://www.drupal.org/project/drupal/issues/3256524 + parent::__construct($connection, $table, $options); + unset($this->queryOptions['return']); + } + /** * {@inheritdoc} */ diff --git a/core/modules/system/tests/modules/driver_test/driver_test.info.yml b/core/modules/system/tests/modules/driver_test/driver_test.info.yml index c36161d2704..ad967305b36 100644 --- a/core/modules/system/tests/modules/driver_test/driver_test.info.yml +++ b/core/modules/system/tests/modules/driver_test/driver_test.info.yml @@ -3,3 +3,6 @@ type: module description: 'Support database contrib driver testing.' package: Testing version: VERSION +dependencies: + - drupal:mysql + - drupal:pgsql diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Delete.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Delete.php new file mode 100644 index 00000000000..9e0ca191345 --- /dev/null +++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Delete.php @@ -0,0 +1,10 @@ +assertEquals('Update value 1', $result->update); } + /** + * Tests deprecation of the 'return' query option. + * + * @covers ::query + * @covers ::prepareStatement + * + * @group legacy + */ + public function testReturnOptionDeprecation() { + $this->expectDeprecation('Passing "return" option to %Aquery() is deprecated in drupal:9.4.0 and is removed in drupal:11.0.0. For data manipulation operations, use dynamic queries instead. See https://www.drupal.org/node/3185520'); + $this->expectDeprecation('Passing "return" option to %AprepareStatement() is deprecated in drupal:9.4.0 and is removed in drupal:11.0.0. For data manipulation operations, use dynamic queries instead. See https://www.drupal.org/node/3185520'); + $this->assertIsInt((int) $this->connection->query('INSERT INTO {test} ([name], [age], [job]) VALUES (:name, :age, :job)', [ + ':name' => 'Magoo', + ':age' => 56, + ':job' => 'Driver', + ], ['return' => Database::RETURN_INSERT_ID])); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Database/StatementTest.php b/core/tests/Drupal/KernelTests/Core/Database/StatementTest.php index b0071a4bf07..f1c0cc80602 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/StatementTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/StatementTest.php @@ -2,7 +2,6 @@ namespace Drupal\KernelTests\Core\Database; -use Drupal\Core\Database\Database; use Drupal\Core\Database\StatementInterface; /** @@ -24,7 +23,6 @@ class StatementTest extends DatabaseTestBase { ':age' => '30', ]; $options = [ - 'return' => Database::RETURN_STATEMENT, 'allow_square_brackets' => FALSE, ]; diff --git a/core/tests/Drupal/Tests/Core/Database/RowCountExceptionTest.php b/core/tests/Drupal/Tests/Core/Database/RowCountExceptionTest.php index 10bfc0b7267..593054d19ca 100644 --- a/core/tests/Drupal/Tests/Core/Database/RowCountExceptionTest.php +++ b/core/tests/Drupal/Tests/Core/Database/RowCountExceptionTest.php @@ -15,7 +15,7 @@ class RowCountExceptionTest extends UnitTestCase { /** * The default exception message. */ - private const DEFAULT_EXCEPTION_MESSAGE = "rowCount() is supported for DELETE, INSERT, or UPDATE statements performed with structured query builders only, since they would not be portable across database engines otherwise. If the query builders are not sufficient, set the 'return' option to Database::RETURN_AFFECTED to get the number of affected rows."; + private const DEFAULT_EXCEPTION_MESSAGE = "rowCount() is supported for DELETE, INSERT, or UPDATE statements performed with structured query builders only, since they would not be portable across database engines otherwise. If the query builders are not sufficient, use a prepareStatement() with an \$allow_row_count argument set to TRUE, execute() the Statement and get the number of affected rows via rowCount()."; /** * Data provider for ::testExceptionMessage()