Passed
Push — dev ( ebcccb...50ae2a )
by Def
15:11 queued 07:32
created

Schema::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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

661
        /** @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...
662
        ?string $scale,
663
        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

663
        /** @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...
664
    ): void {
665 71
        $column->dbType($dbType);
666
667 71
        if (str_contains($dbType, 'FLOAT') || str_contains($dbType, 'DOUBLE')) {
668 16
            $column->type('double');
669 71
        } elseif (str_contains($dbType, 'NUMBER')) {
670 68
            if ($scale === null || $scale > 0) {
671 17
                $column->type('decimal');
672
            } else {
673 68
                $column->type('integer');
674
            }
675 64
        } elseif (str_contains($dbType, 'INTEGER')) {
676
            $column->type('integer');
677 64
        } elseif (str_contains($dbType, 'BLOB')) {
678 18
            $column->type('binary');
679 61
        } elseif (str_contains($dbType, 'CLOB')) {
680 22
            $column->type('text');
681 60
        } elseif (str_contains($dbType, 'TIMESTAMP')) {
682 15
            $column->type('timestamp');
683
        } else {
684 60
            $column->type('string');
685
        }
686
    }
687
688
    /**
689
     * Extracts size, precision and scale information from column's DB type.
690
     *
691
     * @param ColumnSchema $column
692
     * @param string $dbType the column's DB type.
693
     * @param string|null $precision total number of digits.
694
     * @param string|null $scale number of digits on the right of the decimal separator.
695
     * @param string $length length for character types.
696
     */
697 71
    protected function extractColumnSize(
698
        ColumnSchema $column,
699
        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

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