Test Failed
Pull Request — dev (#47)
by Wilmer
08:30
created

SchemaPDOOracle::resolveTableName()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3.054

Importance

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

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

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

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