Passed
Pull Request — master (#14)
by Wilmer
25:49 queued 10:47
created

MssqlSchema::findViewNames()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 13
nc 2
nop 1
dl 0
loc 19
rs 9.8333
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Mssql\Schema;
6
7
use Yiisoft\Arrays\ArrayHelper;
8
use Yiisoft\Db\Constraint\CheckConstraint;
9
use Yiisoft\Db\Constraint\Constraint;
10
use Yiisoft\Db\Constraint\ConstraintFinderInterface;
11
use Yiisoft\Db\Constraint\ConstraintFinderTrait;
12
use Yiisoft\Db\Constraint\DefaultValueConstraint;
13
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
14
use Yiisoft\Db\Constraint\IndexConstraint;
15
use Yiisoft\Db\Mssql\Query\MssqlQueryBuilder;
16
use Yiisoft\Db\Schema\ColumnSchema;
17
use Yiisoft\Db\Schema\Schema;
18
use Yiisoft\Db\View\ViewFinderTrait;
19
20
/**
21
 * Schema is the class for retrieving metadata from MS SQL Server databases (version 2008 and above).
22
 */
23
final class MssqlSchema extends Schema implements ConstraintFinderInterface
24
{
25
    use ViewFinderTrait;
26
    use ConstraintFinderTrait;
27
28
    /**
29
     * @var string the default schema used for the current session.
30
     */
31
    protected ?string $defaultSchema = 'dbo';
32
33
    /**
34
     * @var array mapping from physical column types (keys) to abstract column types (values)
35
     */
36
    private $typeMap = [
37
        // exact numbers
38
        'bigint' => self::TYPE_BIGINT,
39
        'numeric' => self::TYPE_DECIMAL,
40
        'bit' => self::TYPE_SMALLINT,
41
        'smallint' => self::TYPE_SMALLINT,
42
        'decimal' => self::TYPE_DECIMAL,
43
        'smallmoney' => self::TYPE_MONEY,
44
        'int' => self::TYPE_INTEGER,
45
        'tinyint' => self::TYPE_TINYINT,
46
        'money' => self::TYPE_MONEY,
47
        // approximate numbers
48
        'float' => self::TYPE_FLOAT,
49
        'double' => self::TYPE_DOUBLE,
50
        'real' => self::TYPE_FLOAT,
51
        // date and time
52
        'date' => self::TYPE_DATE,
53
        'datetimeoffset' => self::TYPE_DATETIME,
54
        'datetime2' => self::TYPE_DATETIME,
55
        'smalldatetime' => self::TYPE_DATETIME,
56
        'datetime' => self::TYPE_DATETIME,
57
        'time' => self::TYPE_TIME,
58
        // character strings
59
        'char' => self::TYPE_CHAR,
60
        'varchar' => self::TYPE_STRING,
61
        'text' => self::TYPE_TEXT,
62
        // unicode character strings
63
        'nchar' => self::TYPE_CHAR,
64
        'nvarchar' => self::TYPE_STRING,
65
        'ntext' => self::TYPE_TEXT,
66
        // binary strings
67
        'binary' => self::TYPE_BINARY,
68
        'varbinary' => self::TYPE_BINARY,
69
        'image' => self::TYPE_BINARY,
70
        // other data types
71
        // 'cursor' type cannot be used with tables
72
        'timestamp' => self::TYPE_TIMESTAMP,
73
        'hierarchyid' => self::TYPE_STRING,
74
        'uniqueidentifier' => self::TYPE_STRING,
75
        'sql_variant' => self::TYPE_STRING,
76
        'xml' => self::TYPE_STRING,
77
        'table' => self::TYPE_STRING,
78
    ];
79
80
    protected $tableQuoteCharacter = ['[', ']'];
81
    protected $columnQuoteCharacter = ['[', ']'];
82
83
84
    /**
85
     * Resolves the table name and schema name (if any).
86
     *
87
     * @param string $name the table name
88
     *
89
     * @return MssqlTableSchema resolved table, schema, etc. names.
90
     */
91
    protected function resolveTableName(string $name): MssqlTableSchema
92
    {
93
        $resolvedName = new MssqlTableSchema();
94
95
        $parts = $this->getTableNameParts($name);
96
        $partCount = count($parts);
97
98
        if ($partCount === 4) {
99
            // server name, catalog name, schema name and table name passed
100
            $resolvedName->catalogName($parts[1]);
101
            $resolvedName->schemaName($parts[2]);
102
            $resolvedName->name($parts[3]);
103
            $resolvedName->fullName(
104
                $resolvedName->getCatalogName() . '.' . $resolvedName->getSchemaName() . '.' . $resolvedName->getName()
105
            );
106
        } elseif ($partCount === 3) {
107
            // catalog name, schema name and table name passed
108
            $resolvedName->catalogName($parts[0]);
109
            $resolvedName->schemaName($parts[1]);
110
            $resolvedName->name($parts[2]);
111
            $resolvedName->fullName(
112
                $resolvedName->getCatalogName() . '.' . $resolvedName->getSchemaName() . '.' . $resolvedName->getName()
113
            );
114
        } elseif ($partCount === 2) {
115
            // only schema name and table name passed
116
            $resolvedName->schemaName($parts[0]);
117
            $resolvedName->name($parts[1]);
118
            $resolvedName->fullName(
0 ignored issues
show
Bug introduced by
Are you sure $resolvedName->fullName(...chemaName() . '.' : '') of type void can be used in concatenation? ( Ignorable by Annotation )

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

118
            /** @scrutinizer ignore-type */ $resolvedName->fullName(
Loading history...
Bug introduced by
Are you sure the usage of $resolvedName->fullName(...chemaName() . '.' : '') targeting Yiisoft\Db\Schema\TableSchema::fullName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
119
                $resolvedName->getSchemaName() !== $this->defaultSchema ? $resolvedName->getSchemaName() . '.' : ''
120
            ) . $resolvedName->getName();
121
        } else {
122
            // only table name passed
123
            $resolvedName->schemaName($this->defaultSchema);
124
            $resolvedName->name($parts[0]);
125
            $resolvedName->fullName($resolvedName->getName());
126
        }
127
128
        return $resolvedName;
129
    }
130
131
    protected function getTableNameParts(string $name): array
132
    {
133
        $parts = [$name];
134
        preg_match_all('/([^.\[\]]+)|\[([^\[\]]+)\]/', $name, $matches);
135
136
        if (isset($matches[0]) && is_array($matches[0]) && !empty($matches[0])) {
137
            $parts = $matches[0];
138
        }
139
140
        $parts = str_replace(['[', ']'], '', $parts);
141
142
        return $parts;
143
    }
144
145
    /**
146
     * {@inheritdoc}
147
     * @see https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-database-principals-transact-sql
148
     */
149
    protected function findSchemaNames()
150
    {
151
        static $sql = <<<'SQL'
152
SELECT [s].[name]
153
FROM [sys].[schemas] AS [s]
154
INNER JOIN [sys].[database_principals] AS [p] ON [p].[principal_id] = [s].[principal_id]
155
WHERE [p].[is_fixed_role] = 0 AND [p].[sid] IS NOT NULL
156
ORDER BY [s].[name] ASC
157
SQL;
158
159
        return $this->getDb()->createCommand($sql)->queryColumn();
160
    }
161
162
    protected function findTableNames(string $schema = ''): array
163
    {
164
        if ($schema === '') {
165
            $schema = $this->defaultSchema;
166
        }
167
168
        $sql = <<<'SQL'
169
SELECT [t].[table_name]
170
FROM [INFORMATION_SCHEMA].[TABLES] AS [t]
171
WHERE [t].[table_schema] = :schema AND [t].[table_type] IN ('BASE TABLE', 'VIEW')
172
ORDER BY [t].[table_name]
173
SQL;
174
175
        $tables = $this->getDb()->createCommand($sql, [':schema' => $schema])->queryColumn();
176
177
        $tables = array_map(static function ($item) {
178
            return '[' . $item . ']';
179
        }, $tables);
180
181
        return $tables;
182
    }
183
184
    protected function loadTableSchema(string $name): ?MssqlTableSchema
185
    {
186
        $table = new MssqlTableSchema();
187
188
        $this->resolveTableNames($table, $name);
189
        $this->findPrimaryKeys($table);
190
191
        if ($this->findColumns($table)) {
192
            $this->findForeignKeys($table);
193
194
            return $table;
195
        }
196
197
        return null;
198
    }
199
200
    protected function loadTablePrimaryKey($tableName)
201
    {
202
        return $this->loadTableConstraints($tableName, 'primaryKey');
203
    }
204
205
    protected function loadTableForeignKeys($tableName)
206
    {
207
        return $this->loadTableConstraints($tableName, 'foreignKeys');
208
    }
209
210
    protected function loadTableIndexes($tableName)
211
    {
212
        static $sql = <<<'SQL'
213
SELECT
214
    [i].[name] AS [name],
215
    [iccol].[name] AS [column_name],
216
    [i].[is_unique] AS [index_is_unique],
217
    [i].[is_primary_key] AS [index_is_primary]
218
FROM [sys].[indexes] AS [i]
219
INNER JOIN [sys].[index_columns] AS [ic]
220
    ON [ic].[object_id] = [i].[object_id] AND [ic].[index_id] = [i].[index_id]
221
INNER JOIN [sys].[columns] AS [iccol]
222
    ON [iccol].[object_id] = [ic].[object_id] AND [iccol].[column_id] = [ic].[column_id]
223
WHERE [i].[object_id] = OBJECT_ID(:fullName)
224
ORDER BY [ic].[key_ordinal] ASC
225
SQL;
226
227
        $resolvedName = $this->resolveTableName($tableName);
228
        $indexes = $this->getDb()->createCommand($sql, [':fullName' => $resolvedName->getFullName()])->queryAll();
229
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
230
        $indexes = ArrayHelper::index($indexes, null, 'name');
231
232
        $result = [];
233
        foreach ($indexes as $name => $index) {
234
            $result[] = (new IndexConstraint())
235
                ->primary((bool) $index[0]['index_is_primary'])
236
                ->unique((bool) $index[0]['index_is_unique'])
237
                ->name($name)
238
                ->columnNames(ArrayHelper::getColumn($index, 'column_name'));
239
        }
240
241
        return $result;
242
    }
243
244
    protected function loadTableUniques($tableName)
245
    {
246
        return $this->loadTableConstraints($tableName, 'uniques');
247
    }
248
249
    protected function loadTableChecks($tableName)
250
    {
251
        return $this->loadTableConstraints($tableName, 'checks');
252
    }
253
254
    protected function loadTableDefaultValues($tableName)
255
    {
256
        return $this->loadTableConstraints($tableName, 'defaults');
257
    }
258
259
    public function createSavepoint(string $name): void
260
    {
261
        $this->getDb()->createCommand("SAVE TRANSACTION $name")->execute();
262
    }
263
264
    public function releaseSavepoint(string $name): void
265
    {
266
        // does nothing as MSSQL does not support this
267
    }
268
269
    public function rollBackSavepoint(string $name): void
270
    {
271
        $this->getDb()->createCommand("ROLLBACK TRANSACTION $name")->execute();
272
    }
273
274
    /**
275
     * Creates a column schema for the database.
276
     *
277
     * This method may be overridden by child classes to create a DBMS-specific column schema.
278
     *
279
     * @return MssqlColumnSchema column schema instance.
280
     */
281
    protected function createColumnSchema(): MssqlColumnSchema
282
    {
283
        return new MssqlColumnSchema();
284
    }
285
286
    /**
287
     * Creates a query builder for the MSSQL database.
288
     *
289
     * @return QueryBuilder query builder interface.
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Mssql\Schema\QueryBuilder 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...
290
     */
291
    public function createQueryBuilder()
292
    {
293
        return new MssqlQueryBuilder($this->getDb());
0 ignored issues
show
Bug introduced by
It seems like $this->getDb() can also be of type null; however, parameter $db of Yiisoft\Db\Mssql\Query\M...yBuilder::__construct() does only seem to accept Yiisoft\Db\Connection\Connection, 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

293
        return new MssqlQueryBuilder(/** @scrutinizer ignore-type */ $this->getDb());
Loading history...
Bug Best Practice introduced by
The expression return new Yiisoft\Db\Ms...Builder($this->getDb()) returns the type Yiisoft\Db\Mssql\Query\MssqlQueryBuilder which is incompatible with the documented return type Yiisoft\Db\Mssql\Schema\QueryBuilder.
Loading history...
294
    }
295
296
    /**
297
     * Resolves the table name and schema name (if any).
298
     *
299
     * @param MssqlTableSchema $table the table metadata object.
300
     * @param string $name the table name
301
     */
302
    protected function resolveTableNames(MssqlTableSchema $table, string $name)
303
    {
304
        $parts = $this->getTableNameParts($name);
305
        $partCount = count($parts);
306
307
        if ($partCount === 4) {
308
            // server name, catalog name, schema name and table name passed
309
            $table->catalogName($parts[1]);
310
            $table->schemaName($parts[2]);
311
            $table->name($parts[3]);
312
            $table->fullName($table->getCatalogName() . '.' . $table->getSchemaName() . '.' . $table->getName());
313
        } elseif ($partCount === 3) {
314
            // catalog name, schema name and table name passed
315
            $table->catalogName($parts[0]);
316
            $table->schemaName($parts[1]);
317
            $table->name($parts[2]);
318
            $table->fullName($table->getCatalogName() . '.' . $table->getSchemaName() . '.' . $table->getName());
319
        } elseif ($partCount === 2) {
320
            // only schema name and table name passed
321
            $table->schemaName($parts[0]);
322
            $table->name($parts[1]);
323
            $table->fullName(
324
                $table->getSchemaName() !== $this->defaultSchema
325
                ? $table->getSchemaName() . '.' . $table->getName() : $table->getName()
326
            );
327
        } else {
328
            // only table name passed
329
            $table->schemaName($this->defaultSchema);
330
            $table->name($parts[0]);
331
            $table->fullName($table->getName());
332
        }
333
    }
334
335
    /**
336
     * Loads the column information into a {@see ColumnSchema} object.
337
     *
338
     * @param array $info column information.
339
     *
340
     * @return ColumnSchema the column schema object.
341
     */
342
    protected function loadColumnSchema(array $info): ColumnSchema
343
    {
344
        $column = $this->createColumnSchema();
345
346
        $column->name($info['column_name']);
347
        $column->allowNull($info['is_nullable'] === 'YES');
348
        $column->dbType($info['data_type']);
349
        $column->enumValues([]); // mssql has only vague equivalents to enum
350
        $column->primaryKey(false); // primary key will be determined in findColumns() method
351
        $column->autoIncrement($info['is_identity'] == 1);
352
        $column->unsigned(stripos($column->getDbType(), 'unsigned') !== false);
353
        $column->comment($info['comment'] === null ? '' : $info['comment']);
354
        $column->type(self::TYPE_STRING);
355
356
        if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->getDbType(), $matches)) {
357
            $type = $matches[1];
358
359
            if (isset($this->typeMap[$type])) {
360
                $column->type($this->typeMap[$type]);
361
            }
362
363
            if (!empty($matches[2])) {
364
                $values = explode(',', $matches[2]);
365
                $column->precision((int) $values[0]);
366
                $column->size((int) $values[0]);
367
368
                if (isset($values[1])) {
369
                    $column->scale((int) $values[1]);
370
                }
371
372
                if ($column->getSize() === 1 && ($type === 'tinyint' || $type === 'bit')) {
373
                    $column->type('boolean');
374
                } elseif ($type === 'bit') {
375
                    if ($column->getSize() > 32) {
376
                        $column->type('bigint');
377
                    } elseif ($column->getSize() === 32) {
378
                        $column->type('integer');
379
                    }
380
                }
381
            }
382
        }
383
384
        $column->phpType($this->getColumnPhpType($column));
385
386
        if ($info['column_default'] === '(NULL)') {
387
            $info['column_default'] = null;
388
        }
389
390
        if (!$column->isPrimaryKey() && ($column->getType() !== 'timestamp' || $info['column_default'] !== 'CURRENT_TIMESTAMP')) {
391
            $column->defaultValue($column->defaultPhpTypecast($info['column_default']));
392
        }
393
394
        return $column;
395
    }
396
397
    /**
398
     * Collects the metadata of table columns.
399
     *
400
     * @param MssqlTableSchema $table the table metadata.
401
     *
402
     * @return bool whether the table exists in the database.
403
     */
404
    protected function findColumns(MssqlTableSchema $table): bool
405
    {
406
        $columnsTableName = 'INFORMATION_SCHEMA.COLUMNS';
407
        $whereSql = "[t1].[table_name] = " . $this->getDb()->quoteValue($table->getName());
408
409
        if ($table->getCatalogName() !== null) {
410
            $columnsTableName = "{$table->getCatalogName()}.{$columnsTableName}";
411
            $whereSql .= " AND [t1].[table_catalog] = '{$table->getCatalogName()}'";
412
        }
413
414
        if ($table->getSchemaName() !== null) {
415
            $whereSql .= " AND [t1].[table_schema] = '{$table->getSchemaName()}'";
416
        }
417
418
        $columnsTableName = $this->quoteTableName($columnsTableName);
419
420
        $sql = <<<SQL
421
SELECT
422
 [t1].[column_name],
423
 [t1].[is_nullable],
424
 CASE WHEN [t1].[data_type] IN ('char','varchar','nchar','nvarchar','binary','varbinary') THEN
425
    CASE WHEN [t1].[character_maximum_length] = NULL OR [t1].[character_maximum_length] = -1 THEN
426
        [t1].[data_type]
427
    ELSE
428
        [t1].[data_type] + '(' + LTRIM(RTRIM(CONVERT(CHAR,[t1].[character_maximum_length]))) + ')'
429
    END
430
 ELSE
431
    [t1].[data_type]
432
 END AS 'data_type',
433
 [t1].[column_default],
434
 COLUMNPROPERTY(OBJECT_ID([t1].[table_schema] + '.' + [t1].[table_name]), [t1].[column_name], 'IsIdentity') AS is_identity,
435
 (
436
    SELECT CONVERT(VARCHAR, [t2].[value])
437
		FROM [sys].[extended_properties] AS [t2]
438
		WHERE
439
			[t2].[class] = 1 AND
440
			[t2].[class_desc] = 'OBJECT_OR_COLUMN' AND
441
			[t2].[name] = 'MS_Description' AND
442
			[t2].[major_id] = OBJECT_ID([t1].[TABLE_SCHEMA] + '.' + [t1].[table_name]) AND
443
			[t2].[minor_id] = COLUMNPROPERTY(OBJECT_ID([t1].[TABLE_SCHEMA] + '.' + [t1].[TABLE_NAME]), [t1].[COLUMN_NAME], 'ColumnID')
444
 ) as comment
445
FROM {$columnsTableName} AS [t1]
446
WHERE {$whereSql}
447
SQL;
448
449
        try {
450
            $columns = $this->getDb()->createCommand($sql)->queryAll();
451
452
            if (empty($columns)) {
453
                return false;
454
            }
455
        } catch (\Exception $e) {
456
            return false;
457
        }
458
459
        foreach ($columns as $column) {
460
            $column = $this->loadColumnSchema($column);
461
            foreach ($table->getPrimaryKey() as $primaryKey) {
462
                if (strcasecmp($column->getName(), $primaryKey) === 0) {
463
                    $column->primaryKey(true);
464
                    break;
465
                }
466
            }
467
468
            if ($column->isPrimaryKey() && $column->isAutoIncrement()) {
469
                $table->sequenceName('');
470
            }
471
472
            $table->columns($column->getName(), $column);
473
        }
474
475
        return true;
476
    }
477
478
    /**
479
     * Collects the constraint details for the given table and constraint type.
480
     *
481
     * @param MssqlTableSchema $table
482
     * @param string $type either PRIMARY KEY or UNIQUE.
483
     *
484
     * @return array each entry contains index_name and field_name.
485
     */
486
    protected function findTableConstraints(MssqlTableSchema $table, string $type): array
487
    {
488
        $keyColumnUsageTableName = 'INFORMATION_SCHEMA.KEY_COLUMN_USAGE';
489
        $tableConstraintsTableName = 'INFORMATION_SCHEMA.TABLE_CONSTRAINTS';
490
491
        if ($table->getCatalogName() !== null) {
492
            $keyColumnUsageTableName = $table->getCatalogName() . '.' . $keyColumnUsageTableName;
493
            $tableConstraintsTableName = $table->getCatalogName() . '.' . $tableConstraintsTableName;
494
        }
495
496
        $keyColumnUsageTableName = $this->quoteTableName($keyColumnUsageTableName);
497
        $tableConstraintsTableName = $this->quoteTableName($tableConstraintsTableName);
498
499
        $sql = <<<SQL
500
SELECT
501
    [kcu].[constraint_name] AS [index_name],
502
    [kcu].[column_name] AS [field_name]
503
FROM {$keyColumnUsageTableName} AS [kcu]
504
LEFT JOIN {$tableConstraintsTableName} AS [tc] ON
505
    [kcu].[table_schema] = [tc].[table_schema] AND
506
    [kcu].[table_name] = [tc].[table_name] AND
507
    [kcu].[constraint_name] = [tc].[constraint_name]
508
WHERE
509
    [tc].[constraint_type] = :type AND
510
    [kcu].[table_name] = :tableName AND
511
    [kcu].[table_schema] = :schemaName
512
SQL;
513
514
        return $this->getDb()->createCommand(
515
            $sql,
516
            [
517
                ':tableName' => $table->getName(),
518
                ':schemaName' => $table->getSchemaName(),
519
                ':type' => $type,
520
            ]
521
        )->queryAll();
522
    }
523
524
    /**
525
     * Collects the primary key column details for the given table.
526
     *
527
     * @param MssqlTableSchema $table the table metadata
528
     */
529
    protected function findPrimaryKeys(MssqlTableSchema $table)
530
    {
531
        $result = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
532
        foreach ($this->findTableConstraints($table, 'PRIMARY KEY') as $row) {
533
            $table->primaryKey($row['field_name']);
534
        }
535
    }
536
537
    /**
538
     * Collects the foreign key column details for the given table.
539
     *
540
     * @param MssqlTableSchema $table the table metadata
541
     */
542
    protected function findForeignKeys(MssqlTableSchema $table)
543
    {
544
        $object = $table->getName();
545
546
        if ($table->getSchemaName() !== null) {
547
            $object = $table->getSchemaName() . '.' . $object;
548
        }
549
550
        if ($table->getCatalogName() !== null) {
551
            $object = $table->getCatalogName() . '.' . $object;
552
        }
553
554
        // please refer to the following page for more details:
555
        // http://msdn2.microsoft.com/en-us/library/aa175805(SQL.80).aspx
556
        $sql = <<<'SQL'
557
SELECT
558
	[fk].[name] AS [fk_name],
559
	[cp].[name] AS [fk_column_name],
560
	OBJECT_NAME([fk].[referenced_object_id]) AS [uq_table_name],
561
	[cr].[name] AS [uq_column_name]
562
FROM
563
	[sys].[foreign_keys] AS [fk]
564
	INNER JOIN [sys].[foreign_key_columns] AS [fkc] ON
565
		[fk].[object_id] = [fkc].[constraint_object_id]
566
	INNER JOIN [sys].[columns] AS [cp] ON
567
		[fk].[parent_object_id] = [cp].[object_id] AND
568
		[fkc].[parent_column_id] = [cp].[column_id]
569
	INNER JOIN [sys].[columns] AS [cr] ON
570
		[fk].[referenced_object_id] = [cr].[object_id] AND
571
		[fkc].[referenced_column_id] = [cr].[column_id]
572
WHERE
573
	[fk].[parent_object_id] = OBJECT_ID(:object)
574
SQL;
575
576
        $rows = $this->getDb()->createCommand($sql, [':object' => $object])->queryAll();
577
578
        $table->foreignKeys([]);
579
        foreach ($rows as $row) {
580
            if (!isset($table->getForeignKeys()[$row['fk_name']])) {
581
                $fk[$row['fk_name']][] = $row['uq_table_name'];
582
                $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 581. Are you sure it is defined here?
Loading history...
583
            }
584
585
            $fk[$row['fk_name']][$row['fk_column_name']] = $row['uq_column_name'];
586
            $table->foreignKeys($fk);
587
        }
588
    }
589
590
    protected function findViewNames($schema = '')
591
    {
592
        if ($schema === '') {
593
            $schema = $this->defaultSchema;
594
        }
595
596
        $sql = <<<'SQL'
597
SELECT [t].[table_name]
598
FROM [INFORMATION_SCHEMA].[TABLES] AS [t]
599
WHERE [t].[table_schema] = :schema AND [t].[table_type] = 'VIEW'
600
ORDER BY [t].[table_name]
601
SQL;
602
603
        $views = $this->getDb()->createCommand($sql, [':schema' => $schema])->queryColumn();
604
        $views = array_map(static function ($item) {
605
            return '[' . $item . ']';
606
        }, $views);
607
608
        return $views;
609
    }
610
611
    /**
612
     * Returns all unique indexes for the given table.
613
     *
614
     * Each array element is of the following structure:
615
     *
616
     * ```php
617
     * [
618
     *     'IndexName1' => ['col1' [, ...]],
619
     *     'IndexName2' => ['col2' [, ...]],
620
     * ]
621
     * ```
622
     *
623
     * @param MssqlTableSchema $table the table metadata.
624
     *
625
     * @return array all unique indexes for the given table.
626
     */
627
    public function findUniqueIndexes($table): array
628
    {
629
        $result = [];
630
631
        foreach ($this->findTableConstraints($table, 'UNIQUE') as $row) {
632
            $result[$row['index_name']][] = $row['field_name'];
633
        }
634
635
        return $result;
636
    }
637
638
    /**
639
     * Loads multiple types of constraints and returns the specified ones.
640
     *
641
     * @param string $tableName table name.
642
     * @param string $returnType return type:
643
     * - primaryKey
644
     * - foreignKeys
645
     * - uniques
646
     * - checks
647
     * - defaults
648
     *
649
     * @return mixed constraints.
650
     */
651
    private function loadTableConstraints(string $tableName, string $returnType)
652
    {
653
        static $sql = <<<'SQL'
654
SELECT
655
    [o].[name] AS [name],
656
    COALESCE([ccol].[name], [dcol].[name], [fccol].[name], [kiccol].[name]) AS [column_name],
657
    RTRIM([o].[type]) AS [type],
658
    OBJECT_SCHEMA_NAME([f].[referenced_object_id]) AS [foreign_table_schema],
659
    OBJECT_NAME([f].[referenced_object_id]) AS [foreign_table_name],
660
    [ffccol].[name] AS [foreign_column_name],
661
    [f].[update_referential_action_desc] AS [on_update],
662
    [f].[delete_referential_action_desc] AS [on_delete],
663
    [c].[definition] AS [check_expr],
664
    [d].[definition] AS [default_expr]
665
FROM (SELECT OBJECT_ID(:fullName) AS [object_id]) AS [t]
666
INNER JOIN [sys].[objects] AS [o]
667
    ON [o].[parent_object_id] = [t].[object_id] AND [o].[type] IN ('PK', 'UQ', 'C', 'D', 'F')
668
LEFT JOIN [sys].[check_constraints] AS [c]
669
    ON [c].[object_id] = [o].[object_id]
670
LEFT JOIN [sys].[columns] AS [ccol]
671
    ON [ccol].[object_id] = [c].[parent_object_id] AND [ccol].[column_id] = [c].[parent_column_id]
672
LEFT JOIN [sys].[default_constraints] AS [d]
673
    ON [d].[object_id] = [o].[object_id]
674
LEFT JOIN [sys].[columns] AS [dcol]
675
    ON [dcol].[object_id] = [d].[parent_object_id] AND [dcol].[column_id] = [d].[parent_column_id]
676
LEFT JOIN [sys].[key_constraints] AS [k]
677
    ON [k].[object_id] = [o].[object_id]
678
LEFT JOIN [sys].[index_columns] AS [kic]
679
    ON [kic].[object_id] = [k].[parent_object_id] AND [kic].[index_id] = [k].[unique_index_id]
680
LEFT JOIN [sys].[columns] AS [kiccol]
681
    ON [kiccol].[object_id] = [kic].[object_id] AND [kiccol].[column_id] = [kic].[column_id]
682
LEFT JOIN [sys].[foreign_keys] AS [f]
683
    ON [f].[object_id] = [o].[object_id]
684
LEFT JOIN [sys].[foreign_key_columns] AS [fc]
685
    ON [fc].[constraint_object_id] = [o].[object_id]
686
LEFT JOIN [sys].[columns] AS [fccol]
687
    ON [fccol].[object_id] = [fc].[parent_object_id] AND [fccol].[column_id] = [fc].[parent_column_id]
688
LEFT JOIN [sys].[columns] AS [ffccol]
689
    ON [ffccol].[object_id] = [fc].[referenced_object_id] AND [ffccol].[column_id] = [fc].[referenced_column_id]
690
ORDER BY [kic].[key_ordinal] ASC, [fc].[constraint_column_id] ASC
691
SQL;
692
693
        $resolvedName = $this->resolveTableName($tableName);
694
        $constraints = $this->getDb()->createCommand($sql, [':fullName' => $resolvedName->getFullName()])->queryAll();
695
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
696
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
697
        $result = [
698
            'primaryKey' => null,
699
            'foreignKeys' => [],
700
            'uniques' => [],
701
            'checks' => [],
702
            'defaults' => [],
703
        ];
704
705
        foreach ($constraints as $type => $names) {
706
            foreach ($names as $name => $constraint) {
707
                switch ($type) {
708
                    case 'PK':
709
                        $result['primaryKey'] = (new Constraint())
710
                            ->name($name)
711
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
712
                        break;
713
                    case 'F':
714
                        $result['foreignKeys'][] = (new ForeignKeyConstraint())
715
                            ->name($name)
716
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
717
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
718
                            ->foreignTableName($constraint[0]['foreign_table_name'])
719
                            ->foreignColumnNames(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
720
                            ->onDelete(str_replace('_', '', $constraint[0]['on_delete']))
721
                            ->onUpdate(str_replace('_', '', $constraint[0]['on_update']));
722
                        break;
723
                    case 'UQ':
724
                        $result['uniques'][] = (new Constraint())
725
                            ->name($name)
726
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
727
                        break;
728
                    case 'C':
729
                        $result['checks'][] = (new CheckConstraint())
730
                            ->name($name)
731
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
732
                            ->expression($constraint[0]['check_expr']);
733
                        break;
734
                    case 'D':
735
                        $result['defaults'][] = (new DefaultValueConstraint())
736
                            ->name($name)
737
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
738
                            ->value($constraint[0]['default_expr']);
739
                        break;
740
                }
741
            }
742
        }
743
        foreach ($result as $type => $data) {
744
            $this->setTableMetadata($tableName, $type, $data);
745
        }
746
747
        return $result[$returnType];
748
    }
749
750
    public function quoteColumnName(string $name): string
751
    {
752
        if (preg_match('/^\[.*\]$/', $name)) {
753
            return $name;
754
        }
755
756
        return parent::quoteColumnName($name);
757
    }
758
759
    /**
760
     * Retrieving inserted data from a primary key request of type uniqueidentifier (for SQL Server 2005 or later).
761
     */
762
    public function insert($table, $columns)
763
    {
764
        $command = $this->getDb()->createCommand()->insert($table, $columns);
765
        if (!$command->execute()) {
766
            return false;
767
        }
768
769
        $isVersion2005orLater = version_compare($this->getDb()->getSchema()->getServerVersion(), '9', '>=');
770
        $inserted = $isVersion2005orLater ? $command->getPdoStatement()->fetch() : [];
771
772
        $tableSchema = $this->getTableSchema($table);
773
774
        $result = [];
775
        foreach ($tableSchema->primaryKey as $name) {
0 ignored issues
show
Bug introduced by
The property primaryKey is declared private in Yiisoft\Db\Schema\TableSchema and cannot be accessed from this context.
Loading history...
776
            if ($tableSchema->columns[$name]->isAutoIncrement()) {
0 ignored issues
show
Bug introduced by
The property columns is declared private in Yiisoft\Db\Schema\TableSchema and cannot be accessed from this context.
Loading history...
777
                $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

777
                $result[$name] = $this->getLastInsertID(/** @scrutinizer ignore-type */ $tableSchema->getSequenceName());
Loading history...
778
                break;
779
            }
780
            // @see https://github.com/yiisoft/yii2/issues/13828 & https://github.com/yiisoft/yii2/issues/17474
781
            if (isset($inserted[$name])) {
782
                $result[$name] = $inserted[$name];
783
            } elseif (isset($columns[$name])) {
784
                $result[$name] = $columns[$name];
785
            } else {
786
                $result[$name] = $tableSchema->columns[$name]->defaultValue;
787
            }
788
        }
789
790
        return $result;
791
    }
792
}
793