Passed
Pull Request — master (#125)
by Def
53:12 queued 26:35
created

Schema::createColumn()   B

Complexity

Conditions 8
Paths 5

Size

Total Lines 64
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 8.0016

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 8
eloc 35
c 2
b 1
f 0
nc 5
nop 1
dl 0
loc 64
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 494
    public function __construct(protected ConnectionInterface $db, SchemaCache $schemaCache, string $defaultSchema)
55
    {
56 494
        $this->defaultSchema = $defaultSchema;
57 494
        parent::__construct($db, $schemaCache);
58
    }
59
60 194
    protected function resolveTableName(string $name): TableSchemaInterface
61
    {
62 194
        $resolvedName = new TableSchema();
63
64 194
        $parts = array_reverse(
65 194
            $this->db->getQuoter()->getTableNameParts($name)
66 194
        );
67
68 194
        $resolvedName->name($parts[0] ?? '');
69 194
        $resolvedName->schemaName($parts[1] ?? $this->defaultSchema);
70
71 194
        $resolvedName->fullName(
72 194
            $resolvedName->getSchemaName() !== $this->defaultSchema ?
73 194
            implode('.', array_reverse($parts)) : $resolvedName->getName()
74 194
        );
75
76 194
        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 133
    protected function findTableComment(TableSchemaInterface $tableSchema): void
105
    {
106 133
        $sql = <<<SQL
107
        SELECT "COMMENTS"
108
        FROM ALL_TAB_COMMENTS
109
        WHERE
110
              "OWNER" = :schemaName AND
111
              "TABLE_NAME" = :tableName
112 133
        SQL;
113
114 133
        $comment = $this->db->createCommand($sql, [
115 133
            ':schemaName' => $tableSchema->getSchemaName(),
116 133
            ':tableName' => $tableSchema->getName(),
117 133
        ])->queryScalar();
118
119 133
        $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 133
    protected function loadTableSchema(string $name): TableSchemaInterface|null
172
    {
173 133
        $table = $this->resolveTableName($name);
174 133
        $this->findTableComment($table);
175
176 133
        if ($this->findColumns($table)) {
177 117
            $this->findConstraints($table);
178 117
            return $table;
179
        }
180
181 26
        return null;
182
    }
183
184
    /**
185
     * @throws Exception
186
     * @throws InvalidConfigException
187
     * @throws NotSupportedException
188
     * @throws Throwable
189
     */
190 40
    protected function loadTablePrimaryKey(string $tableName): Constraint|null
191
    {
192
        /** @psalm-var mixed $tablePrimaryKey */
193 40
        $tablePrimaryKey = $this->loadTableConstraints($tableName, self::PRIMARY_KEY);
194 40
        return $tablePrimaryKey instanceof Constraint ? $tablePrimaryKey : null;
195
    }
196
197
    /**
198
     * @throws Exception
199
     * @throws InvalidConfigException
200
     * @throws NotSupportedException
201
     * @throws Throwable
202
     */
203 9
    protected function loadTableForeignKeys(string $tableName): array
204
    {
205
        /** @psalm-var mixed $tableForeingKeys */
206 9
        $tableForeingKeys = $this->loadTableConstraints($tableName, self::FOREIGN_KEYS);
207 9
        return is_array($tableForeingKeys) ? $tableForeingKeys : [];
208
    }
209
210
    /**
211
     * @throws Exception
212
     * @throws InvalidConfigException
213
     * @throws NotSupportedException
214
     * @throws Throwable
215
     */
216 31
    protected function loadTableIndexes(string $tableName): array
217
    {
218 31
        $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 31
        SQL;
230
231 31
        $resolvedName = $this->resolveTableName($tableName);
232
233 31
        $indexes = $this->db->createCommand($sql, [
234 31
            ':schemaName' => $resolvedName->getSchemaName(),
235 31
            ':tableName' => $resolvedName->getName(),
236 31
        ])->queryAll();
237
238
        /** @psalm-var array[] $indexes */
239 31
        $indexes = $this->normalizeRowKeyCase($indexes, true);
240 31
        $indexes = ArrayHelper::index($indexes, null, 'name');
241
242 31
        $result = [];
243
244
        /**
245
         * @psalm-var object|string|null $name
246
         * @psalm-var array[] $index
247
         */
248 31
        foreach ($indexes as $name => $index) {
249 28
            $columnNames = ArrayHelper::getColumn($index, 'column_name');
250
251 28
            if ($columnNames[0] === null) {
252 18
                $columnNames[0] = '';
253
            }
254
255 28
            $result[] = (new IndexConstraint())
256 28
                ->primary((bool) $index[0]['index_is_primary'])
257 28
                ->unique((bool) $index[0]['index_is_unique'])
258 28
                ->name($name)
259 28
                ->columnNames($columnNames);
260
        }
261
262 31
        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 16
    protected function loadTableChecks(string $tableName): array
285
    {
286
        /** @psalm-var mixed $tableCheck */
287 16
        $tableCheck = $this->loadTableConstraints($tableName, self::CHECKS);
288 16
        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|string|null $length
310
     */
311 10
    public function createColumnSchemaBuilder(string $type, array|int|string $length = null): ColumnSchemaBuilder
312
    {
313 10
        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 133
    protected function findColumns(TableSchemaInterface $table): bool
327
    {
328 133
        $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
            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 133
        SQL;
351
352 133
        $columns = $this->db->createCommand($sql, [
353 133
            ':tableName' => $table->getName(),
354 133
            ':schemaName' => $table->getSchemaName(),
355 133
        ])->queryAll();
356
357 133
        if ($columns === []) {
358 26
            return false;
359
        }
360
361
        /** @psalm-var string[][] $columns */
362 117
        foreach ($columns as $column) {
363 117
            $column = $this->normalizeRowKeyCase($column, false);
364
365 117
            $c = $this->createColumn($column);
366
367 117
            $table->columns($c->getName(), $c);
368
        }
369
370 117
        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 74
    protected function getTableSequenceName(string $tableName): bool|float|int|string|null
385
    {
386 74
        $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 74
        SQL;
396 74
        $sequenceName = $this->db->createCommand($sequenceNameSql, [':tableName' => $tableName])->queryScalar();
397
398 74
        return $sequenceName === false ? null : $sequenceName;
399
    }
400
401
    /**
402
     * Creates ColumnSchema instance.
403
     */
404 117
    protected function createColumn(array|string $column): ColumnSchema
405
    {
406 117
        $c = $this->createColumnSchema();
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
         *   column_comment: string|null
418
         * } $column
419
         */
420 117
        $c->name($column['column_name']);
421 117
        $c->allowNull($column['nullable'] === 'Y');
422 117
        $c->comment($column['column_comment'] ?? '');
423 117
        $c->primaryKey(false);
424
425 117
        $this->extractColumnType(
426 117
            $c,
427 117
            $column['data_type'],
428 117
            $column['data_precision'],
429 117
            $column['data_scale'],
430 117
            $column['data_length']
431 117
        );
432
433 117
        $this->extractColumnSize(
434 117
            $c,
435 117
            $column['data_type'],
436 117
            $column['data_precision'],
437 117
            $column['data_scale'],
438 117
            $column['data_length']
439 117
        );
440
441 117
        $c->phpType($this->getColumnPhpType($c));
442
443 117
        if (!$c->isPrimaryKey()) {
444 117
            if ($column['data_default'] === null) {
445 114
                $c->defaultValue(null);
446
            } else {
447 76
                $defaultValue = $column['data_default'];
448
449 76
                if ($c->getType() === 'timestamp' && $defaultValue === 'CURRENT_TIMESTAMP') {
450
                    $c->defaultValue(new Expression('CURRENT_TIMESTAMP'));
451
                } else {
452
                    if (
453 76
                        ($len = strlen($defaultValue)) > 2 &&
454 76
                        $defaultValue[0] === "'" &&
455 76
                        $defaultValue[$len - 1] === "'"
456
                    ) {
457 27
                        $defaultValue = substr($defaultValue, 1, -1);
458
                    } else {
459 76
                        $defaultValue = trim($defaultValue);
460
                    }
461
462 76
                    $c->defaultValue($c->phpTypecast($defaultValue));
463
                }
464
            }
465
        }
466
467 117
        return $c;
468
    }
469
470
    /**
471
     * Finds constraints and fills them into TableSchemaInterface object passed.
472
     *
473
     * @throws Exception
474
     * @throws InvalidConfigException
475
     * @throws Throwable
476
     *
477
     * @psalm-suppress PossiblyNullArrayOffset
478
     */
479 117
    protected function findConstraints(TableSchemaInterface $table): void
480
    {
481 117
        $sql = <<<SQL
482
        SELECT
483
            /*+ PUSH_PRED(C) PUSH_PRED(D) PUSH_PRED(E) */
484
            D.CONSTRAINT_NAME,
485
            D.CONSTRAINT_TYPE,
486
            C.COLUMN_NAME,
487
            C.POSITION,
488
            D.R_CONSTRAINT_NAME,
489
            E.TABLE_NAME AS TABLE_REF,
490
            F.COLUMN_NAME AS COLUMN_REF,
491
            C.TABLE_NAME
492
        FROM ALL_CONS_COLUMNS C
493
            INNER JOIN ALL_CONSTRAINTS D ON D.OWNER = C.OWNER AND D.CONSTRAINT_NAME = C.CONSTRAINT_NAME
494
            LEFT JOIN ALL_CONSTRAINTS E ON E.OWNER = D.R_OWNER AND E.CONSTRAINT_NAME = D.R_CONSTRAINT_NAME
495
            LEFT JOIN ALL_CONS_COLUMNS F ON F.OWNER = E.OWNER AND F.CONSTRAINT_NAME = E.CONSTRAINT_NAME AND F.POSITION = C.POSITION
496
        WHERE
497
            C.OWNER = :schemaName
498
            AND C.TABLE_NAME = :tableName
499
            ORDER BY D.CONSTRAINT_NAME, C.POSITION
500 117
        SQL;
501
502
        /**
503
         * @psalm-var array{
504
         *   array{
505
         *     constraint_name: string,
506
         *     constraint_type: string,
507
         *     column_name: string,
508
         *     position: string|null,
509
         *     r_constraint_name: string|null,
510
         *     table_ref: string|null,
511
         *     column_ref: string|null,
512
         *     table_name: string
513
         *   }
514
         * } $rows
515
         */
516 117
        $rows = $this->db->createCommand(
517 117
            $sql,
518 117
            [':tableName' => $table->getName(), ':schemaName' => $table->getSchemaName()]
519 117
        )->queryAll();
520
521 117
        $constraints = [];
522
523 117
        foreach ($rows as $row) {
524
            /** @psalm-var string[] $row */
525 109
            $row = $this->normalizeRowKeyCase($row, false);
526
527 109
            if ($row['constraint_type'] === 'P') {
528 74
                $table->getColumns()[$row['column_name']]->primaryKey(true);
529 74
                $table->primaryKey($row['column_name']);
530
531 74
                if (empty($table->getSequenceName())) {
532 74
                    $table->sequenceName((string) $this->getTableSequenceName($table->getName()));
533
                }
534
            }
535
536 109
            if ($row['constraint_type'] !== 'R') {
537
                /**
538
                 * This condition is not checked in SQL WHERE because of an Oracle Bug:
539
                 *
540
                 * {@see https://github.com/yiisoft/yii2/pull/8844}
541
                 */
542 109
                continue;
543
            }
544
545 11
            $name = $row['constraint_name'];
546
547 11
            if (!isset($constraints[$name])) {
548 11
                $constraints[$name] = [
549 11
                    'tableName' => $row['table_ref'],
550 11
                    'columns' => [],
551 11
                ];
552
            }
553
554 11
            $constraints[$name]['columns'][$row['column_name']] = $row['column_ref'];
555
        }
556
557 117
        foreach ($constraints as $index => $constraint) {
558 11
            $table->foreignKey($index, array_merge([$constraint['tableName']], $constraint['columns']));
559
        }
560
    }
561
562
    /**
563
     * Returns all unique indexes for the given table.
564
     *
565
     * Each array element is of the following structure:.
566
     *
567
     * ```php
568
     * [
569
     *     'IndexName1' => ['col1' [, ...]],
570
     *     'IndexName2' => ['col2' [, ...]],
571
     * ]
572
     * ```
573
     *
574
     * @param TableSchemaInterface $table the table metadata.
575
     *
576
     * @throws Exception
577
     * @throws InvalidConfigException
578
     * @throws Throwable
579
     *
580
     * @return array all unique indexes for the given table.
581
     */
582 1
    public function findUniqueIndexes(TableSchemaInterface $table): array
583
    {
584 1
        $query = <<<SQL
585
        SELECT
586
            DIC.INDEX_NAME,
587
            DIC.COLUMN_NAME
588
        FROM ALL_INDEXES DI
589
            INNER JOIN ALL_IND_COLUMNS DIC ON DI.TABLE_NAME = DIC.TABLE_NAME AND DI.INDEX_NAME = DIC.INDEX_NAME
590
        WHERE
591
            DI.UNIQUENESS = 'UNIQUE'
592
            AND DIC.TABLE_OWNER = :schemaName
593
            AND DIC.TABLE_NAME = :tableName
594
        ORDER BY DIC.TABLE_NAME, DIC.INDEX_NAME, DIC.COLUMN_POSITION
595 1
        SQL;
596 1
        $result = [];
597
598 1
        $rows = $this->db->createCommand(
599 1
            $query,
600 1
            [':tableName' => $table->getName(), ':schemaName' => $table->getschemaName()]
601 1
        )->queryAll();
602
603
        /** @psalm-var array<array{INDEX_NAME: string, COLUMN_NAME: string}> $rows */
604 1
        foreach ($rows as $row) {
605 1
            $result[$row['INDEX_NAME']][] = $row['COLUMN_NAME'];
606
        }
607
608 1
        return $result;
609
    }
610
611
    /**
612
     * Extracts the data types for the given column.
613
     *
614
     * @param string $dbType DB type.
615
     * @param string|null $precision total number of digits.
616
     * @param string|null $scale number of digits on the right of the decimal separator.
617
     * @param string $length length for character types.
618
     */
619 117
    protected function extractColumnType(
620
        ColumnSchema $column,
621
        string $dbType,
622
        string|null $precision,
0 ignored issues
show
Unused Code introduced by
The parameter $precision is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

622
        /** @scrutinizer ignore-unused */ string|null $precision,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
623
        string|null $scale,
624
        string $length
0 ignored issues
show
Unused Code introduced by
The parameter $length is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

624
        /** @scrutinizer ignore-unused */ string $length

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
625
    ): void {
626 117
        $column->dbType($dbType);
627
628 117
        if (str_contains($dbType, 'FLOAT') || str_contains($dbType, 'DOUBLE')) {
629 30
            $column->type(self::TYPE_DOUBLE);
630 117
        } elseif (str_contains($dbType, 'NUMBER')) {
631 113
            if ($scale === null || $scale > 0) {
632 31
                $column->type(self::TYPE_DECIMAL);
633
            } else {
634 113
                $column->type(self::TYPE_INTEGER);
635
            }
636 93
        } elseif (str_contains($dbType, 'BLOB')) {
637 31
            $column->type(self::TYPE_BINARY);
638 90
        } elseif (str_contains($dbType, 'CLOB')) {
639 22
            $column->type(self::TYPE_TEXT);
640 89
        } elseif (str_contains($dbType, 'TIMESTAMP')) {
641 28
            $column->type(self::TYPE_TIMESTAMP);
642
        } else {
643 89
            $column->type(self::TYPE_STRING);
644
        }
645
    }
646
647
    /**
648
     * Extracts size, precision and scale information from column's DB type.
649
     *
650
     * @param string $dbType the column's DB type.
651
     * @param string|null $precision total number of digits.
652
     * @param string|null $scale number of digits on the right of the decimal separator.
653
     * @param string $length length for character types.
654
     */
655 117
    protected function extractColumnSize(
656
        ColumnSchema $column,
657
        string $dbType,
0 ignored issues
show
Unused Code introduced by
The parameter $dbType is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

657
        /** @scrutinizer ignore-unused */ string $dbType,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

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