first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\mysql\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for mysql.
*
* @group mysql
*/
class GenericTest extends GenericModuleTestBase {}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\mysql\Functional;
use Drupal\Core\Database\Database;
use Drupal\FunctionalTests\Installer\InstallerExistingSettingsTest;
/**
* Tests the isolation_level setting with existing database settings.
*
* @group Installer
*/
class InstallerIsolationLevelExistingSettingsTest extends InstallerExistingSettingsTest {
/**
* {@inheritdoc}
*/
protected function prepareEnvironment() {
parent::prepareEnvironment();
$connection_info = Database::getConnectionInfo();
// The isolation_level option is only available for MySQL.
if ($connection_info['default']['driver'] !== 'mysql') {
$this->markTestSkipped("This test does not support the {$connection_info['default']['driver']} database driver.");
}
}
/**
* Verifies that isolation_level is not set in the database settings.
*/
public function testInstaller(): void {
$contents = file_get_contents($this->container->getParameter('app.root') . '/' . $this->siteDirectory . '/settings.php');
// Test that isolation_level was not set.
$this->assertStringNotContainsString("'isolation_level' => 'READ COMMITTED'", $contents);
$this->assertStringNotContainsString("'isolation_level' => 'REPEATABLE READ'", $contents);
// Change the default database connection to use the isolation level from
// the test.
$connection_info = Database::getConnectionInfo();
$driver_test_connection = $connection_info['default'];
// We have asserted that the isolation level was not set.
unset($driver_test_connection['isolation_level']);
unset($driver_test_connection['init_commands']);
Database::renameConnection('default', 'original_database_connection');
Database::addConnectionInfo('default', 'default', $driver_test_connection);
// Close and reopen the database connection, so the database init commands
// get executed.
Database::closeConnection('default', 'default');
$connection = Database::getConnection('default', 'default');
$query = 'SELECT @@SESSION.tx_isolation';
// The database variable "tx_isolation" has been removed in MySQL v8.0 and
// has been replaced by "transaction_isolation".
// @see https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_tx_isolation
if (!$connection->isMariaDb() && version_compare($connection->version(), '8.0.0-AnyName', '>')) {
$query = 'SELECT @@SESSION.transaction_isolation';
}
// Test that transaction level is REPEATABLE READ.
$this->assertEquals('REPEATABLE-READ', $connection->query($query)->fetchField());
// Restore the old database connection.
Database::addConnectionInfo('default', 'default', $connection_info['default']);
Database::closeConnection('default', 'default');
Database::getConnection('default', 'default');
}
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\mysql\Functional;
use Drupal\Core\Database\Database;
use Drupal\FunctionalTests\Installer\InstallerTestBase;
/**
* Tests the isolation_level setting with no database settings.
*
* @group Installer
*/
class InstallerIsolationLevelNoDatabaseSettingsTest extends InstallerTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function prepareEnvironment() {
parent::prepareEnvironment();
// The isolation_level option is only available for MySQL.
$connection_info = Database::getConnectionInfo();
if ($connection_info['default']['driver'] !== 'mysql') {
$this->markTestSkipped("This test does not support the {$connection_info['default']['driver']} database driver.");
}
}
/**
* Verifies that the isolation_level was added to the database settings.
*/
public function testInstaller(): void {
$contents = file_get_contents($this->container->getParameter('app.root') . '/' . $this->siteDirectory . '/settings.php');
// Test that isolation_level was set to "READ COMMITTED".
$this->assertStringContainsString("'isolation_level' => 'READ COMMITTED',", $contents);
// Change the default database connection to use the isolation level from
// the test.
$connection_info = Database::getConnectionInfo();
$driver_test_connection = $connection_info['default'];
// We have asserted that the isolation level was set to 'READ COMMITTED'.
$driver_test_connection['isolation_level'] = 'READ COMMITTED';
unset($driver_test_connection['init_commands']);
Database::renameConnection('default', 'original_database_connection');
Database::addConnectionInfo('default', 'default', $driver_test_connection);
// Close and reopen the database connection, so the database init commands
// get executed.
Database::closeConnection('default', 'default');
$connection = Database::getConnection('default', 'default');
$query = 'SELECT @@SESSION.tx_isolation';
// The database variable "tx_isolation" has been removed in MySQL v8.0 and
// has been replaced by "transaction_isolation".
// @see https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_tx_isolation
if (!$connection->isMariaDb() && version_compare($connection->version(), '8.0.0-AnyName', '>')) {
$query = 'SELECT @@SESSION.transaction_isolation';
}
// Test that transaction level is READ-COMMITTED.
$this->assertEquals('READ-COMMITTED', $connection->query($query)->fetchField());
// Restore the old database connection.
Database::addConnectionInfo('default', 'default', $connection_info['default']);
Database::closeConnection('default', 'default');
Database::getConnection('default', 'default');
}
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\mysql\Functional;
use Drupal\Core\Database\Database;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
/**
* Tests updates MySQL 8 when sql_require_primary_key is on.
*
* This acts as a generic test the Drupal supports this setting and does not
* break during updates.
*
* @group mysql
*/
class Mysql8RequirePrimaryKeyUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected function runDbTasks() {
parent::runDbTasks();
$database = Database::getConnection();
$is_maria = method_exists($database, 'isMariaDb') && $database->isMariaDb();
if ($database->databaseType() !== 'mysql' || $is_maria || version_compare($database->version(), '8.0.13', '<')) {
$this->markTestSkipped('This test only runs on MySQL 8.0.13 and above');
}
$database->query("SET sql_require_primary_key = 1;")->execute();
}
/**
* {@inheritdoc}
*/
protected function prepareSettings() {
parent::prepareSettings();
// Set sql_require_primary_key for any future connections.
$settings['databases']['default']['default']['init_commands'] = (object) [
'value' => ['sql_require_primary_key' => 'SET sql_require_primary_key = 1;'],
'required' => TRUE,
];
$this->writeSettings($settings);
}
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles[] = __DIR__ . '/../../../../system/tests/fixtures/update/drupal-9.4.0.bare.standard.php.gz';
}
/**
* Tests updates.
*/
public function testDatabaseLoaded(): void {
$this->runUpdates();
// Ensure that after updating a user can be created and do a basic test that
// the site is available by logging in.
$this->drupalLogin($this->createUser(admin: TRUE));
$this->assertSession()->statusCodeEquals(200);
}
}

View File

@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\mysql\Functional;
use Drupal\Core\Database\Database;
use Drupal\Tests\BrowserTestBase;
/**
* Tests isolation level warning when the config is set in settings.php.
*
* @group mysql
*/
class RequirementsTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['mysql'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// The isolation_level option is only available for MySQL.
$connection = Database::getConnection();
if ($connection->driver() !== 'mysql') {
$this->markTestSkipped("This test does not support the {$connection->driver()} database driver.");
}
}
/**
* Test the isolation level warning message on status page.
*/
public function testIsolationLevelWarningNotDisplaying(): void {
$admin_user = $this->drupalCreateUser([
'administer site configuration',
'access site reports',
]);
$this->drupalLogin($admin_user);
$connection = Database::getConnection();
// Set the isolation level to a level that produces a warning.
$this->writeIsolationLevelSettings('REPEATABLE READ');
// Check the message is not a warning.
$this->drupalGet('admin/reports/status');
$elements = $this->xpath('//details[@class="system-status-report__entry"]//div[contains(text(), "REPEATABLE-READ")]');
$this->assertCount(1, $elements);
// Ensure it is a warning.
$this->assertStringContainsString('warning', $elements[0]->getParent()->getParent()->find('css', 'summary')->getAttribute('class'));
// Rollback the isolation level to read committed.
$this->writeIsolationLevelSettings('READ COMMITTED');
// Check the message is not a warning.
$this->drupalGet('admin/reports/status');
$elements = $this->xpath('//details[@class="system-status-report__entry"]//div[contains(text(), "READ-COMMITTED")]');
$this->assertCount(1, $elements);
// Ensure it is a not a warning.
$this->assertStringNotContainsString('warning', $elements[0]->getParent()->getParent()->find('css', 'summary')->getAttribute('class'));
$specification = [
'fields' => [
'text' => [
'type' => 'text',
'description' => 'A text field',
],
],
];
$connection->schema()->createTable('test_table_without_primary_key', $specification);
// Set the isolation level to a level that produces a warning.
$this->writeIsolationLevelSettings('REPEATABLE READ');
// Check the message is not a warning.
$this->drupalGet('admin/reports/status');
$elements = $this->xpath('//details[@class="system-status-report__entry"]//div[contains(text(), :text)]', [
':text' => 'The recommended level for Drupal is "READ COMMITTED". For this to work correctly, all tables must have a primary key. The following table(s) do not have a primary key: test_table_without_primary_key.',
]);
$this->assertCount(1, $elements);
$this->assertStringStartsWith('REPEATABLE-READ', $elements[0]->getParent()->getText());
// Ensure it is a warning.
$this->assertStringContainsString('warning', $elements[0]->getParent()->getParent()->find('css', 'summary')->getAttribute('class'));
// Rollback the isolation level to read committed.
$this->writeIsolationLevelSettings('READ COMMITTED');
// Check the message is not a warning.
$this->drupalGet('admin/reports/status');
$elements = $this->xpath('//details[@class="system-status-report__entry"]//div[contains(text(), :text)]', [
':text' => 'For this to work correctly, all tables must have a primary key. The following table(s) do not have a primary key: test_table_without_primary_key.',
]);
$this->assertCount(1, $elements);
$this->assertStringStartsWith('READ-COMMITTED', $elements[0]->getParent()->getText());
// Ensure it is an error.
$this->assertStringContainsString('error', $elements[0]->getParent()->getParent()->find('css', 'summary')->getAttribute('class'));
}
/**
* Writes the isolation level in settings.php.
*
* @param string $isolation_level
* The isolation level.
*/
private function writeIsolationLevelSettings(string $isolation_level) {
$settings['databases']['default']['default']['init_commands'] = (object) [
'value' => [
'isolation_level' => "SET SESSION TRANSACTION ISOLATION LEVEL {$isolation_level}",
],
'required' => TRUE,
];
$this->writeSettings($settings);
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\mysql\Kernel\mysql;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\KernelTests\Core\Database\DriverSpecificDatabaseTestBase;
/**
* MySQL-specific connection tests.
*
* @group Database
*/
class ConnectionTest extends DriverSpecificDatabaseTestBase {
/**
* Ensure that you cannot execute multiple statements on MySQL.
*/
public function testMultipleStatementsForNewPhp(): void {
$this->expectException(DatabaseExceptionWrapper::class);
Database::getConnection('default', 'default')->query('SELECT * FROM {test}; SELECT * FROM {test_people}', [], ['allow_delimiter_in_query' => TRUE]);
}
/**
* Tests deprecation of ::makeSequenceName().
*
* @group legacy
*/
public function testMakeSequenceNameDeprecation(): void {
$this->expectDeprecation("Drupal\\Core\\Database\\Connection::makeSequenceName() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3377046");
$this->assertIsString($this->connection->makeSequenceName('foo', 'bar'));
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\mysql\Kernel\mysql;
use Drupal\KernelTests\Core\Database\DriverSpecificConnectionUnitTestBase;
/**
* MySQL-specific connection unit tests.
*
* @group Database
*/
class ConnectionUnitTest extends DriverSpecificConnectionUnitTestBase {
/**
* Returns a set of queries specific for MySQL.
*/
protected function getQuery(): array {
return [
'connection_id' => 'SELECT CONNECTION_ID()',
'processlist' => 'SHOW PROCESSLIST',
'show_tables' => 'SHOW TABLES',
];
}
}

View File

@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\mysql\Kernel\mysql\Console;
use Drupal\Core\Command\DbDumpCommand;
use Drupal\KernelTests\Core\Database\DriverSpecificKernelTestBase;
use Symfony\Component\Console\Tester\CommandTester;
/**
* Test that the DbDumpCommand works correctly.
*
* @group console
*/
class DbDumpCommandTest extends DriverSpecificKernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['system'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Rebuild the router to ensure a routing table.
\Drupal::service('router.builder')->rebuild();
/** @var \Drupal\Core\Database\Connection $connection */
$connection = $this->container->get('database');
$connection->insert('router')->fields(['name', 'path', 'pattern_outline'])->values(['test', 'test', 'test'])->execute();
// Create a table with a field type not defined in
// \Drupal\Core\Database\Schema::getFieldTypeMap.
$table_name = $connection->getPrefix() . 'foo';
$sql = "create table if not exists `$table_name` (`test` datetime NOT NULL);";
$connection->query($sql)->execute();
}
/**
* Tests the command directly.
*/
public function testDbDumpCommand(): void {
$command = new DbDumpCommand();
$command_tester = new CommandTester($command);
$command_tester->execute([]);
// Assert that insert exists and that some expected fields exist.
$output = $command_tester->getDisplay();
$this->assertStringContainsString("createTable('router", $output, 'Table router found');
$this->assertStringContainsString("insert('router", $output, 'Insert found');
$this->assertStringContainsString("'name' => 'test", $output, 'Insert name field found');
$this->assertStringContainsString("'path' => 'test", $output, 'Insert path field found');
$this->assertStringContainsString("'pattern_outline' => 'test", $output, 'Insert pattern_outline field found');
$this->assertStringContainsString("// phpcs:ignoreFile", $output);
$version = \Drupal::VERSION;
$this->assertStringContainsString("This file was generated by the Drupal {$version} db-tools.php script.", $output);
}
/**
* Tests schema only option.
*/
public function testSchemaOnly(): void {
$command = new DbDumpCommand();
$command_tester = new CommandTester($command);
$command_tester->execute(['--schema-only' => 'router']);
// Assert that insert statement doesn't exist for schema only table.
$output = $command_tester->getDisplay();
$this->assertStringContainsString("createTable('router", $output, 'Table router found');
$this->assertStringNotContainsString("insert('router", $output, 'Insert not found');
$this->assertStringNotContainsString("'name' => 'test", $output, 'Insert name field not found');
$this->assertStringNotContainsString("'path' => 'test", $output, 'Insert path field not found');
$this->assertStringNotContainsString("'pattern_outline' => 'test", $output, 'Insert pattern_outline field not found');
// Assert that insert statement doesn't exist for wildcard schema only match.
$command_tester->execute(['--schema-only' => 'route.*']);
$output = $command_tester->getDisplay();
$this->assertStringContainsString("createTable('router", $output, 'Table router found');
$this->assertStringNotContainsString("insert('router", $output, 'Insert not found');
$this->assertStringNotContainsString("'name' => 'test", $output, 'Insert name field not found');
$this->assertStringNotContainsString("'path' => 'test", $output, 'Insert path field not found');
$this->assertStringNotContainsString("'pattern_outline' => 'test", $output, 'Insert pattern_outline field not found');
}
/**
* Tests insert count option.
*/
public function testInsertCount(): void {
$command = new DbDumpCommand();
$command_tester = new CommandTester($command);
$command_tester->execute(['--insert-count' => '1']);
$router_row_count = (int) $this->container->get('database')->select('router')->countQuery()->execute()->fetchField();
$output = $command_tester->getDisplay();
$this->assertSame($router_row_count, substr_count($output, "insert('router"));
$this->assertGreaterThan(1, $router_row_count);
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\mysql\Kernel\mysql;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\Database\Database;
use Drupal\KernelTests\Core\Database\DriverSpecificKernelTestBase;
/**
* Tests exceptions thrown by queries.
*
* @group Database
*/
class DatabaseExceptionWrapperTest extends DriverSpecificKernelTestBase {
/**
* Tests Connection::prepareStatement exceptions on preparation.
*
* Core database drivers use PDO emulated statements or the StatementPrefetch
* class, which defer the statement check to the moment of the execution. In
* order to test a failure at preparation time, we have to force the
* connection not to emulate statement preparation. Still, this is only valid
* for the MySql driver.
*/
public function testPrepareStatementFailOnPreparation(): void {
$connection_info = Database::getConnectionInfo('default');
$connection_info['default']['pdo'][\PDO::ATTR_EMULATE_PREPARES] = FALSE;
Database::addConnectionInfo('default', 'foo', $connection_info['default']);
$foo_connection = Database::getConnection('foo', 'default');
$this->expectException(DatabaseExceptionWrapper::class);
$stmt = $foo_connection->prepareStatement('bananas', []);
}
/**
* Tests Connection::prepareStatement exception on execution.
*/
public function testPrepareStatementFailOnExecution(): void {
$this->expectException(\PDOException::class);
$stmt = $this->connection->prepareStatement('bananas', []);
$stmt->execute();
}
}

View File

@@ -0,0 +1,274 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\mysql\Kernel\mysql;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Command\DbDumpApplication;
use Drupal\Core\Config\DatabaseStorage;
use Drupal\Core\Database\Database;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\KernelTests\Core\Database\DriverSpecificKernelTestBase;
use Drupal\Tests\Traits\Core\PathAliasTestTrait;
use Drupal\user\Entity\User;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\DependencyInjection\Reference;
/**
* Tests for the database dump commands.
*
* @group Update
*/
class DbDumpTest extends DriverSpecificKernelTestBase {
use PathAliasTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
// @todo system can be removed from this test once
// https://www.drupal.org/project/drupal/issues/2851705 is committed.
'system',
'config',
'dblog',
'menu_link_content',
'link',
'block_content',
'file',
'path_alias',
'user',
];
/**
* Test data to write into config.
*
* @var array
*/
protected $data;
/**
* An array of original table schemas.
*
* @var array
*/
protected $originalTableSchemas = [];
/**
* An array of original table indexes (including primary and unique keys).
*
* @var array
*/
protected $originalTableIndexes = [];
/**
* Tables that should be part of the exported script.
*
* @var array
*/
protected $tables;
/**
* {@inheritdoc}
*
* Register a database cache backend rather than memory-based.
*/
public function register(ContainerBuilder $container) {
parent::register($container);
$container->register('cache_factory', 'Drupal\Core\Cache\DatabaseBackendFactory')
->addArgument(new Reference('database'))
->addArgument(new Reference('cache_tags.invalidator.checksum'))
->addArgument(new Reference('settings'))
->addArgument(new Reference('serialization.phpserialize'))
->addArgument(new Reference(TimeInterface::class));
}
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create some schemas so our export contains tables.
$this->installSchema('dblog', ['watchdog']);
$this->installEntitySchema('block_content');
$this->installEntitySchema('user');
$this->installEntitySchema('file');
$this->installEntitySchema('menu_link_content');
$this->installEntitySchema('path_alias');
// Place some sample config to test for in the export.
$this->data = [
'foo' => $this->randomMachineName(),
'bar' => $this->randomMachineName(),
];
$storage = new DatabaseStorage(Database::getConnection(), 'config');
$storage->write('test_config', $this->data);
// Create user account with some potential syntax issues.
// cspell:disable-next-line
$account = User::create(['mail' => 'q\'uote$dollar@example.com', 'name' => '$dollar']);
$account->save();
// Create a path alias.
$this->createPathAlias('/user/' . $account->id(), '/user/example');
// Create a cache table (this will create 'cache_discovery').
\Drupal::cache('discovery')->set('test', $this->data);
// These are all the tables that should now be in place.
$this->tables = [
'block_content',
'block_content_field_data',
'block_content_field_revision',
'block_content_revision',
'cachetags',
'config',
'cache_bootstrap',
'cache_config',
'cache_discovery',
'cache_entity',
'file_managed',
'menu_link_content',
'menu_link_content_data',
'menu_link_content_revision',
'menu_link_content_field_revision',
'path_alias',
'path_alias_revision',
'user__roles',
'users',
'users_field_data',
'watchdog',
];
}
/**
* Tests the command directly.
*/
public function testDbDumpCommand(): void {
$application = new DbDumpApplication();
$command = $application->find('dump-database-d8-mysql');
$command_tester = new CommandTester($command);
$command_tester->execute([]);
// Tables that are schema-only should not have data exported.
$pattern = preg_quote("\$connection->insert('sessions')");
$this->assertDoesNotMatchRegularExpression('/' . $pattern . '/', $command_tester->getDisplay(), 'Tables defined as schema-only do not have data exported to the script.');
// Table data is exported.
$pattern = preg_quote("\$connection->insert('config')");
$this->assertMatchesRegularExpression('/' . $pattern . '/', $command_tester->getDisplay(), 'Table data is properly exported to the script.');
// The test data are in the dump (serialized).
$pattern = preg_quote(serialize($this->data));
$this->assertMatchesRegularExpression('/' . $pattern . '/', $command_tester->getDisplay(), 'Generated data is found in the exported script.');
// Check that the user account name and email address was properly escaped.
// cspell:disable-next-line
$pattern = preg_quote('"q\'uote\$dollar@example.com"');
$this->assertMatchesRegularExpression('/' . $pattern . '/', $command_tester->getDisplay(), 'The user account email address was properly escaped in the exported script.');
$pattern = preg_quote('\'$dollar\'');
$this->assertMatchesRegularExpression('/' . $pattern . '/', $command_tester->getDisplay(), 'The user account name was properly escaped in the exported script.');
}
/**
* Tests loading the script back into the database.
*/
public function testScriptLoad(): void {
// Generate the script.
$application = new DbDumpApplication();
$command = $application->find('dump-database-d8-mysql');
$command_tester = new CommandTester($command);
$command_tester->execute([]);
$script = $command_tester->getDisplay();
// Store original schemas and drop tables to avoid errors.
$connection = Database::getConnection();
$schema = $connection->schema();
foreach ($this->tables as $table) {
$this->originalTableSchemas[$table] = $this->getTableSchema($table);
$this->originalTableIndexes[$table] = $this->getTableIndexes($table);
$schema->dropTable($table);
}
// This will load the data.
$file = sys_get_temp_dir() . '/' . $this->randomMachineName();
file_put_contents($file, $script);
require_once $file;
// The tables should now exist and the schemas should match the originals.
foreach ($this->tables as $table) {
$this->assertTrue($schema
->tableExists($table), "Table $table created by the database script.");
$this->assertSame($this->originalTableSchemas[$table], $this->getTableSchema($table), "The schema for $table was properly restored.");
$this->assertSame($this->originalTableIndexes[$table], $this->getTableIndexes($table), "The indexes for $table were properly restored.");
}
// Ensure the test config has been replaced.
$config = unserialize($connection->select('config', 'c')->fields('c', ['data'])->condition('name', 'test_config')->execute()->fetchField());
$this->assertSame($this->data, $config, 'Script has properly restored the config table data.');
// Ensure the cache data was not exported.
$this->assertFalse(\Drupal::cache('discovery')
->get('test'), 'Cache data was not exported to the script.');
}
/**
* Helper function to get a simplified schema for a given table.
*
* @param string $table
* The table name.
*
* @return array
* Array keyed by field name, with the values being the field type.
*/
protected function getTableSchema($table) {
// Verify the field type on the data column in the cache table.
// @todo this is MySQL specific.
$query = Database::getConnection()->query("SHOW COLUMNS FROM {" . $table . "}");
$definition = [];
while ($row = $query->fetchAssoc()) {
$definition[$row['Field']] = $row['Type'];
}
return $definition;
}
/**
* Returns indexes for a given table.
*
* @param string $table
* The table to find indexes for.
*
* @return array
* The 'primary key', 'unique keys', and 'indexes' portion of the Drupal
* table schema.
*/
protected function getTableIndexes($table) {
$query = Database::getConnection()->query("SHOW INDEX FROM {" . $table . "}");
$definition = [];
while ($row = $query->fetchAssoc()) {
$index_name = $row['Key_name'];
$column = $row['Column_name'];
// Key the arrays by the index sequence for proper ordering (start at 0).
$order = $row['Seq_in_index'] - 1;
// If specified, add length to the index.
if ($row['Sub_part']) {
$column = [$column, $row['Sub_part']];
}
if ($index_name === 'PRIMARY') {
$definition['primary key'][$order] = $column;
}
elseif ($row['Non_unique'] == 0) {
$definition['unique keys'][$index_name][$order] = $column;
}
else {
$definition['indexes'][$index_name][$order] = $column;
}
}
return $definition;
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\mysql\Kernel\mysql;
use Drupal\Component\Utility\Environment;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\DatabaseException;
use Drupal\KernelTests\Core\Database\DriverSpecificDatabaseTestBase;
/**
* Tests handling of large queries.
*
* @group Database
*/
class LargeQueryTest extends DriverSpecificDatabaseTestBase {
/**
* Tests truncation of messages when max_allowed_packet exception occurs.
*/
public function testMaxAllowedPacketQueryTruncating(): void {
// The max_allowed_packet value is configured per database instance.
// Retrieve the max_allowed_packet value from the current instance and
// check if PHP is configured with sufficient allowed memory to be able
// to generate a query larger than max_allowed_packet.
$max_allowed_packet = $this->connection->query('SELECT @@global.max_allowed_packet')->fetchField();
if (!Environment::checkMemoryLimit($max_allowed_packet + (16 * 1024 * 1024))) {
$this->markTestSkipped('The configured max_allowed_packet exceeds the php memory limit. Therefore the test is skipped.');
}
$long_name = str_repeat('a', $max_allowed_packet + 1);
try {
$this->connection->query('SELECT [name] FROM {test} WHERE [name] = :name', [':name' => $long_name]);
$this->fail("An exception should be thrown for queries larger than 'max_allowed_packet'");
}
catch (DatabaseException $e) {
// Close and re-open the connection. Otherwise we will run into error
// 2006 "MySQL server had gone away" afterwards.
Database::closeConnection();
Database::getConnection();
// Got a packet bigger than 'max_allowed_packet' bytes exception thrown.
$this->assertEquals(1153, $e->getPrevious()->errorInfo[1]);
// 'max_allowed_packet' exception message truncated.
// Use strlen() to count the bytes exactly, not the unicode chars.
$this->assertLessThanOrEqual($max_allowed_packet, strlen($e->getMessage()));
}
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\mysql\Kernel\mysql;
use Drupal\Core\Database\Driver\mysql\Connection;
use Drupal\Core\Database\Driver\mysql\ExceptionHandler;
use Drupal\Core\Database\Driver\mysql\Install\Tasks;
use Drupal\Core\Database\Driver\mysql\Insert;
use Drupal\Core\Database\Driver\mysql\Schema;
use Drupal\Core\Database\Driver\mysql\Upsert;
use Drupal\KernelTests\Core\Database\DriverSpecificDatabaseTestBase;
use Drupal\Tests\Core\Database\Stub\StubPDO;
/**
* Tests the deprecations of the MySQL database driver classes in Core.
*
* @group legacy
* @group Database
*/
class MysqlDriverLegacyTest extends DriverSpecificDatabaseTestBase {
/**
* @covers Drupal\Core\Database\Driver\mysql\Install\Tasks
*/
public function testDeprecationInstallTasks(): void {
$this->expectDeprecation('\Drupal\Core\Database\Driver\mysql\Install\Tasks is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492');
$tasks = new Tasks();
$this->assertInstanceOf(Tasks::class, $tasks);
}
/**
* @covers Drupal\Core\Database\Driver\mysql\Connection
*/
public function testDeprecationConnection(): void {
$this->expectDeprecation('\Drupal\Core\Database\Driver\mysql\Connection is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492');
// @todo https://www.drupal.org/project/drupal/issues/3251084 Remove setting
// the $options parameter.
$options['init_commands']['sql_mode'] = '';
$connection = new Connection($this->createMock(StubPDO::class), $options);
$this->assertInstanceOf(Connection::class, $connection);
}
/**
* @covers Drupal\Core\Database\Driver\mysql\ExceptionHandler
*/
public function testDeprecationExceptionHandler(): void {
$this->expectDeprecation('\Drupal\Core\Database\Driver\mysql\ExceptionHandler is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492');
$handler = new ExceptionHandler();
$this->assertInstanceOf(ExceptionHandler::class, $handler);
}
/**
* @covers Drupal\Core\Database\Driver\mysql\Insert
*/
public function testDeprecationInsert(): void {
$this->expectDeprecation('\Drupal\Core\Database\Driver\mysql\Insert is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492');
$insert = new Insert($this->connection, 'test');
$this->assertInstanceOf(Insert::class, $insert);
}
/**
* @covers Drupal\Core\Database\Driver\mysql\Schema
*/
public function testDeprecationSchema(): void {
$this->expectDeprecation('\Drupal\Core\Database\Driver\mysql\Schema is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492');
$schema = new Schema($this->connection);
$this->assertInstanceOf(Schema::class, $schema);
}
/**
* @covers Drupal\Core\Database\Driver\mysql\Upsert
*/
public function testDeprecationUpsert(): void {
$this->expectDeprecation('\Drupal\Core\Database\Driver\mysql\Upsert is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492');
$upsert = new Upsert($this->connection, 'test');
$this->assertInstanceOf(Upsert::class, $upsert);
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\mysql\Kernel\mysql;
use Drupal\mysql\Driver\Database\mysql\Connection;
use Drupal\KernelTests\Core\Database\DriverSpecificKernelTestBase;
use Drupal\Tests\Core\Database\Stub\StubPDO;
/**
* Tests the deprecations of the MySQL database driver classes in Core.
*
* @group Database
*/
class MysqlDriverTest extends DriverSpecificKernelTestBase {
/**
* @covers \Drupal\mysql\Driver\Database\mysql\Connection
*/
public function testConnection(): void {
$connection = new Connection($this->createMock(StubPDO::class), []);
$this->assertInstanceOf(Connection::class, $connection);
}
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\mysql\Kernel\mysql;
use Drupal\Core\Database\Database;
use Drupal\KernelTests\Core\Database\DriverSpecificDatabaseTestBase;
/**
* Tests the sequences API.
*
* @group Database
* @group legacy
*/
class NextIdTest extends DriverSpecificDatabaseTestBase {
/**
* The modules to enable.
*
* @var array
*/
protected static $modules = ['database_test', 'system'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$table_specification = [
'description' => 'Stores IDs.',
'fields' => [
'value' => [
'description' => 'The value of the sequence.',
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
],
],
'primary key' => ['value'],
];
$this->connection->schema()->createTable('sequences', $table_specification);
}
/**
* Tests that sequences table clear up works when a connection is closed.
*
* @see \Drupal\mysql\Driver\Database\mysql\Connection::__destruct()
*/
public function testDbNextIdClosedConnection(): void {
$this->expectDeprecation('Drupal\Core\Database\Connection::nextId() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Modules should use instead the keyvalue storage for the last used id. See https://www.drupal.org/node/3349345');
$this->expectDeprecation('Drupal\mysql\Driver\Database\mysql\Connection::nextIdDelete() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Modules should use instead the keyvalue storage for the last used id. See https://www.drupal.org/node/3349345');
// Create an additional connection to test closing the connection.
$connection_info = Database::getConnectionInfo();
Database::addConnectionInfo('default', 'next_id', $connection_info['default']);
// Get a few IDs to ensure there the clean up needs to run and there is more
// than one row.
Database::getConnection('next_id')->nextId();
Database::getConnection('next_id')->nextId();
// At this point the sequences table should contain unnecessary rows.
$count = $this->connection->select('sequences')->countQuery()->execute()->fetchField();
$this->assertGreaterThan(1, $count);
// Close the connection.
Database::closeConnection('next_id');
// Test that \Drupal\mysql\Driver\Database\mysql\Connection::__destruct()
// successfully trims the sequences table if the connection is closed.
$count = $this->connection->select('sequences')->countQuery()->execute()->fetchField();
$this->assertEquals(1, $count);
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\mysql\Kernel\mysql\Plugin\views;
use Drupal\Tests\views\Kernel\Plugin\CastedIntFieldJoinTestBase;
/**
* Tests MySQL specific cast handling.
*
* @group Database
*/
class MySqlCastedIntFieldJoinTest extends CastedIntFieldJoinTestBase {
/**
* The db type that should be used for casting fields as integers.
*/
protected string $castingType = 'UNSIGNED';
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\mysql\Kernel\mysql;
use Drupal\Core\Database\Database;
use Drupal\KernelTests\Core\Database\DriverSpecificKernelTestBase;
/**
* Tests that the prefix info for a database schema is correct.
*
* @group Database
*/
class PrefixInfoTest extends DriverSpecificKernelTestBase {
/**
* Tests that DatabaseSchema::getPrefixInfo() returns the right database.
*
* We are testing if the return array of the method
* \Drupal\mysql\Driver\Database\mysql\Schema::getPrefixInfo(). This return
* array is a keyed array with info about amongst other things the database.
* The other two by Drupal core supported databases do not have this variable
* set in the return array.
*/
public function testGetPrefixInfo(): void {
$connection_info = Database::getConnectionInfo('default');
// Copy the default connection info to the 'extra' key.
Database::addConnectionInfo('extra', 'default', $connection_info['default']);
$db1_connection = Database::getConnection('default', 'default');
$db1_schema = $db1_connection->schema();
$db2_connection = Database::getConnection('default', 'extra');
// Get the prefix info for the first database.
$method = new \ReflectionMethod($db1_schema, 'getPrefixInfo');
$db1_info = $method->invoke($db1_schema);
// We change the database after opening the connection, so as to prevent
// connecting to a non-existent database.
$reflection = new \ReflectionObject($db2_connection);
$property = $reflection->getProperty('connectionOptions');
$connection_info['default']['database'] = 'foobar';
$property->setValue($db2_connection, $connection_info['default']);
// For testing purposes, we also change the database info.
$reflection_class = new \ReflectionClass(Database::class);
$property = $reflection_class->getProperty('databaseInfo');
$info = $property->getValue();
$info['extra']['default']['database'] = 'foobar';
$property->setValue(NULL, $info);
$extra_info = Database::getConnectionInfo('extra');
$this->assertSame('foobar', $extra_info['default']['database']);
$db2_schema = $db2_connection->schema();
$db2_info = $method->invoke($db2_schema);
// Each target connection has a different database.
$this->assertNotSame($db2_info['database'], $db1_info['database']);
// The new profile has a different database.
$this->assertSame('foobar', $db2_info['database']);
Database::removeConnection('extra');
}
}

View File

@@ -0,0 +1,345 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\mysql\Kernel\mysql;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\Database\Exception\SchemaTableColumnSizeTooLargeException;
use Drupal\Core\Database\Exception\SchemaTableKeyTooLargeException;
use Drupal\Core\Database\SchemaException;
use Drupal\Core\Database\SchemaObjectDoesNotExistException;
use Drupal\Core\Database\SchemaObjectExistsException;
use Drupal\KernelTests\Core\Database\DriverSpecificSchemaTestBase;
/**
* Tests schema API for the MySQL driver.
*
* @group Database
*/
class SchemaTest extends DriverSpecificSchemaTestBase {
/**
* {@inheritdoc}
*/
public function checkSchemaComment(string $description, string $table, ?string $column = NULL): void {
$comment = $this->schema->getComment($table, $column);
$max_length = $column ? 255 : 60;
$description = Unicode::truncate($description, $max_length, TRUE, TRUE);
$this->assertSame($description, $comment, 'The comment matches the schema description.');
}
/**
* {@inheritdoc}
*/
protected function assertCollation(): void {
// Make sure that varchar fields have the correct collations.
$columns = $this->connection->query('SHOW FULL COLUMNS FROM {test_table}');
foreach ($columns as $column) {
if ($column->Field == 'test_field_string') {
$string_check = $column->Collation;
}
if ($column->Field == 'test_field_string_ascii') {
$string_ascii_check = $column->Collation;
}
}
$this->assertMatchesRegularExpression('#^(utf8mb4_general_ci|utf8mb4_0900_ai_ci)$#', $string_check, 'test_field_string should have a utf8mb4_general_ci or a utf8mb4_0900_ai_ci collation, but it has not.');
$this->assertSame('ascii_general_ci', $string_ascii_check, 'test_field_string_ascii should have a ascii_general_ci collation, but it has not.');
}
/**
* {@inheritdoc}
*/
public function testTableWithSpecificDataType(): void {
$table_specification = [
'description' => 'Schema table description.',
'fields' => [
'timestamp' => [
'mysql_type' => 'timestamp',
'not null' => FALSE,
'default' => NULL,
],
],
];
$this->schema->createTable('test_timestamp', $table_specification);
$this->assertTrue($this->schema->tableExists('test_timestamp'));
}
/**
* Tests that indexes on string fields are limited to 191 characters on MySQL.
*
* @see \Drupal\mysql\Driver\Database\mysql\Schema::getNormalizedIndexes()
*/
public function testIndexLength(): void {
$table_specification = [
'fields' => [
'id' => [
'type' => 'int',
'default' => NULL,
],
'test_field_text' => [
'type' => 'text',
'not null' => TRUE,
],
'test_field_string_long' => [
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
],
'test_field_string_ascii_long' => [
'type' => 'varchar_ascii',
'length' => 255,
],
'test_field_string_short' => [
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
],
],
'indexes' => [
'test_regular' => [
'test_field_text',
'test_field_string_long',
'test_field_string_ascii_long',
'test_field_string_short',
],
'test_length' => [
['test_field_text', 128],
['test_field_string_long', 128],
['test_field_string_ascii_long', 128],
['test_field_string_short', 128],
],
'test_mixed' => [
['test_field_text', 200],
'test_field_string_long',
['test_field_string_ascii_long', 200],
'test_field_string_short',
],
],
];
$this->schema->createTable('test_table_index_length', $table_specification);
// Ensure expected exception thrown when adding index with missing info.
$expected_exception_message = "MySQL needs the 'test_field_text' field specification in order to normalize the 'test_regular' index";
$missing_field_spec = $table_specification;
unset($missing_field_spec['fields']['test_field_text']);
try {
$this->schema->addIndex('test_table_index_length', 'test_separate', [['test_field_text', 200]], $missing_field_spec);
$this->fail('SchemaException not thrown when adding index with missing information.');
}
catch (SchemaException $e) {
$this->assertEquals($expected_exception_message, $e->getMessage());
}
// Add a separate index.
$this->schema->addIndex('test_table_index_length', 'test_separate', [['test_field_text', 200]], $table_specification);
$table_specification_with_new_index = $table_specification;
$table_specification_with_new_index['indexes']['test_separate'] = [['test_field_text', 200]];
// Ensure that the exceptions of addIndex are thrown as expected.
try {
$this->schema->addIndex('test_table_index_length', 'test_separate', [['test_field_text', 200]], $table_specification);
$this->fail('\Drupal\Core\Database\SchemaObjectExistsException exception missed.');
}
catch (SchemaObjectExistsException $e) {
// Expected exception; just continue testing.
}
try {
$this->schema->addIndex('test_table_non_existing', 'test_separate', [['test_field_text', 200]], $table_specification);
$this->fail('\Drupal\Core\Database\SchemaObjectDoesNotExistException exception missed.');
}
catch (SchemaObjectDoesNotExistException $e) {
// Expected exception; just continue testing.
}
// Get index information.
$results = $this->connection->query('SHOW INDEX FROM {test_table_index_length}');
$expected_lengths = [
'test_regular' => [
'test_field_text' => 191,
'test_field_string_long' => 191,
'test_field_string_ascii_long' => NULL,
'test_field_string_short' => NULL,
],
'test_length' => [
'test_field_text' => 128,
'test_field_string_long' => 128,
'test_field_string_ascii_long' => 128,
'test_field_string_short' => NULL,
],
'test_mixed' => [
'test_field_text' => 191,
'test_field_string_long' => 191,
'test_field_string_ascii_long' => 200,
'test_field_string_short' => NULL,
],
'test_separate' => [
'test_field_text' => 191,
],
];
// Count the number of columns defined in the indexes.
$column_count = 0;
foreach ($table_specification_with_new_index['indexes'] as $index) {
foreach ($index as $field) {
$column_count++;
}
}
$test_count = 0;
foreach ($results as $result) {
$this->assertEquals($expected_lengths[$result->Key_name][$result->Column_name], $result->Sub_part, 'Index length matches expected value.');
$test_count++;
}
$this->assertEquals($column_count, $test_count, 'Number of tests matches expected value.');
}
/**
* @covers \Drupal\mysql\Driver\Database\mysql\Schema::introspectIndexSchema
*/
public function testIntrospectIndexSchema(): void {
$table_specification = [
'fields' => [
'id' => [
'type' => 'int',
'not null' => TRUE,
'default' => 0,
],
'test_field_1' => [
'type' => 'int',
'not null' => TRUE,
'default' => 0,
],
'test_field_2' => [
'type' => 'int',
'default' => 0,
],
'test_field_3' => [
'type' => 'int',
'default' => 0,
],
'test_field_4' => [
'type' => 'int',
'default' => 0,
],
'test_field_5' => [
'type' => 'int',
'default' => 0,
],
],
'primary key' => ['id', 'test_field_1'],
'unique keys' => [
'test_field_2' => ['test_field_2'],
'test_field_3_test_field_4' => ['test_field_3', 'test_field_4'],
],
'indexes' => [
'test_field_4' => ['test_field_4'],
'test_field_4_test_field_5' => ['test_field_4', 'test_field_5'],
],
];
$table_name = strtolower($this->getRandomGenerator()->name());
$this->schema->createTable($table_name, $table_specification);
unset($table_specification['fields']);
$introspect_index_schema = new \ReflectionMethod(get_class($this->schema), 'introspectIndexSchema');
$index_schema = $introspect_index_schema->invoke($this->schema, $table_name);
$this->assertEquals($table_specification, $index_schema);
}
/**
* Tests SchemaTableKeyTooLargeException.
*/
public function testSchemaTableKeyTooLargeException(): void {
$this->expectException(SchemaTableKeyTooLargeException::class);
$this->schema->createTable('test_schema', [
'description' => 'Tests SchemaTableKeyTooLargeException.',
'fields' => [
'id' => [
'type' => 'varchar',
'length' => 64,
'not null' => TRUE,
],
'id1' => [
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
],
'id2' => [
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
],
'id3' => [
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
],
'id4' => [
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
],
'id5' => [
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
],
],
'primary key' => ['id'],
'indexes' => [
'key1' => ['id1', 'id2', 'id3', 'id4', 'id5'],
],
]);
}
/**
* Tests SchemaTableColumnSizeTooLargeException.
*/
public function testSchemaTableColumnSizeTooLargeException(): void {
$this->expectException(SchemaTableColumnSizeTooLargeException::class);
$this->expectExceptionMessage("Column length too big for column 'too_large' (max = 16383); use BLOB or TEXT instead");
$this->schema->createTable('test_schema', [
'description' => 'Tests SchemaTableColumnSizeTooLargeException.',
'fields' => [
'too_large' => [
'type' => 'varchar',
'length' => 65536,
'not null' => TRUE,
],
],
]);
}
/**
* Tests adding a primary key when sql_generate_invisible_primary_key is on.
*/
public function testGeneratedInvisiblePrimaryKey(): void {
$is_maria = method_exists($this->connection, 'isMariaDb') && $this->connection->isMariaDb();
if ($this->connection->databaseType() !== 'mysql' || $is_maria || version_compare($this->connection->version(), '8.0.30', '<')) {
$this->markTestSkipped('This test only runs on MySQL 8.0.30 and above');
}
try {
$this->connection->query("SET sql_generate_invisible_primary_key = 1;")->execute();
}
catch (DatabaseExceptionWrapper $e) {
$this->markTestSkipped('This test requires the SESSION_VARIABLES_ADMIN privilege.');
}
$this->schema->createTable('test_primary_key', [
'fields' => [
'foo' => [
'type' => 'varchar',
'length' => 1,
],
],
]);
$this->schema->addField('test_primary_key', 'id', [
'type' => 'serial',
'not null' => TRUE,
], ['primary key' => ['id']]);
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\mysql\Kernel\mysql;
use Drupal\KernelTests\Core\Database\SchemaUniquePrefixedKeysIndexTestBase;
/**
* Tests adding UNIQUE keys to tables.
*
* @group Database
*/
class SchemaUniquePrefixedKeysIndexTest extends SchemaUniquePrefixedKeysIndexTestBase {
/**
* {@inheritdoc}
*/
protected string $columnValue = '1234567890 bar';
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\mysql\Kernel\mysql;
use Drupal\KernelTests\Core\Database\DriverSpecificDatabaseTestBase;
/**
* Tests compatibility of the MySQL driver with various sql_mode options.
*
* @group Database
*/
class SqlModeTest extends DriverSpecificDatabaseTestBase {
/**
* Tests quoting identifiers in queries.
*/
public function testQuotingIdentifiers(): void {
// Use SQL-reserved words for both the table and column names.
$query = $this->connection->query('SELECT [update] FROM {select}');
$this->assertEquals('Update value 1', $query->fetchObject()->update);
$this->assertStringContainsString('SELECT `update` FROM `', $query->getQueryString());
}
/**
* {@inheritdoc}
*/
protected function getDatabaseConnectionInfo() {
$info = parent::getDatabaseConnectionInfo();
// This runs during setUp(), so is not yet skipped for non MySQL databases.
// We defer skipping the test to later in setUp(), so that that can be
// based on databaseType() rather than 'driver', but here all we have to go
// on is 'driver'.
if ($info['default']['driver'] === 'mysql') {
$info['default']['init_commands']['sql_mode'] = "SET sql_mode = ''";
}
return $info;
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\mysql\Kernel\mysql;
use Drupal\KernelTests\Core\Database\DriverSpecificSyntaxTestBase;
/**
* Tests MySql syntax interpretation.
*
* @group Database
*/
class SyntaxTest extends DriverSpecificSyntaxTestBase {
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\mysql\Kernel\mysql;
use Drupal\KernelTests\Core\Database\TemporaryQueryTestBase;
/**
* Tests the temporary query functionality.
*
* @group Database
*/
class TemporaryQueryTest extends TemporaryQueryTestBase {
/**
* Confirms that temporary tables work.
*/
public function testTemporaryQuery(): void {
parent::testTemporaryQuery();
$connection = $this->getConnection();
$table_name_test = $connection->queryTemporary('SELECT [name] FROM {test}', []);
// Assert that the table is indeed a temporary one.
$temporary_table_info = $connection->query("SHOW CREATE TABLE {" . $table_name_test . "}")->fetchAssoc();
$this->assertStringContainsString('CREATE TEMPORARY TABLE', $temporary_table_info['Create Table']);
// Assert that both have the same field names.
$normal_table_fields = $connection->query("SELECT * FROM {test}")->fetch();
$temp_table_name = $connection->queryTemporary('SELECT * FROM {test}');
$temp_table_fields = $connection->query("SELECT * FROM {" . $temp_table_name . "}")->fetch();
$normal_table_fields = array_keys(get_object_vars($normal_table_fields));
$temp_table_fields = array_keys(get_object_vars($temp_table_fields));
$this->assertEmpty(array_diff($normal_table_fields, $temp_table_fields));
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\mysql\Kernel\mysql;
use Drupal\KernelTests\Core\Database\DriverSpecificTransactionTestBase;
/**
* Tests transaction for the MySQL driver.
*
* @group Database
*/
class TransactionTest extends DriverSpecificTransactionTestBase {
}

View File

@@ -0,0 +1,164 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\mysql\Unit;
use Drupal\mysql\Driver\Database\mysql\Connection;
use Drupal\Tests\UnitTestCase;
use Prophecy\Argument;
/**
* Tests MySQL database connections.
*
* @coversDefaultClass \Drupal\mysql\Driver\Database\mysql\Connection
* @group Database
*/
class ConnectionTest extends UnitTestCase {
/**
* A PDO statement prophecy.
*
* @var \PDOStatement|\Prophecy\Prophecy\ObjectProphecy
*/
private $pdoStatement;
/**
* A PDO object prophecy.
*
* @var \PDO|\Prophecy\Prophecy\ObjectProphecy
*/
private $pdoConnection;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->pdoStatement = $this->prophesize(\PDOStatement::class);
$this->pdoConnection = $this->prophesize(\PDO::class);
}
/**
* Creates a Connection object for testing.
*
* @return \Drupal\mysql\Driver\Database\mysql\Connection
*/
private function createConnection(): Connection {
$this->pdoStatement
->setFetchMode(Argument::any())
->shouldBeCalled()
->willReturn(TRUE);
$this->pdoStatement
->execute(Argument::any())
->shouldBeCalled()
->willReturn(TRUE);
$this->pdoConnection
->prepare('SELECT VERSION()', Argument::any())
->shouldBeCalled()
->willReturn($this->pdoStatement->reveal());
/** @var \PDO $pdo_connection */
$pdo_connection = $this->pdoConnection->reveal();
return new class($pdo_connection) extends Connection {
public function __construct(\PDO $connection) {
$this->connection = $connection;
$this->setPrefix('');
}
};
}
/**
* @covers ::version
* @covers ::isMariaDb
* @dataProvider providerVersionAndIsMariaDb
*/
public function testVersionAndIsMariaDb(bool $expected_is_mariadb, string $server_version, string $expected_version): void {
$this->pdoStatement
->fetchColumn(Argument::any())
->shouldBeCalled()
->willReturn($server_version);
$connection = $this->createConnection();
$is_mariadb = $connection->isMariaDb();
$version = $connection->version();
$this->assertSame($expected_is_mariadb, $is_mariadb);
$this->assertSame($expected_version, $version);
}
/**
* Provides test data.
*
* @return array
*/
public static function providerVersionAndIsMariaDb(): array {
return [
// MariaDB.
[
TRUE,
'10.2.0-MariaDB',
'10.2.0-MariaDB',
],
[
TRUE,
'10.2.1-MARIADB',
'10.2.1-MARIADB',
],
[
TRUE,
'10.2.2-alphaX-MARIADB',
'10.2.2-alphaX-MARIADB',
],
[
TRUE,
'5.5.5-10.2.20-MariaDB-1:10.2.20+maria~bionic',
'10.2.20-MariaDB-1:10.2.20+maria~bionic',
],
[
TRUE,
'5.5.5-10.3.22-MariaDB-0+deb10u1',
'10.3.22-MariaDB-0+deb10u1',
],
[
TRUE,
'5.5.5-10.3.22-buzz+-MariaDB-0+deb10u1',
'10.3.22-buzz+-MariaDB-0+deb10u1',
],
// MySQL.
[
FALSE,
'5.5.5-10.2.20-notMariaDB',
'5.5.5-10.2.20-notMariaDB',
],
[
FALSE,
'5.5.5',
'5.5.5',
],
[
FALSE,
'5.5.5-',
'5.5.5-',
],
[
FALSE,
'5.7.28',
'5.7.28',
],
[
FALSE,
'5.7.28-31',
'5.7.28-31',
],
];
}
}

View File

@@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\mysql\Unit;
use Drupal\mysql\Driver\Database\mysql\Connection;
use Drupal\mysql\Driver\Database\mysql\Install\Tasks;
use Drupal\Tests\UnitTestCase;
/**
* Tests the MySQL install tasks.
*
* @coversDefaultClass \Drupal\mysql\Driver\Database\mysql\Install\Tasks
* @group Database
*/
class InstallTasksTest extends UnitTestCase {
/**
* A connection object prophecy.
*
* @var \Drupal\mysql\Driver\Database\mysql\Connection|\Prophecy\Prophecy\ObjectProphecy
*/
private $connection;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->connection = $this->prophesize(Connection::class);
}
/**
* Creates a Tasks object for testing.
*
* @return \Drupal\mysql\Driver\Database\mysql\Install\Tasks
*/
private function createTasks(): Tasks {
/** @var \Drupal\mysql\Driver\Database\mysql\Connection $connection */
$connection = $this->connection->reveal();
return new class($connection) extends Tasks {
private $connection;
public function __construct(Connection $connection) {
$this->connection = $connection;
}
protected function isConnectionActive() {
return TRUE;
}
protected function getConnection() {
return $this->connection;
}
protected function t($string, array $args = [], array $options = []) {
return $string;
}
};
}
/**
* Creates a Tasks object for testing, without connection.
*
* @return \Drupal\mysql\Driver\Database\mysql\Install\Tasks
*/
private function createTasksNoConnection(): Tasks {
return new class() extends Tasks {
protected function isConnectionActive() {
return FALSE;
}
protected function getConnection() {
return NULL;
}
protected function t($string, array $args = [], array $options = []) {
return $string;
}
};
}
/**
* @covers ::minimumVersion
* @covers ::name
* @dataProvider providerNameAndMinimumVersion
*/
public function testNameAndMinimumVersion(bool $is_mariadb, string $expected_name, string $expected_minimum_version): void {
$this->connection
->isMariaDb()
->shouldBeCalledTimes(2)
->willReturn($is_mariadb);
$tasks = $this->createTasks();
$minimum_version = $tasks->minimumVersion();
$name = $tasks->name();
$this->assertSame($expected_minimum_version, $minimum_version);
$this->assertSame($expected_name, $name);
}
/**
* Provides test data.
*
* @return array
*/
public static function providerNameAndMinimumVersion(): array {
return [
[
TRUE,
'MariaDB',
Tasks::MARIADB_MINIMUM_VERSION,
],
[
FALSE,
'MySQL, Percona Server, or equivalent',
Tasks::MYSQL_MINIMUM_VERSION,
],
];
}
/**
* @covers ::name
*/
public function testNameWithNoConnection(): void {
$tasks = $this->createTasksNoConnection();
$this->assertSame('MySQL, MariaDB, Percona Server, or equivalent', $tasks->name());
}
}