diff options
author | webchick <drupal@webchick.net> | 2015-10-02 11:59:39 -0700 |
---|---|---|
committer | webchick <drupal@webchick.net> | 2015-10-02 11:59:39 -0700 |
commit | b29158839c8cb829132323a54230eb85ab7a6765 (patch) | |
tree | fe302a1a86ece0f9cd2b0ce0d807f59d3379d64f | |
parent | b904c4b375ab4cb3b029d2b65512545d2e5192af (diff) | |
download | drupal-b29158839c8cb829132323a54230eb85ab7a6765.tar.gz drupal-b29158839c8cb829132323a54230eb85ab7a6765.zip |
Issue #2550291 by neclimdul, phenaproxima: Improve and generalize database dump tools
12 files changed, 619 insertions, 147 deletions
diff --git a/core/lib/Drupal/Core/Command/DbCommandBase.php b/core/lib/Drupal/Core/Command/DbCommandBase.php new file mode 100644 index 00000000000..bd401d80497 --- /dev/null +++ b/core/lib/Drupal/Core/Command/DbCommandBase.php @@ -0,0 +1,65 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Command\DbCommandBase. + */ + +namespace Drupal\Core\Command; + +use Drupal\Core\Database\Database; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; + +/** + * Base command that abstracts handling of database connection arguments. + */ +class DbCommandBase extends Command { + + /** + * {@inheritdoc} + */ + protected function configure() { + $this->addOption('database', NULL, InputOption::VALUE_OPTIONAL, 'The database connection name to use.', 'default') + ->addOption('database-url', 'db-url', InputOption::VALUE_OPTIONAL, 'A database url to parse and use as the database connection.') + ->addOption('prefix', NULL, InputOption::VALUE_OPTIONAL, 'Override or set the table prefix used in the database connection.'); + } + + /** + * Parse input options decide on a database. + * + * @param \Symfony\Component\Console\Input\InputInterface $input + * Input object. + * @return \Drupal\Core\Database\Connection + */ + protected function getDatabaseConnection(InputInterface $input) { + // Load connection from a url. + if ($input->getOption('database-url')) { + // @todo this could probably be refactored to not use a global connection. + // Ensure database connection isn't set. + if (Database::getConnectionInfo('db-tools')) { + throw new \RuntimeException('Database "db-tools" is already defined. Cannot define database provided.'); + } + $info = Database::convertDbUrlToConnectionInfo($input->getOption('database-url'), \Drupal::root()); + Database::addConnectionInfo('db-tools', 'default', $info); + $key = 'db-tools'; + } + else { + $key = $input->getOption('database'); + } + + // If they supplied a prefix, replace it in the connection information. + $prefix = $input->getOption('prefix'); + if ($prefix) { + $info = Database::getConnectionInfo($key)['default']; + $info['prefix']['default'] = $prefix; + + Database::removeConnection($key); + Database::addConnectionInfo($key, 'default', $info); + } + + return Database::getConnection('default', $key); + } + +} diff --git a/core/lib/Drupal/Core/Command/DbDumpApplication.php b/core/lib/Drupal/Core/Command/DbDumpApplication.php index 080e20f88b2..bb5b715b43b 100644 --- a/core/lib/Drupal/Core/Command/DbDumpApplication.php +++ b/core/lib/Drupal/Core/Command/DbDumpApplication.php @@ -7,8 +7,6 @@ namespace Drupal\Core\Command; -use Drupal\Core\Database\Connection; -use Drupal\Core\Extension\ModuleHandlerInterface; use Symfony\Component\Console\Application; use Symfony\Component\Console\Input\InputInterface; @@ -18,34 +16,6 @@ use Symfony\Component\Console\Input\InputInterface; class DbDumpApplication extends Application { /** - * The database connection. - * - * @var \Drupal\Core\Database\Connection - */ - protected $connection; - - /** - * The module handler. - * - * @var \Drupal\Core\Extension\ModuleHandlerInterface - */ - protected $moduleHandler; - - /** - * Construct the application. - * - * @param \Drupal\Core\Database\Connection $connection - * The database connection. - * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler - * The module handler. - */ - function __construct(Connection $connection, ModuleHandlerInterface $module_handler) { - $this->connection = $connection; - $this->moduleHandler = $module_handler; - parent::__construct(); - } - - /** * {@inheritdoc} */ protected function getCommandName(InputInterface $input) { @@ -58,7 +28,7 @@ class DbDumpApplication extends Application { protected function getDefaultCommands() { // Even though this is a single command, keep the HelpCommand (--help). $default_commands = parent::getDefaultCommands(); - $default_commands[] = new DbDumpCommand($this->connection, $this->moduleHandler); + $default_commands[] = new DbDumpCommand(); return $default_commands; } diff --git a/core/lib/Drupal/Core/Command/DbDumpCommand.php b/core/lib/Drupal/Core/Command/DbDumpCommand.php index 69512705f2d..95e99069d43 100644 --- a/core/lib/Drupal/Core/Command/DbDumpCommand.php +++ b/core/lib/Drupal/Core/Command/DbDumpCommand.php @@ -9,9 +9,8 @@ namespace Drupal\Core\Command; use Drupal\Component\Utility\Variable; use Drupal\Core\Database\Connection; -use Drupal\Core\Extension\ModuleHandlerInterface; -use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** @@ -30,21 +29,7 @@ use Symfony\Component\Console\Output\OutputInterface; * * @see \Drupal\Core\Command\DbDumpApplication */ -class DbDumpCommand extends Command { - - /** - * The database connection. - * - * @var \Drupal\Core\Database\Connection $connection - */ - protected $connection; - - /** - * The module handler. - * - * @var \Drupal\Core\Extension\ModuleHandlerInterface - */ - protected $moduleHandler; +class DbDumpCommand extends DbCommandBase { /** * An array of table patterns to exclude completely. @@ -56,80 +41,77 @@ class DbDumpCommand extends Command { protected $excludeTables = ['simpletest.+']; /** - * Table patterns for which to only dump the schema, no data. - * - * @var array - */ - protected $schemaOnly = ['cache.*', 'sessions', 'watchdog']; - - /** - * Construct the database dump command. - * - * @param \Drupal\Core\Database\Connection $connection - * The database connection to use. - * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler - * The module handler to use. - */ - function __construct(Connection $connection, ModuleHandlerInterface $module_handler) { - // Check this is MySQL. - if ($connection->databaseType() !== 'mysql') { - throw new \RuntimeException('This script can only be used with MySQL database backends.'); - } - - $this->connection = $connection; - $this->moduleHandler = $module_handler; - parent::__construct(); - } - - /** * {@inheritdoc} */ protected function configure() { $this->setName('dump-database-d8-mysql') - ->setDescription('Dump the current database to a generation script'); + ->setDescription('Dump the current database to a generation script') + ->addOption('schema-only', NULL, InputOption::VALUE_OPTIONAL, 'A comma separated list of tables to only export the schema without data.', 'cache.*,sessions,watchdog'); + parent::configure(); } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { + $connection = $this->getDatabaseConnection($input); + // If not explicitly set, disable ANSI which will break generated php. if ($input->hasParameterOption(['--ansi']) !== TRUE) { $output->setDecorated(FALSE); } - $output->writeln($this->generateScript(), OutputInterface::OUTPUT_RAW); + $schema_tables = $input->getOption('schema-only'); + $schema_tables = explode(',', $schema_tables); + + $output->writeln($this->generateScript($connection, $schema_tables), OutputInterface::OUTPUT_RAW); } /** * Generates the database script. * - * @return string + * @param \Drupal\Core\Database\Connection $connection + * The database connection to use. + * @param array $schema_only + * Table patterns for which to only dump the schema, no data. + * @return string The PHP script. * The PHP script. */ - protected function generateScript() { + protected function generateScript(Connection $connection, array $schema_only = []) { $tables = ''; - foreach ($this->getTables() as $table) { - $schema = $this->getTableSchema($table); - $data = $this->getTableData($table); + + $schema_only_patterns = []; + foreach ($schema_only as $match) { + $schema_only_patterns[] = '/^' . $match . '$/'; + } + + foreach ($this->getTables($connection) as $table) { + $schema = $this->getTableSchema($connection, $table); + // Check for schema only. + if (empty($schema_only_patterns) || preg_replace($schema_only_patterns, '', $table)) { + $data = $this->getTableData($connection, $table); + } + else { + $data = []; + } $tables .= $this->getTableScript($table, $schema, $data); } $script = $this->getTemplate(); // Substitute in the tables. $script = str_replace('{{TABLES}}', trim($tables), $script); - // Modules. - $script = str_replace('{{MODULES}}', $this->getModulesScript(), $script); return trim($script); } /** * Returns a list of tables, not including those set to be excluded. * - * @return array + * @param \Drupal\Core\Database\Connection $connection + * The database connection to use. + * @return array An array of table names. * An array of table names. */ - protected function getTables() { - $tables = array_values($this->connection->schema()->findTables('%')); + protected function getTables(Connection $connection) { + $tables = array_values($connection->schema()->findTables('%')); foreach ($tables as $key => $table) { // Remove any explicitly excluded tables. @@ -146,6 +128,8 @@ class DbDumpCommand extends Command { /** * Returns a schema array for a given table. * + * @param \Drupal\Core\Database\Connection $connection + * The database connection to use. * @param string $table * The table name. * @@ -154,14 +138,19 @@ class DbDumpCommand extends Command { * * @todo This implementation is hard-coded for MySQL. */ - protected function getTableSchema($table) { - $query = $this->connection->query("SHOW FULL COLUMNS FROM {" . $table . "}"); + protected function getTableSchema(Connection $connection, $table) { + // Check this is MySQL. + if ($connection->databaseType() !== 'mysql') { + throw new \RuntimeException('This script can only be used with MySQL database backends.'); + } + + $query = $connection->query("SHOW FULL COLUMNS FROM {" . $table . "}"); $definition = []; while (($row = $query->fetchAssoc()) !== FALSE) { $name = $row['Field']; // Parse out the field type and meta information. preg_match('@([a-z]+)(?:\((\d+)(?:,(\d+))?\))?\s*(unsigned)?@', $row['Type'], $matches); - $type = $this->fieldTypeMap($matches[1]); + $type = $this->fieldTypeMap($connection, $matches[1]); if ($row['Extra'] === 'auto_increment') { // If this is an auto increment, then the type is 'serial'. $type = 'serial'; @@ -170,7 +159,7 @@ class DbDumpCommand extends Command { 'type' => $type, 'not null' => $row['Null'] === 'NO', ]; - if ($size = $this->fieldSizeMap($matches[1])) { + if ($size = $this->fieldSizeMap($connection, $matches[1])) { $definition['fields'][$name]['size'] = $size; } if (isset($matches[2]) && $type === 'numeric') { @@ -216,10 +205,10 @@ class DbDumpCommand extends Command { } // Set primary key, unique keys, and indexes. - $this->getTableIndexes($table, $definition); + $this->getTableIndexes($connection, $table, $definition); // Set table collation. - $this->getTableCollation($table, $definition); + $this->getTableCollation($connection, $table, $definition); return $definition; } @@ -227,15 +216,17 @@ class DbDumpCommand extends Command { /** * Adds primary key, unique keys, and index information to the schema. * + * @param \Drupal\Core\Database\Connection $connection + * The database connection to use. * @param string $table * The table to find indexes for. * @param array &$definition * The schema definition to modify. */ - protected function getTableIndexes($table, &$definition) { + protected function getTableIndexes(Connection $connection, $table, &$definition) { // Note, this query doesn't support ordering, so that is worked around // below by keying the array on Seq_in_index. - $query = $this->connection->query("SHOW INDEX FROM {" . $table . "}"); + $query = $connection->query("SHOW INDEX FROM {" . $table . "}"); while (($row = $query->fetchAssoc()) !== FALSE) { $index_name = $row['Key_name']; $column = $row['Column_name']; @@ -262,13 +253,15 @@ class DbDumpCommand extends Command { /** * Set the table collation. * + * @param \Drupal\Core\Database\Connection $connection + * The database connection to use. * @param string $table * The table to find indexes for. * @param array &$definition * The schema definition to modify. */ - protected function getTableCollation($table, &$definition) { - $query = $this->connection->query("SHOW TABLE STATUS LIKE '{" . $table . "}'"); + protected function getTableCollation(Connection $connection, $table, &$definition) { + $query = $connection->query("SHOW TABLE STATUS LIKE '{" . $table . "}'"); $data = $query->fetchAssoc(); // Set `mysql_character_set`. This will be ignored by other backends. @@ -280,21 +273,17 @@ class DbDumpCommand extends Command { * * If a table is set to be schema only, and empty array is returned. * + * @param \Drupal\Core\Database\Connection $connection + * The database connection to use. * @param string $table * The table to query. * * @return array * The data from the table as an array. */ - protected function getTableData($table) { - // Check for schema only. - foreach ($this->schemaOnly as $schema_only) { - if (preg_match('/^' . $schema_only . '$/', $table)) { - return []; - } - } - $order = $this->getFieldOrder($table); - $query = $this->connection->query("SELECT * FROM {" . $table . "} " . $order ); + protected function getTableData(Connection $connection, $table) { + $order = $this->getFieldOrder($connection, $table); + $query = $connection->query("SELECT * FROM {" . $table . "} " . $order); $results = []; while (($row = $query->fetchAssoc()) !== FALSE) { $results[] = $row; @@ -305,6 +294,8 @@ class DbDumpCommand extends Command { /** * Given a database field type, return a Drupal type. * + * @param \Drupal\Core\Database\Connection $connection + * The database connection to use. * @param string $type * The MySQL field type. * @@ -312,9 +303,9 @@ class DbDumpCommand extends Command { * The Drupal schema field type. If there is no mapping, the original field * type is returned. */ - protected function fieldTypeMap($type) { + protected function fieldTypeMap(Connection $connection, $type) { // Convert everything to lowercase. - $map = array_map('strtolower', $this->connection->schema()->getFieldTypeMap()); + $map = array_map('strtolower', $connection->schema()->getFieldTypeMap()); $map = array_flip($map); // The MySql map contains type:size. Remove the size part. @@ -324,15 +315,17 @@ class DbDumpCommand extends Command { /** * Given a database field type, return a Drupal size. * + * @param \Drupal\Core\Database\Connection $connection + * The database connection to use. * @param string $type * The MySQL field type. * * @return string * The Drupal schema field size. */ - protected function fieldSizeMap($type) { + protected function fieldSizeMap(Connection $connection, $type) { // Convert everything to lowercase. - $map = array_map('strtolower', $this->connection->schema()->getFieldTypeMap()); + $map = array_map('strtolower', $connection->schema()->getFieldTypeMap()); $map = array_flip($map); $schema_type = explode(':', $map[$type])[0]; @@ -346,24 +339,26 @@ class DbDumpCommand extends Command { /** * Gets field ordering for a given table. * + * @param \Drupal\Core\Database\Connection $connection + * The database connection to use. * @param string $table * The table name. * * @return string * The order string to append to the query. */ - protected function getFieldOrder($table) { + protected function getFieldOrder(Connection $connection, $table) { // @todo this is MySQL only since there are no Database API functions for // table column data. // @todo this code is duplicated in `core/scripts/migrate-db.sh`. - $connection_info = $this->connection->getConnectionOptions(); + $connection_info = $connection->getConnectionOptions(); // Order by primary keys. $order = ''; $query = "SELECT `COLUMN_NAME` FROM `information_schema`.`COLUMNS` WHERE (`TABLE_SCHEMA` = '" . $connection_info['database'] . "') AND (`TABLE_NAME` = '{" . $table . "}') AND (`COLUMN_KEY` = 'PRI') ORDER BY COLUMN_NAME"; - $results = $this->connection->query($query); + $results = $connection->query($query); while (($row = $results->fetchAssoc()) !== FALSE) { $order .= $row['COLUMN_NAME'] . ', '; } @@ -384,12 +379,9 @@ class DbDumpCommand extends Command { <?php /** * @file - * Filled installation of Drupal 8.0, for test purposes. - * - * This file was generated by the dump-database-d8.php script, from an - * installation of Drupal 8. It has the following modules installed: + * A database agnostic dump for testing purposes. * -{{MODULES}} + * This file was generated by the Drupal 8.0 db-tools.php script. */ use Drupal\Core\Database\Database; @@ -431,20 +423,4 @@ ENDOFSCRIPT; return $output; } - /** - * List of modules enabled for insertion into the script docblock. - * - * @return string - * The formatted list of enabled modules. - */ - protected function getModulesScript() { - $output = ''; - $modules = $this->moduleHandler->getModuleList(); - ksort($modules); - foreach ($modules as $module => $filename) { - $output .= " * - $module\n"; - } - return rtrim($output, "\n"); - } - } diff --git a/core/lib/Drupal/Core/Command/DbImportCommand.php b/core/lib/Drupal/Core/Command/DbImportCommand.php new file mode 100644 index 00000000000..de41089d2b1 --- /dev/null +++ b/core/lib/Drupal/Core/Command/DbImportCommand.php @@ -0,0 +1,72 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Command\DbDumpCommand. + */ + +namespace Drupal\Core\Command; + +use Drupal\Core\Database\Connection; +use Drupal\Core\Database\Database; +use Drupal\Core\Database\SchemaObjectExistsException; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Provides a command to import the current database from a script. + * + * This script runs on databases exported using using one of the database dump + * commands and imports it into the current database connection. + * + * @see \Drupal\Core\Command\DbImportApplication + */ +class DbImportCommand extends DbCommandBase { + + /** + * {@inheritdoc} + */ + protected function configure() { + parent::configure(); + $this->setName('import') + ->setDescription('Import database from a generation script.') + ->addArgument('script', InputOption::VALUE_REQUIRED, 'Import script'); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) { + $script = $input->getArgument('script'); + if (!is_file($script)) { + $output->writeln('File must exist.'); + return; + } + + $connection = $this->getDatabaseConnection($input); + $this->runScript($connection, $script); + $output->writeln('Import completed successfully.'); + } + + /** + * Run the database script. + * + * @param \Drupal\Core\Database\Connection $connection + * Connection used by the script when included. + * @param string $script + * Path to dump script. + */ + protected function runScript(Connection $connection, $script) { + if (substr($script, -3) == '.gz') { + $script = "compress.zlib://$script"; + } + try { + require $script; + } + catch (SchemaObjectExistsException $e) { + throw new \RuntimeException('An existing Drupal installation exists at this location. Try removing all tables or changing the database prefix in your settings.php file.'); + } + } + +} diff --git a/core/lib/Drupal/Core/Command/DbToolsApplication.php b/core/lib/Drupal/Core/Command/DbToolsApplication.php new file mode 100644 index 00000000000..fbf0b4bb169 --- /dev/null +++ b/core/lib/Drupal/Core/Command/DbToolsApplication.php @@ -0,0 +1,34 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Command\DbToolsApplication. + */ + +namespace Drupal\Core\Command; + +use Symfony\Component\Console\Application; + +/** + * Provides a command to import a database generation script. + */ +class DbToolsApplication extends Application { + + /** + * {@inheritdoc} + */ + public function __construct() { + parent::__construct('Database Tools', '8.0.x'); + } + + /** + * {@inheritdoc} + */ + protected function getDefaultCommands() { + $default_commands = parent::getDefaultCommands(); + $default_commands[] = new DbDumpCommand(); + $default_commands[] = new DbImportCommand(); + return $default_commands; + } + +} diff --git a/core/modules/system/src/Tests/Update/DbDumpTest.php b/core/modules/system/src/Tests/Update/DbDumpTest.php index 9d96fac1ea7..e04bb0502f9 100644 --- a/core/modules/system/src/Tests/Update/DbDumpTest.php +++ b/core/modules/system/src/Tests/Update/DbDumpTest.php @@ -151,21 +151,11 @@ class DbDumpTest extends KernelTestBase { return; } - $application = new DbDumpApplication(Database::getConnection(), $this->container->get('module_handler')); + $application = new DbDumpApplication(); $command = $application->find('dump-database-d8-mysql'); $command_tester = new CommandTester($command); $command_tester->execute([]); - // The enabled modules should be present in the docblock. - $modules = static::$modules; - asort($modules); - $pattern = preg_quote(implode("\n * - ", $modules)); - $this->assertTrue(preg_match('/' . $pattern . '/', $command_tester->getDisplay()), 'Module list is contained in the docblock of the script.'); - - // A module that is not enabled should not be listed. - $pattern = preg_quote(" * - telephone"); - $this->assertFalse(preg_match('/' . $pattern . '/', $command_tester->getDisplay()), 'Disabled modules do not appear in the docblock of the script.'); - // Tables that are schema-only should not have data exported. $pattern = preg_quote("\$connection->insert('sessions')"); $this->assertFalse(preg_match('/' . $pattern . '/', $command_tester->getDisplay()), 'Tables defined as schema-only do not have data exported to the script.'); @@ -195,7 +185,7 @@ class DbDumpTest extends KernelTestBase { } // Generate the script. - $application = new DbDumpApplication(Database::getConnection(), $this->container->get('module_handler')); + $application = new DbDumpApplication(); $command = $application->find('dump-database-d8-mysql'); $command_tester = new CommandTester($command); $command_tester->execute([]); diff --git a/core/modules/system/tests/src/Kernel/Scripts/DbCommandBaseTest.php b/core/modules/system/tests/src/Kernel/Scripts/DbCommandBaseTest.php new file mode 100644 index 00000000000..c11770d4903 --- /dev/null +++ b/core/modules/system/tests/src/Kernel/Scripts/DbCommandBaseTest.php @@ -0,0 +1,137 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\system\Kernel\Scripts\DbCommandBaseTest. + */ + +namespace Drupal\Tests\system\Kernel\Scripts; + +use Drupal\Core\Command\DbCommandBase; +use Drupal\Core\Database\Database; +use Drupal\KernelTests\KernelTestBase; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Tester\CommandTester; + +/** + * Test that the DbToolsApplication works correctly. + * + * The way console application's run it is impossible to test. For now we only + * test that we are registering the correct commands. + * + * @group console + */ +class DbCommandBaseTest extends KernelTestBase { + + /** + * Test specifying a database key. + */ + public function testSpecifyDatabaseKey() { + $command = new DbCommandBaseTester(); + $command_tester = new CommandTester($command); + + Database::addConnectionInfo('magic_db', 'default', Database::getConnectionInfo('default')['default']); + + $command_tester->execute([ + '--database' => 'magic_db' + ]); + $this->assertEquals('magic_db', $command->getDatabaseConnection($command_tester->getInput())->getKey(), + 'Special db key is returned'); + } + + /** + * Invalid database names will throw a useful exception. + * + * @expectedException \Drupal\Core\Database\ConnectionNotDefinedException + */ + public function testSpecifyDatabaseDoesNotExist() { + $command = new DbCommandBaseTester(); + $command_tester = new CommandTester($command); + $command_tester->execute([ + '--database' => 'dne' + ]); + $command->getDatabaseConnection($command_tester->getInput()); + } + + /** + * Test supplying database connection as a url. + */ + public function testSpecifyDbUrl() { + $connection_info = Database::getConnectionInfo('default')['default']; + + $command = new DbCommandBaseTester(); + $command_tester = new CommandTester($command); + $command_tester->execute([ + '-db-url' => $connection_info['driver'] . '://' . $connection_info['username'] . ':' . $connection_info['password'] . '@' . $connection_info['host'] . '/' . $connection_info['database'] + ]); + $this->assertEquals('db-tools', $command->getDatabaseConnection($command_tester->getInput())->getKey()); + + Database::removeConnection('db-tools'); + $command_tester->execute([ + '--database-url' => $connection_info['driver'] . '://' . $connection_info['username'] . ':' . $connection_info['password'] . '@' . $connection_info['host'] . '/' . $connection_info['database'] + ]); + $this->assertEquals('db-tools', $command->getDatabaseConnection($command_tester->getInput())->getKey()); + } + + /** + * Test specifying a prefix for different connections. + */ + public function testPrefix() { + if (Database::getConnection()->driver() == 'sqlite') { + $this->markTestSkipped('SQLITE modifies the prefixes so we cannot effectively test it'); + } + + Database::addConnectionInfo('magic_db', 'default', Database::getConnectionInfo('default')['default']); + $command = new DbCommandBaseTester(); + $command_tester = new CommandTester($command); + $command_tester->execute([ + '--database' => 'magic_db', + '--prefix' => 'extra', + ]); + $this->assertEquals('extra', $command->getDatabaseConnection($command_tester->getInput())->tablePrefix()); + + $connection_info = Database::getConnectionInfo('default')['default']; + $command_tester->execute([ + '-db-url' => $connection_info['driver'] . '://' . $connection_info['username'] . ':' . $connection_info['password'] . '@' . $connection_info['host'] . '/' . $connection_info['database'], + '--prefix' => 'extra2', + ]); + $this->assertEquals('extra2', $command->getDatabaseConnection($command_tester->getInput())->tablePrefix()); + + // This breaks simpletest cleanup. +// $command_tester->execute([ +// '--prefix' => 'notsimpletest', +// ]); +// $this->assertEquals('notsimpletest', $command->getDatabaseConnection($command_tester->getInput())->tablePrefix()); + } + +} + +/** + * Concrete command implementation for testing base features. + */ +class DbCommandBaseTester extends DbCommandBase { + + /** + * {@inheritdoc} + */ + public function configure() { + parent::configure(); + $this->setName('test'); + } + + /** + * {@inheritdoc} + */ + public function getDatabaseConnection(InputInterface $input) { + return parent::getDatabaseConnection($input); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) { + // Empty implementation for testing. + } + +} diff --git a/core/modules/system/tests/src/Kernel/Scripts/DbDumpCommandTest.php b/core/modules/system/tests/src/Kernel/Scripts/DbDumpCommandTest.php new file mode 100644 index 00000000000..3f6c768440b --- /dev/null +++ b/core/modules/system/tests/src/Kernel/Scripts/DbDumpCommandTest.php @@ -0,0 +1,88 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\system\Kernel\Scripts\DbDumpCommandTest. + */ + +namespace Drupal\Tests\system\Kernel\Scripts; + +use Drupal\Core\Command\DbDumpCommand; +use Drupal\Core\Database\Database; +use Drupal\KernelTests\KernelTestBase; +use Symfony\Component\Console\Tester\CommandTester; + +/** + * Test that the DbDumpCommand works correctly. + * + * @group console + */ +class DbDumpCommandTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + public static $modules = ['system']; + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + + // Determine what database backend is running, and set the skip flag. + if (Database::getConnection()->databaseType() !== 'mysql') { + $this->markTestSkipped("Skipping test since the DbDumpCommand is currently only compatible with MySQL"); + } + + $this->installSchema('system', 'router'); + + /** @var \Drupal\Core\Database\Connection $connection */ + $connection = $this->container->get('database'); + $connection->insert('router')->fields(['name', 'path', 'pattern_outline'])->values(['test', 'test', 'test'])->execute(); + } + + /** + * Test the command directly. + */ + public function testDbDumpCommand() { + $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->assertContains("createTable('router", $output, 'Table router found'); + $this->assertContains("insert('router", $output, 'Insert found'); + $this->assertContains("'name' => 'test", $output, 'Insert name field found'); + $this->assertContains("'path' => 'test", $output, 'Insert path field found'); + $this->assertContains("'pattern_outline' => 'test", $output, 'Insert pattern_outline field found'); + } + + /** + * Test schema only option. + */ + public function testSchemaOnly() { + $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->assertContains("createTable('router", $output, 'Table router found'); + $this->assertNotContains("insert('router", $output, 'Insert not found'); + $this->assertNotContains("'name' => 'test", $output, 'Insert name field not found'); + $this->assertNotContains("'path' => 'test", $output, 'Insert path field not found'); + $this->assertNotContains("'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->assertContains("createTable('router", $output, 'Table router found'); + $this->assertNotContains("insert('router", $output, 'Insert not found'); + $this->assertNotContains("'name' => 'test", $output, 'Insert name field not found'); + $this->assertNotContains("'path' => 'test", $output, 'Insert path field not found'); + $this->assertNotContains("'pattern_outline' => 'test", $output, 'Insert pattern_outline field not found'); + } + +} diff --git a/core/modules/system/tests/src/Kernel/Scripts/DbImportCommandTest.php b/core/modules/system/tests/src/Kernel/Scripts/DbImportCommandTest.php new file mode 100644 index 00000000000..9dc3407eb2d --- /dev/null +++ b/core/modules/system/tests/src/Kernel/Scripts/DbImportCommandTest.php @@ -0,0 +1,78 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\system\Kernel\Scripts\DbImportCommandTest. + */ + +namespace Drupal\Tests\system\Kernel\Scripts; + +use Drupal\Core\Command\DbImportCommand; +use Drupal\Core\Config\DatabaseStorage; +use Drupal\Core\Database\Database; +use Drupal\KernelTests\KernelTestBase; +use Symfony\Component\Console\Tester\CommandTester; + +/** + * Test that the DbImportCommand works correctly. + * + * @group console + */ +class DbImportCommandTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + public static $modules = ['system', 'config', 'dblog', 'menu_link_content', 'link', 'block_content', 'file', 'user']; + + /** + * Tables that should be part of the exported script. + * + * @var array + */ + protected $tables = [ + 'block_content', + 'block_content_field_data', + 'block_content_field_revision', + 'block_content_revision', + 'cachetags', + 'config', + 'cache_discovery', + 'cache_bootstrap', + 'file_managed', + 'key_value_expire', + 'menu_link_content', + 'menu_link_content_data', + 'semaphore', + 'sessions', + 'url_alias', + 'user__roles', + 'users', + 'users_field_data', + 'watchdog', + ]; + + /** + * Test the command directly. + */ + public function testDbImportCommand() { + /** @var \Drupal\Core\Database\Connection $connection */ + $connection = $this->container->get('database'); + // Drop tables to avoid conflicts. + foreach ($this->tables as $table) { + $connection->schema()->dropTable($table); + } + + $command = new DbImportCommand(); + $command_tester = new CommandTester($command); + $command_tester->execute(['script' => __DIR__ . '/../../../fixtures/update/drupal-8.bare.standard.php.gz']); + + // The tables should now exist. + foreach ($this->tables as $table) { + $this->assertTrue($connection + ->schema() + ->tableExists($table), strtr('Table @table created by the database script.', ['@table' => $table])); + } + } + +} diff --git a/core/modules/system/tests/src/Kernel/Scripts/DbToolsApplicationTest.php b/core/modules/system/tests/src/Kernel/Scripts/DbToolsApplicationTest.php new file mode 100644 index 00000000000..445b30bdca4 --- /dev/null +++ b/core/modules/system/tests/src/Kernel/Scripts/DbToolsApplicationTest.php @@ -0,0 +1,41 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\system\Kernel\Scripts\DbToolsApplicationTest. + */ + +namespace Drupal\Tests\system\Kernel\Scripts; + +use Drupal\Core\Command\DbToolsApplication; +use Drupal\KernelTests\KernelTestBase; + +/** + * Test that the DbToolsApplication works correctly. + * + * The way console application's run it is impossible to test. For now we only + * test that we are registering the correct commands. + * + * @group console + */ +class DbToolsApplicationTest extends KernelTestBase { + + /** + * Test that the dump command is correctly registered. + */ + public function testDumpCommandRegistration() { + $application = new DbToolsApplication(); + $command = $application->find('dump'); + $this->assertInstanceOf('\Drupal\Core\Command\DbDumpCommand', $command); + } + + /** + * Test that the dump command is correctly registered. + */ + public function testImportCommandRegistration() { + $application = new DbToolsApplication(); + $command = $application->find('import'); + $this->assertInstanceOf('\Drupal\Core\Command\DbImportCommand', $command); + } + +} diff --git a/core/scripts/db-tools.php b/core/scripts/db-tools.php new file mode 100644 index 00000000000..316c62fce64 --- /dev/null +++ b/core/scripts/db-tools.php @@ -0,0 +1,22 @@ +#!/usr/bin/env php +<?php + +use Drupal\Core\Command\DbToolsApplication; +use Drupal\Core\DrupalKernel; +use Drupal\Core\Site\Settings; +use Symfony\Component\HttpFoundation\Request; + +if (PHP_SAPI !== 'cli') { + return; +} + +// Bootstrap. +$autoloader = require __DIR__ . '/../../autoload.php'; +require_once __DIR__ . '/../includes/bootstrap.inc'; +$request = Request::createFromGlobals(); +Settings::initialize(dirname(dirname(__DIR__)), DrupalKernel::findSitePath($request), $autoloader); +$kernel = DrupalKernel::createFromRequest($request, $autoloader, 'prod')->boot(); + +// Run the database dump command. +$application = new DbToolsApplication(); +$application->run(); diff --git a/core/scripts/dump-database-d8-mysql.php b/core/scripts/dump-database-d8-mysql.php index b50f4c01f83..c4f75074cea 100755 --- a/core/scripts/dump-database-d8-mysql.php +++ b/core/scripts/dump-database-d8-mysql.php @@ -2,7 +2,6 @@ <?php use Drupal\Core\Command\DbDumpApplication; -use Drupal\Core\Database\Database; use Drupal\Core\DrupalKernel; use Drupal\Core\Site\Settings; use Symfony\Component\HttpFoundation\Request; @@ -19,5 +18,5 @@ Settings::initialize(dirname(dirname(__DIR__)), DrupalKernel::findSitePath($requ $kernel = DrupalKernel::createFromRequest($request, $autoloader, 'prod')->boot(); // Run the database dump command. -$application = new DbDumpApplication(Database::getConnection(), \Drupal::moduleHandler()); +$application = new DbDumpApplication(); $application->run(); |