Issue #3129043 by mondrake, daffie, ankithashetty, ravi.shankar, alexpott, Beakerboy, catch: Move core database drivers to modules of their own
parent
5c92c24178
commit
934f42ae87
|
@ -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",
|
||||
|
|
|
@ -170,9 +170,9 @@ $databases = [];
|
|||
* 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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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')) {
|
||||
|
|
|
@ -272,7 +272,7 @@ class DbDumpCommand extends DbCommandBase {
|
|||
*/
|
||||
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();
|
||||
|
|
|
@ -238,7 +238,7 @@ abstract class Database {
|
|||
|
||||
// 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 @@ abstract class Database {
|
|||
$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 @@ abstract class Database {
|
|||
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'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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 {}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -224,7 +224,7 @@ class StatementPrefetch implements \Iterator, StatementInterface {
|
|||
// 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);
|
||||
|
|
|
@ -162,6 +162,43 @@ final class Settings {
|
|||
// 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
|
||||
|
|
|
@ -516,6 +516,7 @@ trait FunctionalTestSetupTrait {
|
|||
$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.
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -87,10 +87,13 @@ class ConfigImportAllTest extends ModuleTestBase {
|
|||
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 @@ class ConfigImportAllTest extends ModuleTestBase {
|
|||
// 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');
|
||||
|
|
|
@ -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 @@ abstract class MigrateSqlSourceTestBase extends MigrateSourceTestBase {
|
|||
* 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) {
|
||||
|
|
|
@ -6,7 +6,7 @@ use Drupal\KernelTests\KernelTestBase;
|
|||
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 @@ class QueryBatchTest extends KernelTestBase {
|
|||
* 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) {
|
||||
|
|
|
@ -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 @@ class MigrateSqlIdMapTest extends MigrateTestCase {
|
|||
$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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 @@ abstract class MigrateTestCase extends UnitTestCase {
|
|||
* (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 = []) {
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
name: MySQL
|
||||
type: module
|
||||
description: 'Database driver for MySQL.'
|
||||
package: Core
|
||||
version: VERSION
|
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
|
@ -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".
|
||||
*/
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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".
|
||||
*/
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
name: PostgreSQL
|
||||
type: module
|
||||
description: 'Database driver for PostgreSQL.'
|
||||
package: Core
|
||||
version: VERSION
|
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
|
@ -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".
|
||||
*/
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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".
|
||||
*/
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
name: SQLite
|
||||
type: module
|
||||
description: 'Database driver for SQLite.'
|
||||
package: Core
|
||||
version: VERSION
|
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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) . ')';
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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) . '} ';
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 {
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 {
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 {
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
],
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -178,7 +178,7 @@ class StringArgument extends ArgumentPluginBase {
|
|||
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';
|
||||
}
|
||||
|
|
|
@ -97,7 +97,7 @@ class SqliteDateSql implements DateSqlInterface {
|
|||
// 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)) {
|
||||
|
|
|
@ -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
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue