From 934f42ae8706419708d2d0072520554a353b8964 Mon Sep 17 00:00:00 2001 From: Alex Pott <alex.a.pott@googlemail.com> Date: Wed, 29 Dec 2021 17:14:14 +0000 Subject: [PATCH] Issue #3129043 by mondrake, daffie, ankithashetty, ravi.shankar, alexpott, Beakerboy, catch: Move core database drivers to modules of their own --- composer.lock | 8 +- .../scaffold/files/default.settings.php | 6 +- core/composer.json | 6 +- core/includes/install.inc | 7 +- .../lib/Drupal/Core/Command/DbDumpCommand.php | 2 +- core/lib/Drupal/Core/Database/Database.php | 54 +- .../Core/Database/Driver/mysql/Connection.php | 495 +------- .../Driver/mysql/ExceptionHandler.php | 60 +- .../Core/Database/Driver/mysql/Insert.php | 66 +- .../Database/Driver/mysql/Install/Tasks.php | 215 +--- .../Core/Database/Driver/mysql/Schema.php | 714 +---------- .../Core/Database/Driver/mysql/Upsert.php | 44 +- .../Core/Database/Driver/pgsql/Connection.php | 374 +----- .../Core/Database/Driver/pgsql/Delete.php | 30 +- .../Core/Database/Driver/pgsql/Insert.php | 157 +-- .../Database/Driver/pgsql/Install/Tasks.php | 295 +---- .../Core/Database/Driver/pgsql/Schema.php | 1083 +--------------- .../Core/Database/Driver/pgsql/Select.php | 159 +-- .../Core/Database/Driver/pgsql/Truncate.php | 30 +- .../Core/Database/Driver/pgsql/Update.php | 84 +- .../Core/Database/Driver/pgsql/Upsert.php | 125 +- .../Database/Driver/sqlite/Connection.php | 529 +------- .../Core/Database/Driver/sqlite/Insert.php | 51 +- .../Database/Driver/sqlite/Install/Tasks.php | 116 +- .../Core/Database/Driver/sqlite/Schema.php | 836 +------------ .../Core/Database/Driver/sqlite/Select.php | 18 +- .../Core/Database/Driver/sqlite/Statement.php | 150 +-- .../Core/Database/Driver/sqlite/Truncate.php | 21 +- .../Core/Database/Driver/sqlite/Upsert.php | 46 +- .../Core/Database/StatementInterface.php | 8 +- .../Core/Database/StatementPrefetch.php | 2 +- core/lib/Drupal/Core/Site/Settings.php | 37 + .../Core/Test/FunctionalTestSetupTrait.php | 1 + .../src/Unit/CommentStatisticsUnitTest.php | 2 +- .../src/Functional/ConfigImportAllTest.php | 8 +- .../src/Kernel/MigrateSqlSourceTestBase.php | 4 +- .../tests/src/Kernel/QueryBatchTest.php | 4 +- .../tests/src/Unit/MigrateSqlIdMapTest.php | 4 +- .../tests/src/Unit/MigrateTestCase.php | 4 +- core/modules/mysql/mysql.info.yml | 5 + core/modules/mysql/mysql.module | 22 + .../src/Driver/Database/mysql/Connection.php | 495 ++++++++ .../Database/mysql/ExceptionHandler.php | 59 + .../src/Driver/Database/mysql/Insert.php | 65 + .../Driver/Database/mysql/Install/Tasks.php | 214 ++++ .../src/Driver/Database/mysql/Schema.php | 715 +++++++++++ .../src/Driver/Database/mysql/Upsert.php | 43 + core/modules/pgsql/pgsql.info.yml | 5 + core/modules/pgsql/pgsql.module | 22 + .../src/Driver/Database/pgsql/Connection.php | 375 ++++++ .../src/Driver/Database/pgsql/Delete.php | 29 + .../src/Driver/Database/pgsql/Insert.php | 158 +++ .../Driver/Database/pgsql/Install/Tasks.php | 294 +++++ .../src/Driver/Database/pgsql/Schema.php | 1084 +++++++++++++++++ .../src/Driver/Database/pgsql/Select.php | 160 +++ .../src/Driver/Database/pgsql/Truncate.php | 29 + .../src/Driver/Database/pgsql/Update.php | 83 ++ .../src/Driver/Database/pgsql/Upsert.php | 126 ++ core/modules/sqlite/sqlite.info.yml | 5 + core/modules/sqlite/sqlite.module | 22 + .../src/Driver/Database/sqlite/Connection.php | 528 ++++++++ .../src/Driver/Database/sqlite/Insert.php | 52 + .../Driver/Database/sqlite/Install/Tasks.php | 115 ++ .../src/Driver/Database/sqlite/Schema.php | 837 +++++++++++++ .../src/Driver/Database/sqlite/Select.php | 17 + .../src/Driver/Database/sqlite/Statement.php | 151 +++ .../src/Driver/Database/sqlite/Truncate.php | 22 + .../src/Driver/Database/sqlite/Upsert.php | 47 + core/modules/system/system.install | 18 +- core/modules/system/system.post_update.php | 27 + .../src/mysql/Connection.php | 2 +- .../src/mysql/Install/Tasks.php | 2 +- .../src/pgsql/Connection.php | 2 +- .../src/pgsql/Install/Tasks.php | 2 +- .../src/sqlite/Connection.php | 2 +- .../src/sqlite/Install/Tasks.php | 2 +- .../database_test/database_test.install | 2 +- .../Database/DrivertestMysql/Connection.php | 4 +- .../Database/DrivertestMysql/Insert.php | 4 +- .../DrivertestMysql/Install/Tasks.php | 4 +- .../Database/DrivertestMysql/Schema.php | 4 +- .../Database/DrivertestMysql/Upsert.php | 4 +- .../Connection.php | 4 +- .../Insert.php | 4 +- .../Install/Tasks.php | 4 +- .../Schema.php | 4 +- .../Upsert.php | 4 +- .../Database/DrivertestPgsql/Connection.php | 4 +- .../Database/DrivertestPgsql/Delete.php | 4 +- .../Database/DrivertestPgsql/Insert.php | 4 +- .../DrivertestPgsql/Install/Tasks.php | 4 +- .../Database/DrivertestPgsql/Schema.php | 4 +- .../Database/DrivertestPgsql/Select.php | 4 +- .../Database/DrivertestPgsql/Truncate.php | 4 +- .../Database/DrivertestPgsql/Update.php | 4 +- .../Database/DrivertestPgsql/Upsert.php | 4 +- ...UpdateEnableProviderDatabaseDriverTest.php | 45 + .../Plugin/views/argument/StringArgument.php | 2 +- .../src/Plugin/views/query/SqliteDateSql.php | 2 +- .../Framework/Tests/HtRouterTest.php | 2 +- ...yleDatabaseConnectionInSettingsPhpTest.php | 59 + .../Installer/InstallerTest.php | 29 + .../Core/Database/MysqlDriverLegacyTest.php | 88 ++ .../KernelTests/Core/Database/NextIdTest.php | 4 +- .../Core/Database/PgsqlDriverLegacyTest.php | 115 ++ .../Core/Database/PrefixInfoTest.php | 2 +- .../KernelTests/Core/Database/QueryTest.php | 2 +- .../KernelTests/Core/Database/SchemaTest.php | 8 +- .../Core/Database/SelectComplexTest.php | 2 +- .../Core/Database/SqliteDriverLegacyTest.php | 105 ++ .../Core/Database/TransactionTest.php | 2 +- .../Drupal/KernelTests/KernelTestBaseTest.php | 10 + .../Tests/Core/Command/GenerateThemeTest.php | 2 +- .../Tests/Core/Command/QuickStartTest.php | 2 +- .../Tests/Core/Database/DatabaseTest.php | 2 +- .../Database/Driver/mysql/ConnectionTest.php | 6 +- .../Driver/mysql/install/TasksTest.php | 14 +- .../Driver/pgsql/PostgresqlSchemaTest.php | 8 +- .../Database/Driver/sqlite/ConnectionTest.php | 4 +- .../Core/Database/InstallerObjectTest.php | 4 +- .../Tests/Core/Database/UrlConversionTest.php | 95 +- .../Compiler/BackendCompilerPassTest.php | 2 +- .../Tests/Core/Test/TestSetupTraitTest.php | 5 +- sites/default/default.settings.php | 6 +- 124 files changed, 6742 insertions(+), 5654 deletions(-) create mode 100644 core/modules/mysql/mysql.info.yml create mode 100644 core/modules/mysql/mysql.module create mode 100644 core/modules/mysql/src/Driver/Database/mysql/Connection.php create mode 100644 core/modules/mysql/src/Driver/Database/mysql/ExceptionHandler.php create mode 100644 core/modules/mysql/src/Driver/Database/mysql/Insert.php create mode 100644 core/modules/mysql/src/Driver/Database/mysql/Install/Tasks.php create mode 100644 core/modules/mysql/src/Driver/Database/mysql/Schema.php create mode 100644 core/modules/mysql/src/Driver/Database/mysql/Upsert.php create mode 100644 core/modules/pgsql/pgsql.info.yml create mode 100644 core/modules/pgsql/pgsql.module create mode 100644 core/modules/pgsql/src/Driver/Database/pgsql/Connection.php create mode 100644 core/modules/pgsql/src/Driver/Database/pgsql/Delete.php create mode 100644 core/modules/pgsql/src/Driver/Database/pgsql/Insert.php create mode 100644 core/modules/pgsql/src/Driver/Database/pgsql/Install/Tasks.php create mode 100644 core/modules/pgsql/src/Driver/Database/pgsql/Schema.php create mode 100644 core/modules/pgsql/src/Driver/Database/pgsql/Select.php create mode 100644 core/modules/pgsql/src/Driver/Database/pgsql/Truncate.php create mode 100644 core/modules/pgsql/src/Driver/Database/pgsql/Update.php create mode 100644 core/modules/pgsql/src/Driver/Database/pgsql/Upsert.php create mode 100644 core/modules/sqlite/sqlite.info.yml create mode 100644 core/modules/sqlite/sqlite.module create mode 100644 core/modules/sqlite/src/Driver/Database/sqlite/Connection.php create mode 100644 core/modules/sqlite/src/Driver/Database/sqlite/Insert.php create mode 100644 core/modules/sqlite/src/Driver/Database/sqlite/Install/Tasks.php create mode 100644 core/modules/sqlite/src/Driver/Database/sqlite/Schema.php create mode 100644 core/modules/sqlite/src/Driver/Database/sqlite/Select.php create mode 100644 core/modules/sqlite/src/Driver/Database/sqlite/Statement.php create mode 100644 core/modules/sqlite/src/Driver/Database/sqlite/Truncate.php create mode 100644 core/modules/sqlite/src/Driver/Database/sqlite/Upsert.php create mode 100644 core/modules/system/tests/src/Functional/Update/UpdateEnableProviderDatabaseDriverTest.php create mode 100644 core/tests/Drupal/FunctionalTests/ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest.php create mode 100644 core/tests/Drupal/KernelTests/Core/Database/MysqlDriverLegacyTest.php create mode 100644 core/tests/Drupal/KernelTests/Core/Database/PgsqlDriverLegacyTest.php create mode 100644 core/tests/Drupal/KernelTests/Core/Database/SqliteDriverLegacyTest.php diff --git a/composer.lock b/composer.lock index cf7affcdae79..ff85096117ac 100644 --- a/composer.lock +++ b/composer.lock @@ -452,7 +452,7 @@ "dist": { "type": "path", "url": "core", - "reference": "3009c9eaa73ac4f9ff0d92a18714a2f9ac7a8877" + "reference": "fe303578f231198d09504af69ee768be7c455b06" }, "require": { "asm89/stack-cors": "^1.1", @@ -583,12 +583,14 @@ "drupal/migrate_drupal_multilingual": "self.version", "drupal/migrate_drupal_ui": "self.version", "drupal/minimal": "self.version", + "drupal/mysql": "self.version", "drupal/node": "self.version", "drupal/olivero": "self.version", "drupal/options": "self.version", "drupal/page_cache": "self.version", "drupal/path": "self.version", "drupal/path_alias": "self.version", + "drupal/pgsql": "self.version", "drupal/quickedit": "self.version", "drupal/rdf": "self.version", "drupal/responsive_image": "self.version", @@ -598,6 +600,7 @@ "drupal/settings_tray": "self.version", "drupal/seven": "self.version", "drupal/shortcut": "self.version", + "drupal/sqlite": "self.version", "drupal/standard": "self.version", "drupal/stark": "self.version", "drupal/statistics": "self.version", @@ -668,9 +671,6 @@ "lib/Drupal/Core/Cache/DatabaseCacheTagsChecksum.php", "lib/Drupal/Core/Database/Connection.php", "lib/Drupal/Core/Database/Database.php", - "lib/Drupal/Core/Database/Driver/mysql/Connection.php", - "lib/Drupal/Core/Database/Driver/pgsql/Connection.php", - "lib/Drupal/Core/Database/Driver/sqlite/Connection.php", "lib/Drupal/Core/Database/Statement.php", "lib/Drupal/Core/Database/StatementInterface.php", "lib/Drupal/Core/DependencyInjection/Container.php", diff --git a/core/assets/scaffold/files/default.settings.php b/core/assets/scaffold/files/default.settings.php index 718890224b48..4768843bcf07 100644 --- a/core/assets/scaffold/files/default.settings.php +++ b/core/assets/scaffold/files/default.settings.php @@ -170,9 +170,9 @@ * information on these defaults and the potential issues. * * More details can be found in the constructor methods for each driver: - * - \Drupal\Core\Database\Driver\mysql\Connection::__construct() - * - \Drupal\Core\Database\Driver\pgsql\Connection::__construct() - * - \Drupal\Core\Database\Driver\sqlite\Connection::__construct() + * - \Drupal\mysql\Driver\Database\mysql\Connection::__construct() + * - \Drupal\pgsql\Driver\Database\pgsql\Connection::__construct() + * - \Drupal\sqlite\Driver\Database\sqlite\Connection::__construct() * * Sample Database configuration format for PostgreSQL (pgsql): * @code diff --git a/core/composer.json b/core/composer.json index a303372e2d33..fc487126854f 100644 --- a/core/composer.json +++ b/core/composer.json @@ -132,12 +132,14 @@ "drupal/migrate_drupal": "self.version", "drupal/migrate_drupal_multilingual": "self.version", "drupal/migrate_drupal_ui": "self.version", + "drupal/mysql": "self.version", "drupal/node": "self.version", "drupal/olivero": "self.version", "drupal/options": "self.version", "drupal/page_cache": "self.version", "drupal/path": "self.version", "drupal/path_alias": "self.version", + "drupal/pgsql": "self.version", "drupal/quickedit": "self.version", "drupal/rdf": "self.version", "drupal/responsive_image": "self.version", @@ -147,6 +149,7 @@ "drupal/settings_tray": "self.version", "drupal/seven": "self.version", "drupal/shortcut": "self.version", + "drupal/sqlite": "self.version", "drupal/standard": "self.version", "drupal/stark": "self.version", "drupal/statistics": "self.version", @@ -189,9 +192,6 @@ "lib/Drupal/Core/Cache/DatabaseCacheTagsChecksum.php", "lib/Drupal/Core/Database/Connection.php", "lib/Drupal/Core/Database/Database.php", - "lib/Drupal/Core/Database/Driver/mysql/Connection.php", - "lib/Drupal/Core/Database/Driver/pgsql/Connection.php", - "lib/Drupal/Core/Database/Driver/sqlite/Connection.php", "lib/Drupal/Core/Database/Statement.php", "lib/Drupal/Core/Database/StatementInterface.php", "lib/Drupal/Core/DependencyInjection/Container.php", diff --git a/core/includes/install.inc b/core/includes/install.inc index dcb7ffa6911f..da4a89049f57 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -171,12 +171,13 @@ function drupal_get_database_types() { // The internal database driver name is any valid PHP identifier. $mask = ExtensionDiscovery::PHP_FUNCTION_PATTERN; - // Find drivers in the Drupal\Core and Drupal\Driver namespaces. + // Find drivers in the Drupal\Driver namespace. + // @todo remove discovering in the Drupal\Driver namespace in D10. /** @var \Drupal\Core\File\FileSystemInterface $file_system */ $file_system = \Drupal::service('file_system'); - $files = $file_system->scanDirectory(DRUPAL_ROOT . '/core/lib/Drupal/Core/Database/Driver', $mask, ['recurse' => FALSE]); + $files = []; if (is_dir(DRUPAL_ROOT . '/drivers/lib/Drupal/Driver/Database')) { - $files += $file_system->scanDirectory(DRUPAL_ROOT . '/drivers/lib/Drupal/Driver/Database/', $mask, ['recurse' => FALSE]); + $files = $file_system->scanDirectory(DRUPAL_ROOT . '/drivers/lib/Drupal/Driver/Database/', $mask, ['recurse' => FALSE]); } foreach ($files as $file) { if (file_exists($file->uri . '/Install/Tasks.php')) { diff --git a/core/lib/Drupal/Core/Command/DbDumpCommand.php b/core/lib/Drupal/Core/Command/DbDumpCommand.php index c1b467289546..6b2cc7f7029e 100644 --- a/core/lib/Drupal/Core/Command/DbDumpCommand.php +++ b/core/lib/Drupal/Core/Command/DbDumpCommand.php @@ -272,7 +272,7 @@ protected function getTableIndexes(Connection $connection, $table, &$definition) */ protected function getTableCollation(Connection $connection, $table, &$definition) { // Remove identifier quotes from the table name. See - // \Drupal\Core\Database\Driver\mysql\Connection::$identifierQuotes. + // \Drupal\mysql\Driver\Database\mysql\Connection::$identifierQuotes. $table = trim($connection->prefixTables('{' . $table . '}'), '"'); $query = $connection->query("SHOW TABLE STATUS WHERE NAME = :table_name", [':table_name' => $table]); $data = $query->fetchAssoc(); diff --git a/core/lib/Drupal/Core/Database/Database.php b/core/lib/Drupal/Core/Database/Database.php index 2771166e48f9..2394c5eff6ef 100644 --- a/core/lib/Drupal/Core/Database/Database.php +++ b/core/lib/Drupal/Core/Database/Database.php @@ -238,7 +238,7 @@ final public static function parseConnectionInfo(array $info) { // Fallback for Drupal 7 settings.php if namespace is not provided. if (empty($info['namespace'])) { - $info['namespace'] = 'Drupal\\Core\\Database\\Driver\\' . $info['driver']; + $info['namespace'] = 'Drupal\\' . $info['driver'] . '\\Driver\\Database\\' . $info['driver']; } return $info; @@ -465,38 +465,42 @@ public static function convertDbUrlToConnectionInfo($url, $root) { $driver = $matches[1]; // Determine if the database driver is provided by a module. + // @todo https://www.drupal.org/project/drupal/issues/3250999. Refactor when + // all database drivers are provided by modules. $module = NULL; $connection_class = NULL; $url_components = parse_url($url); - if (isset($url_components['query'])) { - parse_str($url_components['query'], $query); - if (isset($query['module']) && $query['module']) { - $module = $query['module']; - // Set up an additional autoloader. We don't use the main autoloader as - // this method can be called before Drupal is installed and is never - // called during regular runtime. - $namespace = "Drupal\\$module\\Driver\\Database\\$driver"; - $psr4_base_directory = Database::findDriverAutoloadDirectory($namespace, $root, TRUE); - $additional_class_loader = new ClassLoader(); - $additional_class_loader->addPsr4($namespace . '\\', $psr4_base_directory); - $additional_class_loader->register(TRUE); - $connection_class = $custom_connection_class = $namespace . '\\Connection'; - } + $url_component_query = $url_components['query'] ?? ''; + parse_str($url_component_query, $query); + + // Add the module key for core database drivers when the module key is not + // set. + if (!isset($query['module']) && in_array($driver, ['mysql', 'pgsql', 'sqlite'], TRUE)) { + $query['module'] = $driver; + } + + if (isset($query['module']) && $query['module']) { + $module = $query['module']; + // Set up an additional autoloader. We don't use the main autoloader as + // this method can be called before Drupal is installed and is never + // called during regular runtime. + $namespace = "Drupal\\$module\\Driver\\Database\\$driver"; + $psr4_base_directory = Database::findDriverAutoloadDirectory($namespace, $root, TRUE); + $additional_class_loader = new ClassLoader(); + $additional_class_loader->addPsr4($namespace . '\\', $psr4_base_directory); + $additional_class_loader->register(TRUE); + $connection_class = $namespace . '\\Connection'; } if (!$module) { // Determine the connection class to use. Discover if the URL has a valid - // driver scheme. Try with Drupal 8 style custom drivers first, since - // those can override/extend the core ones. - $connection_class = $custom_connection_class = "Drupal\\Driver\\Database\\{$driver}\\Connection"; - if (!class_exists($connection_class)) { - // If the URL is not relative to a custom driver, try with core ones. - $connection_class = "Drupal\\Core\\Database\\Driver\\{$driver}\\Connection"; - } + // driver scheme for a Drupal 8 style custom driver. + // @todo Remove this in Drupal 10. + $connection_class = "Drupal\\Driver\\Database\\{$driver}\\Connection"; } if (!class_exists($connection_class)) { - throw new \InvalidArgumentException("Can not convert '$url' to a database connection, class '$custom_connection_class' does not exist"); + throw new \InvalidArgumentException("Can not convert '$url' to a database connection, class '$connection_class' does not exist"); } $options = $connection_class::createConnectionOptionsFromUrl($url, $root); @@ -641,8 +645,8 @@ protected static function getDatabaseDriverNamespace(array $connection_info) { if (isset($connection_info['namespace'])) { return $connection_info['namespace']; } - // Fallback for Drupal 7 settings.php. - return 'Drupal\\Core\\Database\\Driver\\' . $connection_info['driver']; + // Fallback for when the namespace is not provided in settings.php. + return 'Drupal\\' . $connection_info['driver'] . '\\Driver\\Database\\' . $connection_info['driver']; } /** diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php index ef6027256ccc..8ae316c821a7 100644 --- a/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php @@ -2,495 +2,16 @@ namespace Drupal\Core\Database\Driver\mysql; -use Drupal\Core\Database\DatabaseAccessDeniedException; -use Drupal\Core\Database\IntegrityConstraintViolationException; -use Drupal\Core\Database\DatabaseExceptionWrapper; -use Drupal\Core\Database\StatementInterface; -use Drupal\Core\Database\StatementWrapper; -use Drupal\Core\Database\Database; -use Drupal\Core\Database\DatabaseNotFoundException; -use Drupal\Core\Database\DatabaseException; -use Drupal\Core\Database\Connection as DatabaseConnection; -use Drupal\Core\Database\TransactionNoActiveException; +use Drupal\mysql\Driver\Database\mysql\Connection as MysqlConnection; -/** - * @addtogroup database - * @{ - */ +@trigger_error('\Drupal\Core\Database\Driver\mysql\Connection is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED); /** * MySQL implementation of \Drupal\Core\Database\Connection. + * + * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL + * database driver has been moved to the mysql module. + * + * @see https://www.drupal.org/node/3129492 */ -class Connection extends DatabaseConnection { - - /** - * Error code for "Unknown database" error. - */ - const DATABASE_NOT_FOUND = 1049; - - /** - * Error code for "Access denied" error. - */ - const ACCESS_DENIED = 1045; - - /** - * Error code for "Can't initialize character set" error. - */ - const UNSUPPORTED_CHARSET = 2019; - - /** - * Driver-specific error code for "Unknown character set" error. - */ - const UNKNOWN_CHARSET = 1115; - - /** - * SQLSTATE error code for "Syntax error or access rule violation". - */ - const SQLSTATE_SYNTAX_ERROR = 42000; - - /** - * {@inheritdoc} - */ - protected $statementClass = NULL; - - /** - * {@inheritdoc} - */ - protected $statementWrapperClass = StatementWrapper::class; - - /** - * Flag to indicate if the cleanup function in __destruct() should run. - * - * @var bool - */ - protected $needsCleanup = FALSE; - - /** - * Stores the server version after it has been retrieved from the database. - * - * @var string - * - * @see \Drupal\Core\Database\Driver\mysql\Connection::version - */ - private $serverVersion; - - /** - * The minimal possible value for the max_allowed_packet setting of MySQL. - * - * @link https://mariadb.com/kb/en/mariadb/server-system-variables/#max_allowed_packet - * @link https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_max_allowed_packet - * - * @var int - */ - const MIN_MAX_ALLOWED_PACKET = 1024; - - /** - * {@inheritdoc} - */ - protected $identifierQuotes = ['"', '"']; - - /** - * {@inheritdoc} - */ - public function __construct(\PDO $connection, array $connection_options) { - // If the SQL mode doesn't include 'ANSI_QUOTES' (explicitly or via a - // combination mode), then MySQL doesn't interpret a double quote as an - // identifier quote, in which case use the non-ANSI-standard backtick. - // - // Because we still support MySQL 5.7, check for the deprecated combination - // modes as well. - // - // @see https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_ansi_quotes - $ansi_quotes_modes = ['ANSI_QUOTES', 'ANSI', 'DB2', 'MAXDB', 'MSSQL', 'ORACLE', 'POSTGRESQL']; - $is_ansi_quotes_mode = FALSE; - foreach ($ansi_quotes_modes as $mode) { - // None of the modes in $ansi_quotes_modes are substrings of other modes - // that are not in $ansi_quotes_modes, so a simple stripos() does not - // return false positives. - if (stripos($connection_options['init_commands']['sql_mode'], $mode) !== FALSE) { - $is_ansi_quotes_mode = TRUE; - break; - } - } - if ($this->identifierQuotes === ['"', '"'] && !$is_ansi_quotes_mode) { - $this->identifierQuotes = ['`', '`']; - } - parent::__construct($connection, $connection_options); - } - - /** - * {@inheritdoc} - */ - protected function handleQueryException(\PDOException $e, $query, array $args = [], $options = []) { - // In case of attempted INSERT of a record with an undefined column and no - // default value indicated in schema, MySql returns a 1364 error code. - // Throw an IntegrityConstraintViolationException here like the other - // drivers do, to avoid the parent class to throw a generic - // DatabaseExceptionWrapper instead. - if (!empty($e->errorInfo[1]) && $e->errorInfo[1] === 1364) { - @trigger_error('Connection::handleQueryException() is deprecated in drupal:9.2.0 and is removed in drupal:10.0.0. Get a handler through $this->exceptionHandler() instead, and use one of its methods. See https://www.drupal.org/node/3187222', E_USER_DEPRECATED); - $query_string = ($query instanceof StatementInterface) ? $query->getQueryString() : $query; - $message = $e->getMessage() . ": " . $query_string . "; " . print_r($args, TRUE); - throw new IntegrityConstraintViolationException($message, is_int($e->getCode()) ? $e->getCode() : 0, $e); - } - - parent::handleQueryException($e, $query, $args, $options); - } - - /** - * {@inheritdoc} - */ - public static function open(array &$connection_options = []) { - if (isset($connection_options['_dsn_utf8_fallback']) && $connection_options['_dsn_utf8_fallback'] === TRUE) { - // Only used during the installer version check, as a fallback from utf8mb4. - $charset = 'utf8'; - } - else { - $charset = 'utf8mb4'; - } - // The DSN should use either a socket or a host/port. - if (isset($connection_options['unix_socket'])) { - $dsn = 'mysql:unix_socket=' . $connection_options['unix_socket']; - } - else { - // Default to TCP connection on port 3306. - $dsn = 'mysql:host=' . $connection_options['host'] . ';port=' . (empty($connection_options['port']) ? 3306 : $connection_options['port']); - } - // Character set is added to dsn to ensure PDO uses the proper character - // set when escaping. This has security implications. See - // https://www.drupal.org/node/1201452 for further discussion. - $dsn .= ';charset=' . $charset; - if (!empty($connection_options['database'])) { - $dsn .= ';dbname=' . $connection_options['database']; - } - // Allow PDO options to be overridden. - $connection_options += [ - 'pdo' => [], - ]; - $connection_options['pdo'] += [ - \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, - // So we don't have to mess around with cursors and unbuffered queries by default. - \PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => TRUE, - // Make sure MySQL returns all matched rows on update queries including - // rows that actually didn't have to be updated because the values didn't - // change. This matches common behavior among other database systems. - \PDO::MYSQL_ATTR_FOUND_ROWS => TRUE, - // Because MySQL's prepared statements skip the query cache, because it's dumb. - \PDO::ATTR_EMULATE_PREPARES => TRUE, - // Limit SQL to a single statement like mysqli. - \PDO::MYSQL_ATTR_MULTI_STATEMENTS => FALSE, - // Convert numeric values to strings when fetching. In PHP 8.1, - // \PDO::ATTR_EMULATE_PREPARES now behaves the same way as non emulated - // prepares and returns integers. See https://externals.io/message/113294 - // for further discussion. - \PDO::ATTR_STRINGIFY_FETCHES => TRUE, - ]; - - try { - $pdo = new \PDO($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']); - } - catch (\PDOException $e) { - if ($e->getCode() == static::DATABASE_NOT_FOUND) { - throw new DatabaseNotFoundException($e->getMessage(), $e->getCode(), $e); - } - if ($e->getCode() == static::ACCESS_DENIED) { - throw new DatabaseAccessDeniedException($e->getMessage(), $e->getCode(), $e); - } - throw $e; - } - - // Force MySQL to use the UTF-8 character set. Also set the collation, if a - // certain one has been set; otherwise, MySQL defaults to - // 'utf8mb4_general_ci' (MySQL 5) or 'utf8mb4_0900_ai_ci' (MySQL 8) for - // utf8mb4. - if (!empty($connection_options['collation'])) { - $pdo->exec('SET NAMES ' . $charset . ' COLLATE ' . $connection_options['collation']); - } - else { - $pdo->exec('SET NAMES ' . $charset); - } - - // Set MySQL init_commands if not already defined. Default Drupal's MySQL - // behavior to conform more closely to SQL standards. This allows Drupal - // to run almost seamlessly on many different kinds of database systems. - // These settings force MySQL to behave the same as postgresql, or sqlite - // in regards to syntax interpretation and invalid data handling. See - // https://www.drupal.org/node/344575 for further discussion. Also, as MySQL - // 5.5 changed the meaning of TRADITIONAL we need to spell out the modes one - // by one. - $connection_options += [ - 'init_commands' => [], - ]; - - $connection_options['init_commands'] += [ - 'sql_mode' => "SET sql_mode = 'ANSI,TRADITIONAL'", - ]; - - // Execute initial commands. - foreach ($connection_options['init_commands'] as $sql) { - $pdo->exec($sql); - } - - return $pdo; - } - - /** - * {@inheritdoc} - */ - public function __destruct() { - if ($this->needsCleanup) { - $this->nextIdDelete(); - } - parent::__destruct(); - } - - public function queryRange($query, $from, $count, array $args = [], array $options = []) { - return $this->query($query . ' LIMIT ' . (int) $from . ', ' . (int) $count, $args, $options); - } - - /** - * {@inheritdoc} - */ - public function queryTemporary($query, array $args = [], array $options = []) { - @trigger_error('Connection::queryTemporary() is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. There is no replacement. See https://www.drupal.org/node/3211781', E_USER_DEPRECATED); - $tablename = $this->generateTemporaryTableName(); - $this->query('CREATE TEMPORARY TABLE {' . $tablename . '} Engine=MEMORY ' . $query, $args, $options); - return $tablename; - } - - public function driver() { - return 'mysql'; - } - - /** - * {@inheritdoc} - */ - public function version() { - if ($this->isMariaDb()) { - return $this->getMariaDbVersionMatch(); - } - - return $this->getServerVersion(); - } - - /** - * Determines whether the MySQL distribution is MariaDB or not. - * - * @return bool - * Returns TRUE if the distribution is MariaDB, or FALSE if not. - */ - public function isMariaDb(): bool { - return (bool) $this->getMariaDbVersionMatch(); - } - - /** - * Gets the MariaDB portion of the server version. - * - * @return string - * The MariaDB portion of the server version if present, or NULL if not. - */ - protected function getMariaDbVersionMatch(): ?string { - // MariaDB may prefix its version string with '5.5.5-', which should be - // ignored. - // @see https://github.com/MariaDB/server/blob/f6633bf058802ad7da8196d01fd19d75c53f7274/include/mysql_com.h#L42. - $regex = '/^(?:5\.5\.5-)?(\d+\.\d+\.\d+.*-mariadb.*)/i'; - - preg_match($regex, $this->getServerVersion(), $matches); - return (empty($matches[1])) ? NULL : $matches[1]; - } - - /** - * Gets the server version. - * - * @return string - * The PDO server version. - */ - protected function getServerVersion(): string { - if (!$this->serverVersion) { - $this->serverVersion = $this->connection->query('SELECT VERSION()')->fetchColumn(); - } - return $this->serverVersion; - } - - public function databaseType() { - return 'mysql'; - } - - /** - * Overrides \Drupal\Core\Database\Connection::createDatabase(). - * - * @param string $database - * The name of the database to create. - * - * @throws \Drupal\Core\Database\DatabaseNotFoundException - */ - public function createDatabase($database) { - // Escape the database name. - $database = Database::getConnection()->escapeDatabase($database); - - try { - // Create the database and set it as active. - $this->connection->exec("CREATE DATABASE $database"); - $this->connection->exec("USE $database"); - } - catch (\Exception $e) { - throw new DatabaseNotFoundException($e->getMessage()); - } - } - - public function mapConditionOperator($operator) { - // We don't want to override any of the defaults. - return NULL; - } - - public function nextId($existing_id = 0) { - $new_id = $this->query('INSERT INTO {sequences} () VALUES ()', [], ['return' => Database::RETURN_INSERT_ID]); - // 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 - // INSERT, MySQL will generate a larger value. However, there is no way - // of knowing whether this value already exists in the table. MySQL - // provides an INSERT IGNORE which would work, but that can mask problems - // other than duplicate keys. Instead, we use INSERT ... ON DUPLICATE KEY - // 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->needsCleanup = TRUE; - return $new_id; - } - - public function nextIdDelete() { - // While we want to clean up the table to keep it up from occupying too - // much storage and memory, we must keep the highest value in the table - // because InnoDB uses an in-memory auto-increment counter as long as the - // server runs. When the server is stopped and restarted, InnoDB - // reinitializes the counter for each table for the first INSERT to the - // table based solely on values from the table so deleting all values would - // be a problem in this case. Also, TRUNCATE resets the auto increment - // counter. - try { - $max_id = $this->query('SELECT MAX(value) FROM {sequences}')->fetchField(); - // We know we are using MySQL here, no need for the slower ::delete(). - $this->query('DELETE FROM {sequences} WHERE value < :value', [':value' => $max_id]); - } - // During testing, this function is called from shutdown with the - // simpletest prefix stored in $this->connection, and those tables are gone - // by the time shutdown is called so we need to ignore the database - // errors. There is no problem with completely ignoring errors here: if - // these queries fail, the sequence will work just fine, just use a bit - // more database storage and memory. - catch (DatabaseException $e) { - } - } - - /** - * Overridden to work around issues to MySQL not supporting transactional DDL. - */ - protected function popCommittableTransactions() { - // Commit all the committable layers. - foreach (array_reverse($this->transactionLayers) as $name => $active) { - // Stop once we found an active transaction. - if ($active) { - break; - } - - // If there are no more layers left then we should commit. - unset($this->transactionLayers[$name]); - if (empty($this->transactionLayers)) { - $this->doCommit(); - } - else { - // Attempt to release this savepoint in the standard way. - try { - $this->query('RELEASE SAVEPOINT ' . $name); - } - catch (DatabaseExceptionWrapper $e) { - // However, in MySQL (InnoDB), savepoints are automatically committed - // when tables are altered or created (DDL transactions are not - // supported). This can cause exceptions due to trying to release - // savepoints which no longer exist. - // - // To avoid exceptions when no actual error has occurred, we silently - // succeed for MySQL error code 1305 ("SAVEPOINT does not exist"). - if ($e->getPrevious()->errorInfo[1] == '1305') { - // If one SAVEPOINT was released automatically, then all were. - // Therefore, clean the transaction stack. - $this->transactionLayers = []; - // We also have to explain to PDO that the transaction stack has - // been cleaned-up. - $this->doCommit(); - } - else { - throw $e; - } - } - } - } - } - - /** - * {@inheritdoc} - */ - public function rollBack($savepoint_name = 'drupal_transaction') { - // MySQL will automatically commit transactions when tables are altered or - // created (DDL transactions are not supported). Prevent triggering an - // exception to ensure that the error that has caused the rollback is - // properly reported. - if (!$this->connection->inTransaction()) { - // On PHP 7 $this->connection->inTransaction() will return TRUE and - // $this->connection->rollback() does not throw an exception; the - // following code is unreachable. - - // If \Drupal\Core\Database\Connection::rollBack() would throw an - // exception then continue to throw an exception. - if (!$this->inTransaction()) { - throw new TransactionNoActiveException(); - } - // A previous rollback to an earlier savepoint may mean that the savepoint - // in question has already been accidentally committed. - if (!isset($this->transactionLayers[$savepoint_name])) { - throw new TransactionNoActiveException(); - } - - trigger_error('Rollback attempted when there is no active transaction. This can cause data integrity issues.', E_USER_WARNING); - return; - } - return parent::rollBack($savepoint_name); - } - - /** - * {@inheritdoc} - */ - protected function doCommit() { - // MySQL will automatically commit transactions when tables are altered or - // created (DDL transactions are not supported). Prevent triggering an - // exception in this case as all statements have been committed. - if ($this->connection->inTransaction()) { - // On PHP 7 $this->connection->inTransaction() will return TRUE and - // $this->connection->commit() does not throw an exception. - $success = parent::doCommit(); - } - else { - // Process the post-root (non-nested) transaction commit callbacks. The - // following code is copied from - // \Drupal\Core\Database\Connection::doCommit() - $success = TRUE; - if (!empty($this->rootTransactionEndCallbacks)) { - $callbacks = $this->rootTransactionEndCallbacks; - $this->rootTransactionEndCallbacks = []; - foreach ($callbacks as $callback) { - call_user_func($callback, $success); - } - } - } - return $success; - } - -} - - -/** - * @} End of "addtogroup database". - */ +class Connection extends MysqlConnection {} diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/ExceptionHandler.php b/core/lib/Drupal/Core/Database/Driver/mysql/ExceptionHandler.php index 5d16c5b4f978..95c7be731f41 100644 --- a/core/lib/Drupal/Core/Database/Driver/mysql/ExceptionHandler.php +++ b/core/lib/Drupal/Core/Database/Driver/mysql/ExceptionHandler.php @@ -2,58 +2,16 @@ namespace Drupal\Core\Database\Driver\mysql; -use Drupal\Component\Utility\Unicode; -use Drupal\Core\Database\DatabaseExceptionWrapper; -use Drupal\Core\Database\ExceptionHandler as BaseExceptionHandler; -use Drupal\Core\Database\IntegrityConstraintViolationException; -use Drupal\Core\Database\StatementInterface; +use Drupal\mysql\Driver\Database\mysql\ExceptionHandler as MysqlExceptionHandler; + +@trigger_error('\Drupal\Core\Database\Driver\mysql\ExceptionHandler is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED); /** * MySql database exception handler class. + * + * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL + * database driver has been moved to the mysql module. + * + * @see https://www.drupal.org/node/3129492 */ -class ExceptionHandler extends BaseExceptionHandler { - - /** - * {@inheritdoc} - */ - public function handleExecutionException(\Exception $exception, StatementInterface $statement, array $arguments = [], array $options = []): void { - if (array_key_exists('throw_exception', $options)) { - @trigger_error('Passing a \'throw_exception\' option to ' . __METHOD__ . ' is deprecated in drupal:9.2.0 and is removed in drupal:10.0.0. Always catch exceptions. See https://www.drupal.org/node/3201187', E_USER_DEPRECATED); - if (!($options['throw_exception'])) { - return; - } - } - - if ($exception instanceof \PDOException) { - // Wrap the exception in another exception, because PHP does not allow - // overriding Exception::getMessage(). Its message is the extra database - // debug information. - $code = is_int($exception->getCode()) ? $exception->getCode() : 0; - - // If a max_allowed_packet error occurs the message length is truncated. - // This should prevent the error from recurring if the exception is logged - // to the database using dblog or the like. - if (($exception->errorInfo[1] ?? NULL) === 1153) { - $message = Unicode::truncateBytes($exception->getMessage(), Connection::MIN_MAX_ALLOWED_PACKET); - throw new DatabaseExceptionWrapper($message, $code, $exception); - } - - $message = $exception->getMessage() . ": " . $statement->getQueryString() . "; " . print_r($arguments, TRUE); - - // SQLSTATE 23xxx errors indicate an integrity constraint violation. Also, - // in case of attempted INSERT of a record with an undefined column and no - // default value indicated in schema, MySql returns a 1364 error code. - if ( - substr($exception->getCode(), -6, -3) == '23' || - ($exception->errorInfo[1] ?? NULL) === 1364 - ) { - throw new IntegrityConstraintViolationException($message, $code, $exception); - } - - throw new DatabaseExceptionWrapper($message, 0, $exception); - } - - throw $exception; - } - -} +class ExceptionHandler extends MysqlExceptionHandler {} diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Insert.php b/core/lib/Drupal/Core/Database/Driver/mysql/Insert.php index c65642aed0df..6fca6386e138 100644 --- a/core/lib/Drupal/Core/Database/Driver/mysql/Insert.php +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Insert.php @@ -2,64 +2,16 @@ namespace Drupal\Core\Database\Driver\mysql; -use Drupal\Core\Database\Query\Insert as QueryInsert; +use Drupal\mysql\Driver\Database\mysql\Insert as MysqlInsert; + +@trigger_error('\Drupal\Core\Database\Driver\mysql\Insert is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED); /** * MySQL implementation of \Drupal\Core\Database\Query\Insert. + * + * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL + * database driver has been moved to the mysql module. + * + * @see https://www.drupal.org/node/3129492 */ -class Insert extends QueryInsert { - - public function execute() { - if (!$this->preExecute()) { - return NULL; - } - - // 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)) { - $max_placeholder = 0; - $values = []; - foreach ($this->insertValues as $insert_values) { - foreach ($insert_values as $value) { - $values[':db_insert_placeholder_' . $max_placeholder++] = $value; - } - } - } - else { - $values = $this->fromQuery->getArguments(); - } - - $last_insert_id = $this->connection->query((string) $this, $values, $this->queryOptions); - - // Re-initialize the values array so that we can re-use this query. - $this->insertValues = []; - - return $last_insert_id; - } - - public function __toString() { - // Create a sanitized comment string to prepend to the query. - $comments = $this->connection->makeComment($this->comments); - - // Default fields are always placed first for consistency. - $insert_fields = array_merge($this->defaultFields, $this->insertFields); - $insert_fields = array_map(function ($field) { - return $this->connection->escapeField($field); - }, $insert_fields); - - // 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)) { - $insert_fields_string = $insert_fields ? ' (' . implode(', ', $insert_fields) . ') ' : ' '; - return $comments . 'INSERT INTO {' . $this->table . '}' . $insert_fields_string . $this->fromQuery; - } - - $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES '; - - $values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields); - $query .= implode(', ', $values); - - return $query; - } - -} +class Insert extends MysqlInsert {} diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Install/Tasks.php b/core/lib/Drupal/Core/Database/Driver/mysql/Install/Tasks.php index dfac0158b79f..2cb93a9b4f6a 100644 --- a/core/lib/Drupal/Core/Database/Driver/mysql/Install/Tasks.php +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Install/Tasks.php @@ -2,213 +2,16 @@ namespace Drupal\Core\Database\Driver\mysql\Install; -use Drupal\Core\Database\ConnectionNotDefinedException; -use Drupal\Core\Database\Database; -use Drupal\Core\Database\Install\Tasks as InstallTasks; -use Drupal\Core\Database\Driver\mysql\Connection; -use Drupal\Core\Database\DatabaseNotFoundException; +use Drupal\mysql\Driver\Database\mysql\Install\Tasks as MysqlTasks; + +@trigger_error('\Drupal\Core\Database\Driver\mysql\Install\Tasks is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED); /** * Specifies installation tasks for MySQL and equivalent databases. + * + * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL + * database driver has been moved to the mysql module. + * + * @see https://www.drupal.org/node/3129492 */ -class Tasks extends InstallTasks { - - /** - * Minimum required MySQL version. - * - * 5.7.8 is the minimum version that supports the JSON datatype. - * @see https://dev.mysql.com/doc/refman/5.7/en/json.html - */ - const MYSQL_MINIMUM_VERSION = '5.7.8'; - - /** - * Minimum required MariaDB version. - * - * 10.3.7 is the first stable (GA) release in the 10.3 series. - * @see https://mariadb.com/kb/en/changes-improvements-in-mariadb-103/#list-of-all-mariadb-103-releases - */ - const MARIADB_MINIMUM_VERSION = '10.3.7'; - - /** - * Minimum required MySQLnd version. - */ - const MYSQLND_MINIMUM_VERSION = '5.0.9'; - - /** - * Minimum required libmysqlclient version. - */ - const LIBMYSQLCLIENT_MINIMUM_VERSION = '5.5.3'; - - /** - * The PDO driver name for MySQL and equivalent databases. - * - * @var string - */ - protected $pdoDriver = 'mysql'; - - /** - * Constructs a \Drupal\Core\Database\Driver\mysql\Install\Tasks object. - */ - public function __construct() { - $this->tasks[] = [ - 'arguments' => [], - 'function' => 'ensureInnoDbAvailable', - ]; - } - - /** - * {@inheritdoc} - */ - public function name() { - try { - if (!$this->isConnectionActive() || !$this->getConnection() instanceof Connection) { - throw new ConnectionNotDefinedException('The database connection is not active or not a MySql connection'); - } - if ($this->getConnection()->isMariaDb()) { - return $this->t('MariaDB'); - } - return $this->t('MySQL, Percona Server, or equivalent'); - } - catch (ConnectionNotDefinedException $e) { - return $this->t('MySQL, MariaDB, Percona Server, or equivalent'); - } - } - - /** - * {@inheritdoc} - */ - public function minimumVersion() { - if ($this->getConnection()->isMariaDb()) { - return static::MARIADB_MINIMUM_VERSION; - } - return static::MYSQL_MINIMUM_VERSION; - } - - /** - * {@inheritdoc} - */ - protected function connect() { - try { - // This doesn't actually test the connection. - Database::setActiveConnection(); - // Now actually do a check. - try { - Database::getConnection(); - } - catch (\Exception $e) { - // Detect utf8mb4 incompatibility. - if ($e->getCode() == Connection::UNSUPPORTED_CHARSET || ($e->getCode() == Connection::SQLSTATE_SYNTAX_ERROR && $e->errorInfo[1] == Connection::UNKNOWN_CHARSET)) { - $this->fail(t('Your MySQL server and PHP MySQL driver must support utf8mb4 character encoding. Make sure to use a database system that supports this (such as MySQL/MariaDB/Percona 5.5.3 and up), and that the utf8mb4 character set is compiled in. See the <a href=":documentation" target="_blank">MySQL documentation</a> for more information.', [':documentation' => 'https://dev.mysql.com/doc/refman/5.0/en/cannot-initialize-character-set.html'])); - $info = Database::getConnectionInfo(); - $info_copy = $info; - // Set a flag to fall back to utf8. Note: this flag should only be - // used here and is for internal use only. - $info_copy['default']['_dsn_utf8_fallback'] = TRUE; - // In order to change the Database::$databaseInfo array, we need to - // remove the active connection, then re-add it with the new info. - Database::removeConnection('default'); - Database::addConnectionInfo('default', 'default', $info_copy['default']); - // Connect with the new database info, using the utf8 character set so - // that we can run the checkEngineVersion test. - Database::getConnection(); - // Revert to the old settings. - Database::removeConnection('default'); - Database::addConnectionInfo('default', 'default', $info['default']); - } - else { - // Rethrow the exception. - throw $e; - } - } - $this->pass('Drupal can CONNECT to the database ok.'); - } - catch (\Exception $e) { - // Attempt to create the database if it is not found. - if ($e->getCode() == Connection::DATABASE_NOT_FOUND) { - // Remove the database string from connection info. - $connection_info = Database::getConnectionInfo(); - $database = $connection_info['default']['database']; - unset($connection_info['default']['database']); - - // In order to change the Database::$databaseInfo array, need to remove - // the active connection, then re-add it with the new info. - Database::removeConnection('default'); - Database::addConnectionInfo('default', 'default', $connection_info['default']); - - try { - // Now, attempt the connection again; if it's successful, attempt to - // create the database. - Database::getConnection()->createDatabase($database); - Database::closeConnection(); - - // Now, restore the database config. - Database::removeConnection('default'); - $connection_info['default']['database'] = $database; - Database::addConnectionInfo('default', 'default', $connection_info['default']); - - // Check the database connection. - Database::getConnection(); - $this->pass('Drupal can CONNECT to the database ok.'); - } - catch (DatabaseNotFoundException $e) { - // Still no dice; probably a permission issue. Raise the error to the - // installer. - $this->fail(t('Database %database not found. The server reports the following message when attempting to create the database: %error.', ['%database' => $database, '%error' => $e->getMessage()])); - } - } - else { - // Database connection failed for some other reason than a non-existent - // database. - $this->fail(t('Failed to connect to your database server. The server reports the following message: %error.<ul><li>Is the database server running?</li><li>Does the database exist or does the database user have sufficient privileges to create the database?</li><li>Have you entered the correct database name?</li><li>Have you entered the correct username and password?</li><li>Have you entered the correct database hostname and port number?</li></ul>', ['%error' => $e->getMessage()])); - return FALSE; - } - } - return TRUE; - } - - /** - * {@inheritdoc} - */ - public function getFormOptions(array $database) { - $form = parent::getFormOptions($database); - if (empty($form['advanced_options']['port']['#default_value'])) { - $form['advanced_options']['port']['#default_value'] = '3306'; - } - - return $form; - } - - /** - * Ensure that InnoDB is available. - */ - public function ensureInnoDbAvailable() { - $engines = Database::getConnection()->query('SHOW ENGINES')->fetchAllKeyed(); - if (isset($engines['MyISAM']) && $engines['MyISAM'] == 'DEFAULT' && !isset($engines['InnoDB'])) { - $this->fail(t('The MyISAM storage engine is not supported.')); - } - } - - /** - * {@inheritdoc} - */ - protected function checkEngineVersion() { - parent::checkEngineVersion(); - - // Ensure that the MySQL driver supports utf8mb4 encoding. - $version = Database::getConnection()->clientVersion(); - if (FALSE !== strpos($version, 'mysqlnd')) { - // The mysqlnd driver supports utf8mb4 starting at version 5.0.9. - $version = preg_replace('/^\D+([\d.]+).*/', '$1', $version); - if (version_compare($version, self::MYSQLND_MINIMUM_VERSION, '<')) { - $this->fail(t("The MySQLnd driver version %version is less than the minimum required version. Upgrade to MySQLnd version %mysqlnd_minimum_version or up, or alternatively switch mysql drivers to libmysqlclient version %libmysqlclient_minimum_version or up.", ['%version' => $version, '%mysqlnd_minimum_version' => self::MYSQLND_MINIMUM_VERSION, '%libmysqlclient_minimum_version' => self::LIBMYSQLCLIENT_MINIMUM_VERSION])); - } - } - else { - // The libmysqlclient driver supports utf8mb4 starting at version 5.5.3. - if (version_compare($version, self::LIBMYSQLCLIENT_MINIMUM_VERSION, '<')) { - $this->fail(t("The libmysqlclient driver version %version is less than the minimum required version. Upgrade to libmysqlclient version %libmysqlclient_minimum_version or up, or alternatively switch mysql drivers to MySQLnd version %mysqlnd_minimum_version or up.", ['%version' => $version, '%libmysqlclient_minimum_version' => self::LIBMYSQLCLIENT_MINIMUM_VERSION, '%mysqlnd_minimum_version' => self::MYSQLND_MINIMUM_VERSION])); - } - } - } - -} +class Tasks extends MysqlTasks {} diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php b/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php index 283035b835b7..ef9a0f12926f 100644 --- a/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php @@ -2,714 +2,16 @@ namespace Drupal\Core\Database\Driver\mysql; -use Drupal\Core\Database\SchemaException; -use Drupal\Core\Database\SchemaObjectExistsException; -use Drupal\Core\Database\SchemaObjectDoesNotExistException; -use Drupal\Core\Database\Schema as DatabaseSchema; -use Drupal\Component\Utility\Unicode; +use Drupal\mysql\Driver\Database\mysql\Schema as MysqlSchema; -/** - * @addtogroup schemaapi - * @{ - */ +@trigger_error('\Drupal\Core\Database\Driver\mysql\Schema is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED); /** * MySQL implementation of \Drupal\Core\Database\Schema. + * + * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL + * database driver has been moved to the mysql module. + * + * @see https://www.drupal.org/node/3129492 */ -class Schema extends DatabaseSchema { - - /** - * Maximum length of a table comment in MySQL. - */ - const COMMENT_MAX_TABLE = 60; - - /** - * Maximum length of a column comment in MySQL. - */ - const COMMENT_MAX_COLUMN = 255; - - /** - * @var array - * List of MySQL string types. - */ - protected $mysqlStringTypes = [ - 'VARCHAR', - 'CHAR', - 'TINYTEXT', - 'MEDIUMTEXT', - 'LONGTEXT', - 'TEXT', - ]; - - /** - * Get information about the table and database name from the prefix. - * - * @return - * A keyed array with information about the database, table name and prefix. - */ - protected function getPrefixInfo($table = 'default', $add_prefix = TRUE) { - $info = ['prefix' => $this->connection->tablePrefix($table)]; - if ($add_prefix) { - $table = $info['prefix'] . $table; - } - if (($pos = strpos($table, '.')) !== FALSE) { - $info['database'] = substr($table, 0, $pos); - $info['table'] = substr($table, ++$pos); - } - else { - $info['database'] = $this->connection->getConnectionOptions()['database']; - $info['table'] = $table; - } - return $info; - } - - /** - * Build a condition to match a table name against a standard information_schema. - * - * MySQL uses databases like schemas rather than catalogs so when we build - * a condition to query the information_schema.tables, we set the default - * database as the schema unless specified otherwise, and exclude table_catalog - * from the condition criteria. - */ - protected function buildTableNameCondition($table_name, $operator = '=', $add_prefix = TRUE) { - $table_info = $this->getPrefixInfo($table_name, $add_prefix); - - $condition = $this->connection->condition('AND'); - $condition->condition('table_schema', $table_info['database']); - $condition->condition('table_name', $table_info['table'], $operator); - return $condition; - } - - /** - * Generate SQL to create a new table from a Drupal schema definition. - * - * @param $name - * The name of the table to create. - * @param $table - * A Schema API table definition array. - * - * @return - * An array of SQL statements to create the table. - */ - protected function createTableSql($name, $table) { - $info = $this->connection->getConnectionOptions(); - - // Provide defaults if needed. - $table += [ - 'mysql_engine' => 'InnoDB', - 'mysql_character_set' => 'utf8mb4', - ]; - - $sql = "CREATE TABLE {" . $name . "} (\n"; - - // Add the SQL statement for each field. - foreach ($table['fields'] as $field_name => $field) { - $sql .= $this->createFieldSql($field_name, $this->processField($field)) . ", \n"; - } - - // Process keys & indexes. - if (!empty($table['primary key']) && is_array($table['primary key'])) { - $this->ensureNotNullPrimaryKey($table['primary key'], $table['fields']); - } - $keys = $this->createKeysSql($table); - if (count($keys)) { - $sql .= implode(", \n", $keys) . ", \n"; - } - - // Remove the last comma and space. - $sql = substr($sql, 0, -3) . "\n) "; - - $sql .= 'ENGINE = ' . $table['mysql_engine'] . ' DEFAULT CHARACTER SET ' . $table['mysql_character_set']; - // By default, MySQL uses the default collation for new tables, which is - // 'utf8mb4_general_ci' (MySQL 5) or 'utf8mb4_0900_ai_ci' (MySQL 8) for - // utf8mb4. If an alternate collation has been set, it needs to be - // explicitly specified. - // @see \Drupal\Core\Database\Driver\mysql\Schema - if (!empty($info['collation'])) { - $sql .= ' COLLATE ' . $info['collation']; - } - - // Add table comment. - if (!empty($table['description'])) { - $sql .= ' COMMENT ' . $this->prepareComment($table['description'], self::COMMENT_MAX_TABLE); - } - - return [$sql]; - } - - /** - * Create an SQL string for a field to be used in table creation or alteration. - * - * @param string $name - * Name of the field. - * @param array $spec - * The field specification, as per the schema data structure format. - */ - protected function createFieldSql($name, $spec) { - $sql = "`" . $name . "` " . $spec['mysql_type']; - - if (in_array($spec['mysql_type'], $this->mysqlStringTypes)) { - if (isset($spec['length'])) { - $sql .= '(' . $spec['length'] . ')'; - } - if (isset($spec['type']) && $spec['type'] == 'varchar_ascii') { - $sql .= ' CHARACTER SET ascii'; - } - if (!empty($spec['binary'])) { - $sql .= ' BINARY'; - } - // Note we check for the "type" key here. "mysql_type" is VARCHAR: - elseif (isset($spec['type']) && $spec['type'] == 'varchar_ascii') { - $sql .= ' COLLATE ascii_general_ci'; - } - } - elseif (isset($spec['precision']) && isset($spec['scale'])) { - $sql .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')'; - } - - if (!empty($spec['unsigned'])) { - $sql .= ' unsigned'; - } - - if (isset($spec['not null'])) { - if ($spec['not null']) { - $sql .= ' NOT NULL'; - } - else { - $sql .= ' NULL'; - } - } - - if (!empty($spec['auto_increment'])) { - $sql .= ' auto_increment'; - } - - // $spec['default'] can be NULL, so we explicitly check for the key here. - if (array_key_exists('default', $spec)) { - $sql .= ' DEFAULT ' . $this->escapeDefaultValue($spec['default']); - } - - if (empty($spec['not null']) && !isset($spec['default'])) { - $sql .= ' DEFAULT NULL'; - } - - // Add column comment. - if (!empty($spec['description'])) { - $sql .= ' COMMENT ' . $this->prepareComment($spec['description'], self::COMMENT_MAX_COLUMN); - } - - return $sql; - } - - /** - * Set database-engine specific properties for a field. - * - * @param $field - * A field description array, as specified in the schema documentation. - */ - protected function processField($field) { - - if (!isset($field['size'])) { - $field['size'] = 'normal'; - } - - // Set the correct database-engine specific datatype. - // In case one is already provided, force it to uppercase. - if (isset($field['mysql_type'])) { - $field['mysql_type'] = mb_strtoupper($field['mysql_type']); - } - else { - $map = $this->getFieldTypeMap(); - $field['mysql_type'] = $map[$field['type'] . ':' . $field['size']]; - } - - if (isset($field['type']) && $field['type'] == 'serial') { - $field['auto_increment'] = TRUE; - } - - return $field; - } - - /** - * {@inheritdoc} - */ - public function getFieldTypeMap() { - // Put :normal last so it gets preserved by array_flip. This makes - // it much easier for modules (such as schema.module) to map - // database types back into schema types. - // $map does not use drupal_static as its value never changes. - static $map = [ - 'varchar_ascii:normal' => 'VARCHAR', - - 'varchar:normal' => 'VARCHAR', - 'char:normal' => 'CHAR', - - 'text:tiny' => 'TINYTEXT', - 'text:small' => 'TINYTEXT', - 'text:medium' => 'MEDIUMTEXT', - 'text:big' => 'LONGTEXT', - 'text:normal' => 'TEXT', - - 'serial:tiny' => 'TINYINT', - 'serial:small' => 'SMALLINT', - 'serial:medium' => 'MEDIUMINT', - 'serial:big' => 'BIGINT', - 'serial:normal' => 'INT', - - 'int:tiny' => 'TINYINT', - 'int:small' => 'SMALLINT', - 'int:medium' => 'MEDIUMINT', - 'int:big' => 'BIGINT', - 'int:normal' => 'INT', - - 'float:tiny' => 'FLOAT', - 'float:small' => 'FLOAT', - 'float:medium' => 'FLOAT', - 'float:big' => 'DOUBLE', - 'float:normal' => 'FLOAT', - - 'numeric:normal' => 'DECIMAL', - - 'blob:big' => 'LONGBLOB', - 'blob:normal' => 'BLOB', - ]; - return $map; - } - - protected function createKeysSql($spec) { - $keys = []; - - if (!empty($spec['primary key'])) { - $keys[] = 'PRIMARY KEY (' . $this->createKeySql($spec['primary key']) . ')'; - } - if (!empty($spec['unique keys'])) { - foreach ($spec['unique keys'] as $key => $fields) { - $keys[] = 'UNIQUE KEY `' . $key . '` (' . $this->createKeySql($fields) . ')'; - } - } - if (!empty($spec['indexes'])) { - $indexes = $this->getNormalizedIndexes($spec); - foreach ($indexes as $index => $fields) { - $keys[] = 'INDEX `' . $index . '` (' . $this->createKeySql($fields) . ')'; - } - } - - return $keys; - } - - /** - * Gets normalized indexes from a table specification. - * - * Shortens indexes to 191 characters if they apply to utf8mb4-encoded - * fields, in order to comply with the InnoDB index limitation of 756 bytes. - * - * @param array $spec - * The table specification. - * - * @return array - * List of shortened indexes. - * - * @throws \Drupal\Core\Database\SchemaException - * Thrown if field specification is missing. - */ - protected function getNormalizedIndexes(array $spec) { - $indexes = $spec['indexes'] ?? []; - foreach ($indexes as $index_name => $index_fields) { - foreach ($index_fields as $index_key => $index_field) { - // Get the name of the field from the index specification. - $field_name = is_array($index_field) ? $index_field[0] : $index_field; - // Check whether the field is defined in the table specification. - if (isset($spec['fields'][$field_name])) { - // Get the MySQL type from the processed field. - $mysql_field = $this->processField($spec['fields'][$field_name]); - if (in_array($mysql_field['mysql_type'], $this->mysqlStringTypes)) { - // Check whether we need to shorten the index. - if ((!isset($mysql_field['type']) || $mysql_field['type'] != 'varchar_ascii') && (!isset($mysql_field['length']) || $mysql_field['length'] > 191)) { - // Limit the index length to 191 characters. - $this->shortenIndex($indexes[$index_name][$index_key]); - } - } - } - else { - throw new SchemaException("MySQL needs the '$field_name' field specification in order to normalize the '$index_name' index"); - } - } - } - return $indexes; - } - - /** - * Helper function for normalizeIndexes(). - * - * Shortens an index to 191 characters. - * - * @param array $index - * The index array to be used in createKeySql. - * - * @see Drupal\Core\Database\Driver\mysql\Schema::createKeySql() - * @see Drupal\Core\Database\Driver\mysql\Schema::normalizeIndexes() - */ - protected function shortenIndex(&$index) { - if (is_array($index)) { - if ($index[1] > 191) { - $index[1] = 191; - } - } - else { - $index = [$index, 191]; - } - } - - protected function createKeySql($fields) { - $return = []; - foreach ($fields as $field) { - if (is_array($field)) { - $return[] = '`' . $field[0] . '`(' . $field[1] . ')'; - } - else { - $return[] = '`' . $field . '`'; - } - } - return implode(', ', $return); - } - - /** - * {@inheritdoc} - */ - public function renameTable($table, $new_name) { - if (!$this->tableExists($table)) { - throw new SchemaObjectDoesNotExistException("Cannot rename '$table' to '$new_name': table '$table' doesn't exist."); - } - if ($this->tableExists($new_name)) { - throw new SchemaObjectExistsException("Cannot rename '$table' to '$new_name': table '$new_name' already exists."); - } - - $info = $this->getPrefixInfo($new_name); - $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO `' . $info['table'] . '`'); - } - - /** - * {@inheritdoc} - */ - public function dropTable($table) { - if (!$this->tableExists($table)) { - return FALSE; - } - - $this->connection->query('DROP TABLE {' . $table . '}'); - return TRUE; - } - - /** - * {@inheritdoc} - */ - public function addField($table, $field, $spec, $keys_new = []) { - if (!$this->tableExists($table)) { - throw new SchemaObjectDoesNotExistException("Cannot add field '$table.$field': table doesn't exist."); - } - if ($this->fieldExists($table, $field)) { - throw new SchemaObjectExistsException("Cannot add field '$table.$field': field already exists."); - } - - // Fields that are part of a PRIMARY KEY must be added as NOT NULL. - $is_primary_key = isset($keys_new['primary key']) && in_array($field, $keys_new['primary key'], TRUE); - if ($is_primary_key) { - $this->ensureNotNullPrimaryKey($keys_new['primary key'], [$field => $spec]); - } - - $fixnull = FALSE; - if (!empty($spec['not null']) && !isset($spec['default']) && !$is_primary_key) { - $fixnull = TRUE; - $spec['not null'] = FALSE; - } - $query = 'ALTER TABLE {' . $table . '} ADD '; - $query .= $this->createFieldSql($field, $this->processField($spec)); - if ($keys_sql = $this->createKeysSql($keys_new)) { - // Make sure to drop the existing primary key before adding a new one. - // This is only needed when adding a field because this method, unlike - // changeField(), is supposed to handle primary keys automatically. - if (isset($keys_new['primary key']) && $this->indexExists($table, 'PRIMARY')) { - $query .= ', DROP PRIMARY KEY'; - } - - $query .= ', ADD ' . implode(', ADD ', $keys_sql); - } - $this->connection->query($query); - if (isset($spec['initial_from_field'])) { - if (isset($spec['initial'])) { - $expression = 'COALESCE(' . $spec['initial_from_field'] . ', :default_initial_value)'; - $arguments = [':default_initial_value' => $spec['initial']]; - } - else { - $expression = $spec['initial_from_field']; - $arguments = []; - } - $this->connection->update($table) - ->expression($field, $expression, $arguments) - ->execute(); - } - elseif (isset($spec['initial'])) { - $this->connection->update($table) - ->fields([$field => $spec['initial']]) - ->execute(); - } - if ($fixnull) { - $spec['not null'] = TRUE; - $this->changeField($table, $field, $field, $spec); - } - } - - /** - * {@inheritdoc} - */ - public function dropField($table, $field) { - if (!$this->fieldExists($table, $field)) { - return FALSE; - } - - // When dropping a field that is part of a composite primary key MySQL - // automatically removes the field from the primary key, which can leave the - // table in an invalid state. MariaDB 10.2.8 requires explicitly dropping - // the primary key first for this reason. We perform this deletion - // explicitly which also makes the behavior on both MySQL and MariaDB - // consistent with PostgreSQL. - // @see https://mariadb.com/kb/en/library/alter-table - $primary_key = $this->findPrimaryKeyColumns($table); - if ((count($primary_key) > 1) && in_array($field, $primary_key, TRUE)) { - $this->dropPrimaryKey($table); - } - - $this->connection->query('ALTER TABLE {' . $table . '} DROP `' . $field . '`'); - return TRUE; - } - - /** - * {@inheritdoc} - */ - public function indexExists($table, $name) { - // Returns one row for each column in the index. Result is string or FALSE. - // Details at http://dev.mysql.com/doc/refman/5.0/en/show-index.html - $row = $this->connection->query('SHOW INDEX FROM {' . $table . '} WHERE key_name = ' . $this->connection->quote($name))->fetchAssoc(); - return isset($row['Key_name']); - } - - /** - * {@inheritdoc} - */ - public function addPrimaryKey($table, $fields) { - if (!$this->tableExists($table)) { - throw new SchemaObjectDoesNotExistException("Cannot add primary key to table '$table': table doesn't exist."); - } - if ($this->indexExists($table, 'PRIMARY')) { - throw new SchemaObjectExistsException("Cannot add primary key to table '$table': primary key already exists."); - } - - $this->connection->query('ALTER TABLE {' . $table . '} ADD PRIMARY KEY (' . $this->createKeySql($fields) . ')'); - } - - /** - * {@inheritdoc} - */ - public function dropPrimaryKey($table) { - if (!$this->indexExists($table, 'PRIMARY')) { - return FALSE; - } - - $this->connection->query('ALTER TABLE {' . $table . '} DROP PRIMARY KEY'); - return TRUE; - } - - /** - * {@inheritdoc} - */ - protected function findPrimaryKeyColumns($table) { - if (!$this->tableExists($table)) { - return FALSE; - } - $result = $this->connection->query("SHOW KEYS FROM {" . $table . "} WHERE Key_name = 'PRIMARY'")->fetchAllAssoc('Column_name'); - return array_keys($result); - } - - /** - * {@inheritdoc} - */ - public function addUniqueKey($table, $name, $fields) { - if (!$this->tableExists($table)) { - throw new SchemaObjectDoesNotExistException("Cannot add unique key '$name' to table '$table': table doesn't exist."); - } - if ($this->indexExists($table, $name)) { - throw new SchemaObjectExistsException("Cannot add unique key '$name' to table '$table': unique key already exists."); - } - - $this->connection->query('ALTER TABLE {' . $table . '} ADD UNIQUE KEY `' . $name . '` (' . $this->createKeySql($fields) . ')'); - } - - /** - * {@inheritdoc} - */ - public function dropUniqueKey($table, $name) { - if (!$this->indexExists($table, $name)) { - return FALSE; - } - - $this->connection->query('ALTER TABLE {' . $table . '} DROP KEY `' . $name . '`'); - return TRUE; - } - - /** - * {@inheritdoc} - */ - public function addIndex($table, $name, $fields, array $spec) { - if (!$this->tableExists($table)) { - throw new SchemaObjectDoesNotExistException("Cannot add index '$name' to table '$table': table doesn't exist."); - } - if ($this->indexExists($table, $name)) { - throw new SchemaObjectExistsException("Cannot add index '$name' to table '$table': index already exists."); - } - - $spec['indexes'][$name] = $fields; - $indexes = $this->getNormalizedIndexes($spec); - - $this->connection->query('ALTER TABLE {' . $table . '} ADD INDEX `' . $name . '` (' . $this->createKeySql($indexes[$name]) . ')'); - } - - /** - * {@inheritdoc} - */ - public function dropIndex($table, $name) { - if (!$this->indexExists($table, $name)) { - return FALSE; - } - - $this->connection->query('ALTER TABLE {' . $table . '} DROP INDEX `' . $name . '`'); - return TRUE; - } - - /** - * {@inheritdoc} - */ - protected function introspectIndexSchema($table) { - if (!$this->tableExists($table)) { - throw new SchemaObjectDoesNotExistException("The table $table doesn't exist."); - } - - $index_schema = [ - 'primary key' => [], - 'unique keys' => [], - 'indexes' => [], - ]; - - $result = $this->connection->query('SHOW INDEX FROM {' . $table . '}')->fetchAll(); - foreach ($result as $row) { - if ($row->Key_name === 'PRIMARY') { - $index_schema['primary key'][] = $row->Column_name; - } - elseif ($row->Non_unique == 0) { - $index_schema['unique keys'][$row->Key_name][] = $row->Column_name; - } - else { - $index_schema['indexes'][$row->Key_name][] = $row->Column_name; - } - } - - return $index_schema; - } - - /** - * {@inheritdoc} - */ - public function changeField($table, $field, $field_new, $spec, $keys_new = []) { - if (!$this->fieldExists($table, $field)) { - throw new SchemaObjectDoesNotExistException("Cannot change the definition of field '$table.$field': field doesn't exist."); - } - if (($field != $field_new) && $this->fieldExists($table, $field_new)) { - throw new SchemaObjectExistsException("Cannot rename field '$table.$field' to '$field_new': target field already exists."); - } - if (isset($keys_new['primary key']) && in_array($field_new, $keys_new['primary key'], TRUE)) { - $this->ensureNotNullPrimaryKey($keys_new['primary key'], [$field_new => $spec]); - } - - $sql = 'ALTER TABLE {' . $table . '} CHANGE `' . $field . '` ' . $this->createFieldSql($field_new, $this->processField($spec)); - if ($keys_sql = $this->createKeysSql($keys_new)) { - $sql .= ', ADD ' . implode(', ADD ', $keys_sql); - } - $this->connection->query($sql); - } - - /** - * {@inheritdoc} - */ - public function prepareComment($comment, $length = NULL) { - // Truncate comment to maximum comment length. - if (isset($length)) { - // Add table prefixes before truncating. - $comment = Unicode::truncate($this->connection->prefixTables($comment), $length, TRUE, TRUE); - } - // Remove semicolons to avoid triggering multi-statement check. - $comment = strtr($comment, [';' => '.']); - return $this->connection->quote($comment); - } - - /** - * Retrieve a table or column comment. - */ - public function getComment($table, $column = NULL) { - $condition = $this->buildTableNameCondition($table); - if (isset($column)) { - $condition->condition('column_name', $column); - $condition->compile($this->connection, $this); - // Don't use {} around information_schema.columns table. - return $this->connection->query("SELECT column_comment AS column_comment FROM information_schema.columns WHERE " . (string) $condition, $condition->arguments())->fetchField(); - } - $condition->compile($this->connection, $this); - // Don't use {} around information_schema.tables table. - $comment = $this->connection->query("SELECT table_comment AS table_comment FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchField(); - // Work-around for MySQL 5.0 bug http://bugs.mysql.com/bug.php?id=11379 - return preg_replace('/; InnoDB free:.*$/', '', $comment); - } - - /** - * {@inheritdoc} - */ - public function tableExists($table) { - // The information_schema table is very slow to query under MySQL 5.0. - // Instead, we try to select from the table in question. If it fails, - // the most likely reason is that it does not exist. That is dramatically - // faster than using information_schema. - // @link http://bugs.mysql.com/bug.php?id=19588 - // @todo This override should be removed once we require a version of MySQL - // that has that bug fixed. - try { - $this->connection->queryRange("SELECT 1 FROM {" . $table . "}", 0, 1); - return TRUE; - } - catch (\Exception $e) { - return FALSE; - } - } - - /** - * {@inheritdoc} - */ - public function fieldExists($table, $column) { - // The information_schema table is very slow to query under MySQL 5.0. - // Instead, we try to select from the table and field in question. If it - // fails, the most likely reason is that it does not exist. That is - // dramatically faster than using information_schema. - // @link http://bugs.mysql.com/bug.php?id=19588 - // @todo This override should be removed once we require a version of MySQL - // that has that bug fixed. - try { - $this->connection->queryRange("SELECT $column FROM {" . $table . "}", 0, 1); - return TRUE; - } - catch (\Exception $e) { - return FALSE; - } - } - -} - -/** - * @} End of "addtogroup schemaapi". - */ +class Schema extends MysqlSchema {} diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Upsert.php b/core/lib/Drupal/Core/Database/Driver/mysql/Upsert.php index 8eda775c0a74..e8b21ac7bf32 100644 --- a/core/lib/Drupal/Core/Database/Driver/mysql/Upsert.php +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Upsert.php @@ -2,42 +2,16 @@ namespace Drupal\Core\Database\Driver\mysql; -use Drupal\Core\Database\Query\Upsert as QueryUpsert; +use Drupal\mysql\Driver\Database\mysql\Upsert as MysqlUpsert; + +@trigger_error('\Drupal\Core\Database\Driver\mysql\Upsert is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED); /** * MySQL implementation of \Drupal\Core\Database\Query\Upsert. + * + * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL + * database driver has been moved to the mysql module. + * + * @see https://www.drupal.org/node/3129492 */ -class Upsert extends QueryUpsert { - - /** - * {@inheritdoc} - */ - public function __toString() { - // Create a sanitized comment string to prepend to the query. - $comments = $this->connection->makeComment($this->comments); - - // Default fields are always placed first for consistency. - $insert_fields = array_merge($this->defaultFields, $this->insertFields); - $insert_fields = array_map(function ($field) { - return $this->connection->escapeField($field); - }, $insert_fields); - - $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES '; - - $values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields); - $query .= implode(', ', $values); - - // Updating the unique / primary key is not necessary. - unset($insert_fields[$this->key]); - - $update = []; - foreach ($insert_fields as $field) { - $update[] = "$field = VALUES($field)"; - } - - $query .= ' ON DUPLICATE KEY UPDATE ' . implode(', ', $update); - - return $query; - } - -} +class Upsert extends MysqlUpsert {} diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php index 9154a0188a9d..b015bda69416 100644 --- a/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php @@ -2,374 +2,16 @@ namespace Drupal\Core\Database\Driver\pgsql; -use Drupal\Core\Database\Database; -use Drupal\Core\Database\Connection as DatabaseConnection; -use Drupal\Core\Database\DatabaseAccessDeniedException; -use Drupal\Core\Database\DatabaseNotFoundException; -use Drupal\Core\Database\StatementInterface; -use Drupal\Core\Database\StatementWrapper; +use Drupal\pgsql\Driver\Database\pgsql\Connection as PgsqlConnection; -// cSpell:ignore ilike nextval - -/** - * @addtogroup database - * @{ - */ +@trigger_error('\Drupal\Core\Database\Driver\pgsql\Connection is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED); /** * PostgreSQL implementation of \Drupal\Core\Database\Connection. + * + * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL + * database driver has been moved to the pgsql module. + * + * @see https://www.drupal.org/node/3129492 */ -class Connection extends DatabaseConnection { - - /** - * The name by which to obtain a lock for retrieve the next insert id. - */ - const POSTGRESQL_NEXTID_LOCK = 1000; - - /** - * Error code for "Unknown database" error. - */ - const DATABASE_NOT_FOUND = 7; - - /** - * Error code for "Connection failure" errors. - * - * Technically this is an internal error code that will only be shown in the - * PDOException message. It will need to get extracted. - */ - const CONNECTION_FAILURE = '08006'; - - /** - * {@inheritdoc} - */ - protected $statementClass = NULL; - - /** - * {@inheritdoc} - */ - protected $statementWrapperClass = StatementWrapper::class; - - /** - * A map of condition operators to PostgreSQL operators. - * - * In PostgreSQL, 'LIKE' is case-sensitive. ILIKE should be used for - * case-insensitive statements. - */ - protected static $postgresqlConditionOperatorMap = [ - 'LIKE' => ['operator' => 'ILIKE'], - 'LIKE BINARY' => ['operator' => 'LIKE'], - 'NOT LIKE' => ['operator' => 'NOT ILIKE'], - 'REGEXP' => ['operator' => '~*'], - 'NOT REGEXP' => ['operator' => '!~*'], - ]; - - /** - * {@inheritdoc} - */ - protected $transactionalDDLSupport = TRUE; - - /** - * {@inheritdoc} - */ - protected $identifierQuotes = ['"', '"']; - - /** - * Constructs a connection object. - */ - public function __construct(\PDO $connection, array $connection_options) { - parent::__construct($connection, $connection_options); - - // Force PostgreSQL to use the UTF-8 character set by default. - $this->connection->exec("SET NAMES 'UTF8'"); - - // Execute PostgreSQL init_commands. - if (isset($connection_options['init_commands'])) { - $this->connection->exec(implode('; ', $connection_options['init_commands'])); - } - } - - /** - * {@inheritdoc} - */ - public static function open(array &$connection_options = []) { - // Default to TCP connection on port 5432. - if (empty($connection_options['port'])) { - $connection_options['port'] = 5432; - } - - // PostgreSQL in trust mode doesn't require a password to be supplied. - if (empty($connection_options['password'])) { - $connection_options['password'] = NULL; - } - // If the password contains a backslash it is treated as an escape character - // http://bugs.php.net/bug.php?id=53217 - // so backslashes in the password need to be doubled up. - // The bug was reported against pdo_pgsql 1.0.2, backslashes in passwords - // will break on this doubling up when the bug is fixed, so check the version - // elseif (phpversion('pdo_pgsql') < 'version_this_was_fixed_in') { - else { - $connection_options['password'] = str_replace('\\', '\\\\', $connection_options['password']); - } - - $connection_options['database'] = (!empty($connection_options['database']) ? $connection_options['database'] : 'template1'); - $dsn = 'pgsql:host=' . $connection_options['host'] . ' dbname=' . $connection_options['database'] . ' port=' . $connection_options['port']; - - // Allow PDO options to be overridden. - $connection_options += [ - 'pdo' => [], - ]; - $connection_options['pdo'] += [ - \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, - // Prepared statements are most effective for performance when queries - // are recycled (used several times). However, if they are not re-used, - // prepared statements become inefficient. Since most of Drupal's - // prepared queries are not re-used, it should be faster to emulate - // the preparation than to actually ready statements for re-use. If in - // doubt, reset to FALSE and measure performance. - \PDO::ATTR_EMULATE_PREPARES => TRUE, - // Convert numeric values to strings when fetching. - \PDO::ATTR_STRINGIFY_FETCHES => TRUE, - ]; - - try { - $pdo = new \PDO($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']); - } - catch (\PDOException $e) { - if (static::getSQLState($e) == static::CONNECTION_FAILURE) { - if (strpos($e->getMessage(), 'password authentication failed for user') !== FALSE) { - throw new DatabaseAccessDeniedException($e->getMessage(), $e->getCode(), $e); - } - elseif (strpos($e->getMessage(), 'database') !== FALSE && strpos($e->getMessage(), 'does not exist') !== FALSE) { - throw new DatabaseNotFoundException($e->getMessage(), $e->getCode(), $e); - } - } - throw $e; - } - - return $pdo; - } - - /** - * {@inheritdoc} - */ - public function query($query, array $args = [], $options = []) { - $options += $this->defaultOptions(); - - // The PDO PostgreSQL driver has a bug which doesn't type cast booleans - // correctly when parameters are bound using associative arrays. - // @see http://bugs.php.net/bug.php?id=48383 - foreach ($args as &$value) { - if (is_bool($value)) { - $value = (int) $value; - } - } - - // We need to wrap queries with a savepoint if: - // - Currently in a transaction. - // - A 'mimic_implicit_commit' does not exist already. - // - The query is not a savepoint query. - $wrap_with_savepoint = $this->inTransaction() && - !isset($this->transactionLayers['mimic_implicit_commit']) && - !(is_string($query) && ( - stripos($query, 'ROLLBACK TO SAVEPOINT ') === 0 || - stripos($query, 'RELEASE SAVEPOINT ') === 0 || - stripos($query, 'SAVEPOINT ') === 0 - ) - ); - if ($wrap_with_savepoint) { - // 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 - // fails. This is important for tables that are created on demand. For - // example, \Drupal\Core\Cache\DatabaseBackend. - $this->addSavepoint(); - try { - $return = parent::query($query, $args, $options); - $this->releaseSavepoint(); - } - catch (\Exception $e) { - $this->rollbackSavepoint(); - throw $e; - } - } - else { - $return = parent::query($query, $args, $options); - } - - return $return; - } - - /** - * {@inheritdoc} - */ - public function prepareStatement(string $query, array $options, bool $allow_row_count = FALSE): StatementInterface { - // mapConditionOperator converts some operations (LIKE, REGEXP, etc.) to - // PostgreSQL equivalents (ILIKE, ~*, etc.). However PostgreSQL doesn't - // automatically cast the fields to the right type for these operators, - // so we need to alter the query and add the type-cast. - $query = preg_replace('/ ([^ ]+) +(I*LIKE|NOT +I*LIKE|~\*|!~\*) /i', ' ${1}::text ${2} ', $query); - return parent::prepareStatement($query, $options, $allow_row_count); - } - - public function queryRange($query, $from, $count, array $args = [], array $options = []) { - return $this->query($query . ' LIMIT ' . (int) $count . ' OFFSET ' . (int) $from, $args, $options); - } - - /** - * {@inheritdoc} - */ - public function queryTemporary($query, array $args = [], array $options = []) { - @trigger_error('Connection::queryTemporary() is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. There is no replacement. See https://www.drupal.org/node/3211781', E_USER_DEPRECATED); - $tablename = $this->generateTemporaryTableName(); - $this->query('CREATE TEMPORARY TABLE {' . $tablename . '} AS ' . $query, $args, $options); - return $tablename; - } - - public function driver() { - return 'pgsql'; - } - - public function databaseType() { - return 'pgsql'; - } - - /** - * Overrides \Drupal\Core\Database\Connection::createDatabase(). - * - * @param string $database - * The name of the database to create. - * - * @throws \Drupal\Core\Database\DatabaseNotFoundException - */ - public function createDatabase($database) { - // Escape the database name. - $database = Database::getConnection()->escapeDatabase($database); - - // If the PECL intl extension is installed, use it to determine the proper - // locale. Otherwise, fall back to en_US. - if (class_exists('Locale')) { - $locale = \Locale::getDefault(); - } - else { - $locale = 'en_US'; - } - - try { - // Create the database and set it as active. - $this->connection->exec("CREATE DATABASE $database WITH TEMPLATE template0 ENCODING='utf8' LC_CTYPE='$locale.utf8' LC_COLLATE='$locale.utf8'"); - } - catch (\Exception $e) { - throw new DatabaseNotFoundException($e->getMessage()); - } - } - - public function mapConditionOperator($operator) { - return static::$postgresqlConditionOperatorMap[$operator] ?? NULL; - } - - /** - * Retrieve a the next id in a sequence. - * - * PostgreSQL has built in sequences. We'll use these instead of inserting - * and updating a sequences table. - */ - public function nextId($existing = 0) { - - // Retrieve the name of the sequence. This information cannot be cached - // because the prefix may change, for example, like it does in tests. - $sequence_name = $this->makeSequenceName('sequences', 'value'); - - // When PostgreSQL gets a value too small then it will lock the table, - // retry the INSERT and if it's still too small then alter the sequence. - $id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField(); - if ($id > $existing) { - return $id; - } - - // PostgreSQL advisory locks are simply locks to be used by an - // application such as Drupal. This will prevent other Drupal processes - // from altering the sequence while we are. - $this->query("SELECT pg_advisory_lock(" . self::POSTGRESQL_NEXTID_LOCK . ")"); - - // While waiting to obtain the lock, the sequence may have been altered - // so lets try again to obtain an adequate value. - $id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField(); - if ($id > $existing) { - $this->query("SELECT pg_advisory_unlock(" . self::POSTGRESQL_NEXTID_LOCK . ")"); - return $id; - } - - // Reset the sequence to a higher value than the existing id. - $this->query("ALTER SEQUENCE " . $sequence_name . " RESTART WITH " . ($existing + 1)); - - // Retrieve the next id. We know this will be as high as we want it. - $id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField(); - - $this->query("SELECT pg_advisory_unlock(" . self::POSTGRESQL_NEXTID_LOCK . ")"); - - return $id; - } - - /** - * {@inheritdoc} - */ - public function getFullQualifiedTableName($table) { - $options = $this->getConnectionOptions(); - $prefix = $this->tablePrefix($table); - - // The fully qualified table name in PostgreSQL is in the form of - // <database>.<schema>.<table>, so we have to include the 'public' schema in - // the return value. - return $options['database'] . '.public.' . $prefix . $table; - } - - /** - * Add a new savepoint with a unique name. - * - * The main use for this method is to mimic InnoDB functionality, which - * provides an inherent savepoint before any query in a transaction. - * - * @param $savepoint_name - * A string representing the savepoint name. By default, - * "mimic_implicit_commit" is used. - * - * @see Drupal\Core\Database\Connection::pushTransaction() - */ - public function addSavepoint($savepoint_name = 'mimic_implicit_commit') { - if ($this->inTransaction()) { - $this->pushTransaction($savepoint_name); - } - } - - /** - * Release a savepoint by name. - * - * @param $savepoint_name - * A string representing the savepoint name. By default, - * "mimic_implicit_commit" is used. - * - * @see Drupal\Core\Database\Connection::popTransaction() - */ - public function releaseSavepoint($savepoint_name = 'mimic_implicit_commit') { - if (isset($this->transactionLayers[$savepoint_name])) { - $this->popTransaction($savepoint_name); - } - } - - /** - * Rollback a savepoint by name if it exists. - * - * @param $savepoint_name - * A string representing the savepoint name. By default, - * "mimic_implicit_commit" is used. - */ - public function rollbackSavepoint($savepoint_name = 'mimic_implicit_commit') { - if (isset($this->transactionLayers[$savepoint_name])) { - $this->rollBack($savepoint_name); - } - } - -} - -/** - * @} End of "addtogroup database". - */ +class Connection extends PgsqlConnection {} diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Delete.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Delete.php index 2cf178e7b2ed..0c3333003575 100644 --- a/core/lib/Drupal/Core/Database/Driver/pgsql/Delete.php +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Delete.php @@ -2,28 +2,16 @@ namespace Drupal\Core\Database\Driver\pgsql; -use Drupal\Core\Database\Query\Delete as QueryDelete; +use Drupal\pgsql\Driver\Database\pgsql\Delete as PgsqlDelete; + +@trigger_error('\Drupal\Core\Database\Driver\pgsql\Delete is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED); /** * PostgreSQL implementation of \Drupal\Core\Database\Query\Delete. + * + * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL + * database driver has been moved to the pgsql module. + * + * @see https://www.drupal.org/node/3129492 */ -class Delete extends QueryDelete { - - /** - * {@inheritdoc} - */ - public function execute() { - $this->connection->addSavepoint(); - try { - $result = parent::execute(); - } - catch (\Exception $e) { - $this->connection->rollbackSavepoint(); - throw $e; - } - $this->connection->releaseSavepoint(); - - return $result; - } - -} +class Delete extends PgsqlDelete {} diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Insert.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Insert.php index 3f27f4c8c668..0702d38a6d58 100644 --- a/core/lib/Drupal/Core/Database/Driver/pgsql/Insert.php +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Insert.php @@ -2,157 +2,16 @@ namespace Drupal\Core\Database\Driver\pgsql; -use Drupal\Core\Database\DatabaseExceptionWrapper; -use Drupal\Core\Database\IntegrityConstraintViolationException; -use Drupal\Core\Database\Query\Insert as QueryInsert; +use Drupal\pgsql\Driver\Database\pgsql\Insert as PgsqlInsert; -// cSpell:ignore nextval setval - -/** - * @ingroup database - * @{ - */ +@trigger_error('\Drupal\Core\Database\Driver\pgsql\Insert is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED); /** * PostgreSQL implementation of \Drupal\Core\Database\Query\Insert. + * + * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL + * database driver has been moved to the pgsql module. + * + * @see https://www.drupal.org/node/3129492 */ -class Insert extends QueryInsert { - - public function execute() { - if (!$this->preExecute()) { - return NULL; - } - - $stmt = $this->connection->prepareStatement((string) $this, $this->queryOptions); - - // Fetch the list of blobs and sequences used on that table. - $table_information = $this->connection->schema()->queryTableInformation($this->table); - - $max_placeholder = 0; - $blobs = []; - $blob_count = 0; - foreach ($this->insertValues as $insert_values) { - foreach ($this->insertFields as $idx => $field) { - if (isset($table_information->blob_fields[$field]) && $insert_values[$idx] !== NULL) { - $blobs[$blob_count] = fopen('php://memory', 'a'); - fwrite($blobs[$blob_count], $insert_values[$idx]); - rewind($blobs[$blob_count]); - - $stmt->getClientStatement()->bindParam(':db_insert_placeholder_' . $max_placeholder++, $blobs[$blob_count], \PDO::PARAM_LOB); - - // Pre-increment is faster in PHP than increment. - ++$blob_count; - } - else { - $stmt->getClientStatement()->bindParam(':db_insert_placeholder_' . $max_placeholder++, $insert_values[$idx]); - } - } - // Check if values for a serial field has been passed. - if (!empty($table_information->serial_fields)) { - foreach ($table_information->serial_fields as $index => $serial_field) { - $serial_key = array_search($serial_field, $this->insertFields); - if ($serial_key !== FALSE) { - $serial_value = $insert_values[$serial_key]; - - // Sequences must be greater than or equal to 1. - if ($serial_value === NULL || !$serial_value) { - $serial_value = 1; - } - // Set the sequence to the bigger value of either the passed - // value or the max value of the column. It can happen that another - // thread calls nextval() which could lead to a serial number being - // used twice. However, trying to insert a value into a serial - // column should only be done in very rare cases and is not thread - // safe by definition. - $this->connection->query("SELECT setval('" . $table_information->sequences[$index] . "', GREATEST(MAX(" . $serial_field . "), :serial_value)) FROM {" . $this->table . "}", [':serial_value' => (int) $serial_value]); - } - } - } - } - if (!empty($this->fromQuery)) { - // bindParam stores only a reference to the variable that is followed when - // the statement is executed. We pass $arguments[$key] instead of $value - // because the second argument to bindParam is passed by reference and - // the foreach statement assigns the element to the existing reference. - $arguments = $this->fromQuery->getArguments(); - foreach ($arguments as $key => $value) { - $stmt->getClientStatement()->bindParam($key, $arguments[$key]); - } - } - - // 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 - // fails. This is important for tables that are created on demand. For - // example, \Drupal\Core\Cache\DatabaseBackend. - $this->connection->addSavepoint(); - try { - $stmt->execute(NULL, $this->queryOptions); - if (isset($table_information->serial_fields[0])) { - $last_insert_id = $stmt->fetchField(); - } - $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; - } - - // Re-initialize the values array so that we can re-use this query. - $this->insertValues = []; - - return $last_insert_id ?? NULL; - } - - public function __toString() { - // Create a sanitized comment string to prepend to the query. - $comments = $this->connection->makeComment($this->comments); - - // Default fields are always placed first for consistency. - $insert_fields = array_merge($this->defaultFields, $this->insertFields); - - $insert_fields = array_map(function ($f) { - return $this->connection->escapeField($f); - }, $insert_fields); - - // 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)) { - $insert_fields_string = $insert_fields ? ' (' . implode(', ', $insert_fields) . ') ' : ' '; - $query = $comments . 'INSERT INTO {' . $this->table . '}' . $insert_fields_string . $this->fromQuery; - } - else { - $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES '; - - $values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields); - $query .= implode(', ', $values); - } - try { - // Fetch the list of blobs and sequences used on that table. - $table_information = $this->connection->schema()->queryTableInformation($this->table); - if (isset($table_information->serial_fields[0])) { - // Use RETURNING syntax to get the last insert ID in the same INSERT - // query, see https://www.postgresql.org/docs/10/dml-returning.html. - $query .= ' RETURNING ' . $table_information->serial_fields[0]; - } - } - catch (DatabaseExceptionWrapper $e) { - // If we fail to get the table information it is probably because the - // table does not exist yet so adding the returning statement is pointless - // because the query will fail. This happens for tables created on demand, - // for example, cache tables. - } - return $query; - } - -} +class Insert extends PgsqlInsert {} diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php index 1c07dfaf382c..c7104cc0c131 100644 --- a/core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php @@ -2,293 +2,16 @@ namespace Drupal\Core\Database\Driver\pgsql\Install; -use Drupal\Core\Database\Database; -use Drupal\Core\Database\Install\Tasks as InstallTasks; -use Drupal\Core\Database\DatabaseNotFoundException; +use Drupal\pgsql\Driver\Database\pgsql\Install\Tasks as PgsqlTasks; + +@trigger_error('\Drupal\Core\Database\Driver\pgsql\Install\Tasks is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED); /** * Specifies installation tasks for PostgreSQL databases. + * + * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL + * database driver has been moved to the pgsql module. + * + * @see https://www.drupal.org/node/3129492 */ -class Tasks extends InstallTasks { - - /** - * Minimum required PostgreSQL version. - * - * The contrib extension pg_trgm is supposed to be installed. - * - * @see https://www.postgresql.org/docs/10/pgtrgm.html - */ - const PGSQL_MINIMUM_VERSION = '10'; - - /** - * {@inheritdoc} - */ - protected $pdoDriver = 'pgsql'; - - /** - * Constructs a \Drupal\Core\Database\Driver\pgsql\Install\Tasks object. - */ - public function __construct() { - $this->tasks[] = [ - 'function' => 'checkEncoding', - 'arguments' => [], - ]; - $this->tasks[] = [ - 'function' => 'checkBinaryOutput', - 'arguments' => [], - ]; - $this->tasks[] = [ - 'function' => 'checkStandardConformingStrings', - 'arguments' => [], - ]; - $this->tasks[] = [ - 'function' => 'initializeDatabase', - 'arguments' => [], - ]; - } - - /** - * {@inheritdoc} - */ - public function name() { - return t('PostgreSQL'); - } - - /** - * {@inheritdoc} - */ - public function minimumVersion() { - return static::PGSQL_MINIMUM_VERSION; - } - - /** - * {@inheritdoc} - */ - protected function connect() { - try { - // This doesn't actually test the connection. - Database::setActiveConnection(); - // Now actually do a check. - Database::getConnection(); - $this->pass('Drupal can CONNECT to the database ok.'); - } - catch (\Exception $e) { - // Attempt to create the database if it is not found. - if ($e instanceof DatabaseNotFoundException) { - // Remove the database string from connection info. - $connection_info = Database::getConnectionInfo(); - $database = $connection_info['default']['database']; - unset($connection_info['default']['database']); - - // In order to change the Database::$databaseInfo array, need to remove - // the active connection, then re-add it with the new info. - Database::removeConnection('default'); - Database::addConnectionInfo('default', 'default', $connection_info['default']); - - try { - // Now, attempt the connection again; if it's successful, attempt to - // create the database. - Database::getConnection()->createDatabase($database); - Database::closeConnection(); - - // Now, restore the database config. - Database::removeConnection('default'); - $connection_info['default']['database'] = $database; - Database::addConnectionInfo('default', 'default', $connection_info['default']); - - // Check the database connection. - Database::getConnection(); - $this->pass('Drupal can CONNECT to the database ok.'); - } - catch (DatabaseNotFoundException $e) { - // Still no dice; probably a permission issue. Raise the error to the - // installer. - $this->fail(t('Database %database not found. The server reports the following message when attempting to create the database: %error.', ['%database' => $database, '%error' => $e->getMessage()])); - } - } - else { - // Database connection failed for some other reason than a non-existent - // database. - $this->fail(t('Failed to connect to your database server. The server reports the following message: %error.<ul><li>Is the database server running?</li><li>Does the database exist, and have you entered the correct database name?</li><li>Have you entered the correct username and password?</li><li>Have you entered the correct database hostname and port number?</li></ul>', ['%error' => $e->getMessage()])); - return FALSE; - } - } - return TRUE; - } - - /** - * Check encoding is UTF8. - */ - protected function checkEncoding() { - try { - if (Database::getConnection()->query('SHOW server_encoding')->fetchField() == 'UTF8') { - $this->pass(t('Database is encoded in UTF-8')); - } - else { - $this->fail(t('The %driver database must use %encoding encoding to work with Drupal. Recreate the database with %encoding encoding. See <a href="INSTALL.pgsql.txt">INSTALL.pgsql.txt</a> for more details.', [ - '%encoding' => 'UTF8', - '%driver' => $this->name(), - ])); - } - } - catch (\Exception $e) { - $this->fail(t('Drupal could not determine the encoding of the database was set to UTF-8')); - } - } - - /** - * Check Binary Output. - * - * Unserializing does not work on Postgresql 9 when bytea_output is 'hex'. - */ - public function checkBinaryOutput() { - $database_connection = Database::getConnection(); - if (!$this->checkBinaryOutputSuccess()) { - // First try to alter the database. If it fails, raise an error telling - // the user to do it themselves. - $connection_options = $database_connection->getConnectionOptions(); - // It is safe to include the database name directly here, because this - // code is only called when a connection to the database is already - // established, thus the database name is guaranteed to be a correct - // value. - $query = "ALTER DATABASE \"{$connection_options['database']}\" SET bytea_output = 'escape';"; - try { - $database_connection->query($query); - } - catch (\Exception $e) { - // Ignore possible errors when the user doesn't have the necessary - // privileges to ALTER the database. - } - - // Close the database connection so that the configuration parameter - // is applied to the current connection. - Database::closeConnection(); - - // Recheck, if it fails, finally just rely on the end user to do the - // right thing. - if (!$this->checkBinaryOutputSuccess()) { - $replacements = [ - '%setting' => 'bytea_output', - '%current_value' => 'hex', - '%needed_value' => 'escape', - '@query' => $query, - ]; - $this->fail(t("The %setting setting is currently set to '%current_value', but needs to be '%needed_value'. Change this by running the following query: <code>@query</code>", $replacements)); - } - } - } - - /** - * Verify that a binary data roundtrip returns the original string. - */ - protected function checkBinaryOutputSuccess() { - $bytea_output = Database::getConnection()->query("SHOW bytea_output")->fetchField(); - return ($bytea_output == 'escape'); - } - - /** - * Ensures standard_conforming_strings setting is 'on'. - * - * When standard_conforming_strings setting is 'on' string literals ('...') - * treat backslashes literally, as specified in the SQL standard. This allows - * Drupal to convert between bytea, text and varchar columns. - */ - public function checkStandardConformingStrings() { - $database_connection = Database::getConnection(); - if (!$this->checkStandardConformingStringsSuccess()) { - // First try to alter the database. If it fails, raise an error telling - // the user to do it themselves. - $connection_options = $database_connection->getConnectionOptions(); - // It is safe to include the database name directly here, because this - // code is only called when a connection to the database is already - // established, thus the database name is guaranteed to be a correct - // value. - $query = "ALTER DATABASE \"" . $connection_options['database'] . "\" SET standard_conforming_strings = 'on';"; - try { - $database_connection->query($query); - } - catch (\Exception $e) { - // Ignore possible errors when the user doesn't have the necessary - // privileges to ALTER the database. - } - - // Close the database connection so that the configuration parameter - // is applied to the current connection. - Database::closeConnection(); - - // Recheck, if it fails, finally just rely on the end user to do the - // right thing. - if (!$this->checkStandardConformingStringsSuccess()) { - $replacements = [ - '%setting' => 'standard_conforming_strings', - '%current_value' => 'off', - '%needed_value' => 'on', - '@query' => $query, - ]; - $this->fail(t("The %setting setting is currently set to '%current_value', but needs to be '%needed_value'. Change this by running the following query: <code>@query</code>", $replacements)); - } - } - } - - /** - * Verifies the standard_conforming_strings setting. - */ - protected function checkStandardConformingStringsSuccess() { - $standard_conforming_strings = Database::getConnection()->query("SHOW standard_conforming_strings")->fetchField(); - return ($standard_conforming_strings == 'on'); - } - - /** - * Make PostgreSQL Drupal friendly. - */ - public function initializeDatabase() { - // We create some functions using global names instead of prefixing them - // like we do with table names. This is so that we don't double up if more - // than one instance of Drupal is running on a single database. We therefore - // avoid trying to create them again in that case. - // At the same time checking for the existence of the function fixes - // concurrency issues, when both try to update at the same time. - try { - $connection = Database::getConnection(); - // When testing, two installs might try to run the CREATE FUNCTION queries - // at the same time. Do not let that happen. - $connection->query('SELECT pg_advisory_lock(1)'); - // Don't use {} around pg_proc table. - if (!$connection->query("SELECT COUNT(*) FROM pg_proc WHERE proname = 'rand'")->fetchField()) { - $connection->query('CREATE OR REPLACE FUNCTION "rand"() RETURNS float AS - \'SELECT random();\' - LANGUAGE \'sql\'', - [], - ['allow_delimiter_in_query' => TRUE] - ); - } - - if (!$connection->query("SELECT COUNT(*) FROM pg_proc WHERE proname = 'substring_index'")->fetchField()) { - $connection->query('CREATE OR REPLACE FUNCTION "substring_index"(text, text, integer) RETURNS text AS - \'SELECT array_to_string((string_to_array($1, $2)) [1:$3], $2);\' - LANGUAGE \'sql\'', - [], - ['allow_delimiter_in_query' => TRUE, 'allow_square_brackets' => TRUE] - ); - } - $connection->query('SELECT pg_advisory_unlock(1)'); - - $this->pass(t('PostgreSQL has initialized itself.')); - } - catch (\Exception $e) { - $this->fail(t('Drupal could not be correctly setup with the existing database due to the following error: @error.', ['@error' => $e->getMessage()])); - } - } - - /** - * {@inheritdoc} - */ - public function getFormOptions(array $database) { - $form = parent::getFormOptions($database); - if (empty($form['advanced_options']['port']['#default_value'])) { - $form['advanced_options']['port']['#default_value'] = '5432'; - } - return $form; - } - -} +class Tasks extends PgsqlTasks {} diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php index 59795d39210c..2dbe382ec58b 100644 --- a/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php @@ -2,1083 +2,16 @@ namespace Drupal\Core\Database\Driver\pgsql; -use Drupal\Core\Database\SchemaObjectExistsException; -use Drupal\Core\Database\SchemaObjectDoesNotExistException; -use Drupal\Core\Database\Schema as DatabaseSchema; +use Drupal\pgsql\Driver\Database\pgsql\Schema as PgsqlSchema; -// cSpell:ignore adbin adnum adrelid adsrc attisdropped attname attnum attrdef -// cSpell:ignore attrelid atttypid atttypmod bigserial conkey conname conrelid -// cSpell:ignore contype fillfactor indexname indexrelid indisprimary indkey -// cSpell:ignore indrelid nextval nspname regclass relkind relname relnamespace -// cSpell:ignore schemaname setval - -/** - * @addtogroup schemaapi - * @{ - */ +@trigger_error('\Drupal\Core\Database\Driver\pgsql\Schema is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED); /** * PostgreSQL implementation of \Drupal\Core\Database\Schema. + * + * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL + * database driver has been moved to the pgsql module. + * + * @see https://www.drupal.org/node/3129492 */ -class Schema extends DatabaseSchema { - - /** - * A cache of information about blob columns and sequences of tables. - * - * This is collected by Schema::queryTableInformation(), by introspecting the - * database. - * - * @see \Drupal\Core\Database\Driver\pgsql\Schema::queryTableInformation() - * @var array - */ - protected $tableInformation = []; - - /** - * The maximum allowed length for index, primary key and constraint names. - * - * Value will usually be set to a 63 chars limit but PostgreSQL allows - * to higher this value before compiling, so we need to check for that. - * - * @var int - */ - protected $maxIdentifierLength; - - /** - * PostgreSQL's temporary namespace name. - * - * @var string - */ - protected $tempNamespaceName; - - /** - * Make sure to limit identifiers according to PostgreSQL compiled in length. - * - * PostgreSQL allows in standard configuration identifiers no longer than 63 - * chars for table/relation names, indexes, primary keys, and constraints. So - * we map all identifiers that are too long to drupal_base64hash_tag, where - * tag is one of: - * - idx for indexes - * - key for constraints - * - pkey for primary keys - * - seq for sequences - * - * @param string $table_identifier_part - * The first argument used to build the identifier string. This usually - * refers to a table/relation name. - * @param string $column_identifier_part - * The second argument used to build the identifier string. This usually - * refers to one or more column names. - * @param string $tag - * The identifier tag. It can be one of 'idx', 'key', 'pkey' or 'seq'. - * @param string $separator - * (optional) The separator used to glue together the aforementioned - * identifier parts. Defaults to '__'. - * - * @return string - * The index/constraint/pkey identifier. - */ - protected function ensureIdentifiersLength($table_identifier_part, $column_identifier_part, $tag, $separator = '__') { - $info = $this->getPrefixInfo($table_identifier_part); - $table_identifier_part = $info['table']; - $identifierName = implode($separator, [$table_identifier_part, $column_identifier_part, $tag]); - - // Retrieve the max identifier length which is usually 63 characters - // but can be altered before PostgreSQL is compiled so we need to check. - if (empty($this->maxIdentifierLength)) { - $this->maxIdentifierLength = $this->connection->query("SHOW max_identifier_length")->fetchField(); - } - - if (strlen($identifierName) > $this->maxIdentifierLength) { - $saveIdentifier = '"drupal_' . $this->hashBase64($identifierName) . '_' . $tag . '"'; - } - else { - $saveIdentifier = $identifierName; - } - return $saveIdentifier; - } - - /** - * Fetch the list of blobs and sequences used on a table. - * - * We introspect the database to collect the information required by insert - * and update queries. - * - * @param string $table - * The non-prefixed name of the table. - * - * @return mixed|object - * An object with two member variables: - * - 'blob_fields' that lists all the blob fields in the table. - * - 'sequences' that lists the sequences used in that table. - * - * @throws \Exception - * Exception thrown when the query for the table information fails. - */ - public function queryTableInformation($table) { - // Generate a key to reference this table's information on. - $key = $this->connection->prefixTables('{' . $table . '}'); - - // Take into account that temporary tables are stored in a different schema. - // \Drupal\Core\Database\Connection::generateTemporaryTableName() sets the - // 'db_temporary_' prefix to all temporary tables. - if (strpos($key, '.') === FALSE && strpos($table, 'db_temporary_') === FALSE) { - $key = 'public.' . $key; - } - else { - $key = $this->getTempNamespaceName() . '.' . $key; - } - - if (!isset($this->tableInformation[$key])) { - $table_information = (object) [ - 'blob_fields' => [], - 'sequences' => [], - ]; - $this->connection->addSavepoint(); - - try { - // The bytea columns and sequences for a table can be found in - // pg_attribute, which is significantly faster than querying the - // information_schema. The data type of a field can be found by lookup - // of the attribute ID, and the default value must be extracted from the - // node tree for the attribute definition instead of the historical - // human-readable column, adsrc. - $sql = <<<'EOD' -SELECT pg_attribute.attname AS column_name, format_type(pg_attribute.atttypid, pg_attribute.atttypmod) AS data_type, pg_get_expr(pg_attrdef.adbin, pg_attribute.attrelid) AS column_default -FROM pg_attribute -LEFT JOIN pg_attrdef ON pg_attrdef.adrelid = pg_attribute.attrelid AND pg_attrdef.adnum = pg_attribute.attnum -WHERE pg_attribute.attnum > 0 -AND NOT pg_attribute.attisdropped -AND pg_attribute.attrelid = :key::regclass -AND (format_type(pg_attribute.atttypid, pg_attribute.atttypmod) = 'bytea' -OR pg_get_expr(pg_attrdef.adbin, pg_attribute.attrelid) LIKE 'nextval%') -EOD; - $result = $this->connection->query($sql, [ - ':key' => $key, - ]); - } - catch (\Exception $e) { - $this->connection->rollbackSavepoint(); - throw $e; - } - $this->connection->releaseSavepoint(); - - // If the table information does not yet exist in the PostgreSQL - // metadata, then return the default table information here, so that it - // will not be cached. - if (empty($result)) { - return $table_information; - } - - foreach ($result as $column) { - if ($column->data_type == 'bytea') { - $table_information->blob_fields[$column->column_name] = TRUE; - } - elseif (preg_match("/nextval\('([^']+)'/", $column->column_default, $matches)) { - // We must know of any sequences in the table structure to help us - // return the last insert id. If there is more than 1 sequences the - // first one (index 0 of the sequences array) will be used. - $table_information->sequences[] = $matches[1]; - $table_information->serial_fields[] = $column->column_name; - } - } - $this->tableInformation[$key] = $table_information; - } - return $this->tableInformation[$key]; - } - - /** - * Gets PostgreSQL's temporary namespace name. - * - * @return string - * PostgreSQL's temporary namespace name. - */ - protected function getTempNamespaceName() { - if (!isset($this->tempNamespaceName)) { - $this->tempNamespaceName = $this->connection->query('SELECT nspname FROM pg_namespace WHERE oid = pg_my_temp_schema()')->fetchField(); - } - return $this->tempNamespaceName; - } - - /** - * Resets information about table blobs, sequences and serial fields. - * - * @param $table - * The non-prefixed name of the table. - */ - protected function resetTableInformation($table) { - $key = $this->connection->prefixTables('{' . $table . '}'); - if (strpos($key, '.') === FALSE) { - $key = 'public.' . $key; - } - unset($this->tableInformation[$key]); - } - - /** - * Fetches the list of constraints used on a field. - * - * We introspect the database to collect the information required by field - * alteration. - * - * @param string $table - * The non-prefixed name of the table. - * @param string $field - * The name of the field. - * @param string $constraint_type - * (optional) The type of the constraint. This can be one of the following: - * - c: check constraint; - * - f: foreign key constraint; - * - p: primary key constraint; - * - u: unique constraint; - * - t: constraint trigger; - * - x: exclusion constraint. - * Defaults to 'c' for a CHECK constraint. - * @see https://www.postgresql.org/docs/current/catalog-pg-constraint.html - * - * @return array - * An array containing all the constraint names for the field. - * - * @throws \Exception - * Exception thrown when the query for the table information fails. - */ - public function queryFieldInformation($table, $field, $constraint_type = 'c') { - assert(in_array($constraint_type, ['c', 'f', 'p', 'u', 't', 'x'])); - $prefixInfo = $this->getPrefixInfo($table, TRUE); - - // Split the key into schema and table for querying. - $schema = $prefixInfo['schema']; - $table_name = $prefixInfo['table']; - - $this->connection->addSavepoint(); - - try { - $checks = $this->connection->query("SELECT conname FROM pg_class cl INNER JOIN pg_constraint co ON co.conrelid = cl.oid INNER JOIN pg_attribute attr ON attr.attrelid = cl.oid AND attr.attnum = ANY (co.conkey) INNER JOIN pg_namespace ns ON cl.relnamespace = ns.oid WHERE co.contype = :constraint_type AND ns.nspname = :schema AND cl.relname = :table AND attr.attname = :column", [ - ':constraint_type' => $constraint_type, - ':schema' => $schema, - ':table' => $table_name, - ':column' => $field, - ]); - } - catch (\Exception $e) { - $this->connection->rollbackSavepoint(); - throw $e; - } - - $this->connection->releaseSavepoint(); - - $field_information = $checks->fetchCol(); - - return $field_information; - } - - /** - * Generate SQL to create a new table from a Drupal schema definition. - * - * @param string $name - * The name of the table to create. - * @param array $table - * A Schema API table definition array. - * - * @return array - * An array of SQL statements to create the table. - */ - protected function createTableSql($name, $table) { - $sql_fields = []; - foreach ($table['fields'] as $field_name => $field) { - $sql_fields[] = $this->createFieldSql($field_name, $this->processField($field)); - } - - $sql_keys = []; - if (!empty($table['primary key']) && is_array($table['primary key'])) { - $this->ensureNotNullPrimaryKey($table['primary key'], $table['fields']); - $sql_keys[] = 'CONSTRAINT ' . $this->ensureIdentifiersLength($name, '', 'pkey') . ' PRIMARY KEY (' . $this->createPrimaryKeySql($table['primary key']) . ')'; - } - if (isset($table['unique keys']) && is_array($table['unique keys'])) { - foreach ($table['unique keys'] as $key_name => $key) { - $sql_keys[] = 'CONSTRAINT ' . $this->ensureIdentifiersLength($name, $key_name, 'key') . ' UNIQUE (' . implode(', ', $key) . ')'; - } - } - - $sql = "CREATE TABLE {" . $name . "} (\n\t"; - $sql .= implode(",\n\t", $sql_fields); - if (count($sql_keys) > 0) { - $sql .= ",\n\t"; - } - $sql .= implode(",\n\t", $sql_keys); - $sql .= "\n)"; - $statements[] = $sql; - - if (isset($table['indexes']) && is_array($table['indexes'])) { - foreach ($table['indexes'] as $key_name => $key) { - $statements[] = $this->_createIndexSql($name, $key_name, $key); - } - } - - // Add table comment. - if (!empty($table['description'])) { - $statements[] = 'COMMENT ON TABLE {' . $name . '} IS ' . $this->prepareComment($table['description']); - } - - // Add column comments. - foreach ($table['fields'] as $field_name => $field) { - if (!empty($field['description'])) { - $statements[] = 'COMMENT ON COLUMN {' . $name . '}.' . $field_name . ' IS ' . $this->prepareComment($field['description']); - } - } - - return $statements; - } - - /** - * Create an SQL string for a field to be used in table creation or - * alteration. - * - * @param $name - * Name of the field. - * @param $spec - * The field specification, as per the schema data structure format. - */ - protected function createFieldSql($name, $spec) { - // The PostgreSQL server converts names into lowercase, unless quoted. - $sql = '"' . $name . '" ' . $spec['pgsql_type']; - - if (isset($spec['type']) && $spec['type'] == 'serial') { - unset($spec['not null']); - } - - if (in_array($spec['pgsql_type'], ['varchar', 'character']) && isset($spec['length'])) { - $sql .= '(' . $spec['length'] . ')'; - } - elseif (isset($spec['precision']) && isset($spec['scale'])) { - $sql .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')'; - } - - if (!empty($spec['unsigned'])) { - $sql .= " CHECK ($name >= 0)"; - } - - if (isset($spec['not null'])) { - if ($spec['not null']) { - $sql .= ' NOT NULL'; - } - else { - $sql .= ' NULL'; - } - } - if (array_key_exists('default', $spec)) { - $default = $this->escapeDefaultValue($spec['default']); - $sql .= " default $default"; - } - - return $sql; - } - - /** - * Set database-engine specific properties for a field. - * - * @param $field - * A field description array, as specified in the schema documentation. - */ - protected function processField($field) { - if (!isset($field['size'])) { - $field['size'] = 'normal'; - } - - // Set the correct database-engine specific datatype. - // In case one is already provided, force it to lowercase. - if (isset($field['pgsql_type'])) { - $field['pgsql_type'] = mb_strtolower($field['pgsql_type']); - } - else { - $map = $this->getFieldTypeMap(); - $field['pgsql_type'] = $map[$field['type'] . ':' . $field['size']]; - } - - if (!empty($field['unsigned'])) { - // Unsigned data types are not supported in PostgreSQL 10. In MySQL, - // they are used to ensure a positive number is inserted and it also - // doubles the maximum integer size that can be stored in a field. - // The PostgreSQL schema in Drupal creates a check constraint - // to ensure that a value inserted is >= 0. To provide the extra - // integer capacity, here, we bump up the column field size. - if (!isset($map)) { - $map = $this->getFieldTypeMap(); - } - switch ($field['pgsql_type']) { - case 'smallint': - $field['pgsql_type'] = $map['int:medium']; - break; - - case 'int': - $field['pgsql_type'] = $map['int:big']; - break; - } - } - if (isset($field['type']) && $field['type'] == 'serial') { - unset($field['not null']); - } - return $field; - } - - /** - * {@inheritdoc} - */ - public function getFieldTypeMap() { - // Put :normal last so it gets preserved by array_flip. This makes - // it much easier for modules (such as schema.module) to map - // database types back into schema types. - // $map does not use drupal_static as its value never changes. - static $map = [ - 'varchar_ascii:normal' => 'varchar', - - 'varchar:normal' => 'varchar', - 'char:normal' => 'character', - - 'text:tiny' => 'text', - 'text:small' => 'text', - 'text:medium' => 'text', - 'text:big' => 'text', - 'text:normal' => 'text', - - 'int:tiny' => 'smallint', - 'int:small' => 'smallint', - 'int:medium' => 'int', - 'int:big' => 'bigint', - 'int:normal' => 'int', - - 'float:tiny' => 'real', - 'float:small' => 'real', - 'float:medium' => 'real', - 'float:big' => 'double precision', - 'float:normal' => 'real', - - 'numeric:normal' => 'numeric', - - 'blob:big' => 'bytea', - 'blob:normal' => 'bytea', - - 'serial:tiny' => 'serial', - 'serial:small' => 'serial', - 'serial:medium' => 'serial', - 'serial:big' => 'bigserial', - 'serial:normal' => 'serial', - ]; - return $map; - } - - protected function _createKeySql($fields) { - $return = []; - foreach ($fields as $field) { - if (is_array($field)) { - $return[] = 'substr(' . $field[0] . ', 1, ' . $field[1] . ')'; - } - else { - $return[] = '"' . $field . '"'; - } - } - return implode(', ', $return); - } - - /** - * Create the SQL expression for primary keys. - * - * Postgresql does not support key length. It does support fillfactor, but - * that requires a separate database lookup for each column in the key. The - * key length defined in the schema is ignored. - */ - protected function createPrimaryKeySql($fields) { - $return = []; - foreach ($fields as $field) { - if (is_array($field)) { - $return[] = '"' . $field[0] . '"'; - } - else { - $return[] = '"' . $field . '"'; - } - } - return implode(', ', $return); - } - - /** - * {@inheritdoc} - */ - public function tableExists($table) { - $prefixInfo = $this->getPrefixInfo($table, TRUE); - - return (bool) $this->connection->query("SELECT 1 FROM pg_tables WHERE schemaname = :schema AND tablename = :table", [':schema' => $prefixInfo['schema'], ':table' => $prefixInfo['table']])->fetchField(); - } - - /** - * {@inheritdoc} - */ - public function findTables($table_expression) { - $individually_prefixed_tables = $this->connection->getUnprefixedTablesMap(); - $default_prefix = $this->connection->tablePrefix(); - $default_prefix_length = strlen($default_prefix); - $tables = []; - - // Load all the tables up front in order to take into account per-table - // prefixes. The actual matching is done at the bottom of the method. - $results = $this->connection->query("SELECT tablename FROM pg_tables WHERE schemaname = :schema", [':schema' => $this->defaultSchema]); - foreach ($results as $table) { - // Take into account tables that have an individual prefix. - if (isset($individually_prefixed_tables[$table->tablename])) { - $prefix_length = strlen($this->connection->tablePrefix($individually_prefixed_tables[$table->tablename])); - } - elseif ($default_prefix && substr($table->tablename, 0, $default_prefix_length) !== $default_prefix) { - // This table name does not start the default prefix, which means that - // it is not managed by Drupal so it should be excluded from the result. - continue; - } - else { - $prefix_length = $default_prefix_length; - } - - // Remove the prefix from the returned tables. - $unprefixed_table_name = substr($table->tablename, $prefix_length); - - // The pattern can match a table which is the same as the prefix. That - // will become an empty string when we remove the prefix, which will - // probably surprise the caller, besides not being a prefixed table. So - // remove it. - if (!empty($unprefixed_table_name)) { - $tables[$unprefixed_table_name] = $unprefixed_table_name; - } - } - - // Convert the table expression from its SQL LIKE syntax to a regular - // expression and escape the delimiter that will be used for matching. - $table_expression = str_replace(['%', '_'], ['.*?', '.'], preg_quote($table_expression, '/')); - $tables = preg_grep('/^' . $table_expression . '$/i', $tables); - - return $tables; - } - - /** - * {@inheritdoc} - */ - public function renameTable($table, $new_name) { - if (!$this->tableExists($table)) { - throw new SchemaObjectDoesNotExistException("Cannot rename '$table' to '$new_name': table '$table' doesn't exist."); - } - if ($this->tableExists($new_name)) { - throw new SchemaObjectExistsException("Cannot rename '$table' to '$new_name': table '$new_name' already exists."); - } - - // Get the schema and tablename for the old table. - $old_full_name = str_replace('"', '', $this->connection->prefixTables('{' . $table . '}')); - [$old_schema, $old_table_name] = strpos($old_full_name, '.') ? explode('.', $old_full_name) : ['public', $old_full_name]; - - // Index names and constraint names are global in PostgreSQL, so we need to - // rename them when renaming the table. - $indexes = $this->connection->query('SELECT indexname FROM pg_indexes WHERE schemaname = :schema AND tablename = :table', [':schema' => $old_schema, ':table' => $old_table_name]); - - foreach ($indexes as $index) { - // Get the index type by suffix, e.g. idx/key/pkey - $index_type = substr($index->indexname, strrpos($index->indexname, '_') + 1); - - // If the index is already rewritten by ensureIdentifiersLength() to not - // exceed the 63 chars limit of PostgreSQL, we need to take care of that. - // cSpell:disable-next-line - // Example (drupal_Gk7Su_T1jcBHVuvSPeP22_I3Ni4GrVEgTYlIYnBJkro_idx). - if (strpos($index->indexname, 'drupal_') !== FALSE) { - preg_match('/^drupal_(.*)_' . preg_quote($index_type) . '/', $index->indexname, $matches); - $index_name = $matches[1]; - } - else { - // Make sure to remove the suffix from index names, because - // $this->ensureIdentifiersLength() will add the suffix again and thus - // would result in a wrong index name. - preg_match('/^' . preg_quote($old_full_name) . '__(.*)__' . preg_quote($index_type) . '/', $index->indexname, $matches); - $index_name = $matches[1]; - } - $this->connection->query('ALTER INDEX "' . $index->indexname . '" RENAME TO ' . $this->ensureIdentifiersLength($new_name, $index_name, $index_type) . ''); - } - - // Ensure the new table name does not include schema syntax. - $prefixInfo = $this->getPrefixInfo($new_name); - - // Rename sequences if the table contains serial fields. - $info = $this->queryTableInformation($table); - if (!empty($info->serial_fields)) { - foreach ($info->serial_fields as $field) { - // The initial name of the sequence is generated automatically by - // PostgreSQL when the table is created, so we need to use - // pg_get_serial_sequence() to retrieve it. - $old_sequence = $this->connection->query("SELECT pg_get_serial_sequence('" . $old_full_name . "', '" . $field . "')")->fetchField(); - - // If the new sequence name exceeds the maximum identifier length limit, - // it will not match the pattern that is automatically applied by - // PostgreSQL on table creation, but that's ok because - // pg_get_serial_sequence() will return our non-standard name on - // subsequent table renames. - $new_sequence = $this->ensureIdentifiersLength($new_name, $field, 'seq', '_'); - - $this->connection->query('ALTER SEQUENCE ' . $old_sequence . ' RENAME TO ' . $new_sequence); - } - } - // Now rename the table. - $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO ' . $prefixInfo['table']); - $this->resetTableInformation($table); - } - - /** - * {@inheritdoc} - */ - public function dropTable($table) { - if (!$this->tableExists($table)) { - return FALSE; - } - - $this->connection->query('DROP TABLE {' . $table . '}'); - $this->resetTableInformation($table); - return TRUE; - } - - /** - * {@inheritdoc} - */ - public function addField($table, $field, $spec, $new_keys = []) { - if (!$this->tableExists($table)) { - throw new SchemaObjectDoesNotExistException("Cannot add field '$table.$field': table doesn't exist."); - } - if ($this->fieldExists($table, $field)) { - throw new SchemaObjectExistsException("Cannot add field '$table.$field': field already exists."); - } - - // Fields that are part of a PRIMARY KEY must be added as NOT NULL. - $is_primary_key = isset($new_keys['primary key']) && in_array($field, $new_keys['primary key'], TRUE); - if ($is_primary_key) { - $this->ensureNotNullPrimaryKey($new_keys['primary key'], [$field => $spec]); - } - - $fixnull = FALSE; - if (!empty($spec['not null']) && !isset($spec['default']) && !$is_primary_key) { - $fixnull = TRUE; - $spec['not null'] = FALSE; - } - $query = 'ALTER TABLE {' . $table . '} ADD COLUMN '; - $query .= $this->createFieldSql($field, $this->processField($spec)); - $this->connection->query($query); - if (isset($spec['initial_from_field'])) { - if (isset($spec['initial'])) { - $expression = 'COALESCE(' . $spec['initial_from_field'] . ', :default_initial_value)'; - $arguments = [':default_initial_value' => $spec['initial']]; - } - else { - $expression = $spec['initial_from_field']; - $arguments = []; - } - $this->connection->update($table) - ->expression($field, $expression, $arguments) - ->execute(); - } - elseif (isset($spec['initial'])) { - $this->connection->update($table) - ->fields([$field => $spec['initial']]) - ->execute(); - } - if ($fixnull) { - $this->connection->query("ALTER TABLE {" . $table . "} ALTER $field SET NOT NULL"); - } - if (isset($new_keys)) { - // Make sure to drop the existing primary key before adding a new one. - // This is only needed when adding a field because this method, unlike - // changeField(), is supposed to handle primary keys automatically. - if (isset($new_keys['primary key']) && $this->constraintExists($table, 'pkey')) { - $this->dropPrimaryKey($table); - } - $this->_createKeys($table, $new_keys); - } - // Add column comment. - if (!empty($spec['description'])) { - $this->connection->query('COMMENT ON COLUMN {' . $table . '}.' . $field . ' IS ' . $this->prepareComment($spec['description'])); - } - $this->resetTableInformation($table); - } - - /** - * {@inheritdoc} - */ - public function dropField($table, $field) { - if (!$this->fieldExists($table, $field)) { - return FALSE; - } - - $this->connection->query('ALTER TABLE {' . $table . '} DROP COLUMN "' . $field . '"'); - $this->resetTableInformation($table); - return TRUE; - } - - /** - * {@inheritdoc} - */ - public function fieldExists($table, $column) { - $prefixInfo = $this->getPrefixInfo($table); - - return (bool) $this->connection->query("SELECT 1 FROM pg_attribute WHERE attrelid = :key::regclass AND attname = :column AND NOT attisdropped AND attnum > 0", [':key' => $prefixInfo['schema'] . '.' . $prefixInfo['table'], ':column' => $column])->fetchField(); - } - - /** - * {@inheritdoc} - */ - public function indexExists($table, $name) { - // Details https://www.postgresql.org/docs/10/view-pg-indexes.html - $index_name = $this->ensureIdentifiersLength($table, $name, 'idx'); - // Remove leading and trailing quotes because the index name is in a WHERE - // clause and not used as an identifier. - $index_name = str_replace('"', '', $index_name); - return (bool) $this->connection->query("SELECT 1 FROM pg_indexes WHERE indexname = '$index_name'")->fetchField(); - } - - /** - * Helper function: check if a constraint (PK, FK, UK) exists. - * - * @param string $table - * The name of the table. - * @param string $name - * The name of the constraint (typically 'pkey' or '[constraint]__key'). - * - * @return bool - * TRUE if the constraint exists, FALSE otherwise. - */ - public function constraintExists($table, $name) { - // ::ensureIdentifiersLength() expects three parameters, although not - // explicitly stated in its signature, thus we split our constraint name in - // a proper name and a suffix. - if ($name == 'pkey') { - $suffix = $name; - $name = ''; - } - else { - $pos = strrpos($name, '__'); - $suffix = substr($name, $pos + 2); - $name = substr($name, 0, $pos); - } - $constraint_name = $this->ensureIdentifiersLength($table, $name, $suffix); - // Remove leading and trailing quotes because the index name is in a WHERE - // clause and not used as an identifier. - $constraint_name = str_replace('"', '', $constraint_name); - return (bool) $this->connection->query("SELECT 1 FROM pg_constraint WHERE conname = '$constraint_name'")->fetchField(); - } - - /** - * {@inheritdoc} - */ - public function addPrimaryKey($table, $fields) { - if (!$this->tableExists($table)) { - throw new SchemaObjectDoesNotExistException("Cannot add primary key to table '$table': table doesn't exist."); - } - if ($this->constraintExists($table, 'pkey')) { - throw new SchemaObjectExistsException("Cannot add primary key to table '$table': primary key already exists."); - } - - $this->connection->query('ALTER TABLE {' . $table . '} ADD CONSTRAINT ' . $this->ensureIdentifiersLength($table, '', 'pkey') . ' PRIMARY KEY (' . $this->createPrimaryKeySql($fields) . ')'); - $this->resetTableInformation($table); - } - - /** - * {@inheritdoc} - */ - public function dropPrimaryKey($table) { - if (!$this->constraintExists($table, 'pkey')) { - return FALSE; - } - - $this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT ' . $this->ensureIdentifiersLength($table, '', 'pkey')); - $this->resetTableInformation($table); - return TRUE; - } - - /** - * {@inheritdoc} - */ - protected function findPrimaryKeyColumns($table) { - if (!$this->tableExists($table)) { - return FALSE; - } - return $this->connection->query("SELECT array_position(i.indkey, a.attnum) AS position, a.attname FROM pg_index i JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) WHERE i.indrelid = '{" . $table . "}'::regclass AND i.indisprimary ORDER BY position")->fetchAllKeyed(); - } - - /** - * {@inheritdoc} - */ - public function addUniqueKey($table, $name, $fields) { - if (!$this->tableExists($table)) { - throw new SchemaObjectDoesNotExistException("Cannot add unique key '$name' to table '$table': table doesn't exist."); - } - if ($this->constraintExists($table, $name . '__key')) { - throw new SchemaObjectExistsException("Cannot add unique key '$name' to table '$table': unique key already exists."); - } - - $this->connection->query('ALTER TABLE {' . $table . '} ADD CONSTRAINT ' . $this->ensureIdentifiersLength($table, $name, 'key') . ' UNIQUE (' . implode(',', $fields) . ')'); - $this->resetTableInformation($table); - } - - /** - * {@inheritdoc} - */ - public function dropUniqueKey($table, $name) { - if (!$this->constraintExists($table, $name . '__key')) { - return FALSE; - } - - $this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT ' . $this->ensureIdentifiersLength($table, $name, 'key')); - $this->resetTableInformation($table); - return TRUE; - } - - /** - * {@inheritdoc} - */ - public function addIndex($table, $name, $fields, array $spec) { - if (!$this->tableExists($table)) { - throw new SchemaObjectDoesNotExistException("Cannot add index '$name' to table '$table': table doesn't exist."); - } - if ($this->indexExists($table, $name)) { - throw new SchemaObjectExistsException("Cannot add index '$name' to table '$table': index already exists."); - } - - $this->connection->query($this->_createIndexSql($table, $name, $fields)); - $this->resetTableInformation($table); - } - - /** - * {@inheritdoc} - */ - public function dropIndex($table, $name) { - if (!$this->indexExists($table, $name)) { - return FALSE; - } - - $this->connection->query('DROP INDEX ' . $this->ensureIdentifiersLength($table, $name, 'idx')); - $this->resetTableInformation($table); - return TRUE; - } - - /** - * {@inheritdoc} - */ - protected function introspectIndexSchema($table) { - if (!$this->tableExists($table)) { - throw new SchemaObjectDoesNotExistException("The table $table doesn't exist."); - } - - $index_schema = [ - 'primary key' => [], - 'unique keys' => [], - 'indexes' => [], - ]; - - // Get the schema and tablename for the table without identifier quotes. - $full_name = str_replace('"', '', $this->connection->prefixTables('{' . $table . '}')); - $result = $this->connection->query("SELECT i.relname AS index_name, a.attname AS column_name FROM pg_class t, pg_class i, pg_index ix, pg_attribute a WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid AND a.attnum = ANY(ix.indkey) AND t.relkind = 'r' AND t.relname = :table_name ORDER BY index_name ASC, column_name ASC", [ - ':table_name' => $full_name, - ])->fetchAll(); - foreach ($result as $row) { - if (preg_match('/_pkey$/', $row->index_name)) { - $index_schema['primary key'][] = $row->column_name; - } - elseif (preg_match('/_key$/', $row->index_name)) { - $index_schema['unique keys'][$row->index_name][] = $row->column_name; - } - elseif (preg_match('/_idx$/', $row->index_name)) { - $index_schema['indexes'][$row->index_name][] = $row->column_name; - } - } - - return $index_schema; - } - - /** - * {@inheritdoc} - */ - public function changeField($table, $field, $field_new, $spec, $new_keys = []) { - if (!$this->fieldExists($table, $field)) { - throw new SchemaObjectDoesNotExistException("Cannot change the definition of field '$table.$field': field doesn't exist."); - } - if (($field != $field_new) && $this->fieldExists($table, $field_new)) { - throw new SchemaObjectExistsException("Cannot rename field '$table.$field' to '$field_new': target field already exists."); - } - if (isset($new_keys['primary key']) && in_array($field_new, $new_keys['primary key'], TRUE)) { - $this->ensureNotNullPrimaryKey($new_keys['primary key'], [$field_new => $spec]); - } - - $spec = $this->processField($spec); - - // Type 'serial' is known to PostgreSQL, but only during table creation, - // not when altering. Because of that, we create it here as an 'int'. After - // we create it we manually re-apply the sequence. - if (in_array($spec['pgsql_type'], ['serial', 'bigserial'])) { - $field_def = 'int'; - } - else { - $field_def = $spec['pgsql_type']; - } - - if (in_array($spec['pgsql_type'], ['varchar', 'character', 'text']) && isset($spec['length'])) { - $field_def .= '(' . $spec['length'] . ')'; - } - elseif (isset($spec['precision']) && isset($spec['scale'])) { - $field_def .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')'; - } - - // Remove old check constraints. - $field_info = $this->queryFieldInformation($table, $field); - - foreach ($field_info as $check) { - $this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT "' . $check . '"'); - } - - // Remove old default. - $this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN "' . $field . '" DROP DEFAULT'); - - // Convert field type. - // Usually, we do this via a simple typecast 'USING fieldname::type'. But - // the typecast does not work for conversions to bytea. - // @see http://www.postgresql.org/docs/current/static/datatype-binary.html - $table_information = $this->queryTableInformation($table); - $is_bytea = !empty($table_information->blob_fields[$field]); - if ($spec['pgsql_type'] != 'bytea') { - if ($is_bytea) { - $this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $field_def . ' USING convert_from("' . $field . '"' . ", 'UTF8')"); - } - else { - $this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $field_def . ' USING "' . $field . '"::' . $field_def); - } - } - else { - // Do not attempt to convert a field that is bytea already. - if (!$is_bytea) { - // Convert to a bytea type by using the SQL replace() function to - // convert any single backslashes in the field content to double - // backslashes ('\' to '\\'). - $this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $field_def . ' USING decode(replace("' . $field . '"' . ", E'\\\\', E'\\\\\\\\'), 'escape');"); - } - } - - if (isset($spec['not null'])) { - if ($spec['not null']) { - $null_action = 'SET NOT NULL'; - } - else { - $null_action = 'DROP NOT NULL'; - } - $this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" ' . $null_action); - } - - if (in_array($spec['pgsql_type'], ['serial', 'bigserial'])) { - // Type "serial" is known to PostgreSQL, but *only* during table creation, - // not when altering. Because of that, the sequence needs to be created - // and initialized by hand. - $seq = $this->connection->makeSequenceName($table, $field_new); - $this->connection->query("CREATE SEQUENCE " . $seq); - // Set sequence to maximal field value to not conflict with existing - // entries. - $this->connection->query("SELECT setval('" . $seq . "', MAX(\"" . $field . '")) FROM {' . $table . "}"); - $this->connection->query('ALTER TABLE {' . $table . '} ALTER ' . $field . ' SET DEFAULT nextval(' . $this->connection->quote($seq) . ')'); - } - - // Rename the column if necessary. - if ($field != $field_new) { - $this->connection->query('ALTER TABLE {' . $table . '} RENAME "' . $field . '" TO "' . $field_new . '"'); - } - - // Add unsigned check if necessary. - if (!empty($spec['unsigned'])) { - $this->connection->query('ALTER TABLE {' . $table . '} ADD CHECK ("' . $field_new . '" >= 0)'); - } - - // Add default if necessary. - if (isset($spec['default'])) { - $this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN "' . $field_new . '" SET DEFAULT ' . $this->escapeDefaultValue($spec['default'])); - } - - // Change description if necessary. - if (!empty($spec['description'])) { - $this->connection->query('COMMENT ON COLUMN {' . $table . '}."' . $field_new . '" IS ' . $this->prepareComment($spec['description'])); - } - - if (isset($new_keys)) { - $this->_createKeys($table, $new_keys); - } - $this->resetTableInformation($table); - } - - protected function _createIndexSql($table, $name, $fields) { - $query = 'CREATE INDEX ' . $this->ensureIdentifiersLength($table, $name, 'idx') . ' ON {' . $table . '} ('; - $query .= $this->_createKeySql($fields) . ')'; - return $query; - } - - protected function _createKeys($table, $new_keys) { - if (isset($new_keys['primary key'])) { - $this->addPrimaryKey($table, $new_keys['primary key']); - } - if (isset($new_keys['unique keys'])) { - foreach ($new_keys['unique keys'] as $name => $fields) { - $this->addUniqueKey($table, $name, $fields); - } - } - if (isset($new_keys['indexes'])) { - foreach ($new_keys['indexes'] as $name => $fields) { - // Even though $new_keys is not a full schema it still has 'indexes' and - // so is a partial schema. Technically addIndex() doesn't do anything - // with it so passing an empty array would work as well. - $this->addIndex($table, $name, $fields, $new_keys); - } - } - } - - /** - * Retrieve a table or column comment. - */ - public function getComment($table, $column = NULL) { - $info = $this->getPrefixInfo($table); - // Don't use {} around pg_class, pg_attribute tables. - if (isset($column)) { - return $this->connection->query('SELECT col_description(oid, attnum) FROM pg_class, pg_attribute WHERE attrelid = oid AND relname = ? AND attname = ?', [$info['table'], $column])->fetchField(); - } - else { - return $this->connection->query('SELECT obj_description(oid, ?) FROM pg_class WHERE relname = ?', ['pg_class', $info['table']])->fetchField(); - } - } - - /** - * Calculates a base-64 encoded, PostgreSQL-safe sha-256 hash per PostgreSQL - * documentation: 4.1. Lexical Structure. - * - * @param $data - * String to be hashed. - * - * @return string - * A base-64 encoded sha-256 hash, with + and / replaced with _ and any = - * padding characters removed. - */ - protected function hashBase64($data) { - $hash = base64_encode(hash('sha256', $data, TRUE)); - // Modify the hash so it's safe to use in PostgreSQL identifiers. - return strtr($hash, ['+' => '_', '/' => '_', '=' => '']); - } - - /** - * Determines whether the PostgreSQL extension is created. - * - * @param string $name - * The name of the extension. - * - * @return bool - * Return TRUE when the extension is created, FALSE otherwise. - * - * @internal - */ - public function extensionExists($name): bool { - return (bool) $this->connection->query('SELECT installed_version FROM pg_available_extensions WHERE name = :name', [ - ':name' => $name, - ])->fetchField(); - } - -} - -/** - * @} End of "addtogroup schemaapi". - */ +class Schema extends PgsqlSchema {} diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Select.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Select.php index 2a9bc4a58726..169aaed84410 100644 --- a/core/lib/Drupal/Core/Database/Driver/pgsql/Select.php +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Select.php @@ -2,159 +2,16 @@ namespace Drupal\Core\Database\Driver\pgsql; -use Drupal\Core\Database\Query\Select as QuerySelect; +use Drupal\pgsql\Driver\Database\pgsql\Select as PgsqlSelect; -/** - * @addtogroup database - * @{ - */ +@trigger_error('\Drupal\Core\Database\Driver\pgsql\Select is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED); /** * PostgreSQL implementation of \Drupal\Core\Database\Query\Select. + * + * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL + * database driver has been moved to the pgsql module. + * + * @see https://www.drupal.org/node/3129492 */ -class Select extends QuerySelect { - - public function orderRandom() { - $alias = $this->addExpression('RANDOM()', 'random_field'); - $this->orderBy($alias); - return $this; - } - - /** - * Overrides SelectQuery::orderBy(). - * - * PostgreSQL adheres strictly to the SQL-92 standard and requires that when - * using DISTINCT or GROUP BY conditions, fields and expressions that are - * ordered on also need to be selected. This is a best effort implementation - * to handle the cases that can be automated by adding the field if it is not - * yet selected. - * - * @code - * $query = \Drupal::database()->select('example', 'e'); - * $query->join('example_revision', 'er', '[e].[vid] = [er].[vid]'); - * $query - * ->distinct() - * ->fields('e') - * ->orderBy('timestamp'); - * @endcode - * - * In this query, it is not possible (without relying on the schema) to know - * whether timestamp belongs to example_revision and needs to be added or - * belongs to node and is already selected. Queries like this will need to be - * corrected in the original query by adding an explicit call to - * SelectQuery::addField() or SelectQuery::fields(). - * - * Since this has a small performance impact, both by the additional - * processing in this function and in the database that needs to return the - * additional fields, this is done as an override instead of implementing it - * directly in SelectQuery::orderBy(). - */ - public function orderBy($field, $direction = 'ASC') { - // Only allow ASC and DESC, default to ASC. - // Emulate MySQL default behavior to sort NULL values first for ascending, - // and last for descending. - // @see http://www.postgresql.org/docs/9.3/static/queries-order.html - $direction = strtoupper($direction) == 'DESC' ? 'DESC NULLS LAST' : 'ASC NULLS FIRST'; - $this->order[$field] = $direction; - - if ($this->hasTag('entity_query')) { - return $this; - } - - // If there is a table alias specified, split it up. - if (strpos($field, '.') !== FALSE) { - [$table, $table_field] = explode('.', $field); - } - // Figure out if the field has already been added. - foreach ($this->fields as $existing_field) { - if (!empty($table)) { - // If table alias is given, check if field and table exists. - if ($existing_field['table'] == $table && $existing_field['field'] == $table_field) { - return $this; - } - } - else { - // If there is no table, simply check if the field exists as a field or - // an aliased field. - if ($existing_field['alias'] == $field) { - return $this; - } - } - } - - // Also check expression aliases. - foreach ($this->expressions as $expression) { - if ($expression['alias'] == $this->connection->escapeAlias($field)) { - return $this; - } - } - - // If a table loads all fields, it can not be added again. It would - // result in an ambiguous alias error because that field would be loaded - // twice: Once through table_alias.* and once directly. If the field - // actually belongs to a different table, it must be added manually. - foreach ($this->tables as $table) { - if (!empty($table['all_fields'])) { - return $this; - } - } - - // If $field contains characters which are not allowed in a field name - // it is considered an expression, these can't be handled automatically - // either. - if ($this->connection->escapeField($field) != $field) { - return $this; - } - - // This is a case that can be handled automatically, add the field. - $this->addField(NULL, $field); - return $this; - } - - /** - * {@inheritdoc} - */ - public function addExpression($expression, $alias = NULL, $arguments = []) { - if (empty($alias)) { - $alias = 'expression'; - } - - // This implements counting in the same manner as the parent method. - $alias_candidate = $alias; - $count = 2; - while (!empty($this->expressions[$alias_candidate])) { - $alias_candidate = $alias . '_' . $count++; - } - $alias = $alias_candidate; - - $this->expressions[$alias] = [ - 'expression' => $expression, - 'alias' => $this->connection->escapeAlias($alias_candidate), - 'arguments' => $arguments, - ]; - - return $alias; - } - - /** - * {@inheritdoc} - */ - public function execute() { - $this->connection->addSavepoint(); - try { - $result = parent::execute(); - } - catch (\Exception $e) { - $this->connection->rollbackSavepoint(); - throw $e; - } - $this->connection->releaseSavepoint(); - - return $result; - } - -} - -/** - * @} End of "addtogroup database". - */ +class Select extends PgsqlSelect {} diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Truncate.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Truncate.php index 0d9947b96f28..01ed66c34455 100644 --- a/core/lib/Drupal/Core/Database/Driver/pgsql/Truncate.php +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Truncate.php @@ -2,28 +2,16 @@ namespace Drupal\Core\Database\Driver\pgsql; -use Drupal\Core\Database\Query\Truncate as QueryTruncate; +use Drupal\pgsql\Driver\Database\pgsql\Truncate as PgsqlTruncate; + +@trigger_error('\Drupal\Core\Database\Driver\pgsql\Truncate is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED); /** * PostgreSQL implementation of \Drupal\Core\Database\Query\Truncate. + * + * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL + * database driver has been moved to the pgsql module. + * + * @see https://www.drupal.org/node/3129492 */ -class Truncate extends QueryTruncate { - - /** - * {@inheritdoc} - */ - public function execute() { - $this->connection->addSavepoint(); - try { - $result = parent::execute(); - } - catch (\Exception $e) { - $this->connection->rollbackSavepoint(); - throw $e; - } - $this->connection->releaseSavepoint(); - - return $result; - } - -} +class Truncate extends PgsqlTruncate {} diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Update.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Update.php index 258379fff054..0e31b0dd2001 100644 --- a/core/lib/Drupal/Core/Database/Driver/pgsql/Update.php +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Update.php @@ -2,82 +2,16 @@ namespace Drupal\Core\Database\Driver\pgsql; -use Drupal\Core\Database\Query\Update as QueryUpdate; -use Drupal\Core\Database\Query\SelectInterface; +use Drupal\pgsql\Driver\Database\pgsql\Update as PgsqlUpdate; + +@trigger_error('\Drupal\Core\Database\Driver\pgsql\Update is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED); /** * PostgreSQL implementation of \Drupal\Core\Database\Query\Update. + * + * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL + * database driver has been moved to the pgsql module. + * + * @see https://www.drupal.org/node/3129492 */ -class Update extends QueryUpdate { - - public function execute() { - $max_placeholder = 0; - $blobs = []; - $blob_count = 0; - - // Because we filter $fields the same way here and in __toString(), the - // placeholders will all match up properly. - $stmt = $this->connection->prepareStatement((string) $this, $this->queryOptions, TRUE); - - // Fetch the list of blobs and sequences used on that table. - $table_information = $this->connection->schema()->queryTableInformation($this->table); - - // Expressions take priority over literal fields, so we process those first - // and remove any literal fields that conflict. - $fields = $this->fields; - foreach ($this->expressionFields as $field => $data) { - if (!empty($data['arguments'])) { - foreach ($data['arguments'] as $placeholder => $argument) { - // We assume that an expression will never happen on a BLOB field, - // which is a fairly safe assumption to make since in most cases - // it would be an invalid query anyway. - $stmt->getClientStatement()->bindParam($placeholder, $data['arguments'][$placeholder]); - } - } - if ($data['expression'] instanceof SelectInterface) { - $data['expression']->compile($this->connection, $this); - $select_query_arguments = $data['expression']->arguments(); - foreach ($select_query_arguments as $placeholder => $argument) { - $stmt->getClientStatement()->bindParam($placeholder, $select_query_arguments[$placeholder]); - } - } - unset($fields[$field]); - } - - foreach ($fields as $field => $value) { - $placeholder = ':db_update_placeholder_' . ($max_placeholder++); - - if (isset($table_information->blob_fields[$field]) && $value !== NULL) { - $blobs[$blob_count] = fopen('php://memory', 'a'); - fwrite($blobs[$blob_count], $value); - rewind($blobs[$blob_count]); - $stmt->getClientStatement()->bindParam($placeholder, $blobs[$blob_count], \PDO::PARAM_LOB); - ++$blob_count; - } - else { - $stmt->getClientStatement()->bindParam($placeholder, $fields[$field]); - } - } - - if (count($this->condition)) { - $this->condition->compile($this->connection, $this); - - $arguments = $this->condition->arguments(); - foreach ($arguments as $placeholder => $value) { - $stmt->getClientStatement()->bindParam($placeholder, $arguments[$placeholder]); - } - } - - $this->connection->addSavepoint(); - try { - $stmt->execute(NULL, $this->queryOptions); - $this->connection->releaseSavepoint(); - return $stmt->rowCount(); - } - catch (\Exception $e) { - $this->connection->rollbackSavepoint(); - $this->connection->exceptionHandler()->handleExecutionException($e, $stmt, [], $this->queryOptions); - } - } - -} +class Update extends PgsqlUpdate {} diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Upsert.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Upsert.php index 934a3d6625d7..e1eb3d95c54d 100644 --- a/core/lib/Drupal/Core/Database/Driver/pgsql/Upsert.php +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Upsert.php @@ -2,125 +2,16 @@ namespace Drupal\Core\Database\Driver\pgsql; -use Drupal\Core\Database\Query\Upsert as QueryUpsert; +use Drupal\pgsql\Driver\Database\pgsql\Upsert as PgsqlUpsert; -// cSpell:ignore nextval setval +@trigger_error('\Drupal\Core\Database\Driver\pgsql\Upsert is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED); /** * PostgreSQL implementation of \Drupal\Core\Database\Query\Upsert. + * + * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL + * database driver has been moved to the pgsql module. + * + * @see https://www.drupal.org/node/3129492 */ -class Upsert extends QueryUpsert { - - /** - * {@inheritdoc} - */ - public function execute() { - if (!$this->preExecute()) { - return NULL; - } - - $stmt = $this->connection->prepareStatement((string) $this, $this->queryOptions, TRUE); - - // Fetch the list of blobs and sequences used on that table. - $table_information = $this->connection->schema()->queryTableInformation($this->table); - - $max_placeholder = 0; - $blobs = []; - $blob_count = 0; - foreach ($this->insertValues as $insert_values) { - foreach ($this->insertFields as $idx => $field) { - if (isset($table_information->blob_fields[$field]) && $insert_values[$idx] !== NULL) { - $blobs[$blob_count] = fopen('php://memory', 'a'); - fwrite($blobs[$blob_count], $insert_values[$idx]); - rewind($blobs[$blob_count]); - - $stmt->getClientStatement()->bindParam(':db_insert_placeholder_' . $max_placeholder++, $blobs[$blob_count], \PDO::PARAM_LOB); - - // Pre-increment is faster in PHP than increment. - ++$blob_count; - } - else { - $stmt->getClientStatement()->bindParam(':db_insert_placeholder_' . $max_placeholder++, $insert_values[$idx]); - } - } - // Check if values for a serial field has been passed. - if (!empty($table_information->serial_fields)) { - foreach ($table_information->serial_fields as $index => $serial_field) { - $serial_key = array_search($serial_field, $this->insertFields); - if ($serial_key !== FALSE) { - $serial_value = $insert_values[$serial_key]; - - // Sequences must be greater than or equal to 1. - if ($serial_value === NULL || !$serial_value) { - $serial_value = 1; - } - // Set the sequence to the bigger value of either the passed - // value or the max value of the column. It can happen that another - // thread calls nextval() which could lead to a serial number being - // used twice. However, trying to insert a value into a serial - // column should only be done in very rare cases and is not thread - // safe by definition. - $this->connection->query("SELECT setval('" . $table_information->sequences[$index] . "', GREATEST(MAX(" . $serial_field . "), :serial_value)) FROM {" . $this->table . "}", [':serial_value' => (int) $serial_value]); - } - } - } - } - - $options = $this->queryOptions; - if (!empty($table_information->sequences)) { - $options['sequence_name'] = $table_information->sequences[0]; - } - - // Re-initialize the values array so that we can re-use this query. - $this->insertValues = []; - - // 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 - // fails. This is important for tables that are created on demand. For - // example, \Drupal\Core\Cache\DatabaseBackend. - $this->connection->addSavepoint(); - try { - $stmt->execute(NULL, $options); - $this->connection->releaseSavepoint(); - return $stmt->rowCount(); - } - catch (\Exception $e) { - $this->connection->rollbackSavepoint(); - $this->connection->exceptionHandler()->handleExecutionException($e, $stmt, [], $options); - } - } - - /** - * {@inheritdoc} - */ - public function __toString() { - // Create a sanitized comment string to prepend to the query. - $comments = $this->connection->makeComment($this->comments); - - // Default fields are always placed first for consistency. - $insert_fields = array_merge($this->defaultFields, $this->insertFields); - $insert_fields = array_map(function ($field) { - return $this->connection->escapeField($field); - }, $insert_fields); - - $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES '; - - $values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields); - $query .= implode(', ', $values); - - // Updating the unique / primary key is not necessary. - unset($insert_fields[$this->key]); - - $update = []; - foreach ($insert_fields as $field) { - // The "excluded." prefix causes the field to refer to the value for field - // that would have been inserted had there been no conflict. - $update[] = "$field = EXCLUDED.$field"; - } - - $query .= ' ON CONFLICT (' . $this->connection->escapeField($this->key) . ') DO UPDATE SET ' . implode(', ', $update); - - return $query; - } - -} +class Upsert extends PgsqlUpsert {} diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php index 210b2a64088d..c4b130d1f045 100644 --- a/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php @@ -2,527 +2,16 @@ namespace Drupal\Core\Database\Driver\sqlite; -use Drupal\Core\Database\DatabaseNotFoundException; -use Drupal\Core\Database\Connection as DatabaseConnection; -use Drupal\Core\Database\StatementInterface; +use Drupal\sqlite\Driver\Database\sqlite\Connection as SqliteConnection; + +@trigger_error('\Drupal\Core\Database\Driver\sqlite\Connection is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED); /** * SQLite implementation of \Drupal\Core\Database\Connection. + * + * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite + * database driver has been moved to the sqlite module. + * + * @see https://www.drupal.org/node/3129492 */ -class Connection extends DatabaseConnection { - - /** - * Error code for "Unable to open database file" error. - */ - const DATABASE_NOT_FOUND = 14; - - /** - * {@inheritdoc} - */ - protected $statementClass = NULL; - - /** - * {@inheritdoc} - */ - protected $statementWrapperClass = NULL; - - /** - * Whether or not the active transaction (if any) will be rolled back. - * - * @var bool - */ - protected $willRollback; - - /** - * A map of condition operators to SQLite operators. - * - * We don't want to override any of the defaults. - */ - protected static $sqliteConditionOperatorMap = [ - 'LIKE' => ['postfix' => " ESCAPE '\\'"], - 'NOT LIKE' => ['postfix' => " ESCAPE '\\'"], - 'LIKE BINARY' => ['postfix' => " ESCAPE '\\'", 'operator' => 'GLOB'], - 'NOT LIKE BINARY' => ['postfix' => " ESCAPE '\\'", 'operator' => 'NOT GLOB'], - ]; - - /** - * All databases attached to the current database. - * - * This is used to allow prefixes to be safely handled without locking the - * table. - * - * @var array - */ - protected $attachedDatabases = []; - - /** - * Whether or not a table has been dropped this request. - * - * The destructor will only try to get rid of unnecessary databases if there - * is potential of them being empty. - * - * This variable is set to public because Schema needs to - * access it. However, it should not be manually set. - * - * @var bool - */ - public $tableDropped = FALSE; - - /** - * {@inheritdoc} - */ - protected $transactionalDDLSupport = TRUE; - - /** - * {@inheritdoc} - */ - protected $identifierQuotes = ['"', '"']; - - /** - * Constructs a \Drupal\Core\Database\Driver\sqlite\Connection object. - */ - public function __construct(\PDO $connection, array $connection_options) { - parent::__construct($connection, $connection_options); - - // Attach one database for each registered prefix. - $prefixes = $this->prefixes; - foreach ($prefixes as &$prefix) { - // Empty prefix means query the main database -- no need to attach - // anything. - if ($prefix !== '') { - $this->attachDatabase($prefix); - // Add a ., so queries become prefix.table, which is proper syntax for - // querying an attached database. - $prefix .= '.'; - } - } - - // Regenerate the prefixes replacement table. - $this->setPrefix($prefixes); - } - - /** - * {@inheritdoc} - */ - public static function open(array &$connection_options = []) { - // Allow PDO options to be overridden. - $connection_options += [ - 'pdo' => [], - ]; - $connection_options['pdo'] += [ - \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, - // Convert numeric values to strings when fetching. - \PDO::ATTR_STRINGIFY_FETCHES => TRUE, - ]; - - try { - $pdo = new \PDO('sqlite:' . $connection_options['database'], '', '', $connection_options['pdo']); - } - catch (\PDOException $e) { - if ($e->getCode() == static::DATABASE_NOT_FOUND) { - throw new DatabaseNotFoundException($e->getMessage(), $e->getCode(), $e); - } - // SQLite doesn't have a distinct error code for access denied, so don't - // deal with that case. - throw $e; - } - - // Create functions needed by SQLite. - $pdo->sqliteCreateFunction('if', [__CLASS__, 'sqlFunctionIf']); - $pdo->sqliteCreateFunction('greatest', [__CLASS__, 'sqlFunctionGreatest']); - $pdo->sqliteCreateFunction('least', [__CLASS__, 'sqlFunctionLeast']); - $pdo->sqliteCreateFunction('pow', 'pow', 2); - $pdo->sqliteCreateFunction('exp', 'exp', 1); - $pdo->sqliteCreateFunction('length', 'strlen', 1); - $pdo->sqliteCreateFunction('md5', 'md5', 1); - $pdo->sqliteCreateFunction('concat', [__CLASS__, 'sqlFunctionConcat']); - $pdo->sqliteCreateFunction('concat_ws', [__CLASS__, 'sqlFunctionConcatWs']); - $pdo->sqliteCreateFunction('substring', [__CLASS__, 'sqlFunctionSubstring'], 3); - $pdo->sqliteCreateFunction('substring_index', [__CLASS__, 'sqlFunctionSubstringIndex'], 3); - $pdo->sqliteCreateFunction('rand', [__CLASS__, 'sqlFunctionRand']); - $pdo->sqliteCreateFunction('regexp', [__CLASS__, 'sqlFunctionRegexp']); - - // SQLite does not support the LIKE BINARY operator, so we overload the - // non-standard GLOB operator for case-sensitive matching. Another option - // would have been to override another non-standard operator, MATCH, but - // that does not support the NOT keyword prefix. - $pdo->sqliteCreateFunction('glob', [__CLASS__, 'sqlFunctionLikeBinary']); - - // Create a user-space case-insensitive collation with UTF-8 support. - $pdo->sqliteCreateCollation('NOCASE_UTF8', ['Drupal\Component\Utility\Unicode', 'strcasecmp']); - - // Set SQLite init_commands if not already defined. Enable the Write-Ahead - // Logging (WAL) for SQLite. See https://www.drupal.org/node/2348137 and - // https://www.sqlite.org/wal.html. - $connection_options += [ - 'init_commands' => [], - ]; - $connection_options['init_commands'] += [ - 'wal' => "PRAGMA journal_mode=WAL", - ]; - - // Execute sqlite init_commands. - if (isset($connection_options['init_commands'])) { - $pdo->exec(implode('; ', $connection_options['init_commands'])); - } - - return $pdo; - } - - /** - * Destructor for the SQLite connection. - * - * We prune empty databases on destruct, but only if tables have been - * dropped. This is especially needed when running the test suite, which - * creates and destroy databases several times in a row. - */ - public function __destruct() { - if ($this->tableDropped && !empty($this->attachedDatabases)) { - foreach ($this->attachedDatabases as $prefix) { - // Check if the database is now empty, ignore the internal SQLite tables. - try { - $count = $this->query('SELECT COUNT(*) FROM ' . $prefix . '.sqlite_master WHERE type = :type AND name NOT LIKE :pattern', [':type' => 'table', ':pattern' => 'sqlite_%'])->fetchField(); - - // We can prune the database file if it doesn't have any tables. - if ($count == 0 && $this->connectionOptions['database'] != ':memory:' && file_exists($this->connectionOptions['database'] . '-' . $prefix)) { - // Detach the database. - $this->query('DETACH DATABASE :schema', [':schema' => $prefix]); - // Destroy the database file. - unlink($this->connectionOptions['database'] . '-' . $prefix); - } - } - catch (\Exception $e) { - // Ignore the exception and continue. There is nothing we can do here - // to report the error or fail safe. - } - } - } - parent::__destruct(); - } - - /** - * {@inheritdoc} - */ - public function attachDatabase(string $database): void { - // Only attach the database once. - if (!isset($this->attachedDatabases[$database])) { - // In memory database use ':memory:' as database name. According to - // http://www.sqlite.org/inmemorydb.html it will open a unique database so - // attaching it twice is not a problem. - $database_file = $this->connectionOptions['database'] !== ':memory:' ? $this->connectionOptions['database'] . '-' . $database : $this->connectionOptions['database']; - $this->query('ATTACH DATABASE :database_file AS :database', [':database_file' => $database_file, ':database' => $database]); - $this->attachedDatabases[$database] = $database; - } - } - - /** - * Gets all the attached databases. - * - * @return array - * An array of attached database names. - * - * @see \Drupal\Core\Database\Driver\sqlite\Connection::__construct() - */ - public function getAttachedDatabases() { - return $this->attachedDatabases; - } - - /** - * SQLite compatibility implementation for the IF() SQL function. - */ - public static function sqlFunctionIf($condition, $expr1, $expr2 = NULL) { - return $condition ? $expr1 : $expr2; - } - - /** - * SQLite compatibility implementation for the GREATEST() SQL function. - */ - public static function sqlFunctionGreatest() { - $args = func_get_args(); - foreach ($args as $v) { - if (!isset($v)) { - unset($args); - } - } - if (count($args)) { - return max($args); - } - else { - return NULL; - } - } - - /** - * SQLite compatibility implementation for the LEAST() SQL function. - */ - public static function sqlFunctionLeast() { - // Remove all NULL, FALSE and empty strings values but leaves 0 (zero) values. - $values = array_filter(func_get_args(), 'strlen'); - - return count($values) < 1 ? NULL : min($values); - } - - /** - * SQLite compatibility implementation for the CONCAT() SQL function. - */ - public static function sqlFunctionConcat() { - $args = func_get_args(); - return implode('', $args); - } - - /** - * SQLite compatibility implementation for the CONCAT_WS() SQL function. - * - * @see http://dev.mysql.com/doc/refman/5.6/en/string-functions.html#function_concat-ws - */ - public static function sqlFunctionConcatWs() { - $args = func_get_args(); - $separator = array_shift($args); - // If the separator is NULL, the result is NULL. - if ($separator === FALSE || is_null($separator)) { - return NULL; - } - // Skip any NULL values after the separator argument. - $args = array_filter($args, function ($value) { - return !is_null($value); - }); - return implode($separator, $args); - } - - /** - * SQLite compatibility implementation for the SUBSTRING() SQL function. - */ - public static function sqlFunctionSubstring($string, $from, $length) { - return substr($string, $from - 1, $length); - } - - /** - * SQLite compatibility implementation for the SUBSTRING_INDEX() SQL function. - */ - public static function sqlFunctionSubstringIndex($string, $delimiter, $count) { - // If string is empty, simply return an empty string. - if (empty($string)) { - return ''; - } - $end = 0; - for ($i = 0; $i < $count; $i++) { - $end = strpos($string, $delimiter, $end + 1); - if ($end === FALSE) { - $end = strlen($string); - } - } - return substr($string, 0, $end); - } - - /** - * SQLite compatibility implementation for the RAND() SQL function. - */ - public static function sqlFunctionRand($seed = NULL) { - if (isset($seed)) { - mt_srand($seed); - } - return mt_rand() / mt_getrandmax(); - } - - /** - * SQLite compatibility implementation for the REGEXP SQL operator. - * - * The REGEXP operator is natively known, but not implemented by default. - * - * @see http://www.sqlite.org/lang_expr.html#regexp - */ - public static function sqlFunctionRegexp($pattern, $subject) { - // preg_quote() cannot be used here, since $pattern may contain reserved - // regular expression characters already (such as ^, $, etc). Therefore, - // use a rare character as PCRE delimiter. - $pattern = '#' . addcslashes($pattern, '#') . '#i'; - return preg_match($pattern, $subject); - } - - /** - * SQLite compatibility implementation for the LIKE BINARY SQL operator. - * - * SQLite supports case-sensitive LIKE operations through the - * 'case_sensitive_like' PRAGMA statement, but only for ASCII characters, so - * we have to provide our own implementation with UTF-8 support. - * - * @see https://sqlite.org/pragma.html#pragma_case_sensitive_like - * @see https://sqlite.org/lang_expr.html#like - */ - public static function sqlFunctionLikeBinary($pattern, $subject) { - // Replace the SQL LIKE wildcard meta-characters with the equivalent regular - // expression meta-characters and escape the delimiter that will be used for - // matching. - $pattern = str_replace(['%', '_'], ['.*?', '.'], preg_quote($pattern, '/')); - return preg_match('/^' . $pattern . '$/', $subject); - } - - /** - * {@inheritdoc} - */ - public function prepare($statement, array $driver_options = []) { - @trigger_error('Connection::prepare() is deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Database drivers should instantiate \PDOStatement objects by calling \PDO::prepare in their Connection::prepareStatement method instead. \PDO::prepare should not be called outside of driver code. See https://www.drupal.org/node/3137786', E_USER_DEPRECATED); - return new Statement($this->connection, $this, $statement, $driver_options); - } - - /** - * {@inheritdoc} - */ - protected function handleQueryException(\PDOException $e, $query, array $args = [], $options = []) { - // The database schema might be changed by another process in between the - // time that the statement was prepared and the time the statement was run - // (e.g. usually happens when running tests). In this case, we need to - // re-run the query. - // @see http://www.sqlite.org/faq.html#q15 - // @see http://www.sqlite.org/rescode.html#schema - if (!empty($e->errorInfo[1]) && $e->errorInfo[1] === 17) { - @trigger_error('Connection::handleQueryException() is deprecated in drupal:9.2.0 and is removed in drupal:10.0.0. Get a handler through $this->exceptionHandler() instead, and use one of its methods. See https://www.drupal.org/node/3187222', E_USER_DEPRECATED); - return $this->query($query, $args, $options); - } - - parent::handleQueryException($e, $query, $args, $options); - } - - public function queryRange($query, $from, $count, array $args = [], array $options = []) { - return $this->query($query . ' LIMIT ' . (int) $from . ', ' . (int) $count, $args, $options); - } - - /** - * {@inheritdoc} - */ - public function queryTemporary($query, array $args = [], array $options = []) { - @trigger_error('Connection::queryTemporary() is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. There is no replacement. See https://www.drupal.org/node/3211781', E_USER_DEPRECATED); - // Generate a new temporary table name and protect it from prefixing. - // SQLite requires that temporary tables to be non-qualified. - $tablename = $this->generateTemporaryTableName(); - $prefixes = $this->prefixes; - $prefixes[$tablename] = ''; - $this->setPrefix($prefixes); - - $this->query('CREATE TEMPORARY TABLE ' . $tablename . ' AS ' . $query, $args, $options); - return $tablename; - } - - public function driver() { - return 'sqlite'; - } - - public function databaseType() { - return 'sqlite'; - } - - /** - * Overrides \Drupal\Core\Database\Connection::createDatabase(). - * - * @param string $database - * The name of the database to create. - * - * @throws \Drupal\Core\Database\DatabaseNotFoundException - */ - public function createDatabase($database) { - // Verify the database is writable. - $db_directory = new \SplFileInfo(dirname($database)); - if (!$db_directory->isDir() && !\Drupal::service('file_system')->mkdir($db_directory->getPathName(), 0755, TRUE)) { - throw new DatabaseNotFoundException('Unable to create database directory ' . $db_directory->getPathName()); - } - } - - public function mapConditionOperator($operator) { - return static::$sqliteConditionOperatorMap[$operator] ?? NULL; - } - - /** - * {@inheritdoc} - */ - public function prepareStatement(string $query, array $options, bool $allow_row_count = FALSE): StatementInterface { - try { - $query = $this->preprocessStatement($query, $options); - $statement = new Statement($this->connection, $this, $query, $options['pdo'] ?? [], $allow_row_count); - } - catch (\Exception $e) { - $this->exceptionHandler()->handleStatementException($e, $query, $options); - } - return $statement; - } - - public function nextId($existing_id = 0) { - $this->startTransaction(); - // We can safely use literal queries here instead of the slower query - // builder because if a given database breaks here then it can simply - // override nextId. However, this is unlikely as we deal with short strings - // and integers and no known databases require special handling for those - // simple cases. If another transaction wants to write the same row, it will - // wait until this transaction commits. - $stmt = $this->prepareStatement('UPDATE {sequences} SET [value] = GREATEST([value], :existing_id) + 1', [], TRUE); - $args = [':existing_id' => $existing_id]; - try { - $stmt->execute($args); - } - catch (\Exception $e) { - $this->exceptionHandler()->handleExecutionException($e, $stmt, $args, []); - } - if ($stmt->rowCount() === 0) { - $this->query('INSERT INTO {sequences} ([value]) VALUES (:existing_id + 1)', $args); - } - // The transaction gets committed when the transaction object gets destroyed - // because it gets out of scope. - return $this->query('SELECT [value] FROM {sequences}')->fetchField(); - } - - /** - * {@inheritdoc} - */ - public function getFullQualifiedTableName($table) { - $prefix = $this->tablePrefix($table); - - // Don't include the SQLite database file name as part of the table name. - return $prefix . $table; - } - - /** - * {@inheritdoc} - */ - public static function createConnectionOptionsFromUrl($url, $root) { - $database = parent::createConnectionOptionsFromUrl($url, $root); - - // A SQLite database path with two leading slashes indicates a system path. - // Otherwise the path is relative to the Drupal root. - $url_components = parse_url($url); - if ($url_components['path'][0] === '/') { - $url_components['path'] = substr($url_components['path'], 1); - } - if ($url_components['path'][0] === '/' || $url_components['path'] === ':memory:') { - $database['database'] = $url_components['path']; - } - else { - $database['database'] = $root . '/' . $url_components['path']; - } - - // User credentials and system port are irrelevant for SQLite. - unset( - $database['username'], - $database['password'], - $database['port'] - ); - - return $database; - } - - /** - * {@inheritdoc} - */ - public static function createUrlFromConnectionOptions(array $connection_options) { - if (!isset($connection_options['driver'], $connection_options['database'])) { - throw new \InvalidArgumentException("As a minimum, the connection options array must contain at least the 'driver' and 'database' keys"); - } - - $db_url = 'sqlite://localhost/' . $connection_options['database']; - - if (isset($connection_options['prefix']) && $connection_options['prefix'] !== '') { - $db_url .= '#' . $connection_options['prefix']; - } - - return $db_url; - } - -} +class Connection extends SqliteConnection {} diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Insert.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Insert.php index 4273dd6536fe..1685c8ba02bb 100644 --- a/core/lib/Drupal/Core/Database/Driver/sqlite/Insert.php +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Insert.php @@ -2,51 +2,16 @@ namespace Drupal\Core\Database\Driver\sqlite; -use Drupal\Core\Database\Query\Insert as QueryInsert; +use Drupal\sqlite\Driver\Database\sqlite\Insert as SqliteInsert; + +@trigger_error('\Drupal\Core\Database\Driver\sqlite\Insert is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED); /** * SQLite implementation of \Drupal\Core\Database\Query\Insert. * - * We ignore all the default fields and use the clever SQLite syntax: - * INSERT INTO table DEFAULT VALUES - * for degenerated "default only" queries. + * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite + * database driver has been moved to the sqlite module. + * + * @see https://www.drupal.org/node/3129492 */ -class Insert extends QueryInsert { - - public function execute() { - if (!$this->preExecute()) { - return NULL; - } - if (count($this->insertFields) || !empty($this->fromQuery)) { - return parent::execute(); - } - else { - return $this->connection->query('INSERT INTO {' . $this->table . '} DEFAULT VALUES', [], $this->queryOptions); - } - } - - public function __toString() { - // Create a sanitized comment string to prepend to the query. - $comments = $this->connection->makeComment($this->comments); - - // Produce as many generic placeholders as necessary. - $placeholders = []; - if (!empty($this->insertFields)) { - $placeholders = array_fill(0, count($this->insertFields), '?'); - } - - $insert_fields = array_map(function ($field) { - return $this->connection->escapeField($field); - }, $this->insertFields); - - // 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)) { - $insert_fields_string = $insert_fields ? ' (' . implode(', ', $insert_fields) . ') ' : ' '; - return $comments . 'INSERT INTO {' . $this->table . '}' . $insert_fields_string . $this->fromQuery; - } - - return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES (' . implode(', ', $placeholders) . ')'; - } - -} +class Insert extends SqliteInsert {} diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php index aecaa6925ba0..22929b619430 100644 --- a/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php @@ -2,114 +2,16 @@ namespace Drupal\Core\Database\Driver\sqlite\Install; -use Drupal\Core\Database\Database; -use Drupal\Core\Database\Driver\sqlite\Connection; -use Drupal\Core\Database\DatabaseNotFoundException; -use Drupal\Core\Database\Install\Tasks as InstallTasks; +use Drupal\sqlite\Driver\Database\sqlite\Install\Tasks as SqliteTasks; + +@trigger_error('\Drupal\Core\Database\Driver\sqlite\Install\Tasks is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED); /** * Specifies installation tasks for SQLite databases. + * + * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite + * database driver has been moved to the sqlite module. + * + * @see https://www.drupal.org/node/3129492 */ -class Tasks extends InstallTasks { - - /** - * Minimum required SQLite version. - * - * Use to build sqlite library with json1 option for JSON datatype support. - * @see https://www.sqlite.org/json1.html - */ - const SQLITE_MINIMUM_VERSION = '3.26'; - - /** - * {@inheritdoc} - */ - protected $pdoDriver = 'sqlite'; - - /** - * {@inheritdoc} - */ - public function name() { - return t('SQLite'); - } - - /** - * {@inheritdoc} - */ - public function minimumVersion() { - return static::SQLITE_MINIMUM_VERSION; - } - - /** - * {@inheritdoc} - */ - public function getFormOptions(array $database) { - $form = parent::getFormOptions($database); - - // Remove the options that only apply to client/server style databases. - unset($form['username'], $form['password'], $form['advanced_options']['host'], $form['advanced_options']['port']); - - // Make the text more accurate for SQLite. - $form['database']['#title'] = t('Database file'); - $form['database']['#description'] = t('The absolute path to the file where @drupal data will be stored. This must be writable by the web server and should exist outside of the web root.', ['@drupal' => drupal_install_profile_distribution_name()]); - $default_database = \Drupal::getContainer()->getParameter('site.path') . '/files/.ht.sqlite'; - $form['database']['#default_value'] = empty($database['database']) ? $default_database : $database['database']; - return $form; - } - - /** - * {@inheritdoc} - */ - protected function connect() { - try { - // This doesn't actually test the connection. - Database::setActiveConnection(); - // Now actually do a check. - Database::getConnection(); - $this->pass('Drupal can CONNECT to the database ok.'); - } - catch (\Exception $e) { - // Attempt to create the database if it is not found. - if ($e->getCode() == Connection::DATABASE_NOT_FOUND) { - // Remove the database string from connection info. - $connection_info = Database::getConnectionInfo(); - $database = $connection_info['default']['database']; - - // We cannot use \Drupal::service('file_system')->getTempDirectory() - // here because we haven't yet successfully connected to the database. - $connection_info['default']['database'] = \Drupal::service('file_system')->tempnam(sys_get_temp_dir(), 'sqlite'); - - // In order to change the Database::$databaseInfo array, need to remove - // the active connection, then re-add it with the new info. - Database::removeConnection('default'); - Database::addConnectionInfo('default', 'default', $connection_info['default']); - - try { - Database::getConnection()->createDatabase($database); - Database::closeConnection(); - - // Now, restore the database config. - Database::removeConnection('default'); - $connection_info['default']['database'] = $database; - Database::addConnectionInfo('default', 'default', $connection_info['default']); - - // Check the database connection. - Database::getConnection(); - $this->pass('Drupal can CONNECT to the database ok.'); - } - catch (DatabaseNotFoundException $e) { - // Still no dice; probably a permission issue. Raise the error to the - // installer. - $this->fail(t('Failed to open or create database file %database. The database engine reports the following message when attempting to create the database: %error.', ['%database' => $database, '%error' => $e->getMessage()])); - } - } - else { - // Database connection failed for some other reason than a non-existent - // database. - $this->fail(t('Failed to connect to database. The database engine reports the following message: %error.<ul><li>Does the database file exist?</li><li>Does web server have permission to write to the database file?</li>Does the web server have permission to write to the directory the database file should be created in?</li></ul>', ['%error' => $e->getMessage()])); - return FALSE; - } - } - return TRUE; - } - -} +class Tasks extends SqliteTasks {} diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php index 1895522e810a..b880d2c45bdf 100644 --- a/core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php @@ -2,836 +2,16 @@ namespace Drupal\Core\Database\Driver\sqlite; -use Drupal\Core\Database\SchemaObjectExistsException; -use Drupal\Core\Database\SchemaObjectDoesNotExistException; -use Drupal\Core\Database\Schema as DatabaseSchema; +use Drupal\sqlite\Driver\Database\sqlite\Schema as SqliteSchema; -/** - * @ingroup schemaapi - * @{ - */ +@trigger_error('\Drupal\Core\Database\Driver\sqlite\Schema is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED); /** * SQLite implementation of \Drupal\Core\Database\Schema. + * + * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite + * database driver has been moved to the sqlite module. + * + * @see https://www.drupal.org/node/3129492 */ -class Schema extends DatabaseSchema { - - /** - * Override DatabaseSchema::$defaultSchema. - * - * @var string - */ - protected $defaultSchema = 'main'; - - /** - * {@inheritdoc} - */ - public function tableExists($table) { - $info = $this->getPrefixInfo($table); - - // Don't use {} around sqlite_master table. - return (bool) $this->connection->query('SELECT 1 FROM ' . $info['schema'] . '.sqlite_master WHERE type = :type AND name = :name', [':type' => 'table', ':name' => $info['table']])->fetchField(); - } - - /** - * {@inheritdoc} - */ - public function fieldExists($table, $column) { - $schema = $this->introspectSchema($table); - return !empty($schema['fields'][$column]); - } - - /** - * Generate SQL to create a new table from a Drupal schema definition. - * - * @param $name - * The name of the table to create. - * @param $table - * A Schema API table definition array. - * - * @return - * An array of SQL statements to create the table. - */ - public function createTableSql($name, $table) { - if (!empty($table['primary key']) && is_array($table['primary key'])) { - $this->ensureNotNullPrimaryKey($table['primary key'], $table['fields']); - } - - $sql = []; - $sql[] = "CREATE TABLE {" . $name . "} (\n" . $this->createColumnsSql($name, $table) . "\n)\n"; - return array_merge($sql, $this->createIndexSql($name, $table)); - } - - /** - * Build the SQL expression for indexes. - */ - protected function createIndexSql($tablename, $schema) { - $sql = []; - $info = $this->getPrefixInfo($tablename); - if (!empty($schema['unique keys'])) { - foreach ($schema['unique keys'] as $key => $fields) { - $sql[] = 'CREATE UNIQUE INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $key . ' ON ' . $info['table'] . ' (' . $this->createKeySql($fields) . ")\n"; - } - } - if (!empty($schema['indexes'])) { - foreach ($schema['indexes'] as $key => $fields) { - $sql[] = 'CREATE INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $key . ' ON ' . $info['table'] . ' (' . $this->createKeySql($fields) . ")\n"; - } - } - return $sql; - } - - /** - * Build the SQL expression for creating columns. - */ - protected function createColumnsSql($tablename, $schema) { - $sql_array = []; - - // Add the SQL statement for each field. - foreach ($schema['fields'] as $name => $field) { - if (isset($field['type']) && $field['type'] == 'serial') { - if (isset($schema['primary key']) && ($key = array_search($name, $schema['primary key'])) !== FALSE) { - unset($schema['primary key'][$key]); - } - } - $sql_array[] = $this->createFieldSql($name, $this->processField($field)); - } - - // Process keys. - if (!empty($schema['primary key'])) { - $sql_array[] = " PRIMARY KEY (" . $this->createKeySql($schema['primary key']) . ")"; - } - - return implode(", \n", $sql_array); - } - - /** - * Build the SQL expression for keys. - */ - protected function createKeySql($fields) { - $return = []; - foreach ($fields as $field) { - if (is_array($field)) { - $return[] = $field[0]; - } - else { - $return[] = $field; - } - } - return implode(', ', $return); - } - - /** - * Set database-engine specific properties for a field. - * - * @param $field - * A field description array, as specified in the schema documentation. - */ - protected function processField($field) { - if (!isset($field['size'])) { - $field['size'] = 'normal'; - } - - // Set the correct database-engine specific datatype. - // In case one is already provided, force it to uppercase. - if (isset($field['sqlite_type'])) { - $field['sqlite_type'] = mb_strtoupper($field['sqlite_type']); - } - else { - $map = $this->getFieldTypeMap(); - $field['sqlite_type'] = $map[$field['type'] . ':' . $field['size']]; - - // Numeric fields with a specified scale have to be stored as floats. - if ($field['sqlite_type'] === 'NUMERIC' && isset($field['scale'])) { - $field['sqlite_type'] = 'FLOAT'; - } - } - - if (isset($field['type']) && $field['type'] == 'serial') { - $field['auto_increment'] = TRUE; - } - - return $field; - } - - /** - * Create an SQL string for a field to be used in table creation or alteration. - * - * Before passing a field out of a schema definition into this function it has - * to be processed by self::processField(). - * - * @param $name - * Name of the field. - * @param $spec - * The field specification, as per the schema data structure format. - */ - protected function createFieldSql($name, $spec) { - $name = $this->connection->escapeField($name); - if (!empty($spec['auto_increment'])) { - $sql = $name . " INTEGER PRIMARY KEY AUTOINCREMENT"; - if (!empty($spec['unsigned'])) { - $sql .= ' CHECK (' . $name . '>= 0)'; - } - } - else { - $sql = $name . ' ' . $spec['sqlite_type']; - - if (in_array($spec['sqlite_type'], ['VARCHAR', 'TEXT'])) { - if (isset($spec['length'])) { - $sql .= '(' . $spec['length'] . ')'; - } - - if (isset($spec['binary']) && $spec['binary'] === FALSE) { - $sql .= ' COLLATE NOCASE_UTF8'; - } - } - - if (isset($spec['not null'])) { - if ($spec['not null']) { - $sql .= ' NOT NULL'; - } - else { - $sql .= ' NULL'; - } - } - - if (!empty($spec['unsigned'])) { - $sql .= ' CHECK (' . $name . '>= 0)'; - } - - if (isset($spec['default'])) { - if (is_string($spec['default'])) { - $spec['default'] = $this->connection->quote($spec['default']); - } - $sql .= ' DEFAULT ' . $spec['default']; - } - - if (empty($spec['not null']) && !isset($spec['default'])) { - $sql .= ' DEFAULT NULL'; - } - } - return $sql; - } - - /** - * {@inheritdoc} - */ - public function getFieldTypeMap() { - // Put :normal last so it gets preserved by array_flip. This makes - // it much easier for modules (such as schema.module) to map - // database types back into schema types. - // $map does not use drupal_static as its value never changes. - static $map = [ - 'varchar_ascii:normal' => 'VARCHAR', - - 'varchar:normal' => 'VARCHAR', - 'char:normal' => 'CHAR', - - 'text:tiny' => 'TEXT', - 'text:small' => 'TEXT', - 'text:medium' => 'TEXT', - 'text:big' => 'TEXT', - 'text:normal' => 'TEXT', - - 'serial:tiny' => 'INTEGER', - 'serial:small' => 'INTEGER', - 'serial:medium' => 'INTEGER', - 'serial:big' => 'INTEGER', - 'serial:normal' => 'INTEGER', - - 'int:tiny' => 'INTEGER', - 'int:small' => 'INTEGER', - 'int:medium' => 'INTEGER', - 'int:big' => 'INTEGER', - 'int:normal' => 'INTEGER', - - 'float:tiny' => 'FLOAT', - 'float:small' => 'FLOAT', - 'float:medium' => 'FLOAT', - 'float:big' => 'FLOAT', - 'float:normal' => 'FLOAT', - - 'numeric:normal' => 'NUMERIC', - - 'blob:big' => 'BLOB', - 'blob:normal' => 'BLOB', - ]; - return $map; - } - - /** - * {@inheritdoc} - */ - public function renameTable($table, $new_name) { - if (!$this->tableExists($table)) { - throw new SchemaObjectDoesNotExistException("Cannot rename '$table' to '$new_name': table '$table' doesn't exist."); - } - if ($this->tableExists($new_name)) { - throw new SchemaObjectExistsException("Cannot rename '$table' to '$new_name': table '$new_name' already exists."); - } - - $schema = $this->introspectSchema($table); - - // SQLite doesn't allow you to rename tables outside of the current - // database. So the syntax '... RENAME TO database.table' would fail. - // So we must determine the full table name here rather than surrounding - // the table with curly braces in case the db_prefix contains a reference - // to a database outside of our existing database. - $info = $this->getPrefixInfo($new_name); - $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO ' . $info['table']); - - // Drop the indexes, there is no RENAME INDEX command in SQLite. - if (!empty($schema['unique keys'])) { - foreach ($schema['unique keys'] as $key => $fields) { - $this->dropIndex($table, $key); - } - } - if (!empty($schema['indexes'])) { - foreach ($schema['indexes'] as $index => $fields) { - $this->dropIndex($table, $index); - } - } - - // Recreate the indexes. - $statements = $this->createIndexSql($new_name, $schema); - foreach ($statements as $statement) { - $this->connection->query($statement); - } - } - - /** - * {@inheritdoc} - */ - public function dropTable($table) { - if (!$this->tableExists($table)) { - return FALSE; - } - $this->connection->tableDropped = TRUE; - $this->connection->query('DROP TABLE {' . $table . '}'); - return TRUE; - } - - /** - * {@inheritdoc} - */ - public function addField($table, $field, $specification, $keys_new = []) { - if (!$this->tableExists($table)) { - throw new SchemaObjectDoesNotExistException("Cannot add field '$table.$field': table doesn't exist."); - } - if ($this->fieldExists($table, $field)) { - throw new SchemaObjectExistsException("Cannot add field '$table.$field': field already exists."); - } - if (isset($keys_new['primary key']) && in_array($field, $keys_new['primary key'], TRUE)) { - $this->ensureNotNullPrimaryKey($keys_new['primary key'], [$field => $specification]); - } - - // SQLite doesn't have a full-featured ALTER TABLE statement. It only - // supports adding new fields to a table, in some simple cases. In most - // cases, we have to create a new table and copy the data over. - if (empty($keys_new) && (empty($specification['not null']) || isset($specification['default']))) { - // When we don't have to create new keys and we are not creating a - // NOT NULL column without a default value, we can use the quicker version. - $query = 'ALTER TABLE {' . $table . '} ADD ' . $this->createFieldSql($field, $this->processField($specification)); - $this->connection->query($query); - - // Apply the initial value if set. - if (isset($specification['initial_from_field'])) { - if (isset($specification['initial'])) { - $expression = 'COALESCE(' . $specification['initial_from_field'] . ', :default_initial_value)'; - $arguments = [':default_initial_value' => $specification['initial']]; - } - else { - $expression = $specification['initial_from_field']; - $arguments = []; - } - $this->connection->update($table) - ->expression($field, $expression, $arguments) - ->execute(); - } - elseif (isset($specification['initial'])) { - $this->connection->update($table) - ->fields([$field => $specification['initial']]) - ->execute(); - } - } - else { - // We cannot add the field directly. Use the slower table alteration - // method, starting from the old schema. - $old_schema = $this->introspectSchema($table); - $new_schema = $old_schema; - - // Add the new field. - $new_schema['fields'][$field] = $specification; - - // Build the mapping between the old fields and the new fields. - $mapping = []; - if (isset($specification['initial_from_field'])) { - // If we have an initial value, copy it over. - if (isset($specification['initial'])) { - $expression = 'COALESCE(' . $specification['initial_from_field'] . ', :default_initial_value)'; - $arguments = [':default_initial_value' => $specification['initial']]; - } - else { - $expression = $specification['initial_from_field']; - $arguments = []; - } - $mapping[$field] = [ - 'expression' => $expression, - 'arguments' => $arguments, - ]; - } - elseif (isset($specification['initial'])) { - // If we have an initial value, copy it over. - $mapping[$field] = [ - 'expression' => ':newfieldinitial', - 'arguments' => [':newfieldinitial' => $specification['initial']], - ]; - } - else { - // Else use the default of the field. - $mapping[$field] = NULL; - } - - // Add the new indexes. - $new_schema = array_merge($new_schema, $keys_new); - - $this->alterTable($table, $old_schema, $new_schema, $mapping); - } - } - - /** - * Create a table with a new schema containing the old content. - * - * As SQLite does not support ALTER TABLE (with a few exceptions) it is - * necessary to create a new table and copy over the old content. - * - * @param $table - * Name of the table to be altered. - * @param $old_schema - * The old schema array for the table. - * @param $new_schema - * The new schema array for the table. - * @param $mapping - * An optional mapping between the fields of the old specification and the - * fields of the new specification. An associative array, whose keys are - * the fields of the new table, and values can take two possible forms: - * - a simple string, which is interpreted as the name of a field of the - * old table, - * - an associative array with two keys 'expression' and 'arguments', - * that will be used as an expression field. - */ - protected function alterTable($table, $old_schema, $new_schema, array $mapping = []) { - $i = 0; - do { - $new_table = $table . '_' . $i++; - } while ($this->tableExists($new_table)); - - $this->createTable($new_table, $new_schema); - - // Build a SQL query to migrate the data from the old table to the new. - $select = $this->connection->select($table); - - // Complete the mapping. - $possible_keys = array_keys($new_schema['fields']); - $mapping += array_combine($possible_keys, $possible_keys); - - // Now add the fields. - foreach ($mapping as $field_alias => $field_source) { - // Just ignore this field (ie. use its default value). - if (!isset($field_source)) { - continue; - } - - if (is_array($field_source)) { - $select->addExpression($field_source['expression'], $field_alias, $field_source['arguments']); - } - else { - $select->addField($table, $field_source, $field_alias); - } - } - - // Execute the data migration query. - $this->connection->insert($new_table) - ->from($select) - ->execute(); - - $old_count = $this->connection->query('SELECT COUNT(*) FROM {' . $table . '}')->fetchField(); - $new_count = $this->connection->query('SELECT COUNT(*) FROM {' . $new_table . '}')->fetchField(); - if ($old_count == $new_count) { - $this->dropTable($table); - $this->renameTable($new_table, $table); - } - } - - /** - * Find out the schema of a table. - * - * This function uses introspection methods provided by the database to - * create a schema array. This is useful, for example, during update when - * the old schema is not available. - * - * @param $table - * Name of the table. - * - * @return - * An array representing the schema. - * - * @throws \Exception - * If a column of the table could not be parsed. - */ - protected function introspectSchema($table) { - $mapped_fields = array_flip($this->getFieldTypeMap()); - $schema = [ - 'fields' => [], - 'primary key' => [], - 'unique keys' => [], - 'indexes' => [], - ]; - - $info = $this->getPrefixInfo($table); - $result = $this->connection->query('PRAGMA ' . $info['schema'] . '.table_info(' . $info['table'] . ')'); - foreach ($result as $row) { - if (preg_match('/^([^(]+)\((.*)\)$/', $row->type, $matches)) { - $type = $matches[1]; - $length = $matches[2]; - } - else { - $type = $row->type; - $length = NULL; - } - if (isset($mapped_fields[$type])) { - [$type, $size] = explode(':', $mapped_fields[$type]); - $schema['fields'][$row->name] = [ - 'type' => $type, - 'size' => $size, - 'not null' => !empty($row->notnull) || $row->pk !== "0", - ]; - if ($length) { - $schema['fields'][$row->name]['length'] = $length; - } - - // Convert the default into a properly typed value. - if ($row->dflt_value === 'NULL') { - $schema['fields'][$row->name]['default'] = NULL; - } - elseif (is_string($row->dflt_value) && $row->dflt_value[0] === '\'') { - // Remove the wrapping single quotes. And replace duplicate single - // quotes with a single quote. - $schema['fields'][$row->name]['default'] = str_replace("''", "'", substr($row->dflt_value, 1, -1)); - } - elseif (is_numeric($row->dflt_value)) { - // Adding 0 to a string will cause PHP to convert it to a float or - // an integer depending on what the string is. For example: - // - '1' + 0 = 1 - // - '1.0' + 0 = 1.0 - $schema['fields'][$row->name]['default'] = $row->dflt_value + 0; - } - else { - $schema['fields'][$row->name]['default'] = $row->dflt_value; - } - // $row->pk contains a number that reflects the primary key order. We - // use that as the key and sort (by key) below to return the primary key - // in the same order that it is stored in. - if ($row->pk) { - $schema['primary key'][$row->pk] = $row->name; - } - } - else { - throw new \Exception("Unable to parse the column type " . $row->type); - } - } - ksort($schema['primary key']); - // Re-key the array because $row->pk starts counting at 1. - $schema['primary key'] = array_values($schema['primary key']); - - $indexes = []; - $result = $this->connection->query('PRAGMA ' . $info['schema'] . '.index_list(' . $info['table'] . ')'); - foreach ($result as $row) { - if (strpos($row->name, 'sqlite_autoindex_') !== 0) { - $indexes[] = [ - 'schema_key' => $row->unique ? 'unique keys' : 'indexes', - 'name' => $row->name, - ]; - } - } - foreach ($indexes as $index) { - $name = $index['name']; - // Get index name without prefix. - $index_name = substr($name, strlen($info['table']) + 1); - $result = $this->connection->query('PRAGMA ' . $info['schema'] . '.index_info(' . $name . ')'); - foreach ($result as $row) { - $schema[$index['schema_key']][$index_name][] = $row->name; - } - } - return $schema; - } - - /** - * {@inheritdoc} - */ - public function dropField($table, $field) { - if (!$this->fieldExists($table, $field)) { - return FALSE; - } - - $old_schema = $this->introspectSchema($table); - $new_schema = $old_schema; - - unset($new_schema['fields'][$field]); - - // Drop the primary key if the field to drop is part of it. This is - // consistent with the behavior on PostgreSQL. - // @see \Drupal\Core\Database\Driver\mysql\Schema::dropField() - if (isset($new_schema['primary key']) && in_array($field, $new_schema['primary key'], TRUE)) { - unset($new_schema['primary key']); - } - - // Handle possible index changes. - foreach ($new_schema['indexes'] as $index => $fields) { - foreach ($fields as $key => $field_name) { - if ($field_name == $field) { - unset($new_schema['indexes'][$index][$key]); - } - } - // If this index has no more fields then remove it. - if (empty($new_schema['indexes'][$index])) { - unset($new_schema['indexes'][$index]); - } - } - $this->alterTable($table, $old_schema, $new_schema); - return TRUE; - } - - /** - * {@inheritdoc} - */ - public function changeField($table, $field, $field_new, $spec, $keys_new = []) { - if (!$this->fieldExists($table, $field)) { - throw new SchemaObjectDoesNotExistException("Cannot change the definition of field '$table.$field': field doesn't exist."); - } - if (($field != $field_new) && $this->fieldExists($table, $field_new)) { - throw new SchemaObjectExistsException("Cannot rename field '$table.$field' to '$field_new': target field already exists."); - } - if (isset($keys_new['primary key']) && in_array($field_new, $keys_new['primary key'], TRUE)) { - $this->ensureNotNullPrimaryKey($keys_new['primary key'], [$field_new => $spec]); - } - - $old_schema = $this->introspectSchema($table); - $new_schema = $old_schema; - - // Map the old field to the new field. - if ($field != $field_new) { - $mapping[$field_new] = $field; - } - else { - $mapping = []; - } - - // Remove the previous definition and swap in the new one. - unset($new_schema['fields'][$field]); - $new_schema['fields'][$field_new] = $spec; - - // Map the former indexes to the new column name. - $new_schema['primary key'] = $this->mapKeyDefinition($new_schema['primary key'], $mapping); - foreach (['unique keys', 'indexes'] as $k) { - foreach ($new_schema[$k] as &$key_definition) { - $key_definition = $this->mapKeyDefinition($key_definition, $mapping); - } - } - - // Add in the keys from $keys_new. - if (isset($keys_new['primary key'])) { - $new_schema['primary key'] = $keys_new['primary key']; - } - foreach (['unique keys', 'indexes'] as $k) { - if (!empty($keys_new[$k])) { - $new_schema[$k] = $keys_new[$k] + $new_schema[$k]; - } - } - - $this->alterTable($table, $old_schema, $new_schema, $mapping); - } - - /** - * Utility method: rename columns in an index definition according to a new mapping. - * - * @param $key_definition - * The key definition. - * @param $mapping - * The new mapping. - */ - protected function mapKeyDefinition(array $key_definition, array $mapping) { - foreach ($key_definition as &$field) { - // The key definition can be an array($field, $length). - if (is_array($field)) { - $field = &$field[0]; - } - - $mapped_field = array_search($field, $mapping, TRUE); - if ($mapped_field !== FALSE) { - $field = $mapped_field; - } - } - return $key_definition; - } - - /** - * {@inheritdoc} - */ - public function addIndex($table, $name, $fields, array $spec) { - if (!$this->tableExists($table)) { - throw new SchemaObjectDoesNotExistException("Cannot add index '$name' to table '$table': table doesn't exist."); - } - if ($this->indexExists($table, $name)) { - throw new SchemaObjectExistsException("Cannot add index '$name' to table '$table': index already exists."); - } - - $schema['indexes'][$name] = $fields; - $statements = $this->createIndexSql($table, $schema); - foreach ($statements as $statement) { - $this->connection->query($statement); - } - } - - /** - * {@inheritdoc} - */ - public function indexExists($table, $name) { - $info = $this->getPrefixInfo($table); - - return $this->connection->query('PRAGMA ' . $info['schema'] . '.index_info(' . $info['table'] . '_' . $name . ')')->fetchField() != ''; - } - - /** - * {@inheritdoc} - */ - public function dropIndex($table, $name) { - if (!$this->indexExists($table, $name)) { - return FALSE; - } - - $info = $this->getPrefixInfo($table); - - $this->connection->query('DROP INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $name); - return TRUE; - } - - /** - * {@inheritdoc} - */ - public function addUniqueKey($table, $name, $fields) { - if (!$this->tableExists($table)) { - throw new SchemaObjectDoesNotExistException("Cannot add unique key '$name' to table '$table': table doesn't exist."); - } - if ($this->indexExists($table, $name)) { - throw new SchemaObjectExistsException("Cannot add unique key '$name' to table '$table': unique key already exists."); - } - - $schema['unique keys'][$name] = $fields; - $statements = $this->createIndexSql($table, $schema); - foreach ($statements as $statement) { - $this->connection->query($statement); - } - } - - /** - * {@inheritdoc} - */ - public function dropUniqueKey($table, $name) { - if (!$this->indexExists($table, $name)) { - return FALSE; - } - - $info = $this->getPrefixInfo($table); - - $this->connection->query('DROP INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $name); - return TRUE; - } - - /** - * {@inheritdoc} - */ - public function addPrimaryKey($table, $fields) { - if (!$this->tableExists($table)) { - throw new SchemaObjectDoesNotExistException("Cannot add primary key to table '$table': table doesn't exist."); - } - - $old_schema = $this->introspectSchema($table); - $new_schema = $old_schema; - - if (!empty($new_schema['primary key'])) { - throw new SchemaObjectExistsException("Cannot add primary key to table '$table': primary key already exists."); - } - - $new_schema['primary key'] = $fields; - $this->ensureNotNullPrimaryKey($new_schema['primary key'], $new_schema['fields']); - $this->alterTable($table, $old_schema, $new_schema); - } - - /** - * {@inheritdoc} - */ - public function dropPrimaryKey($table) { - $old_schema = $this->introspectSchema($table); - $new_schema = $old_schema; - - if (empty($new_schema['primary key'])) { - return FALSE; - } - - unset($new_schema['primary key']); - $this->alterTable($table, $old_schema, $new_schema); - return TRUE; - } - - /** - * {@inheritdoc} - */ - protected function findPrimaryKeyColumns($table) { - if (!$this->tableExists($table)) { - return FALSE; - } - $schema = $this->introspectSchema($table); - return $schema['primary key']; - } - - /** - * {@inheritdoc} - */ - protected function introspectIndexSchema($table) { - if (!$this->tableExists($table)) { - throw new SchemaObjectDoesNotExistException("The table $table doesn't exist."); - } - $schema = $this->introspectSchema($table); - unset($schema['fields']); - return $schema; - } - - /** - * {@inheritdoc} - */ - public function findTables($table_expression) { - $tables = []; - - // The SQLite implementation doesn't need to use the same filtering strategy - // as the parent one because individually prefixed tables live in their own - // schema (database), which means that neither the main database nor any - // attached one will contain a prefixed table name, so we just need to loop - // over all known schemas and filter by the user-supplied table expression. - $attached_dbs = $this->connection->getAttachedDatabases(); - foreach ($attached_dbs as $schema) { - // Can't use query placeholders for the schema because the query would - // have to be :prefixsqlite_master, which does not work. We also need to - // ignore the internal SQLite tables. - $result = $this->connection->query("SELECT name FROM " . $schema . ".sqlite_master WHERE type = :type AND name LIKE :table_name AND name NOT LIKE :pattern", [ - ':type' => 'table', - ':table_name' => $table_expression, - ':pattern' => 'sqlite_%', - ]); - $tables += $result->fetchAllKeyed(0, 0); - } - - return $tables; - } - -} +class Schema extends SqliteSchema {} diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Select.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Select.php index 5eaa5157d860..8cfe31cf3f23 100644 --- a/core/lib/Drupal/Core/Database/Driver/sqlite/Select.php +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Select.php @@ -2,16 +2,16 @@ namespace Drupal\Core\Database\Driver\sqlite; -use Drupal\Core\Database\Query\Select as QuerySelect; +use Drupal\sqlite\Driver\Database\sqlite\Select as SqliteSelect; + +@trigger_error('\Drupal\Core\Database\Driver\sqlite\Select is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED); /** * SQLite implementation of \Drupal\Core\Database\Query\Select. + * + * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite + * database driver has been moved to the sqlite module. + * + * @see https://www.drupal.org/node/3129492 */ -class Select extends QuerySelect { - - public function forUpdate($set = TRUE) { - // SQLite does not support FOR UPDATE so nothing to do. - return $this; - } - -} +class Select extends SqliteSelect {} diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Statement.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Statement.php index 1e895629ea14..c189fe84943e 100644 --- a/core/lib/Drupal/Core/Database/Driver/sqlite/Statement.php +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Statement.php @@ -2,150 +2,16 @@ namespace Drupal\Core\Database\Driver\sqlite; -use Drupal\Core\Database\StatementPrefetch; -use Drupal\Core\Database\StatementInterface; +use Drupal\sqlite\Driver\Database\sqlite\Statement as SqliteStatement; + +@trigger_error('\Drupal\Core\Database\Driver\sqlite\Statement is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED); /** * SQLite implementation of \Drupal\Core\Database\Statement. * - * The PDO SQLite driver only closes SELECT statements when the PDOStatement - * destructor is called and SQLite does not allow data change (INSERT, - * UPDATE etc) on a table which has open SELECT statements. This is a - * user-space mock of PDOStatement that buffers all the data and doesn't - * have those limitations. + * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite + * database driver has been moved to the sqlite module. + * + * @see https://www.drupal.org/node/3129492 */ -class Statement extends StatementPrefetch implements StatementInterface { - - /** - * {@inheritdoc} - * - * The PDO SQLite layer doesn't replace numeric placeholders in queries - * correctly, and this makes numeric expressions (such as COUNT(*) >= :count) - * fail. We replace numeric placeholders in the query ourselves to work - * around this bug. - * - * See http://bugs.php.net/bug.php?id=45259 for more details. - */ - protected function getStatement($query, &$args = []) { - if (is_array($args) && !empty($args)) { - // Check if $args is a simple numeric array. - if (range(0, count($args) - 1) === array_keys($args)) { - // In that case, we have unnamed placeholders. - $count = 0; - $new_args = []; - foreach ($args as $value) { - if (is_float($value) || is_int($value)) { - if (is_float($value)) { - // Force the conversion to float so as not to loose precision - // in the automatic cast. - $value = sprintf('%F', $value); - } - $query = substr_replace($query, $value, strpos($query, '?'), 1); - } - else { - $placeholder = ':db_statement_placeholder_' . $count++; - $query = substr_replace($query, $placeholder, strpos($query, '?'), 1); - $new_args[$placeholder] = $value; - } - } - $args = $new_args; - } - else { - // Else, this is using named placeholders. - foreach ($args as $placeholder => $value) { - if (is_float($value) || is_int($value)) { - if (is_float($value)) { - // Force the conversion to float so as not to loose precision - // in the automatic cast. - $value = sprintf('%F', $value); - } - - // We will remove this placeholder from the query as PDO throws an - // exception if the number of placeholders in the query and the - // arguments does not match. - unset($args[$placeholder]); - // PDO allows placeholders to not be prefixed by a colon. See - // http://marc.info/?l=php-internals&m=111234321827149&w=2 for - // more. - if ($placeholder[0] != ':') { - $placeholder = ":$placeholder"; - } - // When replacing the placeholders, make sure we search for the - // exact placeholder. For example, if searching for - // ':db_placeholder_1', do not replace ':db_placeholder_11'. - $query = preg_replace('/' . preg_quote($placeholder) . '\b/', $value, $query); - } - } - } - } - - return $this->pdoConnection->prepare($query); - } - - /** - * {@inheritdoc} - */ - public function execute($args = [], $options = []) { - try { - $return = parent::execute($args, $options); - } - catch (\PDOException $e) { - // The database schema might be changed by another process in between the - // time that the statement was prepared and the time the statement was run - // (e.g. usually happens when running tests). In this case, we need to - // re-run the query. - // @see http://www.sqlite.org/faq.html#q15 - // @see http://www.sqlite.org/rescode.html#schema - if (!empty($e->errorInfo[1]) && $e->errorInfo[1] === 17) { - // The schema has changed. SQLite specifies that we must resend the query. - $return = parent::execute($args, $options); - } - else { - // Rethrow the exception. - throw $e; - } - } - - // In some weird cases, SQLite will prefix some column names by the name - // of the table. We post-process the data, by renaming the column names - // using the same convention as MySQL and PostgreSQL. - $rename_columns = []; - foreach ($this->columnNames as $k => $column) { - // In some SQLite versions, SELECT DISTINCT(field) will return "(field)" - // instead of "field". - if (preg_match("/^\((.*)\)$/", $column, $matches)) { - $rename_columns[$column] = $matches[1]; - $this->columnNames[$k] = $matches[1]; - $column = $matches[1]; - } - - // Remove "table." prefixes. - if (preg_match("/^.*\.(.*)$/", $column, $matches)) { - $rename_columns[$column] = $matches[1]; - $this->columnNames[$k] = $matches[1]; - } - } - if ($rename_columns) { - // DatabaseStatementPrefetch already extracted the first row, - // put it back into the result set. - if (isset($this->currentRow)) { - $this->data[0] = &$this->currentRow; - } - - // Then rename all the columns across the result set. - foreach ($this->data as $k => $row) { - foreach ($rename_columns as $old_column => $new_column) { - $this->data[$k][$new_column] = $this->data[$k][$old_column]; - unset($this->data[$k][$old_column]); - } - } - - // Finally, extract the first row again. - $this->currentRow = $this->data[0]; - unset($this->data[0]); - } - - return $return; - } - -} +class Statement extends SqliteStatement {} diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Truncate.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Truncate.php index 386912f87801..d9f83cac5b1f 100644 --- a/core/lib/Drupal/Core/Database/Driver/sqlite/Truncate.php +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Truncate.php @@ -2,21 +2,16 @@ namespace Drupal\Core\Database\Driver\sqlite; -use Drupal\Core\Database\Query\Truncate as QueryTruncate; +use Drupal\sqlite\Driver\Database\sqlite\Truncate as SqliteTruncate; + +@trigger_error('\Drupal\Core\Database\Driver\sqlite\Truncate is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED); /** * SQLite implementation of \Drupal\Core\Database\Query\Truncate. * - * SQLite doesn't support TRUNCATE, but a DELETE query with no condition has - * exactly the effect (it is implemented by DROPing the table). + * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite + * database driver has been moved to the sqlite module. + * + * @see https://www.drupal.org/node/3129492 */ -class Truncate extends QueryTruncate { - - public function __toString() { - // Create a sanitized comment string to prepend to the query. - $comments = $this->connection->makeComment($this->comments); - - return $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '} '; - } - -} +class Truncate extends SqliteTruncate {} diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Upsert.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Upsert.php index f97f6c8d8b4c..9a6ad99d2908 100644 --- a/core/lib/Drupal/Core/Database/Driver/sqlite/Upsert.php +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Upsert.php @@ -2,46 +2,16 @@ namespace Drupal\Core\Database\Driver\sqlite; -use Drupal\Core\Database\Query\Upsert as QueryUpsert; +use Drupal\sqlite\Driver\Database\sqlite\Upsert as SqliteUpsert; + +@trigger_error('\Drupal\Core\Database\Driver\sqlite\Upsert is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED); /** * SQLite implementation of \Drupal\Core\Database\Query\Upsert. * - * @see https://www.sqlite.org/lang_UPSERT.html + * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite + * database driver has been moved to the sqlite module. + * + * @see https://www.drupal.org/node/3129492 */ -class Upsert extends QueryUpsert { - - /** - * {@inheritdoc} - */ - public function __toString() { - // Create a sanitized comment string to prepend to the query. - $comments = $this->connection->makeComment($this->comments); - - // Default fields are always placed first for consistency. - $insert_fields = array_merge($this->defaultFields, $this->insertFields); - $insert_fields = array_map(function ($field) { - return $this->connection->escapeField($field); - }, $insert_fields); - - $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES '; - - $values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields); - $query .= implode(', ', $values); - - // Updating the unique / primary key is not necessary. - unset($insert_fields[$this->key]); - - $update = []; - foreach ($insert_fields as $field) { - // The "excluded." prefix causes the field to refer to the value for field - // that would have been inserted had there been no conflict. - $update[] = "$field = EXCLUDED.$field"; - } - - $query .= ' ON CONFLICT (' . $this->connection->escapeField($this->key) . ') DO UPDATE SET ' . implode(', ', $update); - - return $query; - } - -} +class Upsert extends SqliteUpsert {} diff --git a/core/lib/Drupal/Core/Database/StatementInterface.php b/core/lib/Drupal/Core/Database/StatementInterface.php index 8bfb76201329..0bb030d0172e 100644 --- a/core/lib/Drupal/Core/Database/StatementInterface.php +++ b/core/lib/Drupal/Core/Database/StatementInterface.php @@ -2,18 +2,20 @@ namespace Drupal\Core\Database; +// cSpell:ignore mydriver + /** * Represents a prepared statement. * - * Child implementations should either extend PDOStatement: + * Child implementations should either extend StatementWrapper: * @code - * class Drupal\Core\Database\Driver\oracle\Statement extends PDOStatement implements Drupal\Core\Database\StatementInterface {} + * class Drupal\mymodule\Driver\Database\mydriver\Statement extends Drupal\Core\Database\StatementWrapper {} * @endcode * or define their own class. If defining their own class, they will also have * to implement either the Iterator or IteratorAggregate interface before * Drupal\Core\Database\StatementInterface: * @code - * class Drupal\Core\Database\Driver\oracle\Statement implements Iterator, Drupal\Core\Database\StatementInterface {} + * class Drupal\mymodule\Driver\Database\mydriver\Statement implements Iterator, Drupal\Core\Database\StatementInterface {} * @endcode * * @ingroup database diff --git a/core/lib/Drupal/Core/Database/StatementPrefetch.php b/core/lib/Drupal/Core/Database/StatementPrefetch.php index ab68dec877b4..267b6a85d55f 100644 --- a/core/lib/Drupal/Core/Database/StatementPrefetch.php +++ b/core/lib/Drupal/Core/Database/StatementPrefetch.php @@ -224,7 +224,7 @@ public function execute($args = [], $options = []) { // as soon as possible. $this->data = $statement->fetchAll(\PDO::FETCH_ASSOC); // Destroy the statement as soon as possible. See the documentation of - // \Drupal\Core\Database\Driver\sqlite\Statement for an explanation. + // \Drupal\sqlite\Driver\Database\sqlite\Statement for an explanation. unset($statement); $this->resultRowCount = count($this->data); diff --git a/core/lib/Drupal/Core/Site/Settings.php b/core/lib/Drupal/Core/Site/Settings.php index 31b2b8f0afa5..3998719e8bc9 100644 --- a/core/lib/Drupal/Core/Site/Settings.php +++ b/core/lib/Drupal/Core/Site/Settings.php @@ -162,6 +162,43 @@ public static function initialize($app_root, $site_path, &$class_loader) { // Initialize databases. foreach ($databases as $key => $targets) { foreach ($targets as $target => $info) { + // Backwards compatibility layer for Drupal 8 style database connection + // arrays. Those do not have the 'autoload' key set for core database + // drivers. + if (empty($info['autoload'])) { + switch (strtolower($info['driver'])) { + case 'mysql': + $info['autoload'] = 'core/modules/mysql/src/Driver/Database/mysql/'; + break; + + case 'pgsql': + $info['autoload'] = 'core/modules/pgsql/src/Driver/Database/pgsql/'; + break; + + case 'sqlite': + $info['autoload'] = 'core/modules/sqlite/src/Driver/Database/sqlite/'; + break; + } + } + // Backwards compatibility layer for Drupal 8 style database connection + // arrays. Those have the wrong 'namespace' key set, or not set at all + // for core supported database drivers. + if (empty($info['namespace']) || (strpos($info['namespace'], 'Drupal\\Core\\Database\\Driver\\') === 0)) { + switch (strtolower($info['driver'])) { + case 'mysql': + $info['namespace'] = 'Drupal\\mysql\\Driver\\Database\\mysql'; + break; + + case 'pgsql': + $info['namespace'] = 'Drupal\\pgsql\\Driver\\Database\\pgsql'; + break; + + case 'sqlite': + $info['namespace'] = 'Drupal\\sqlite\\Driver\\Database\\sqlite'; + break; + } + } + Database::addConnectionInfo($key, $target, $info); // If the database driver is provided by a module, then its code may // need to be instantiated prior to when the module's root namespace diff --git a/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php b/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php index e1a234c92973..244d0eeb468e 100644 --- a/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php +++ b/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php @@ -516,6 +516,7 @@ protected function installParameters() { $driver = $connection_info['default']['driver']; unset($connection_info['default']['driver']); unset($connection_info['default']['namespace']); + unset($connection_info['default']['autoload']); unset($connection_info['default']['pdo']); unset($connection_info['default']['init_commands']); // Remove database connection info that is not used by SQLite. diff --git a/core/modules/comment/tests/src/Unit/CommentStatisticsUnitTest.php b/core/modules/comment/tests/src/Unit/CommentStatisticsUnitTest.php index d8c390dfe876..b0412210e717 100644 --- a/core/modules/comment/tests/src/Unit/CommentStatisticsUnitTest.php +++ b/core/modules/comment/tests/src/Unit/CommentStatisticsUnitTest.php @@ -51,7 +51,7 @@ class CommentStatisticsUnitTest extends UnitTestCase { * Sets up required mocks and the CommentStatistics service under test. */ protected function setUp(): void { - $this->statement = $this->getMockBuilder('Drupal\Core\Database\Driver\sqlite\Statement') + $this->statement = $this->getMockBuilder('Drupal\sqlite\Driver\Database\sqlite\Statement') ->disableOriginalConstructor() ->getMock(); diff --git a/core/modules/config/tests/src/Functional/ConfigImportAllTest.php b/core/modules/config/tests/src/Functional/ConfigImportAllTest.php index f6311e0dab06..e0b1ed891b81 100644 --- a/core/modules/config/tests/src/Functional/ConfigImportAllTest.php +++ b/core/modules/config/tests/src/Functional/ConfigImportAllTest.php @@ -87,10 +87,13 @@ public function testInstallUninstall() { field_purge_batch(1000); $all_modules = \Drupal::service('extension.list.module')->getList(); + $database_module = \Drupal::service('database')->getProvider(); + $expected_modules = ['path_alias', 'system', 'user', 'standard', $database_module]; // Ensure that only core required modules and the install profile can not be uninstalled. $validation_reasons = \Drupal::service('module_installer')->validateUninstall(array_keys($all_modules)); - $this->assertEquals(['path_alias', 'system', 'user', 'standard'], array_keys($validation_reasons)); + $validation_modules = array_keys($validation_reasons); + $this->assertEqualsCanonicalizing($expected_modules, $validation_modules); $modules_to_uninstall = array_filter($all_modules, function ($module) use ($validation_reasons) { // Filter required and not enabled modules. @@ -103,6 +106,9 @@ public function testInstallUninstall() { // Can not uninstall config and use admin/config/development/configuration! unset($modules_to_uninstall['config']); + // Can not uninstall the database module. + unset($modules_to_uninstall[$database_module]); + $this->assertTrue(isset($modules_to_uninstall['comment']), 'The comment module will be disabled'); $this->assertTrue(isset($modules_to_uninstall['file']), 'The File module will be disabled'); $this->assertTrue(isset($modules_to_uninstall['editor']), 'The Editor module will be disabled'); diff --git a/core/modules/migrate/tests/src/Kernel/MigrateSqlSourceTestBase.php b/core/modules/migrate/tests/src/Kernel/MigrateSqlSourceTestBase.php index bbb4168badcb..fcae35cb646b 100644 --- a/core/modules/migrate/tests/src/Kernel/MigrateSqlSourceTestBase.php +++ b/core/modules/migrate/tests/src/Kernel/MigrateSqlSourceTestBase.php @@ -3,7 +3,7 @@ namespace Drupal\Tests\migrate\Kernel; use Drupal\Core\Cache\MemoryCounterBackendFactory; -use Drupal\Core\Database\Driver\sqlite\Connection; +use Drupal\sqlite\Driver\Database\sqlite\Connection; use Drupal\Core\DependencyInjection\ContainerBuilder; /** @@ -26,7 +26,7 @@ public function register(ContainerBuilder $container) { * The source data, keyed by table name. Each table is an array containing * the rows in that table. * - * @return \Drupal\Core\Database\Driver\sqlite\Connection + * @return \Drupal\sqlite\Driver\Database\sqlite\Connection * The SQLite database connection. */ protected function getDatabase(array $source_data) { diff --git a/core/modules/migrate/tests/src/Kernel/QueryBatchTest.php b/core/modules/migrate/tests/src/Kernel/QueryBatchTest.php index 7844e4144dc3..a7f6e3db1524 100644 --- a/core/modules/migrate/tests/src/Kernel/QueryBatchTest.php +++ b/core/modules/migrate/tests/src/Kernel/QueryBatchTest.php @@ -6,7 +6,7 @@ use Drupal\migrate\MigrateException; use Drupal\migrate\Plugin\MigrateIdMapInterface; use Drupal\migrate\Plugin\MigrationInterface; -use Drupal\Core\Database\Driver\sqlite\Connection; +use Drupal\sqlite\Driver\Database\sqlite\Connection; /** * Tests query batching. @@ -224,7 +224,7 @@ protected function getPlugin($configuration) { * The source data, keyed by table name. Each table is an array containing * the rows in that table. * - * @return \Drupal\Core\Database\Driver\sqlite\Connection + * @return \Drupal\sqlite\Driver\Database\sqlite\Connection * The SQLite database connection. */ protected function getDatabase(array $source_data) { diff --git a/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php b/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php index 31f456233b91..7908fd912dce 100644 --- a/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php +++ b/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php @@ -2,7 +2,7 @@ namespace Drupal\Tests\migrate\Unit; -use Drupal\Core\Database\Driver\sqlite\Connection; +use Drupal\sqlite\Driver\Database\sqlite\Connection; use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\MigrateException; use Drupal\migrate\Plugin\MigrateIdMapInterface; @@ -970,7 +970,7 @@ public function testGetQualifiedMapTablePrefix() { $qualified_map_table = $this->getIdMap()->getQualifiedMapTableName(); // The SQLite driver is a special flower. It will prefix tables with // PREFIX.TABLE, instead of the standard PREFIXTABLE. - // @see \Drupal\Core\Database\Driver\sqlite\Connection::__construct() + // @see \Drupal\sqlite\Driver\Database\sqlite\Connection::__construct() $this->assertEquals('prefix.migrate_map_sql_idmap_test', $qualified_map_table); } diff --git a/core/modules/migrate/tests/src/Unit/MigrateTestCase.php b/core/modules/migrate/tests/src/Unit/MigrateTestCase.php index aef21d1a720b..6021fab24097 100644 --- a/core/modules/migrate/tests/src/Unit/MigrateTestCase.php +++ b/core/modules/migrate/tests/src/Unit/MigrateTestCase.php @@ -2,7 +2,7 @@ namespace Drupal\Tests\migrate\Unit; -use Drupal\Core\Database\Driver\sqlite\Connection; +use Drupal\sqlite\Driver\Database\sqlite\Connection; use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\migrate\Plugin\MigrateIdMapInterface; use Drupal\migrate\Plugin\MigrationInterface; @@ -106,7 +106,7 @@ protected function getMigration($id_map = NULL) { * (optional) Options for the database connection. Defaults to an empty * array. * - * @return \Drupal\Core\Database\Driver\sqlite\Connection + * @return \Drupal\sqlite\Driver\Database\sqlite\Connection * The database connection. */ protected function getDatabase(array $database_contents, $connection_options = []) { diff --git a/core/modules/mysql/mysql.info.yml b/core/modules/mysql/mysql.info.yml new file mode 100644 index 000000000000..3a85112110b1 --- /dev/null +++ b/core/modules/mysql/mysql.info.yml @@ -0,0 +1,5 @@ +name: MySQL +type: module +description: 'Database driver for MySQL.' +package: Core +version: VERSION diff --git a/core/modules/mysql/mysql.module b/core/modules/mysql/mysql.module new file mode 100644 index 000000000000..a8572bf8650b --- /dev/null +++ b/core/modules/mysql/mysql.module @@ -0,0 +1,22 @@ +<?php + +/** + * @file + * The MySQL module provides the connection between Drupal and a MySQL, MariaDB or equivalent database. + */ + +use Drupal\Core\Routing\RouteMatchInterface; + +/** + * Implements hook_help(). + */ +function mysql_help($route_name, RouteMatchInterface $route_match) { + switch ($route_name) { + case 'help.page.mysql': + $output = ''; + $output .= '<h3>' . t('About') . '</h3>'; + $output .= '<p>' . t('The MySQL module provides the connection between Drupal and a MySQL, MariaDB or equivalent database. For more information, see the <a href=":mysql">online documentation for the MySQL module</a>.', [':mysql' => 'https://www.drupal.org/documentation/modules/mysql']) . '</p>'; + return $output; + + } +} diff --git a/core/modules/mysql/src/Driver/Database/mysql/Connection.php b/core/modules/mysql/src/Driver/Database/mysql/Connection.php new file mode 100644 index 000000000000..4eb7700bf658 --- /dev/null +++ b/core/modules/mysql/src/Driver/Database/mysql/Connection.php @@ -0,0 +1,495 @@ +<?php + +namespace Drupal\mysql\Driver\Database\mysql; + +use Drupal\Core\Database\DatabaseAccessDeniedException; +use Drupal\Core\Database\IntegrityConstraintViolationException; +use Drupal\Core\Database\DatabaseExceptionWrapper; +use Drupal\Core\Database\StatementWrapper; +use Drupal\Core\Database\Database; +use Drupal\Core\Database\DatabaseNotFoundException; +use Drupal\Core\Database\DatabaseException; +use Drupal\Core\Database\Connection as DatabaseConnection; +use Drupal\Core\Database\TransactionNoActiveException; + +/** + * @addtogroup database + * @{ + */ + +/** + * MySQL implementation of \Drupal\Core\Database\Connection. + */ +class Connection extends DatabaseConnection { + + /** + * Error code for "Unknown database" error. + */ + const DATABASE_NOT_FOUND = 1049; + + /** + * Error code for "Access denied" error. + */ + const ACCESS_DENIED = 1045; + + /** + * Error code for "Can't initialize character set" error. + */ + const UNSUPPORTED_CHARSET = 2019; + + /** + * Driver-specific error code for "Unknown character set" error. + */ + const UNKNOWN_CHARSET = 1115; + + /** + * SQLSTATE error code for "Syntax error or access rule violation". + */ + const SQLSTATE_SYNTAX_ERROR = 42000; + + /** + * {@inheritdoc} + */ + protected $statementClass = NULL; + + /** + * {@inheritdoc} + */ + protected $statementWrapperClass = StatementWrapper::class; + + /** + * Flag to indicate if the cleanup function in __destruct() should run. + * + * @var bool + */ + protected $needsCleanup = FALSE; + + /** + * Stores the server version after it has been retrieved from the database. + * + * @var string + * + * @see \Drupal\mysql\Driver\Database\mysql\Connection::version + */ + private $serverVersion; + + /** + * The minimal possible value for the max_allowed_packet setting of MySQL. + * + * @link https://mariadb.com/kb/en/mariadb/server-system-variables/#max_allowed_packet + * @link https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_max_allowed_packet + * + * @var int + */ + const MIN_MAX_ALLOWED_PACKET = 1024; + + /** + * {@inheritdoc} + */ + protected $identifierQuotes = ['"', '"']; + + /** + * {@inheritdoc} + */ + public function __construct(\PDO $connection, array $connection_options) { + // If the SQL mode doesn't include 'ANSI_QUOTES' (explicitly or via a + // combination mode), then MySQL doesn't interpret a double quote as an + // identifier quote, in which case use the non-ANSI-standard backtick. + // + // Because we still support MySQL 5.7, check for the deprecated combination + // modes as well. + // + // @see https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_ansi_quotes + $ansi_quotes_modes = ['ANSI_QUOTES', 'ANSI', 'DB2', 'MAXDB', 'MSSQL', 'ORACLE', 'POSTGRESQL']; + $is_ansi_quotes_mode = FALSE; + foreach ($ansi_quotes_modes as $mode) { + // None of the modes in $ansi_quotes_modes are substrings of other modes + // that are not in $ansi_quotes_modes, so a simple stripos() does not + // return false positives. + if (stripos($connection_options['init_commands']['sql_mode'], $mode) !== FALSE) { + $is_ansi_quotes_mode = TRUE; + break; + } + } + if ($this->identifierQuotes === ['"', '"'] && !$is_ansi_quotes_mode) { + $this->identifierQuotes = ['`', '`']; + } + parent::__construct($connection, $connection_options); + } + + /** + * {@inheritdoc} + */ + protected function handleQueryException(\PDOException $e, $query, array $args = [], $options = []) { + // In case of attempted INSERT of a record with an undefined column and no + // default value indicated in schema, MySql returns a 1364 error code. + // Throw an IntegrityConstraintViolationException here like the other + // drivers do, to avoid the parent class to throw a generic + // DatabaseExceptionWrapper instead. + if (!empty($e->errorInfo[1]) && $e->errorInfo[1] === 1364) { + @trigger_error('Connection::handleQueryException() is deprecated in drupal:9.2.0 and is removed in drupal:10.0.0. Get a handler through $this->exceptionHandler() instead, and use one of its methods. See https://www.drupal.org/node/3187222', E_USER_DEPRECATED); + $query_string = ($query instanceof StatementInterface) ? $query->getQueryString() : $query; + $message = $e->getMessage() . ": " . $query_string . "; " . print_r($args, TRUE); + throw new IntegrityConstraintViolationException($message, is_int($e->getCode()) ? $e->getCode() : 0, $e); + } + + parent::handleQueryException($e, $query, $args, $options); + } + + /** + * {@inheritdoc} + */ + public static function open(array &$connection_options = []) { + if (isset($connection_options['_dsn_utf8_fallback']) && $connection_options['_dsn_utf8_fallback'] === TRUE) { + // Only used during the installer version check, as a fallback from utf8mb4. + $charset = 'utf8'; + } + else { + $charset = 'utf8mb4'; + } + // The DSN should use either a socket or a host/port. + if (isset($connection_options['unix_socket'])) { + $dsn = 'mysql:unix_socket=' . $connection_options['unix_socket']; + } + else { + // Default to TCP connection on port 3306. + $dsn = 'mysql:host=' . $connection_options['host'] . ';port=' . (empty($connection_options['port']) ? 3306 : $connection_options['port']); + } + // Character set is added to dsn to ensure PDO uses the proper character + // set when escaping. This has security implications. See + // https://www.drupal.org/node/1201452 for further discussion. + $dsn .= ';charset=' . $charset; + if (!empty($connection_options['database'])) { + $dsn .= ';dbname=' . $connection_options['database']; + } + // Allow PDO options to be overridden. + $connection_options += [ + 'pdo' => [], + ]; + $connection_options['pdo'] += [ + \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, + // So we don't have to mess around with cursors and unbuffered queries by default. + \PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => TRUE, + // Make sure MySQL returns all matched rows on update queries including + // rows that actually didn't have to be updated because the values didn't + // change. This matches common behavior among other database systems. + \PDO::MYSQL_ATTR_FOUND_ROWS => TRUE, + // Because MySQL's prepared statements skip the query cache, because it's dumb. + \PDO::ATTR_EMULATE_PREPARES => TRUE, + // Limit SQL to a single statement like mysqli. + \PDO::MYSQL_ATTR_MULTI_STATEMENTS => FALSE, + // Convert numeric values to strings when fetching. In PHP 8.1, + // \PDO::ATTR_EMULATE_PREPARES now behaves the same way as non emulated + // prepares and returns integers. See https://externals.io/message/113294 + // for further discussion. + \PDO::ATTR_STRINGIFY_FETCHES => TRUE, + ]; + + try { + $pdo = new \PDO($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']); + } + catch (\PDOException $e) { + if ($e->getCode() == static::DATABASE_NOT_FOUND) { + throw new DatabaseNotFoundException($e->getMessage(), $e->getCode(), $e); + } + if ($e->getCode() == static::ACCESS_DENIED) { + throw new DatabaseAccessDeniedException($e->getMessage(), $e->getCode(), $e); + } + throw $e; + } + + // Force MySQL to use the UTF-8 character set. Also set the collation, if a + // certain one has been set; otherwise, MySQL defaults to + // 'utf8mb4_general_ci' (MySQL 5) or 'utf8mb4_0900_ai_ci' (MySQL 8) for + // utf8mb4. + if (!empty($connection_options['collation'])) { + $pdo->exec('SET NAMES ' . $charset . ' COLLATE ' . $connection_options['collation']); + } + else { + $pdo->exec('SET NAMES ' . $charset); + } + + // Set MySQL init_commands if not already defined. Default Drupal's MySQL + // behavior to conform more closely to SQL standards. This allows Drupal + // to run almost seamlessly on many different kinds of database systems. + // These settings force MySQL to behave the same as postgresql, or sqlite + // in regards to syntax interpretation and invalid data handling. See + // https://www.drupal.org/node/344575 for further discussion. Also, as MySQL + // 5.5 changed the meaning of TRADITIONAL we need to spell out the modes one + // by one. + $connection_options += [ + 'init_commands' => [], + ]; + + $connection_options['init_commands'] += [ + 'sql_mode' => "SET sql_mode = 'ANSI,TRADITIONAL'", + ]; + + // Execute initial commands. + foreach ($connection_options['init_commands'] as $sql) { + $pdo->exec($sql); + } + + return $pdo; + } + + /** + * {@inheritdoc} + */ + public function __destruct() { + if ($this->needsCleanup) { + $this->nextIdDelete(); + } + parent::__destruct(); + } + + public function queryRange($query, $from, $count, array $args = [], array $options = []) { + return $this->query($query . ' LIMIT ' . (int) $from . ', ' . (int) $count, $args, $options); + } + + /** + * {@inheritdoc} + */ + public function queryTemporary($query, array $args = [], array $options = []) { + @trigger_error('Connection::queryTemporary() is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. There is no replacement. See https://www.drupal.org/node/3211781', E_USER_DEPRECATED); + $tablename = $this->generateTemporaryTableName(); + $this->query('CREATE TEMPORARY TABLE {' . $tablename . '} Engine=MEMORY ' . $query, $args, $options); + return $tablename; + } + + public function driver() { + return 'mysql'; + } + + /** + * {@inheritdoc} + */ + public function version() { + if ($this->isMariaDb()) { + return $this->getMariaDbVersionMatch(); + } + + return $this->getServerVersion(); + } + + /** + * Determines whether the MySQL distribution is MariaDB or not. + * + * @return bool + * Returns TRUE if the distribution is MariaDB, or FALSE if not. + */ + public function isMariaDb(): bool { + return (bool) $this->getMariaDbVersionMatch(); + } + + /** + * Gets the MariaDB portion of the server version. + * + * @return string + * The MariaDB portion of the server version if present, or NULL if not. + */ + protected function getMariaDbVersionMatch(): ?string { + // MariaDB may prefix its version string with '5.5.5-', which should be + // ignored. + // @see https://github.com/MariaDB/server/blob/f6633bf058802ad7da8196d01fd19d75c53f7274/include/mysql_com.h#L42. + $regex = '/^(?:5\.5\.5-)?(\d+\.\d+\.\d+.*-mariadb.*)/i'; + + preg_match($regex, $this->getServerVersion(), $matches); + return (empty($matches[1])) ? NULL : $matches[1]; + } + + /** + * Gets the server version. + * + * @return string + * The PDO server version. + */ + protected function getServerVersion(): string { + if (!$this->serverVersion) { + $this->serverVersion = $this->connection->query('SELECT VERSION()')->fetchColumn(); + } + return $this->serverVersion; + } + + public function databaseType() { + return 'mysql'; + } + + /** + * Overrides \Drupal\Core\Database\Connection::createDatabase(). + * + * @param string $database + * The name of the database to create. + * + * @throws \Drupal\Core\Database\DatabaseNotFoundException + */ + public function createDatabase($database) { + // Escape the database name. + $database = Database::getConnection()->escapeDatabase($database); + + try { + // Create the database and set it as active. + $this->connection->exec("CREATE DATABASE $database"); + $this->connection->exec("USE $database"); + } + catch (\Exception $e) { + throw new DatabaseNotFoundException($e->getMessage()); + } + } + + public function mapConditionOperator($operator) { + // We don't want to override any of the defaults. + return NULL; + } + + public function nextId($existing_id = 0) { + $new_id = $this->query('INSERT INTO {sequences} () VALUES ()', [], ['return' => Database::RETURN_INSERT_ID]); + // 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 + // INSERT, MySQL will generate a larger value. However, there is no way + // of knowing whether this value already exists in the table. MySQL + // provides an INSERT IGNORE which would work, but that can mask problems + // other than duplicate keys. Instead, we use INSERT ... ON DUPLICATE KEY + // 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->needsCleanup = TRUE; + return $new_id; + } + + public function nextIdDelete() { + // While we want to clean up the table to keep it up from occupying too + // much storage and memory, we must keep the highest value in the table + // because InnoDB uses an in-memory auto-increment counter as long as the + // server runs. When the server is stopped and restarted, InnoDB + // reinitializes the counter for each table for the first INSERT to the + // table based solely on values from the table so deleting all values would + // be a problem in this case. Also, TRUNCATE resets the auto increment + // counter. + try { + $max_id = $this->query('SELECT MAX(value) FROM {sequences}')->fetchField(); + // We know we are using MySQL here, no need for the slower ::delete(). + $this->query('DELETE FROM {sequences} WHERE value < :value', [':value' => $max_id]); + } + // During testing, this function is called from shutdown with the + // simpletest prefix stored in $this->connection, and those tables are gone + // by the time shutdown is called so we need to ignore the database + // errors. There is no problem with completely ignoring errors here: if + // these queries fail, the sequence will work just fine, just use a bit + // more database storage and memory. + catch (DatabaseException $e) { + } + } + + /** + * Overridden to work around issues to MySQL not supporting transactional DDL. + */ + protected function popCommittableTransactions() { + // Commit all the committable layers. + foreach (array_reverse($this->transactionLayers) as $name => $active) { + // Stop once we found an active transaction. + if ($active) { + break; + } + + // If there are no more layers left then we should commit. + unset($this->transactionLayers[$name]); + if (empty($this->transactionLayers)) { + $this->doCommit(); + } + else { + // Attempt to release this savepoint in the standard way. + try { + $this->query('RELEASE SAVEPOINT ' . $name); + } + catch (DatabaseExceptionWrapper $e) { + // However, in MySQL (InnoDB), savepoints are automatically committed + // when tables are altered or created (DDL transactions are not + // supported). This can cause exceptions due to trying to release + // savepoints which no longer exist. + // + // To avoid exceptions when no actual error has occurred, we silently + // succeed for MySQL error code 1305 ("SAVEPOINT does not exist"). + if ($e->getPrevious()->errorInfo[1] == '1305') { + // If one SAVEPOINT was released automatically, then all were. + // Therefore, clean the transaction stack. + $this->transactionLayers = []; + // We also have to explain to PDO that the transaction stack has + // been cleaned-up. + $this->doCommit(); + } + else { + throw $e; + } + } + } + } + } + + /** + * {@inheritdoc} + */ + public function rollBack($savepoint_name = 'drupal_transaction') { + // MySQL will automatically commit transactions when tables are altered or + // created (DDL transactions are not supported). Prevent triggering an + // exception to ensure that the error that has caused the rollback is + // properly reported. + if (!$this->connection->inTransaction()) { + // On PHP 7 $this->connection->inTransaction() will return TRUE and + // $this->connection->rollback() does not throw an exception; the + // following code is unreachable. + + // If \Drupal\Core\Database\Connection::rollBack() would throw an + // exception then continue to throw an exception. + if (!$this->inTransaction()) { + throw new TransactionNoActiveException(); + } + // A previous rollback to an earlier savepoint may mean that the savepoint + // in question has already been accidentally committed. + if (!isset($this->transactionLayers[$savepoint_name])) { + throw new TransactionNoActiveException(); + } + + trigger_error('Rollback attempted when there is no active transaction. This can cause data integrity issues.', E_USER_WARNING); + return; + } + return parent::rollBack($savepoint_name); + } + + /** + * {@inheritdoc} + */ + protected function doCommit() { + // MySQL will automatically commit transactions when tables are altered or + // created (DDL transactions are not supported). Prevent triggering an + // exception in this case as all statements have been committed. + if ($this->connection->inTransaction()) { + // On PHP 7 $this->connection->inTransaction() will return TRUE and + // $this->connection->commit() does not throw an exception. + $success = parent::doCommit(); + } + else { + // Process the post-root (non-nested) transaction commit callbacks. The + // following code is copied from + // \Drupal\Core\Database\Connection::doCommit() + $success = TRUE; + if (!empty($this->rootTransactionEndCallbacks)) { + $callbacks = $this->rootTransactionEndCallbacks; + $this->rootTransactionEndCallbacks = []; + foreach ($callbacks as $callback) { + call_user_func($callback, $success); + } + } + } + return $success; + } + +} + + +/** + * @} End of "addtogroup database". + */ diff --git a/core/modules/mysql/src/Driver/Database/mysql/ExceptionHandler.php b/core/modules/mysql/src/Driver/Database/mysql/ExceptionHandler.php new file mode 100644 index 000000000000..18ca77ab258e --- /dev/null +++ b/core/modules/mysql/src/Driver/Database/mysql/ExceptionHandler.php @@ -0,0 +1,59 @@ +<?php + +namespace Drupal\mysql\Driver\Database\mysql; + +use Drupal\Component\Utility\Unicode; +use Drupal\Core\Database\DatabaseExceptionWrapper; +use Drupal\Core\Database\ExceptionHandler as BaseExceptionHandler; +use Drupal\Core\Database\IntegrityConstraintViolationException; +use Drupal\Core\Database\StatementInterface; + +/** + * MySql database exception handler class. + */ +class ExceptionHandler extends BaseExceptionHandler { + + /** + * {@inheritdoc} + */ + public function handleExecutionException(\Exception $exception, StatementInterface $statement, array $arguments = [], array $options = []): void { + if (array_key_exists('throw_exception', $options)) { + @trigger_error('Passing a \'throw_exception\' option to ' . __METHOD__ . ' is deprecated in drupal:9.2.0 and is removed in drupal:10.0.0. Always catch exceptions. See https://www.drupal.org/node/3201187', E_USER_DEPRECATED); + if (!($options['throw_exception'])) { + return; + } + } + + if ($exception instanceof \PDOException) { + // Wrap the exception in another exception, because PHP does not allow + // overriding Exception::getMessage(). Its message is the extra database + // debug information. + $code = is_int($exception->getCode()) ? $exception->getCode() : 0; + + // If a max_allowed_packet error occurs the message length is truncated. + // This should prevent the error from recurring if the exception is logged + // to the database using dblog or the like. + if (($exception->errorInfo[1] ?? NULL) === 1153) { + $message = Unicode::truncateBytes($exception->getMessage(), Connection::MIN_MAX_ALLOWED_PACKET); + throw new DatabaseExceptionWrapper($message, $code, $exception); + } + + $message = $exception->getMessage() . ": " . $statement->getQueryString() . "; " . print_r($arguments, TRUE); + + // SQLSTATE 23xxx errors indicate an integrity constraint violation. Also, + // in case of attempted INSERT of a record with an undefined column and no + // default value indicated in schema, MySql returns a 1364 error code. + if ( + substr($exception->getCode(), -6, -3) == '23' || + ($exception->errorInfo[1] ?? NULL) === 1364 + ) { + throw new IntegrityConstraintViolationException($message, $code, $exception); + } + + throw new DatabaseExceptionWrapper($message, 0, $exception); + } + + throw $exception; + } + +} diff --git a/core/modules/mysql/src/Driver/Database/mysql/Insert.php b/core/modules/mysql/src/Driver/Database/mysql/Insert.php new file mode 100644 index 000000000000..616985210398 --- /dev/null +++ b/core/modules/mysql/src/Driver/Database/mysql/Insert.php @@ -0,0 +1,65 @@ +<?php + +namespace Drupal\mysql\Driver\Database\mysql; + +use Drupal\Core\Database\Query\Insert as QueryInsert; + +/** + * MySQL implementation of \Drupal\Core\Database\Query\Insert. + */ +class Insert extends QueryInsert { + + public function execute() { + if (!$this->preExecute()) { + return NULL; + } + + // 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)) { + $max_placeholder = 0; + $values = []; + foreach ($this->insertValues as $insert_values) { + foreach ($insert_values as $value) { + $values[':db_insert_placeholder_' . $max_placeholder++] = $value; + } + } + } + else { + $values = $this->fromQuery->getArguments(); + } + + $last_insert_id = $this->connection->query((string) $this, $values, $this->queryOptions); + + // Re-initialize the values array so that we can re-use this query. + $this->insertValues = []; + + return $last_insert_id; + } + + public function __toString() { + // Create a sanitized comment string to prepend to the query. + $comments = $this->connection->makeComment($this->comments); + + // Default fields are always placed first for consistency. + $insert_fields = array_merge($this->defaultFields, $this->insertFields); + $insert_fields = array_map(function ($field) { + return $this->connection->escapeField($field); + }, $insert_fields); + + // 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)) { + $insert_fields_string = $insert_fields ? ' (' . implode(', ', $insert_fields) . ') ' : ' '; + return $comments . 'INSERT INTO {' . $this->table . '}' . $insert_fields_string . $this->fromQuery; + } + + $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES '; + + $values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields); + $query .= implode(', ', $values); + + return $query; + } + +} diff --git a/core/modules/mysql/src/Driver/Database/mysql/Install/Tasks.php b/core/modules/mysql/src/Driver/Database/mysql/Install/Tasks.php new file mode 100644 index 000000000000..a6ad642425d0 --- /dev/null +++ b/core/modules/mysql/src/Driver/Database/mysql/Install/Tasks.php @@ -0,0 +1,214 @@ +<?php + +namespace Drupal\mysql\Driver\Database\mysql\Install; + +use Drupal\Core\Database\ConnectionNotDefinedException; +use Drupal\Core\Database\Database; +use Drupal\Core\Database\Install\Tasks as InstallTasks; +use Drupal\mysql\Driver\Database\mysql\Connection; +use Drupal\Core\Database\DatabaseNotFoundException; + +/** + * Specifies installation tasks for MySQL and equivalent databases. + */ +class Tasks extends InstallTasks { + + /** + * Minimum required MySQL version. + * + * 5.7.8 is the minimum version that supports the JSON datatype. + * @see https://dev.mysql.com/doc/refman/5.7/en/json.html + */ + const MYSQL_MINIMUM_VERSION = '5.7.8'; + + /** + * Minimum required MariaDB version. + * + * 10.3.7 is the first stable (GA) release in the 10.3 series. + * @see https://mariadb.com/kb/en/changes-improvements-in-mariadb-103/#list-of-all-mariadb-103-releases + */ + const MARIADB_MINIMUM_VERSION = '10.3.7'; + + /** + * Minimum required MySQLnd version. + */ + const MYSQLND_MINIMUM_VERSION = '5.0.9'; + + /** + * Minimum required libmysqlclient version. + */ + const LIBMYSQLCLIENT_MINIMUM_VERSION = '5.5.3'; + + /** + * The PDO driver name for MySQL and equivalent databases. + * + * @var string + */ + protected $pdoDriver = 'mysql'; + + /** + * Constructs a \Drupal\mysql\Driver\Database\mysql\Install\Tasks object. + */ + public function __construct() { + $this->tasks[] = [ + 'arguments' => [], + 'function' => 'ensureInnoDbAvailable', + ]; + } + + /** + * {@inheritdoc} + */ + public function name() { + try { + if (!$this->isConnectionActive() || !$this->getConnection() instanceof Connection) { + throw new ConnectionNotDefinedException('The database connection is not active or not a MySql connection'); + } + if ($this->getConnection()->isMariaDb()) { + return $this->t('MariaDB'); + } + return $this->t('MySQL, Percona Server, or equivalent'); + } + catch (ConnectionNotDefinedException $e) { + return $this->t('MySQL, MariaDB, Percona Server, or equivalent'); + } + } + + /** + * {@inheritdoc} + */ + public function minimumVersion() { + if ($this->getConnection()->isMariaDb()) { + return static::MARIADB_MINIMUM_VERSION; + } + return static::MYSQL_MINIMUM_VERSION; + } + + /** + * {@inheritdoc} + */ + protected function connect() { + try { + // This doesn't actually test the connection. + Database::setActiveConnection(); + // Now actually do a check. + try { + Database::getConnection(); + } + catch (\Exception $e) { + // Detect utf8mb4 incompatibility. + if ($e->getCode() == Connection::UNSUPPORTED_CHARSET || ($e->getCode() == Connection::SQLSTATE_SYNTAX_ERROR && $e->errorInfo[1] == Connection::UNKNOWN_CHARSET)) { + $this->fail(t('Your MySQL server and PHP MySQL driver must support utf8mb4 character encoding. Make sure to use a database system that supports this (such as MySQL/MariaDB/Percona 5.5.3 and up), and that the utf8mb4 character set is compiled in. See the <a href=":documentation" target="_blank">MySQL documentation</a> for more information.', [':documentation' => 'https://dev.mysql.com/doc/refman/5.0/en/cannot-initialize-character-set.html'])); + $info = Database::getConnectionInfo(); + $info_copy = $info; + // Set a flag to fall back to utf8. Note: this flag should only be + // used here and is for internal use only. + $info_copy['default']['_dsn_utf8_fallback'] = TRUE; + // In order to change the Database::$databaseInfo array, we need to + // remove the active connection, then re-add it with the new info. + Database::removeConnection('default'); + Database::addConnectionInfo('default', 'default', $info_copy['default']); + // Connect with the new database info, using the utf8 character set so + // that we can run the checkEngineVersion test. + Database::getConnection(); + // Revert to the old settings. + Database::removeConnection('default'); + Database::addConnectionInfo('default', 'default', $info['default']); + } + else { + // Rethrow the exception. + throw $e; + } + } + $this->pass('Drupal can CONNECT to the database ok.'); + } + catch (\Exception $e) { + // Attempt to create the database if it is not found. + if ($e->getCode() == Connection::DATABASE_NOT_FOUND) { + // Remove the database string from connection info. + $connection_info = Database::getConnectionInfo(); + $database = $connection_info['default']['database']; + unset($connection_info['default']['database']); + + // In order to change the Database::$databaseInfo array, need to remove + // the active connection, then re-add it with the new info. + Database::removeConnection('default'); + Database::addConnectionInfo('default', 'default', $connection_info['default']); + + try { + // Now, attempt the connection again; if it's successful, attempt to + // create the database. + Database::getConnection()->createDatabase($database); + Database::closeConnection(); + + // Now, restore the database config. + Database::removeConnection('default'); + $connection_info['default']['database'] = $database; + Database::addConnectionInfo('default', 'default', $connection_info['default']); + + // Check the database connection. + Database::getConnection(); + $this->pass('Drupal can CONNECT to the database ok.'); + } + catch (DatabaseNotFoundException $e) { + // Still no dice; probably a permission issue. Raise the error to the + // installer. + $this->fail(t('Database %database not found. The server reports the following message when attempting to create the database: %error.', ['%database' => $database, '%error' => $e->getMessage()])); + } + } + else { + // Database connection failed for some other reason than a non-existent + // database. + $this->fail(t('Failed to connect to your database server. The server reports the following message: %error.<ul><li>Is the database server running?</li><li>Does the database exist or does the database user have sufficient privileges to create the database?</li><li>Have you entered the correct database name?</li><li>Have you entered the correct username and password?</li><li>Have you entered the correct database hostname and port number?</li></ul>', ['%error' => $e->getMessage()])); + return FALSE; + } + } + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function getFormOptions(array $database) { + $form = parent::getFormOptions($database); + if (empty($form['advanced_options']['port']['#default_value'])) { + $form['advanced_options']['port']['#default_value'] = '3306'; + } + + return $form; + } + + /** + * Ensure that InnoDB is available. + */ + public function ensureInnoDbAvailable() { + $engines = Database::getConnection()->query('SHOW ENGINES')->fetchAllKeyed(); + if (isset($engines['MyISAM']) && $engines['MyISAM'] == 'DEFAULT' && !isset($engines['InnoDB'])) { + $this->fail(t('The MyISAM storage engine is not supported.')); + } + } + + /** + * {@inheritdoc} + */ + protected function checkEngineVersion() { + parent::checkEngineVersion(); + + // Ensure that the MySQL driver supports utf8mb4 encoding. + $version = Database::getConnection()->clientVersion(); + if (FALSE !== strpos($version, 'mysqlnd')) { + // The mysqlnd driver supports utf8mb4 starting at version 5.0.9. + $version = preg_replace('/^\D+([\d.]+).*/', '$1', $version); + if (version_compare($version, self::MYSQLND_MINIMUM_VERSION, '<')) { + $this->fail(t("The MySQLnd driver version %version is less than the minimum required version. Upgrade to MySQLnd version %mysqlnd_minimum_version or up, or alternatively switch mysql drivers to libmysqlclient version %libmysqlclient_minimum_version or up.", ['%version' => $version, '%mysqlnd_minimum_version' => self::MYSQLND_MINIMUM_VERSION, '%libmysqlclient_minimum_version' => self::LIBMYSQLCLIENT_MINIMUM_VERSION])); + } + } + else { + // The libmysqlclient driver supports utf8mb4 starting at version 5.5.3. + if (version_compare($version, self::LIBMYSQLCLIENT_MINIMUM_VERSION, '<')) { + $this->fail(t("The libmysqlclient driver version %version is less than the minimum required version. Upgrade to libmysqlclient version %libmysqlclient_minimum_version or up, or alternatively switch mysql drivers to MySQLnd version %mysqlnd_minimum_version or up.", ['%version' => $version, '%libmysqlclient_minimum_version' => self::LIBMYSQLCLIENT_MINIMUM_VERSION, '%mysqlnd_minimum_version' => self::MYSQLND_MINIMUM_VERSION])); + } + } + } + +} diff --git a/core/modules/mysql/src/Driver/Database/mysql/Schema.php b/core/modules/mysql/src/Driver/Database/mysql/Schema.php new file mode 100644 index 000000000000..2771e9763594 --- /dev/null +++ b/core/modules/mysql/src/Driver/Database/mysql/Schema.php @@ -0,0 +1,715 @@ +<?php + +namespace Drupal\mysql\Driver\Database\mysql; + +use Drupal\Core\Database\SchemaException; +use Drupal\Core\Database\SchemaObjectExistsException; +use Drupal\Core\Database\SchemaObjectDoesNotExistException; +use Drupal\Core\Database\Schema as DatabaseSchema; +use Drupal\Component\Utility\Unicode; + +/** + * @addtogroup schemaapi + * @{ + */ + +/** + * MySQL implementation of \Drupal\Core\Database\Schema. + */ +class Schema extends DatabaseSchema { + + /** + * Maximum length of a table comment in MySQL. + */ + const COMMENT_MAX_TABLE = 60; + + /** + * Maximum length of a column comment in MySQL. + */ + const COMMENT_MAX_COLUMN = 255; + + /** + * @var array + * List of MySQL string types. + */ + protected $mysqlStringTypes = [ + 'VARCHAR', + 'CHAR', + 'TINYTEXT', + 'MEDIUMTEXT', + 'LONGTEXT', + 'TEXT', + ]; + + /** + * Get information about the table and database name from the prefix. + * + * @return + * A keyed array with information about the database, table name and prefix. + */ + protected function getPrefixInfo($table = 'default', $add_prefix = TRUE) { + $info = ['prefix' => $this->connection->tablePrefix($table)]; + if ($add_prefix) { + $table = $info['prefix'] . $table; + } + if (($pos = strpos($table, '.')) !== FALSE) { + $info['database'] = substr($table, 0, $pos); + $info['table'] = substr($table, ++$pos); + } + else { + $info['database'] = $this->connection->getConnectionOptions()['database']; + $info['table'] = $table; + } + return $info; + } + + /** + * Build a condition to match a table name against a standard information_schema. + * + * MySQL uses databases like schemas rather than catalogs so when we build + * a condition to query the information_schema.tables, we set the default + * database as the schema unless specified otherwise, and exclude table_catalog + * from the condition criteria. + */ + protected function buildTableNameCondition($table_name, $operator = '=', $add_prefix = TRUE) { + $table_info = $this->getPrefixInfo($table_name, $add_prefix); + + $condition = $this->connection->condition('AND'); + $condition->condition('table_schema', $table_info['database']); + $condition->condition('table_name', $table_info['table'], $operator); + return $condition; + } + + /** + * Generate SQL to create a new table from a Drupal schema definition. + * + * @param $name + * The name of the table to create. + * @param $table + * A Schema API table definition array. + * + * @return + * An array of SQL statements to create the table. + */ + protected function createTableSql($name, $table) { + $info = $this->connection->getConnectionOptions(); + + // Provide defaults if needed. + $table += [ + 'mysql_engine' => 'InnoDB', + 'mysql_character_set' => 'utf8mb4', + ]; + + $sql = "CREATE TABLE {" . $name . "} (\n"; + + // Add the SQL statement for each field. + foreach ($table['fields'] as $field_name => $field) { + $sql .= $this->createFieldSql($field_name, $this->processField($field)) . ", \n"; + } + + // Process keys & indexes. + if (!empty($table['primary key']) && is_array($table['primary key'])) { + $this->ensureNotNullPrimaryKey($table['primary key'], $table['fields']); + } + $keys = $this->createKeysSql($table); + if (count($keys)) { + $sql .= implode(", \n", $keys) . ", \n"; + } + + // Remove the last comma and space. + $sql = substr($sql, 0, -3) . "\n) "; + + $sql .= 'ENGINE = ' . $table['mysql_engine'] . ' DEFAULT CHARACTER SET ' . $table['mysql_character_set']; + // By default, MySQL uses the default collation for new tables, which is + // 'utf8mb4_general_ci' (MySQL 5) or 'utf8mb4_0900_ai_ci' (MySQL 8) for + // utf8mb4. If an alternate collation has been set, it needs to be + // explicitly specified. + // @see \Drupal\mysql\Driver\Database\mysql\Schema + if (!empty($info['collation'])) { + $sql .= ' COLLATE ' . $info['collation']; + } + + // Add table comment. + if (!empty($table['description'])) { + $sql .= ' COMMENT ' . $this->prepareComment($table['description'], self::COMMENT_MAX_TABLE); + } + + return [$sql]; + } + + /** + * Create an SQL string for a field to be used in table creation or alteration. + * + * @param string $name + * Name of the field. + * @param array $spec + * The field specification, as per the schema data structure format. + */ + protected function createFieldSql($name, $spec) { + $sql = "`" . $name . "` " . $spec['mysql_type']; + + if (in_array($spec['mysql_type'], $this->mysqlStringTypes)) { + if (isset($spec['length'])) { + $sql .= '(' . $spec['length'] . ')'; + } + if (isset($spec['type']) && $spec['type'] == 'varchar_ascii') { + $sql .= ' CHARACTER SET ascii'; + } + if (!empty($spec['binary'])) { + $sql .= ' BINARY'; + } + // Note we check for the "type" key here. "mysql_type" is VARCHAR: + elseif (isset($spec['type']) && $spec['type'] == 'varchar_ascii') { + $sql .= ' COLLATE ascii_general_ci'; + } + } + elseif (isset($spec['precision']) && isset($spec['scale'])) { + $sql .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')'; + } + + if (!empty($spec['unsigned'])) { + $sql .= ' unsigned'; + } + + if (isset($spec['not null'])) { + if ($spec['not null']) { + $sql .= ' NOT NULL'; + } + else { + $sql .= ' NULL'; + } + } + + if (!empty($spec['auto_increment'])) { + $sql .= ' auto_increment'; + } + + // $spec['default'] can be NULL, so we explicitly check for the key here. + if (array_key_exists('default', $spec)) { + $sql .= ' DEFAULT ' . $this->escapeDefaultValue($spec['default']); + } + + if (empty($spec['not null']) && !isset($spec['default'])) { + $sql .= ' DEFAULT NULL'; + } + + // Add column comment. + if (!empty($spec['description'])) { + $sql .= ' COMMENT ' . $this->prepareComment($spec['description'], self::COMMENT_MAX_COLUMN); + } + + return $sql; + } + + /** + * Set database-engine specific properties for a field. + * + * @param $field + * A field description array, as specified in the schema documentation. + */ + protected function processField($field) { + + if (!isset($field['size'])) { + $field['size'] = 'normal'; + } + + // Set the correct database-engine specific datatype. + // In case one is already provided, force it to uppercase. + if (isset($field['mysql_type'])) { + $field['mysql_type'] = mb_strtoupper($field['mysql_type']); + } + else { + $map = $this->getFieldTypeMap(); + $field['mysql_type'] = $map[$field['type'] . ':' . $field['size']]; + } + + if (isset($field['type']) && $field['type'] == 'serial') { + $field['auto_increment'] = TRUE; + } + + return $field; + } + + /** + * {@inheritdoc} + */ + public function getFieldTypeMap() { + // Put :normal last so it gets preserved by array_flip. This makes + // it much easier for modules (such as schema.module) to map + // database types back into schema types. + // $map does not use drupal_static as its value never changes. + static $map = [ + 'varchar_ascii:normal' => 'VARCHAR', + + 'varchar:normal' => 'VARCHAR', + 'char:normal' => 'CHAR', + + 'text:tiny' => 'TINYTEXT', + 'text:small' => 'TINYTEXT', + 'text:medium' => 'MEDIUMTEXT', + 'text:big' => 'LONGTEXT', + 'text:normal' => 'TEXT', + + 'serial:tiny' => 'TINYINT', + 'serial:small' => 'SMALLINT', + 'serial:medium' => 'MEDIUMINT', + 'serial:big' => 'BIGINT', + 'serial:normal' => 'INT', + + 'int:tiny' => 'TINYINT', + 'int:small' => 'SMALLINT', + 'int:medium' => 'MEDIUMINT', + 'int:big' => 'BIGINT', + 'int:normal' => 'INT', + + 'float:tiny' => 'FLOAT', + 'float:small' => 'FLOAT', + 'float:medium' => 'FLOAT', + 'float:big' => 'DOUBLE', + 'float:normal' => 'FLOAT', + + 'numeric:normal' => 'DECIMAL', + + 'blob:big' => 'LONGBLOB', + 'blob:normal' => 'BLOB', + ]; + return $map; + } + + protected function createKeysSql($spec) { + $keys = []; + + if (!empty($spec['primary key'])) { + $keys[] = 'PRIMARY KEY (' . $this->createKeySql($spec['primary key']) . ')'; + } + if (!empty($spec['unique keys'])) { + foreach ($spec['unique keys'] as $key => $fields) { + $keys[] = 'UNIQUE KEY `' . $key . '` (' . $this->createKeySql($fields) . ')'; + } + } + if (!empty($spec['indexes'])) { + $indexes = $this->getNormalizedIndexes($spec); + foreach ($indexes as $index => $fields) { + $keys[] = 'INDEX `' . $index . '` (' . $this->createKeySql($fields) . ')'; + } + } + + return $keys; + } + + /** + * Gets normalized indexes from a table specification. + * + * Shortens indexes to 191 characters if they apply to utf8mb4-encoded + * fields, in order to comply with the InnoDB index limitation of 756 bytes. + * + * @param array $spec + * The table specification. + * + * @return array + * List of shortened indexes. + * + * @throws \Drupal\Core\Database\SchemaException + * Thrown if field specification is missing. + */ + protected function getNormalizedIndexes(array $spec) { + $indexes = $spec['indexes'] ?? []; + foreach ($indexes as $index_name => $index_fields) { + foreach ($index_fields as $index_key => $index_field) { + // Get the name of the field from the index specification. + $field_name = is_array($index_field) ? $index_field[0] : $index_field; + // Check whether the field is defined in the table specification. + if (isset($spec['fields'][$field_name])) { + // Get the MySQL type from the processed field. + $mysql_field = $this->processField($spec['fields'][$field_name]); + if (in_array($mysql_field['mysql_type'], $this->mysqlStringTypes)) { + // Check whether we need to shorten the index. + if ((!isset($mysql_field['type']) || $mysql_field['type'] != 'varchar_ascii') && (!isset($mysql_field['length']) || $mysql_field['length'] > 191)) { + // Limit the index length to 191 characters. + $this->shortenIndex($indexes[$index_name][$index_key]); + } + } + } + else { + throw new SchemaException("MySQL needs the '$field_name' field specification in order to normalize the '$index_name' index"); + } + } + } + return $indexes; + } + + /** + * Helper function for normalizeIndexes(). + * + * Shortens an index to 191 characters. + * + * @param array $index + * The index array to be used in createKeySql. + * + * @see Drupal\mysql\Driver\Database\mysql\Schema::createKeySql() + * @see Drupal\mysql\Driver\Database\mysql\Schema::normalizeIndexes() + */ + protected function shortenIndex(&$index) { + if (is_array($index)) { + if ($index[1] > 191) { + $index[1] = 191; + } + } + else { + $index = [$index, 191]; + } + } + + protected function createKeySql($fields) { + $return = []; + foreach ($fields as $field) { + if (is_array($field)) { + $return[] = '`' . $field[0] . '`(' . $field[1] . ')'; + } + else { + $return[] = '`' . $field . '`'; + } + } + return implode(', ', $return); + } + + /** + * {@inheritdoc} + */ + public function renameTable($table, $new_name) { + if (!$this->tableExists($table)) { + throw new SchemaObjectDoesNotExistException("Cannot rename '$table' to '$new_name': table '$table' doesn't exist."); + } + if ($this->tableExists($new_name)) { + throw new SchemaObjectExistsException("Cannot rename '$table' to '$new_name': table '$new_name' already exists."); + } + + $info = $this->getPrefixInfo($new_name); + $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO `' . $info['table'] . '`'); + } + + /** + * {@inheritdoc} + */ + public function dropTable($table) { + if (!$this->tableExists($table)) { + return FALSE; + } + + $this->connection->query('DROP TABLE {' . $table . '}'); + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function addField($table, $field, $spec, $keys_new = []) { + if (!$this->tableExists($table)) { + throw new SchemaObjectDoesNotExistException("Cannot add field '$table.$field': table doesn't exist."); + } + if ($this->fieldExists($table, $field)) { + throw new SchemaObjectExistsException("Cannot add field '$table.$field': field already exists."); + } + + // Fields that are part of a PRIMARY KEY must be added as NOT NULL. + $is_primary_key = isset($keys_new['primary key']) && in_array($field, $keys_new['primary key'], TRUE); + if ($is_primary_key) { + $this->ensureNotNullPrimaryKey($keys_new['primary key'], [$field => $spec]); + } + + $fixnull = FALSE; + if (!empty($spec['not null']) && !isset($spec['default']) && !$is_primary_key) { + $fixnull = TRUE; + $spec['not null'] = FALSE; + } + $query = 'ALTER TABLE {' . $table . '} ADD '; + $query .= $this->createFieldSql($field, $this->processField($spec)); + if ($keys_sql = $this->createKeysSql($keys_new)) { + // Make sure to drop the existing primary key before adding a new one. + // This is only needed when adding a field because this method, unlike + // changeField(), is supposed to handle primary keys automatically. + if (isset($keys_new['primary key']) && $this->indexExists($table, 'PRIMARY')) { + $query .= ', DROP PRIMARY KEY'; + } + + $query .= ', ADD ' . implode(', ADD ', $keys_sql); + } + $this->connection->query($query); + if (isset($spec['initial_from_field'])) { + if (isset($spec['initial'])) { + $expression = 'COALESCE(' . $spec['initial_from_field'] . ', :default_initial_value)'; + $arguments = [':default_initial_value' => $spec['initial']]; + } + else { + $expression = $spec['initial_from_field']; + $arguments = []; + } + $this->connection->update($table) + ->expression($field, $expression, $arguments) + ->execute(); + } + elseif (isset($spec['initial'])) { + $this->connection->update($table) + ->fields([$field => $spec['initial']]) + ->execute(); + } + if ($fixnull) { + $spec['not null'] = TRUE; + $this->changeField($table, $field, $field, $spec); + } + } + + /** + * {@inheritdoc} + */ + public function dropField($table, $field) { + if (!$this->fieldExists($table, $field)) { + return FALSE; + } + + // When dropping a field that is part of a composite primary key MySQL + // automatically removes the field from the primary key, which can leave the + // table in an invalid state. MariaDB 10.2.8 requires explicitly dropping + // the primary key first for this reason. We perform this deletion + // explicitly which also makes the behavior on both MySQL and MariaDB + // consistent with PostgreSQL. + // @see https://mariadb.com/kb/en/library/alter-table + $primary_key = $this->findPrimaryKeyColumns($table); + if ((count($primary_key) > 1) && in_array($field, $primary_key, TRUE)) { + $this->dropPrimaryKey($table); + } + + $this->connection->query('ALTER TABLE {' . $table . '} DROP `' . $field . '`'); + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function indexExists($table, $name) { + // Returns one row for each column in the index. Result is string or FALSE. + // Details at http://dev.mysql.com/doc/refman/5.0/en/show-index.html + $row = $this->connection->query('SHOW INDEX FROM {' . $table . '} WHERE key_name = ' . $this->connection->quote($name))->fetchAssoc(); + return isset($row['Key_name']); + } + + /** + * {@inheritdoc} + */ + public function addPrimaryKey($table, $fields) { + if (!$this->tableExists($table)) { + throw new SchemaObjectDoesNotExistException("Cannot add primary key to table '$table': table doesn't exist."); + } + if ($this->indexExists($table, 'PRIMARY')) { + throw new SchemaObjectExistsException("Cannot add primary key to table '$table': primary key already exists."); + } + + $this->connection->query('ALTER TABLE {' . $table . '} ADD PRIMARY KEY (' . $this->createKeySql($fields) . ')'); + } + + /** + * {@inheritdoc} + */ + public function dropPrimaryKey($table) { + if (!$this->indexExists($table, 'PRIMARY')) { + return FALSE; + } + + $this->connection->query('ALTER TABLE {' . $table . '} DROP PRIMARY KEY'); + return TRUE; + } + + /** + * {@inheritdoc} + */ + protected function findPrimaryKeyColumns($table) { + if (!$this->tableExists($table)) { + return FALSE; + } + $result = $this->connection->query("SHOW KEYS FROM {" . $table . "} WHERE Key_name = 'PRIMARY'")->fetchAllAssoc('Column_name'); + return array_keys($result); + } + + /** + * {@inheritdoc} + */ + public function addUniqueKey($table, $name, $fields) { + if (!$this->tableExists($table)) { + throw new SchemaObjectDoesNotExistException("Cannot add unique key '$name' to table '$table': table doesn't exist."); + } + if ($this->indexExists($table, $name)) { + throw new SchemaObjectExistsException("Cannot add unique key '$name' to table '$table': unique key already exists."); + } + + $this->connection->query('ALTER TABLE {' . $table . '} ADD UNIQUE KEY `' . $name . '` (' . $this->createKeySql($fields) . ')'); + } + + /** + * {@inheritdoc} + */ + public function dropUniqueKey($table, $name) { + if (!$this->indexExists($table, $name)) { + return FALSE; + } + + $this->connection->query('ALTER TABLE {' . $table . '} DROP KEY `' . $name . '`'); + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function addIndex($table, $name, $fields, array $spec) { + if (!$this->tableExists($table)) { + throw new SchemaObjectDoesNotExistException("Cannot add index '$name' to table '$table': table doesn't exist."); + } + if ($this->indexExists($table, $name)) { + throw new SchemaObjectExistsException("Cannot add index '$name' to table '$table': index already exists."); + } + + $spec['indexes'][$name] = $fields; + $indexes = $this->getNormalizedIndexes($spec); + + $this->connection->query('ALTER TABLE {' . $table . '} ADD INDEX `' . $name . '` (' . $this->createKeySql($indexes[$name]) . ')'); + } + + /** + * {@inheritdoc} + */ + public function dropIndex($table, $name) { + if (!$this->indexExists($table, $name)) { + return FALSE; + } + + $this->connection->query('ALTER TABLE {' . $table . '} DROP INDEX `' . $name . '`'); + return TRUE; + } + + /** + * {@inheritdoc} + */ + protected function introspectIndexSchema($table) { + if (!$this->tableExists($table)) { + throw new SchemaObjectDoesNotExistException("The table $table doesn't exist."); + } + + $index_schema = [ + 'primary key' => [], + 'unique keys' => [], + 'indexes' => [], + ]; + + $result = $this->connection->query('SHOW INDEX FROM {' . $table . '}')->fetchAll(); + foreach ($result as $row) { + if ($row->Key_name === 'PRIMARY') { + $index_schema['primary key'][] = $row->Column_name; + } + elseif ($row->Non_unique == 0) { + $index_schema['unique keys'][$row->Key_name][] = $row->Column_name; + } + else { + $index_schema['indexes'][$row->Key_name][] = $row->Column_name; + } + } + + return $index_schema; + } + + /** + * {@inheritdoc} + */ + public function changeField($table, $field, $field_new, $spec, $keys_new = []) { + if (!$this->fieldExists($table, $field)) { + throw new SchemaObjectDoesNotExistException("Cannot change the definition of field '$table.$field': field doesn't exist."); + } + if (($field != $field_new) && $this->fieldExists($table, $field_new)) { + throw new SchemaObjectExistsException("Cannot rename field '$table.$field' to '$field_new': target field already exists."); + } + if (isset($keys_new['primary key']) && in_array($field_new, $keys_new['primary key'], TRUE)) { + $this->ensureNotNullPrimaryKey($keys_new['primary key'], [$field_new => $spec]); + } + + $sql = 'ALTER TABLE {' . $table . '} CHANGE `' . $field . '` ' . $this->createFieldSql($field_new, $this->processField($spec)); + if ($keys_sql = $this->createKeysSql($keys_new)) { + $sql .= ', ADD ' . implode(', ADD ', $keys_sql); + } + $this->connection->query($sql); + } + + /** + * {@inheritdoc} + */ + public function prepareComment($comment, $length = NULL) { + // Truncate comment to maximum comment length. + if (isset($length)) { + // Add table prefixes before truncating. + $comment = Unicode::truncate($this->connection->prefixTables($comment), $length, TRUE, TRUE); + } + // Remove semicolons to avoid triggering multi-statement check. + $comment = strtr($comment, [';' => '.']); + return $this->connection->quote($comment); + } + + /** + * Retrieve a table or column comment. + */ + public function getComment($table, $column = NULL) { + $condition = $this->buildTableNameCondition($table); + if (isset($column)) { + $condition->condition('column_name', $column); + $condition->compile($this->connection, $this); + // Don't use {} around information_schema.columns table. + return $this->connection->query("SELECT column_comment AS column_comment FROM information_schema.columns WHERE " . (string) $condition, $condition->arguments())->fetchField(); + } + $condition->compile($this->connection, $this); + // Don't use {} around information_schema.tables table. + $comment = $this->connection->query("SELECT table_comment AS table_comment FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchField(); + // Work-around for MySQL 5.0 bug http://bugs.mysql.com/bug.php?id=11379 + return preg_replace('/; InnoDB free:.*$/', '', $comment); + } + + /** + * {@inheritdoc} + */ + public function tableExists($table) { + // The information_schema table is very slow to query under MySQL 5.0. + // Instead, we try to select from the table in question. If it fails, + // the most likely reason is that it does not exist. That is dramatically + // faster than using information_schema. + // @link http://bugs.mysql.com/bug.php?id=19588 + // @todo This override should be removed once we require a version of MySQL + // that has that bug fixed. + try { + $this->connection->queryRange("SELECT 1 FROM {" . $table . "}", 0, 1); + return TRUE; + } + catch (\Exception $e) { + return FALSE; + } + } + + /** + * {@inheritdoc} + */ + public function fieldExists($table, $column) { + // The information_schema table is very slow to query under MySQL 5.0. + // Instead, we try to select from the table and field in question. If it + // fails, the most likely reason is that it does not exist. That is + // dramatically faster than using information_schema. + // @link http://bugs.mysql.com/bug.php?id=19588 + // @todo This override should be removed once we require a version of MySQL + // that has that bug fixed. + try { + $this->connection->queryRange("SELECT $column FROM {" . $table . "}", 0, 1); + return TRUE; + } + catch (\Exception $e) { + return FALSE; + } + } + +} + +/** + * @} End of "addtogroup schemaapi". + */ diff --git a/core/modules/mysql/src/Driver/Database/mysql/Upsert.php b/core/modules/mysql/src/Driver/Database/mysql/Upsert.php new file mode 100644 index 000000000000..0e5f7d3b50e9 --- /dev/null +++ b/core/modules/mysql/src/Driver/Database/mysql/Upsert.php @@ -0,0 +1,43 @@ +<?php + +namespace Drupal\mysql\Driver\Database\mysql; + +use Drupal\Core\Database\Query\Upsert as QueryUpsert; + +/** + * MySQL implementation of \Drupal\Core\Database\Query\Upsert. + */ +class Upsert extends QueryUpsert { + + /** + * {@inheritdoc} + */ + public function __toString() { + // Create a sanitized comment string to prepend to the query. + $comments = $this->connection->makeComment($this->comments); + + // Default fields are always placed first for consistency. + $insert_fields = array_merge($this->defaultFields, $this->insertFields); + $insert_fields = array_map(function ($field) { + return $this->connection->escapeField($field); + }, $insert_fields); + + $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES '; + + $values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields); + $query .= implode(', ', $values); + + // Updating the unique / primary key is not necessary. + unset($insert_fields[$this->key]); + + $update = []; + foreach ($insert_fields as $field) { + $update[] = "$field = VALUES($field)"; + } + + $query .= ' ON DUPLICATE KEY UPDATE ' . implode(', ', $update); + + return $query; + } + +} diff --git a/core/modules/pgsql/pgsql.info.yml b/core/modules/pgsql/pgsql.info.yml new file mode 100644 index 000000000000..93ad5d8e1fa0 --- /dev/null +++ b/core/modules/pgsql/pgsql.info.yml @@ -0,0 +1,5 @@ +name: PostgreSQL +type: module +description: 'Database driver for PostgreSQL.' +package: Core +version: VERSION diff --git a/core/modules/pgsql/pgsql.module b/core/modules/pgsql/pgsql.module new file mode 100644 index 000000000000..4d9027bc432f --- /dev/null +++ b/core/modules/pgsql/pgsql.module @@ -0,0 +1,22 @@ +<?php + +/** + * @file + * The PostgreSQL module provides the connection between Drupal and a PostgreSQL database. + */ + +use Drupal\Core\Routing\RouteMatchInterface; + +/** + * Implements hook_help(). + */ +function pgsql_help($route_name, RouteMatchInterface $route_match) { + switch ($route_name) { + case 'help.page.pgsql': + $output = ''; + $output .= '<h3>' . t('About') . '</h3>'; + $output .= '<p>' . t('The PostgreSQL module provides the connection between Drupal and a PostgreSQL database. For more information, see the <a href=":pgsql">online documentation for the PostgreSQL module</a>.', [':pgsql' => 'https://www.drupal.org/documentation/modules/pgsql']) . '</p>'; + return $output; + + } +} diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Connection.php b/core/modules/pgsql/src/Driver/Database/pgsql/Connection.php new file mode 100644 index 000000000000..fc2ab6508f8b --- /dev/null +++ b/core/modules/pgsql/src/Driver/Database/pgsql/Connection.php @@ -0,0 +1,375 @@ +<?php + +namespace Drupal\pgsql\Driver\Database\pgsql; + +use Drupal\Core\Database\Database; +use Drupal\Core\Database\Connection as DatabaseConnection; +use Drupal\Core\Database\DatabaseAccessDeniedException; +use Drupal\Core\Database\DatabaseNotFoundException; +use Drupal\Core\Database\StatementInterface; +use Drupal\Core\Database\StatementWrapper; + +// cSpell:ignore ilike nextval + +/** + * @addtogroup database + * @{ + */ + +/** + * PostgreSQL implementation of \Drupal\Core\Database\Connection. + */ +class Connection extends DatabaseConnection { + + /** + * The name by which to obtain a lock for retrieve the next insert id. + */ + const POSTGRESQL_NEXTID_LOCK = 1000; + + /** + * Error code for "Unknown database" error. + */ + const DATABASE_NOT_FOUND = 7; + + /** + * Error code for "Connection failure" errors. + * + * Technically this is an internal error code that will only be shown in the + * PDOException message. It will need to get extracted. + */ + const CONNECTION_FAILURE = '08006'; + + /** + * {@inheritdoc} + */ + protected $statementClass = NULL; + + /** + * {@inheritdoc} + */ + protected $statementWrapperClass = StatementWrapper::class; + + /** + * A map of condition operators to PostgreSQL operators. + * + * In PostgreSQL, 'LIKE' is case-sensitive. ILIKE should be used for + * case-insensitive statements. + */ + protected static $postgresqlConditionOperatorMap = [ + 'LIKE' => ['operator' => 'ILIKE'], + 'LIKE BINARY' => ['operator' => 'LIKE'], + 'NOT LIKE' => ['operator' => 'NOT ILIKE'], + 'REGEXP' => ['operator' => '~*'], + 'NOT REGEXP' => ['operator' => '!~*'], + ]; + + /** + * {@inheritdoc} + */ + protected $transactionalDDLSupport = TRUE; + + /** + * {@inheritdoc} + */ + protected $identifierQuotes = ['"', '"']; + + /** + * Constructs a connection object. + */ + public function __construct(\PDO $connection, array $connection_options) { + parent::__construct($connection, $connection_options); + + // Force PostgreSQL to use the UTF-8 character set by default. + $this->connection->exec("SET NAMES 'UTF8'"); + + // Execute PostgreSQL init_commands. + if (isset($connection_options['init_commands'])) { + $this->connection->exec(implode('; ', $connection_options['init_commands'])); + } + } + + /** + * {@inheritdoc} + */ + public static function open(array &$connection_options = []) { + // Default to TCP connection on port 5432. + if (empty($connection_options['port'])) { + $connection_options['port'] = 5432; + } + + // PostgreSQL in trust mode doesn't require a password to be supplied. + if (empty($connection_options['password'])) { + $connection_options['password'] = NULL; + } + // If the password contains a backslash it is treated as an escape character + // http://bugs.php.net/bug.php?id=53217 + // so backslashes in the password need to be doubled up. + // The bug was reported against pdo_pgsql 1.0.2, backslashes in passwords + // will break on this doubling up when the bug is fixed, so check the version + // elseif (phpversion('pdo_pgsql') < 'version_this_was_fixed_in') { + else { + $connection_options['password'] = str_replace('\\', '\\\\', $connection_options['password']); + } + + $connection_options['database'] = (!empty($connection_options['database']) ? $connection_options['database'] : 'template1'); + $dsn = 'pgsql:host=' . $connection_options['host'] . ' dbname=' . $connection_options['database'] . ' port=' . $connection_options['port']; + + // Allow PDO options to be overridden. + $connection_options += [ + 'pdo' => [], + ]; + $connection_options['pdo'] += [ + \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, + // Prepared statements are most effective for performance when queries + // are recycled (used several times). However, if they are not re-used, + // prepared statements become inefficient. Since most of Drupal's + // prepared queries are not re-used, it should be faster to emulate + // the preparation than to actually ready statements for re-use. If in + // doubt, reset to FALSE and measure performance. + \PDO::ATTR_EMULATE_PREPARES => TRUE, + // Convert numeric values to strings when fetching. + \PDO::ATTR_STRINGIFY_FETCHES => TRUE, + ]; + + try { + $pdo = new \PDO($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']); + } + catch (\PDOException $e) { + if (static::getSQLState($e) == static::CONNECTION_FAILURE) { + if (strpos($e->getMessage(), 'password authentication failed for user') !== FALSE) { + throw new DatabaseAccessDeniedException($e->getMessage(), $e->getCode(), $e); + } + elseif (strpos($e->getMessage(), 'database') !== FALSE && strpos($e->getMessage(), 'does not exist') !== FALSE) { + throw new DatabaseNotFoundException($e->getMessage(), $e->getCode(), $e); + } + } + throw $e; + } + + return $pdo; + } + + /** + * {@inheritdoc} + */ + public function query($query, array $args = [], $options = []) { + $options += $this->defaultOptions(); + + // The PDO PostgreSQL driver has a bug which doesn't type cast booleans + // correctly when parameters are bound using associative arrays. + // @see http://bugs.php.net/bug.php?id=48383 + foreach ($args as &$value) { + if (is_bool($value)) { + $value = (int) $value; + } + } + + // We need to wrap queries with a savepoint if: + // - Currently in a transaction. + // - A 'mimic_implicit_commit' does not exist already. + // - The query is not a savepoint query. + $wrap_with_savepoint = $this->inTransaction() && + !isset($this->transactionLayers['mimic_implicit_commit']) && + !(is_string($query) && ( + stripos($query, 'ROLLBACK TO SAVEPOINT ') === 0 || + stripos($query, 'RELEASE SAVEPOINT ') === 0 || + stripos($query, 'SAVEPOINT ') === 0 + ) + ); + if ($wrap_with_savepoint) { + // 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 + // fails. This is important for tables that are created on demand. For + // example, \Drupal\Core\Cache\DatabaseBackend. + $this->addSavepoint(); + try { + $return = parent::query($query, $args, $options); + $this->releaseSavepoint(); + } + catch (\Exception $e) { + $this->rollbackSavepoint(); + throw $e; + } + } + else { + $return = parent::query($query, $args, $options); + } + + return $return; + } + + /** + * {@inheritdoc} + */ + public function prepareStatement(string $query, array $options, bool $allow_row_count = FALSE): StatementInterface { + // mapConditionOperator converts some operations (LIKE, REGEXP, etc.) to + // PostgreSQL equivalents (ILIKE, ~*, etc.). However PostgreSQL doesn't + // automatically cast the fields to the right type for these operators, + // so we need to alter the query and add the type-cast. + $query = preg_replace('/ ([^ ]+) +(I*LIKE|NOT +I*LIKE|~\*|!~\*) /i', ' ${1}::text ${2} ', $query); + return parent::prepareStatement($query, $options, $allow_row_count); + } + + public function queryRange($query, $from, $count, array $args = [], array $options = []) { + return $this->query($query . ' LIMIT ' . (int) $count . ' OFFSET ' . (int) $from, $args, $options); + } + + /** + * {@inheritdoc} + */ + public function queryTemporary($query, array $args = [], array $options = []) { + @trigger_error('Connection::queryTemporary() is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. There is no replacement. See https://www.drupal.org/node/3211781', E_USER_DEPRECATED); + $tablename = $this->generateTemporaryTableName(); + $this->query('CREATE TEMPORARY TABLE {' . $tablename . '} AS ' . $query, $args, $options); + return $tablename; + } + + public function driver() { + return 'pgsql'; + } + + public function databaseType() { + return 'pgsql'; + } + + /** + * Overrides \Drupal\Core\Database\Connection::createDatabase(). + * + * @param string $database + * The name of the database to create. + * + * @throws \Drupal\Core\Database\DatabaseNotFoundException + */ + public function createDatabase($database) { + // Escape the database name. + $database = Database::getConnection()->escapeDatabase($database); + + // If the PECL intl extension is installed, use it to determine the proper + // locale. Otherwise, fall back to en_US. + if (class_exists('Locale')) { + $locale = \Locale::getDefault(); + } + else { + $locale = 'en_US'; + } + + try { + // Create the database and set it as active. + $this->connection->exec("CREATE DATABASE $database WITH TEMPLATE template0 ENCODING='utf8' LC_CTYPE='$locale.utf8' LC_COLLATE='$locale.utf8'"); + } + catch (\Exception $e) { + throw new DatabaseNotFoundException($e->getMessage()); + } + } + + public function mapConditionOperator($operator) { + return static::$postgresqlConditionOperatorMap[$operator] ?? NULL; + } + + /** + * Retrieve a the next id in a sequence. + * + * PostgreSQL has built in sequences. We'll use these instead of inserting + * and updating a sequences table. + */ + public function nextId($existing = 0) { + + // Retrieve the name of the sequence. This information cannot be cached + // because the prefix may change, for example, like it does in tests. + $sequence_name = $this->makeSequenceName('sequences', 'value'); + + // When PostgreSQL gets a value too small then it will lock the table, + // retry the INSERT and if it's still too small then alter the sequence. + $id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField(); + if ($id > $existing) { + return $id; + } + + // PostgreSQL advisory locks are simply locks to be used by an + // application such as Drupal. This will prevent other Drupal processes + // from altering the sequence while we are. + $this->query("SELECT pg_advisory_lock(" . self::POSTGRESQL_NEXTID_LOCK . ")"); + + // While waiting to obtain the lock, the sequence may have been altered + // so lets try again to obtain an adequate value. + $id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField(); + if ($id > $existing) { + $this->query("SELECT pg_advisory_unlock(" . self::POSTGRESQL_NEXTID_LOCK . ")"); + return $id; + } + + // Reset the sequence to a higher value than the existing id. + $this->query("ALTER SEQUENCE " . $sequence_name . " RESTART WITH " . ($existing + 1)); + + // Retrieve the next id. We know this will be as high as we want it. + $id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField(); + + $this->query("SELECT pg_advisory_unlock(" . self::POSTGRESQL_NEXTID_LOCK . ")"); + + return $id; + } + + /** + * {@inheritdoc} + */ + public function getFullQualifiedTableName($table) { + $options = $this->getConnectionOptions(); + $prefix = $this->tablePrefix($table); + + // The fully qualified table name in PostgreSQL is in the form of + // <database>.<schema>.<table>, so we have to include the 'public' schema in + // the return value. + return $options['database'] . '.public.' . $prefix . $table; + } + + /** + * Add a new savepoint with a unique name. + * + * The main use for this method is to mimic InnoDB functionality, which + * provides an inherent savepoint before any query in a transaction. + * + * @param $savepoint_name + * A string representing the savepoint name. By default, + * "mimic_implicit_commit" is used. + * + * @see Drupal\Core\Database\Connection::pushTransaction() + */ + public function addSavepoint($savepoint_name = 'mimic_implicit_commit') { + if ($this->inTransaction()) { + $this->pushTransaction($savepoint_name); + } + } + + /** + * Release a savepoint by name. + * + * @param $savepoint_name + * A string representing the savepoint name. By default, + * "mimic_implicit_commit" is used. + * + * @see Drupal\Core\Database\Connection::popTransaction() + */ + public function releaseSavepoint($savepoint_name = 'mimic_implicit_commit') { + if (isset($this->transactionLayers[$savepoint_name])) { + $this->popTransaction($savepoint_name); + } + } + + /** + * Rollback a savepoint by name if it exists. + * + * @param $savepoint_name + * A string representing the savepoint name. By default, + * "mimic_implicit_commit" is used. + */ + public function rollbackSavepoint($savepoint_name = 'mimic_implicit_commit') { + if (isset($this->transactionLayers[$savepoint_name])) { + $this->rollBack($savepoint_name); + } + } + +} + +/** + * @} End of "addtogroup database". + */ diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Delete.php b/core/modules/pgsql/src/Driver/Database/pgsql/Delete.php new file mode 100644 index 000000000000..9585a9c4bc52 --- /dev/null +++ b/core/modules/pgsql/src/Driver/Database/pgsql/Delete.php @@ -0,0 +1,29 @@ +<?php + +namespace Drupal\pgsql\Driver\Database\pgsql; + +use Drupal\Core\Database\Query\Delete as QueryDelete; + +/** + * PostgreSQL implementation of \Drupal\Core\Database\Query\Delete. + */ +class Delete extends QueryDelete { + + /** + * {@inheritdoc} + */ + public function execute() { + $this->connection->addSavepoint(); + try { + $result = parent::execute(); + } + catch (\Exception $e) { + $this->connection->rollbackSavepoint(); + throw $e; + } + $this->connection->releaseSavepoint(); + + return $result; + } + +} diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Insert.php b/core/modules/pgsql/src/Driver/Database/pgsql/Insert.php new file mode 100644 index 000000000000..1b53274729d8 --- /dev/null +++ b/core/modules/pgsql/src/Driver/Database/pgsql/Insert.php @@ -0,0 +1,158 @@ +<?php + +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 + +/** + * @ingroup database + * @{ + */ + +/** + * PostgreSQL implementation of \Drupal\Core\Database\Query\Insert. + */ +class Insert extends QueryInsert { + + public function execute() { + if (!$this->preExecute()) { + return NULL; + } + + $stmt = $this->connection->prepareStatement((string) $this, $this->queryOptions); + + // Fetch the list of blobs and sequences used on that table. + $table_information = $this->connection->schema()->queryTableInformation($this->table); + + $max_placeholder = 0; + $blobs = []; + $blob_count = 0; + foreach ($this->insertValues as $insert_values) { + foreach ($this->insertFields as $idx => $field) { + if (isset($table_information->blob_fields[$field]) && $insert_values[$idx] !== NULL) { + $blobs[$blob_count] = fopen('php://memory', 'a'); + fwrite($blobs[$blob_count], $insert_values[$idx]); + rewind($blobs[$blob_count]); + + $stmt->getClientStatement()->bindParam(':db_insert_placeholder_' . $max_placeholder++, $blobs[$blob_count], \PDO::PARAM_LOB); + + // Pre-increment is faster in PHP than increment. + ++$blob_count; + } + else { + $stmt->getClientStatement()->bindParam(':db_insert_placeholder_' . $max_placeholder++, $insert_values[$idx]); + } + } + // Check if values for a serial field has been passed. + if (!empty($table_information->serial_fields)) { + foreach ($table_information->serial_fields as $index => $serial_field) { + $serial_key = array_search($serial_field, $this->insertFields); + if ($serial_key !== FALSE) { + $serial_value = $insert_values[$serial_key]; + + // Sequences must be greater than or equal to 1. + if ($serial_value === NULL || !$serial_value) { + $serial_value = 1; + } + // Set the sequence to the bigger value of either the passed + // value or the max value of the column. It can happen that another + // thread calls nextval() which could lead to a serial number being + // used twice. However, trying to insert a value into a serial + // column should only be done in very rare cases and is not thread + // safe by definition. + $this->connection->query("SELECT setval('" . $table_information->sequences[$index] . "', GREATEST(MAX(" . $serial_field . "), :serial_value)) FROM {" . $this->table . "}", [':serial_value' => (int) $serial_value]); + } + } + } + } + if (!empty($this->fromQuery)) { + // bindParam stores only a reference to the variable that is followed when + // the statement is executed. We pass $arguments[$key] instead of $value + // because the second argument to bindParam is passed by reference and + // the foreach statement assigns the element to the existing reference. + $arguments = $this->fromQuery->getArguments(); + foreach ($arguments as $key => $value) { + $stmt->getClientStatement()->bindParam($key, $arguments[$key]); + } + } + + // 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 + // fails. This is important for tables that are created on demand. For + // example, \Drupal\Core\Cache\DatabaseBackend. + $this->connection->addSavepoint(); + try { + $stmt->execute(NULL, $this->queryOptions); + if (isset($table_information->serial_fields[0])) { + $last_insert_id = $stmt->fetchField(); + } + $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; + } + + // Re-initialize the values array so that we can re-use this query. + $this->insertValues = []; + + return $last_insert_id ?? NULL; + } + + public function __toString() { + // Create a sanitized comment string to prepend to the query. + $comments = $this->connection->makeComment($this->comments); + + // Default fields are always placed first for consistency. + $insert_fields = array_merge($this->defaultFields, $this->insertFields); + + $insert_fields = array_map(function ($f) { + return $this->connection->escapeField($f); + }, $insert_fields); + + // 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)) { + $insert_fields_string = $insert_fields ? ' (' . implode(', ', $insert_fields) . ') ' : ' '; + $query = $comments . 'INSERT INTO {' . $this->table . '}' . $insert_fields_string . $this->fromQuery; + } + else { + $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES '; + + $values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields); + $query .= implode(', ', $values); + } + try { + // Fetch the list of blobs and sequences used on that table. + $table_information = $this->connection->schema()->queryTableInformation($this->table); + if (isset($table_information->serial_fields[0])) { + // Use RETURNING syntax to get the last insert ID in the same INSERT + // query, see https://www.postgresql.org/docs/10/dml-returning.html. + $query .= ' RETURNING ' . $table_information->serial_fields[0]; + } + } + catch (DatabaseExceptionWrapper $e) { + // If we fail to get the table information it is probably because the + // table does not exist yet so adding the returning statement is pointless + // because the query will fail. This happens for tables created on demand, + // for example, cache tables. + } + return $query; + } + +} diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Install/Tasks.php b/core/modules/pgsql/src/Driver/Database/pgsql/Install/Tasks.php new file mode 100644 index 000000000000..a58a0fa62eec --- /dev/null +++ b/core/modules/pgsql/src/Driver/Database/pgsql/Install/Tasks.php @@ -0,0 +1,294 @@ +<?php + +namespace Drupal\pgsql\Driver\Database\pgsql\Install; + +use Drupal\Core\Database\Database; +use Drupal\Core\Database\Install\Tasks as InstallTasks; +use Drupal\Core\Database\DatabaseNotFoundException; + +/** + * Specifies installation tasks for PostgreSQL databases. + */ +class Tasks extends InstallTasks { + + /** + * Minimum required PostgreSQL version. + * + * The contrib extension pg_trgm is supposed to be installed. + * + * @see https://www.postgresql.org/docs/10/pgtrgm.html + */ + const PGSQL_MINIMUM_VERSION = '10'; + + /** + * {@inheritdoc} + */ + protected $pdoDriver = 'pgsql'; + + /** + * Constructs a \Drupal\pgsql\Driver\Database\pgsql\Install\Tasks object. + */ + public function __construct() { + $this->tasks[] = [ + 'function' => 'checkEncoding', + 'arguments' => [], + ]; + $this->tasks[] = [ + 'function' => 'checkBinaryOutput', + 'arguments' => [], + ]; + $this->tasks[] = [ + 'function' => 'checkStandardConformingStrings', + 'arguments' => [], + ]; + $this->tasks[] = [ + 'function' => 'initializeDatabase', + 'arguments' => [], + ]; + } + + /** + * {@inheritdoc} + */ + public function name() { + return t('PostgreSQL'); + } + + /** + * {@inheritdoc} + */ + public function minimumVersion() { + return static::PGSQL_MINIMUM_VERSION; + } + + /** + * {@inheritdoc} + */ + protected function connect() { + try { + // This doesn't actually test the connection. + Database::setActiveConnection(); + // Now actually do a check. + Database::getConnection(); + $this->pass('Drupal can CONNECT to the database ok.'); + } + catch (\Exception $e) { + // Attempt to create the database if it is not found. + if ($e instanceof DatabaseNotFoundException) { + // Remove the database string from connection info. + $connection_info = Database::getConnectionInfo(); + $database = $connection_info['default']['database']; + unset($connection_info['default']['database']); + + // In order to change the Database::$databaseInfo array, need to remove + // the active connection, then re-add it with the new info. + Database::removeConnection('default'); + Database::addConnectionInfo('default', 'default', $connection_info['default']); + + try { + // Now, attempt the connection again; if it's successful, attempt to + // create the database. + Database::getConnection()->createDatabase($database); + Database::closeConnection(); + + // Now, restore the database config. + Database::removeConnection('default'); + $connection_info['default']['database'] = $database; + Database::addConnectionInfo('default', 'default', $connection_info['default']); + + // Check the database connection. + Database::getConnection(); + $this->pass('Drupal can CONNECT to the database ok.'); + } + catch (DatabaseNotFoundException $e) { + // Still no dice; probably a permission issue. Raise the error to the + // installer. + $this->fail(t('Database %database not found. The server reports the following message when attempting to create the database: %error.', ['%database' => $database, '%error' => $e->getMessage()])); + } + } + else { + // Database connection failed for some other reason than a non-existent + // database. + $this->fail(t('Failed to connect to your database server. The server reports the following message: %error.<ul><li>Is the database server running?</li><li>Does the database exist, and have you entered the correct database name?</li><li>Have you entered the correct username and password?</li><li>Have you entered the correct database hostname and port number?</li></ul>', ['%error' => $e->getMessage()])); + return FALSE; + } + } + return TRUE; + } + + /** + * Check encoding is UTF8. + */ + protected function checkEncoding() { + try { + if (Database::getConnection()->query('SHOW server_encoding')->fetchField() == 'UTF8') { + $this->pass(t('Database is encoded in UTF-8')); + } + else { + $this->fail(t('The %driver database must use %encoding encoding to work with Drupal. Recreate the database with %encoding encoding. See <a href="INSTALL.pgsql.txt">INSTALL.pgsql.txt</a> for more details.', [ + '%encoding' => 'UTF8', + '%driver' => $this->name(), + ])); + } + } + catch (\Exception $e) { + $this->fail(t('Drupal could not determine the encoding of the database was set to UTF-8')); + } + } + + /** + * Check Binary Output. + * + * Unserializing does not work on Postgresql 9 when bytea_output is 'hex'. + */ + public function checkBinaryOutput() { + $database_connection = Database::getConnection(); + if (!$this->checkBinaryOutputSuccess()) { + // First try to alter the database. If it fails, raise an error telling + // the user to do it themselves. + $connection_options = $database_connection->getConnectionOptions(); + // It is safe to include the database name directly here, because this + // code is only called when a connection to the database is already + // established, thus the database name is guaranteed to be a correct + // value. + $query = "ALTER DATABASE \"{$connection_options['database']}\" SET bytea_output = 'escape';"; + try { + $database_connection->query($query); + } + catch (\Exception $e) { + // Ignore possible errors when the user doesn't have the necessary + // privileges to ALTER the database. + } + + // Close the database connection so that the configuration parameter + // is applied to the current connection. + Database::closeConnection(); + + // Recheck, if it fails, finally just rely on the end user to do the + // right thing. + if (!$this->checkBinaryOutputSuccess()) { + $replacements = [ + '%setting' => 'bytea_output', + '%current_value' => 'hex', + '%needed_value' => 'escape', + '@query' => $query, + ]; + $this->fail(t("The %setting setting is currently set to '%current_value', but needs to be '%needed_value'. Change this by running the following query: <code>@query</code>", $replacements)); + } + } + } + + /** + * Verify that a binary data roundtrip returns the original string. + */ + protected function checkBinaryOutputSuccess() { + $bytea_output = Database::getConnection()->query("SHOW bytea_output")->fetchField(); + return ($bytea_output == 'escape'); + } + + /** + * Ensures standard_conforming_strings setting is 'on'. + * + * When standard_conforming_strings setting is 'on' string literals ('...') + * treat backslashes literally, as specified in the SQL standard. This allows + * Drupal to convert between bytea, text and varchar columns. + */ + public function checkStandardConformingStrings() { + $database_connection = Database::getConnection(); + if (!$this->checkStandardConformingStringsSuccess()) { + // First try to alter the database. If it fails, raise an error telling + // the user to do it themselves. + $connection_options = $database_connection->getConnectionOptions(); + // It is safe to include the database name directly here, because this + // code is only called when a connection to the database is already + // established, thus the database name is guaranteed to be a correct + // value. + $query = "ALTER DATABASE \"" . $connection_options['database'] . "\" SET standard_conforming_strings = 'on';"; + try { + $database_connection->query($query); + } + catch (\Exception $e) { + // Ignore possible errors when the user doesn't have the necessary + // privileges to ALTER the database. + } + + // Close the database connection so that the configuration parameter + // is applied to the current connection. + Database::closeConnection(); + + // Recheck, if it fails, finally just rely on the end user to do the + // right thing. + if (!$this->checkStandardConformingStringsSuccess()) { + $replacements = [ + '%setting' => 'standard_conforming_strings', + '%current_value' => 'off', + '%needed_value' => 'on', + '@query' => $query, + ]; + $this->fail(t("The %setting setting is currently set to '%current_value', but needs to be '%needed_value'. Change this by running the following query: <code>@query</code>", $replacements)); + } + } + } + + /** + * Verifies the standard_conforming_strings setting. + */ + protected function checkStandardConformingStringsSuccess() { + $standard_conforming_strings = Database::getConnection()->query("SHOW standard_conforming_strings")->fetchField(); + return ($standard_conforming_strings == 'on'); + } + + /** + * Make PostgreSQL Drupal friendly. + */ + public function initializeDatabase() { + // We create some functions using global names instead of prefixing them + // like we do with table names. This is so that we don't double up if more + // than one instance of Drupal is running on a single database. We therefore + // avoid trying to create them again in that case. + // At the same time checking for the existence of the function fixes + // concurrency issues, when both try to update at the same time. + try { + $connection = Database::getConnection(); + // When testing, two installs might try to run the CREATE FUNCTION queries + // at the same time. Do not let that happen. + $connection->query('SELECT pg_advisory_lock(1)'); + // Don't use {} around pg_proc table. + if (!$connection->query("SELECT COUNT(*) FROM pg_proc WHERE proname = 'rand'")->fetchField()) { + $connection->query('CREATE OR REPLACE FUNCTION "rand"() RETURNS float AS + \'SELECT random();\' + LANGUAGE \'sql\'', + [], + ['allow_delimiter_in_query' => TRUE] + ); + } + + if (!$connection->query("SELECT COUNT(*) FROM pg_proc WHERE proname = 'substring_index'")->fetchField()) { + $connection->query('CREATE OR REPLACE FUNCTION "substring_index"(text, text, integer) RETURNS text AS + \'SELECT array_to_string((string_to_array($1, $2)) [1:$3], $2);\' + LANGUAGE \'sql\'', + [], + ['allow_delimiter_in_query' => TRUE, 'allow_square_brackets' => TRUE] + ); + } + $connection->query('SELECT pg_advisory_unlock(1)'); + + $this->pass(t('PostgreSQL has initialized itself.')); + } + catch (\Exception $e) { + $this->fail(t('Drupal could not be correctly setup with the existing database due to the following error: @error.', ['@error' => $e->getMessage()])); + } + } + + /** + * {@inheritdoc} + */ + public function getFormOptions(array $database) { + $form = parent::getFormOptions($database); + if (empty($form['advanced_options']['port']['#default_value'])) { + $form['advanced_options']['port']['#default_value'] = '5432'; + } + return $form; + } + +} diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Schema.php b/core/modules/pgsql/src/Driver/Database/pgsql/Schema.php new file mode 100644 index 000000000000..ecf514018c9c --- /dev/null +++ b/core/modules/pgsql/src/Driver/Database/pgsql/Schema.php @@ -0,0 +1,1084 @@ +<?php + +namespace Drupal\pgsql\Driver\Database\pgsql; + +use Drupal\Core\Database\SchemaObjectExistsException; +use Drupal\Core\Database\SchemaObjectDoesNotExistException; +use Drupal\Core\Database\Schema as DatabaseSchema; + +// cSpell:ignore adbin adnum adrelid adsrc attisdropped attname attnum attrdef +// cSpell:ignore attrelid atttypid atttypmod bigserial conkey conname conrelid +// cSpell:ignore contype fillfactor indexname indexrelid indisprimary indkey +// cSpell:ignore indrelid nextval nspname regclass relkind relname relnamespace +// cSpell:ignore schemaname setval + +/** + * @addtogroup schemaapi + * @{ + */ + +/** + * PostgreSQL implementation of \Drupal\Core\Database\Schema. + */ +class Schema extends DatabaseSchema { + + /** + * A cache of information about blob columns and sequences of tables. + * + * This is collected by Schema::queryTableInformation(), by introspecting the + * database. + * + * @see \Drupal\pgsql\Driver\Database\pgsql\Schema::queryTableInformation() + * @var array + */ + protected $tableInformation = []; + + /** + * The maximum allowed length for index, primary key and constraint names. + * + * Value will usually be set to a 63 chars limit but PostgreSQL allows + * to higher this value before compiling, so we need to check for that. + * + * @var int + */ + protected $maxIdentifierLength; + + /** + * PostgreSQL's temporary namespace name. + * + * @var string + */ + protected $tempNamespaceName; + + /** + * Make sure to limit identifiers according to PostgreSQL compiled in length. + * + * PostgreSQL allows in standard configuration identifiers no longer than 63 + * chars for table/relation names, indexes, primary keys, and constraints. So + * we map all identifiers that are too long to drupal_base64hash_tag, where + * tag is one of: + * - idx for indexes + * - key for constraints + * - pkey for primary keys + * - seq for sequences + * + * @param string $table_identifier_part + * The first argument used to build the identifier string. This usually + * refers to a table/relation name. + * @param string $column_identifier_part + * The second argument used to build the identifier string. This usually + * refers to one or more column names. + * @param string $tag + * The identifier tag. It can be one of 'idx', 'key', 'pkey' or 'seq'. + * @param string $separator + * (optional) The separator used to glue together the aforementioned + * identifier parts. Defaults to '__'. + * + * @return string + * The index/constraint/pkey identifier. + */ + protected function ensureIdentifiersLength($table_identifier_part, $column_identifier_part, $tag, $separator = '__') { + $info = $this->getPrefixInfo($table_identifier_part); + $table_identifier_part = $info['table']; + $identifierName = implode($separator, [$table_identifier_part, $column_identifier_part, $tag]); + + // Retrieve the max identifier length which is usually 63 characters + // but can be altered before PostgreSQL is compiled so we need to check. + if (empty($this->maxIdentifierLength)) { + $this->maxIdentifierLength = $this->connection->query("SHOW max_identifier_length")->fetchField(); + } + + if (strlen($identifierName) > $this->maxIdentifierLength) { + $saveIdentifier = '"drupal_' . $this->hashBase64($identifierName) . '_' . $tag . '"'; + } + else { + $saveIdentifier = $identifierName; + } + return $saveIdentifier; + } + + /** + * Fetch the list of blobs and sequences used on a table. + * + * We introspect the database to collect the information required by insert + * and update queries. + * + * @param string $table + * The non-prefixed name of the table. + * + * @return mixed|object + * An object with two member variables: + * - 'blob_fields' that lists all the blob fields in the table. + * - 'sequences' that lists the sequences used in that table. + * + * @throws \Exception + * Exception thrown when the query for the table information fails. + */ + public function queryTableInformation($table) { + // Generate a key to reference this table's information on. + $key = $this->connection->prefixTables('{' . $table . '}'); + + // Take into account that temporary tables are stored in a different schema. + // \Drupal\Core\Database\Connection::generateTemporaryTableName() sets the + // 'db_temporary_' prefix to all temporary tables. + if (strpos($key, '.') === FALSE && strpos($table, 'db_temporary_') === FALSE) { + $key = 'public.' . $key; + } + else { + $key = $this->getTempNamespaceName() . '.' . $key; + } + + if (!isset($this->tableInformation[$key])) { + $table_information = (object) [ + 'blob_fields' => [], + 'sequences' => [], + ]; + $this->connection->addSavepoint(); + + try { + // The bytea columns and sequences for a table can be found in + // pg_attribute, which is significantly faster than querying the + // information_schema. The data type of a field can be found by lookup + // of the attribute ID, and the default value must be extracted from the + // node tree for the attribute definition instead of the historical + // human-readable column, adsrc. + $sql = <<<'EOD' +SELECT pg_attribute.attname AS column_name, format_type(pg_attribute.atttypid, pg_attribute.atttypmod) AS data_type, pg_get_expr(pg_attrdef.adbin, pg_attribute.attrelid) AS column_default +FROM pg_attribute +LEFT JOIN pg_attrdef ON pg_attrdef.adrelid = pg_attribute.attrelid AND pg_attrdef.adnum = pg_attribute.attnum +WHERE pg_attribute.attnum > 0 +AND NOT pg_attribute.attisdropped +AND pg_attribute.attrelid = :key::regclass +AND (format_type(pg_attribute.atttypid, pg_attribute.atttypmod) = 'bytea' +OR pg_get_expr(pg_attrdef.adbin, pg_attribute.attrelid) LIKE 'nextval%') +EOD; + $result = $this->connection->query($sql, [ + ':key' => $key, + ]); + } + catch (\Exception $e) { + $this->connection->rollbackSavepoint(); + throw $e; + } + $this->connection->releaseSavepoint(); + + // If the table information does not yet exist in the PostgreSQL + // metadata, then return the default table information here, so that it + // will not be cached. + if (empty($result)) { + return $table_information; + } + + foreach ($result as $column) { + if ($column->data_type == 'bytea') { + $table_information->blob_fields[$column->column_name] = TRUE; + } + elseif (preg_match("/nextval\('([^']+)'/", $column->column_default, $matches)) { + // We must know of any sequences in the table structure to help us + // return the last insert id. If there is more than 1 sequences the + // first one (index 0 of the sequences array) will be used. + $table_information->sequences[] = $matches[1]; + $table_information->serial_fields[] = $column->column_name; + } + } + $this->tableInformation[$key] = $table_information; + } + return $this->tableInformation[$key]; + } + + /** + * Gets PostgreSQL's temporary namespace name. + * + * @return string + * PostgreSQL's temporary namespace name. + */ + protected function getTempNamespaceName() { + if (!isset($this->tempNamespaceName)) { + $this->tempNamespaceName = $this->connection->query('SELECT nspname FROM pg_namespace WHERE oid = pg_my_temp_schema()')->fetchField(); + } + return $this->tempNamespaceName; + } + + /** + * Resets information about table blobs, sequences and serial fields. + * + * @param $table + * The non-prefixed name of the table. + */ + protected function resetTableInformation($table) { + $key = $this->connection->prefixTables('{' . $table . '}'); + if (strpos($key, '.') === FALSE) { + $key = 'public.' . $key; + } + unset($this->tableInformation[$key]); + } + + /** + * Fetches the list of constraints used on a field. + * + * We introspect the database to collect the information required by field + * alteration. + * + * @param string $table + * The non-prefixed name of the table. + * @param string $field + * The name of the field. + * @param string $constraint_type + * (optional) The type of the constraint. This can be one of the following: + * - c: check constraint; + * - f: foreign key constraint; + * - p: primary key constraint; + * - u: unique constraint; + * - t: constraint trigger; + * - x: exclusion constraint. + * Defaults to 'c' for a CHECK constraint. + * @see https://www.postgresql.org/docs/current/catalog-pg-constraint.html + * + * @return array + * An array containing all the constraint names for the field. + * + * @throws \Exception + * Exception thrown when the query for the table information fails. + */ + public function queryFieldInformation($table, $field, $constraint_type = 'c') { + assert(in_array($constraint_type, ['c', 'f', 'p', 'u', 't', 'x'])); + $prefixInfo = $this->getPrefixInfo($table, TRUE); + + // Split the key into schema and table for querying. + $schema = $prefixInfo['schema']; + $table_name = $prefixInfo['table']; + + $this->connection->addSavepoint(); + + try { + $checks = $this->connection->query("SELECT conname FROM pg_class cl INNER JOIN pg_constraint co ON co.conrelid = cl.oid INNER JOIN pg_attribute attr ON attr.attrelid = cl.oid AND attr.attnum = ANY (co.conkey) INNER JOIN pg_namespace ns ON cl.relnamespace = ns.oid WHERE co.contype = :constraint_type AND ns.nspname = :schema AND cl.relname = :table AND attr.attname = :column", [ + ':constraint_type' => $constraint_type, + ':schema' => $schema, + ':table' => $table_name, + ':column' => $field, + ]); + } + catch (\Exception $e) { + $this->connection->rollbackSavepoint(); + throw $e; + } + + $this->connection->releaseSavepoint(); + + $field_information = $checks->fetchCol(); + + return $field_information; + } + + /** + * Generate SQL to create a new table from a Drupal schema definition. + * + * @param string $name + * The name of the table to create. + * @param array $table + * A Schema API table definition array. + * + * @return array + * An array of SQL statements to create the table. + */ + protected function createTableSql($name, $table) { + $sql_fields = []; + foreach ($table['fields'] as $field_name => $field) { + $sql_fields[] = $this->createFieldSql($field_name, $this->processField($field)); + } + + $sql_keys = []; + if (!empty($table['primary key']) && is_array($table['primary key'])) { + $this->ensureNotNullPrimaryKey($table['primary key'], $table['fields']); + $sql_keys[] = 'CONSTRAINT ' . $this->ensureIdentifiersLength($name, '', 'pkey') . ' PRIMARY KEY (' . $this->createPrimaryKeySql($table['primary key']) . ')'; + } + if (isset($table['unique keys']) && is_array($table['unique keys'])) { + foreach ($table['unique keys'] as $key_name => $key) { + $sql_keys[] = 'CONSTRAINT ' . $this->ensureIdentifiersLength($name, $key_name, 'key') . ' UNIQUE (' . implode(', ', $key) . ')'; + } + } + + $sql = "CREATE TABLE {" . $name . "} (\n\t"; + $sql .= implode(",\n\t", $sql_fields); + if (count($sql_keys) > 0) { + $sql .= ",\n\t"; + } + $sql .= implode(",\n\t", $sql_keys); + $sql .= "\n)"; + $statements[] = $sql; + + if (isset($table['indexes']) && is_array($table['indexes'])) { + foreach ($table['indexes'] as $key_name => $key) { + $statements[] = $this->_createIndexSql($name, $key_name, $key); + } + } + + // Add table comment. + if (!empty($table['description'])) { + $statements[] = 'COMMENT ON TABLE {' . $name . '} IS ' . $this->prepareComment($table['description']); + } + + // Add column comments. + foreach ($table['fields'] as $field_name => $field) { + if (!empty($field['description'])) { + $statements[] = 'COMMENT ON COLUMN {' . $name . '}.' . $field_name . ' IS ' . $this->prepareComment($field['description']); + } + } + + return $statements; + } + + /** + * Create an SQL string for a field to be used in table creation or + * alteration. + * + * @param $name + * Name of the field. + * @param $spec + * The field specification, as per the schema data structure format. + */ + protected function createFieldSql($name, $spec) { + // The PostgreSQL server converts names into lowercase, unless quoted. + $sql = '"' . $name . '" ' . $spec['pgsql_type']; + + if (isset($spec['type']) && $spec['type'] == 'serial') { + unset($spec['not null']); + } + + if (in_array($spec['pgsql_type'], ['varchar', 'character']) && isset($spec['length'])) { + $sql .= '(' . $spec['length'] . ')'; + } + elseif (isset($spec['precision']) && isset($spec['scale'])) { + $sql .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')'; + } + + if (!empty($spec['unsigned'])) { + $sql .= " CHECK ($name >= 0)"; + } + + if (isset($spec['not null'])) { + if ($spec['not null']) { + $sql .= ' NOT NULL'; + } + else { + $sql .= ' NULL'; + } + } + if (array_key_exists('default', $spec)) { + $default = $this->escapeDefaultValue($spec['default']); + $sql .= " default $default"; + } + + return $sql; + } + + /** + * Set database-engine specific properties for a field. + * + * @param $field + * A field description array, as specified in the schema documentation. + */ + protected function processField($field) { + if (!isset($field['size'])) { + $field['size'] = 'normal'; + } + + // Set the correct database-engine specific datatype. + // In case one is already provided, force it to lowercase. + if (isset($field['pgsql_type'])) { + $field['pgsql_type'] = mb_strtolower($field['pgsql_type']); + } + else { + $map = $this->getFieldTypeMap(); + $field['pgsql_type'] = $map[$field['type'] . ':' . $field['size']]; + } + + if (!empty($field['unsigned'])) { + // Unsigned data types are not supported in PostgreSQL 10. In MySQL, + // they are used to ensure a positive number is inserted and it also + // doubles the maximum integer size that can be stored in a field. + // The PostgreSQL schema in Drupal creates a check constraint + // to ensure that a value inserted is >= 0. To provide the extra + // integer capacity, here, we bump up the column field size. + if (!isset($map)) { + $map = $this->getFieldTypeMap(); + } + switch ($field['pgsql_type']) { + case 'smallint': + $field['pgsql_type'] = $map['int:medium']; + break; + + case 'int': + $field['pgsql_type'] = $map['int:big']; + break; + } + } + if (isset($field['type']) && $field['type'] == 'serial') { + unset($field['not null']); + } + return $field; + } + + /** + * {@inheritdoc} + */ + public function getFieldTypeMap() { + // Put :normal last so it gets preserved by array_flip. This makes + // it much easier for modules (such as schema.module) to map + // database types back into schema types. + // $map does not use drupal_static as its value never changes. + static $map = [ + 'varchar_ascii:normal' => 'varchar', + + 'varchar:normal' => 'varchar', + 'char:normal' => 'character', + + 'text:tiny' => 'text', + 'text:small' => 'text', + 'text:medium' => 'text', + 'text:big' => 'text', + 'text:normal' => 'text', + + 'int:tiny' => 'smallint', + 'int:small' => 'smallint', + 'int:medium' => 'int', + 'int:big' => 'bigint', + 'int:normal' => 'int', + + 'float:tiny' => 'real', + 'float:small' => 'real', + 'float:medium' => 'real', + 'float:big' => 'double precision', + 'float:normal' => 'real', + + 'numeric:normal' => 'numeric', + + 'blob:big' => 'bytea', + 'blob:normal' => 'bytea', + + 'serial:tiny' => 'serial', + 'serial:small' => 'serial', + 'serial:medium' => 'serial', + 'serial:big' => 'bigserial', + 'serial:normal' => 'serial', + ]; + return $map; + } + + protected function _createKeySql($fields) { + $return = []; + foreach ($fields as $field) { + if (is_array($field)) { + $return[] = 'substr(' . $field[0] . ', 1, ' . $field[1] . ')'; + } + else { + $return[] = '"' . $field . '"'; + } + } + return implode(', ', $return); + } + + /** + * Create the SQL expression for primary keys. + * + * Postgresql does not support key length. It does support fillfactor, but + * that requires a separate database lookup for each column in the key. The + * key length defined in the schema is ignored. + */ + protected function createPrimaryKeySql($fields) { + $return = []; + foreach ($fields as $field) { + if (is_array($field)) { + $return[] = '"' . $field[0] . '"'; + } + else { + $return[] = '"' . $field . '"'; + } + } + return implode(', ', $return); + } + + /** + * {@inheritdoc} + */ + public function tableExists($table) { + $prefixInfo = $this->getPrefixInfo($table, TRUE); + + return (bool) $this->connection->query("SELECT 1 FROM pg_tables WHERE schemaname = :schema AND tablename = :table", [':schema' => $prefixInfo['schema'], ':table' => $prefixInfo['table']])->fetchField(); + } + + /** + * {@inheritdoc} + */ + public function findTables($table_expression) { + $individually_prefixed_tables = $this->connection->getUnprefixedTablesMap(); + $default_prefix = $this->connection->tablePrefix(); + $default_prefix_length = strlen($default_prefix); + $tables = []; + + // Load all the tables up front in order to take into account per-table + // prefixes. The actual matching is done at the bottom of the method. + $results = $this->connection->query("SELECT tablename FROM pg_tables WHERE schemaname = :schema", [':schema' => $this->defaultSchema]); + foreach ($results as $table) { + // Take into account tables that have an individual prefix. + if (isset($individually_prefixed_tables[$table->tablename])) { + $prefix_length = strlen($this->connection->tablePrefix($individually_prefixed_tables[$table->tablename])); + } + elseif ($default_prefix && substr($table->tablename, 0, $default_prefix_length) !== $default_prefix) { + // This table name does not start the default prefix, which means that + // it is not managed by Drupal so it should be excluded from the result. + continue; + } + else { + $prefix_length = $default_prefix_length; + } + + // Remove the prefix from the returned tables. + $unprefixed_table_name = substr($table->tablename, $prefix_length); + + // The pattern can match a table which is the same as the prefix. That + // will become an empty string when we remove the prefix, which will + // probably surprise the caller, besides not being a prefixed table. So + // remove it. + if (!empty($unprefixed_table_name)) { + $tables[$unprefixed_table_name] = $unprefixed_table_name; + } + } + + // Convert the table expression from its SQL LIKE syntax to a regular + // expression and escape the delimiter that will be used for matching. + $table_expression = str_replace(['%', '_'], ['.*?', '.'], preg_quote($table_expression, '/')); + $tables = preg_grep('/^' . $table_expression . '$/i', $tables); + + return $tables; + } + + /** + * {@inheritdoc} + */ + public function renameTable($table, $new_name) { + if (!$this->tableExists($table)) { + throw new SchemaObjectDoesNotExistException("Cannot rename '$table' to '$new_name': table '$table' doesn't exist."); + } + if ($this->tableExists($new_name)) { + throw new SchemaObjectExistsException("Cannot rename '$table' to '$new_name': table '$new_name' already exists."); + } + + // Get the schema and tablename for the old table. + $old_full_name = str_replace('"', '', $this->connection->prefixTables('{' . $table . '}')); + [$old_schema, $old_table_name] = strpos($old_full_name, '.') ? explode('.', $old_full_name) : ['public', $old_full_name]; + + // Index names and constraint names are global in PostgreSQL, so we need to + // rename them when renaming the table. + $indexes = $this->connection->query('SELECT indexname FROM pg_indexes WHERE schemaname = :schema AND tablename = :table', [':schema' => $old_schema, ':table' => $old_table_name]); + + foreach ($indexes as $index) { + // Get the index type by suffix, e.g. idx/key/pkey + $index_type = substr($index->indexname, strrpos($index->indexname, '_') + 1); + + // If the index is already rewritten by ensureIdentifiersLength() to not + // exceed the 63 chars limit of PostgreSQL, we need to take care of that. + // cSpell:disable-next-line + // Example (drupal_Gk7Su_T1jcBHVuvSPeP22_I3Ni4GrVEgTYlIYnBJkro_idx). + if (strpos($index->indexname, 'drupal_') !== FALSE) { + preg_match('/^drupal_(.*)_' . preg_quote($index_type) . '/', $index->indexname, $matches); + $index_name = $matches[1]; + } + else { + // Make sure to remove the suffix from index names, because + // $this->ensureIdentifiersLength() will add the suffix again and thus + // would result in a wrong index name. + preg_match('/^' . preg_quote($old_full_name) . '__(.*)__' . preg_quote($index_type) . '/', $index->indexname, $matches); + $index_name = $matches[1]; + } + $this->connection->query('ALTER INDEX "' . $index->indexname . '" RENAME TO ' . $this->ensureIdentifiersLength($new_name, $index_name, $index_type) . ''); + } + + // Ensure the new table name does not include schema syntax. + $prefixInfo = $this->getPrefixInfo($new_name); + + // Rename sequences if the table contains serial fields. + $info = $this->queryTableInformation($table); + if (!empty($info->serial_fields)) { + foreach ($info->serial_fields as $field) { + // The initial name of the sequence is generated automatically by + // PostgreSQL when the table is created, so we need to use + // pg_get_serial_sequence() to retrieve it. + $old_sequence = $this->connection->query("SELECT pg_get_serial_sequence('" . $old_full_name . "', '" . $field . "')")->fetchField(); + + // If the new sequence name exceeds the maximum identifier length limit, + // it will not match the pattern that is automatically applied by + // PostgreSQL on table creation, but that's ok because + // pg_get_serial_sequence() will return our non-standard name on + // subsequent table renames. + $new_sequence = $this->ensureIdentifiersLength($new_name, $field, 'seq', '_'); + + $this->connection->query('ALTER SEQUENCE ' . $old_sequence . ' RENAME TO ' . $new_sequence); + } + } + // Now rename the table. + $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO ' . $prefixInfo['table']); + $this->resetTableInformation($table); + } + + /** + * {@inheritdoc} + */ + public function dropTable($table) { + if (!$this->tableExists($table)) { + return FALSE; + } + + $this->connection->query('DROP TABLE {' . $table . '}'); + $this->resetTableInformation($table); + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function addField($table, $field, $spec, $new_keys = []) { + if (!$this->tableExists($table)) { + throw new SchemaObjectDoesNotExistException("Cannot add field '$table.$field': table doesn't exist."); + } + if ($this->fieldExists($table, $field)) { + throw new SchemaObjectExistsException("Cannot add field '$table.$field': field already exists."); + } + + // Fields that are part of a PRIMARY KEY must be added as NOT NULL. + $is_primary_key = isset($new_keys['primary key']) && in_array($field, $new_keys['primary key'], TRUE); + if ($is_primary_key) { + $this->ensureNotNullPrimaryKey($new_keys['primary key'], [$field => $spec]); + } + + $fixnull = FALSE; + if (!empty($spec['not null']) && !isset($spec['default']) && !$is_primary_key) { + $fixnull = TRUE; + $spec['not null'] = FALSE; + } + $query = 'ALTER TABLE {' . $table . '} ADD COLUMN '; + $query .= $this->createFieldSql($field, $this->processField($spec)); + $this->connection->query($query); + if (isset($spec['initial_from_field'])) { + if (isset($spec['initial'])) { + $expression = 'COALESCE(' . $spec['initial_from_field'] . ', :default_initial_value)'; + $arguments = [':default_initial_value' => $spec['initial']]; + } + else { + $expression = $spec['initial_from_field']; + $arguments = []; + } + $this->connection->update($table) + ->expression($field, $expression, $arguments) + ->execute(); + } + elseif (isset($spec['initial'])) { + $this->connection->update($table) + ->fields([$field => $spec['initial']]) + ->execute(); + } + if ($fixnull) { + $this->connection->query("ALTER TABLE {" . $table . "} ALTER $field SET NOT NULL"); + } + if (isset($new_keys)) { + // Make sure to drop the existing primary key before adding a new one. + // This is only needed when adding a field because this method, unlike + // changeField(), is supposed to handle primary keys automatically. + if (isset($new_keys['primary key']) && $this->constraintExists($table, 'pkey')) { + $this->dropPrimaryKey($table); + } + $this->_createKeys($table, $new_keys); + } + // Add column comment. + if (!empty($spec['description'])) { + $this->connection->query('COMMENT ON COLUMN {' . $table . '}.' . $field . ' IS ' . $this->prepareComment($spec['description'])); + } + $this->resetTableInformation($table); + } + + /** + * {@inheritdoc} + */ + public function dropField($table, $field) { + if (!$this->fieldExists($table, $field)) { + return FALSE; + } + + $this->connection->query('ALTER TABLE {' . $table . '} DROP COLUMN "' . $field . '"'); + $this->resetTableInformation($table); + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function fieldExists($table, $column) { + $prefixInfo = $this->getPrefixInfo($table); + + return (bool) $this->connection->query("SELECT 1 FROM pg_attribute WHERE attrelid = :key::regclass AND attname = :column AND NOT attisdropped AND attnum > 0", [':key' => $prefixInfo['schema'] . '.' . $prefixInfo['table'], ':column' => $column])->fetchField(); + } + + /** + * {@inheritdoc} + */ + public function indexExists($table, $name) { + // Details https://www.postgresql.org/docs/10/view-pg-indexes.html + $index_name = $this->ensureIdentifiersLength($table, $name, 'idx'); + // Remove leading and trailing quotes because the index name is in a WHERE + // clause and not used as an identifier. + $index_name = str_replace('"', '', $index_name); + return (bool) $this->connection->query("SELECT 1 FROM pg_indexes WHERE indexname = '$index_name'")->fetchField(); + } + + /** + * Helper function: check if a constraint (PK, FK, UK) exists. + * + * @param string $table + * The name of the table. + * @param string $name + * The name of the constraint (typically 'pkey' or '[constraint]__key'). + * + * @return bool + * TRUE if the constraint exists, FALSE otherwise. + */ + public function constraintExists($table, $name) { + // ::ensureIdentifiersLength() expects three parameters, although not + // explicitly stated in its signature, thus we split our constraint name in + // a proper name and a suffix. + if ($name == 'pkey') { + $suffix = $name; + $name = ''; + } + else { + $pos = strrpos($name, '__'); + $suffix = substr($name, $pos + 2); + $name = substr($name, 0, $pos); + } + $constraint_name = $this->ensureIdentifiersLength($table, $name, $suffix); + // Remove leading and trailing quotes because the index name is in a WHERE + // clause and not used as an identifier. + $constraint_name = str_replace('"', '', $constraint_name); + return (bool) $this->connection->query("SELECT 1 FROM pg_constraint WHERE conname = '$constraint_name'")->fetchField(); + } + + /** + * {@inheritdoc} + */ + public function addPrimaryKey($table, $fields) { + if (!$this->tableExists($table)) { + throw new SchemaObjectDoesNotExistException("Cannot add primary key to table '$table': table doesn't exist."); + } + if ($this->constraintExists($table, 'pkey')) { + throw new SchemaObjectExistsException("Cannot add primary key to table '$table': primary key already exists."); + } + + $this->connection->query('ALTER TABLE {' . $table . '} ADD CONSTRAINT ' . $this->ensureIdentifiersLength($table, '', 'pkey') . ' PRIMARY KEY (' . $this->createPrimaryKeySql($fields) . ')'); + $this->resetTableInformation($table); + } + + /** + * {@inheritdoc} + */ + public function dropPrimaryKey($table) { + if (!$this->constraintExists($table, 'pkey')) { + return FALSE; + } + + $this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT ' . $this->ensureIdentifiersLength($table, '', 'pkey')); + $this->resetTableInformation($table); + return TRUE; + } + + /** + * {@inheritdoc} + */ + protected function findPrimaryKeyColumns($table) { + if (!$this->tableExists($table)) { + return FALSE; + } + return $this->connection->query("SELECT array_position(i.indkey, a.attnum) AS position, a.attname FROM pg_index i JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) WHERE i.indrelid = '{" . $table . "}'::regclass AND i.indisprimary ORDER BY position")->fetchAllKeyed(); + } + + /** + * {@inheritdoc} + */ + public function addUniqueKey($table, $name, $fields) { + if (!$this->tableExists($table)) { + throw new SchemaObjectDoesNotExistException("Cannot add unique key '$name' to table '$table': table doesn't exist."); + } + if ($this->constraintExists($table, $name . '__key')) { + throw new SchemaObjectExistsException("Cannot add unique key '$name' to table '$table': unique key already exists."); + } + + $this->connection->query('ALTER TABLE {' . $table . '} ADD CONSTRAINT ' . $this->ensureIdentifiersLength($table, $name, 'key') . ' UNIQUE (' . implode(',', $fields) . ')'); + $this->resetTableInformation($table); + } + + /** + * {@inheritdoc} + */ + public function dropUniqueKey($table, $name) { + if (!$this->constraintExists($table, $name . '__key')) { + return FALSE; + } + + $this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT ' . $this->ensureIdentifiersLength($table, $name, 'key')); + $this->resetTableInformation($table); + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function addIndex($table, $name, $fields, array $spec) { + if (!$this->tableExists($table)) { + throw new SchemaObjectDoesNotExistException("Cannot add index '$name' to table '$table': table doesn't exist."); + } + if ($this->indexExists($table, $name)) { + throw new SchemaObjectExistsException("Cannot add index '$name' to table '$table': index already exists."); + } + + $this->connection->query($this->_createIndexSql($table, $name, $fields)); + $this->resetTableInformation($table); + } + + /** + * {@inheritdoc} + */ + public function dropIndex($table, $name) { + if (!$this->indexExists($table, $name)) { + return FALSE; + } + + $this->connection->query('DROP INDEX ' . $this->ensureIdentifiersLength($table, $name, 'idx')); + $this->resetTableInformation($table); + return TRUE; + } + + /** + * {@inheritdoc} + */ + protected function introspectIndexSchema($table) { + if (!$this->tableExists($table)) { + throw new SchemaObjectDoesNotExistException("The table $table doesn't exist."); + } + + $index_schema = [ + 'primary key' => [], + 'unique keys' => [], + 'indexes' => [], + ]; + + // Get the schema and tablename for the table without identifier quotes. + $full_name = str_replace('"', '', $this->connection->prefixTables('{' . $table . '}')); + $result = $this->connection->query("SELECT i.relname AS index_name, a.attname AS column_name FROM pg_class t, pg_class i, pg_index ix, pg_attribute a WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid AND a.attnum = ANY(ix.indkey) AND t.relkind = 'r' AND t.relname = :table_name ORDER BY index_name ASC, column_name ASC", [ + ':table_name' => $full_name, + ])->fetchAll(); + foreach ($result as $row) { + if (preg_match('/_pkey$/', $row->index_name)) { + $index_schema['primary key'][] = $row->column_name; + } + elseif (preg_match('/_key$/', $row->index_name)) { + $index_schema['unique keys'][$row->index_name][] = $row->column_name; + } + elseif (preg_match('/_idx$/', $row->index_name)) { + $index_schema['indexes'][$row->index_name][] = $row->column_name; + } + } + + return $index_schema; + } + + /** + * {@inheritdoc} + */ + public function changeField($table, $field, $field_new, $spec, $new_keys = []) { + if (!$this->fieldExists($table, $field)) { + throw new SchemaObjectDoesNotExistException("Cannot change the definition of field '$table.$field': field doesn't exist."); + } + if (($field != $field_new) && $this->fieldExists($table, $field_new)) { + throw new SchemaObjectExistsException("Cannot rename field '$table.$field' to '$field_new': target field already exists."); + } + if (isset($new_keys['primary key']) && in_array($field_new, $new_keys['primary key'], TRUE)) { + $this->ensureNotNullPrimaryKey($new_keys['primary key'], [$field_new => $spec]); + } + + $spec = $this->processField($spec); + + // Type 'serial' is known to PostgreSQL, but only during table creation, + // not when altering. Because of that, we create it here as an 'int'. After + // we create it we manually re-apply the sequence. + if (in_array($spec['pgsql_type'], ['serial', 'bigserial'])) { + $field_def = 'int'; + } + else { + $field_def = $spec['pgsql_type']; + } + + if (in_array($spec['pgsql_type'], ['varchar', 'character', 'text']) && isset($spec['length'])) { + $field_def .= '(' . $spec['length'] . ')'; + } + elseif (isset($spec['precision']) && isset($spec['scale'])) { + $field_def .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')'; + } + + // Remove old check constraints. + $field_info = $this->queryFieldInformation($table, $field); + + foreach ($field_info as $check) { + $this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT "' . $check . '"'); + } + + // Remove old default. + $this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN "' . $field . '" DROP DEFAULT'); + + // Convert field type. + // Usually, we do this via a simple typecast 'USING fieldname::type'. But + // the typecast does not work for conversions to bytea. + // @see http://www.postgresql.org/docs/current/static/datatype-binary.html + $table_information = $this->queryTableInformation($table); + $is_bytea = !empty($table_information->blob_fields[$field]); + if ($spec['pgsql_type'] != 'bytea') { + if ($is_bytea) { + $this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $field_def . ' USING convert_from("' . $field . '"' . ", 'UTF8')"); + } + else { + $this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $field_def . ' USING "' . $field . '"::' . $field_def); + } + } + else { + // Do not attempt to convert a field that is bytea already. + if (!$is_bytea) { + // Convert to a bytea type by using the SQL replace() function to + // convert any single backslashes in the field content to double + // backslashes ('\' to '\\'). + $this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $field_def . ' USING decode(replace("' . $field . '"' . ", E'\\\\', E'\\\\\\\\'), 'escape');"); + } + } + + if (isset($spec['not null'])) { + if ($spec['not null']) { + $null_action = 'SET NOT NULL'; + } + else { + $null_action = 'DROP NOT NULL'; + } + $this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" ' . $null_action); + } + + if (in_array($spec['pgsql_type'], ['serial', 'bigserial'])) { + // Type "serial" is known to PostgreSQL, but *only* during table creation, + // not when altering. Because of that, the sequence needs to be created + // and initialized by hand. + $seq = $this->connection->makeSequenceName($table, $field_new); + $this->connection->query("CREATE SEQUENCE " . $seq); + // Set sequence to maximal field value to not conflict with existing + // entries. + $this->connection->query("SELECT setval('" . $seq . "', MAX(\"" . $field . '")) FROM {' . $table . "}"); + $this->connection->query('ALTER TABLE {' . $table . '} ALTER ' . $field . ' SET DEFAULT nextval(' . $this->connection->quote($seq) . ')'); + } + + // Rename the column if necessary. + if ($field != $field_new) { + $this->connection->query('ALTER TABLE {' . $table . '} RENAME "' . $field . '" TO "' . $field_new . '"'); + } + + // Add unsigned check if necessary. + if (!empty($spec['unsigned'])) { + $this->connection->query('ALTER TABLE {' . $table . '} ADD CHECK ("' . $field_new . '" >= 0)'); + } + + // Add default if necessary. + if (isset($spec['default'])) { + $this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN "' . $field_new . '" SET DEFAULT ' . $this->escapeDefaultValue($spec['default'])); + } + + // Change description if necessary. + if (!empty($spec['description'])) { + $this->connection->query('COMMENT ON COLUMN {' . $table . '}."' . $field_new . '" IS ' . $this->prepareComment($spec['description'])); + } + + if (isset($new_keys)) { + $this->_createKeys($table, $new_keys); + } + $this->resetTableInformation($table); + } + + protected function _createIndexSql($table, $name, $fields) { + $query = 'CREATE INDEX ' . $this->ensureIdentifiersLength($table, $name, 'idx') . ' ON {' . $table . '} ('; + $query .= $this->_createKeySql($fields) . ')'; + return $query; + } + + protected function _createKeys($table, $new_keys) { + if (isset($new_keys['primary key'])) { + $this->addPrimaryKey($table, $new_keys['primary key']); + } + if (isset($new_keys['unique keys'])) { + foreach ($new_keys['unique keys'] as $name => $fields) { + $this->addUniqueKey($table, $name, $fields); + } + } + if (isset($new_keys['indexes'])) { + foreach ($new_keys['indexes'] as $name => $fields) { + // Even though $new_keys is not a full schema it still has 'indexes' and + // so is a partial schema. Technically addIndex() doesn't do anything + // with it so passing an empty array would work as well. + $this->addIndex($table, $name, $fields, $new_keys); + } + } + } + + /** + * Retrieve a table or column comment. + */ + public function getComment($table, $column = NULL) { + $info = $this->getPrefixInfo($table); + // Don't use {} around pg_class, pg_attribute tables. + if (isset($column)) { + return $this->connection->query('SELECT col_description(oid, attnum) FROM pg_class, pg_attribute WHERE attrelid = oid AND relname = ? AND attname = ?', [$info['table'], $column])->fetchField(); + } + else { + return $this->connection->query('SELECT obj_description(oid, ?) FROM pg_class WHERE relname = ?', ['pg_class', $info['table']])->fetchField(); + } + } + + /** + * Calculates a base-64 encoded, PostgreSQL-safe sha-256 hash per PostgreSQL + * documentation: 4.1. Lexical Structure. + * + * @param $data + * String to be hashed. + * + * @return string + * A base-64 encoded sha-256 hash, with + and / replaced with _ and any = + * padding characters removed. + */ + protected function hashBase64($data) { + $hash = base64_encode(hash('sha256', $data, TRUE)); + // Modify the hash so it's safe to use in PostgreSQL identifiers. + return strtr($hash, ['+' => '_', '/' => '_', '=' => '']); + } + + /** + * Determines whether the PostgreSQL extension is created. + * + * @param string $name + * The name of the extension. + * + * @return bool + * Return TRUE when the extension is created, FALSE otherwise. + * + * @internal + */ + public function extensionExists($name): bool { + return (bool) $this->connection->query('SELECT installed_version FROM pg_available_extensions WHERE name = :name', [ + ':name' => $name, + ])->fetchField(); + } + +} + +/** + * @} End of "addtogroup schemaapi". + */ diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Select.php b/core/modules/pgsql/src/Driver/Database/pgsql/Select.php new file mode 100644 index 000000000000..959b6092d92b --- /dev/null +++ b/core/modules/pgsql/src/Driver/Database/pgsql/Select.php @@ -0,0 +1,160 @@ +<?php + +namespace Drupal\pgsql\Driver\Database\pgsql; + +use Drupal\Core\Database\Query\Select as QuerySelect; + +/** + * @addtogroup database + * @{ + */ + +/** + * PostgreSQL implementation of \Drupal\Core\Database\Query\Select. + */ +class Select extends QuerySelect { + + public function orderRandom() { + $alias = $this->addExpression('RANDOM()', 'random_field'); + $this->orderBy($alias); + return $this; + } + + /** + * Overrides SelectQuery::orderBy(). + * + * PostgreSQL adheres strictly to the SQL-92 standard and requires that when + * using DISTINCT or GROUP BY conditions, fields and expressions that are + * ordered on also need to be selected. This is a best effort implementation + * to handle the cases that can be automated by adding the field if it is not + * yet selected. + * + * @code + * $query = \Drupal::database()->select('example', 'e'); + * $query->join('example_revision', 'er', '[e].[vid] = [er].[vid]'); + * $query + * ->distinct() + * ->fields('e') + * ->orderBy('timestamp'); + * @endcode + * + * In this query, it is not possible (without relying on the schema) to know + * whether timestamp belongs to example_revision and needs to be added or + * belongs to node and is already selected. Queries like this will need to be + * corrected in the original query by adding an explicit call to + * SelectQuery::addField() or SelectQuery::fields(). + * + * Since this has a small performance impact, both by the additional + * processing in this function and in the database that needs to return the + * additional fields, this is done as an override instead of implementing it + * directly in SelectQuery::orderBy(). + */ + public function orderBy($field, $direction = 'ASC') { + // Only allow ASC and DESC, default to ASC. + // Emulate MySQL default behavior to sort NULL values first for ascending, + // and last for descending. + // @see http://www.postgresql.org/docs/9.3/static/queries-order.html + $direction = strtoupper($direction) == 'DESC' ? 'DESC NULLS LAST' : 'ASC NULLS FIRST'; + $this->order[$field] = $direction; + + if ($this->hasTag('entity_query')) { + return $this; + } + + // If there is a table alias specified, split it up. + if (strpos($field, '.') !== FALSE) { + [$table, $table_field] = explode('.', $field); + } + // Figure out if the field has already been added. + foreach ($this->fields as $existing_field) { + if (!empty($table)) { + // If table alias is given, check if field and table exists. + if ($existing_field['table'] == $table && $existing_field['field'] == $table_field) { + return $this; + } + } + else { + // If there is no table, simply check if the field exists as a field or + // an aliased field. + if ($existing_field['alias'] == $field) { + return $this; + } + } + } + + // Also check expression aliases. + foreach ($this->expressions as $expression) { + if ($expression['alias'] == $this->connection->escapeAlias($field)) { + return $this; + } + } + + // If a table loads all fields, it can not be added again. It would + // result in an ambiguous alias error because that field would be loaded + // twice: Once through table_alias.* and once directly. If the field + // actually belongs to a different table, it must be added manually. + foreach ($this->tables as $table) { + if (!empty($table['all_fields'])) { + return $this; + } + } + + // If $field contains characters which are not allowed in a field name + // it is considered an expression, these can't be handled automatically + // either. + if ($this->connection->escapeField($field) != $field) { + return $this; + } + + // This is a case that can be handled automatically, add the field. + $this->addField(NULL, $field); + return $this; + } + + /** + * {@inheritdoc} + */ + public function addExpression($expression, $alias = NULL, $arguments = []) { + if (empty($alias)) { + $alias = 'expression'; + } + + // This implements counting in the same manner as the parent method. + $alias_candidate = $alias; + $count = 2; + while (!empty($this->expressions[$alias_candidate])) { + $alias_candidate = $alias . '_' . $count++; + } + $alias = $alias_candidate; + + $this->expressions[$alias] = [ + 'expression' => $expression, + 'alias' => $this->connection->escapeAlias($alias_candidate), + 'arguments' => $arguments, + ]; + + return $alias; + } + + /** + * {@inheritdoc} + */ + public function execute() { + $this->connection->addSavepoint(); + try { + $result = parent::execute(); + } + catch (\Exception $e) { + $this->connection->rollbackSavepoint(); + throw $e; + } + $this->connection->releaseSavepoint(); + + return $result; + } + +} + +/** + * @} End of "addtogroup database". + */ diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Truncate.php b/core/modules/pgsql/src/Driver/Database/pgsql/Truncate.php new file mode 100644 index 000000000000..18115e0a7d05 --- /dev/null +++ b/core/modules/pgsql/src/Driver/Database/pgsql/Truncate.php @@ -0,0 +1,29 @@ +<?php + +namespace Drupal\pgsql\Driver\Database\pgsql; + +use Drupal\Core\Database\Query\Truncate as QueryTruncate; + +/** + * PostgreSQL implementation of \Drupal\Core\Database\Query\Truncate. + */ +class Truncate extends QueryTruncate { + + /** + * {@inheritdoc} + */ + public function execute() { + $this->connection->addSavepoint(); + try { + $result = parent::execute(); + } + catch (\Exception $e) { + $this->connection->rollbackSavepoint(); + throw $e; + } + $this->connection->releaseSavepoint(); + + return $result; + } + +} diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Update.php b/core/modules/pgsql/src/Driver/Database/pgsql/Update.php new file mode 100644 index 000000000000..d3f2ebf6431d --- /dev/null +++ b/core/modules/pgsql/src/Driver/Database/pgsql/Update.php @@ -0,0 +1,83 @@ +<?php + +namespace Drupal\pgsql\Driver\Database\pgsql; + +use Drupal\Core\Database\Query\Update as QueryUpdate; +use Drupal\Core\Database\Query\SelectInterface; + +/** + * PostgreSQL implementation of \Drupal\Core\Database\Query\Update. + */ +class Update extends QueryUpdate { + + public function execute() { + $max_placeholder = 0; + $blobs = []; + $blob_count = 0; + + // Because we filter $fields the same way here and in __toString(), the + // placeholders will all match up properly. + $stmt = $this->connection->prepareStatement((string) $this, $this->queryOptions, TRUE); + + // Fetch the list of blobs and sequences used on that table. + $table_information = $this->connection->schema()->queryTableInformation($this->table); + + // Expressions take priority over literal fields, so we process those first + // and remove any literal fields that conflict. + $fields = $this->fields; + foreach ($this->expressionFields as $field => $data) { + if (!empty($data['arguments'])) { + foreach ($data['arguments'] as $placeholder => $argument) { + // We assume that an expression will never happen on a BLOB field, + // which is a fairly safe assumption to make since in most cases + // it would be an invalid query anyway. + $stmt->getClientStatement()->bindParam($placeholder, $data['arguments'][$placeholder]); + } + } + if ($data['expression'] instanceof SelectInterface) { + $data['expression']->compile($this->connection, $this); + $select_query_arguments = $data['expression']->arguments(); + foreach ($select_query_arguments as $placeholder => $argument) { + $stmt->getClientStatement()->bindParam($placeholder, $select_query_arguments[$placeholder]); + } + } + unset($fields[$field]); + } + + foreach ($fields as $field => $value) { + $placeholder = ':db_update_placeholder_' . ($max_placeholder++); + + if (isset($table_information->blob_fields[$field]) && $value !== NULL) { + $blobs[$blob_count] = fopen('php://memory', 'a'); + fwrite($blobs[$blob_count], $value); + rewind($blobs[$blob_count]); + $stmt->getClientStatement()->bindParam($placeholder, $blobs[$blob_count], \PDO::PARAM_LOB); + ++$blob_count; + } + else { + $stmt->getClientStatement()->bindParam($placeholder, $fields[$field]); + } + } + + if (count($this->condition)) { + $this->condition->compile($this->connection, $this); + + $arguments = $this->condition->arguments(); + foreach ($arguments as $placeholder => $value) { + $stmt->getClientStatement()->bindParam($placeholder, $arguments[$placeholder]); + } + } + + $this->connection->addSavepoint(); + try { + $stmt->execute(NULL, $this->queryOptions); + $this->connection->releaseSavepoint(); + return $stmt->rowCount(); + } + catch (\Exception $e) { + $this->connection->rollbackSavepoint(); + $this->connection->exceptionHandler()->handleExecutionException($e, $stmt, [], $this->queryOptions); + } + } + +} diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Upsert.php b/core/modules/pgsql/src/Driver/Database/pgsql/Upsert.php new file mode 100644 index 000000000000..35823a270b1c --- /dev/null +++ b/core/modules/pgsql/src/Driver/Database/pgsql/Upsert.php @@ -0,0 +1,126 @@ +<?php + +namespace Drupal\pgsql\Driver\Database\pgsql; + +use Drupal\Core\Database\Query\Upsert as QueryUpsert; + +// cSpell:ignore nextval setval + +/** + * PostgreSQL implementation of \Drupal\Core\Database\Query\Upsert. + */ +class Upsert extends QueryUpsert { + + /** + * {@inheritdoc} + */ + public function execute() { + if (!$this->preExecute()) { + return NULL; + } + + $stmt = $this->connection->prepareStatement((string) $this, $this->queryOptions, TRUE); + + // Fetch the list of blobs and sequences used on that table. + $table_information = $this->connection->schema()->queryTableInformation($this->table); + + $max_placeholder = 0; + $blobs = []; + $blob_count = 0; + foreach ($this->insertValues as $insert_values) { + foreach ($this->insertFields as $idx => $field) { + if (isset($table_information->blob_fields[$field]) && $insert_values[$idx] !== NULL) { + $blobs[$blob_count] = fopen('php://memory', 'a'); + fwrite($blobs[$blob_count], $insert_values[$idx]); + rewind($blobs[$blob_count]); + + $stmt->getClientStatement()->bindParam(':db_insert_placeholder_' . $max_placeholder++, $blobs[$blob_count], \PDO::PARAM_LOB); + + // Pre-increment is faster in PHP than increment. + ++$blob_count; + } + else { + $stmt->getClientStatement()->bindParam(':db_insert_placeholder_' . $max_placeholder++, $insert_values[$idx]); + } + } + // Check if values for a serial field has been passed. + if (!empty($table_information->serial_fields)) { + foreach ($table_information->serial_fields as $index => $serial_field) { + $serial_key = array_search($serial_field, $this->insertFields); + if ($serial_key !== FALSE) { + $serial_value = $insert_values[$serial_key]; + + // Sequences must be greater than or equal to 1. + if ($serial_value === NULL || !$serial_value) { + $serial_value = 1; + } + // Set the sequence to the bigger value of either the passed + // value or the max value of the column. It can happen that another + // thread calls nextval() which could lead to a serial number being + // used twice. However, trying to insert a value into a serial + // column should only be done in very rare cases and is not thread + // safe by definition. + $this->connection->query("SELECT setval('" . $table_information->sequences[$index] . "', GREATEST(MAX(" . $serial_field . "), :serial_value)) FROM {" . $this->table . "}", [':serial_value' => (int) $serial_value]); + } + } + } + } + + $options = $this->queryOptions; + if (!empty($table_information->sequences)) { + $options['sequence_name'] = $table_information->sequences[0]; + } + + // Re-initialize the values array so that we can re-use this query. + $this->insertValues = []; + + // 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 + // fails. This is important for tables that are created on demand. For + // example, \Drupal\Core\Cache\DatabaseBackend. + $this->connection->addSavepoint(); + try { + $stmt->execute(NULL, $options); + $this->connection->releaseSavepoint(); + return $stmt->rowCount(); + } + catch (\Exception $e) { + $this->connection->rollbackSavepoint(); + $this->connection->exceptionHandler()->handleExecutionException($e, $stmt, [], $options); + } + } + + /** + * {@inheritdoc} + */ + public function __toString() { + // Create a sanitized comment string to prepend to the query. + $comments = $this->connection->makeComment($this->comments); + + // Default fields are always placed first for consistency. + $insert_fields = array_merge($this->defaultFields, $this->insertFields); + $insert_fields = array_map(function ($field) { + return $this->connection->escapeField($field); + }, $insert_fields); + + $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES '; + + $values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields); + $query .= implode(', ', $values); + + // Updating the unique / primary key is not necessary. + unset($insert_fields[$this->key]); + + $update = []; + foreach ($insert_fields as $field) { + // The "excluded." prefix causes the field to refer to the value for field + // that would have been inserted had there been no conflict. + $update[] = "$field = EXCLUDED.$field"; + } + + $query .= ' ON CONFLICT (' . $this->connection->escapeField($this->key) . ') DO UPDATE SET ' . implode(', ', $update); + + return $query; + } + +} diff --git a/core/modules/sqlite/sqlite.info.yml b/core/modules/sqlite/sqlite.info.yml new file mode 100644 index 000000000000..a5f55f4e5318 --- /dev/null +++ b/core/modules/sqlite/sqlite.info.yml @@ -0,0 +1,5 @@ +name: SQLite +type: module +description: 'Database driver for SQLite.' +package: Core +version: VERSION diff --git a/core/modules/sqlite/sqlite.module b/core/modules/sqlite/sqlite.module new file mode 100644 index 000000000000..4cfb9923aac9 --- /dev/null +++ b/core/modules/sqlite/sqlite.module @@ -0,0 +1,22 @@ +<?php + +/** + * @file + * The SQLite module provides the connection between Drupal and a SQLite database. + */ + +use Drupal\Core\Routing\RouteMatchInterface; + +/** + * Implements hook_help(). + */ +function sqlite_help($route_name, RouteMatchInterface $route_match) { + switch ($route_name) { + case 'help.page.sqlite': + $output = ''; + $output .= '<h3>' . t('About') . '</h3>'; + $output .= '<p>' . t('The SQLite module provides the connection between Drupal and a SQLite database. For more information, see the <a href=":sqlite">online documentation for the SQLite module</a>.', [':sqlite' => 'https://www.drupal.org/documentation/modules/sqlite']) . '</p>'; + return $output; + + } +} diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php b/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php new file mode 100644 index 000000000000..5a1ab3b9d42b --- /dev/null +++ b/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php @@ -0,0 +1,528 @@ +<?php + +namespace Drupal\sqlite\Driver\Database\sqlite; + +use Drupal\Core\Database\DatabaseNotFoundException; +use Drupal\Core\Database\Connection as DatabaseConnection; +use Drupal\Core\Database\StatementInterface; + +/** + * SQLite implementation of \Drupal\Core\Database\Connection. + */ +class Connection extends DatabaseConnection { + + /** + * Error code for "Unable to open database file" error. + */ + const DATABASE_NOT_FOUND = 14; + + /** + * {@inheritdoc} + */ + protected $statementClass = NULL; + + /** + * {@inheritdoc} + */ + protected $statementWrapperClass = NULL; + + /** + * Whether or not the active transaction (if any) will be rolled back. + * + * @var bool + */ + protected $willRollback; + + /** + * A map of condition operators to SQLite operators. + * + * We don't want to override any of the defaults. + */ + protected static $sqliteConditionOperatorMap = [ + 'LIKE' => ['postfix' => " ESCAPE '\\'"], + 'NOT LIKE' => ['postfix' => " ESCAPE '\\'"], + 'LIKE BINARY' => ['postfix' => " ESCAPE '\\'", 'operator' => 'GLOB'], + 'NOT LIKE BINARY' => ['postfix' => " ESCAPE '\\'", 'operator' => 'NOT GLOB'], + ]; + + /** + * All databases attached to the current database. + * + * This is used to allow prefixes to be safely handled without locking the + * table. + * + * @var array + */ + protected $attachedDatabases = []; + + /** + * Whether or not a table has been dropped this request. + * + * The destructor will only try to get rid of unnecessary databases if there + * is potential of them being empty. + * + * This variable is set to public because Schema needs to + * access it. However, it should not be manually set. + * + * @var bool + */ + public $tableDropped = FALSE; + + /** + * {@inheritdoc} + */ + protected $transactionalDDLSupport = TRUE; + + /** + * {@inheritdoc} + */ + protected $identifierQuotes = ['"', '"']; + + /** + * Constructs a \Drupal\sqlite\Driver\Database\sqlite\Connection object. + */ + public function __construct(\PDO $connection, array $connection_options) { + parent::__construct($connection, $connection_options); + + // Attach one database for each registered prefix. + $prefixes = $this->prefixes; + foreach ($prefixes as &$prefix) { + // Empty prefix means query the main database -- no need to attach + // anything. + if ($prefix !== '') { + $this->attachDatabase($prefix); + // Add a ., so queries become prefix.table, which is proper syntax for + // querying an attached database. + $prefix .= '.'; + } + } + + // Regenerate the prefixes replacement table. + $this->setPrefix($prefixes); + } + + /** + * {@inheritdoc} + */ + public static function open(array &$connection_options = []) { + // Allow PDO options to be overridden. + $connection_options += [ + 'pdo' => [], + ]; + $connection_options['pdo'] += [ + \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, + // Convert numeric values to strings when fetching. + \PDO::ATTR_STRINGIFY_FETCHES => TRUE, + ]; + + try { + $pdo = new \PDO('sqlite:' . $connection_options['database'], '', '', $connection_options['pdo']); + } + catch (\PDOException $e) { + if ($e->getCode() == static::DATABASE_NOT_FOUND) { + throw new DatabaseNotFoundException($e->getMessage(), $e->getCode(), $e); + } + // SQLite doesn't have a distinct error code for access denied, so don't + // deal with that case. + throw $e; + } + + // Create functions needed by SQLite. + $pdo->sqliteCreateFunction('if', [__CLASS__, 'sqlFunctionIf']); + $pdo->sqliteCreateFunction('greatest', [__CLASS__, 'sqlFunctionGreatest']); + $pdo->sqliteCreateFunction('least', [__CLASS__, 'sqlFunctionLeast']); + $pdo->sqliteCreateFunction('pow', 'pow', 2); + $pdo->sqliteCreateFunction('exp', 'exp', 1); + $pdo->sqliteCreateFunction('length', 'strlen', 1); + $pdo->sqliteCreateFunction('md5', 'md5', 1); + $pdo->sqliteCreateFunction('concat', [__CLASS__, 'sqlFunctionConcat']); + $pdo->sqliteCreateFunction('concat_ws', [__CLASS__, 'sqlFunctionConcatWs']); + $pdo->sqliteCreateFunction('substring', [__CLASS__, 'sqlFunctionSubstring'], 3); + $pdo->sqliteCreateFunction('substring_index', [__CLASS__, 'sqlFunctionSubstringIndex'], 3); + $pdo->sqliteCreateFunction('rand', [__CLASS__, 'sqlFunctionRand']); + $pdo->sqliteCreateFunction('regexp', [__CLASS__, 'sqlFunctionRegexp']); + + // SQLite does not support the LIKE BINARY operator, so we overload the + // non-standard GLOB operator for case-sensitive matching. Another option + // would have been to override another non-standard operator, MATCH, but + // that does not support the NOT keyword prefix. + $pdo->sqliteCreateFunction('glob', [__CLASS__, 'sqlFunctionLikeBinary']); + + // Create a user-space case-insensitive collation with UTF-8 support. + $pdo->sqliteCreateCollation('NOCASE_UTF8', ['Drupal\Component\Utility\Unicode', 'strcasecmp']); + + // Set SQLite init_commands if not already defined. Enable the Write-Ahead + // Logging (WAL) for SQLite. See https://www.drupal.org/node/2348137 and + // https://www.sqlite.org/wal.html. + $connection_options += [ + 'init_commands' => [], + ]; + $connection_options['init_commands'] += [ + 'wal' => "PRAGMA journal_mode=WAL", + ]; + + // Execute sqlite init_commands. + if (isset($connection_options['init_commands'])) { + $pdo->exec(implode('; ', $connection_options['init_commands'])); + } + + return $pdo; + } + + /** + * Destructor for the SQLite connection. + * + * We prune empty databases on destruct, but only if tables have been + * dropped. This is especially needed when running the test suite, which + * creates and destroy databases several times in a row. + */ + public function __destruct() { + if ($this->tableDropped && !empty($this->attachedDatabases)) { + foreach ($this->attachedDatabases as $prefix) { + // Check if the database is now empty, ignore the internal SQLite tables. + try { + $count = $this->query('SELECT COUNT(*) FROM ' . $prefix . '.sqlite_master WHERE type = :type AND name NOT LIKE :pattern', [':type' => 'table', ':pattern' => 'sqlite_%'])->fetchField(); + + // We can prune the database file if it doesn't have any tables. + if ($count == 0 && $this->connectionOptions['database'] != ':memory:' && file_exists($this->connectionOptions['database'] . '-' . $prefix)) { + // Detach the database. + $this->query('DETACH DATABASE :schema', [':schema' => $prefix]); + // Destroy the database file. + unlink($this->connectionOptions['database'] . '-' . $prefix); + } + } + catch (\Exception $e) { + // Ignore the exception and continue. There is nothing we can do here + // to report the error or fail safe. + } + } + } + parent::__destruct(); + } + + /** + * {@inheritdoc} + */ + public function attachDatabase(string $database): void { + // Only attach the database once. + if (!isset($this->attachedDatabases[$database])) { + // In memory database use ':memory:' as database name. According to + // http://www.sqlite.org/inmemorydb.html it will open a unique database so + // attaching it twice is not a problem. + $database_file = $this->connectionOptions['database'] !== ':memory:' ? $this->connectionOptions['database'] . '-' . $database : $this->connectionOptions['database']; + $this->query('ATTACH DATABASE :database_file AS :database', [':database_file' => $database_file, ':database' => $database]); + $this->attachedDatabases[$database] = $database; + } + } + + /** + * Gets all the attached databases. + * + * @return array + * An array of attached database names. + * + * @see \Drupal\sqlite\Driver\Database\sqlite\Connection::__construct() + */ + public function getAttachedDatabases() { + return $this->attachedDatabases; + } + + /** + * SQLite compatibility implementation for the IF() SQL function. + */ + public static function sqlFunctionIf($condition, $expr1, $expr2 = NULL) { + return $condition ? $expr1 : $expr2; + } + + /** + * SQLite compatibility implementation for the GREATEST() SQL function. + */ + public static function sqlFunctionGreatest() { + $args = func_get_args(); + foreach ($args as $v) { + if (!isset($v)) { + unset($args); + } + } + if (count($args)) { + return max($args); + } + else { + return NULL; + } + } + + /** + * SQLite compatibility implementation for the LEAST() SQL function. + */ + public static function sqlFunctionLeast() { + // Remove all NULL, FALSE and empty strings values but leaves 0 (zero) values. + $values = array_filter(func_get_args(), 'strlen'); + + return count($values) < 1 ? NULL : min($values); + } + + /** + * SQLite compatibility implementation for the CONCAT() SQL function. + */ + public static function sqlFunctionConcat() { + $args = func_get_args(); + return implode('', $args); + } + + /** + * SQLite compatibility implementation for the CONCAT_WS() SQL function. + * + * @see http://dev.mysql.com/doc/refman/5.6/en/string-functions.html#function_concat-ws + */ + public static function sqlFunctionConcatWs() { + $args = func_get_args(); + $separator = array_shift($args); + // If the separator is NULL, the result is NULL. + if ($separator === FALSE || is_null($separator)) { + return NULL; + } + // Skip any NULL values after the separator argument. + $args = array_filter($args, function ($value) { + return !is_null($value); + }); + return implode($separator, $args); + } + + /** + * SQLite compatibility implementation for the SUBSTRING() SQL function. + */ + public static function sqlFunctionSubstring($string, $from, $length) { + return substr($string, $from - 1, $length); + } + + /** + * SQLite compatibility implementation for the SUBSTRING_INDEX() SQL function. + */ + public static function sqlFunctionSubstringIndex($string, $delimiter, $count) { + // If string is empty, simply return an empty string. + if (empty($string)) { + return ''; + } + $end = 0; + for ($i = 0; $i < $count; $i++) { + $end = strpos($string, $delimiter, $end + 1); + if ($end === FALSE) { + $end = strlen($string); + } + } + return substr($string, 0, $end); + } + + /** + * SQLite compatibility implementation for the RAND() SQL function. + */ + public static function sqlFunctionRand($seed = NULL) { + if (isset($seed)) { + mt_srand($seed); + } + return mt_rand() / mt_getrandmax(); + } + + /** + * SQLite compatibility implementation for the REGEXP SQL operator. + * + * The REGEXP operator is natively known, but not implemented by default. + * + * @see http://www.sqlite.org/lang_expr.html#regexp + */ + public static function sqlFunctionRegexp($pattern, $subject) { + // preg_quote() cannot be used here, since $pattern may contain reserved + // regular expression characters already (such as ^, $, etc). Therefore, + // use a rare character as PCRE delimiter. + $pattern = '#' . addcslashes($pattern, '#') . '#i'; + return preg_match($pattern, $subject); + } + + /** + * SQLite compatibility implementation for the LIKE BINARY SQL operator. + * + * SQLite supports case-sensitive LIKE operations through the + * 'case_sensitive_like' PRAGMA statement, but only for ASCII characters, so + * we have to provide our own implementation with UTF-8 support. + * + * @see https://sqlite.org/pragma.html#pragma_case_sensitive_like + * @see https://sqlite.org/lang_expr.html#like + */ + public static function sqlFunctionLikeBinary($pattern, $subject) { + // Replace the SQL LIKE wildcard meta-characters with the equivalent regular + // expression meta-characters and escape the delimiter that will be used for + // matching. + $pattern = str_replace(['%', '_'], ['.*?', '.'], preg_quote($pattern, '/')); + return preg_match('/^' . $pattern . '$/', $subject); + } + + /** + * {@inheritdoc} + */ + public function prepare($statement, array $driver_options = []) { + @trigger_error('Connection::prepare() is deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Database drivers should instantiate \PDOStatement objects by calling \PDO::prepare in their Connection::prepareStatement method instead. \PDO::prepare should not be called outside of driver code. See https://www.drupal.org/node/3137786', E_USER_DEPRECATED); + return new Statement($this->connection, $this, $statement, $driver_options); + } + + /** + * {@inheritdoc} + */ + protected function handleQueryException(\PDOException $e, $query, array $args = [], $options = []) { + // The database schema might be changed by another process in between the + // time that the statement was prepared and the time the statement was run + // (e.g. usually happens when running tests). In this case, we need to + // re-run the query. + // @see http://www.sqlite.org/faq.html#q15 + // @see http://www.sqlite.org/rescode.html#schema + if (!empty($e->errorInfo[1]) && $e->errorInfo[1] === 17) { + @trigger_error('Connection::handleQueryException() is deprecated in drupal:9.2.0 and is removed in drupal:10.0.0. Get a handler through $this->exceptionHandler() instead, and use one of its methods. See https://www.drupal.org/node/3187222', E_USER_DEPRECATED); + return $this->query($query, $args, $options); + } + + parent::handleQueryException($e, $query, $args, $options); + } + + public function queryRange($query, $from, $count, array $args = [], array $options = []) { + return $this->query($query . ' LIMIT ' . (int) $from . ', ' . (int) $count, $args, $options); + } + + /** + * {@inheritdoc} + */ + public function queryTemporary($query, array $args = [], array $options = []) { + @trigger_error('Connection::queryTemporary() is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. There is no replacement. See https://www.drupal.org/node/3211781', E_USER_DEPRECATED); + // Generate a new temporary table name and protect it from prefixing. + // SQLite requires that temporary tables to be non-qualified. + $tablename = $this->generateTemporaryTableName(); + $prefixes = $this->prefixes; + $prefixes[$tablename] = ''; + $this->setPrefix($prefixes); + + $this->query('CREATE TEMPORARY TABLE ' . $tablename . ' AS ' . $query, $args, $options); + return $tablename; + } + + public function driver() { + return 'sqlite'; + } + + public function databaseType() { + return 'sqlite'; + } + + /** + * Overrides \Drupal\Core\Database\Connection::createDatabase(). + * + * @param string $database + * The name of the database to create. + * + * @throws \Drupal\Core\Database\DatabaseNotFoundException + */ + public function createDatabase($database) { + // Verify the database is writable. + $db_directory = new \SplFileInfo(dirname($database)); + if (!$db_directory->isDir() && !\Drupal::service('file_system')->mkdir($db_directory->getPathName(), 0755, TRUE)) { + throw new DatabaseNotFoundException('Unable to create database directory ' . $db_directory->getPathName()); + } + } + + public function mapConditionOperator($operator) { + return static::$sqliteConditionOperatorMap[$operator] ?? NULL; + } + + /** + * {@inheritdoc} + */ + public function prepareStatement(string $query, array $options, bool $allow_row_count = FALSE): StatementInterface { + try { + $query = $this->preprocessStatement($query, $options); + $statement = new Statement($this->connection, $this, $query, $options['pdo'] ?? [], $allow_row_count); + } + catch (\Exception $e) { + $this->exceptionHandler()->handleStatementException($e, $query, $options); + } + return $statement; + } + + public function nextId($existing_id = 0) { + $this->startTransaction(); + // We can safely use literal queries here instead of the slower query + // builder because if a given database breaks here then it can simply + // override nextId. However, this is unlikely as we deal with short strings + // and integers and no known databases require special handling for those + // simple cases. If another transaction wants to write the same row, it will + // wait until this transaction commits. + $stmt = $this->prepareStatement('UPDATE {sequences} SET [value] = GREATEST([value], :existing_id) + 1', [], TRUE); + $args = [':existing_id' => $existing_id]; + try { + $stmt->execute($args); + } + catch (\Exception $e) { + $this->exceptionHandler()->handleExecutionException($e, $stmt, $args, []); + } + if ($stmt->rowCount() === 0) { + $this->query('INSERT INTO {sequences} ([value]) VALUES (:existing_id + 1)', $args); + } + // The transaction gets committed when the transaction object gets destroyed + // because it gets out of scope. + return $this->query('SELECT [value] FROM {sequences}')->fetchField(); + } + + /** + * {@inheritdoc} + */ + public function getFullQualifiedTableName($table) { + $prefix = $this->tablePrefix($table); + + // Don't include the SQLite database file name as part of the table name. + return $prefix . $table; + } + + /** + * {@inheritdoc} + */ + public static function createConnectionOptionsFromUrl($url, $root) { + $database = parent::createConnectionOptionsFromUrl($url, $root); + + // A SQLite database path with two leading slashes indicates a system path. + // Otherwise the path is relative to the Drupal root. + $url_components = parse_url($url); + if ($url_components['path'][0] === '/') { + $url_components['path'] = substr($url_components['path'], 1); + } + if ($url_components['path'][0] === '/' || $url_components['path'] === ':memory:') { + $database['database'] = $url_components['path']; + } + else { + $database['database'] = $root . '/' . $url_components['path']; + } + + // User credentials and system port are irrelevant for SQLite. + unset( + $database['username'], + $database['password'], + $database['port'] + ); + + return $database; + } + + /** + * {@inheritdoc} + */ + public static function createUrlFromConnectionOptions(array $connection_options) { + if (!isset($connection_options['driver'], $connection_options['database'])) { + throw new \InvalidArgumentException("As a minimum, the connection options array must contain at least the 'driver' and 'database' keys"); + } + + $db_url = 'sqlite://localhost/' . $connection_options['database'] . '?module=sqlite'; + + if (isset($connection_options['prefix']) && $connection_options['prefix'] !== '') { + $db_url .= '#' . $connection_options['prefix']; + } + + return $db_url; + } + +} diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Insert.php b/core/modules/sqlite/src/Driver/Database/sqlite/Insert.php new file mode 100644 index 000000000000..d1cc245e273e --- /dev/null +++ b/core/modules/sqlite/src/Driver/Database/sqlite/Insert.php @@ -0,0 +1,52 @@ +<?php + +namespace Drupal\sqlite\Driver\Database\sqlite; + +use Drupal\Core\Database\Query\Insert as QueryInsert; + +/** + * SQLite implementation of \Drupal\Core\Database\Query\Insert. + * + * We ignore all the default fields and use the clever SQLite syntax: + * INSERT INTO table DEFAULT VALUES + * for degenerated "default only" queries. + */ +class Insert extends QueryInsert { + + public function execute() { + if (!$this->preExecute()) { + return NULL; + } + if (count($this->insertFields) || !empty($this->fromQuery)) { + return parent::execute(); + } + else { + return $this->connection->query('INSERT INTO {' . $this->table . '} DEFAULT VALUES', [], $this->queryOptions); + } + } + + public function __toString() { + // Create a sanitized comment string to prepend to the query. + $comments = $this->connection->makeComment($this->comments); + + // Produce as many generic placeholders as necessary. + $placeholders = []; + if (!empty($this->insertFields)) { + $placeholders = array_fill(0, count($this->insertFields), '?'); + } + + $insert_fields = array_map(function ($field) { + return $this->connection->escapeField($field); + }, $this->insertFields); + + // 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)) { + $insert_fields_string = $insert_fields ? ' (' . implode(', ', $insert_fields) . ') ' : ' '; + return $comments . 'INSERT INTO {' . $this->table . '}' . $insert_fields_string . $this->fromQuery; + } + + return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES (' . implode(', ', $placeholders) . ')'; + } + +} diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Install/Tasks.php b/core/modules/sqlite/src/Driver/Database/sqlite/Install/Tasks.php new file mode 100644 index 000000000000..4d262a5ac4a0 --- /dev/null +++ b/core/modules/sqlite/src/Driver/Database/sqlite/Install/Tasks.php @@ -0,0 +1,115 @@ +<?php + +namespace Drupal\sqlite\Driver\Database\sqlite\Install; + +use Drupal\Core\Database\Database; +use Drupal\sqlite\Driver\Database\sqlite\Connection; +use Drupal\Core\Database\DatabaseNotFoundException; +use Drupal\Core\Database\Install\Tasks as InstallTasks; + +/** + * Specifies installation tasks for SQLite databases. + */ +class Tasks extends InstallTasks { + + /** + * Minimum required SQLite version. + * + * Use to build sqlite library with json1 option for JSON datatype support. + * @see https://www.sqlite.org/json1.html + */ + const SQLITE_MINIMUM_VERSION = '3.26'; + + /** + * {@inheritdoc} + */ + protected $pdoDriver = 'sqlite'; + + /** + * {@inheritdoc} + */ + public function name() { + return t('SQLite'); + } + + /** + * {@inheritdoc} + */ + public function minimumVersion() { + return static::SQLITE_MINIMUM_VERSION; + } + + /** + * {@inheritdoc} + */ + public function getFormOptions(array $database) { + $form = parent::getFormOptions($database); + + // Remove the options that only apply to client/server style databases. + unset($form['username'], $form['password'], $form['advanced_options']['host'], $form['advanced_options']['port']); + + // Make the text more accurate for SQLite. + $form['database']['#title'] = t('Database file'); + $form['database']['#description'] = t('The absolute path to the file where @drupal data will be stored. This must be writable by the web server and should exist outside of the web root.', ['@drupal' => drupal_install_profile_distribution_name()]); + $default_database = \Drupal::getContainer()->getParameter('site.path') . '/files/.ht.sqlite'; + $form['database']['#default_value'] = empty($database['database']) ? $default_database : $database['database']; + return $form; + } + + /** + * {@inheritdoc} + */ + protected function connect() { + try { + // This doesn't actually test the connection. + Database::setActiveConnection(); + // Now actually do a check. + Database::getConnection(); + $this->pass('Drupal can CONNECT to the database ok.'); + } + catch (\Exception $e) { + // Attempt to create the database if it is not found. + if ($e->getCode() == Connection::DATABASE_NOT_FOUND) { + // Remove the database string from connection info. + $connection_info = Database::getConnectionInfo(); + $database = $connection_info['default']['database']; + + // We cannot use \Drupal::service('file_system')->getTempDirectory() + // here because we haven't yet successfully connected to the database. + $connection_info['default']['database'] = \Drupal::service('file_system')->tempnam(sys_get_temp_dir(), 'sqlite'); + + // In order to change the Database::$databaseInfo array, need to remove + // the active connection, then re-add it with the new info. + Database::removeConnection('default'); + Database::addConnectionInfo('default', 'default', $connection_info['default']); + + try { + Database::getConnection()->createDatabase($database); + Database::closeConnection(); + + // Now, restore the database config. + Database::removeConnection('default'); + $connection_info['default']['database'] = $database; + Database::addConnectionInfo('default', 'default', $connection_info['default']); + + // Check the database connection. + Database::getConnection(); + $this->pass('Drupal can CONNECT to the database ok.'); + } + catch (DatabaseNotFoundException $e) { + // Still no dice; probably a permission issue. Raise the error to the + // installer. + $this->fail(t('Failed to open or create database file %database. The database engine reports the following message when attempting to create the database: %error.', ['%database' => $database, '%error' => $e->getMessage()])); + } + } + else { + // Database connection failed for some other reason than a non-existent + // database. + $this->fail(t('Failed to connect to database. The database engine reports the following message: %error.<ul><li>Does the database file exist?</li><li>Does web server have permission to write to the database file?</li>Does the web server have permission to write to the directory the database file should be created in?</li></ul>', ['%error' => $e->getMessage()])); + return FALSE; + } + } + return TRUE; + } + +} diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Schema.php b/core/modules/sqlite/src/Driver/Database/sqlite/Schema.php new file mode 100644 index 000000000000..dd88410715f3 --- /dev/null +++ b/core/modules/sqlite/src/Driver/Database/sqlite/Schema.php @@ -0,0 +1,837 @@ +<?php + +namespace Drupal\sqlite\Driver\Database\sqlite; + +use Drupal\Core\Database\SchemaObjectExistsException; +use Drupal\Core\Database\SchemaObjectDoesNotExistException; +use Drupal\Core\Database\Schema as DatabaseSchema; + +/** + * @ingroup schemaapi + * @{ + */ + +/** + * SQLite implementation of \Drupal\Core\Database\Schema. + */ +class Schema extends DatabaseSchema { + + /** + * Override DatabaseSchema::$defaultSchema. + * + * @var string + */ + protected $defaultSchema = 'main'; + + /** + * {@inheritdoc} + */ + public function tableExists($table) { + $info = $this->getPrefixInfo($table); + + // Don't use {} around sqlite_master table. + return (bool) $this->connection->query('SELECT 1 FROM ' . $info['schema'] . '.sqlite_master WHERE type = :type AND name = :name', [':type' => 'table', ':name' => $info['table']])->fetchField(); + } + + /** + * {@inheritdoc} + */ + public function fieldExists($table, $column) { + $schema = $this->introspectSchema($table); + return !empty($schema['fields'][$column]); + } + + /** + * Generate SQL to create a new table from a Drupal schema definition. + * + * @param $name + * The name of the table to create. + * @param $table + * A Schema API table definition array. + * + * @return + * An array of SQL statements to create the table. + */ + public function createTableSql($name, $table) { + if (!empty($table['primary key']) && is_array($table['primary key'])) { + $this->ensureNotNullPrimaryKey($table['primary key'], $table['fields']); + } + + $sql = []; + $sql[] = "CREATE TABLE {" . $name . "} (\n" . $this->createColumnsSql($name, $table) . "\n)\n"; + return array_merge($sql, $this->createIndexSql($name, $table)); + } + + /** + * Build the SQL expression for indexes. + */ + protected function createIndexSql($tablename, $schema) { + $sql = []; + $info = $this->getPrefixInfo($tablename); + if (!empty($schema['unique keys'])) { + foreach ($schema['unique keys'] as $key => $fields) { + $sql[] = 'CREATE UNIQUE INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $key . ' ON ' . $info['table'] . ' (' . $this->createKeySql($fields) . ")\n"; + } + } + if (!empty($schema['indexes'])) { + foreach ($schema['indexes'] as $key => $fields) { + $sql[] = 'CREATE INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $key . ' ON ' . $info['table'] . ' (' . $this->createKeySql($fields) . ")\n"; + } + } + return $sql; + } + + /** + * Build the SQL expression for creating columns. + */ + protected function createColumnsSql($tablename, $schema) { + $sql_array = []; + + // Add the SQL statement for each field. + foreach ($schema['fields'] as $name => $field) { + if (isset($field['type']) && $field['type'] == 'serial') { + if (isset($schema['primary key']) && ($key = array_search($name, $schema['primary key'])) !== FALSE) { + unset($schema['primary key'][$key]); + } + } + $sql_array[] = $this->createFieldSql($name, $this->processField($field)); + } + + // Process keys. + if (!empty($schema['primary key'])) { + $sql_array[] = " PRIMARY KEY (" . $this->createKeySql($schema['primary key']) . ")"; + } + + return implode(", \n", $sql_array); + } + + /** + * Build the SQL expression for keys. + */ + protected function createKeySql($fields) { + $return = []; + foreach ($fields as $field) { + if (is_array($field)) { + $return[] = $field[0]; + } + else { + $return[] = $field; + } + } + return implode(', ', $return); + } + + /** + * Set database-engine specific properties for a field. + * + * @param $field + * A field description array, as specified in the schema documentation. + */ + protected function processField($field) { + if (!isset($field['size'])) { + $field['size'] = 'normal'; + } + + // Set the correct database-engine specific datatype. + // In case one is already provided, force it to uppercase. + if (isset($field['sqlite_type'])) { + $field['sqlite_type'] = mb_strtoupper($field['sqlite_type']); + } + else { + $map = $this->getFieldTypeMap(); + $field['sqlite_type'] = $map[$field['type'] . ':' . $field['size']]; + + // Numeric fields with a specified scale have to be stored as floats. + if ($field['sqlite_type'] === 'NUMERIC' && isset($field['scale'])) { + $field['sqlite_type'] = 'FLOAT'; + } + } + + if (isset($field['type']) && $field['type'] == 'serial') { + $field['auto_increment'] = TRUE; + } + + return $field; + } + + /** + * Create an SQL string for a field to be used in table creation or alteration. + * + * Before passing a field out of a schema definition into this function it has + * to be processed by self::processField(). + * + * @param $name + * Name of the field. + * @param $spec + * The field specification, as per the schema data structure format. + */ + protected function createFieldSql($name, $spec) { + $name = $this->connection->escapeField($name); + if (!empty($spec['auto_increment'])) { + $sql = $name . " INTEGER PRIMARY KEY AUTOINCREMENT"; + if (!empty($spec['unsigned'])) { + $sql .= ' CHECK (' . $name . '>= 0)'; + } + } + else { + $sql = $name . ' ' . $spec['sqlite_type']; + + if (in_array($spec['sqlite_type'], ['VARCHAR', 'TEXT'])) { + if (isset($spec['length'])) { + $sql .= '(' . $spec['length'] . ')'; + } + + if (isset($spec['binary']) && $spec['binary'] === FALSE) { + $sql .= ' COLLATE NOCASE_UTF8'; + } + } + + if (isset($spec['not null'])) { + if ($spec['not null']) { + $sql .= ' NOT NULL'; + } + else { + $sql .= ' NULL'; + } + } + + if (!empty($spec['unsigned'])) { + $sql .= ' CHECK (' . $name . '>= 0)'; + } + + if (isset($spec['default'])) { + if (is_string($spec['default'])) { + $spec['default'] = $this->connection->quote($spec['default']); + } + $sql .= ' DEFAULT ' . $spec['default']; + } + + if (empty($spec['not null']) && !isset($spec['default'])) { + $sql .= ' DEFAULT NULL'; + } + } + return $sql; + } + + /** + * {@inheritdoc} + */ + public function getFieldTypeMap() { + // Put :normal last so it gets preserved by array_flip. This makes + // it much easier for modules (such as schema.module) to map + // database types back into schema types. + // $map does not use drupal_static as its value never changes. + static $map = [ + 'varchar_ascii:normal' => 'VARCHAR', + + 'varchar:normal' => 'VARCHAR', + 'char:normal' => 'CHAR', + + 'text:tiny' => 'TEXT', + 'text:small' => 'TEXT', + 'text:medium' => 'TEXT', + 'text:big' => 'TEXT', + 'text:normal' => 'TEXT', + + 'serial:tiny' => 'INTEGER', + 'serial:small' => 'INTEGER', + 'serial:medium' => 'INTEGER', + 'serial:big' => 'INTEGER', + 'serial:normal' => 'INTEGER', + + 'int:tiny' => 'INTEGER', + 'int:small' => 'INTEGER', + 'int:medium' => 'INTEGER', + 'int:big' => 'INTEGER', + 'int:normal' => 'INTEGER', + + 'float:tiny' => 'FLOAT', + 'float:small' => 'FLOAT', + 'float:medium' => 'FLOAT', + 'float:big' => 'FLOAT', + 'float:normal' => 'FLOAT', + + 'numeric:normal' => 'NUMERIC', + + 'blob:big' => 'BLOB', + 'blob:normal' => 'BLOB', + ]; + return $map; + } + + /** + * {@inheritdoc} + */ + public function renameTable($table, $new_name) { + if (!$this->tableExists($table)) { + throw new SchemaObjectDoesNotExistException("Cannot rename '$table' to '$new_name': table '$table' doesn't exist."); + } + if ($this->tableExists($new_name)) { + throw new SchemaObjectExistsException("Cannot rename '$table' to '$new_name': table '$new_name' already exists."); + } + + $schema = $this->introspectSchema($table); + + // SQLite doesn't allow you to rename tables outside of the current + // database. So the syntax '... RENAME TO database.table' would fail. + // So we must determine the full table name here rather than surrounding + // the table with curly braces in case the db_prefix contains a reference + // to a database outside of our existing database. + $info = $this->getPrefixInfo($new_name); + $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO ' . $info['table']); + + // Drop the indexes, there is no RENAME INDEX command in SQLite. + if (!empty($schema['unique keys'])) { + foreach ($schema['unique keys'] as $key => $fields) { + $this->dropIndex($table, $key); + } + } + if (!empty($schema['indexes'])) { + foreach ($schema['indexes'] as $index => $fields) { + $this->dropIndex($table, $index); + } + } + + // Recreate the indexes. + $statements = $this->createIndexSql($new_name, $schema); + foreach ($statements as $statement) { + $this->connection->query($statement); + } + } + + /** + * {@inheritdoc} + */ + public function dropTable($table) { + if (!$this->tableExists($table)) { + return FALSE; + } + $this->connection->tableDropped = TRUE; + $this->connection->query('DROP TABLE {' . $table . '}'); + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function addField($table, $field, $specification, $keys_new = []) { + if (!$this->tableExists($table)) { + throw new SchemaObjectDoesNotExistException("Cannot add field '$table.$field': table doesn't exist."); + } + if ($this->fieldExists($table, $field)) { + throw new SchemaObjectExistsException("Cannot add field '$table.$field': field already exists."); + } + if (isset($keys_new['primary key']) && in_array($field, $keys_new['primary key'], TRUE)) { + $this->ensureNotNullPrimaryKey($keys_new['primary key'], [$field => $specification]); + } + + // SQLite doesn't have a full-featured ALTER TABLE statement. It only + // supports adding new fields to a table, in some simple cases. In most + // cases, we have to create a new table and copy the data over. + if (empty($keys_new) && (empty($specification['not null']) || isset($specification['default']))) { + // When we don't have to create new keys and we are not creating a + // NOT NULL column without a default value, we can use the quicker version. + $query = 'ALTER TABLE {' . $table . '} ADD ' . $this->createFieldSql($field, $this->processField($specification)); + $this->connection->query($query); + + // Apply the initial value if set. + if (isset($specification['initial_from_field'])) { + if (isset($specification['initial'])) { + $expression = 'COALESCE(' . $specification['initial_from_field'] . ', :default_initial_value)'; + $arguments = [':default_initial_value' => $specification['initial']]; + } + else { + $expression = $specification['initial_from_field']; + $arguments = []; + } + $this->connection->update($table) + ->expression($field, $expression, $arguments) + ->execute(); + } + elseif (isset($specification['initial'])) { + $this->connection->update($table) + ->fields([$field => $specification['initial']]) + ->execute(); + } + } + else { + // We cannot add the field directly. Use the slower table alteration + // method, starting from the old schema. + $old_schema = $this->introspectSchema($table); + $new_schema = $old_schema; + + // Add the new field. + $new_schema['fields'][$field] = $specification; + + // Build the mapping between the old fields and the new fields. + $mapping = []; + if (isset($specification['initial_from_field'])) { + // If we have an initial value, copy it over. + if (isset($specification['initial'])) { + $expression = 'COALESCE(' . $specification['initial_from_field'] . ', :default_initial_value)'; + $arguments = [':default_initial_value' => $specification['initial']]; + } + else { + $expression = $specification['initial_from_field']; + $arguments = []; + } + $mapping[$field] = [ + 'expression' => $expression, + 'arguments' => $arguments, + ]; + } + elseif (isset($specification['initial'])) { + // If we have an initial value, copy it over. + $mapping[$field] = [ + 'expression' => ':newfieldinitial', + 'arguments' => [':newfieldinitial' => $specification['initial']], + ]; + } + else { + // Else use the default of the field. + $mapping[$field] = NULL; + } + + // Add the new indexes. + $new_schema = array_merge($new_schema, $keys_new); + + $this->alterTable($table, $old_schema, $new_schema, $mapping); + } + } + + /** + * Create a table with a new schema containing the old content. + * + * As SQLite does not support ALTER TABLE (with a few exceptions) it is + * necessary to create a new table and copy over the old content. + * + * @param $table + * Name of the table to be altered. + * @param $old_schema + * The old schema array for the table. + * @param $new_schema + * The new schema array for the table. + * @param $mapping + * An optional mapping between the fields of the old specification and the + * fields of the new specification. An associative array, whose keys are + * the fields of the new table, and values can take two possible forms: + * - a simple string, which is interpreted as the name of a field of the + * old table, + * - an associative array with two keys 'expression' and 'arguments', + * that will be used as an expression field. + */ + protected function alterTable($table, $old_schema, $new_schema, array $mapping = []) { + $i = 0; + do { + $new_table = $table . '_' . $i++; + } while ($this->tableExists($new_table)); + + $this->createTable($new_table, $new_schema); + + // Build a SQL query to migrate the data from the old table to the new. + $select = $this->connection->select($table); + + // Complete the mapping. + $possible_keys = array_keys($new_schema['fields']); + $mapping += array_combine($possible_keys, $possible_keys); + + // Now add the fields. + foreach ($mapping as $field_alias => $field_source) { + // Just ignore this field (ie. use its default value). + if (!isset($field_source)) { + continue; + } + + if (is_array($field_source)) { + $select->addExpression($field_source['expression'], $field_alias, $field_source['arguments']); + } + else { + $select->addField($table, $field_source, $field_alias); + } + } + + // Execute the data migration query. + $this->connection->insert($new_table) + ->from($select) + ->execute(); + + $old_count = $this->connection->query('SELECT COUNT(*) FROM {' . $table . '}')->fetchField(); + $new_count = $this->connection->query('SELECT COUNT(*) FROM {' . $new_table . '}')->fetchField(); + if ($old_count == $new_count) { + $this->dropTable($table); + $this->renameTable($new_table, $table); + } + } + + /** + * Find out the schema of a table. + * + * This function uses introspection methods provided by the database to + * create a schema array. This is useful, for example, during update when + * the old schema is not available. + * + * @param $table + * Name of the table. + * + * @return + * An array representing the schema. + * + * @throws \Exception + * If a column of the table could not be parsed. + */ + protected function introspectSchema($table) { + $mapped_fields = array_flip($this->getFieldTypeMap()); + $schema = [ + 'fields' => [], + 'primary key' => [], + 'unique keys' => [], + 'indexes' => [], + ]; + + $info = $this->getPrefixInfo($table); + $result = $this->connection->query('PRAGMA ' . $info['schema'] . '.table_info(' . $info['table'] . ')'); + foreach ($result as $row) { + if (preg_match('/^([^(]+)\((.*)\)$/', $row->type, $matches)) { + $type = $matches[1]; + $length = $matches[2]; + } + else { + $type = $row->type; + $length = NULL; + } + if (isset($mapped_fields[$type])) { + [$type, $size] = explode(':', $mapped_fields[$type]); + $schema['fields'][$row->name] = [ + 'type' => $type, + 'size' => $size, + 'not null' => !empty($row->notnull) || $row->pk !== "0", + ]; + if ($length) { + $schema['fields'][$row->name]['length'] = $length; + } + + // Convert the default into a properly typed value. + if ($row->dflt_value === 'NULL') { + $schema['fields'][$row->name]['default'] = NULL; + } + elseif (is_string($row->dflt_value) && $row->dflt_value[0] === '\'') { + // Remove the wrapping single quotes. And replace duplicate single + // quotes with a single quote. + $schema['fields'][$row->name]['default'] = str_replace("''", "'", substr($row->dflt_value, 1, -1)); + } + elseif (is_numeric($row->dflt_value)) { + // Adding 0 to a string will cause PHP to convert it to a float or + // an integer depending on what the string is. For example: + // - '1' + 0 = 1 + // - '1.0' + 0 = 1.0 + $schema['fields'][$row->name]['default'] = $row->dflt_value + 0; + } + else { + $schema['fields'][$row->name]['default'] = $row->dflt_value; + } + // $row->pk contains a number that reflects the primary key order. We + // use that as the key and sort (by key) below to return the primary key + // in the same order that it is stored in. + if ($row->pk) { + $schema['primary key'][$row->pk] = $row->name; + } + } + else { + throw new \Exception("Unable to parse the column type " . $row->type); + } + } + ksort($schema['primary key']); + // Re-key the array because $row->pk starts counting at 1. + $schema['primary key'] = array_values($schema['primary key']); + + $indexes = []; + $result = $this->connection->query('PRAGMA ' . $info['schema'] . '.index_list(' . $info['table'] . ')'); + foreach ($result as $row) { + if (strpos($row->name, 'sqlite_autoindex_') !== 0) { + $indexes[] = [ + 'schema_key' => $row->unique ? 'unique keys' : 'indexes', + 'name' => $row->name, + ]; + } + } + foreach ($indexes as $index) { + $name = $index['name']; + // Get index name without prefix. + $index_name = substr($name, strlen($info['table']) + 1); + $result = $this->connection->query('PRAGMA ' . $info['schema'] . '.index_info(' . $name . ')'); + foreach ($result as $row) { + $schema[$index['schema_key']][$index_name][] = $row->name; + } + } + return $schema; + } + + /** + * {@inheritdoc} + */ + public function dropField($table, $field) { + if (!$this->fieldExists($table, $field)) { + return FALSE; + } + + $old_schema = $this->introspectSchema($table); + $new_schema = $old_schema; + + unset($new_schema['fields'][$field]); + + // Drop the primary key if the field to drop is part of it. This is + // consistent with the behavior on PostgreSQL. + // @see \Drupal\mysql\Driver\Database\mysql\Schema::dropField() + if (isset($new_schema['primary key']) && in_array($field, $new_schema['primary key'], TRUE)) { + unset($new_schema['primary key']); + } + + // Handle possible index changes. + foreach ($new_schema['indexes'] as $index => $fields) { + foreach ($fields as $key => $field_name) { + if ($field_name == $field) { + unset($new_schema['indexes'][$index][$key]); + } + } + // If this index has no more fields then remove it. + if (empty($new_schema['indexes'][$index])) { + unset($new_schema['indexes'][$index]); + } + } + $this->alterTable($table, $old_schema, $new_schema); + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function changeField($table, $field, $field_new, $spec, $keys_new = []) { + if (!$this->fieldExists($table, $field)) { + throw new SchemaObjectDoesNotExistException("Cannot change the definition of field '$table.$field': field doesn't exist."); + } + if (($field != $field_new) && $this->fieldExists($table, $field_new)) { + throw new SchemaObjectExistsException("Cannot rename field '$table.$field' to '$field_new': target field already exists."); + } + if (isset($keys_new['primary key']) && in_array($field_new, $keys_new['primary key'], TRUE)) { + $this->ensureNotNullPrimaryKey($keys_new['primary key'], [$field_new => $spec]); + } + + $old_schema = $this->introspectSchema($table); + $new_schema = $old_schema; + + // Map the old field to the new field. + if ($field != $field_new) { + $mapping[$field_new] = $field; + } + else { + $mapping = []; + } + + // Remove the previous definition and swap in the new one. + unset($new_schema['fields'][$field]); + $new_schema['fields'][$field_new] = $spec; + + // Map the former indexes to the new column name. + $new_schema['primary key'] = $this->mapKeyDefinition($new_schema['primary key'], $mapping); + foreach (['unique keys', 'indexes'] as $k) { + foreach ($new_schema[$k] as &$key_definition) { + $key_definition = $this->mapKeyDefinition($key_definition, $mapping); + } + } + + // Add in the keys from $keys_new. + if (isset($keys_new['primary key'])) { + $new_schema['primary key'] = $keys_new['primary key']; + } + foreach (['unique keys', 'indexes'] as $k) { + if (!empty($keys_new[$k])) { + $new_schema[$k] = $keys_new[$k] + $new_schema[$k]; + } + } + + $this->alterTable($table, $old_schema, $new_schema, $mapping); + } + + /** + * Utility method: rename columns in an index definition according to a new mapping. + * + * @param $key_definition + * The key definition. + * @param $mapping + * The new mapping. + */ + protected function mapKeyDefinition(array $key_definition, array $mapping) { + foreach ($key_definition as &$field) { + // The key definition can be an array($field, $length). + if (is_array($field)) { + $field = &$field[0]; + } + + $mapped_field = array_search($field, $mapping, TRUE); + if ($mapped_field !== FALSE) { + $field = $mapped_field; + } + } + return $key_definition; + } + + /** + * {@inheritdoc} + */ + public function addIndex($table, $name, $fields, array $spec) { + if (!$this->tableExists($table)) { + throw new SchemaObjectDoesNotExistException("Cannot add index '$name' to table '$table': table doesn't exist."); + } + if ($this->indexExists($table, $name)) { + throw new SchemaObjectExistsException("Cannot add index '$name' to table '$table': index already exists."); + } + + $schema['indexes'][$name] = $fields; + $statements = $this->createIndexSql($table, $schema); + foreach ($statements as $statement) { + $this->connection->query($statement); + } + } + + /** + * {@inheritdoc} + */ + public function indexExists($table, $name) { + $info = $this->getPrefixInfo($table); + + return $this->connection->query('PRAGMA ' . $info['schema'] . '.index_info(' . $info['table'] . '_' . $name . ')')->fetchField() != ''; + } + + /** + * {@inheritdoc} + */ + public function dropIndex($table, $name) { + if (!$this->indexExists($table, $name)) { + return FALSE; + } + + $info = $this->getPrefixInfo($table); + + $this->connection->query('DROP INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $name); + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function addUniqueKey($table, $name, $fields) { + if (!$this->tableExists($table)) { + throw new SchemaObjectDoesNotExistException("Cannot add unique key '$name' to table '$table': table doesn't exist."); + } + if ($this->indexExists($table, $name)) { + throw new SchemaObjectExistsException("Cannot add unique key '$name' to table '$table': unique key already exists."); + } + + $schema['unique keys'][$name] = $fields; + $statements = $this->createIndexSql($table, $schema); + foreach ($statements as $statement) { + $this->connection->query($statement); + } + } + + /** + * {@inheritdoc} + */ + public function dropUniqueKey($table, $name) { + if (!$this->indexExists($table, $name)) { + return FALSE; + } + + $info = $this->getPrefixInfo($table); + + $this->connection->query('DROP INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $name); + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function addPrimaryKey($table, $fields) { + if (!$this->tableExists($table)) { + throw new SchemaObjectDoesNotExistException("Cannot add primary key to table '$table': table doesn't exist."); + } + + $old_schema = $this->introspectSchema($table); + $new_schema = $old_schema; + + if (!empty($new_schema['primary key'])) { + throw new SchemaObjectExistsException("Cannot add primary key to table '$table': primary key already exists."); + } + + $new_schema['primary key'] = $fields; + $this->ensureNotNullPrimaryKey($new_schema['primary key'], $new_schema['fields']); + $this->alterTable($table, $old_schema, $new_schema); + } + + /** + * {@inheritdoc} + */ + public function dropPrimaryKey($table) { + $old_schema = $this->introspectSchema($table); + $new_schema = $old_schema; + + if (empty($new_schema['primary key'])) { + return FALSE; + } + + unset($new_schema['primary key']); + $this->alterTable($table, $old_schema, $new_schema); + return TRUE; + } + + /** + * {@inheritdoc} + */ + protected function findPrimaryKeyColumns($table) { + if (!$this->tableExists($table)) { + return FALSE; + } + $schema = $this->introspectSchema($table); + return $schema['primary key']; + } + + /** + * {@inheritdoc} + */ + protected function introspectIndexSchema($table) { + if (!$this->tableExists($table)) { + throw new SchemaObjectDoesNotExistException("The table $table doesn't exist."); + } + $schema = $this->introspectSchema($table); + unset($schema['fields']); + return $schema; + } + + /** + * {@inheritdoc} + */ + public function findTables($table_expression) { + $tables = []; + + // The SQLite implementation doesn't need to use the same filtering strategy + // as the parent one because individually prefixed tables live in their own + // schema (database), which means that neither the main database nor any + // attached one will contain a prefixed table name, so we just need to loop + // over all known schemas and filter by the user-supplied table expression. + $attached_dbs = $this->connection->getAttachedDatabases(); + foreach ($attached_dbs as $schema) { + // Can't use query placeholders for the schema because the query would + // have to be :prefixsqlite_master, which does not work. We also need to + // ignore the internal SQLite tables. + $result = $this->connection->query("SELECT name FROM " . $schema . ".sqlite_master WHERE type = :type AND name LIKE :table_name AND name NOT LIKE :pattern", [ + ':type' => 'table', + ':table_name' => $table_expression, + ':pattern' => 'sqlite_%', + ]); + $tables += $result->fetchAllKeyed(0, 0); + } + + return $tables; + } + +} diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Select.php b/core/modules/sqlite/src/Driver/Database/sqlite/Select.php new file mode 100644 index 000000000000..5ee521af8b22 --- /dev/null +++ b/core/modules/sqlite/src/Driver/Database/sqlite/Select.php @@ -0,0 +1,17 @@ +<?php + +namespace Drupal\sqlite\Driver\Database\sqlite; + +use Drupal\Core\Database\Query\Select as QuerySelect; + +/** + * SQLite implementation of \Drupal\Core\Database\Query\Select. + */ +class Select extends QuerySelect { + + 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/Statement.php b/core/modules/sqlite/src/Driver/Database/sqlite/Statement.php new file mode 100644 index 000000000000..5de75a5decf8 --- /dev/null +++ b/core/modules/sqlite/src/Driver/Database/sqlite/Statement.php @@ -0,0 +1,151 @@ +<?php + +namespace Drupal\sqlite\Driver\Database\sqlite; + +use Drupal\Core\Database\StatementPrefetch; +use Drupal\Core\Database\StatementInterface; + +/** + * SQLite implementation of \Drupal\Core\Database\Statement. + * + * The PDO SQLite driver only closes SELECT statements when the PDOStatement + * destructor is called and SQLite does not allow data change (INSERT, + * UPDATE etc) on a table which has open SELECT statements. This is a + * user-space mock of PDOStatement that buffers all the data and doesn't + * have those limitations. + */ +class Statement extends StatementPrefetch implements StatementInterface { + + /** + * {@inheritdoc} + * + * The PDO SQLite layer doesn't replace numeric placeholders in queries + * correctly, and this makes numeric expressions (such as COUNT(*) >= :count) + * fail. We replace numeric placeholders in the query ourselves to work + * around this bug. + * + * See http://bugs.php.net/bug.php?id=45259 for more details. + */ + protected function getStatement($query, &$args = []) { + if (is_array($args) && !empty($args)) { + // Check if $args is a simple numeric array. + if (range(0, count($args) - 1) === array_keys($args)) { + // In that case, we have unnamed placeholders. + $count = 0; + $new_args = []; + foreach ($args as $value) { + if (is_float($value) || is_int($value)) { + if (is_float($value)) { + // Force the conversion to float so as not to loose precision + // in the automatic cast. + $value = sprintf('%F', $value); + } + $query = substr_replace($query, $value, strpos($query, '?'), 1); + } + else { + $placeholder = ':db_statement_placeholder_' . $count++; + $query = substr_replace($query, $placeholder, strpos($query, '?'), 1); + $new_args[$placeholder] = $value; + } + } + $args = $new_args; + } + else { + // Else, this is using named placeholders. + foreach ($args as $placeholder => $value) { + if (is_float($value) || is_int($value)) { + if (is_float($value)) { + // Force the conversion to float so as not to loose precision + // in the automatic cast. + $value = sprintf('%F', $value); + } + + // We will remove this placeholder from the query as PDO throws an + // exception if the number of placeholders in the query and the + // arguments does not match. + unset($args[$placeholder]); + // PDO allows placeholders to not be prefixed by a colon. See + // http://marc.info/?l=php-internals&m=111234321827149&w=2 for + // more. + if ($placeholder[0] != ':') { + $placeholder = ":$placeholder"; + } + // When replacing the placeholders, make sure we search for the + // exact placeholder. For example, if searching for + // ':db_placeholder_1', do not replace ':db_placeholder_11'. + $query = preg_replace('/' . preg_quote($placeholder) . '\b/', $value, $query); + } + } + } + } + + return $this->pdoConnection->prepare($query); + } + + /** + * {@inheritdoc} + */ + public function execute($args = [], $options = []) { + try { + $return = parent::execute($args, $options); + } + catch (\PDOException $e) { + // The database schema might be changed by another process in between the + // time that the statement was prepared and the time the statement was run + // (e.g. usually happens when running tests). In this case, we need to + // re-run the query. + // @see http://www.sqlite.org/faq.html#q15 + // @see http://www.sqlite.org/rescode.html#schema + if (!empty($e->errorInfo[1]) && $e->errorInfo[1] === 17) { + // The schema has changed. SQLite specifies that we must resend the query. + $return = parent::execute($args, $options); + } + else { + // Rethrow the exception. + throw $e; + } + } + + // In some weird cases, SQLite will prefix some column names by the name + // of the table. We post-process the data, by renaming the column names + // using the same convention as MySQL and PostgreSQL. + $rename_columns = []; + foreach ($this->columnNames as $k => $column) { + // In some SQLite versions, SELECT DISTINCT(field) will return "(field)" + // instead of "field". + if (preg_match("/^\((.*)\)$/", $column, $matches)) { + $rename_columns[$column] = $matches[1]; + $this->columnNames[$k] = $matches[1]; + $column = $matches[1]; + } + + // Remove "table." prefixes. + if (preg_match("/^.*\.(.*)$/", $column, $matches)) { + $rename_columns[$column] = $matches[1]; + $this->columnNames[$k] = $matches[1]; + } + } + if ($rename_columns) { + // DatabaseStatementPrefetch already extracted the first row, + // put it back into the result set. + if (isset($this->currentRow)) { + $this->data[0] = &$this->currentRow; + } + + // Then rename all the columns across the result set. + foreach ($this->data as $k => $row) { + foreach ($rename_columns as $old_column => $new_column) { + $this->data[$k][$new_column] = $this->data[$k][$old_column]; + unset($this->data[$k][$old_column]); + } + } + + // Finally, extract the first row again. + $this->currentRow = $this->data[0]; + unset($this->data[0]); + } + + return $return; + } + +} diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Truncate.php b/core/modules/sqlite/src/Driver/Database/sqlite/Truncate.php new file mode 100644 index 000000000000..f1535fb0196d --- /dev/null +++ b/core/modules/sqlite/src/Driver/Database/sqlite/Truncate.php @@ -0,0 +1,22 @@ +<?php + +namespace Drupal\sqlite\Driver\Database\sqlite; + +use Drupal\Core\Database\Query\Truncate as QueryTruncate; + +/** + * SQLite implementation of \Drupal\Core\Database\Query\Truncate. + * + * SQLite doesn't support TRUNCATE, but a DELETE query with no condition has + * exactly the effect (it is implemented by DROPing the table). + */ +class Truncate extends QueryTruncate { + + public function __toString() { + // Create a sanitized comment string to prepend to the query. + $comments = $this->connection->makeComment($this->comments); + + return $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '} '; + } + +} diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Upsert.php b/core/modules/sqlite/src/Driver/Database/sqlite/Upsert.php new file mode 100644 index 000000000000..599742725310 --- /dev/null +++ b/core/modules/sqlite/src/Driver/Database/sqlite/Upsert.php @@ -0,0 +1,47 @@ +<?php + +namespace Drupal\sqlite\Driver\Database\sqlite; + +use Drupal\Core\Database\Query\Upsert as QueryUpsert; + +/** + * SQLite implementation of \Drupal\Core\Database\Query\Upsert. + * + * @see https://www.sqlite.org/lang_UPSERT.html + */ +class Upsert extends QueryUpsert { + + /** + * {@inheritdoc} + */ + public function __toString() { + // Create a sanitized comment string to prepend to the query. + $comments = $this->connection->makeComment($this->comments); + + // Default fields are always placed first for consistency. + $insert_fields = array_merge($this->defaultFields, $this->insertFields); + $insert_fields = array_map(function ($field) { + return $this->connection->escapeField($field); + }, $insert_fields); + + $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES '; + + $values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields); + $query .= implode(', ', $values); + + // Updating the unique / primary key is not necessary. + unset($insert_fields[$this->key]); + + $update = []; + foreach ($insert_fields as $field) { + // The "excluded." prefix causes the field to refer to the value for field + // that would have been inserted had there been no conflict. + $update[] = "$field = EXCLUDED.$field"; + } + + $query .= ' ON CONFLICT (' . $this->connection->escapeField($this->key) . ') DO UPDATE SET ' . implode(', ', $update); + + return $query; + } + +} diff --git a/core/modules/system/system.install b/core/modules/system/system.install index f07133bcd427..3a8a5b2dcb62 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -1167,12 +1167,18 @@ function system_requirements($phase) { if ($provider !== 'core' && !\Drupal::moduleHandler()->moduleExists($provider)) { $autoload = $connection->getConnectionOptions()['autoload'] ?? ''; if (($pos = strpos($autoload, 'src/Driver/Database/')) !== FALSE) { - $requirements['database_driver_provided_by_module'] = [ - 'title' => t('Database driver provided by module'), - 'value' => t('Not enabled'), - 'description' => t('The current database driver is provided by the module: %module. The module is currently not enabled. You should immediately <a href=":enable">enable</a> the module.', ['%module' => $provider, ':enable' => Url::fromRoute('system.modules_list')->toString()]), - 'severity' => REQUIREMENT_ERROR, - ]; + $post_update_registry = \Drupal::service('update.post_update_registry'); + $pending_updates = $post_update_registry->getPendingUpdateInformation(); + if (!in_array('enable_provider_database_driver', array_keys($pending_updates['system']['pending'] ?? []), TRUE)) { + // Only show the warning when the post update function has run and + // the module that is providing the database driver is not enabled. + $requirements['database_driver_provided_by_module'] = [ + 'title' => t('Database driver provided by module'), + 'value' => t('Not enabled'), + 'description' => t('The current database driver is provided by the module: %module. The module is currently not enabled. You should immediately <a href=":enable">enable</a> the module.', ['%module' => $provider, ':enable' => Url::fromRoute('system.modules_list')->toString()]), + 'severity' => REQUIREMENT_ERROR, + ]; + } } } } diff --git a/core/modules/system/system.post_update.php b/core/modules/system/system.post_update.php index 7ddcda6476b2..a7230ecd2939 100644 --- a/core/modules/system/system.post_update.php +++ b/core/modules/system/system.post_update.php @@ -7,6 +7,7 @@ use Drupal\Core\Site\Settings; use Drupal\Core\Config\Entity\ConfigEntityUpdater; +use Drupal\Core\Database\Database; use Drupal\Core\Entity\Display\EntityDisplayInterface; use Drupal\Core\Entity\Display\EntityViewDisplayInterface; use Drupal\Core\Entity\ContentEntityType; @@ -233,3 +234,29 @@ function system_post_update_sort_all_config(&$sandbox) { $sandbox['#finished'] = 1; } } + +/** + * Enable the modules that are providing the listed database drivers. + */ +function system_post_update_enable_provider_database_driver() { + $modules_to_install = []; + foreach (Database::getAllConnectionInfo() as $targets) { + foreach ($targets as $target) { + // Provider determination taken from Connection::getProvider(). + [$first, $second] = explode('\\', $target['namespace'] ?? '', 3); + $provider = ($first === 'Drupal' && strtolower($second) === $second) ? $second : 'core'; + if ($provider !== 'core' && !\Drupal::moduleHandler()->moduleExists($provider)) { + $autoload = $target['autoload'] ?? ''; + // We are only enabling the module for database drivers that are + // provided by a module. + if (str_contains($autoload, 'src/Driver/Database/')) { + $modules_to_install[$provider] = TRUE; + } + } + } + } + + if ($modules_to_install !== []) { + \Drupal::service('module_installer')->install(array_keys($modules_to_install)); + } +} diff --git a/core/modules/system/tests/modules/database_statement_monitoring_test/src/mysql/Connection.php b/core/modules/system/tests/modules/database_statement_monitoring_test/src/mysql/Connection.php index dc5dab25ac2f..caa96940090d 100644 --- a/core/modules/system/tests/modules/database_statement_monitoring_test/src/mysql/Connection.php +++ b/core/modules/system/tests/modules/database_statement_monitoring_test/src/mysql/Connection.php @@ -2,7 +2,7 @@ namespace Drupal\database_statement_monitoring_test\mysql; -use Drupal\Core\Database\Driver\mysql\Connection as BaseConnection; +use Drupal\mysql\Driver\Database\mysql\Connection as BaseConnection; use Drupal\database_statement_monitoring_test\LoggedStatementsTrait; /** diff --git a/core/modules/system/tests/modules/database_statement_monitoring_test/src/mysql/Install/Tasks.php b/core/modules/system/tests/modules/database_statement_monitoring_test/src/mysql/Install/Tasks.php index 443072d47453..338e136cd5c0 100644 --- a/core/modules/system/tests/modules/database_statement_monitoring_test/src/mysql/Install/Tasks.php +++ b/core/modules/system/tests/modules/database_statement_monitoring_test/src/mysql/Install/Tasks.php @@ -2,7 +2,7 @@ namespace Drupal\database_statement_monitoring_test\mysql\Install; -use Drupal\Core\Database\Driver\mysql\Install\Tasks as BaseTasks; +use Drupal\mysql\Driver\Database\mysql\Install\Tasks as BaseTasks; class Tasks extends BaseTasks { } diff --git a/core/modules/system/tests/modules/database_statement_monitoring_test/src/pgsql/Connection.php b/core/modules/system/tests/modules/database_statement_monitoring_test/src/pgsql/Connection.php index 86f004e916cc..43995d325a84 100644 --- a/core/modules/system/tests/modules/database_statement_monitoring_test/src/pgsql/Connection.php +++ b/core/modules/system/tests/modules/database_statement_monitoring_test/src/pgsql/Connection.php @@ -2,7 +2,7 @@ namespace Drupal\database_statement_monitoring_test\pgsql; -use Drupal\Core\Database\Driver\pgsql\Connection as BaseConnection; +use Drupal\pgsql\Driver\Database\pgsql\Connection as BaseConnection; use Drupal\database_statement_monitoring_test\LoggedStatementsTrait; /** diff --git a/core/modules/system/tests/modules/database_statement_monitoring_test/src/pgsql/Install/Tasks.php b/core/modules/system/tests/modules/database_statement_monitoring_test/src/pgsql/Install/Tasks.php index c51bb2541a9b..0b95ddf53d4f 100644 --- a/core/modules/system/tests/modules/database_statement_monitoring_test/src/pgsql/Install/Tasks.php +++ b/core/modules/system/tests/modules/database_statement_monitoring_test/src/pgsql/Install/Tasks.php @@ -2,7 +2,7 @@ namespace Drupal\database_statement_monitoring_test\pgsql\Install; -use Drupal\Core\Database\Driver\pgsql\Install\Tasks as BaseTasks; +use Drupal\pgsql\Driver\Database\pgsql\Install\Tasks as BaseTasks; class Tasks extends BaseTasks { } diff --git a/core/modules/system/tests/modules/database_statement_monitoring_test/src/sqlite/Connection.php b/core/modules/system/tests/modules/database_statement_monitoring_test/src/sqlite/Connection.php index fea32d6798ab..2b4201a9763a 100644 --- a/core/modules/system/tests/modules/database_statement_monitoring_test/src/sqlite/Connection.php +++ b/core/modules/system/tests/modules/database_statement_monitoring_test/src/sqlite/Connection.php @@ -2,7 +2,7 @@ namespace Drupal\database_statement_monitoring_test\sqlite; -use Drupal\Core\Database\Driver\sqlite\Connection as BaseConnection; +use Drupal\sqlite\Driver\Database\sqlite\Connection as BaseConnection; use Drupal\database_statement_monitoring_test\LoggedStatementsTrait; /** diff --git a/core/modules/system/tests/modules/database_statement_monitoring_test/src/sqlite/Install/Tasks.php b/core/modules/system/tests/modules/database_statement_monitoring_test/src/sqlite/Install/Tasks.php index 41d5962fefa3..4827f8c18b92 100644 --- a/core/modules/system/tests/modules/database_statement_monitoring_test/src/sqlite/Install/Tasks.php +++ b/core/modules/system/tests/modules/database_statement_monitoring_test/src/sqlite/Install/Tasks.php @@ -2,7 +2,7 @@ namespace Drupal\database_statement_monitoring_test\sqlite\Install; -use Drupal\Core\Database\Driver\sqlite\Install\Tasks as BaseTasks; +use Drupal\sqlite\Driver\Database\sqlite\Install\Tasks as BaseTasks; class Tasks extends BaseTasks { } diff --git a/core/modules/system/tests/modules/database_test/database_test.install b/core/modules/system/tests/modules/database_test/database_test.install index 47ed774a590a..855a518031a8 100644 --- a/core/modules/system/tests/modules/database_test/database_test.install +++ b/core/modules/system/tests/modules/database_test/database_test.install @@ -321,7 +321,7 @@ function database_test_schema() { 'id' => [ 'description' => 'Simple unique ID.', // Using a serial as an ID properly tests - // \Drupal\Core\Database\Driver\pgsql\Upsert. + // \Drupal\pgsql\Driver\Database\pgsql\Upsert. 'type' => 'serial', 'not null' => TRUE, ], diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Connection.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Connection.php index 9138084acf18..a87a0d386951 100644 --- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Connection.php +++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Connection.php @@ -2,7 +2,9 @@ namespace Drupal\driver_test\Driver\Database\DrivertestMysql; -use Drupal\Core\Database\Driver\mysql\Connection as CoreConnection; +include_once dirname(__DIR__, 8) . '/mysql/src/Driver/Database/mysql/Connection.php'; + +use Drupal\mysql\Driver\Database\mysql\Connection as CoreConnection; /** * MySQL test implementation of \Drupal\Core\Database\Connection. diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Insert.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Insert.php index a9545d77c0f5..8c15b608fe46 100644 --- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Insert.php +++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Insert.php @@ -2,7 +2,9 @@ namespace Drupal\driver_test\Driver\Database\DrivertestMysql; -use Drupal\Core\Database\Driver\mysql\Insert as CoreInsert; +include_once dirname(__DIR__, 8) . '/mysql/src/Driver/Database/mysql/Insert.php'; + +use Drupal\mysql\Driver\Database\mysql\Insert as CoreInsert; /** * MySQL test implementation of \Drupal\Core\Database\Query\Insert. diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Install/Tasks.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Install/Tasks.php index 5c10c2c6be1c..2b20ba147f96 100644 --- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Install/Tasks.php +++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Install/Tasks.php @@ -2,7 +2,9 @@ namespace Drupal\driver_test\Driver\Database\DrivertestMysql\Install; -use Drupal\Core\Database\Driver\mysql\Install\Tasks as CoreTasks; +include_once dirname(__DIR__, 9) . '/mysql/src/Driver/Database/mysql/Install/Tasks.php'; + +use Drupal\mysql\Driver\Database\mysql\Install\Tasks as CoreTasks; /** * Specifies installation tasks for MySQL test databases. diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Schema.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Schema.php index 69277a13914f..b3a2dc01fef5 100644 --- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Schema.php +++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Schema.php @@ -2,7 +2,9 @@ namespace Drupal\driver_test\Driver\Database\DrivertestMysql; -use Drupal\Core\Database\Driver\mysql\Schema as CoreSchema; +include_once dirname(__DIR__, 8) . '/mysql/src/Driver/Database/mysql/Schema.php'; + +use Drupal\mysql\Driver\Database\mysql\Schema as CoreSchema; /** * MySQL test implementation of \Drupal\Core\Database\Schema. diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Upsert.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Upsert.php index 8b4bb482588e..dd2d71af8c77 100644 --- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Upsert.php +++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Upsert.php @@ -2,7 +2,9 @@ namespace Drupal\driver_test\Driver\Database\DrivertestMysql; -use Drupal\Core\Database\Driver\mysql\Upsert as CoreUpsert; +include_once dirname(__DIR__, 8) . '/mysql/src/Driver/Database/mysql/Upsert.php'; + +use Drupal\mysql\Driver\Database\mysql\Upsert as CoreUpsert; /** * MySQL test implementation of \Drupal\Core\Database\Query\Upsert. diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Connection.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Connection.php index c7ec0fed72a8..6ef463cb1577 100644 --- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Connection.php +++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Connection.php @@ -2,7 +2,9 @@ namespace Drupal\driver_test\Driver\Database\DrivertestMysqlDeprecatedVersion; -use Drupal\Core\Database\Driver\mysql\Connection as CoreConnection; +include_once dirname(__DIR__, 8) . '/mysql/src/Driver/Database/mysql/Connection.php'; + +use Drupal\mysql\Driver\Database\mysql\Connection as CoreConnection; /** * MySQL test implementation of \Drupal\Core\Database\Connection. diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Insert.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Insert.php index 86affc1a349c..f1a54e3cacc4 100644 --- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Insert.php +++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Insert.php @@ -2,7 +2,9 @@ namespace Drupal\driver_test\Driver\Database\DrivertestMysqlDeprecatedVersion; -use Drupal\Core\Database\Driver\mysql\Insert as CoreInsert; +include_once dirname(__DIR__, 8) . '/mysql/src/Driver/Database/mysql/Insert.php'; + +use Drupal\mysql\Driver\Database\mysql\Insert as CoreInsert; /** * MySQL test implementation of \Drupal\Core\Database\Query\Insert. diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Install/Tasks.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Install/Tasks.php index c768de5721ef..647268c7b81b 100644 --- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Install/Tasks.php +++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Install/Tasks.php @@ -2,7 +2,9 @@ namespace Drupal\driver_test\Driver\Database\DrivertestMysqlDeprecatedVersion\Install; -use Drupal\Core\Database\Driver\mysql\Install\Tasks as CoreTasks; +include_once dirname(__DIR__, 9) . '/mysql/src/Driver/Database/mysql/Install/Tasks.php'; + +use Drupal\mysql\Driver\Database\mysql\Install\Tasks as CoreTasks; /** * Specifies installation tasks for MySQL test databases. diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Schema.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Schema.php index fef11ed4de0b..4e739215848e 100644 --- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Schema.php +++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Schema.php @@ -2,7 +2,9 @@ namespace Drupal\driver_test\Driver\Database\DrivertestMysqlDeprecatedVersion; -use Drupal\Core\Database\Driver\mysql\Schema as CoreSchema; +include_once dirname(__DIR__, 8) . '/mysql/src/Driver/Database/mysql/Schema.php'; + +use Drupal\mysql\Driver\Database\mysql\Schema as CoreSchema; /** * MySQL test implementation of \Drupal\Core\Database\Schema. diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Upsert.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Upsert.php index 78ee82d34db4..2513c7357b40 100644 --- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Upsert.php +++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Upsert.php @@ -2,7 +2,9 @@ namespace Drupal\driver_test\Driver\Database\DrivertestMysqlDeprecatedVersion; -use Drupal\Core\Database\Driver\mysql\Upsert as CoreUpsert; +include_once dirname(__DIR__, 8) . '/mysql/src/Driver/Database/mysql/Upsert.php'; + +use Drupal\mysql\Driver\Database\mysql\Upsert as CoreUpsert; /** * MySQL test implementation of \Drupal\Core\Database\Query\Upsert. diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Connection.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Connection.php index 87dcf7dd4a67..e45d48ade944 100644 --- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Connection.php +++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Connection.php @@ -2,7 +2,9 @@ namespace Drupal\driver_test\Driver\Database\DrivertestPgsql; -use Drupal\Core\Database\Driver\pgsql\Connection as CoreConnection; +include_once dirname(__DIR__, 8) . '/pgsql/src/Driver/Database/pgsql/Connection.php'; + +use Drupal\pgsql\Driver\Database\pgsql\Connection as CoreConnection; /** * PostgreSQL implementation of \Drupal\Core\Database\Connection. diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Delete.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Delete.php index 5340c8afb026..92081533cbaa 100644 --- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Delete.php +++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Delete.php @@ -2,7 +2,9 @@ namespace Drupal\driver_test\Driver\Database\DrivertestPgsql; -use Drupal\Core\Database\Driver\pgsql\Delete as CoreDelete; +include_once dirname(__DIR__, 8) . '/pgsql/src/Driver/Database/pgsql/Delete.php'; + +use Drupal\pgsql\Driver\Database\pgsql\Delete as CoreDelete; /** * PostgreSQL implementation of \Drupal\Core\Database\Query\Delete. diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Insert.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Insert.php index a2f6f0791afc..957373ffc38f 100644 --- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Insert.php +++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Insert.php @@ -2,7 +2,9 @@ namespace Drupal\driver_test\Driver\Database\DrivertestPgsql; -use Drupal\Core\Database\Driver\pgsql\Insert as CoreInsert; +include_once dirname(__DIR__, 8) . '/pgsql/src/Driver/Database/pgsql/Insert.php'; + +use Drupal\pgsql\Driver\Database\pgsql\Insert as CoreInsert; /** * PostgreSQL implementation of \Drupal\Core\Database\Query\Insert. diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Install/Tasks.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Install/Tasks.php index 55705d832a01..a8c096438f23 100644 --- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Install/Tasks.php +++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Install/Tasks.php @@ -2,7 +2,9 @@ namespace Drupal\driver_test\Driver\Database\DrivertestPgsql\Install; -use Drupal\Core\Database\Driver\pgsql\Install\Tasks as CoreTasks; +include_once dirname(__DIR__, 9) . '/pgsql/src/Driver/Database/pgsql/Install/Tasks.php'; + +use Drupal\pgsql\Driver\Database\pgsql\Install\Tasks as CoreTasks; /** * Specifies installation tasks for PostgreSQL databases. diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Schema.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Schema.php index 8cfa96912699..df0c45c22200 100644 --- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Schema.php +++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Schema.php @@ -2,7 +2,9 @@ namespace Drupal\driver_test\Driver\Database\DrivertestPgsql; -use Drupal\Core\Database\Driver\pgsql\Schema as CoreSchema; +include_once dirname(__DIR__, 8) . '/pgsql/src/Driver/Database/pgsql/Schema.php'; + +use Drupal\pgsql\Driver\Database\pgsql\Schema as CoreSchema; /** * PostgreSQL implementation of \Drupal\Core\Database\Schema. diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Select.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Select.php index f0faf65ca65b..b11fbefe1148 100644 --- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Select.php +++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Select.php @@ -2,7 +2,9 @@ namespace Drupal\driver_test\Driver\Database\DrivertestPgsql; -use Drupal\Core\Database\Driver\pgsql\Select as CoreSelect; +include_once dirname(__DIR__, 8) . '/pgsql/src/Driver/Database/pgsql/Select.php'; + +use Drupal\pgsql\Driver\Database\pgsql\Select as CoreSelect; /** * PostgreSQL implementation of \Drupal\Core\Database\Query\Select. diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Truncate.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Truncate.php index c1b4322d5b79..61b58711ff97 100644 --- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Truncate.php +++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Truncate.php @@ -2,7 +2,9 @@ namespace Drupal\driver_test\Driver\Database\DrivertestPgsql; -use Drupal\Core\Database\Driver\pgsql\Truncate as CoreTruncate; +include_once dirname(__DIR__, 8) . '/pgsql/src/Driver/Database/pgsql/Truncate.php'; + +use Drupal\pgsql\Driver\Database\pgsql\Truncate as CoreTruncate; /** * PostgreSQL implementation of \Drupal\Core\Database\Query\Truncate. diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Update.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Update.php index d5ed1ed5f08f..e30ace4bc8ec 100644 --- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Update.php +++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Update.php @@ -2,7 +2,9 @@ namespace Drupal\driver_test\Driver\Database\DrivertestPgsql; -use Drupal\Core\Database\Driver\pgsql\Update as CoreUpdate; +include_once dirname(__DIR__, 8) . '/pgsql/src/Driver/Database/pgsql/Update.php'; + +use Drupal\pgsql\Driver\Database\pgsql\Update as CoreUpdate; /** * PostgreSQL implementation of \Drupal\Core\Database\Query\Update. diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Upsert.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Upsert.php index 2237a755cf44..b36b039bf9ba 100644 --- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Upsert.php +++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Upsert.php @@ -2,7 +2,9 @@ namespace Drupal\driver_test\Driver\Database\DrivertestPgsql; -use Drupal\Core\Database\Driver\pgsql\Upsert as CoreUpsert; +include_once dirname(__DIR__, 8) . '/pgsql/src/Driver/Database/pgsql/Upsert.php'; + +use Drupal\pgsql\Driver\Database\pgsql\Upsert as CoreUpsert; /** * PostgreSQL implementation of \Drupal\Core\Database\Query\Upsert. diff --git a/core/modules/system/tests/src/Functional/Update/UpdateEnableProviderDatabaseDriverTest.php b/core/modules/system/tests/src/Functional/Update/UpdateEnableProviderDatabaseDriverTest.php new file mode 100644 index 000000000000..563d5f4072ec --- /dev/null +++ b/core/modules/system/tests/src/Functional/Update/UpdateEnableProviderDatabaseDriverTest.php @@ -0,0 +1,45 @@ +<?php + +namespace Drupal\Tests\system\Functional\Update; + +use Drupal\Core\Database\Database; +use Drupal\FunctionalTests\Update\UpdatePathTestBase; + +/** + * Tests that update hooks are enabling the database driver providing module. + * + * @group Update + */ +class UpdateEnableProviderDatabaseDriverTest extends UpdatePathTestBase { + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + protected function setDatabaseDumpFiles() { + $this->databaseDumpFiles = [ + __DIR__ . '/../../../fixtures/update/drupal-8.8.0.bare.standard.php.gz', + ]; + } + + /** + * Tests that post update hooks are properly run. + */ + public function testPostUpdateEnableProviderDatabaseDriverHook() { + $connection = Database::getConnection(); + $provider = $connection->getProvider(); + + $this->assertFalse(\Drupal::moduleHandler()->moduleExists($provider)); + + // Running the updates enables the module that is providing the database + // driver. + $this->runUpdates(); + + $this->assertTrue(\Drupal::moduleHandler()->moduleExists($provider)); + } + +} diff --git a/core/modules/views/src/Plugin/views/argument/StringArgument.php b/core/modules/views/src/Plugin/views/argument/StringArgument.php index be47b6959256..ef85b80e6386 100644 --- a/core/modules/views/src/Plugin/views/argument/StringArgument.php +++ b/core/modules/views/src/Plugin/views/argument/StringArgument.php @@ -178,7 +178,7 @@ public function getFormula() { if ($this->options['case'] != 'none') { // Support case-insensitive substring comparisons for SQLite by using the // 'NOCASE_UTF8' collation. - // @see Drupal\Core\Database\Driver\sqlite\Connection::open() + // @see Drupal\sqlite\Driver\Database\sqlite\Connection::open() if (Database::getConnection()->databaseType() == 'sqlite') { $formula .= ' COLLATE NOCASE_UTF8'; } diff --git a/core/modules/views/src/Plugin/views/query/SqliteDateSql.php b/core/modules/views/src/Plugin/views/query/SqliteDateSql.php index d6006ac71f97..5ee488689c03 100644 --- a/core/modules/views/src/Plugin/views/query/SqliteDateSql.php +++ b/core/modules/views/src/Plugin/views/query/SqliteDateSql.php @@ -97,7 +97,7 @@ public function getDateFormat($field, $format) { // case the comparison value is a float, integer, or numeric. All of the // above SQLite format tokens only produce integers. However, the given // $format may contain 'Y-m-d', which results in a string. - // @see \Drupal\Core\Database\Driver\sqlite\Connection::expandArguments() + // @see \Drupal\sqlite\Driver\Database\sqlite\Connection::expandArguments() // @see http://www.sqlite.org/lang_datefunc.html // @see http://www.sqlite.org/lang_expr.html#castexpr if (preg_match('/^(?:%\w)+$/', $format)) { diff --git a/core/tests/Drupal/BuildTests/Framework/Tests/HtRouterTest.php b/core/tests/Drupal/BuildTests/Framework/Tests/HtRouterTest.php index c96a0f194744..721766587f36 100644 --- a/core/tests/Drupal/BuildTests/Framework/Tests/HtRouterTest.php +++ b/core/tests/Drupal/BuildTests/Framework/Tests/HtRouterTest.php @@ -3,7 +3,7 @@ namespace Drupal\BuildTests\Framework\Tests; use Drupal\BuildTests\QuickStart\QuickStartTestBase; -use Drupal\Core\Database\Driver\sqlite\Install\Tasks; +use Drupal\sqlite\Driver\Database\sqlite\Install\Tasks; /** * @coversDefaultClass \Drupal\BuildTests\Framework\BuildTestBase diff --git a/core/tests/Drupal/FunctionalTests/ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest.php b/core/tests/Drupal/FunctionalTests/ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest.php new file mode 100644 index 000000000000..855387f695d5 --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest.php @@ -0,0 +1,59 @@ +<?php + +namespace Drupal\FunctionalTests; + +use Drupal\Core\Database\Database; +use Drupal\Tests\BrowserTestBase; + +/** + * @group Database + */ +class ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest extends BrowserTestBase { + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + $driver = Database::getConnection()->driver(); + if (!in_array($driver, ['mysql', 'pgsql', 'sqlite'])) { + $this->markTestSkipped("This test does not support the {$driver} database driver."); + } + + $filename = $this->siteDirectory . '/settings.php'; + chmod($filename, 0777); + $contents = file_get_contents($filename); + + $autoload = "'autoload' => 'core/modules/$driver/src/Driver/Database/$driver/',"; + $contents = str_replace($autoload, '', $contents); + $namespace_search = "'namespace' => 'Drupal\\\\$driver\\\\Driver\\\\Database\\\\$driver',"; + $namespace_replace = "'namespace' => 'Drupal\\\\Core\\\\Database\\\\Driver\\\\$driver',"; + $contents = str_replace($namespace_search, $namespace_replace, $contents); + file_put_contents($filename, $contents); + } + + /** + * Confirms that the site works with Drupal 8 style database connection array. + */ + public function testExistingDrupal8StyleDatabaseConnectionInSettingsPhp() { + $this->drupalLogin($this->drupalCreateUser()); + $this->assertSession()->addressEquals('user/2'); + $this->assertSession()->statusCodeEquals(200); + + // Make sure that we are have tested with the Drupal 8 style database + // connection array. + $filename = $this->siteDirectory . '/settings.php'; + $contents = file_get_contents($filename); + $driver = Database::getConnection()->driver(); + $this->assertStringContainsString("'namespace' => 'Drupal\\\\Core\\\\Database\\\\Driver\\\\$driver',", $contents); + $this->assertStringContainsString("'driver' => '$driver',", $contents); + $this->assertStringNotContainsString("'autoload' => 'core/modules/$driver/src/Driver/Database/$driver/", $contents); + } + +} diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerTest.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerTest.php index 4b3f1c605e01..118a77124577 100644 --- a/core/tests/Drupal/FunctionalTests/Installer/InstallerTest.php +++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerTest.php @@ -2,8 +2,10 @@ namespace Drupal\FunctionalTests\Installer; +use Drupal\Core\Database\Database; use Drupal\Core\Routing\RoutingEvents; use Drupal\Core\Test\PerformanceTestRecorder; +use Drupal\Core\Extension\ModuleUninstallValidatorException; /** * Tests the interactive installer. @@ -118,4 +120,31 @@ protected function visitInstaller() { $this->assertSession()->titleEquals('Choose language | Drupal'); } + /** + * Confirms that the installation succeeded. + */ + public function testInstalled() { + $this->assertSession()->addressEquals('user/1'); + $this->assertSession()->statusCodeEquals(200); + + $database = Database::getConnection(); + $module = $database->getProvider(); + $module_handler = \Drupal::service('module_handler'); + + // Assert that the module that is providing the database driver has been + // installed. + $this->assertTrue($module_handler->moduleExists($module)); + + // The module that is providing the database driver should be uninstallable. + try { + $this->container->get('module_installer')->uninstall([$module]); + $this->fail("Uninstalled $module module."); + } + catch (ModuleUninstallValidatorException $e) { + $module_name = $module_handler->getName($module); + $driver = $database->driver(); + $this->assertStringContainsString("The module '$module_name' is providing the database driver '$driver'.", $e->getMessage()); + } + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Database/MysqlDriverLegacyTest.php b/core/tests/Drupal/KernelTests/Core/Database/MysqlDriverLegacyTest.php new file mode 100644 index 000000000000..549c1986c0a4 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Database/MysqlDriverLegacyTest.php @@ -0,0 +1,88 @@ +<?php + +namespace Drupal\KernelTests\Core\Database; + +use Drupal\Core\Database\Driver\mysql\Connection; +use Drupal\Core\Database\Driver\mysql\ExceptionHandler; +use Drupal\Core\Database\Driver\mysql\Install\Tasks; +use Drupal\Core\Database\Driver\mysql\Insert; +use Drupal\Core\Database\Driver\mysql\Schema; +use Drupal\Core\Database\Driver\mysql\Upsert; +use Drupal\Tests\Core\Database\Stub\StubPDO; + +/** + * Tests the deprecations of the MySQL database driver classes in Core. + * + * @group legacy + * @group Database + */ +class MysqlDriverLegacyTest extends DatabaseTestBase { + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + if ($this->connection->driver() !== 'mysql') { + $this->markTestSkipped('Only test the deprecation message for the MySQL database driver classes in Core.'); + } + } + + /** + * @covers Drupal\Core\Database\Driver\mysql\Install\Tasks + */ + public function testDeprecationInstallTasks() { + $this->expectDeprecation('\Drupal\Core\Database\Driver\mysql\Install\Tasks is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492'); + $tasks = new Tasks(); + $this->assertInstanceOf(Tasks::class, $tasks); + } + + /** + * @covers Drupal\Core\Database\Driver\mysql\Connection + */ + public function testDeprecationConnection() { + $this->expectDeprecation('\Drupal\Core\Database\Driver\mysql\Connection is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492'); + // @todo https://www.drupal.org/project/drupal/issues/3251084 Remove setting + // the $options parameter. + $options['init_commands']['sql_mode'] = ''; + $connection = new Connection($this->createMock(StubPDO::class), $options); + $this->assertInstanceOf(Connection::class, $connection); + } + + /** + * @covers Drupal\Core\Database\Driver\mysql\ExceptionHandler + */ + public function testDeprecationExceptionHandler() { + $this->expectDeprecation('\Drupal\Core\Database\Driver\mysql\ExceptionHandler is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492'); + $handler = new ExceptionHandler(); + $this->assertInstanceOf(ExceptionHandler::class, $handler); + } + + /** + * @covers Drupal\Core\Database\Driver\mysql\Insert + */ + public function testDeprecationInsert() { + $this->expectDeprecation('\Drupal\Core\Database\Driver\mysql\Insert is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492'); + $insert = new Insert($this->connection, 'test'); + $this->assertInstanceOf(Insert::class, $insert); + } + + /** + * @covers Drupal\Core\Database\Driver\mysql\Schema + */ + public function testDeprecationSchema() { + $this->expectDeprecation('\Drupal\Core\Database\Driver\mysql\Schema is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492'); + $schema = new Schema($this->connection); + $this->assertInstanceOf(Schema::class, $schema); + } + + /** + * @covers Drupal\Core\Database\Driver\mysql\Upsert + */ + public function testDeprecationUpsert() { + $this->expectDeprecation('\Drupal\Core\Database\Driver\mysql\Upsert is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492'); + $upsert = new Upsert($this->connection, 'test'); + $this->assertInstanceOf(Upsert::class, $upsert); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Database/NextIdTest.php b/core/tests/Drupal/KernelTests/Core/Database/NextIdTest.php index 3655be917bee..3dfe5866bad8 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/NextIdTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/NextIdTest.php @@ -43,7 +43,7 @@ public function testDbNextId() { /** * Tests that sequences table clear up works when a connection is closed. * - * @see \Drupal\Core\Database\Driver\mysql\Connection::__destruct() + * @see \Drupal\mysql\Driver\Database\mysql\Connection::__destruct() */ public function testDbNextIdClosedConnection() { // Only run this test for the 'mysql' driver. @@ -67,7 +67,7 @@ public function testDbNextIdClosedConnection() { // Close the connection. Database::closeConnection('next_id'); - // Test that \Drupal\Core\Database\Driver\mysql\Connection::__destruct() + // Test that \Drupal\mysql\Driver\Database\mysql\Connection::__destruct() // successfully trims the sequences table if the connection is closed. $count = $this->connection->select('sequences')->countQuery()->execute()->fetchField(); $this->assertEquals(1, $count); diff --git a/core/tests/Drupal/KernelTests/Core/Database/PgsqlDriverLegacyTest.php b/core/tests/Drupal/KernelTests/Core/Database/PgsqlDriverLegacyTest.php new file mode 100644 index 000000000000..273e1f960709 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Database/PgsqlDriverLegacyTest.php @@ -0,0 +1,115 @@ +<?php + +namespace Drupal\KernelTests\Core\Database; + +use Drupal\Core\Database\Driver\pgsql\Connection; +use Drupal\Core\Database\Driver\pgsql\Delete; +use Drupal\Core\Database\Driver\pgsql\Install\Tasks; +use Drupal\Core\Database\Driver\pgsql\Insert; +use Drupal\Core\Database\Driver\pgsql\Schema; +use Drupal\Core\Database\Driver\pgsql\Select; +use Drupal\Core\Database\Driver\pgsql\Truncate; +use Drupal\Core\Database\Driver\pgsql\Update; +use Drupal\Core\Database\Driver\pgsql\Upsert; +use Drupal\Tests\Core\Database\Stub\StubPDO; + +/** + * Tests the deprecations of the PostgreSQL database driver classes in Core. + * + * @group legacy + * @group Database + */ +class PgsqlDriverLegacyTest extends DatabaseTestBase { + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + if ($this->connection->driver() !== 'pgsql') { + $this->markTestSkipped('Only test the deprecation message for the PostgreSQL database driver classes in Core.'); + } + } + + /** + * @covers Drupal\Core\Database\Driver\pgsql\Install\Tasks + */ + public function testDeprecationInstallTasks() { + $this->expectDeprecation('\Drupal\Core\Database\Driver\pgsql\Install\Tasks is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492'); + $tasks = new Tasks(); + $this->assertInstanceOf(Tasks::class, $tasks); + } + + /** + * @covers Drupal\Core\Database\Driver\pgsql\Connection + */ + public function testDeprecationConnection() { + $this->expectDeprecation('\Drupal\Core\Database\Driver\pgsql\Connection is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492'); + $connection = new Connection($this->createMock(StubPDO::class), []); + $this->assertInstanceOf(Connection::class, $connection); + } + + /** + * @covers Drupal\Core\Database\Driver\pgsql\Delete + */ + public function testDeprecationDelete() { + $this->expectDeprecation('\Drupal\Core\Database\Driver\pgsql\Delete is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492'); + $delete = new Delete($this->connection, 'test'); + $this->assertInstanceOf(Delete::class, $delete); + } + + /** + * @covers Drupal\Core\Database\Driver\pgsql\Insert + */ + public function testDeprecationInsert() { + $this->expectDeprecation('\Drupal\Core\Database\Driver\pgsql\Insert is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492'); + $insert = new Insert($this->connection, 'test'); + $this->assertInstanceOf(Insert::class, $insert); + } + + /** + * @covers Drupal\Core\Database\Driver\pgsql\Schema + */ + public function testDeprecationSchema() { + $this->expectDeprecation('\Drupal\Core\Database\Driver\pgsql\Schema is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492'); + $schema = new Schema($this->connection); + $this->assertInstanceOf(Schema::class, $schema); + } + + /** + * @covers Drupal\Core\Database\Driver\pgsql\Select + */ + public function testDeprecationSelect() { + $this->expectDeprecation('\Drupal\Core\Database\Driver\pgsql\Select is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492'); + $select = new Select($this->connection, 'test'); + $this->assertInstanceOf(Select::class, $select); + } + + /** + * @covers Drupal\Core\Database\Driver\pgsql\Truncate + */ + public function testDeprecationTruncate() { + $this->expectDeprecation('\Drupal\Core\Database\Driver\pgsql\Truncate is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492'); + $truncate = new Truncate($this->connection, 'test'); + $this->assertInstanceOf(Truncate::class, $truncate); + } + + /** + * @covers Drupal\Core\Database\Driver\pgsql\Update + */ + public function testDeprecationUpdate() { + $this->expectDeprecation('\Drupal\Core\Database\Driver\pgsql\Update is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492'); + $update = new Update($this->connection, 'test'); + $this->assertInstanceOf(Update::class, $update); + } + + /** + * @covers Drupal\Core\Database\Driver\pgsql\Upsert + */ + public function testDeprecationUpsert() { + $this->expectDeprecation('\Drupal\Core\Database\Driver\pgsql\Upsert is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492'); + $upsert = new Upsert($this->connection, 'test'); + $this->assertInstanceOf(Upsert::class, $upsert); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Database/PrefixInfoTest.php b/core/tests/Drupal/KernelTests/Core/Database/PrefixInfoTest.php index efa0c53ed8db..2afec6de82bf 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/PrefixInfoTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/PrefixInfoTest.php @@ -15,7 +15,7 @@ class PrefixInfoTest extends DatabaseTestBase { * Tests that DatabaseSchema::getPrefixInfo() returns the right database. * * We are testing if the return array of the method - * \Drupal\Core\Database\Driver\mysql\Schema::getPrefixInfo(). This return + * \Drupal\mysql\Driver\Database\mysql\Schema::getPrefixInfo(). This return * array is a keyed array with info about amongst other things the database. * The other two by Drupal core supported databases do not have this variable * set in the return array. diff --git a/core/tests/Drupal/KernelTests/Core/Database/QueryTest.php b/core/tests/Drupal/KernelTests/Core/Database/QueryTest.php index 1172a0f0503c..32f3a78d0b4a 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/QueryTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/QueryTest.php @@ -138,7 +138,7 @@ public function testConditionOperatorArgumentsSQLInjection() { /** * Tests numeric query parameter expansion in expressions. * - * @see \Drupal\Core\Database\Driver\sqlite\Statement::getStatement() + * @see \Drupal\sqlite\Driver\Database\sqlite\Statement::getStatement() * @see http://bugs.php.net/bug.php?id=45259 */ public function testNumericExpressionSubstitution() { diff --git a/core/tests/Drupal/KernelTests/Core/Database/SchemaTest.php b/core/tests/Drupal/KernelTests/Core/Database/SchemaTest.php index bf73ba4609c8..678e0eee91ad 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/SchemaTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/SchemaTest.php @@ -276,9 +276,9 @@ public function testSchema() { } /** - * @covers \Drupal\Core\Database\Driver\mysql\Schema::introspectIndexSchema - * @covers \Drupal\Core\Database\Driver\pgsql\Schema::introspectIndexSchema - * @covers \Drupal\Core\Database\Driver\sqlite\Schema::introspectIndexSchema + * @covers \Drupal\mysql\Driver\Database\mysql\Schema::introspectIndexSchema + * @covers \Drupal\pgsql\Driver\Database\pgsql\Schema::introspectIndexSchema + * @covers \Drupal\sqlite\Driver\Database\sqlite\Schema::introspectIndexSchema */ public function testIntrospectIndexSchema() { $table_specification = [ @@ -355,7 +355,7 @@ public function testIntrospectIndexSchema() { /** * Tests that indexes on string fields are limited to 191 characters on MySQL. * - * @see \Drupal\Core\Database\Driver\mysql\Schema::getNormalizedIndexes() + * @see \Drupal\mysql\Driver\Database\mysql\Schema::getNormalizedIndexes() */ public function testIndexLength() { if ($this->connection->databaseType() !== 'mysql') { diff --git a/core/tests/Drupal/KernelTests/Core/Database/SelectComplexTest.php b/core/tests/Drupal/KernelTests/Core/Database/SelectComplexTest.php index 44ef6ccb4795..281f3d4d57c9 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/SelectComplexTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/SelectComplexTest.php @@ -238,7 +238,7 @@ public function testCountQueryRemovals() { // Check that the ordering clause is handled properly. $orderby = $query->getOrderBy(); // The orderby string is different for PostgreSQL. - // @see Drupal\Core\Database\Driver\pgsql\Select::orderBy() + // @see Drupal\pgsql\Driver\Database\pgsql\Select::orderBy() $db_type = Database::getConnection()->databaseType(); $this->assertEquals($db_type == 'pgsql' ? 'ASC NULLS FIRST' : 'ASC', $orderby['name'], 'Query correctly sets ordering clause.'); $orderby = $count->getOrderBy(); diff --git a/core/tests/Drupal/KernelTests/Core/Database/SqliteDriverLegacyTest.php b/core/tests/Drupal/KernelTests/Core/Database/SqliteDriverLegacyTest.php new file mode 100644 index 000000000000..f3a34c7389fd --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Database/SqliteDriverLegacyTest.php @@ -0,0 +1,105 @@ +<?php + +namespace Drupal\KernelTests\Core\Database; + +use Drupal\Core\Database\Driver\sqlite\Connection; +use Drupal\Core\Database\Driver\sqlite\Install\Tasks; +use Drupal\Core\Database\Driver\sqlite\Insert; +use Drupal\Core\Database\Driver\sqlite\Schema; +use Drupal\Core\Database\Driver\sqlite\Select; +use Drupal\Core\Database\Driver\sqlite\Statement; +use Drupal\Core\Database\Driver\sqlite\Truncate; +use Drupal\Core\Database\Driver\sqlite\Upsert; +use Drupal\Tests\Core\Database\Stub\StubPDO; + +/** + * Tests the deprecations of the SQLite database driver classes in Core. + * + * @group legacy + * @group Database + */ +class SqliteDriverLegacyTest extends DatabaseTestBase { + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + if ($this->connection->driver() !== 'sqlite') { + $this->markTestSkipped('Only test the deprecation message for the SQLite database driver classes in Core.'); + } + } + + /** + * @covers Drupal\Core\Database\Driver\sqlite\Install\Tasks + */ + public function testDeprecationInstallTasks() { + $this->expectDeprecation('\Drupal\Core\Database\Driver\sqlite\Install\Tasks is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492'); + $tasks = new Tasks(); + $this->assertInstanceOf(Tasks::class, $tasks); + } + + /** + * @covers Drupal\Core\Database\Driver\sqlite\Connection + */ + public function testDeprecationConnection() { + $this->expectDeprecation('\Drupal\Core\Database\Driver\sqlite\Connection is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492'); + $connection = new Connection($this->createMock(StubPDO::class), []); + $this->assertInstanceOf(Connection::class, $connection); + } + + /** + * @covers Drupal\Core\Database\Driver\sqlite\Insert + */ + public function testDeprecationInsert() { + $this->expectDeprecation('\Drupal\Core\Database\Driver\sqlite\Insert is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492'); + $insert = new Insert($this->connection, 'test'); + $this->assertInstanceOf(Insert::class, $insert); + } + + /** + * @covers Drupal\Core\Database\Driver\sqlite\Schema + */ + public function testDeprecationSchema() { + $this->expectDeprecation('\Drupal\Core\Database\Driver\sqlite\Schema is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492'); + $schema = new Schema($this->connection); + $this->assertInstanceOf(Schema::class, $schema); + } + + /** + * @covers Drupal\Core\Database\Driver\sqlite\Select + */ + public function testDeprecationSelect() { + $this->expectDeprecation('\Drupal\Core\Database\Driver\sqlite\Select is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492'); + $select = new Select($this->connection, 'test'); + $this->assertInstanceOf(Select::class, $select); + } + + /** + * @covers Drupal\Core\Database\Driver\sqlite\Statement + */ + public function testDeprecationStatement() { + $this->expectDeprecation('\Drupal\Core\Database\Driver\sqlite\Statement is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492'); + $statement = new Statement($this->createMock(StubPDO::class), $this->connection, '', []); + $this->assertInstanceOf(Statement::class, $statement); + } + + /** + * @covers Drupal\Core\Database\Driver\sqlite\Truncate + */ + public function testDeprecationTruncate() { + $this->expectDeprecation('\Drupal\Core\Database\Driver\sqlite\Truncate is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492'); + $truncate = new Truncate($this->connection, 'test'); + $this->assertInstanceOf(Truncate::class, $truncate); + } + + /** + * @covers Drupal\Core\Database\Driver\sqlite\Upsert + */ + public function testDeprecationUpsert() { + $this->expectDeprecation('\Drupal\Core\Database\Driver\sqlite\Upsert is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492'); + $upsert = new Upsert($this->connection, 'test'); + $this->assertInstanceOf(Upsert::class, $upsert); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php b/core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php index 7aa37c70ef8b..8adc5840631e 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php @@ -263,7 +263,7 @@ public function testTransactionWithDdlStatement() { try { // Rollback the outer transaction. $transaction->rollBack(); - // @see \Drupal\Core\Database\Driver\mysql\Connection::rollBack() + // @see \Drupal\mysql\Driver\Database\mysql\Connection::rollBack() $this->fail('Rolling back a transaction containing DDL should produce a warning.'); } catch (Warning $warning) { diff --git a/core/tests/Drupal/KernelTests/KernelTestBaseTest.php b/core/tests/Drupal/KernelTests/KernelTestBaseTest.php index 7ca986ef3509..378576dbcd58 100644 --- a/core/tests/Drupal/KernelTests/KernelTestBaseTest.php +++ b/core/tests/Drupal/KernelTests/KernelTestBaseTest.php @@ -441,4 +441,14 @@ public function testVarDump() { $this->assertStringContainsString('test_role', StreamCapturer::$cache); } + /** + * @covers ::bootEnvironment + */ + public function testDatabaseDriverModuleEnabled() { + $module = Database::getConnection()->getProvider(); + + // Test that the module that is providing the database driver is enabled. + $this->assertSame(1, \Drupal::service('extension.list.module')->get($module)->status); + } + } diff --git a/core/tests/Drupal/Tests/Core/Command/GenerateThemeTest.php b/core/tests/Drupal/Tests/Core/Command/GenerateThemeTest.php index c203cae17d4b..76f6a00eb0f6 100644 --- a/core/tests/Drupal/Tests/Core/Command/GenerateThemeTest.php +++ b/core/tests/Drupal/Tests/Core/Command/GenerateThemeTest.php @@ -3,7 +3,7 @@ namespace Drupal\Tests\Core\Command; use Drupal\BuildTests\QuickStart\QuickStartTestBase; -use Drupal\Core\Database\Driver\sqlite\Install\Tasks; +use Drupal\sqlite\Driver\Database\sqlite\Install\Tasks; use Symfony\Component\Process\PhpExecutableFinder; use Symfony\Component\Process\Process; diff --git a/core/tests/Drupal/Tests/Core/Command/QuickStartTest.php b/core/tests/Drupal/Tests/Core/Command/QuickStartTest.php index 917179c83bd9..081ab4950d95 100644 --- a/core/tests/Drupal/Tests/Core/Command/QuickStartTest.php +++ b/core/tests/Drupal/Tests/Core/Command/QuickStartTest.php @@ -2,7 +2,7 @@ namespace Drupal\Tests\Core\Command; -use Drupal\Core\Database\Driver\sqlite\Install\Tasks; +use Drupal\sqlite\Driver\Database\sqlite\Install\Tasks; use Drupal\Core\Test\TestDatabase; use Drupal\Tests\BrowserTestBase; use GuzzleHttp\Client; diff --git a/core/tests/Drupal/Tests/Core/Database/DatabaseTest.php b/core/tests/Drupal/Tests/Core/Database/DatabaseTest.php index 7c7dcd25752f..1eb469399fd0 100644 --- a/core/tests/Drupal/Tests/Core/Database/DatabaseTest.php +++ b/core/tests/Drupal/Tests/Core/Database/DatabaseTest.php @@ -70,7 +70,7 @@ public function testFindDriverAutoloadDirectory($expected, $namespace) { */ public function providerFindDriverAutoloadDirectory() { return [ - 'core mysql' => [FALSE, 'Drupal\Core\Database\Driver\mysql'], + 'core mysql' => ['core/modules/mysql/src/Driver/Database/mysql/', 'Drupal\mysql\Driver\Database\mysql'], 'D8 custom fake' => [FALSE, 'Drupal\Driver\Database\corefake'], 'module mysql' => ['core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/', 'Drupal\driver_test\Driver\Database\DrivertestMysql'], ]; diff --git a/core/tests/Drupal/Tests/Core/Database/Driver/mysql/ConnectionTest.php b/core/tests/Drupal/Tests/Core/Database/Driver/mysql/ConnectionTest.php index d6913c8a7056..e8d637022522 100644 --- a/core/tests/Drupal/Tests/Core/Database/Driver/mysql/ConnectionTest.php +++ b/core/tests/Drupal/Tests/Core/Database/Driver/mysql/ConnectionTest.php @@ -2,13 +2,13 @@ namespace Drupal\Tests\Core\Database\Driver\mysql; -use Drupal\Core\Database\Driver\mysql\Connection; +use Drupal\mysql\Driver\Database\mysql\Connection; use Drupal\Tests\UnitTestCase; /** * Tests MySQL database connections. * - * @coversDefaultClass \Drupal\Core\Database\Driver\mysql\Connection + * @coversDefaultClass \Drupal\mysql\Driver\Database\mysql\Connection * @group Database */ class ConnectionTest extends UnitTestCase { @@ -38,7 +38,7 @@ protected function setUp(): void { /** * Creates a Connection object for testing. * - * @return \Drupal\Core\Database\Driver\mysql\Connection + * @return \Drupal\mysql\Driver\Database\mysql\Connection */ private function createConnection(): Connection { /** @var \PDO $pdo_connection */ diff --git a/core/tests/Drupal/Tests/Core/Database/Driver/mysql/install/TasksTest.php b/core/tests/Drupal/Tests/Core/Database/Driver/mysql/install/TasksTest.php index d6ef38a27f4a..b8b6915aea2d 100644 --- a/core/tests/Drupal/Tests/Core/Database/Driver/mysql/install/TasksTest.php +++ b/core/tests/Drupal/Tests/Core/Database/Driver/mysql/install/TasksTest.php @@ -2,14 +2,14 @@ namespace Drupal\Tests\Core\Database\Driver\mysql\install; -use Drupal\Core\Database\Driver\mysql\Connection; -use Drupal\Core\Database\Driver\mysql\Install\Tasks; +use Drupal\mysql\Driver\Database\mysql\Connection; +use Drupal\mysql\Driver\Database\mysql\Install\Tasks; use Drupal\Tests\UnitTestCase; /** * Tests the MySQL install tasks. * - * @coversDefaultClass \Drupal\Core\Database\Driver\mysql\Install\Tasks + * @coversDefaultClass \Drupal\mysql\Driver\Database\mysql\Install\Tasks * @group Database */ class TasksTest extends UnitTestCase { @@ -17,7 +17,7 @@ class TasksTest extends UnitTestCase { /** * A connection object prophecy. * - * @var \Drupal\Core\Database\Driver\mysql\Connection|\Prophecy\Prophecy\ObjectProphecy + * @var \Drupal\mysql\Driver\Database\mysql\Connection|\Prophecy\Prophecy\ObjectProphecy */ private $connection; @@ -31,10 +31,10 @@ protected function setUp(): void { /** * Creates a Tasks object for testing. * - * @return \Drupal\Core\Database\Driver\mysql\Install\Tasks + * @return \Drupal\mysql\Driver\Database\mysql\Install\Tasks */ private function createTasks(): Tasks { - /** @var \Drupal\Core\Database\Driver\mysql\Connection $connection */ + /** @var \Drupal\mysql\Driver\Database\mysql\Connection $connection */ $connection = $this->connection->reveal(); return new class($connection) extends Tasks { @@ -63,7 +63,7 @@ protected function t($string, array $args = [], array $options = []) { /** * Creates a Tasks object for testing, without connection. * - * @return \Drupal\Core\Database\Driver\mysql\Install\Tasks + * @return \Drupal\mysql\Driver\Database\mysql\Install\Tasks */ private function createTasksNoConnection(): Tasks { return new class() extends Tasks { diff --git a/core/tests/Drupal/Tests/Core/Database/Driver/pgsql/PostgresqlSchemaTest.php b/core/tests/Drupal/Tests/Core/Database/Driver/pgsql/PostgresqlSchemaTest.php index df2a05f116dc..0e50eb18408b 100644 --- a/core/tests/Drupal/Tests/Core/Database/Driver/pgsql/PostgresqlSchemaTest.php +++ b/core/tests/Drupal/Tests/Core/Database/Driver/pgsql/PostgresqlSchemaTest.php @@ -2,13 +2,13 @@ namespace Drupal\Tests\Core\Database\Driver\pgsql; -use Drupal\Core\Database\Driver\pgsql\Schema; +use Drupal\pgsql\Driver\Database\pgsql\Schema; use Drupal\Tests\UnitTestCase; // cSpell:ignore conname /** - * @coversDefaultClass \Drupal\Core\Database\Driver\pgsql\Schema + * @coversDefaultClass \Drupal\pgsql\Driver\Database\pgsql\Schema * @group Database */ class PostgresqlSchemaTest extends UnitTestCase { @@ -16,7 +16,7 @@ class PostgresqlSchemaTest extends UnitTestCase { /** * The PostgreSql DB connection. * - * @var \PHPUnit\Framework\MockObject\MockObject|\Drupal\Core\Database\Driver\pgsql\Connection + * @var \PHPUnit\Framework\MockObject\MockObject|\Drupal\pgsql\Driver\Database\pgsql\Connection */ protected $connection; @@ -26,7 +26,7 @@ class PostgresqlSchemaTest extends UnitTestCase { protected function setUp(): void { parent::setUp(); - $this->connection = $this->getMockBuilder('\Drupal\Core\Database\Driver\pgsql\Connection') + $this->connection = $this->getMockBuilder('\Drupal\pgsql\Driver\Database\pgsql\Connection') ->disableOriginalConstructor() ->getMock(); } diff --git a/core/tests/Drupal/Tests/Core/Database/Driver/sqlite/ConnectionTest.php b/core/tests/Drupal/Tests/Core/Database/Driver/sqlite/ConnectionTest.php index 3d872ceb779d..3e52d6aecca8 100644 --- a/core/tests/Drupal/Tests/Core/Database/Driver/sqlite/ConnectionTest.php +++ b/core/tests/Drupal/Tests/Core/Database/Driver/sqlite/ConnectionTest.php @@ -2,12 +2,12 @@ namespace Drupal\Tests\Core\Database\Driver\sqlite; -use Drupal\Core\Database\Driver\sqlite\Connection; +use Drupal\sqlite\Driver\Database\sqlite\Connection; use Drupal\Tests\Core\Database\Stub\StubPDO; use Drupal\Tests\UnitTestCase; /** - * @coversDefaultClass \Drupal\Core\Database\Driver\sqlite\Connection + * @coversDefaultClass \Drupal\sqlite\Driver\Database\sqlite\Connection * @group Database */ class ConnectionTest extends UnitTestCase { diff --git a/core/tests/Drupal/Tests/Core/Database/InstallerObjectTest.php b/core/tests/Drupal/Tests/Core/Database/InstallerObjectTest.php index 15f823a688fd..278ace0becd4 100644 --- a/core/tests/Drupal/Tests/Core/Database/InstallerObjectTest.php +++ b/core/tests/Drupal/Tests/Core/Database/InstallerObjectTest.php @@ -3,7 +3,7 @@ namespace Drupal\Tests\Core\Database; use Composer\Autoload\ClassLoader; -use Drupal\Core\Database\Driver\mysql\Install\Tasks as MysqlInstallTasks; +use Drupal\mysql\Driver\Database\mysql\Install\Tasks as MysqlInstallTasks; use Drupal\Driver\Database\fake\Install\Tasks as FakeInstallTasks; use Drupal\Driver\Database\corefake\Install\Tasks as CustomCoreFakeInstallTasks; use Drupal\driver_test\Driver\Database\DrivertestMysql\Install\Tasks as DriverTestMysqlInstallTasks; @@ -58,7 +58,7 @@ public function testDbInstallerObject($driver, $namespace, $expected_class_name) public function providerDbInstallerObject() { return [ // A driver only in the core namespace. - ['mysql', NULL, MysqlInstallTasks::class], + ['mysql', "Drupal\\mysql\\Driver\\Database\\mysql", MysqlInstallTasks::class], // A driver only in the custom namespace. ['fake', "Drupal\\Driver\\Database\\fake", FakeInstallTasks::class], diff --git a/core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php b/core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php index 2dd58125f7fd..fb1c9dde8df2 100644 --- a/core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php +++ b/core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php @@ -48,8 +48,8 @@ protected function setUp(): void { * * @dataProvider providerConvertDbUrlToConnectionInfo */ - public function testDbUrlToConnectionConversion($root, $url, $database_array) { - $result = Database::convertDbUrlToConnectionInfo($url, $root ?: $this->root); + public function testDbUrlToConnectionConversion($url, $database_array) { + $result = Database::convertDbUrlToConnectionInfo($url, $this->root); $this->assertEquals($database_array, $result); } @@ -58,14 +58,13 @@ public function testDbUrlToConnectionConversion($root, $url, $database_array) { * * @return array * Array of arrays with the following elements: - * - root: The baseroot string, only used with sqlite drivers. * - url: The full URL string to be tested. * - database_array: An array containing the expected results. */ public function providerConvertDbUrlToConnectionInfo() { + $root = dirname(__FILE__, 7); return [ 'MySql without prefix' => [ - '', 'mysql://test_user:test_pass@test_host:3306/test_database', [ 'driver' => 'mysql', @@ -74,21 +73,21 @@ public function providerConvertDbUrlToConnectionInfo() { 'host' => 'test_host', 'database' => 'test_database', 'port' => 3306, - 'namespace' => 'Drupal\Core\Database\Driver\mysql', + 'namespace' => 'Drupal\mysql\Driver\Database\mysql', + 'autoload' => 'core/modules/mysql/src/Driver/Database/mysql/', ], ], 'SQLite, relative to root, without prefix' => [ - '/var/www/d8', 'sqlite://localhost/test_database', [ 'driver' => 'sqlite', 'host' => 'localhost', - 'database' => '/var/www/d8/test_database', - 'namespace' => 'Drupal\Core\Database\Driver\sqlite', + 'database' => $root . '/test_database', + 'namespace' => 'Drupal\sqlite\Driver\Database\sqlite', + 'autoload' => 'core/modules/sqlite/src/Driver/Database/sqlite/', ], ], 'MySql with prefix' => [ - '', 'mysql://test_user:test_pass@test_host:3306/test_database#bar', [ 'driver' => 'mysql', @@ -98,32 +97,32 @@ public function providerConvertDbUrlToConnectionInfo() { 'database' => 'test_database', 'prefix' => 'bar', 'port' => 3306, - 'namespace' => 'Drupal\Core\Database\Driver\mysql', + 'namespace' => 'Drupal\mysql\Driver\Database\mysql', + 'autoload' => 'core/modules/mysql/src/Driver/Database/mysql/', ], ], 'SQLite, relative to root, with prefix' => [ - '/var/www/d8', 'sqlite://localhost/test_database#foo', [ 'driver' => 'sqlite', 'host' => 'localhost', - 'database' => '/var/www/d8/test_database', + 'database' => $root . '/test_database', 'prefix' => 'foo', - 'namespace' => 'Drupal\Core\Database\Driver\sqlite', + 'namespace' => 'Drupal\sqlite\Driver\Database\sqlite', + 'autoload' => 'core/modules/sqlite/src/Driver/Database/sqlite/', ], ], 'SQLite, absolute path, without prefix' => [ - '/var/www/d8', 'sqlite://localhost//baz/test_database', [ 'driver' => 'sqlite', 'host' => 'localhost', 'database' => '/baz/test_database', - 'namespace' => 'Drupal\Core\Database\Driver\sqlite', + 'namespace' => 'Drupal\sqlite\Driver\Database\sqlite', + 'autoload' => 'core/modules/sqlite/src/Driver/Database/sqlite/', ], ], 'MySQL contrib test driver without prefix' => [ - '', 'DrivertestMysql://test_user:test_pass@test_host:3306/test_database?module=driver_test', [ 'driver' => 'DrivertestMysql', @@ -137,7 +136,6 @@ public function providerConvertDbUrlToConnectionInfo() { ], ], 'MySQL contrib test driver with prefix' => [ - '', 'DrivertestMysql://test_user:test_pass@test_host:3306/test_database?module=driver_test#bar', [ 'driver' => 'DrivertestMysql', @@ -152,7 +150,6 @@ public function providerConvertDbUrlToConnectionInfo() { ], ], 'PostgreSQL contrib test driver without prefix' => [ - '', 'DrivertestPgsql://test_user:test_pass@test_host:5432/test_database?module=driver_test', [ 'driver' => 'DrivertestPgsql', @@ -166,7 +163,6 @@ public function providerConvertDbUrlToConnectionInfo() { ], ], 'PostgreSQL contrib test driver with prefix' => [ - '', 'DrivertestPgsql://test_user:test_pass@test_host:5432/test_database?module=driver_test#bar', [ 'driver' => 'DrivertestPgsql', @@ -181,7 +177,6 @@ public function providerConvertDbUrlToConnectionInfo() { ], ], 'MySql with a custom query parameter' => [ - '', 'mysql://test_user:test_pass@test_host:3306/test_database?extra=value', [ 'driver' => 'mysql', @@ -190,7 +185,55 @@ public function providerConvertDbUrlToConnectionInfo() { 'host' => 'test_host', 'database' => 'test_database', 'port' => 3306, - 'namespace' => 'Drupal\Core\Database\Driver\mysql', + 'namespace' => 'Drupal\mysql\Driver\Database\mysql', + 'autoload' => 'core/modules/mysql/src/Driver/Database/mysql/', + ], + ], + 'MySql with the module name mysql' => [ + 'mysql://test_user:test_pass@test_host:3306/test_database?module=mysql', + [ + 'driver' => 'mysql', + 'username' => 'test_user', + 'password' => 'test_pass', + 'host' => 'test_host', + 'database' => 'test_database', + 'port' => 3306, + 'namespace' => 'Drupal\mysql\Driver\Database\mysql', + 'autoload' => 'core/modules/mysql/src/Driver/Database/mysql/', + ], + ], + 'PostgreSql without the module name set' => [ + 'pgsql://test_user:test_pass@test_host/test_database', + [ + 'driver' => 'pgsql', + 'username' => 'test_user', + 'password' => 'test_pass', + 'host' => 'test_host', + 'database' => 'test_database', + 'namespace' => 'Drupal\pgsql\Driver\Database\pgsql', + 'autoload' => 'core/modules/pgsql/src/Driver/Database/pgsql/', + ], + ], + 'PostgreSql with the module name pgsql' => [ + 'pgsql://test_user:test_pass@test_host/test_database?module=pgsql', + [ + 'driver' => 'pgsql', + 'username' => 'test_user', + 'password' => 'test_pass', + 'host' => 'test_host', + 'database' => 'test_database', + 'namespace' => 'Drupal\pgsql\Driver\Database\pgsql', + 'autoload' => 'core/modules/pgsql/src/Driver/Database/pgsql/', + ], + ], + 'SQLite, relative to root, without prefix and with the module name sqlite' => [ + 'sqlite://localhost/test_database?module=sqlite', + [ + 'driver' => 'sqlite', + 'host' => 'localhost', + 'database' => $root . '/test_database', + 'namespace' => 'Drupal\sqlite\Driver\Database\sqlite', + 'autoload' => 'core/modules/sqlite/src/Driver/Database/sqlite/', ], ], ]; @@ -258,7 +301,7 @@ public function providerGetConnectionInfoAsUrl() { 'port' => '3306', 'driver' => 'mysql', ]; - $expected_url1 = 'mysql://test_user:test_pass@test_host:3306/test_database'; + $expected_url1 = 'mysql://test_user:test_pass@test_host:3306/test_database?module=mysql'; $info2 = [ 'database' => 'test_database', @@ -269,20 +312,20 @@ public function providerGetConnectionInfoAsUrl() { 'port' => '3306', 'driver' => 'mysql', ]; - $expected_url2 = 'mysql://test_user:test_pass@test_host:3306/test_database#pre'; + $expected_url2 = 'mysql://test_user:test_pass@test_host:3306/test_database?module=mysql#pre'; $info3 = [ 'database' => 'test_database', 'driver' => 'sqlite', ]; - $expected_url3 = 'sqlite://localhost/test_database'; + $expected_url3 = 'sqlite://localhost/test_database?module=sqlite'; $info4 = [ 'database' => 'test_database', 'driver' => 'sqlite', 'prefix' => 'pre', ]; - $expected_url4 = 'sqlite://localhost/test_database#pre'; + $expected_url4 = 'sqlite://localhost/test_database?module=sqlite#pre'; $info5 = [ 'database' => 'test_database', @@ -382,7 +425,7 @@ public function providerInvalidArgumentGetConnectionInfoAsUrl() { [ 'driver' => 'sqlite', 'host' => 'localhost', - 'namespace' => 'Drupal\Core\Database\Driver\sqlite', + 'namespace' => 'Drupal\sqlite\Driver\Database\sqlite', ], "As a minimum, the connection options array must contain at least the 'driver' and 'database' keys", ], diff --git a/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/BackendCompilerPassTest.php b/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/BackendCompilerPassTest.php index fa9a371d001b..2d7b5505db00 100644 --- a/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/BackendCompilerPassTest.php +++ b/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/BackendCompilerPassTest.php @@ -124,7 +124,7 @@ protected function getSqliteContainer($service) { $container = new ContainerBuilder(); $container->setDefinition('service', $service); $container->setDefinition('sqlite.service', new Definition(__NAMESPACE__ . '\\ServiceClassSqlite')); - $mock = $this->getMockBuilder('Drupal\Core\Database\Driver\sqlite\Connection')->onlyMethods([])->disableOriginalConstructor()->getMock(); + $mock = $this->getMockBuilder('Drupal\sqlite\Driver\Database\sqlite\Connection')->onlyMethods([])->disableOriginalConstructor()->getMock(); $container->set('database', $mock); return $container; } diff --git a/core/tests/Drupal/Tests/Core/Test/TestSetupTraitTest.php b/core/tests/Drupal/Tests/Core/Test/TestSetupTraitTest.php index d0b828356661..7b88af74511c 100644 --- a/core/tests/Drupal/Tests/Core/Test/TestSetupTraitTest.php +++ b/core/tests/Drupal/Tests/Core/Test/TestSetupTraitTest.php @@ -25,8 +25,9 @@ class TestSetupTraitTest extends UnitTestCase { * @covers ::changeDatabasePrefix */ public function testChangeDatabasePrefix() { + $root = dirname(__FILE__, 7); putenv('SIMPLETEST_DB=pgsql://user:pass@127.0.0.1/db'); - $connection_info = Database::convertDbUrlToConnectionInfo('mysql://user:pass@localhost/db', ''); + $connection_info = Database::convertDbUrlToConnectionInfo('mysql://user:pass@localhost/db', $root); Database::addConnectionInfo('default', 'default', $connection_info); $this->assertEquals('mysql', Database::getConnectionInfo()['default']['driver']); $this->assertEquals('localhost', Database::getConnectionInfo()['default']['host']); @@ -35,7 +36,7 @@ public function testChangeDatabasePrefix() { // used to avoid unnecessary set up. $test_setup = $this->getMockForTrait(TestSetupTrait::class); $test_setup->databasePrefix = 'testDbPrefix'; - $test_setup->root = ''; + $test_setup->root = $root; $method = new \ReflectionMethod(get_class($test_setup), 'changeDatabasePrefix'); $method->setAccessible(TRUE); diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index 718890224b48..4768843bcf07 100644 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -170,9 +170,9 @@ * information on these defaults and the potential issues. * * More details can be found in the constructor methods for each driver: - * - \Drupal\Core\Database\Driver\mysql\Connection::__construct() - * - \Drupal\Core\Database\Driver\pgsql\Connection::__construct() - * - \Drupal\Core\Database\Driver\sqlite\Connection::__construct() + * - \Drupal\mysql\Driver\Database\mysql\Connection::__construct() + * - \Drupal\pgsql\Driver\Database\pgsql\Connection::__construct() + * - \Drupal\sqlite\Driver\Database\sqlite\Connection::__construct() * * Sample Database configuration format for PostgreSQL (pgsql): * @code -- GitLab