Passed
Branch master (0bfa07)
by Wilmer
30:00 queued 05:24
created

Schema::findTableComment()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 16
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

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

618
        /** @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...
619
        string|null $scale,
620
        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

620
        /** @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...
621
    ): void {
622 105
        $column->dbType($dbType);
623
624 105
        if (str_contains($dbType, 'FLOAT') || str_contains($dbType, 'DOUBLE')) {
625 19
            $column->type(self::TYPE_DOUBLE);
626 105
        } elseif (str_contains($dbType, 'NUMBER')) {
627 101
            if ($scale === null || $scale > 0) {
628 20
                $column->type(self::TYPE_DECIMAL);
629
            } else {
630 101
                $column->type(self::TYPE_INTEGER);
631
            }
632 79
        } elseif (str_contains($dbType, 'INTEGER')) {
633
            $column->type(self::TYPE_INTEGER);
634 79
        } elseif (str_contains($dbType, 'BLOB')) {
635 21
            $column->type(self::TYPE_BINARY);
636 76
        } elseif (str_contains($dbType, 'CLOB')) {
637 22
            $column->type(self::TYPE_TEXT);
638 75
        } elseif (str_contains($dbType, 'TIMESTAMP')) {
639 18
            $column->type(self::TYPE_TIMESTAMP);
640
        } else {
641 75
            $column->type(self::TYPE_STRING);
642
        }
643
    }
644
645
    /**
646
     * Extracts size, precision and scale information from column's DB type.
647
     *
648
     * @param string $dbType the column's 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 105
    protected function extractColumnSize(
654
        ColumnSchema $column,
655
        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

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