Test Failed
Push — fix-update-with-expressions ( c5f2bd...e60186 )
by Sergei
35:48 queued 22:00
created

Schema::findTableComment()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 16
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 12
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 16
rs 9.8666
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Oracle;
6
7
use Throwable;
8
use Yiisoft\Db\Cache\SchemaCache;
9
use Yiisoft\Db\Connection\ConnectionInterface;
10
use Yiisoft\Db\Constraint\CheckConstraint;
11
use Yiisoft\Db\Constraint\Constraint;
12
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
13
use Yiisoft\Db\Constraint\IndexConstraint;
14
use Yiisoft\Db\Driver\Pdo\AbstractPdoSchema;
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\Helper\DbArrayHelper;
20
use Yiisoft\Db\Schema\Builder\ColumnInterface;
21
use Yiisoft\Db\Schema\ColumnSchemaInterface;
22
use Yiisoft\Db\Schema\TableSchemaInterface;
23
24
use function array_change_key_case;
25
use function array_map;
26
use function array_merge;
27
use function array_reverse;
28
use function implode;
29
use function is_array;
30
use function md5;
31
use function preg_match;
32
use function preg_replace;
33
use function serialize;
34
use function str_replace;
35
use function strtolower;
36
use function trim;
37
38
/**
39
 * Implements the Oracle Server specific schema, supporting Oracle Server 11C and above.
40
 *
41
 * @psalm-type ColumnInfoArray = array{
42
 *   column_name: string,
43
 *   data_type: string,
44
 *   data_precision: string|null,
45
 *   data_scale: string|null,
46
 *   data_length: string,
47
 *   nullable: string,
48
 *   data_default: string|null,
49
 *   is_pk: string|null,
50
 *   identity_column: string,
51
 *   column_comment: string|null
52
 * }
53
 *
54
 * @psalm-type ConstraintArray = array<
55
 *   array-key,
56
 *   array {
57
 *     name: string,
58
 *     column_name: string,
59
 *     type: string,
60
 *     foreign_table_schema: string|null,
61
 *     foreign_table_name: string|null,
62
 *     foreign_column_name: string|null,
63
 *     on_update: string,
64
 *     on_delete: string,
65
 *     check_expr: string
66
 *   }
67
 * >
68
 */
69
final class Schema extends AbstractPdoSchema
70
{
71
    /**
72
     * The mapping from physical column types (keys) to abstract column types (values).
73
     *
74
     * @link https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/Data-Types.html
75
     *
76
     * @var string[]
77
     */
78
    private const TYPE_MAP = [
79
        'char' => self::TYPE_CHAR,
80
        'nchar' => self::TYPE_CHAR,
81
        'varchar2' => self::TYPE_STRING,
82
        'nvarchar2' => self::TYPE_STRING,
83
        'clob' => self::TYPE_TEXT,
84
        'nclob' => self::TYPE_TEXT,
85
        'blob' => self::TYPE_BINARY,
86
        'bfile' => self::TYPE_BINARY,
87
        'long raw' => self::TYPE_BINARY,
88
        'raw' => self::TYPE_BINARY,
89
        'number' => self::TYPE_DECIMAL,
90
        'binary_float' => self::TYPE_FLOAT, // 32 bit
91
        'binary_double' => self::TYPE_DOUBLE, // 64 bit
92
        'float' => self::TYPE_DOUBLE, // 126 bit
93
        'timestamp' => self::TYPE_TIMESTAMP,
94
        'timestamp with time zone' => self::TYPE_TIMESTAMP,
95
        'timestamp with local time zone' => self::TYPE_TIMESTAMP,
96
        'date' => self::TYPE_DATE,
97
        'interval day to second' => self::TYPE_TIME,
98
99
        /** Deprecated */
100
        'long' => self::TYPE_TEXT,
101
    ];
102
103
    public function __construct(protected ConnectionInterface $db, SchemaCache $schemaCache, string $defaultSchema)
104
    {
105
        $this->defaultSchema = $defaultSchema;
106
        parent::__construct($db, $schemaCache);
107
    }
108
109
    public function createColumn(string $type, array|int|string $length = null): ColumnInterface
110
    {
111
        return new Column($type, $length);
112
    }
113
114
    protected function resolveTableName(string $name): TableSchemaInterface
115
    {
116
        $resolvedName = new TableSchema();
117
118
        $parts = array_reverse(
119
            $this->db->getQuoter()->getTableNameParts($name)
120
        );
121
122
        $resolvedName->name($parts[0] ?? '');
123
        $resolvedName->schemaName($parts[1] ?? $this->defaultSchema);
124
125
        $resolvedName->fullName(
126
            $resolvedName->getSchemaName() !== $this->defaultSchema ?
127
            implode('.', array_reverse($parts)) : $resolvedName->getName()
128
        );
129
130
        return $resolvedName;
131
    }
132
133
    /**
134
     * @link https://docs.oracle.com/cd/B28359_01/server.111/b28337/tdpsg_user_accounts.htm
135
     *
136
     * @throws Exception
137
     * @throws InvalidConfigException
138
     * @throws NotSupportedException
139
     * @throws Throwable
140
     */
141
    protected function findSchemaNames(): array
142
    {
143
        $sql = <<<SQL
144
        SELECT "u"."USERNAME"
145
        FROM "DBA_USERS" "u"
146
        WHERE "u"."DEFAULT_TABLESPACE" NOT IN ('SYSTEM', 'SYSAUX')
147
        ORDER BY "u"."USERNAME" ASC
148
        SQL;
149
150
        return $this->db->createCommand($sql)->queryColumn();
151
    }
152
153
    /**
154
     * @throws Exception
155
     * @throws InvalidConfigException
156
     * @throws Throwable
157
     */
158
    protected function findTableComment(TableSchemaInterface $tableSchema): void
159
    {
160
        $sql = <<<SQL
161
        SELECT "COMMENTS"
162
        FROM ALL_TAB_COMMENTS
163
        WHERE
164
              "OWNER" = :schemaName AND
165
              "TABLE_NAME" = :tableName
166
        SQL;
167
168
        $comment = $this->db->createCommand($sql, [
169
            ':schemaName' => $tableSchema->getSchemaName(),
170
            ':tableName' => $tableSchema->getName(),
171
        ])->queryScalar();
172
173
        $tableSchema->comment(is_string($comment) ? $comment : null);
174
    }
175
176
    /**
177
     * @throws Exception
178
     * @throws InvalidConfigException
179
     * @throws Throwable
180
     */
181
    protected function findTableNames(string $schema = ''): array
182
    {
183
        if ($schema === '') {
184
            $sql = <<<SQL
185
            SELECT TABLE_NAME
186
            FROM USER_TABLES
187
            UNION ALL
188
            SELECT VIEW_NAME AS TABLE_NAME
189
            FROM USER_VIEWS
190
            UNION ALL
191
            SELECT MVIEW_NAME AS TABLE_NAME
192
            FROM USER_MVIEWS
193
            ORDER BY TABLE_NAME
194
            SQL;
195
196
            $command = $this->db->createCommand($sql);
197
        } else {
198
            $sql = <<<SQL
199
            SELECT OBJECT_NAME AS TABLE_NAME
200
            FROM ALL_OBJECTS
201
            WHERE OBJECT_TYPE IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW') AND OWNER = :schema
202
            ORDER BY OBJECT_NAME
203
            SQL;
204
            $command = $this->db->createCommand($sql, [':schema' => $schema]);
205
        }
206
207
        $rows = $command->queryAll();
208
        $names = [];
209
210
        /** @psalm-var string[][] $rows */
211
        foreach ($rows as $row) {
212
            /** @psalm-var string[] $row */
213
            $row = array_change_key_case($row);
214
            $names[] = $row['table_name'];
215
        }
216
217
        return $names;
218
    }
219
220
    /**
221
     * @throws Exception
222
     * @throws InvalidConfigException
223
     * @throws Throwable
224
     */
225
    protected function loadTableSchema(string $name): TableSchemaInterface|null
226
    {
227
        $table = $this->resolveTableName($name);
228
        $this->findTableComment($table);
229
230
        if ($this->findColumns($table)) {
231
            $this->findConstraints($table);
232
            return $table;
233
        }
234
235
        return null;
236
    }
237
238
    /**
239
     * @throws Exception
240
     * @throws InvalidConfigException
241
     * @throws NotSupportedException
242
     * @throws Throwable
243
     */
244
    protected function loadTablePrimaryKey(string $tableName): Constraint|null
245
    {
246
        /** @psalm-var mixed $tablePrimaryKey */
247
        $tablePrimaryKey = $this->loadTableConstraints($tableName, self::PRIMARY_KEY);
248
        return $tablePrimaryKey instanceof Constraint ? $tablePrimaryKey : null;
249
    }
250
251
    /**
252
     * @throws Exception
253
     * @throws InvalidConfigException
254
     * @throws NotSupportedException
255
     * @throws Throwable
256
     */
257
    protected function loadTableForeignKeys(string $tableName): array
258
    {
259
        /** @psalm-var mixed $tableForeignKeys */
260
        $tableForeignKeys = $this->loadTableConstraints($tableName, self::FOREIGN_KEYS);
261
        return is_array($tableForeignKeys) ? $tableForeignKeys : [];
262
    }
263
264
    /**
265
     * @throws Exception
266
     * @throws InvalidConfigException
267
     * @throws NotSupportedException
268
     * @throws Throwable
269
     */
270
    protected function loadTableIndexes(string $tableName): array
271
    {
272
        $sql = <<<SQL
273
        SELECT "ui"."INDEX_NAME" AS "name", "uicol"."COLUMN_NAME" AS "column_name",
274
        CASE "ui"."UNIQUENESS" WHEN 'UNIQUE' THEN 1 ELSE 0 END AS "index_is_unique",
275
        CASE WHEN "uc"."CONSTRAINT_NAME" IS NOT NULL THEN 1 ELSE 0 END AS "index_is_primary"
276
        FROM "SYS"."USER_INDEXES" "ui"
277
        LEFT JOIN "SYS"."USER_IND_COLUMNS" "uicol"
278
        ON "uicol"."INDEX_NAME" = "ui"."INDEX_NAME"
279
        LEFT JOIN "SYS"."USER_CONSTRAINTS" "uc"
280
        ON "uc"."OWNER" = "ui"."TABLE_OWNER" AND "uc"."CONSTRAINT_NAME" = "ui"."INDEX_NAME" AND "uc"."CONSTRAINT_TYPE" = 'P'
281
        WHERE "ui"."TABLE_OWNER" = :schemaName AND "ui"."TABLE_NAME" = :tableName
282
        ORDER BY "uicol"."COLUMN_POSITION" ASC
283
        SQL;
284
285
        $resolvedName = $this->resolveTableName($tableName);
286
        $indexes = $this->db->createCommand($sql, [
287
            ':schemaName' => $resolvedName->getSchemaName(),
288
            ':tableName' => $resolvedName->getName(),
289
        ])->queryAll();
290
291
        /** @psalm-var array[] $indexes */
292
        $indexes = array_map('array_change_key_case', $indexes);
293
        $indexes = DbArrayHelper::index($indexes, null, ['name']);
294
295
        $result = [];
296
297
        /**
298
         * @psalm-var object|string|null $name
299
         * @psalm-var array[] $index
300
         */
301
        foreach ($indexes as $name => $index) {
302
            $columnNames = DbArrayHelper::getColumn($index, 'column_name');
303
304
            if ($columnNames[0] === null) {
305
                $columnNames[0] = '';
306
            }
307
308
            $result[] = (new IndexConstraint())
309
                ->primary((bool) $index[0]['index_is_primary'])
310
                ->unique((bool) $index[0]['index_is_unique'])
311
                ->name($name)
312
                ->columnNames($columnNames);
313
        }
314
315
        return $result;
316
    }
317
318
    /**
319
     * @throws Exception
320
     * @throws InvalidConfigException
321
     * @throws NotSupportedException
322
     * @throws Throwable
323
     */
324
    protected function loadTableUniques(string $tableName): array
325
    {
326
        /** @psalm-var mixed $tableUniques */
327
        $tableUniques = $this->loadTableConstraints($tableName, self::UNIQUES);
328
        return is_array($tableUniques) ? $tableUniques : [];
329
    }
330
331
    /**
332
     * @throws Exception
333
     * @throws InvalidConfigException
334
     * @throws NotSupportedException
335
     * @throws Throwable
336
     */
337
    protected function loadTableChecks(string $tableName): array
338
    {
339
        /** @psalm-var mixed $tableCheck */
340
        $tableCheck = $this->loadTableConstraints($tableName, self::CHECKS);
341
        return is_array($tableCheck) ? $tableCheck : [];
342
    }
343
344
    /**
345
     * @throws NotSupportedException If this method is called.
346
     */
347
    protected function loadTableDefaultValues(string $tableName): array
348
    {
349
        throw new NotSupportedException(__METHOD__ . ' is not supported by Oracle.');
350
    }
351
352
    /**
353
     * Collects the table column metadata.
354
     *
355
     * @param TableSchemaInterface $table The table schema.
356
     *
357
     * @throws Exception
358
     * @throws Throwable
359
     *
360
     * @return bool Whether the table exists.
361
     */
362
    protected function findColumns(TableSchemaInterface $table): bool
363
    {
364
        $sql = <<<SQL
365
        SELECT
366
            A.COLUMN_NAME,
367
            A.DATA_TYPE,
368
            A.DATA_PRECISION,
369
            A.DATA_SCALE,
370
            A.IDENTITY_COLUMN,
371
            (
372
            CASE A.CHAR_USED WHEN 'C' THEN A.CHAR_LENGTH
373
                ELSE A.DATA_LENGTH
374
            END
375
            ) AS DATA_LENGTH,
376
            A.NULLABLE,
377
            A.DATA_DEFAULT,
378
            (
379
                SELECT COUNT(*)
380
                FROM ALL_CONSTRAINTS AC
381
                INNER JOIN ALL_CONS_COLUMNS ACC ON ACC.CONSTRAINT_NAME=AC.CONSTRAINT_NAME
382
                WHERE
383
                     AC.OWNER = A.OWNER
384
                   AND AC.TABLE_NAME = B.OBJECT_NAME
385
                   AND ACC.COLUMN_NAME = A.COLUMN_NAME
386
                   AND AC.CONSTRAINT_TYPE = 'P'
387
            ) AS IS_PK,
388
            COM.COMMENTS AS COLUMN_COMMENT
389
        FROM ALL_TAB_COLUMNS A
390
            INNER JOIN ALL_OBJECTS B ON B.OWNER = A.OWNER AND LTRIM(B.OBJECT_NAME) = LTRIM(A.TABLE_NAME)
391
            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)
392
        WHERE
393
            A.OWNER = :schemaName
394
            AND B.OBJECT_TYPE IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW')
395
            AND B.OBJECT_NAME = :tableName
396
        ORDER BY A.COLUMN_ID
397
        SQL;
398
399
        $columns = $this->db->createCommand($sql, [
400
            ':tableName' => $table->getName(),
401
            ':schemaName' => $table->getSchemaName(),
402
        ])->queryAll();
403
404
        if ($columns === []) {
405
            return false;
406
        }
407
408
        /** @psalm-var string[][] $columns */
409
        foreach ($columns as $column) {
410
            /** @psalm-var ColumnInfoArray $column */
411
            $column = array_change_key_case($column);
412
413
            $c = $this->createColumnSchema($column);
414
415
            $table->column($c->getName(), $c);
416
        }
417
418
        return true;
419
    }
420
421
    /**
422
     * Sequence name of table.
423
     *
424
     * @throws Exception
425
     * @throws InvalidConfigException
426
     * @throws Throwable
427
     *
428
     * @return string|null Whether the sequence exists.
429
     *
430
     * @internal TableSchemaInterface `$table->getName()` The table schema.
431
     */
432
    protected function getTableSequenceName(string $tableName): string|null
433
    {
434
        $sequenceNameSql = <<<SQL
435
        SELECT
436
            UD.REFERENCED_NAME AS SEQUENCE_NAME
437
        FROM USER_DEPENDENCIES UD
438
            JOIN USER_TRIGGERS UT ON (UT.TRIGGER_NAME = UD.NAME)
439
        WHERE
440
            UT.TABLE_NAME = :tableName
441
            AND UD.TYPE = 'TRIGGER'
442
            AND UD.REFERENCED_TYPE = 'SEQUENCE'
443
        SQL;
444
        $sequenceName = $this->db->createCommand($sequenceNameSql, [':tableName' => $tableName])->queryScalar();
445
446
        /** @var string|null */
447
        return $sequenceName === false ? null : $sequenceName;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $sequenceName ===... ? null : $sequenceName could return the type true which is incompatible with the type-hinted return null|string. Consider adding an additional type-check to rule them out.
Loading history...
448
    }
449
450
    /**
451
     * Creates ColumnSchema instance.
452
     *
453
     * @psalm-param ColumnInfoArray $info
454
     */
455
    protected function createColumnSchema(array $info): ColumnSchemaInterface
456
    {
457
        $column = new ColumnSchema($info['column_name']);
458
        $column->allowNull($info['nullable'] === 'Y');
459
        $column->comment($info['column_comment']);
460
        $column->primaryKey((bool) $info['is_pk']);
461
        $column->autoIncrement($info['identity_column'] === 'YES');
462
        $column->size((int) $info['data_length']);
463
        $column->precision($info['data_precision'] !== null ? (int) $info['data_precision'] : null);
464
        $column->scale($info['data_scale'] !== null ? (int) $info['data_scale'] : null);
465
        $column->dbType($info['data_type']);
466
        $column->type($this->extractColumnType($column));
467
        $column->phpType($this->getColumnPhpType($column));
468
        $column->defaultValue($this->normalizeDefaultValue($info['data_default'], $column));
469
470
        return $column;
471
    }
472
473
    /**
474
     * Converts column's default value according to {@see ColumnSchema::phpType} after retrieval from the database.
475
     *
476
     * @param string|null $defaultValue The default value retrieved from the database.
477
     * @param ColumnSchemaInterface $column The column schema object.
478
     *
479
     * @return mixed The normalized default value.
480
     */
481
    private function normalizeDefaultValue(string|null $defaultValue, ColumnSchemaInterface $column): mixed
482
    {
483
        if ($defaultValue === null || $column->isPrimaryKey()) {
484
            return null;
485
        }
486
487
        $defaultValue = trim($defaultValue);
488
489
        if ($defaultValue === 'NULL') {
490
            return null;
491
        }
492
493
        if ($column->getType() === self::TYPE_TIMESTAMP && $defaultValue === 'CURRENT_TIMESTAMP') {
494
            return new Expression($defaultValue);
495
        }
496
497
        if (preg_match("/^'(.*)'$/s", $defaultValue, $matches) === 1) {
498
            $defaultValue = str_replace("''", "'", $matches[1]);
499
        }
500
501
        return $column->phpTypecast($defaultValue);
502
    }
503
504
    /**
505
     * Finds constraints and fills them into TableSchemaInterface object passed.
506
     *
507
     * @throws Exception
508
     * @throws InvalidConfigException
509
     * @throws Throwable
510
     *
511
     * @psalm-suppress PossiblyNullArrayOffset
512
     */
513
    protected function findConstraints(TableSchemaInterface $table): void
514
    {
515
        $sql = <<<SQL
516
        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
        /**
537
         * @psalm-var array{
538
         *   array{
539
         *     constraint_name: string,
540
         *     constraint_type: string,
541
         *     column_name: string,
542
         *     position: string|null,
543
         *     r_constraint_name: string|null,
544
         *     table_ref: string|null,
545
         *     column_ref: string|null,
546
         *     table_name: string
547
         *   }
548
         * } $rows
549
         */
550
        $rows = $this->db->createCommand(
551
            $sql,
552
            [':tableName' => $table->getName(), ':schemaName' => $table->getSchemaName()]
553
        )->queryAll();
554
555
        $constraints = [];
556
557
        foreach ($rows as $row) {
558
            /** @psalm-var string[] $row */
559
            $row = array_change_key_case($row);
560
561
            if ($row['constraint_type'] === 'P') {
562
                $table->getColumns()[$row['column_name']]->primaryKey(true);
563
                $table->primaryKey($row['column_name']);
564
565
                if (empty($table->getSequenceName())) {
566
                    $table->sequenceName($this->getTableSequenceName($table->getName()));
567
                }
568
            }
569
570
            if ($row['constraint_type'] !== 'R') {
571
                /**
572
                 * This condition isn't checked in `WHERE` because of an Oracle Bug:
573
                 *
574
                 * @link https://github.com/yiisoft/yii2/pull/8844
575
                 */
576
                continue;
577
            }
578
579
            $name = $row['constraint_name'];
580
581
            if (!isset($constraints[$name])) {
582
                $constraints[$name] = [
583
                    'tableName' => $row['table_ref'],
584
                    'columns' => [],
585
                ];
586
            }
587
588
            $constraints[$name]['columns'][$row['column_name']] = $row['column_ref'];
589
        }
590
591
        foreach ($constraints as $index => $constraint) {
592
            $table->foreignKey($index, array_merge([$constraint['tableName']], $constraint['columns']));
593
        }
594
    }
595
596
    /**
597
     * Returns all unique indexes for the given table.
598
     *
599
     * Each array element is of the following structure:.
600
     *
601
     * ```php
602
     * [
603
     *     'IndexName1' => ['col1' [, ...]],
604
     *     'IndexName2' => ['col2' [, ...]],
605
     * ]
606
     * ```
607
     *
608
     * @param TableSchemaInterface $table The table metadata.
609
     *
610
     * @throws Exception
611
     * @throws InvalidConfigException
612
     * @throws Throwable
613
     *
614
     * @return array All unique indexes for the given table.
615
     */
616
    public function findUniqueIndexes(TableSchemaInterface $table): array
617
    {
618
        $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
        $result = [];
631
632
        $rows = $this->db->createCommand(
633
            $query,
634
            [':tableName' => $table->getName(), ':schemaName' => $table->getschemaName()]
635
        )->queryAll();
636
637
        /** @psalm-var array<array{INDEX_NAME: string, COLUMN_NAME: string}> $rows */
638
        foreach ($rows as $row) {
639
            $result[$row['INDEX_NAME']][] = $row['COLUMN_NAME'];
640
        }
641
642
        return $result;
643
    }
644
645
    /**
646
     * Extracts the data type for the given column.
647
     *
648
     * @param ColumnSchemaInterface $column The column schema object.
649
     *
650
     * @return string The abstract column type.
651
     */
652
    private function extractColumnType(ColumnSchemaInterface $column): string
653
    {
654
        $dbType = strtolower((string) $column->getDbType());
655
656
        if ($dbType === 'number') {
657
            return match ($column->getScale()) {
658
                null => self::TYPE_DOUBLE,
659
                0 => self::TYPE_INTEGER,
660
                default => self::TYPE_DECIMAL,
661
            };
662
        }
663
664
        $dbType = preg_replace('/\([^)]+\)/', '', $dbType);
665
666
        if ($dbType === 'interval day to second' && $column->getPrecision() > 0) {
667
            return self::TYPE_STRING;
668
        }
669
670
        return self::TYPE_MAP[$dbType] ?? self::TYPE_STRING;
671
    }
672
673
    /**
674
     * Loads multiple types of constraints and returns the specified ones.
675
     *
676
     * @param string $tableName The table name.
677
     * @param string $returnType The return type:
678
     * - primaryKey
679
     * - foreignKeys
680
     * - uniques
681
     * - checks
682
     *
683
     * @throws Exception
684
     * @throws InvalidConfigException
685
     * @throws NotSupportedException
686
     * @throws Throwable
687
     *
688
     * @return mixed Constraints.
689
     */
690
    private function loadTableConstraints(string $tableName, string $returnType): mixed
691
    {
692
        $sql = <<<SQL
693
        SELECT
694
            "uc"."CONSTRAINT_NAME" AS "name",
695
            "uccol"."COLUMN_NAME" AS "column_name",
696
            "uc"."CONSTRAINT_TYPE" AS "type",
697
            "fuc"."OWNER" AS "foreign_table_schema",
698
            "fuc"."TABLE_NAME" AS "foreign_table_name",
699
            "fuccol"."COLUMN_NAME" AS "foreign_column_name",
700
            "uc"."DELETE_RULE" AS "on_delete",
701
            "uc"."SEARCH_CONDITION" AS "check_expr"
702
        FROM "USER_CONSTRAINTS" "uc"
703
        INNER JOIN "USER_CONS_COLUMNS" "uccol"
704
        ON "uccol"."OWNER" = "uc"."OWNER" AND "uccol"."CONSTRAINT_NAME" = "uc"."CONSTRAINT_NAME"
705
        LEFT JOIN "USER_CONSTRAINTS" "fuc"
706
        ON "fuc"."OWNER" = "uc"."R_OWNER" AND "fuc"."CONSTRAINT_NAME" = "uc"."R_CONSTRAINT_NAME"
707
        LEFT JOIN "USER_CONS_COLUMNS" "fuccol"
708
        ON "fuccol"."OWNER" = "fuc"."OWNER" AND "fuccol"."CONSTRAINT_NAME" = "fuc"."CONSTRAINT_NAME" AND "fuccol"."POSITION" = "uccol"."POSITION"
709
        WHERE "uc"."OWNER" = :schemaName AND "uc"."TABLE_NAME" = :tableName
710
        ORDER BY "uccol"."POSITION" ASC
711
        SQL;
712
713
        $resolvedName = $this->resolveTableName($tableName);
714
        $constraints = $this->db->createCommand($sql, [
715
            ':schemaName' => $resolvedName->getSchemaName(),
716
            ':tableName' => $resolvedName->getName(),
717
        ])->queryAll();
718
719
        /** @psalm-var array[] $constraints */
720
        $constraints = array_map('array_change_key_case', $constraints);
721
        $constraints = DbArrayHelper::index($constraints, null, ['type', 'name']);
722
723
        $result = [
724
            self::PRIMARY_KEY => null,
725
            self::FOREIGN_KEYS => [],
726
            self::UNIQUES => [],
727
            self::CHECKS => [],
728
        ];
729
730
        /**
731
         * @psalm-var string $type
732
         * @psalm-var array $names
733
         */
734
        foreach ($constraints as $type => $names) {
735
            /**
736
             * @psalm-var object|string|null $name
737
             * @psalm-var ConstraintArray $constraint
738
             */
739
            foreach ($names as $name => $constraint) {
740
                switch ($type) {
741
                    case 'P':
742
                        $result[self::PRIMARY_KEY] = (new Constraint())
743
                            ->name($name)
744
                            ->columnNames(DbArrayHelper::getColumn($constraint, 'column_name'));
745
                        break;
746
                    case 'R':
747
                        $result[self::FOREIGN_KEYS][] = (new ForeignKeyConstraint())
748
                            ->name($name)
749
                            ->columnNames(DbArrayHelper::getColumn($constraint, 'column_name'))
750
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
751
                            ->foreignTableName($constraint[0]['foreign_table_name'])
752
                            ->foreignColumnNames(DbArrayHelper::getColumn($constraint, 'foreign_column_name'))
753
                            ->onDelete($constraint[0]['on_delete'])
754
                            ->onUpdate(null);
755
                        break;
756
                    case 'U':
757
                        $result[self::UNIQUES][] = (new Constraint())
758
                            ->name($name)
759
                            ->columnNames(DbArrayHelper::getColumn($constraint, 'column_name'));
760
                        break;
761
                    case 'C':
762
                        $result[self::CHECKS][] = (new CheckConstraint())
763
                            ->name($name)
764
                            ->columnNames(DbArrayHelper::getColumn($constraint, 'column_name'))
765
                            ->expression($constraint[0]['check_expr']);
766
                        break;
767
                }
768
            }
769
        }
770
771
        foreach ($result as $type => $data) {
772
            $this->setTableMetadata($tableName, $type, $data);
773
        }
774
775
        return $result[$returnType];
776
    }
777
778
    /**
779
     * @throws Exception
780
     * @throws InvalidConfigException
781
     * @throws Throwable
782
     */
783
    protected function findViewNames(string $schema = ''): array
784
    {
785
        $sql = match ($schema) {
786
            '' => <<<SQL
787
            SELECT VIEW_NAME FROM USER_VIEWS
788
            SQL,
789
            default => <<<SQL
790
            SELECT VIEW_NAME FROM ALL_VIEWS WHERE OWNER = '$schema'
791
            SQL,
792
        };
793
794
        /** @psalm-var string[][] $views */
795
        $views = $this->db->createCommand($sql)->queryAll();
796
797
        foreach ($views as $key => $view) {
798
            $views[$key] = $view['VIEW_NAME'];
799
        }
800
801
        return $views;
802
    }
803
804
    /**
805
     * Returns the cache key for the specified table name.
806
     *
807
     * @param string $name The table name.
808
     *
809
     * @return array The cache key.
810
     *
811
     * @psalm-suppress DeprecatedMethod
812
     */
813
    protected function getCacheKey(string $name): array
814
    {
815
        return array_merge([self::class], $this->generateCacheKey(), [$this->getRawTableName($name)]);
0 ignored issues
show
Deprecated Code introduced by
The function Yiisoft\Db\Schema\Abstra...hema::getRawTableName() has been deprecated: Use {@see Quoter::getRawTableName()}. Will be removed in version 2.0.0. ( Ignorable by Annotation )

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

815
        return array_merge([self::class], $this->generateCacheKey(), [/** @scrutinizer ignore-deprecated */ $this->getRawTableName($name)]);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
816
    }
817
818
    /**
819
     * Returns the cache tag name.
820
     *
821
     * This allows {@see refresh()} to invalidate all cached table schemas.
822
     *
823
     * @return string The cache tag name.
824
     */
825
    protected function getCacheTag(): string
826
    {
827
        return md5(serialize(array_merge([self::class], $this->generateCacheKey())));
828
    }
829
}
830