summaryrefslogtreecommitdiff
path: root/vendor/doctrine/dbal/src/Schema/MySQLSchemaManager.php
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/doctrine/dbal/src/Schema/MySQLSchemaManager.php')
-rw-r--r--vendor/doctrine/dbal/src/Schema/MySQLSchemaManager.php548
1 files changed, 548 insertions, 0 deletions
diff --git a/vendor/doctrine/dbal/src/Schema/MySQLSchemaManager.php b/vendor/doctrine/dbal/src/Schema/MySQLSchemaManager.php
new file mode 100644
index 0000000..249be13
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/MySQLSchemaManager.php
@@ -0,0 +1,548 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7use Doctrine\DBAL\Exception;
8use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
9use Doctrine\DBAL\Platforms\MariaDBPlatform;
10use Doctrine\DBAL\Platforms\MySQL;
11use Doctrine\DBAL\Platforms\MySQL\CharsetMetadataProvider\CachingCharsetMetadataProvider;
12use Doctrine\DBAL\Platforms\MySQL\CharsetMetadataProvider\ConnectionCharsetMetadataProvider;
13use Doctrine\DBAL\Platforms\MySQL\CollationMetadataProvider\CachingCollationMetadataProvider;
14use Doctrine\DBAL\Platforms\MySQL\CollationMetadataProvider\ConnectionCollationMetadataProvider;
15use Doctrine\DBAL\Platforms\MySQL\DefaultTableOptions;
16use Doctrine\DBAL\Result;
17use Doctrine\DBAL\Types\Type;
18
19use function array_change_key_case;
20use function assert;
21use function explode;
22use function implode;
23use function is_string;
24use function preg_match;
25use function str_contains;
26use function strtok;
27use function strtolower;
28use function strtr;
29
30use const CASE_LOWER;
31
32/**
33 * Schema manager for the MySQL RDBMS.
34 *
35 * @extends AbstractSchemaManager<AbstractMySQLPlatform>
36 */
37class MySQLSchemaManager extends AbstractSchemaManager
38{
39 /** @see https://mariadb.com/kb/en/library/string-literals/#escape-sequences */
40 private const MARIADB_ESCAPE_SEQUENCES = [
41 '\\0' => "\0",
42 "\\'" => "'",
43 '\\"' => '"',
44 '\\b' => "\b",
45 '\\n' => "\n",
46 '\\r' => "\r",
47 '\\t' => "\t",
48 '\\Z' => "\x1a",
49 '\\\\' => '\\',
50 '\\%' => '%',
51 '\\_' => '_',
52
53 // Internally, MariaDB escapes single quotes using the standard syntax
54 "''" => "'",
55 ];
56
57 private ?DefaultTableOptions $defaultTableOptions = null;
58
59 /**
60 * {@inheritDoc}
61 */
62 protected function _getPortableTableDefinition(array $table): string
63 {
64 return $table['TABLE_NAME'];
65 }
66
67 /**
68 * {@inheritDoc}
69 */
70 protected function _getPortableViewDefinition(array $view): View
71 {
72 return new View($view['TABLE_NAME'], $view['VIEW_DEFINITION']);
73 }
74
75 /**
76 * {@inheritDoc}
77 */
78 protected function _getPortableTableIndexesList(array $tableIndexes, string $tableName): array
79 {
80 foreach ($tableIndexes as $k => $v) {
81 $v = array_change_key_case($v, CASE_LOWER);
82 if ($v['key_name'] === 'PRIMARY') {
83 $v['primary'] = true;
84 } else {
85 $v['primary'] = false;
86 }
87
88 if (str_contains($v['index_type'], 'FULLTEXT')) {
89 $v['flags'] = ['FULLTEXT'];
90 } elseif (str_contains($v['index_type'], 'SPATIAL')) {
91 $v['flags'] = ['SPATIAL'];
92 }
93
94 // Ignore prohibited prefix `length` for spatial index
95 if (! str_contains($v['index_type'], 'SPATIAL')) {
96 $v['length'] = isset($v['sub_part']) ? (int) $v['sub_part'] : null;
97 }
98
99 $tableIndexes[$k] = $v;
100 }
101
102 return parent::_getPortableTableIndexesList($tableIndexes, $tableName);
103 }
104
105 /**
106 * {@inheritDoc}
107 */
108 protected function _getPortableDatabaseDefinition(array $database): string
109 {
110 return $database['Database'];
111 }
112
113 /**
114 * {@inheritDoc}
115 */
116 protected function _getPortableTableColumnDefinition(array $tableColumn): Column
117 {
118 $tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
119
120 $dbType = strtolower($tableColumn['type']);
121 $dbType = strtok($dbType, '(), ');
122 assert(is_string($dbType));
123
124 $length = $tableColumn['length'] ?? strtok('(), ');
125
126 $fixed = false;
127
128 if (! isset($tableColumn['name'])) {
129 $tableColumn['name'] = '';
130 }
131
132 $scale = 0;
133 $precision = null;
134
135 $type = $this->platform->getDoctrineTypeMapping($dbType);
136
137 switch ($dbType) {
138 case 'char':
139 case 'binary':
140 $fixed = true;
141 break;
142
143 case 'float':
144 case 'double':
145 case 'real':
146 case 'numeric':
147 case 'decimal':
148 if (
149 preg_match(
150 '([A-Za-z]+\(([0-9]+),([0-9]+)\))',
151 $tableColumn['type'],
152 $match,
153 ) === 1
154 ) {
155 $precision = (int) $match[1];
156 $scale = (int) $match[2];
157 $length = null;
158 }
159
160 break;
161
162 case 'tinytext':
163 $length = AbstractMySQLPlatform::LENGTH_LIMIT_TINYTEXT;
164 break;
165
166 case 'text':
167 $length = AbstractMySQLPlatform::LENGTH_LIMIT_TEXT;
168 break;
169
170 case 'mediumtext':
171 $length = AbstractMySQLPlatform::LENGTH_LIMIT_MEDIUMTEXT;
172 break;
173
174 case 'tinyblob':
175 $length = AbstractMySQLPlatform::LENGTH_LIMIT_TINYBLOB;
176 break;
177
178 case 'blob':
179 $length = AbstractMySQLPlatform::LENGTH_LIMIT_BLOB;
180 break;
181
182 case 'mediumblob':
183 $length = AbstractMySQLPlatform::LENGTH_LIMIT_MEDIUMBLOB;
184 break;
185
186 case 'tinyint':
187 case 'smallint':
188 case 'mediumint':
189 case 'int':
190 case 'integer':
191 case 'bigint':
192 case 'year':
193 $length = null;
194 break;
195 }
196
197 if ($this->platform instanceof MariaDBPlatform) {
198 $columnDefault = $this->getMariaDBColumnDefault($this->platform, $tableColumn['default']);
199 } else {
200 $columnDefault = $tableColumn['default'];
201 }
202
203 $options = [
204 'length' => $length !== null ? (int) $length : null,
205 'unsigned' => str_contains($tableColumn['type'], 'unsigned'),
206 'fixed' => $fixed,
207 'default' => $columnDefault,
208 'notnull' => $tableColumn['null'] !== 'YES',
209 'scale' => $scale,
210 'precision' => $precision,
211 'autoincrement' => str_contains($tableColumn['extra'], 'auto_increment'),
212 ];
213
214 if (isset($tableColumn['comment'])) {
215 $options['comment'] = $tableColumn['comment'];
216 }
217
218 $column = new Column($tableColumn['field'], Type::getType($type), $options);
219
220 if (isset($tableColumn['characterset'])) {
221 $column->setPlatformOption('charset', $tableColumn['characterset']);
222 }
223
224 if (isset($tableColumn['collation'])) {
225 $column->setPlatformOption('collation', $tableColumn['collation']);
226 }
227
228 return $column;
229 }
230
231 /**
232 * Return Doctrine/Mysql-compatible column default values for MariaDB 10.2.7+ servers.
233 *
234 * - Since MariaDb 10.2.7 column defaults stored in information_schema are now quoted
235 * to distinguish them from expressions (see MDEV-10134).
236 * - CURRENT_TIMESTAMP, CURRENT_TIME, CURRENT_DATE are stored in information_schema
237 * as current_timestamp(), currdate(), currtime()
238 * - Quoted 'NULL' is not enforced by Maria, it is technically possible to have
239 * null in some circumstances (see https://jira.mariadb.org/browse/MDEV-14053)
240 * - \' is always stored as '' in information_schema (normalized)
241 *
242 * @link https://mariadb.com/kb/en/library/information-schema-columns-table/
243 * @link https://jira.mariadb.org/browse/MDEV-13132
244 *
245 * @param string|null $columnDefault default value as stored in information_schema for MariaDB >= 10.2.7
246 */
247 private function getMariaDBColumnDefault(MariaDBPlatform $platform, ?string $columnDefault): ?string
248 {
249 if ($columnDefault === 'NULL' || $columnDefault === null) {
250 return null;
251 }
252
253 if (preg_match('/^\'(.*)\'$/', $columnDefault, $matches) === 1) {
254 return strtr($matches[1], self::MARIADB_ESCAPE_SEQUENCES);
255 }
256
257 return match ($columnDefault) {
258 'current_timestamp()' => $platform->getCurrentTimestampSQL(),
259 'curdate()' => $platform->getCurrentDateSQL(),
260 'curtime()' => $platform->getCurrentTimeSQL(),
261 default => $columnDefault,
262 };
263 }
264
265 /**
266 * {@inheritDoc}
267 */
268 protected function _getPortableTableForeignKeysList(array $tableForeignKeys): array
269 {
270 $list = [];
271 foreach ($tableForeignKeys as $value) {
272 $value = array_change_key_case($value, CASE_LOWER);
273 if (! isset($list[$value['constraint_name']])) {
274 if (! isset($value['delete_rule']) || $value['delete_rule'] === 'RESTRICT') {
275 $value['delete_rule'] = null;
276 }
277
278 if (! isset($value['update_rule']) || $value['update_rule'] === 'RESTRICT') {
279 $value['update_rule'] = null;
280 }
281
282 $list[$value['constraint_name']] = [
283 'name' => $value['constraint_name'],
284 'local' => [],
285 'foreign' => [],
286 'foreignTable' => $value['referenced_table_name'],
287 'onDelete' => $value['delete_rule'],
288 'onUpdate' => $value['update_rule'],
289 ];
290 }
291
292 $list[$value['constraint_name']]['local'][] = $value['column_name'];
293 $list[$value['constraint_name']]['foreign'][] = $value['referenced_column_name'];
294 }
295
296 return parent::_getPortableTableForeignKeysList($list);
297 }
298
299 /**
300 * {@inheritDoc}
301 */
302 protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint
303 {
304 return new ForeignKeyConstraint(
305 $tableForeignKey['local'],
306 $tableForeignKey['foreignTable'],
307 $tableForeignKey['foreign'],
308 $tableForeignKey['name'],
309 [
310 'onDelete' => $tableForeignKey['onDelete'],
311 'onUpdate' => $tableForeignKey['onUpdate'],
312 ],
313 );
314 }
315
316 /** @throws Exception */
317 public function createComparator(): Comparator
318 {
319 return new MySQL\Comparator(
320 $this->platform,
321 new CachingCharsetMetadataProvider(
322 new ConnectionCharsetMetadataProvider($this->connection),
323 ),
324 new CachingCollationMetadataProvider(
325 new ConnectionCollationMetadataProvider($this->connection),
326 ),
327 $this->getDefaultTableOptions(),
328 );
329 }
330
331 protected function selectTableNames(string $databaseName): Result
332 {
333 $sql = <<<'SQL'
334SELECT TABLE_NAME
335FROM information_schema.TABLES
336WHERE TABLE_SCHEMA = ?
337 AND TABLE_TYPE = 'BASE TABLE'
338ORDER BY TABLE_NAME
339SQL;
340
341 return $this->connection->executeQuery($sql, [$databaseName]);
342 }
343
344 protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result
345 {
346 $columnTypeSQL = $this->platform->getColumnTypeSQLSnippet('c', $databaseName);
347
348 $sql = 'SELECT';
349
350 if ($tableName === null) {
351 $sql .= ' c.TABLE_NAME,';
352 }
353
354 $sql .= <<<SQL
355 c.COLUMN_NAME AS field,
356 $columnTypeSQL AS type,
357 c.IS_NULLABLE AS `null`,
358 c.COLUMN_KEY AS `key`,
359 c.COLUMN_DEFAULT AS `default`,
360 c.EXTRA,
361 c.COLUMN_COMMENT AS comment,
362 c.CHARACTER_SET_NAME AS characterset,
363 c.COLLATION_NAME AS collation
364FROM information_schema.COLUMNS c
365 INNER JOIN information_schema.TABLES t
366 ON t.TABLE_NAME = c.TABLE_NAME
367SQL;
368
369 // The schema name is passed multiple times as a literal in the WHERE clause instead of using a JOIN condition
370 // in order to avoid performance issues on MySQL older than 8.0 and the corresponding MariaDB versions
371 // caused by https://bugs.mysql.com/bug.php?id=81347
372 $conditions = ['c.TABLE_SCHEMA = ?', 't.TABLE_SCHEMA = ?', "t.TABLE_TYPE = 'BASE TABLE'"];
373 $params = [$databaseName, $databaseName];
374
375 if ($tableName !== null) {
376 $conditions[] = 't.TABLE_NAME = ?';
377 $params[] = $tableName;
378 }
379
380 $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY ORDINAL_POSITION';
381
382 return $this->connection->executeQuery($sql, $params);
383 }
384
385 protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result
386 {
387 $sql = 'SELECT';
388
389 if ($tableName === null) {
390 $sql .= ' TABLE_NAME,';
391 }
392
393 $sql .= <<<'SQL'
394 NON_UNIQUE AS Non_Unique,
395 INDEX_NAME AS Key_name,
396 COLUMN_NAME AS Column_Name,
397 SUB_PART AS Sub_Part,
398 INDEX_TYPE AS Index_Type
399FROM information_schema.STATISTICS
400SQL;
401
402 $conditions = ['TABLE_SCHEMA = ?'];
403 $params = [$databaseName];
404
405 if ($tableName !== null) {
406 $conditions[] = 'TABLE_NAME = ?';
407 $params[] = $tableName;
408 }
409
410 $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY SEQ_IN_INDEX';
411
412 return $this->connection->executeQuery($sql, $params);
413 }
414
415 protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result
416 {
417 $sql = 'SELECT DISTINCT';
418
419 if ($tableName === null) {
420 $sql .= ' k.TABLE_NAME,';
421 }
422
423 $sql .= <<<'SQL'
424 k.CONSTRAINT_NAME,
425 k.COLUMN_NAME,
426 k.REFERENCED_TABLE_NAME,
427 k.REFERENCED_COLUMN_NAME,
428 k.ORDINAL_POSITION,
429 c.UPDATE_RULE,
430 c.DELETE_RULE
431FROM information_schema.key_column_usage k
432INNER JOIN information_schema.referential_constraints c
433ON c.CONSTRAINT_NAME = k.CONSTRAINT_NAME
434AND c.TABLE_NAME = k.TABLE_NAME
435SQL;
436
437 $conditions = ['k.TABLE_SCHEMA = ?'];
438 $params = [$databaseName];
439
440 if ($tableName !== null) {
441 $conditions[] = 'k.TABLE_NAME = ?';
442 $params[] = $tableName;
443 }
444
445 // The schema name is passed multiple times in the WHERE clause instead of using a JOIN condition
446 // in order to avoid performance issues on MySQL older than 8.0 and the corresponding MariaDB versions
447 // caused by https://bugs.mysql.com/bug.php?id=81347
448 $conditions[] = 'c.CONSTRAINT_SCHEMA = ?';
449 $params[] = $databaseName;
450
451 $conditions[] = 'k.REFERENCED_COLUMN_NAME IS NOT NULL';
452
453 $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY k.ORDINAL_POSITION';
454
455 return $this->connection->executeQuery($sql, $params);
456 }
457
458 /**
459 * {@inheritDoc}
460 */
461 protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array
462 {
463 // MariaDB-10.10.1 added FULL_COLLATION_NAME to the information_schema.COLLATION_CHARACTER_SET_APPLICABILITY.
464 // A base collation like uca1400_ai_ci can refer to multiple character sets. The value in the
465 // information_schema.TABLES.TABLE_COLLATION corresponds to the full collation name.
466 // The MariaDB executable comment syntax with version, /*M!101001, is exclusively executed on
467 // MariaDB-10.10.1+ servers for backwards compatibility, and compatiblity to MySQL servers.
468 $sql = <<<'SQL'
469 SELECT t.TABLE_NAME,
470 t.ENGINE,
471 t.AUTO_INCREMENT,
472 t.TABLE_COMMENT,
473 t.CREATE_OPTIONS,
474 t.TABLE_COLLATION,
475 ccsa.CHARACTER_SET_NAME
476 FROM information_schema.TABLES t
477 INNER JOIN information_schema.COLLATION_CHARACTER_SET_APPLICABILITY ccsa
478 ON /*M!101001 ccsa.FULL_COLLATION_NAME = t.TABLE_COLLATION OR */
479 ccsa.COLLATION_NAME = t.TABLE_COLLATION
480SQL;
481
482 $conditions = ['t.TABLE_SCHEMA = ?'];
483 $params = [$databaseName];
484
485 if ($tableName !== null) {
486 $conditions[] = 't.TABLE_NAME = ?';
487 $params[] = $tableName;
488 }
489
490 $conditions[] = "t.TABLE_TYPE = 'BASE TABLE'";
491
492 $sql .= ' WHERE ' . implode(' AND ', $conditions);
493
494 /** @var array<string,array<string,mixed>> $metadata */
495 $metadata = $this->connection->executeQuery($sql, $params)
496 ->fetchAllAssociativeIndexed();
497
498 $tableOptions = [];
499 foreach ($metadata as $table => $data) {
500 $data = array_change_key_case($data, CASE_LOWER);
501
502 $tableOptions[$table] = [
503 'engine' => $data['engine'],
504 'collation' => $data['table_collation'],
505 'charset' => $data['character_set_name'],
506 'autoincrement' => $data['auto_increment'],
507 'comment' => $data['table_comment'],
508 'create_options' => $this->parseCreateOptions($data['create_options']),
509 ];
510 }
511
512 return $tableOptions;
513 }
514
515 /** @return array<string, string>|array<string, true> */
516 private function parseCreateOptions(?string $string): array
517 {
518 $options = [];
519
520 if ($string === null || $string === '') {
521 return $options;
522 }
523
524 foreach (explode(' ', $string) as $pair) {
525 $parts = explode('=', $pair, 2);
526
527 $options[$parts[0]] = $parts[1] ?? true;
528 }
529
530 return $options;
531 }
532
533 /** @throws Exception */
534 private function getDefaultTableOptions(): DefaultTableOptions
535 {
536 if ($this->defaultTableOptions === null) {
537 $row = $this->connection->fetchNumeric(
538 'SELECT @@character_set_database, @@collation_database',
539 );
540
541 assert($row !== false);
542
543 $this->defaultTableOptions = new DefaultTableOptions(...$row);
544 }
545
546 return $this->defaultTableOptions;
547 }
548}