Passed
Push — master ( 255a28...9b125d )
by Wilmer
01:52 queued 12s
created

Schema::loadColumnSchema()   C

Complexity

Conditions 15
Paths 92

Size

Total Lines 53
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 16.2371

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 15
eloc 33
c 1
b 0
f 0
nc 92
nop 1
dl 0
loc 53
ccs 28
cts 34
cp 0.8235
crap 16.2371
rs 5.9166

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Mssql;
6
7
use Throwable;
8
use Yiisoft\Arrays\ArrayHelper;
9
use Yiisoft\Db\Constraint\CheckConstraint;
10
use Yiisoft\Db\Constraint\Constraint;
11
use Yiisoft\Db\Constraint\DefaultValueConstraint;
12
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
13
use Yiisoft\Db\Constraint\IndexConstraint;
14
use Yiisoft\Db\Exception\Exception;
15
use Yiisoft\Db\Exception\InvalidCallException;
16
use Yiisoft\Db\Exception\InvalidConfigException;
17
use Yiisoft\Db\Schema\ColumnSchemaBuilder;
18
use Yiisoft\Db\Schema\Schema as AbstractSchema;
19
use Yiisoft\Db\View\ViewFinderTrait;
20
21
use function array_map;
22
use function count;
23
use function explode;
24
use function preg_match;
25
use function preg_match_all;
26
use function str_replace;
27
use function strcasecmp;
28
use function stripos;
29
use function version_compare;
30
31
/**
32
 * Schema is the class for retrieving metadata from MS SQL Server databases (version 2008 and above).
33
 */
34
final class Schema extends AbstractSchema
35
{
36
    use ViewFinderTrait;
37
38
    /**
39
     * @var string|null the default schema used for the current session.
40
     */
41
    protected ?string $defaultSchema = 'dbo';
42
43
    /**
44
     * @var array mapping from physical column types (keys) to abstract column types (values)
45
     */
46
    private array $typeMap = [
47
        /** exact numbers */
48
        'bigint' => self::TYPE_BIGINT,
49
        'numeric' => self::TYPE_DECIMAL,
50
        'bit' => self::TYPE_SMALLINT,
51
        'smallint' => self::TYPE_SMALLINT,
52
        'decimal' => self::TYPE_DECIMAL,
53
        'smallmoney' => self::TYPE_MONEY,
54
        'int' => self::TYPE_INTEGER,
55
        'tinyint' => self::TYPE_TINYINT,
56
        'money' => self::TYPE_MONEY,
57
58
        /** approximate numbers */
59
        'float' => self::TYPE_FLOAT,
60
        'double' => self::TYPE_DOUBLE,
61
        'real' => self::TYPE_FLOAT,
62
63
        /** date and time */
64
        'date' => self::TYPE_DATE,
65
        'datetimeoffset' => self::TYPE_DATETIME,
66
        'datetime2' => self::TYPE_DATETIME,
67
        'smalldatetime' => self::TYPE_DATETIME,
68
        'datetime' => self::TYPE_DATETIME,
69
        'time' => self::TYPE_TIME,
70
71
        /** character strings */
72
        'char' => self::TYPE_CHAR,
73
        'varchar' => self::TYPE_STRING,
74
        'text' => self::TYPE_TEXT,
75
76
        /** unicode character strings */
77
        'nchar' => self::TYPE_CHAR,
78
        'nvarchar' => self::TYPE_STRING,
79
        'ntext' => self::TYPE_TEXT,
80
81
        /** binary strings */
82
        'binary' => self::TYPE_BINARY,
83
        'varbinary' => self::TYPE_BINARY,
84
        'image' => self::TYPE_BINARY,
85
86
        /**
87
         * other data types 'cursor' type cannot be used with tables
88
         */
89
        'timestamp' => self::TYPE_TIMESTAMP,
90
        'hierarchyid' => self::TYPE_STRING,
91
        'uniqueidentifier' => self::TYPE_STRING,
92
        'sql_variant' => self::TYPE_STRING,
93
        'xml' => self::TYPE_STRING,
94
        'table' => self::TYPE_STRING,
95
    ];
96
97
    /** @var array|string */
98
    protected $tableQuoteCharacter = ['[', ']'];
99
100
    /** @var array|string */
101
    protected $columnQuoteCharacter = ['[', ']'];
102
103
    /**
104
     * Resolves the table name and schema name (if any).
105
     *
106
     * @param string $name the table name.
107
     *
108
     * @return TableSchema resolved table, schema, etc. names.
109
     */
110 84
    protected function resolveTableName(string $name): TableSchema
111
    {
112 84
        $resolvedName = new TableSchema();
113
114 84
        $parts = $this->getTableNameParts($name);
115
116 84
        $partCount = count($parts);
117
118 84
        if ($partCount === 4) {
119
            /** server name, catalog name, schema name and table name passed */
120
            $resolvedName->catalogName($parts[1]);
121
            $resolvedName->schemaName($parts[2]);
122
            $resolvedName->name($parts[3]);
123
            $resolvedName->fullName(
124
                $resolvedName->getCatalogName() . '.' . $resolvedName->getSchemaName() . '.' . $resolvedName->getName()
125
            );
126 84
        } elseif ($partCount === 3) {
127
            /** catalog name, schema name and table name passed */
128
            $resolvedName->catalogName($parts[0]);
129
            $resolvedName->schemaName($parts[1]);
130
            $resolvedName->name($parts[2]);
131
            $resolvedName->fullName(
132
                $resolvedName->getCatalogName() . '.' . $resolvedName->getSchemaName() . '.' . $resolvedName->getName()
133
            );
134 84
        } elseif ($partCount === 2) {
135
            /** only schema name and table name passed */
136
            $resolvedName->schemaName($parts[0]);
137
            $resolvedName->name($parts[1]);
138
            $resolvedName->fullName(
139
                ($resolvedName->getSchemaName() !== $this->defaultSchema
140
                    ? $resolvedName->getSchemaName() . '.' : '') . $resolvedName->getName()
141
            );
142
        } else {
143
            /** only table name passed */
144 84
            $resolvedName->schemaName($this->defaultSchema);
145 84
            $resolvedName->name($parts[0]);
146 84
            $resolvedName->fullName($resolvedName->getName());
147
        }
148
149 84
        return $resolvedName;
150
    }
151
152
    /**
153
     * Splits full table name into parts.
154
     *
155
     * @param string $name
156
     *
157
     * @return array
158
     */
159 164
    protected function getTableNameParts(string $name): array
160
    {
161 164
        $parts = [$name];
162
163 164
        preg_match_all('/([^.\[\]]+)|\[([^\[\]]+)]/', $name, $matches);
164
165 164
        if (isset($matches[0]) && !empty($matches[0])) {
166 164
            $parts = $matches[0];
167
        }
168
169 164
        return str_replace(['[', ']'], '', $parts);
170
    }
171
172
    /**
173
     * Returns all schema names in the database, including the default one but not system schemas.
174
     *
175
     * This method should be overridden by child classes in order to support this feature because the default
176
     * implementation simply throws an exception.
177
     *
178
     * @throws Exception|InvalidConfigException|Throwable
179
     *
180
     * @return array all schema names in the database, except system schemas.
181
     *
182
     * {@see https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-database-principals-transact-sql}
183
     */
184 1
    protected function findSchemaNames(): array
185
    {
186
        static $sql = <<<'SQL'
187
SELECT [s].[name]
188
FROM [sys].[schemas] AS [s]
189
INNER JOIN [sys].[database_principals] AS [p] ON [p].[principal_id] = [s].[principal_id]
190
WHERE [p].[is_fixed_role] = 0 AND [p].[sid] IS NOT NULL
191
ORDER BY [s].[name] ASC
192
SQL;
193
194 1
        return $this->getDb()->createCommand($sql)->queryColumn();
195
    }
196
197
    /**
198
     * Returns all table names in the database.
199
     *
200
     * This method should be overridden by child classes in order to support this feature because the default
201
     * implementation simply throws an exception.
202
     *
203
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
204
     *
205
     * @throws Exception|InvalidConfigException|Throwable
206
     *
207
     * @return array all table names in the database. The names have NO schema name prefix.
208
     */
209 5
    protected function findTableNames(string $schema = ''): array
210
    {
211 5
        if ($schema === '') {
212 5
            $schema = $this->defaultSchema;
213
        }
214
215 5
        $sql = <<<'SQL'
216
SELECT [t].[table_name]
217
FROM [INFORMATION_SCHEMA].[TABLES] AS [t]
218
WHERE [t].[table_schema] = :schema AND [t].[table_type] IN ('BASE TABLE', 'VIEW')
219
ORDER BY [t].[table_name]
220
SQL;
221
222 5
        $tables = $this->getDb()->createCommand($sql, [':schema' => $schema])->queryColumn();
223
224 5
        $tables = array_map(static function ($item) {
225 5
            return '[' . $item . ']';
226
        }, $tables);
227
228 5
        return $tables;
229
    }
230
231
    /**
232
     * Loads the metadata for the specified table.
233
     *
234
     * @param string $name table name.
235
     *
236
     * @throws Exception|InvalidConfigException|Throwable
237
     *
238
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
239
     */
240 97
    protected function loadTableSchema(string $name): ?TableSchema
241
    {
242 97
        $table = new TableSchema();
243
244 97
        $this->resolveTableNames($table, $name);
245 97
        $this->findPrimaryKeys($table);
246
247 97
        if ($this->findColumns($table)) {
248 91
            $this->findForeignKeys($table);
249
250 91
            return $table;
251
        }
252
253 16
        return null;
254
    }
255
256
    /**
257
     * Loads a primary key for the given table.
258
     *
259
     * @param string $tableName table name.
260
     *
261
     * @throws Exception|InvalidConfigException|Throwable
262
     *
263
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.
264
     */
265 31
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
266
    {
267 31
        return $this->loadTableConstraints($tableName, 'primaryKey');
268
    }
269
270
    /**
271
     * Loads all foreign keys for the given table.
272
     *
273
     * @param string $tableName table name.
274
     *
275
     * @throws Exception|InvalidConfigException|Throwable
276
     *
277
     * @return ForeignKeyConstraint[] foreign keys for the given table.
278
     */
279 4
    protected function loadTableForeignKeys(string $tableName): array
280
    {
281 4
        return $this->loadTableConstraints($tableName, 'foreignKeys');
282
    }
283
284
    /**
285
     * Loads all indexes for the given table.
286
     *
287
     * @param string $tableName table name.
288
     *
289
     * @throws Exception|InvalidConfigException|Throwable
290
     *
291
     * @return array indexes for the given table.
292
     */
293 29
    protected function loadTableIndexes(string $tableName): array
294
    {
295
        static $sql = <<<'SQL'
296
SELECT
297
    [i].[name] AS [name],
298
    [iccol].[name] AS [column_name],
299
    [i].[is_unique] AS [index_is_unique],
300
    [i].[is_primary_key] AS [index_is_primary]
301
FROM [sys].[indexes] AS [i]
302
INNER JOIN [sys].[index_columns] AS [ic]
303
    ON [ic].[object_id] = [i].[object_id] AND [ic].[index_id] = [i].[index_id]
304
INNER JOIN [sys].[columns] AS [iccol]
305
    ON [iccol].[object_id] = [ic].[object_id] AND [iccol].[column_id] = [ic].[column_id]
306
WHERE [i].[object_id] = OBJECT_ID(:fullName)
307
ORDER BY [ic].[key_ordinal] ASC
308
SQL;
309
310 29
        $resolvedName = $this->resolveTableName($tableName);
311 29
        $indexes = $this->getDb()->createCommand($sql, [':fullName' => $resolvedName->getFullName()])->queryAll();
312 29
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
313 29
        $indexes = ArrayHelper::index($indexes, null, 'name');
314
315 29
        $result = [];
316
317 29
        foreach ($indexes as $name => $index) {
318 26
            $result[] = (new IndexConstraint())
319 26
                ->primary((bool) $index[0]['index_is_primary'])
320 26
                ->unique((bool) $index[0]['index_is_unique'])
321 26
                ->columnNames(ArrayHelper::getColumn($index, 'column_name'))
322 26
                ->name($name);
323
        }
324
325 29
        return $result;
326
    }
327
328
    /**
329
     * Loads all unique constraints for the given table.
330
     *
331
     * @param string $tableName table name.
332
     *
333
     * @throws Exception|InvalidConfigException|Throwable
334
     *
335
     * @return Constraint[] unique constraints for the given table.
336
     */
337 13
    protected function loadTableUniques(string $tableName): array
338
    {
339 13
        return $this->loadTableConstraints($tableName, 'uniques');
340
    }
341
342
    /**
343
     * Loads all check constraints for the given table.
344
     *
345
     * @param string $tableName table name.
346
     *
347
     * @throws Exception|InvalidConfigException|Throwable
348
     *
349
     * @return CheckConstraint[] check constraints for the given table.
350
     */
351 13
    protected function loadTableChecks(string $tableName): array
352
    {
353 13
        return $this->loadTableConstraints($tableName, 'checks');
354
    }
355
356
    /**
357
     * Loads all default value constraints for the given table.
358
     *
359
     * @param string $tableName table name.
360
     *
361
     * @throws Exception|InvalidConfigException|Throwable
362
     *
363
     * @return DefaultValueConstraint[] default value constraints for the given table.
364
     */
365 13
    protected function loadTableDefaultValues(string $tableName): array
366
    {
367 13
        return $this->loadTableConstraints($tableName, 'defaults');
368
    }
369
370
    /**
371
     * Creates a new savepoint.
372
     *
373
     * @param string $name the savepoint name.
374
     *
375
     * @throws Exception|InvalidConfigException|Throwable
376
     */
377 1
    public function createSavepoint(string $name): void
378
    {
379 1
        $this->getDb()->createCommand("SAVE TRANSACTION $name")->execute();
380
    }
381
382
    /**
383
     * Releases an existing savepoint.
384
     *
385
     * @param string $name the savepoint name.
386
     */
387
    public function releaseSavepoint(string $name): void
388
    {
389
        /** does nothing as MSSQL does not support this */
390
    }
391
392
    /**
393
     * Rolls back to a previously created savepoint.
394
     *
395
     * @param string $name the savepoint name.
396
     *
397
     * @throws Exception|InvalidConfigException|Throwable
398
     */
399 1
    public function rollBackSavepoint(string $name): void
400
    {
401 1
        $this->getDb()->createCommand("ROLLBACK TRANSACTION $name")->execute();
402
    }
403
404
    /**
405
     * Creates a column schema for the database.
406
     *
407
     * This method may be overridden by child classes to create a DBMS-specific column schema.
408
     *
409
     * @return ColumnSchema column schema instance.
410
     */
411 91
    protected function createColumnSchema(): ColumnSchema
412
    {
413 91
        return new ColumnSchema();
414
    }
415
416
    /**
417
     * Creates a query builder for the MSSQL database.
418
     *
419
     * @return QueryBuilder query builder interface.
420
     */
421 65
    public function createQueryBuilder(): QueryBuilder
422
    {
423 65
        return new QueryBuilder($this->getDb());
424
    }
425
426
    /**
427
     * Resolves the table name and schema name (if any).
428
     *
429
     * @param TableSchema $table the table metadata object.
430
     * @param string $name the table name
431
     */
432 97
    protected function resolveTableNames(TableSchema $table, string $name): void
433
    {
434 97
        $parts = $this->getTableNameParts($name);
435
436 97
        $partCount = count($parts);
437
438 97
        if ($partCount === 4) {
439
            /** server name, catalog name, schema name and table name passed */
440
            $table->catalogName($parts[1]);
441
            $table->schemaName($parts[2]);
442
            $table->name($parts[3]);
443
            $table->fullName($table->getCatalogName() . '.' . $table->getSchemaName() . '.' . $table->getName());
444 97
        } elseif ($partCount === 3) {
445
            /** catalog name, schema name and table name passed */
446
            $table->catalogName($parts[0]);
447
            $table->schemaName($parts[1]);
448
            $table->name($parts[2]);
449
            $table->fullName($table->getCatalogName() . '.' . $table->getSchemaName() . '.' . $table->getName());
450 97
        } elseif ($partCount === 2) {
451
            /** only schema name and table name passed */
452 3
            $table->schemaName($parts[0]);
453 3
            $table->name($parts[1]);
454 3
            $table->fullName(
455 3
                $table->getSchemaName() !== $this->defaultSchema
456 3
                ? $table->getSchemaName() . '.' . $table->getName() : $table->getName()
457
            );
458
        } else {
459
            /** only table name passed */
460 94
            $table->schemaName($this->defaultSchema);
461 94
            $table->name($parts[0]);
462 94
            $table->fullName($table->getName());
463
        }
464
    }
465
466
    /**
467
     * Loads the column information into a {@see ColumnSchema} object.
468
     *
469
     * @param array $info column information.
470
     *
471
     * @return ColumnSchema the column schema object.
472
     */
473 91
    protected function loadColumnSchema(array $info): ColumnSchema
474
    {
475 91
        $column = $this->createColumnSchema();
476
477 91
        $column->name($info['column_name']);
478 91
        $column->allowNull($info['is_nullable'] === 'YES');
479 91
        $column->dbType($info['data_type']);
480 91
        $column->enumValues([]); // mssql has only vague equivalents to enum
481 91
        $column->primaryKey(false); // primary key will be determined in findColumns() method
482 91
        $column->autoIncrement($info['is_identity'] === 1);
483 91
        $column->unsigned(stripos($column->getDbType(), 'unsigned') !== false);
484 91
        $column->comment($info['comment'] ?? '');
485 91
        $column->type(self::TYPE_STRING);
486
487 91
        if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $column->getDbType(), $matches)) {
488 91
            $type = $matches[1];
489
490 91
            if (isset($this->typeMap[$type])) {
491 91
                $column->type($this->typeMap[$type]);
492
            }
493
494 91
            if (!empty($matches[2])) {
495 75
                $values = explode(',', $matches[2]);
496 75
                $column->precision((int) $values[0]);
497 75
                $column->size((int) $values[0]);
498
499 75
                if (isset($values[1])) {
500
                    $column->scale((int) $values[1]);
501
                }
502
503 75
                if ($column->getSize() === 1 && ($type === 'tinyint' || $type === 'bit')) {
504
                    $column->type('boolean');
505 75
                } elseif ($type === 'bit') {
506
                    if ($column->getSize() > 32) {
507
                        $column->type('bigint');
508
                    } elseif ($column->getSize() === 32) {
509
                        $column->type('integer');
510
                    }
511
                }
512
            }
513
        }
514
515 91
        $column->phpType($this->getColumnPhpType($column));
516
517 91
        if ($info['column_default'] === '(NULL)') {
518 4
            $info['column_default'] = null;
519
        }
520
521 91
        if (!$column->isPrimaryKey() && ($column->getType() !== 'timestamp' || $info['column_default'] !== 'CURRENT_TIMESTAMP')) {
522 91
            $column->defaultValue($column->defaultPhpTypecast($info['column_default']));
523
        }
524
525 91
        return $column;
526
    }
527
528
    /**
529
     * Collects the metadata of table columns.
530
     *
531
     * @param TableSchema $table the table metadata.
532
     *
533
     * @throws Throwable
534
     *
535
     * @return bool whether the table exists in the database.
536
     */
537 97
    protected function findColumns(TableSchema $table): bool
538
    {
539 97
        $columnsTableName = 'INFORMATION_SCHEMA.COLUMNS';
540 97
        $whereSql = '[t1].[table_name] = ' . $this->getDb()->quoteValue($table->getName());
541
542 97
        if ($table->getCatalogName() !== null) {
543
            $columnsTableName = "{$table->getCatalogName()}.{$columnsTableName}";
544
            $whereSql .= " AND [t1].[table_catalog] = '{$table->getCatalogName()}'";
545
        }
546
547 97
        if ($table->getSchemaName() !== null) {
548 97
            $whereSql .= " AND [t1].[table_schema] = '{$table->getSchemaName()}'";
549
        }
550
551 97
        $columnsTableName = $this->quoteTableName($columnsTableName);
552
553 97
        $sql = <<<SQL
554
SELECT
555
 [t1].[column_name],
556
 [t1].[is_nullable],
557
 CASE WHEN [t1].[data_type] IN ('char','varchar','nchar','nvarchar','binary','varbinary') THEN
558
    CASE WHEN [t1].[character_maximum_length] = NULL OR [t1].[character_maximum_length] = -1 THEN
559
        [t1].[data_type]
560
    ELSE
561
        [t1].[data_type] + '(' + LTRIM(RTRIM(CONVERT(CHAR,[t1].[character_maximum_length]))) + ')'
562
    END
563
 ELSE
564
    [t1].[data_type]
565
 END AS 'data_type',
566
 [t1].[column_default],
567
 COLUMNPROPERTY(OBJECT_ID([t1].[table_schema] + '.' + [t1].[table_name]), [t1].[column_name], 'IsIdentity') AS is_identity,
568
 (
569
    SELECT CONVERT(VARCHAR, [t2].[value])
570
		FROM [sys].[extended_properties] AS [t2]
571
		WHERE
572
			[t2].[class] = 1 AND
573
			[t2].[class_desc] = 'OBJECT_OR_COLUMN' AND
574
			[t2].[name] = 'MS_Description' AND
575
			[t2].[major_id] = OBJECT_ID([t1].[TABLE_SCHEMA] + '.' + [t1].[table_name]) AND
576
			[t2].[minor_id] = COLUMNPROPERTY(OBJECT_ID([t1].[TABLE_SCHEMA] + '.' + [t1].[TABLE_NAME]), [t1].[COLUMN_NAME], 'ColumnID')
577
 ) as comment
578
FROM {$columnsTableName} AS [t1]
579
WHERE {$whereSql}
580
SQL;
581
582
        try {
583 97
            $columns = $this->getDb()->createCommand($sql)->queryAll();
584
585 97
            if (empty($columns)) {
586 97
                return false;
587
            }
588
        } catch (Exception $e) {
589
            return false;
590
        }
591
592 91
        foreach ($columns as $column) {
593 91
            $column = $this->loadColumnSchema($column);
594 91
            foreach ($table->getPrimaryKey() as $primaryKey) {
595 58
                if (strcasecmp($column->getName(), $primaryKey) === 0) {
596 58
                    $column->primaryKey(true);
597 58
                    break;
598
                }
599
            }
600
601 91
            if ($column->isPrimaryKey() && $column->isAutoIncrement()) {
602
                $table->sequenceName('');
603
            }
604
605 91
            $table->columns($column->getName(), $column);
606
        }
607
608 91
        return true;
609
    }
610
611
    /**
612
     * Collects the constraint details for the given table and constraint type.
613
     *
614
     * @param TableSchema $table
615
     * @param string $type either PRIMARY KEY or UNIQUE.
616
     *
617
     * @throws Exception|InvalidConfigException|Throwable
618
     *
619
     * @return array each entry contains index_name and field_name.
620
     */
621 97
    protected function findTableConstraints(TableSchema $table, string $type): array
622
    {
623 97
        $keyColumnUsageTableName = 'INFORMATION_SCHEMA.KEY_COLUMN_USAGE';
624 97
        $tableConstraintsTableName = 'INFORMATION_SCHEMA.TABLE_CONSTRAINTS';
625
626 97
        if ($table->getCatalogName() !== null) {
627
            $keyColumnUsageTableName = $table->getCatalogName() . '.' . $keyColumnUsageTableName;
628
            $tableConstraintsTableName = $table->getCatalogName() . '.' . $tableConstraintsTableName;
629
        }
630
631 97
        $keyColumnUsageTableName = $this->quoteTableName($keyColumnUsageTableName);
632 97
        $tableConstraintsTableName = $this->quoteTableName($tableConstraintsTableName);
633
634 97
        $sql = <<<SQL
635
SELECT
636
    [kcu].[constraint_name] AS [index_name],
637
    [kcu].[column_name] AS [field_name]
638
FROM {$keyColumnUsageTableName} AS [kcu]
639
LEFT JOIN {$tableConstraintsTableName} AS [tc] ON
640
    [kcu].[table_schema] = [tc].[table_schema] AND
641
    [kcu].[table_name] = [tc].[table_name] AND
642
    [kcu].[constraint_name] = [tc].[constraint_name]
643
WHERE
644
    [tc].[constraint_type] = :type AND
645
    [kcu].[table_name] = :tableName AND
646
    [kcu].[table_schema] = :schemaName
647
SQL;
648
649 97
        return $this->getDb()->createCommand(
650
            $sql,
651
            [
652 97
                ':tableName' => $table->getName(),
653 97
                ':schemaName' => $table->getSchemaName(),
654
                ':type' => $type,
655
            ]
656 97
        )->queryAll();
657
    }
658
659
    /**
660
     * Collects the primary key column details for the given table.
661
     *
662
     * @param TableSchema $table the table metadata
663
     *
664
     * @throws Exception|InvalidConfigException|Throwable
665
     */
666 97
    protected function findPrimaryKeys(TableSchema $table): void
667
    {
668 97
        foreach ($this->findTableConstraints($table, 'PRIMARY KEY') as $row) {
669 58
            $table->primaryKey($row['field_name']);
670
        }
671
    }
672
673
    /**
674
     * Collects the foreign key column details for the given table.
675
     *
676
     * @param TableSchema $table the table metadata
677
     *
678
     * @throws Exception|InvalidConfigException|Throwable
679
     */
680 91
    protected function findForeignKeys(TableSchema $table): void
681
    {
682 91
        $object = $table->getName();
683
684 91
        if ($table->getSchemaName() !== null) {
685 91
            $object = $table->getSchemaName() . '.' . $object;
686
        }
687
688 91
        if ($table->getCatalogName() !== null) {
689
            $object = $table->getCatalogName() . '.' . $object;
690
        }
691
692
        /**
693
         * Please refer to the following page for more details:
694
         * {@see http://msdn2.microsoft.com/en-us/library/aa175805(SQL.80).aspx}
695
         */
696 91
        $sql = <<<'SQL'
697
SELECT
698
	[fk].[name] AS [fk_name],
699
	[cp].[name] AS [fk_column_name],
700
	OBJECT_NAME([fk].[referenced_object_id]) AS [uq_table_name],
701
	[cr].[name] AS [uq_column_name]
702
FROM
703
	[sys].[foreign_keys] AS [fk]
704
	INNER JOIN [sys].[foreign_key_columns] AS [fkc] ON
705
		[fk].[object_id] = [fkc].[constraint_object_id]
706
	INNER JOIN [sys].[columns] AS [cp] ON
707
		[fk].[parent_object_id] = [cp].[object_id] AND
708
		[fkc].[parent_column_id] = [cp].[column_id]
709
	INNER JOIN [sys].[columns] AS [cr] ON
710
		[fk].[referenced_object_id] = [cr].[object_id] AND
711
		[fkc].[referenced_column_id] = [cr].[column_id]
712
WHERE
713
	[fk].[parent_object_id] = OBJECT_ID(:object)
714
SQL;
715
716 91
        $rows = $this->getDb()->createCommand($sql, [':object' => $object])->queryAll();
717
718 91
        $table->foreignKeys([]);
719
720 91
        foreach ($rows as $row) {
721 5
            if (!isset($table->getForeignKeys()[$row['fk_name']])) {
722 5
                $fk[$row['fk_name']][] = $row['uq_table_name'];
723 5
                $table->foreignKeys($fk);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $fk seems to be defined later in this foreach loop on line 722. Are you sure it is defined here?
Loading history...
724
            }
725
726 5
            $fk[$row['fk_name']][$row['fk_column_name']] = $row['uq_column_name'];
727 5
            $table->foreignKeys($fk);
728
        }
729
    }
730
731
    /**
732
     * Returns all views names in the database.
733
     *
734
     * @param string $schema the schema of the views. Defaults to empty string, meaning the current or default schema.
735
     *
736
     * @throws Exception|InvalidConfigException|Throwable
737
     *
738
     * @return array all views names in the database. The names have NO schema name prefix.
739
     */
740
    protected function findViewNames(string $schema = ''): array
741
    {
742
        if ($schema === '') {
743
            $schema = $this->defaultSchema;
744
        }
745
746
        $sql = <<<'SQL'
747
SELECT [t].[table_name]
748
FROM [INFORMATION_SCHEMA].[TABLES] AS [t]
749
WHERE [t].[table_schema] = :schema AND [t].[table_type] = 'VIEW'
750
ORDER BY [t].[table_name]
751
SQL;
752
753
        $views = $this->getDb()->createCommand($sql, [':schema' => $schema])->queryColumn();
754
        $views = array_map(static function ($item) {
755
            return '[' . $item . ']';
756
        }, $views);
757
758
        return $views;
759
    }
760
761
    /**
762
     * Returns all unique indexes for the given table.
763
     *
764
     * Each array element is of the following structure:
765
     *
766
     * ```php
767
     * [
768
     *     'IndexName1' => ['col1' [, ...]],
769
     *     'IndexName2' => ['col2' [, ...]],
770
     * ]
771
     * ```
772
     *
773
     * @param TableSchema $table the table metadata.
774
     *
775
     * @throws Exception|InvalidConfigException|Throwable
776
     *
777
     * @return array all unique indexes for the given table.
778
     */
779
    public function findUniqueIndexes(TableSchema $table): array
780
    {
781
        $result = [];
782
783
        foreach ($this->findTableConstraints($table, 'UNIQUE') as $row) {
784
            $result[$row['index_name']][] = $row['field_name'];
785
        }
786
787
        return $result;
788
    }
789
790
    /**
791
     * Loads multiple types of constraints and returns the specified ones.
792
     *
793
     * @param string $tableName table name.
794
     * @param string $returnType return type:
795
     * - primaryKey
796
     * - foreignKeys
797
     * - uniques
798
     * - checks
799
     * - defaults
800
     *
801
     * @throws Exception|InvalidConfigException|Throwable
802
     *
803
     * @return mixed constraints.
804
     */
805 74
    private function loadTableConstraints(string $tableName, string $returnType)
806
    {
807
        static $sql = <<<'SQL'
808
SELECT
809
    [o].[name] AS [name],
810
    COALESCE([ccol].[name], [dcol].[name], [fccol].[name], [kiccol].[name]) AS [column_name],
811
    RTRIM([o].[type]) AS [type],
812
    OBJECT_SCHEMA_NAME([f].[referenced_object_id]) AS [foreign_table_schema],
813
    OBJECT_NAME([f].[referenced_object_id]) AS [foreign_table_name],
814
    [ffccol].[name] AS [foreign_column_name],
815
    [f].[update_referential_action_desc] AS [on_update],
816
    [f].[delete_referential_action_desc] AS [on_delete],
817
    [c].[definition] AS [check_expr],
818
    [d].[definition] AS [default_expr]
819
FROM (SELECT OBJECT_ID(:fullName) AS [object_id]) AS [t]
820
INNER JOIN [sys].[objects] AS [o]
821
    ON [o].[parent_object_id] = [t].[object_id] AND [o].[type] IN ('PK', 'UQ', 'C', 'D', 'F')
822
LEFT JOIN [sys].[check_constraints] AS [c]
823
    ON [c].[object_id] = [o].[object_id]
824
LEFT JOIN [sys].[columns] AS [ccol]
825
    ON [ccol].[object_id] = [c].[parent_object_id] AND [ccol].[column_id] = [c].[parent_column_id]
826
LEFT JOIN [sys].[default_constraints] AS [d]
827
    ON [d].[object_id] = [o].[object_id]
828
LEFT JOIN [sys].[columns] AS [dcol]
829
    ON [dcol].[object_id] = [d].[parent_object_id] AND [dcol].[column_id] = [d].[parent_column_id]
830
LEFT JOIN [sys].[key_constraints] AS [k]
831
    ON [k].[object_id] = [o].[object_id]
832
LEFT JOIN [sys].[index_columns] AS [kic]
833
    ON [kic].[object_id] = [k].[parent_object_id] AND [kic].[index_id] = [k].[unique_index_id]
834
LEFT JOIN [sys].[columns] AS [kiccol]
835
    ON [kiccol].[object_id] = [kic].[object_id] AND [kiccol].[column_id] = [kic].[column_id]
836
LEFT JOIN [sys].[foreign_keys] AS [f]
837
    ON [f].[object_id] = [o].[object_id]
838
LEFT JOIN [sys].[foreign_key_columns] AS [fc]
839
    ON [fc].[constraint_object_id] = [o].[object_id]
840
LEFT JOIN [sys].[columns] AS [fccol]
841
    ON [fccol].[object_id] = [fc].[parent_object_id] AND [fccol].[column_id] = [fc].[parent_column_id]
842
LEFT JOIN [sys].[columns] AS [ffccol]
843
    ON [ffccol].[object_id] = [fc].[referenced_object_id] AND [ffccol].[column_id] = [fc].[referenced_column_id]
844
ORDER BY [kic].[key_ordinal] ASC, [fc].[constraint_column_id] ASC
845
SQL;
846
847 74
        $resolvedName = $this->resolveTableName($tableName);
848 74
        $constraints = $this->getDb()->createCommand($sql, [':fullName' => $resolvedName->getFullName()])->queryAll();
849 74
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
850 74
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
851 74
        $result = [
852
            'primaryKey' => null,
853
            'foreignKeys' => [],
854
            'uniques' => [],
855
            'checks' => [],
856
            'defaults' => [],
857
        ];
858
859 74
        foreach ($constraints as $type => $names) {
860 74
            foreach ($names as $name => $constraint) {
861 74
                switch ($type) {
862 74
                    case 'PK':
863
                        /** @var Constraint */
864 53
                        $result['primaryKey'] = (new Constraint())
865 53
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
866 53
                            ->name($name);
867 53
                        break;
868 74
                    case 'F':
869 16
                        $result['foreignKeys'][] = (new ForeignKeyConstraint())
870 16
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
871 16
                            ->foreignTableName($constraint[0]['foreign_table_name'])
872 16
                            ->foreignColumnNames(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
873 16
                            ->onDelete(str_replace('_', '', $constraint[0]['on_delete']))
874 16
                            ->onUpdate(str_replace('_', '', $constraint[0]['on_update']))
875 16
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
876 16
                            ->name($name);
877 16
                        break;
878 59
                    case 'UQ':
879 57
                        $result['uniques'][] = (new Constraint())
880 57
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
881 57
                            ->name($name);
882 57
                        break;
883 31
                    case 'C':
884 13
                        $result['checks'][] = (new CheckConstraint())
885 13
                            ->expression($constraint[0]['check_expr'])
886 13
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
887 13
                            ->name($name);
888 13
                        break;
889 30
                    case 'D':
890 30
                        $result['defaults'][] = (new DefaultValueConstraint())
891 30
                            ->value($constraint[0]['default_expr'])
892 30
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
893 30
                            ->name($name);
894 30
                        break;
895
                }
896
            }
897
        }
898 74
        foreach ($result as $type => $data) {
899 74
            $this->setTableMetadata($tableName, $type, $data);
900
        }
901
902 74
        return $result[$returnType];
903
    }
904
905
    /**
906
     * Quotes a column name for use in a query.
907
     *
908
     * If the column name contains prefix, the prefix will also be properly quoted. If the column name is already quoted
909
     * or contains '(', '[[' or '{{', then this method will do nothing.
910
     *
911
     * @param string $name column name.
912
     *
913
     * @return string the properly quoted column name.
914
     *
915
     * {@see quoteSimpleColumnName()}
916
     */
917 199
    public function quoteColumnName(string $name): string
918
    {
919 199
        if (preg_match('/^\[.*]$/', $name)) {
920 27
            return $name;
921
        }
922
923 199
        return parent::quoteColumnName($name);
924
    }
925
926
    /**
927
     * Executes the INSERT command, returning primary key values.
928
     *
929
     * @param string $table the table that new rows will be inserted into.
930
     * @param array $columns the column data (name => value) to be inserted into the table.
931
     *
932
     * @throws Exception|InvalidCallException|InvalidConfigException|Throwable
933
     *
934
     * @return array|false primary key values or false if the command fails.
935
     */
936 1
    public function insert(string $table, array $columns)
937
    {
938 1
        $command = $this->getDb()->createCommand()->insert($table, $columns);
939 1
        if (!$command->execute()) {
940
            return false;
941
        }
942
943 1
        $isVersion2005orLater = version_compare($this->getDb()->getSchema()->getServerVersion(), '9', '>=');
944 1
        $inserted = $isVersion2005orLater ? $command->getPdoStatement()->fetch() : [];
945
946 1
        $tableSchema = $this->getTableSchema($table);
947
948 1
        $result = [];
949 1
        foreach ($tableSchema->getPrimaryKey() as $name) {
950
            /**
951
             * {@see https://github.com/yiisoft/yii2/issues/13828 & https://github.com/yiisoft/yii2/issues/17474
952
             */
953 1
            if (isset($inserted[$name])) {
954 1
                $result[$name] = $inserted[$name];
955
            } elseif ($tableSchema->getColumns()[$name]->isAutoIncrement()) {
956
                // for a version earlier than 2005
957
                $result[$name] = $this->getLastInsertID($tableSchema->getSequenceName());
0 ignored issues
show
Bug introduced by
It seems like $tableSchema->getSequenceName() can also be of type null; however, parameter $sequenceName of Yiisoft\Db\Schema\Schema::getLastInsertID() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

957
                $result[$name] = $this->getLastInsertID(/** @scrutinizer ignore-type */ $tableSchema->getSequenceName());
Loading history...
958
            } elseif (isset($columns[$name])) {
959
                $result[$name] = $columns[$name];
960
            } else {
961
                $result[$name] = $tableSchema->getColumns()[$name]->getDefaultValue();
962
            }
963
        }
964
965 1
        return $result;
966
    }
967
968
    /**
969
     * Create a column schema builder instance giving the type and value precision.
970
     *
971
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
972
     *
973
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
974
     * @param array|int|string|null $length length or precision of the column. See {@see ColumnSchemaBuilder::$length}.
975
     *
976
     * @return ColumnSchemaBuilder column schema builder instance
977
     */
978 3
    public function createColumnSchemaBuilder(string $type, $length = null): ColumnSchemaBuilder
979
    {
980 3
        return new ColumnSchemaBuilder($type, $length);
981
    }
982
}
983