Passed
Push — adopt ( 41d06a )
by Sergei
53:41 queued 24:25
created

Schema::createColumn()   B

Complexity

Conditions 10
Paths 6

Size

Total Lines 63
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 10.0036

Importance

Changes 0
Metric Value
cc 10
eloc 34
nc 6
nop 1
dl 0
loc 63
ccs 29
cts 30
cp 0.9667
crap 10.0036
rs 7.6666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

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

652
        /** @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...
653
        ?string $scale,
654
        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

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

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