Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

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