Test Failed
Pull Request — master (#88)
by Def
23:07 queued 10:00
created

Schema::loadTableUniques()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 2
nop 1
dl 0
loc 5
ccs 0
cts 0
cp 0
crap 6
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Oracle;
6
7
use Throwable;
8
use Yiisoft\Arrays\ArrayHelper;
9
use Yiisoft\Db\Cache\SchemaCache;
10
use Yiisoft\Db\Connection\ConnectionInterface;
11
use Yiisoft\Db\Constraint\CheckConstraint;
12
use Yiisoft\Db\Constraint\Constraint;
13
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
14
use Yiisoft\Db\Constraint\IndexConstraint;
15
use Yiisoft\Db\Exception\Exception;
16
use Yiisoft\Db\Exception\InvalidConfigException;
17
use Yiisoft\Db\Exception\NotSupportedException;
18
use Yiisoft\Db\Expression\Expression;
19
use Yiisoft\Db\Schema\Schema as AbstractSchema;
20
use Yiisoft\Db\Schema\TableSchemaInterface;
21
22
use function array_change_key_case;
23
use function array_map;
24
use function array_merge;
25
use function explode;
26
use function is_array;
27
use function md5;
28
use function preg_replace;
29
use function serialize;
30
use function str_contains;
31
use function str_replace;
32
use function stripos;
33
use function strlen;
34
use function substr;
35
use function trim;
36
37
/**
38
 * Schema is the class for retrieving metadata from an Oracle database.
39
 *
40
 * @property string $lastInsertID The row ID of the last row inserted, or the last value retrieved from the
41
 * sequence object. This property is read-only.
42
 *
43
 * @psalm-type ConstraintArray = array<
44
 *   array-key,
45
 *   array {
46
 *     name: string,
47
 *     column_name: string,
48
 *     type: string,
49
 *     foreign_table_schema: string|null,
50
 *     foreign_table_name: string|null,
51
 *     foreign_column_name: string|null,
52
 *     on_update: string,
53
 *     on_delete: string,
54
 *     check_expr: string
55
 *   }
56
 * >
57
 */
58
final class Schema extends AbstractSchema
59
{
60 361
    public function __construct(protected ConnectionInterface $db, SchemaCache $schemaCache, string $defaultSchema)
61
    {
62 361
        $this->defaultSchema = $defaultSchema;
63 361
        parent::__construct($db, $schemaCache);
0 ignored issues
show
Bug introduced by
$db of type Yiisoft\Db\Connection\ConnectionInterface is incompatible with the type Yiisoft\Db\Cache\SchemaCache expected by parameter $schemaCache of Yiisoft\Db\Schema\Schema::__construct(). ( Ignorable by Annotation )

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

63
        parent::__construct(/** @scrutinizer ignore-type */ $db, $schemaCache);
Loading history...
Unused Code introduced by
The call to Yiisoft\Db\Schema\Schema::__construct() has too many arguments starting with $schemaCache. ( Ignorable by Annotation )

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

63
        parent::/** @scrutinizer ignore-call */ 
64
                __construct($db, $schemaCache);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
64
    }
65
66 75
    protected function resolveTableName(string $name): TableSchemaInterface
67
    {
68 75
        $resolvedName = new TableSchema();
69
70 75
        $parts = array_reverse(
71
            $this->db->getQuoter()->getTableNameParts($name)
72 75
        );
73
74
        $resolvedName->name($parts[0] ?? '');
75
        $resolvedName->schemaName($parts[1] ?? $this->defaultSchema);
76 75
77 75
        $resolvedName->fullName($resolvedName->getSchemaName() !== $this->defaultSchema ?
78
            implode('.', array_reverse($parts)) : $resolvedName->getName()
79
        );
80 75
81 75
        return $resolvedName;
82
    }
83 75
84
    /**
85 75
     * @link https://docs.oracle.com/cd/B28359_01/server.111/b28337/tdpsg_user_accounts.htm
86
     */
87
    protected function findSchemaNames(): array
88
    {
89
        $sql = <<<SQL
90
        SELECT "u"."USERNAME"
91 1
        FROM "DBA_USERS" "u"
92
        WHERE "u"."DEFAULT_TABLESPACE" NOT IN ('SYSTEM', 'SYSAUX')
93 1
        ORDER BY "u"."USERNAME" ASC
94
        SQL;
95
96
        return $this->db->createCommand($sql)->queryColumn();
97
    }
98
99
    /**
100 1
     * @param string $schema
101
     *
102
     * @throws Exception|InvalidConfigException|Throwable
103
     *
104
     * @return array
105
     */
106
    protected function findTableNames(string $schema = ''): array
107
    {
108
        if ($schema === '') {
109
            $sql = <<<SQL
110 10
            SELECT TABLE_NAME
111
            FROM USER_TABLES
112 10
            UNION ALL
113 10
            SELECT VIEW_NAME AS TABLE_NAME
114
            FROM USER_VIEWS
115
            UNION ALL
116
            SELECT MVIEW_NAME AS TABLE_NAME
117
            FROM USER_MVIEWS
118
            ORDER BY TABLE_NAME
119
            SQL;
120
121
            $command = $this->db->createCommand($sql);
122
        } else {
123
            $sql = <<<SQL
124
            SELECT OBJECT_NAME AS TABLE_NAME
125 10
            FROM ALL_OBJECTS
126
            WHERE OBJECT_TYPE IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW') AND OWNER = :schema
127
            ORDER BY OBJECT_NAME
128
            SQL;
129
            $command = $this->db->createCommand($sql, [':schema' => $schema]);
130
        }
131
132
        $rows = $command->queryAll();
133
        $names = [];
134
135
        /** @psalm-var string[][] $rows */
136 10
        foreach ($rows as $row) {
137 10
            /** @psalm-var string[] $row */
138
            $row = $this->normalizeRowKeyCase($row, false);
139
            $names[] = $row['table_name'];
140 10
        }
141
142 10
        return $names;
143 10
    }
144
145
    /**
146 10
     * @param string $name
147
     *
148
     * @throws Exception|InvalidConfigException|Throwable
149
     *
150
     * @return TableSchemaInterface|null
151
     */
152
    protected function loadTableSchema(string $name): ?TableSchemaInterface
153
    {
154
        $table = $this->resolveTableName($name);
155
156 89
        if ($this->findColumns($table)) {
157
            $this->findConstraints($table);
158 89
            return $table;
159
        }
160 89
161
        return null;
162 89
    }
163 75
164 75
    /**
165
     * @param string $tableName
166
     *
167 22
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
168
     *
169
     * @return Constraint|null
170
     */
171
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
172
    {
173
        /** @var mixed */
174
        $tablePrimaryKey = $this->loadTableConstraints($tableName, self::PRIMARY_KEY);
175
        return $tablePrimaryKey instanceof Constraint ? $tablePrimaryKey : null;
176
    }
177 31
178
    /**
179
     * @param string $tableName
180 31
     *
181 31
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
182
     *
183
     * @return array
184
     */
185
    protected function loadTableForeignKeys(string $tableName): array
186
    {
187
        /** @var mixed */
188
        $tableForeingKeys = $this->loadTableConstraints($tableName, self::FOREIGN_KEYS);
189
        return is_array($tableForeingKeys) ? $tableForeingKeys : [];
190
    }
191 5
192
    /**
193
     * @param string $tableName
194 5
     *
195 5
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
196
     *
197
     * @return array
198
     */
199
    protected function loadTableIndexes(string $tableName): array
200
    {
201
        $sql = <<<SQL
202
        SELECT "ui"."INDEX_NAME" AS "name", "uicol"."COLUMN_NAME" AS "column_name",
203
        CASE "ui"."UNIQUENESS" WHEN 'UNIQUE' THEN 1 ELSE 0 END AS "index_is_unique",
204
        CASE WHEN "uc"."CONSTRAINT_NAME" IS NOT NULL THEN 1 ELSE 0 END AS "index_is_primary"
205 28
        FROM "SYS"."USER_INDEXES" "ui"
206
        LEFT JOIN "SYS"."USER_IND_COLUMNS" "uicol"
207 28
        ON "uicol"."INDEX_NAME" = "ui"."INDEX_NAME"
208
        LEFT JOIN "SYS"."USER_CONSTRAINTS" "uc"
209
        ON "uc"."OWNER" = "ui"."TABLE_OWNER" AND "uc"."CONSTRAINT_NAME" = "ui"."INDEX_NAME" AND "uc"."CONSTRAINT_TYPE" = 'P'
210
        WHERE "ui"."TABLE_OWNER" = :schemaName AND "ui"."TABLE_NAME" = :tableName
211
        ORDER BY "uicol"."COLUMN_POSITION" ASC
212
        SQL;
213
214
        $resolvedName = $this->resolveTableName($tableName);
215
216
        $indexes = $this->db->createCommand($sql, [
217
            ':schemaName' => $resolvedName->getSchemaName(),
218
            ':tableName' => $resolvedName->getName(),
219
        ])->queryAll();
220 28
221
        /** @psalm-var array[] $indexes */
222 28
        $indexes = $this->normalizeRowKeyCase($indexes, true);
223 28
        $indexes = ArrayHelper::index($indexes, null, 'name');
224 28
225 28
        $result = [];
226
227
        /**
228 28
         * @psalm-var object|string|null $name
229 28
         * @psalm-var array[] $index
230
         */
231 28
        foreach ($indexes as $name => $index) {
232
            $columnNames = ArrayHelper::getColumn($index, 'column_name');
233
234
            if ($columnNames[0] === null) {
235
                $columnNames[0] = '';
236
            }
237 28
238 25
            $result[] = (new IndexConstraint())
239
                ->primary((bool) $index[0]['index_is_primary'])
240 25
                ->unique((bool) $index[0]['index_is_unique'])
241 18
                ->name($name)
242
                ->columnNames($columnNames);
243
        }
244 25
245 25
        return $result;
246 25
    }
247 25
248 25
    /**
249
     * @param string $tableName
250
     *
251 28
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
252
     *
253
     * @return array
254
     */
255
    protected function loadTableUniques(string $tableName): array
256
    {
257
        /** @var mixed */
258
        $tableUniques = $this->loadTableConstraints($tableName, self::UNIQUES);
259
        return is_array($tableUniques) ? $tableUniques : [];
260
    }
261 14
262
    /**
263
     * @param string $tableName
264 14
     *
265 14
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
266
     *
267
     * @return array
268
     */
269
    protected function loadTableChecks(string $tableName): array
270
    {
271
        /** @var mixed */
272
        $tableCheck = $this->loadTableConstraints($tableName, self::CHECKS);
273
        return is_array($tableCheck) ? $tableCheck : [];
274
    }
275 14
276
    /**
277
     * @param string $tableName
278 14
     *
279 14
     * @throws NotSupportedException if this method is called.
280
     *
281
     * @return array
282
     */
283
    protected function loadTableDefaultValues(string $tableName): array
284
    {
285
        throw new NotSupportedException('Oracle does not support default value constraints.');
286
    }
287
288
    /**
289 12
     * Create a column schema builder instance giving the type and value precision.
290
     *
291 12
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
292
     *
293
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
294
     * @param array|int|string|null $length length or precision of the column {@see ColumnSchemaBuilder::$length}.
295
     *
296
     * @return ColumnSchemaBuilder column schema builder instance
297
     *
298
     * @psalm-param string[]|int|string|null $length
299
     */
300
    public function createColumnSchemaBuilder(string $type, array|int|string $length = null): ColumnSchemaBuilder
301
    {
302
        return new ColumnSchemaBuilder($type, $length);
303
    }
304
305
    /**
306 3
     * Collects the table column metadata.
307
     *
308 3
     * @param TableSchemaInterface $table the table schema.
309
     *
310
     * @throws Exception|Throwable
311
     *
312
     * @return bool whether the table exists.
313
     */
314
    protected function findColumns(TableSchemaInterface $table): bool
315
    {
316
        $sql = <<<SQL
317 89
        SELECT
318
            A.COLUMN_NAME,
319 89
            A.DATA_TYPE,
320
            A.DATA_PRECISION,
321 89
            A.DATA_SCALE,
322
            (
323
            CASE A.CHAR_USED WHEN 'C' THEN A.CHAR_LENGTH
324
                ELSE A.DATA_LENGTH
325 89
            END
326 89
            ) AS DATA_LENGTH,
327
            A.NULLABLE,
328
            A.DATA_DEFAULT,
329 89
            COM.COMMENTS AS COLUMN_COMMENT
330 89
        FROM ALL_TAB_COLUMNS A
331
            INNER JOIN ALL_OBJECTS B ON B.OWNER = A.OWNER AND LTRIM(B.OBJECT_NAME) = LTRIM(A.TABLE_NAME)
332
            LEFT JOIN ALL_COL_COMMENTS COM ON (A.OWNER = COM.OWNER AND A.TABLE_NAME = COM.TABLE_NAME AND A.COLUMN_NAME = COM.COLUMN_NAME)
333
        WHERE
334
            A.OWNER = :schemaName
335
            AND B.OBJECT_TYPE IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW')
336
            AND B.OBJECT_NAME = :tableName
337
        ORDER BY A.COLUMN_ID
338
        SQL;
339
340
        try {
341
            $columns = $this->db->createCommand($sql, [
342 89
                ':tableName' => $table->getName(),
343
                ':schemaName' => $table->getSchemaName(),
344 89
            ])->queryAll();
345
        } catch (Exception) {
346
            return false;
347
        }
348
349
        if (empty($columns)) {
350
            return false;
351
        }
352
353
        /** @psalm-var string[][] $columns */
354
        foreach ($columns as $column) {
355
            $column = $this->normalizeRowKeyCase($column, false);
356
357
            $c = $this->createColumn($column);
358
359
            $table->columns($c->getName(), $c);
360
        }
361
362
        return true;
363
    }
364
365
    /**
366
     * Sequence name of table.
367
     *
368
     * @param string $tableName
369 89
     *
370 89
     * @throws Exception|InvalidConfigException|Throwable
371 89
     *
372 89
     * @return bool|float|int|string|null whether the sequence exists.
373
     *
374
     * @internal TableSchemaInterface `$table->getName()` the table schema.
375
     */
376
    protected function getTableSequenceName(string $tableName): bool|float|int|string|null
377 89
    {
378 22
        $sequenceNameSql = <<<SQL
379
        SELECT
380
            UD.REFERENCED_NAME AS SEQUENCE_NAME
381
        FROM USER_DEPENDENCIES UD
382 75
            JOIN USER_TRIGGERS UT ON (UT.TRIGGER_NAME = UD.NAME)
383 75
        WHERE
384
            UT.TABLE_NAME = :tableName
385 75
            AND UD.TYPE = 'TRIGGER'
386
            AND UD.REFERENCED_TYPE = 'SEQUENCE'
387 75
        SQL;
388
        $sequenceName = $this->db->createCommand($sequenceNameSql, [':tableName' => $tableName])->queryScalar();
389
390 75
        return $sequenceName === false ? null : $sequenceName;
391
    }
392
393
    /**
394
     * @inheritDoc
395
     */
396
    public function getLastInsertID(string $sequenceName = null): string
397
    {
398
        return $this->db->getLastInsertID($sequenceName);
399
    }
400
401
    /**
402
     * Creates ColumnSchema instance.
403
     *
404 56
     * @param array|string $column
405
     *
406 56
     * @return ColumnSchema
407
     */
408
    protected function createColumn(array|string $column): ColumnSchema
409
    {
410
        $c = $this->createColumnSchema();
411
412
        /**
413
         * @psalm-var array{
414
         *   column_name: string,
415
         *   data_type: string,
416 56
         *   data_precision: string,
417
         *   data_scale: string,
418 56
         *   data_length: string,
419
         *   nullable: string,
420
         *   data_default: string|null,
421
         *   column_comment: string|null
422
         * } $column
423
         */
424
        $c->name($column['column_name']);
425
        $c->allowNull($column['nullable'] === 'Y');
426
        $c->comment($column['column_comment'] ?? '');
427
        $c->primaryKey(false);
428
429
        $this->extractColumnType(
430
            $c,
431
            $column['data_type'],
432
            $column['data_precision'],
433
            $column['data_scale'],
434
            $column['data_length']
435
        );
436 75
437
        $this->extractColumnSize(
438 75
            $c,
439
            $column['data_type'],
440
            $column['data_precision'],
441
            $column['data_scale'],
442
            $column['data_length']
443
        );
444
445
        $c->phpType($this->getColumnPhpType($c));
446
447
        if (!$c->isPrimaryKey()) {
448
            if ($column['data_default'] !== null && stripos($column['data_default'], 'timestamp') !== false) {
449
                $c->defaultValue(null);
450
            } else {
451
                $defaultValue = $column['data_default'];
452 75
453 75
                if ($c->getType() === 'timestamp' && $defaultValue === 'CURRENT_TIMESTAMP') {
454 75
                    $c->defaultValue(new Expression('CURRENT_TIMESTAMP'));
455 75
                } else {
456
                    if ($defaultValue !== null) {
457 75
                        if (($len = strlen($defaultValue)) > 2 && $defaultValue[0] === "'"
458
                            && $defaultValue[$len - 1] === "'"
459 75
                        ) {
460 75
                            $defaultValue = substr((string) $column['data_default'], 1, -1);
461 75
                        } else {
462 75
                            $defaultValue = trim($defaultValue);
463
                        }
464
                    }
465 75
                    $c->defaultValue($c->phpTypecast($defaultValue));
466
                }
467 75
            }
468 75
        }
469 75
470 75
        return $c;
471
    }
472
473 75
    /**
474
     * Finds constraints and fills them into TableSchemaInterface object passed.
475 75
     *
476 75
     * @param TableSchemaInterface $table
477 15
     *
478
     * @throws Exception|InvalidConfigException|Throwable
479 75
     *
480
     * @psalm-suppress PossiblyNullArrayOffset
481 75
     */
482
    protected function findConstraints(TableSchemaInterface $table): void
483
    {
484 75
        $sql = <<<SQL
485 53
        SELECT
486 53
            /*+ PUSH_PRED(C) PUSH_PRED(D) PUSH_PRED(E) */
487
            D.CONSTRAINT_NAME,
488 15
            D.CONSTRAINT_TYPE,
489
            C.COLUMN_NAME,
490 53
            C.POSITION,
491
            D.R_CONSTRAINT_NAME,
492
            E.TABLE_NAME AS TABLE_REF,
493 75
            F.COLUMN_NAME AS COLUMN_REF,
494
            C.TABLE_NAME
495
        FROM ALL_CONS_COLUMNS C
496
            INNER JOIN ALL_CONSTRAINTS D ON D.OWNER = C.OWNER AND D.CONSTRAINT_NAME = C.CONSTRAINT_NAME
497
            LEFT JOIN ALL_CONSTRAINTS E ON E.OWNER = D.R_OWNER AND E.CONSTRAINT_NAME = D.R_CONSTRAINT_NAME
498 75
            LEFT JOIN ALL_CONS_COLUMNS F ON F.OWNER = E.OWNER AND F.CONSTRAINT_NAME = E.CONSTRAINT_NAME AND F.POSITION = C.POSITION
499
        WHERE
500
            C.OWNER = :schemaName
501
            AND C.TABLE_NAME = :tableName
502
            ORDER BY D.CONSTRAINT_NAME, C.POSITION
503
        SQL;
504
505
        /**
506
         * @psalm-var array{
507
         *   array{
508
         *     constraint_name: string,
509
         *     constraint_type: string,
510 75
         *     column_name: string,
511
         *     position: string|null,
512 75
         *     r_constraint_name: string|null,
513
         *     table_ref: string|null,
514
         *     column_ref: string|null,
515
         *     table_name: string
516
         *   }
517
         * } $rows
518
         */
519
        $rows = $this->db->createCommand(
520
            $sql,
521
            [':tableName' => $table->getName(), ':schemaName' => $table->getSchemaName()]
522
        )->queryAll();
523
524
        $constraints = [];
525
526
        foreach ($rows as $row) {
527
            /** @psalm-var string[] $row */
528
            $row = $this->normalizeRowKeyCase($row, false);
529
530
            if ($row['constraint_type'] === 'P') {
531
                $table->getColumns()[$row['column_name']]->primaryKey(true);
532
                $table->primaryKey($row['column_name']);
533
534
                if (empty($table->getSequenceName())) {
535
                    $table->sequenceName((string) $this->getTableSequenceName($table->getName()));
536
                }
537
            }
538
539
            if ($row['constraint_type'] !== 'R') {
540
                /**
541
                 * This condition is not checked in SQL WHERE because of an Oracle Bug:
542
                 *
543
                 * {@see https://github.com/yiisoft/yii2/pull/8844}
544
                 */
545
                continue;
546
            }
547 75
548
            $name = $row['constraint_name'];
549 75
550 75
            if (!isset($constraints[$name])) {
551
                $constraints[$name] = [
552 75
                    'tableName' => $row['table_ref'],
553
                    'columns' => [],
554 75
                ];
555
            }
556 69
557
            $constraints[$name]['columns'][$row['column_name']] = $row['column_ref'];
558 69
        }
559 56
560 56
        foreach ($constraints as $index => $constraint) {
561
            $table->foreignKey($index, array_merge([$constraint['tableName']], $constraint['columns']));
562 56
        }
563 56
    }
564
565
    /**
566
     * Returns all unique indexes for the given table.
567 69
     *
568
     * Each array element is of the following structure:.
569
     *
570
     * ```php
571
     * [
572
     *     'IndexName1' => ['col1' [, ...]],
573 69
     *     'IndexName2' => ['col2' [, ...]],
574
     * ]
575
     * ```
576 8
     *
577
     * @param TableSchemaInterface $table the table metadata.
578 8
     *
579 8
     * @throws Exception|InvalidConfigException|Throwable
580 8
     *
581
     * @return array all unique indexes for the given table.
582
     */
583
    public function findUniqueIndexes(TableSchemaInterface $table): array
584
    {
585 8
        $query = <<<SQL
586
        SELECT
587
            DIC.INDEX_NAME,
588 75
            DIC.COLUMN_NAME
589 8
        FROM ALL_INDEXES DI
590
            INNER JOIN ALL_IND_COLUMNS DIC ON DI.TABLE_NAME = DIC.TABLE_NAME AND DI.INDEX_NAME = DIC.INDEX_NAME
591
        WHERE
592
            DI.UNIQUENESS = 'UNIQUE'
593
            AND DIC.TABLE_OWNER = :schemaName
594
            AND DIC.TABLE_NAME = :tableName
595
        ORDER BY DIC.TABLE_NAME, DIC.INDEX_NAME, DIC.COLUMN_POSITION
596
        SQL;
597
        $result = [];
598
599
        $rows = $this->db->createCommand(
600
            $query,
601
            [':tableName' => $table->getName(), ':schemaName' => $table->getschemaName()]
602
        )->queryAll();
603
604
        /** @psalm-var array<array{INDEX_NAME: string, COLUMN_NAME: string}> $rows */
605
        foreach ($rows as $row) {
606
            $result[$row['INDEX_NAME']][] = $row['COLUMN_NAME'];
607
        }
608
609
        return $result;
610
    }
611 1
612
    /**
613 1
     * Extracts the data types for the given column.
614
     *
615
     * @param ColumnSchema $column
616
     * @param string $dbType DB type.
617
     * @param string|null $precision total number of digits.
618
     * @param string|null $scale number of digits on the right of the decimal separator.
619
     * @param string $length length for character types.
620
     */
621
    protected function extractColumnType(
622
        ColumnSchema $column,
623
        string $dbType,
624
        ?string $precision,
0 ignored issues
show
Unused Code introduced by
The parameter $precision is not used and could be removed. ( Ignorable by Annotation )

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

624
        /** @scrutinizer ignore-unused */ ?string $precision,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
625 1
        ?string $scale,
626
        string $length
0 ignored issues
show
Unused Code introduced by
The parameter $length is not used and could be removed. ( Ignorable by Annotation )

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

626
        /** @scrutinizer ignore-unused */ string $length

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
627 1
    ): void {
628
        $column->dbType($dbType);
629 1
630 1
        if (str_contains($dbType, 'FLOAT') || str_contains($dbType, 'DOUBLE')) {
631
            $column->type(self::TYPE_DOUBLE);
632
        } elseif (str_contains($dbType, 'NUMBER')) {
633 1
            if ($scale === null || $scale > 0) {
634 1
                $column->type(self::TYPE_DECIMAL);
635
            } else {
636
                $column->type(self::TYPE_INTEGER);
637 1
            }
638
        } elseif (str_contains($dbType, 'INTEGER')) {
639
            $column->type(self::TYPE_INTEGER);
640
        } elseif (str_contains($dbType, 'BLOB')) {
641
            $column->type(self::TYPE_BINARY);
642
        } elseif (str_contains($dbType, 'CLOB')) {
643
            $column->type(self::TYPE_TEXT);
644
        } elseif (str_contains($dbType, 'TIMESTAMP')) {
645
            $column->type(self::TYPE_TIMESTAMP);
646
        } else {
647
            $column->type(self::TYPE_STRING);
648
        }
649 75
    }
650
651
    /**
652
     * Extracts size, precision and scale information from column's DB type.
653
     *
654
     * @param ColumnSchema $column
655
     * @param string $dbType the column's DB type.
656 75
     * @param string|null $precision total number of digits.
657
     * @param string|null $scale number of digits on the right of the decimal separator.
658 75
     * @param string $length length for character types.
659 17
     */
660 75
    protected function extractColumnSize(
661 72
        ColumnSchema $column,
662 18
        string $dbType,
0 ignored issues
show
Unused Code introduced by
The parameter $dbType is not used and could be removed. ( Ignorable by Annotation )

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

662
        /** @scrutinizer ignore-unused */ string $dbType,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
663
        ?string $precision,
664 72
        ?string $scale,
665
        string $length
666 68
    ): void {
667
        $column->size(trim($length) === '' ? null : (int) $length);
668 68
        $column->precision(trim((string) $precision) === '' ? null : (int) $precision);
669 19
        $column->scale($scale === '' || $scale === null ? null : (int) $scale);
670 65
    }
671 22
672 64
    /**
673 16
     * Loads multiple types of constraints and returns the specified ones.
674
     *
675 64
     * @param string $tableName table name.
676
     * @param string $returnType return type:
677
     * - primaryKey
678
     * - foreignKeys
679
     * - uniques
680
     * - checks
681
     *
682
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
683
     *
684
     * @return mixed constraints.
685
     */
686
    private function loadTableConstraints(string $tableName, string $returnType): mixed
687
    {
688 75
        $sql = <<<SQL
689
        SELECT
690
            "uc"."CONSTRAINT_NAME" AS "name",
691
            "uccol"."COLUMN_NAME" AS "column_name",
692
            "uc"."CONSTRAINT_TYPE" AS "type",
693
            "fuc"."OWNER" AS "foreign_table_schema",
694
            "fuc"."TABLE_NAME" AS "foreign_table_name",
695 75
            "fuccol"."COLUMN_NAME" AS "foreign_column_name",
696 75
            "uc"."DELETE_RULE" AS "on_delete",
697 75
            "uc"."SEARCH_CONDITION" AS "check_expr"
698
        FROM "USER_CONSTRAINTS" "uc"
699
        INNER JOIN "USER_CONS_COLUMNS" "uccol"
700
        ON "uccol"."OWNER" = "uc"."OWNER" AND "uccol"."CONSTRAINT_NAME" = "uc"."CONSTRAINT_NAME"
701
        LEFT JOIN "USER_CONSTRAINTS" "fuc"
702
        ON "fuc"."OWNER" = "uc"."R_OWNER" AND "fuc"."CONSTRAINT_NAME" = "uc"."R_CONSTRAINT_NAME"
703
        LEFT JOIN "USER_CONS_COLUMNS" "fuccol"
704
        ON "fuccol"."OWNER" = "fuc"."OWNER" AND "fuccol"."CONSTRAINT_NAME" = "fuc"."CONSTRAINT_NAME" AND "fuccol"."POSITION" = "uccol"."POSITION"
705
        WHERE "uc"."OWNER" = :schemaName AND "uc"."TABLE_NAME" = :tableName
706
        ORDER BY "uccol"."POSITION" ASC
707
        SQL;
708
709
        $resolvedName = $this->resolveTableName($tableName);
710
711
        $constraints = $this->db->createCommand($sql, [
712
            ':schemaName' => $resolvedName->getSchemaName(),
713
            ':tableName' => $resolvedName->getName(),
714 64
        ])->queryAll();
715
716 64
        /** @var Constraint[] $constraints */
717
        $constraints = $this->normalizeRowKeyCase($constraints, true);
718
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
719
720
        $result = [
721
            self::PRIMARY_KEY => null,
722
            self::FOREIGN_KEYS => [],
723
            self::UNIQUES => [],
724
            self::CHECKS => [],
725
        ];
726
727
        /**
728
         * @var string $type
729
         * @var array $names
730
         */
731
        foreach ($constraints as $type => $names) {
732
            /**
733
             * @psalm-var object|string|null $name
734
             * @psalm-var ConstraintArray $constraint
735
             */
736
            foreach ($names as $name => $constraint) {
737 64
                switch ($type) {
738
                    case 'P':
739 64
                        $result[self::PRIMARY_KEY] = (new Constraint())
740 64
                            ->name($name)
741 64
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
742 64
                        break;
743
                    case 'R':
744
                        $result[self::FOREIGN_KEYS][] = (new ForeignKeyConstraint())
745 64
                            ->name($name)
746 64
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
747
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
748 64
                            ->foreignTableName($constraint[0]['foreign_table_name'])
749
                            ->foreignColumnNames(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
750 64
                            ->onDelete($constraint[0]['on_delete'])
751 64
                            ->onUpdate(null);
752 64
                        break;
753
                    case 'U':
754
                        $result[self::UNIQUES][] = (new Constraint())
755
                            ->name($name)
756
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
757
                        break;
758
                    case 'C':
759 64
                        $result[self::CHECKS][] = (new CheckConstraint())
760
                            ->name($name)
761
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
762
                            ->expression($constraint[0]['check_expr']);
763
                        break;
764 64
                }
765 64
            }
766 64
        }
767 49
768 49
        foreach ($result as $type => $data) {
769 49
            $this->setTableMetadata($tableName, $type, $data);
770 49
        }
771 64
772 17
        return $result[$returnType];
773 17
    }
774 17
775 17
    /**
776 17
     * Creates a column schema for the database.
777 17
     *
778 17
     * This method may be overridden by child classes to create a DBMS-specific column schema.
779 17
     *
780 17
     * @return ColumnSchema column schema instance.
781 64
     */
782 50
    protected function createColumnSchema(): ColumnSchema
783 50
    {
784 50
        return new ColumnSchema();
785 50
    }
786 64
787 64
    /**
788 64
     * Returns the cache key for the specified table name.
789 64
     *
790 64
     * @param string $name the table name.
791 64
     *
792
     * @return array the cache key.
793
     */
794
    protected function getCacheKey(string $name): array
795
    {
796 64
        return array_merge([__CLASS__], $this->db->getCacheKey(), [$this->getRawTableName($name)]);
797 64
    }
798
799
    /**
800 64
     * Returns the cache tag name.
801
     *
802
     * This allows {@see refresh()} to invalidate all cached table schemas.
803
     *
804
     * @return string the cache tag name.
805
     */
806
    protected function getCacheTag(): string
807
    {
808
        return md5(serialize(array_merge([__CLASS__], $this->db->getCacheKey())));
809
    }
810 75
811
    /**
812 75
     * Changes row's array key case to lower.
813
     *
814
     * @param array $row row's array or an array of row's arrays.
815
     * @param bool $multiple whether multiple rows or a single row passed.
816
     *
817
     * @return array normalized row or rows.
818
     */
819
    protected function normalizeRowKeyCase(array $row, bool $multiple): array
820
    {
821
        if ($multiple) {
822
            return array_map(static function (array $row) {
823
                return array_change_key_case($row, CASE_LOWER);
824
            }, $row);
825 154
        }
826
827 154
        return array_change_key_case($row, CASE_LOWER);
828 21
    }
829
830 21
    /**
831
     * @return bool whether this DBMS supports [savepoint](http://en.wikipedia.org/wiki/Savepoint).
832
     */
833 154
    public function supportsSavepoint(): bool
834
    {
835
        return $this->db->isSavepointEnabled();
836
    }
837
}
838