comparison core/lib/Drupal/Core/Command/DbDumpCommand.php @ 0:4c8ae668cc8c

Initial import (non-working)
author Chris Cannam
date Wed, 29 Nov 2017 16:09:58 +0000
parents
children 129ea1e6d783
comparison
equal deleted inserted replaced
-1:000000000000 0:4c8ae668cc8c
1 <?php
2
3 namespace Drupal\Core\Command;
4
5 use Drupal\Component\Utility\Variable;
6 use Drupal\Core\Database\Connection;
7 use Symfony\Component\Console\Input\InputInterface;
8 use Symfony\Component\Console\Input\InputOption;
9 use Symfony\Component\Console\Output\OutputInterface;
10
11 /**
12 * Provides a command to dump the current database to a script.
13 *
14 * This script exports all tables in the given database, and all data (except
15 * for tables denoted as schema-only). The resulting script creates the tables
16 * and populates them with the exported data.
17 *
18 * @todo This command is currently only compatible with MySQL. Making it
19 * backend-agnostic will require \Drupal\Core\Database\Schema support the
20 * ability to retrieve table schema information. Note that using a raw
21 * SQL dump file here (eg, generated from mysqldump or pg_dump) is not an
22 * option since these tend to still be database-backend specific.
23 * @see https://www.drupal.org/node/301038
24 *
25 * @see \Drupal\Core\Command\DbDumpApplication
26 */
27 class DbDumpCommand extends DbCommandBase {
28
29 /**
30 * An array of table patterns to exclude completely.
31 *
32 * This excludes any lingering simpletest tables generated during test runs.
33 *
34 * @var array
35 */
36 protected $excludeTables = ['test[0-9]+'];
37
38 /**
39 * {@inheritdoc}
40 */
41 protected function configure() {
42 $this->setName('dump-database-d8-mysql')
43 ->setDescription('Dump the current database to a generation script')
44 ->addOption('schema-only', NULL, InputOption::VALUE_OPTIONAL, 'A comma separated list of tables to only export the schema without data.', 'cache.*,sessions,watchdog');
45 parent::configure();
46 }
47
48 /**
49 * {@inheritdoc}
50 */
51 protected function execute(InputInterface $input, OutputInterface $output) {
52 $connection = $this->getDatabaseConnection($input);
53
54 // If not explicitly set, disable ANSI which will break generated php.
55 if ($input->hasParameterOption(['--ansi']) !== TRUE) {
56 $output->setDecorated(FALSE);
57 }
58
59 $schema_tables = $input->getOption('schema-only');
60 $schema_tables = explode(',', $schema_tables);
61
62 $output->writeln($this->generateScript($connection, $schema_tables), OutputInterface::OUTPUT_RAW);
63 }
64
65 /**
66 * Generates the database script.
67 *
68 * @param \Drupal\Core\Database\Connection $connection
69 * The database connection to use.
70 * @param array $schema_only
71 * Table patterns for which to only dump the schema, no data.
72 * @return string
73 * The PHP script.
74 */
75 protected function generateScript(Connection $connection, array $schema_only = []) {
76 $tables = '';
77
78 $schema_only_patterns = [];
79 foreach ($schema_only as $match) {
80 $schema_only_patterns[] = '/^' . $match . '$/';
81 }
82
83 foreach ($this->getTables($connection) as $table) {
84 $schema = $this->getTableSchema($connection, $table);
85 // Check for schema only.
86 if (empty($schema_only_patterns) || preg_replace($schema_only_patterns, '', $table)) {
87 $data = $this->getTableData($connection, $table);
88 }
89 else {
90 $data = [];
91 }
92 $tables .= $this->getTableScript($table, $schema, $data);
93 }
94 $script = $this->getTemplate();
95 // Substitute in the tables.
96 $script = str_replace('{{TABLES}}', trim($tables), $script);
97 return trim($script);
98 }
99
100 /**
101 * Returns a list of tables, not including those set to be excluded.
102 *
103 * @param \Drupal\Core\Database\Connection $connection
104 * The database connection to use.
105 * @return array
106 * An array of table names.
107 */
108 protected function getTables(Connection $connection) {
109 $tables = array_values($connection->schema()->findTables('%'));
110
111 foreach ($tables as $key => $table) {
112 // Remove any explicitly excluded tables.
113 foreach ($this->excludeTables as $pattern) {
114 if (preg_match('/^' . $pattern . '$/', $table)) {
115 unset($tables[$key]);
116 }
117 }
118 }
119
120 return $tables;
121 }
122
123 /**
124 * Returns a schema array for a given table.
125 *
126 * @param \Drupal\Core\Database\Connection $connection
127 * The database connection to use.
128 * @param string $table
129 * The table name.
130 *
131 * @return array
132 * A schema array (as defined by hook_schema()).
133 *
134 * @todo This implementation is hard-coded for MySQL.
135 */
136 protected function getTableSchema(Connection $connection, $table) {
137 // Check this is MySQL.
138 if ($connection->databaseType() !== 'mysql') {
139 throw new \RuntimeException('This script can only be used with MySQL database backends.');
140 }
141
142 $query = $connection->query("SHOW FULL COLUMNS FROM {" . $table . "}");
143 $definition = [];
144 while (($row = $query->fetchAssoc()) !== FALSE) {
145 $name = $row['Field'];
146 // Parse out the field type and meta information.
147 preg_match('@([a-z]+)(?:\((\d+)(?:,(\d+))?\))?\s*(unsigned)?@', $row['Type'], $matches);
148 $type = $this->fieldTypeMap($connection, $matches[1]);
149 if ($row['Extra'] === 'auto_increment') {
150 // If this is an auto increment, then the type is 'serial'.
151 $type = 'serial';
152 }
153 $definition['fields'][$name] = [
154 'type' => $type,
155 'not null' => $row['Null'] === 'NO',
156 ];
157 if ($size = $this->fieldSizeMap($connection, $matches[1])) {
158 $definition['fields'][$name]['size'] = $size;
159 }
160 if (isset($matches[2]) && $type === 'numeric') {
161 // Add precision and scale.
162 $definition['fields'][$name]['precision'] = $matches[2];
163 $definition['fields'][$name]['scale'] = $matches[3];
164 }
165 elseif ($type === 'time' || $type === 'datetime') {
166 // @todo Core doesn't support these, but copied from `migrate-db.sh` for now.
167 // Convert to varchar.
168 $definition['fields'][$name]['type'] = 'varchar';
169 $definition['fields'][$name]['length'] = '100';
170 }
171 elseif (!isset($definition['fields'][$name]['size'])) {
172 // Try use the provided length, if it doesn't exist default to 100. It's
173 // not great but good enough for our dumps at this point.
174 $definition['fields'][$name]['length'] = isset($matches[2]) ? $matches[2] : 100;
175 }
176
177 if (isset($row['Default'])) {
178 $definition['fields'][$name]['default'] = $row['Default'];
179 }
180
181 if (isset($matches[4])) {
182 $definition['fields'][$name]['unsigned'] = TRUE;
183 }
184
185 // Check for the 'varchar_ascii' type that should be 'binary'.
186 if (isset($row['Collation']) && $row['Collation'] == 'ascii_bin') {
187 $definition['fields'][$name]['type'] = 'varchar_ascii';
188 $definition['fields'][$name]['binary'] = TRUE;
189 }
190
191 // Check for the non-binary 'varchar_ascii'.
192 if (isset($row['Collation']) && $row['Collation'] == 'ascii_general_ci') {
193 $definition['fields'][$name]['type'] = 'varchar_ascii';
194 }
195
196 // Check for the 'utf8_bin' collation.
197 if (isset($row['Collation']) && $row['Collation'] == 'utf8_bin') {
198 $definition['fields'][$name]['binary'] = TRUE;
199 }
200 }
201
202 // Set primary key, unique keys, and indexes.
203 $this->getTableIndexes($connection, $table, $definition);
204
205 // Set table collation.
206 $this->getTableCollation($connection, $table, $definition);
207
208 return $definition;
209 }
210
211 /**
212 * Adds primary key, unique keys, and index information to the schema.
213 *
214 * @param \Drupal\Core\Database\Connection $connection
215 * The database connection to use.
216 * @param string $table
217 * The table to find indexes for.
218 * @param array &$definition
219 * The schema definition to modify.
220 */
221 protected function getTableIndexes(Connection $connection, $table, &$definition) {
222 // Note, this query doesn't support ordering, so that is worked around
223 // below by keying the array on Seq_in_index.
224 $query = $connection->query("SHOW INDEX FROM {" . $table . "}");
225 while (($row = $query->fetchAssoc()) !== FALSE) {
226 $index_name = $row['Key_name'];
227 $column = $row['Column_name'];
228 // Key the arrays by the index sequence for proper ordering (start at 0).
229 $order = $row['Seq_in_index'] - 1;
230
231 // If specified, add length to the index.
232 if ($row['Sub_part']) {
233 $column = [$column, $row['Sub_part']];
234 }
235
236 if ($index_name === 'PRIMARY') {
237 $definition['primary key'][$order] = $column;
238 }
239 elseif ($row['Non_unique'] == 0) {
240 $definition['unique keys'][$index_name][$order] = $column;
241 }
242 else {
243 $definition['indexes'][$index_name][$order] = $column;
244 }
245 }
246 }
247
248 /**
249 * Set the table collation.
250 *
251 * @param \Drupal\Core\Database\Connection $connection
252 * The database connection to use.
253 * @param string $table
254 * The table to find indexes for.
255 * @param array &$definition
256 * The schema definition to modify.
257 */
258 protected function getTableCollation(Connection $connection, $table, &$definition) {
259 $query = $connection->query("SHOW TABLE STATUS LIKE '{" . $table . "}'");
260 $data = $query->fetchAssoc();
261
262 // Set `mysql_character_set`. This will be ignored by other backends.
263 $definition['mysql_character_set'] = str_replace('_general_ci', '', $data['Collation']);
264 }
265
266 /**
267 * Gets all data from a given table.
268 *
269 * If a table is set to be schema only, and empty array is returned.
270 *
271 * @param \Drupal\Core\Database\Connection $connection
272 * The database connection to use.
273 * @param string $table
274 * The table to query.
275 *
276 * @return array
277 * The data from the table as an array.
278 */
279 protected function getTableData(Connection $connection, $table) {
280 $order = $this->getFieldOrder($connection, $table);
281 $query = $connection->query("SELECT * FROM {" . $table . "} " . $order);
282 $results = [];
283 while (($row = $query->fetchAssoc()) !== FALSE) {
284 $results[] = $row;
285 }
286 return $results;
287 }
288
289 /**
290 * Given a database field type, return a Drupal type.
291 *
292 * @param \Drupal\Core\Database\Connection $connection
293 * The database connection to use.
294 * @param string $type
295 * The MySQL field type.
296 *
297 * @return string
298 * The Drupal schema field type. If there is no mapping, the original field
299 * type is returned.
300 */
301 protected function fieldTypeMap(Connection $connection, $type) {
302 // Convert everything to lowercase.
303 $map = array_map('strtolower', $connection->schema()->getFieldTypeMap());
304 $map = array_flip($map);
305
306 // The MySql map contains type:size. Remove the size part.
307 return isset($map[$type]) ? explode(':', $map[$type])[0] : $type;
308 }
309
310 /**
311 * Given a database field type, return a Drupal size.
312 *
313 * @param \Drupal\Core\Database\Connection $connection
314 * The database connection to use.
315 * @param string $type
316 * The MySQL field type.
317 *
318 * @return string
319 * The Drupal schema field size.
320 */
321 protected function fieldSizeMap(Connection $connection, $type) {
322 // Convert everything to lowercase.
323 $map = array_map('strtolower', $connection->schema()->getFieldTypeMap());
324 $map = array_flip($map);
325
326 $schema_type = explode(':', $map[$type])[0];
327 // Only specify size on these types.
328 if (in_array($schema_type, ['blob', 'float', 'int', 'text'])) {
329 // The MySql map contains type:size. Remove the type part.
330 return explode(':', $map[$type])[1];
331 }
332 }
333
334 /**
335 * Gets field ordering for a given table.
336 *
337 * @param \Drupal\Core\Database\Connection $connection
338 * The database connection to use.
339 * @param string $table
340 * The table name.
341 *
342 * @return string
343 * The order string to append to the query.
344 */
345 protected function getFieldOrder(Connection $connection, $table) {
346 // @todo this is MySQL only since there are no Database API functions for
347 // table column data.
348 // @todo this code is duplicated in `core/scripts/migrate-db.sh`.
349 $connection_info = $connection->getConnectionOptions();
350 // Order by primary keys.
351 $order = '';
352 $query = "SELECT `COLUMN_NAME` FROM `information_schema`.`COLUMNS`
353 WHERE (`TABLE_SCHEMA` = '" . $connection_info['database'] . "')
354 AND (`TABLE_NAME` = '{" . $table . "}') AND (`COLUMN_KEY` = 'PRI')
355 ORDER BY COLUMN_NAME";
356 $results = $connection->query($query);
357 while (($row = $results->fetchAssoc()) !== FALSE) {
358 $order .= $row['COLUMN_NAME'] . ', ';
359 }
360 if (!empty($order)) {
361 $order = ' ORDER BY ' . rtrim($order, ', ');
362 }
363 return $order;
364 }
365
366 /**
367 * The script template.
368 *
369 * @return string
370 * The template for the generated PHP script.
371 */
372 protected function getTemplate() {
373 // The template contains an instruction for the file to be ignored by PHPCS.
374 // This is because the files can be huge and coding standards are
375 // irrelevant.
376 $script = <<<'ENDOFSCRIPT'
377 <?php
378 // @codingStandardsIgnoreFile
379 /**
380 * @file
381 * A database agnostic dump for testing purposes.
382 *
383 * This file was generated by the Drupal 8.0 db-tools.php script.
384 */
385
386 use Drupal\Core\Database\Database;
387
388 $connection = Database::getConnection();
389
390 {{TABLES}}
391
392 ENDOFSCRIPT;
393 return $script;
394 }
395
396 /**
397 * The part of the script for each table.
398 *
399 * @param string $table
400 * Table name.
401 * @param array $schema
402 * Drupal schema definition.
403 * @param array $data
404 * Data for the table.
405 *
406 * @return string
407 * The table create statement, and if there is data, the insert command.
408 */
409 protected function getTableScript($table, array $schema, array $data) {
410 $output = '';
411 $output .= "\$connection->schema()->createTable('" . $table . "', " . Variable::export($schema) . ");\n\n";
412 if (!empty($data)) {
413 $insert = '';
414 foreach ($data as $record) {
415 $insert .= "->values(" . Variable::export($record) . ")\n";
416 }
417 $output .= "\$connection->insert('" . $table . "')\n"
418 . "->fields(" . Variable::export(array_keys($schema['fields'])) . ")\n"
419 . $insert
420 . "->execute();\n\n";
421 }
422 return $output;
423 }
424
425 }