Passed
Push — master ( cc0fbc...ca47bf )
by Def
30:58 queued 06:34
created

Schema::getRawTableName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 9
ccs 5
cts 5
cp 1
crap 2
rs 10
c 0
b 0
f 0

1 Method

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

622
        /** @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...
623
        ?string $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 80
        $column->dbType($dbType);
627
628 80
        if (str_contains($dbType, 'FLOAT') || str_contains($dbType, 'DOUBLE')) {
629 19
            $column->type(self::TYPE_DOUBLE);
630 80
        } elseif (str_contains($dbType, 'NUMBER')) {
631 77
            if ($scale === null || $scale > 0) {
632 20
                $column->type(self::TYPE_DECIMAL);
633
            } else {
634 77
                $column->type(self::TYPE_INTEGER);
635
            }
636 73
        } elseif (str_contains($dbType, 'INTEGER')) {
637
            $column->type(self::TYPE_INTEGER);
638 73
        } elseif (str_contains($dbType, 'BLOB')) {
639 21
            $column->type(self::TYPE_BINARY);
640 70
        } elseif (str_contains($dbType, 'CLOB')) {
641 22
            $column->type(self::TYPE_TEXT);
642 69
        } elseif (str_contains($dbType, 'TIMESTAMP')) {
643 18
            $column->type(self::TYPE_TIMESTAMP);
644
        } else {
645 69
            $column->type(self::TYPE_STRING);
646
        }
647
    }
648
649
    /**
650
     * Extracts size, precision and scale information from column's DB type.
651
     *
652
     * @param ColumnSchema $column
653
     * @param string $dbType the column's DB type.
654
     * @param string|null $precision total number of digits.
655
     * @param string|null $scale number of digits on the right of the decimal separator.
656
     * @param string $length length for character types.
657
     */
658 80
    protected function extractColumnSize(
659
        ColumnSchema $column,
660
        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

660
        /** @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...
661
        ?string $precision,
662
        ?string $scale,
663
        string $length
664
    ): void {
665 80
        $column->size(trim($length) === '' ? null : (int) $length);
666 80
        $column->precision(trim((string) $precision) === '' ? null : (int) $precision);
667 80
        $column->scale($scale === '' || $scale === null ? null : (int) $scale);
668
    }
669
670
    /**
671
     * Loads multiple types of constraints and returns the specified ones.
672
     *
673
     * @param string $tableName table name.
674
     * @param string $returnType return type:
675
     * - primaryKey
676
     * - foreignKeys
677
     * - uniques
678
     * - checks
679
     *
680
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
681
     *
682
     * @return mixed constraints.
683
     */
684 71
    private function loadTableConstraints(string $tableName, string $returnType): mixed
685
    {
686 71
        $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
        SQL;
706
707 71
        $resolvedName = $this->resolveTableName($tableName);
708
709 71
        $constraints = $this->db->createCommand($sql, [
710 71
            ':schemaName' => $resolvedName->getSchemaName(),
711 71
            ':tableName' => $resolvedName->getName(),
712 71
        ])->queryAll();
713
714
        /** @var Constraint[] $constraints */
715 71
        $constraints = $this->normalizeRowKeyCase($constraints, true);
716 71
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
717
718 71
        $result = [
719
            self::PRIMARY_KEY => null,
720 71
            self::FOREIGN_KEYS => [],
721 71
            self::UNIQUES => [],
722 71
            self::CHECKS => [],
723
        ];
724
725
        /**
726
         * @var string $type
727
         * @var array $names
728
         */
729 71
        foreach ($constraints as $type => $names) {
730
            /**
731
             * @psalm-var object|string|null $name
732
             * @psalm-var ConstraintArray $constraint
733
             */
734 64
            foreach ($names as $name => $constraint) {
735 64
                switch ($type) {
736 64
                    case 'P':
737 49
                        $result[self::PRIMARY_KEY] = (new Constraint())
738 49
                            ->name($name)
739 49
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
740 49
                        break;
741 64
                    case 'R':
742 17
                        $result[self::FOREIGN_KEYS][] = (new ForeignKeyConstraint())
743 17
                            ->name($name)
744 17
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
745 17
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
746 17
                            ->foreignTableName($constraint[0]['foreign_table_name'])
747 17
                            ->foreignColumnNames(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
748 17
                            ->onDelete($constraint[0]['on_delete'])
749 17
                            ->onUpdate(null);
750 17
                        break;
751 64
                    case 'U':
752 50
                        $result[self::UNIQUES][] = (new Constraint())
753 50
                            ->name($name)
754 50
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
755 50
                        break;
756 64
                    case 'C':
757 64
                        $result[self::CHECKS][] = (new CheckConstraint())
758 64
                            ->name($name)
759 64
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
760 64
                            ->expression($constraint[0]['check_expr']);
761 64
                        break;
762
                }
763
            }
764
        }
765
766 71
        foreach ($result as $type => $data) {
767 71
            $this->setTableMetadata($tableName, $type, $data);
768
        }
769
770 71
        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 80
    protected function createColumnSchema(): ColumnSchema
781
    {
782 80
        return new ColumnSchema();
783
    }
784
785
    /**
786
     * Returns the cache key for the specified table name.
787
     *
788
     * @param string $name the table name.
789
     *
790
     * @return array the cache key.
791
     */
792 161
    protected function getCacheKey(string $name): array
793
    {
794 161
        return array_merge([__CLASS__], $this->db->getCacheKey(), [$this->getRawTableName($name)]);
795
    }
796
797
    /**
798
     * Returns the cache tag name.
799
     *
800
     * This allows {@see refresh()} to invalidate all cached table schemas.
801
     *
802
     * @return string the cache tag name.
803
     */
804 161
    protected function getCacheTag(): string
805
    {
806 161
        return md5(serialize(array_merge([__CLASS__], $this->db->getCacheKey())));
807
    }
808
809
    /**
810
     * Changes row's array key case to lower.
811
     *
812
     * @param array $row row's array or an array of row's arrays.
813
     * @param bool $multiple whether multiple rows or a single row passed.
814
     *
815
     * @return array normalized row or rows.
816
     */
817 147
    protected function normalizeRowKeyCase(array $row, bool $multiple): array
818
    {
819 147
        if ($multiple) {
820 82
            return array_map(static function (array $row) {
821 72
                return array_change_key_case($row, CASE_LOWER);
822
            }, $row);
823
        }
824
825 87
        return array_change_key_case($row, CASE_LOWER);
826
    }
827
828
    /**
829
     * @return bool whether this DBMS supports [savepoint](http://en.wikipedia.org/wiki/Savepoint).
830
     */
831 5
    public function supportsSavepoint(): bool
832
    {
833 5
        return $this->db->isSavepointEnabled();
834
    }
835
}
836