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

674
        /** @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...
675
        ?string $scale,
676
        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

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

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