Commit b2915883 authored by webchick's avatar webchick

Issue #2550291 by neclimdul, phenaproxima: Improve and generalize database dump tools

parent b904c4b3
<?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);
}
}
......@@ -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;
......@@ -17,34 +15,6 @@
*/
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}
*/
......@@ -58,7 +28,7 @@ protected function getCommandName(InputInterface $input) {
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;
}
......
<?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.');
}
}
}
<?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;
}
}
......@@ -151,21 +151,11 @@ public function testDbDumpCommand() {
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 @@ public function testScriptLoad() {
}
// 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([]);
......
<?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.
}
}
<?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');
}
}
<?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]));
}
}
}
<?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);
}
}
#!/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/