Commit d79e4a65 authored by catch's avatar catch
Browse files

Issue #3106531 by mondrake, daffie, Rithesh BK, catch, xjm: Notify in Status...

Issue #3106531 by mondrake, daffie, Rithesh BK, catch, xjm: Notify in Status Report that per-table database prefixes are no longer supported, and will throw errors in Drupal 10.0
parent e1ab1133
......@@ -138,50 +138,17 @@
* request as needed. The fourth line creates a new database with a name of
* "extra".
*
* You can optionally set prefixes for some or all database table names
* by using the 'prefix' setting. If a prefix is specified, the table
* name will be prepended with its value. Be sure to use valid database
* characters only, usually alphanumeric and underscore. If no prefixes
* are desired, leave it as an empty string ''.
* You can optionally set a prefix for all database table names by using the
* 'prefix' setting. If a prefix is specified, the table name will be prepended
* with its value. Be sure to use valid database characters only, usually
* alphanumeric and underscore. If no prefix is desired, do not set the 'prefix'
* key or set its value to an empty string ''.
*
* To have all database names prefixed, set 'prefix' as a string:
* For example, to have all database table prefixed with 'main_', set:
* @code
* 'prefix' => 'main_',
* @endcode
*
* Per-table prefixes are deprecated as of Drupal 8.2, and will be removed in
* Drupal 9.0. After that, only a single prefix for all tables will be
* supported.
*
* To provide prefixes for specific tables, set 'prefix' as an array.
* The array's keys are the table names and the values are the prefixes.
* The 'default' element is mandatory and holds the prefix for any tables
* not specified elsewhere in the array. Example:
* @code
* 'prefix' => [
* 'default' => 'main_',
* 'users' => 'shared_',
* 'sessions' => 'shared_',
* 'role' => 'shared_',
* 'authmap' => 'shared_',
* ],
* @endcode
* You can also use a reference to a schema/database as a prefix. This may be
* useful if your Drupal installation exists in a schema that is not the default
* or you want to access several databases from the same code base at the same
* time.
* Example:
* @code
* 'prefix' => [
* 'default' => 'main.',
* 'users' => 'shared.',
* 'sessions' => 'shared.',
* 'role' => 'shared.',
* 'authmap' => 'shared.',
* ];
* @endcode
* NOTE: MySQL and SQLite's definition of a schema is a database.
*
* Advanced users can add or override initial commands to execute when
* connecting to the database server, as well as PDO connection settings. For
* example, to enable MySQL SELECT queries to exceed the max_join_size system
......
......@@ -49,7 +49,7 @@ protected function getDatabaseConnection(InputInterface $input) {
$prefix = $input->getOption('prefix');
if ($prefix) {
$info = Database::getConnectionInfo($key)['default'];
$info['prefix']['default'] = $prefix;
$info['prefix'] = $prefix;
Database::removeConnection($key);
Database::addConnectionInfo($key, 'default', $info);
......
......@@ -227,6 +227,8 @@ abstract class Connection {
* - prefix
* - namespace
* - Other driver-specific options.
* An 'extra_prefix' option may be present to allow BC for attaching
* per-table prefixes, but it is meant for internal use only.
*/
public function __construct(\PDO $connection, array $connection_options) {
if ($this->identifierQuotes === NULL) {
......@@ -235,12 +237,47 @@ public function __construct(\PDO $connection, array $connection_options) {
}
assert(count($this->identifierQuotes) === 2 && Inspector::assertAllStrings($this->identifierQuotes), '\Drupal\Core\Database\Connection::$identifierQuotes must contain 2 string values');
// The 'transactions' option is deprecated.
if (isset($connection_options['transactions'])) {
@trigger_error('Passing a \'transactions\' connection option to ' . __METHOD__ . ' is deprecated in drupal:9.1.0 and is removed in drupal:10.0.0. All database drivers must support transactions. See https://www.drupal.org/node/2278745', E_USER_DEPRECATED);
unset($connection_options['transactions']);
}
// Manage the table prefix.
if (isset($connection_options['prefix']) && is_array($connection_options['prefix'])) {
if (count($connection_options['prefix']) > 1) {
// If there are keys left besides the 'default' one, we are in a
// multi-prefix scenario (for per-table prefixing, or migrations).
// In that case, we put the non-default keys in a 'extra_prefix' key
// to avoid mixing up with the normal 'prefix', which is a string since
// Drupal 9.1.0.
$prefix = $connection_options['prefix']['default'] ?? '';
unset($connection_options['prefix']['default']);
if (isset($connection_options['extra_prefix'])) {
$connection_options['extra_prefix'] = array_merge($connection_options['extra_prefix'], $connection_options['prefix']);
}
else {
$connection_options['extra_prefix'] = $connection_options['prefix'];
}
}
else {
$prefix = $connection_options['prefix']['default'] ?? '';
}
$connection_options['prefix'] = $prefix;
}
// Initialize and prepare the connection prefix.
if (!isset($connection_options['extra_prefix'])) {
$prefix = $connection_options['prefix'] ?? '';
}
else {
$default_prefix = $connection_options['prefix'] ?? '';
$prefix = $connection_options['extra_prefix'];
$prefix['default'] = $default_prefix;
}
$this->setPrefix($prefix);
// Work out the database driver namespace if none is provided. This normally
// written to setting.php by installer or set by
// \Drupal\Core\Database\Database::parseConnectionInfo().
......@@ -254,9 +291,6 @@ public function __construct(\PDO $connection, array $connection_options) {
@trigger_error('Support for database drivers located in the "drivers/lib/Drupal/Driver/Database" directory is deprecated in drupal:9.1.0 and is removed in drupal:10.0.0. Contributed and custom database drivers should be provided by modules and use the namespace "Drupal\MODULE_NAME\Driver\Database\DRIVER_NAME". See https://www.drupal.org/node/3123251', E_USER_DEPRECATED);
}
// Initialize and prepare the connection prefix.
$this->setPrefix(isset($connection_options['prefix']) ? $connection_options['prefix'] : '');
// Set a Statement class, unless the driver opted out.
// @todo remove this in Drupal 10 https://www.drupal.org/node/3177490
if (!empty($this->statementClass)) {
......@@ -407,8 +441,7 @@ public function getConnectionOptions() {
* Set the list of prefixes used by this database connection.
*
* @param array|string $prefix
* Either a single prefix, or an array of prefixes, in any of the multiple
* forms documented in default.settings.php.
* Either a single prefix, or an array of prefixes.
*/
protected function setPrefix($prefix) {
if (is_array($prefix)) {
......@@ -1994,7 +2027,7 @@ public static function createConnectionOptionsFromUrl($url, $root) {
}
if (!empty($url_components['fragment'])) {
$database['prefix']['default'] = $url_components['fragment'];
$database['prefix'] = $url_components['fragment'];
}
return $database;
......@@ -2050,8 +2083,8 @@ public static function createUrlFromConnectionOptions(array $connection_options)
$db_url .= '?module=' . $connection_options['module'];
}
if (isset($connection_options['prefix']['default']) && $connection_options['prefix']['default'] !== '') {
$db_url .= '#' . $connection_options['prefix']['default'];
if (isset($connection_options['prefix']) && $connection_options['prefix'] !== '') {
$db_url .= '#' . $connection_options['prefix'];
}
return $db_url;
......
......@@ -216,17 +216,24 @@ final public static function parseConnectionInfo(array $info) {
}
// Parse the prefix information.
// @todo in Drupal 10, fail hard if $info['prefix'] is an array.
// @see https://www.drupal.org/project/drupal/issues/3124382
if (!isset($info['prefix'])) {
// Default to an empty prefix.
$info['prefix'] = [
'default' => '',
];
}
elseif (!is_array($info['prefix'])) {
// Transform the flat form into an array form.
$info['prefix'] = [
'default' => $info['prefix'],
];
$info['prefix'] = '';
}
elseif (is_array($info['prefix'])) {
$prefix = $info['prefix']['default'] ?? '';
unset($info['prefix']['default']);
// If there are keys left besides the 'default' one, we are in a
// multi-prefix scenario (for per-table prefixing, or migrations).
// In that case, we put the non-default keys in a 'extra_prefix' key
// to avoid mixing up with the normal 'prefix', which is a string since
// Drupal 9.1.0.
if (count($info['prefix'])) {
$info['extra_prefix'] = $info['prefix'];
}
$info['prefix'] = $prefix;
}
// Fallback for Drupal 7 settings.php if namespace is not provided.
......
......@@ -514,8 +514,8 @@ public static function createUrlFromConnectionOptions(array $connection_options)
$db_url = 'sqlite://localhost/' . $connection_options['database'];
if (isset($connection_options['prefix']['default']) && $connection_options['prefix']['default'] !== NULL && $connection_options['prefix']['default'] !== '') {
$db_url .= '#' . $connection_options['prefix']['default'];
if (isset($connection_options['prefix']) && $connection_options['prefix'] !== '') {
$db_url .= '#' . $connection_options['prefix'];
}
return $db_url;
......
......@@ -514,7 +514,6 @@ protected function rebuildAll() {
protected function installParameters() {
$connection_info = Database::getConnectionInfo();
$driver = $connection_info['default']['driver'];
$connection_info['default']['prefix'] = $connection_info['default']['prefix']['default'];
unset($connection_info['default']['driver']);
unset($connection_info['default']['namespace']);
unset($connection_info['default']['pdo']);
......
......@@ -167,9 +167,7 @@ protected function changeDatabasePrefix() {
foreach ($connection_info as $target => $value) {
// Replace the full table prefix definition to ensure that no table
// prefixes of the test runner leak into the test.
$connection_info[$target]['prefix'] = [
'default' => $value['prefix']['default'] . $this->databasePrefix,
];
$connection_info[$target]['prefix'] = $value['prefix'] . $this->databasePrefix;
}
Database::addConnectionInfo('default', 'default', $connection_info['default']);
}
......
......@@ -90,14 +90,14 @@ private function createMigrationConnection() {
}
$connection_info = Database::getConnectionInfo('default');
foreach ($connection_info as $target => $value) {
$prefix = is_array($value['prefix']) ? $value['prefix']['default'] : $value['prefix'];
$prefix = $value['prefix'];
// Simpletest uses 7 character prefixes at most so this can't cause
// collisions.
$connection_info[$target]['prefix']['default'] = $prefix . '0';
$connection_info[$target]['prefix'] = $prefix . '0';
// Add the original simpletest prefix so SQLite can attach its database.
// @see \Drupal\Core\Database\Driver\sqlite\Connection::init()
$connection_info[$target]['prefix'][$value['prefix']['default']] = $value['prefix']['default'];
$connection_info[$target]['extra_prefix'][$prefix] = $prefix;
}
Database::addConnectionInfo('migrate', 'default', $connection_info['default']);
}
......
......@@ -135,14 +135,14 @@ public function testGetDefinitions() {
}
$connection_info = Database::getConnectionInfo('default');
foreach ($connection_info as $target => $value) {
$prefix = is_array($value['prefix']) ? $value['prefix']['default'] : $value['prefix'];
$prefix = $value['prefix'];
// Simpletest uses 7 character prefixes at most so this can't cause
// collisions.
$connection_info[$target]['prefix']['default'] = $prefix . '0';
$connection_info[$target]['prefix'] = $prefix . '0';
// Add the original simpletest prefix so SQLite can attach its database.
// @see \Drupal\Core\Database\Driver\sqlite\Connection::init()
$connection_info[$target]['prefix'][$value['prefix']['default']] = $value['prefix']['default'];
$connection_info[$target]['extra_prefix'][$prefix] = $prefix;
}
Database::addConnectionInfo('migrate', 'default', $connection_info['default']);
......
......@@ -107,7 +107,7 @@ protected function createMigrationConnection() {
$connection_info['prefix'] = '';
}
else {
$prefix = is_array($connection_info['prefix']) ? $connection_info['prefix']['default'] : $connection_info['prefix'];
$prefix = $connection_info['prefix'];
// Simpletest uses fixed length prefixes. Create a new prefix for the
// source database. Adding to the end of the prefix ensures that
// \Drupal\simpletest\TestBase::restoreEnvironment() will remove the
......@@ -295,7 +295,6 @@ protected function getCredentials() {
$connection_options = $this->sourceDatabase->getConnectionOptions();
$version = $this->getLegacyDrupalVersion($this->sourceDatabase);
$driver = $connection_options['driver'];
$connection_options['prefix'] = $connection_options['prefix']['default'];
// Use the driver connection form to get the correct options out of the
// database settings. This supports all of the databases we test against.
......
......@@ -119,7 +119,6 @@ public function testFilePath(string $file_private_path, string $file_public_path
$connection_options = $this->sourceDatabase->getConnectionOptions();
$driver = $connection_options['driver'];
$connection_options['prefix'] = $connection_options['prefix']['default'];
// Use the driver connection form to get the correct options out of the
// database settings. This supports all of the databases we test against.
......
......@@ -390,6 +390,18 @@ function system_requirements($phase) {
}
}
// Verify there are no database entries in settings.php with table prefix in
// array format.
if ($phase === 'runtime' || $phase === 'update') {
if (_system_check_array_table_prefixes(\Drupal::getContainer()->getParameter('app.root'), \Drupal::getContainer()->getParameter('site.path')) === TRUE) {
$requirements['database_table_prefixes'] = [
'title' => t('Database table prefixes'),
'value' => t("There is at least one database entry in the \$database array in settings.php that has a 'prefix' value in the format of an array. Per-table prefixes are no longer supported. Change your settings.php file to ensure the value of every 'prefix' entry is a single string."),
'severity' => REQUIREMENT_WARNING,
];
}
}
if ($phase == 'install' || $phase == 'update') {
// Test for PDO (database).
$requirements['database_extensions'] = [
......@@ -1509,3 +1521,23 @@ function _system_advisories_requirements(array &$requirements): void {
}
}
}
/**
* Checks if there are 'prefix' entries in array format for tables.
*/
function _system_check_array_table_prefixes($app_root, $site_path) {
if (is_readable($app_root . '/' . $site_path . '/settings.php')) {
include $app_root . '/' . $site_path . '/settings.php';
}
if (empty($databases)) {
return FALSE;
}
foreach ($databases as $database) {
foreach ($database as $target) {
if (isset($target['prefix']) && is_array($target['prefix'])) {
return TRUE;
}
}
}
return FALSE;
}
......@@ -644,9 +644,7 @@ function simpletest_script_setup_database($new = FALSE) {
$databases['test-runner']['default'] = [
'driver' => 'sqlite',
'database' => $sqlite,
'prefix' => [
'default' => '',
],
'prefix' => '',
];
// Create the test runner SQLite database, unless it exists already.
if ($new && !file_exists($sqlite)) {
......
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Tests the interactive installer with deprecated table prefix array.
*
* @group Installer
*/
class InstallerWithTablePrefixArrayTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Ensures that the status report raises the warning after installation.
*/
public function testInstall(): void {
$this->drupalGet('admin/reports/status');
$this->assertSession()->pageTextNotContains("There is at least one database entry in the \$database array in settings.php that has a 'prefix' value in the format of an array. Per-table prefixes are no longer supported.");
// Add a database with a multi-entry 'prefix' array.
$settings['databases']['test_fu']['default'] = (object) [
'value' => [
'database' => 'drupal_db',
'prefix' => ['default' => 'foo', 'other_table' => 'qux'],
'host' => 'localhost',
'namespace' => 'Drupal\Core\Database\Driver\sqlite',
'driver' => 'sqlite',
],
'required' => TRUE,
];
$this->writeSettings($settings);
$this->drupalGet('admin/reports/status');
$this->assertSession()->pageTextContains("There is at least one database entry in the \$database array in settings.php that has a 'prefix' value in the format of an array. Per-table prefixes are no longer supported.");
}
}
......@@ -165,6 +165,43 @@ public function testPDOStatementQueryDeprecation(): void {
$this->assertNotNull($db->query($stmt->getClientStatement()));
}
/**
* Tests per-table prefix connection option.
*/
public function testPerTablePrefixOption() {
$connection_info = Database::getConnectionInfo('default');
$new_connection_info = $connection_info['default'];
$new_connection_info['prefix'] = [
'default' => $connection_info['default']['prefix'],
'test_table' => $connection_info['default']['prefix'] . '_bar',
];
Database::addConnectionInfo('default', 'foo', $new_connection_info);
$foo_connection = Database::getConnection('foo', 'default');
$this->assertInstanceOf(Connection::class, $foo_connection);
$this->assertIsString($foo_connection->getConnectionOptions()['prefix']);
$this->assertSame($connection_info['default']['prefix'], $foo_connection->getConnectionOptions()['prefix']);
$this->assertSame([
'test_table' => $connection_info['default']['prefix'] . '_bar',
], $foo_connection->getConnectionOptions()['extra_prefix']);
}
/**
* Tests the prefix connection option in array form.
*/
public function testPrefixArrayOption() {
$connection_info = Database::getConnectionInfo('default');
$new_connection_info = $connection_info['default'];
$new_connection_info['prefix'] = [
'default' => $connection_info['default']['prefix'],
];
Database::addConnectionInfo('default', 'foo', $new_connection_info);
$foo_connection = Database::getConnection('foo', 'default');
$this->assertInstanceOf(Connection::class, $foo_connection);
$this->assertIsString($foo_connection->getConnectionOptions()['prefix']);
$this->assertSame($connection_info['default']['prefix'], $foo_connection->getConnectionOptions()['prefix']);
$this->assertArrayNotHasKey('extra_prefix', $foo_connection->getConnectionOptions());
}
/**
* Ensure that you cannot execute multiple statements on MySQL.
*/
......
......@@ -1204,7 +1204,10 @@ public function testFindTables() {
// Add per-table prefix to the second table.
$new_connection_info = $connection_info['default'];
$new_connection_info['prefix']['test_2_table'] = $new_connection_info['prefix']['default'] . '_shared_';
$new_connection_info['prefix'] = [
'default' => $connection_info['default']['prefix'],
'test_2_table' => $connection_info['default']['prefix'] . '_shared_',
];
Database::addConnectionInfo('test', 'default', $new_connection_info);
Database::setActiveConnection('test');
$test_schema = Database::getConnection()->schema();
......
......@@ -466,9 +466,7 @@ protected function getDatabaseConnectionInfo() {
foreach ($connection_info as $target => $value) {
// Replace the full table prefix definition to ensure that no table
// prefixes of the test runner leak into the test.
$connection_info[$target]['prefix'] = [
'default' => $this->databasePrefix,
];
$connection_info[$target]['prefix'] = $this->databasePrefix;
}
}
return $connection_info;
......@@ -646,9 +644,9 @@ protected function tearDown() {
// Remove all prefixed tables.
$original_connection_info = Database::getConnectionInfo('simpletest_original_default');
$original_prefix = $original_connection_info['default']['prefix']['default'] ?? NULL;
$original_prefix = $original_connection_info['default']['prefix'] ?? NULL;
$test_connection_info = Database::getConnectionInfo('default');
$test_prefix = $test_connection_info['default']['prefix']['default'] ?? NULL;
$test_prefix = $test_connection_info['default']['prefix'] ?? NULL;
if ($original_prefix != $test_prefix) {
$tables = Database::getConnection()->schema()->findTables('%');
foreach ($tables as $table) {
......
......@@ -56,7 +56,7 @@ public function testBootEnvironment() {
*/
public function testGetDatabaseConnectionInfoWithOutManualSetDbUrl() {
$options = $this->container->get('database')->getConnectionOptions();
$this->assertSame($this->databasePrefix, $options['prefix']['default']);
$this->assertSame($this->databasePrefix, $options['prefix']);
}
/**
......
......@@ -81,7 +81,7 @@ protected function tearDown(TestDatabase $test_database, $db_url): void {
// Connect to the test database.
$root = dirname(__DIR__, 5);
$database = Database::convertDbUrlToConnectionInfo($db_url, $root);
$database['prefix'] = ['default' => $test_database->getDatabasePrefix()];
$database['prefix'] = $test_database->getDatabasePrefix();
Database::addConnectionInfo(__CLASS__, 'default', $database);
// Remove all the tables.
......
......@@ -424,9 +424,9 @@ public static function filePreDeleteCallback($path) {
protected function cleanupEnvironment() {
// Remove all prefixed tables.
$original_connection_info = Database::getConnectionInfo('simpletest_original_default');
$original_prefix = $original_connection_info['default']['prefix']['default'];
$original_prefix = $original_connection_info['default']['prefix'];
$test_connection_info = Database::getConnectionInfo('default');
$test_prefix = $test_connection_info['default']['prefix']['default'];
$test_prefix = $test_connection_info['default']['prefix'];
if ($original_prefix != $test_prefix) {
$tables = Database::getConnection()->schema()->findTables('%');
foreach ($tables as $table) {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment