Test Failed
Pull Request — master (#236)
by Sergei
27:35 queued 24:01
created

Schema::loadColumnSchema()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 18
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 14
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 18
ccs 11
cts 11
cp 1
crap 3
rs 9.7998
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Oracle;
6
7
use Throwable;
8
use Yiisoft\Db\Cache\SchemaCache;
9
use Yiisoft\Db\Connection\ConnectionInterface;
10
use Yiisoft\Db\Constraint\CheckConstraint;
11
use Yiisoft\Db\Constraint\Constraint;
12
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
13
use Yiisoft\Db\Constraint\IndexConstraint;
14
use Yiisoft\Db\Driver\Pdo\AbstractPdoSchema;
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\Helper\DbArrayHelper;
20
use Yiisoft\Db\Schema\Builder\ColumnInterface;
21
use Yiisoft\Db\Schema\Column\ColumnSchemaInterface;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Schema\Column\ColumnSchemaInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
22
use Yiisoft\Db\Schema\TableSchemaInterface;
23
24
use function array_merge;
25
use function array_reverse;
26
use function implode;
27
use function is_array;
28
use function md5;
29
use function preg_match;
30
use function preg_replace;
31
use function serialize;
32
use function str_replace;
33
use function strtolower;
34
use function trim;
35
36
/**
37
 * Implements the Oracle Server specific schema, supporting Oracle Server 11C and above.
38
 *
39
 * @psalm-type ColumnInfoArray = array{
40
 *   column_name: string,
41
 *   data_type: string,
42
 *   data_precision: string|null,
43
 *   data_scale: string|null,
44
 *   data_length: string,
45
 *   nullable: string,
46
 *   data_default: string|null,
47
 *   is_pk: string|null,
48
 *   identity_column: string,
49
 *   column_comment: string|null
50
 * }
51
 *
52
 * @psalm-type ConstraintArray = array<
53
 *   array-key,
54
 *   array {
55
 *     name: string,
56
 *     column_name: string,
57
 *     type: string,
58
 *     foreign_table_schema: string|null,
59
 *     foreign_table_name: string|null,
60
 *     foreign_column_name: string|null,
61
 *     on_update: string,
62
 *     on_delete: string,
63
 *     check_expr: string
64
 *   }
65
 * >
66
 */
67
final class Schema extends AbstractPdoSchema
68
{
69
    /**
70
     * @var array The mapping from physical column types (keys) to abstract column types (values).
71
     *
72
     * @link https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/Data-Types.html
73
     *
74
     * @psalm-var string[]
75
     */
76
    private array $typeMap = [
77
        'char' => self::TYPE_CHAR,
78
        'nchar' => self::TYPE_CHAR,
79
        'varchar2' => self::TYPE_STRING,
80
        'nvarchar2' => self::TYPE_STRING,
81
        'clob' => self::TYPE_TEXT,
82
        'nclob' => self::TYPE_TEXT,
83
        'blob' => self::TYPE_BINARY,
84
        'bfile' => self::TYPE_BINARY,
85
        'long raw' => self::TYPE_BINARY,
86
        'raw' => self::TYPE_BINARY,
87
        'number' => self::TYPE_DECIMAL,
88
        'binary_float' => self::TYPE_FLOAT, // 32 bit
89
        'binary_double' => self::TYPE_DOUBLE, // 64 bit
90
        'float' => self::TYPE_DOUBLE, // 126 bit
91
        'timestamp' => self::TYPE_TIMESTAMP,
92
        'timestamp with time zone' => self::TYPE_TIMESTAMP,
93
        'timestamp with local time zone' => self::TYPE_TIMESTAMP,
94
        'date' => self::TYPE_DATE,
95
        'interval day to second' => self::TYPE_TIME,
96
97
        /** Deprecated */
98
        'long' => self::TYPE_TEXT,
99
    ];
100
101 544
    public function __construct(protected ConnectionInterface $db, SchemaCache $schemaCache, string $defaultSchema)
102
    {
103 544
        $this->defaultSchema = $defaultSchema;
104 544
        parent::__construct($db, $schemaCache);
105
    }
106
107 15
    public function createColumn(string $type, array|int|string $length = null): ColumnInterface
108
    {
109 15
        return new Column($type, $length);
110
    }
111
112 206
    protected function resolveTableName(string $name): TableSchemaInterface
113
    {
114 206
        $resolvedName = new TableSchema();
115
116 206
        $parts = array_reverse(
117 206
            $this->db->getQuoter()->getTableNameParts($name)
118 206
        );
119
120 206
        $resolvedName->name($parts[0] ?? '');
121 206
        $resolvedName->schemaName($parts[1] ?? $this->defaultSchema);
122
123 206
        $resolvedName->fullName(
124 206
            $resolvedName->getSchemaName() !== $this->defaultSchema ?
125 206
            implode('.', array_reverse($parts)) : $resolvedName->getName()
126 206
        );
127
128 206
        return $resolvedName;
129
    }
130
131
    /**
132
     * @link https://docs.oracle.com/cd/B28359_01/server.111/b28337/tdpsg_user_accounts.htm
133
     *
134
     * @throws Exception
135
     * @throws InvalidConfigException
136
     * @throws NotSupportedException
137
     * @throws Throwable
138
     */
139 1
    protected function findSchemaNames(): array
140
    {
141 1
        $sql = <<<SQL
142
        SELECT "u"."USERNAME"
143
        FROM "DBA_USERS" "u"
144
        WHERE "u"."DEFAULT_TABLESPACE" NOT IN ('SYSTEM', 'SYSAUX')
145
        ORDER BY "u"."USERNAME" ASC
146 1
        SQL;
147
148 1
        return $this->db->createCommand($sql)->queryColumn();
149
    }
150
151
    /**
152
     * @throws Exception
153
     * @throws InvalidConfigException
154
     * @throws Throwable
155
     */
156 147
    protected function findTableComment(TableSchemaInterface $tableSchema): void
157
    {
158 147
        $sql = <<<SQL
159
        SELECT "COMMENTS"
160
        FROM ALL_TAB_COMMENTS
161
        WHERE
162
              "OWNER" = :schemaName AND
163
              "TABLE_NAME" = :tableName
164 147
        SQL;
165
166 147
        $comment = $this->db->createCommand($sql, [
167 147
            ':schemaName' => $tableSchema->getSchemaName(),
168 147
            ':tableName' => $tableSchema->getName(),
169 147
        ])->queryScalar();
170
171 147
        $tableSchema->comment(is_string($comment) ? $comment : null);
172
    }
173
174
    /**
175
     * @throws Exception
176
     * @throws InvalidConfigException
177
     * @throws Throwable
178
     */
179 12
    protected function findTableNames(string $schema = ''): array
180
    {
181 12
        if ($schema === '') {
182 11
            $sql = <<<SQL
183
            SELECT TABLE_NAME
184
            FROM USER_TABLES
185
            UNION ALL
186
            SELECT VIEW_NAME AS TABLE_NAME
187
            FROM USER_VIEWS
188
            UNION ALL
189
            SELECT MVIEW_NAME AS TABLE_NAME
190
            FROM USER_MVIEWS
191
            ORDER BY TABLE_NAME
192 11
            SQL;
193
194 11
            $command = $this->db->createCommand($sql);
195
        } else {
196 1
            $sql = <<<SQL
197
            SELECT OBJECT_NAME AS TABLE_NAME
198
            FROM ALL_OBJECTS
199
            WHERE OBJECT_TYPE IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW') AND OWNER = :schema
200
            ORDER BY OBJECT_NAME
201 1
            SQL;
202 1
            $command = $this->db->createCommand($sql, [':schema' => $schema]);
203
        }
204
205 12
        $rows = $command->queryAll();
206 12
        $names = [];
207
208
        /** @psalm-var string[][] $rows */
209 12
        foreach ($rows as $row) {
210
            /** @psalm-var string[] $row */
211 12
            $row = $this->normalizeRowKeyCase($row, false);
212 12
            $names[] = $row['table_name'];
213
        }
214
215 12
        return $names;
216
    }
217
218
    /**
219
     * @throws Exception
220
     * @throws InvalidConfigException
221
     * @throws Throwable
222
     */
223 147
    protected function loadTableSchema(string $name): TableSchemaInterface|null
224
    {
225 147
        $table = $this->resolveTableName($name);
226 147
        $this->findTableComment($table);
227
228 147
        if ($this->findColumns($table)) {
229 130
            $this->findConstraints($table);
230 130
            return $table;
231
        }
232
233 33
        return null;
234
    }
235
236
    /**
237
     * @throws Exception
238
     * @throws InvalidConfigException
239
     * @throws NotSupportedException
240
     * @throws Throwable
241
     */
242 47
    protected function loadTablePrimaryKey(string $tableName): Constraint|null
243
    {
244
        /** @psalm-var mixed $tablePrimaryKey */
245 47
        $tablePrimaryKey = $this->loadTableConstraints($tableName, self::PRIMARY_KEY);
246 47
        return $tablePrimaryKey instanceof Constraint ? $tablePrimaryKey : null;
247
    }
248
249
    /**
250
     * @throws Exception
251
     * @throws InvalidConfigException
252
     * @throws NotSupportedException
253
     * @throws Throwable
254
     */
255 8
    protected function loadTableForeignKeys(string $tableName): array
256
    {
257
        /** @psalm-var mixed $tableForeignKeys */
258 8
        $tableForeignKeys = $this->loadTableConstraints($tableName, self::FOREIGN_KEYS);
259 8
        return is_array($tableForeignKeys) ? $tableForeignKeys : [];
260
    }
261
262
    /**
263
     * @throws Exception
264
     * @throws InvalidConfigException
265
     * @throws NotSupportedException
266
     * @throws Throwable
267
     */
268 38
    protected function loadTableIndexes(string $tableName): array
269
    {
270 38
        $sql = <<<SQL
271
        SELECT "ui"."INDEX_NAME" AS "name", "uicol"."COLUMN_NAME" AS "column_name",
272
        CASE "ui"."UNIQUENESS" WHEN 'UNIQUE' THEN 1 ELSE 0 END AS "index_is_unique",
273
        CASE WHEN "uc"."CONSTRAINT_NAME" IS NOT NULL THEN 1 ELSE 0 END AS "index_is_primary"
274
        FROM "SYS"."USER_INDEXES" "ui"
275
        LEFT JOIN "SYS"."USER_IND_COLUMNS" "uicol"
276
        ON "uicol"."INDEX_NAME" = "ui"."INDEX_NAME"
277
        LEFT JOIN "SYS"."USER_CONSTRAINTS" "uc"
278
        ON "uc"."OWNER" = "ui"."TABLE_OWNER" AND "uc"."CONSTRAINT_NAME" = "ui"."INDEX_NAME" AND "uc"."CONSTRAINT_TYPE" = 'P'
279
        WHERE "ui"."TABLE_OWNER" = :schemaName AND "ui"."TABLE_NAME" = :tableName
280
        ORDER BY "uicol"."COLUMN_POSITION" ASC
281 38
        SQL;
282
283 38
        $resolvedName = $this->resolveTableName($tableName);
284 38
        $indexes = $this->db->createCommand($sql, [
285 38
            ':schemaName' => $resolvedName->getSchemaName(),
286 38
            ':tableName' => $resolvedName->getName(),
287 38
        ])->queryAll();
288
289
        /** @psalm-var array[] $indexes */
290 38
        $indexes = $this->normalizeRowKeyCase($indexes, true);
291 38
        $indexes = DbArrayHelper::index($indexes, null, ['name']);
292
293 38
        $result = [];
294
295
        /**
296
         * @psalm-var object|string|null $name
297
         * @psalm-var array[] $index
298
         */
299 38
        foreach ($indexes as $name => $index) {
300 35
            $columnNames = DbArrayHelper::getColumn($index, 'column_name');
301
302 35
            if ($columnNames[0] === null) {
303 20
                $columnNames[0] = '';
304
            }
305
306 35
            $result[] = (new IndexConstraint())
307 35
                ->primary((bool) $index[0]['index_is_primary'])
308 35
                ->unique((bool) $index[0]['index_is_unique'])
309 35
                ->name($name)
310 35
                ->columnNames($columnNames);
311
        }
312
313 38
        return $result;
314
    }
315
316
    /**
317
     * @throws Exception
318
     * @throws InvalidConfigException
319
     * @throws NotSupportedException
320
     * @throws Throwable
321
     */
322 17
    protected function loadTableUniques(string $tableName): array
323
    {
324
        /** @psalm-var mixed $tableUniques */
325 17
        $tableUniques = $this->loadTableConstraints($tableName, self::UNIQUES);
326 17
        return is_array($tableUniques) ? $tableUniques : [];
327
    }
328
329
    /**
330
     * @throws Exception
331
     * @throws InvalidConfigException
332
     * @throws NotSupportedException
333
     * @throws Throwable
334
     */
335 17
    protected function loadTableChecks(string $tableName): array
336
    {
337
        /** @psalm-var mixed $tableCheck */
338 17
        $tableCheck = $this->loadTableConstraints($tableName, self::CHECKS);
339 17
        return is_array($tableCheck) ? $tableCheck : [];
340
    }
341
342
    /**
343
     * @throws NotSupportedException If this method is called.
344
     */
345 13
    protected function loadTableDefaultValues(string $tableName): array
346
    {
347 13
        throw new NotSupportedException(__METHOD__ . ' is not supported by Oracle.');
348
    }
349
350
    /**
351
     * Collects the table column metadata.
352
     *
353
     * @param TableSchemaInterface $table The table schema.
354
     *
355
     * @throws Exception
356
     * @throws Throwable
357
     *
358
     * @return bool Whether the table exists.
359
     */
360 147
    protected function findColumns(TableSchemaInterface $table): bool
361
    {
362 147
        $sql = <<<SQL
363
        SELECT
364
            A.COLUMN_NAME,
365
            A.DATA_TYPE,
366
            A.DATA_PRECISION,
367
            A.DATA_SCALE,
368
            A.IDENTITY_COLUMN,
369
            (
370
            CASE A.CHAR_USED WHEN 'C' THEN A.CHAR_LENGTH
371
                ELSE A.DATA_LENGTH
372
            END
373
            ) AS DATA_LENGTH,
374
            A.NULLABLE,
375
            A.DATA_DEFAULT,
376
            (
377
                SELECT COUNT(*)
378
                FROM ALL_CONSTRAINTS AC
379
                INNER JOIN ALL_CONS_COLUMNS ACC ON ACC.CONSTRAINT_NAME=AC.CONSTRAINT_NAME
380
                WHERE
381
                     AC.OWNER = A.OWNER
382
                   AND AC.TABLE_NAME = B.OBJECT_NAME
383
                   AND ACC.COLUMN_NAME = A.COLUMN_NAME
384
                   AND AC.CONSTRAINT_TYPE = 'P'
385
            ) AS IS_PK,
386
            COM.COMMENTS AS COLUMN_COMMENT
387
        FROM ALL_TAB_COLUMNS A
388
            INNER JOIN ALL_OBJECTS B ON B.OWNER = A.OWNER AND LTRIM(B.OBJECT_NAME) = LTRIM(A.TABLE_NAME)
389
            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)
390
        WHERE
391
            A.OWNER = :schemaName
392
            AND B.OBJECT_TYPE IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW')
393
            AND B.OBJECT_NAME = :tableName
394
        ORDER BY A.COLUMN_ID
395 147
        SQL;
396
397 147
        $columns = $this->db->createCommand($sql, [
398 147
            ':tableName' => $table->getName(),
399 147
            ':schemaName' => $table->getSchemaName(),
400 147
        ])->queryAll();
401
402 147
        if ($columns === []) {
403 33
            return false;
404
        }
405
406
        /** @psalm-var ColumnInfoArray $info */
407 130
        foreach ($columns as $info) {
408
            /** @psalm-var ColumnInfoArray $info */
409 130
            $info = $this->normalizeRowKeyCase($info, false);
410
411 130
            $column = $this->loadColumnSchema($info);
412
413 130
            $table->column($column->getName(), $column);
414
        }
415
416 130
        return true;
417
    }
418
419
    /**
420
     * Sequence name of table.
421
     *
422
     * @throws Exception
423
     * @throws InvalidConfigException
424
     * @throws Throwable
425
     *
426
     * @return bool|float|int|string|null Whether the sequence exists.
427
     *
428
     * @internal TableSchemaInterface `$table->getName()` The table schema.
429
     */
430 80
    protected function getTableSequenceName(string $tableName): bool|float|int|string|null
431
    {
432 80
        $sequenceNameSql = <<<SQL
433
        SELECT
434
            UD.REFERENCED_NAME AS SEQUENCE_NAME
435
        FROM USER_DEPENDENCIES UD
436
            JOIN USER_TRIGGERS UT ON (UT.TRIGGER_NAME = UD.NAME)
437
        WHERE
438
            UT.TABLE_NAME = :tableName
439
            AND UD.TYPE = 'TRIGGER'
440
            AND UD.REFERENCED_TYPE = 'SEQUENCE'
441 80
        SQL;
442 80
        $sequenceName = $this->db->createCommand($sequenceNameSql, [':tableName' => $tableName])->queryScalar();
443
444 80
        return $sequenceName === false ? null : $sequenceName;
445
    }
446
447
    /**
448
     * Loads the column information into a {@see ColumnSchemaInterface} object.
449
     *
450
     * @param array $info The column information.
451
     *
452 130
     * @return ColumnSchemaInterface The column schema object.
453
     *
454 130
     * @psalm-param ColumnInfoArray $info The column information.
455 130
     */
456 130
    private function loadColumnSchema(array $info): ColumnSchemaInterface
457 130
    {
458 130
        $dbType = $info['data_type'];
459 130
        $type = $this->extractColumnType($dbType, $info);
460 130
        /** @psalm-var ColumnInfoArray $info */
461 130
        $column = $this->createColumnSchema($type, $info['column_name']);
0 ignored issues
show
Bug introduced by
The method createColumnSchema() does not exist on Yiisoft\Db\Oracle\Schema. Did you maybe mean createColumn()? ( Ignorable by Annotation )

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

461
        /** @scrutinizer ignore-call */ 
462
        $column = $this->createColumnSchema($type, $info['column_name']);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
462 130
        $column->allowNull($info['nullable'] === 'Y');
463 130
        $column->comment($info['column_comment']);
464 130
        $column->primaryKey((bool) $info['is_pk']);
465 130
        $column->autoIncrement($info['identity_column'] === 'YES');
466
        $column->size((int) $info['data_length']);
467 130
        $column->precision($info['data_precision'] !== null ? (int) $info['data_precision'] : null);
468
        $column->scale($info['data_scale'] !== null ? (int) $info['data_scale'] : null);
469
        $column->dbType($dbType);
470
        $column->phpType($this->getColumnPhpType($type));
0 ignored issues
show
Bug introduced by
$type of type string is incompatible with the type Yiisoft\Db\Schema\ColumnSchemaInterface expected by parameter $column of Yiisoft\Db\Schema\Abstra...ema::getColumnPhpType(). ( Ignorable by Annotation )

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

470
        $column->phpType($this->getColumnPhpType(/** @scrutinizer ignore-type */ $type));
Loading history...
471
        $column->defaultValue($this->normalizeDefaultValue($info['data_default'], $column));
472
473
        return $column;
474
    }
475
476
    protected function createPhpTypeColumnSchema(string $phpType, string $name): ColumnSchemaInterface
477
    {
478 130
        if ($phpType === self::PHP_TYPE_RESOURCE) {
479
            return new BinaryColumnSchema($name);
480 130
        }
481 127
482
        return parent::createPhpTypeColumnSchema($phpType, $name);
483
    }
484 72
485
    /**
486 72
     * Converts column's default value according to {@see ColumnSchema::phpType} after retrieval from the database.
487 26
     *
488
     * @param string|null $defaultValue The default value retrieved from the database.
489
     * @param ColumnSchemaInterface $column The column schema object.
490 72
     *
491 26
     * @return mixed The normalized default value.
492
     */
493
    private function normalizeDefaultValue(string|null $defaultValue, ColumnSchemaInterface $column): mixed
494 72
    {
495 26
        if ($defaultValue === null || $column->isPrimaryKey()) {
496
            return null;
497
        }
498 72
499
        $defaultValue = trim($defaultValue);
500
501
        if ($defaultValue === 'NULL') {
502
            return null;
503
        }
504
505
        if ($column->getType() === self::TYPE_TIMESTAMP && $defaultValue === 'CURRENT_TIMESTAMP') {
506
            return new Expression($defaultValue);
507
        }
508
509
        if (preg_match("/^'(.*)'$/s", $defaultValue, $matches) === 1) {
510 130
            $defaultValue = str_replace("''", "'", $matches[1]);
511
        }
512 130
513
        return $column->phpTypecast($defaultValue);
514
    }
515
516
    /**
517
     * Finds constraints and fills them into TableSchemaInterface object passed.
518
     *
519
     * @throws Exception
520
     * @throws InvalidConfigException
521
     * @throws Throwable
522
     *
523
     * @psalm-suppress PossiblyNullArrayOffset
524
     */
525
    protected function findConstraints(TableSchemaInterface $table): void
526
    {
527
        $sql = <<<SQL
528
        SELECT
529
            /*+ PUSH_PRED(C) PUSH_PRED(D) PUSH_PRED(E) */
530
            D.CONSTRAINT_NAME,
531 130
            D.CONSTRAINT_TYPE,
532
            C.COLUMN_NAME,
533
            C.POSITION,
534
            D.R_CONSTRAINT_NAME,
535
            E.TABLE_NAME AS TABLE_REF,
536
            F.COLUMN_NAME AS COLUMN_REF,
537
            C.TABLE_NAME
538
        FROM ALL_CONS_COLUMNS C
539
            INNER JOIN ALL_CONSTRAINTS D ON D.OWNER = C.OWNER AND D.CONSTRAINT_NAME = C.CONSTRAINT_NAME
540
            LEFT JOIN ALL_CONSTRAINTS E ON E.OWNER = D.R_OWNER AND E.CONSTRAINT_NAME = D.R_CONSTRAINT_NAME
541
            LEFT JOIN ALL_CONS_COLUMNS F ON F.OWNER = E.OWNER AND F.CONSTRAINT_NAME = E.CONSTRAINT_NAME AND F.POSITION = C.POSITION
542
        WHERE
543
            C.OWNER = :schemaName
544
            AND C.TABLE_NAME = :tableName
545
            ORDER BY D.CONSTRAINT_NAME, C.POSITION
546
        SQL;
547 130
548 130
        /**
549 130
         * @psalm-var array{
550 130
         *   array{
551
         *     constraint_name: string,
552 130
         *     constraint_type: string,
553
         *     column_name: string,
554 130
         *     position: string|null,
555
         *     r_constraint_name: string|null,
556 120
         *     table_ref: string|null,
557
         *     column_ref: string|null,
558 120
         *     table_name: string
559 80
         *   }
560 80
         * } $rows
561
         */
562 80
        $rows = $this->db->createCommand(
563 80
            $sql,
564
            [':tableName' => $table->getName(), ':schemaName' => $table->getSchemaName()]
565
        )->queryAll();
566
567 120
        $constraints = [];
568
569
        foreach ($rows as $row) {
570
            /** @psalm-var string[] $row */
571
            $row = $this->normalizeRowKeyCase($row, false);
572
573 120
            if ($row['constraint_type'] === 'P') {
574
                $table->getColumns()[$row['column_name']]->primaryKey(true);
575
                $table->primaryKey($row['column_name']);
576 14
577
                if (empty($table->getSequenceName())) {
578 14
                    $table->sequenceName((string) $this->getTableSequenceName($table->getName()));
579 14
                }
580 14
            }
581 14
582 14
            if ($row['constraint_type'] !== 'R') {
583
                /**
584
                 * This condition isn't checked in `WHERE` because of an Oracle Bug:
585 14
                 *
586
                 * @link https://github.com/yiisoft/yii2/pull/8844
587
                 */
588 130
                continue;
589 14
            }
590
591
            $name = $row['constraint_name'];
592
593
            if (!isset($constraints[$name])) {
594
                $constraints[$name] = [
595
                    'tableName' => $row['table_ref'],
596
                    'columns' => [],
597
                ];
598
            }
599
600
            $constraints[$name]['columns'][$row['column_name']] = $row['column_ref'];
601
        }
602
603
        foreach ($constraints as $index => $constraint) {
604
            $table->foreignKey($index, array_merge([$constraint['tableName']], $constraint['columns']));
605
        }
606
    }
607
608
    /**
609
     * Returns all unique indexes for the given table.
610
     *
611
     * Each array element is of the following structure:.
612
     *
613 1
     * ```php
614
     * [
615 1
     *     'IndexName1' => ['col1' [, ...]],
616
     *     'IndexName2' => ['col2' [, ...]],
617
     * ]
618
     * ```
619
     *
620
     * @param TableSchemaInterface $table The table metadata.
621
     *
622
     * @throws Exception
623
     * @throws InvalidConfigException
624
     * @throws Throwable
625
     *
626 1
     * @return array All unique indexes for the given table.
627 1
     */
628
    public function findUniqueIndexes(TableSchemaInterface $table): array
629 1
    {
630 1
        $query = <<<SQL
631 1
        SELECT
632 1
            DIC.INDEX_NAME,
633
            DIC.COLUMN_NAME
634
        FROM ALL_INDEXES DI
635 1
            INNER JOIN ALL_IND_COLUMNS DIC ON DI.TABLE_NAME = DIC.TABLE_NAME AND DI.INDEX_NAME = DIC.INDEX_NAME
636 1
        WHERE
637
            DI.UNIQUENESS = 'UNIQUE'
638
            AND DIC.TABLE_OWNER = :schemaName
639 1
            AND DIC.TABLE_NAME = :tableName
640
        ORDER BY DIC.TABLE_NAME, DIC.INDEX_NAME, DIC.COLUMN_POSITION
641
        SQL;
642
        $result = [];
643
644
        $rows = $this->db->createCommand(
645
            $query,
646
            [':tableName' => $table->getName(), ':schemaName' => $table->getschemaName()]
647
        )->queryAll();
648
649 130
        /** @psalm-var array<array{INDEX_NAME: string, COLUMN_NAME: string}> $rows */
650
        foreach ($rows as $row) {
651 130
            $result[$row['INDEX_NAME']][] = $row['COLUMN_NAME'];
652
        }
653 130
654 122
        return $result;
655 122
    }
656 122
657 122
    /**
658 122
     * Extracts the data type for the given column.
659
     *
660
     * @param string $dbType The database data type
661 102
     * @param array $info Column information.
662
     * @psalm-param ColumnInfoArray $info
663 102
     *
664 26
     * @return string The abstract column type.
665
     */
666
    private function extractColumnType(string $dbType, array $info): string
667 102
    {
668
        $dbType = strtolower($dbType);
669
670
        if ($dbType === 'number') {
671
            $scale = $info['data_scale'] !== null ? (int) $info['data_scale'] : null;
672
673
            return match ($scale) {
674
                null => self::TYPE_DOUBLE,
675
                0 => self::TYPE_INTEGER,
676
                default => self::TYPE_DECIMAL,
677
            };
678
        }
679
680
        $dbType = preg_replace('/\([^)]+\)/', '', $dbType);
681
682
        if ($dbType === 'interval day to second' && $info['data_precision'] > 0) {
683
            return self::TYPE_STRING;
684
        }
685
686
        return $this->typeMap[$dbType] ?? self::TYPE_STRING;
687 89
    }
688
689 89
    /**
690
     * Loads multiple types of constraints and returns the specified ones.
691
     *
692
     * @param string $tableName The table name.
693
     * @param string $returnType The return type:
694
     * - primaryKey
695
     * - foreignKeys
696
     * - uniques
697
     * - checks
698
     *
699
     * @throws Exception
700
     * @throws InvalidConfigException
701
     * @throws NotSupportedException
702
     * @throws Throwable
703
     *
704
     * @return mixed Constraints.
705
     */
706
    private function loadTableConstraints(string $tableName, string $returnType): mixed
707
    {
708 89
        $sql = <<<SQL
709
        SELECT
710 89
            "uc"."CONSTRAINT_NAME" AS "name",
711 89
            "uccol"."COLUMN_NAME" AS "column_name",
712 89
            "uc"."CONSTRAINT_TYPE" AS "type",
713 89
            "fuc"."OWNER" AS "foreign_table_schema",
714 89
            "fuc"."TABLE_NAME" AS "foreign_table_name",
715
            "fuccol"."COLUMN_NAME" AS "foreign_column_name",
716
            "uc"."DELETE_RULE" AS "on_delete",
717 89
            "uc"."SEARCH_CONDITION" AS "check_expr"
718 89
        FROM "USER_CONSTRAINTS" "uc"
719
        INNER JOIN "USER_CONS_COLUMNS" "uccol"
720 89
        ON "uccol"."OWNER" = "uc"."OWNER" AND "uccol"."CONSTRAINT_NAME" = "uc"."CONSTRAINT_NAME"
721 89
        LEFT JOIN "USER_CONSTRAINTS" "fuc"
722 89
        ON "fuc"."OWNER" = "uc"."R_OWNER" AND "fuc"."CONSTRAINT_NAME" = "uc"."R_CONSTRAINT_NAME"
723 89
        LEFT JOIN "USER_CONS_COLUMNS" "fuccol"
724 89
        ON "fuccol"."OWNER" = "fuc"."OWNER" AND "fuccol"."CONSTRAINT_NAME" = "fuc"."CONSTRAINT_NAME" AND "fuccol"."POSITION" = "uccol"."POSITION"
725 89
        WHERE "uc"."OWNER" = :schemaName AND "uc"."TABLE_NAME" = :tableName
726
        ORDER BY "uccol"."POSITION" ASC
727
        SQL;
728
729
        $resolvedName = $this->resolveTableName($tableName);
730
        $constraints = $this->db->createCommand($sql, [
731 89
            ':schemaName' => $resolvedName->getSchemaName(),
732
            ':tableName' => $resolvedName->getName(),
733
        ])->queryAll();
734
735
        /** @psalm-var array[] $constraints */
736 82
        $constraints = $this->normalizeRowKeyCase($constraints, true);
737
        $constraints = DbArrayHelper::index($constraints, null, ['type', 'name']);
738 82
739 57
        $result = [
740 57
            self::PRIMARY_KEY => null,
741 57
            self::FOREIGN_KEYS => [],
742 57
            self::UNIQUES => [],
743 82
            self::CHECKS => [],
744 19
        ];
745 19
746 19
        /**
747 19
         * @psalm-var string $type
748 19
         * @psalm-var array $names
749 19
         */
750 19
        foreach ($constraints as $type => $names) {
751 19
            /**
752 19
             * @psalm-var object|string|null $name
753 82
             * @psalm-var ConstraintArray $constraint
754 58
             */
755 58
            foreach ($names as $name => $constraint) {
756 58
                switch ($type) {
757 58
                    case 'P':
758 82
                        $result[self::PRIMARY_KEY] = (new Constraint())
759 82
                            ->name($name)
760 82
                            ->columnNames(DbArrayHelper::getColumn($constraint, 'column_name'));
761 82
                        break;
762 82
                    case 'R':
763 82
                        $result[self::FOREIGN_KEYS][] = (new ForeignKeyConstraint())
764
                            ->name($name)
765
                            ->columnNames(DbArrayHelper::getColumn($constraint, 'column_name'))
766
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
767
                            ->foreignTableName($constraint[0]['foreign_table_name'])
768 89
                            ->foreignColumnNames(DbArrayHelper::getColumn($constraint, 'foreign_column_name'))
769 89
                            ->onDelete($constraint[0]['on_delete'])
770
                            ->onUpdate(null);
771
                        break;
772 89
                    case 'U':
773
                        $result[self::UNIQUES][] = (new Constraint())
774
                            ->name($name)
775
                            ->columnNames(DbArrayHelper::getColumn($constraint, 'column_name'));
776
                        break;
777
                    case 'C':
778
                        $result[self::CHECKS][] = (new CheckConstraint())
779
                            ->name($name)
780 2
                            ->columnNames(DbArrayHelper::getColumn($constraint, 'column_name'))
781
                            ->expression($constraint[0]['check_expr']);
782 2
                        break;
783 2
                }
784
            }
785 2
        }
786 2
787 2
        foreach ($result as $type => $data) {
788 2
            $this->setTableMetadata($tableName, $type, $data);
789 2
        }
790
791
        return $result[$returnType];
792 2
    }
793
794 2
    /**
795 2
     * @throws Exception
796
     * @throws InvalidConfigException
797
     * @throws Throwable
798 2
     */
799
    protected function findViewNames(string $schema = ''): array
800
    {
801
        $sql = match ($schema) {
802
            '' => <<<SQL
803
            SELECT VIEW_NAME FROM USER_VIEWS
804
            SQL,
805
            default => <<<SQL
806
            SELECT VIEW_NAME FROM ALL_VIEWS WHERE OWNER = '$schema'
807
            SQL,
808 245
        };
809
810 245
        /** @psalm-var string[][] $views */
811
        $views = $this->db->createCommand($sql)->queryAll();
812
813
        foreach ($views as $key => $view) {
814
            $views[$key] = $view['VIEW_NAME'];
815
        }
816
817
        return $views;
818
    }
819
820 207
    /**
821
     * Returns the cache key for the specified table name.
822 207
     *
823
     * @param string $name The table name.
824
     *
825
     * @return array The cache key.
826
     */
827
    protected function getCacheKey(string $name): array
828
    {
829
        return array_merge([self::class], $this->generateCacheKey(), [$this->getRawTableName($name)]);
830
    }
831
832
    /**
833
     * Returns the cache tag name.
834
     *
835
     * This allows {@see refresh()} to invalidate all cached table schemas.
836
     *
837
     * @return string The cache tag name.
838
     */
839
    protected function getCacheTag(): string
840
    {
841
        return md5(serialize(array_merge([self::class], $this->generateCacheKey())));
842
    }
843
}
844