Passed
Pull Request — master (#45)
by Def
01:26
created

Schema::findColumns()   B

Complexity

Conditions 10
Paths 36

Size

Total Lines 72
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 10.7111

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 52
c 1
b 0
f 0
nc 36
nop 1
dl 0
loc 72
ccs 21
cts 26
cp 0.8077
crap 10.7111
rs 7.1806

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 function array_map;
8
use function count;
9
use function explode;
10
use function preg_match;
11
use function preg_match_all;
12
use function str_replace;
13
use function strcasecmp;
14
use function stripos;
15
use Throwable;
16
use function version_compare;
17
use Yiisoft\Arrays\ArrayHelper;
18
use Yiisoft\Db\Constraint\CheckConstraint;
19
use Yiisoft\Db\Constraint\Constraint;
20
use Yiisoft\Db\Constraint\ConstraintFinderInterface;
21
use Yiisoft\Db\Constraint\ConstraintFinderTrait;
22
23
use Yiisoft\Db\Constraint\DefaultValueConstraint;
24
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
25
use Yiisoft\Db\Constraint\IndexConstraint;
26
use Yiisoft\Db\Exception\Exception;
27
use Yiisoft\Db\Exception\InvalidCallException;
28
use Yiisoft\Db\Exception\InvalidConfigException;
29
use Yiisoft\Db\Schema\ColumnSchemaBuilder;
30
use Yiisoft\Db\Schema\Schema as AbstractSchema;
31
use Yiisoft\Db\View\ViewFinderTrait;
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
    protected function resolveTableName(string $name): TableSchema
114 83
    {
115
        $resolvedName = new TableSchema();
116 83
117
        $parts = $this->getTableNameParts($name);
118 83
119
        $partCount = count($parts);
120 83
121
        if ($partCount === 4) {
122 83
            /** 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
        } elseif ($partCount === 3) {
130 83
            /** 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
        } elseif ($partCount === 2) {
138 83
            /** 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
            $resolvedName->schemaName($this->defaultSchema);
148 83
            $resolvedName->name($parts[0]);
149 83
            $resolvedName->fullName($resolvedName->getName());
150 83
        }
151
152
        return $resolvedName;
153 83
    }
154
155
    /**
156
     * Splits full table name into parts.
157
     *
158
     * @param string $name
159
     *
160
     * @return array
161
     */
162
    protected function getTableNameParts(string $name): array
163 158
    {
164
        $parts = [$name];
165 158
166
        preg_match_all('/([^.\[\]]+)|\[([^\[\]]+)]/', $name, $matches);
167 158
168
        if (isset($matches[0]) && !empty($matches[0])) {
169 158
            $parts = $matches[0];
170 158
        }
171
172
        return str_replace(['[', ']'], '', $parts);
173 158
    }
174
175 158
    /**
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
    protected function findSchemaNames(): array
188
    {
189
        static $sql = <<<'SQL'
190 1
SELECT [s].[name]
191
FROM [sys].[schemas] AS [s]
192 1
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
        return $this->getDb()->createCommand($sql)->queryColumn();
198
    }
199
200 1
    /**
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
    protected function findTableNames(string $schema = ''): array
213
    {
214
        if ($schema === '') {
215 5
            $schema = $this->defaultSchema;
216
        }
217 5
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 5
ORDER BY [t].[table_name]
223
SQL;
224
225
        $tables = $this->getDb()->createCommand($sql, [':schema' => $schema])->queryColumn();
226
227
        $tables = array_map(static function ($item) {
228 5
            return '[' . $item . ']';
229
        }, $tables);
230 5
231 5
        return $tables;
232 5
    }
233
234 5
    /**
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
    protected function loadTableSchema(string $name): ?TableSchema
244
    {
245
        $table = new TableSchema();
246 91
247
        $this->resolveTableNames($table, $name);
248 91
        $this->findPrimaryKeys($table);
249
250 91
        if ($this->findColumns($table)) {
251 91
            $this->findForeignKeys($table);
252
253 91
            return $table;
254 85
        }
255
256 85
        return null;
257
    }
258
259 15
    /**
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
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
269
    {
270
        return $this->loadTableConstraints($tableName, 'primaryKey');
271 30
    }
272
273 30
    /**
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
    protected function loadTableForeignKeys(string $tableName): array
283
    {
284
        return $this->loadTableConstraints($tableName, 'foreignKeys');
285 4
    }
286
287 4
    /**
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
    protected function loadTableIndexes(string $tableName): array
297
    {
298
        static $sql = <<<'SQL'
299 28
SELECT
300
    [i].[name] AS [name],
301 28
    [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
        $resolvedName = $this->resolveTableName($tableName);
314
        $indexes = $this->getDb()->createCommand($sql, [':fullName' => $resolvedName->getFullName()])->queryAll();
315
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
316 28
        $indexes = ArrayHelper::index($indexes, null, 'name');
317 28
318 28
        $result = [];
319 28
320
        foreach ($indexes as $name => $index) {
321 28
            $result[] = (new IndexConstraint())
322
                ->primary((bool) $index[0]['index_is_primary'])
323 28
                ->unique((bool) $index[0]['index_is_unique'])
324 25
                ->columnNames(ArrayHelper::getColumn($index, 'column_name'))
325 25
                ->name($name);
326 25
        }
327 25
328 25
        return $result;
329
    }
330
331 28
    /**
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
    protected function loadTableUniques(string $tableName): array
341
    {
342
        return $this->loadTableConstraints($tableName, 'uniques');
343 13
    }
344
345 13
    /**
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
    protected function loadTableChecks(string $tableName): array
355
    {
356
        return $this->loadTableConstraints($tableName, 'checks');
357 13
    }
358
359 13
    /**
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
    protected function loadTableDefaultValues(string $tableName): array
369
    {
370
        return $this->loadTableConstraints($tableName, 'defaults');
371 13
    }
372
373 13
    /**
374
     * Creates a new savepoint.
375
     *
376
     * @param string $name the savepoint name.
377
     *
378
     * @throws Exception|InvalidConfigException|Throwable
379
     */
380
    public function createSavepoint(string $name): void
381
    {
382
        $this->getDb()->createCommand("SAVE TRANSACTION $name")->execute();
383 1
    }
384
385 1
    /**
386 1
     * 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
    public function rollBackSavepoint(string $name): void
403
    {
404
        $this->getDb()->createCommand("ROLLBACK TRANSACTION $name")->execute();
405 1
    }
406
407 1
    /**
408 1
     * 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
    protected function createColumnSchema(): ColumnSchema
415
    {
416
        return new ColumnSchema();
417 85
    }
418
419 85
    /**
420
     * Creates a query builder for the MSSQL database.
421
     *
422
     * @return QueryBuilder query builder interface.
423
     */
424
    public function createQueryBuilder(): QueryBuilder
425
    {
426
        return new QueryBuilder($this->getDb());
427 60
    }
428
429 60
    /**
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
    protected function resolveTableNames(TableSchema $table, string $name): void
436
    {
437
        $parts = $this->getTableNameParts($name);
438 91
439
        $partCount = count($parts);
440 91
441
        if ($partCount === 4) {
442 91
            /** server name, catalog name, schema name and table name passed */
443
            $table->catalogName($parts[1]);
444 91
            $table->schemaName($parts[2]);
445
            $table->name($parts[3]);
446
            $table->fullName($table->getCatalogName() . '.' . $table->getSchemaName() . '.' . $table->getName());
447
        } elseif ($partCount === 3) {
448
            /** catalog name, schema name and table name passed */
449
            $table->catalogName($parts[0]);
450 91
            $table->schemaName($parts[1]);
451
            $table->name($parts[2]);
452
            $table->fullName($table->getCatalogName() . '.' . $table->getSchemaName() . '.' . $table->getName());
453
        } elseif ($partCount === 2) {
454
            /** only schema name and table name passed */
455
            $table->schemaName($parts[0]);
456 91
            $table->name($parts[1]);
457
            $table->fullName(
458 3
                $table->getSchemaName() !== $this->defaultSchema
459 3
                ? $table->getSchemaName() . '.' . $table->getName() : $table->getName()
460 3
            );
461 3
        } else {
462 3
            /** only table name passed */
463
            $table->schemaName($this->defaultSchema);
464
            $table->name($parts[0]);
465
            $table->fullName($table->getName());
466 88
        }
467 88
    }
468 88
469
    /**
470 91
     * 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
    protected function loadColumnSchema(array $info): ColumnSchema
477
    {
478
        $column = $this->createColumnSchema();
479 85
480
        $column->name($info['column_name']);
481 85
        $column->allowNull($info['is_nullable'] === 'YES');
482
        $column->dbType($info['data_type']);
483 85
        $column->enumValues([]); // mssql has only vague equivalents to enum
484 85
        $column->primaryKey(false); // primary key will be determined in findColumns() method
485 85
        $column->autoIncrement($info['is_identity'] === 1);
486 85
        $column->unsigned(stripos($column->getDbType(), 'unsigned') !== false);
487 85
        $column->comment($info['comment'] ?? '');
488 85
        $column->type(self::TYPE_STRING);
489 85
490 85
        if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $column->getDbType(), $matches)) {
491 85
            $type = $matches[1];
492
493 85
            if (isset($this->typeMap[$type])) {
494 85
                $column->type($this->typeMap[$type]);
495
            }
496 85
497 85
            if (!empty($matches[2])) {
498
                $values = explode(',', $matches[2]);
499
                $column->precision((int) $values[0]);
500 85
                $column->size((int) $values[0]);
501 75
502 75
                if (isset($values[1])) {
503 75
                    $column->scale((int) $values[1]);
504
                }
505 75
506
                if ($column->getSize() === 1 && ($type === 'tinyint' || $type === 'bit')) {
507
                    $column->type('boolean');
508
                } elseif ($type === 'bit') {
509 75
                    if ($column->getSize() > 32) {
510
                        $column->type('bigint');
511 75
                    } elseif ($column->getSize() === 32) {
512
                        $column->type('integer');
513
                    }
514
                }
515
            }
516
        }
517
518
        $column->phpType($this->getColumnPhpType($column));
519
520
        if ($info['column_default'] === '(NULL)') {
521 85
            $info['column_default'] = null;
522
        }
523 85
524 4
        if (!$column->isPrimaryKey() && ($column->getType() !== 'timestamp' || $info['column_default'] !== 'CURRENT_TIMESTAMP')) {
525
            $column->defaultValue($column->defaultPhpTypecast($info['column_default']));
526
        }
527 85
528 85
        return $column;
529
    }
530
531 85
    /**
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
    protected function findColumns(TableSchema $table): bool
541
    {
542
        $columnsTableName = 'INFORMATION_SCHEMA.COLUMNS';
543 91
        $whereSql = '[t1].[table_name] = ' . $this->getDb()->quoteValue($table->getName());
544
545 91
        if ($table->getCatalogName() !== null) {
546 91
            $columnsTableName = "{$table->getCatalogName()}.{$columnsTableName}";
547
            $whereSql .= " AND [t1].[table_catalog] = '{$table->getCatalogName()}'";
548 91
        }
549
550
        if ($table->getSchemaName() !== null) {
551
            $whereSql .= " AND [t1].[table_schema] = '{$table->getSchemaName()}'";
552
        }
553 91
554 91
        $columnsTableName = $this->quoteTableName($columnsTableName);
555
556
        $sql = <<<SQL
557 91
SELECT
558
 [t1].[column_name],
559
 [t1].[is_nullable],
560 91
 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
FROM {$columnsTableName} AS [t1]
582
WHERE {$whereSql}
583
SQL;
584 91
585 91
        try {
586
            $columns = $this->getDb()->createCommand($sql)->queryAll();
587
588
            if (empty($columns)) {
589 91
                return false;
590
            }
591 91
        } catch (Exception $e) {
592 91
            return false;
593
        }
594
595
        foreach ($columns as $column) {
596
            $column = $this->loadColumnSchema($column);
597
            foreach ($table->getPrimaryKey() as $primaryKey) {
598 85
                if (strcasecmp($column->getName(), $primaryKey) === 0) {
599 85
                    $column->primaryKey(true);
600 85
                    break;
601 57
                }
602 57
            }
603 57
604
            if ($column->isPrimaryKey() && $column->isAutoIncrement()) {
605
                $table->sequenceName('');
606
            }
607 85
608
            $table->columns($column->getName(), $column);
609
        }
610
611 85
        return true;
612
    }
613
614 85
    /**
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
    protected function findTableConstraints(TableSchema $table, string $type): array
625
    {
626
        $keyColumnUsageTableName = 'INFORMATION_SCHEMA.KEY_COLUMN_USAGE';
627 91
        $tableConstraintsTableName = 'INFORMATION_SCHEMA.TABLE_CONSTRAINTS';
628
629 91
        if ($table->getCatalogName() !== null) {
630 91
            $keyColumnUsageTableName = $table->getCatalogName() . '.' . $keyColumnUsageTableName;
631
            $tableConstraintsTableName = $table->getCatalogName() . '.' . $tableConstraintsTableName;
632 91
        }
633
634
        $keyColumnUsageTableName = $this->quoteTableName($keyColumnUsageTableName);
635
        $tableConstraintsTableName = $this->quoteTableName($tableConstraintsTableName);
636
637 91
        $sql = <<<SQL
638 91
SELECT
639
    [kcu].[constraint_name] AS [index_name],
640
    [kcu].[column_name] AS [field_name]
641 91
FROM {$keyColumnUsageTableName} AS [kcu]
642
LEFT JOIN {$tableConstraintsTableName} AS [tc] ON
643
    [kcu].[table_schema] = [tc].[table_schema] AND
644 91
    [kcu].[table_name] = [tc].[table_name] AND
645 91
    [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
        return $this->getDb()->createCommand(
653
            $sql,
654
            [
655 91
                ':tableName' => $table->getName(),
656
                ':schemaName' => $table->getSchemaName(),
657
                ':type' => $type,
658 91
            ]
659 91
        )->queryAll();
660 91
    }
661
662 91
    /**
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
    protected function findPrimaryKeys(TableSchema $table): void
670
    {
671
        foreach ($this->findTableConstraints($table, 'PRIMARY KEY') as $row) {
672 91
            $table->primaryKey($row['field_name']);
673
        }
674 91
    }
675 57
676
    /**
677 91
     * 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
    protected function findForeignKeys(TableSchema $table): void
684
    {
685
        $object = $table->getName();
686 85
687
        if ($table->getSchemaName() !== null) {
688 85
            $object = $table->getSchemaName() . '.' . $object;
689
        }
690 85
691 85
        if ($table->getCatalogName() !== null) {
692
            $object = $table->getCatalogName() . '.' . $object;
693
        }
694 85
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
        $sql = <<<'SQL'
700
SELECT
701
	[fk].[name] AS [fk_name],
702
	[cp].[name] AS [fk_column_name],
703 85
	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
        $rows = $this->getDb()->createCommand($sql, [':object' => $object])->queryAll();
720
721
        $table->foreignKeys([]);
722 85
723
        foreach ($rows as $row) {
724 85
            if (!isset($table->getForeignKeys()[$row['fk_name']])) {
725
                $fk[$row['fk_name']][] = $row['uq_table_name'];
726 85
                $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 5
            }
728 5
729 5
            $fk[$row['fk_name']][$row['fk_column_name']] = $row['uq_column_name'];
730
            $table->foreignKeys($fk);
731
        }
732 5
    }
733 5
734
    /**
735 85
     * 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
    private function loadTableConstraints(string $tableName, string $returnType)
809
    {
810
        static $sql = <<<'SQL'
811 73
SELECT
812
    [o].[name] AS [name],
813 73
    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
        $resolvedName = $this->resolveTableName($tableName);
851
        $constraints = $this->getDb()->createCommand($sql, [':fullName' => $resolvedName->getFullName()])->queryAll();
852
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
853 73
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
854 73
        $result = [
855 73
            'primaryKey' => null,
856 73
            'foreignKeys' => [],
857
            'uniques' => [],
858 73
            'checks' => [],
859
            'defaults' => [],
860
        ];
861
862
        foreach ($constraints as $type => $names) {
863
            foreach ($names as $name => $constraint) {
864
                switch ($type) {
865 73
                    case 'PK':
866 73
                        /** @var Constraint */
867
                        $result['primaryKey'] = (new Constraint())
868 73
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
869
                            ->name($name);
870 53
                        break;
871 53
                    case 'F':
872 53
                        $result['foreignKeys'][] = (new ForeignKeyConstraint())
873 53
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
874 73
                            ->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 16
                    case 'UQ':
882 16
                        $result['uniques'][] = (new Constraint())
883 16
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
884 58
                            ->name($name);
885 56
                        break;
886 56
                    case 'C':
887 56
                        $result['checks'][] = (new CheckConstraint())
888 56
                            ->expression($constraint[0]['check_expr'])
889 31
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
890 13
                            ->name($name);
891 13
                        break;
892 13
                    case 'D':
893 13
                        $result['defaults'][] = (new DefaultValueConstraint())
894 13
                            ->value($constraint[0]['default_expr'])
895 30
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
896 30
                            ->name($name);
897 30
                        break;
898 30
                }
899 30
            }
900 30
        }
901
        foreach ($result as $type => $data) {
902
            $this->setTableMetadata($tableName, $type, $data);
903
        }
904 73
905 73
        return $result[$returnType];
906
    }
907
908 73
    /**
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
    public function quoteColumnName(string $name): string
921
    {
922
        if (preg_match('/^\[.*]$/', $name)) {
923 194
            return $name;
924
        }
925 194
926 26
        return parent::quoteColumnName($name);
927
    }
928
929 194
    /**
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
    public function insert(string $table, array $columns)
940
    {
941
        $command = $this->getDb()->createCommand()->insert($table, $columns);
942
        if (!$command->execute()) {
943
            return false;
944
        }
945
946
        $isVersion2005orLater = version_compare($this->getDb()->getSchema()->getServerVersion(), '9', '>=');
947
        $inserted = $isVersion2005orLater ? $command->getPdoStatement()->fetch() : [];
948
949
        $tableSchema = $this->getTableSchema($table);
950
951
        $result = [];
952
        foreach ($tableSchema->getPrimaryKey() as $name) {
953
            /**
954
             * {@see https://github.com/yiisoft/yii2/issues/13828 & https://github.com/yiisoft/yii2/issues/17474
955
             */
956
            if (isset($inserted[$name])) {
957
                $result[$name] = $inserted[$name];
958
            } elseif (isset($columns[$name])) {
959
                $result[$name] = $columns[$name];
960
            } else {
961
                $result[$name] = $tableSchema->getColumns()[$name]->getDefaultValue();
962
            }
963
        }
964
965
        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
    public function createColumnSchemaBuilder(string $type, $length = null): ColumnSchemaBuilder
979
    {
980
        return new ColumnSchemaBuilder($type, $length, $this->getDb());
981
    }
982
}
983