Passed
Pull Request — master (#39)
by Wilmer
07:07
created

Schema   F

Complexity

Total Complexity 88

Size/Duplication

Total Lines 819
Duplicated Lines 0 %

Test Coverage

Coverage 84.12%

Importance

Changes 0
Metric Value
eloc 384
dl 0
loc 819
ccs 233
cts 277
cp 0.8412
rs 2
c 0
b 0
f 0
wmc 88

27 Methods

Rating   Name   Duplication   Size   Complexity  
A extractColumnSize() 0 10 5
A __construct() 0 5 1
A resolveTableNames() 0 14 3
A loadTableForeignKeys() 0 3 1
B insert() 0 52 8
A findSchemaNames() 0 10 1
A loadTablePrimaryKey() 0 3 1
A findColumns() 0 50 5
A quoteSimpleTableName() 0 3 2
A createColumnSchemaBuilder() 0 3 1
A loadTableSchema() 0 12 2
B findConstraints() 0 69 8
B createColumn() 0 51 10
B loadTableConstraints() 0 80 8
A loadTableDefaultValues() 0 3 1
A createColumnSchema() 0 3 1
A findTableNames() 0 43 4
A resolveTableName() 0 20 3
A getLastInsertID() 0 12 2
A loadTableIndexes() 0 45 3
B extractColumnType() 0 27 10
A loadTableUniques() 0 3 1
A createQueryBuilder() 0 3 1
A findUniqueIndexes() 0 26 2
A loadTableChecks() 0 3 1
A releaseSavepoint() 0 2 1
A getTableSequenceName() 0 15 2

How to fix   Complexity   

Complex Class

Complex classes like Schema often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Schema, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Oracle;
6
7
use PDO;
8
use Throwable;
9
use Yiisoft\Arrays\ArrayHelper;
10
use Yiisoft\Db\Cache\SchemaCache;
11
use Yiisoft\Db\Connection\Connection;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Yiisoft\Db\Oracle\Connection. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
12
use Yiisoft\Db\Constraint\CheckConstraint;
13
use Yiisoft\Db\Constraint\Constraint;
14
use Yiisoft\Db\Constraint\ConstraintFinderInterface;
15
use Yiisoft\Db\Constraint\ConstraintFinderTrait;
16
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
17
use Yiisoft\Db\Constraint\IndexConstraint;
18
use Yiisoft\Db\Exception\Exception;
19
use Yiisoft\Db\Exception\IntegrityException;
20
use Yiisoft\Db\Exception\InvalidCallException;
21
use Yiisoft\Db\Exception\InvalidConfigException;
22
use Yiisoft\Db\Exception\NotSupportedException;
23
use Yiisoft\Db\Expression\Expression;
24
use Yiisoft\Db\Schema\ColumnSchema;
25
use Yiisoft\Db\Schema\Schema as AbstractSchema;
26
27
/**
28
 * Schema is the class for retrieving metadata from an Oracle database.
29
 *
30
 * @property string $lastInsertID The row ID of the last row inserted, or the last value retrieved from the
31
 * sequence object. This property is read-only.
32
 */
33
final class Schema extends AbstractSchema implements ConstraintFinderInterface
34
{
35
    use ConstraintFinderTrait;
36
37
    /**
38
     * @var array map of DB errors and corresponding exceptions.
39
     *
40
     * If left part is found in DB error message exception class from the right part is used.
41
     */
42
    protected array $exceptionMap = [
43
        'ORA-00001: unique constraint' => IntegrityException::class,
44
    ];
45
46
    protected $tableQuoteCharacter = '"';
47
48 309
    public function __construct(Connection $db, SchemaCache $schemaCache)
49
    {
50 309
        $this->defaultSchema = strtoupper($db->getUsername());
0 ignored issues
show
Bug introduced by
It seems like $db->getUsername() can also be of type null; however, parameter $string of strtoupper() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

50
        $this->defaultSchema = strtoupper(/** @scrutinizer ignore-type */ $db->getUsername());
Loading history...
51
52 309
        parent::__construct($db, $schemaCache);
53 309
    }
54
55 70
    protected function resolveTableName(string $name): TableSchema
56
    {
57 70
        $resolvedName = new TableSchema();
58
59 70
        $parts = explode('.', str_replace('"', '', $name));
60
61 70
        if (isset($parts[1])) {
62
            $resolvedName->schemaName($parts[0]);
63
            $resolvedName->name($parts[1]);
64
        } else {
65 70
            $resolvedName->schemaName($this->defaultSchema);
66 70
            $resolvedName->name($name);
67
        }
68
69 70
        $fullName = ($resolvedName->getSchemaName() !== $this->defaultSchema
70 70
            ? $resolvedName->getSchemaName() . '.' : '') . $resolvedName->getName();
71
72 70
        $resolvedName->fullName($fullName);
73
74 70
        return $resolvedName;
75
    }
76
77
    /**
78
     * @see https://docs.oracle.com/cd/B28359_01/server.111/b28337/tdpsg_user_accounts.htm
79
     */
80 1
    protected function findSchemaNames(): array
81
    {
82 1
        static $sql = <<<'SQL'
83
SELECT "u"."USERNAME"
84
FROM "DBA_USERS" "u"
85
WHERE "u"."DEFAULT_TABLESPACE" NOT IN ('SYSTEM', 'SYSAUX')
86
ORDER BY "u"."USERNAME" ASC
87
SQL;
88
89 1
        return $this->getDb()->createCommand($sql)->queryColumn();
90
    }
91
92
    /**
93
     * @param string $schema
94
     *
95
     * @throws Exception|InvalidConfigException|Throwable
96
     *
97
     * @return array
98
     */
99 5
    protected function findTableNames(string $schema = ''): array
100
    {
101 5
        if ($schema === '') {
102 5
            $sql = <<<'SQL'
103
SELECT
104
    TABLE_NAME
105
FROM USER_TABLES
106
UNION ALL
107
SELECT
108
    VIEW_NAME AS TABLE_NAME
109
FROM USER_VIEWS
110
UNION ALL
111
SELECT
112
    MVIEW_NAME AS TABLE_NAME
113
FROM USER_MVIEWS
114
ORDER BY TABLE_NAME
115
SQL;
116
117 5
            $command = $this->getDb()->createCommand($sql);
118
        } else {
119
            $sql = <<<'SQL'
120
SELECT
121
    OBJECT_NAME AS TABLE_NAME
122
FROM ALL_OBJECTS
123
WHERE
124
    OBJECT_TYPE IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW')
125
    AND OWNER = :schema
126
ORDER BY OBJECT_NAME
127
SQL;
128
            $command = $this->getDb()->createCommand($sql, [':schema' => $schema]);
129
        }
130
131 5
        $rows = $command->queryAll();
132 5
        $names = [];
133
134 5
        foreach ($rows as $row) {
135 5
            if ($this->getDb()->getSlavePdo()->getAttribute(PDO::ATTR_CASE) === PDO::CASE_LOWER) {
136 1
                $row = array_change_key_case($row, CASE_UPPER);
137
            }
138 5
            $names[] = $row['TABLE_NAME'];
139
        }
140
141 5
        return $names;
142
    }
143
144
    /**
145
     * @param string $name
146
     *
147
     * @throws Exception|InvalidConfigException|Throwable
148
     *
149
     * @return TableSchema|null
150
     */
151 80
    protected function loadTableSchema(string $name): ?TableSchema
152
    {
153 80
        $table = new TableSchema();
154
155 80
        $this->resolveTableNames($table, $name);
156
157 80
        if ($this->findColumns($table)) {
158 67
            $this->findConstraints($table);
159 67
            return $table;
160
        }
161
162 21
        return null;
163
    }
164
165
    /**
166
     * @param string $tableName
167
     *
168
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
169
     *
170
     * @return Constraint|null
171
     */
172 30
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
173
    {
174 30
        return $this->loadTableConstraints($tableName, 'primaryKey');
175
    }
176
177
    /**
178
     * @param string $tableName
179
     *
180
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
181
     *
182
     * @return array
183
     */
184 4
    protected function loadTableForeignKeys(string $tableName): array
185
    {
186 4
        return $this->loadTableConstraints($tableName, 'foreignKeys');
187
    }
188
189
    /**
190
     * @param string $tableName
191
     *
192
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
193
     *
194
     * @return array
195
     */
196 27
    protected function loadTableIndexes(string $tableName): array
197
    {
198 27
        static $sql = <<<'SQL'
199
SELECT
200
    /*+ PUSH_PRED("ui") PUSH_PRED("uicol") PUSH_PRED("uc") */
201
    "ui"."INDEX_NAME" AS "name",
202
    "uicol"."COLUMN_NAME" AS "column_name",
203
    CASE "ui"."UNIQUENESS" WHEN 'UNIQUE' THEN 1 ELSE 0 END AS "index_is_unique",
204
    CASE WHEN "uc"."CONSTRAINT_NAME" IS NOT NULL THEN 1 ELSE 0 END AS "index_is_primary"
205
FROM "SYS"."USER_INDEXES" "ui"
206
LEFT JOIN "SYS"."USER_IND_COLUMNS" "uicol"
207
    ON "uicol"."INDEX_NAME" = "ui"."INDEX_NAME"
208
LEFT JOIN "SYS"."USER_CONSTRAINTS" "uc"
209
    ON "uc"."OWNER" = "ui"."TABLE_OWNER" AND "uc"."CONSTRAINT_NAME" = "ui"."INDEX_NAME" AND "uc"."CONSTRAINT_TYPE" = 'P'
210
WHERE "ui"."TABLE_OWNER" = :schemaName AND "ui"."TABLE_NAME" = :tableName
211
ORDER BY "uicol"."COLUMN_POSITION" ASC
212
SQL;
213
214 27
        $resolvedName = $this->resolveTableName($tableName);
215
216 27
        $indexes = $this->getDb()->createCommand($sql, [
217 27
            ':schemaName' => $resolvedName->getSchemaName(),
218 27
            ':tableName' => $resolvedName->getName(),
219 27
        ])->queryAll();
220
221 27
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
222
223 27
        $indexes = ArrayHelper::index($indexes, null, 'name');
224
225 27
        $result = [];
226 27
        foreach ($indexes as $name => $index) {
227 24
            $columnNames = ArrayHelper::getColumn($index, 'column_name');
228
229 24
            if ($columnNames[0] === null) {
230 17
                $columnNames[0] = '';
231
            }
232
233 24
            $result[] = (new IndexConstraint())
234 24
                ->primary((bool) $index[0]['index_is_primary'])
235 24
                ->unique((bool) $index[0]['index_is_unique'])
236 24
                ->name($name)
237 24
                ->columnNames($columnNames);
238
        }
239
240 27
        return $result;
241
    }
242
243
    /**
244
     * @param string $tableName
245
     *
246
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
247
     *
248
     * @return array
249
     */
250 13
    protected function loadTableUniques(string $tableName): array
251
    {
252 13
        return $this->loadTableConstraints($tableName, 'uniques');
253
    }
254
255
    /**
256
     * @param string $tableName
257
     *
258
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
259
     *
260
     * @return array
261
     */
262 13
    protected function loadTableChecks(string $tableName): array
263
    {
264 13
        return $this->loadTableConstraints($tableName, 'checks');
265
    }
266
267
    /**
268
     * @param string $tableName
269
     *
270
     * @throws NotSupportedException if this method is called.
271
     *
272
     * @return array
273
     */
274 12
    protected function loadTableDefaultValues(string $tableName): array
0 ignored issues
show
Unused Code introduced by
The parameter $tableName 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

274
    protected function loadTableDefaultValues(/** @scrutinizer ignore-unused */ string $tableName): array

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...
275
    {
276 12
        throw new NotSupportedException('Oracle does not support default value constraints.');
277
    }
278
279
    public function releaseSavepoint(string $name): void
280
    {
281
        /* does nothing as Oracle does not support this */
282
    }
283
284 134
    public function quoteSimpleTableName(string $name): string
285
    {
286 134
        return strpos($name, '"') !== false ? $name : '"' . $name . '"';
287
    }
288
289 57
    public function createQueryBuilder(): QueryBuilder
290
    {
291 57
        return new QueryBuilder($this->getDb());
292
    }
293
294
    /**
295
     * Create a column schema builder instance giving the type and value precision.
296
     *
297
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
298
     *
299
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
300
     * @param array|int|string $length length or precision of the column {@see ColumnSchemaBuilder::$length}.
301
     *
302
     * @return ColumnSchemaBuilder column schema builder instance
303
     */
304 3
    public function createColumnSchemaBuilder(string $type, $length = null): ColumnSchemaBuilder
305
    {
306 3
        return new ColumnSchemaBuilder($type, $length);
307
    }
308
309
    /**
310
     * Resolves the table name and schema name (if any).
311
     *
312
     * @param TableSchema $table the table metadata object
313
     * @param string $name the table name
314
     */
315 80
    protected function resolveTableNames(TableSchema $table, string $name): void
316
    {
317 80
        $parts = explode('.', str_replace('"', '', $name));
318
319 80
        if (isset($parts[1])) {
320
            $table->schemaName($parts[0]);
321
            $table->name($parts[1]);
322
        } else {
323 80
            $table->schemaName($this->defaultSchema);
324 80
            $table->name($name);
325
        }
326
327 80
        $table->fullName($table->getSchemaName() !== $this->defaultSchema
328 80
            ? $table->getSchemaName() . '.' . $table->getName() : $table->getName());
329 80
    }
330
331
    /**
332
     * Collects the table column metadata.
333
     *
334
     * @param TableSchema $table the table schema.
335
     *
336
     * @throws Exception|Throwable
337
     *
338
     * @return bool whether the table exists.
339
     */
340 80
    protected function findColumns(TableSchema $table): bool
341
    {
342 80
        $sql = <<<'SQL'
343
SELECT
344
    A.COLUMN_NAME,
345
    A.DATA_TYPE,
346
    A.DATA_PRECISION,
347
    A.DATA_SCALE,
348
    (
349
      CASE A.CHAR_USED WHEN 'C' THEN A.CHAR_LENGTH
350
        ELSE A.DATA_LENGTH
351
      END
352
    ) AS DATA_LENGTH,
353
    A.NULLABLE,
354
    A.DATA_DEFAULT,
355
    COM.COMMENTS AS COLUMN_COMMENT
356
FROM ALL_TAB_COLUMNS A
357
    INNER JOIN ALL_OBJECTS B ON B.OWNER = A.OWNER AND LTRIM(B.OBJECT_NAME) = LTRIM(A.TABLE_NAME)
358
    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)
359
WHERE
360
    A.OWNER = :schemaName
361
    AND B.OBJECT_TYPE IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW')
362
    AND B.OBJECT_NAME = :tableName
363
ORDER BY A.COLUMN_ID
364
SQL;
365
366
        try {
367 80
            $columns = $this->getDb()->createCommand($sql, [
368 80
                ':tableName' => $table->getName(),
369 80
                ':schemaName' => $table->getSchemaName(),
370 80
            ])->queryAll();
371
        } catch (Exception $e) {
372
            return false;
373
        }
374
375 80
        if (empty($columns)) {
376 21
            return false;
377
        }
378
379 67
        foreach ($columns as $column) {
380 67
            if ($this->getDb()->getSlavePdo()->getAttribute(PDO::ATTR_CASE) === PDO::CASE_LOWER) {
381 1
                $column = array_change_key_case($column, CASE_UPPER);
382
            }
383
384 67
            $c = $this->createColumn($column);
385
386 67
            $table->columns($c->getName(), $c);
387
        }
388
389 67
        return true;
390
    }
391
392
    /**
393
     * Sequence name of table.
394
     *
395
     * @param string $tableName
396
     *
397
     * @throws Exception|InvalidConfigException|Throwable
398
     *
399
     * @return string|null whether the sequence exists.
400
     *
401
     * @internal TableSchema `$table->getName()` the table schema.
402
     */
403 49
    protected function getTableSequenceName(string $tableName): ?string
404
    {
405 49
        $sequenceNameSql = <<<SQL
406
SELECT
407
    UD.REFERENCED_NAME AS SEQUENCE_NAME
408
FROM USER_DEPENDENCIES UD
409
    JOIN USER_TRIGGERS UT ON (UT.TRIGGER_NAME = UD.NAME)
410
WHERE
411
    UT.TABLE_NAME = :tableName
412
    AND UD.TYPE = 'TRIGGER'
413
    AND UD.REFERENCED_TYPE = 'SEQUENCE'
414
SQL;
415 49
        $sequenceName = $this->getDb()->createCommand($sequenceNameSql, [':tableName' => $tableName])->queryScalar();
416
417 49
        return $sequenceName === false ? null : $sequenceName;
418
    }
419
420
    /**
421
     * @Overrides method in class 'Schema'
422
     *
423
     * {@see https://secure.php.net/manual/en/function.PDO-lastInsertId.php} -> Oracle does not support this.
424
     *
425
     * Returns the ID of the last inserted row or sequence value.
426
     *
427
     * @param string $sequenceName name of the sequence object (required by some DBMS)
428
     *
429
     * @throws Exception|InvalidCallException|InvalidConfigException|Throwable if the DB connection is not active.
430
     *
431
     * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object.
432
     */
433 3
    public function getLastInsertID(string $sequenceName = ''): string
434
    {
435 3
        if ($this->getDb()->isActive()) {
436
            /* get the last insert id from the master connection */
437 3
            $sequenceName = $this->quoteSimpleTableName($sequenceName);
438
439 3
            return $this->getDb()->useMaster(function (Connection $db) use ($sequenceName) {
440 3
                return $db->createCommand("SELECT {$sequenceName}.CURRVAL FROM DUAL")->queryScalar();
441 3
            });
442
        }
443
444
        throw new InvalidCallException('DB Connection is not active.');
445
    }
446
447
    /**
448
     * Creates ColumnSchema instance.
449
     *
450
     * @param array|string $column
451
     *
452
     * @return ColumnSchema
453
     */
454 67
    protected function createColumn($column): ColumnSchema
455
    {
456 67
        $c = $this->createColumnSchema();
457
458 67
        $c->name($column['COLUMN_NAME']);
459 67
        $c->allowNull($column['NULLABLE'] === 'Y');
460 67
        $c->comment($column['COLUMN_COMMENT'] ?? '');
461 67
        $c->primaryKey(false);
462
463 67
        $this->extractColumnType(
464 67
            $c,
465 67
            $column['DATA_TYPE'],
466 67
            $column['DATA_PRECISION'],
467 67
            $column['DATA_SCALE'],
468 67
            $column['DATA_LENGTH']
469
        );
470
471 67
        $this->extractColumnSize(
472 67
            $c,
473 67
            $column['DATA_TYPE'],
474 67
            $column['DATA_PRECISION'],
475 67
            $column['DATA_SCALE'],
476 67
            $column['DATA_LENGTH']
477
        );
478
479 67
        $c->phpType($this->getColumnPhpType($c));
480
481 67
        if (!$c->isPrimaryKey()) {
482 67
            if ($column['DATA_DEFAULT'] !== null && stripos($column['DATA_DEFAULT'], 'timestamp') !== false) {
483 14
                $c->defaultValue(null);
484
            } else {
485 67
                $defaultValue = $column['DATA_DEFAULT'];
486
487 67
                if ($c->getType() === 'timestamp' && $defaultValue === 'CURRENT_TIMESTAMP') {
488
                    $c->defaultValue(new Expression('CURRENT_TIMESTAMP'));
489
                } else {
490 67
                    if ($defaultValue !== null) {
491 51
                        if (($len = strlen($defaultValue)) > 2 && $defaultValue[0] === "'"
492 51
                            && $defaultValue[$len - 1] === "'"
493
                        ) {
494 14
                            $defaultValue = substr($column['DATA_DEFAULT'], 1, -1);
495
                        } else {
496 51
                            $defaultValue = trim($defaultValue);
497
                        }
498
                    }
499 67
                    $c->defaultValue($c->phpTypecast($defaultValue));
500
                }
501
            }
502
        }
503
504 67
        return $c;
505
    }
506
507
    /**
508
     * Finds constraints and fills them into TableSchema object passed.
509
     *
510
     * @param TableSchema $table
511
     *
512
     * @throws Exception|InvalidConfigException|Throwable
513
     */
514 67
    protected function findConstraints(TableSchema $table): void
515
    {
516 67
        $sql = <<<'SQL'
517
SELECT
518
    /*+ PUSH_PRED(C) PUSH_PRED(D) PUSH_PRED(E) */
519
    D.CONSTRAINT_NAME,
520
    D.CONSTRAINT_TYPE,
521
    C.COLUMN_NAME,
522
    C.POSITION,
523
    D.R_CONSTRAINT_NAME,
524
    E.TABLE_NAME AS TABLE_REF,
525
    F.COLUMN_NAME AS COLUMN_REF,
526
    C.TABLE_NAME
527
FROM ALL_CONS_COLUMNS C
528
    INNER JOIN ALL_CONSTRAINTS D ON D.OWNER = C.OWNER AND D.CONSTRAINT_NAME = C.CONSTRAINT_NAME
529
    LEFT JOIN ALL_CONSTRAINTS E ON E.OWNER = D.R_OWNER AND E.CONSTRAINT_NAME = D.R_CONSTRAINT_NAME
530
    LEFT JOIN ALL_CONS_COLUMNS F ON F.OWNER = E.OWNER AND F.CONSTRAINT_NAME = E.CONSTRAINT_NAME AND F.POSITION = C.POSITION
531
WHERE
532
    C.OWNER = :schemaName
533
    AND C.TABLE_NAME = :tableName
534
ORDER BY D.CONSTRAINT_NAME, C.POSITION
535
SQL;
536
537 67
        $command = $this->getDb()->createCommand($sql, [
538 67
            ':tableName' => $table->getName(),
539 67
            ':schemaName' => $table->getSchemaName(),
540
        ]);
541
542 67
        $constraints = [];
543
544 67
        foreach ($command->queryAll() as $row) {
545 61
            if ($this->getDb()->getSlavePdo()->getAttribute(PDO::ATTR_CASE) === PDO::CASE_LOWER) {
546 1
                $row = array_change_key_case($row, CASE_UPPER);
547
            }
548
549 61
            if ($row['CONSTRAINT_TYPE'] === 'P') {
550 49
                $table->getColumns()[$row['COLUMN_NAME']]->primaryKey(true);
551 49
                $table->primaryKey($row['COLUMN_NAME']);
552
553 49
                if (empty($table->getSequenceName())) {
554 49
                    $table->sequenceName($this->getTableSequenceName($table->getName()));
555
                }
556
            }
557
558 61
            if ($row['CONSTRAINT_TYPE'] !== 'R') {
559
                /**
560
                 * This condition is not checked in SQL WHERE because of an Oracle Bug:
561
                 *
562
                 * {@see https://github.com/yiisoft/yii2/pull/8844}
563
                 */
564 61
                continue;
565
            }
566
567 8
            $name = $row['CONSTRAINT_NAME'];
568
569 8
            if (!isset($constraints[$name])) {
570
                $constraints[$name] = [
571 8
                    'tableName' => $row['TABLE_REF'],
572
                    'columns' => [],
573
                ];
574
            }
575
576 8
            $constraints[$name]['columns'][$row['COLUMN_NAME']] = $row['COLUMN_REF'];
577
        }
578
579 67
        foreach ($constraints as $constraint) {
580 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...
581
582 8
            $table->foreignKey(array_merge([$constraint['tableName']], $constraint['columns']));
583
        }
584 67
    }
585
586
    /**
587
     * Returns all unique indexes for the given table.
588
     *
589
     * Each array element is of the following structure:.
590
     *
591
     * ```php
592
     * [
593
     *     'IndexName1' => ['col1' [, ...]],
594
     *     'IndexName2' => ['col2' [, ...]],
595
     * ]
596
     * ```
597
     *
598
     * @param TableSchema $table the table metadata.
599
     *
600
     * @throws Exception|InvalidConfigException|Throwable
601
     *
602
     * @return array all unique indexes for the given table.
603
     */
604 1
    public function findUniqueIndexes(TableSchema $table): array
605
    {
606 1
        $query = <<<'SQL'
607
SELECT
608
    DIC.INDEX_NAME,
609
    DIC.COLUMN_NAME
610
FROM ALL_INDEXES DI
611
    INNER JOIN ALL_IND_COLUMNS DIC ON DI.TABLE_NAME = DIC.TABLE_NAME AND DI.INDEX_NAME = DIC.INDEX_NAME
612
WHERE
613
    DI.UNIQUENESS = 'UNIQUE'
614
    AND DIC.TABLE_OWNER = :schemaName
615
    AND DIC.TABLE_NAME = :tableName
616
ORDER BY DIC.TABLE_NAME, DIC.INDEX_NAME, DIC.COLUMN_POSITION
617
SQL;
618 1
        $result = [];
619
620 1
        $command = $this->getDb()->createCommand($query, [
621 1
            ':tableName' => $table->getName(),
622 1
            ':schemaName' => $table->getschemaName(),
623
        ]);
624
625 1
        foreach ($command->queryAll() as $row) {
626 1
            $result[$row['INDEX_NAME']][] = $row['COLUMN_NAME'];
627
        }
628
629 1
        return $result;
630
    }
631
632
    /**
633
     * Extracts the data types for the given column.
634
     *
635
     * @param ColumnSchema $column
636
     * @param string $dbType DB type.
637
     * @param string|null $precision total number of digits.
638
     * @param string|null $scale number of digits on the right of the decimal separator.
639
     * @param string $length length for character types.
640
     */
641 67
    protected function extractColumnType(
642
        ColumnSchema $column,
643
        string $dbType,
644
        ?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

644
        /** @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...
645
        ?string $scale,
646
        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

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

682
        /** @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...
683
        ?string $precision,
684
        ?string $scale,
685
        string $length
686
    ): void {
687 67
        $column->size(trim($length) === '' ? null : (int) $length);
688 67
        $column->precision(trim((string) $precision) === '' ? null : (int) $precision);
689 67
        $column->scale($scale === '' || $scale === null ? null : (int) $scale);
690 67
    }
691
692
    public function insert($table, $columns)
693
    {
694
        $params = [];
695
        $returnParams = [];
696
        $sql = $this->getDb()->getQueryBuilder()->insert($table, $columns, $params);
697
        $tableSchema = $this->getTableSchema($table);
698
        $returnColumns = $tableSchema->getPrimaryKey();
699
700
        if (!empty($returnColumns)) {
701
            $columnSchemas = $tableSchema->getColumns();
702
703
            $returning = [];
704
            foreach ($returnColumns as $name) {
705
                $phName = QueryBuilder::PARAM_PREFIX . (count($params) + count($returnParams));
706
707
                $returnParams[$phName] = [
708
                    'column' => $name,
709
                    'value' => '',
710
                ];
711
712
                if (!isset($columnSchemas[$name]) || $columnSchemas[$name]->getPhpType() !== 'integer') {
713
                    $returnParams[$phName]['dataType'] = PDO::PARAM_STR;
714
                } else {
715
                    $returnParams[$phName]['dataType'] = PDO::PARAM_INT;
716
                }
717
718
                $returnParams[$phName]['size'] = $columnSchemas[$name]->getSize() ?? -1;
719
720
                $returning[] = $this->quoteColumnName($name);
721
            }
722
723
            $sql .= ' RETURNING ' . implode(', ', $returning) . ' INTO ' . implode(', ', array_keys($returnParams));
724
        }
725
726
        $command = $this->getDb()->createCommand($sql, $params);
727
728
        $command->prepare(false);
729
730
        foreach ($returnParams as $name => &$value) {
731
            $command->getPdoStatement()->bindParam($name, $value['value'], $value['dataType'], $value['size']);
732
        }
733
734
        if (!$command->execute()) {
735
            return false;
736
        }
737
738
        $result = [];
739
        foreach ($returnParams as $value) {
740
            $result[$value['column']] = $value['value'];
741
        }
742
743
        return $result;
744
    }
745
746
    /**
747
     * Loads multiple types of constraints and returns the specified ones.
748
     *
749
     * @param string $tableName table name.
750
     * @param string $returnType return type:
751
     * - primaryKey
752
     * - foreignKeys
753
     * - uniques
754
     * - checks
755
     *
756
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
757
     *
758
     * @return mixed constraints.
759
     */
760 60
    private function loadTableConstraints(string $tableName, string $returnType)
761
    {
762 60
        $sql = <<<'SQL'
763
SELECT
764
    /*+ PUSH_PRED("uc") PUSH_PRED("uccol") PUSH_PRED("fuc") */
765
    "uc"."CONSTRAINT_NAME" AS "name",
766
    "uccol"."COLUMN_NAME" AS "column_name",
767
    "uc"."CONSTRAINT_TYPE" AS "type",
768
    "fuc"."OWNER" AS "foreign_table_schema",
769
    "fuc"."TABLE_NAME" AS "foreign_table_name",
770
    "fuccol"."COLUMN_NAME" AS "foreign_column_name",
771
    "uc"."DELETE_RULE" AS "on_delete",
772
    "uc"."SEARCH_CONDITION" AS "check_expr"
773
FROM "USER_CONSTRAINTS" "uc"
774
INNER JOIN "USER_CONS_COLUMNS" "uccol"
775
    ON "uccol"."OWNER" = "uc"."OWNER" AND "uccol"."CONSTRAINT_NAME" = "uc"."CONSTRAINT_NAME"
776
LEFT JOIN "USER_CONSTRAINTS" "fuc"
777
    ON "fuc"."OWNER" = "uc"."R_OWNER" AND "fuc"."CONSTRAINT_NAME" = "uc"."R_CONSTRAINT_NAME"
778
LEFT JOIN "USER_CONS_COLUMNS" "fuccol"
779
    ON "fuccol"."OWNER" = "fuc"."OWNER" AND "fuccol"."CONSTRAINT_NAME" = "fuc"."CONSTRAINT_NAME" AND "fuccol"."POSITION" = "uccol"."POSITION"
780
WHERE "uc"."OWNER" = :schemaName AND "uc"."TABLE_NAME" = :tableName
781
ORDER BY "uccol"."POSITION" ASC
782
SQL;
783
784 60
        $resolvedName = $this->resolveTableName($tableName);
785
786 60
        $constraints = $this->getDb()->createCommand($sql, [
787 60
            ':schemaName' => $resolvedName->getSchemaName(),
788 60
            ':tableName' => $resolvedName->getName(),
789 60
        ])->queryAll();
790
791 60
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
792
793 60
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
794
795 60
        $result = [
796
            'primaryKey' => null,
797
            'foreignKeys' => [],
798
            'uniques' => [],
799
            'checks' => [],
800
        ];
801
802 60
        foreach ($constraints as $type => $names) {
803 60
            foreach ($names as $name => $constraint) {
804 60
                switch ($type) {
805 60
                    case 'P':
806 45
                        $result['primaryKey'] = (new Constraint())
807 45
                            ->name($name)
808 45
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
809 45
                        break;
810 60
                    case 'R':
811 13
                        $result['foreignKeys'][] = (new ForeignKeyConstraint())
812 13
                            ->name($name)
813 13
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
814 13
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
815 13
                            ->foreignTableName($constraint[0]['foreign_table_name'])
816 13
                            ->foreignColumnNames(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
817 13
                            ->onDelete($constraint[0]['on_delete'])
818 13
                            ->onUpdate(null);
819 13
                        break;
820 60
                    case 'U':
821 46
                        $result['uniques'][] = (new Constraint())
822 46
                            ->name($name)
823 46
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
824 46
                        break;
825 60
                    case 'C':
826 60
                        $result['checks'][] = (new CheckConstraint())
827 60
                            ->name($name)
828 60
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
829 60
                            ->expression($constraint[0]['check_expr']);
830 60
                        break;
831
                }
832
            }
833
        }
834
835 60
        foreach ($result as $type => $data) {
836 60
            $this->setTableMetadata($tableName, $type, $data);
837
        }
838
839 60
        return $result[$returnType];
840
    }
841
842
    /**
843
     * Creates a column schema for the database.
844
     *
845
     * This method may be overridden by child classes to create a DBMS-specific column schema.
846
     *
847
     * @return ColumnSchema column schema instance.
848
     */
849 67
    protected function createColumnSchema(): ColumnSchema
850
    {
851 67
        return new ColumnSchema();
852
    }
853
}
854