diff --git a/core/.phpstan-baseline.php b/core/.phpstan-baseline.php index 5937ccef90f83ae11ea53e6cbb611c4bf8e73886..cc39d334b5b7ee901aec05a975a91bf1b4981426 100644 --- a/core/.phpstan-baseline.php +++ b/core/.phpstan-baseline.php @@ -29817,12 +29817,6 @@ 'count' => 1, 'path' => __DIR__ . '/modules/pgsql/src/Driver/Database/pgsql/Connection.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\pgsql\\\\Driver\\\\Database\\\\pgsql\\\\Connection\\:\\:setPrefix\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/pgsql/src/Driver/Database/pgsql/Connection.php', -]; $ignoreErrors[] = [ 'message' => '#^Method Drupal\\\\pgsql\\\\Driver\\\\Database\\\\pgsql\\\\Install\\\\Tasks\\:\\:checkBinaryOutput\\(\\) has no return type specified\\.$#', 'identifier' => 'missingType.return', @@ -55676,6 +55670,12 @@ 'count' => 1, 'path' => __DIR__ . '/tests/Drupal/Tests/Core/Database/Stub/StubConnection.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method Drupal\\\\Tests\\\\Core\\\\Database\\\\Stub\\\\StubLegacyConnection\\:\\:createDatabase\\(\\) has no return type specified\\.$#', + 'identifier' => 'missingType.return', + 'count' => 1, + 'path' => __DIR__ . '/tests/Drupal/Tests/Core/Database/Stub/StubLegacyConnection.php', +]; $ignoreErrors[] = [ 'message' => '#^Method Drupal\\\\Tests\\\\Core\\\\Database\\\\Stub\\\\StubSchema\\:\\:addField\\(\\) has no return type specified\\.$#', 'identifier' => 'missingType.return', diff --git a/core/lib/Drupal/Core/Cache/DatabaseBackend.php b/core/lib/Drupal/Core/Cache/DatabaseBackend.php index efae811b7679a66c32a046fcd63058ce13913c7c..e41ffee9069773c568810e7f7b7363f09a05840d 100644 --- a/core/lib/Drupal/Core/Cache/DatabaseBackend.php +++ b/core/lib/Drupal/Core/Cache/DatabaseBackend.php @@ -128,7 +128,7 @@ public function getMultiple(&$cids, $allow_invalid = FALSE) { // ::select() is a much smaller proportion of the request. $result = []; try { - $result = $this->connection->query('SELECT [cid], [data], [created], [expire], [serialized], [tags], [checksum] FROM {' . $this->connection->escapeTable($this->bin) . '} WHERE [cid] IN ( :cids[] ) ORDER BY [cid]', [':cids[]' => array_keys($cid_mapping)]); + $result = $this->connection->query('SELECT [cid], [data], [created], [expire], [serialized], [tags], [checksum] FROM {' . $this->bin . '} WHERE [cid] IN ( :cids[] ) ORDER BY [cid]', [':cids[]' => array_keys($cid_mapping)]); } catch (\Exception) { // Nothing to do. diff --git a/core/lib/Drupal/Core/Config/DatabaseStorage.php b/core/lib/Drupal/Core/Config/DatabaseStorage.php index 60da9de12470e0950a429aa086ddb81c5f075dfe..53bb9584f34aa53c4da23e3ec1c6139a0e9976b6 100644 --- a/core/lib/Drupal/Core/Config/DatabaseStorage.php +++ b/core/lib/Drupal/Core/Config/DatabaseStorage.php @@ -65,7 +65,7 @@ public function __construct(Connection $connection, $table, array $options = [], */ public function exists($name) { try { - return (bool) $this->connection->queryRange('SELECT 1 FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [collection] = :collection AND [name] = :name', 0, 1, [ + return (bool) $this->connection->queryRange('SELECT 1 FROM {' . $this->table . '} WHERE [collection] = :collection AND [name] = :name', 0, 1, [ ':collection' => $this->collection, ':name' => $name, ], $this->options)->fetchField(); @@ -86,7 +86,7 @@ public function exists($name) { public function read($name) { $data = FALSE; try { - $raw = $this->connection->query('SELECT [data] FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [collection] = :collection AND [name] = :name', [ + $raw = $this->connection->query('SELECT [data] FROM {' . $this->table . '} WHERE [collection] = :collection AND [name] = :name', [ ':collection' => $this->collection, ':name' => $name, ], $this->options)->fetchField(); @@ -115,7 +115,7 @@ public function readMultiple(array $names) { $list = []; try { $list = $this->connection - ->query('SELECT [name], [data] FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [collection] = :collection AND [name] IN ( :names[] )', [ + ->query('SELECT [name], [data] FROM {' . $this->table . '} WHERE [collection] = :collection AND [name] IN ( :names[] )', [ ':collection' => $this->collection, ':names[]' => $names, ], $this->options) @@ -342,7 +342,7 @@ public function getCollectionName() { */ public function getAllCollectionNames() { try { - return $this->connection->query('SELECT DISTINCT [collection] FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [collection] <> :collection ORDER by [collection]', [ + return $this->connection->query('SELECT DISTINCT [collection] FROM {' . $this->table . '} WHERE [collection] <> :collection ORDER by [collection]', [ ':collection' => StorageInterface::DEFAULT_COLLECTION, ])->fetchCol(); } diff --git a/core/lib/Drupal/Core/Database/Connection.php b/core/lib/Drupal/Core/Database/Connection.php index 41bd043c5c8f45846e4e5ac8fc78c89621b6b60f..7380f733d552a812f51e219e60a5a7426633721c 100644 --- a/core/lib/Drupal/Core/Database/Connection.php +++ b/core/lib/Drupal/Core/Database/Connection.php @@ -2,9 +2,10 @@ namespace Drupal\Core\Database; -use Drupal\Component\Assertion\Inspector; use Drupal\Core\Database\Event\DatabaseEvent; use Drupal\Core\Database\Exception\EventException; +use Drupal\Core\Database\Identifier\IdentifierHandlerBase; +use Drupal\Core\Database\Identifier\IdentifierType; use Drupal\Core\Database\Query\Condition; use Drupal\Core\Database\Query\Delete; use Drupal\Core\Database\Query\Insert; @@ -101,30 +102,6 @@ abstract class Connection { */ protected $schema = NULL; - /** - * The prefix used by this database connection. - * - * @var string - */ - protected string $prefix; - - /** - * Replacements to fully qualify {table} placeholders in SQL strings. - * - * An array of two strings, the first being the replacement for opening curly - * brace '{', the second for closing curly brace '}'. - * - * @var string[] - */ - protected array $tablePlaceholderReplacements; - - /** - * List of escaped table names, keyed by unescaped names. - * - * @var array - */ - protected $escapedTables = []; - /** * List of escaped field names, keyed by unescaped names. * @@ -142,17 +119,6 @@ abstract class Connection { */ protected $escapedAliases = []; - /** - * The identifier quote characters for the database type. - * - * An array containing the start and end identifier quote characters for the - * database type. The ANSI SQL standard identifier quote character is a double - * quotation mark. - * - * @var string[] - */ - protected $identifierQuotes; - /** * Tracks the database API events to be dispatched. * @@ -166,6 +132,11 @@ abstract class Connection { */ protected TransactionManagerInterface $transactionManager; + /** + * The identifiers handler. + */ + public readonly IdentifierHandlerBase $identifiers; + /** * Constructs a Connection object. * @@ -176,13 +147,17 @@ abstract class Connection { * - prefix * - namespace * - Other driver-specific options. - */ - public function __construct(object $connection, array $connection_options) { - assert(count($this->identifierQuotes) === 2 && Inspector::assertAllStrings($this->identifierQuotes), '\Drupal\Core\Database\Connection::$identifierQuotes must contain 2 string values'); - + * @param \Drupal\Core\Database\Identifier\IdentifierHandlerBase|null $identifierHandler + * The identifiers handler. + */ + public function __construct( + object $connection, + array $connection_options, + ?IdentifierHandlerBase $identifierHandler = NULL, + ) { // Manage the table prefix. $connection_options['prefix'] = $connection_options['prefix'] ?? ''; - $this->setPrefix($connection_options['prefix']); + assert(is_string($connection_options['prefix']), 'The \'prefix\' connection option to ' . __METHOD__ . '() must be a string.'); // Work out the database driver namespace if none is provided. This normally // written to setting.php by installer or set by @@ -191,10 +166,109 @@ public function __construct(object $connection, array $connection_options) { $connection_options['namespace'] = (new \ReflectionObject($this))->getNamespaceName(); } + if ($identifierHandler) { + $this->identifiers = $identifierHandler; + } + else { + @trigger_error("Not passing an IdentifierHandler object to " . __METHOD__ . "() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. See https://www.drupal.org/node/3513282", E_USER_DEPRECATED); + $this->identifiers = new class($connection_options['prefix'] ?? '') extends IdentifierHandlerBase { + + /** + * {@inheritdoc} + */ + public function getMaxLength(IdentifierType $type): int { + return 128; + } + + }; + } + $this->connection = $connection; $this->connectionOptions = $connection_options; } + /** + * Implements the magic __get() method. + */ + public function __get(string $name): mixed { + switch ($name) { + case 'prefix': + @trigger_error("Accessing Connection::\${$name} is deprecated in drupal:11.2.0 and the property is removed from drupal:12.0.0. Use IdentifierHandler methods instead. See https://www.drupal.org/node/3513282", E_USER_DEPRECATED); + return $this->identifiers->tablePrefix ?? ''; + + case 'escapedTables': + @trigger_error("Accessing Connection::\${$name} is deprecated in drupal:11.2.0 and the property is removed from drupal:12.0.0. Use IdentifierHandler methods instead. See https://www.drupal.org/node/3513282", E_USER_DEPRECATED); + return []; + + case 'identifierQuotes': + @trigger_error("Accessing Connection::\${$name} is deprecated in drupal:11.2.0 and the property is removed from drupal:12.0.0. Use IdentifierHandler methods instead. See https://www.drupal.org/node/3513282", E_USER_DEPRECATED); + return $this->identifiers->identifierQuotes ?? ''; + + case 'tablePlaceholderReplacements': + @trigger_error("Accessing Connection::\${$name} is deprecated in drupal:11.2.0 and the property is removed from drupal:12.0.0. Use IdentifierHandler methods instead. See https://www.drupal.org/node/3513282", E_USER_DEPRECATED); + $identifierQuotes = $this->identifiers->identifierQuotes ?? ['"', '"']; + return [ + $identifierQuotes[0] . str_replace('.', $identifierQuotes[1] . '.' . $identifierQuotes[0], $this->identifiers->tablePrefix ?? ''), + $identifierQuotes[1], + ]; + + default: + throw new \LogicException("The \${$name} property is undefined in " . __CLASS__); + + } + } + + /** + * Implements the magic __set() method. + */ + public function __set(string $name, mixed $value): void { + switch ($name) { + case 'prefix': + case 'escapedTables': + case 'identifierQuotes': + case 'tablePlaceholderReplacements': + @trigger_error("Accessing Connection::\${$name} is deprecated in drupal:11.2.0 and the property is removed from drupal:12.0.0. Use IdentifierHandler methods instead. See https://www.drupal.org/node/3513282", E_USER_DEPRECATED); + return; + + default: + throw new \LogicException("The \${$name} property is undefined in " . __CLASS__); + } + } + + /** + * Implements the magic __isset() method. + */ + public function __isset(string $name): bool { + switch ($name) { + case 'prefix': + case 'escapedTables': + case 'identifierQuotes': + case 'tablePlaceholderReplacements': + @trigger_error("Accessing Connection::\${$name} is deprecated in drupal:11.2.0 and the property is removed from drupal:12.0.0. Use IdentifierHandler methods instead. See https://www.drupal.org/node/3513282", E_USER_DEPRECATED); + return TRUE; + + default: + throw new \LogicException("The \${$name} property is undefined in " . __CLASS__); + } + } + + /** + * Implements the magic __unset() method. + */ + public function __unset(string $name): void { + switch ($name) { + case 'prefix': + case 'escapedTables': + case 'identifierQuotes': + case 'tablePlaceholderReplacements': + @trigger_error("Accessing Connection::\${$name} is deprecated in drupal:11.2.0 and the property is removed from drupal:12.0.0. Use IdentifierHandler methods instead. See https://www.drupal.org/node/3513282", E_USER_DEPRECATED); + return; + + default: + throw new \LogicException("The \${$name} property is undefined in " . __CLASS__); + } + } + /** * Opens a client connection. * @@ -327,9 +401,15 @@ public function attachDatabase(string $database): void { * * @return string * The table prefix. + * + * @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use + * IdentifierHandler methods instead. + * + * @see https://www.drupal.org/node/3513282 */ public function getPrefix(): string { - return $this->prefix; + @trigger_error(__METHOD__ . "() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use IdentifierHandler methods instead. See https://www.drupal.org/node/3513282", E_USER_DEPRECATED); + return $this->identifiers->tablePrefix; } /** @@ -337,14 +417,14 @@ public function getPrefix(): string { * * @param string $prefix * A single prefix. + * + * @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Pass the + * table prefix to the IdentifierHandler constructor instead. + * + * @see https://www.drupal.org/node/3513282 */ protected function setPrefix($prefix) { - assert(is_string($prefix), 'The \'$prefix\' argument to ' . __METHOD__ . '() must be a string'); - $this->prefix = $prefix; - $this->tablePlaceholderReplacements = [ - $this->identifierQuotes[0] . str_replace('.', $this->identifierQuotes[1] . '.' . $this->identifierQuotes[0], $prefix), - $this->identifierQuotes[1], - ]; + @trigger_error(__METHOD__ . "() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Pass the table prefix to the IdentifierHandler constructor instead. See https://www.drupal.org/node/3513282", E_USER_DEPRECATED); } /** @@ -362,7 +442,12 @@ protected function setPrefix($prefix) { * The properly-prefixed string. */ public function prefixTables($sql) { - return str_replace(['{', '}'], $this->tablePlaceholderReplacements, $sql); + $replacements = $tables = []; + preg_match_all('/(\{(\S*)\})/', $sql, $tables, PREG_SET_ORDER, 0); + foreach ($tables as $table) { + $replacements[$table[1]] = $this->identifiers->table($table[2])->forMachine(); + } + return str_replace(array_keys($replacements), array_values($replacements), $sql); } /** @@ -386,7 +471,7 @@ public function prefixTables($sql) { * This method should only be called by database API code. */ public function quoteIdentifiers($sql) { - return str_replace(['[', ']'], $this->identifierQuotes, $sql); + return str_replace(['[', ']'], $this->identifiers->identifierQuotes, $sql); } /** @@ -399,9 +484,13 @@ public function quoteIdentifiers($sql) { * The fully qualified table name. */ public function getFullQualifiedTableName($table) { - $options = $this->getConnectionOptions(); - $prefix = $this->getPrefix(); - return $options['database'] . '.' . $prefix . $table; + $tableIdentifier = $this->identifiers->table($table); + // If already fully qualified, just pass it on. + if ($tableIdentifier->database || $tableIdentifier->schema) { + return $tableIdentifier->forMachine(); + } + // Return as "<schema>"."<table>". + return $this->identifiers->schema($this->getConnectionOptions()['database'])->quotedMachineName . '.' . $tableIdentifier->quotedMachineName; } /** @@ -983,11 +1072,15 @@ public function condition($conjunction) { * * @return string * The sanitized database name. + * + * @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use + * IdentifierHandler methods instead. + * + * @see https://www.drupal.org/node/3513282 */ public function escapeDatabase($database) { - $database = preg_replace('/[^A-Za-z0-9_]+/', '', $database); - [$start_quote, $end_quote] = $this->identifierQuotes; - return $start_quote . $database . $end_quote; + @trigger_error(__METHOD__ . "() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use IdentifierHandler methods instead. See https://www.drupal.org/node/3513282", E_USER_DEPRECATED); + return $this->identifiers->schema($database)->forMachine(); } /** @@ -1004,14 +1097,16 @@ public function escapeDatabase($database) { * @return string * The sanitized table name. * + * @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use + * IdentifierHandler methods instead. + * + * @see https://www.drupal.org/node/3513282 * @see \Drupal\Core\Database\Connection::prefixTables() * @see \Drupal\Core\Database\Connection::setPrefix() */ public function escapeTable($table) { - if (!isset($this->escapedTables[$table])) { - $this->escapedTables[$table] = preg_replace('/[^A-Za-z0-9_.]+/', '', $table); - } - return $this->escapedTables[$table]; + @trigger_error(__METHOD__ . "() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use IdentifierHandler methods instead. See https://www.drupal.org/node/3513282", E_USER_DEPRECATED); + return $this->identifiers->table($table)->canonical(); } /** @@ -1030,7 +1125,7 @@ public function escapeTable($table) { public function escapeField($field) { if (!isset($this->escapedFields[$field])) { $escaped = preg_replace('/[^A-Za-z0-9_.]+/', '', $field); - [$start_quote, $end_quote] = $this->identifierQuotes; + [$start_quote, $end_quote] = $this->identifiers->identifierQuotes; // Sometimes fields have the format table_alias.field. In such cases // both identifiers should be quoted, for example, "table_alias"."field". $this->escapedFields[$field] = $start_quote . str_replace('.', $end_quote . '.' . $start_quote, $escaped) . $end_quote; @@ -1054,7 +1149,7 @@ public function escapeField($field) { */ public function escapeAlias($field) { if (!isset($this->escapedAliases[$field])) { - [$start_quote, $end_quote] = $this->identifierQuotes; + [$start_quote, $end_quote] = $this->identifiers->identifierQuotes; $this->escapedAliases[$field] = $start_quote . preg_replace('/[^A-Za-z0-9_]+/', '', $field) . $end_quote; } return $this->escapedAliases[$field]; @@ -1242,6 +1337,8 @@ abstract public function databaseType(); * * @param string $database * The name of the database to create. + * + * @throws \Drupal\Core\Database\DatabaseNotFoundException */ abstract public function createDatabase($database); diff --git a/core/lib/Drupal/Core/Database/Exception/IdentifierException.php b/core/lib/Drupal/Core/Database/Exception/IdentifierException.php new file mode 100644 index 0000000000000000000000000000000000000000..a069749f918d8d56dddea7609ffeff616e1421e4 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Exception/IdentifierException.php @@ -0,0 +1,13 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Database\Exception; + +use Drupal\Core\Database\DatabaseException; + +/** + * Exception thrown by the identifier handling API. + */ +class IdentifierException extends \RuntimeException implements DatabaseException { +} diff --git a/core/lib/Drupal/Core/Database/Identifier/Database.php b/core/lib/Drupal/Core/Database/Identifier/Database.php new file mode 100644 index 0000000000000000000000000000000000000000..6f5173303e5f351deae277a79450cb1fd3e8fb94 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Identifier/Database.php @@ -0,0 +1,24 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Database\Identifier; + +/** + * Handles a database identifier. + * + * When using full notation, a table can be identified as + * [database.][schema.]table. + */ +final class Database extends IdentifierBase { + + public function __construct( + IdentifierHandlerBase $identifierHandler, + string $identifier, + ) { + $canonicalName = $identifierHandler->canonicalize($identifier, IdentifierType::Database); + $machineName = $identifierHandler->resolveForMachine($canonicalName, [], IdentifierType::Database); + parent::__construct($identifier, $canonicalName, $machineName, $identifierHandler->quote($machineName)); + } + +} diff --git a/core/lib/Drupal/Core/Database/Identifier/IdentifierBase.php b/core/lib/Drupal/Core/Database/Identifier/IdentifierBase.php new file mode 100644 index 0000000000000000000000000000000000000000..68d9266a183b0b4d105d09437f2809ad3fe6fdf2 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Identifier/IdentifierBase.php @@ -0,0 +1,50 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Database\Identifier; + +/** + * The base class for database identifier value objects. + */ +abstract class IdentifierBase implements \Stringable { + + public function __construct( + public readonly string $identifier, + public readonly string $canonicalName, + public readonly string $machineName, + public readonly string $quotedMachineName, + ) { + } + + /** + * Returns the canonical name of the identifier. + * + * @return string + * The canonical name of the identifier. + */ + public function canonical(): string { + return $this->canonicalName; + } + + /** + * Returns the identifier in a format suitable for including in SQL queries. + * + * @return string + * The identifier in a format suitable for including in SQL queries. + */ + public function forMachine(): string { + return $this->quotedMachineName; + } + + /** + * Returns the identifier in a format suitable for including in SQL queries. + * + * @return string + * The identifier in a format suitable for including in SQL queries. + */ + public function __toString(): string { + return $this->forMachine(); + } + +} diff --git a/core/lib/Drupal/Core/Database/Identifier/IdentifierHandlerBase.php b/core/lib/Drupal/Core/Database/Identifier/IdentifierHandlerBase.php new file mode 100644 index 0000000000000000000000000000000000000000..f67092dcabdf100b4d40350dbd08bb9cfe074f22 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Identifier/IdentifierHandlerBase.php @@ -0,0 +1,367 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Database\Identifier; + +use Drupal\Component\Assertion\Inspector; +use Drupal\Core\Database\Exception\IdentifierException; + +/** + * Base class to handle identifier value objects. + * + * Database drivers should extend this class to implement db-specific + * limitations and behaviors. + */ +abstract class IdentifierHandlerBase { + + /** + * A cache of all identifiers handled. + */ + protected array $identifiers; + + /** + * Constructor. + * + * @param string $tablePrefix + * The table prefix to be used by the database connection. + * @param array{0:string, 1:string} $identifierQuotes + * The identifier quote characters for the database type. An array + * containing the start and end identifier quote characters for the + * database type. The ANSI SQL standard identifier quote character is a + * double quotation mark. + */ + public function __construct( + public readonly string $tablePrefix, + public readonly array $identifierQuotes = ['"', '"'], + ) { + assert(count($this->identifierQuotes) === 2 && Inspector::assertAllStrings($this->identifierQuotes), __CLASS__ . '::$identifierQuotes must contain 2 string values'); + } + + /** + * Returns a database identifier value object. + * + * @param string|\Drupal\Core\Database\Identifier\Database $identifier + * A database name as a string, or a Database value object. + * + * @return \Drupal\Core\Database\Identifier\Database + * A database identifier value object. + */ + public function database(string|Database $identifier): Database { + return $this->getIdentifierValueObject(IdentifierType::Database, $identifier); + } + + /** + * Returns a schema identifier value object. + * + * @param string|\Drupal\Core\Database\Identifier\Schema $identifier + * A schema name as a string, or a Schema value object. + * + * @return \Drupal\Core\Database\Identifier\Schema + * A schema identifier value object. + */ + public function schema(string|Schema $identifier): Schema { + return $this->getIdentifierValueObject(IdentifierType::Schema, $identifier); + } + + /** + * Returns a table identifier value object. + * + * @param string|\Drupal\Core\Database\Identifier\Table $identifier + * A table name as a string, or a Table value object. + * + * @return \Drupal\Core\Database\Identifier\Table + * A table identifier value object. + */ + public function table(string|Table $identifier): Table { + return $this->getIdentifierValueObject(IdentifierType::Table, $identifier); + } + + /** + * Returns the value object of an identifier. + * + * @param \Drupal\Core\Database\Identifier\IdentifierType $type + * The type of identifier. + * @param string|\Drupal\Core\Database\Identifier\IdentifierBase $identifier + * An identifier as a string, or an identifier value object. + * + * @return \Drupal\Core\Database\Identifier\IdentifierBase + * An identifier value object. + */ + protected function getIdentifierValueObject(IdentifierType $type, string|IdentifierBase $identifier): IdentifierBase { + $valueObjectClass = IdentifierType::valueObjectClass($type); + if ($identifier instanceof $valueObjectClass) { + $identifier = $identifier->identifier; + } + if ($this->isCached($identifier, $type)) { + $valueObject = $this->fromCache($identifier, $type); + } + else { + $valueObject = new $valueObjectClass($this, $identifier); + $this->toCache($identifier, $type, $valueObject); + } + return $valueObject; + } + + /** + * Adds an identifier value object to the local cache. + * + * @param string $identifier + * The raw identifier string. + * @param \Drupal\Core\Database\Identifier\IdentifierType $type + * The type of identifier. + * @param \Drupal\Core\Database\Identifier\IdentifierBase $identifierValueObject + * The identifier value object. + */ + protected function toCache(string $identifier, IdentifierType $type, IdentifierBase $identifierValueObject): void { + $this->identifiers['identifier'][$identifier][$type->value] = $identifierValueObject; + } + + /** + * Checks if an identifier value object is present in the local cache. + * + * @param string $identifier + * The raw identifier string. + * @param \Drupal\Core\Database\Identifier\IdentifierType $type + * The type of identifier. + * + * @return bool + * TRUE if the identifier value object is available in tha local cache, + * FALSE otherwise. + */ + protected function isCached(string $identifier, IdentifierType $type): bool { + return isset($this->identifiers['identifier'][$identifier][$type->value]); + } + + /** + * Gets an identifier value object from the local cache. + * + * @param string $identifier + * The raw identifier string. + * @param \Drupal\Core\Database\Identifier\IdentifierType $type + * The type of identifier. + * + * @return \Drupal\Core\Database\Identifier\IdentifierBase + * The identifier value object. + */ + protected function fromCache(string $identifier, IdentifierType $type): IdentifierBase { + return $this->identifiers['identifier'][$identifier][$type->value]; + } + + /** + * Returns the maximum length, in bytes, of an identifier type. + * + * @return positive-int + * The maximum length, in bytes, of an identifier type. + */ + abstract public function getMaxLength(IdentifierType $type): int; + + /** + * Returns a string with initial and final quote characters. + * + * @param string $value + * The input string. + * + * @return string + * The quoted string. + */ + public function quote(string $value): string { + return $this->identifierQuotes[0] . $value . $this->identifierQuotes[1]; + } + + /** + * Returns a canonicalized and validated identifier string. + * + * Standard SQL identifiers designate basic Latin letters, digits 0-9, + * dollar and underscore as valid characters. Drupal is stricter in the + * sense that the dollar character is not allowed. + * + * @param string $identifier + * A raw identifier string. Can include quote characters and any character + * in general. + * @param \Drupal\Core\Database\Identifier\IdentifierType $type + * The type of identifier. + * + * @return string + * A canonicalized and validated identifier string. + * + * @throws \Drupal\Core\Database\Exception\IdentifierException + * If the identifier is invalid. + */ + public function canonicalize(string $identifier, IdentifierType $type): string { + $canonicalName = preg_replace('/[^A-Za-z0-9_]+/', '', $identifier); + $this->validateCanonicalName($identifier, $canonicalName, $type); + return $canonicalName; + } + + /** + * Validates a canonicalized identifier string. + * + * @param string $identifier + * A raw identifier string. Can include quote characters and any character + * in general. + * @param string $canonicalName + * A canonical identifier string. + * @param \Drupal\Core\Database\Identifier\IdentifierType $type + * The type of identifier. + * + * @return true + * Upon successful validation. + * + * @throws \Drupal\Core\Database\Exception\IdentifierException + * If the identifier is invalid. + */ + protected function validateCanonicalName(string $identifier, string $canonicalName, IdentifierType $type): TRUE { + $canonicalNameLength = strlen($canonicalName); + if ($canonicalNameLength > $this->getMaxLength($type) || $canonicalNameLength === 0) { + throw new IdentifierException(sprintf( + 'The length of the %s identifier \'%s\' once canonicalized to \'%s\' is invalid (maximum allowed: %d)', + $type->value, + $identifier, + $canonicalName, + $this->getMaxLength($type), + )); + } + return TRUE; + } + + /** + * Shortens an identifier's canonical name by adding an hash. + * + * This method calculates an hash of the canonical name and then returns a + * string suitable for machine use. The hash is inserted in the middle of + * the remaining part of the canonical name once a prefix has been added. + * + * @param string $canonicalName + * A canonical identifier string. + * @param array<string,mixed> $info + * An associative array of context information. + * @param \Drupal\Core\Database\Identifier\IdentifierType $type + * The type of identifier. + * @param string $prefix + * A prefix that cannot be part of the shortening. + * @param positive-int $length + * The maximum length of the returned string. + * @param positive-int $hashLength + * The length of the hashed part in the returned string. + * + * @return string + * The shortened string. + * + * @throws \Drupal\Core\Database\Exception\IdentifierException + * If a shortened string could not be calculated. + */ + protected function cropByHashing(string $canonicalName, array $info, IdentifierType $type, string $prefix, int $length, int $hashLength): string { + $allowedLength = $length - strlen($prefix) - $hashLength; + if ($allowedLength < 4) { + throw new IdentifierException(sprintf( + '%s canonical identifier \'%s\' cannot be converted into a machine identifier%s', + ucfirst($type->value), + $canonicalName, + $prefix !== '' ? "; prefix '{$prefix}'" : '', + )); + } + $hash = substr(hash('sha256', $canonicalName), 0, $hashLength); + $lSize = (int) ($allowedLength / 2); + $rSize = $allowedLength - $lSize; + return $prefix . substr($canonicalName, 0, $lSize) . $hash . substr($canonicalName, -$rSize); + } + + /** + * Returns the machine accepted string for an identifier. + * + * This method converts a canonical identifier in the machine readable + * version. It could shorten the canonical name or perform other + * transformation as necessary. The returned value is stored in the + * identifier's $machineName property. + * + * @param string $canonicalName + * A canonical identifier string. + * @param array<string,mixed> $info + * An associative array of context information. + * @param \Drupal\Core\Database\Identifier\IdentifierType $type + * The type of identifier. + * + * @return string + * The machine accepted string for an identifier. + * + * @throws \Drupal\Core\Database\Exception\IdentifierException + * If a machine string could not be determined. + */ + public function resolveForMachine(string $canonicalName, array $info, IdentifierType $type): string { + return match ($type) { + IdentifierType::Table => $this->resolveTableForMachine($canonicalName, $info), + default => $canonicalName, + }; + } + + /** + * Returns the machine accepted string for a table. + * + * @param string $canonicalName + * A canonical table name string. + * @param array<string,mixed> $info + * An associative array of context information. + * + * @return string + * The machine accepted string for a table. + * + * @throws \Drupal\Core\Database\Exception\IdentifierException + * If a machine string could not be determined. + */ + protected function resolveTableForMachine(string $canonicalName, array $info): string { + if (strlen($info['needs_prefix'] ? $this->tablePrefix : '' . $canonicalName) > $this->getMaxLength(IdentifierType::Table)) { + throw new IdentifierException(sprintf( + 'The machine length of the %s canonicalized identifier \'%s\' once table prefix \'%s\' is added is invalid (maximum allowed: %d)', + IdentifierType::Table->value, + $canonicalName, + $this->tablePrefix, + $this->getMaxLength(IdentifierType::Table), + )); + } + return $info['needs_prefix'] ? $this->tablePrefix . $canonicalName : $canonicalName; + } + + /** + * Parses a raw table identifier string into its components. + * + * A raw table identifier may include database and/or schema information in + * the format [database.][schema.]table and may include quote characters. + * This method returns the parts that can be used to get a Table identifier + * value object. + * + * @param string $identifier + * A raw table identifier string. + * + * @return array{database: \Drupal\Core\Database\Identifier\Database|null,schema: \Drupal\Core\Database\Identifier\Schema|null, table: string, needs_prefix: bool} + * The parts that can be used to get a Table identifier value object. + * + * @throws \Drupal\Core\Database\Exception\IdentifierException + * If an error occurred. + */ + public function parseTableIdentifier(string $identifier): array { + $parts = explode(".", $identifier); + [$database, $schema, $table] = match (count($parts)) { + 1 => [NULL, NULL, $parts[0]], + 2 => [NULL, $this->schema($parts[0]), $parts[1]], + 3 => [$this->database($parts[0]), $this->schema($parts[1]), $parts[2]], + default => throw new IdentifierException(sprintf( + 'The table identifier \'%s\' does not comply with the syntax [database.][schema.]table', + $identifier, + )), + }; + if ($this->tablePrefix !== '') { + $needsPrefix = count($parts) === 1; + } + else { + $needsPrefix = FALSE; + } + return [ + 'database' => $database, + 'schema' => $schema, + 'table' => $table, + 'needs_prefix' => $needsPrefix, + ]; + } + +} diff --git a/core/lib/Drupal/Core/Database/Identifier/IdentifierType.php b/core/lib/Drupal/Core/Database/Identifier/IdentifierType.php new file mode 100644 index 0000000000000000000000000000000000000000..67515d8e179dca17c6c91c26bf7d7369a936eb49 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Identifier/IdentifierType.php @@ -0,0 +1,39 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Database\Identifier; + +/** + * Enumeration of database identifier types. + */ +enum IdentifierType: string { + case Database = 'database'; + case Schema = 'schema'; + case Sequence = 'sequence'; + case Table = 'table'; + case Column = 'column'; + case Index = 'index'; + + case Alias = 'alias'; + + case Unknown = 'unknown'; + + /** + * Returns the value object class for an identifier type. + * + * @param Drupal\Core\Database\Identifier\IdentifierType $case + * The identifier type. + * + * @return class-string<\Drupal\Core\Database\Identifier\IdentifierBase> + * The class of the identifier type value object. + */ + public static function valueObjectClass(self $case): string { + return match ($case) { + IdentifierType::Database => Database::class, + IdentifierType::Schema => Schema::class, + IdentifierType::Table => Table::class, + }; + } + +} diff --git a/core/lib/Drupal/Core/Database/Identifier/Schema.php b/core/lib/Drupal/Core/Database/Identifier/Schema.php new file mode 100644 index 0000000000000000000000000000000000000000..e9df7f0d56e4c70b2901749d67ab29cec5046c26 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Identifier/Schema.php @@ -0,0 +1,24 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Database\Identifier; + +/** + * Handles a schema identifier. + * + * When using full notation, a table can be identified as + * [database.][schema.]table. + */ +final class Schema extends IdentifierBase { + + public function __construct( + IdentifierHandlerBase $identifierHandler, + string $identifier, + ) { + $canonicalName = $identifierHandler->canonicalize($identifier, IdentifierType::Schema); + $machineName = $identifierHandler->resolveForMachine($canonicalName, [], IdentifierType::Schema); + parent::__construct($identifier, $canonicalName, $machineName, $identifierHandler->quote($machineName)); + } + +} diff --git a/core/lib/Drupal/Core/Database/Identifier/Table.php b/core/lib/Drupal/Core/Database/Identifier/Table.php new file mode 100644 index 0000000000000000000000000000000000000000..45a7386291e3b9d73e8526206b3ef8ef9214d6bd --- /dev/null +++ b/core/lib/Drupal/Core/Database/Identifier/Table.php @@ -0,0 +1,65 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Database\Identifier; + +/** + * Handles a table identifier. + * + * When using full notation, a table can be identified as + * [database.][schema.]table. + */ +final class Table extends IdentifierBase { + + /** + * The database identifier, if specified. + */ + public readonly ?Database $database; + + /** + * The schema identifier, if specified. + */ + public readonly ?Schema $schema; + + /** + * Whether the table requires to be prefixed. + */ + public readonly bool $needsPrefix; + + public function __construct( + IdentifierHandlerBase $identifierHandler, + string $identifier, + ) { + $parts = $identifierHandler->parseTableIdentifier($identifier); + + $canonicalName = $identifierHandler->canonicalize($parts['table'], IdentifierType::Table); + $machineName = $identifierHandler->resolveForMachine($canonicalName, $parts, IdentifierType::Table); + parent::__construct($identifier, $canonicalName, $machineName, $identifierHandler->quote($machineName)); + + $this->database = $parts['database']; + $this->schema = $parts['schema']; + $this->needsPrefix = $parts['needs_prefix']; + } + + /** + * {@inheritdoc} + */ + public function canonical(): string { + $ret = isset($this->database) ? $this->database->canonicalName . '.' : ''; + $ret .= isset($this->schema) ? $this->schema->canonicalName . '.' : ''; + $ret .= $this->canonicalName; + return $ret; + } + + /** + * {@inheritdoc} + */ + public function forMachine(): string { + $ret = isset($this->database) ? $this->database->quotedMachineName . '.' : ''; + $ret .= isset($this->schema) ? $this->schema->quotedMachineName . '.' : ''; + $ret .= $this->quotedMachineName; + return $ret; + } + +} diff --git a/core/lib/Drupal/Core/Database/Query/Delete.php b/core/lib/Drupal/Core/Database/Query/Delete.php index 946f91240e1f4b3a2cdf099214dc9ce0cce3c3fc..da1e4748adf1bc26566ca6334b20e38134ba2ccc 100644 --- a/core/lib/Drupal/Core/Database/Query/Delete.php +++ b/core/lib/Drupal/Core/Database/Query/Delete.php @@ -70,7 +70,7 @@ public function __toString() { // Create a sanitized comment string to prepend to the query. $comments = $this->connection->makeComment($this->comments); - $query = $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '} '; + $query = $comments . 'DELETE FROM ' . $this->connection->identifiers->table($this->table)->forMachine(); if (count($this->condition)) { diff --git a/core/lib/Drupal/Core/Database/Query/Insert.php b/core/lib/Drupal/Core/Database/Query/Insert.php index 4657e5c3e5e82330a1401c8f6b7bbbaf1b45229d..60bccaea343c6a90a96d2d3b2873907204053eb8 100644 --- a/core/lib/Drupal/Core/Database/Query/Insert.php +++ b/core/lib/Drupal/Core/Database/Query/Insert.php @@ -116,7 +116,7 @@ public function __toString() { $insert_fields = array_merge($this->defaultFields, $this->insertFields); if (!empty($this->fromQuery)) { - return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') ' . $this->fromQuery; + return $comments . 'INSERT INTO ' . $this->connection->identifiers->table($this->table)->forMachine() . ' (' . implode(', ', $insert_fields) . ') ' . $this->fromQuery; } // For simplicity, we will use the $placeholders array to inject @@ -126,7 +126,7 @@ public function __toString() { $placeholders = array_pad($placeholders, count($this->defaultFields), 'default'); $placeholders = array_pad($placeholders, count($this->insertFields), '?'); - return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES (' . implode(', ', $placeholders) . ')'; + return $comments . 'INSERT INTO ' . $this->connection->identifiers->table($this->table)->forMachine() . ' (' . implode(', ', $insert_fields) . ') VALUES (' . implode(', ', $placeholders) . ')'; } /** diff --git a/core/lib/Drupal/Core/Database/Query/Select.php b/core/lib/Drupal/Core/Database/Query/Select.php index d284ea285a5b0fc749041b977dd635b0febfd6f4..2e3f0f49d5fd4de069909e815d1009cf95d6eb75 100644 --- a/core/lib/Drupal/Core/Database/Query/Select.php +++ b/core/lib/Drupal/Core/Database/Query/Select.php @@ -854,11 +854,7 @@ public function __toString() { $table_string = '(' . (string) $subquery . ')'; } else { - $table_string = $this->connection->escapeTable($table['table']); - // Do not attempt prefixing cross database / schema queries. - if (!str_contains($table_string, '.')) { - $table_string = '{' . $table_string . '}'; - } + $table_string = $this->connection->identifiers->table($table['table'])->forMachine(); } // Don't use the AS keyword for table aliases, as some diff --git a/core/lib/Drupal/Core/Database/Query/Truncate.php b/core/lib/Drupal/Core/Database/Query/Truncate.php index 33676e7534afdfd7a2c1b0fda03d87cbce3b7699..044340cc33d8da37a3fff6cb295dc53500daf98d 100644 --- a/core/lib/Drupal/Core/Database/Query/Truncate.php +++ b/core/lib/Drupal/Core/Database/Query/Truncate.php @@ -75,10 +75,10 @@ public function __toString() { // The statement actually built depends on whether a transaction is active. // @see ::execute() if ($this->connection->inTransaction()) { - return $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '}'; + return $comments . 'DELETE FROM ' . $this->connection->identifiers->table($this->table)->forMachine(); } else { - return $comments . 'TRUNCATE {' . $this->connection->escapeTable($this->table) . '} '; + return $comments . 'TRUNCATE ' . $this->connection->identifiers->table($this->table)->forMachine(); } } diff --git a/core/lib/Drupal/Core/Database/Query/Update.php b/core/lib/Drupal/Core/Database/Query/Update.php index 651bb674e755a23773a5799e251a17d813f427eb..82e33988b70231c1e0d04c67f4a087bb01267289 100644 --- a/core/lib/Drupal/Core/Database/Query/Update.php +++ b/core/lib/Drupal/Core/Database/Query/Update.php @@ -166,7 +166,7 @@ public function __toString() { $update_fields[] = $this->connection->escapeField($field) . '=' . $placeholders[$max_placeholder++]; } - $query = $comments . 'UPDATE {' . $this->connection->escapeTable($this->table) . '} SET ' . implode(', ', $update_fields); + $query = $comments . 'UPDATE ' . $this->connection->identifiers->table($this->table)->forMachine() . ' SET ' . implode(', ', $update_fields); if (count($this->condition)) { $this->condition->compile($this->connection, $this); diff --git a/core/lib/Drupal/Core/Database/Schema.php b/core/lib/Drupal/Core/Database/Schema.php index 97e73b1bf8cbd1fd8aa6640c9c9a56c751919bdb..9073ca3fbaed76f38727abb3f16780032258223a 100644 --- a/core/lib/Drupal/Core/Database/Schema.php +++ b/core/lib/Drupal/Core/Database/Schema.php @@ -82,9 +82,10 @@ public function nextPlaceholder() { * A keyed array with information about the schema, table name and prefix. */ protected function getPrefixInfo($table = 'default', $add_prefix = TRUE) { + @trigger_error(__METHOD__ . "() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use IdentifierHandler methods instead. See https://www.drupal.org/node/7654132", E_USER_DEPRECATED); $info = [ 'schema' => $this->defaultSchema, - 'prefix' => $this->connection->getPrefix(), + 'prefix' => isset($this->connection->identifiers) ? $this->connection->identifiers->tablePrefix : '', ]; if ($add_prefix) { $table = $info['prefix'] . $table; @@ -111,6 +112,7 @@ protected function getPrefixInfo($table = 'default', $add_prefix = TRUE) { * This prevents using {} around non-table names like indexes and keys. */ public function prefixNonTable($table) { + @trigger_error(__METHOD__ . "() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use IdentifierHandler methods instead. See https://www.drupal.org/node/7654132", E_USER_DEPRECATED); $args = func_get_args(); $info = $this->getPrefixInfo($table); $args[0] = $info['table']; @@ -223,7 +225,7 @@ public function findTables($table_expression) { $condition = $this->buildTableNameCondition('%', 'LIKE'); $condition->compile($this->connection, $this); - $prefix = $this->connection->getPrefix(); + $prefix = $this->connection->identifiers->tablePrefix; $prefix_length = strlen($prefix); $tables = []; // Normally, we would heartily discourage the use of string diff --git a/core/lib/Drupal/Core/KeyValueStore/DatabaseStorage.php b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorage.php index ea5247bc750b603a64a9b198727442c02c36e12a..97c84d8f704e1b2395f97c65eaf3f84a3e9e9d22 100644 --- a/core/lib/Drupal/Core/KeyValueStore/DatabaseStorage.php +++ b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorage.php @@ -63,7 +63,7 @@ public function __construct($collection, SerializationInterface $serializer, Con */ public function has($key) { try { - return (bool) $this->connection->query('SELECT 1 FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [collection] = :collection AND [name] = :key', [ + return (bool) $this->connection->query('SELECT 1 FROM {' . $this->table . '} WHERE [collection] = :collection AND [name] = :key', [ ':collection' => $this->collection, ':key' => $key, ])->fetchField(); @@ -81,7 +81,7 @@ public function getMultiple(array $keys) { $values = []; try { $result = $this->connection - ->query('SELECT [name], [value] FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [name] IN ( :keys[] ) AND [collection] = :collection', [ + ->query('SELECT [name], [value] FROM {' . $this->table . '} WHERE [name] IN ( :keys[] ) AND [collection] = :collection', [ ':keys[]' => $keys, ':collection' => $this->collection, ]) @@ -105,7 +105,7 @@ public function getMultiple(array $keys) { */ public function getAll() { try { - $result = $this->connection->query('SELECT [name], [value] FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [collection] = :collection', [':collection' => $this->collection]); + $result = $this->connection->query('SELECT [name], [value] FROM {' . $this->table . '} WHERE [collection] = :collection', [':collection' => $this->collection]); } catch (\Exception $e) { $this->catchException($e); diff --git a/core/lib/Drupal/Core/KeyValueStore/DatabaseStorageExpirable.php b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorageExpirable.php index 8813d9e2abe60b5780e794931cec780089fe33d8..aa0c2b3ccc6c41aaca14ad8bfccd193bfb8439a0 100644 --- a/core/lib/Drupal/Core/KeyValueStore/DatabaseStorageExpirable.php +++ b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorageExpirable.php @@ -43,7 +43,7 @@ public function __construct( */ public function has($key) { try { - return (bool) $this->connection->query('SELECT 1 FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [collection] = :collection AND [name] = :key AND [expire] > :now', [ + return (bool) $this->connection->query('SELECT 1 FROM {' . $this->table . '} WHERE [collection] = :collection AND [name] = :key AND [expire] > :now', [ ':collection' => $this->collection, ':key' => $key, ':now' => $this->time->getRequestTime(), @@ -61,7 +61,7 @@ public function has($key) { public function getMultiple(array $keys) { try { $values = $this->connection->query( - 'SELECT [name], [value] FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [expire] > :now AND [name] IN ( :keys[] ) AND [collection] = :collection', + 'SELECT [name], [value] FROM {' . $this->table . '} WHERE [expire] > :now AND [name] IN ( :keys[] ) AND [collection] = :collection', [ ':now' => $this->time->getRequestTime(), ':keys[]' => $keys, @@ -85,7 +85,7 @@ public function getMultiple(array $keys) { public function getAll() { try { $values = $this->connection->query( - 'SELECT [name], [value] FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [collection] = :collection AND [expire] > :now', + 'SELECT [name], [value] FROM {' . $this->table . '} WHERE [collection] = :collection AND [expire] > :now', [ ':collection' => $this->collection, ':now' => $this->time->getRequestTime(), diff --git a/core/lib/Drupal/Core/Routing/RouteProvider.php b/core/lib/Drupal/Core/Routing/RouteProvider.php index ab06f6ea274de085ab4b5e386bb4bfaaed5d3bdd..aeb047c79c704bfac8ff557c187215b4b49cbdff 100644 --- a/core/lib/Drupal/Core/Routing/RouteProvider.php +++ b/core/lib/Drupal/Core/Routing/RouteProvider.php @@ -243,7 +243,7 @@ public function preLoadRoutes($names) { } else { try { - $result = $this->connection->query('SELECT [name], [route] FROM {' . $this->connection->escapeTable($this->tableName) . '} WHERE [name] IN ( :names[] )', [':names[]' => $routes_to_load]); + $result = $this->connection->query('SELECT [name], [route] FROM {' . $this->tableName . '} WHERE [name] IN ( :names[] )', [':names[]' => $routes_to_load]); $routes = $result->fetchAllKeyed(); $this->cache->set($cid, $routes, Cache::PERMANENT, ['routes']); @@ -380,7 +380,7 @@ protected function getRoutesByPath($path) { // trailing wildcard parts as long as the pattern matches, since we // dump the route pattern without those optional parts. try { - $routes = $this->connection->query("SELECT [name], [route], [fit] FROM {" . $this->connection->escapeTable($this->tableName) . "} WHERE [pattern_outline] IN ( :patterns[] ) AND [number_parts] >= :count_parts", [ + $routes = $this->connection->query("SELECT [name], [route], [fit] FROM {" . $this->tableName . "} WHERE [pattern_outline] IN ( :patterns[] ) AND [number_parts] >= :count_parts", [ ':patterns[]' => $ancestors, ':count_parts' => count($parts), ]) diff --git a/core/modules/language/tests/src/Kernel/Migrate/d7/MigrateLanguageContentCommentSettingsTest.php b/core/modules/language/tests/src/Kernel/Migrate/d7/MigrateLanguageContentCommentSettingsTest.php index 16923949a2e12dd6d09bdf27f4d04eafa0239a61..2a2ab96cd6825220b575fe11c7d9a7169fa3f820 100644 --- a/core/modules/language/tests/src/Kernel/Migrate/d7/MigrateLanguageContentCommentSettingsTest.php +++ b/core/modules/language/tests/src/Kernel/Migrate/d7/MigrateLanguageContentCommentSettingsTest.php @@ -43,7 +43,7 @@ protected function setUp(): void { */ public function testLanguageCommentSettings(): void { // Confirm there is no message about a missing bundle. - $this->assertEmpty($this->migrateMessages, $this->migrateMessages['error'][0] ?? ''); + $this->assertEmpty($this->migrateMessages, isset($this->migrateMessages['error'][0]) ? (string) $this->migrateMessages['error'][0] : ''); // Article and Blog content type have multilingual settings of 'Enabled, // with Translation'. Assert that comments are translatable and the default diff --git a/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php b/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php index faec03c72cc45d585b3bf603c4ffd4ac1580a09a..53b598bb14db68863b9db5848372bb43885b5d1c 100644 --- a/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php +++ b/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php @@ -183,7 +183,7 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition // Default generated table names, limited to 63 characters. $machine_name = str_replace(':', '__', $this->migration->id()); - $prefix_length = strlen($this->database->getPrefix()); + $prefix_length = isset($this->database->identifiers) ? strlen($this->database->identifiers->tablePrefix) : 0; $this->mapTableName = 'migrate_map_' . mb_strtolower($machine_name); $this->mapTableName = mb_substr($this->mapTableName, 0, 63 - $prefix_length); $this->messageTableName = 'migrate_message_' . mb_strtolower($machine_name); diff --git a/core/modules/migrate/tests/src/Kernel/MigrateTestBase.php b/core/modules/migrate/tests/src/Kernel/MigrateTestBase.php index 4f4089b3ac370a253174ab7094c20d272ce574bb..6cc3c5634d330287c3a30b4e8279d0172e09e68a 100644 --- a/core/modules/migrate/tests/src/Kernel/MigrateTestBase.php +++ b/core/modules/migrate/tests/src/Kernel/MigrateTestBase.php @@ -203,7 +203,7 @@ public function display($message, $type = 'status') { $this->migrateMessages[$type][] = $message; } else { - $this->assertEquals('status', $type, $message); + $this->assertEquals('status', $type, (string) $message); } } diff --git a/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php b/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php index b162742a2e35ff486e2a01b6cd8a7ccab49090e7..25f6edff4571616baa089a486d46839c3cad0642 100644 --- a/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php +++ b/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php @@ -1027,7 +1027,7 @@ public function testGetQualifiedMapTablePrefix(): void { // The SQLite driver is a special flower. It will prefix tables with // PREFIX.TABLE, instead of the standard PREFIXTABLE. // @see \Drupal\sqlite\Driver\Database\sqlite\Connection::__construct() - $this->assertEquals('prefix.migrate_map_sql_idmap_test', $qualified_map_table); + $this->assertEquals('"prefix"."migrate_map_sql_idmap_test"', $qualified_map_table); } /** diff --git a/core/modules/mysql/src/Driver/Database/mysql/Connection.php b/core/modules/mysql/src/Driver/Database/mysql/Connection.php index bc4a360d4ae1d07450aa70df97ffdd85bf10409d..8748db1203ef39fffa5da3d80ec87661ba2bc2a0 100644 --- a/core/modules/mysql/src/Driver/Database/mysql/Connection.php +++ b/core/modules/mysql/src/Driver/Database/mysql/Connection.php @@ -60,11 +60,6 @@ class Connection extends DatabaseConnection implements SupportsTemporaryTablesIn */ const MIN_MAX_ALLOWED_PACKET = 1024; - /** - * {@inheritdoc} - */ - protected $identifierQuotes = ['"', '"']; - /** * {@inheritdoc} */ @@ -88,10 +83,15 @@ public function __construct(\PDO $connection, array $connection_options) { } } - if ($this->identifierQuotes === ['"', '"'] && !$is_ansi_quotes_mode) { - $this->identifierQuotes = ['`', '`']; - } - parent::__construct($connection, $connection_options); + // Manage the table prefix. + $connection_options['prefix'] = $connection_options['prefix'] ?? ''; + assert(is_string($connection_options['prefix']), 'The \'prefix\' connection option to ' . __METHOD__ . '() must be a string.'); + + parent::__construct( + $connection, + $connection_options, + new IdentifierHandler($connection_options['prefix'], $is_ansi_quotes_mode ? ['"', '"'] : ['`', '`']), + ); } /** @@ -295,16 +295,11 @@ public function databaseType() { } /** - * Overrides \Drupal\Core\Database\Connection::createDatabase(). - * - * @param string $database - * The name of the database to create. - * - * @throws \Drupal\Core\Database\DatabaseNotFoundException + * {@inheritdoc} */ public function createDatabase($database) { // Escape the database name. - $database = Database::getConnection()->escapeDatabase($database); + $database = Database::getConnection()->identifiers->schema($database)->forMachine(); try { // Create the database and set it as active. diff --git a/core/modules/mysql/src/Driver/Database/mysql/IdentifierHandler.php b/core/modules/mysql/src/Driver/Database/mysql/IdentifierHandler.php new file mode 100644 index 0000000000000000000000000000000000000000..6bd320862c1122cdd5d85ab48dc855d761c36903 --- /dev/null +++ b/core/modules/mysql/src/Driver/Database/mysql/IdentifierHandler.php @@ -0,0 +1,76 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\mysql\Driver\Database\mysql; + +use Drupal\Core\Database\Exception\IdentifierException; +use Drupal\Core\Database\Identifier\IdentifierHandlerBase; +use Drupal\Core\Database\Identifier\IdentifierType; + +/** + * MySQL implementation of the identifier handler. + */ +class IdentifierHandler extends IdentifierHandlerBase { + + /** + * {@inheritdoc} + */ + public function getMaxLength(IdentifierType $type): int { + // @see https://dev.mysql.com/doc/refman/8.4/en/identifier-length.html + return match ($type) { + IdentifierType::Alias => 256, + default => 64, + }; + } + + /** + * {@inheritdoc} + */ + protected function validateCanonicalName(string $identifier, string $canonicalName, IdentifierType $type): true { + return match ($type) { + IdentifierType::Table => TRUE, + default => parent::validateCanonicalName($identifier, $canonicalName, $type), + }; + } + + /** + * {@inheritdoc} + */ + public function parseTableIdentifier(string $identifier): array { + $parts = parent::parseTableIdentifier($identifier); + if ($parts['database']) { + throw new IdentifierException(sprintf( + 'MySql does not support the syntax [database.][schema.]table for the table identifier \'%s\'. Avoid specifying the \'database\' part', + $identifier, + )); + } + return $parts; + } + + /** + * {@inheritdoc} + */ + protected function resolveTableForMachine(string $canonicalName, array $info): string { + if ($info['schema']) { + // We are processing a fully qualified table identifier, canonical name + // should not be processed, just checked it does not exceed max length. + if (strlen($canonicalName) > $this->getMaxLength(IdentifierType::Table)) { + throw new IdentifierException(sprintf( + 'Table identifier \'%s\' exceeds maximum allowed length (%d)', + $canonicalName, + $this->getMaxLength(IdentifierType::Table), + )); + } + return $canonicalName; + } + + $prefix = $info['needs_prefix'] ? $this->tablePrefix : ''; + if (strlen($prefix . $canonicalName) > $this->getMaxLength(IdentifierType::Table)) { + // We shorten too long table names. + return $this->cropByHashing($canonicalName, $info, IdentifierType::Table, $prefix, $this->getMaxLength(IdentifierType::Table), 10); + } + return $prefix . $canonicalName; + } + +} diff --git a/core/modules/mysql/src/Driver/Database/mysql/Schema.php b/core/modules/mysql/src/Driver/Database/mysql/Schema.php index 85e71af18d1214521a0e91f4a2be32a3bcba9622..2cc75a078ab9074e82e2592a9446928e04ad32e4 100644 --- a/core/modules/mysql/src/Driver/Database/mysql/Schema.php +++ b/core/modules/mysql/src/Driver/Database/mysql/Schema.php @@ -51,7 +51,7 @@ class Schema extends DatabaseSchema { * A keyed array with information about the database, table name and prefix. */ protected function getPrefixInfo($table = 'default', $add_prefix = TRUE) { - $info = ['prefix' => $this->connection->getPrefix()]; + $info = ['prefix' => $this->connection->identifiers->tablePrefix]; if ($add_prefix) { $table = $info['prefix'] . $table; } diff --git a/core/modules/mysql/tests/src/Kernel/mysql/Console/DbDumpCommandTest.php b/core/modules/mysql/tests/src/Kernel/mysql/Console/DbDumpCommandTest.php index 66d781b5eb953ed8a224a60ea838f6b3145c9685..d6ab61ec886c6f1343b8f7d5808f2e3187e84f77 100644 --- a/core/modules/mysql/tests/src/Kernel/mysql/Console/DbDumpCommandTest.php +++ b/core/modules/mysql/tests/src/Kernel/mysql/Console/DbDumpCommandTest.php @@ -35,7 +35,7 @@ protected function setUp(): void { // Create a table with a field type not defined in // \Drupal\Core\Database\Schema::getFieldTypeMap. - $table_name = $connection->getPrefix() . 'foo'; + $table_name = $connection->identifiers->tablePrefix . 'foo'; $sql = "create table if not exists `$table_name` (`test` datetime NOT NULL);"; $connection->query($sql)->execute(); } diff --git a/core/modules/mysql/tests/src/Kernel/mysql/MysqlDriverTest.php b/core/modules/mysql/tests/src/Kernel/mysql/MysqlDriverTest.php index 4fcb75b60aa018bd4272ee39a11d82fa9a1342dc..48905dc0e6d6c4228df9748448bec73db67b1754 100644 --- a/core/modules/mysql/tests/src/Kernel/mysql/MysqlDriverTest.php +++ b/core/modules/mysql/tests/src/Kernel/mysql/MysqlDriverTest.php @@ -19,7 +19,7 @@ class MysqlDriverTest extends DriverSpecificKernelTestBase { * @covers \Drupal\mysql\Driver\Database\mysql\Connection */ public function testConnection(): void { - $connection = new Connection($this->createMock(StubPDO::class), []); + $connection = new Connection($this->createMock(StubPDO::class), ['prefix' => '']); $this->assertInstanceOf(Connection::class, $connection); } diff --git a/core/modules/mysql/tests/src/Unit/ConnectionTest.php b/core/modules/mysql/tests/src/Unit/ConnectionTest.php index 8865c764e193b0b300c859e92cd6e838ec1f700f..a839c4c1f6e82f3ed55e83fccd7e0363da4839a4 100644 --- a/core/modules/mysql/tests/src/Unit/ConnectionTest.php +++ b/core/modules/mysql/tests/src/Unit/ConnectionTest.php @@ -68,8 +68,7 @@ private function createConnection(): Connection { return new class($pdo_connection) extends Connection { public function __construct(\PDO $connection) { - $this->connection = $connection; - $this->setPrefix(''); + parent::__construct($connection, ['prefix' => '']); } }; diff --git a/core/modules/mysql/tests/src/Unit/IdentifierTest.php b/core/modules/mysql/tests/src/Unit/IdentifierTest.php new file mode 100644 index 0000000000000000000000000000000000000000..7efff85160245bcdfc1ff36b327333aea3c96cd8 --- /dev/null +++ b/core/modules/mysql/tests/src/Unit/IdentifierTest.php @@ -0,0 +1,199 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\mysql\Unit; + +use Drupal\Core\Database\Exception\IdentifierException; +use Drupal\Core\Database\Identifier\IdentifierType; +use Drupal\mysql\Driver\Database\mysql\IdentifierHandler; +use Drupal\Tests\UnitTestCase; + +/** + * Tests MySQL database identifiers. + * + * @coversDefaultClass \Drupal\mysql\Driver\Database\mysql\IdentifierHandler + * @group Database + */ +class IdentifierTest extends UnitTestCase { + + // cSpell:disable + + /** + * Data provider for testTable. + * + * @return array + * An associative array of test case data. + */ + public static function providerTable(): array { + return [ + 'No prefix' => [ + 'identifier' => 'nocase', + 'prefix' => '', + 'expectedCanonical' => 'nocase', + 'expectedMachine' => '"nocase"', + ], + 'Prefix' => [ + 'identifier' => 'nocase', + 'prefix' => 'foobar', + 'expectedCanonical' => 'nocase', + 'expectedMachine' => '"foobarnocase"', + ], + 'No prefix, camelCase' => [ + 'identifier' => 'camelCase', + 'prefix' => '', + 'expectedCanonical' => 'camelCase', + 'expectedMachine' => '"camelCase"', + ], + 'Prefix, camelCase' => [ + 'identifier' => 'camelCase', + 'prefix' => 'foobar', + 'expectedCanonical' => 'camelCase', + 'expectedMachine' => '"foobarcamelCase"', + ], + 'No prefix, backtick' => [ + 'identifier' => '`backtick`', + 'prefix' => '', + 'expectedCanonical' => 'backtick', + 'expectedMachine' => '"backtick"', + ], + 'No prefix, brackets' => [ + 'identifier' => '[brackets]', + 'prefix' => '', + 'expectedCanonical' => 'brackets', + 'expectedMachine' => '"brackets"', + ], + 'No prefix, remove slash' => [ + 'identifier' => 'no/case', + 'prefix' => '', + 'expectedCanonical' => 'nocase', + 'expectedMachine' => '"nocase"', + ], + 'No prefix, remove quote' => [ + 'identifier' => 'no"case', + 'prefix' => '', + 'expectedCanonical' => 'nocase', + 'expectedMachine' => '"nocase"', + ], + 'No prefix, shortened machine name' => [ + 'identifier' => 'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'prefix' => '', + 'expectedCanonical' => 'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'expectedMachine' => '"VeryVeryVeryVeryHungryHungr25fa0c3753ngryHungryHungryCaterpillar"', + ], + 'Prefix, shortened machine name' => [ + 'identifier' => 'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'prefix' => 'foobar', + 'expectedCanonical' => 'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'expectedMachine' => '"foobarVeryVeryVeryVeryHungryHu25fa0c3753yHungryHungryCaterpillar"', + ], + 'Prefix one less than overflow, shortened machine name' => [ + 'identifier' => 'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'prefix' => 'prefix____123456789012345678901234567890123456789', + 'expectedCanonical' => 'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'expectedMachine' => '"prefix____123456789012345678901234567890123456789Ve25fa0c3753lar"', + ], + 'Prefix just right, shortened machine name' => [ + 'identifier' => 'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'prefix' => 'prefix____1234567890123456789012345678901234567890', + 'expectedCanonical' => 'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'expectedMachine' => '"prefix____1234567890123456789012345678901234567890Ve25fa0c3753ar"', + ], + 'Prefix one more than overflow, shortened machine name' => [ + 'identifier' => 'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'prefix' => 'prefix____12345678901234567890123456789012345678901', + 'expectedCanonical' => 'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'expectedMachine' => '', + 'expectedException' => 'Table canonical identifier \'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar\' cannot be converted into a machine identifier; prefix \'prefix____12345678901234567890123456789012345678901\'', + ], + 'Prefix too long to fit, table does not require shortening' => [ + 'identifier' => 'nocase', + 'prefix' => 'VeryVeryVeryVeryVeryVeryVeryVeryLongLongLongLongLongLongLongPrefix', + 'expectedCanonical' => 'nocase', + 'expectedMachine' => '', + 'expectedException' => 'Table canonical identifier \'nocase\' cannot be converted into a machine identifier; prefix \'VeryVeryVeryVeryVeryVeryVeryVeryLongLongLongLongLongLongLongPrefix\'', + ], + 'Prefix too long to fit, table requires shortening' => [ + 'identifier' => 'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'prefix' => 'VeryVeryVeryVeryVeryVeryVeryVeryLongLongLongLongLongLongLongPrefix', + 'expectedCanonical' => 'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'expectedMachine' => '', + 'expectedException' => 'Table canonical identifier \'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar\' cannot be converted into a machine identifier; prefix \'VeryVeryVeryVeryVeryVeryVeryVeryLongLongLongLongLongLongLongPrefix\'', + ], + // Sometimes, table names are following the pattern database.schema.table. + 'Fully qualified - too many parts: 4' => [ + 'identifier' => 'Chowra.Teressa.Bompuka.Katchal', + 'prefix' => '', + 'expectedCanonical' => '', + 'expectedMachine' => '', + 'expectedException' => 'The table identifier \'Chowra.Teressa.Bompuka.Katchal\' does not comply with the syntax [database.][schema.]table', + ], + 'Fully qualified - too many parts: MySql not supporting \'database\'' => [ + 'identifier' => 'Chowra.Teressa.Bompuka', + 'prefix' => '', + 'expectedCanonical' => '', + 'expectedMachine' => '', + 'expectedException' => 'MySql does not support the syntax [database.][schema.]table for the table identifier \'Chowra.Teressa.Bompuka\'. Avoid specifying the \'database\' part', + ], + 'Fully qualified - no prefix' => [ + 'identifier' => '"Nancowry"."Tillangchong"', + 'prefix' => '', + 'expectedCanonical' => 'Nancowry.Tillangchong', + 'expectedMachine' => '"Nancowry"."Tillangchong"', + ], + 'Fully qualified - prefix' => [ + 'identifier' => '"Nancowry"."Tillangchong"', + 'prefix' => 'foobar', + 'expectedCanonical' => 'Nancowry.Tillangchong', + 'expectedMachine' => '"Nancowry"."Tillangchong"', + ], + 'Fully qualified - prefix not duplicated' => [ + 'identifier' => 'Nancowry.foobarTillangchong', + 'prefix' => 'foobar', + 'expectedCanonical' => 'Nancowry.foobarTillangchong', + 'expectedMachine' => '"Nancowry"."foobarTillangchong"', + ], + 'Fully qualified - remove not canonical characters' => [ + 'identifier' => '!Nancowry?.$Tillangchong%%%', + 'prefix' => '', + 'expectedCanonical' => 'Nancowry.Tillangchong', + 'expectedMachine' => '"Nancowry"."Tillangchong"', + ], + 'Fully qualified - invalid schema name length' => [ + 'identifier' => 'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar.Trinket', + 'prefix' => '', + 'expectedCanonical' => '', + 'expectedMachine' => '', + 'expectedException' => 'The length of the schema identifier \'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar\' once canonicalized to \'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar\' is invalid (maximum allowed: 64)', + ], + 'Fully qualified - invalid table name length' => [ + 'identifier' => 'Camorta.VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'prefix' => '', + 'expectedCanonical' => 'Camorta.VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'expectedMachine' => '', + 'expectedException' => 'Table identifier \'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar\' exceeds maximum allowed length (64)', + ], + ]; + } + + // cSpell:enable + + /** + * Tests table identifiers. + * + * @dataProvider providerTable + */ + public function testTable(string $identifier, string $prefix = '', ?string $expectedCanonical = '', ?string $expectedMachine = '', ?string $expectedException = NULL): void { + $handler = new IdentifierHandler($prefix); + if ($expectedException) { + $this->expectException(IdentifierException::class); + $this->expectExceptionMessage($expectedException); + } + $this->assertSame($expectedCanonical, $handler->table($identifier)->canonical()); + $this->assertSame($expectedMachine, $handler->table($identifier)->forMachine()); + // The machine name includes the quote characters so we need to subtract + // those from the length. + $this->assertLessThanOrEqual($handler->getMaxLength(IdentifierType::Table), strlen($handler->table($identifier)->machineName), 'Invalid machine table length.'); + } + +} diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Connection.php b/core/modules/pgsql/src/Driver/Database/pgsql/Connection.php index 40b2de75cf051b28830fbe92559f39ba2a93349f..a9d5d4e1e155455fbf2fc00a9f2f0f35a113b1ea 100644 --- a/core/modules/pgsql/src/Driver/Database/pgsql/Connection.php +++ b/core/modules/pgsql/src/Driver/Database/pgsql/Connection.php @@ -68,11 +68,6 @@ class Connection extends DatabaseConnection implements SupportsTemporaryTablesIn */ protected $transactionalDDLSupport = TRUE; - /** - * {@inheritdoc} - */ - protected $identifierQuotes = ['"', '"']; - /** * An array of transaction savepoints. * @@ -92,17 +87,14 @@ class Connection extends DatabaseConnection implements SupportsTemporaryTablesIn * Constructs a connection object. */ public function __construct(\PDO $connection, array $connection_options) { - // Sanitize the schema name here, so we do not have to do it in other - // functions. - if (isset($connection_options['schema']) && ($connection_options['schema'] !== 'public')) { - $connection_options['schema'] = preg_replace('/[^A-Za-z0-9_]+/', '', $connection_options['schema']); - } + // Manage the table prefix. + $prefix = $connection_options['prefix'] ?? ''; + assert(is_string($prefix), 'The \'prefix\' connection option to ' . __METHOD__ . '() must be a string.'); - // We need to set the connectionOptions before the parent, because setPrefix - // needs this. - $this->connectionOptions = $connection_options; + // Manage the schema name. + $defaultSchema = $connection_options['schema'] ?? ''; - parent::__construct($connection, $connection_options); + parent::__construct($connection, $connection_options, new IdentifierHandler($prefix, $defaultSchema)); // Force PostgreSQL to use the UTF-8 character set by default. $this->connection->exec("SET NAMES 'UTF8'"); @@ -113,26 +105,6 @@ public function __construct(\PDO $connection, array $connection_options) { } } - /** - * {@inheritdoc} - */ - protected function setPrefix($prefix) { - assert(is_string($prefix), 'The \'$prefix\' argument to ' . __METHOD__ . '() must be a string'); - $this->prefix = $prefix; - - // Add the schema name if it is not set to public, otherwise it will use the - // default schema name. - $quoted_schema = ''; - if (isset($this->connectionOptions['schema']) && ($this->connectionOptions['schema'] !== 'public')) { - $quoted_schema = $this->identifierQuotes[0] . $this->connectionOptions['schema'] . $this->identifierQuotes[1] . '.'; - } - - $this->tablePlaceholderReplacements = [ - $quoted_schema . $this->identifierQuotes[0] . str_replace('.', $this->identifierQuotes[1] . '.' . $this->identifierQuotes[0], $prefix), - $this->identifierQuotes[1], - ]; - } - /** * {@inheritdoc} */ @@ -287,16 +259,11 @@ public function databaseType() { } /** - * Overrides \Drupal\Core\Database\Connection::createDatabase(). - * - * @param string $database - * The name of the database to create. - * - * @throws \Drupal\Core\Database\DatabaseNotFoundException + * {@inheritdoc} */ public function createDatabase($database) { // Escape the database name. - $database = Database::getConnection()->escapeDatabase($database); + $database = Database::getConnection()->identifiers->database($database)->forMachine(); $db_created = FALSE; // Try to determine the proper locales for character classification and @@ -354,19 +321,25 @@ public function makeSequenceName($table, $field) { $sequence_name = $this->prefixTables('{' . $table . '}_' . $field . '_seq'); // Remove identifier quotes as we are constructing a new name from a // prefixed and quoted table name. - return str_replace($this->identifierQuotes, '', $sequence_name); + return str_replace($this->identifiers->identifierQuotes, '', $sequence_name); } /** * {@inheritdoc} */ public function getFullQualifiedTableName($table) { + $tableIdentifier = $this->identifiers->table($table); + // If already fully qualified, just pass it on. + if ($tableIdentifier->database || $tableIdentifier->schema) { + return $tableIdentifier->forMachine(); + } + // The fully qualified table name in PostgreSQL is in the form of + // "<database>"."<schema>"."<table>". $options = $this->getConnectionOptions(); $schema = $options['schema'] ?? 'public'; - - // The fully qualified table name in PostgreSQL is in the form of - // <database>.<schema>.<table>. - return $options['database'] . '.' . $schema . '.' . $this->getPrefix() . $table; + return $this->identifiers->database($options['database'])->quotedMachineName . '.' . + $this->identifiers->schema($schema)->quotedMachineName . '.' . + $tableIdentifier->quotedMachineName; } /** diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/IdentifierHandler.php b/core/modules/pgsql/src/Driver/Database/pgsql/IdentifierHandler.php new file mode 100644 index 0000000000000000000000000000000000000000..441087406833fc8578a6011ab2c4b0c33fcde091 --- /dev/null +++ b/core/modules/pgsql/src/Driver/Database/pgsql/IdentifierHandler.php @@ -0,0 +1,104 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\pgsql\Driver\Database\pgsql; + +use Drupal\Core\Database\Exception\IdentifierException; +use Drupal\Core\Database\Identifier\IdentifierHandlerBase; +use Drupal\Core\Database\Identifier\IdentifierType; +use Drupal\Core\Database\Identifier\Schema; + +/** + * PostgreSql implementation of the identifier handler. + */ +class IdentifierHandler extends IdentifierHandlerBase { + + /** + * The default schema identifier, if specified. + */ + public readonly ?Schema $defaultSchema; + + /** + * Constructor. + * + * @param string $tablePrefix + * The table prefix to be used by the database connection. + * @param string $defaultSchema + * The default schema to use for database operations. If not specified, + * it's assumed to use the 'public' schema. + * @param array{0:string, 1:string} $identifierQuotes + * The identifier quote characters. + */ + public function __construct( + string $tablePrefix, + string $defaultSchema = '', + array $identifierQuotes = ['"', '"'], + ) { + parent::__construct($tablePrefix, $identifierQuotes); + if ($defaultSchema !== '' && $defaultSchema !== 'public') { + $this->defaultSchema = $this->schema($defaultSchema); + } + else { + $this->defaultSchema = NULL; + } + } + + /** + * {@inheritdoc} + */ + public function getMaxLength(IdentifierType $type): int { + // @see https://www.postgresql.org/docs/current/limits.html + return 63; + } + + /** + * {@inheritdoc} + */ + protected function validateCanonicalName(string $identifier, string $canonicalName, IdentifierType $type): true { + return match ($type) { + IdentifierType::Table => TRUE, + default => parent::validateCanonicalName($identifier, $canonicalName, $type), + }; + } + + /** + * {@inheritdoc} + */ + protected function resolveTableForMachine(string $canonicalName, array $info): string { + if ($info['schema'] && ($info['schema_default_added'] ?? FALSE) === FALSE) { + // We are processing a fully qualified table identifier, canonical name + // should not be processed, just checked it does not exceed max length. + if (strlen($canonicalName) > $this->getMaxLength(IdentifierType::Table)) { + throw new IdentifierException(sprintf( + 'Table identifier \'%s\' exceeds maximum allowed length (%d)', + $canonicalName, + $this->getMaxLength(IdentifierType::Table), + )); + } + return $canonicalName; + } + + $prefix = $info['needs_prefix'] ? $this->tablePrefix : ''; + if (strlen($prefix . $canonicalName) > $this->getMaxLength(IdentifierType::Table)) { + // We shorten too long table names. + return $this->cropByHashing($canonicalName, $info, IdentifierType::Table, $prefix, $this->getMaxLength(IdentifierType::Table), 10); + } + return $prefix . $canonicalName; + } + + /** + * {@inheritdoc} + */ + public function parseTableIdentifier(string $identifier): array { + $parts = parent::parseTableIdentifier($identifier); + if (isset($this->defaultSchema) && $parts['schema'] === NULL) { + // When a non-public schema is defined for the connection, machine names + // for table identifiers should be in the "schema"."table" format. + $parts['schema'] = $this->defaultSchema; + $parts['schema_default_added'] = TRUE; + } + return $parts; + } + +} diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Schema.php b/core/modules/pgsql/src/Driver/Database/pgsql/Schema.php index a4585e15da7abcb1e1bc5c3f251c2c223fd86905..3b992977cce0198ce260b0a8f5de24c4158b326a 100644 --- a/core/modules/pgsql/src/Driver/Database/pgsql/Schema.php +++ b/core/modules/pgsql/src/Driver/Database/pgsql/Schema.php @@ -128,7 +128,7 @@ protected function ensureIdentifiersLength($table_identifier_part, $column_ident */ public function queryTableInformation($table) { // Generate a key to reference this table's information on. - $prefixed_table = $this->connection->getPrefix() . $table; + $prefixed_table = $this->connection->identifiers->tablePrefix . $table; $key = $this->connection->prefixTables('{' . $table . '}'); // Take into account that temporary tables are stored in a different schema. @@ -220,7 +220,7 @@ protected function getTempNamespaceName() { * The non-prefixed name of the table. */ protected function resetTableInformation($table) { - $key = $this->defaultSchema . '.' . $this->connection->getPrefix() . $table; + $key = $this->defaultSchema . '.' . $this->connection->identifiers->tablePrefix . $table; unset($this->tableInformation[$key]); } @@ -521,7 +521,7 @@ public function tableExists($table, $add_prefix = TRUE) { * {@inheritdoc} */ public function findTables($table_expression) { - $prefix = $this->connection->getPrefix(); + $prefix = $this->connection->identifiers->tablePrefix; $prefix_length = strlen($prefix); $tables = []; @@ -567,7 +567,7 @@ public function renameTable($table, $new_name) { } // Get the schema and tablename for the old table. - $table_name = $this->connection->getPrefix() . $table; + $table_name = $this->connection->identifiers->tablePrefix . $table; // Index names and constraint names are global in PostgreSQL, so we need to // rename them when renaming the table. $indexes = $this->connection->query('SELECT indexname FROM pg_indexes WHERE schemaname = :schema AND tablename = :table', [':schema' => $this->defaultSchema, ':table' => $table_name]); @@ -734,7 +734,7 @@ public function indexExists($table, $name) { $sql_params = [ ':schema' => $this->defaultSchema, - ':table' => $this->connection->getPrefix() . $table, + ':table' => $this->connection->identifiers->tablePrefix . $table, ':index' => $index_name, ]; return (bool) $this->connection->query("SELECT 1 FROM pg_indexes WHERE schemaname = :schema AND tablename = :table AND indexname = :index", $sql_params)->fetchField(); @@ -1118,7 +1118,7 @@ public function extensionExists($name): bool { protected function getSequenceName(string $table, string $column): ?string { return $this->connection ->query("SELECT pg_get_serial_sequence(:table, :column)", [ - ':table' => $this->defaultSchema . '.' . $this->connection->getPrefix() . $table, + ':table' => $this->defaultSchema . '.' . $this->connection->identifiers->tablePrefix . $table, ':column' => $column, ]) ->fetchField(); diff --git a/core/modules/pgsql/tests/src/Kernel/pgsql/NonPublicSchemaTest.php b/core/modules/pgsql/tests/src/Kernel/pgsql/NonPublicSchemaTest.php index 2f7386b4306be165d688324456d3519e188a24fb..dbb8babe612fc75cc0e1eccf0dc63faf8061cd67 100644 --- a/core/modules/pgsql/tests/src/Kernel/pgsql/NonPublicSchemaTest.php +++ b/core/modules/pgsql/tests/src/Kernel/pgsql/NonPublicSchemaTest.php @@ -118,7 +118,7 @@ public function testExtensionExists(): void { $this->assertTrue($this->testingFakeConnection->schema()->tableExists('faking_table')); // Hardcoded assertion that we created the table in the non-public schema. - $this->assertCount(1, $this->testingFakeConnection->query("SELECT * FROM pg_tables WHERE schemaname = 'testing_fake' AND tablename = :prefixedTable", [':prefixedTable' => $this->testingFakeConnection->getPrefix() . "faking_table"])->fetchAll()); + $this->assertCount(1, $this->testingFakeConnection->query("SELECT * FROM pg_tables WHERE schemaname = 'testing_fake' AND tablename = :prefixedTable", [':prefixedTable' => $this->testingFakeConnection->identifiers->tablePrefix . "faking_table"])->fetchAll()); } /** @@ -288,11 +288,11 @@ public function testIndex(): void { $this->assertTrue($this->testingFakeConnection->schema()->indexExists('faking_table', 'test_field')); - $results = $this->testingFakeConnection->query("SELECT * FROM pg_indexes WHERE indexname = :indexname", [':indexname' => $this->testingFakeConnection->getPrefix() . 'faking_table__test_field__idx'])->fetchAll(); + $results = $this->testingFakeConnection->query("SELECT * FROM pg_indexes WHERE indexname = :indexname", [':indexname' => $this->testingFakeConnection->identifiers->tablePrefix . 'faking_table__test_field__idx'])->fetchAll(); $this->assertCount(1, $results); $this->assertSame('testing_fake', $results[0]->schemaname); - $this->assertSame($this->testingFakeConnection->getPrefix() . 'faking_table', $results[0]->tablename); + $this->assertSame($this->testingFakeConnection->identifiers->tablePrefix . 'faking_table', $results[0]->tablename); $this->assertStringContainsString('USING btree (test_field)', $results[0]->indexdef); $this->testingFakeConnection->schema()->dropIndex('faking_table', 'test_field'); @@ -315,12 +315,12 @@ public function testUniqueKey(): void { // phpcs:ignore // $this->assertTrue($this->testingFakeConnection->schema()->indexExists('faking_table', 'test_field')); - $results = $this->testingFakeConnection->query("SELECT * FROM pg_indexes WHERE indexname = :indexname", [':indexname' => $this->testingFakeConnection->getPrefix() . 'faking_table__test_field__key'])->fetchAll(); + $results = $this->testingFakeConnection->query("SELECT * FROM pg_indexes WHERE indexname = :indexname", [':indexname' => $this->testingFakeConnection->identifiers->tablePrefix . 'faking_table__test_field__key'])->fetchAll(); // Check the unique key columns. $this->assertCount(1, $results); $this->assertSame('testing_fake', $results[0]->schemaname); - $this->assertSame($this->testingFakeConnection->getPrefix() . 'faking_table', $results[0]->tablename); + $this->assertSame($this->testingFakeConnection->identifiers->tablePrefix . 'faking_table', $results[0]->tablename); $this->assertStringContainsString('USING btree (test_field)', $results[0]->indexdef); $this->testingFakeConnection->schema()->dropUniqueKey('faking_table', 'test_field'); @@ -348,7 +348,7 @@ public function testPrimaryKey(): void { $this->assertCount(1, $results); $this->assertSame('testing_fake', $results[0]->schemaname); - $this->assertSame($this->testingFakeConnection->getPrefix() . 'faking_table', $results[0]->tablename); + $this->assertSame($this->testingFakeConnection->identifiers->tablePrefix . 'faking_table', $results[0]->tablename); $this->assertStringContainsString('USING btree (id)', $results[0]->indexdef); $find_primary_keys_columns = new \ReflectionMethod(get_class($this->testingFakeConnection->schema()), 'findPrimaryKeyColumns'); @@ -371,7 +371,7 @@ public function testTable(): void { $result = $this->testingFakeConnection->query("SELECT * FROM information_schema.tables WHERE table_schema = 'testing_fake'")->fetchAll(); $this->assertFalse($this->testingFakeConnection->schema()->tableExists('faking_table')); $this->assertTrue($this->testingFakeConnection->schema()->tableExists('new_faking_table')); - $this->assertEquals($this->testingFakeConnection->getPrefix() . 'new_faking_table', $result[0]->table_name); + $this->assertEquals($this->testingFakeConnection->identifiers->tablePrefix . 'new_faking_table', $result[0]->table_name); $this->assertEquals('testing_fake', $result[0]->table_schema); sort($tables); $this->assertEquals(['new_faking_table'], $tables); diff --git a/core/modules/pgsql/tests/src/Kernel/pgsql/SchemaTest.php b/core/modules/pgsql/tests/src/Kernel/pgsql/SchemaTest.php index e10ed33d1903613076e3631c591fce991313af80..e26e298bd74015f072a1fab40e649be9da3052b7 100644 --- a/core/modules/pgsql/tests/src/Kernel/pgsql/SchemaTest.php +++ b/core/modules/pgsql/tests/src/Kernel/pgsql/SchemaTest.php @@ -281,18 +281,18 @@ public function testPgsqlSequences(): void { // Retrieves a sequence name that is owned by the table and column. $sequence_name = $this->connection ->query("SELECT pg_get_serial_sequence(:table, :column)", [ - ':table' => $this->connection->getPrefix() . 'sequence_test', + ':table' => $this->connection->identifiers->tablePrefix . 'sequence_test', ':column' => 'uid', ]) ->fetchField(); $schema = $this->connection->getConnectionOptions()['schema'] ?? 'public'; - $this->assertEquals($schema . '.' . $this->connection->getPrefix() . 'sequence_test_uid_seq', $sequence_name); + $this->assertEquals($schema . '.' . $this->connection->identifiers->tablePrefix . 'sequence_test_uid_seq', $sequence_name); // Checks if the sequence exists. $this->assertTrue((bool) \Drupal::database() ->query("SELECT c.relname FROM pg_class as c WHERE c.relkind = 'S' AND c.relname = :name", [ - ':name' => $this->connection->getPrefix() . 'sequence_test_uid_seq', + ':name' => $this->connection->identifiers->tablePrefix . 'sequence_test_uid_seq', ]) ->fetchField()); @@ -304,7 +304,7 @@ public function testPgsqlSequences(): void { AND d.refobjsubid > 0 AND d.classid = 'pg_class'::regclass", [':seq_name' => $sequence_name])->fetchObject(); - $this->assertEquals($this->connection->getPrefix() . 'sequence_test', $sequence_owner->table_name); + $this->assertEquals($this->connection->identifiers->tablePrefix . 'sequence_test', $sequence_owner->table_name); $this->assertEquals('uid', $sequence_owner->field_name, 'New sequence is owned by its table.'); } @@ -323,7 +323,7 @@ public function testTableExists(): void { ], ]; $this->schema->createTable($table_name, $table_specification); - $prefixed_table_name = $this->connection->getPrefix($table_name) . $table_name; + $prefixed_table_name = $this->connection->identifiers->tablePrefix . $table_name; // Three different calls to the method Schema::tableExists() with an // unprefixed table name. diff --git a/core/modules/pgsql/tests/src/Unit/IdentifierTest.php b/core/modules/pgsql/tests/src/Unit/IdentifierTest.php new file mode 100644 index 0000000000000000000000000000000000000000..9f6e48aeb907dce6b7354762b7f467ba98666aab --- /dev/null +++ b/core/modules/pgsql/tests/src/Unit/IdentifierTest.php @@ -0,0 +1,197 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\pgsql\Unit; + +use Drupal\Core\Database\Exception\IdentifierException; +use Drupal\Core\Database\Identifier\IdentifierType; +use Drupal\pgsql\Driver\Database\pgsql\IdentifierHandler; +use Drupal\Tests\UnitTestCase; + +/** + * Tests PostgreSql database identifiers. + * + * @coversDefaultClass \Drupal\pgsql\Driver\Database\pgsql\IdentifierHandler + * @group Database + */ +class IdentifierTest extends UnitTestCase { + + // cSpell:disable + + /** + * Data provider for testTable. + * + * @return array + * An associative array of test case data. + */ + public static function providerTable(): array { + return [ + 'No prefix' => [ + 'identifier' => 'nocase', + 'prefix' => '', + 'expectedCanonical' => 'nocase', + 'expectedMachine' => '"nocase"', + ], + 'Prefix' => [ + 'identifier' => 'nocase', + 'prefix' => 'foobar', + 'expectedCanonical' => 'nocase', + 'expectedMachine' => '"foobarnocase"', + ], + 'No prefix, camelCase' => [ + 'identifier' => 'camelCase', + 'prefix' => '', + 'expectedCanonical' => 'camelCase', + 'expectedMachine' => '"camelCase"', + ], + 'Prefix, camelCase' => [ + 'identifier' => 'camelCase', + 'prefix' => 'foobar', + 'expectedCanonical' => 'camelCase', + 'expectedMachine' => '"foobarcamelCase"', + ], + 'No prefix, backtick' => [ + 'identifier' => '`backtick`', + 'prefix' => '', + 'expectedCanonical' => 'backtick', + 'expectedMachine' => '"backtick"', + ], + 'No prefix, brackets' => [ + 'identifier' => '[brackets]', + 'prefix' => '', + 'expectedCanonical' => 'brackets', + 'expectedMachine' => '"brackets"', + ], + 'No prefix, remove slash' => [ + 'identifier' => 'no/case', + 'prefix' => '', + 'expectedCanonical' => 'nocase', + 'expectedMachine' => '"nocase"', + ], + 'No prefix, remove quote' => [ + 'identifier' => 'no"case', + 'prefix' => '', + 'expectedCanonical' => 'nocase', + 'expectedMachine' => '"nocase"', + ], + 'No prefix, shortened machine name' => [ + 'identifier' => 'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'prefix' => '', + 'expectedCanonical' => 'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'expectedMachine' => '"VeryVeryVeryVeryHungryHung25fa0c3753ngryHungryHungryCaterpillar"', + ], + 'Prefix, shortened machine name' => [ + 'identifier' => 'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'prefix' => 'foobar', + 'expectedCanonical' => 'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'expectedMachine' => '"foobarVeryVeryVeryVeryHungryH25fa0c3753yHungryHungryCaterpillar"', + ], + 'Prefix one less than overflow, shortened machine name' => [ + 'identifier' => 'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'prefix' => 'prefix____12345678901234567890123456789012345678', + 'expectedCanonical' => 'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'expectedMachine' => '"prefix____12345678901234567890123456789012345678Ve25fa0c3753lar"', + ], + 'Prefix just right, shortened machine name' => [ + 'identifier' => 'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'prefix' => 'prefix____123456789012345678901234567890123456789', + 'expectedCanonical' => 'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'expectedMachine' => '"prefix____123456789012345678901234567890123456789Ve25fa0c3753ar"', + ], + 'Prefix one more than overflow, shortened machine name' => [ + 'identifier' => 'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'prefix' => 'prefix____1234567890123456789012345678901234567890', + 'expectedCanonical' => 'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'expectedMachine' => '', + 'expectedException' => 'Table canonical identifier \'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar\' cannot be converted into a machine identifier; prefix \'prefix____1234567890123456789012345678901234567890\'', + ], + 'Prefix too long to fit, table does not require shortening' => [ + 'identifier' => 'nocase', + 'prefix' => 'VeryVeryVeryVeryVeryVeryVeryVeryLongLongLongLongLongLongLongPrefix', + 'expectedCanonical' => 'nocase', + 'expectedMachine' => '', + 'expectedException' => 'Table canonical identifier \'nocase\' cannot be converted into a machine identifier; prefix \'VeryVeryVeryVeryVeryVeryVeryVeryLongLongLongLongLongLongLongPrefix\'', + ], + 'Prefix too long to fit, table requires shortening' => [ + 'identifier' => 'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'prefix' => 'VeryVeryVeryVeryVeryVeryVeryVeryLongLongLongLongLongLongLongPrefix', + 'expectedCanonical' => 'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'expectedMachine' => '', + 'expectedException' => 'Table canonical identifier \'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar\' cannot be converted into a machine identifier; prefix \'VeryVeryVeryVeryVeryVeryVeryVeryLongLongLongLongLongLongLongPrefix\'', + ], + // Sometimes, table names are following the pattern database.schema.table. + 'Fully qualified - too many parts: 4' => [ + 'identifier' => 'Chowra.Teressa.Bompuka.Katchal', + 'prefix' => '', + 'expectedCanonical' => '', + 'expectedMachine' => '', + 'expectedException' => 'The table identifier \'Chowra.Teressa.Bompuka.Katchal\' does not comply with the syntax [database.][schema.]table', + ], + 'Fully qualified - no prefix' => [ + 'identifier' => '"Laouk"."Nancowry"."Tillangchong"', + 'prefix' => '', + 'expectedCanonical' => 'Laouk.Nancowry.Tillangchong', + 'expectedMachine' => '"Laouk"."Nancowry"."Tillangchong"', + ], + 'Fully qualified - prefix' => [ + 'identifier' => '"Laouk"."Nancowry"."Tillangchong"', + 'prefix' => 'foobar', + 'expectedCanonical' => 'Laouk.Nancowry.Tillangchong', + 'expectedMachine' => '"Laouk"."Nancowry"."Tillangchong"', + ], + 'Fully qualified - prefix not duplicated' => [ + 'identifier' => 'Laouk.Nancowry.foobarTillangchong', + 'prefix' => 'foobar', + 'expectedCanonical' => 'Laouk.Nancowry.foobarTillangchong', + 'expectedMachine' => '"Laouk"."Nancowry"."foobarTillangchong"', + ], + 'Fully qualified - remove not canonical characters' => [ + 'identifier' => '&Laouk£.!Nancowry?.$Tillangchong%%%', + 'prefix' => '', + 'expectedCanonical' => 'Laouk.Nancowry.Tillangchong', + 'expectedMachine' => '"Laouk"."Nancowry"."Tillangchong"', + ], + 'Fully qualified - invalid database name length' => [ + 'identifier' => 'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar.Laouk.Trinket', + 'prefix' => '', + 'expectedCanonical' => '', + 'expectedMachine' => '', + 'expectedException' => 'The length of the database identifier \'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar\' once canonicalized to \'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar\' is invalid (maximum allowed: 63)', + ], + 'Fully qualified - invalid schema name length' => [ + 'identifier' => 'Laouk.VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar.Trinket', + 'prefix' => '', + 'expectedCanonical' => '', + 'expectedMachine' => '', + 'expectedException' => 'The length of the schema identifier \'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar\' once canonicalized to \'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar\' is invalid (maximum allowed: 63)', + ], + 'Fully qualified - invalid table name length' => [ + 'identifier' => 'Laouk.Camorta.VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'prefix' => '', + 'expectedCanonical' => 'Laouk.Camorta.VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'expectedMachine' => '', + 'expectedException' => 'Table identifier \'VeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar\' exceeds maximum allowed length (63)', + ], + ]; + } + + // cSpell:enable + + /** + * @dataProvider providerTable + */ + public function testTable(string $identifier, string $prefix = '', ?string $expectedCanonical = '', ?string $expectedMachine = '', ?string $expectedException = NULL): void { + $handler = new IdentifierHandler($prefix); + if ($expectedException) { + $this->expectException(IdentifierException::class); + $this->expectExceptionMessage($expectedException); + } + $this->assertSame($expectedCanonical, $handler->table($identifier)->canonical()); + $this->assertSame($expectedMachine, $handler->table($identifier)->forMachine()); + // The machine name includes the quote characters so we need to subtract + // those from the length. + $this->assertLessThanOrEqual($handler->getMaxLength(IdentifierType::Table), strlen($handler->table($identifier)->machineName), 'Invalid machine table length.'); + } + +} diff --git a/core/modules/pgsql/tests/src/Unit/SchemaTest.php b/core/modules/pgsql/tests/src/Unit/SchemaTest.php index 4a554a014de3c990c69c755076b108d1ea0adfcd..d78688b6b482cc712f848a66eaee6e0c72a513a0 100644 --- a/core/modules/pgsql/tests/src/Unit/SchemaTest.php +++ b/core/modules/pgsql/tests/src/Unit/SchemaTest.php @@ -32,7 +32,6 @@ public function testComputedConstraintName($table_name, $name, $expected): void $connection = $this->prophesize('\Drupal\pgsql\Driver\Database\pgsql\Connection'); $connection->getConnectionOptions()->willReturn([]); - $connection->getPrefix()->willReturn(''); $statement = $this->prophesize('\Drupal\Core\Database\StatementInterface'); $statement->fetchField()->willReturn($max_identifier_length); diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php b/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php index 0a353dceed05ce51d151de60f99ed24ac0e7f1ac..85c7473c04637d683056789a7b84e3060c47c10f 100644 --- a/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php +++ b/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php @@ -67,28 +67,23 @@ class Connection extends DatabaseConnection implements SupportsTemporaryTablesIn */ 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); - // Empty prefix means query the main database -- no need to attach anything. - $prefix = $this->connectionOptions['prefix'] ?? ''; + $prefix = $connection_options['prefix'] ?? ''; + assert(is_string($prefix), 'The \'prefix\' connection option to ' . __METHOD__ . '() must be a string.'); if ($prefix !== '') { - $this->attachDatabase($prefix); + $attachedDatabaseName = $prefix; // Add a ., so queries become prefix.table, which is proper syntax for // querying an attached database. $prefix .= '.'; } - - // Regenerate the prefix. - $this->setPrefix($prefix); + parent::__construct($connection, $connection_options, new IdentifierHandler($prefix)); + if (isset($attachedDatabaseName)) { + $this->attachDatabase($attachedDatabaseName); + } } /** @@ -387,12 +382,7 @@ public function databaseType() { } /** - * Overrides \Drupal\Core\Database\Connection::createDatabase(). - * - * @param string $database - * The name of the database to create. - * - * @throws \Drupal\Core\Database\DatabaseNotFoundException + * {@inheritdoc} */ public function createDatabase($database) { // Verify the database is writable. @@ -432,10 +422,8 @@ public function prepareStatement(string $query, array $options, bool $allow_row_ * {@inheritdoc} */ public function getFullQualifiedTableName($table) { - $prefix = $this->getPrefix(); - // Don't include the SQLite database file name as part of the table name. - return $prefix . $table; + return $this->identifiers->table($table)->forMachine(); } /** diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/IdentifierHandler.php b/core/modules/sqlite/src/Driver/Database/sqlite/IdentifierHandler.php new file mode 100644 index 0000000000000000000000000000000000000000..dca9b770b91d9f7254ae46563d3b7c1e2ba420ff --- /dev/null +++ b/core/modules/sqlite/src/Driver/Database/sqlite/IdentifierHandler.php @@ -0,0 +1,53 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\sqlite\Driver\Database\sqlite; + +use Drupal\Core\Database\Exception\IdentifierException; +use Drupal\Core\Database\Identifier\IdentifierHandlerBase; +use Drupal\Core\Database\Identifier\IdentifierType; + +/** + * SQLite implementation of the identifier handler. + */ +class IdentifierHandler extends IdentifierHandlerBase { + + /** + * {@inheritdoc} + */ + public function getMaxLength(IdentifierType $type): int { + // There is no hard limit on identifier length in SQLite, so we just use + // common sense: identifiers longer than 128 characters are hardly + // readable. + // @see https://www.sqlite.org/limits.html + // @see https://stackoverflow.com/questions/8135013/table-name-limit-in-sqlite-android + return 128; + } + + /** + * {@inheritdoc} + */ + public function parseTableIdentifier(string $identifier): array { + $parts = parent::parseTableIdentifier($identifier); + if ($parts['database']) { + throw new IdentifierException(sprintf( + 'SQLite does not support the syntax [database.][schema.]table for the table identifier \'%s\'. Avoid specifying the \'database\' part', + $identifier, + )); + } + if ($this->tablePrefix !== '' && $parts['schema'] === NULL) { + $parts['schema'] = $this->schema(rtrim($this->tablePrefix, '.')); + $parts['needs_prefix'] = FALSE; + } + return $parts; + } + + /** + * {@inheritdoc} + */ + protected function resolveTableForMachine(string $canonicalName, array $info): string { + return $canonicalName; + } + +} diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Schema.php b/core/modules/sqlite/src/Driver/Database/sqlite/Schema.php index 3779e7625195c342548095a4aa305be5a73292fc..2f091e316a8c8be85adc1b514f5e04ee42ec3102 100644 --- a/core/modules/sqlite/src/Driver/Database/sqlite/Schema.php +++ b/core/modules/sqlite/src/Driver/Database/sqlite/Schema.php @@ -2,6 +2,7 @@ namespace Drupal\sqlite\Driver\Database\sqlite; +use Drupal\Core\Database\Identifier\Table as TableIdentifier; use Drupal\Core\Database\SchemaObjectExistsException; use Drupal\Core\Database\SchemaObjectDoesNotExistException; use Drupal\Core\Database\Schema as DatabaseSchema; @@ -29,16 +30,30 @@ class Schema extends DatabaseSchema { * {@inheritdoc} */ public function tableExists($table, $add_prefix = TRUE) { - $info = $this->getPrefixInfo($table, $add_prefix); + if (!$table instanceof TableIdentifier) { + $table = $this->connection->identifiers->table($table); + } + assert($table instanceof TableIdentifier); + + $schema = $table->schema ?? $this->defaultSchema; - // 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(); + $sql = sprintf( + 'SELECT 1 FROM %s WHERE type = :type AND name = :name', + $this->connection->identifiers->table($schema . '.sqlite_master'), + ); + + return (bool) $this->connection->query($sql, [':type' => 'table', ':name' => $table->machineName])->fetchField(); } /** * {@inheritdoc} */ public function fieldExists($table, $column) { + if (!$table instanceof TableIdentifier) { + $table = $this->connection->identifiers->table($table); + } + assert($table instanceof TableIdentifier); + $schema = $this->introspectSchema($table); return !empty($schema['fields'][$column]); } @@ -47,12 +62,17 @@ public function fieldExists($table, $column) { * {@inheritdoc} */ public function createTableSql($name, $table) { + if (!$name instanceof TableIdentifier) { + $name = $this->connection->identifiers->table($name); + } + assert($name instanceof TableIdentifier); + 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"; + $sql[] = "CREATE TABLE " . $name->forMachine() . " (\n" . $this->createColumnsSql($name, $table) . "\n)\n"; return array_merge($sql, $this->createIndexSql($name, $table)); } @@ -60,16 +80,41 @@ public function createTableSql($name, $table) { * Build the SQL expression for indexes. */ protected function createIndexSql($tablename, $schema) { + if (!$tablename instanceof TableIdentifier) { + @trigger_error("Passing a table identifier as a string to " . __METHOD__ . "() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Pass a Table identifier value object instead. See https://www.drupal.org/node/7654132", E_USER_DEPRECATED); + $table = $this->connection->identifiers->table($tablename); + } + else { + $table = $tablename; + } + assert($table instanceof TableIdentifier); + + // In SQLite, the 'CREATE [UNIQUE] INDEX' DDL statements requires that the + // table name be NOT prefixed by the schema name. We cannot use the + // Table::forMachine() method but should rather pick + // Table->quotedMachineName directly. + // @see https://www.sqlite.org/syntax/create-index-stmt.html $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"; + $sql[] = sprintf( + "CREATE UNIQUE INDEX %s.%s ON %s (%s)\n", + $table->schema, + '[' . $table->machineName . '_' . $key . ']', + $table->quotedMachineName, + $this->createKeySql($fields), + ); } } 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"; + $sql[] = sprintf( + "CREATE INDEX %s.%s ON %s (%s)\n", + $table->schema, + '[' . $table->machineName . '_' . $key . ']', + $table->quotedMachineName, + $this->createKeySql($fields), + ); } } return $sql; @@ -79,6 +124,15 @@ protected function createIndexSql($tablename, $schema) { * Build the SQL expression for creating columns. */ protected function createColumnsSql($tablename, $schema) { + if (!$tablename instanceof TableIdentifier) { + @trigger_error("Passing a table identifier as a string to " . __METHOD__ . "() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Pass a Table identifier value object instead. See https://www.drupal.org/node/7654132", E_USER_DEPRECATED); + $table = $this->connection->identifiers->table($tablename); + } + else { + $table = $tablename; + } + assert($table instanceof TableIdentifier); + $sql_array = []; // Add the SQL statement for each field. @@ -257,6 +311,15 @@ public function getFieldTypeMap() { * {@inheritdoc} */ public function renameTable($table, $new_name) { + if (!$table instanceof TableIdentifier) { + $table = $this->connection->identifiers->table($table); + } + assert($table instanceof TableIdentifier); + if (!$new_name instanceof TableIdentifier) { + $new_name = $this->connection->identifiers->table($new_name); + } + assert($new_name instanceof TableIdentifier); + if (!$this->tableExists($table)) { throw new SchemaObjectDoesNotExistException("Cannot rename '$table' to '$new_name': table '$table' doesn't exist."); } @@ -266,13 +329,12 @@ public function renameTable($table, $new_name) { $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->executeDdlStatement('ALTER TABLE {' . $table . '} RENAME TO [' . $info['table'] . ']'); + // SQLite doesn't allow you to rename tables outside of the current schema, + // so the syntax '... RENAME TO schema.table' would fail. We cannot use the + // Table::forMachine() method but should rather pick + // Table->quotedMachineName directly. + // @see https://www.sqlite.org/syntax/alter-table-stmt.html + $this->executeDdlStatement(sprintf('ALTER TABLE %s RENAME TO %s', $table->forMachine(), $new_name->quotedMachineName)); // Drop the indexes, there is no RENAME INDEX command in SQLite. if (!empty($schema['unique keys'])) { @@ -297,11 +359,16 @@ public function renameTable($table, $new_name) { * {@inheritdoc} */ public function dropTable($table) { + if (!$table instanceof TableIdentifier) { + $table = $this->connection->identifiers->table($table); + } + assert($table instanceof TableIdentifier); + if (!$this->tableExists($table)) { return FALSE; } $this->connection->tableDropped = TRUE; - $this->executeDdlStatement('DROP TABLE {' . $table . '}'); + $this->executeDdlStatement('DROP TABLE ' . $table->forMachine()); return TRUE; } @@ -309,6 +376,11 @@ public function dropTable($table) { * {@inheritdoc} */ public function addField($table, $field, $specification, $keys_new = []) { + if (!$table instanceof TableIdentifier) { + $table = $this->connection->identifiers->table($table); + } + assert($table instanceof TableIdentifier); + if (!$this->tableExists($table)) { throw new SchemaObjectDoesNotExistException("Cannot add field '$table.$field': table doesn't exist."); } @@ -325,7 +397,7 @@ public function addField($table, $field, $specification, $keys_new = []) { 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)); + $query = 'ALTER TABLE ' . $table->forMachine() . ' ADD ' . $this->createFieldSql($field, $this->processField($specification)); $this->executeDdlStatement($query); // Apply the initial value if set. @@ -338,12 +410,12 @@ public function addField($table, $field, $specification, $keys_new = []) { $expression = $specification['initial_from_field']; $arguments = []; } - $this->connection->update($table) + $this->connection->update($table->identifier) ->expression($field, $expression, $arguments) ->execute(); } elseif (isset($specification['initial'])) { - $this->connection->update($table) + $this->connection->update($table->identifier) ->fields([$field => $specification['initial']]) ->execute(); } @@ -415,15 +487,21 @@ public function addField($table, $field, $specification, $keys_new = []) { * that will be used as an expression field. */ protected function alterTable($table, $old_schema, $new_schema, array $mapping = []) { + if (!$table instanceof TableIdentifier) { + @trigger_error("Passing a table identifier as a string to " . __METHOD__ . "() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Pass a Table identifier value object instead. See https://www.drupal.org/node/7654132", E_USER_DEPRECATED); + $table = $this->connection->identifiers->table($table); + } + assert($table instanceof TableIdentifier); + $i = 0; do { - $new_table = $table . '_' . $i++; + $new_table = $this->connection->identifiers->table($table->canonicalName . '_' . $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); + $select = $this->connection->select($table->identifier); // Complete the mapping. $possible_keys = array_keys($new_schema['fields']); @@ -445,12 +523,12 @@ protected function alterTable($table, $old_schema, $new_schema, array $mapping = } // Execute the data migration query. - $this->connection->insert($new_table) + $this->connection->insert($new_table->identifier) ->from($select) ->execute(); - $old_count = $this->connection->query('SELECT COUNT(*) FROM {' . $table . '}')->fetchField(); - $new_count = $this->connection->query('SELECT COUNT(*) FROM {' . $new_table . '}')->fetchField(); + $old_count = $this->connection->query('SELECT COUNT(*) FROM ' . $table->forMachine())->fetchField(); + $new_count = $this->connection->query('SELECT COUNT(*) FROM ' . $new_table->forMachine())->fetchField(); if ($old_count == $new_count) { $this->dropTable($table); $this->renameTable($new_table, $table); @@ -474,6 +552,12 @@ protected function alterTable($table, $old_schema, $new_schema, array $mapping = * If a column of the table could not be parsed. */ protected function introspectSchema($table) { + if (!$table instanceof TableIdentifier) { + @trigger_error("Passing a table identifier as a string to " . __METHOD__ . "() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Pass a Table identifier value object instead. See https://www.drupal.org/node/7654132", E_USER_DEPRECATED); + $table = $this->connection->identifiers->table($table); + } + assert($table instanceof TableIdentifier); + $mapped_fields = array_flip($this->getFieldTypeMap()); $schema = [ 'fields' => [], @@ -482,8 +566,7 @@ protected function introspectSchema($table) { 'indexes' => [], ]; - $info = $this->getPrefixInfo($table); - $result = $this->connection->query('PRAGMA [' . $info['schema'] . '].table_info([' . $info['table'] . '])'); + $result = $this->connection->query('PRAGMA ' . $table->schema . '.table_info(' . $table->quotedMachineName . ')'); foreach ($result as $row) { if (preg_match('/^([^(]+)\((.*)\)$/', $row->type, $matches)) { $type = $matches[1]; @@ -539,7 +622,7 @@ protected function introspectSchema($table) { $schema['primary key'] = array_values($schema['primary key']); $indexes = []; - $result = $this->connection->query('PRAGMA [' . $info['schema'] . '].index_list([' . $info['table'] . '])'); + $result = $this->connection->query('PRAGMA ' . $table->schema . '.index_list(' . $table->quotedMachineName . ')'); foreach ($result as $row) { if (!str_starts_with($row->name, 'sqlite_autoindex_')) { $indexes[] = [ @@ -551,8 +634,8 @@ protected function introspectSchema($table) { 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 . '])'); + $index_name = substr($name, strlen($table->machineName) + 1); + $result = $this->connection->query('PRAGMA ' . $table->schema . '.index_info([' . $name . '])'); foreach ($result as $row) { $schema[$index['schema_key']][$index_name][] = $row->name; } @@ -564,6 +647,11 @@ protected function introspectSchema($table) { * {@inheritdoc} */ public function dropField($table, $field) { + if (!$table instanceof TableIdentifier) { + $table = $this->connection->identifiers->table($table); + } + assert($table instanceof TableIdentifier); + if (!$this->fieldExists($table, $field)) { return FALSE; } @@ -600,6 +688,11 @@ public function dropField($table, $field) { * {@inheritdoc} */ public function changeField($table, $field, $field_new, $spec, $keys_new = []) { + if (!$table instanceof TableIdentifier) { + $table = $this->connection->identifiers->table($table); + } + assert($table instanceof TableIdentifier); + if (!$this->fieldExists($table, $field)) { throw new SchemaObjectDoesNotExistException("Cannot change the definition of field '$table.$field': field doesn't exist."); } @@ -673,6 +766,11 @@ protected function mapKeyDefinition(array $key_definition, array $mapping) { * {@inheritdoc} */ public function addIndex($table, $name, $fields, array $spec) { + if (!$table instanceof TableIdentifier) { + $table = $this->connection->identifiers->table($table); + } + assert($table instanceof TableIdentifier); + if (!$this->tableExists($table)) { throw new SchemaObjectDoesNotExistException("Cannot add index '$name' to table '$table': table doesn't exist."); } @@ -691,29 +789,40 @@ public function addIndex($table, $name, $fields, array $spec) { * {@inheritdoc} */ public function indexExists($table, $name) { - $info = $this->getPrefixInfo($table); + if (!$table instanceof TableIdentifier) { + $table = $this->connection->identifiers->table($table); + } + assert($table instanceof TableIdentifier); - return $this->connection->query('PRAGMA [' . $info['schema'] . '].index_info([' . $info['table'] . '_' . $name . '])')->fetchField() != ''; + return $this->connection->query('PRAGMA ' . $table->schema . '.index_info([' . $table->machineName . '_' . $name . '])')->fetchField() != ''; } /** * {@inheritdoc} */ public function dropIndex($table, $name) { + if (!$table instanceof TableIdentifier) { + $table = $this->connection->identifiers->table($table); + } + assert($table instanceof TableIdentifier); + if (!$this->indexExists($table, $name)) { return FALSE; } - $info = $this->getPrefixInfo($table); - - $this->executeDdlStatement('DROP INDEX [' . $info['schema'] . '].[' . $info['table'] . '_' . $name . ']'); - return TRUE; + $this->executeDdlStatement('DROP INDEX ' . $table->schema . '.[' . $table->machineName . '_' . $name . ']'); + return TRUE; } /** * {@inheritdoc} */ public function addUniqueKey($table, $name, $fields) { + if (!$table instanceof TableIdentifier) { + $table = $this->connection->identifiers->table($table); + } + assert($table instanceof TableIdentifier); + if (!$this->tableExists($table)) { throw new SchemaObjectDoesNotExistException("Cannot add unique key '$name' to table '$table': table doesn't exist."); } @@ -732,13 +841,16 @@ public function addUniqueKey($table, $name, $fields) { * {@inheritdoc} */ public function dropUniqueKey($table, $name) { + if (!$table instanceof TableIdentifier) { + $table = $this->connection->identifiers->table($table); + } + assert($table instanceof TableIdentifier); + if (!$this->indexExists($table, $name)) { return FALSE; } - $info = $this->getPrefixInfo($table); - - $this->executeDdlStatement('DROP INDEX [' . $info['schema'] . '].[' . $info['table'] . '_' . $name . ']'); + $this->executeDdlStatement('DROP INDEX ' . $table->schema . '.[' . $table->machineName . '_' . $name . ']'); return TRUE; } @@ -746,6 +858,11 @@ public function dropUniqueKey($table, $name) { * {@inheritdoc} */ public function addPrimaryKey($table, $fields) { + if (!$table instanceof TableIdentifier) { + $table = $this->connection->identifiers->table($table); + } + assert($table instanceof TableIdentifier); + if (!$this->tableExists($table)) { throw new SchemaObjectDoesNotExistException("Cannot add primary key to table '$table': table doesn't exist."); } @@ -766,6 +883,11 @@ public function addPrimaryKey($table, $fields) { * {@inheritdoc} */ public function dropPrimaryKey($table) { + if (!$table instanceof TableIdentifier) { + $table = $this->connection->identifiers->table($table); + } + assert($table instanceof TableIdentifier); + $old_schema = $this->introspectSchema($table); $new_schema = $old_schema; @@ -782,6 +904,12 @@ public function dropPrimaryKey($table) { * {@inheritdoc} */ protected function findPrimaryKeyColumns($table) { + if (!$table instanceof TableIdentifier) { + @trigger_error("Passing a table identifier as a string to " . __METHOD__ . "() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Pass a Table identifier value object instead. See https://www.drupal.org/node/7654132", E_USER_DEPRECATED); + $table = $this->connection->identifiers->table($table); + } + assert($table instanceof TableIdentifier); + if (!$this->tableExists($table)) { return FALSE; } @@ -793,6 +921,13 @@ protected function findPrimaryKeyColumns($table) { * {@inheritdoc} */ protected function introspectIndexSchema($table) { + if (!$table instanceof TableIdentifier) { + @trigger_error("Passing a table identifier as a string to " . __METHOD__ . "() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Pass a Table identifier value object instead. See https://www.drupal.org/node/7654132", E_USER_DEPRECATED); +# throw new \Exception("Passing a table identifier as a string to " . __METHOD__ . "() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Pass a Table identifier value object instead. See https://www.drupal.org/node/7654132"); + $table = $this->connection->identifiers->table($table); + } + assert($table instanceof TableIdentifier); + if (!$this->tableExists($table)) { throw new SchemaObjectDoesNotExistException("The table $table doesn't exist."); } diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Truncate.php b/core/modules/sqlite/src/Driver/Database/sqlite/Truncate.php index 29d82f47792139470f21041a3df64485d645464f..192b6bcbfdcb1539e66fc485a363f2665e7b0907 100644 --- a/core/modules/sqlite/src/Driver/Database/sqlite/Truncate.php +++ b/core/modules/sqlite/src/Driver/Database/sqlite/Truncate.php @@ -19,7 +19,7 @@ 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) . '} '; + return $comments . 'DELETE FROM ' . $this->connection->identifiers->table($this->table)->forMachine(); } } diff --git a/core/modules/sqlite/tests/src/Kernel/sqlite/SchemaTest.php b/core/modules/sqlite/tests/src/Kernel/sqlite/SchemaTest.php index 8848c8586604f2fba0b9156c61dd869b9e5647af..9a9ea16c537f2dfe18a3f94d170e5ec20802bf7d 100644 --- a/core/modules/sqlite/tests/src/Kernel/sqlite/SchemaTest.php +++ b/core/modules/sqlite/tests/src/Kernel/sqlite/SchemaTest.php @@ -96,7 +96,7 @@ public function testIntrospectIndexSchema(): void { unset($table_specification['fields']); $introspect_index_schema = new \ReflectionMethod(get_class($this->schema), 'introspectIndexSchema'); - $index_schema = $introspect_index_schema->invoke($this->schema, $table_name); + $index_schema = $introspect_index_schema->invoke($this->schema, $this->connection->identifiers->table($table_name)); $this->assertEquals($table_specification, $index_schema); } diff --git a/core/modules/sqlite/tests/src/Unit/IdentifierTest.php b/core/modules/sqlite/tests/src/Unit/IdentifierTest.php new file mode 100644 index 0000000000000000000000000000000000000000..f27928b8253b402f3471dc1029a9925a8068ddb6 --- /dev/null +++ b/core/modules/sqlite/tests/src/Unit/IdentifierTest.php @@ -0,0 +1,168 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\sqlite\Unit; + +use Drupal\Core\Database\Exception\IdentifierException; +use Drupal\Core\Database\Identifier\IdentifierType; +use Drupal\sqlite\Driver\Database\sqlite\IdentifierHandler; +use Drupal\Tests\UnitTestCase; + +/** + * Tests SQLite database identifiers. + * + * @coversDefaultClass \Drupal\sqlite\Driver\Database\sqlite\IdentifierHandler + * @group Database + */ +class IdentifierTest extends UnitTestCase { + + // cSpell:disable + + /** + * Data provider for testTable. + * + * @return array + * An associative array of test case data. + */ + public static function providerTable(): array { + return [ + 'No prefix' => [ + 'identifier' => 'nocase', + 'prefix' => '', + 'expectedCanonical' => 'nocase', + 'expectedMachine' => '"nocase"', + ], + 'Prefix' => [ + 'identifier' => 'nocase', + 'prefix' => 'foobar', + 'expectedCanonical' => 'foobar.nocase', + 'expectedMachine' => '"foobar"."nocase"', + ], + 'No prefix, camelCase' => [ + 'identifier' => 'camelCase', + 'prefix' => '', + 'expectedCanonical' => 'camelCase', + 'expectedMachine' => '"camelCase"', + ], + 'Prefix, camelCase' => [ + 'identifier' => 'camelCase', + 'prefix' => 'foobar', + 'expectedCanonical' => 'foobar.camelCase', + 'expectedMachine' => '"foobar"."camelCase"', + ], + 'No prefix, backtick' => [ + 'identifier' => '`backtick`', + 'prefix' => '', + 'expectedCanonical' => 'backtick', + 'expectedMachine' => '"backtick"', + ], + 'No prefix, brackets' => [ + 'identifier' => '[brackets]', + 'prefix' => '', + 'expectedCanonical' => 'brackets', + 'expectedMachine' => '"brackets"', + ], + 'No prefix, remove slash' => [ + 'identifier' => 'no/case', + 'prefix' => '', + 'expectedCanonical' => 'nocase', + 'expectedMachine' => '"nocase"', + ], + 'No prefix, remove quote' => [ + 'identifier' => 'no"case', + 'prefix' => '', + 'expectedCanonical' => 'nocase', + 'expectedMachine' => '"nocase"', + ], + 'No prefix, too long table name' => [ + 'identifier' => 'VeryVeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryVeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'prefix' => '', + 'expectedCanonical' => '', + 'expectedMachine' => '', + 'expectedException' => 'The length of the table identifier \'VeryVeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryVeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar\' once canonicalized to \'VeryVeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryVeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar\' is invalid (maximum allowed: 128)', + ], + 'Prefix too long to fit' => [ + 'identifier' => 'nocase', + 'prefix' => 'VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongLongLongLongLongLongLongVeryVeryVeryVeryVeryVeryVeryVeryLongLongLongLongLongLongLongPrefix', + 'expectedCanonical' => '', + 'expectedMachine' => '', + 'expectedException' => 'The length of the schema identifier \'VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongLongLongLongLongLongLongVeryVeryVeryVeryVeryVeryVeryVeryLongLongLongLongLongLongLongPrefix\' once canonicalized to \'VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongLongLongLongLongLongLongVeryVeryVeryVeryVeryVeryVeryVeryLongLongLongLongLongLongLongPrefix\' is invalid (maximum allowed: 128)', + ], + // Sometimes, table names are following the pattern database.schema.table. + 'Fully qualified - too many parts: 4' => [ + 'identifier' => 'Chowra.Teressa.Bompuka.Katchal', + 'prefix' => '', + 'expectedCanonical' => '', + 'expectedMachine' => '', + 'expectedException' => 'The table identifier \'Chowra.Teressa.Bompuka.Katchal\' does not comply with the syntax [database.][schema.]table', + ], + 'Fully qualified - too many parts: SQLite not supporting \'database\'' => [ + 'identifier' => 'Chowra.Teressa.Bompuka', + 'prefix' => '', + 'expectedCanonical' => '', + 'expectedMachine' => '', + 'expectedException' => 'SQLite does not support the syntax [database.][schema.]table for the table identifier \'Chowra.Teressa.Bompuka\'. Avoid specifying the \'database\' part', + ], + 'Fully qualified - no prefix' => [ + 'identifier' => '"Nancowry"."Tillangchong"', + 'prefix' => '', + 'expectedCanonical' => 'Nancowry.Tillangchong', + 'expectedMachine' => '"Nancowry"."Tillangchong"', + ], + 'Fully qualified - prefix' => [ + 'identifier' => '"Nancowry"."Tillangchong"', + 'prefix' => 'foobar', + 'expectedCanonical' => 'Nancowry.Tillangchong', + 'expectedMachine' => '"Nancowry"."Tillangchong"', + ], + 'Fully qualified - prefix not duplicated' => [ + 'identifier' => 'Nancowry.foobarTillangchong', + 'prefix' => 'foobar', + 'expectedCanonical' => 'Nancowry.foobarTillangchong', + 'expectedMachine' => '"Nancowry"."foobarTillangchong"', + ], + 'Fully qualified - remove not canonical characters' => [ + 'identifier' => '!Nancowry?.$Tillangchong%%%', + 'prefix' => '', + 'expectedCanonical' => 'Nancowry.Tillangchong', + 'expectedMachine' => '"Nancowry"."Tillangchong"', + ], + 'Fully qualified - invalid schema name length' => [ + 'identifier' => 'VeryVeryVeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryVeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar.Trinket', + 'prefix' => '', + 'expectedCanonical' => '', + 'expectedMachine' => '', + 'expectedException' => 'The length of the schema identifier \'VeryVeryVeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryVeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar\' once canonicalized to \'VeryVeryVeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryVeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar\' is invalid (maximum allowed: 128)', + ], + 'Fully qualified - invalid table name length' => [ + 'identifier' => 'Camorta.VeryVeryVeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryVeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar', + 'prefix' => '', + 'expectedCanonical' => '', + 'expectedMachine' => '', + 'expectedException' => 'The length of the table identifier \'VeryVeryVeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryVeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar\' once canonicalized to \'VeryVeryVeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryVeryVeryVeryVeryHungryHungryHungryHungryHungryHungryHungryCaterpillar\' is invalid (maximum allowed: 128)', + ], + ]; + } + + // cSpell:enable + + /** + * Tests table identifiers. + * + * @dataProvider providerTable + */ + public function testTable(string $identifier, string $prefix = '', ?string $expectedCanonical = '', ?string $expectedMachine = '', ?string $expectedException = NULL): void { + $handler = new IdentifierHandler($prefix); + if ($expectedException) { + $this->expectException(IdentifierException::class); + $this->expectExceptionMessage($expectedException); + } + $this->assertSame($expectedCanonical, $handler->table($identifier)->canonical()); + $this->assertSame($expectedMachine, $handler->table($identifier)->forMachine()); + // The machine name includes the quote characters so we need to subtract + // those from the length. + $this->assertLessThanOrEqual($handler->getMaxLength(IdentifierType::Table), strlen($handler->table($identifier)->machineName), 'Invalid machine table length.'); + } + +} diff --git a/core/modules/system/tests/src/Functional/System/ErrorHandlerTest.php b/core/modules/system/tests/src/Functional/System/ErrorHandlerTest.php index 82ba0c8896a18e6df18125464f9e5695b8c3a3be..44488c32a9c9f3c94477ecf3e094a9eeeec363fd 100644 --- a/core/modules/system/tests/src/Functional/System/ErrorHandlerTest.php +++ b/core/modules/system/tests/src/Functional/System/ErrorHandlerTest.php @@ -132,9 +132,7 @@ public function testExceptionHandler(): void { $message = str_replace(["\r", "\n"], ' ', $message); $error_pdo_exception = [ '%type' => 'DatabaseExceptionWrapper', - '@message' => PHP_VERSION_ID >= 80400 ? - $message : - 'SELECT "b".* FROM {bananas_are_awesome} "b"', + '@message' => PHP_VERSION_ID >= 80400 ? $message : '/SELECT "b"\.\* FROM .*bananas_are_awesome. "b"/', '%function' => 'Drupal\error_test\Controller\ErrorTestController->triggerPDOException()', '%line' => 64, '%file' => $this->getModulePath('error_test') . '/error_test.module', @@ -160,7 +158,12 @@ public function testExceptionHandler(): void { $this->assertSession()->pageTextContains($error_pdo_exception['%type']); // Assert statement improved since static queries adds table alias in the // error message. - $this->assertSession()->pageTextContains($error_pdo_exception['@message']); + if (PHP_VERSION_ID >= 80400) { + $this->assertSession()->pageTextContains($error_pdo_exception['@message']); + } + else { + $this->assertSession()->pageTextMatches($error_pdo_exception['@message']); + } $error_details = new FormattableMarkup('in %function (line ', $error_pdo_exception); $this->assertSession()->responseContains($error_details); $this->drupalGet('error-test/trigger-renderer-exception'); diff --git a/core/modules/system/tests/src/Kernel/Scripts/DbCommandBaseTest.php b/core/modules/system/tests/src/Kernel/Scripts/DbCommandBaseTest.php index 8856055b8ce55ff9fe455a1967db56838e47994d..07a841f9f4d1331002e067de15712ef560dde637 100644 --- a/core/modules/system/tests/src/Kernel/Scripts/DbCommandBaseTest.php +++ b/core/modules/system/tests/src/Kernel/Scripts/DbCommandBaseTest.php @@ -84,13 +84,13 @@ public function testPrefix(): void { '--database' => 'magic_db', '--prefix' => 'extra', ]); - $this->assertEquals('extra', $command->getDatabaseConnection($command_tester->getInput())->getPrefix()); + $this->assertEquals('extra', $command->getDatabaseConnection($command_tester->getInput())->identifiers->tablePrefix); $command_tester->execute([ '-db-url' => Database::getConnectionInfoAsUrl(), '--prefix' => 'extra2', ]); - $this->assertEquals('extra2', $command->getDatabaseConnection($command_tester->getInput())->getPrefix()); + $this->assertEquals('extra2', $command->getDatabaseConnection($command_tester->getInput())->identifiers->tablePrefix); // This breaks test cleanup. // @code diff --git a/core/modules/views_ui/tests/src/Functional/PreviewTest.php b/core/modules/views_ui/tests/src/Functional/PreviewTest.php index e0baf9193c053916d737f7d6d287d38646c65c49..ae2e8cdbe14db55673b67f10a3cd3d65618a5707 100644 --- a/core/modules/views_ui/tests/src/Functional/PreviewTest.php +++ b/core/modules/views_ui/tests/src/Functional/PreviewTest.php @@ -123,13 +123,8 @@ public function testPreviewUI(): void { $this->assertSession()->pageTextContains('Query execute time'); $this->assertSession()->pageTextContains('View render time'); $this->assertSession()->responseContains('<strong>Query</strong>'); - $query_string = <<<SQL -SELECT "views_test_data"."name" AS "views_test_data_name" -FROM -{views_test_data} "views_test_data" -WHERE (views_test_data.id = '100') -SQL; - $this->assertSession()->assertEscaped($query_string); + $query_string = '/SELECT "views_test_data"\."name" AS "views_test_data_name" FROM .*views_test_data" "views_test_data" WHERE \(views_test_data\.id = \'100\'\)/'; + $this->assertSession()->pageTextMatches($query_string); // Test that the statistics and query are rendered above the preview. $this->assertLessThan(strpos($this->getSession()->getPage()->getContent(), 'js-view-dom-id'), strpos($this->getSession()->getPage()->getContent(), 'views-query-info')); diff --git a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryNodePagePerformanceTest.php b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryNodePagePerformanceTest.php index 8718a00e7d4b66ba1472b95023acd3a181a5440e..aa2cddd606cca0a7f5e508a9a925948c82b3a3a3 100644 --- a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryNodePagePerformanceTest.php +++ b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryNodePagePerformanceTest.php @@ -279,9 +279,9 @@ protected function testNodePageWarmCache(): void { 'SELECT "t".* FROM "node_revision__field_tags" "t" WHERE ("revision_id" IN ("75")) AND ("deleted" = 0) AND ("langcode" IN ("en", "es", "und", "zxx")) ORDER BY "delta" ASC', 'SELECT "t".* FROM "node_revision__layout_builder__layout" "t" WHERE ("revision_id" IN ("75")) AND ("deleted" = 0) AND ("langcode" IN ("en", "es", "und", "zxx")) ORDER BY "delta" ASC', 'INSERT INTO "semaphore" ("name", "value", "expire") VALUES ("theme_registry:runtime:umami:Drupal\Core\Utility\ThemeRegistry", "LOCK_ID", "EXPIRE")', - 'DELETE FROM "semaphore" WHERE ("name" = "theme_registry:runtime:umami:Drupal\Core\Utility\ThemeRegistry") AND ("value" = "LOCK_ID")', + 'DELETE FROM "semaphore" WHERE ("name" = "theme_registry:runtime:umami:Drupal\Core\Utility\ThemeRegistry") AND ("value" = "LOCK_ID")', 'INSERT INTO "semaphore" ("name", "value", "expire") VALUES ("active-trail:route:entity.node.canonical:route_parameters:a:1:{s:4:"node";s:1:"1";}:Drupal\Core\Cache\CacheCollector", "LOCK_ID", "EXPIRE")', - 'DELETE FROM "semaphore" WHERE ("name" = "active-trail:route:entity.node.canonical:route_parameters:a:1:{s:4:"node";s:1:"1";}:Drupal\Core\Cache\CacheCollector") AND ("value" = "LOCK_ID")', + 'DELETE FROM "semaphore" WHERE ("name" = "active-trail:route:entity.node.canonical:route_parameters:a:1:{s:4:"node";s:1:"1";}:Drupal\Core\Cache\CacheCollector") AND ("value" = "LOCK_ID")', ]; $recorded_queries = $performance_data->getQueries(); $this->assertSame($expected_queries, $recorded_queries); diff --git a/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php b/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php index 036447895e2730644369dbc3e20ae659f0e2053a..dacaad0474dcfca4637b190b28acfd6149d41b75 100644 --- a/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php +++ b/core/profiles/standard/tests/src/FunctionalJavascript/StandardPerformanceTest.php @@ -115,13 +115,13 @@ protected function testAnonymous(): void { 'SELECT "menu_tree"."menu_name" AS "menu_name", "menu_tree"."route_name" AS "route_name", "menu_tree"."route_parameters" AS "route_parameters", "menu_tree"."url" AS "url", "menu_tree"."title" AS "title", "menu_tree"."description" AS "description", "menu_tree"."parent" AS "parent", "menu_tree"."weight" AS "weight", "menu_tree"."options" AS "options", "menu_tree"."expanded" AS "expanded", "menu_tree"."enabled" AS "enabled", "menu_tree"."provider" AS "provider", "menu_tree"."metadata" AS "metadata", "menu_tree"."class" AS "class", "menu_tree"."form_class" AS "form_class", "menu_tree"."id" AS "id" FROM "menu_tree" "menu_tree" WHERE ("route_name" = "view.frontpage.page_1") AND ("route_param_key" = "view_id=frontpage&display_id=page_1") AND ("menu_name" = "main") ORDER BY "depth" ASC, "weight" ASC, "id" ASC', 'SELECT "menu_tree"."menu_name" AS "menu_name", "menu_tree"."route_name" AS "route_name", "menu_tree"."route_parameters" AS "route_parameters", "menu_tree"."url" AS "url", "menu_tree"."title" AS "title", "menu_tree"."description" AS "description", "menu_tree"."parent" AS "parent", "menu_tree"."weight" AS "weight", "menu_tree"."options" AS "options", "menu_tree"."expanded" AS "expanded", "menu_tree"."enabled" AS "enabled", "menu_tree"."provider" AS "provider", "menu_tree"."metadata" AS "metadata", "menu_tree"."class" AS "class", "menu_tree"."form_class" AS "form_class", "menu_tree"."id" AS "id" FROM "menu_tree" "menu_tree" WHERE ("route_name" = "view.frontpage.page_1") AND ("route_param_key" = "view_id=frontpage&display_id=page_1") AND ("menu_name" = "account") ORDER BY "depth" ASC, "weight" ASC, "id" ASC', 'INSERT INTO "semaphore" ("name", "value", "expire") VALUES ("theme_registry:runtime:stark:Drupal\Core\Utility\ThemeRegistry", "LOCK_ID", "EXPIRE")', - 'DELETE FROM "semaphore" WHERE ("name" = "theme_registry:runtime:stark:Drupal\Core\Utility\ThemeRegistry") AND ("value" = "LOCK_ID")', + 'DELETE FROM "semaphore" WHERE ("name" = "theme_registry:runtime:stark:Drupal\Core\Utility\ThemeRegistry") AND ("value" = "LOCK_ID")', 'INSERT INTO "semaphore" ("name", "value", "expire") VALUES ("library_info:stark:Drupal\Core\Cache\CacheCollector", "LOCK_ID", "EXPIRE")', - 'DELETE FROM "semaphore" WHERE ("name" = "library_info:stark:Drupal\Core\Cache\CacheCollector") AND ("value" = "LOCK_ID")', + 'DELETE FROM "semaphore" WHERE ("name" = "library_info:stark:Drupal\Core\Cache\CacheCollector") AND ("value" = "LOCK_ID")', 'INSERT INTO "semaphore" ("name", "value", "expire") VALUES ("path_alias_prefix_list:Drupal\Core\Cache\CacheCollector", "LOCK_ID", "EXPIRE")', - 'DELETE FROM "semaphore" WHERE ("name" = "path_alias_prefix_list:Drupal\Core\Cache\CacheCollector") AND ("value" = "LOCK_ID")', + 'DELETE FROM "semaphore" WHERE ("name" = "path_alias_prefix_list:Drupal\Core\Cache\CacheCollector") AND ("value" = "LOCK_ID")', 'INSERT INTO "semaphore" ("name", "value", "expire") VALUES ("active-trail:route:view.frontpage.page_1:route_parameters:a:2:{s:10:"display_id";s:6:"page_1";s:7:"view_id";s:9:"frontpage";}:Drupal\Core\Cache\CacheCollector", "LOCK_ID", "EXPIRE")', - 'DELETE FROM "semaphore" WHERE ("name" = "active-trail:route:view.frontpage.page_1:route_parameters:a:2:{s:10:"display_id";s:6:"page_1";s:7:"view_id";s:9:"frontpage";}:Drupal\Core\Cache\CacheCollector") AND ("value" = "LOCK_ID")', + 'DELETE FROM "semaphore" WHERE ("name" = "active-trail:route:view.frontpage.page_1:route_parameters:a:2:{s:10:"display_id";s:6:"page_1";s:7:"view_id";s:9:"frontpage";}:Drupal\Core\Cache\CacheCollector") AND ("value" = "LOCK_ID")', ]; $recorded_queries = $performance_data->getQueries(); $this->assertSame($expected_queries, $recorded_queries); @@ -217,9 +217,9 @@ protected function testAnonymous(): void { 'SELECT "menu_tree"."menu_name" AS "menu_name", "menu_tree"."route_name" AS "route_name", "menu_tree"."route_parameters" AS "route_parameters", "menu_tree"."url" AS "url", "menu_tree"."title" AS "title", "menu_tree"."description" AS "description", "menu_tree"."parent" AS "parent", "menu_tree"."weight" AS "weight", "menu_tree"."options" AS "options", "menu_tree"."expanded" AS "expanded", "menu_tree"."enabled" AS "enabled", "menu_tree"."provider" AS "provider", "menu_tree"."metadata" AS "metadata", "menu_tree"."class" AS "class", "menu_tree"."form_class" AS "form_class", "menu_tree"."id" AS "id" FROM "menu_tree" "menu_tree" WHERE ("route_name" = "entity.node.canonical") AND ("route_param_key" = "node=1") AND ("menu_name" = "main") ORDER BY "depth" ASC, "weight" ASC, "id" ASC', 'SELECT "menu_tree"."menu_name" AS "menu_name", "menu_tree"."route_name" AS "route_name", "menu_tree"."route_parameters" AS "route_parameters", "menu_tree"."url" AS "url", "menu_tree"."title" AS "title", "menu_tree"."description" AS "description", "menu_tree"."parent" AS "parent", "menu_tree"."weight" AS "weight", "menu_tree"."options" AS "options", "menu_tree"."expanded" AS "expanded", "menu_tree"."enabled" AS "enabled", "menu_tree"."provider" AS "provider", "menu_tree"."metadata" AS "metadata", "menu_tree"."class" AS "class", "menu_tree"."form_class" AS "form_class", "menu_tree"."id" AS "id" FROM "menu_tree" "menu_tree" WHERE ("route_name" = "entity.node.canonical") AND ("route_param_key" = "node=1") AND ("menu_name" = "account") ORDER BY "depth" ASC, "weight" ASC, "id" ASC', 'INSERT INTO "semaphore" ("name", "value", "expire") VALUES ("theme_registry:runtime:stark:Drupal\Core\Utility\ThemeRegistry", "LOCK_ID", "EXPIRE")', - 'DELETE FROM "semaphore" WHERE ("name" = "theme_registry:runtime:stark:Drupal\Core\Utility\ThemeRegistry") AND ("value" = "LOCK_ID")', + 'DELETE FROM "semaphore" WHERE ("name" = "theme_registry:runtime:stark:Drupal\Core\Utility\ThemeRegistry") AND ("value" = "LOCK_ID")', 'INSERT INTO "semaphore" ("name", "value", "expire") VALUES ("active-trail:route:entity.node.canonical:route_parameters:a:1:{s:4:"node";s:1:"1";}:Drupal\Core\Cache\CacheCollector", "LOCK_ID", "EXPIRE")', - 'DELETE FROM "semaphore" WHERE ("name" = "active-trail:route:entity.node.canonical:route_parameters:a:1:{s:4:"node";s:1:"1";}:Drupal\Core\Cache\CacheCollector") AND ("value" = "LOCK_ID")', + 'DELETE FROM "semaphore" WHERE ("name" = "active-trail:route:entity.node.canonical:route_parameters:a:1:{s:4:"node";s:1:"1";}:Drupal\Core\Cache\CacheCollector") AND ("value" = "LOCK_ID")', ]; $recorded_queries = $performance_data->getQueries(); $this->assertSame($expected_queries, $recorded_queries); @@ -300,7 +300,7 @@ protected function testAnonymous(): void { 'SELECT "ud".* FROM "users_data" "ud" WHERE ("module" = "contact") AND ("uid" = "2") AND ("name" = "enabled")', 'SELECT "name", "data" FROM "config" WHERE "collection" = "" AND "name" IN ( "contact.settings" )', 'INSERT INTO "semaphore" ("name", "value", "expire") VALUES ("active-trail:route:entity.user.canonical:route_parameters:a:1:{s:4:"user";s:1:"2";}:Drupal\Core\Cache\CacheCollector", "LOCK_ID", "EXPIRE")', - 'DELETE FROM "semaphore" WHERE ("name" = "active-trail:route:entity.user.canonical:route_parameters:a:1:{s:4:"user";s:1:"2";}:Drupal\Core\Cache\CacheCollector") AND ("value" = "LOCK_ID")', + 'DELETE FROM "semaphore" WHERE ("name" = "active-trail:route:entity.user.canonical:route_parameters:a:1:{s:4:"user";s:1:"2";}:Drupal\Core\Cache\CacheCollector") AND ("value" = "LOCK_ID")', ]; $recorded_queries = $performance_data->getQueries(); $this->assertSame($expected_queries, $recorded_queries); diff --git a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSchemaTestBase.php b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSchemaTestBase.php index ca5fb32936b55bc2ec02c054ea0eb8fc02187227..f2efcd9cecf94d92e2710ed7ba3b4e760972bc0d 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSchemaTestBase.php +++ b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSchemaTestBase.php @@ -255,7 +255,7 @@ public function testSchema(): void { // Test the primary key columns. $method = new \ReflectionMethod(get_class($this->schema), 'findPrimaryKeyColumns'); - $this->assertSame(['test_serial'], $method->invoke($this->schema, 'test_table')); + $this->assertSame(['test_serial'], $method->invoke($this->schema, $this->connection->identifiers->table('test_table'))); $this->assertTrue($this->tryInsert(), 'Insert with a serial succeeded.'); $max1 = $this->connection->query('SELECT MAX([test_serial]) FROM {test_table}')->fetchField(); @@ -270,7 +270,7 @@ public function testSchema(): void { $this->schema->addField('test_table', 'test_composite_primary_key', ['type' => 'int', 'not null' => TRUE, 'default' => 0], ['primary key' => ['test_serial', 'test_composite_primary_key']]); // Test the primary key columns. - $this->assertSame(['test_serial', 'test_composite_primary_key'], $method->invoke($this->schema, 'test_table')); + $this->assertSame(['test_serial', 'test_composite_primary_key'], $method->invoke($this->schema, $this->connection->identifiers->table('test_table'))); // Test renaming of keys and constraints. $this->schema->dropTable('test_table'); @@ -295,21 +295,21 @@ public function testSchema(): void { // SQLite does not have any limit. Use the lowest common value and create a // table name as long as possible in order to cover edge cases around // identifier names for the table's primary or unique key constraints. - $table_name = strtolower($this->getRandomGenerator()->name(63 - strlen($this->getDatabasePrefix()))); - $this->schema->createTable($table_name, $table_specification); + $table = $this->connection->identifiers->table(strtolower($this->getRandomGenerator()->name(63 - strlen($this->getDatabasePrefix())))); + $this->schema->createTable($table, $table_specification); - $this->assertIndexOnColumns($table_name, ['id'], 'primary'); - $this->assertIndexOnColumns($table_name, ['test_field'], 'unique'); + $this->assertIndexOnColumns($table, ['id'], 'primary'); + $this->assertIndexOnColumns($table, ['test_field'], 'unique'); - $new_table_name = strtolower($this->getRandomGenerator()->name(63 - strlen($this->getDatabasePrefix()))); - $this->assertNull($this->schema->renameTable($table_name, $new_table_name)); + $new_table = $this->connection->identifiers->table(strtolower($this->getRandomGenerator()->name(63 - strlen($this->getDatabasePrefix())))); + $this->assertNull($this->schema->renameTable($table, $new_table)); // Test for renamed primary and unique keys. - $this->assertIndexOnColumns($new_table_name, ['id'], 'primary'); - $this->assertIndexOnColumns($new_table_name, ['test_field'], 'unique'); + $this->assertIndexOnColumns($new_table, ['id'], 'primary'); + $this->assertIndexOnColumns($new_table, ['test_field'], 'unique'); // Check that the ID sequence gets renamed when the table is renamed. - $this->checkSequenceRenaming($new_table_name); + $this->checkSequenceRenaming($new_table->identifier); } /** @@ -582,7 +582,7 @@ public function testSchemaChangePrimaryKey(array $initial_primary_key, array $re $find_primary_key_columns = new \ReflectionMethod(get_class($this->schema), 'findPrimaryKeyColumns'); // Test making the field the primary key of the table upon creation. - $table_name = 'test_table'; + $table_name = $this->connection->identifiers->table('test_table'); $table_spec = [ 'fields' => [ 'test_field' => ['type' => 'int', 'not null' => TRUE], @@ -908,7 +908,7 @@ public function testFindPrimaryKeyColumns(): void { ], 'primary key' => ['id'], ]); - $this->assertSame(['id'], $method->invoke($this->schema, 'table_with_pk_0')); + $this->assertSame(['id'], $method->invoke($this->schema, $this->connection->identifiers->table('table_with_pk_0'))); // Test with multiple column primary key. $this->schema->createTable('table_with_pk_1', [ @@ -929,7 +929,7 @@ public function testFindPrimaryKeyColumns(): void { ], 'primary key' => ['id0', 'id1'], ]); - $this->assertSame(['id0', 'id1'], $method->invoke($this->schema, 'table_with_pk_1')); + $this->assertSame(['id0', 'id1'], $method->invoke($this->schema, $this->connection->identifiers->table('table_with_pk_1'))); // Test with multiple column primary key and not being the first column of // the table definition. @@ -955,7 +955,7 @@ public function testFindPrimaryKeyColumns(): void { ], 'primary key' => ['id4', 'id3'], ]); - $this->assertSame(['id4', 'id3'], $method->invoke($this->schema, 'table_with_pk_2')); + $this->assertSame(['id4', 'id3'], $method->invoke($this->schema, $this->connection->identifiers->table('table_with_pk_2'))); // Test with multiple column primary key in a different order. For the // PostgreSQL and the SQLite drivers is sorting used to get the primary key @@ -982,7 +982,7 @@ public function testFindPrimaryKeyColumns(): void { ], 'primary key' => ['id3', 'test_field_2', 'id4'], ]); - $this->assertSame(['id3', 'test_field_2', 'id4'], $method->invoke($this->schema, 'table_with_pk_3')); + $this->assertSame(['id3', 'test_field_2', 'id4'], $method->invoke($this->schema, $this->connection->identifiers->table('table_with_pk_3'))); // Test with table without a primary key. $this->schema->createTable('table_without_pk_1', [ @@ -998,7 +998,7 @@ public function testFindPrimaryKeyColumns(): void { ], ], ]); - $this->assertSame([], $method->invoke($this->schema, 'table_without_pk_1')); + $this->assertSame([], $method->invoke($this->schema, $this->connection->identifiers->table('table_without_pk_1'))); // Test with table with an empty primary key. $this->schema->createTable('table_without_pk_2', [ @@ -1015,10 +1015,10 @@ public function testFindPrimaryKeyColumns(): void { ], 'primary key' => [], ]); - $this->assertSame([], $method->invoke($this->schema, 'table_without_pk_2')); + $this->assertSame([], $method->invoke($this->schema, $this->connection->identifiers->table('table_without_pk_2'))); // Test with non existing table. - $this->assertFalse($method->invoke($this->schema, 'non_existing_table')); + $this->assertFalse($method->invoke($this->schema, $this->connection->identifiers->table('non_existing_table'))); } /** @@ -1271,7 +1271,7 @@ public function testReservedKeywordsForNaming(): void { // Check the primary key columns. $find_primary_key_columns = new \ReflectionMethod(get_class($this->schema), 'findPrimaryKeyColumns'); - $this->assertEquals([$field_name], $find_primary_key_columns->invoke($this->schema, $table_name_new)); + $this->assertEquals([$field_name], $find_primary_key_columns->invoke($this->schema, $this->connection->identifiers->table($table_name_new))); // Dropping a primary key. $this->schema->dropPrimaryKey($table_name_new); @@ -1288,7 +1288,7 @@ public function testReservedKeywordsForNaming(): void { // Check the unique key columns. $introspect_index_schema = new \ReflectionMethod(get_class($this->schema), 'introspectIndexSchema'); - $this->assertEquals([$field_name_new], $introspect_index_schema->invoke($this->schema, $table_name_new)['unique keys'][$unique_key_introspect_name]); + $this->assertEquals([$field_name_new], $introspect_index_schema->invoke($this->schema, $this->connection->identifiers->table($table_name_new))['unique keys'][$unique_key_introspect_name]); // Dropping an unique key $this->schema->dropUniqueKey($table_name_new, $unique_key_name); @@ -1303,7 +1303,7 @@ public function testReservedKeywordsForNaming(): void { $this->assertTrue($this->schema->indexExists($table_name_new, $index_name)); // Check the index columns. - $this->assertEquals(['update'], $introspect_index_schema->invoke($this->schema, $table_name_new)['indexes'][$index_introspect_name]); + $this->assertEquals(['update'], $introspect_index_schema->invoke($this->schema, $this->connection->identifiers->table($table_name_new))['indexes'][$index_introspect_name]); // Dropping an index. $this->schema->dropIndex($table_name_new, $index_name); diff --git a/core/tests/Drupal/Tests/Core/Database/ConnectionLegacyTest.php b/core/tests/Drupal/Tests/Core/Database/ConnectionLegacyTest.php new file mode 100644 index 0000000000000000000000000000000000000000..f298be160dc6cfa44cf63ee67b5f17b732ec0ab4 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Database/ConnectionLegacyTest.php @@ -0,0 +1,81 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\Core\Database; + +use Drupal\Tests\Core\Database\Stub\StubLegacyConnection; +use Drupal\Tests\Core\Database\Stub\StubPDO; +use Drupal\Tests\UnitTestCase; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; + +/** + * Tests deprecations of the Connection class. + * + * @group Database + */ +class ConnectionLegacyTest extends UnitTestCase { + + /** + * Tests that missing IdentifierHandler throws a deprecation. + */ + #[IgnoreDeprecations] + public function testMissingIdentifierHandler(): void { + $this->expectDeprecation('Not passing an IdentifierHandler object to Drupal\\Core\\Database\\Connection::__construct() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. See https://www.drupal.org/node/3513282'); + $connection = new StubLegacyConnection($this->createMock(StubPDO::class), ['prefix' => 'foo']); + $this->assertInstanceOf(StubLegacyConnection::class, $connection); + } + + /** + * Deprecation of Connection::$prefix. + */ + #[IgnoreDeprecations] + public function testPrefix(): void { + $this->expectDeprecation('Accessing Connection::$prefix is deprecated in drupal:11.2.0 and the property is removed from drupal:12.0.0. Use IdentifierHandler methods instead. See https://www.drupal.org/node/3513282'); + $connection = new StubLegacyConnection($this->createMock(StubPDO::class), ['prefix' => 'foo']); + $this->assertSame('foo', $connection->prefix); + $this->expectDeprecation('Accessing Connection::$prefix is deprecated in drupal:11.2.0 and the property is removed from drupal:12.0.0. Use IdentifierHandler methods instead. See https://www.drupal.org/node/3513282'); + $connection->prefix = 'bar'; + $this->assertSame('foo', $connection->prefix); + } + + /** + * Deprecation of Connection::$escapedTables. + */ + #[IgnoreDeprecations] + public function testEscapedTables(): void { + $this->expectDeprecation('Accessing Connection::$escapedTables is deprecated in drupal:11.2.0 and the property is removed from drupal:12.0.0. Use IdentifierHandler methods instead. See https://www.drupal.org/node/3513282'); + $connection = new StubLegacyConnection($this->createMock(StubPDO::class), ['prefix' => 'foo']); + $this->assertSame([], $connection->escapedTables); + $this->expectDeprecation('Accessing Connection::$escapedTables is deprecated in drupal:11.2.0 and the property is removed from drupal:12.0.0. Use IdentifierHandler methods instead. See https://www.drupal.org/node/3513282'); + $connection->escapedTables = 'bar'; + $this->assertSame([], $connection->escapedTables); + } + + /** + * Deprecation of Connection::$identifierQuotes. + */ + #[IgnoreDeprecations] + public function testIdentifierQuotes(): void { + $this->expectDeprecation('Accessing Connection::$identifierQuotes is deprecated in drupal:11.2.0 and the property is removed from drupal:12.0.0. Use IdentifierHandler methods instead. See https://www.drupal.org/node/3513282'); + $connection = new StubLegacyConnection($this->createMock(StubPDO::class), ['prefix' => 'foo']); + $this->assertSame(['"', '"'], $connection->identifierQuotes); + $this->expectDeprecation('Accessing Connection::$identifierQuotes is deprecated in drupal:11.2.0 and the property is removed from drupal:12.0.0. Use IdentifierHandler methods instead. See https://www.drupal.org/node/3513282'); + $connection->identifierQuotes = 'bar'; + $this->assertSame(['"', '"'], $connection->identifierQuotes); + } + + /** + * Deprecation of Connection::$tablePlaceholderReplacements. + */ + #[IgnoreDeprecations] + public function testTablePlaceholderReplacements(): void { + $this->expectDeprecation('Accessing Connection::$tablePlaceholderReplacements is deprecated in drupal:11.2.0 and the property is removed from drupal:12.0.0. Use IdentifierHandler methods instead. See https://www.drupal.org/node/3513282'); + $connection = new StubLegacyConnection($this->createMock(StubPDO::class), ['prefix' => 'foo']); + $this->assertSame(['"foo', '"'], $connection->tablePlaceholderReplacements); + $this->expectDeprecation('Accessing Connection::$tablePlaceholderReplacements is deprecated in drupal:11.2.0 and the property is removed from drupal:12.0.0. Use IdentifierHandler methods instead. See https://www.drupal.org/node/3513282'); + $connection->tablePlaceholderReplacements = 'bar'; + $this->assertSame(['"foo', '"'], $connection->tablePlaceholderReplacements); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php b/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php index f9b25e8e70723b7dac051c6fd68526fbc23231b0..0b307223bf34803a9ad009e91a4c9605d85862fb 100644 --- a/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php +++ b/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php @@ -30,7 +30,7 @@ class ConnectionTest extends UnitTestCase { * - Arguments to pass to Connection::setPrefix(). * - Expected result from Connection::getPrefix(). */ - public static function providerPrefixRoundTrip() { + public static function providerPrefixRoundTrip(): array { return [ [ [ @@ -49,16 +49,19 @@ public static function providerPrefixRoundTrip() { } /** - * Exercise setPrefix() and getPrefix(). - * - * @dataProvider providerPrefixRoundTrip + * Exercise legacy setPrefix() and getPrefix(). */ + #[IgnoreDeprecations] + #[DataProvider('providerPrefixRoundTrip')] public function testPrefixRoundTrip($expected, $prefix_info): void { - $mock_pdo = $this->createMock('Drupal\Tests\Core\Database\Stub\StubPDO'); - $connection = new StubConnection($mock_pdo, []); + $this->expectDeprecation('Drupal\\Core\\Database\\Connection::setPrefix() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Pass the table prefix to the IdentifierHandler constructor instead. See https://www.drupal.org/node/3513282'); + $this->expectDeprecation('Drupal\\Core\\Database\\Connection::getPrefix() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use IdentifierHandler methods instead. See https://www.drupal.org/node/3513282'); + + $mock_pdo = $this->createMock(StubPDO::class); + $connection = new StubConnection($mock_pdo, ['prefix' => $prefix_info]); // setPrefix() is protected, so we make it accessible with reflection. - $reflection = new \ReflectionClass('Drupal\Tests\Core\Database\Stub\StubConnection'); + $reflection = new \ReflectionClass(StubConnection::class); $set_prefix = $reflection->getMethod('setPrefix'); // Set the prefix data. @@ -458,7 +461,7 @@ public function testFilterComments($expected, $comment): void { * testEscapeField. The first value is the expected value, and the second * value is the value to test. */ - public static function providerEscapeTables() { + public static function providerEscapeTables(): array { return [ ['nocase', 'nocase'], ['camelCase', 'camelCase'], @@ -475,10 +478,12 @@ public static function providerEscapeTables() { } /** - * @covers ::escapeTable - * @dataProvider providerEscapeTables + * Tests legacy ::escapeTable. */ + #[IgnoreDeprecations] + #[DataProvider('providerEscapeTables')] public function testEscapeTable($expected, $name, array $identifier_quote = ['"', '"']): void { + $this->expectDeprecation('Drupal\\Core\\Database\\Connection::escapeTable() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use IdentifierHandler methods instead. See https://www.drupal.org/node/3513282'); $mock_pdo = $this->createMock(StubPDO::class); $connection = new StubConnection($mock_pdo, [], $identifier_quote); @@ -558,7 +563,7 @@ public function testEscapeField($expected, $name, array $identifier_quote = ['"' * testEscapeField. The first value is the expected value, and the second * value is the value to test. */ - public static function providerEscapeDatabase() { + public static function providerEscapeDatabase(): array { return [ ['/name/', 'name', ['/', '/']], ['`backtick`', 'backtick', ['`', '`']], @@ -569,10 +574,12 @@ public static function providerEscapeDatabase() { } /** - * @covers ::escapeDatabase - * @dataProvider providerEscapeDatabase + * Tests legacy ::escapeDatabase. */ + #[IgnoreDeprecations] + #[DataProvider('providerEscapeDatabase')] public function testEscapeDatabase($expected, $name, array $identifier_quote = ['"', '"']): void { + $this->expectDeprecation('Drupal\\Core\\Database\\Connection::escapeDatabase() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use IdentifierHandler methods instead. See https://www.drupal.org/node/3513282'); $mock_pdo = $this->createMock(StubPDO::class); $connection = new StubConnection($mock_pdo, [], $identifier_quote); @@ -580,21 +587,21 @@ public function testEscapeDatabase($expected, $name, array $identifier_quote = [ } /** - * @covers ::__construct + * Tests identifier quotes. */ public function testIdentifierQuotesAssertCount(): void { $this->expectException(\AssertionError::class); - $this->expectExceptionMessage('\Drupal\Core\Database\Connection::$identifierQuotes must contain 2 string values'); + $this->expectExceptionMessage('Drupal\\Core\\Database\\Identifier\\IdentifierHandlerBase::$identifierQuotes must contain 2 string values'); $mock_pdo = $this->createMock(StubPDO::class); new StubConnection($mock_pdo, [], ['"']); } /** - * @covers ::__construct + * Tests identifier quotes. */ public function testIdentifierQuotesAssertString(): void { $this->expectException(\AssertionError::class); - $this->expectExceptionMessage('\Drupal\Core\Database\Connection::$identifierQuotes must contain 2 string values'); + $this->expectExceptionMessage('Drupal\\Core\\Database\\Identifier\\IdentifierHandlerBase::$identifierQuotes must contain 2 string values'); $mock_pdo = $this->createMock(StubPDO::class); new StubConnection($mock_pdo, [], [0, '1']); } diff --git a/core/tests/Drupal/Tests/Core/Database/SchemaIntrospectionTestTrait.php b/core/tests/Drupal/Tests/Core/Database/SchemaIntrospectionTestTrait.php index 2168fdb2a112bef977d57c772d3cc3f15c0776b0..9cb02730939c42bf6fe2e0fbdd58456cfe7fd134 100644 --- a/core/tests/Drupal/Tests/Core/Database/SchemaIntrospectionTestTrait.php +++ b/core/tests/Drupal/Tests/Core/Database/SchemaIntrospectionTestTrait.php @@ -4,6 +4,8 @@ namespace Drupal\Tests\Core\Database; +use Drupal\Core\Database\Identifier\Table as TableIdentifier; + /** * Provides methods for testing database schema characteristics. */ @@ -21,6 +23,15 @@ trait SchemaIntrospectionTestTrait { * 'primary'. Defaults to 'index'. */ protected function assertIndexOnColumns($table_name, array $column_names, $index_type = 'index') { + if (!$table_name instanceof TableIdentifier) { + @trigger_error("Passing a table identifier as a string to " . __METHOD__ . "() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Pass a Table identifier value object instead. See https://www.drupal.org/node/7654132", E_USER_DEPRECATED); + $table = $this->connection->identifiers->table($table_name); + } + else { + $table = $table_name; + } + assert($table instanceof TableIdentifier); + foreach ($this->getIndexColumnNames($table_name, $index_type) as $index_columns) { if ($column_names == $index_columns) { $this->assertTrue(TRUE); @@ -42,7 +53,16 @@ protected function assertIndexOnColumns($table_name, array $column_names, $index * 'primary'. Defaults to 'index'. */ protected function assertNoIndexOnColumns($table_name, array $column_names, $index_type = 'index') { - foreach ($this->getIndexColumnNames($table_name, $index_type) as $index_columns) { + if (!$table_name instanceof TableIdentifier) { + @trigger_error("Passing a table identifier as a string to " . __METHOD__ . "() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Pass a Table identifier value object instead. See https://www.drupal.org/node/7654132", E_USER_DEPRECATED); + $table = $this->connection->identifiers->table($table_name); + } + else { + $table = $table_name; + } + assert($table instanceof TableIdentifier); + + foreach ($this->getIndexColumnNames($table, $index_type) as $index_columns) { if ($column_names == $index_columns) { $this->assertTrue(FALSE); } @@ -63,11 +83,20 @@ protected function assertNoIndexOnColumns($table_name, array $column_names, $ind * the given type. */ protected function getIndexColumnNames($table_name, $index_type) { + if (!$table_name instanceof TableIdentifier) { + @trigger_error("Passing a table identifier as a string to " . __METHOD__ . "() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Pass a Table identifier value object instead. See https://www.drupal.org/node/7654132", E_USER_DEPRECATED); + $table = $this->connection->identifiers->table($table_name); + } + else { + $table = $table_name; + } + assert($table instanceof TableIdentifier); + assert(in_array($index_type, ['index', 'unique', 'primary'], TRUE)); $schema = \Drupal::database()->schema(); $introspect_index_schema = new \ReflectionMethod(get_class($schema), 'introspectIndexSchema'); - $index_schema = $introspect_index_schema->invoke($schema, $table_name); + $index_schema = $introspect_index_schema->invoke($schema, $table); // Filter the indexes by type. if ($index_type === 'primary') { diff --git a/core/tests/Drupal/Tests/Core/Database/Stub/StubConnection.php b/core/tests/Drupal/Tests/Core/Database/Stub/StubConnection.php index 4b31c06c2e6711c2d92eea1932feb03eea283400..3b57f4a5f6f84e94a6f4e205a9f9a8950b932483 100644 --- a/core/tests/Drupal/Tests/Core/Database/Stub/StubConnection.php +++ b/core/tests/Drupal/Tests/Core/Database/Stub/StubConnection.php @@ -41,8 +41,11 @@ class StubConnection extends Connection { * The identifier quote characters. Defaults to an empty strings. */ public function __construct(\PDO $connection, array $connection_options, $identifier_quotes = ['', '']) { - $this->identifierQuotes = $identifier_quotes; - parent::__construct($connection, $connection_options); + parent::__construct( + $connection, + $connection_options, + new StubIdentifierHandler($connection_options['prefix'] ?? '', $identifier_quotes), + ); } /** diff --git a/core/tests/Drupal/Tests/Core/Database/Stub/StubIdentifierHandler.php b/core/tests/Drupal/Tests/Core/Database/Stub/StubIdentifierHandler.php new file mode 100644 index 0000000000000000000000000000000000000000..b3d4e0b66fb87fd6440f1814e4b526c6aa24c1be --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Database/Stub/StubIdentifierHandler.php @@ -0,0 +1,22 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\Core\Database\Stub; + +use Drupal\Core\Database\Identifier\IdentifierHandlerBase; +use Drupal\Core\Database\Identifier\IdentifierType; + +/** + * A stub of the abstract IdentifierHandlerBase class for testing purposes. + */ +class StubIdentifierHandler extends IdentifierHandlerBase { + + /** + * {@inheritdoc} + */ + public function getMaxLength(IdentifierType $type): int { + return 4000; + } + +} diff --git a/core/tests/Drupal/Tests/Core/Database/Stub/StubLegacyConnection.php b/core/tests/Drupal/Tests/Core/Database/Stub/StubLegacyConnection.php new file mode 100644 index 0000000000000000000000000000000000000000..cef6b9befebd1ce94a511d06707882d3845ab43a --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Database/Stub/StubLegacyConnection.php @@ -0,0 +1,129 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\Core\Database\Stub; + +use Drupal\Core\Database\Connection; +use Drupal\Core\Database\ExceptionHandler; +use Drupal\Core\Database\Log; +use Drupal\Core\Database\StatementWrapperIterator; +use Drupal\Tests\Core\Database\Stub\Driver\Schema; + +/** + * A stub of the abstract Connection class for testing purposes. + * + * Includes minimal implementations of Connection's abstract methods. + */ +class StubLegacyConnection extends Connection { + + /** + * {@inheritdoc} + */ + protected $statementWrapperClass = StatementWrapperIterator::class; + + /** + * Public property so we can test driver loading mechanism. + * + * @var string + * @see driver(). + */ + public $driver = 'stub'; + + /** + * Constructs a Connection object. + * + * @param \PDO $connection + * An object of the PDO class representing a database connection. + * @param array $connection_options + * An array of options for the connection. + */ + public function __construct(\PDO $connection, array $connection_options) { + parent::__construct( + $connection, + $connection_options, + ); + } + + /** + * {@inheritdoc} + */ + public static function open(array &$connection_options = []) { + return new \stdClass(); + } + + /** + * {@inheritdoc} + */ + public function queryRange($query, $from, $count, array $args = [], array $options = []) { + return NULL; + } + + /** + * {@inheritdoc} + */ + public function driver() { + return $this->driver; + } + + /** + * {@inheritdoc} + */ + public function databaseType() { + return 'stub'; + } + + /** + * {@inheritdoc} + */ + public function createDatabase($database) {} + + /** + * {@inheritdoc} + */ + public function mapConditionOperator($operator) { + return NULL; + } + + /** + * Helper method to test database classes are not included in backtraces. + * + * @return array + * The caller stack entry. + */ + public function testLogCaller() { + return (new Log())->findCaller(); + } + + /** + * {@inheritdoc} + */ + public function exceptionHandler() { + return new ExceptionHandler(); + } + + /** + * {@inheritdoc} + */ + public function upsert($table, array $options = []) { + return new StubUpsert($this, $table, $options); + } + + /** + * {@inheritdoc} + */ + public function schema() { + if (empty($this->schema)) { + $this->schema = new Schema(); + } + return $this->schema; + } + + /** + * {@inheritdoc} + */ + public function condition($conjunction) { + return new StubCondition($conjunction); + } + +}