Passed
Branch dev (260d54)
by Wilmer
09:57
created

SchemaPDOOracle::createColumn()   B

Complexity

Conditions 10
Paths 6

Size

Total Lines 63
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 10.0036

Importance

Changes 0
Metric Value
cc 10
eloc 34
c 0
b 0
f 0
nc 6
nop 1
dl 0
loc 63
ccs 29
cts 30
cp 0.9667
crap 10.0036
rs 7.6666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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

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

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