Passed
Push — master ( a14138...77ef53 )
by Alexander
08:08
created

Schema::loadColumnSchema()   C

Complexity

Conditions 15
Paths 92

Size

Total Lines 53
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 20.247

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

990
                $result[$name] = $this->getLastInsertID(/** @scrutinizer ignore-type */ $tableSchema->getSequenceName());
Loading history...
991
            } elseif (isset($columns[$name])) {
992
                $result[$name] = $columns[$name];
993
            } else {
994
                $result[$name] = $tableSchema->getColumns()[$name]->getDefaultValue();
995
            }
996
        }
997
998
        return $result;
999
    }
1000
1001
    /**
1002
     * Create a column schema builder instance giving the type and value precision.
1003
     *
1004
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
1005
     *
1006
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
1007
     * @param array|int|string|null $length length or precision of the column. See {@see ColumnSchemaBuilder::$length}.
1008
     *
1009
     * @return ColumnSchemaBuilder column schema builder instance
1010
     */
1011
    public function createColumnSchemaBuilder(string $type, $length = null): ColumnSchemaBuilder
1012
    {
1013
        return new ColumnSchemaBuilder($type, $length, $this->getDb());
1014
    }
1015
}
1016