Passed
Push — master ( dae9a9...ebb464 )
by Sergei
20:43 queued 13:54
created

Schema::findColumns()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 57
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 3

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 3
eloc 45
c 3
b 0
f 0
nc 3
nop 1
dl 0
loc 57
ccs 14
cts 14
cp 1
crap 3
rs 9.2

How to fix   Long Method   

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\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\ColumnSchemaInterface;
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
     * 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
     * @var string[]
75
     */
76
    private const TYPE_MAP = [
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 559
    public function __construct(protected ConnectionInterface $db, SchemaCache $schemaCache, string $defaultSchema)
102
    {
103 559
        $this->defaultSchema = $defaultSchema;
104 559
        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 213
    protected function resolveTableName(string $name): TableSchemaInterface
113
    {
114 213
        $resolvedName = new TableSchema();
115
116 213
        $parts = array_reverse(
117 213
            $this->db->getQuoter()->getTableNameParts($name)
118 213
        );
119
120 213
        $resolvedName->name($parts[0] ?? '');
121 213
        $resolvedName->schemaName($parts[1] ?? $this->defaultSchema);
122
123 213
        $resolvedName->fullName(
124 213
            $resolvedName->getSchemaName() !== $this->defaultSchema ?
125 213
            implode('.', array_reverse($parts)) : $resolvedName->getName()
126 213
        );
127
128 213
        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 151
    protected function findTableComment(TableSchemaInterface $tableSchema): void
157
    {
158 151
        $sql = <<<SQL
159
        SELECT "COMMENTS"
160
        FROM ALL_TAB_COMMENTS
161
        WHERE
162
              "OWNER" = :schemaName AND
163
              "TABLE_NAME" = :tableName
164 151
        SQL;
165
166 151
        $comment = $this->db->createCommand($sql, [
167 151
            ':schemaName' => $tableSchema->getSchemaName(),
168 151
            ':tableName' => $tableSchema->getName(),
169 151
        ])->queryScalar();
170
171 151
        $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 151
    protected function loadTableSchema(string $name): TableSchemaInterface|null
224
    {
225 151
        $table = $this->resolveTableName($name);
226 151
        $this->findTableComment($table);
227
228 151
        if ($this->findColumns($table)) {
229 134
            $this->findConstraints($table);
230 134
            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 48
    protected function loadTablePrimaryKey(string $tableName): Constraint|null
243
    {
244
        /** @psalm-var mixed $tablePrimaryKey */
245 48
        $tablePrimaryKey = $this->loadTableConstraints($tableName, self::PRIMARY_KEY);
246 48
        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 39
    protected function loadTableIndexes(string $tableName): array
269
    {
270 39
        $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 39
        SQL;
282
283 39
        $resolvedName = $this->resolveTableName($tableName);
284 39
        $indexes = $this->db->createCommand($sql, [
285 39
            ':schemaName' => $resolvedName->getSchemaName(),
286 39
            ':tableName' => $resolvedName->getName(),
287 39
        ])->queryAll();
288
289
        /** @psalm-var array[] $indexes */
290 39
        $indexes = $this->normalizeRowKeyCase($indexes, true);
291 39
        $indexes = DbArrayHelper::index($indexes, null, ['name']);
292
293 39
        $result = [];
294
295
        /**
296
         * @psalm-var object|string|null $name
297
         * @psalm-var array[] $index
298
         */
299 39
        foreach ($indexes as $name => $index) {
300 36
            $columnNames = DbArrayHelper::getColumn($index, 'column_name');
301
302 36
            if ($columnNames[0] === null) {
303 21
                $columnNames[0] = '';
304
            }
305
306 36
            $result[] = (new IndexConstraint())
307 36
                ->primary((bool) $index[0]['index_is_primary'])
308 36
                ->unique((bool) $index[0]['index_is_unique'])
309 36
                ->name($name)
310 36
                ->columnNames($columnNames);
311
        }
312
313 39
        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 151
    protected function findColumns(TableSchemaInterface $table): bool
361
    {
362 151
        $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 151
        SQL;
396
397 151
        $columns = $this->db->createCommand($sql, [
398 151
            ':tableName' => $table->getName(),
399 151
            ':schemaName' => $table->getSchemaName(),
400 151
        ])->queryAll();
401
402 151
        if ($columns === []) {
403 33
            return false;
404
        }
405
406
        /** @psalm-var string[][] $columns */
407 134
        foreach ($columns as $column) {
408
            /** @psalm-var ColumnInfoArray $column */
409 134
            $column = $this->normalizeRowKeyCase($column, false);
410
411 134
            $c = $this->createColumnSchema($column);
412
413 134
            $table->column($c->getName(), $c);
414
        }
415
416 134
        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 78
    protected function getTableSequenceName(string $tableName): bool|float|int|string|null
431
    {
432 78
        $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 78
        SQL;
442 78
        $sequenceName = $this->db->createCommand($sequenceNameSql, [':tableName' => $tableName])->queryScalar();
443
444 78
        return $sequenceName === false ? null : $sequenceName;
445
    }
446
447
    /**
448
     * Creates ColumnSchema instance.
449
     *
450
     * @psalm-param ColumnInfoArray $info
451
     */
452 134
    protected function createColumnSchema(array $info): ColumnSchemaInterface
453
    {
454 134
        $column = new ColumnSchema($info['column_name']);
455 134
        $column->allowNull($info['nullable'] === 'Y');
456 134
        $column->comment($info['column_comment']);
457 134
        $column->primaryKey((bool) $info['is_pk']);
458 134
        $column->autoIncrement($info['identity_column'] === 'YES');
459 134
        $column->size((int) $info['data_length']);
460 134
        $column->precision($info['data_precision'] !== null ? (int) $info['data_precision'] : null);
461 134
        $column->scale($info['data_scale'] !== null ? (int) $info['data_scale'] : null);
462 134
        $column->dbType($info['data_type']);
463 134
        $column->type($this->extractColumnType($column));
464 134
        $column->phpType($this->getColumnPhpType($column));
465 134
        $column->defaultValue($this->normalizeDefaultValue($info['data_default'], $column));
466
467 134
        return $column;
468
    }
469
470
    /**
471
     * Converts column's default value according to {@see ColumnSchema::phpType} after retrieval from the database.
472
     *
473
     * @param string|null $defaultValue The default value retrieved from the database.
474
     * @param ColumnSchemaInterface $column The column schema object.
475
     *
476
     * @return mixed The normalized default value.
477
     */
478 134
    private function normalizeDefaultValue(string|null $defaultValue, ColumnSchemaInterface $column): mixed
479
    {
480 134
        if ($defaultValue === null || $column->isPrimaryKey()) {
481 130
            return null;
482
        }
483
484 76
        $defaultValue = trim($defaultValue);
485
486 76
        if ($defaultValue === 'NULL') {
487 31
            return null;
488
        }
489
490 76
        if ($column->getType() === self::TYPE_TIMESTAMP && $defaultValue === 'CURRENT_TIMESTAMP') {
491 31
            return new Expression($defaultValue);
492
        }
493
494 76
        if (preg_match("/^'(.*)'$/s", $defaultValue, $matches) === 1) {
495 31
            $defaultValue = str_replace("''", "'", $matches[1]);
496
        }
497
498 76
        return $column->phpTypecast($defaultValue);
499
    }
500
501
    /**
502
     * Finds constraints and fills them into TableSchemaInterface object passed.
503
     *
504
     * @throws Exception
505
     * @throws InvalidConfigException
506
     * @throws Throwable
507
     *
508
     * @psalm-suppress PossiblyNullArrayOffset
509
     */
510 134
    protected function findConstraints(TableSchemaInterface $table): void
511
    {
512 134
        $sql = <<<SQL
513
        SELECT
514
            /*+ PUSH_PRED(C) PUSH_PRED(D) PUSH_PRED(E) */
515
            D.CONSTRAINT_NAME,
516
            D.CONSTRAINT_TYPE,
517
            C.COLUMN_NAME,
518
            C.POSITION,
519
            D.R_CONSTRAINT_NAME,
520
            E.TABLE_NAME AS TABLE_REF,
521
            F.COLUMN_NAME AS COLUMN_REF,
522
            C.TABLE_NAME
523
        FROM ALL_CONS_COLUMNS C
524
            INNER JOIN ALL_CONSTRAINTS D ON D.OWNER = C.OWNER AND D.CONSTRAINT_NAME = C.CONSTRAINT_NAME
525
            LEFT JOIN ALL_CONSTRAINTS E ON E.OWNER = D.R_OWNER AND E.CONSTRAINT_NAME = D.R_CONSTRAINT_NAME
526
            LEFT JOIN ALL_CONS_COLUMNS F ON F.OWNER = E.OWNER AND F.CONSTRAINT_NAME = E.CONSTRAINT_NAME AND F.POSITION = C.POSITION
527
        WHERE
528
            C.OWNER = :schemaName
529
            AND C.TABLE_NAME = :tableName
530
            ORDER BY D.CONSTRAINT_NAME, C.POSITION
531 134
        SQL;
532
533
        /**
534
         * @psalm-var array{
535
         *   array{
536
         *     constraint_name: string,
537
         *     constraint_type: string,
538
         *     column_name: string,
539
         *     position: string|null,
540
         *     r_constraint_name: string|null,
541
         *     table_ref: string|null,
542
         *     column_ref: string|null,
543
         *     table_name: string
544
         *   }
545
         * } $rows
546
         */
547 134
        $rows = $this->db->createCommand(
548 134
            $sql,
549 134
            [':tableName' => $table->getName(), ':schemaName' => $table->getSchemaName()]
550 134
        )->queryAll();
551
552 134
        $constraints = [];
553
554 134
        foreach ($rows as $row) {
555
            /** @psalm-var string[] $row */
556 123
            $row = $this->normalizeRowKeyCase($row, false);
557
558 123
            if ($row['constraint_type'] === 'P') {
559 78
                $table->getColumns()[$row['column_name']]->primaryKey(true);
560 78
                $table->primaryKey($row['column_name']);
561
562 78
                if (empty($table->getSequenceName())) {
563 78
                    $table->sequenceName((string) $this->getTableSequenceName($table->getName()));
564
                }
565
            }
566
567 123
            if ($row['constraint_type'] !== 'R') {
568
                /**
569
                 * This condition isn't checked in `WHERE` because of an Oracle Bug:
570
                 *
571
                 * @link https://github.com/yiisoft/yii2/pull/8844
572
                 */
573 123
                continue;
574
            }
575
576 14
            $name = $row['constraint_name'];
577
578 14
            if (!isset($constraints[$name])) {
579 14
                $constraints[$name] = [
580 14
                    'tableName' => $row['table_ref'],
581 14
                    'columns' => [],
582 14
                ];
583
            }
584
585 14
            $constraints[$name]['columns'][$row['column_name']] = $row['column_ref'];
586
        }
587
588 134
        foreach ($constraints as $index => $constraint) {
589 14
            $table->foreignKey($index, array_merge([$constraint['tableName']], $constraint['columns']));
590
        }
591
    }
592
593
    /**
594
     * Returns all unique indexes for the given table.
595
     *
596
     * Each array element is of the following structure:.
597
     *
598
     * ```php
599
     * [
600
     *     'IndexName1' => ['col1' [, ...]],
601
     *     'IndexName2' => ['col2' [, ...]],
602
     * ]
603
     * ```
604
     *
605
     * @param TableSchemaInterface $table The table metadata.
606
     *
607
     * @throws Exception
608
     * @throws InvalidConfigException
609
     * @throws Throwable
610
     *
611
     * @return array All unique indexes for the given table.
612
     */
613 1
    public function findUniqueIndexes(TableSchemaInterface $table): array
614
    {
615 1
        $query = <<<SQL
616
        SELECT
617
            DIC.INDEX_NAME,
618
            DIC.COLUMN_NAME
619
        FROM ALL_INDEXES DI
620
            INNER JOIN ALL_IND_COLUMNS DIC ON DI.TABLE_NAME = DIC.TABLE_NAME AND DI.INDEX_NAME = DIC.INDEX_NAME
621
        WHERE
622
            DI.UNIQUENESS = 'UNIQUE'
623
            AND DIC.TABLE_OWNER = :schemaName
624
            AND DIC.TABLE_NAME = :tableName
625
        ORDER BY DIC.TABLE_NAME, DIC.INDEX_NAME, DIC.COLUMN_POSITION
626 1
        SQL;
627 1
        $result = [];
628
629 1
        $rows = $this->db->createCommand(
630 1
            $query,
631 1
            [':tableName' => $table->getName(), ':schemaName' => $table->getschemaName()]
632 1
        )->queryAll();
633
634
        /** @psalm-var array<array{INDEX_NAME: string, COLUMN_NAME: string}> $rows */
635 1
        foreach ($rows as $row) {
636 1
            $result[$row['INDEX_NAME']][] = $row['COLUMN_NAME'];
637
        }
638
639 1
        return $result;
640
    }
641
642
    /**
643
     * Extracts the data type for the given column.
644
     *
645
     * @param ColumnSchemaInterface $column The column schema object.
646
     *
647
     * @return string The abstract column type.
648
     */
649 134
    private function extractColumnType(ColumnSchemaInterface $column): string
650
    {
651 134
        $dbType = strtolower((string) $column->getDbType());
652
653 134
        if ($dbType === 'number') {
654 126
            return match ($column->getScale()) {
655 126
                null => self::TYPE_DOUBLE,
656 126
                0 => self::TYPE_INTEGER,
657 126
                default => self::TYPE_DECIMAL,
658 126
            };
659
        }
660
661 106
        $dbType = preg_replace('/\([^)]+\)/', '', $dbType);
662
663 106
        if ($dbType === 'interval day to second' && $column->getPrecision() > 0) {
664 31
            return self::TYPE_STRING;
665
        }
666
667 106
        return self::TYPE_MAP[$dbType] ?? self::TYPE_STRING;
668
    }
669
670
    /**
671
     * Loads multiple types of constraints and returns the specified ones.
672
     *
673
     * @param string $tableName The table name.
674
     * @param string $returnType The return type:
675
     * - primaryKey
676
     * - foreignKeys
677
     * - uniques
678
     * - checks
679
     *
680
     * @throws Exception
681
     * @throws InvalidConfigException
682
     * @throws NotSupportedException
683
     * @throws Throwable
684
     *
685
     * @return mixed Constraints.
686
     */
687 90
    private function loadTableConstraints(string $tableName, string $returnType): mixed
688
    {
689 90
        $sql = <<<SQL
690
        SELECT
691
            "uc"."CONSTRAINT_NAME" AS "name",
692
            "uccol"."COLUMN_NAME" AS "column_name",
693
            "uc"."CONSTRAINT_TYPE" AS "type",
694
            "fuc"."OWNER" AS "foreign_table_schema",
695
            "fuc"."TABLE_NAME" AS "foreign_table_name",
696
            "fuccol"."COLUMN_NAME" AS "foreign_column_name",
697
            "uc"."DELETE_RULE" AS "on_delete",
698
            "uc"."SEARCH_CONDITION" AS "check_expr"
699
        FROM "USER_CONSTRAINTS" "uc"
700
        INNER JOIN "USER_CONS_COLUMNS" "uccol"
701
        ON "uccol"."OWNER" = "uc"."OWNER" AND "uccol"."CONSTRAINT_NAME" = "uc"."CONSTRAINT_NAME"
702
        LEFT JOIN "USER_CONSTRAINTS" "fuc"
703
        ON "fuc"."OWNER" = "uc"."R_OWNER" AND "fuc"."CONSTRAINT_NAME" = "uc"."R_CONSTRAINT_NAME"
704
        LEFT JOIN "USER_CONS_COLUMNS" "fuccol"
705
        ON "fuccol"."OWNER" = "fuc"."OWNER" AND "fuccol"."CONSTRAINT_NAME" = "fuc"."CONSTRAINT_NAME" AND "fuccol"."POSITION" = "uccol"."POSITION"
706
        WHERE "uc"."OWNER" = :schemaName AND "uc"."TABLE_NAME" = :tableName
707
        ORDER BY "uccol"."POSITION" ASC
708 90
        SQL;
709
710 90
        $resolvedName = $this->resolveTableName($tableName);
711 90
        $constraints = $this->db->createCommand($sql, [
712 90
            ':schemaName' => $resolvedName->getSchemaName(),
713 90
            ':tableName' => $resolvedName->getName(),
714 90
        ])->queryAll();
715
716
        /** @psalm-var array[] $constraints */
717 90
        $constraints = $this->normalizeRowKeyCase($constraints, true);
718 90
        $constraints = DbArrayHelper::index($constraints, null, ['type', 'name']);
719
720 90
        $result = [
721 90
            self::PRIMARY_KEY => null,
722 90
            self::FOREIGN_KEYS => [],
723 90
            self::UNIQUES => [],
724 90
            self::CHECKS => [],
725 90
        ];
726
727
        /**
728
         * @psalm-var string $type
729
         * @psalm-var array $names
730
         */
731 90
        foreach ($constraints as $type => $names) {
732
            /**
733
             * @psalm-var object|string|null $name
734
             * @psalm-var ConstraintArray $constraint
735
             */
736 83
            foreach ($names as $name => $constraint) {
737
                switch ($type) {
738 83
                    case 'P':
739 58
                        $result[self::PRIMARY_KEY] = (new Constraint())
740 58
                            ->name($name)
741 58
                            ->columnNames(DbArrayHelper::getColumn($constraint, 'column_name'));
742 58
                        break;
743 83
                    case 'R':
744 19
                        $result[self::FOREIGN_KEYS][] = (new ForeignKeyConstraint())
745 19
                            ->name($name)
746 19
                            ->columnNames(DbArrayHelper::getColumn($constraint, 'column_name'))
747 19
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
748 19
                            ->foreignTableName($constraint[0]['foreign_table_name'])
749 19
                            ->foreignColumnNames(DbArrayHelper::getColumn($constraint, 'foreign_column_name'))
750 19
                            ->onDelete($constraint[0]['on_delete'])
751 19
                            ->onUpdate(null);
752 19
                        break;
753 83
                    case 'U':
754 59
                        $result[self::UNIQUES][] = (new Constraint())
755 59
                            ->name($name)
756 59
                            ->columnNames(DbArrayHelper::getColumn($constraint, 'column_name'));
757 59
                        break;
758 83
                    case 'C':
759 83
                        $result[self::CHECKS][] = (new CheckConstraint())
760 83
                            ->name($name)
761 83
                            ->columnNames(DbArrayHelper::getColumn($constraint, 'column_name'))
762 83
                            ->expression($constraint[0]['check_expr']);
763 83
                        break;
764
                }
765
            }
766
        }
767
768 90
        foreach ($result as $type => $data) {
769 90
            $this->setTableMetadata($tableName, $type, $data);
770
        }
771
772 90
        return $result[$returnType];
773
    }
774
775
    /**
776
     * @throws Exception
777
     * @throws InvalidConfigException
778
     * @throws Throwable
779
     */
780 2
    protected function findViewNames(string $schema = ''): array
781
    {
782 2
        $sql = match ($schema) {
783 2
            '' => <<<SQL
784
            SELECT VIEW_NAME FROM USER_VIEWS
785 2
            SQL,
786 2
            default => <<<SQL
787 2
            SELECT VIEW_NAME FROM ALL_VIEWS WHERE OWNER = '$schema'
788 2
            SQL,
789 2
        };
790
791
        /** @psalm-var string[][] $views */
792 2
        $views = $this->db->createCommand($sql)->queryAll();
793
794 2
        foreach ($views as $key => $view) {
795 2
            $views[$key] = $view['VIEW_NAME'];
796
        }
797
798 2
        return $views;
799
    }
800
801
    /**
802
     * Returns the cache key for the specified table name.
803
     *
804
     * @param string $name The table name.
805
     *
806
     * @return array The cache key.
807
     */
808 249
    protected function getCacheKey(string $name): array
809
    {
810 249
        return array_merge([self::class], $this->generateCacheKey(), [$this->getRawTableName($name)]);
811
    }
812
813
    /**
814
     * Returns the cache tag name.
815
     *
816
     * This allows {@see refresh()} to invalidate all cached table schemas.
817
     *
818
     * @return string The cache tag name.
819
     */
820 214
    protected function getCacheTag(): string
821
    {
822 214
        return md5(serialize(array_merge([self::class], $this->generateCacheKey())));
823
    }
824
}
825