Passed
Pull Request — dev (#52)
by Wilmer
09:10
created

SchemaPDOOracle::createColumnSchemaBuilder()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

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

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

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