Test Failed
Pull Request — dev (#48)
by Wilmer
25:37 queued 08:28
created

SchemaPDOOracle::findTableNames()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 37
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4.25

Importance

Changes 0
Metric Value
cc 4
eloc 28
nc 6
nop 1
dl 0
loc 37
ccs 6
cts 8
cp 0.75
crap 4.25
rs 9.472
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
    public function __construct(private ConnectionPDOInterface $db, SchemaCache $schemaCache)
52
    {
53
        $this->defaultSchema = strtoupper($db->getDriver()->getUsername());
54
        parent::__construct($schemaCache);
55
    }
56
57
    protected function resolveTableName(string $name): TableSchema
58
    {
59
        $resolvedName = new TableSchema();
60
61 347
        $parts = explode('.', str_replace('"', '', $name));
62
63 347
        if (isset($parts[1])) {
64 347
            $resolvedName->schemaName($parts[0]);
65 347
            $resolvedName->name($parts[1]);
66
        } else {
67 70
            $resolvedName->schemaName($this->defaultSchema);
68
            $resolvedName->name($name);
69 70
        }
70
71 70
        $fullName = ($resolvedName->getSchemaName() !== $this->defaultSchema
72
            ? $resolvedName->getSchemaName() . '.' : '') . $resolvedName->getName();
73 70
74
        $resolvedName->fullName($fullName);
75
76
        return $resolvedName;
77 70
    }
78 70
79
    /**
80
     * @see https://docs.oracle.com/cd/B28359_01/server.111/b28337/tdpsg_user_accounts.htm
81 70
     */
82 70
    protected function findSchemaNames(): array
83
    {
84 70
        $sql = <<<SQL
85
        SELECT "u"."USERNAME"
86 70
        FROM "DBA_USERS" "u"
87
        WHERE "u"."DEFAULT_TABLESPACE" NOT IN ('SYSTEM', 'SYSAUX')
88
        ORDER BY "u"."USERNAME" ASC
89
        SQL;
90
91
        return $this->db->createCommand($sql)->queryColumn();
92 1
    }
93
94 1
    /**
95
     * @param string $schema
96
     *
97
     * @throws Exception|InvalidConfigException|Throwable
98
     *
99
     * @return array
100
     */
101 1
    protected function findTableNames(string $schema = ''): array
102
    {
103
        if ($schema === '') {
104
            $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 5
            SELECT MVIEW_NAME AS TABLE_NAME
112
            FROM USER_MVIEWS
113 5
            ORDER BY TABLE_NAME
114 5
            SQL;
115
116
            $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 5
127
        $rows = $command->queryAll();
128
        $names = [];
129
130
        foreach ($rows as $row) {
131
            if ($this->db->getSlavePdo()->getAttribute(PDO::ATTR_CASE) === PDO::CASE_LOWER) {
132
                $row = array_change_key_case($row, CASE_UPPER);
133
            }
134
            $names[] = $row['TABLE_NAME'];
135
        }
136
137 5
        return $names;
138 5
    }
139
140 5
    /**
141 5
     * @param string $name
142 1
     *
143
     * @throws Exception|InvalidConfigException|Throwable
144 5
     *
145
     * @return TableSchema|null
146
     */
147 5
    protected function loadTableSchema(string $name): ?TableSchema
148
    {
149
        $table = new TableSchema();
150
151
        $this->resolveTableNames($table, $name);
152
153
        if ($this->findColumns($table)) {
154
            $this->findConstraints($table);
155
            return $table;
156
        }
157 83
158
        return null;
159 83
    }
160
161 83
    /**
162
     * @param string $tableName
163 83
     *
164 70
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
165 70
     *
166
     * @return Constraint|null
167
     */
168 21
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
169
    {
170
        return $this->loadTableConstraints($tableName, 'primaryKey');
171
    }
172
173
    /**
174
     * @param string $tableName
175
     *
176
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
177
     *
178 30
     * @return array
179
     */
180 30
    protected function loadTableForeignKeys(string $tableName): array
181
    {
182
        return $this->loadTableConstraints($tableName, 'foreignKeys');
183
    }
184
185
    /**
186
     * @param string $tableName
187
     *
188
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
189
     *
190 4
     * @return array
191
     */
192 4
    protected function loadTableIndexes(string $tableName): array
193
    {
194
        $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 27
        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 27
        ORDER BY "uicol"."COLUMN_POSITION" ASC
205
        SQL;
206
207
        $resolvedName = $this->resolveTableName($tableName);
208
209
        $indexes = $this->db->createCommand($sql, [
210
            ':schemaName' => $resolvedName->getSchemaName(),
211
            ':tableName' => $resolvedName->getName(),
212
        ])->queryAll();
213
214
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
215
216
        $indexes = ArrayHelper::index($indexes, null, 'name');
217 27
218
        $result = [];
219 27
220 27
        /**
221 27
         * @psalm-var object|string|null $name
222 27
         * @psalm-var array[] $index
223
         */
224 27
        foreach ($indexes as $name => $index) {
225
            $columnNames = ArrayHelper::getColumn($index, 'column_name');
226 27
227
            if ($columnNames[0] === null) {
228 27
                $columnNames[0] = '';
229
            }
230
231
            $result[] = (new IndexConstraint())
232
                ->primary((bool) $index[0]['index_is_primary'])
233
                ->unique((bool) $index[0]['index_is_unique'])
234 27
                ->name($name)
235 24
                ->columnNames($columnNames);
236
        }
237 24
238 17
        return $result;
239
    }
240
241 24
    /**
242 24
     * @param string $tableName
243 24
     *
244 24
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
245 24
     *
246
     * @return array
247
     */
248 27
    protected function loadTableUniques(string $tableName): array
249
    {
250
        return $this->loadTableConstraints($tableName, 'uniques');
251
    }
252
253
    /**
254
     * @param string $tableName
255
     *
256
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
257
     *
258 13
     * @return array
259
     */
260 13
    protected function loadTableChecks(string $tableName): array
261
    {
262
        return $this->loadTableConstraints($tableName, 'checks');
263
    }
264
265
    /**
266
     * @param string $tableName
267
     *
268
     * @throws NotSupportedException if this method is called.
269
     *
270 13
     * @return array
271
     */
272 13
    protected function loadTableDefaultValues(string $tableName): array
273
    {
274
        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 12
    public function quoteSimpleTableName(string $name): string
283
    {
284 12
        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 3
     * @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 3
     *
295
     * @return ColumnSchemaBuilder column schema builder instance
296
     */
297
    public function createColumnSchemaBuilder(string $type, array|int|string $length = null): ColumnSchemaBuilder
298
    {
299
        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 3
     */
308
    protected function resolveTableNames(TableSchema $table, string $name): void
309 3
    {
310
        $parts = explode('.', str_replace('"', '', $name));
311
312
        if (isset($parts[1])) {
313
            $table->schemaName($parts[0]);
314
            $table->name($parts[1]);
315
        } else {
316
            $table->schemaName($this->defaultSchema);
317
            $table->name($name);
318 83
        }
319
320 83
        $table->fullName($table->getSchemaName() !== $this->defaultSchema
321
            ? $table->getSchemaName() . '.' . $table->getName() : $table->getName());
322 83
    }
323
324
    /**
325
     * Collects the table column metadata.
326 83
     *
327 83
     * @param TableSchema $table the table schema.
328
     *
329
     * @throws Exception|Throwable
330 83
     *
331 83
     * @return bool whether the table exists.
332 83
     */
333
    protected function findColumns(TableSchema $table): bool
334
    {
335
        $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 83
        ELSE A.DATA_LENGTH
344
      END
345 83
    ) 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
            $columns = $this->db->createCommand($sql, [
361
                ':tableName' => $table->getName(),
362
                ':schemaName' => $table->getSchemaName(),
363
            ])->queryAll();
364
        } catch (Exception $e) {
365
            return false;
366
        }
367
368
        if (empty($columns)) {
369
            return false;
370 83
        }
371 83
372 83
        foreach ($columns as $column) {
373 83
            if ($this->db->getSlavePdo()->getAttribute(PDO::ATTR_CASE) === PDO::CASE_LOWER) {
374
                $column = array_change_key_case($column, CASE_UPPER);
375
            }
376
377
            $c = $this->createColumn($column);
378 83
379 21
            $table->columns($c->getName(), $c);
380
        }
381
382 70
        return true;
383 70
    }
384 1
385
    /**
386
     * Sequence name of table.
387 70
     *
388
     * @param string $tableName
389 70
     *
390
     * @throws Exception|InvalidConfigException|Throwable
391
     *
392 70
     * @return int|string|null whether the sequence exists.
393
     *
394
     * @internal TableSchema `$table->getName()` the table schema.
395
     */
396
    protected function getTableSequenceName(string $tableName): string|int|null
397
    {
398
        $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 52
            AND UD.REFERENCED_TYPE = 'SEQUENCE'
407
        SQL;
408 52
        $sequenceName = $this->db->createCommand($sequenceNameSql, [':tableName' => $tableName])->queryScalar();
409
410
        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 52
     * Returns the ID of the last inserted row or sequence value.
419
     *
420 52
     * @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
    public function getLastInsertID(string $sequenceName = ''): string
427
    {
428
        if ($this->db->isActive()) {
429
            /* get the last insert id from the master connection */
430
            $sequenceName = $this->quoteSimpleTableName($sequenceName);
431
432
            return $this->db->useMaster(function (ConnectionPDOInterface $db) use ($sequenceName) {
433
                return $db->createCommand("SELECT $sequenceName.CURRVAL FROM DUAL")->queryScalar();
434
            });
435
        }
436 3
437
        throw new InvalidCallException('DB Connection is not active.');
438 3
    }
439
440 3
    /**
441
     * Creates ColumnSchema instance.
442 3
     *
443 3
     * @param array|string $column
444 3
     *
445
     * @return ColumnSchema
446
     */
447
    protected function createColumn(array|string $column): ColumnSchema
448
    {
449
        $c = $this->createColumnSchema();
450
451
        $c->name($column['COLUMN_NAME']);
452
        $c->allowNull($column['NULLABLE'] === 'Y');
453
        $c->comment($column['COLUMN_COMMENT'] ?? '');
454
        $c->primaryKey(false);
455
456
        $this->extractColumnType(
457 70
            $c,
458
            $column['DATA_TYPE'],
459 70
            $column['DATA_PRECISION'],
460
            $column['DATA_SCALE'],
461 70
            $column['DATA_LENGTH']
462 70
        );
463 70
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 70
        );
471 70
472
        $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 70
                $c->defaultValue(null);
477 70
            } else {
478 70
                $defaultValue = $column['DATA_DEFAULT'];
479 70
480
                if ($c->getType() === 'timestamp' && $defaultValue === 'CURRENT_TIMESTAMP') {
481
                    $c->defaultValue(new Expression('CURRENT_TIMESTAMP'));
482 70
                } else {
483
                    if ($defaultValue !== null) {
484 70
                        if (($len = strlen($defaultValue)) > 2 && $defaultValue[0] === "'"
485 70
                            && $defaultValue[$len - 1] === "'"
486 14
                        ) {
487
                            $defaultValue = substr($column['DATA_DEFAULT'], 1, -1);
488 70
                        } else {
489
                            $defaultValue = trim($defaultValue);
490 70
                        }
491
                    }
492
                    $c->defaultValue($c->phpTypecast($defaultValue));
493 70
                }
494 51
            }
495 51
        }
496
497 14
        return $c;
498
    }
499 51
500
    /**
501
     * Finds constraints and fills them into TableSchema object passed.
502 70
     *
503
     * @param TableSchema $table
504
     *
505
     * @throws Exception|InvalidConfigException|Throwable
506
     */
507 70
    protected function findConstraints(TableSchema $table): void
508
    {
509
        $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 70
    E.TABLE_NAME AS TABLE_REF,
518
    F.COLUMN_NAME AS COLUMN_REF,
519 70
    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
        $command = $this->db->createCommand($sql, [
531
            ':tableName' => $table->getName(),
532
            ':schemaName' => $table->getSchemaName(),
533
        ]);
534
535
        $constraints = [];
536
537
        foreach ($command->queryAll() as $row) {
538
            if ($this->db->getSlavePdo()->getAttribute(PDO::ATTR_CASE) === PDO::CASE_LOWER) {
539
                $row = array_change_key_case($row, CASE_UPPER);
540 70
            }
541 70
542 70
            if ($row['CONSTRAINT_TYPE'] === 'P') {
543
                $table->getColumns()[$row['COLUMN_NAME']]->primaryKey(true);
544
                $table->primaryKey($row['COLUMN_NAME']);
545 70
546
                if (empty($table->getSequenceName())) {
547 70
                    $table->sequenceName($this->getTableSequenceName($table->getName()));
548 64
                }
549 1
            }
550
551
            if ($row['CONSTRAINT_TYPE'] !== 'R') {
552 64
                /**
553 52
                 * This condition is not checked in SQL WHERE because of an Oracle Bug:
554 52
                 *
555
                 * {@see https://github.com/yiisoft/yii2/pull/8844}
556 52
                 */
557 52
                continue;
558
            }
559
560
            $name = $row['CONSTRAINT_NAME'];
561 64
562
            if (!isset($constraints[$name])) {
563
                $constraints[$name] = [
564
                    'tableName' => $row['TABLE_REF'],
565
                    'columns' => [],
566
                ];
567 64
            }
568
569
            $constraints[$name]['columns'][$row['COLUMN_NAME']] = $row['COLUMN_REF'];
570 8
        }
571
572 8
        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 8
     * Returns all unique indexes for the given table.
580
     *
581
     * Each array element is of the following structure:.
582 70
     *
583 8
     * ```php
584 8
     * [
585
     *     'IndexName1' => ['col1' [, ...]],
586 70
     *     '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
    public function findUniqueIndexes(TableSchema $table): array
597
    {
598
        $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 1
    AND DIC.TABLE_OWNER = :schemaName
607
    AND DIC.TABLE_NAME = :tableName
608 1
ORDER BY DIC.TABLE_NAME, DIC.INDEX_NAME, DIC.COLUMN_POSITION
609
SQL;
610
        $result = [];
611
612
        $command = $this->db->createCommand($query, [
613
            ':tableName' => $table->getName(),
614
            ':schemaName' => $table->getschemaName(),
615
        ]);
616
617
        foreach ($command->queryAll() as $row) {
618
            $result[$row['INDEX_NAME']][] = $row['COLUMN_NAME'];
619
        }
620 1
621
        return $result;
622 1
    }
623 1
624 1
    /**
625
     * Extracts the data types for the given column.
626
     *
627 1
     * @param ColumnSchema $column
628 1
     * @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 1
     * @param string $length length for character types.
632
     */
633
    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
        $column->dbType($dbType);
641
642
        if (str_contains($dbType, 'FLOAT') || str_contains($dbType, 'DOUBLE')) {
643 70
            $column->type('double');
644
        } elseif (str_contains($dbType, 'NUMBER')) {
645
            if ($scale === null || $scale > 0) {
646
                $column->type('decimal');
647
            } else {
648
                $column->type('integer');
649
            }
650 70
        } elseif (str_contains($dbType, 'INTEGER')) {
651
            $column->type('integer');
652 70
        } elseif (str_contains($dbType, 'BLOB')) {
653 16
            $column->type('binary');
654 70
        } elseif (str_contains($dbType, 'CLOB')) {
655 67
            $column->type('text');
656 17
        } elseif (str_contains($dbType, 'TIMESTAMP')) {
657
            $column->type('timestamp');
658 67
        } else {
659
            $column->type('string');
660 63
        }
661
    }
662 63
663 18
    /**
664 60
     * Extracts size, precision and scale information from column's DB type.
665 22
     *
666 59
     * @param ColumnSchema $column
667 15
     * @param string $dbType the column's DB type.
668
     * @param string|null $precision total number of digits.
669 59
     * @param string|null $scale number of digits on the right of the decimal separator.
670
     * @param string $length length for character types.
671 70
     */
672
    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
        $column->size(trim($length) === '' ? null : (int) $length);
680
        $column->precision(trim((string) $precision) === '' ? null : (int) $precision);
681
        $column->scale($scale === '' || $scale === null ? null : (int) $scale);
682 70
    }
683
684
    /**
685
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException|Throwable
686
     */
687
    public function insert($table, $columns): bool|array
688
    {
689 70
        $params = [];
690 70
        $returnParams = [];
691 70
        $sql = $this->db->getQueryBuilder()->insert($table, $columns, $params);
692 70
        $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
    private function loadTableConstraints(string $tableName, string $returnType): mixed
756
    {
757
        $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 60
            "uc"."DELETE_RULE" AS "on_delete",
766
            "uc"."SEARCH_CONDITION" AS "check_expr"
767 60
        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
        $resolvedName = $this->resolveTableName($tableName);
779
780
        $constraints = $this->db->createCommand($sql, [
781
            ':schemaName' => $resolvedName->getSchemaName(),
782
            ':tableName' => $resolvedName->getName(),
783
        ])->queryAll();
784
785
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
786
787
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
788 60
789
        $result = [
790 60
            'primaryKey' => null,
791 60
            'foreignKeys' => [],
792 60
            'uniques' => [],
793 60
            'checks' => [],
794
        ];
795 60
796
        /**
797 60
         * @var string $type
798
         * @var array $names
799 60
         */
800
        foreach ($constraints as $type => $names) {
801
            /**
802
             * @psalm-var object|string|null $name
803
             * @psalm-var ConstraintArray $constraint
804
             */
805
            foreach ($names as $name => $constraint) {
806
                switch ($type) {
807
                    case 'P':
808
                        $result['primaryKey'] = (new Constraint())
809
                            ->name($name)
810 60
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
811
                        break;
812
                    case 'R':
813
                        $result['foreignKeys'][] = (new ForeignKeyConstraint())
814
                            ->name($name)
815 60
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
816 60
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
817 60
                            ->foreignTableName($constraint[0]['foreign_table_name'])
818 45
                            ->foreignColumnNames(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
819 45
                            ->onDelete($constraint[0]['on_delete'])
820 45
                            ->onUpdate(null);
821 45
                        break;
822 60
                    case 'U':
823 13
                        $result['uniques'][] = (new Constraint())
824 13
                            ->name($name)
825 13
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
826 13
                        break;
827 13
                    case 'C':
828 13
                        $result['checks'][] = (new CheckConstraint())
829 13
                            ->name($name)
830 13
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
831 13
                            ->expression($constraint[0]['check_expr']);
832 60
                        break;
833 46
                }
834 46
            }
835 46
        }
836 46
837 60
        foreach ($result as $type => $data) {
838 60
            $this->setTableMetadata($tableName, $type, $data);
839 60
        }
840 60
841 60
        return $result[$returnType];
842 60
    }
843
844
    /**
845
     * Creates a column schema for the database.
846
     *
847 60
     * This method may be overridden by child classes to create a DBMS-specific column schema.
848 60
     *
849
     * @return ColumnSchema column schema instance.
850
     */
851 60
    protected function createColumnSchema(): ColumnSchema
852
    {
853
        return new ColumnSchema();
854
    }
855
856
    public function rollBackSavepoint(string $name): void
857
    {
858
        $this->db->createCommand("ROLLBACK TO SAVEPOINT $name")->execute();
859
    }
860
861 70
    public function setTransactionIsolationLevel(string $level): void
862
    {
863 70
        $this->db->createCommand("SET TRANSACTION ISOLATION LEVEL $level")->execute();
864
    }
865
866 1
    /**
867
     * Returns the actual name of a given table name.
868 1
     *
869 1
     * This method will strip off curly brackets from the given table name and replace the percentage character '%' with
870
     * {@see ConnectionInterface::tablePrefix}.
871 2
     *
872
     * @param string $name the table name to be converted.
873 2
     *
874 2
     * @return string the real name of the given table name.
875
     */
876
    public function getRawTableName(string $name): string
877
    {
878
        if (str_contains($name, '{{')) {
879
            $name = preg_replace('/{{(.*?)}}/', '\1', $name);
880
881
            return str_replace('%', $this->db->getTablePrefix(), $name);
882
        }
883
884
        return $name;
885
    }
886 143
887
    /**
888 143
     * Returns the cache key for the specified table name.
889 21
     *
890
     * @param string $name the table name.
891 21
     *
892
     * @return array the cache key.
893
     */
894 143
    protected function getCacheKey(string $name): array
895
    {
896
        return [
897
            __CLASS__,
898
            $this->db->getDriver()->getDsn(),
899
            $this->db->getDriver()->getUsername(),
900
            $this->getRawTableName($name),
901
        ];
902
    }
903
904 143
    /**
905
     * Returns the cache tag name.
906
     *
907 143
     * This allows {@see refresh()} to invalidate all cached table schemas.
908 143
     *
909 143
     * @return string the cache tag name.
910 143
     */
911
    protected function getCacheTag(): string
912
    {
913
        return md5(serialize([
914
            __CLASS__,
915
            $this->db->getDriver()->getDsn(),
916
            $this->db->getDriver()->getUsername(),
917
        ]));
918
    }
919
920
    /**
921 143
     * Changes row's array key case to lower if PDO's one is set to uppercase.
922
     *
923 143
     * @param array $row row's array or an array of row's arrays.
924 143
     * @param bool $multiple whether multiple rows or a single row passed.
925 143
     *
926 143
     * @throws Exception
927
     *
928
     * @return array normalized row or rows.
929
     */
930
    protected function normalizePdoRowKeyCase(array $row, bool $multiple): array
931
    {
932
        if ($this->db->getSlavePdo()->getAttribute(PDO::ATTR_CASE) !== PDO::CASE_UPPER) {
933
            return $row;
934
        }
935
936
        if ($multiple) {
937
            return array_map(static function (array $row) {
938
                return array_change_key_case($row, CASE_LOWER);
939
            }, $row);
940 70
        }
941
942 70
        return array_change_key_case($row, CASE_LOWER);
943 54
    }
944
945
    /**
946 16
     * @return bool whether this DBMS supports [savepoint](http://en.wikipedia.org/wiki/Savepoint).
947 16
     */
948 15
    public function supportsSavepoint(): bool
949 16
    {
950
        return $this->db->isSavepointEnabled();
951
    }
952
953
    /**
954
     * Creates a new savepoint.
955
     *
956
     * @param string $name the savepoint name
957
     *
958 2
     * @throws Exception|InvalidConfigException|Throwable
959
     */
960 2
    public function createSavepoint(string $name): void
961
    {
962
        $this->db->createCommand("SAVEPOINT $name")->execute();
963
    }
964
}
965