Passed
Push — remove-test-schema-trait ( 2f86f9 )
by Wilmer
41:39
created

Schema::findViewNames()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

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

614
        /** @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...
615
        string|null $scale,
616
        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

616
        /** @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...
617
    ): void {
618 117
        $column->dbType($dbType);
619
620 117
        if (str_contains($dbType, 'FLOAT') || str_contains($dbType, 'DOUBLE')) {
621 30
            $column->type(self::TYPE_DOUBLE);
622 117
        } elseif (str_contains($dbType, 'NUMBER')) {
623 113
            if ($scale === null || $scale > 0) {
624 31
                $column->type(self::TYPE_DECIMAL);
625
            } else {
626 113
                $column->type(self::TYPE_INTEGER);
627
            }
628 93
        } elseif (str_contains($dbType, 'BLOB')) {
629 31
            $column->type(self::TYPE_BINARY);
630 90
        } elseif (str_contains($dbType, 'CLOB')) {
631 22
            $column->type(self::TYPE_TEXT);
632 89
        } elseif (str_contains($dbType, 'TIMESTAMP')) {
633 28
            $column->type(self::TYPE_TIMESTAMP);
634
        } else {
635 89
            $column->type(self::TYPE_STRING);
636
        }
637
    }
638
639
    /**
640
     * Extracts size, precision and scale information from column's DB type.
641
     *
642
     * @param string $dbType the column's DB type.
643
     * @param string|null $precision total number of digits.
644
     * @param string|null $scale number of digits on the right of the decimal separator.
645
     * @param string $length length for character types.
646
     */
647 117
    protected function extractColumnSize(
648
        ColumnSchema $column,
649
        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

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