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\Exception;
6
use Doctrine\DBAL\Platforms\SQLServer;
7
use Doctrine\DBAL\Platforms\SQLServerPlatform;
8
use Doctrine\DBAL\Result;
9
use Doctrine\DBAL\Types\Type;
10
use Doctrine\Deprecations\Deprecation;
11
 
12
use function array_change_key_case;
13
use function assert;
14
use function count;
15
use function explode;
16
use function implode;
17
use function is_string;
18
use function preg_match;
19
use function sprintf;
20
use function str_replace;
21
use function strpos;
22
use function strtok;
23
 
24
use const CASE_LOWER;
25
 
26
/**
27
 * SQL Server Schema Manager.
28
 *
29
 * @extends AbstractSchemaManager<SQLServerPlatform>
30
 */
31
class SQLServerSchemaManager extends AbstractSchemaManager
32
{
33
    private ?string $databaseCollation = null;
34
 
35
    /**
36
     * {@inheritDoc}
37
     */
38
    public function listTableNames()
39
    {
40
        return $this->doListTableNames();
41
    }
42
 
43
    /**
44
     * {@inheritDoc}
45
     */
46
    public function listTables()
47
    {
48
        return $this->doListTables();
49
    }
50
 
51
    /**
52
     * {@inheritDoc}
53
     *
54
     * @deprecated Use {@see introspectTable()} instead.
55
     */
56
    public function listTableDetails($name)
57
    {
58
        Deprecation::trigger(
59
            'doctrine/dbal',
60
            'https://github.com/doctrine/dbal/pull/5595',
61
            '%s is deprecated. Use introspectTable() instead.',
62
            __METHOD__,
63
        );
64
 
65
        return $this->doListTableDetails($name);
66
    }
67
 
68
    /**
69
     * {@inheritDoc}
70
     */
71
    public function listTableColumns($table, $database = null)
72
    {
73
        return $this->doListTableColumns($table, $database);
74
    }
75
 
76
    /**
77
     * {@inheritDoc}
78
     */
79
    public function listTableIndexes($table)
80
    {
81
        return $this->doListTableIndexes($table);
82
    }
83
 
84
    /**
85
     * {@inheritDoc}
86
     */
87
    public function listTableForeignKeys($table, $database = null)
88
    {
89
        return $this->doListTableForeignKeys($table, $database);
90
    }
91
 
92
    /**
93
     * {@inheritDoc}
94
     */
95
    public function listSchemaNames(): array
96
    {
97
        return $this->_conn->fetchFirstColumn(
98
            <<<'SQL'
99
SELECT name
100
FROM   sys.schemas
101
WHERE  name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')
102
SQL,
103
        );
104
    }
105
 
106
    /**
107
     * {@inheritdoc}
108
     */
109
    protected function _getPortableSequenceDefinition($sequence)
110
    {
111
        return new Sequence($sequence['name'], (int) $sequence['increment'], (int) $sequence['start_value']);
112
    }
113
 
114
    /**
115
     * {@inheritdoc}
116
     */
117
    protected function _getPortableTableColumnDefinition($tableColumn)
118
    {
119
        $dbType = strtok($tableColumn['type'], '(), ');
120
        assert(is_string($dbType));
121
 
122
        $fixed   = null;
123
        $length  = (int) $tableColumn['length'];
124
        $default = $tableColumn['default'];
125
 
126
        if (! isset($tableColumn['name'])) {
127
            $tableColumn['name'] = '';
128
        }
129
 
130
        if ($default !== null) {
131
            $default = $this->parseDefaultExpression($default);
132
        }
133
 
134
        switch ($dbType) {
135
            case 'nchar':
136
            case 'nvarchar':
137
            case 'ntext':
138
                // Unicode data requires 2 bytes per character
139
                $length /= 2;
140
                break;
141
 
142
            case 'varchar':
143
                // TEXT type is returned as VARCHAR(MAX) with a length of -1
144
                if ($length === -1) {
145
                    $dbType = 'text';
146
                }
147
 
148
                break;
149
 
150
            case 'varbinary':
151
                if ($length === -1) {
152
                    $dbType = 'blob';
153
                }
154
 
155
                break;
156
        }
157
 
158
        if ($dbType === 'char' || $dbType === 'nchar' || $dbType === 'binary') {
159
            $fixed = true;
160
        }
161
 
162
        $type                   = $this->_platform->getDoctrineTypeMapping($dbType);
163
        $type                   = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type);
164
        $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type);
165
 
166
        $options = [
167
            'unsigned'      => false,
168
            'fixed'         => (bool) $fixed,
169
            'default'       => $default,
170
            'notnull'       => (bool) $tableColumn['notnull'],
171
            'scale'         => $tableColumn['scale'],
172
            'precision'     => $tableColumn['precision'],
173
            'autoincrement' => (bool) $tableColumn['autoincrement'],
174
            'comment'       => $tableColumn['comment'] !== '' ? $tableColumn['comment'] : null,
175
        ];
176
 
177
        if ($length !== 0 && ($type === 'text' || $type === 'string' || $type === 'binary')) {
178
            $options['length'] = $length;
179
        }
180
 
181
        $column = new Column($tableColumn['name'], Type::getType($type), $options);
182
 
183
        if (isset($tableColumn['collation']) && $tableColumn['collation'] !== 'NULL') {
184
            $column->setPlatformOption('collation', $tableColumn['collation']);
185
        }
186
 
187
        return $column;
188
    }
189
 
190
    private function parseDefaultExpression(string $value): ?string
191
    {
192
        while (preg_match('/^\((.*)\)$/s', $value, $matches)) {
193
            $value = $matches[1];
194
        }
195
 
196
        if ($value === 'NULL') {
197
            return null;
198
        }
199
 
200
        if (preg_match('/^\'(.*)\'$/s', $value, $matches) === 1) {
201
            $value = str_replace("''", "'", $matches[1]);
202
        }
203
 
204
        if ($value === 'getdate()') {
205
            return $this->_platform->getCurrentTimestampSQL();
206
        }
207
 
208
        return $value;
209
    }
210
 
211
    /**
212
     * {@inheritdoc}
213
     */
214
    protected function _getPortableTableForeignKeysList($tableForeignKeys)
215
    {
216
        $foreignKeys = [];
217
 
218
        foreach ($tableForeignKeys as $tableForeignKey) {
219
            $name = $tableForeignKey['ForeignKey'];
220
 
221
            if (! isset($foreignKeys[$name])) {
222
                $foreignKeys[$name] = [
223
                    'local_columns' => [$tableForeignKey['ColumnName']],
224
                    'foreign_table' => $tableForeignKey['ReferenceTableName'],
225
                    'foreign_columns' => [$tableForeignKey['ReferenceColumnName']],
226
                    'name' => $name,
227
                    'options' => [
228
                        'onUpdate' => str_replace('_', ' ', $tableForeignKey['update_referential_action_desc']),
229
                        'onDelete' => str_replace('_', ' ', $tableForeignKey['delete_referential_action_desc']),
230
                    ],
231
                ];
232
            } else {
233
                $foreignKeys[$name]['local_columns'][]   = $tableForeignKey['ColumnName'];
234
                $foreignKeys[$name]['foreign_columns'][] = $tableForeignKey['ReferenceColumnName'];
235
            }
236
        }
237
 
238
        return parent::_getPortableTableForeignKeysList($foreignKeys);
239
    }
240
 
241
    /**
242
     * {@inheritdoc}
243
     */
244
    protected function _getPortableTableIndexesList($tableIndexes, $tableName = null)
245
    {
246
        foreach ($tableIndexes as &$tableIndex) {
247
            $tableIndex['non_unique'] = (bool) $tableIndex['non_unique'];
248
            $tableIndex['primary']    = (bool) $tableIndex['primary'];
249
            $tableIndex['flags']      = $tableIndex['flags'] ? [$tableIndex['flags']] : null;
250
        }
251
 
252
        return parent::_getPortableTableIndexesList($tableIndexes, $tableName);
253
    }
254
 
255
    /**
256
     * {@inheritdoc}
257
     */
258
    protected function _getPortableTableForeignKeyDefinition($tableForeignKey)
259
    {
260
        return new ForeignKeyConstraint(
261
            $tableForeignKey['local_columns'],
262
            $tableForeignKey['foreign_table'],
263
            $tableForeignKey['foreign_columns'],
264
            $tableForeignKey['name'],
265
            $tableForeignKey['options'],
266
        );
267
    }
268
 
269
    /**
270
     * {@inheritdoc}
271
     */
272
    protected function _getPortableTableDefinition($table)
273
    {
274
        if ($table['schema_name'] !== 'dbo') {
275
            return $table['schema_name'] . '.' . $table['table_name'];
276
        }
277
 
278
        return $table['table_name'];
279
    }
280
 
281
    /**
282
     * {@inheritdoc}
283
     */
284
    protected function _getPortableDatabaseDefinition($database)
285
    {
286
        return $database['name'];
287
    }
288
 
289
    /**
290
     * {@inheritdoc}
291
     *
292
     * @deprecated Use {@see listSchemaNames()} instead.
293
     */
294
    protected function getPortableNamespaceDefinition(array $namespace)
295
    {
296
        Deprecation::triggerIfCalledFromOutside(
297
            'doctrine/dbal',
298
            'https://github.com/doctrine/dbal/issues/4503',
299
            'SQLServerSchemaManager::getPortableNamespaceDefinition() is deprecated,'
300
                . ' use SQLServerSchemaManager::listSchemaNames() instead.',
301
        );
302
 
303
        return $namespace['name'];
304
    }
305
 
306
    /**
307
     * {@inheritdoc}
308
     */
309
    protected function _getPortableViewDefinition($view)
310
    {
311
        // @todo
312
        return new View($view['name'], $view['definition']);
313
    }
314
 
315
    /**
316
     * {@inheritdoc}
317
     */
318
    public function alterTable(TableDiff $tableDiff)
319
    {
320
        $droppedColumns = $tableDiff->getDroppedColumns();
321
 
322
        if (count($droppedColumns) > 0) {
323
            $tableName = ($tableDiff->getOldTable() ?? $tableDiff->getName($this->_platform))->getName();
324
 
325
            foreach ($droppedColumns as $col) {
326
                foreach ($this->getColumnConstraints($tableName, $col->getName()) as $constraint) {
327
                    $this->_conn->executeStatement(
328
                        sprintf(
329
                            'ALTER TABLE %s DROP CONSTRAINT %s',
330
                            $tableName,
331
                            $constraint,
332
                        ),
333
                    );
334
                }
335
            }
336
        }
337
 
338
        parent::alterTable($tableDiff);
339
    }
340
 
341
    /**
342
     * Returns the names of the constraints for a given column.
343
     *
344
     * @return iterable<string>
345
     *
346
     * @throws Exception
347
     */
348
    private function getColumnConstraints(string $table, string $column): iterable
349
    {
350
        return $this->_conn->iterateColumn(
351
            <<<'SQL'
352
SELECT o.name
353
FROM sys.objects o
354
         INNER JOIN sys.objects t
355
                    ON t.object_id = o.parent_object_id
356
                        AND t.type = 'U'
357
         INNER JOIN sys.default_constraints dc
358
                    ON dc.object_id = o.object_id
359
         INNER JOIN sys.columns c
360
                    ON c.column_id = dc.parent_column_id
361
                        AND c.object_id = t.object_id
362
WHERE t.name = ?
363
  AND c.name = ?
364
SQL
365
            ,
366
            [$table, $column],
367
        );
368
    }
369
 
370
    /** @throws Exception */
371
    public function createComparator(): Comparator
372
    {
373
        return new SQLServer\Comparator($this->_platform, $this->getDatabaseCollation());
374
    }
375
 
376
    /** @throws Exception */
377
    private function getDatabaseCollation(): string
378
    {
379
        if ($this->databaseCollation === null) {
380
            $databaseCollation = $this->_conn->fetchOne(
381
                'SELECT collation_name FROM sys.databases WHERE name = '
382
                . $this->_platform->getCurrentDatabaseExpression(),
383
            );
384
 
385
            // a database is always selected, even if omitted in the connection parameters
386
            assert(is_string($databaseCollation));
387
 
388
            $this->databaseCollation = $databaseCollation;
389
        }
390
 
391
        return $this->databaseCollation;
392
    }
393
 
394
    protected function selectTableNames(string $databaseName): Result
395
    {
396
        // The "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams
397
        $sql = <<<'SQL'
398
SELECT name AS table_name,
399
       SCHEMA_NAME(schema_id) AS schema_name
400
FROM sys.objects
401
WHERE type = 'U'
402
  AND name != 'sysdiagrams'
403
ORDER BY name
404
SQL;
405
 
406
        return $this->_conn->executeQuery($sql, [$databaseName]);
407
    }
408
 
409
    protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result
410
    {
411
        $sql = 'SELECT';
412
 
413
        if ($tableName === null) {
414
            $sql .= ' obj.name AS table_name, scm.name AS schema_name,';
415
        }
416
 
417
        $sql .= <<<'SQL'
418
                          col.name,
419
                          type.name AS type,
420
                          col.max_length AS length,
421
                          ~col.is_nullable AS notnull,
422
                          def.definition AS [default],
423
                          col.scale,
424
                          col.precision,
425
                          col.is_identity AS autoincrement,
426
                          col.collation_name AS collation,
427
                          -- CAST avoids driver error for sql_variant type
428
                          CAST(prop.value AS NVARCHAR(MAX)) AS comment
429
                FROM      sys.columns AS col
430
                JOIN      sys.types AS type
431
                ON        col.user_type_id = type.user_type_id
432
                JOIN      sys.objects AS obj
433
                ON        col.object_id = obj.object_id
434
                JOIN      sys.schemas AS scm
435
                ON        obj.schema_id = scm.schema_id
436
                LEFT JOIN sys.default_constraints def
437
                ON        col.default_object_id = def.object_id
438
                AND       col.object_id = def.parent_object_id
439
                LEFT JOIN sys.extended_properties AS prop
440
                ON        obj.object_id = prop.major_id
441
                AND       col.column_id = prop.minor_id
442
                AND       prop.name = 'MS_Description'
443
SQL;
444
 
445
        // The "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams
446
        $conditions = ["obj.type = 'U'", "obj.name != 'sysdiagrams'"];
447
        $params     = [];
448
 
449
        if ($tableName !== null) {
450
            $conditions[] = $this->getTableWhereClause($tableName, 'scm.name', 'obj.name');
451
        }
452
 
453
        $sql .= ' WHERE ' . implode(' AND ', $conditions);
454
 
455
        return $this->_conn->executeQuery($sql, $params);
456
    }
457
 
458
    protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result
459
    {
460
        $sql = 'SELECT';
461
 
462
        if ($tableName === null) {
463
            $sql .= ' tbl.name AS table_name, scm.name AS schema_name,';
464
        }
465
 
466
        $sql .= <<<'SQL'
467
                       idx.name AS key_name,
468
                       col.name AS column_name,
469
                       ~idx.is_unique AS non_unique,
470
                       idx.is_primary_key AS [primary],
471
                       CASE idx.type
472
                           WHEN '1' THEN 'clustered'
473
                           WHEN '2' THEN 'nonclustered'
474
                           ELSE NULL
475
                       END AS flags
476
                FROM sys.tables AS tbl
477
                JOIN sys.schemas AS scm
478
                  ON tbl.schema_id = scm.schema_id
479
                JOIN sys.indexes AS idx
480
                  ON tbl.object_id = idx.object_id
481
                JOIN sys.index_columns AS idxcol
482
                  ON idx.object_id = idxcol.object_id
483
                 AND idx.index_id = idxcol.index_id
484
                JOIN sys.columns AS col
485
                  ON idxcol.object_id = col.object_id
486
                 AND idxcol.column_id = col.column_id
487
SQL;
488
 
489
        $conditions = [];
490
        $params     = [];
491
 
492
        if ($tableName !== null) {
493
            $conditions[] = $this->getTableWhereClause($tableName, 'scm.name', 'tbl.name');
494
            $sql         .= ' WHERE ' . implode(' AND ', $conditions);
495
        }
496
 
497
        $sql .= ' ORDER BY idx.index_id, idxcol.key_ordinal';
498
 
499
        return $this->_conn->executeQuery($sql, $params);
500
    }
501
 
502
    protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result
503
    {
504
        $sql = 'SELECT';
505
 
506
        if ($tableName === null) {
507
            $sql .= ' OBJECT_NAME (f.parent_object_id) AS table_name, SCHEMA_NAME(f.schema_id) AS schema_name,';
508
        }
509
 
510
        $sql .= <<<'SQL'
511
                f.name AS ForeignKey,
512
                SCHEMA_NAME (f.SCHEMA_ID) AS SchemaName,
513
                OBJECT_NAME (f.parent_object_id) AS TableName,
514
                COL_NAME (fc.parent_object_id,fc.parent_column_id) AS ColumnName,
515
                SCHEMA_NAME (o.SCHEMA_ID) ReferenceSchemaName,
516
                OBJECT_NAME (f.referenced_object_id) AS ReferenceTableName,
517
                COL_NAME(fc.referenced_object_id,fc.referenced_column_id) AS ReferenceColumnName,
518
                f.delete_referential_action_desc,
519
                f.update_referential_action_desc
520
                FROM sys.foreign_keys AS f
521
                INNER JOIN sys.foreign_key_columns AS fc
522
                INNER JOIN sys.objects AS o ON o.OBJECT_ID = fc.referenced_object_id
523
                ON f.OBJECT_ID = fc.constraint_object_id
524
SQL;
525
 
526
        $conditions = [];
527
        $params     = [];
528
 
529
        if ($tableName !== null) {
530
            $conditions[] = $this->getTableWhereClause(
531
                $tableName,
532
                'SCHEMA_NAME(f.schema_id)',
533
                'OBJECT_NAME(f.parent_object_id)',
534
            );
535
 
536
            $sql .= ' WHERE ' . implode(' AND ', $conditions);
537
        }
538
 
539
        $sql .= ' ORDER BY fc.constraint_column_id';
540
 
541
        return $this->_conn->executeQuery($sql, $params);
542
    }
543
 
544
    /**
545
     * {@inheritDoc}
546
     */
547
    protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array
548
    {
549
        $sql = <<<'SQL'
550
          SELECT
551
            tbl.name,
552
            p.value AS [table_comment]
553
          FROM
554
            sys.tables AS tbl
555
            INNER JOIN sys.extended_properties AS p ON p.major_id=tbl.object_id AND p.minor_id=0 AND p.class=1
556
SQL;
557
 
558
        $conditions = ["SCHEMA_NAME(tbl.schema_id) = N'dbo'", "p.name = N'MS_Description'"];
559
        $params     = [];
560
 
561
        if ($tableName !== null) {
562
            $conditions[] = "tbl.name = N'" . $tableName . "'";
563
        }
564
 
565
        $sql .= ' WHERE ' . implode(' AND ', $conditions);
566
 
567
        /** @var array<string,array<string,mixed>> $metadata */
568
        $metadata = $this->_conn->executeQuery($sql, $params)
569
            ->fetchAllAssociativeIndexed();
570
 
571
        $tableOptions = [];
572
        foreach ($metadata as $table => $data) {
573
            $data = array_change_key_case($data, CASE_LOWER);
574
 
575
            $tableOptions[$table] = [
576
                'comment' => $data['table_comment'],
577
            ];
578
        }
579
 
580
        return $tableOptions;
581
    }
582
 
583
    /**
584
     * Returns the where clause to filter schema and table name in a query.
585
     *
586
     * @param string $table        The full qualified name of the table.
587
     * @param string $schemaColumn The name of the column to compare the schema to in the where clause.
588
     * @param string $tableColumn  The name of the column to compare the table to in the where clause.
589
     */
590
    private function getTableWhereClause($table, $schemaColumn, $tableColumn): string
591
    {
592
        if (strpos($table, '.') !== false) {
593
            [$schema, $table] = explode('.', $table);
594
            $schema           = $this->_platform->quoteStringLiteral($schema);
595
            $table            = $this->_platform->quoteStringLiteral($table);
596
        } else {
597
            $schema = 'SCHEMA_NAME()';
598
            $table  = $this->_platform->quoteStringLiteral($table);
599
        }
600
 
601
        return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema);
602
    }
603
}