Passed
Pull Request — dev (#67)
by Wilmer
41:56 queued 17:22
created

Schema::findSchemaNames()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2.0185

Importance

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

656
        /** @scrutinizer ignore-unused */ ?string $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...
657
        ?string $scale,
658
        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

658
        /** @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...
659
    ): void {
660 74
        $column->dbType($dbType);
661
662 74
        if (str_contains($dbType, 'FLOAT') || str_contains($dbType, 'DOUBLE')) {
663 16
            $column->type(self::TYPE_DOUBLE);
664 74
        } elseif (str_contains($dbType, 'NUMBER')) {
665 71
            if ($scale === null || $scale > 0) {
666 17
                $column->type(self::TYPE_DECIMAL);
667
            } else {
668 71
                $column->type(self::TYPE_INTEGER);
669
            }
670 67
        } elseif (str_contains($dbType, 'INTEGER')) {
671
            $column->type(self::TYPE_INTEGER);
672 67
        } elseif (str_contains($dbType, 'BLOB')) {
673 18
            $column->type(self::TYPE_BINARY);
674 64
        } elseif (str_contains($dbType, 'CLOB')) {
675 22
            $column->type(self::TYPE_TEXT);
676 63
        } elseif (str_contains($dbType, 'TIMESTAMP')) {
677 15
            $column->type(self::TYPE_TIMESTAMP);
678
        } else {
679 63
            $column->type(self::TYPE_STRING);
680
        }
681
    }
682
683
    /**
684
     * Extracts size, precision and scale information from column's DB type.
685
     *
686
     * @param ColumnSchema $column
687
     * @param string $dbType the column's DB type.
688
     * @param string|null $precision total number of digits.
689
     * @param string|null $scale number of digits on the right of the decimal separator.
690
     * @param string $length length for character types.
691
     */
692 74
    protected function extractColumnSize(
693
        ColumnSchema $column,
694
        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

694
        /** @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...
695
        ?string $precision,
696
        ?string $scale,
697
        string $length
698
    ): void {
699 74
        $column->size(trim($length) === '' ? null : (int) $length);
700 74
        $column->precision(trim((string) $precision) === '' ? null : (int) $precision);
701 74
        $column->scale($scale === '' || $scale === null ? null : (int) $scale);
702
    }
703
704
    /**
705
     * Loads multiple types of constraints and returns the specified ones.
706
     *
707
     * @param string $tableName table name.
708
     * @param string $returnType return type:
709
     * - primaryKey
710
     * - foreignKeys
711
     * - uniques
712
     * - checks
713
     *
714
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
715
     *
716
     * @return mixed constraints.
717
     */
718 64
    private function loadTableConstraints(string $tableName, string $returnType): mixed
719
    {
720 64
        $sql = <<<SQL
721
        SELECT
722
            "uc"."CONSTRAINT_NAME" AS "name",
723
            "uccol"."COLUMN_NAME" AS "column_name",
724
            "uc"."CONSTRAINT_TYPE" AS "type",
725
            "fuc"."OWNER" AS "foreign_table_schema",
726
            "fuc"."TABLE_NAME" AS "foreign_table_name",
727
            "fuccol"."COLUMN_NAME" AS "foreign_column_name",
728
            "uc"."DELETE_RULE" AS "on_delete",
729
            "uc"."SEARCH_CONDITION" AS "check_expr"
730
        FROM "USER_CONSTRAINTS" "uc"
731
        INNER JOIN "USER_CONS_COLUMNS" "uccol"
732
        ON "uccol"."OWNER" = "uc"."OWNER" AND "uccol"."CONSTRAINT_NAME" = "uc"."CONSTRAINT_NAME"
733
        LEFT JOIN "USER_CONSTRAINTS" "fuc"
734
        ON "fuc"."OWNER" = "uc"."R_OWNER" AND "fuc"."CONSTRAINT_NAME" = "uc"."R_CONSTRAINT_NAME"
735
        LEFT JOIN "USER_CONS_COLUMNS" "fuccol"
736
        ON "fuccol"."OWNER" = "fuc"."OWNER" AND "fuccol"."CONSTRAINT_NAME" = "fuc"."CONSTRAINT_NAME" AND "fuccol"."POSITION" = "uccol"."POSITION"
737
        WHERE "uc"."OWNER" = :schemaName AND "uc"."TABLE_NAME" = :tableName
738
        ORDER BY "uccol"."POSITION" ASC
739
        SQL;
740
741 64
        $resolvedName = $this->resolveTableName($tableName);
742
743 64
        $constraints = $this->db->createCommand($sql, [
744 64
            ':schemaName' => $resolvedName->getSchemaName(),
745 64
            ':tableName' => $resolvedName->getName(),
746 64
        ])->queryAll();
747
748
        /** @var Constraint[] $constraints */
749 64
        $constraints = $this->normalizeRowKeyCase($constraints, true);
750 64
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
751
752 64
        $result = [
753
            self::PRIMARY_KEY => null,
754 64
            self::FOREIGN_KEYS => [],
755 64
            self::UNIQUES => [],
756 64
            self::CHECKS => [],
757
        ];
758
759
        /**
760
         * @var string $type
761
         * @var array $names
762
         */
763 64
        foreach ($constraints as $type => $names) {
764
            /**
765
             * @psalm-var object|string|null $name
766
             * @psalm-var ConstraintArray $constraint
767
             */
768 64
            foreach ($names as $name => $constraint) {
769 64
                switch ($type) {
770 64
                    case 'P':
771 49
                        $result[self::PRIMARY_KEY] = (new Constraint())
772 49
                            ->name($name)
773 49
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
774 49
                        break;
775 64
                    case 'R':
776 17
                        $result[self::FOREIGN_KEYS][] = (new ForeignKeyConstraint())
777 17
                            ->name($name)
778 17
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
779 17
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
780 17
                            ->foreignTableName($constraint[0]['foreign_table_name'])
781 17
                            ->foreignColumnNames(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
782 17
                            ->onDelete($constraint[0]['on_delete'])
783 17
                            ->onUpdate(null);
784 17
                        break;
785 64
                    case 'U':
786 50
                        $result[self::UNIQUES][] = (new Constraint())
787 50
                            ->name($name)
788 50
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
789 50
                        break;
790 64
                    case 'C':
791 64
                        $result[self::CHECKS][] = (new CheckConstraint())
792 64
                            ->name($name)
793 64
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
794 64
                            ->expression($constraint[0]['check_expr']);
795 64
                        break;
796
                }
797
            }
798
        }
799
800 64
        foreach ($result as $type => $data) {
801 64
            $this->setTableMetadata($tableName, $type, $data);
802
        }
803
804 64
        return $result[$returnType];
805
    }
806
807
    /**
808
     * Creates a column schema for the database.
809
     *
810
     * This method may be overridden by child classes to create a DBMS-specific column schema.
811
     *
812
     * @return ColumnSchema column schema instance.
813
     */
814 74
    protected function createColumnSchema(): ColumnSchema
815
    {
816 74
        return new ColumnSchema();
817
    }
818
819
    /**
820
     * Returns the actual name of a given table name.
821
     *
822
     * This method will strip off curly brackets from the given table name and replace the percentage character '%' with
823
     * {@see ConnectionInterface::tablePrefix}.
824
     *
825
     * @param string $name the table name to be converted.
826
     *
827
     * @return string the real name of the given table name.
828
     */
829 152
    public function getRawTableName(string $name): string
830
    {
831 152
        if (str_contains($name, '{{')) {
832 21
            $name = preg_replace('/{{(.*?)}}/', '\1', $name);
833
834 21
            return str_replace('%', $this->db->getTablePrefix(), $name);
835
        }
836
837 152
        return $name;
838
    }
839
840
    /**
841
     * Returns the cache key for the specified table name.
842
     *
843
     * @param string $name the table name.
844
     *
845
     * @return array the cache key.
846
     */
847 152
    protected function getCacheKey(string $name): array
848
    {
849 152
        return array_merge([__CLASS__], $this->db->getCacheKey(), [$this->getRawTableName($name)]);
850
    }
851
852
    /**
853
     * Returns the cache tag name.
854
     *
855
     * This allows {@see refresh()} to invalidate all cached table schemas.
856
     *
857
     * @return string the cache tag name.
858
     */
859 152
    protected function getCacheTag(): string
860
    {
861 152
        return md5(serialize(array_merge([__CLASS__], $this->db->getCacheKey())));
862
    }
863
864
    /**
865
     * Changes row's array key case to lower.
866
     *
867
     * @param array $row row's array or an array of row's arrays.
868
     * @param bool $multiple whether multiple rows or a single row passed.
869
     *
870
     * @return array normalized row or rows.
871
     */
872 134
    protected function normalizeRowKeyCase(array $row, bool $multiple): array
873
    {
874 134
        if ($multiple) {
875 75
            return array_map(static function (array $row) {
876 72
                return array_change_key_case($row, CASE_LOWER);
877
            }, $row);
878
        }
879
880 81
        return array_change_key_case($row, CASE_LOWER);
881
    }
882
883
    /**
884
     * @return bool whether this DBMS supports [savepoint](http://en.wikipedia.org/wiki/Savepoint).
885
     */
886 5
    public function supportsSavepoint(): bool
887
    {
888 5
        return $this->db->isSavepointEnabled();
889
    }
890
}
891