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

Schema::createColumn()   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
c 1
b 0
f 0
nc 5
nop 1
dl 0
loc 65
ccs 33
cts 34
cp 0.9706
crap 8.0016
rs 8.1155

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\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