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

Schema::createColumnSchemaBuilder()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

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

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