Schema   F
last analyzed

Complexity

Total Complexity 96

Size/Duplication

Total Lines 813
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
eloc 430
dl 0
loc 813
ccs 0
cts 317
cp 0
rs 2
c 0
b 0
f 0
wmc 96

29 Methods

Rating   Name   Duplication   Size   Complexity  
A findTableConstraints() 0 33 2
B insert() 0 27 7
A findViewNames() 0 14 2
C loadTableConstraints() 0 103 9
A findPrimaryKeys() 0 7 2
A createColumnSchemaBuilder() 0 3 1
A findUniqueIndexes() 0 8 2
B booleanTypeLegacy() 0 13 7
A findForeignKeys() 0 42 5
A quoteColumnName() 0 7 2
B findColumns() 0 66 10
A findSchemaNames() 0 11 1
A findTableNames() 0 13 2
A loadTableIndexes() 0 34 2
A loadTableUniques() 0 3 1
A releaseSavepoint() 0 2 1
A createSavepoint() 0 3 1
A loadTableChecks() 0 3 1
A loadTableSchema() 0 11 2
A loadTablePrimaryKey() 0 3 1
A getSchemaMetadata() 0 18 4
A resolveTableName() 0 29 5
A createQueryBuilder() 0 3 1
A resolveTableNames() 0 25 5
A rollBackSavepoint() 0 3 1
A getTableNameParts() 0 11 4
A loadTableDefaultValues() 0 3 1
A loadTableForeignKeys() 0 3 1
C loadColumnSchema() 0 50 13

How to fix   Complexity   

Complex Class

Complex classes like Schema often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Schema, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * @link https://www.yiiframework.com/
5
 * @copyright Copyright (c) 2008 Yii Software LLC
6
 * @license https://www.yiiframework.com/license/
7
 */
8
9
namespace yii\db\mssql;
10
11
use Yii;
12
use yii\db\CheckConstraint;
13
use yii\db\Constraint;
14
use yii\db\ConstraintFinderInterface;
15
use yii\db\ConstraintFinderTrait;
16
use yii\db\DefaultValueConstraint;
17
use yii\db\ForeignKeyConstraint;
18
use yii\db\IndexConstraint;
19
use yii\db\ViewFinderTrait;
20
use yii\helpers\ArrayHelper;
21
22
/**
23
 * Schema is the class for retrieving metadata from MS SQL Server databases (version 2008 and above).
24
 *
25
 * @author Timur Ruziev <[email protected]>
26
 * @since 2.0
27
 */
28
class Schema extends \yii\db\Schema implements ConstraintFinderInterface
29
{
30
    use ViewFinderTrait;
31
    use ConstraintFinderTrait;
32
33
    /**
34
     * {@inheritdoc}
35
     */
36
    public $columnSchemaClass = 'yii\db\mssql\ColumnSchema';
37
    /**
38
     * @var string the default schema used for the current session.
39
     */
40
    public $defaultSchema = 'dbo';
41
    /**
42
     * @var array mapping from physical column types (keys) to abstract column types (values)
43
     */
44
    public $typeMap = [
45
        // exact numbers
46
        'bigint' => self::TYPE_BIGINT,
47
        'numeric' => self::TYPE_DECIMAL,
48
        'bit' => self::TYPE_SMALLINT,
49
        'smallint' => self::TYPE_SMALLINT,
50
        'decimal' => self::TYPE_DECIMAL,
51
        'smallmoney' => self::TYPE_MONEY,
52
        'int' => self::TYPE_INTEGER,
53
        'tinyint' => self::TYPE_TINYINT,
54
        'money' => self::TYPE_MONEY,
55
        // approximate numbers
56
        'float' => self::TYPE_FLOAT,
57
        'double' => self::TYPE_DOUBLE,
58
        'real' => self::TYPE_FLOAT,
59
        // date and time
60
        'date' => self::TYPE_DATE,
61
        'datetimeoffset' => self::TYPE_DATETIME,
62
        'datetime2' => self::TYPE_DATETIME,
63
        'smalldatetime' => self::TYPE_DATETIME,
64
        'datetime' => self::TYPE_DATETIME,
65
        'time' => self::TYPE_TIME,
66
        // character strings
67
        'char' => self::TYPE_CHAR,
68
        'varchar' => self::TYPE_STRING,
69
        'text' => self::TYPE_TEXT,
70
        // unicode character strings
71
        'nchar' => self::TYPE_CHAR,
72
        'nvarchar' => self::TYPE_STRING,
73
        'ntext' => self::TYPE_TEXT,
74
        // binary strings
75
        'binary' => self::TYPE_BINARY,
76
        'varbinary' => self::TYPE_BINARY,
77
        'image' => self::TYPE_BINARY,
78
        // other data types
79
        // 'cursor' type cannot be used with tables
80
        'timestamp' => self::TYPE_TIMESTAMP,
81
        'hierarchyid' => self::TYPE_STRING,
82
        'uniqueidentifier' => self::TYPE_STRING,
83
        'sql_variant' => self::TYPE_STRING,
84
        'xml' => self::TYPE_STRING,
85
        'table' => self::TYPE_STRING,
86
    ];
87
88
    /**
89
     * {@inheritdoc}
90
     */
91
    protected $tableQuoteCharacter = ['[', ']'];
92
    /**
93
     * {@inheritdoc}
94
     */
95
    protected $columnQuoteCharacter = ['[', ']'];
96
97
98
    /**
99
     * Resolves the table name and schema name (if any).
100
     * @param string $name the table name
101
     * @return TableSchema resolved table, schema, etc. names.
102
     */
103
    protected function resolveTableName($name)
104
    {
105
        $resolvedName = new TableSchema();
106
        $parts = $this->getTableNameParts($name);
107
        $partCount = count($parts);
108
        if ($partCount === 4) {
109
            // server name, catalog name, schema name and table name passed
110
            $resolvedName->catalogName = $parts[1];
111
            $resolvedName->schemaName = $parts[2];
112
            $resolvedName->name = $parts[3];
113
            $resolvedName->fullName = $resolvedName->catalogName . '.' . $resolvedName->schemaName . '.' . $resolvedName->name;
114
        } elseif ($partCount === 3) {
115
            // catalog name, schema name and table name passed
116
            $resolvedName->catalogName = $parts[0];
117
            $resolvedName->schemaName = $parts[1];
118
            $resolvedName->name = $parts[2];
119
            $resolvedName->fullName = $resolvedName->catalogName . '.' . $resolvedName->schemaName . '.' . $resolvedName->name;
120
        } elseif ($partCount === 2) {
121
            // only schema name and table name passed
122
            $resolvedName->schemaName = $parts[0];
123
            $resolvedName->name = $parts[1];
124
            $resolvedName->fullName = ($resolvedName->schemaName !== $this->defaultSchema ? $resolvedName->schemaName . '.' : '') . $resolvedName->name;
125
        } else {
126
            // only table name passed
127
            $resolvedName->schemaName = $this->defaultSchema;
128
            $resolvedName->fullName = $resolvedName->name = $parts[0];
129
        }
130
131
        return $resolvedName;
132
    }
133
134
    /**
135
     * {@inheritDoc}
136
     * @param string $name
137
     * @return array
138
     * @since 2.0.22
139
     */
140
    protected function getTableNameParts($name)
141
    {
142
        $parts = [$name];
143
        preg_match_all('/([^.\[\]]+)|\[([^\[\]]+)\]/', $name, $matches);
144
        if (isset($matches[0]) && is_array($matches[0]) && !empty($matches[0])) {
145
            $parts = $matches[0];
146
        }
147
148
        $parts = str_replace(['[', ']'], '', $parts);
149
150
        return $parts;
151
    }
152
153
    /**
154
     * {@inheritdoc}
155
     * @see https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-database-principals-transact-sql
156
     */
157
    protected function findSchemaNames()
158
    {
159
        static $sql = <<<'SQL'
160
SELECT [s].[name]
161
FROM [sys].[schemas] AS [s]
162
INNER JOIN [sys].[database_principals] AS [p] ON [p].[principal_id] = [s].[principal_id]
163
WHERE [p].[is_fixed_role] = 0 AND [p].[sid] IS NOT NULL
164
ORDER BY [s].[name] ASC
165
SQL;
166
167
        return $this->db->createCommand($sql)->queryColumn();
168
    }
169
170
    /**
171
     * {@inheritdoc}
172
     */
173
    protected function findTableNames($schema = '')
174
    {
175
        if ($schema === '') {
176
            $schema = $this->defaultSchema;
177
        }
178
179
        $sql = <<<'SQL'
180
SELECT [t].[table_name]
181
FROM [INFORMATION_SCHEMA].[TABLES] AS [t]
182
WHERE [t].[table_schema] = :schema AND [t].[table_type] IN ('BASE TABLE', 'VIEW')
183
ORDER BY [t].[table_name]
184
SQL;
185
        return $this->db->createCommand($sql, [':schema' => $schema])->queryColumn();
186
    }
187
188
    /**
189
     * {@inheritdoc}
190
     */
191
    protected function loadTableSchema($name)
192
    {
193
        $table = new TableSchema();
194
        $this->resolveTableNames($table, $name);
195
        $this->findPrimaryKeys($table);
196
        if ($this->findColumns($table)) {
197
            $this->findForeignKeys($table);
198
            return $table;
199
        }
200
201
        return null;
202
    }
203
204
    /**
205
     * {@inheritdoc}
206
     */
207
    protected function getSchemaMetadata($schema, $type, $refresh)
208
    {
209
        $metadata = [];
210
        $methodName = 'getTable' . ucfirst($type);
211
        $tableNames = array_map(function ($table) {
212
            return $this->quoteSimpleTableName($table);
213
        }, $this->getTableNames($schema, $refresh));
214
        foreach ($tableNames as $name) {
215
            if ($schema !== '') {
216
                $name = $schema . '.' . $name;
217
            }
218
            $tableMetadata = $this->$methodName($name, $refresh);
219
            if ($tableMetadata !== null) {
220
                $metadata[] = $tableMetadata;
221
            }
222
        }
223
224
        return $metadata;
225
    }
226
227
    /**
228
     * {@inheritdoc}
229
     */
230
    protected function loadTablePrimaryKey($tableName)
231
    {
232
        return $this->loadTableConstraints($tableName, 'primaryKey');
233
    }
234
235
    /**
236
     * {@inheritdoc}
237
     */
238
    protected function loadTableForeignKeys($tableName)
239
    {
240
        return $this->loadTableConstraints($tableName, 'foreignKeys');
241
    }
242
243
    /**
244
     * {@inheritdoc}
245
     */
246
    protected function loadTableIndexes($tableName)
247
    {
248
        static $sql = <<<'SQL'
249
SELECT
250
    [i].[name] AS [name],
251
    [iccol].[name] AS [column_name],
252
    [i].[is_unique] AS [index_is_unique],
253
    [i].[is_primary_key] AS [index_is_primary]
254
FROM [sys].[indexes] AS [i]
255
INNER JOIN [sys].[index_columns] AS [ic]
256
    ON [ic].[object_id] = [i].[object_id] AND [ic].[index_id] = [i].[index_id]
257
INNER JOIN [sys].[columns] AS [iccol]
258
    ON [iccol].[object_id] = [ic].[object_id] AND [iccol].[column_id] = [ic].[column_id]
259
WHERE [i].[object_id] = OBJECT_ID(:fullName)
260
ORDER BY [ic].[key_ordinal] ASC
261
SQL;
262
263
        $resolvedName = $this->resolveTableName($tableName);
264
        $indexes = $this->db->createCommand($sql, [
265
            ':fullName' => $resolvedName->fullName,
266
        ])->queryAll();
267
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
268
        $indexes = ArrayHelper::index($indexes, null, 'name');
269
        $result = [];
270
        foreach ($indexes as $name => $index) {
271
            $result[] = new IndexConstraint([
272
                'isPrimary' => (bool)$index[0]['index_is_primary'],
273
                'isUnique' => (bool)$index[0]['index_is_unique'],
274
                'name' => $name,
275
                'columnNames' => ArrayHelper::getColumn($index, 'column_name'),
276
            ]);
277
        }
278
279
        return $result;
280
    }
281
282
    /**
283
     * {@inheritdoc}
284
     */
285
    protected function loadTableUniques($tableName)
286
    {
287
        return $this->loadTableConstraints($tableName, 'uniques');
288
    }
289
290
    /**
291
     * {@inheritdoc}
292
     */
293
    protected function loadTableChecks($tableName)
294
    {
295
        return $this->loadTableConstraints($tableName, 'checks');
296
    }
297
298
    /**
299
     * {@inheritdoc}
300
     */
301
    protected function loadTableDefaultValues($tableName)
302
    {
303
        return $this->loadTableConstraints($tableName, 'defaults');
304
    }
305
306
    /**
307
     * {@inheritdoc}
308
     */
309
    public function createSavepoint($name)
310
    {
311
        $this->db->createCommand("SAVE TRANSACTION $name")->execute();
312
    }
313
314
    /**
315
     * {@inheritdoc}
316
     */
317
    public function releaseSavepoint($name)
318
    {
319
        // does nothing as MSSQL does not support this
320
    }
321
322
    /**
323
     * {@inheritdoc}
324
     */
325
    public function rollBackSavepoint($name)
326
    {
327
        $this->db->createCommand("ROLLBACK TRANSACTION $name")->execute();
328
    }
329
330
    /**
331
     * Creates a query builder for the MSSQL database.
332
     * @return QueryBuilder query builder interface.
333
     */
334
    public function createQueryBuilder()
335
    {
336
        return Yii::createObject(QueryBuilder::className(), [$this->db]);
0 ignored issues
show
Deprecated Code introduced by
The function yii\base\BaseObject::className() has been deprecated: since 2.0.14. On PHP >=5.5, use `::class` instead. ( Ignorable by Annotation )

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

336
        return Yii::createObject(/** @scrutinizer ignore-deprecated */ QueryBuilder::className(), [$this->db]);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
337
    }
338
339
    /**
340
     * Resolves the table name and schema name (if any).
341
     * @param TableSchema $table the table metadata object
342
     * @param string $name the table name
343
     */
344
    protected function resolveTableNames($table, $name)
345
    {
346
        $parts = $this->getTableNameParts($name);
347
        $partCount = count($parts);
348
        if ($partCount === 4) {
349
            // server name, catalog name, schema name and table name passed
350
            $table->catalogName = $parts[1];
351
            $table->schemaName = $parts[2];
352
            $table->name = $parts[3];
353
            $table->fullName = $table->catalogName . '.' . $table->schemaName . '.' . $table->name;
354
        } elseif ($partCount === 3) {
355
            // catalog name, schema name and table name passed
356
            $table->catalogName = $parts[0];
357
            $table->schemaName = $parts[1];
358
            $table->name = $parts[2];
359
            $table->fullName = $table->catalogName . '.' . $table->schemaName . '.' . $table->name;
360
        } elseif ($partCount === 2) {
361
            // only schema name and table name passed
362
            $table->schemaName = $parts[0];
363
            $table->name = $parts[1];
364
            $table->fullName = $table->schemaName !== $this->defaultSchema ? $table->schemaName . '.' . $table->name : $table->name;
365
        } else {
366
            // only table name passed
367
            $table->schemaName = $this->defaultSchema;
368
            $table->fullName = $table->name = $parts[0];
369
        }
370
    }
371
372
    /**
373
     * Loads the column information into a [[ColumnSchema]] object.
374
     * @param array $info column information
375
     * @return ColumnSchema the column schema object
376
     */
377
    protected function loadColumnSchema($info)
378
    {
379
        $isVersion2017orLater = version_compare($this->db->getSchema()->getServerVersion(), '14', '>=');
380
        $column = $this->createColumnSchema();
381
382
        $column->name = $info['column_name'];
383
        $column->allowNull = $info['is_nullable'] === 'YES';
384
        $column->dbType = $info['data_type'];
385
        $column->enumValues = []; // mssql has only vague equivalents to enum
386
        $column->isPrimaryKey = null; // primary key will be determined in findColumns() method
387
        $column->autoIncrement = $info['is_identity'] == 1;
388
        $column->isComputed = (bool)$info['is_computed'];
0 ignored issues
show
Bug Best Practice introduced by
The property isComputed does not exist on yii\db\ColumnSchema. Since you implemented __set, consider adding a @property annotation.
Loading history...
389
        $column->unsigned = stripos($column->dbType, 'unsigned') !== false;
390
        $column->comment = $info['comment'] === null ? '' : $info['comment'];
391
392
        $column->type = self::TYPE_STRING;
393
        if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) {
394
            $type = $matches[1];
395
            if (isset($this->typeMap[$type])) {
396
                $column->type = $this->typeMap[$type];
397
            }
398
399
            if ($isVersion2017orLater && $type === 'bit') {
400
                $column->type = 'boolean';
401
            }
402
403
            if (!empty($matches[2])) {
404
                $values = explode(',', $matches[2]);
405
                $column->size = $column->precision = (int) $values[0];
406
407
                if (isset($values[1])) {
408
                    $column->scale = (int) $values[1];
409
                }
410
411
                if ($isVersion2017orLater === false) {
412
                    $column->type = $this->booleanTypeLegacy($column->size, $type);
413
                }
414
            }
415
        }
416
417
        $column->phpType = $this->getColumnPhpType($column);
418
419
        if ($info['column_default'] === '(NULL)') {
420
            $info['column_default'] = null;
421
        }
422
        if (!$column->isPrimaryKey && ($column->type !== 'timestamp' || $info['column_default'] !== 'CURRENT_TIMESTAMP')) {
423
            $column->defaultValue = $column->defaultPhpTypecast($info['column_default']);
0 ignored issues
show
Bug introduced by
The method defaultPhpTypecast() does not exist on yii\db\ColumnSchema. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

423
            /** @scrutinizer ignore-call */ 
424
            $column->defaultValue = $column->defaultPhpTypecast($info['column_default']);
Loading history...
424
        }
425
426
        return $column;
427
    }
428
429
    /**
430
     * Collects the metadata of table columns.
431
     * @param TableSchema $table the table metadata
432
     * @return bool whether the table exists in the database
433
     */
434
    protected function findColumns($table)
435
    {
436
        $columnsTableName = 'INFORMATION_SCHEMA.COLUMNS';
437
        $whereSql = '[t1].[table_name] = ' . $this->db->quoteValue($table->name);
438
        if ($table->catalogName !== null) {
439
            $columnsTableName = "{$table->catalogName}.{$columnsTableName}";
440
            $whereSql .= " AND [t1].[table_catalog] = '{$table->catalogName}'";
441
        }
442
        if ($table->schemaName !== null) {
443
            $whereSql .= " AND [t1].[table_schema] = '{$table->schemaName}'";
444
        }
445
        $columnsTableName = $this->quoteTableName($columnsTableName);
446
447
        $sql = <<<SQL
448
SELECT
449
 [t1].[column_name],
450
 [t1].[is_nullable],
451
 CASE WHEN [t1].[data_type] IN ('char','varchar','nchar','nvarchar','binary','varbinary') THEN
452
    CASE WHEN [t1].[character_maximum_length] = NULL OR [t1].[character_maximum_length] = -1 THEN
453
        [t1].[data_type]
454
    ELSE
455
        [t1].[data_type] + '(' + LTRIM(RTRIM(CONVERT(CHAR,[t1].[character_maximum_length]))) + ')'
456
    END
457
 ELSE
458
    [t1].[data_type]
459
 END AS 'data_type',
460
 [t1].[column_default],
461
 COLUMNPROPERTY(OBJECT_ID([t1].[table_schema] + '.' + [t1].[table_name]), [t1].[column_name], 'IsIdentity') AS is_identity,
462
 COLUMNPROPERTY(OBJECT_ID([t1].[table_schema] + '.' + [t1].[table_name]), [t1].[column_name], 'IsComputed') AS is_computed,
463
 (
464
    SELECT CONVERT(VARCHAR, [t2].[value])
465
		FROM [sys].[extended_properties] AS [t2]
466
		WHERE
467
			[t2].[class] = 1 AND
468
			[t2].[class_desc] = 'OBJECT_OR_COLUMN' AND
469
			[t2].[name] = 'MS_Description' AND
470
			[t2].[major_id] = OBJECT_ID([t1].[TABLE_SCHEMA] + '.' + [t1].[table_name]) AND
471
			[t2].[minor_id] = COLUMNPROPERTY(OBJECT_ID([t1].[TABLE_SCHEMA] + '.' + [t1].[TABLE_NAME]), [t1].[COLUMN_NAME], 'ColumnID')
472
 ) as comment
473
FROM {$columnsTableName} AS [t1]
474
WHERE {$whereSql}
475
SQL;
476
477
        try {
478
            $columns = $this->db->createCommand($sql)->queryAll();
479
            if (empty($columns)) {
480
                return false;
481
            }
482
        } catch (\Exception $e) {
483
            return false;
484
        }
485
        foreach ($columns as $column) {
486
            $column = $this->loadColumnSchema($column);
487
            foreach ($table->primaryKey as $primaryKey) {
488
                if (strcasecmp($column->name, $primaryKey) === 0) {
489
                    $column->isPrimaryKey = true;
490
                    break;
491
                }
492
            }
493
            if ($column->isPrimaryKey && $column->autoIncrement) {
494
                $table->sequenceName = '';
495
            }
496
            $table->columns[$column->name] = $column;
497
        }
498
499
        return true;
500
    }
501
502
    /**
503
     * Collects the constraint details for the given table and constraint type.
504
     * @param TableSchema $table
505
     * @param string $type either PRIMARY KEY or UNIQUE
506
     * @return array each entry contains index_name and field_name
507
     * @since 2.0.4
508
     */
509
    protected function findTableConstraints($table, $type)
510
    {
511
        $keyColumnUsageTableName = 'INFORMATION_SCHEMA.KEY_COLUMN_USAGE';
512
        $tableConstraintsTableName = 'INFORMATION_SCHEMA.TABLE_CONSTRAINTS';
513
        if ($table->catalogName !== null) {
514
            $keyColumnUsageTableName = $table->catalogName . '.' . $keyColumnUsageTableName;
515
            $tableConstraintsTableName = $table->catalogName . '.' . $tableConstraintsTableName;
516
        }
517
        $keyColumnUsageTableName = $this->quoteTableName($keyColumnUsageTableName);
518
        $tableConstraintsTableName = $this->quoteTableName($tableConstraintsTableName);
519
520
        $sql = <<<SQL
521
SELECT
522
    [kcu].[constraint_name] AS [index_name],
523
    [kcu].[column_name] AS [field_name]
524
FROM {$keyColumnUsageTableName} AS [kcu]
525
LEFT JOIN {$tableConstraintsTableName} AS [tc] ON
526
    [kcu].[table_schema] = [tc].[table_schema] AND
527
    [kcu].[table_name] = [tc].[table_name] AND
528
    [kcu].[constraint_name] = [tc].[constraint_name]
529
WHERE
530
    [tc].[constraint_type] = :type AND
531
    [kcu].[table_name] = :tableName AND
532
    [kcu].[table_schema] = :schemaName
533
SQL;
534
535
        return $this->db
536
            ->createCommand($sql, [
537
                ':tableName' => $table->name,
538
                ':schemaName' => $table->schemaName,
539
                ':type' => $type,
540
            ])
541
            ->queryAll();
542
    }
543
544
    /**
545
     * Collects the primary key column details for the given table.
546
     * @param TableSchema $table the table metadata
547
     */
548
    protected function findPrimaryKeys($table)
549
    {
550
        $result = [];
551
        foreach ($this->findTableConstraints($table, 'PRIMARY KEY') as $row) {
552
            $result[] = $row['field_name'];
553
        }
554
        $table->primaryKey = $result;
555
    }
556
557
    /**
558
     * Collects the foreign key column details for the given table.
559
     * @param TableSchema $table the table metadata
560
     */
561
    protected function findForeignKeys($table)
562
    {
563
        $object = $table->name;
564
        if ($table->schemaName !== null) {
565
            $object = $table->schemaName . '.' . $object;
566
        }
567
        if ($table->catalogName !== null) {
568
            $object = $table->catalogName . '.' . $object;
569
        }
570
571
        // please refer to the following page for more details:
572
        // http://msdn2.microsoft.com/en-us/library/aa175805(SQL.80).aspx
573
        $sql = <<<'SQL'
574
SELECT
575
	[fk].[name] AS [fk_name],
576
	[cp].[name] AS [fk_column_name],
577
	OBJECT_NAME([fk].[referenced_object_id]) AS [uq_table_name],
578
	[cr].[name] AS [uq_column_name]
579
FROM
580
	[sys].[foreign_keys] AS [fk]
581
	INNER JOIN [sys].[foreign_key_columns] AS [fkc] ON
582
		[fk].[object_id] = [fkc].[constraint_object_id]
583
	INNER JOIN [sys].[columns] AS [cp] ON
584
		[fk].[parent_object_id] = [cp].[object_id] AND
585
		[fkc].[parent_column_id] = [cp].[column_id]
586
	INNER JOIN [sys].[columns] AS [cr] ON
587
		[fk].[referenced_object_id] = [cr].[object_id] AND
588
		[fkc].[referenced_column_id] = [cr].[column_id]
589
WHERE
590
	[fk].[parent_object_id] = OBJECT_ID(:object)
591
SQL;
592
593
        $rows = $this->db->createCommand($sql, [
594
            ':object' => $object,
595
        ])->queryAll();
596
597
        $table->foreignKeys = [];
598
        foreach ($rows as $row) {
599
            if (!isset($table->foreignKeys[$row['fk_name']])) {
600
                $table->foreignKeys[$row['fk_name']][] = $row['uq_table_name'];
601
            }
602
            $table->foreignKeys[$row['fk_name']][$row['fk_column_name']] = $row['uq_column_name'];
603
        }
604
    }
605
606
    /**
607
     * {@inheritdoc}
608
     */
609
    protected function findViewNames($schema = '')
610
    {
611
        if ($schema === '') {
612
            $schema = $this->defaultSchema;
613
        }
614
615
        $sql = <<<'SQL'
616
SELECT [t].[table_name]
617
FROM [INFORMATION_SCHEMA].[TABLES] AS [t]
618
WHERE [t].[table_schema] = :schema AND [t].[table_type] = 'VIEW'
619
ORDER BY [t].[table_name]
620
SQL;
621
622
        return $this->db->createCommand($sql, [':schema' => $schema])->queryColumn();
623
    }
624
625
    /**
626
     * Returns all unique indexes for the given table.
627
     *
628
     * Each array element is of the following structure:
629
     *
630
     * ```php
631
     * [
632
     *     'IndexName1' => ['col1' [, ...]],
633
     *     'IndexName2' => ['col2' [, ...]],
634
     * ]
635
     * ```
636
     *
637
     * @param TableSchema $table the table metadata
638
     * @return array all unique indexes for the given table.
639
     * @since 2.0.4
640
     */
641
    public function findUniqueIndexes($table)
642
    {
643
        $result = [];
644
        foreach ($this->findTableConstraints($table, 'UNIQUE') as $row) {
645
            $result[$row['index_name']][] = $row['field_name'];
646
        }
647
648
        return $result;
649
    }
650
651
    /**
652
     * Loads multiple types of constraints and returns the specified ones.
653
     * @param string $tableName table name.
654
     * @param string $returnType return type:
655
     * - primaryKey
656
     * - foreignKeys
657
     * - uniques
658
     * - checks
659
     * - defaults
660
     * @return mixed constraints.
661
     */
662
    private function loadTableConstraints($tableName, $returnType)
663
    {
664
        static $sql = <<<'SQL'
665
SELECT
666
    [o].[name] AS [name],
667
    COALESCE([ccol].[name], [dcol].[name], [fccol].[name], [kiccol].[name]) AS [column_name],
668
    RTRIM([o].[type]) AS [type],
669
    OBJECT_SCHEMA_NAME([f].[referenced_object_id]) AS [foreign_table_schema],
670
    OBJECT_NAME([f].[referenced_object_id]) AS [foreign_table_name],
671
    [ffccol].[name] AS [foreign_column_name],
672
    [f].[update_referential_action_desc] AS [on_update],
673
    [f].[delete_referential_action_desc] AS [on_delete],
674
    [c].[definition] AS [check_expr],
675
    [d].[definition] AS [default_expr]
676
FROM (SELECT OBJECT_ID(:fullName) AS [object_id]) AS [t]
677
INNER JOIN [sys].[objects] AS [o]
678
    ON [o].[parent_object_id] = [t].[object_id] AND [o].[type] IN ('PK', 'UQ', 'C', 'D', 'F')
679
LEFT JOIN [sys].[check_constraints] AS [c]
680
    ON [c].[object_id] = [o].[object_id]
681
LEFT JOIN [sys].[columns] AS [ccol]
682
    ON [ccol].[object_id] = [c].[parent_object_id] AND [ccol].[column_id] = [c].[parent_column_id]
683
LEFT JOIN [sys].[default_constraints] AS [d]
684
    ON [d].[object_id] = [o].[object_id]
685
LEFT JOIN [sys].[columns] AS [dcol]
686
    ON [dcol].[object_id] = [d].[parent_object_id] AND [dcol].[column_id] = [d].[parent_column_id]
687
LEFT JOIN [sys].[key_constraints] AS [k]
688
    ON [k].[object_id] = [o].[object_id]
689
LEFT JOIN [sys].[index_columns] AS [kic]
690
    ON [kic].[object_id] = [k].[parent_object_id] AND [kic].[index_id] = [k].[unique_index_id]
691
LEFT JOIN [sys].[columns] AS [kiccol]
692
    ON [kiccol].[object_id] = [kic].[object_id] AND [kiccol].[column_id] = [kic].[column_id]
693
LEFT JOIN [sys].[foreign_keys] AS [f]
694
    ON [f].[object_id] = [o].[object_id]
695
LEFT JOIN [sys].[foreign_key_columns] AS [fc]
696
    ON [fc].[constraint_object_id] = [o].[object_id]
697
LEFT JOIN [sys].[columns] AS [fccol]
698
    ON [fccol].[object_id] = [fc].[parent_object_id] AND [fccol].[column_id] = [fc].[parent_column_id]
699
LEFT JOIN [sys].[columns] AS [ffccol]
700
    ON [ffccol].[object_id] = [fc].[referenced_object_id] AND [ffccol].[column_id] = [fc].[referenced_column_id]
701
ORDER BY [kic].[key_ordinal] ASC, [fc].[constraint_column_id] ASC
702
SQL;
703
704
        $resolvedName = $this->resolveTableName($tableName);
705
        $constraints = $this->db->createCommand($sql, [
706
            ':fullName' => $resolvedName->fullName,
707
        ])->queryAll();
708
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
709
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
710
        $result = [
711
            'primaryKey' => null,
712
            'foreignKeys' => [],
713
            'uniques' => [],
714
            'checks' => [],
715
            'defaults' => [],
716
        ];
717
        foreach ($constraints as $type => $names) {
718
            foreach ($names as $name => $constraint) {
719
                switch ($type) {
720
                    case 'PK':
721
                        $result['primaryKey'] = new Constraint([
722
                            'name' => $name,
723
                            'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
724
                        ]);
725
                        break;
726
                    case 'F':
727
                        $result['foreignKeys'][] = new ForeignKeyConstraint([
728
                            'name' => $name,
729
                            'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
730
                            'foreignSchemaName' => $constraint[0]['foreign_table_schema'],
731
                            'foreignTableName' => $constraint[0]['foreign_table_name'],
732
                            'foreignColumnNames' => ArrayHelper::getColumn($constraint, 'foreign_column_name'),
733
                            'onDelete' => str_replace('_', '', $constraint[0]['on_delete']),
734
                            'onUpdate' => str_replace('_', '', $constraint[0]['on_update']),
735
                        ]);
736
                        break;
737
                    case 'UQ':
738
                        $result['uniques'][] = new Constraint([
739
                            'name' => $name,
740
                            'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
741
                        ]);
742
                        break;
743
                    case 'C':
744
                        $result['checks'][] = new CheckConstraint([
745
                            'name' => $name,
746
                            'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
747
                            'expression' => $constraint[0]['check_expr'],
748
                        ]);
749
                        break;
750
                    case 'D':
751
                        $result['defaults'][] = new DefaultValueConstraint([
752
                            'name' => $name,
753
                            'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
754
                            'value' => $constraint[0]['default_expr'],
755
                        ]);
756
                        break;
757
                }
758
            }
759
        }
760
        foreach ($result as $type => $data) {
761
            $this->setTableMetadata($tableName, $type, $data);
762
        }
763
764
        return $result[$returnType];
765
    }
766
767
    /**
768
     * {@inheritdoc}
769
     */
770
    public function quoteColumnName($name)
771
    {
772
        if (preg_match('/^\[.*\]$/', $name)) {
773
            return $name;
774
        }
775
776
        return parent::quoteColumnName($name);
777
    }
778
779
    /**
780
     * Retrieving inserted data from a primary key request of type uniqueidentifier (for SQL Server 2005 or later)
781
     * {@inheritdoc}
782
     */
783
    public function insert($table, $columns)
784
    {
785
        $command = $this->db->createCommand()->insert($table, $columns);
786
        if (!$command->execute()) {
787
            return false;
788
        }
789
790
        $isVersion2005orLater = version_compare($this->db->getSchema()->getServerVersion(), '9', '>=');
791
        $inserted = $isVersion2005orLater ? $command->pdoStatement->fetch() : [];
792
793
        $tableSchema = $this->getTableSchema($table);
794
        $result = [];
795
        foreach ($tableSchema->primaryKey as $name) {
796
            // @see https://github.com/yiisoft/yii2/issues/13828 & https://github.com/yiisoft/yii2/issues/17474
797
            if (isset($inserted[$name])) {
798
                $result[$name] = $inserted[$name];
799
            } elseif ($tableSchema->columns[$name]->autoIncrement) {
800
                // for a version earlier than 2005
801
                $result[$name] = $this->getLastInsertID($tableSchema->sequenceName);
802
            } elseif (isset($columns[$name])) {
803
                $result[$name] = $columns[$name];
804
            } else {
805
                $result[$name] = $tableSchema->columns[$name]->defaultValue;
806
            }
807
        }
808
809
        return $result;
810
    }
811
812
    /**
813
     * {@inheritdoc}
814
     */
815
    public function createColumnSchemaBuilder($type, $length = null)
816
    {
817
        return Yii::createObject(ColumnSchemaBuilder::className(), [$type, $length, $this->db]);
0 ignored issues
show
Deprecated Code introduced by
The function yii\base\BaseObject::className() has been deprecated: since 2.0.14. On PHP >=5.5, use `::class` instead. ( Ignorable by Annotation )

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

817
        return Yii::createObject(/** @scrutinizer ignore-deprecated */ ColumnSchemaBuilder::className(), [$type, $length, $this->db]);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
818
    }
819
820
    /**
821
     * Assigns a type boolean for the column type bit, for legacy versions of MSSQL.
822
     *
823
     * @param int $size column size.
824
     * @param string $type column type.
825
     *
826
     * @return string column type.
827
     */
828
    private function booleanTypeLegacy($size, $type)
829
    {
830
        if ($size === 1 && ($type === 'tinyint' || $type === 'bit')) {
831
            return 'boolean';
832
        } elseif ($type === 'bit') {
833
            if ($size > 32) {
834
                return 'bigint';
835
            } elseif ($size === 32) {
836
                return 'integer';
837
            }
838
        }
839
840
        return $type;
841
    }
842
}
843