This repository has been archived on 2025-01-19. You can view files and clone it, but cannot push or open issues or pull requests.
drupalcampbristol/core/scripts/migrate-db.sh

402 lines
13 KiB
Bash
Raw Normal View History

#!/usr/bin/env php
<?php
/**
* This script is designed for assisting core developers working on Migrate to
* regenerate the Migrate dump files. Technically this script can be used to
* dump any database into a PHP representation, but that's not its primary
* use case.
*
* Dump files only need to be updated when you're adding or updating tests which
* need new Drupal source data. Drupal 6 and 7 are supported by this script. The
* version of Drupal will be auto-detected during dumping by scanning the system
* table's schema_version column.
*
* To dump a database, you must have a connection to it defined in settings.php.
* Then you can run this script like so:
* migrate-db.sh --dump --database=CONNECTION_KEY
*
* To restore a Drupal 6 database from dump files:
* migrate-db.sh --restore --core=6 --database=CONNECTION_KEY
*
* And to restore a Drupal 7 DB:
* migrate-db.sh --restore --core=7 --database=CONNECTION_KEY
*
* You can also validate a set of dumps to ensure that they haven't been altered.
* For Drupal 6 and 7, respectively:
* migrate-db.sh --validate --core=6
* migrate-db.sh --validate --core=7
*
* --dump and --restore always require the --database option. --validate and --restore
* always require the --core option, which can accept values of 6, 6.x, 7, or 7.x.
*/
use Doctrine\Common\Inflector\Inflector;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\Connection;
use Drupal\Component\Utility\Variable;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Site\Settings;
use Symfony\Component\HttpFoundation\Request;
if (PHP_SAPI !== 'cli') {
return;
}
$autoloader = require __DIR__ . '/../../autoload.php';
require_once __DIR__ . '/../includes/bootstrap.inc';
$request = Request::createFromGlobals();
Settings::initialize(dirname(dirname(__DIR__)), DrupalKernel::findSitePath($request), $autoloader);
// Fully bootstrap Drupal so that things like file_scan_directory() can be used
// (for validating and restoring, and possibly other things).
$kernel = DrupalKernel::createFromRequest($request, $autoloader, 'prod');
$kernel->boot();
$kernel->loadLegacyIncludes();
$options = getopt('', array('database:', 'dump', 'restore', 'validate', 'core:'));
if (isset($options['dump'])) {
if (empty($options['database'])) {
echo "Missing required --database option.\n";
return;
}
$connection = Database::getConnection('default', $options['database']);
$connection_info = $connection->getConnectionOptions();
$version = _get_core_version_from_database($connection);
$output_folder = DRUPAL_ROOT . '/core/modules/migrate_drupal/src/Tests/Table/' . $version;
$class_template = '<?php
/**
* @file
* Contains \Drupal\migrate_drupal\Tests\Table\{{DRUPAL_VERSION}}\{{CLASS_NAME}}.
*
* THIS IS A GENERATED FILE. DO NOT EDIT.
*
* @see core/scripts/migrate-db.sh
* @see https://www.drupal.org/sandbox/benjy/2405029
*/
namespace Drupal\migrate_drupal\Tests\Table\{{DRUPAL_VERSION}};
use Drupal\migrate_drupal\Tests\Dump\DrupalDumpBase;
/**
* Generated file to represent the {{TABLE}} table.
*/
class {{CLASS_NAME}} extends DrupalDumpBase {
public function load() {
$this->createTable("{{TABLE}}", {{TABLE_DEFINITION}});
$this->database->insert("{{TABLE}}")->fields({{PHP_FIELDS}})
{{PHP_VALUES}}->execute();
}
}
';
// Generate a list of tables.
$tables = $connection->query('SHOW TABLES')->fetchCol();
// Get all character sets, keyed by table name.
$character_sets = $connection->query('SELECT T.TABLE_NAME, CCSA.CHARACTER_SET_NAME FROM information_schema.TABLES T INNER JOIN information_schema.COLLATION_CHARACTER_SET_APPLICABILITY CCSA ON CCSA.COLLATION_NAME = T.TABLE_COLLATION WHERE T.TABLE_SCHEMA = \'' . $connection_info['database'] . '\'')
->fetchAllKeyed();
foreach ($tables as $table) {
// Generate the class name.
$class = Inflector::classify($table);
// 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 = rtrim ($order, ", ");
$order = ' ORDER BY ' . $order;
}
// Generate the field values.
$query = $connection->query(_db_get_query($table) . $order);
$values = '';
// Only dump the actual table values if we're NOT looking at a cache table,
// watchdog or sessions tables.
if (substr($table, 0, 5) !== 'cache' && !in_array($table, array('watchdog', 'sessions'))) {
while(($row = $query->fetchAssoc()) !== FALSE) {
$values .= '->values(' . Variable::export($row, ' ') . ')';
}
}
// Generate the field names.
$query = $connection->query("SHOW COLUMNS FROM {$table}");
$definition = [];
while(($row = $query->fetchAssoc()) !== FALSE) {
$field_name = $row['Field'];
// Parse out the field type and meta information.
preg_match('@([a-z]+)(?:\((\d+)(?:,(\d+))?\))?\s*(unsigned)?@', $row['Type'], $matches);
$field_type = _db_field_type_map($matches[1]);
// If it's auto-increment then make it a serial instead.
if ($row['Extra'] === 'auto_increment') {
$field_type = 'serial';
}
// Add primary key entries as needed.
if ($row['Key'] === 'PRI') {
$definition['primary key'][] = $field_name;
}
// All fields have a type and not null.
$definition['fields'][$field_name] = [
'type' => $field_type,
'not null' => $row['Null'] === 'NO',
];
// If this is a numeric field, the meta will be precision and scale.
if (isset($matches[2]) && $field_type === 'numeric') {
$definition['fields'][$field_name]['precision'] = $matches[2];
$definition['fields'][$field_name]['scale'] = $matches[3];
}
elseif ($field_type === 'time' || $field_type === 'datetime') {
// We use varchar to replace the D6 datetime and time fields.
$definition['fields'][$field_name]['type'] = 'varchar';
$definition['fields'][$field_name]['length'] = '100';
}
else {
// 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'][$field_name]['length'] = isset($matches[2]) ? $matches[2] : 100;
}
if (isset($row['Default'])) {
$definition['fields'][$field_name]['default'] = $row['Default'];
}
if (isset($matches[4])) {
$definition['fields'][$field_name]['unsigned'] = TRUE;
}
}
$fields = Variable::export(array_keys($definition['fields']), ' ');
if ($connection->driver() == 'mysql') {
$definition['mysql_character_set'] = $character_sets[$table];
}
$definition = Variable::export($definition, ' ');
// Do our substitutions.
$php = str_replace('{{TABLE}}', $table, $class_template);
$php = str_replace('{{DRUPAL_VERSION}}', $version, $php);
$php = str_replace('{{CLASS_NAME}}', $class, $php);
$php = str_replace('{{PHP_VALUES}}', $values, $php);
$php = str_replace('{{PHP_FIELDS}}', $fields, $php);
$php = str_replace('{{TABLE_DEFINITION}}', $definition, $php);
// Save the file.
$php = implode("\n", array_map('rtrim', explode("\n", $php)));
// Hash the dump code so that the restore script can easily determine if it
// has been mucked with manually.
$php .= '#' . md5($php) . "\n";
file_put_contents("$output_folder/$class.php", $php);
}
}
elseif (isset($options['restore'])) {
if (!\Drupal::moduleHandler()->moduleExists('migrate_drupal')) {
echo "The migrate_drupal module must be enabled to restore a database.\n";
return;
}
elseif (empty($options['database'])) {
echo "Missing required --database option.\n";
return;
}
$connection = Database::getConnection('default', $options['database']);
$version = _get_core_version_from_options();
if ($version) {
$tables_dir = DRUPAL_ROOT . '/core/modules/migrate_drupal/src/Tests/Table/' . $version;
$tables = file_scan_directory($tables_dir, '/.php$/', array('recurse' => FALSE));
foreach ($tables as $table) {
if (table_is_valid($table->uri)) {
restore_table($table->uri, $connection);
}
else {
echo "Skipping invalid table {$table->uri}\n";
}
}
}
else {
echo "Missing --core option.\n";
return;
}
}
elseif (isset($options['validate'])) {
$version = _get_core_version_from_options();
if ($version) {
$tables_dir = DRUPAL_ROOT . '/core/modules/migrate_drupal/src/Tests/Table/' . $version;
$tables = file_scan_directory($tables_dir, '/.php$/', array('recurse' => FALSE));
foreach ($tables as $table) {
echo (table_is_valid($table->uri) ? 'OK' : 'INVALID') . ": {$table->uri}\n";
}
}
else {
echo "Missing --core option.\n";
return;
}
}
else {
echo "Invalid options.\n";
return;
}
/**
* Restores a table from a dump file.
*
* @param string $path
* The path to the dump file.
* @param \Drupal\Core\Database\Connection $connection
* The target database connection.
*/
function restore_table($path, Connection $connection) {
require_once $path;
$version = _get_core_version_from_options();
$class = 'Drupal\migrate_drupal\Tests\Table\\' . $version . '\\' . substr(basename($path), 0, -4);
try {
(new $class($connection))->load();
}
catch (\Exception $e) {
echo 'ERROR: ' . $e->getMessage() . ' [' . get_class($e) . "]\n";
}
}
/**
* Validates a dump file by reading in the MD5 of the file contents (last 32 bytes)
* and comparing them with the MD5 of everything except those last 32 bytes.
*
* @param string $path
* The path to the dump file.
*
* @return bool
*/
function table_is_valid($path) {
// The call to rtrim() is important, since we need to extract a specific
// number of bytes from the end of the file.
$contents = rtrim(file_get_contents($path));
$dump = substr($contents, 0, -33);
$hash = substr($contents, -32);
return (md5($dump) === $hash);
}
/**
* Statically maps the --core option to either 'd6' or 'd7', or NULL if the --core
* option's value is unrecognized.
*
* @return string|null
*/
function _get_core_version_from_options() {
global $options;
if (isset($options['core'])) {
switch ($options['core']) {
case '6':
case '6.x':
return 'd6';
case '7':
case '7.x':
return 'd7';
default:
break;
}
}
}
/**
* Reads a Drupal 6 or 7 database to determine its major core version.
*
* @param \Drupal\Core\Database\Connection $connection
* The database connection.
*
* @return string
*
* @throws \UnexpectedValueException if the discovered core version is unrecognized
* or unsupported.
*/
function _get_core_version_from_database(Connection $connection) {
$version = $connection
->select('system')
->fields('system', array('schema_version'))
->condition('name', 'system')
->execute()
->fetchField();
if ($version >= 7000) {
return 'd7';
}
elseif ($version >= 6000) {
return 'd6';
}
else {
throw new \UnexpectedValueException("Unknown Drupal core version.");
}
}
/**
* Statically maps a SQL field type to a Schema API type. If there is no mapping, the
* original field type is returned.
*
* @param string $sql_type
* The field type as known to the database.
*
* @return string
*/
function _db_field_type_map($sql_type) {
$map = array(
'longtext' => 'text',
'tinytext' => 'text',
'mediumtext' => 'text',
'tinyint' => 'int',
'smallint' => 'int',
'mediumint' => 'int',
'bigint' => 'int',
'int' => 'int',
'double' => 'numeric',
'float' => 'numeric',
'decimal' => 'numeric',
'longblob' => 'blob',
);
return isset($map[$sql_type]) ? $map[$sql_type] : $sql_type;
}
/**
* Returns the appropriate SQL query string to fetch all values from a table.
*
* @param string $table
* The table's name.
*
* @return string
*/
function _db_get_query($table) {
$queries = array(
'users' => 'SELECT * FROM {users} WHERE uid NOT IN (0,1)',
// Volatile state variables should always be ignored. We don't want to
// exclude all cache_* variables, since that would exclude cache_lifetime,
// which is configuration, not state.
'variable' => "SELECT * FROM {variable} WHERE name NOT LIKE 'cache_flush_%' AND name NOT IN ('cache', 'drupal_css_cache_files', 'javascript_parsed', 'statistics_day_timestamp', 'update_last_check')",
);
return isset($queries[$table]) ? $queries[$table] : 'SELECT * FROM {' . $table .'}';
}