Passed
Push — dev ( c7af07...16f8ac )
by Def
16:08 queued 08:01
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 350
    public function __construct(private ConnectionPDOInterface $db, SchemaCache $schemaCache, string $defaultSchema)
65
    {
66 350
        $this->defaultSchema = $defaultSchema;
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 144
        return array_merge([__CLASS__], $this->db->getCacheKey(), [$this->getRawTableName($name)]);
891
    }
892
893
    /**
894
     * Returns the cache tag name.
895
     *
896
     * This allows {@see refresh()} to invalidate all cached table schemas.
897
     *
898
     * @return string the cache tag name.
899
     */
900 144
    protected function getCacheTag(): string
901
    {
902 144
        return md5(serialize(array_merge([__CLASS__], $this->db->getCacheKey())));
903
    }
904
905
    /**
906
     * Changes row's array key case to lower if PDO's one is set to uppercase.
907
     *
908
     * @param array $row row's array or an array of row's arrays.
909
     * @param bool $multiple whether multiple rows or a single row passed.
910
     *
911
     * @throws Exception
912
     *
913
     * @return array normalized row or rows.
914
     */
915 70
    protected function normalizePdoRowKeyCase(array $row, bool $multiple): array
916
    {
917 70
        if ($this->db->getActivePDO()?->getAttribute(PDO::ATTR_CASE) !== PDO::CASE_UPPER) {
918 54
            return $row;
919
        }
920
921 16
        if ($multiple) {
922 16
            return array_map(static function (array $row) {
923 15
                return array_change_key_case($row, CASE_LOWER);
924
            }, $row);
925
        }
926
927
        return array_change_key_case($row, CASE_LOWER);
928
    }
929
930
    /**
931
     * @return bool whether this DBMS supports [savepoint](http://en.wikipedia.org/wiki/Savepoint).
932
     */
933 2
    public function supportsSavepoint(): bool
934
    {
935 2
        return $this->db->isSavepointEnabled();
936
    }
937
938
    /**
939
     * Creates a new savepoint.
940
     *
941
     * @param string $name the savepoint name
942
     *
943
     * @throws Exception|InvalidConfigException|Throwable
944
     */
945 1
    public function createSavepoint(string $name): void
946
    {
947 1
        $this->db->createCommand("SAVEPOINT $name")->execute();
948
    }
949
}
950