Passed
Branch dev (1bbda8)
by Wilmer
14:10 queued 05:58
created

SchemaPDOOracle::createSavepoint()   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 1
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\InvalidArgumentException;
18
use Yiisoft\Db\Exception\InvalidCallException;
19
use Yiisoft\Db\Exception\InvalidConfigException;
20
use Yiisoft\Db\Exception\NotSupportedException;
21
use Yiisoft\Db\Expression\Expression;
22
use Yiisoft\Db\Oracle\ColumnSchema;
23
use Yiisoft\Db\Oracle\ColumnSchemaBuilder;
24
use Yiisoft\Db\Oracle\TableSchema;
25
use Yiisoft\Db\Query\QueryBuilder;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Query\QueryBuilder was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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

636
        /** @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...
637
        ?string $scale,
638
        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

638
        /** @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...
639
    ): void {
640 70
        $column->dbType($dbType);
641
642 70
        if (str_contains($dbType, 'FLOAT') || str_contains($dbType, 'DOUBLE')) {
643 16
            $column->type('double');
644 70
        } elseif (str_contains($dbType, 'NUMBER')) {
645 67
            if ($scale === null || $scale > 0) {
646 17
                $column->type('decimal');
647
            } else {
648 67
                $column->type('integer');
649
            }
650 63
        } elseif (str_contains($dbType, 'INTEGER')) {
651
            $column->type('integer');
652 63
        } elseif (str_contains($dbType, 'BLOB')) {
653 18
            $column->type('binary');
654 60
        } elseif (str_contains($dbType, 'CLOB')) {
655 22
            $column->type('text');
656 59
        } elseif (str_contains($dbType, 'TIMESTAMP')) {
657 15
            $column->type('timestamp');
658
        } else {
659 59
            $column->type('string');
660
        }
661
    }
662
663
    /**
664
     * Extracts size, precision and scale information from column's DB type.
665
     *
666
     * @param ColumnSchema $column
667
     * @param string $dbType the column's 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 70
    protected function extractColumnSize(
673
        ColumnSchema $column,
674
        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

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