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