Passed
Push — master ( c77b03...911b01 )
by Alexander
32:33 queued 12:30
created

Schema::insert()   B

Complexity

Conditions 8
Paths 12

Size

Total Lines 52
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 55.1554

Importance

Changes 0
Metric Value
cc 8
eloc 30
nc 12
nop 2
dl 0
loc 52
ccs 3
cts 31
cp 0.0968
crap 55.1554
rs 8.1954
c 0
b 0
f 0

1 Method

Rating   Name   Duplication   Size   Complexity  
A Schema::extractColumnSize() 0 10 5

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

657
        /** @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...
658
        ?string $scale,
659
        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

659
        /** @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...
660
    ): void {
661 75
        $column->dbType($dbType);
662
663 75
        if (str_contains($dbType, 'FLOAT') || str_contains($dbType, 'DOUBLE')) {
664 17
            $column->type(self::TYPE_DOUBLE);
665 75
        } elseif (str_contains($dbType, 'NUMBER')) {
666 72
            if ($scale === null || $scale > 0) {
667 18
                $column->type(self::TYPE_DECIMAL);
668
            } else {
669 72
                $column->type(self::TYPE_INTEGER);
670
            }
671 68
        } elseif (str_contains($dbType, 'INTEGER')) {
672
            $column->type(self::TYPE_INTEGER);
673 68
        } elseif (str_contains($dbType, 'BLOB')) {
674 19
            $column->type(self::TYPE_BINARY);
675 65
        } elseif (str_contains($dbType, 'CLOB')) {
676 22
            $column->type(self::TYPE_TEXT);
677 64
        } elseif (str_contains($dbType, 'TIMESTAMP')) {
678 16
            $column->type(self::TYPE_TIMESTAMP);
679
        } else {
680 64
            $column->type(self::TYPE_STRING);
681
        }
682
    }
683
684
    /**
685
     * Extracts size, precision and scale information from column's DB type.
686
     *
687
     * @param ColumnSchema $column
688
     * @param string $dbType the column's DB type.
689
     * @param string|null $precision total number of digits.
690
     * @param string|null $scale number of digits on the right of the decimal separator.
691
     * @param string $length length for character types.
692
     */
693 75
    protected function extractColumnSize(
694
        ColumnSchema $column,
695
        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

695
        /** @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...
696
        ?string $precision,
697
        ?string $scale,
698
        string $length
699
    ): void {
700 75
        $column->size(trim($length) === '' ? null : (int) $length);
701 75
        $column->precision(trim((string) $precision) === '' ? null : (int) $precision);
702 75
        $column->scale($scale === '' || $scale === null ? null : (int) $scale);
703
    }
704
705
    /**
706
     * Loads multiple types of constraints and returns the specified ones.
707
     *
708
     * @param string $tableName table name.
709
     * @param string $returnType return type:
710
     * - primaryKey
711
     * - foreignKeys
712
     * - uniques
713
     * - checks
714
     *
715
     * @throws Exception|InvalidConfigException|NotSupportedException|Throwable
716
     *
717
     * @return mixed constraints.
718
     */
719 64
    private function loadTableConstraints(string $tableName, string $returnType): mixed
720
    {
721 64
        $sql = <<<SQL
722
        SELECT
723
            "uc"."CONSTRAINT_NAME" AS "name",
724
            "uccol"."COLUMN_NAME" AS "column_name",
725
            "uc"."CONSTRAINT_TYPE" AS "type",
726
            "fuc"."OWNER" AS "foreign_table_schema",
727
            "fuc"."TABLE_NAME" AS "foreign_table_name",
728
            "fuccol"."COLUMN_NAME" AS "foreign_column_name",
729
            "uc"."DELETE_RULE" AS "on_delete",
730
            "uc"."SEARCH_CONDITION" AS "check_expr"
731
        FROM "USER_CONSTRAINTS" "uc"
732
        INNER JOIN "USER_CONS_COLUMNS" "uccol"
733
        ON "uccol"."OWNER" = "uc"."OWNER" AND "uccol"."CONSTRAINT_NAME" = "uc"."CONSTRAINT_NAME"
734
        LEFT JOIN "USER_CONSTRAINTS" "fuc"
735
        ON "fuc"."OWNER" = "uc"."R_OWNER" AND "fuc"."CONSTRAINT_NAME" = "uc"."R_CONSTRAINT_NAME"
736
        LEFT JOIN "USER_CONS_COLUMNS" "fuccol"
737
        ON "fuccol"."OWNER" = "fuc"."OWNER" AND "fuccol"."CONSTRAINT_NAME" = "fuc"."CONSTRAINT_NAME" AND "fuccol"."POSITION" = "uccol"."POSITION"
738
        WHERE "uc"."OWNER" = :schemaName AND "uc"."TABLE_NAME" = :tableName
739
        ORDER BY "uccol"."POSITION" ASC
740
        SQL;
741
742 64
        $resolvedName = $this->resolveTableName($tableName);
743
744 64
        $constraints = $this->db->createCommand($sql, [
745 64
            ':schemaName' => $resolvedName->getSchemaName(),
746 64
            ':tableName' => $resolvedName->getName(),
747 64
        ])->queryAll();
748
749
        /** @var Constraint[] $constraints */
750 64
        $constraints = $this->normalizeRowKeyCase($constraints, true);
751 64
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
752
753 64
        $result = [
754
            self::PRIMARY_KEY => null,
755 64
            self::FOREIGN_KEYS => [],
756 64
            self::UNIQUES => [],
757 64
            self::CHECKS => [],
758
        ];
759
760
        /**
761
         * @var string $type
762
         * @var array $names
763
         */
764 64
        foreach ($constraints as $type => $names) {
765
            /**
766
             * @psalm-var object|string|null $name
767
             * @psalm-var ConstraintArray $constraint
768
             */
769 64
            foreach ($names as $name => $constraint) {
770 64
                switch ($type) {
771 64
                    case 'P':
772 49
                        $result[self::PRIMARY_KEY] = (new Constraint())
773 49
                            ->name($name)
774 49
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
775 49
                        break;
776 64
                    case 'R':
777 17
                        $result[self::FOREIGN_KEYS][] = (new ForeignKeyConstraint())
778 17
                            ->name($name)
779 17
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
780 17
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
781 17
                            ->foreignTableName($constraint[0]['foreign_table_name'])
782 17
                            ->foreignColumnNames(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
783 17
                            ->onDelete($constraint[0]['on_delete'])
784 17
                            ->onUpdate(null);
785 17
                        break;
786 64
                    case 'U':
787 50
                        $result[self::UNIQUES][] = (new Constraint())
788 50
                            ->name($name)
789 50
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
790 50
                        break;
791 64
                    case 'C':
792 64
                        $result[self::CHECKS][] = (new CheckConstraint())
793 64
                            ->name($name)
794 64
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
795 64
                            ->expression($constraint[0]['check_expr']);
796 64
                        break;
797
                }
798
            }
799
        }
800
801 64
        foreach ($result as $type => $data) {
802 64
            $this->setTableMetadata($tableName, $type, $data);
803
        }
804
805 64
        return $result[$returnType];
806
    }
807
808
    /**
809
     * Creates a column schema for the database.
810
     *
811
     * This method may be overridden by child classes to create a DBMS-specific column schema.
812
     *
813
     * @return ColumnSchema column schema instance.
814
     */
815 75
    protected function createColumnSchema(): ColumnSchema
816
    {
817 75
        return new ColumnSchema();
818
    }
819
820
    /**
821
     * Returns the actual name of a given table name.
822
     *
823
     * This method will strip off curly brackets from the given table name and replace the percentage character '%' with
824
     * {@see ConnectionInterface::tablePrefix}.
825
     *
826
     * @param string $name the table name to be converted.
827
     *
828
     * @return string the real name of the given table name.
829
     */
830 153
    public function getRawTableName(string $name): string
831
    {
832 153
        if (str_contains($name, '{{')) {
833 21
            $name = preg_replace('/{{(.*?)}}/', '\1', $name);
834
835 21
            return str_replace('%', $this->db->getTablePrefix(), $name);
836
        }
837
838 153
        return $name;
839
    }
840
841
    /**
842
     * Returns the cache key for the specified table name.
843
     *
844
     * @param string $name the table name.
845
     *
846
     * @return array the cache key.
847
     */
848 153
    protected function getCacheKey(string $name): array
849
    {
850 153
        return array_merge([__CLASS__], $this->db->getCacheKey(), [$this->getRawTableName($name)]);
851
    }
852
853
    /**
854
     * Returns the cache tag name.
855
     *
856
     * This allows {@see refresh()} to invalidate all cached table schemas.
857
     *
858
     * @return string the cache tag name.
859
     */
860 153
    protected function getCacheTag(): string
861
    {
862 153
        return md5(serialize(array_merge([__CLASS__], $this->db->getCacheKey())));
863
    }
864
865
    /**
866
     * Changes row's array key case to lower.
867
     *
868
     * @param array $row row's array or an array of row's arrays.
869
     * @param bool $multiple whether multiple rows or a single row passed.
870
     *
871
     * @return array normalized row or rows.
872
     */
873 135
    protected function normalizeRowKeyCase(array $row, bool $multiple): array
874
    {
875 135
        if ($multiple) {
876 75
            return array_map(static function (array $row) {
877 72
                return array_change_key_case($row, CASE_LOWER);
878
            }, $row);
879
        }
880
881 82
        return array_change_key_case($row, CASE_LOWER);
882
    }
883
884
    /**
885
     * @return bool whether this DBMS supports [savepoint](http://en.wikipedia.org/wiki/Savepoint).
886
     */
887 5
    public function supportsSavepoint(): bool
888
    {
889 5
        return $this->db->isSavepointEnabled();
890
    }
891
}
892