Passed
Pull Request — master (#137)
by Wilmer
53:52
created

Schema::findSchemaNames()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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

633
        /** @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...
634
        string|null $scale,
635
        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

635
        /** @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...
636
    ): void {
637 116
        $column->dbType($dbType);
638
639 116
        if (str_contains($dbType, 'FLOAT') || str_contains($dbType, 'DOUBLE')) {
640 28
            $column->type(self::TYPE_DOUBLE);
641 116
        } elseif (str_contains($dbType, 'NUMBER')) {
642 113
            if ($scale === null || $scale > 0) {
643 29
                $column->type(self::TYPE_DECIMAL);
644
            } else {
645 113
                $column->type(self::TYPE_INTEGER);
646
            }
647 87
        } elseif (str_contains($dbType, 'BLOB')) {
648 29
            $column->type(self::TYPE_BINARY);
649 84
        } elseif (str_contains($dbType, 'CLOB')) {
650 24
            $column->type(self::TYPE_TEXT);
651 83
        } elseif (str_contains($dbType, 'TIMESTAMP')) {
652 26
            $column->type(self::TYPE_TIMESTAMP);
653
        } else {
654 83
            $column->type(self::TYPE_STRING);
655
        }
656
    }
657
658
    /**
659
     * Extracts size, precision and scale information from column's DB type.
660
     *
661
     * @param string $dbType the column's DB type.
662
     * @param string|null $precision total number of digits.
663
     * @param string|null $scale number of digits on the right of the decimal separator.
664
     * @param string $length length for character types.
665
     */
666 116
    protected function extractColumnSize(
667
        ColumnSchema $column,
668
        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

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