Passed
Pull Request — master (#190)
by Def
06:04
created

Schema::createColumnSchema()   B

Complexity

Conditions 8
Paths 5

Size

Total Lines 65
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 8.0016

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 35
nc 5
nop 1
dl 0
loc 65
ccs 33
cts 34
cp 0.9706
crap 8.0016
rs 8.1155
c 1
b 0
f 0

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

623
        /** @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...
624
        string|null $scale,
625
        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

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

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