Passed
Push — master ( add693...b324e9 )
by Wilmer
13:51
created

MssqlSchema::loadColumnSchema()   C

Complexity

Conditions 15
Paths 92

Size

Total Lines 53
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 15
eloc 33
c 1
b 0
f 0
nc 92
nop 1
dl 0
loc 53
rs 5.9166

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Mssql\Schema;
6
7
use Yiisoft\Arrays\ArrayHelper;
8
use Yiisoft\Db\Constraint\CheckConstraint;
9
use Yiisoft\Db\Constraint\Constraint;
10
use Yiisoft\Db\Constraint\ConstraintFinderInterface;
11
use Yiisoft\Db\Constraint\ConstraintFinderTrait;
12
use Yiisoft\Db\Constraint\DefaultValueConstraint;
13
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
14
use Yiisoft\Db\Constraint\IndexConstraint;
15
use Yiisoft\Db\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\Query\MssqlQueryBuilder;
21
use Yiisoft\Db\Schema\ColumnSchema;
22
use Yiisoft\Db\Schema\Schema;
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 MssqlSchema extends Schema 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
94
         * 'cursor' type cannot be used with tables
95
         */
96
        'timestamp' => self::TYPE_TIMESTAMP,
97
        'hierarchyid' => self::TYPE_STRING,
98
        'uniqueidentifier' => self::TYPE_STRING,
99
        'sql_variant' => self::TYPE_STRING,
100
        'xml' => self::TYPE_STRING,
101
        'table' => self::TYPE_STRING,
102
    ];
103
104
    protected $tableQuoteCharacter = ['[', ']'];
105
    protected $columnQuoteCharacter = ['[', ']'];
106
107
    /**
108
     * Resolves the table name and schema name (if any).
109
     *
110
     * @param string $name the table name.
111
     *
112
     * @return MssqlTableSchema resolved table, schema, etc. names.
113
     */
114
    protected function resolveTableName(string $name): MssqlTableSchema
115
    {
116
        $resolvedName = new MssqlTableSchema();
117
118
        $parts = $this->getTableNameParts($name);
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 MssqlTableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
250
     */
251
    protected function loadTableSchema(string $name): ?MssqlTableSchema
252
    {
253
        $table = new MssqlTableSchema();
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
        foreach ($indexes as $name => $index) {
334
            $result[] = (new IndexConstraint())
335
                ->primary((bool) $index[0]['index_is_primary'])
336
                ->unique((bool) $index[0]['index_is_unique'])
337
                ->name($name)
338
                ->columnNames(ArrayHelper::getColumn($index, 'column_name'));
339
        }
340
341
        return $result;
342
    }
343
344
    /**
345
     * Loads all unique constraints for the given table.
346
     *
347
     * @param string $tableName table name.
348
     *
349
     * @throws Exception
350
     * @throws InvalidArgumentException
351
     * @throws InvalidConfigException
352
     *
353
     * @return Constraint[] unique constraints for the given table.
354
     */
355
    protected function loadTableUniques(string $tableName): array
356
    {
357
        return $this->loadTableConstraints($tableName, 'uniques');
358
    }
359
360
    /**
361
     * Loads all check constraints for the given table.
362
     *
363
     * @param string $tableName table name.
364
     *
365
     * @throws Exception
366
     * @throws InvalidArgumentException
367
     * @throws InvalidConfigException
368
     *
369
     * @return CheckConstraint[] check constraints for the given table.
370
     */
371
    protected function loadTableChecks(string $tableName): array
372
    {
373
        return $this->loadTableConstraints($tableName, 'checks');
374
    }
375
376
    /**
377
     * Loads all default value constraints for the given table.
378
     *
379
     * @param string $tableName table name.
380
     *
381
     * @throws Exception
382
     * @throws InvalidArgumentException
383
     * @throws InvalidConfigException
384
     *
385
     * @return DefaultValueConstraint[] default value constraints for the given table.
386
     */
387
    protected function loadTableDefaultValues(string $tableName): array
388
    {
389
        return $this->loadTableConstraints($tableName, 'defaults');
390
    }
391
392
    /**
393
     * Creates a new savepoint.
394
     *
395
     * @param string $name the savepoint name.
396
     *
397
     * @throws Exception
398
     * @throws InvalidConfigException
399
     */
400
    public function createSavepoint(string $name): void
401
    {
402
        $this->getDb()->createCommand("SAVE TRANSACTION $name")->execute();
403
    }
404
405
    /**
406
     * Releases an existing savepoint.
407
     *
408
     * @param string $name the savepoint name.
409
     */
410
    public function releaseSavepoint(string $name): void
411
    {
412
        /* does nothing as MSSQL does not support this */
413
    }
414
415
    /**
416
     * Rolls back to a previously created savepoint.
417
     *
418
     * @param string $name the savepoint name.
419
     *
420
     * @throws Exception
421
     * @throws InvalidConfigException
422
     */
423
    public function rollBackSavepoint(string $name): void
424
    {
425
        $this->getDb()->createCommand("ROLLBACK TRANSACTION $name")->execute();
426
    }
427
428
    /**
429
     * Creates a column schema for the database.
430
     *
431
     * This method may be overridden by child classes to create a DBMS-specific column schema.
432
     *
433
     * @return MssqlColumnSchema column schema instance.
434
     */
435
    protected function createColumnSchema(): MssqlColumnSchema
436
    {
437
        return new MssqlColumnSchema();
438
    }
439
440
    /**
441
     * Creates a query builder for the MSSQL database.
442
     *
443
     * @return MssqlQueryBuilder query builder interface.
444
     */
445
    public function createQueryBuilder(): MssqlQueryBuilder
446
    {
447
        return new MssqlQueryBuilder($this->getDb());
0 ignored issues
show
Bug introduced by
It seems like $this->getDb() can also be of type null; however, parameter $db of Yiisoft\Db\Mssql\Query\M...yBuilder::__construct() does only seem to accept Yiisoft\Db\Connection\Connection, maybe add an additional type check? ( Ignorable by Annotation )

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

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

990
                $result[$name] = $this->getLastInsertID(/** @scrutinizer ignore-type */ $tableSchema->getSequenceName());
Loading history...
991
                break;
992
            }
993
            /**
994
             * {@see https://github.com/yiisoft/yii2/issues/13828 & https://github.com/yiisoft/yii2/issues/17474
995
             */
996
            if (isset($inserted[$name])) {
997
                $result[$name] = $inserted[$name];
998
            } elseif (isset($columns[$name])) {
999
                $result[$name] = $columns[$name];
1000
            } else {
1001
                $result[$name] = $tableSchema->getColumns()[$name]->getDefaultValue();
1002
            }
1003
        }
1004
1005
        return $result;
1006
    }
1007
}
1008