Passed
Push — master ( 647f42...eee75a )
by Wilmer
04:02
created

Schema::resolveTableNames()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3.0987

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 2
nop 2
dl 0
loc 14
ccs 7
cts 9
cp 0.7778
crap 3.0987
rs 9.9666
c 0
b 0
f 0
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\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...
11
use Yiisoft\Db\Constraint\CheckConstraint;
12
use Yiisoft\Db\Constraint\Constraint;
13
use Yiisoft\Db\Constraint\ConstraintFinderInterface;
14
use Yiisoft\Db\Constraint\ConstraintFinderTrait;
15
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
16
use Yiisoft\Db\Constraint\IndexConstraint;
17
use Yiisoft\Db\Exception\Exception;
18
use Yiisoft\Db\Exception\InvalidCallException;
19
use Yiisoft\Db\Exception\IntegrityException;
20
use Yiisoft\Db\Exception\InvalidConfigException;
21
use Yiisoft\Db\Exception\NotSupportedException;
22
use Yiisoft\Db\Expression\Expression;
23
use Yiisoft\Db\Schema\ColumnSchema;
24
use Yiisoft\Db\Schema\Schema as AbstractSchema;
25
26
/**
27
 * Schema is the class for retrieving metadata from an Oracle database.
28
 *
29
 * @property string $lastInsertID The row ID of the last row inserted, or the last value retrieved from the
30
 * sequence object. This property is read-only.
31
 */
32
final class Schema extends AbstractSchema implements ConstraintFinderInterface
33
{
34
    use ConstraintFinderTrait;
35
36
    /**
37
     * @var array map of DB errors and corresponding exceptions.
38
     *
39
     * If left part is found in DB error message exception class from the right part is used.
40
     */
41
    protected array $exceptionMap = [
42
        'ORA-00001: unique constraint' => IntegrityException::class,
43
    ];
44
45
    protected $tableQuoteCharacter = '"';
46
47 305
    public function __construct(Connection $db)
48
    {
49 305
        $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

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

273
    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...
274
    {
275 12
        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
283 134
    public function quoteSimpleTableName(string $name): string
284
    {
285 134
        return strpos($name, '"') !== false ? $name : '"' . $name . '"';
286
    }
287
288 57
    public function createQueryBuilder(): QueryBuilder
289
    {
290 57
        return new QueryBuilder($this->getDb());
291
    }
292
293
    /**
294
     * Create a column schema builder instance giving the type and value precision.
295
     *
296
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
297
     *
298
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
299
     * @param array|int|string $length length or precision of the column {@see ColumnSchemaBuilder::$length}.
300
     *
301
     * @return ColumnSchemaBuilder column schema builder instance
302
     */
303 3
    public function createColumnSchemaBuilder(string $type, $length = null): ColumnSchemaBuilder
304
    {
305 3
        return new ColumnSchemaBuilder($type, $length);
306
    }
307
308
    /**
309
     * Resolves the table name and schema name (if any).
310
     *
311
     * @param TableSchema $table the table metadata object
312
     * @param string $name the table name
313
     */
314 79
    protected function resolveTableNames(TableSchema $table, string $name): void
315
    {
316 79
        $parts = explode('.', str_replace('"', '', $name));
317
318 79
        if (isset($parts[1])) {
319
            $table->schemaName($parts[0]);
320
            $table->name($parts[1]);
321
        } else {
322 79
            $table->schemaName($this->defaultSchema);
323 79
            $table->name($name);
324
        }
325
326 79
        $table->fullName($table->getSchemaName() !== $this->defaultSchema
327 79
            ? $table->getSchemaName() . '.' . $table->getName() : $table->getName());
328 79
    }
329
330
    /**
331
     * Collects the table column metadata.
332
     *
333
     * @param TableSchema $table the table schema.
334
     *
335
     * @throws Exception|Throwable
336
     *
337
     * @return bool whether the table exists.
338
     */
339 79
    protected function findColumns(TableSchema $table): bool
340
    {
341
        $sql = <<<'SQL'
342 79
SELECT
343
    A.COLUMN_NAME,
344
    A.DATA_TYPE,
345
    A.DATA_PRECISION,
346
    A.DATA_SCALE,
347
    (
348
      CASE A.CHAR_USED WHEN 'C' THEN A.CHAR_LENGTH
349
        ELSE A.DATA_LENGTH
350
      END
351
    ) AS DATA_LENGTH,
352
    A.NULLABLE,
353
    A.DATA_DEFAULT,
354
    COM.COMMENTS AS COLUMN_COMMENT
355
FROM ALL_TAB_COLUMNS A
356
    INNER JOIN ALL_OBJECTS B ON B.OWNER = A.OWNER AND LTRIM(B.OBJECT_NAME) = LTRIM(A.TABLE_NAME)
357
    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)
358
WHERE
359
    A.OWNER = :schemaName
360
    AND B.OBJECT_TYPE IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW')
361
    AND B.OBJECT_NAME = :tableName
362
ORDER BY A.COLUMN_ID
363
SQL;
364
365
        try {
366 79
            $columns = $this->getDb()->createCommand($sql, [
367 79
                ':tableName' => $table->getName(),
368 79
                ':schemaName' => $table->getSchemaName(),
369 79
            ])->queryAll();
370
        } catch (Exception $e) {
371
            return false;
372
        }
373
374 79
        if (empty($columns)) {
375 21
            return false;
376
        }
377
378 66
        foreach ($columns as $column) {
379 66
            if ($this->getDb()->getSlavePdo()->getAttribute(PDO::ATTR_CASE) === PDO::CASE_LOWER) {
380 1
                $column = array_change_key_case($column, CASE_UPPER);
381
            }
382
383 66
            $c = $this->createColumn($column);
384
385 66
            $table->columns($c->getName(), $c);
386
        }
387
388 66
        return true;
389
    }
390
391
    /**
392
     * Sequence name of table.
393
     *
394
     * @param string $tableName
395
     *
396
     * @throws Exception|InvalidConfigException|Throwable
397
     *
398
     * @return string|null whether the sequence exists.
399
     *
400
     * @internal TableSchema `$table->getName()` the table schema.
401
     */
402 49
    protected function getTableSequenceName(string $tableName): ?string
403
    {
404
        $sequenceNameSql = <<<SQL
405 49
SELECT
406
    UD.REFERENCED_NAME AS SEQUENCE_NAME
407
FROM USER_DEPENDENCIES UD
408
    JOIN USER_TRIGGERS UT ON (UT.TRIGGER_NAME = UD.NAME)
409
WHERE
410
    UT.TABLE_NAME = :tableName
411
    AND UD.TYPE = 'TRIGGER'
412
    AND UD.REFERENCED_TYPE = 'SEQUENCE'
413
SQL;
414 49
        $sequenceName = $this->getDb()->createCommand($sequenceNameSql, [':tableName' => $tableName])->queryScalar();
415
416 49
        return $sequenceName === false ? null : $sequenceName;
417
    }
418
419
    /**
420
     * @Overrides method in class 'Schema'
421
     *
422
     * {@see https://secure.php.net/manual/en/function.PDO-lastInsertId.php} -> Oracle does not support this.
423
     *
424
     * Returns the ID of the last inserted row or sequence value.
425
     *
426
     * @param string $sequenceName name of the sequence object (required by some DBMS)
427
     *
428
     * @throws Exception|InvalidCallException|InvalidConfigException|Throwable if the DB connection is not active.
429
     *
430
     * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object.
431
     */
432 3
    public function getLastInsertID(string $sequenceName = ''): string
433
    {
434 3
        if ($this->getDb()->isActive()) {
435
            /* get the last insert id from the master connection */
436 3
            $sequenceName = $this->quoteSimpleTableName($sequenceName);
437
438 3
            return $this->getDb()->useMaster(function (Connection $db) use ($sequenceName) {
439 3
                return $db->createCommand("SELECT {$sequenceName}.CURRVAL FROM DUAL")->queryScalar();
440 3
            });
441
        }
442
443
        throw new InvalidCallException('DB Connection is not active.');
444
    }
445
446
    /**
447
     * Creates ColumnSchema instance.
448
     *
449
     * @param array|string $column
450
     *
451
     * @return ColumnSchema
452
     */
453 66
    protected function createColumn($column): ColumnSchema
454
    {
455 66
        $c = $this->createColumnSchema();
456
457 66
        $c->name($column['COLUMN_NAME']);
458 66
        $c->allowNull($column['NULLABLE'] === 'Y');
459 66
        $c->comment($column['COLUMN_COMMENT'] ?? '');
460 66
        $c->primaryKey(false);
461
462 66
        $this->extractColumnType(
463 66
            $c,
464 66
            $column['DATA_TYPE'],
465 66
            $column['DATA_PRECISION'],
466 66
            $column['DATA_SCALE'],
467 66
            $column['DATA_LENGTH']
468
        );
469
470 66
        $this->extractColumnSize(
471 66
            $c,
472 66
            $column['DATA_TYPE'],
473 66
            $column['DATA_PRECISION'],
474 66
            $column['DATA_SCALE'],
475 66
            $column['DATA_LENGTH']
476
        );
477
478 66
        $c->phpType($this->getColumnPhpType($c));
479
480 66
        if (!$c->isPrimaryKey()) {
481 66
            if ($column['DATA_DEFAULT'] !== null && stripos($column['DATA_DEFAULT'], 'timestamp') !== false) {
482 14
                $c->defaultValue(null);
483
            } else {
484 66
                $defaultValue = $column['DATA_DEFAULT'];
485
486 66
                if ($c->getType() === 'timestamp' && $defaultValue === 'CURRENT_TIMESTAMP') {
487
                    $c->defaultValue(new Expression('CURRENT_TIMESTAMP'));
488
                } else {
489 66
                    if ($defaultValue !== null) {
490 50
                        if (($len = strlen($defaultValue)) > 2 && $defaultValue[0] === "'"
491 50
                            && $defaultValue[$len - 1] === "'"
492
                        ) {
493 14
                            $defaultValue = substr($column['DATA_DEFAULT'], 1, -1);
494
                        } else {
495 50
                            $defaultValue = trim($defaultValue);
496
                        }
497
                    }
498 66
                    $c->defaultValue($c->phpTypecast($defaultValue));
499
                }
500
            }
501
        }
502
503 66
        return $c;
504
    }
505
506
    /**
507
     * Finds constraints and fills them into TableSchema object passed.
508
     *
509
     * @param TableSchema $table
510
     *
511
     * @throws Exception|InvalidConfigException|Throwable
512
     */
513 66
    protected function findConstraints(TableSchema $table): void
514
    {
515
        $sql = <<<'SQL'
516 66
SELECT
517
    /*+ PUSH_PRED(C) PUSH_PRED(D) PUSH_PRED(E) */
518
    D.CONSTRAINT_NAME,
519
    D.CONSTRAINT_TYPE,
520
    C.COLUMN_NAME,
521
    C.POSITION,
522
    D.R_CONSTRAINT_NAME,
523
    E.TABLE_NAME AS TABLE_REF,
524
    F.COLUMN_NAME AS COLUMN_REF,
525
    C.TABLE_NAME
526
FROM ALL_CONS_COLUMNS C
527
    INNER JOIN ALL_CONSTRAINTS D ON D.OWNER = C.OWNER AND D.CONSTRAINT_NAME = C.CONSTRAINT_NAME
528
    LEFT JOIN ALL_CONSTRAINTS E ON E.OWNER = D.R_OWNER AND E.CONSTRAINT_NAME = D.R_CONSTRAINT_NAME
529
    LEFT JOIN ALL_CONS_COLUMNS F ON F.OWNER = E.OWNER AND F.CONSTRAINT_NAME = E.CONSTRAINT_NAME AND F.POSITION = C.POSITION
530
WHERE
531
    C.OWNER = :schemaName
532
    AND C.TABLE_NAME = :tableName
533
ORDER BY D.CONSTRAINT_NAME, C.POSITION
534
SQL;
535
536 66
        $command = $this->getDb()->createCommand($sql, [
537 66
            ':tableName' => $table->getName(),
538 66
            ':schemaName' => $table->getSchemaName(),
539
        ]);
540
541 66
        $constraints = [];
542
543 66
        foreach ($command->queryAll() as $row) {
544 61
            if ($this->getDb()->getSlavePdo()->getAttribute(PDO::ATTR_CASE) === PDO::CASE_LOWER) {
545 1
                $row = array_change_key_case($row, CASE_UPPER);
546
            }
547
548 61
            if ($row['CONSTRAINT_TYPE'] === 'P') {
549 49
                $table->getColumns()[$row['COLUMN_NAME']]->primaryKey(true);
550 49
                $table->primaryKey($row['COLUMN_NAME']);
551
552 49
                if (empty($table->getSequenceName())) {
553 49
                    $table->sequenceName($this->getTableSequenceName($table->getName()));
0 ignored issues
show
Bug introduced by
It seems like $table->getName() can also be of type null; however, parameter $tableName of Yiisoft\Db\Oracle\Schema::getTableSequenceName() 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

553
                    $table->sequenceName($this->getTableSequenceName(/** @scrutinizer ignore-type */ $table->getName()));
Loading history...
554
                }
555
            }
556
557 61
            if ($row['CONSTRAINT_TYPE'] !== 'R') {
558
                /**
559
                 * This condition is not checked in SQL WHERE because of an Oracle Bug:
560
                 *
561
                 * {@see https://github.com/yiisoft/yii2/pull/8844}
562
                 */
563 61
                continue;
564
            }
565
566 8
            $name = $row['CONSTRAINT_NAME'];
567
568 8
            if (!isset($constraints[$name])) {
569 8
                $constraints[$name] = [
570 8
                    'tableName' => $row['TABLE_REF'],
571
                    'columns' => [],
572
                ];
573
            }
574
575 8
            $constraints[$name]['columns'][$row['COLUMN_NAME']] = $row['COLUMN_REF'];
576
        }
577
578 66
        foreach ($constraints as $constraint) {
579 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...
580
581 8
            $table->foreignKey(array_merge([$constraint['tableName']], $constraint['columns']));
582
        }
583 66
    }
584
585
    /**
586
     * Returns all unique indexes for the given table.
587
     *
588
     * Each array element is of the following structure:.
589
     *
590
     * ```php
591
     * [
592
     *     'IndexName1' => ['col1' [, ...]],
593
     *     'IndexName2' => ['col2' [, ...]],
594
     * ]
595
     * ```
596
     *
597
     * @param TableSchema $table the table metadata.
598
     *
599
     * @throws Exception|InvalidConfigException|Throwable
600
     *
601
     * @return array all unique indexes for the given table.
602
     */
603 1
    public function findUniqueIndexes(TableSchema $table): array
604
    {
605
        $query = <<<'SQL'
606 1
SELECT
607
    DIC.INDEX_NAME,
608
    DIC.COLUMN_NAME
609
FROM ALL_INDEXES DI
610
    INNER JOIN ALL_IND_COLUMNS DIC ON DI.TABLE_NAME = DIC.TABLE_NAME AND DI.INDEX_NAME = DIC.INDEX_NAME
611
WHERE
612
    DI.UNIQUENESS = 'UNIQUE'
613
    AND DIC.TABLE_OWNER = :schemaName
614
    AND DIC.TABLE_NAME = :tableName
615
ORDER BY DIC.TABLE_NAME, DIC.INDEX_NAME, DIC.COLUMN_POSITION
616
SQL;
617 1
        $result = [];
618
619 1
        $command = $this->getDb()->createCommand($query, [
620 1
            ':tableName' => $table->getName(),
621 1
            ':schemaName' => $table->getschemaName(),
622
        ]);
623
624 1
        foreach ($command->queryAll() as $row) {
625 1
            $result[$row['INDEX_NAME']][] = $row['COLUMN_NAME'];
626
        }
627
628 1
        return $result;
629
    }
630
631
    /**
632
     * Extracts the data types for the given column.
633
     *
634
     * @param ColumnSchema $column
635
     * @param string $dbType DB type.
636
     * @param string|null $precision total number of digits.
637
     * @param string|null $scale number of digits on the right of the decimal separator.
638
     * @param string $length length for character types.
639
     */
640 66
    protected function extractColumnType(
641
        ColumnSchema $column,
642
        string $dbType,
643
        ?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

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

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

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