Passed
Push — dev ( 1bbda8...5c17e2 )
by Def
18:33 queued 09:04
created

SchemaPDOOracle   F

Complexity

Total Complexity 98

Size/Duplication

Total Lines 914
Duplicated Lines 0 %

Test Coverage

Coverage 86.25%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 393
dl 0
loc 914
ccs 251
cts 291
cp 0.8625
rs 2
c 1
b 0
f 0
wmc 98

How to fix   Complexity   

Complex Class

Complex classes like SchemaPDOOracle often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SchemaPDOOracle, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Oracle\PDO;
6
7
use PDO;
8
use Throwable;
9
use Yiisoft\Arrays\ArrayHelper;
10
use Yiisoft\Db\Cache\SchemaCache;
11
use Yiisoft\Db\Connection\ConnectionPDOInterface;
12
use Yiisoft\Db\Constraint\CheckConstraint;
13
use Yiisoft\Db\Constraint\Constraint;
14
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
15
use Yiisoft\Db\Constraint\IndexConstraint;
16
use Yiisoft\Db\Exception\Exception;
17
use Yiisoft\Db\Exception\InvalidCallException;
18
use Yiisoft\Db\Exception\InvalidConfigException;
19
use Yiisoft\Db\Exception\NotSupportedException;
20
use Yiisoft\Db\Expression\Expression;
21
use Yiisoft\Db\Oracle\ColumnSchema;
22
use Yiisoft\Db\Oracle\ColumnSchemaBuilder;
23
use Yiisoft\Db\Oracle\TableSchema;
24
use Yiisoft\Db\Schema\Schema;
25
26
/**
27
 * Schema is the class for retrieving metadata from an Oracle database.
28
 *
29
 * @property string $lastInsertID The row ID of the last row inserted, or the last value retrieved from the
30
 * sequence object. This property is read-only.
31
 *
32
 * @psalm-type ConstraintArray = array<
33
 *   array-key,
34
 *   array {
35
 *     name: string,
36
 *     column_name: string,
37
 *     type: string,
38
 *     foreign_table_schema: string|null,
39
 *     foreign_table_name: string|null,
40
 *     foreign_column_name: string|null,
41
 *     on_update: string,
42
 *     on_delete: string,
43
 *     check_expr: string
44
 *   }
45
 * >
46
 */
47
final class SchemaPDOOracle extends Schema
48
{
49 348
    public function __construct(private ConnectionPDOInterface $db, SchemaCache $schemaCache)
50
    {
51 348
        $this->defaultSchema = strtoupper($db->getDriver()->getUsername());
52 348
        parent::__construct($schemaCache);
53
    }
54
55 70
    protected function resolveTableName(string $name): TableSchema
56
    {
57 70
        $resolvedName = new TableSchema();
58
59 70
        $parts = explode('.', str_replace('"', '', $name));
60
61 70
        if (isset($parts[1])) {
62
            $resolvedName->schemaName($parts[0]);
63
            $resolvedName->name($parts[1]);
64
        } else {
65 70
            $resolvedName->schemaName($this->defaultSchema);
66 70
            $resolvedName->name($name);
67
        }
68
69 70
        $fullName = ($resolvedName->getSchemaName() !== $this->defaultSchema
70 70
            ? $resolvedName->getSchemaName() . '.' : '') . $resolvedName->getName();
71
72 70
        $resolvedName->fullName($fullName);
73
74 70
        return $resolvedName;
75
    }
76
77
    /**
78
     * @see https://docs.oracle.com/cd/B28359_01/server.111/b28337/tdpsg_user_accounts.htm
79
     */
80 1
    protected function findSchemaNames(): array
81
    {
82 1
        $sql = <<<SQL
83
        SELECT "u"."USERNAME"
84
        FROM "DBA_USERS" "u"
85
        WHERE "u"."DEFAULT_TABLESPACE" NOT IN ('SYSTEM', 'SYSAUX')
86
        ORDER BY "u"."USERNAME" ASC
87
        SQL;
88
89 1
        return $this->db->createCommand($sql)->queryColumn();
90
    }
91
92
    /**
93
     * @param string $schema
94
     *
95
     * @throws Exception|InvalidConfigException|Throwable
96
     *
97
     * @return array
98
     */
99 5
    protected function findTableNames(string $schema = ''): array
100
    {
101 5
        if ($schema === '') {
102 5
            $sql = <<<SQL
103
            SELECT TABLE_NAME
104
            FROM USER_TABLES
105
            UNION ALL
106
            SELECT VIEW_NAME AS TABLE_NAME
107
            FROM USER_VIEWS
108
            UNION ALL
109
            SELECT MVIEW_NAME AS TABLE_NAME
110
            FROM USER_MVIEWS
111
            ORDER BY TABLE_NAME
112
            SQL;
113
114 5
            $command = $this->db->createCommand($sql);
115
        } else {
116
            $sql = <<<SQL
117
            SELECT OBJECT_NAME AS TABLE_NAME
118
            FROM ALL_OBJECTS
119
            WHERE OBJECT_TYPE IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW') AND OWNER = :schema
120
            ORDER BY OBJECT_NAME
121
            SQL;
122
            $command = $this->db->createCommand($sql, [':schema' => $schema]);
123
        }
124
125 5
        $rows = $command->queryAll();
126 5
        $names = [];
127
128 5
        foreach ($rows as $row) {
129 5
            if ($this->db->getSlavePdo()->getAttribute(PDO::ATTR_CASE) === PDO::CASE_LOWER) {
130 1
                $row = array_change_key_case($row, CASE_UPPER);
131
            }
132 5
            $names[] = $row['TABLE_NAME'];
133
        }
134
135 5
        return $names;
136
    }
137
138
    /**
139
     * @param string $name
140
     *
141
     * @throws Exception|InvalidConfigException|Throwable
142
     *
143
     * @return TableSchema|null
144
     */
145 84
    protected function loadTableSchema(string $name): ?TableSchema
146
    {
147 84
        $table = new TableSchema();
148
149 84
        $this->resolveTableNames($table, $name);
150
151 84
        if ($this->findColumns($table)) {
152 71
            $this->findConstraints($table);
153 71
            return $table;
154
        }
155
156 21
        return null;
157
    }
158
159
    /**
160
     * @param string $tableName
161
     *
162
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
163
     *
164
     * @return Constraint|null
165
     */
166 30
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
167
    {
168 30
        return $this->loadTableConstraints($tableName, 'primaryKey');
169
    }
170
171
    /**
172
     * @param string $tableName
173
     *
174
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
175
     *
176
     * @return array
177
     */
178 4
    protected function loadTableForeignKeys(string $tableName): array
179
    {
180 4
        return $this->loadTableConstraints($tableName, 'foreignKeys');
181
    }
182
183
    /**
184
     * @param string $tableName
185
     *
186
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
187
     *
188
     * @return array
189
     */
190 27
    protected function loadTableIndexes(string $tableName): array
191
    {
192 27
        $sql = <<<SQL
193
        SELECT "ui"."INDEX_NAME" AS "name", "uicol"."COLUMN_NAME" AS "column_name",
194
        CASE "ui"."UNIQUENESS" WHEN 'UNIQUE' THEN 1 ELSE 0 END AS "index_is_unique",
195
        CASE WHEN "uc"."CONSTRAINT_NAME" IS NOT NULL THEN 1 ELSE 0 END AS "index_is_primary"
196
        FROM "SYS"."USER_INDEXES" "ui"
197
        LEFT JOIN "SYS"."USER_IND_COLUMNS" "uicol"
198
        ON "uicol"."INDEX_NAME" = "ui"."INDEX_NAME"
199
        LEFT JOIN "SYS"."USER_CONSTRAINTS" "uc"
200
        ON "uc"."OWNER" = "ui"."TABLE_OWNER" AND "uc"."CONSTRAINT_NAME" = "ui"."INDEX_NAME" AND "uc"."CONSTRAINT_TYPE" = 'P'
201
        WHERE "ui"."TABLE_OWNER" = :schemaName AND "ui"."TABLE_NAME" = :tableName
202
        ORDER BY "uicol"."COLUMN_POSITION" ASC
203
        SQL;
204
205 27
        $resolvedName = $this->resolveTableName($tableName);
206
207 27
        $indexes = $this->db->createCommand($sql, [
208 27
            ':schemaName' => $resolvedName->getSchemaName(),
209 27
            ':tableName' => $resolvedName->getName(),
210 27
        ])->queryAll();
211
212 27
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
213
214 27
        $indexes = ArrayHelper::index($indexes, null, 'name');
215
216 27
        $result = [];
217
218
        /**
219
         * @psalm-var object|string|null $name
220
         * @psalm-var array[] $index
221
         */
222 27
        foreach ($indexes as $name => $index) {
223 24
            $columnNames = ArrayHelper::getColumn($index, 'column_name');
224
225 24
            if ($columnNames[0] === null) {
226 17
                $columnNames[0] = '';
227
            }
228
229 24
            $result[] = (new IndexConstraint())
230 24
                ->primary((bool) $index[0]['index_is_primary'])
231 24
                ->unique((bool) $index[0]['index_is_unique'])
232 24
                ->name($name)
233 24
                ->columnNames($columnNames);
234
        }
235
236 27
        return $result;
237
    }
238
239
    /**
240
     * @param string $tableName
241
     *
242
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
243
     *
244
     * @return array
245
     */
246 13
    protected function loadTableUniques(string $tableName): array
247
    {
248 13
        return $this->loadTableConstraints($tableName, 'uniques');
249
    }
250
251
    /**
252
     * @param string $tableName
253
     *
254
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
255
     *
256
     * @return array
257
     */
258 13
    protected function loadTableChecks(string $tableName): array
259
    {
260 13
        return $this->loadTableConstraints($tableName, 'checks');
261
    }
262
263
    /**
264
     * @param string $tableName
265
     *
266
     * @throws NotSupportedException if this method is called.
267
     *
268
     * @return array
269
     */
270 12
    protected function loadTableDefaultValues(string $tableName): array
271
    {
272 12
        throw new NotSupportedException('Oracle does not support default value constraints.');
273
    }
274
275
    public function releaseSavepoint(string $name): void
276
    {
277
        /* does nothing as Oracle does not support this */
278
    }
279
280 3
    public function quoteSimpleTableName(string $name): string
281
    {
282 3
        return str_contains($name, '"') ? $name : '"' . $name . '"';
283
    }
284
285
    /**
286
     * Create a column schema builder instance giving the type and value precision.
287
     *
288
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
289
     *
290
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
291
     * @param array|int|string|null $length length or precision of the column {@see ColumnSchemaBuilder::$length}.
292
     *
293
     * @return ColumnSchemaBuilder column schema builder instance
294
     */
295 3
    public function createColumnSchemaBuilder(string $type, array|int|string $length = null): ColumnSchemaBuilder
296
    {
297 3
        return new ColumnSchemaBuilder($type, $length);
298
    }
299
300
    /**
301
     * Resolves the table name and schema name (if any).
302
     *
303
     * @param TableSchema $table the table metadata object
304
     * @param string $name the table name
305
     */
306 84
    protected function resolveTableNames(TableSchema $table, string $name): void
307
    {
308 84
        $parts = explode('.', str_replace('"', '', $name));
309
310 84
        if (isset($parts[1])) {
311
            $table->schemaName($parts[0]);
312
            $table->name($parts[1]);
313
        } else {
314 84
            $table->schemaName($this->defaultSchema);
315 84
            $table->name($name);
316
        }
317
318 84
        $table->fullName($table->getSchemaName() !== $this->defaultSchema
319 84
            ? $table->getSchemaName() . '.' . $table->getName() : $table->getName());
320
    }
321
322
    /**
323
     * Collects the table column metadata.
324
     *
325
     * @param TableSchema $table the table schema.
326
     *
327
     * @throws Exception|Throwable
328
     *
329
     * @return bool whether the table exists.
330
     */
331 84
    protected function findColumns(TableSchema $table): bool
332
    {
333 84
        $sql = <<<'SQL'
334
SELECT
335
    A.COLUMN_NAME,
336
    A.DATA_TYPE,
337
    A.DATA_PRECISION,
338
    A.DATA_SCALE,
339
    (
340
      CASE A.CHAR_USED WHEN 'C' THEN A.CHAR_LENGTH
341
        ELSE A.DATA_LENGTH
342
      END
343
    ) AS DATA_LENGTH,
344
    A.NULLABLE,
345
    A.DATA_DEFAULT,
346
    COM.COMMENTS AS COLUMN_COMMENT
347
FROM ALL_TAB_COLUMNS A
348
    INNER JOIN ALL_OBJECTS B ON B.OWNER = A.OWNER AND LTRIM(B.OBJECT_NAME) = LTRIM(A.TABLE_NAME)
349
    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)
350
WHERE
351
    A.OWNER = :schemaName
352
    AND B.OBJECT_TYPE IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW')
353
    AND B.OBJECT_NAME = :tableName
354
ORDER BY A.COLUMN_ID
355
SQL;
356
357
        try {
358 84
            $columns = $this->db->createCommand($sql, [
359 84
                ':tableName' => $table->getName(),
360 84
                ':schemaName' => $table->getSchemaName(),
361 84
            ])->queryAll();
362
        } catch (Exception $e) {
363
            return false;
364
        }
365
366 84
        if (empty($columns)) {
367 21
            return false;
368
        }
369
370 71
        foreach ($columns as $column) {
371 71
            if ($this->db->getSlavePdo()->getAttribute(PDO::ATTR_CASE) === PDO::CASE_LOWER) {
372 1
                $column = array_change_key_case($column, CASE_UPPER);
373
            }
374
375 71
            $c = $this->createColumn($column);
376
377 71
            $table->columns($c->getName(), $c);
378
        }
379
380 71
        return true;
381
    }
382
383
    /**
384
     * Sequence name of table.
385
     *
386
     * @param string $tableName
387
     *
388
     * @throws Exception|InvalidConfigException|Throwable
389
     *
390
     * @return int|string|null whether the sequence exists.
391
     *
392
     * @internal TableSchema `$table->getName()` the table schema.
393
     */
394 53
    protected function getTableSequenceName(string $tableName): string|int|null
395
    {
396 53
        $sequenceNameSql = <<<SQL
397
        SELECT
398
            UD.REFERENCED_NAME AS SEQUENCE_NAME
399
        FROM USER_DEPENDENCIES UD
400
            JOIN USER_TRIGGERS UT ON (UT.TRIGGER_NAME = UD.NAME)
401
        WHERE
402
            UT.TABLE_NAME = :tableName
403
            AND UD.TYPE = 'TRIGGER'
404
            AND UD.REFERENCED_TYPE = 'SEQUENCE'
405
        SQL;
406 53
        $sequenceName = $this->db->createCommand($sequenceNameSql, [':tableName' => $tableName])->queryScalar();
407
408 53
        return $sequenceName === false ? null : $sequenceName;
409
    }
410
411
    /**
412
     * @Overrides method in class 'Schema'
413
     *
414
     * {@see https://secure.php.net/manual/en/function.PDO-lastInsertId.php} -> Oracle does not support this.
415
     *
416
     * Returns the ID of the last inserted row or sequence value.
417
     *
418
     * @param string $sequenceName name of the sequence object (required by some DBMS)
419
     *
420
     * @throws Exception|InvalidCallException|InvalidConfigException|Throwable if the DB connection is not active.
421
     *
422
     * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object.
423
     */
424 3
    public function getLastInsertID(string $sequenceName = ''): string
425
    {
426 3
        if ($this->db->isActive()) {
427
            /* get the last insert id from the master connection */
428 3
            $sequenceName = $this->quoteSimpleTableName($sequenceName);
429
430 3
            return $this->db->useMaster(function (ConnectionPDOInterface $db) use ($sequenceName) {
431 3
                return $db->createCommand("SELECT $sequenceName.CURRVAL FROM DUAL")->queryScalar();
432
            });
433
        }
434
435
        throw new InvalidCallException('DB Connection is not active.');
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 71
        $c->name($column['COLUMN_NAME']);
450 71
        $c->allowNull($column['NULLABLE'] === 'Y');
451 71
        $c->comment($column['COLUMN_COMMENT'] ?? '');
452 71
        $c->primaryKey(false);
453
454 71
        $this->extractColumnType(
455
            $c,
456 71
            $column['DATA_TYPE'],
457 71
            $column['DATA_PRECISION'],
458 71
            $column['DATA_SCALE'],
459 71
            $column['DATA_LENGTH']
460
        );
461
462 71
        $this->extractColumnSize(
463
            $c,
464 71
            $column['DATA_TYPE'],
465 71
            $column['DATA_PRECISION'],
466 71
            $column['DATA_SCALE'],
467 71
            $column['DATA_LENGTH']
468
        );
469
470 71
        $c->phpType($this->getColumnPhpType($c));
471
472 71
        if (!$c->isPrimaryKey()) {
473 71
            if ($column['DATA_DEFAULT'] !== null && stripos($column['DATA_DEFAULT'], 'timestamp') !== false) {
474 14
                $c->defaultValue(null);
475
            } else {
476 71
                $defaultValue = $column['DATA_DEFAULT'];
477
478 71
                if ($c->getType() === 'timestamp' && $defaultValue === 'CURRENT_TIMESTAMP') {
479
                    $c->defaultValue(new Expression('CURRENT_TIMESTAMP'));
480
                } else {
481 71
                    if ($defaultValue !== null) {
482 52
                        if (($len = strlen($defaultValue)) > 2 && $defaultValue[0] === "'"
483 52
                            && $defaultValue[$len - 1] === "'"
484
                        ) {
485 14
                            $defaultValue = substr($column['DATA_DEFAULT'], 1, -1);
486
                        } else {
487 52
                            $defaultValue = trim($defaultValue);
488
                        }
489
                    }
490 71
                    $c->defaultValue($c->phpTypecast($defaultValue));
491
                }
492
            }
493
        }
494
495 71
        return $c;
496
    }
497
498
    /**
499
     * Finds constraints and fills them into TableSchema object passed.
500
     *
501
     * @param TableSchema $table
502
     *
503
     * @throws Exception|InvalidConfigException|Throwable
504
     */
505 71
    protected function findConstraints(TableSchema $table): void
506
    {
507 71
        $sql = <<<'SQL'
508
SELECT
509
    /*+ PUSH_PRED(C) PUSH_PRED(D) PUSH_PRED(E) */
510
    D.CONSTRAINT_NAME,
511
    D.CONSTRAINT_TYPE,
512
    C.COLUMN_NAME,
513
    C.POSITION,
514
    D.R_CONSTRAINT_NAME,
515
    E.TABLE_NAME AS TABLE_REF,
516
    F.COLUMN_NAME AS COLUMN_REF,
517
    C.TABLE_NAME
518
FROM ALL_CONS_COLUMNS C
519
    INNER JOIN ALL_CONSTRAINTS D ON D.OWNER = C.OWNER AND D.CONSTRAINT_NAME = C.CONSTRAINT_NAME
520
    LEFT JOIN ALL_CONSTRAINTS E ON E.OWNER = D.R_OWNER AND E.CONSTRAINT_NAME = D.R_CONSTRAINT_NAME
521
    LEFT JOIN ALL_CONS_COLUMNS F ON F.OWNER = E.OWNER AND F.CONSTRAINT_NAME = E.CONSTRAINT_NAME AND F.POSITION = C.POSITION
522
WHERE
523
    C.OWNER = :schemaName
524
    AND C.TABLE_NAME = :tableName
525
ORDER BY D.CONSTRAINT_NAME, C.POSITION
526
SQL;
527
528 71
        $command = $this->db->createCommand($sql, [
529 71
            ':tableName' => $table->getName(),
530 71
            ':schemaName' => $table->getSchemaName(),
531
        ]);
532
533 71
        $constraints = [];
534
535 71
        foreach ($command->queryAll() as $row) {
536 65
            if ($this->db->getSlavePdo()->getAttribute(PDO::ATTR_CASE) === PDO::CASE_LOWER) {
537 1
                $row = array_change_key_case($row, CASE_UPPER);
538
            }
539
540 65
            if ($row['CONSTRAINT_TYPE'] === 'P') {
541 53
                $table->getColumns()[$row['COLUMN_NAME']]->primaryKey(true);
542 53
                $table->primaryKey($row['COLUMN_NAME']);
543
544 53
                if (empty($table->getSequenceName())) {
545 53
                    $table->sequenceName($this->getTableSequenceName($table->getName()));
546
                }
547
            }
548
549 65
            if ($row['CONSTRAINT_TYPE'] !== 'R') {
550
                /**
551
                 * This condition is not checked in SQL WHERE because of an Oracle Bug:
552
                 *
553
                 * {@see https://github.com/yiisoft/yii2/pull/8844}
554
                 */
555 65
                continue;
556
            }
557
558 8
            $name = $row['CONSTRAINT_NAME'];
559
560 8
            if (!isset($constraints[$name])) {
561 8
                $constraints[$name] = [
562 8
                    'tableName' => $row['TABLE_REF'],
563
                    'columns' => [],
564
                ];
565
            }
566
567 8
            $constraints[$name]['columns'][$row['COLUMN_NAME']] = $row['COLUMN_REF'];
568
        }
569
570 71
        foreach ($constraints as $constraint) {
571 8
            $name = current(array_keys($constraint));
572 8
            $table->foreignKey(array_merge([$constraint['tableName']], $constraint['columns']));
573
        }
574
    }
575
576
    /**
577
     * Returns all unique indexes for the given table.
578
     *
579
     * Each array element is of the following structure:.
580
     *
581
     * ```php
582
     * [
583
     *     'IndexName1' => ['col1' [, ...]],
584
     *     'IndexName2' => ['col2' [, ...]],
585
     * ]
586
     * ```
587
     *
588
     * @param TableSchema $table the table metadata.
589
     *
590
     * @throws Exception|InvalidConfigException|Throwable
591
     *
592
     * @return array all unique indexes for the given table.
593
     */
594 1
    public function findUniqueIndexes(TableSchema $table): array
595
    {
596 1
        $query = <<<'SQL'
597
SELECT
598
    DIC.INDEX_NAME,
599
    DIC.COLUMN_NAME
600
FROM ALL_INDEXES DI
601
    INNER JOIN ALL_IND_COLUMNS DIC ON DI.TABLE_NAME = DIC.TABLE_NAME AND DI.INDEX_NAME = DIC.INDEX_NAME
602
WHERE
603
    DI.UNIQUENESS = 'UNIQUE'
604
    AND DIC.TABLE_OWNER = :schemaName
605
    AND DIC.TABLE_NAME = :tableName
606
ORDER BY DIC.TABLE_NAME, DIC.INDEX_NAME, DIC.COLUMN_POSITION
607
SQL;
608 1
        $result = [];
609
610 1
        $command = $this->db->createCommand($query, [
611 1
            ':tableName' => $table->getName(),
612 1
            ':schemaName' => $table->getschemaName(),
613
        ]);
614
615 1
        foreach ($command->queryAll() as $row) {
616 1
            $result[$row['INDEX_NAME']][] = $row['COLUMN_NAME'];
617
        }
618
619 1
        return $result;
620
    }
621
622
    /**
623
     * Extracts the data types for the given column.
624
     *
625
     * @param ColumnSchema $column
626
     * @param string $dbType DB type.
627
     * @param string|null $precision total number of digits.
628
     * @param string|null $scale number of digits on the right of the decimal separator.
629
     * @param string $length length for character types.
630
     */
631 71
    protected function extractColumnType(
632
        ColumnSchema $column,
633
        string $dbType,
634
        ?string $precision,
635
        ?string $scale,
636
        string $length
637
    ): void {
638 71
        $column->dbType($dbType);
639
640 71
        if (str_contains($dbType, 'FLOAT') || str_contains($dbType, 'DOUBLE')) {
641 16
            $column->type('double');
642 71
        } elseif (str_contains($dbType, 'NUMBER')) {
643 68
            if ($scale === null || $scale > 0) {
644 17
                $column->type('decimal');
645
            } else {
646 68
                $column->type('integer');
647
            }
648 64
        } elseif (str_contains($dbType, 'INTEGER')) {
649
            $column->type('integer');
650 64
        } elseif (str_contains($dbType, 'BLOB')) {
651 18
            $column->type('binary');
652 61
        } elseif (str_contains($dbType, 'CLOB')) {
653 22
            $column->type('text');
654 60
        } elseif (str_contains($dbType, 'TIMESTAMP')) {
655 15
            $column->type('timestamp');
656
        } else {
657 60
            $column->type('string');
658
        }
659
    }
660
661
    /**
662
     * Extracts size, precision and scale information from column's DB type.
663
     *
664
     * @param ColumnSchema $column
665
     * @param string $dbType the column's DB type.
666
     * @param string|null $precision total number of digits.
667
     * @param string|null $scale number of digits on the right of the decimal separator.
668
     * @param string $length length for character types.
669
     */
670 71
    protected function extractColumnSize(
671
        ColumnSchema $column,
672
        string $dbType,
673
        ?string $precision,
674
        ?string $scale,
675
        string $length
676
    ): void {
677 71
        $column->size(trim($length) === '' ? null : (int) $length);
678 71
        $column->precision(trim((string) $precision) === '' ? null : (int) $precision);
679 71
        $column->scale($scale === '' || $scale === null ? null : (int) $scale);
680
    }
681
682
    /**
683
     * Loads multiple types of constraints and returns the specified ones.
684
     *
685
     * @param string $tableName table name.
686
     * @param string $returnType return type:
687
     * - primaryKey
688
     * - foreignKeys
689
     * - uniques
690
     * - checks
691
     *
692
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
693
     *
694
     * @return mixed constraints.
695
     */
696 60
    private function loadTableConstraints(string $tableName, string $returnType): mixed
697
    {
698 60
        $sql = <<<SQL
699
        SELECT
700
            "uc"."CONSTRAINT_NAME" AS "name",
701
            "uccol"."COLUMN_NAME" AS "column_name",
702
            "uc"."CONSTRAINT_TYPE" AS "type",
703
            "fuc"."OWNER" AS "foreign_table_schema",
704
            "fuc"."TABLE_NAME" AS "foreign_table_name",
705
            "fuccol"."COLUMN_NAME" AS "foreign_column_name",
706
            "uc"."DELETE_RULE" AS "on_delete",
707
            "uc"."SEARCH_CONDITION" AS "check_expr"
708
        FROM "USER_CONSTRAINTS" "uc"
709
        INNER JOIN "USER_CONS_COLUMNS" "uccol"
710
        ON "uccol"."OWNER" = "uc"."OWNER" AND "uccol"."CONSTRAINT_NAME" = "uc"."CONSTRAINT_NAME"
711
        LEFT JOIN "USER_CONSTRAINTS" "fuc"
712
        ON "fuc"."OWNER" = "uc"."R_OWNER" AND "fuc"."CONSTRAINT_NAME" = "uc"."R_CONSTRAINT_NAME"
713
        LEFT JOIN "USER_CONS_COLUMNS" "fuccol"
714
        ON "fuccol"."OWNER" = "fuc"."OWNER" AND "fuccol"."CONSTRAINT_NAME" = "fuc"."CONSTRAINT_NAME" AND "fuccol"."POSITION" = "uccol"."POSITION"
715
        WHERE "uc"."OWNER" = :schemaName AND "uc"."TABLE_NAME" = :tableName
716
        ORDER BY "uccol"."POSITION" ASC
717
        SQL;
718
719 60
        $resolvedName = $this->resolveTableName($tableName);
720
721 60
        $constraints = $this->db->createCommand($sql, [
722 60
            ':schemaName' => $resolvedName->getSchemaName(),
723 60
            ':tableName' => $resolvedName->getName(),
724 60
        ])->queryAll();
725
726 60
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
727
728 60
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
729
730 60
        $result = [
731
            'primaryKey' => null,
732
            'foreignKeys' => [],
733
            'uniques' => [],
734
            'checks' => [],
735
        ];
736
737
        /**
738
         * @var string $type
739
         * @var array $names
740
         */
741 60
        foreach ($constraints as $type => $names) {
742
            /**
743
             * @psalm-var object|string|null $name
744
             * @psalm-var ConstraintArray $constraint
745
             */
746 60
            foreach ($names as $name => $constraint) {
747 60
                switch ($type) {
748 60
                    case 'P':
749 45
                        $result['primaryKey'] = (new Constraint())
750 45
                            ->name($name)
751 45
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
752 45
                        break;
753 60
                    case 'R':
754 13
                        $result['foreignKeys'][] = (new ForeignKeyConstraint())
755 13
                            ->name($name)
756 13
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
757 13
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
758 13
                            ->foreignTableName($constraint[0]['foreign_table_name'])
759 13
                            ->foreignColumnNames(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
760 13
                            ->onDelete($constraint[0]['on_delete'])
761 13
                            ->onUpdate(null);
762 13
                        break;
763 60
                    case 'U':
764 46
                        $result['uniques'][] = (new Constraint())
765 46
                            ->name($name)
766 46
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
767 46
                        break;
768 60
                    case 'C':
769 60
                        $result['checks'][] = (new CheckConstraint())
770 60
                            ->name($name)
771 60
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
772 60
                            ->expression($constraint[0]['check_expr']);
773 60
                        break;
774
                }
775
            }
776
        }
777
778 60
        foreach ($result as $type => $data) {
779 60
            $this->setTableMetadata($tableName, $type, $data);
780
        }
781
782 60
        return $result[$returnType];
783
    }
784
785
    /**
786
     * Creates a column schema for the database.
787
     *
788
     * This method may be overridden by child classes to create a DBMS-specific column schema.
789
     *
790
     * @return ColumnSchema column schema instance.
791
     */
792 71
    protected function createColumnSchema(): ColumnSchema
793
    {
794 71
        return new ColumnSchema();
795
    }
796
797 1
    public function rollBackSavepoint(string $name): void
798
    {
799 1
        $this->db->createCommand("ROLLBACK TO SAVEPOINT $name")->execute();
800
    }
801
802 2
    public function setTransactionIsolationLevel(string $level): void
803
    {
804 2
        $this->db->createCommand("SET TRANSACTION ISOLATION LEVEL $level")->execute();
805
    }
806
807
    /**
808
     * Returns the actual name of a given table name.
809
     *
810
     * This method will strip off curly brackets from the given table name and replace the percentage character '%' with
811
     * {@see ConnectionInterface::tablePrefix}.
812
     *
813
     * @param string $name the table name to be converted.
814
     *
815
     * @return string the real name of the given table name.
816
     */
817 144
    public function getRawTableName(string $name): string
818
    {
819 144
        if (str_contains($name, '{{')) {
820 21
            $name = preg_replace('/{{(.*?)}}/', '\1', $name);
821
822 21
            return str_replace('%', $this->db->getTablePrefix(), $name);
823
        }
824
825 144
        return $name;
826
    }
827
828
    /**
829
     * Returns the cache key for the specified table name.
830
     *
831
     * @param string $name the table name.
832
     *
833
     * @return array the cache key.
834
     */
835 144
    protected function getCacheKey(string $name): array
836
    {
837
        return [
838
            __CLASS__,
839 144
            $this->db->getDriver()->getDsn(),
840 144
            $this->db->getDriver()->getUsername(),
841 144
            $this->getRawTableName($name),
842
        ];
843
    }
844
845
    /**
846
     * Returns the cache tag name.
847
     *
848
     * This allows {@see refresh()} to invalidate all cached table schemas.
849
     *
850
     * @return string the cache tag name.
851
     */
852 144
    protected function getCacheTag(): string
853
    {
854 144
        return md5(serialize([
855
            __CLASS__,
856 144
            $this->db->getDriver()->getDsn(),
857 144
            $this->db->getDriver()->getUsername(),
858
        ]));
859
    }
860
861
    /**
862
     * Changes row's array key case to lower if PDO's one is set to uppercase.
863
     *
864
     * @param array $row row's array or an array of row's arrays.
865
     * @param bool $multiple whether multiple rows or a single row passed.
866
     *
867
     * @throws Exception
868
     *
869
     * @return array normalized row or rows.
870
     */
871 70
    protected function normalizePdoRowKeyCase(array $row, bool $multiple): array
872
    {
873 70
        if ($this->db->getSlavePdo()->getAttribute(PDO::ATTR_CASE) !== PDO::CASE_UPPER) {
874 54
            return $row;
875
        }
876
877 16
        if ($multiple) {
878 16
            return array_map(static function (array $row) {
879 15
                return array_change_key_case($row, CASE_LOWER);
880
            }, $row);
881
        }
882
883
        return array_change_key_case($row, CASE_LOWER);
884
    }
885
886
    /**
887
     * @return bool whether this DBMS supports [savepoint](http://en.wikipedia.org/wiki/Savepoint).
888
     */
889 2
    public function supportsSavepoint(): bool
890
    {
891 2
        return $this->db->isSavepointEnabled();
892
    }
893
894
    /**
895
     * Creates a new savepoint.
896
     *
897
     * @param string $name the savepoint name
898
     *
899
     * @throws Exception|InvalidConfigException|Throwable
900
     */
901 1
    public function createSavepoint(string $name): void
902
    {
903 1
        $this->db->createCommand("SAVEPOINT $name")->execute();
904
    }
905
}
906