Passed
Push — master ( 35dac6...248128 )
by Wilmer
14:35 queued 07:47
created

Schema   F

Complexity

Total Complexity 72

Size/Duplication

Total Lines 767
Duplicated Lines 0 %

Test Coverage

Coverage 99.61%

Importance

Changes 11
Bugs 1 Features 1
Metric Value
eloc 351
c 11
b 1
f 1
dl 0
loc 767
ccs 255
cts 256
cp 0.9961
rs 2.64
wmc 72

24 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A getCacheKey() 0 3 1
A getCacheTag() 0 3 1
A extractColumnSize() 0 10 5
B findConstraints() 0 80 7
B extractColumnType() 0 25 9
A findUniqueIndexes() 0 27 2
B loadTableConstraints() 0 86 8
A loadTableForeignKeys() 0 5 2
A findSchemaNames() 0 10 1
A loadTablePrimaryKey() 0 5 2
A findColumns() 0 55 3
A loadTableSchema() 0 11 2
A createColumn() 0 3 1
A loadTableDefaultValues() 0 3 1
A findTableNames() 0 37 3
A resolveTableName() 0 17 2
A loadTableIndexes() 0 46 3
A loadTableUniques() 0 5 2
A loadTableChecks() 0 5 2
A findTableComment() 0 16 2
A getTableSequenceName() 0 15 2
A findViewNames() 0 19 2
B createColumnSchema() 0 50 8

How to fix   Complexity   

Complex Class

Complex classes like Schema often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Schema, and based on these observations, apply Extract Interface, too.

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

622
        /** @scrutinizer ignore-unused */ string|null $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...
623
        string|null $scale,
624
        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

624
        /** @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...
625
    ): void {
626 129
        $column->dbType($dbType);
627
628 129
        if (str_contains($dbType, 'FLOAT') || str_contains($dbType, 'DOUBLE')) {
629 28
            $column->type(self::TYPE_DOUBLE);
630 129
        } elseif (str_contains($dbType, 'NUMBER')) {
631 121
            if ($scale === null || $scale > 0) {
632 31
                $column->type(self::TYPE_DECIMAL);
633
            } else {
634 121
                $column->type(self::TYPE_INTEGER);
635
            }
636 97
        } elseif (str_contains($dbType, 'BLOB')) {
637 30
            $column->type(self::TYPE_BINARY);
638 93
        } elseif (str_contains($dbType, 'CLOB')) {
639 24
            $column->type(self::TYPE_TEXT);
640 92
        } elseif (str_contains($dbType, 'TIMESTAMP')) {
641 26
            $column->type(self::TYPE_TIMESTAMP);
642
        } else {
643 92
            $column->type(self::TYPE_STRING);
644
        }
645
    }
646
647
    /**
648
     * Extracts size, precision and scale information from column's DB type.
649
     *
650
     * @param string $dbType The column's DB type.
651
     * @param string|null $precision Total number of digits.
652
     * @param string|null $scale Number of digits on the right of the decimal separator.
653
     * @param string $length The length for character types.
654
     */
655 129
    protected function extractColumnSize(
656
        ColumnSchema $column,
657
        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

657
        /** @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...
658
        string|null $precision,
659
        string|null $scale,
660
        string $length
661
    ): void {
662 129
        $column->size(trim($length) === '' ? null : (int) $length);
663 129
        $column->precision(trim((string) $precision) === '' ? null : (int) $precision);
664 129
        $column->scale($scale === '' || $scale === null ? null : (int) $scale);
665
    }
666
667
    /**
668
     * Loads multiple types of constraints and returns the specified ones.
669
     *
670
     * @param string $tableName The table name.
671
     * @param string $returnType The return type:
672
     * - primaryKey
673
     * - foreignKeys
674
     * - uniques
675
     * - checks
676
     *
677
     * @throws Exception
678
     * @throws InvalidConfigException
679
     * @throws NotSupportedException
680
     * @throws Throwable
681
     *
682
     * @return mixed Constraints.
683
     */
684 89
    private function loadTableConstraints(string $tableName, string $returnType): mixed
685
    {
686 89
        $sql = <<<SQL
687
        SELECT
688
            "uc"."CONSTRAINT_NAME" AS "name",
689
            "uccol"."COLUMN_NAME" AS "column_name",
690
            "uc"."CONSTRAINT_TYPE" AS "type",
691
            "fuc"."OWNER" AS "foreign_table_schema",
692
            "fuc"."TABLE_NAME" AS "foreign_table_name",
693
            "fuccol"."COLUMN_NAME" AS "foreign_column_name",
694
            "uc"."DELETE_RULE" AS "on_delete",
695
            "uc"."SEARCH_CONDITION" AS "check_expr"
696
        FROM "USER_CONSTRAINTS" "uc"
697
        INNER JOIN "USER_CONS_COLUMNS" "uccol"
698
        ON "uccol"."OWNER" = "uc"."OWNER" AND "uccol"."CONSTRAINT_NAME" = "uc"."CONSTRAINT_NAME"
699
        LEFT JOIN "USER_CONSTRAINTS" "fuc"
700
        ON "fuc"."OWNER" = "uc"."R_OWNER" AND "fuc"."CONSTRAINT_NAME" = "uc"."R_CONSTRAINT_NAME"
701
        LEFT JOIN "USER_CONS_COLUMNS" "fuccol"
702
        ON "fuccol"."OWNER" = "fuc"."OWNER" AND "fuccol"."CONSTRAINT_NAME" = "fuc"."CONSTRAINT_NAME" AND "fuccol"."POSITION" = "uccol"."POSITION"
703
        WHERE "uc"."OWNER" = :schemaName AND "uc"."TABLE_NAME" = :tableName
704
        ORDER BY "uccol"."POSITION" ASC
705 89
        SQL;
706
707 89
        $resolvedName = $this->resolveTableName($tableName);
708 89
        $constraints = $this->db->createCommand($sql, [
709 89
            ':schemaName' => $resolvedName->getSchemaName(),
710 89
            ':tableName' => $resolvedName->getName(),
711 89
        ])->queryAll();
712
713
        /** @psalm-var array[] $constraints */
714 89
        $constraints = $this->normalizeRowKeyCase($constraints, true);
715 89
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
716
717 89
        $result = [
718 89
            self::PRIMARY_KEY => null,
719 89
            self::FOREIGN_KEYS => [],
720 89
            self::UNIQUES => [],
721 89
            self::CHECKS => [],
722 89
        ];
723
724
        /**
725
         * @psalm-var string $type
726
         * @psalm-var array $names
727
         */
728 89
        foreach ($constraints as $type => $names) {
729
            /**
730
             * @psalm-var object|string|null $name
731
             * @psalm-var ConstraintArray $constraint
732
             */
733 82
            foreach ($names as $name => $constraint) {
734
                switch ($type) {
735 82
                    case 'P':
736 57
                        $result[self::PRIMARY_KEY] = (new Constraint())
737 57
                            ->name($name)
738 57
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
739 57
                        break;
740 82
                    case 'R':
741 19
                        $result[self::FOREIGN_KEYS][] = (new ForeignKeyConstraint())
742 19
                            ->name($name)
743 19
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
744 19
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
745 19
                            ->foreignTableName($constraint[0]['foreign_table_name'])
746 19
                            ->foreignColumnNames(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
747 19
                            ->onDelete($constraint[0]['on_delete'])
748 19
                            ->onUpdate(null);
749 19
                        break;
750 82
                    case 'U':
751 58
                        $result[self::UNIQUES][] = (new Constraint())
752 58
                            ->name($name)
753 58
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
754 58
                        break;
755 82
                    case 'C':
756 82
                        $result[self::CHECKS][] = (new CheckConstraint())
757 82
                            ->name($name)
758 82
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
759 82
                            ->expression($constraint[0]['check_expr']);
760 82
                        break;
761
                }
762
            }
763
        }
764
765 89
        foreach ($result as $type => $data) {
766 89
            $this->setTableMetadata($tableName, $type, $data);
767
        }
768
769 89
        return $result[$returnType];
770
    }
771
772
    /**
773
     * @throws Exception
774
     * @throws InvalidConfigException
775
     * @throws Throwable
776
     */
777 2
    protected function findViewNames(string $schema = ''): array
778
    {
779 2
        $sql = match ($schema) {
780 2
            '' => <<<SQL
781
            SELECT VIEW_NAME FROM USER_VIEWS
782 2
            SQL,
783 2
            default => <<<SQL
784 2
            SELECT VIEW_NAME FROM ALL_VIEWS WHERE OWNER = '$schema'
785 2
            SQL,
786 2
        };
787
788
        /** @psalm-var string[][] $views */
789 2
        $views = $this->db->createCommand($sql)->queryAll();
790
791 2
        foreach ($views as $key => $view) {
792 2
            $views[$key] = $view['VIEW_NAME'];
793
        }
794
795 2
        return $views;
796
    }
797
798
    /**
799
     * Returns the cache key for the specified table name.
800
     *
801
     * @param string $name The table name.
802
     *
803
     * @return array The cache key.
804
     */
805 243
    protected function getCacheKey(string $name): array
806
    {
807 243
        return array_merge([self::class], $this->db->getCacheKey(), [$this->getRawTableName($name)]);
808
    }
809
810
    /**
811
     * Returns the cache tag name.
812
     *
813
     * This allows {@see refresh()} to invalidate all cached table schemas.
814
     *
815
     * @return string The cache tag name.
816
     */
817 244
    protected function getCacheTag(): string
818
    {
819 244
        return md5(serialize(array_merge([self::class], $this->db->getCacheKey())));
820
    }
821
}
822