Passed
Pull Request — dev (#52)
by Wilmer
09:20
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
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, 'primaryKey');
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, 'foreignKeys');
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, '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, '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 3
    public function quoteSimpleTableName(string $name): string
305
    {
306 3
        return str_contains($name, '"') ? $name : '"' . $name . '"';
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->getSlavePdo()?->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 the master connection */
455 3
            $sequenceName = $this->quoteSimpleTableName($sequenceName);
456
457 3
            return (string) $this->db->useMaster(static function (ConnectionPDOInterface $db) use ($sequenceName) {
458 3
                return $db->createCommand("SELECT $sequenceName.CURRVAL FROM DUAL")->queryScalar();
459 3
            });
460
        }
461
462
        throw new InvalidCallException('DB Connection is not active.');
463
    }
464
465
    /**
466
     * Creates ColumnSchema instance.
467
     *
468
     * @param array|string $column
469
     *
470
     * @return ColumnSchema
471
     */
472 71
    protected function createColumn(array|string $column): ColumnSchema
473
    {
474 71
        $c = $this->createColumnSchema();
475
476
        /**
477
         * @psalm-var array{
478
         *   COLUMN_NAME: string,
479
         *   DATA_TYPE: string,
480
         *   DATA_PRECISION: string,
481
         *   DATA_SCALE: string,
482
         *   DATA_LENGTH: string,
483
         *   NULLABLE: string,
484
         *   DATA_DEFAULT: string|null,
485
         *   COLUMN_COMMENT: string|null
486
         * } $column
487
         */
488 71
        $c->name($column['COLUMN_NAME']);
489 71
        $c->allowNull($column['NULLABLE'] === 'Y');
490 71
        $c->comment($column['COLUMN_COMMENT'] ?? '');
491 71
        $c->primaryKey(false);
492
493 71
        $this->extractColumnType(
494
            $c,
495 71
            $column['DATA_TYPE'],
496 71
            $column['DATA_PRECISION'],
497 71
            $column['DATA_SCALE'],
498 71
            $column['DATA_LENGTH']
499
        );
500
501 71
        $this->extractColumnSize(
502
            $c,
503 71
            $column['DATA_TYPE'],
504 71
            $column['DATA_PRECISION'],
505 71
            $column['DATA_SCALE'],
506 71
            $column['DATA_LENGTH']
507
        );
508
509 71
        $c->phpType($this->getColumnPhpType($c));
510
511 71
        if (!$c->isPrimaryKey()) {
512 71
            if ($column['DATA_DEFAULT'] !== null && stripos($column['DATA_DEFAULT'], 'timestamp') !== false) {
513 14
                $c->defaultValue(null);
514
            } else {
515 71
                $defaultValue = $column['DATA_DEFAULT'];
516
517 71
                if ($c->getType() === 'timestamp' && $defaultValue === 'CURRENT_TIMESTAMP') {
518
                    $c->defaultValue(new Expression('CURRENT_TIMESTAMP'));
519
                } else {
520 71
                    if ($defaultValue !== null) {
521 52
                        if (($len = strlen($defaultValue)) > 2 && $defaultValue[0] === "'"
522 52
                            && $defaultValue[$len - 1] === "'"
523
                        ) {
524 14
                            $defaultValue = substr((string) $column['DATA_DEFAULT'], 1, -1);
525
                        } else {
526 52
                            $defaultValue = trim($defaultValue);
527
                        }
528
                    }
529 71
                    $c->defaultValue($c->phpTypecast($defaultValue));
530
                }
531
            }
532
        }
533
534 71
        return $c;
535
    }
536
537
    /**
538
     * Finds constraints and fills them into TableSchema object passed.
539
     *
540
     * @param TableSchema $table
541
     *
542
     * @throws Exception|InvalidConfigException|Throwable
543
     *
544
     * @psalm-suppress PossiblyNullArrayOffset
545
     */
546 71
    protected function findConstraints(TableSchema $table): void
547
    {
548 71
        $sql = <<<SQL
549
        SELECT
550
            /*+ PUSH_PRED(C) PUSH_PRED(D) PUSH_PRED(E) */
551
            D.CONSTRAINT_NAME,
552
            D.CONSTRAINT_TYPE,
553
            C.COLUMN_NAME,
554
            C.POSITION,
555
            D.R_CONSTRAINT_NAME,
556
            E.TABLE_NAME AS TABLE_REF,
557
            F.COLUMN_NAME AS COLUMN_REF,
558
            C.TABLE_NAME
559
        FROM ALL_CONS_COLUMNS C
560
            INNER JOIN ALL_CONSTRAINTS D ON D.OWNER = C.OWNER AND D.CONSTRAINT_NAME = C.CONSTRAINT_NAME
561
            LEFT JOIN ALL_CONSTRAINTS E ON E.OWNER = D.R_OWNER AND E.CONSTRAINT_NAME = D.R_CONSTRAINT_NAME
562
            LEFT JOIN ALL_CONS_COLUMNS F ON F.OWNER = E.OWNER AND F.CONSTRAINT_NAME = E.CONSTRAINT_NAME AND F.POSITION = C.POSITION
563
        WHERE
564
            C.OWNER = :schemaName
565
            AND C.TABLE_NAME = :tableName
566
            ORDER BY D.CONSTRAINT_NAME, C.POSITION
567
        SQL;
568
569
        /**
570
         * @psalm-var array{
571
         *   array{
572
         *     CONSTRAINT_NAME: string,
573
         *     CONSTRAINT_TYPE: string,
574
         *     COLUMN_NAME: string,
575
         *     POSITION: string|null,
576
         *     R_CONSTRAINT_NAME: string|null,
577
         *     TABLE_REF: string|null,
578
         *     COLUMN_REF: string|null,
579
         *     TABLE_NAME: string
580
         *   }
581
         * } $rows
582
         */
583 71
        $rows = $this->db->createCommand(
584
            $sql,
585 71
            [':tableName' => $table->getName(), ':schemaName' => $table->getSchemaName()]
586 71
        )->queryAll();
587
588 71
        $constraints = [];
589
590 71
        foreach ($rows as $row) {
591 65
            if ($this->db->getSlavePdo()?->getAttribute(PDO::ATTR_CASE) === PDO::CASE_LOWER) {
592 1
                $row = array_change_key_case($row, CASE_UPPER);
593
            }
594
595 65
            if ($row['CONSTRAINT_TYPE'] === 'P') {
596 53
                $table->getColumns()[(string) $row['COLUMN_NAME']]->primaryKey(true);
597 53
                $table->primaryKey((string) $row['COLUMN_NAME']);
598
599 53
                if (empty($table->getSequenceName())) {
600 53
                    $table->sequenceName((string) $this->getTableSequenceName($table->getName()));
601
                }
602
            }
603
604 65
            if ($row['CONSTRAINT_TYPE'] !== 'R') {
605
                /**
606
                 * This condition is not checked in SQL WHERE because of an Oracle Bug:
607
                 *
608
                 * {@see https://github.com/yiisoft/yii2/pull/8844}
609
                 */
610 65
                continue;
611
            }
612
613 8
            $name = (string) $row['CONSTRAINT_NAME'];
614
615 8
            if (!isset($constraints[$name])) {
616 8
                $constraints[$name] = [
617 8
                    'tableName' => $row['TABLE_REF'],
618
                    'columns' => [],
619
                ];
620
            }
621
622 8
            $constraints[$name]['columns'][$row['COLUMN_NAME']] = $row['COLUMN_REF'];
623
        }
624
625 71
        foreach ($constraints as $constraint) {
626 8
            $table->foreignKey(array_merge([$constraint['tableName']], $constraint['columns']));
627
        }
628
    }
629
630
    /**
631
     * Returns all unique indexes for the given table.
632
     *
633
     * Each array element is of the following structure:.
634
     *
635
     * ```php
636
     * [
637
     *     'IndexName1' => ['col1' [, ...]],
638
     *     'IndexName2' => ['col2' [, ...]],
639
     * ]
640
     * ```
641
     *
642
     * @param TableSchema $table the table metadata.
643
     *
644
     * @throws Exception|InvalidConfigException|Throwable
645
     *
646
     * @return array all unique indexes for the given table.
647
     */
648 1
    public function findUniqueIndexes(TableSchema $table): array
649
    {
650 1
        $query = <<<SQL
651
        SELECT
652
            DIC.INDEX_NAME,
653
            DIC.COLUMN_NAME
654
        FROM ALL_INDEXES DI
655
            INNER JOIN ALL_IND_COLUMNS DIC ON DI.TABLE_NAME = DIC.TABLE_NAME AND DI.INDEX_NAME = DIC.INDEX_NAME
656
        WHERE
657
            DI.UNIQUENESS = 'UNIQUE'
658
            AND DIC.TABLE_OWNER = :schemaName
659
            AND DIC.TABLE_NAME = :tableName
660
        ORDER BY DIC.TABLE_NAME, DIC.INDEX_NAME, DIC.COLUMN_POSITION
661
        SQL;
662 1
        $result = [];
663
664 1
        $rows = $this->db->createCommand(
665
            $query,
666 1
            [':tableName' => $table->getName(), ':schemaName' => $table->getschemaName()]
667 1
        )->queryAll();
668
669
        /** @psalm-var array<array{INDEX_NAME: string, COLUMN_NAME: string}> $rows */
670 1
        foreach ($rows as $row) {
671 1
            $result[$row['INDEX_NAME']][] = $row['COLUMN_NAME'];
672
        }
673
674 1
        return $result;
675
    }
676
677
    /**
678
     * Extracts the data types for the given column.
679
     *
680
     * @param ColumnSchema $column
681
     * @param string $dbType DB type.
682
     * @param string|null $precision total number of digits.
683
     * @param string|null $scale number of digits on the right of the decimal separator.
684
     * @param string $length length for character types.
685
     */
686 71
    protected function extractColumnType(
687
        ColumnSchema $column,
688
        string $dbType,
689
        ?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

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

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

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