Passed
Push — dev ( 84c82b...85c32d )
by Def
20:18 queued 10:34
created

SchemaPDOOracle   F

Complexity

Total Complexity 93

Size/Duplication

Total Lines 895
Duplicated Lines 0 %

Test Coverage

Coverage 95.13%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 368
c 2
b 1
f 0
dl 0
loc 895
ccs 254
cts 267
cp 0.9513
rs 2
wmc 93

32 Methods

Rating   Name   Duplication   Size   Complexity  
A createColumnSchemaBuilder() 0 3 1
A __construct() 0 4 1
A getTableSequenceName() 0 15 2
A resolveTableName() 0 20 3
A findColumns() 0 51 5
A getCacheTag() 0 6 1
A loadTableForeignKeys() 0 5 2
A releaseSavepoint() 0 2 1
A createColumnSchema() 0 3 1
A loadTableIndexes() 0 47 3
A normalizePdoRowKeyCase() 0 13 3
B extractColumnType() 0 27 10
B loadTableConstraints() 0 87 8
A getCacheKey() 0 7 1
A setTransactionIsolationLevel() 0 3 1
A loadTableDefaultValues() 0 3 1
A supportsSavepoint() 0 3 1
B createColumn() 0 63 10
A loadTableChecks() 0 5 2
A extractColumnSize() 0 10 5
A createSavepoint() 0 3 1
A findTableNames() 0 38 4
A loadTableUniques() 0 5 2
A resolveTableNames() 0 14 3
A findSchemaNames() 0 15 2
A rollBackSavepoint() 0 3 1
B findConstraints() 0 81 8
A loadTableSchema() 0 12 2
A getRawTableName() 0 9 2
A findUniqueIndexes() 0 27 2
A loadTablePrimaryKey() 0 5 2
A getLastInsertID() 0 10 2

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

687
        /** @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...
688
        ?string $scale,
689
        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

689
        /** @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...
690
    ): void {
691 71
        $column->dbType($dbType);
692
693 71
        if (str_contains($dbType, 'FLOAT') || str_contains($dbType, 'DOUBLE')) {
694 16
            $column->type('double');
695 71
        } elseif (str_contains($dbType, 'NUMBER')) {
696 68
            if ($scale === null || $scale > 0) {
697 17
                $column->type('decimal');
698
            } else {
699 68
                $column->type('integer');
700
            }
701 64
        } elseif (str_contains($dbType, 'INTEGER')) {
702
            $column->type('integer');
703 64
        } elseif (str_contains($dbType, 'BLOB')) {
704 18
            $column->type('binary');
705 61
        } elseif (str_contains($dbType, 'CLOB')) {
706 22
            $column->type('text');
707 60
        } elseif (str_contains($dbType, 'TIMESTAMP')) {
708 15
            $column->type('timestamp');
709
        } else {
710 60
            $column->type('string');
711
        }
712
    }
713
714
    /**
715
     * Extracts size, precision and scale information from column's DB type.
716
     *
717
     * @param ColumnSchema $column
718
     * @param string $dbType the column's DB type.
719
     * @param string|null $precision total number of digits.
720
     * @param string|null $scale number of digits on the right of the decimal separator.
721
     * @param string $length length for character types.
722
     */
723 71
    protected function extractColumnSize(
724
        ColumnSchema $column,
725
        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

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