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

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