Mercurial > hg > isophonics-drupal-site
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 } |