Completed
Pull Request — 2.1 (#15718)
by Alex
17:00
created

Schema::loadColumnSchema()   D

Complexity

Conditions 16
Paths 184

Size

Total Lines 48
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 272

Importance

Changes 0
Metric Value
dl 0
loc 48
rs 4.6845
c 0
b 0
f 0
ccs 0
cts 43
cp 0
cc 16
eloc 33
nc 184
nop 1
crap 272

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\db\mssql;
9
10
use yii\db\CheckConstraint;
11
use yii\db\ColumnSchema;
12
use yii\db\Constraint;
13
use yii\db\ConstraintFinderInterface;
14
use yii\db\ConstraintFinderTrait;
15
use yii\db\DefaultValueConstraint;
16
use yii\db\ForeignKeyConstraint;
17
use yii\db\IndexConstraint;
18
use yii\db\ViewFinderTrait;
19
use yii\helpers\ArrayHelper;
20
21
/**
22
 * Schema is the class for retrieving metadata from MS SQL Server databases (version 2008 and above).
23
 *
24
 * @author Timur Ruziev <[email protected]>
25
 * @since 2.0
26
 */
27
class Schema extends \yii\db\Schema implements ConstraintFinderInterface
28
{
29
    use ViewFinderTrait;
30
    use ConstraintFinderTrait;
31
32
    /**
33
     * @var string the default schema used for the current session.
34
     */
35
    public $defaultSchema = 'dbo';
36
    /**
37
     * @var array mapping from physical column types (keys) to abstract column types (values)
38
     */
39
    public $typeMap = [
40
        // exact numbers
41
        'bigint' => self::TYPE_BIGINT,
42
        'numeric' => self::TYPE_DECIMAL,
43
        'bit' => self::TYPE_SMALLINT,
44
        'smallint' => self::TYPE_SMALLINT,
45
        'decimal' => self::TYPE_DECIMAL,
46
        'smallmoney' => self::TYPE_MONEY,
47
        'int' => self::TYPE_INTEGER,
48
        'tinyint' => self::TYPE_TINYINT,
49
        'money' => self::TYPE_MONEY,
50
        // approximate numbers
51
        'float' => self::TYPE_FLOAT,
52
        'double' => self::TYPE_DOUBLE,
53
        'real' => self::TYPE_FLOAT,
54
        // date and time
55
        'date' => self::TYPE_DATE,
56
        'datetimeoffset' => self::TYPE_DATETIME,
57
        'datetime2' => self::TYPE_DATETIME,
58
        'smalldatetime' => self::TYPE_DATETIME,
59
        'datetime' => self::TYPE_DATETIME,
60
        'time' => self::TYPE_TIME,
61
        // character strings
62
        'char' => self::TYPE_CHAR,
63
        'varchar' => self::TYPE_STRING,
64
        'text' => self::TYPE_TEXT,
65
        // unicode character strings
66
        'nchar' => self::TYPE_CHAR,
67
        'nvarchar' => self::TYPE_STRING,
68
        'ntext' => self::TYPE_TEXT,
69
        // binary strings
70
        'binary' => self::TYPE_BINARY,
71
        'varbinary' => self::TYPE_BINARY,
72
        'image' => self::TYPE_BINARY,
73
        // other data types
74
        // 'cursor' type cannot be used with tables
75
        'timestamp' => self::TYPE_TIMESTAMP,
76
        'hierarchyid' => self::TYPE_STRING,
77
        'uniqueidentifier' => self::TYPE_STRING,
78
        'sql_variant' => self::TYPE_STRING,
79
        'xml' => self::TYPE_STRING,
80
        'table' => self::TYPE_STRING,
81
    ];
82
83
    /**
84
     * @inheritDoc
85
     */
86
    protected $tableQuoteCharacter = ['[', ']'];
87
    /**
88
     * @inheritDoc
89
     */
90
    protected $columnQuoteCharacter = ['[', ']'];
91
92
93
    /**
94
     * Resolves the table name and schema name (if any).
95
     * @param string $name the table name
96
     * @return TableSchema resolved table, schema, etc. names.
97
     */
98
    protected function resolveTableName($name)
99
    {
100
        $resolvedName = new TableSchema();
101
        $parts = explode('.', str_replace(['[', ']'], '', $name));
102
        $partCount = count($parts);
103
        if ($partCount === 4) {
104
            // server name, catalog name, schema name and table name passed
105
            $resolvedName->catalogName = $parts[1];
106
            $resolvedName->schemaName = $parts[2];
107
            $resolvedName->name = $parts[3];
108
            $resolvedName->fullName = $resolvedName->catalogName . '.' . $resolvedName->schemaName . '.' . $resolvedName->name;
109
        } elseif ($partCount === 3) {
110
            // catalog name, schema name and table name passed
111
            $resolvedName->catalogName = $parts[0];
112
            $resolvedName->schemaName = $parts[1];
113
            $resolvedName->name = $parts[2];
114
            $resolvedName->fullName = $resolvedName->catalogName . '.' . $resolvedName->schemaName . '.' . $resolvedName->name;
115
        } elseif ($partCount === 2) {
116
            // only schema name and table name passed
117
            $resolvedName->schemaName = $parts[0];
118
            $resolvedName->name = $parts[1];
119
            $resolvedName->fullName = ($resolvedName->schemaName !== $this->defaultSchema ? $resolvedName->schemaName . '.' : '') . $resolvedName->name;
120
        } else {
121
            // only table name passed
122
            $resolvedName->schemaName = $this->defaultSchema;
123
            $resolvedName->fullName = $resolvedName->name = $parts[0];
124
        }
125
126
        return $resolvedName;
127
    }
128
129
    /**
130
     * @inheritDoc
131
     * @see https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-database-principals-transact-sql
132
     */
133
    protected function findSchemaNames()
134
    {
135
        static $sql = <<<'SQL'
136
SELECT [s].[name]
137
FROM [sys].[schemas] AS [s]
138
INNER JOIN [sys].[database_principals] AS [p] ON [p].[principal_id] = [s].[principal_id]
139
WHERE [p].[is_fixed_role] = 0 AND [p].[sid] IS NOT NULL
140
ORDER BY [s].[name] ASC
141
SQL;
142
143
        return $this->db->createCommand($sql)->queryColumn();
144
    }
145
146
    /**
147
     * @inheritDoc
148
     */
149
    protected function findTableNames($schema = '')
150
    {
151
        if ($schema === '') {
152
            $schema = $this->defaultSchema;
153
        }
154
155
        $sql = <<<'SQL'
156
SELECT [t].[table_name]
157
FROM [INFORMATION_SCHEMA].[TABLES] AS [t]
158
WHERE [t].[table_schema] = :schema AND [t].[table_type] IN ('BASE TABLE', 'VIEW')
159
ORDER BY [t].[table_name]
160
SQL;
161
162
        return $this->db->createCommand($sql, [':schema' => $schema])->queryColumn();
163
    }
164
165
    /**
166
     * @inheritDoc
167
     */
168
    protected function loadTableSchema($name)
169
    {
170
        $table = new TableSchema();
171
        $this->resolveTableNames($table, $name);
172
        $this->findPrimaryKeys($table);
173
        if ($this->findColumns($table)) {
174
            $this->findForeignKeys($table);
175
            return $table;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $table; (yii\db\mssql\TableSchema) is incompatible with the return type declared by the abstract method yii\db\Schema::loadTableSchema of type yii\db\TableSchema|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
176
        }
177
178
        return null;
179
    }
180
181
    /**
182
     * @inheritDoc
183
     */
184
    protected function loadTablePrimaryKey($tableName)
185
    {
186
        return $this->loadTableConstraints($tableName, 'primaryKey');
187
    }
188
189
    /**
190
     * @inheritDoc
191
     */
192
    protected function loadTableForeignKeys($tableName)
193
    {
194
        return $this->loadTableConstraints($tableName, 'foreignKeys');
195
    }
196
197
    /**
198
     * @inheritDoc
199
     */
200
    protected function loadTableIndexes($tableName)
201
    {
202
        static $sql = <<<'SQL'
203
SELECT
204
    [i].[name] AS [name],
205
    [iccol].[name] AS [column_name],
206
    [i].[is_unique] AS [index_is_unique],
207
    [i].[is_primary_key] AS [index_is_primary]
208
FROM [sys].[indexes] AS [i]
209
INNER JOIN [sys].[index_columns] AS [ic]
210
    ON [ic].[object_id] = [i].[object_id] AND [ic].[index_id] = [i].[index_id]
211
INNER JOIN [sys].[columns] AS [iccol]
212
    ON [iccol].[object_id] = [ic].[object_id] AND [iccol].[column_id] = [ic].[column_id]
213
WHERE [i].[object_id] = OBJECT_ID(:fullName)
214
ORDER BY [ic].[key_ordinal] ASC
215
SQL;
216
217
        $resolvedName = $this->resolveTableName($tableName);
218
        $indexes = $this->db->createCommand($sql, [
219
            ':fullName' => $resolvedName->fullName,
220
        ])->queryAll();
221
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
222
        $indexes = ArrayHelper::index($indexes, null, 'name');
223
        $result = [];
224
        foreach ($indexes as $name => $index) {
225
            $result[] = new IndexConstraint([
226
                'isPrimary' => (bool) $index[0]['index_is_primary'],
227
                'isUnique' => (bool) $index[0]['index_is_unique'],
228
                'name' => $name,
229
                'columnNames' => ArrayHelper::getColumn($index, 'column_name'),
230
            ]);
231
        }
232
233
        return $result;
234
    }
235
236
    /**
237
     * @inheritDoc
238
     */
239
    protected function loadTableUniques($tableName)
240
    {
241
        return $this->loadTableConstraints($tableName, 'uniques');
242
    }
243
244
    /**
245
     * @inheritDoc
246
     */
247
    protected function loadTableChecks($tableName)
248
    {
249
        return $this->loadTableConstraints($tableName, 'checks');
250
    }
251
252
    /**
253
     * @inheritDoc
254
     */
255
    protected function loadTableDefaultValues($tableName)
256
    {
257
        return $this->loadTableConstraints($tableName, 'defaults');
258
    }
259
260
    /**
261
     * {@inheritdoc}
262
     */
263
    public function createSavepoint($name)
264
    {
265
        $this->db->createCommand("SAVE TRANSACTION $name")->execute();
266
    }
267
268
    /**
269
     * {@inheritdoc}
270
     */
271
    public function releaseSavepoint($name)
272
    {
273
        // does nothing as MSSQL does not support this
274
    }
275
276
    /**
277
     * {@inheritdoc}
278
     */
279
    public function rollBackSavepoint($name)
280
    {
281
        $this->db->createCommand("ROLLBACK TRANSACTION $name")->execute();
282
    }
283
284
    /**
285
     * Creates a query builder for the MSSQL database.
286
     * @return QueryBuilder query builder interface.
287
     */
288
    public function createQueryBuilder()
289
    {
290
        return new QueryBuilder($this->db);
291
    }
292
293
    /**
294
     * Resolves the table name and schema name (if any).
295
     * @param TableSchema $table the table metadata object
296
     * @param string $name the table name
297
     */
298
    protected function resolveTableNames($table, $name)
299
    {
300
        $parts = explode('.', str_replace(['[', ']'], '', $name));
301
        $partCount = count($parts);
302
        if ($partCount === 4) {
303
            // server name, catalog name, schema name and table name passed
304
            $table->catalogName = $parts[1];
305
            $table->schemaName = $parts[2];
306
            $table->name = $parts[3];
307
            $table->fullName = $table->catalogName . '.' . $table->schemaName . '.' . $table->name;
308
        } elseif ($partCount === 3) {
309
            // catalog name, schema name and table name passed
310
            $table->catalogName = $parts[0];
311
            $table->schemaName = $parts[1];
312
            $table->name = $parts[2];
313
            $table->fullName = $table->catalogName . '.' . $table->schemaName . '.' . $table->name;
314
        } elseif ($partCount === 2) {
315
            // only schema name and table name passed
316
            $table->schemaName = $parts[0];
317
            $table->name = $parts[1];
318
            $table->fullName = $table->schemaName !== $this->defaultSchema ? $table->schemaName . '.' . $table->name : $table->name;
319
        } else {
320
            // only table name passed
321
            $table->schemaName = $this->defaultSchema;
322
            $table->fullName = $table->name = $parts[0];
323
        }
324
    }
325
326
    /**
327
     * Loads the column information into a [[ColumnSchema]] object.
328
     * @param array $info column information
329
     * @return ColumnSchema the column schema object
330
     */
331
    protected function loadColumnSchema($info)
332
    {
333
        $column = $this->createColumnSchema();
334
335
        $column->name = $info['column_name'];
336
        $column->allowNull = $info['is_nullable'] === 'YES';
337
        $column->dbType = $info['data_type'];
338
        $column->enumValues = []; // mssql has only vague equivalents to enum
339
        $column->isPrimaryKey = null; // primary key will be determined in findColumns() method
340
        $column->autoIncrement = $info['is_identity'] == 1;
341
        $column->unsigned = stripos($column->dbType, 'unsigned') !== false;
342
        $column->comment = $info['comment'] === null ? '' : $info['comment'];
343
344
        $column->type = self::TYPE_STRING;
345
        if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) {
346
            $type = $matches[1];
347
            if (isset($this->typeMap[$type])) {
348
                $column->type = $this->typeMap[$type];
349
            }
350
            if (!empty($matches[2])) {
351
                $values = explode(',', $matches[2]);
352
                $column->size = $column->precision = (int) $values[0];
353
                if (isset($values[1])) {
354
                    $column->scale = (int) $values[1];
355
                }
356
                if ($column->size === 1 && ($type === 'tinyint' || $type === 'bit')) {
357
                    $column->type = 'boolean';
358
                } elseif ($type === 'bit') {
359
                    if ($column->size > 32) {
360
                        $column->type = 'bigint';
361
                    } elseif ($column->size === 32) {
362
                        $column->type = 'integer';
363
                    }
364
                }
365
            }
366
        }
367
368
        $column->phpType = $this->getColumnPhpType($column);
369
370
        if ($info['column_default'] === '(NULL)') {
371
            $info['column_default'] = null;
372
        }
373
        if (!$column->isPrimaryKey && ($column->type !== 'timestamp' || $info['column_default'] !== 'CURRENT_TIMESTAMP')) {
374
            $column->defaultValue = $column->phpTypecast($info['column_default']);
375
        }
376
377
        return $column;
378
    }
379
380
    /**
381
     * Collects the metadata of table columns.
382
     * @param TableSchema $table the table metadata
383
     * @return bool whether the table exists in the database
384
     */
385
    protected function findColumns($table)
386
    {
387
        $columnsTableName = 'INFORMATION_SCHEMA.COLUMNS';
388
        $whereSql = "[t1].[table_name] = '{$table->name}'";
389
        if ($table->catalogName !== null) {
390
            $columnsTableName = "{$table->catalogName}.{$columnsTableName}";
391
            $whereSql .= " AND [t1].[table_catalog] = '{$table->catalogName}'";
392
        }
393
        if ($table->schemaName !== null) {
394
            $whereSql .= " AND [t1].[table_schema] = '{$table->schemaName}'";
395
        }
396
        $columnsTableName = $this->quoteTableName($columnsTableName);
397
398
        $sql = <<<SQL
399
SELECT
400
 [t1].[column_name],
401
 [t1].[is_nullable],
402
 [t1].[data_type],
403
 [t1].[column_default],
404
 COLUMNPROPERTY(OBJECT_ID([t1].[table_schema] + '.' + [t1].[table_name]), [t1].[column_name], 'IsIdentity') AS is_identity,
405
 (
406
    SELECT CONVERT(VARCHAR, [t2].[value])
407
		FROM [sys].[extended_properties] AS [t2]
408
		WHERE
409
			[t2].[class] = 1 AND
410
			[t2].[class_desc] = 'OBJECT_OR_COLUMN' AND
411
			[t2].[name] = 'MS_Description' AND
412
			[t2].[major_id] = OBJECT_ID([t1].[TABLE_SCHEMA] + '.' + [t1].[table_name]) AND
413
			[t2].[minor_id] = COLUMNPROPERTY(OBJECT_ID([t1].[TABLE_SCHEMA] + '.' + [t1].[TABLE_NAME]), [t1].[COLUMN_NAME], 'ColumnID')
414
 ) as comment
415
FROM {$columnsTableName} AS [t1]
416
WHERE {$whereSql}
417
SQL;
418
419
        try {
420
            $columns = $this->db->createCommand($sql)->queryAll();
421
            if (empty($columns)) {
422
                return false;
423
            }
424
        } catch (\Exception $e) {
425
            return false;
426
        }
427
        foreach ($columns as $column) {
428
            $column = $this->loadColumnSchema($column);
429
            foreach ($table->primaryKey as $primaryKey) {
430
                if (strcasecmp($column->name, $primaryKey) === 0) {
431
                    $column->isPrimaryKey = true;
432
                    break;
433
                }
434
            }
435
            if ($column->isPrimaryKey && $column->autoIncrement) {
436
                $table->sequenceName = '';
437
            }
438
            $table->columns[$column->name] = $column;
439
        }
440
441
        return true;
442
    }
443
444
    /**
445
     * Collects the constraint details for the given table and constraint type.
446
     * @param TableSchema $table
447
     * @param string $type either PRIMARY KEY or UNIQUE
448
     * @return array each entry contains index_name and field_name
449
     * @since 2.0.4
450
     */
451
    protected function findTableConstraints($table, $type)
452
    {
453
        $keyColumnUsageTableName = 'INFORMATION_SCHEMA.KEY_COLUMN_USAGE';
454
        $tableConstraintsTableName = 'INFORMATION_SCHEMA.TABLE_CONSTRAINTS';
455
        if ($table->catalogName !== null) {
456
            $keyColumnUsageTableName = $table->catalogName . '.' . $keyColumnUsageTableName;
457
            $tableConstraintsTableName = $table->catalogName . '.' . $tableConstraintsTableName;
458
        }
459
        $keyColumnUsageTableName = $this->quoteTableName($keyColumnUsageTableName);
460
        $tableConstraintsTableName = $this->quoteTableName($tableConstraintsTableName);
461
462
        $sql = <<<SQL
463
SELECT
464
    [kcu].[constraint_name] AS [index_name],
465
    [kcu].[column_name] AS [field_name]
466
FROM {$keyColumnUsageTableName} AS [kcu]
467
LEFT JOIN {$tableConstraintsTableName} AS [tc] ON
468
    [kcu].[table_schema] = [tc].[table_schema] AND
469
    [kcu].[table_name] = [tc].[table_name] AND
470
    [kcu].[constraint_name] = [tc].[constraint_name]
471
WHERE
472
    [tc].[constraint_type] = :type AND
473
    [kcu].[table_name] = :tableName AND
474
    [kcu].[table_schema] = :schemaName
475
SQL;
476
477
        return $this->db
478
            ->createCommand($sql, [
479
                ':tableName' => $table->name,
480
                ':schemaName' => $table->schemaName,
481
                ':type' => $type,
482
            ])
483
            ->queryAll();
484
    }
485
486
    /**
487
     * Collects the primary key column details for the given table.
488
     * @param TableSchema $table the table metadata
489
     */
490
    protected function findPrimaryKeys($table)
491
    {
492
        $result = [];
493
        foreach ($this->findTableConstraints($table, 'PRIMARY KEY') as $row) {
494
            $result[] = $row['field_name'];
495
        }
496
        $table->primaryKey = $result;
497
    }
498
499
    /**
500
     * Collects the foreign key column details for the given table.
501
     * @param TableSchema $table the table metadata
502
     */
503
    protected function findForeignKeys($table)
504
    {
505
        $object = $table->name;
506
        if ($table->schemaName !== null) {
507
            $object = $table->schemaName . '.' . $object;
508
        }
509
        if ($table->catalogName !== null) {
510
            $object = $table->catalogName . '.' . $object;
511
        }
512
513
        // please refer to the following page for more details:
514
        // http://msdn2.microsoft.com/en-us/library/aa175805(SQL.80).aspx
515
        $sql = <<<'SQL'
516
SELECT
517
	[fk].[name] AS [fk_name],
518
	[cp].[name] AS [fk_column_name],
519
	OBJECT_NAME([fk].[referenced_object_id]) AS [uq_table_name],
520
	[cr].[name] AS [uq_column_name]
521
FROM
522
	[sys].[foreign_keys] AS [fk]
523
	INNER JOIN [sys].[foreign_key_columns] AS [fkc] ON
524
		[fk].[object_id] = [fkc].[constraint_object_id]
525
	INNER JOIN [sys].[columns] AS [cp] ON
526
		[fk].[parent_object_id] = [cp].[object_id] AND
527
		[fkc].[parent_column_id] = [cp].[column_id]
528
	INNER JOIN [sys].[columns] AS [cr] ON
529
		[fk].[referenced_object_id] = [cr].[object_id] AND
530
		[fkc].[referenced_column_id] = [cr].[column_id]
531
WHERE
532
	[fk].[parent_object_id] = OBJECT_ID(:object)
533
SQL;
534
535
        $rows = $this->db->createCommand($sql, [
536
            ':object' => $object,
537
        ])->queryAll();
538
539
        $table->foreignKeys = [];
540
        foreach ($rows as $row) {
541
            $table->foreignKeys[$row['fk_name']] = [$row['uq_table_name'], $row['fk_column_name'] => $row['uq_column_name']];
542
        }
543
    }
544
545
    /**
546
     * {@inheritdoc}
547
     */
548
    protected function findViewNames($schema = '')
549
    {
550
        if ($schema === '') {
551
            $schema = $this->defaultSchema;
552
        }
553
554
        $sql = <<<'SQL'
555
SELECT [t].[table_name]
556
FROM [INFORMATION_SCHEMA].[TABLES] AS [t]
557
WHERE [t].[table_schema] = :schema AND [t].[table_type] = 'VIEW'
558
ORDER BY [t].[table_name]
559
SQL;
560
561
        return $this->db->createCommand($sql, [':schema' => $schema])->queryColumn();
562
    }
563
564
    /**
565
     * Returns all unique indexes for the given table.
566
     *
567
     * Each array element is of the following structure:
568
     *
569
     * ```php
570
     * [
571
     *     'IndexName1' => ['col1' [, ...]],
572
     *     'IndexName2' => ['col2' [, ...]],
573
     * ]
574
     * ```
575
     *
576
     * @param TableSchema $table the table metadata
577
     * @return array all unique indexes for the given table.
578
     * @since 2.0.4
579
     */
580
    public function findUniqueIndexes($table)
581
    {
582
        $result = [];
583
        foreach ($this->findTableConstraints($table, 'UNIQUE') as $row) {
584
            $result[$row['index_name']][] = $row['field_name'];
585
        }
586
587
        return $result;
588
    }
589
590
    /**
591
     * Loads multiple types of constraints and returns the specified ones.
592
     * @param string $tableName table name.
593
     * @param string $returnType return type:
594
     * - primaryKey
595
     * - foreignKeys
596
     * - uniques
597
     * - checks
598
     * - defaults
599
     * @return mixed constraints.
600
     */
601
    private function loadTableConstraints($tableName, $returnType)
602
    {
603
        static $sql = <<<'SQL'
604
SELECT
605
    [o].[name] AS [name],
606
    COALESCE([ccol].[name], [dcol].[name], [fccol].[name], [kiccol].[name]) AS [column_name],
607
    RTRIM([o].[type]) AS [type],
608
    OBJECT_SCHEMA_NAME([f].[referenced_object_id]) AS [foreign_table_schema],
609
    OBJECT_NAME([f].[referenced_object_id]) AS [foreign_table_name],
610
    [ffccol].[name] AS [foreign_column_name],
611
    [f].[update_referential_action_desc] AS [on_update],
612
    [f].[delete_referential_action_desc] AS [on_delete],
613
    [c].[definition] AS [check_expr],
614
    [d].[definition] AS [default_expr]
615
FROM (SELECT OBJECT_ID(:fullName) AS [object_id]) AS [t]
616
INNER JOIN [sys].[objects] AS [o]
617
    ON [o].[parent_object_id] = [t].[object_id] AND [o].[type] IN ('PK', 'UQ', 'C', 'D', 'F')
618
LEFT JOIN [sys].[check_constraints] AS [c]
619
    ON [c].[object_id] = [o].[object_id]
620
LEFT JOIN [sys].[columns] AS [ccol]
621
    ON [ccol].[object_id] = [c].[parent_object_id] AND [ccol].[column_id] = [c].[parent_column_id]
622
LEFT JOIN [sys].[default_constraints] AS [d]
623
    ON [d].[object_id] = [o].[object_id]
624
LEFT JOIN [sys].[columns] AS [dcol]
625
    ON [dcol].[object_id] = [d].[parent_object_id] AND [dcol].[column_id] = [d].[parent_column_id]
626
LEFT JOIN [sys].[key_constraints] AS [k]
627
    ON [k].[object_id] = [o].[object_id]
628
LEFT JOIN [sys].[index_columns] AS [kic]
629
    ON [kic].[object_id] = [k].[parent_object_id] AND [kic].[index_id] = [k].[unique_index_id]
630
LEFT JOIN [sys].[columns] AS [kiccol]
631
    ON [kiccol].[object_id] = [kic].[object_id] AND [kiccol].[column_id] = [kic].[column_id]
632
LEFT JOIN [sys].[foreign_keys] AS [f]
633
    ON [f].[object_id] = [o].[object_id]
634
LEFT JOIN [sys].[foreign_key_columns] AS [fc]
635
    ON [fc].[constraint_object_id] = [o].[object_id]
636
LEFT JOIN [sys].[columns] AS [fccol]
637
    ON [fccol].[object_id] = [fc].[parent_object_id] AND [fccol].[column_id] = [fc].[parent_column_id]
638
LEFT JOIN [sys].[columns] AS [ffccol]
639
    ON [ffccol].[object_id] = [fc].[referenced_object_id] AND [ffccol].[column_id] = [fc].[referenced_column_id]
640
ORDER BY [kic].[key_ordinal] ASC, [fc].[constraint_column_id] ASC
641
SQL;
642
643
        $resolvedName = $this->resolveTableName($tableName);
644
        $constraints = $this->db->createCommand($sql, [
645
            ':fullName' => $resolvedName->fullName,
646
        ])->queryAll();
647
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
648
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
649
        $result = [
650
            'primaryKey' => null,
651
            'foreignKeys' => [],
652
            'uniques' => [],
653
            'checks' => [],
654
            'defaults' => [],
655
        ];
656
        foreach ($constraints as $type => $names) {
657
            foreach ($names as $name => $constraint) {
658
                switch ($type) {
659
                    case 'PK':
660
                        $result['primaryKey'] = new Constraint([
661
                            'name' => $name,
662
                            'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
663
                        ]);
664
                        break;
665
                    case 'F':
666
                        $result['foreignKeys'][] = new ForeignKeyConstraint([
667
                            'name' => $name,
668
                            'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
669
                            'foreignSchemaName' => $constraint[0]['foreign_table_schema'],
670
                            'foreignTableName' => $constraint[0]['foreign_table_name'],
671
                            'foreignColumnNames' => ArrayHelper::getColumn($constraint, 'foreign_column_name'),
672
                            'onDelete' => str_replace('_', '', $constraint[0]['on_delete']),
673
                            'onUpdate' => str_replace('_', '', $constraint[0]['on_update']),
674
                        ]);
675
                        break;
676
                    case 'UQ':
677
                        $result['uniques'][] = new Constraint([
678
                            'name' => $name,
679
                            'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
680
                        ]);
681
                        break;
682
                    case 'C':
683
                        $result['checks'][] = new CheckConstraint([
684
                            'name' => $name,
685
                            'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
686
                            'expression' => $constraint[0]['check_expr'],
687
                        ]);
688
                        break;
689
                    case 'D':
690
                        $result['defaults'][] = new DefaultValueConstraint([
691
                            'name' => $name,
692
                            'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
693
                            'value' => $constraint[0]['default_expr'],
694
                        ]);
695
                        break;
696
                }
697
            }
698
        }
699
        foreach ($result as $type => $data) {
700
            $this->setTableMetadata($tableName, $type, $data);
701
        }
702
703
        return $result[$returnType];
704
    }
705
}
706