Test Failed
Pull Request — dev (#49)
by Def
07:48
created

SchemaPDOOracle::findColumns()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 50
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 5.0488

Importance

Changes 0
Metric Value
cc 5
eloc 38
nc 5
nop 1
dl 0
loc 50
ccs 14
cts 16
cp 0.875
crap 5.0488
rs 9.0008
c 0
b 0
f 0
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
    public function __construct(private ConnectionPDOInterface $db, SchemaCache $schemaCache)
50
    {
51 346
        $this->defaultSchema = strtoupper($db->getDriver()->getUsername());
52
        parent::__construct($schemaCache);
53 346
    }
54 346
55
    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 70
            $resolvedName->name($parts[1]);
64
        } else {
65
            $resolvedName->schemaName($this->defaultSchema);
66
            $resolvedName->name($name);
67 70
        }
68 70
69
        $fullName = ($resolvedName->getSchemaName() !== $this->defaultSchema
70
            ? $resolvedName->getSchemaName() . '.' : '') . $resolvedName->getName();
71 70
72 70
        $resolvedName->fullName($fullName);
73
74 70
        return $resolvedName;
75
    }
76 70
77
    /**
78
     * @see https://docs.oracle.com/cd/B28359_01/server.111/b28337/tdpsg_user_accounts.htm
79
     */
80
    protected function findSchemaNames(): array
81
    {
82 1
        $sql = <<<SQL
83
        SELECT "u"."USERNAME"
84 1
        FROM "DBA_USERS" "u"
85
        WHERE "u"."DEFAULT_TABLESPACE" NOT IN ('SYSTEM', 'SYSAUX')
86
        ORDER BY "u"."USERNAME" ASC
87
        SQL;
88
89
        return $this->db->createCommand($sql)->queryColumn();
90
    }
91 1
92
    /**
93
     * @param string $schema
94
     *
95
     * @throws Exception|InvalidConfigException|Throwable
96
     *
97
     * @return array
98
     */
99
    protected function findTableNames(string $schema = ''): array
100
    {
101 5
        if ($schema === '') {
102
            $sql = <<<SQL
103 5
            SELECT TABLE_NAME
104 5
            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
            $command = $this->db->createCommand($sql);
115
        } else {
116 5
            $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
        $rows = $command->queryAll();
126
        $names = [];
127 5
128 5
        foreach ($rows as $row) {
129
            if ($this->db->getSlavePdo()->getAttribute(PDO::ATTR_CASE) === PDO::CASE_LOWER) {
130 5
                $row = array_change_key_case($row, CASE_UPPER);
131 5
            }
132 1
            $names[] = $row['TABLE_NAME'];
133
        }
134 5
135
        return $names;
136
    }
137 5
138
    /**
139
     * @param string $name
140
     *
141
     * @throws Exception|InvalidConfigException|Throwable
142
     *
143
     * @return TableSchema|null
144
     */
145
    protected function loadTableSchema(string $name): ?TableSchema
146
    {
147 83
        $table = new TableSchema();
148
149 83
        $this->resolveTableNames($table, $name);
150
151 83
        if ($this->findColumns($table)) {
152
            $this->findConstraints($table);
153 83
            return $table;
154 70
        }
155 70
156
        return null;
157
    }
158 21
159
    /**
160
     * @param string $tableName
161
     *
162
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
163
     *
164
     * @return Constraint|null
165
     */
166
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
167
    {
168 30
        return $this->loadTableConstraints($tableName, 'primaryKey');
169
    }
170 30
171
    /**
172
     * @param string $tableName
173
     *
174
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
175
     *
176
     * @return array
177
     */
178
    protected function loadTableForeignKeys(string $tableName): array
179
    {
180 4
        return $this->loadTableConstraints($tableName, 'foreignKeys');
181
    }
182 4
183
    /**
184
     * @param string $tableName
185
     *
186
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
187
     *
188
     * @return array
189
     */
190
    protected function loadTableIndexes(string $tableName): array
191
    {
192 27
        $sql = <<<SQL
193
        SELECT "ui"."INDEX_NAME" AS "name", "uicol"."COLUMN_NAME" AS "column_name",
194 27
        CASE "ui"."UNIQUENESS" WHEN 'UNIQUE' THEN 1 ELSE 0 END AS "index_is_unique",
195
        CASE WHEN "uc"."CONSTRAINT_NAME" IS NOT NULL THEN 1 ELSE 0 END AS "index_is_primary"
196
        FROM "SYS"."USER_INDEXES" "ui"
197
        LEFT JOIN "SYS"."USER_IND_COLUMNS" "uicol"
198
        ON "uicol"."INDEX_NAME" = "ui"."INDEX_NAME"
199
        LEFT JOIN "SYS"."USER_CONSTRAINTS" "uc"
200
        ON "uc"."OWNER" = "ui"."TABLE_OWNER" AND "uc"."CONSTRAINT_NAME" = "ui"."INDEX_NAME" AND "uc"."CONSTRAINT_TYPE" = 'P'
201
        WHERE "ui"."TABLE_OWNER" = :schemaName AND "ui"."TABLE_NAME" = :tableName
202
        ORDER BY "uicol"."COLUMN_POSITION" ASC
203
        SQL;
204
205
        $resolvedName = $this->resolveTableName($tableName);
206
207 27
        $indexes = $this->db->createCommand($sql, [
208
            ':schemaName' => $resolvedName->getSchemaName(),
209 27
            ':tableName' => $resolvedName->getName(),
210 27
        ])->queryAll();
211 27
212 27
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
213
214 27
        $indexes = ArrayHelper::index($indexes, null, 'name');
215
216 27
        $result = [];
217
218 27
        /**
219
         * @psalm-var object|string|null $name
220
         * @psalm-var array[] $index
221
         */
222
        foreach ($indexes as $name => $index) {
223
            $columnNames = ArrayHelper::getColumn($index, 'column_name');
224 27
225 24
            if ($columnNames[0] === null) {
226
                $columnNames[0] = '';
227 24
            }
228 17
229
            $result[] = (new IndexConstraint())
230
                ->primary((bool) $index[0]['index_is_primary'])
231 24
                ->unique((bool) $index[0]['index_is_unique'])
232 24
                ->name($name)
233 24
                ->columnNames($columnNames);
234 24
        }
235 24
236
        return $result;
237
    }
238 27
239
    /**
240
     * @param string $tableName
241
     *
242
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
243
     *
244
     * @return array
245
     */
246
    protected function loadTableUniques(string $tableName): array
247
    {
248 13
        return $this->loadTableConstraints($tableName, 'uniques');
249
    }
250 13
251
    /**
252
     * @param string $tableName
253
     *
254
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
255
     *
256
     * @return array
257
     */
258
    protected function loadTableChecks(string $tableName): array
259
    {
260 13
        return $this->loadTableConstraints($tableName, 'checks');
261
    }
262 13
263
    /**
264
     * @param string $tableName
265
     *
266
     * @throws NotSupportedException if this method is called.
267
     *
268
     * @return array
269
     */
270
    protected function loadTableDefaultValues(string $tableName): array
271
    {
272 12
        throw new NotSupportedException('Oracle does not support default value constraints.');
273
    }
274 12
275
    public function releaseSavepoint(string $name): void
276
    {
277
        /* does nothing as Oracle does not support this */
278
    }
279
280
    public function quoteSimpleTableName(string $name): string
281
    {
282 3
        return str_contains($name, '"') ? $name : '"' . $name . '"';
283
    }
284 3
285
    /**
286
     * Create a column schema builder instance giving the type and value precision.
287
     *
288
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
289
     *
290
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
291
     * @param array|int|string|null $length length or precision of the column {@see ColumnSchemaBuilder::$length}.
292
     *
293
     * @return ColumnSchemaBuilder column schema builder instance
294
     */
295
    public function createColumnSchemaBuilder(string $type, array|int|string $length = null): ColumnSchemaBuilder
296
    {
297 3
        return new ColumnSchemaBuilder($type, $length);
298
    }
299 3
300
    /**
301
     * Resolves the table name and schema name (if any).
302
     *
303
     * @param TableSchema $table the table metadata object
304
     * @param string $name the table name
305
     */
306
    protected function resolveTableNames(TableSchema $table, string $name): void
307
    {
308 83
        $parts = explode('.', str_replace('"', '', $name));
309
310 83
        if (isset($parts[1])) {
311
            $table->schemaName($parts[0]);
312 83
            $table->name($parts[1]);
313
        } else {
314
            $table->schemaName($this->defaultSchema);
315
            $table->name($name);
316 83
        }
317 83
318
        $table->fullName($table->getSchemaName() !== $this->defaultSchema
319
            ? $table->getSchemaName() . '.' . $table->getName() : $table->getName());
320 83
    }
321 83
322
    /**
323
     * Collects the table column metadata.
324
     *
325
     * @param TableSchema $table the table schema.
326
     *
327
     * @throws Exception|Throwable
328
     *
329
     * @return bool whether the table exists.
330
     */
331
    protected function findColumns(TableSchema $table): bool
332
    {
333 83
        $sql = <<<'SQL'
334
SELECT
335 83
    A.COLUMN_NAME,
336
    A.DATA_TYPE,
337
    A.DATA_PRECISION,
338
    A.DATA_SCALE,
339
    (
340
      CASE A.CHAR_USED WHEN 'C' THEN A.CHAR_LENGTH
341
        ELSE A.DATA_LENGTH
342
      END
343
    ) AS DATA_LENGTH,
344
    A.NULLABLE,
345
    A.DATA_DEFAULT,
346
    COM.COMMENTS AS COLUMN_COMMENT
347
FROM ALL_TAB_COLUMNS A
348
    INNER JOIN ALL_OBJECTS B ON B.OWNER = A.OWNER AND LTRIM(B.OBJECT_NAME) = LTRIM(A.TABLE_NAME)
349
    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)
350
WHERE
351
    A.OWNER = :schemaName
352
    AND B.OBJECT_TYPE IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW')
353
    AND B.OBJECT_NAME = :tableName
354
ORDER BY A.COLUMN_ID
355
SQL;
356
357
        try {
358
            $columns = $this->db->createCommand($sql, [
359
                ':tableName' => $table->getName(),
360 83
                ':schemaName' => $table->getSchemaName(),
361 83
            ])->queryAll();
362 83
        } catch (Exception $e) {
363 83
            return false;
364
        }
365
366
        if (empty($columns)) {
367
            return false;
368 83
        }
369 21
370
        foreach ($columns as $column) {
371
            if ($this->db->getSlavePdo()->getAttribute(PDO::ATTR_CASE) === PDO::CASE_LOWER) {
372 70
                $column = array_change_key_case($column, CASE_UPPER);
373 70
            }
374 1
375
            $c = $this->createColumn($column);
376
377 70
            $table->columns($c->getName(), $c);
378
        }
379 70
380
        return true;
381
    }
382 70
383
    /**
384
     * Sequence name of table.
385
     *
386
     * @param string $tableName
387
     *
388
     * @throws Exception|InvalidConfigException|Throwable
389
     *
390
     * @return int|string|null whether the sequence exists.
391
     *
392
     * @internal TableSchema `$table->getName()` the table schema.
393
     */
394
    protected function getTableSequenceName(string $tableName): string|int|null
395
    {
396 52
        $sequenceNameSql = <<<SQL
397
        SELECT
398 52
            UD.REFERENCED_NAME AS SEQUENCE_NAME
399
        FROM USER_DEPENDENCIES UD
400
            JOIN USER_TRIGGERS UT ON (UT.TRIGGER_NAME = UD.NAME)
401
        WHERE
402
            UT.TABLE_NAME = :tableName
403
            AND UD.TYPE = 'TRIGGER'
404
            AND UD.REFERENCED_TYPE = 'SEQUENCE'
405
        SQL;
406
        $sequenceName = $this->db->createCommand($sequenceNameSql, [':tableName' => $tableName])->queryScalar();
407
408 52
        return $sequenceName === false ? null : $sequenceName;
409
    }
410 52
411
    /**
412
     * @Overrides method in class 'Schema'
413
     *
414
     * {@see https://secure.php.net/manual/en/function.PDO-lastInsertId.php} -> Oracle does not support this.
415
     *
416
     * Returns the ID of the last inserted row or sequence value.
417
     *
418
     * @param string $sequenceName name of the sequence object (required by some DBMS)
419
     *
420
     * @throws Exception|InvalidCallException|InvalidConfigException|Throwable if the DB connection is not active.
421
     *
422
     * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object.
423
     */
424
    public function getLastInsertID(string $sequenceName = ''): string
425
    {
426 3
        if ($this->db->isActive()) {
427
            /* get the last insert id from the master connection */
428 3
            $sequenceName = $this->quoteSimpleTableName($sequenceName);
429
430 3
            return $this->db->useMaster(function (ConnectionPDOInterface $db) use ($sequenceName) {
431
                return $db->createCommand("SELECT $sequenceName.CURRVAL FROM DUAL")->queryScalar();
432 3
            });
433 3
        }
434
435
        throw new InvalidCallException('DB Connection is not active.');
436
    }
437
438
    /**
439
     * Creates ColumnSchema instance.
440
     *
441
     * @param array|string $column
442
     *
443
     * @return ColumnSchema
444
     */
445
    protected function createColumn(array|string $column): ColumnSchema
446
    {
447 70
        $c = $this->createColumnSchema();
448
449 70
        $c->name($column['COLUMN_NAME']);
450
        $c->allowNull($column['NULLABLE'] === 'Y');
451 70
        $c->comment($column['COLUMN_COMMENT'] ?? '');
452 70
        $c->primaryKey(false);
453 70
454 70
        $this->extractColumnType(
455
            $c,
456 70
            $column['DATA_TYPE'],
457
            $column['DATA_PRECISION'],
458 70
            $column['DATA_SCALE'],
459 70
            $column['DATA_LENGTH']
460 70
        );
461 70
462
        $this->extractColumnSize(
463
            $c,
464 70
            $column['DATA_TYPE'],
465
            $column['DATA_PRECISION'],
466 70
            $column['DATA_SCALE'],
467 70
            $column['DATA_LENGTH']
468 70
        );
469 70
470
        $c->phpType($this->getColumnPhpType($c));
471
472 70
        if (!$c->isPrimaryKey()) {
473
            if ($column['DATA_DEFAULT'] !== null && stripos($column['DATA_DEFAULT'], 'timestamp') !== false) {
474 70
                $c->defaultValue(null);
475 70
            } else {
476 14
                $defaultValue = $column['DATA_DEFAULT'];
477
478 70
                if ($c->getType() === 'timestamp' && $defaultValue === 'CURRENT_TIMESTAMP') {
479
                    $c->defaultValue(new Expression('CURRENT_TIMESTAMP'));
480 70
                } else {
481
                    if ($defaultValue !== null) {
482
                        if (($len = strlen($defaultValue)) > 2 && $defaultValue[0] === "'"
483 70
                            && $defaultValue[$len - 1] === "'"
484 51
                        ) {
485 51
                            $defaultValue = substr($column['DATA_DEFAULT'], 1, -1);
486
                        } else {
487 14
                            $defaultValue = trim($defaultValue);
488
                        }
489 51
                    }
490
                    $c->defaultValue($c->phpTypecast($defaultValue));
491
                }
492 70
            }
493
        }
494
495
        return $c;
496
    }
497 70
498
    /**
499
     * Finds constraints and fills them into TableSchema object passed.
500
     *
501
     * @param TableSchema $table
502
     *
503
     * @throws Exception|InvalidConfigException|Throwable
504
     */
505
    protected function findConstraints(TableSchema $table): void
506
    {
507 70
        $sql = <<<'SQL'
508
SELECT
509 70
    /*+ PUSH_PRED(C) PUSH_PRED(D) PUSH_PRED(E) */
510
    D.CONSTRAINT_NAME,
511
    D.CONSTRAINT_TYPE,
512
    C.COLUMN_NAME,
513
    C.POSITION,
514
    D.R_CONSTRAINT_NAME,
515
    E.TABLE_NAME AS TABLE_REF,
516
    F.COLUMN_NAME AS COLUMN_REF,
517
    C.TABLE_NAME
518
FROM ALL_CONS_COLUMNS C
519
    INNER JOIN ALL_CONSTRAINTS D ON D.OWNER = C.OWNER AND D.CONSTRAINT_NAME = C.CONSTRAINT_NAME
520
    LEFT JOIN ALL_CONSTRAINTS E ON E.OWNER = D.R_OWNER AND E.CONSTRAINT_NAME = D.R_CONSTRAINT_NAME
521
    LEFT JOIN ALL_CONS_COLUMNS F ON F.OWNER = E.OWNER AND F.CONSTRAINT_NAME = E.CONSTRAINT_NAME AND F.POSITION = C.POSITION
522
WHERE
523
    C.OWNER = :schemaName
524
    AND C.TABLE_NAME = :tableName
525
ORDER BY D.CONSTRAINT_NAME, C.POSITION
526
SQL;
527
528
        $command = $this->db->createCommand($sql, [
529
            ':tableName' => $table->getName(),
530 70
            ':schemaName' => $table->getSchemaName(),
531 70
        ]);
532 70
533
        $constraints = [];
534
535 70
        foreach ($command->queryAll() as $row) {
536
            if ($this->db->getSlavePdo()->getAttribute(PDO::ATTR_CASE) === PDO::CASE_LOWER) {
537 70
                $row = array_change_key_case($row, CASE_UPPER);
538 64
            }
539 1
540
            if ($row['CONSTRAINT_TYPE'] === 'P') {
541
                $table->getColumns()[$row['COLUMN_NAME']]->primaryKey(true);
542 64
                $table->primaryKey($row['COLUMN_NAME']);
543 52
544 52
                if (empty($table->getSequenceName())) {
545
                    $table->sequenceName($this->getTableSequenceName($table->getName()));
546 52
                }
547 52
            }
548
549
            if ($row['CONSTRAINT_TYPE'] !== 'R') {
550
                /**
551 64
                 * This condition is not checked in SQL WHERE because of an Oracle Bug:
552
                 *
553
                 * {@see https://github.com/yiisoft/yii2/pull/8844}
554
                 */
555
                continue;
556
            }
557 64
558
            $name = $row['CONSTRAINT_NAME'];
559
560 8
            if (!isset($constraints[$name])) {
561
                $constraints[$name] = [
562 8
                    'tableName' => $row['TABLE_REF'],
563 8
                    'columns' => [],
564 8
                ];
565
            }
566
567
            $constraints[$name]['columns'][$row['COLUMN_NAME']] = $row['COLUMN_REF'];
568
        }
569 8
570
        foreach ($constraints as $constraint) {
571
            $name = current(array_keys($constraint));
572 70
            $table->foreignKey(array_merge([$constraint['tableName']], $constraint['columns']));
573 8
        }
574 8
    }
575
576
    /**
577
     * Returns all unique indexes for the given table.
578
     *
579
     * Each array element is of the following structure:.
580
     *
581
     * ```php
582
     * [
583
     *     'IndexName1' => ['col1' [, ...]],
584
     *     'IndexName2' => ['col2' [, ...]],
585
     * ]
586
     * ```
587
     *
588
     * @param TableSchema $table the table metadata.
589
     *
590
     * @throws Exception|InvalidConfigException|Throwable
591
     *
592
     * @return array all unique indexes for the given table.
593
     */
594
    public function findUniqueIndexes(TableSchema $table): array
595
    {
596 1
        $query = <<<'SQL'
597
SELECT
598 1
    DIC.INDEX_NAME,
599
    DIC.COLUMN_NAME
600
FROM ALL_INDEXES DI
601
    INNER JOIN ALL_IND_COLUMNS DIC ON DI.TABLE_NAME = DIC.TABLE_NAME AND DI.INDEX_NAME = DIC.INDEX_NAME
602
WHERE
603
    DI.UNIQUENESS = 'UNIQUE'
604
    AND DIC.TABLE_OWNER = :schemaName
605
    AND DIC.TABLE_NAME = :tableName
606
ORDER BY DIC.TABLE_NAME, DIC.INDEX_NAME, DIC.COLUMN_POSITION
607
SQL;
608
        $result = [];
609
610 1
        $command = $this->db->createCommand($query, [
611
            ':tableName' => $table->getName(),
612 1
            ':schemaName' => $table->getschemaName(),
613 1
        ]);
614 1
615
        foreach ($command->queryAll() as $row) {
616
            $result[$row['INDEX_NAME']][] = $row['COLUMN_NAME'];
617 1
        }
618 1
619
        return $result;
620
    }
621 1
622
    /**
623
     * Extracts the data types for the given column.
624
     *
625
     * @param ColumnSchema $column
626
     * @param string $dbType DB type.
627
     * @param string|null $precision total number of digits.
628
     * @param string|null $scale number of digits on the right of the decimal separator.
629
     * @param string $length length for character types.
630
     */
631
    protected function extractColumnType(
632
        ColumnSchema $column,
633 70
        string $dbType,
634
        ?string $precision,
635
        ?string $scale,
636
        string $length
637
    ): void {
638
        $column->dbType($dbType);
639
640 70
        if (str_contains($dbType, 'FLOAT') || str_contains($dbType, 'DOUBLE')) {
641
            $column->type('double');
642 70
        } elseif (str_contains($dbType, 'NUMBER')) {
643 16
            if ($scale === null || $scale > 0) {
644 70
                $column->type('decimal');
645 67
            } else {
646 17
                $column->type('integer');
647
            }
648 67
        } elseif (str_contains($dbType, 'INTEGER')) {
649
            $column->type('integer');
650 63
        } elseif (str_contains($dbType, 'BLOB')) {
651
            $column->type('binary');
652 63
        } elseif (str_contains($dbType, 'CLOB')) {
653 18
            $column->type('text');
654 60
        } elseif (str_contains($dbType, 'TIMESTAMP')) {
655 22
            $column->type('timestamp');
656 59
        } else {
657 15
            $column->type('string');
658
        }
659 59
    }
660
661
    /**
662
     * Extracts size, precision and scale information from column's DB type.
663
     *
664
     * @param ColumnSchema $column
665
     * @param string $dbType the column's DB type.
666
     * @param string|null $precision total number of digits.
667
     * @param string|null $scale number of digits on the right of the decimal separator.
668
     * @param string $length length for character types.
669
     */
670
    protected function extractColumnSize(
671
        ColumnSchema $column,
672 70
        string $dbType,
673
        ?string $precision,
674
        ?string $scale,
675
        string $length
676
    ): void {
677
        $column->size(trim($length) === '' ? null : (int) $length);
678
        $column->precision(trim((string) $precision) === '' ? null : (int) $precision);
679 70
        $column->scale($scale === '' || $scale === null ? null : (int) $scale);
680 70
    }
681 70
682
    /**
683
     * Loads multiple types of constraints and returns the specified ones.
684
     *
685
     * @param string $tableName table name.
686
     * @param string $returnType return type:
687
     * - primaryKey
688
     * - foreignKeys
689
     * - uniques
690
     * - checks
691
     *
692
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
693
     *
694
     * @return mixed constraints.
695
     */
696
    private function loadTableConstraints(string $tableName, string $returnType): mixed
697
    {
698
        $sql = <<<SQL
699
        SELECT
700
            "uc"."CONSTRAINT_NAME" AS "name",
701
            "uccol"."COLUMN_NAME" AS "column_name",
702
            "uc"."CONSTRAINT_TYPE" AS "type",
703
            "fuc"."OWNER" AS "foreign_table_schema",
704
            "fuc"."TABLE_NAME" AS "foreign_table_name",
705
            "fuccol"."COLUMN_NAME" AS "foreign_column_name",
706
            "uc"."DELETE_RULE" AS "on_delete",
707
            "uc"."SEARCH_CONDITION" AS "check_expr"
708
        FROM "USER_CONSTRAINTS" "uc"
709
        INNER JOIN "USER_CONS_COLUMNS" "uccol"
710
        ON "uccol"."OWNER" = "uc"."OWNER" AND "uccol"."CONSTRAINT_NAME" = "uc"."CONSTRAINT_NAME"
711
        LEFT JOIN "USER_CONSTRAINTS" "fuc"
712
        ON "fuc"."OWNER" = "uc"."R_OWNER" AND "fuc"."CONSTRAINT_NAME" = "uc"."R_CONSTRAINT_NAME"
713
        LEFT JOIN "USER_CONS_COLUMNS" "fuccol"
714
        ON "fuccol"."OWNER" = "fuc"."OWNER" AND "fuccol"."CONSTRAINT_NAME" = "fuc"."CONSTRAINT_NAME" AND "fuccol"."POSITION" = "uccol"."POSITION"
715
        WHERE "uc"."OWNER" = :schemaName AND "uc"."TABLE_NAME" = :tableName
716
        ORDER BY "uccol"."POSITION" ASC
717
        SQL;
718
719
        $resolvedName = $this->resolveTableName($tableName);
720
721
        $constraints = $this->db->createCommand($sql, [
722
            ':schemaName' => $resolvedName->getSchemaName(),
723
            ':tableName' => $resolvedName->getName(),
724
        ])->queryAll();
725
726
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
727
728
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
729
730
        $result = [
731
            'primaryKey' => null,
732
            'foreignKeys' => [],
733
            'uniques' => [],
734
            'checks' => [],
735
        ];
736
737
        /**
738
         * @var string $type
739
         * @var array $names
740
         */
741
        foreach ($constraints as $type => $names) {
742
            /**
743
             * @psalm-var object|string|null $name
744
             * @psalm-var ConstraintArray $constraint
745
             */
746
            foreach ($names as $name => $constraint) {
747
                switch ($type) {
748
                    case 'P':
749
                        $result['primaryKey'] = (new Constraint())
750
                            ->name($name)
751
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
752
                        break;
753
                    case 'R':
754
                        $result['foreignKeys'][] = (new ForeignKeyConstraint())
755 60
                            ->name($name)
756
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
757 60
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
758
                            ->foreignTableName($constraint[0]['foreign_table_name'])
759
                            ->foreignColumnNames(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
760
                            ->onDelete($constraint[0]['on_delete'])
761
                            ->onUpdate(null);
762
                        break;
763
                    case 'U':
764
                        $result['uniques'][] = (new Constraint())
765
                            ->name($name)
766
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
767
                        break;
768
                    case 'C':
769
                        $result['checks'][] = (new CheckConstraint())
770
                            ->name($name)
771
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
772
                            ->expression($constraint[0]['check_expr']);
773
                        break;
774
                }
775
            }
776
        }
777
778 60
        foreach ($result as $type => $data) {
779
            $this->setTableMetadata($tableName, $type, $data);
780 60
        }
781 60
782 60
        return $result[$returnType];
783 60
    }
784
785 60
    /**
786
     * Creates a column schema for the database.
787 60
     *
788
     * This method may be overridden by child classes to create a DBMS-specific column schema.
789 60
     *
790
     * @return ColumnSchema column schema instance.
791
     */
792
    protected function createColumnSchema(): ColumnSchema
793
    {
794
        return new ColumnSchema();
795
    }
796
797
    public function rollBackSavepoint(string $name): void
798
    {
799
        $this->db->createCommand("ROLLBACK TO SAVEPOINT $name")->execute();
800 60
    }
801
802
    public function setTransactionIsolationLevel(string $level): void
803
    {
804
        $this->db->createCommand("SET TRANSACTION ISOLATION LEVEL $level")->execute();
805 60
    }
806 60
807 60
    /**
808 45
     * Returns the actual name of a given table name.
809 45
     *
810 45
     * This method will strip off curly brackets from the given table name and replace the percentage character '%' with
811 45
     * {@see ConnectionInterface::tablePrefix}.
812 60
     *
813 13
     * @param string $name the table name to be converted.
814 13
     *
815 13
     * @return string the real name of the given table name.
816 13
     */
817 13
    public function getRawTableName(string $name): string
818 13
    {
819 13
        if (str_contains($name, '{{')) {
820 13
            $name = preg_replace('/{{(.*?)}}/', '\1', $name);
821 13
822 60
            return str_replace('%', $this->db->getTablePrefix(), $name);
823 46
        }
824 46
825 46
        return $name;
826 46
    }
827 60
828 60
    /**
829 60
     * Returns the cache key for the specified table name.
830 60
     *
831 60
     * @param string $name the table name.
832 60
     *
833
     * @return array the cache key.
834
     */
835
    protected function getCacheKey(string $name): array
836
    {
837 60
        return [
838 60
            __CLASS__,
839
            $this->db->getDriver()->getDsn(),
840
            $this->db->getDriver()->getUsername(),
841 60
            $this->getRawTableName($name),
842
        ];
843
    }
844
845
    /**
846
     * Returns the cache tag name.
847
     *
848
     * This allows {@see refresh()} to invalidate all cached table schemas.
849
     *
850
     * @return string the cache tag name.
851 70
     */
852
    protected function getCacheTag(): string
853 70
    {
854
        return md5(serialize([
855
            __CLASS__,
856 1
            $this->db->getDriver()->getDsn(),
857
            $this->db->getDriver()->getUsername(),
858 1
        ]));
859
    }
860
861 2
    /**
862
     * Changes row's array key case to lower if PDO's one is set to uppercase.
863 2
     *
864
     * @param array $row row's array or an array of row's arrays.
865
     * @param bool $multiple whether multiple rows or a single row passed.
866
     *
867
     * @throws Exception
868
     *
869
     * @return array normalized row or rows.
870
     */
871
    protected function normalizePdoRowKeyCase(array $row, bool $multiple): array
872
    {
873
        if ($this->db->getSlavePdo()->getAttribute(PDO::ATTR_CASE) !== PDO::CASE_UPPER) {
874
            return $row;
875
        }
876 143
877
        if ($multiple) {
878 143
            return array_map(static function (array $row) {
879 21
                return array_change_key_case($row, CASE_LOWER);
880
            }, $row);
881 21
        }
882
883
        return array_change_key_case($row, CASE_LOWER);
884 143
    }
885
886
    /**
887
     * @return bool whether this DBMS supports [savepoint](http://en.wikipedia.org/wiki/Savepoint).
888
     */
889
    public function supportsSavepoint(): bool
890
    {
891
        return $this->db->isSavepointEnabled();
892
    }
893
894 143
    /**
895
     * Creates a new savepoint.
896
     *
897
     * @param string $name the savepoint name
898 143
     *
899 143
     * @throws Exception|InvalidConfigException|Throwable
900 143
     */
901
    public function createSavepoint(string $name): void
902
    {
903
        $this->db->createCommand("SAVEPOINT $name")->execute();
904
    }
905
}
906