Test Failed
Pull Request — master (#110)
by Def
17:15 queued 04:16
created

Schema::findForeignKeys()   A

Complexity

Conditions 5
Paths 12

Size

Total Lines 52
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 5.0488

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 31
c 1
b 0
f 0
nc 12
nop 1
dl 0
loc 52
ccs 14
cts 16
cp 0.875
crap 5.0488
rs 9.1128

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Mssql;
6
7
use Throwable;
8
use Yiisoft\Arrays\ArrayHelper;
9
use Yiisoft\Db\Cache\SchemaCache;
10
use Yiisoft\Db\Connection\ConnectionInterface;
11
use Yiisoft\Db\Constraint\CheckConstraint;
12
use Yiisoft\Db\Constraint\Constraint;
13
use Yiisoft\Db\Constraint\DefaultValueConstraint;
14
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
15
use Yiisoft\Db\Constraint\IndexConstraint;
16
use Yiisoft\Db\Exception\Exception;
17
use Yiisoft\Db\Exception\InvalidConfigException;
18
use Yiisoft\Db\Schema\ColumnSchemaBuilder;
19
use Yiisoft\Db\Schema\ColumnSchemaInterface;
20
use Yiisoft\Db\Schema\Schema as AbstractSchema;
21
use Yiisoft\Db\Schema\TableNameInterface;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Schema\TableNameInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
22
use Yiisoft\Db\Schema\TableSchemaInterface;
23
24
use function array_change_key_case;
25
use function array_map;
26
use function explode;
27
use function is_array;
28
use function md5;
29
use function preg_match;
30
use function serialize;
31
use function str_replace;
32
use function strcasecmp;
33
use function stripos;
34
35
/**
36
 * Schema is the class for retrieving metadata from MS SQL Server databases (version 2008 and above).
37
 *
38
 * @psalm-type ColumnArray = array{
39
 *   column_name: string,
40
 *   is_nullable: string,
41
 *   data_type: string,
42
 *   column_default: mixed,
43
 *   is_identity: string,
44
 *   comment: null|string
45
 * }
46
 *
47
 * @psalm-type ConstraintArray = array<
48
 *   array-key,
49
 *   array {
50
 *     name: string,
51
 *     column_name: string,
52
 *     type: string,
53
 *     foreign_table_schema: string|null,
54
 *     foreign_table_name: string|null,
55
 *     foreign_column_name: string|null,
56
 *     on_update: string,
57
 *     on_delete: string,
58
 *     check_expr: string,
59
 *     default_expr: string
60
 *   }
61
 * >
62
 */
63
final class Schema extends AbstractSchema
64
{
65
    public const DEFAULTS = 'defaults';
66
67
    /**
68
     * @var string|null the default schema used for the current session.
69
     */
70
    protected ?string $defaultSchema = 'dbo';
71
72
    /**
73
     * @var array mapping from physical column types (keys) to abstract column types (values)
74
     *
75
     * @psalm-var string[]
76
     */
77
    private array $typeMap = [
78
        /** exact numbers */
79
        'bigint' => self::TYPE_BIGINT,
80
        'numeric' => self::TYPE_DECIMAL,
81
        'bit' => self::TYPE_SMALLINT,
82
        'smallint' => self::TYPE_SMALLINT,
83
        'decimal' => self::TYPE_DECIMAL,
84
        'smallmoney' => self::TYPE_MONEY,
85
        'int' => self::TYPE_INTEGER,
86
        'tinyint' => self::TYPE_TINYINT,
87
        'money' => self::TYPE_MONEY,
88
89
        /** approximate numbers */
90
        'float' => self::TYPE_FLOAT,
91
        'double' => self::TYPE_DOUBLE,
92
        'real' => self::TYPE_FLOAT,
93
94
        /** date and time */
95
        'date' => self::TYPE_DATE,
96
        'datetimeoffset' => self::TYPE_DATETIME,
97
        'datetime2' => self::TYPE_DATETIME,
98
        'smalldatetime' => self::TYPE_DATETIME,
99
        'datetime' => self::TYPE_DATETIME,
100
        'time' => self::TYPE_TIME,
101
102
        /** character strings */
103
        'char' => self::TYPE_CHAR,
104
        'varchar' => self::TYPE_STRING,
105
        'text' => self::TYPE_TEXT,
106
107
        /** unicode character strings */
108
        'nchar' => self::TYPE_CHAR,
109
        'nvarchar' => self::TYPE_STRING,
110
        'ntext' => self::TYPE_TEXT,
111
112
        /** binary strings */
113
        'binary' => self::TYPE_BINARY,
114
        'varbinary' => self::TYPE_BINARY,
115
        'image' => self::TYPE_BINARY,
116
117
        /**
118
         * other data types 'cursor' type cannot be used with tables
119
         */
120
        'timestamp' => self::TYPE_TIMESTAMP,
121
        'hierarchyid' => self::TYPE_STRING,
122
        'uniqueidentifier' => self::TYPE_STRING,
123
        'sql_variant' => self::TYPE_STRING,
124
        'xml' => self::TYPE_STRING,
125
        'table' => self::TYPE_STRING,
126
    ];
127
128
    /**
129
     * Resolves the table name and schema name (if any).
130
     *
131 390
     * @param TableNameInterface $name the table name.
132
     *
133 390
     * @return TableSchemaInterface resolved table, schema, etc. names.
134
     *
135
     * @todo also see case `wrongBehaviour` in \Yiisoft\Db\TestSupport\TestCommandTrait::batchInsertSqlProviderTrait
136
     */
137
    protected function resolveTableName(TableNameInterface $name): TableSchemaInterface
138
    {
139
        $resolvedName = new TableSchema();
140
141
        $resolvedName->serverName($name->getServerName());
0 ignored issues
show
Bug introduced by
The method serverName() does not exist on Yiisoft\Db\Mssql\TableSchema. ( Ignorable by Annotation )

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

141
        $resolvedName->/** @scrutinizer ignore-call */ 
142
                       serverName($name->getServerName());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
142
        $resolvedName->catalogName($name->getCatalogName());
143
        $resolvedName->schemaName($name->getSchemaName() ?? $this->defaultSchema);
144
        $resolvedName->name($name->getTableName());
145
        $resolvedName->fullName((string) $name);
146 180
147
        return $resolvedName;
148 180
    }
149 180
150 180
    /**
151
     * Returns all schema names in the database, including the default one but not system schemas.
152 180
     *
153
     * This method should be overridden by child classes in order to support this feature because the default
154
     * implementation simply throws an exception.
155
     *
156
     * @throws Exception|InvalidConfigException|Throwable
157
     *
158
     * @return array All schema names in the database, except system schemas.
159
     *
160
     * @link https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-database-principals-transact-sql
161
     */
162 180
    protected function findSchemaNames(): array
163
    {
164
        $sql = <<<SQL
165
        SELECT [s].[name]
166
        FROM [sys].[schemas] AS [s]
167
        INNER JOIN [sys].[database_principals] AS [p] ON [p].[principal_id] = [s].[principal_id]
168
        WHERE [p].[is_fixed_role] = 0 AND [p].[sid] IS NOT NULL
169
        ORDER BY [s].[name] ASC
170
        SQL;
171
172 180
        return $this->db->createCommand($sql)->queryColumn();
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Mssql\Schema. Did you maybe forget to declare it?
Loading history...
173
    }
174 3
175 3
    /**
176 3
     * Returns all table names in the database.
177
     *
178 3
     * This method should be overridden by child classes in order to support this feature because the default
179 3
     * implementation simply throws an exception.
180 3
     *
181
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
182
     *
183
     * @throws Exception|InvalidConfigException|Throwable
184 177
     *
185 177
     * @return array All table names in the database. The names have NO schema name prefix.
186 177
     */
187
    protected function findTableNames(string $schema = ''): array
188
    {
189 180
        if ($schema === '') {
190
            $schema = $this->defaultSchema;
191
        }
192
193
        $sql = <<<SQL
194
        SELECT [t].[table_name]
195
        FROM [INFORMATION_SCHEMA].[TABLES] AS [t]
196
        WHERE [t].[table_schema] = :schema AND [t].[table_type] IN ('BASE TABLE', 'VIEW')
197
        ORDER BY [t].[table_name]
198
        SQL;
199
200
        $tables = $this->db->createCommand($sql, [':schema' => $schema])->queryColumn();
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Mssql\Schema. Did you maybe forget to declare it?
Loading history...
201 180
202
        return array_map(static fn (string $item): string => '[' . $item . ']', $tables);
203 180
    }
204
205 180
    /**
206
     * Loads the metadata for the specified table.
207 180
     *
208 180
     * @param TableNameInterface $name table name.
209
     *
210
     * @throws Exception|InvalidConfigException|Throwable
211
     *
212 180
     * @return TableSchemaInterface|null DBMS-dependent table metadata, `null` if the table does not exist.
213
     */
214
    protected function loadTableSchema(TableNameInterface $name): ?TableSchemaInterface
215
    {
216
        $table = $this->resolveTableName($name);
217
        $this->findPrimaryKeys($table);
218
219
        if ($this->findColumns($table)) {
220
            $this->findForeignKeys($table);
221
            return $table;
222
        }
223
224
        return null;
225
    }
226
227 1
    /**
228
     * Loads a primary key for the given table.
229 1
     *
230
     * @param TableNameInterface $tableName table name.
231
     *
232
     * @throws Exception|InvalidConfigException|Throwable
233
     *
234
     * @return Constraint|null The primary key for the given table, `null` if the table has no primary key.
235
     */
236
    protected function loadTablePrimaryKey(TableNameInterface $tableName): ?Constraint
237 1
    {
238
        /** @var mixed */
239
        $tablePrimaryKey = $this->loadTableConstraints($tableName, self::PRIMARY_KEY);
240
        return $tablePrimaryKey instanceof Constraint ? $tablePrimaryKey : null;
241
    }
242
243
    /**
244
     * Loads all foreign keys for the given table.
245
     *
246
     * @param TableNameInterface $tableName table name.
247
     *
248
     * @throws Exception|InvalidConfigException|Throwable
249
     *
250
     * @return array The foreign keys for the given table.
251
     */
252 11
    protected function loadTableForeignKeys(TableNameInterface $tableName): array
253
    {
254 11
        /** @var mixed */
255 11
        $tableForeingKeys = $this->loadTableConstraints($tableName, self::FOREIGN_KEYS);
256
        return is_array($tableForeingKeys) ? $tableForeingKeys : [];
257
    }
258 11
259
    /**
260
     * Loads all indexes for the given table.
261
     *
262
     * @param TableNameInterface $tableName table name.
263
     *
264
     * @throws Exception|InvalidConfigException|Throwable
265 11
     *
266
     * @return array indexes for the given table.
267 11
     */
268
    protected function loadTableIndexes(TableNameInterface $tableName): array
269
    {
270
        $sql = <<<SQL
271
        SELECT
272
            [i].[name] AS [name],
273
            [iccol].[name] AS [column_name],
274
            [i].[is_unique] AS [index_is_unique],
275
            [i].[is_primary_key] AS [index_is_primary]
276
        FROM [sys].[indexes] AS [i]
277
        INNER JOIN [sys].[index_columns] AS [ic]
278
            ON [ic].[object_id] = [i].[object_id] AND [ic].[index_id] = [i].[index_id]
279 114
        INNER JOIN [sys].[columns] AS [iccol]
280
            ON [iccol].[object_id] = [ic].[object_id] AND [iccol].[column_id] = [ic].[column_id]
281 114
        WHERE [i].[object_id] = OBJECT_ID(:fullName)
282 114
        ORDER BY [ic].[key_ordinal] ASC
283
        SQL;
284 114
285 105
        $resolvedName = $this->resolveTableName($tableName);
286 105
        $indexes = $this->db->createCommand($sql, [':fullName' => $resolvedName->getFullName()])->queryAll();
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Mssql\Schema. Did you maybe forget to declare it?
Loading history...
287
288
        /** @psalm-var array[] $indexes */
289 19
        $indexes = $this->normalizeRowKeyCase($indexes, true);
290
        $indexes = ArrayHelper::index($indexes, null, 'name');
291
292
        $result = [];
293
294
        /**
295
         * @psalm-var array<
296
         *   string,
297
         *   array<
298
         *     array-key,
299
         *     array{array-key, name: string, column_name: string, index_is_unique: string, index_is_primary: string}
300
         *   >
301 32
         * > $indexes
302
         */
303
        foreach ($indexes as $name => $index) {
304 32
            $result[] = (new IndexConstraint())
305 32
                ->primary((bool) $index[0]['index_is_primary'])
306
                ->unique((bool) $index[0]['index_is_unique'])
307
                ->columnNames(ArrayHelper::getColumn($index, 'column_name'))
308
                ->name($name);
309
        }
310
311
        return $result;
312
    }
313
314
    /**
315
     * Loads all unique constraints for the given table.
316
     *
317 5
     * @param TableNameInterface $tableName table name.
318
     *
319
     * @throws Exception|InvalidConfigException|Throwable
320 5
     *
321 5
     * @return array The unique constraints for the given table.
322
     */
323
    protected function loadTableUniques(TableNameInterface $tableName): array
324
    {
325
        /** @var mixed */
326
        $tableUniques = $this->loadTableConstraints($tableName, self::UNIQUES);
327
        return is_array($tableUniques) ? $tableUniques : [];
328
    }
329
330
    /**
331
     * Loads all check constraints for the given table.
332
     *
333 30
     * @param TableNameInterface $tableName table name.
334
     *
335 30
     * @throws Exception|InvalidConfigException|Throwable
336
     *
337
     * @return array The check constraints for the given table.
338
     */
339
    protected function loadTableChecks(TableNameInterface $tableName): array
340
    {
341
        /** @var mixed */
342
        $tableCheck = $this->loadTableConstraints($tableName, self::CHECKS);
343
        return is_array($tableCheck) ? $tableCheck : [];
344
    }
345
346
    /**
347
     * Loads all default value constraints for the given table.
348
     *
349
     * @param TableNameInterface $tableName table name.
350 30
     *
351 30
     * @throws Exception|InvalidConfigException|Throwable
352
     *
353
     * @return array The default value constraints for the given table.
354 30
     */
355 30
    protected function loadTableDefaultValues(TableNameInterface $tableName): array
356
    {
357 30
        /** @var mixed */
358
        $tableDefault = $this->loadTableConstraints($tableName, self::DEFAULTS);
359
        return is_array($tableDefault) ? $tableDefault : [];
360
    }
361
362
    /**
363
     * Creates a column schema for the database.
364
     *
365
     * This method may be overridden by child classes to create a DBMS-specific column schema.
366
     *
367
     * @return ColumnSchema column schema instance.
368 30
     */
369 27
    protected function createColumnSchema(): ColumnSchema
370 27
    {
371 27
        return new ColumnSchema();
372 27
    }
373 27
374
    /**
375
     * Loads the column information into a {@see ColumnSchemaInterface} object.
376 30
     *
377
     * @psalm-param ColumnArray $info The column information.
378
     *
379
     * @return ColumnSchemaInterface the column schema object.
380
     */
381
    protected function loadColumnSchema(array $info): ColumnSchemaInterface
382
    {
383
        $column = $this->createColumnSchema();
384
385
        $column->name($info['column_name']);
386
        $column->allowNull($info['is_nullable'] === 'YES');
387
        $column->dbType($info['data_type']);
388 14
        $column->enumValues([]); // mssql has only vague equivalents to enum
389
        $column->primaryKey(false); // primary key will be determined in findColumns() method
390
        $column->autoIncrement($info['is_identity'] === '1');
391 14
        $column->unsigned(stripos($column->getDbType(), 'unsigned') !== false);
392 14
        $column->comment($info['comment'] ?? '');
393
        $column->type(self::TYPE_STRING);
394
395
        if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $column->getDbType(), $matches)) {
396
            $type = $matches[1];
397
398
            if (isset($this->typeMap[$type])) {
399
                $column->type($this->typeMap[$type]);
400
            }
401
402
            if (!empty($matches[2])) {
403
                $values = explode(',', $matches[2]);
404 14
                $column->precision((int) $values[0]);
405
                $column->size((int) $values[0]);
406
407 14
                if (isset($values[1])) {
408 14
                    $column->scale((int) $values[1]);
409
                }
410
411
                if ($column->getSize() === 1 && ($type === 'tinyint' || $type === 'bit')) {
412
                    $column->type(self::TYPE_BOOLEAN);
413
                } elseif ($type === 'bit') {
414
                    if ($column->getSize() > 32) {
415
                        $column->type(self::TYPE_BIGINT);
416
                    } elseif ($column->getSize() === 32) {
417
                        $column->type(self::TYPE_INTEGER);
418
                    }
419
                }
420 14
            }
421
        }
422
423 14
        $column->phpType($this->getColumnPhpType($column));
424 14
425
        if ($info['column_default'] === '(NULL)') {
426
            $info['column_default'] = null;
427
        }
428
429
        if (!$column->isPrimaryKey() && ($column->getType() !== 'timestamp' || $info['column_default'] !== 'CURRENT_TIMESTAMP')) {
430
            $column->defaultValue($column->defaultPhpTypecast($info['column_default']));
431
        }
432
433
        return $column;
434 105
    }
435
436 105
    /**
437
     * Collects the metadata of table columns.
438
     *
439
     * @param TableSchemaInterface $table the table metadata.
440
     *
441
     * @throws Throwable
442
     *
443
     * @return bool whether the table exists in the database.
444
     */
445
    protected function findColumns(TableSchemaInterface $table): bool
446 105
    {
447
        $columnsTableName = 'INFORMATION_SCHEMA.COLUMNS';
448 105
        $whereSql = '[t1].[table_name] = ' . (string) $this->db->getQuoter()->quoteValue($table->getName());
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Mssql\Schema. Did you maybe forget to declare it?
Loading history...
449
450 105
        if ($table->getCatalogName() !== null) {
451 105
            $columnsTableName = "{$table->getCatalogName()}.$columnsTableName";
452 105
            $whereSql .= " AND [t1].[table_catalog] = '{$table->getCatalogName()}'";
453 105
        }
454 105
455 105
        if ($table->getSchemaName() !== null) {
456 105
            $whereSql .= " AND [t1].[table_schema] = '{$table->getSchemaName()}'";
457 105
        }
458 105
459
        $columnsTableName = $this->db->getQuoter()->quoteTableName($columnsTableName);
460 105
461 105
        $sql = <<<SQL
462
        SELECT
463 105
            [t1].[column_name],
464 105
            [t1].[is_nullable],
465
        CASE WHEN [t1].[data_type] IN ('char','varchar','nchar','nvarchar','binary','varbinary') THEN
466
        CASE WHEN [t1].[character_maximum_length] = NULL OR [t1].[character_maximum_length] = -1 THEN
467 105
            [t1].[data_type]
468 88
        ELSE
469 88
            [t1].[data_type] + '(' + LTRIM(RTRIM(CONVERT(CHAR,[t1].[character_maximum_length]))) + ')'
470 88
        END
471
        ELSE
472 88
            [t1].[data_type]
473
        END AS 'data_type',
474
        [t1].[column_default],
475
        COLUMNPROPERTY(OBJECT_ID([t1].[table_schema] + '.' + [t1].[table_name]), [t1].[column_name], 'IsIdentity') AS is_identity,
476 88
        (
477
        SELECT CONVERT(VARCHAR, [t2].[value])
478 88
        FROM [sys].[extended_properties] AS [t2]
479
        WHERE
480
        [t2].[class] = 1 AND
481
        [t2].[class_desc] = 'OBJECT_OR_COLUMN' AND
482
        [t2].[name] = 'MS_Description' AND
483
        [t2].[major_id] = OBJECT_ID([t1].[TABLE_SCHEMA] + '.' + [t1].[table_name]) AND
484
        [t2].[minor_id] = COLUMNPROPERTY(OBJECT_ID([t1].[TABLE_SCHEMA] + '.' + [t1].[TABLE_NAME]), [t1].[COLUMN_NAME], 'ColumnID')
485
        ) as comment
486
        FROM $columnsTableName AS [t1]
487
        WHERE $whereSql
488 105
        SQL;
489
490 105
        try {
491 4
            /** @psalm-var ColumnArray[] */
492
            $columns = $this->db->createCommand($sql)->queryAll();
493
494 105
            if (empty($columns)) {
495 105
                return false;
496
            }
497
        } catch (Exception) {
498 105
            return false;
499
        }
500
501
        foreach ($columns as $column) {
502
            $column = $this->loadColumnSchema($column);
503
            foreach ($table->getPrimaryKey() as $primaryKey) {
504
                if (strcasecmp($column->getName(), $primaryKey) === 0) {
505
                    $column->primaryKey(true);
506
                    break;
507
                }
508
            }
509
510 114
            if ($column->isPrimaryKey() && $column->isAutoIncrement()) {
511
                $table->sequenceName('');
512 114
            }
513 114
514
            $table->columns($column->getName(), $column);
515 114
        }
516
517
        return true;
518
    }
519
520 114
    /**
521 114
     * Collects the constraint details for the given table and constraint type.
522
     *
523
     * @param TableSchemaInterface $table
524 114
     * @param string $type either PRIMARY KEY or UNIQUE.
525
     *
526 114
     * @throws Exception|InvalidConfigException|Throwable
527
     *
528
     * @return array each entry contains index_name and field_name.
529
     */
530
    protected function findTableConstraints(TableSchemaInterface $table, string $type): array
531
    {
532
        $keyColumnUsageTableName = 'INFORMATION_SCHEMA.KEY_COLUMN_USAGE';
533
        $tableConstraintsTableName = 'INFORMATION_SCHEMA.TABLE_CONSTRAINTS';
534
535
        $catalogName = $table->getCatalogName();
536
        if ($catalogName !== null) {
537
            $keyColumnUsageTableName = $catalogName . '.' . $keyColumnUsageTableName;
538
            $tableConstraintsTableName = $catalogName . '.' . $tableConstraintsTableName;
539
        }
540
541
        $keyColumnUsageTableName = $this->db->getQuoter()->quoteTableName($keyColumnUsageTableName);
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Mssql\Schema. Did you maybe forget to declare it?
Loading history...
542
        $tableConstraintsTableName = $this->db->getQuoter()->quoteTableName($tableConstraintsTableName);
543
544
        $sql = <<<SQL
545
        SELECT
546
            [kcu].[constraint_name] AS [index_name],
547
            [kcu].[column_name] AS [field_name]
548
        FROM $keyColumnUsageTableName AS [kcu]
549
        LEFT JOIN $tableConstraintsTableName AS [tc] ON
550
            [kcu].[table_schema] = [tc].[table_schema] AND
551
            [kcu].[table_name] = [tc].[table_name] AND
552
            [kcu].[constraint_name] = [tc].[constraint_name]
553
        WHERE
554
            [tc].[constraint_type] = :type AND
555
            [kcu].[table_name] = :tableName AND
556
            [kcu].[table_schema] = :schemaName
557 114
        SQL;
558
559 114
        return $this->db->createCommand(
560 114
            $sql,
561
            [
562
                ':tableName' => $table->getName(),
563
                ':schemaName' => $table->getSchemaName(),
564
                ':type' => $type,
565
            ]
566 105
        )->queryAll();
567 105
    }
568 105
569 69
    /**
570 69
     * Collects the primary key column details for the given table.
571 69
     *
572
     * @param TableSchemaInterface $table the table metadata
573
     *
574
     * @throws Exception|InvalidConfigException|Throwable
575 105
     */
576 68
    protected function findPrimaryKeys(TableSchemaInterface $table): void
577
    {
578
        /** @psalm-var array<array-key, array{index_name: string, field_name: string}> $primaryKeys */
579 105
        $primaryKeys = $this->findTableConstraints($table, 'PRIMARY KEY');
580
581
        foreach ($primaryKeys as $row) {
582 105
            $table->primaryKey($row['field_name']);
583
        }
584
    }
585
586
    /**
587
     * Collects the foreign key column details for the given table.
588
     *
589
     * @param TableSchemaInterface $table the table metadata
590
     *
591
     * @throws Exception|InvalidConfigException|Throwable
592
     */
593
    protected function findForeignKeys(TableSchemaInterface $table): void
594
    {
595 114
        $catalogName = $table->getCatalogName();
596
        $fk = [];
597 114
        $object = $table->getName();
598 114
        $schemaName = $table->getSchemaName();
599
600 114
        if ($schemaName !== null) {
601 114
            $object = $schemaName . '.' . $object;
602
        }
603
604
        if ($catalogName !== null) {
605
            $object = $catalogName . '.' . $object;
606 114
        }
607 114
608
        /**
609 114
         * Please refer to the following page for more details:
610
         * {@see http://msdn2.microsoft.com/en-us/library/aa175805(SQL.80).aspx}
611
         */
612
        $sql = <<<SQL
613
        SELECT
614
        [fk].[name] AS [fk_name],
615
        [cp].[name] AS [fk_column_name],
616
        OBJECT_NAME([fk].[referenced_object_id]) AS [uq_table_name],
617
        [cr].[name] AS [uq_column_name]
618
        FROM [sys].[foreign_keys] AS [fk]
619
        INNER JOIN [sys].[foreign_key_columns] AS [fkc]
620
            ON [fk].[object_id] = [fkc].[constraint_object_id]
621
        INNER JOIN [sys].[columns] AS [cp]
622
            ON [fk].[parent_object_id] = [cp].[object_id] AND [fkc].[parent_column_id] = [cp].[column_id]
623
        INNER JOIN [sys].[columns] AS [cr]
624 114
            ON [fk].[referenced_object_id] = [cr].[object_id] AND [fkc].[referenced_column_id] = [cr].[column_id]
625
        WHERE [fk].[parent_object_id] = OBJECT_ID(:object)
626
        SQL;
627 114
628 114
        /**
629
         * @psalm-var array<
630
         *   array-key,
631 114
         *   array{fk_name: string, fk_column_name: string, uq_table_name: string, uq_column_name: string}
632
         * > $rows
633
         */
634
        $rows = $this->db->createCommand($sql, [':object' => $object])->queryAll();
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Mssql\Schema. Did you maybe forget to declare it?
Loading history...
635
        $table->foreignKeys([]);
636
637
        foreach ($rows as $row) {
638
            if (!isset($table->getForeignKeys()[$row['fk_name']])) {
639
                $fk[$row['fk_name']][] = $row['uq_table_name'];
640
                $table->foreignKeys($fk);
641 114
            }
642
643
            $fk[$row['fk_name']][$row['fk_column_name']] = $row['uq_column_name'];
644 114
            $table->foreignKeys($fk);
645
        }
646 114
    }
647 69
648
    /**
649
     * @throws Exception|InvalidConfigException|Throwable
650
     */
651
    protected function findViewNames(string $schema = ''): array
652
    {
653
        if ($schema === '') {
654
            $schema = $this->defaultSchema;
655
        }
656
657
        $sql = <<<SQL
658 105
        SELECT [t].[table_name]
659
        FROM [INFORMATION_SCHEMA].[TABLES] AS [t]
660 105
        WHERE [t].[table_schema] = :schema AND [t].[table_type] = 'VIEW'
661 105
        ORDER BY [t].[table_name]
662 105
        SQL;
663 105
664
        $views = $this->db->createCommand($sql, [':schema' => $schema])->queryColumn();
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Mssql\Schema. Did you maybe forget to declare it?
Loading history...
665 105
666 105
        return array_map(static fn (string $item): string => '[' . $item . ']', $views);
667
    }
668
669 105
    /**
670
     * Returns all unique indexes for the given table.
671
     *
672
     * Each array element is of the following structure:
673
     *
674
     * ```php
675
     * [
676
     *     'IndexName1' => ['col1' [, ...]],
677 105
     *     'IndexName2' => ['col2' [, ...]],
678
     * ]
679
     * ```
680
     *
681
     * @param TableSchemaInterface $table the table metadata.
682
     *
683
     * @throws Exception|InvalidConfigException|Throwable
684
     *
685
     * @return array all unique indexes for the given table.
686
     */
687
    public function findUniqueIndexes(TableSchemaInterface $table): array
688
    {
689
        $result = [];
690
691
        /** @psalm-var array<array-key, array{index_name: string, field_name: string}> $tableUniqueConstraints */
692
        $tableUniqueConstraints = $this->findTableConstraints($table, 'UNIQUE');
693
694
        foreach ($tableUniqueConstraints as $row) {
695
            $result[$row['index_name']][] = $row['field_name'];
696
        }
697
698
        return $result;
699 105
    }
700 105
701
    /**
702 105
     * Loads multiple types of constraints and returns the specified ones.
703 6
     *
704 6
     * @param TableNameInterface $tableName table name.
705 6
     * @param string $returnType return type:
706
     * - primaryKey
707
     * - foreignKeys
708 6
     * - uniques
709 6
     * - checks
710
     * - defaults
711
     *
712
     * @throws Exception|InvalidConfigException|Throwable
713
     *
714
     * @return mixed constraints.
715
     */
716 3
    private function loadTableConstraints(TableNameInterface $tableName, string $returnType): mixed
717
    {
718 3
        $sql = <<<SQL
719 1
        SELECT
720
            [o].[name] AS [name],
721
            COALESCE([ccol].[name], [dcol].[name], [fccol].[name], [kiccol].[name]) AS [column_name],
722 3
            RTRIM([o].[type]) AS [type],
723
            OBJECT_SCHEMA_NAME([f].[referenced_object_id]) AS [foreign_table_schema],
724
            OBJECT_NAME([f].[referenced_object_id]) AS [foreign_table_name],
725
            [ffccol].[name] AS [foreign_column_name],
726
            [f].[update_referential_action_desc] AS [on_update],
727
            [f].[delete_referential_action_desc] AS [on_delete],
728
            [c].[definition] AS [check_expr],
729 3
            [d].[definition] AS [default_expr]
730
        FROM (SELECT OBJECT_ID(:fullName) AS [object_id]) AS [t]
731 3
        INNER JOIN [sys].[objects] AS [o]
732
            ON [o].[parent_object_id] = [t].[object_id] AND [o].[type] IN ('PK', 'UQ', 'C', 'D', 'F')
733
        LEFT JOIN [sys].[check_constraints] AS [c]
734
            ON [c].[object_id] = [o].[object_id]
735
        LEFT JOIN [sys].[columns] AS [ccol]
736
            ON [ccol].[object_id] = [c].[parent_object_id] AND [ccol].[column_id] = [c].[parent_column_id]
737
        LEFT JOIN [sys].[default_constraints] AS [d]
738
            ON [d].[object_id] = [o].[object_id]
739
        LEFT JOIN [sys].[columns] AS [dcol]
740
            ON [dcol].[object_id] = [d].[parent_object_id] AND [dcol].[column_id] = [d].[parent_column_id]
741
        LEFT JOIN [sys].[key_constraints] AS [k]
742
            ON [k].[object_id] = [o].[object_id]
743
        LEFT JOIN [sys].[index_columns] AS [kic]
744
            ON [kic].[object_id] = [k].[parent_object_id] AND [kic].[index_id] = [k].[unique_index_id]
745
        LEFT JOIN [sys].[columns] AS [kiccol]
746
            ON [kiccol].[object_id] = [kic].[object_id] AND [kiccol].[column_id] = [kic].[column_id]
747
        LEFT JOIN [sys].[foreign_keys] AS [f]
748
            ON [f].[object_id] = [o].[object_id]
749
        LEFT JOIN [sys].[foreign_key_columns] AS [fc]
750
            ON [fc].[constraint_object_id] = [o].[object_id]
751
        LEFT JOIN [sys].[columns] AS [fccol]
752 1
            ON [fccol].[object_id] = [fc].[parent_object_id] AND [fccol].[column_id] = [fc].[parent_column_id]
753
        LEFT JOIN [sys].[columns] AS [ffccol]
754 1
            ON [ffccol].[object_id] = [fc].[referenced_object_id] AND [ffccol].[column_id] = [fc].[referenced_column_id]
755
        ORDER BY [kic].[key_ordinal] ASC, [fc].[constraint_column_id] ASC
756
        SQL;
757 1
758
        $resolvedName = $this->resolveTableName($tableName);
759 1
        $constraints = $this->db->createCommand($sql, [':fullName' => $resolvedName->getFullName()])->queryAll();
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Mssql\Schema. Did you maybe forget to declare it?
Loading history...
760 1
761
        /** @psalm-var array[] $constraints */
762
        $constraints = $this->normalizeRowKeyCase($constraints, true);
763 1
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
764
765
        $result = [
766
            self::PRIMARY_KEY => null,
767
            self::FOREIGN_KEYS => [],
768
            self::UNIQUES => [],
769
            self::CHECKS => [],
770
            self::DEFAULTS => [],
771
        ];
772
773
        /** @psalm-var array<array-key, array> $constraints */
774
        foreach ($constraints as $type => $names) {
775
            /**
776
             * @psalm-var object|string|null $name
777
             * @psalm-var ConstraintArray $constraint
778
             */
779
            foreach ($names as $name => $constraint) {
780
                switch ($type) {
781 79
                    case 'PK':
782
                        /** @var Constraint */
783 79
                        $result[self::PRIMARY_KEY] = (new Constraint())
784
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
785
                            ->name($name);
786
                        break;
787
                    case 'F':
788
                        $result[self::FOREIGN_KEYS][] = (new ForeignKeyConstraint())
789
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
790
                            ->foreignTableName($constraint[0]['foreign_table_name'])
791
                            ->foreignColumnNames(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
792
                            ->onDelete(str_replace('_', '', $constraint[0]['on_delete']))
793
                            ->onUpdate(str_replace('_', '', $constraint[0]['on_update']))
794
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
795
                            ->name($name);
796
                        break;
797
                    case 'UQ':
798
                        $result[self::UNIQUES][] = (new Constraint())
799
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
800
                            ->name($name);
801
                        break;
802
                    case 'C':
803
                        $result[self::CHECKS][] = (new CheckConstraint())
804
                            ->expression($constraint[0]['check_expr'])
805
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
806
                            ->name($name);
807
                        break;
808
                    case 'D':
809
                        $result[self::DEFAULTS][] = (new DefaultValueConstraint())
810
                            ->value($constraint[0]['default_expr'])
811
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
812
                            ->name($name);
813
                        break;
814
                }
815
            }
816
        }
817
818
        foreach ($result as $type => $data) {
819
            $this->setTableMetadata($tableName, $type, $data);
820
        }
821
822
        return $result[$returnType];
823 79
    }
824 79
825
    /**
826
     * Create a column schema builder instance giving the type and value precision.
827 79
     *
828 79
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
829
     *
830 79
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
831
     * @param array|int|string|null $length length or precision of the column. See {@see ColumnSchemaBuilder::$length}.
832 79
     *
833 79
     * @return ColumnSchemaBuilder column schema builder instance
834 79
     *
835 79
     * @psalm-param array<array-key, string>|int|null|string $length
836
     */
837
    public function createColumnSchemaBuilder(string $type, array|int|string $length = null): ColumnSchemaBuilder
838
    {
839 79
        return new ColumnSchemaBuilder($type, $length);
840
    }
841
842
    /**
843
     * Returns the actual name of a given table name.
844 79
     *
845 79
     * This method will strip off curly brackets from the given table name and replace the percentage character '%' with
846 79
     * {@see ConnectionInterface::tablePrefix}.
847
     *
848 58
     * @param string|TableNameInterface $name the table name to be converted.
849 58
     *
850 58
     * @return string the real name of the given table name.
851 58
     */
852 79
    public function getRawTableName(string|TableNameInterface $name): string
853 21
    {
854 21
        return (string) $name;
855 21
    }
856 21
857 21
    /**
858 21
     * Returns the cache key for the specified table name.
859 21
     *
860 21
     * @param string $name the table name.
861 21
     *
862 64
     * @return array the cache key.
863 62
     */
864 62
    protected function getCacheKey(string $name): array
865 62
    {
866 62
        return array_merge([__CLASS__], $this->db->getCacheKey(), [$name]);
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Mssql\Schema. Did you maybe forget to declare it?
Loading history...
867 36
    }
868 18
869 18
    /**
870 18
     * Returns the cache tag name.
871 18
     *
872 18
     * This allows {@see refresh()} to invalidate all cached table schemas.
873 35
     *
874 35
     * @return string the cache tag name.
875 35
     */
876 35
    protected function getCacheTag(): string
877 35
    {
878 35
        return md5(serialize(array_merge([__CLASS__], $this->db->getCacheKey())));
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Mssql\Schema. Did you maybe forget to declare it?
Loading history...
879
    }
880
881
    /**
882
     * @return bool whether this DBMS supports [savepoint](http://en.wikipedia.org/wiki/Savepoint).
883 79
     */
884 79
    public function supportsSavepoint(): bool
885
    {
886
        return $this->db->isSavepointEnabled();
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Mssql\Schema. Did you maybe forget to declare it?
Loading history...
887 79
    }
888
889
    /**
890
     * Changes row's array key case to lower.
891
     *
892
     * @param array $row row's array or an array of row's arrays.
893
     * @param bool $multiple whether multiple rows or a single row passed.
894
     *
895
     * @return array normalized row or rows.
896
     */
897
    protected function normalizeRowKeyCase(array $row, bool $multiple): array
898
    {
899
        if ($multiple) {
900
            return array_map(static function (array $row) {
901
                return array_change_key_case($row, CASE_LOWER);
902 3
            }, $row);
903
        }
904 3
905
        return array_change_key_case($row, CASE_LOWER);
906
    }
907
908
    /**
909
     * @inheritDoc
910
     */
911
    public function getLastInsertID(?string $sequenceName = null): string
912
    {
913
        return $this->db->getLastInsertID($sequenceName);
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist on Yiisoft\Db\Mssql\Schema. Did you maybe forget to declare it?
Loading history...
914
    }
915
}
916