Completed
Push — master ( f1a1d9...18cd95 )
by Paweł
30s queued 17s
created

Schema::createColumnSchemaBuilder()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 3
ccs 0
cts 0
cp 0
crap 2
rs 10
c 0
b 0
f 0
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\Constraint;
12
use yii\db\ConstraintFinderInterface;
13
use yii\db\ConstraintFinderTrait;
14
use yii\db\DefaultValueConstraint;
15
use yii\db\ForeignKeyConstraint;
16
use yii\db\IndexConstraint;
17
use yii\db\ViewFinderTrait;
18
use yii\helpers\ArrayHelper;
19
20
/**
21
 * Schema is the class for retrieving metadata from MS SQL Server databases (version 2008 and above).
22
 *
23
 * @author Timur Ruziev <[email protected]>
24
 * @since 2.0
25
 */
26
class Schema extends \yii\db\Schema implements ConstraintFinderInterface
27
{
28
    use ViewFinderTrait;
29
    use ConstraintFinderTrait;
30
31
    /**
32
     * {@inheritdoc}
33
     */
34
    public $columnSchemaClass = 'yii\db\mssql\ColumnSchema';
35
    /**
36
     * @var string the default schema used for the current session.
37
     */
38
    public $defaultSchema = 'dbo';
39
    /**
40
     * @var array mapping from physical column types (keys) to abstract column types (values)
41
     */
42
    public $typeMap = [
43
        // exact numbers
44
        'bigint' => self::TYPE_BIGINT,
45
        'numeric' => self::TYPE_DECIMAL,
46
        'bit' => self::TYPE_SMALLINT,
47
        'smallint' => self::TYPE_SMALLINT,
48
        'decimal' => self::TYPE_DECIMAL,
49
        'smallmoney' => self::TYPE_MONEY,
50
        'int' => self::TYPE_INTEGER,
51
        'tinyint' => self::TYPE_TINYINT,
52
        'money' => self::TYPE_MONEY,
53
        // approximate numbers
54
        'float' => self::TYPE_FLOAT,
55
        'double' => self::TYPE_DOUBLE,
56
        'real' => self::TYPE_FLOAT,
57
        // date and time
58
        'date' => self::TYPE_DATE,
59
        'datetimeoffset' => self::TYPE_DATETIME,
60
        'datetime2' => self::TYPE_DATETIME,
61
        'smalldatetime' => self::TYPE_DATETIME,
62
        'datetime' => self::TYPE_DATETIME,
63
        'time' => self::TYPE_TIME,
64
        // character strings
65
        'char' => self::TYPE_CHAR,
66
        'varchar' => self::TYPE_STRING,
67
        'text' => self::TYPE_TEXT,
68
        // unicode character strings
69
        'nchar' => self::TYPE_CHAR,
70
        'nvarchar' => self::TYPE_STRING,
71
        'ntext' => self::TYPE_TEXT,
72
        // binary strings
73
        'binary' => self::TYPE_BINARY,
74
        'varbinary' => self::TYPE_BINARY,
75
        'image' => self::TYPE_BINARY,
76
        // other data types
77
        // 'cursor' type cannot be used with tables
78
        'timestamp' => self::TYPE_TIMESTAMP,
79
        'hierarchyid' => self::TYPE_STRING,
80
        'uniqueidentifier' => self::TYPE_STRING,
81
        'sql_variant' => self::TYPE_STRING,
82
        'xml' => self::TYPE_STRING,
83
        'table' => self::TYPE_STRING,
84
    ];
85
86
    /**
87
     * {@inheritdoc}
88
     */
89
    protected $tableQuoteCharacter = ['[', ']'];
90
    /**
91
     * {@inheritdoc}
92
     */
93
    protected $columnQuoteCharacter = ['[', ']'];
94
95
96
    /**
97
     * Resolves the table name and schema name (if any).
98
     * @param string $name the table name
99
     * @return TableSchema resolved table, schema, etc. names.
100
     */
101
    protected function resolveTableName($name)
102
    {
103
        $resolvedName = new TableSchema();
104
        $parts = $this->getTableNameParts($name);
105
        $partCount = count($parts);
106
        if ($partCount === 4) {
107
            // server name, catalog name, schema name and table name passed
108
            $resolvedName->catalogName = $parts[1];
109
            $resolvedName->schemaName = $parts[2];
110
            $resolvedName->name = $parts[3];
111
            $resolvedName->fullName = $resolvedName->catalogName . '.' . $resolvedName->schemaName . '.' . $resolvedName->name;
112
        } elseif ($partCount === 3) {
113
            // catalog name, schema name and table name passed
114
            $resolvedName->catalogName = $parts[0];
115
            $resolvedName->schemaName = $parts[1];
116
            $resolvedName->name = $parts[2];
117
            $resolvedName->fullName = $resolvedName->catalogName . '.' . $resolvedName->schemaName . '.' . $resolvedName->name;
118
        } elseif ($partCount === 2) {
119
            // only schema name and table name passed
120
            $resolvedName->schemaName = $parts[0];
121
            $resolvedName->name = $parts[1];
122
            $resolvedName->fullName = ($resolvedName->schemaName !== $this->defaultSchema ? $resolvedName->schemaName . '.' : '') . $resolvedName->name;
123
        } else {
124
            // only table name passed
125
            $resolvedName->schemaName = $this->defaultSchema;
126
            $resolvedName->fullName = $resolvedName->name = $parts[0];
127
        }
128
129
        return $resolvedName;
130
    }
131
132
    /**
133
     * {@inheritDoc}
134
     * @param string $name
135
     * @return array
136
     * @since 2.0.22
137
     */
138
    protected function getTableNameParts($name)
139
    {
140
        $parts = [$name];
141
        preg_match_all('/([^.\[\]]+)|\[([^\[\]]+)\]/', $name, $matches);
142
        if (isset($matches[0]) && is_array($matches[0]) && !empty($matches[0])) {
143
            $parts = $matches[0];
144
        }
145
146
        $parts = str_replace(['[', ']'], '', $parts);
147
148
        return $parts;
149
    }
150
151
    /**
152
     * {@inheritdoc}
153
     * @see https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-database-principals-transact-sql
154
     */
155
    protected function findSchemaNames()
156
    {
157
        static $sql = <<<'SQL'
158
SELECT [s].[name]
159
FROM [sys].[schemas] AS [s]
160
INNER JOIN [sys].[database_principals] AS [p] ON [p].[principal_id] = [s].[principal_id]
161
WHERE [p].[is_fixed_role] = 0 AND [p].[sid] IS NOT NULL
162
ORDER BY [s].[name] ASC
163
SQL;
164
165
        return $this->db->createCommand($sql)->queryColumn();
166
    }
167
168
    /**
169
     * {@inheritdoc}
170
     */
171
    protected function findTableNames($schema = '')
172
    {
173
        if ($schema === '') {
174
            $schema = $this->defaultSchema;
175
        }
176
177
        $sql = <<<'SQL'
178
SELECT [t].[table_name]
179
FROM [INFORMATION_SCHEMA].[TABLES] AS [t]
180
WHERE [t].[table_schema] = :schema AND [t].[table_type] IN ('BASE TABLE', 'VIEW')
181
ORDER BY [t].[table_name]
182
SQL;
183
        return $this->db->createCommand($sql, [':schema' => $schema])->queryColumn();
184
    }
185
186
    /**
187
     * {@inheritdoc}
188
     */
189
    protected function loadTableSchema($name)
190
    {
191
        $table = new TableSchema();
192
        $this->resolveTableNames($table, $name);
193
        $this->findPrimaryKeys($table);
194
        if ($this->findColumns($table)) {
195
            $this->findForeignKeys($table);
196
            return $table;
197
        }
198
199
        return null;
200
    }
201
202
    /**
203
     * {@inheritdoc}
204
     */
205
    protected function getSchemaMetadata($schema, $type, $refresh)
206
    {
207
        $metadata = [];
208
        $methodName = 'getTable' . ucfirst($type);
209
        $tableNames = array_map(function ($table) {
210
            return $this->quoteSimpleTableName($table);
211
        }, $this->getTableNames($schema, $refresh));
212
        foreach ($tableNames as $name) {
213
            if ($schema !== '') {
214
                $name = $schema . '.' . $name;
215
            }
216
            $tableMetadata = $this->$methodName($name, $refresh);
217
            if ($tableMetadata !== null) {
218
                $metadata[] = $tableMetadata;
219
            }
220
        }
221
222
        return $metadata;
223
    }
224
225
    /**
226
     * {@inheritdoc}
227
     */
228
    protected function loadTablePrimaryKey($tableName)
229
    {
230
        return $this->loadTableConstraints($tableName, 'primaryKey');
231
    }
232
233
    /**
234
     * {@inheritdoc}
235
     */
236
    protected function loadTableForeignKeys($tableName)
237
    {
238
        return $this->loadTableConstraints($tableName, 'foreignKeys');
239
    }
240
241
    /**
242
     * {@inheritdoc}
243
     */
244
    protected function loadTableIndexes($tableName)
245
    {
246
        static $sql = <<<'SQL'
247
SELECT
248
    [i].[name] AS [name],
249
    [iccol].[name] AS [column_name],
250
    [i].[is_unique] AS [index_is_unique],
251
    [i].[is_primary_key] AS [index_is_primary]
252
FROM [sys].[indexes] AS [i]
253
INNER JOIN [sys].[index_columns] AS [ic]
254
    ON [ic].[object_id] = [i].[object_id] AND [ic].[index_id] = [i].[index_id]
255
INNER JOIN [sys].[columns] AS [iccol]
256
    ON [iccol].[object_id] = [ic].[object_id] AND [iccol].[column_id] = [ic].[column_id]
257
WHERE [i].[object_id] = OBJECT_ID(:fullName)
258
ORDER BY [ic].[key_ordinal] ASC
259
SQL;
260
261
        $resolvedName = $this->resolveTableName($tableName);
262
        $indexes = $this->db->createCommand($sql, [
263
            ':fullName' => $resolvedName->fullName,
264
        ])->queryAll();
265
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
266
        $indexes = ArrayHelper::index($indexes, null, 'name');
267
        $result = [];
268
        foreach ($indexes as $name => $index) {
269
            $result[] = new IndexConstraint([
270
                'isPrimary' => (bool)$index[0]['index_is_primary'],
271
                'isUnique' => (bool)$index[0]['index_is_unique'],
272
                'name' => $name,
273
                'columnNames' => ArrayHelper::getColumn($index, 'column_name'),
274
            ]);
275
        }
276
277
        return $result;
278
    }
279
280
    /**
281
     * {@inheritdoc}
282
     */
283
    protected function loadTableUniques($tableName)
284
    {
285
        return $this->loadTableConstraints($tableName, 'uniques');
286
    }
287
288
    /**
289
     * {@inheritdoc}
290
     */
291
    protected function loadTableChecks($tableName)
292
    {
293
        return $this->loadTableConstraints($tableName, 'checks');
294
    }
295
296
    /**
297
     * {@inheritdoc}
298
     */
299
    protected function loadTableDefaultValues($tableName)
300
    {
301
        return $this->loadTableConstraints($tableName, 'defaults');
302
    }
303
304
    /**
305
     * {@inheritdoc}
306
     */
307
    public function createSavepoint($name)
308
    {
309
        $this->db->createCommand("SAVE TRANSACTION $name")->execute();
310
    }
311
312
    /**
313
     * {@inheritdoc}
314
     */
315
    public function releaseSavepoint($name)
316
    {
317
        // does nothing as MSSQL does not support this
318
    }
319
320
    /**
321
     * {@inheritdoc}
322
     */
323
    public function rollBackSavepoint($name)
324
    {
325
        $this->db->createCommand("ROLLBACK TRANSACTION $name")->execute();
326
    }
327
328
    /**
329
     * Creates a query builder for the MSSQL database.
330
     * @return QueryBuilder query builder interface.
331
     */
332
    public function createQueryBuilder()
333
    {
334
        return new QueryBuilder($this->db);
335
    }
336
337
    /**
338
     * Resolves the table name and schema name (if any).
339
     * @param TableSchema $table the table metadata object
340
     * @param string $name the table name
341
     */
342
    protected function resolveTableNames($table, $name)
343
    {
344
        $parts = $this->getTableNameParts($name);
345
        $partCount = count($parts);
346
        if ($partCount === 4) {
347
            // server name, catalog name, schema name and table name passed
348
            $table->catalogName = $parts[1];
349
            $table->schemaName = $parts[2];
350
            $table->name = $parts[3];
351
            $table->fullName = $table->catalogName . '.' . $table->schemaName . '.' . $table->name;
352
        } elseif ($partCount === 3) {
353
            // catalog name, schema name and table name passed
354
            $table->catalogName = $parts[0];
355
            $table->schemaName = $parts[1];
356
            $table->name = $parts[2];
357
            $table->fullName = $table->catalogName . '.' . $table->schemaName . '.' . $table->name;
358
        } elseif ($partCount === 2) {
359
            // only schema name and table name passed
360
            $table->schemaName = $parts[0];
361
            $table->name = $parts[1];
362
            $table->fullName = $table->schemaName !== $this->defaultSchema ? $table->schemaName . '.' . $table->name : $table->name;
363
        } else {
364
            // only table name passed
365
            $table->schemaName = $this->defaultSchema;
366
            $table->fullName = $table->name = $parts[0];
367
        }
368
    }
369
370
    /**
371
     * Loads the column information into a [[ColumnSchema]] object.
372
     * @param array $info column information
373
     * @return ColumnSchema the column schema object
374
     */
375
    protected function loadColumnSchema($info)
376
    {
377
        $column = $this->createColumnSchema();
378
379
        $column->name = $info['column_name'];
380
        $column->allowNull = $info['is_nullable'] === 'YES';
381
        $column->dbType = $info['data_type'];
382
        $column->enumValues = []; // mssql has only vague equivalents to enum
383
        $column->isPrimaryKey = null; // primary key will be determined in findColumns() method
384
        $column->autoIncrement = $info['is_identity'] == 1;
385
        $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...
386
        $column->unsigned = stripos($column->dbType, 'unsigned') !== false;
387
        $column->comment = $info['comment'] === null ? '' : $info['comment'];
388
389
        $column->type = self::TYPE_STRING;
390
        if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) {
391
            $type = $matches[1];
392
            if (isset($this->typeMap[$type])) {
393
                $column->type = $this->typeMap[$type];
394
            }
395
            if (!empty($matches[2])) {
396
                $values = explode(',', $matches[2]);
397
                $column->size = $column->precision = (int)$values[0];
398
                if (isset($values[1])) {
399
                    $column->scale = (int)$values[1];
400
                }
401
                if ($column->size === 1 && ($type === 'tinyint' || $type === 'bit')) {
402
                    $column->type = 'boolean';
403
                } elseif ($type === 'bit') {
404
                    if ($column->size > 32) {
405
                        $column->type = 'bigint';
406
                    } elseif ($column->size === 32) {
407
                        $column->type = 'integer';
408
                    }
409
                }
410
            }
411
        }
412
413
        $column->phpType = $this->getColumnPhpType($column);
414
415
        if ($info['column_default'] === '(NULL)') {
416
            $info['column_default'] = null;
417
        }
418
        if (!$column->isPrimaryKey && ($column->type !== 'timestamp' || $info['column_default'] !== 'CURRENT_TIMESTAMP')) {
419
            $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

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