Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
0.00% |
0 / 13 |
CRAP | |
0.00% |
0 / 178 |
DbDumpCommand | |
0.00% |
0 / 1 |
|
0.00% |
0 / 13 |
2550 | |
0.00% |
0 / 178 |
configure | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 5 |
|||
execute | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 8 |
|||
generateScript | |
0.00% |
0 / 1 |
30 | |
0.00% |
0 / 19 |
|||
getTables | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 10 |
|||
getTableSchema | |
0.00% |
0 / 1 |
380 | |
0.00% |
0 / 51 |
|||
getTableIndexes | |
0.00% |
0 / 1 |
30 | |
0.00% |
0 / 19 |
|||
getTableCollation | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 4 |
|||
getTableData | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 8 |
|||
fieldTypeMap | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 4 |
|||
fieldSizeMap | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 7 |
|||
getFieldOrder | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 15 |
|||
getTemplate | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 14 |
|||
getTableScript | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 14 |
<?php | |
/** | |
* @file | |
* Contains \Drupal\Core\Command\DbDumpCommand. | |
*/ | |
namespace Drupal\Core\Command; | |
use Drupal\Component\Utility\Variable; | |
use Drupal\Core\Database\Connection; | |
use Symfony\Component\Console\Input\InputInterface; | |
use Symfony\Component\Console\Input\InputOption; | |
use Symfony\Component\Console\Output\OutputInterface; | |
/** | |
* Provides a command to dump the current database to a script. | |
* | |
* This script exports all tables in the given database, and all data (except | |
* for tables denoted as schema-only). The resulting script creates the tables | |
* and populates them with the exported data. | |
* | |
* @todo This command is currently only compatible with MySQL. Making it | |
* backend-agnostic will require \Drupal\Core\Database\Schema support the | |
* ability to retrieve table schema information. Note that using a raw | |
* SQL dump file here (eg, generated from mysqldump or pg_dump) is not an | |
* option since these tend to still be database-backend specific. | |
* @see https://www.drupal.org/node/301038 | |
* | |
* @see \Drupal\Core\Command\DbDumpApplication | |
*/ | |
class DbDumpCommand extends DbCommandBase { | |
/** | |
* An array of table patterns to exclude completely. | |
* | |
* This excludes any lingering simpletest tables generated during test runs. | |
* | |
* @var array | |
*/ | |
protected $excludeTables = ['simpletest.+']; | |
/** | |
* {@inheritdoc} | |
*/ | |
protected function configure() { | |
$this->setName('dump-database-d8-mysql') | |
->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); | |
} | |
$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. | |
* | |
* @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(Connection $connection, array $schema_only = []) { | |
$tables = ''; | |
$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); | |
return trim($script); | |
} | |
/** | |
* Returns a list of tables, not including those set to be excluded. | |
* | |
* @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(Connection $connection) { | |
$tables = array_values($connection->schema()->findTables('%')); | |
foreach ($tables as $key => $table) { | |
// Remove any explicitly excluded tables. | |
foreach ($this->excludeTables as $pattern) { | |
if (preg_match('/^' . $pattern . '$/', $table)) { | |
unset($tables[$key]); | |
} | |
} | |
} | |
return $tables; | |
} | |
/** | |
* 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. | |
* | |
* @return array | |
* A schema array (as defined by hook_schema()). | |
* | |
* @todo This implementation is hard-coded for MySQL. | |
*/ | |
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($connection, $matches[1]); | |
if ($row['Extra'] === 'auto_increment') { | |
// If this is an auto increment, then the type is 'serial'. | |
$type = 'serial'; | |
} | |
$definition['fields'][$name] = [ | |
'type' => $type, | |
'not null' => $row['Null'] === 'NO', | |
]; | |
if ($size = $this->fieldSizeMap($connection, $matches[1])) { | |
$definition['fields'][$name]['size'] = $size; | |
} | |
if (isset($matches[2]) && $type === 'numeric') { | |
// Add precision and scale. | |
$definition['fields'][$name]['precision'] = $matches[2]; | |
$definition['fields'][$name]['scale'] = $matches[3]; | |
} | |
elseif ($type === 'time' || $type === 'datetime') { | |
// @todo Core doesn't support these, but copied from `migrate-db.sh` for now. | |
// Convert to varchar. | |
$definition['fields'][$name]['type'] = 'varchar'; | |
$definition['fields'][$name]['length'] = '100'; | |
} | |
elseif (!isset($definition['fields'][$name]['size'])) { | |
// Try use the provided length, if it doesn't exist default to 100. It's | |
// not great but good enough for our dumps at this point. | |
$definition['fields'][$name]['length'] = isset($matches[2]) ? $matches[2] : 100; | |
} | |
if (isset($row['Default'])) { | |
$definition['fields'][$name]['default'] = $row['Default']; | |
} | |
if (isset($matches[4])) { | |
$definition['fields'][$name]['unsigned'] = TRUE; | |
} | |
// Check for the 'varchar_ascii' type that should be 'binary'. | |
if (isset($row['Collation']) && $row['Collation'] == 'ascii_bin') { | |
$definition['fields'][$name]['type'] = 'varchar_ascii'; | |
$definition['fields'][$name]['binary'] = TRUE; | |
} | |
// Check for the non-binary 'varchar_ascii'. | |
if (isset($row['Collation']) && $row['Collation'] == 'ascii_general_ci') { | |
$definition['fields'][$name]['type'] = 'varchar_ascii'; | |
} | |
// Check for the 'utf8_bin' collation. | |
if (isset($row['Collation']) && $row['Collation'] == 'utf8_bin') { | |
$definition['fields'][$name]['binary'] = TRUE; | |
} | |
} | |
// Set primary key, unique keys, and indexes. | |
$this->getTableIndexes($connection, $table, $definition); | |
// Set table collation. | |
$this->getTableCollation($connection, $table, $definition); | |
return $definition; | |
} | |
/** | |
* 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(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 = $connection->query("SHOW INDEX FROM {" . $table . "}"); | |
while (($row = $query->fetchAssoc()) !== FALSE) { | |
$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; | |
} | |
} | |
} | |
/** | |
* 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(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. | |
$definition['mysql_character_set'] = str_replace('_general_ci', '', $data['Collation']); | |
} | |
/** | |
* Gets all data from a given table. | |
* | |
* 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(Connection $connection, $table) { | |
$order = $this->getFieldOrder($connection, $table); | |
$query = $connection->query("SELECT * FROM {" . $table . "} " . $order); | |
$results = []; | |
while (($row = $query->fetchAssoc()) !== FALSE) { | |
$results[] = $row; | |
} | |
return $results; | |
} | |
/** | |
* 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. | |
* | |
* @return string | |
* The Drupal schema field type. If there is no mapping, the original field | |
* type is returned. | |
*/ | |
protected function fieldTypeMap(Connection $connection, $type) { | |
// Convert everything to lowercase. | |
$map = array_map('strtolower', $connection->schema()->getFieldTypeMap()); | |
$map = array_flip($map); | |
// The MySql map contains type:size. Remove the size part. | |
return isset($map[$type]) ? explode(':', $map[$type])[0] : $type; | |
} | |
/** | |
* 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(Connection $connection, $type) { | |
// Convert everything to lowercase. | |
$map = array_map('strtolower', $connection->schema()->getFieldTypeMap()); | |
$map = array_flip($map); | |
$schema_type = explode(':', $map[$type])[0]; | |
// Only specify size on these types. | |
if (in_array($schema_type, ['blob', 'float', 'int', 'text'])) { | |
// The MySql map contains type:size. Remove the type part. | |
return explode(':', $map[$type])[1]; | |
} | |
} | |
/** | |
* 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(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 = $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 = $connection->query($query); | |
while (($row = $results->fetchAssoc()) !== FALSE) { | |
$order .= $row['COLUMN_NAME'] . ', '; | |
} | |
if (!empty($order)) { | |
$order = ' ORDER BY ' . rtrim($order, ', '); | |
} | |
return $order; | |
} | |
/** | |
* The script template. | |
* | |
* @return string | |
* The template for the generated PHP script. | |
*/ | |
protected function getTemplate() { | |
$script = <<<'ENDOFSCRIPT' | |
<?php | |
/** | |
* @file | |
* A database agnostic dump for testing purposes. | |
* | |
* This file was generated by the Drupal 8.0 db-tools.php script. | |
*/ | |
use Drupal\Core\Database\Database; | |
$connection = Database::getConnection(); | |
{{TABLES}} | |
ENDOFSCRIPT; | |
return $script; | |
} | |
/** | |
* The part of the script for each table. | |
* | |
* @param string $table | |
* Table name. | |
* @param array $schema | |
* Drupal schema definition. | |
* @param array $data | |
* Data for the table. | |
* | |
* @return string | |
* The table create statement, and if there is data, the insert command. | |
*/ | |
protected function getTableScript($table, array $schema, array $data) { | |
$output = ''; | |
$output .= "\$connection->schema()->createTable('" . $table . "', " . Variable::export($schema) . ");\n\n"; | |
if (!empty($data)) { | |
$insert = ''; | |
foreach ($data as $record) { | |
$insert .= "->values(" . Variable::export($record) . ")\n"; | |
} | |
$output .= "\$connection->insert('" . $table . "')\n" | |
. "->fields(" . Variable::export(array_keys($schema['fields'])) . ")\n" | |
. $insert | |
. "->execute();\n\n"; | |
} | |
return $output; | |
} | |
} |