Passed
Push — master ( e5e54a...8779b5 )
by Wilmer
10:52
created

Schema::loadTablePrimaryKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

}

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

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

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

Loading history...
Bug introduced by
Are you sure $resolvedName->fullName(...chemaName() . '.' : '') of type void can be used in concatenation? ( Ignorable by Annotation )

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

141
            /** @scrutinizer ignore-type */ $resolvedName->fullName(
Loading history...
142
                $resolvedName->getSchemaName() !== $this->defaultSchema ? $resolvedName->getSchemaName() . '.' : ''
143
            ) . $resolvedName->getName();
144
        } else {
145
            /* only table name passed */
146
            $resolvedName->schemaName($this->defaultSchema);
147
            $resolvedName->name($parts[0]);
148
            $resolvedName->fullName($resolvedName->getName());
149
        }
150
151
        return $resolvedName;
152
    }
153
154
    /**
155
     * Splits full table name into parts.
156
     *
157
     * @param string $name
158
     *
159
     * @return array
160
     */
161
    protected function getTableNameParts(string $name): array
162
    {
163
        $parts = [$name];
164
165
        preg_match_all('/([^.\[\]]+)|\[([^\[\]]+)]/', $name, $matches);
166
167
        if (isset($matches[0]) && is_array($matches[0]) && !empty($matches[0])) {
168
            $parts = $matches[0];
169
        }
170
171
        $parts = str_replace(['[', ']'], '', $parts);
172
173
        return $parts;
174
    }
175
176
    /**
177
     * Returns all schema names in the database, including the default one but not system schemas.
178
     *
179
     * This method should be overridden by child classes in order to support this feature because the default
180
     * implementation simply throws an exception.
181
     *
182
     * @throws Exception
183
     * @throws InvalidArgumentException
184
     * @throws InvalidConfigException
185
     *
186
     * @return array all schema names in the database, except system schemas.
187
     *
188
     * {@see https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-database-principals-transact-sql}
189
     */
190
    protected function findSchemaNames(): array
191
    {
192
        static $sql = <<<'SQL'
193
SELECT [s].[name]
194
FROM [sys].[schemas] AS [s]
195
INNER JOIN [sys].[database_principals] AS [p] ON [p].[principal_id] = [s].[principal_id]
196
WHERE [p].[is_fixed_role] = 0 AND [p].[sid] IS NOT NULL
197
ORDER BY [s].[name] ASC
198
SQL;
199
200
        return $this->getDb()->createCommand($sql)->queryColumn();
201
    }
202
203
    /**
204
     * Returns all table names in the database.
205
     *
206
     * This method should be overridden by child classes in order to support this feature because the default
207
     * implementation simply throws an exception.
208
     *
209
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
210
     *
211
     * @throws Exception
212
     * @throws InvalidArgumentException
213
     * @throws InvalidConfigException
214
     *
215
     * @return array all table names in the database. The names have NO schema name prefix.
216
     */
217
    protected function findTableNames(string $schema = ''): array
218
    {
219
        if ($schema === '') {
220
            $schema = $this->defaultSchema;
221
        }
222
223
        $sql = <<<'SQL'
224
SELECT [t].[table_name]
225
FROM [INFORMATION_SCHEMA].[TABLES] AS [t]
226
WHERE [t].[table_schema] = :schema AND [t].[table_type] IN ('BASE TABLE', 'VIEW')
227
ORDER BY [t].[table_name]
228
SQL;
229
230
        $tables = $this->getDb()->createCommand($sql, [':schema' => $schema])->queryColumn();
231
232
        $tables = array_map(static function ($item) {
233
            return '[' . $item . ']';
234
        }, $tables);
235
236
        return $tables;
237
    }
238
239
    /**
240
     * Loads the metadata for the specified table.
241
     *
242
     * @param string $name table name.
243
     *
244
     * @throws Exception
245
     * @throws InvalidArgumentException
246
     * @throws InvalidConfigException
247
     * @throws NotSupportedException
248
     *
249
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
250
     */
251
    protected function loadTableSchema(string $name): ?TableSchema
252
    {
253
        $table = new TableSchema();
254
255
        $this->resolveTableNames($table, $name);
256
        $this->findPrimaryKeys($table);
257
258
        if ($this->findColumns($table)) {
259
            $this->findForeignKeys($table);
260
261
            return $table;
262
        }
263
264
        return null;
265
    }
266
267
    /**
268
     * Loads a primary key for the given table.
269
     *
270
     * @param string $tableName table name.
271
     *
272
     * @throws Exception
273
     * @throws InvalidArgumentException
274
     * @throws InvalidConfigException
275
     *
276
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.
277
     */
278
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
279
    {
280
        return $this->loadTableConstraints($tableName, 'primaryKey');
281
    }
282
283
    /**
284
     * Loads all foreign keys for the given table.
285
     *
286
     * @param string $tableName table name.
287
     *
288
     * @throws Exception
289
     * @throws InvalidArgumentException
290
     * @throws InvalidConfigException
291
     *
292
     * @return ForeignKeyConstraint[] foreign keys for the given table.
293
     */
294
    protected function loadTableForeignKeys(string $tableName): array
295
    {
296
        return $this->loadTableConstraints($tableName, 'foreignKeys');
297
    }
298
299
    /**
300
     * Loads all indexes for the given table.
301
     *
302
     * @param string $tableName table name.
303
     *
304
     * @throws Exception
305
     * @throws InvalidArgumentException
306
     * @throws InvalidConfigException
307
     *
308
     * @return IndexConstraint[] indexes for the given table.
309
     */
310
    protected function loadTableIndexes(string $tableName): array
311
    {
312
        static $sql = <<<'SQL'
313
SELECT
314
    [i].[name] AS [name],
315
    [iccol].[name] AS [column_name],
316
    [i].[is_unique] AS [index_is_unique],
317
    [i].[is_primary_key] AS [index_is_primary]
318
FROM [sys].[indexes] AS [i]
319
INNER JOIN [sys].[index_columns] AS [ic]
320
    ON [ic].[object_id] = [i].[object_id] AND [ic].[index_id] = [i].[index_id]
321
INNER JOIN [sys].[columns] AS [iccol]
322
    ON [iccol].[object_id] = [ic].[object_id] AND [iccol].[column_id] = [ic].[column_id]
323
WHERE [i].[object_id] = OBJECT_ID(:fullName)
324
ORDER BY [ic].[key_ordinal] ASC
325
SQL;
326
327
        $resolvedName = $this->resolveTableName($tableName);
328
        $indexes = $this->getDb()->createCommand($sql, [':fullName' => $resolvedName->getFullName()])->queryAll();
329
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
330
        $indexes = ArrayHelper::index($indexes, null, 'name');
331
332
        $result = [];
333
334
        foreach ($indexes as $name => $index) {
335
            $result[] = (new IndexConstraint())
336
                ->primary((bool) $index[0]['index_is_primary'])
337
                ->unique((bool) $index[0]['index_is_unique'])
338
                ->name($name)
339
                ->columnNames(ArrayHelper::getColumn($index, 'column_name'));
340
        }
341
342
        return $result;
343
    }
344
345
    /**
346
     * Loads all unique constraints for the given table.
347
     *
348
     * @param string $tableName table name.
349
     *
350
     * @throws Exception
351
     * @throws InvalidArgumentException
352
     * @throws InvalidConfigException
353
     *
354
     * @return Constraint[] unique constraints for the given table.
355
     */
356
    protected function loadTableUniques(string $tableName): array
357
    {
358
        return $this->loadTableConstraints($tableName, 'uniques');
359
    }
360
361
    /**
362
     * Loads all check constraints for the given table.
363
     *
364
     * @param string $tableName table name.
365
     *
366
     * @throws Exception
367
     * @throws InvalidArgumentException
368
     * @throws InvalidConfigException
369
     *
370
     * @return CheckConstraint[] check constraints for the given table.
371
     */
372
    protected function loadTableChecks(string $tableName): array
373
    {
374
        return $this->loadTableConstraints($tableName, 'checks');
375
    }
376
377
    /**
378
     * Loads all default value constraints for the given table.
379
     *
380
     * @param string $tableName table name.
381
     *
382
     * @throws Exception
383
     * @throws InvalidArgumentException
384
     * @throws InvalidConfigException
385
     *
386
     * @return DefaultValueConstraint[] default value constraints for the given table.
387
     */
388
    protected function loadTableDefaultValues(string $tableName): array
389
    {
390
        return $this->loadTableConstraints($tableName, 'defaults');
391
    }
392
393
    /**
394
     * Creates a new savepoint.
395
     *
396
     * @param string $name the savepoint name.
397
     *
398
     * @throws Exception
399
     * @throws InvalidConfigException
400
     */
401
    public function createSavepoint(string $name): void
402
    {
403
        $this->getDb()->createCommand("SAVE TRANSACTION $name")->execute();
404
    }
405
406
    /**
407
     * Releases an existing savepoint.
408
     *
409
     * @param string $name the savepoint name.
410
     */
411
    public function releaseSavepoint(string $name): void
412
    {
413
        /* does nothing as MSSQL does not support this */
414
    }
415
416
    /**
417
     * Rolls back to a previously created savepoint.
418
     *
419
     * @param string $name the savepoint name.
420
     *
421
     * @throws Exception
422
     * @throws InvalidConfigException
423
     */
424
    public function rollBackSavepoint(string $name): void
425
    {
426
        $this->getDb()->createCommand("ROLLBACK TRANSACTION $name")->execute();
427
    }
428
429
    /**
430
     * Creates a column schema for the database.
431
     *
432
     * This method may be overridden by child classes to create a DBMS-specific column schema.
433
     *
434
     * @return ColumnSchema column schema instance.
435
     */
436
    protected function createColumnSchema(): ColumnSchema
437
    {
438
        return new ColumnSchema();
439
    }
440
441
    /**
442
     * Creates a query builder for the MSSQL database.
443
     *
444
     * @return QueryBuilder query builder interface.
445
     */
446
    public function createQueryBuilder(): QueryBuilder
447
    {
448
        return new QueryBuilder($this->getDb());
0 ignored issues
show
Bug introduced by
It seems like $this->getDb() can also be of type null; however, parameter $db of Yiisoft\Db\Mssql\QueryBuilder::__construct() does only seem to accept Yiisoft\Db\Connection\Connection, maybe add an additional type check? ( Ignorable by Annotation )

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

448
        return new QueryBuilder(/** @scrutinizer ignore-type */ $this->getDb());
Loading history...
449
    }
450
451
    /**
452
     * Resolves the table name and schema name (if any).
453
     *
454
     * @param TableSchema $table the table metadata object.
455
     * @param string $name the table name
456
     */
457
    protected function resolveTableNames(TableSchema $table, string $name)
458
    {
459
        $parts = $this->getTableNameParts($name);
460
461
        $partCount = count($parts);
462
463
        if ($partCount === 4) {
464
            /* server name, catalog name, schema name and table name passed */
465
            $table->catalogName($parts[1]);
466
            $table->schemaName($parts[2]);
467
            $table->name($parts[3]);
468
            $table->fullName($table->getCatalogName() . '.' . $table->getSchemaName() . '.' . $table->getName());
469
        } elseif ($partCount === 3) {
470
            /* catalog name, schema name and table name passed */
471
            $table->catalogName($parts[0]);
472
            $table->schemaName($parts[1]);
473
            $table->name($parts[2]);
474
            $table->fullName($table->getCatalogName() . '.' . $table->getSchemaName() . '.' . $table->getName());
475
        } elseif ($partCount === 2) {
476
            /* only schema name and table name passed */
477
            $table->schemaName($parts[0]);
478
            $table->name($parts[1]);
479
            $table->fullName(
480
                $table->getSchemaName() !== $this->defaultSchema
481
                ? $table->getSchemaName() . '.' . $table->getName() : $table->getName()
482
            );
483
        } else {
484
            /* only table name passed */
485
            $table->schemaName($this->defaultSchema);
486
            $table->name($parts[0]);
487
            $table->fullName($table->getName());
488
        }
489
    }
490
491
    /**
492
     * Loads the column information into a {@see ColumnSchema} object.
493
     *
494
     * @param array $info column information.
495
     *
496
     * @return ColumnSchema the column schema object.
497
     */
498
    protected function loadColumnSchema(array $info): ColumnSchema
499
    {
500
        $column = $this->createColumnSchema();
501
502
        $column->name($info['column_name']);
503
        $column->allowNull($info['is_nullable'] === 'YES');
504
        $column->dbType($info['data_type']);
505
        $column->enumValues([]); // mssql has only vague equivalents to enum
506
        $column->primaryKey(false); // primary key will be determined in findColumns() method
507
        $column->autoIncrement($info['is_identity'] == 1);
508
        $column->unsigned(stripos($column->getDbType(), 'unsigned') !== false);
509
        $column->comment($info['comment'] ?? '');
510
        $column->type(self::TYPE_STRING);
511
512
        if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->getDbType(), $matches)) {
513
            $type = $matches[1];
514
515
            if (isset($this->typeMap[$type])) {
516
                $column->type($this->typeMap[$type]);
517
            }
518
519
            if (!empty($matches[2])) {
520
                $values = explode(',', $matches[2]);
521
                $column->precision((int) $values[0]);
522
                $column->size((int) $values[0]);
523
524
                if (isset($values[1])) {
525
                    $column->scale((int) $values[1]);
526
                }
527
528
                if ($column->getSize() === 1 && ($type === 'tinyint' || $type === 'bit')) {
529
                    $column->type('boolean');
530
                } elseif ($type === 'bit') {
531
                    if ($column->getSize() > 32) {
532
                        $column->type('bigint');
533
                    } elseif ($column->getSize() === 32) {
534
                        $column->type('integer');
535
                    }
536
                }
537
            }
538
        }
539
540
        $column->phpType($this->getColumnPhpType($column));
541
542
        if ($info['column_default'] === '(NULL)') {
543
            $info['column_default'] = null;
544
        }
545
546
        if (!$column->isPrimaryKey() && ($column->getType() !== 'timestamp' || $info['column_default'] !== 'CURRENT_TIMESTAMP')) {
547
            $column->defaultValue($column->defaultPhpTypecast($info['column_default']));
548
        }
549
550
        return $column;
551
    }
552
553
    /**
554
     * Collects the metadata of table columns.
555
     *
556
     * @param TableSchema $table the table metadata.
557
     *
558
     * @throws Exception
559
     * @throws InvalidConfigException
560
     * @throws NotSupportedException
561
     *
562
     * @return bool whether the table exists in the database.
563
     */
564
    protected function findColumns(TableSchema $table): bool
565
    {
566
        $columnsTableName = 'INFORMATION_SCHEMA.COLUMNS';
567
        $whereSql = "[t1].[table_name] = " . $this->getDb()->quoteValue($table->getName());
568
569
        if ($table->getCatalogName() !== null) {
570
            $columnsTableName = "{$table->getCatalogName()}.{$columnsTableName}";
571
            $whereSql .= " AND [t1].[table_catalog] = '{$table->getCatalogName()}'";
572
        }
573
574
        if ($table->getSchemaName() !== null) {
575
            $whereSql .= " AND [t1].[table_schema] = '{$table->getSchemaName()}'";
576
        }
577
578
        $columnsTableName = $this->quoteTableName($columnsTableName);
579
580
        $sql = <<<SQL
581
SELECT
582
 [t1].[column_name],
583
 [t1].[is_nullable],
584
 CASE WHEN [t1].[data_type] IN ('char','varchar','nchar','nvarchar','binary','varbinary') THEN
585
    CASE WHEN [t1].[character_maximum_length] = NULL OR [t1].[character_maximum_length] = -1 THEN
586
        [t1].[data_type]
587
    ELSE
588
        [t1].[data_type] + '(' + LTRIM(RTRIM(CONVERT(CHAR,[t1].[character_maximum_length]))) + ')'
589
    END
590
 ELSE
591
    [t1].[data_type]
592
 END AS 'data_type',
593
 [t1].[column_default],
594
 COLUMNPROPERTY(OBJECT_ID([t1].[table_schema] + '.' + [t1].[table_name]), [t1].[column_name], 'IsIdentity') AS is_identity,
595
 (
596
    SELECT CONVERT(VARCHAR, [t2].[value])
597
		FROM [sys].[extended_properties] AS [t2]
598
		WHERE
599
			[t2].[class] = 1 AND
600
			[t2].[class_desc] = 'OBJECT_OR_COLUMN' AND
601
			[t2].[name] = 'MS_Description' AND
602
			[t2].[major_id] = OBJECT_ID([t1].[TABLE_SCHEMA] + '.' + [t1].[table_name]) AND
603
			[t2].[minor_id] = COLUMNPROPERTY(OBJECT_ID([t1].[TABLE_SCHEMA] + '.' + [t1].[TABLE_NAME]), [t1].[COLUMN_NAME], 'ColumnID')
604
 ) as comment
605
FROM {$columnsTableName} AS [t1]
606
WHERE {$whereSql}
607
SQL;
608
609
        try {
610
            $columns = $this->getDb()->createCommand($sql)->queryAll();
611
612
            if (empty($columns)) {
613
                return false;
614
            }
615
        } catch (Exception $e) {
616
            return false;
617
        }
618
619
        foreach ($columns as $column) {
620
            $column = $this->loadColumnSchema($column);
621
            foreach ($table->getPrimaryKey() as $primaryKey) {
622
                if (strcasecmp($column->getName(), $primaryKey) === 0) {
623
                    $column->primaryKey(true);
624
                    break;
625
                }
626
            }
627
628
            if ($column->isPrimaryKey() && $column->isAutoIncrement()) {
629
                $table->sequenceName('');
630
            }
631
632
            $table->columns($column->getName(), $column);
633
        }
634
635
        return true;
636
    }
637
638
    /**
639
     * Collects the constraint details for the given table and constraint type.
640
     *
641
     * @param TableSchema $table
642
     * @param string $type either PRIMARY KEY or UNIQUE.
643
     *
644
     * @throws Exception
645
     * @throws InvalidArgumentException
646
     * @throws InvalidConfigException
647
     *
648
     * @return array each entry contains index_name and field_name.
649
     */
650
    protected function findTableConstraints(TableSchema $table, string $type): array
651
    {
652
        $keyColumnUsageTableName = 'INFORMATION_SCHEMA.KEY_COLUMN_USAGE';
653
        $tableConstraintsTableName = 'INFORMATION_SCHEMA.TABLE_CONSTRAINTS';
654
655
        if ($table->getCatalogName() !== null) {
656
            $keyColumnUsageTableName = $table->getCatalogName() . '.' . $keyColumnUsageTableName;
657
            $tableConstraintsTableName = $table->getCatalogName() . '.' . $tableConstraintsTableName;
658
        }
659
660
        $keyColumnUsageTableName = $this->quoteTableName($keyColumnUsageTableName);
661
        $tableConstraintsTableName = $this->quoteTableName($tableConstraintsTableName);
662
663
        $sql = <<<SQL
664
SELECT
665
    [kcu].[constraint_name] AS [index_name],
666
    [kcu].[column_name] AS [field_name]
667
FROM {$keyColumnUsageTableName} AS [kcu]
668
LEFT JOIN {$tableConstraintsTableName} AS [tc] ON
669
    [kcu].[table_schema] = [tc].[table_schema] AND
670
    [kcu].[table_name] = [tc].[table_name] AND
671
    [kcu].[constraint_name] = [tc].[constraint_name]
672
WHERE
673
    [tc].[constraint_type] = :type AND
674
    [kcu].[table_name] = :tableName AND
675
    [kcu].[table_schema] = :schemaName
676
SQL;
677
678
        return $this->getDb()->createCommand(
679
            $sql,
680
            [
681
                ':tableName' => $table->getName(),
682
                ':schemaName' => $table->getSchemaName(),
683
                ':type' => $type,
684
            ]
685
        )->queryAll();
686
    }
687
688
    /**
689
     * Collects the primary key column details for the given table.
690
     *
691
     * @param TableSchema $table the table metadata
692
     *
693
     * @throws Exception
694
     * @throws InvalidArgumentException
695
     * @throws InvalidConfigException
696
     */
697
    protected function findPrimaryKeys(TableSchema $table): void
698
    {
699
        $result = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
700
701
        foreach ($this->findTableConstraints($table, 'PRIMARY KEY') as $row) {
702
            $table->primaryKey($row['field_name']);
703
        }
704
    }
705
706
    /**
707
     * Collects the foreign key column details for the given table.
708
     *
709
     * @param TableSchema $table the table metadata
710
     *
711
     * @throws Exception
712
     * @throws InvalidArgumentException
713
     * @throws InvalidConfigException
714
     */
715
    protected function findForeignKeys(TableSchema $table): void
716
    {
717
        $object = $table->getName();
718
719
        if ($table->getSchemaName() !== null) {
720
            $object = $table->getSchemaName() . '.' . $object;
721
        }
722
723
        if ($table->getCatalogName() !== null) {
724
            $object = $table->getCatalogName() . '.' . $object;
725
        }
726
727
        /**
728
         * Please refer to the following page for more details:
729
         * {@see http://msdn2.microsoft.com/en-us/library/aa175805(SQL.80).aspx}
730
         */
731
        $sql = <<<'SQL'
732
SELECT
733
	[fk].[name] AS [fk_name],
734
	[cp].[name] AS [fk_column_name],
735
	OBJECT_NAME([fk].[referenced_object_id]) AS [uq_table_name],
736
	[cr].[name] AS [uq_column_name]
737
FROM
738
	[sys].[foreign_keys] AS [fk]
739
	INNER JOIN [sys].[foreign_key_columns] AS [fkc] ON
740
		[fk].[object_id] = [fkc].[constraint_object_id]
741
	INNER JOIN [sys].[columns] AS [cp] ON
742
		[fk].[parent_object_id] = [cp].[object_id] AND
743
		[fkc].[parent_column_id] = [cp].[column_id]
744
	INNER JOIN [sys].[columns] AS [cr] ON
745
		[fk].[referenced_object_id] = [cr].[object_id] AND
746
		[fkc].[referenced_column_id] = [cr].[column_id]
747
WHERE
748
	[fk].[parent_object_id] = OBJECT_ID(:object)
749
SQL;
750
751
        $rows = $this->getDb()->createCommand($sql, [':object' => $object])->queryAll();
752
753
        $table->foreignKeys([]);
754
755
        foreach ($rows as $row) {
756
            if (!isset($table->getForeignKeys()[$row['fk_name']])) {
757
                $fk[$row['fk_name']][] = $row['uq_table_name'];
758
                $table->foreignKeys($fk);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $fk seems to be defined later in this foreach loop on line 757. Are you sure it is defined here?
Loading history...
759
            }
760
761
            $fk[$row['fk_name']][$row['fk_column_name']] = $row['uq_column_name'];
762
            $table->foreignKeys($fk);
763
        }
764
    }
765
766
    /**
767
     * Returns all views names in the database.
768
     *
769
     * @param string $schema the schema of the views. Defaults to empty string, meaning the current or default schema.
770
     *
771
     * @throws Exception
772
     * @throws InvalidArgumentException
773
     * @throws InvalidConfigException
774
     *
775
     * @return array all views names in the database. The names have NO schema name prefix.
776
     */
777
    protected function findViewNames(string $schema = ''): array
778
    {
779
        if ($schema === '') {
780
            $schema = $this->defaultSchema;
781
        }
782
783
        $sql = <<<'SQL'
784
SELECT [t].[table_name]
785
FROM [INFORMATION_SCHEMA].[TABLES] AS [t]
786
WHERE [t].[table_schema] = :schema AND [t].[table_type] = 'VIEW'
787
ORDER BY [t].[table_name]
788
SQL;
789
790
        $views = $this->getDb()->createCommand($sql, [':schema' => $schema])->queryColumn();
791
        $views = array_map(static function ($item) {
792
            return '[' . $item . ']';
793
        }, $views);
794
795
        return $views;
796
    }
797
798
    /**
799
     * Returns all unique indexes for the given table.
800
     *
801
     * Each array element is of the following structure:
802
     *
803
     * ```php
804
     * [
805
     *     'IndexName1' => ['col1' [, ...]],
806
     *     'IndexName2' => ['col2' [, ...]],
807
     * ]
808
     * ```
809
     *
810
     * @param TableSchema $table the table metadata.
811
     *
812
     * @throws Exception
813
     * @throws InvalidArgumentException
814
     * @throws InvalidConfigException
815
     *
816
     * @return array all unique indexes for the given table.
817
     */
818
    public function findUniqueIndexes(TableSchema $table): array
819
    {
820
        $result = [];
821
822
        foreach ($this->findTableConstraints($table, 'UNIQUE') as $row) {
823
            $result[$row['index_name']][] = $row['field_name'];
824
        }
825
826
        return $result;
827
    }
828
829
    /**
830
     * Loads multiple types of constraints and returns the specified ones.
831
     *
832
     * @param string $tableName table name.
833
     * @param string $returnType return type:
834
     * - primaryKey
835
     * - foreignKeys
836
     * - uniques
837
     * - checks
838
     * - defaults
839
     *
840
     * @throws Exception
841
     * @throws InvalidArgumentException
842
     * @throws InvalidConfigException
843
     *
844
     * @return mixed constraints.
845
     */
846
    private function loadTableConstraints(string $tableName, string $returnType)
847
    {
848
        static $sql = <<<'SQL'
849
SELECT
850
    [o].[name] AS [name],
851
    COALESCE([ccol].[name], [dcol].[name], [fccol].[name], [kiccol].[name]) AS [column_name],
852
    RTRIM([o].[type]) AS [type],
853
    OBJECT_SCHEMA_NAME([f].[referenced_object_id]) AS [foreign_table_schema],
854
    OBJECT_NAME([f].[referenced_object_id]) AS [foreign_table_name],
855
    [ffccol].[name] AS [foreign_column_name],
856
    [f].[update_referential_action_desc] AS [on_update],
857
    [f].[delete_referential_action_desc] AS [on_delete],
858
    [c].[definition] AS [check_expr],
859
    [d].[definition] AS [default_expr]
860
FROM (SELECT OBJECT_ID(:fullName) AS [object_id]) AS [t]
861
INNER JOIN [sys].[objects] AS [o]
862
    ON [o].[parent_object_id] = [t].[object_id] AND [o].[type] IN ('PK', 'UQ', 'C', 'D', 'F')
863
LEFT JOIN [sys].[check_constraints] AS [c]
864
    ON [c].[object_id] = [o].[object_id]
865
LEFT JOIN [sys].[columns] AS [ccol]
866
    ON [ccol].[object_id] = [c].[parent_object_id] AND [ccol].[column_id] = [c].[parent_column_id]
867
LEFT JOIN [sys].[default_constraints] AS [d]
868
    ON [d].[object_id] = [o].[object_id]
869
LEFT JOIN [sys].[columns] AS [dcol]
870
    ON [dcol].[object_id] = [d].[parent_object_id] AND [dcol].[column_id] = [d].[parent_column_id]
871
LEFT JOIN [sys].[key_constraints] AS [k]
872
    ON [k].[object_id] = [o].[object_id]
873
LEFT JOIN [sys].[index_columns] AS [kic]
874
    ON [kic].[object_id] = [k].[parent_object_id] AND [kic].[index_id] = [k].[unique_index_id]
875
LEFT JOIN [sys].[columns] AS [kiccol]
876
    ON [kiccol].[object_id] = [kic].[object_id] AND [kiccol].[column_id] = [kic].[column_id]
877
LEFT JOIN [sys].[foreign_keys] AS [f]
878
    ON [f].[object_id] = [o].[object_id]
879
LEFT JOIN [sys].[foreign_key_columns] AS [fc]
880
    ON [fc].[constraint_object_id] = [o].[object_id]
881
LEFT JOIN [sys].[columns] AS [fccol]
882
    ON [fccol].[object_id] = [fc].[parent_object_id] AND [fccol].[column_id] = [fc].[parent_column_id]
883
LEFT JOIN [sys].[columns] AS [ffccol]
884
    ON [ffccol].[object_id] = [fc].[referenced_object_id] AND [ffccol].[column_id] = [fc].[referenced_column_id]
885
ORDER BY [kic].[key_ordinal] ASC, [fc].[constraint_column_id] ASC
886
SQL;
887
888
        $resolvedName = $this->resolveTableName($tableName);
889
        $constraints = $this->getDb()->createCommand($sql, [':fullName' => $resolvedName->getFullName()])->queryAll();
890
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
891
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
892
        $result = [
893
            'primaryKey' => null,
894
            'foreignKeys' => [],
895
            'uniques' => [],
896
            'checks' => [],
897
            'defaults' => [],
898
        ];
899
900
        foreach ($constraints as $type => $names) {
901
            foreach ($names as $name => $constraint) {
902
                switch ($type) {
903
                    case 'PK':
904
                        $result['primaryKey'] = (new Constraint())
905
                            ->name($name)
906
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
907
                        break;
908
                    case 'F':
909
                        $result['foreignKeys'][] = (new ForeignKeyConstraint())
910
                            ->name($name)
911
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
912
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
913
                            ->foreignTableName($constraint[0]['foreign_table_name'])
914
                            ->foreignColumnNames(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
915
                            ->onDelete(str_replace('_', '', $constraint[0]['on_delete']))
916
                            ->onUpdate(str_replace('_', '', $constraint[0]['on_update']));
917
                        break;
918
                    case 'UQ':
919
                        $result['uniques'][] = (new Constraint())
920
                            ->name($name)
921
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
922
                        break;
923
                    case 'C':
924
                        $result['checks'][] = (new CheckConstraint())
925
                            ->name($name)
926
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
927
                            ->expression($constraint[0]['check_expr']);
928
                        break;
929
                    case 'D':
930
                        $result['defaults'][] = (new DefaultValueConstraint())
931
                            ->name($name)
932
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
933
                            ->value($constraint[0]['default_expr']);
934
                        break;
935
                }
936
            }
937
        }
938
        foreach ($result as $type => $data) {
939
            $this->setTableMetadata($tableName, $type, $data);
940
        }
941
942
        return $result[$returnType];
943
    }
944
945
    /**
946
     * Quotes a column name for use in a query.
947
     *
948
     * If the column name contains prefix, the prefix will also be properly quoted. If the column name is already quoted
949
     * or contains '(', '[[' or '{{', then this method will do nothing.
950
     *
951
     * @param string $name column name.
952
     *
953
     * @return string the properly quoted column name.
954
     *
955
     * {@see quoteSimpleColumnName()}
956
     */
957
    public function quoteColumnName(string $name): string
958
    {
959
        if (preg_match('/^\[.*]$/', $name)) {
960
            return $name;
961
        }
962
963
        return parent::quoteColumnName($name);
964
    }
965
966
    /**
967
     * Executes the INSERT command, returning primary key values.
968
     *
969
     * @param string $table the table that new rows will be inserted into.
970
     * @param array $columns the column data (name => value) to be inserted into the table.
971
     *
972
     * @throws Exception
973
     * @throws InvalidConfigException
974
     * @throws NotSupportedException
975
     * @throws InvalidCallException
976
     *
977
     * @return array|false primary key values or false if the command fails.
978
     */
979
    public function insert(string $table, array $columns)
980
    {
981
        $command = $this->getDb()->createCommand()->insert($table, $columns);
982
        if (!$command->execute()) {
983
            return false;
984
        }
985
986
        $isVersion2005orLater = version_compare($this->getDb()->getSchema()->getServerVersion(), '9', '>=');
987
        $inserted = $isVersion2005orLater ? $command->getPdoStatement()->fetch() : [];
988
989
        $tableSchema = $this->getTableSchema($table);
990
991
        $result = [];
992
        foreach ($tableSchema->getPrimaryKey() as $name) {
993
            if ($tableSchema->getColumns()[$name]->isAutoIncrement()) {
994
                $result[$name] = $this->getLastInsertID($tableSchema->getSequenceName());
0 ignored issues
show
Bug introduced by
It seems like $tableSchema->getSequenceName() can also be of type null; however, parameter $sequenceName of Yiisoft\Db\Schema\Schema::getLastInsertID() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

994
                $result[$name] = $this->getLastInsertID(/** @scrutinizer ignore-type */ $tableSchema->getSequenceName());
Loading history...
995
                break;
996
            }
997
            /**
998
             * {@see https://github.com/yiisoft/yii2/issues/13828 & https://github.com/yiisoft/yii2/issues/17474
999
             */
1000
            if (isset($inserted[$name])) {
1001
                $result[$name] = $inserted[$name];
1002
            } elseif (isset($columns[$name])) {
1003
                $result[$name] = $columns[$name];
1004
            } else {
1005
                $result[$name] = $tableSchema->getColumns()[$name]->getDefaultValue();
1006
            }
1007
        }
1008
1009
        return $result;
1010
    }
1011
1012
    /**
1013
     * Create a column schema builder instance giving the type and value precision.
1014
     *
1015
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
1016
     *
1017
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
1018
     * @param int|string|array $length length or precision of the column. See {@see ColumnSchemaBuilder::$length}.
1019
     *
1020
     * @return ColumnSchemaBuilder column schema builder instance
1021
     */
1022
    public function createColumnSchemaBuilder(string $type, $length = null): ColumnSchemaBuilder
1023
    {
1024
        return new ColumnSchemaBuilder($type, $length, $this->getDb());
1025
    }
1026
}
1027