Passed
Push — dev ( 4e189c...34f311 )
by Def
17:31 queued 14:44
created

SchemaPDOPgsql::findColumns()   D

Complexity

Conditions 23
Paths 96

Size

Total Lines 158
Code Lines 70

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 47
CRAP Score 23.0359

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 23
eloc 70
c 1
b 0
f 0
nc 96
nop 1
dl 0
loc 158
ccs 47
cts 49
cp 0.9592
crap 23.0359
rs 4.1666

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\Pgsql\PDO;
6
7
use JsonException;
8
use PDO;
9
use Throwable;
10
use Yiisoft\Arrays\ArrayHelper;
11
use Yiisoft\Db\Cache\SchemaCache;
12
use Yiisoft\Db\Connection\ConnectionPDOInterface;
13
use Yiisoft\Db\Constraint\CheckConstraint;
14
use Yiisoft\Db\Constraint\Constraint;
15
use Yiisoft\Db\Constraint\DefaultValueConstraint;
16
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
17
use Yiisoft\Db\Constraint\IndexConstraint;
18
use Yiisoft\Db\Exception\Exception;
19
use Yiisoft\Db\Exception\InvalidCallException;
20
use Yiisoft\Db\Exception\InvalidConfigException;
21
use Yiisoft\Db\Exception\NotSupportedException;
22
use Yiisoft\Db\Expression\Expression;
23
use Yiisoft\Db\Pgsql\ColumnSchema;
24
use Yiisoft\Db\Pgsql\TableSchema;
25
use Yiisoft\Db\Schema\ColumnSchemaBuilder;
26
use Yiisoft\Db\Schema\Schema;
27
use Yiisoft\Db\View\ViewInterface;
28
29
use function array_change_key_case;
30
use function array_merge;
31
use function array_unique;
32
use function array_values;
33
use function bindec;
34
use function explode;
35
use function preg_match;
36
use function preg_replace;
37
use function str_replace;
38
use function substr;
39
40
/**
41
 * The class Schema is the class for retrieving metadata from a PostgreSQL database
42
 * (version 9.6 and above).
43
 *
44
 * @psalm-type ColumnArray = array{
45
 *   table_schema: string,
46
 *   table_name: string,
47
 *   column_name: string,
48
 *   data_type: string,
49
 *   type_type: string|null,
50
 *   character_maximum_length: int,
51
 *   column_comment: string|null,
52
 *   modifier: int,
53
 *   is_nullable: bool,
54
 *   column_default: mixed,
55
 *   is_autoinc: bool,
56
 *   sequence_name: string|null,
57
 *   enum_values: array<array-key, float|int|string>|string|null,
58
 *   numeric_precision: int|null,
59
 *   numeric_scale: int|null,
60
 *   size: string|null,
61
 *   is_pkey: bool|null,
62
 *   dimension: int
63
 * }
64
 *
65
 * @psalm-type ConstraintArray = array<
66
 *   array-key,
67
 *   array {
68
 *     name: string,
69
 *     column_name: string,
70
 *     type: string,
71
 *     foreign_table_schema: string|null,
72
 *     foreign_table_name: string|null,
73
 *     foreign_column_name: string|null,
74
 *     on_update: string,
75
 *     on_delete: string,
76
 *     check_expr: string
77
 *   }
78
 * >
79
 *
80
 * @psalm-type FindConstraintArray = array{
81
 *   constraint_name: string,
82
 *   column_name: string,
83
 *   foreign_table_name: string,
84
 *   foreign_table_schema: string,
85
 *   foreign_column_name: string,
86
 * }
87
 */
88
final class SchemaPDOPgsql extends Schema implements ViewInterface
89
{
90
    public const TYPE_JSONB = 'jsonb';
91
92
    /**
93
     * @var array The mapping from physical column types (keys) to abstract column types (values).
94
     *
95
     * {@see http://www.postgresql.org/docs/current/static/datatype.html#DATATYPE-TABLE}
96
     *
97
     * @psalm-var string[]
98
     */
99
    private array $typeMap = [
100
        'bit' => self::TYPE_INTEGER,
101
        'bit varying' => self::TYPE_INTEGER,
102
        'varbit' => self::TYPE_INTEGER,
103
        'bool' => self::TYPE_BOOLEAN,
104
        'boolean' => self::TYPE_BOOLEAN,
105
        'box' => self::TYPE_STRING,
106
        'circle' => self::TYPE_STRING,
107
        'point' => self::TYPE_STRING,
108
        'line' => self::TYPE_STRING,
109
        'lseg' => self::TYPE_STRING,
110
        'polygon' => self::TYPE_STRING,
111
        'path' => self::TYPE_STRING,
112
        'character' => self::TYPE_CHAR,
113
        'char' => self::TYPE_CHAR,
114
        'bpchar' => self::TYPE_CHAR,
115
        'character varying' => self::TYPE_STRING,
116
        'varchar' => self::TYPE_STRING,
117
        'text' => self::TYPE_TEXT,
118
        'bytea' => self::TYPE_BINARY,
119
        'cidr' => self::TYPE_STRING,
120
        'inet' => self::TYPE_STRING,
121
        'macaddr' => self::TYPE_STRING,
122
        'real' => self::TYPE_FLOAT,
123
        'float4' => self::TYPE_FLOAT,
124
        'double precision' => self::TYPE_DOUBLE,
125
        'float8' => self::TYPE_DOUBLE,
126
        'decimal' => self::TYPE_DECIMAL,
127
        'numeric' => self::TYPE_DECIMAL,
128
        'money' => self::TYPE_MONEY,
129
        'smallint' => self::TYPE_SMALLINT,
130
        'int2' => self::TYPE_SMALLINT,
131
        'int4' => self::TYPE_INTEGER,
132
        'int' => self::TYPE_INTEGER,
133
        'integer' => self::TYPE_INTEGER,
134
        'bigint' => self::TYPE_BIGINT,
135
        'int8' => self::TYPE_BIGINT,
136
        'oid' => self::TYPE_BIGINT, // should not be used. it's pg internal!
137
        'smallserial' => self::TYPE_SMALLINT,
138
        'serial2' => self::TYPE_SMALLINT,
139
        'serial4' => self::TYPE_INTEGER,
140
        'serial' => self::TYPE_INTEGER,
141
        'bigserial' => self::TYPE_BIGINT,
142
        'serial8' => self::TYPE_BIGINT,
143
        'pg_lsn' => self::TYPE_BIGINT,
144
        'date' => self::TYPE_DATE,
145
        'interval' => self::TYPE_STRING,
146
        'time without time zone' => self::TYPE_TIME,
147
        'time' => self::TYPE_TIME,
148
        'time with time zone' => self::TYPE_TIME,
149
        'timetz' => self::TYPE_TIME,
150
        'timestamp without time zone' => self::TYPE_TIMESTAMP,
151
        'timestamp' => self::TYPE_TIMESTAMP,
152
        'timestamp with time zone' => self::TYPE_TIMESTAMP,
153
        'timestamptz' => self::TYPE_TIMESTAMP,
154
        'abstime' => self::TYPE_TIMESTAMP,
155
        'tsquery' => self::TYPE_STRING,
156
        'tsvector' => self::TYPE_STRING,
157
        'txid_snapshot' => self::TYPE_STRING,
158
        'unknown' => self::TYPE_STRING,
159
        'uuid' => self::TYPE_STRING,
160
        'json' => self::TYPE_JSON,
161
        'jsonb' => self::TYPE_JSON,
162
        'xml' => self::TYPE_STRING,
163
    ];
164
165
    private array $viewNames = [];
166
167 420
    public function __construct(private ConnectionPDOInterface $db, SchemaCache $schemaCache)
168
    {
169 420
        parent::__construct($schemaCache);
170
    }
171
172
    /**
173
     * @var string|null the default schema used for the current session.
174
     */
175
    protected ?string $defaultSchema = 'public';
176
177
    /**
178
     * @var string|string[] character used to quote schema, table, etc. names. An array of 2 characters can be used in
179
     * case starting and ending characters are different.
180
     */
181
    protected string|array $tableQuoteCharacter = '"';
182
183
    /**
184
     * Resolves the table name and schema name (if any).
185
     *
186
     * @param string $name the table name.
187
     *
188
     * @return TableSchema with resolved table, schema, etc. names.
189
     *
190
     * {@see TableSchema}
191
     */
192 76
    protected function resolveTableName(string $name): TableSchema
193
    {
194 76
        $resolvedName = new TableSchema();
195
196 76
        $parts = explode('.', str_replace('"', '', $name));
197
198 76
        if (isset($parts[1])) {
199 5
            $resolvedName->schemaName($parts[0]);
200 5
            $resolvedName->name($parts[1]);
201
        } else {
202 71
            $resolvedName->schemaName($this->defaultSchema);
203 71
            $resolvedName->name($name);
204
        }
205
206 76
        $resolvedName->fullName(
207
            (
208 76
                $resolvedName->getSchemaName() !== $this->defaultSchema ?
209
                    (string) $resolvedName->getSchemaName() . '.' :
210 76
                    ''
211 76
            ) . $resolvedName->getName()
212
        );
213
214 76
        return $resolvedName;
215
    }
216
217
    /**
218
     * Returns all schema names in the database, including the default one but not system schemas.
219
     *
220
     * This method should be overridden by child classes in order to support this feature because the default
221
     * implementation simply throws an exception.
222
     *
223
     * @throws Exception|InvalidConfigException|Throwable
224
     *
225
     * @return array all schema names in the database, except system schemas.
226
     */
227 2
    protected function findSchemaNames(): array
228
    {
229 2
        $sql = <<<SQL
230
        SELECT "ns"."nspname"
231
        FROM "pg_namespace" AS "ns"
232
        WHERE "ns"."nspname" != 'information_schema' AND "ns"."nspname" NOT LIKE 'pg_%'
233
        ORDER BY "ns"."nspname" ASC
234
        SQL;
235
236 2
        $schemaNames = $this->db->createCommand($sql)->queryColumn();
237 2
        if (!$schemaNames) {
238
            return [];
239
        }
240
241 2
        return $schemaNames;
242
    }
243
244
    /**
245
     * Returns all table names in the database.
246
     *
247
     * This method should be overridden by child classes in order to support this feature because the default
248
     * implementation simply throws an exception.
249
     *
250
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
251
     *
252
     * @throws Exception|InvalidConfigException|Throwable
253
     *
254
     * @return array all table names in the database. The names have NO schema name prefix.
255
     */
256 5
    protected function findTableNames(string $schema = ''): array
257
    {
258 5
        if ($schema === '') {
259 5
            $schema = $this->defaultSchema;
260
        }
261
262 5
        $sql = <<<SQL
263
        SELECT c.relname AS table_name
264
        FROM pg_class c
265
        INNER JOIN pg_namespace ns ON ns.oid = c.relnamespace
266
        WHERE ns.nspname = :schemaName AND c.relkind IN ('r','v','m','f', 'p')
267
        ORDER BY c.relname
268
        SQL;
269
270 5
        $tableNames = $this->db->createCommand($sql, [':schemaName' => $schema])->queryColumn();
271 5
        if (!$tableNames) {
272
            return [];
273
        }
274
275 5
        return $tableNames;
276
    }
277
278
    /**
279
     * Loads the metadata for the specified table.
280
     *
281
     * @param string $name table name.
282
     *
283
     * @throws Exception|InvalidConfigException|Throwable
284
     *
285
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
286
     */
287 104
    protected function loadTableSchema(string $name): ?TableSchema
288
    {
289 104
        $table = new TableSchema();
290
291 104
        $this->resolveTableNames($table, $name);
292
293 104
        if ($this->findColumns($table)) {
294 98
            $this->findConstraints($table);
295 98
            return $table;
296
        }
297
298 17
        return null;
299
    }
300
301
    /**
302
     * Loads a primary key for the given table.
303
     *
304
     * @param string $tableName table name.
305
     *
306
     * @throws Exception|InvalidConfigException|Throwable
307
     *
308
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.
309
     */
310 31
    protected function loadTablePrimaryKey(string $tableName): ?Constraint
311
    {
312 31
        $tablePrimaryKey = $this->loadTableConstraints($tableName, 'primaryKey');
313
314 31
        return $tablePrimaryKey instanceof Constraint ? $tablePrimaryKey : null;
315
    }
316
317
    /**
318
     * Loads all foreign keys for the given table.
319
     *
320
     * @param string $tableName table name.
321
     *
322
     * @throws Exception|InvalidConfigException|Throwable
323
     *
324
     * @return array foreign keys for the given table.
325
     *
326
     * @psaml-return array|ForeignKeyConstraint[]
327
     */
328 4
    protected function loadTableForeignKeys(string $tableName): array
329
    {
330 4
        $tableForeignKeys = $this->loadTableConstraints($tableName, 'foreignKeys');
331
332 4
        return is_array($tableForeignKeys) ? $tableForeignKeys : [];
333
    }
334
335
    /**
336
     * Loads all indexes for the given table.
337
     *
338
     * @param string $tableName table name.
339
     *
340
     * @throws Exception|InvalidConfigException|Throwable
341
     *
342
     * @return IndexConstraint[] indexes for the given table.
343
     */
344 28
    protected function loadTableIndexes(string $tableName): array
345
    {
346 28
        $sql = <<<SQL
347
        SELECT
348
            "ic"."relname" AS "name",
349
            "ia"."attname" AS "column_name",
350
            "i"."indisunique" AS "index_is_unique",
351
            "i"."indisprimary" AS "index_is_primary"
352
        FROM "pg_class" AS "tc"
353
        INNER JOIN "pg_namespace" AS "tcns"
354
            ON "tcns"."oid" = "tc"."relnamespace"
355
        INNER JOIN "pg_index" AS "i"
356
            ON "i"."indrelid" = "tc"."oid"
357
        INNER JOIN "pg_class" AS "ic"
358
            ON "ic"."oid" = "i"."indexrelid"
359
        INNER JOIN "pg_attribute" AS "ia"
360
            ON "ia"."attrelid" = "i"."indexrelid"
361
        WHERE "tcns"."nspname" = :schemaName AND "tc"."relname" = :tableName
362
        ORDER BY "ia"."attnum" ASC
363
        SQL;
364
365 28
        $resolvedName = $this->resolveTableName($tableName);
366
367 28
        $indexes = $this->db->createCommand($sql, [
368 28
            ':schemaName' => $resolvedName->getSchemaName(),
369 28
            ':tableName' => $resolvedName->getName(),
370 28
        ])->queryAll();
371
372
        /** @var array[] @indexes */
373 28
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
374 28
        $indexes = ArrayHelper::index($indexes, null, 'name');
375 28
        $result = [];
376
377
        /**
378
         * @var object|string|null $name
379
         * @var array<
380
         *   array-key,
381
         *   array{
382
         *     name: string,
383
         *     column_name: string,
384
         *     index_is_unique: bool,
385
         *     index_is_primary: bool
386
         *   }
387
         * > $index
388
         */
389 28
        foreach ($indexes as $name => $index) {
390 25
            $ic = (new IndexConstraint())
391 25
                ->name($name)
392 25
                ->columnNames(ArrayHelper::getColumn($index, 'column_name'))
393 25
                ->primary($index[0]['index_is_primary'])
394 25
                ->unique($index[0]['index_is_unique']);
395
396 25
            $result[] = $ic;
397
        }
398
399 28
        return $result;
400
    }
401
402
    /**
403
     * Loads all unique constraints for the given table.
404
     *
405
     * @param string $tableName table name.
406
     *
407
     * @throws Exception|InvalidConfigException|Throwable
408
     *
409
     * @return array unique constraints for the given table.
410
     *
411
     * @psalm-return array|Constraint[]
412
     */
413 13
    protected function loadTableUniques(string $tableName): array
414
    {
415 13
        $tableUniques = $this->loadTableConstraints($tableName, 'uniques');
416
417 13
        return is_array($tableUniques) ? $tableUniques : [];
418
    }
419
420
    /**
421
     * Loads all check constraints for the given table.
422
     *
423
     * @param string $tableName table name.
424
     *
425
     * @throws Exception|InvalidConfigException|Throwable
426
     *
427
     * @return array check constraints for the given table.
428
     *
429
     * @psaml-return array|CheckConstraint[]
430
     */
431 13
    protected function loadTableChecks(string $tableName): array
432
    {
433 13
        $tableChecks = $this->loadTableConstraints($tableName, 'checks');
434
435 13
        return is_array($tableChecks) ? $tableChecks : [];
436
    }
437
438
    /**
439
     * Loads all default value constraints for the given table.
440
     *
441
     * @param string $tableName table name.
442
     *
443
     * @throws NotSupportedException
444
     *
445
     * @return DefaultValueConstraint[] default value constraints for the given table.
446
     */
447 12
    protected function loadTableDefaultValues(string $tableName): array
448
    {
449 12
        throw new NotSupportedException('PostgreSQL does not support default value constraints.');
450
    }
451
452
    /**
453
     * Resolves the table name and schema name (if any).
454
     *
455
     * @param TableSchema $table the table metadata object.
456
     * @param string $name the table name
457
     */
458 104
    protected function resolveTableNames(TableSchema $table, string $name): void
459
    {
460 104
        $parts = explode('.', str_replace('"', '', $name));
461
462 104
        if (isset($parts[1])) {
463
            $table->schemaName($parts[0]);
464
            $table->name($parts[1]);
465
        } else {
466 104
            $table->schemaName($this->defaultSchema);
467 104
            $table->name($parts[0]);
468
        }
469
470 104
        if ($table->getSchemaName() !== $this->defaultSchema) {
471
            $name = (string) $table->getSchemaName() . '.' . $table->getName();
472
        } else {
473 104
            $name = $table->getName();
474
        }
475
476 104
        $table->fullName($name);
477
    }
478
479
    /**
480
     * @throws Exception|InvalidConfigException|Throwable
481
     */
482 1
    public function findViewNames(string $schema = ''): array
483
    {
484 1
        if ($schema === '') {
485
            $schema = $this->defaultSchema;
486
        }
487
488 1
        $sql = <<<SQL
489
        SELECT c.relname AS table_name
490
        FROM pg_class c
491
        INNER JOIN pg_namespace ns ON ns.oid = c.relnamespace
492
        WHERE ns.nspname = :schemaName AND (c.relkind = 'v' OR c.relkind = 'm')
493
        ORDER BY c.relname
494
        SQL;
495
496 1
        $viewNames = $this->db->createCommand($sql, [':schemaName' => $schema])->queryColumn();
497 1
        if (!$viewNames) {
498
            return [];
499
        }
500
501 1
        return $viewNames;
502
    }
503
504
    /**
505
     * Collects the foreign key column details for the given table.
506
     *
507
     * @param TableSchema $table the table metadata
508
     *
509
     * @throws Exception|InvalidConfigException|Throwable
510
     */
511 98
    protected function findConstraints(TableSchema $table): void
512
    {
513 98
        $tableName = $table->getName();
514 98
        $tableSchema = $table->getSchemaName();
515
516
        /** @var mixed */
517 98
        $tableName = $this->db->getQuoter()->quoteValue($tableName);
518
519 98
        if ($tableSchema !== null) {
520
            /** @var mixed */
521 98
            $tableSchema = $this->db->getQuoter()->quoteValue($tableSchema);
522
        }
523
524
        /**
525
         * We need to extract the constraints de hard way since:
526
         * {@see http://www.postgresql.org/message-id/[email protected]}
527
         */
528
529 98
        $sql = <<<SQL
530
        SELECT
531
            ct.conname as constraint_name,
532
            a.attname as column_name,
533
            fc.relname as foreign_table_name,
534
            fns.nspname as foreign_table_schema,
535
            fa.attname as foreign_column_name
536
            FROM
537
            (SELECT ct.conname, ct.conrelid, ct.confrelid, ct.conkey, ct.contype, ct.confkey,
538
                generate_subscripts(ct.conkey, 1) AS s
539
                FROM pg_constraint ct
540
            ) AS ct
541
            inner join pg_class c on c.oid=ct.conrelid
542
            inner join pg_namespace ns on c.relnamespace=ns.oid
543
            inner join pg_attribute a on a.attrelid=ct.conrelid and a.attnum = ct.conkey[ct.s]
544
            left join pg_class fc on fc.oid=ct.confrelid
545
            left join pg_namespace fns on fc.relnamespace=fns.oid
546
            left join pg_attribute fa on fa.attrelid=ct.confrelid and fa.attnum = ct.confkey[ct.s]
547
        WHERE
548
            ct.contype='f'
549
            and c.relname=$tableName
550
            and ns.nspname=$tableSchema
551
        ORDER BY
552
            fns.nspname, fc.relname, a.attnum
553
        SQL;
554
555
        /** @var array{array{tableName: string, columns: array}} $constraints */
556 98
        $constraints = [];
557
558 98
        $pdo = $this->db->getActivePDO();
559
560
        /**
561
         * @psalm-var array<
562
         *   array{
563
         *     constraint_name: string,
564
         *     column_name: string,
565
         *     foreign_table_name: string,
566
         *     foreign_table_schema: string,
567
         *     foreign_column_name: string,
568
         *   }
569
         * > $rows
570
         */
571 98
        $rows = $this->db->createCommand($sql)->queryAll();
572
573 98
        foreach ($rows as $constraint) {
574 9
            if ($pdo !== null && $pdo->getAttribute(PDO::ATTR_CASE) === PDO::CASE_UPPER) {
575
                $constraint = array_change_key_case($constraint, CASE_LOWER);
576
            }
577
578 9
            if ($constraint['foreign_table_schema'] !== $this->defaultSchema) {
579
                $foreignTable = $constraint['foreign_table_schema'] . '.' . $constraint['foreign_table_name'];
580
            } else {
581 9
                $foreignTable = $constraint['foreign_table_name'];
582
            }
583
584 9
            $name = $constraint['constraint_name'];
585
586 9
            if (!isset($constraints[$name])) {
587 9
                $constraints[$name] = [
588
                    'tableName' => $foreignTable,
589
                    'columns' => [],
590
                ];
591
            }
592
593 9
            $constraints[$name]['columns'][$constraint['column_name']] = $constraint['foreign_column_name'];
594
        }
595
596
        /**
597
         * @var int|string $foreingKeyName.
598
         * @var array{tableName: string, columns: array} $constraint
599
         */
600 98
        foreach ($constraints as $foreingKeyName => $constraint) {
601 9
            $table->foreignKey(
602 9
                (string) $foreingKeyName,
603 9
                array_merge([$constraint['tableName']], $constraint['columns'])
604
            );
605
        }
606
    }
607
608
    /**
609
     * Gets information about given table unique indexes.
610
     *
611
     * @param TableSchema $table the table metadata.
612
     *
613
     * @throws Exception|InvalidConfigException|Throwable
614
     *
615
     * @return array with index and column names.
616
     */
617 1
    protected function getUniqueIndexInformation(TableSchema $table): array
618
    {
619 1
        $sql = <<<'SQL'
620
        SELECT
621
            i.relname as indexname,
622
            pg_get_indexdef(idx.indexrelid, k + 1, TRUE) AS columnname
623
        FROM (
624
            SELECT *, generate_subscripts(indkey, 1) AS k
625
            FROM pg_index
626
        ) idx
627
        INNER JOIN pg_class i ON i.oid = idx.indexrelid
628
        INNER JOIN pg_class c ON c.oid = idx.indrelid
629
        INNER JOIN pg_namespace ns ON c.relnamespace = ns.oid
630
        WHERE idx.indisprimary = FALSE AND idx.indisunique = TRUE
631
        AND c.relname = :tableName AND ns.nspname = :schemaName
632
        ORDER BY i.relname, k
633
        SQL;
634
635 1
        return $this->db->createCommand($sql, [
636 1
            ':schemaName' => $table->getSchemaName(),
637 1
            ':tableName' => $table->getName(),
638 1
        ])->queryAll();
639
    }
640
641
    /**
642
     * Returns all unique indexes for the given table.
643
     *
644
     * Each array element is of the following structure:
645
     *
646
     * ```php
647
     * [
648
     *     'IndexName1' => ['col1' [, ...]],
649
     *     'IndexName2' => ['col2' [, ...]],
650
     * ]
651
     * ```
652
     *
653
     * @param TableSchema $table the table metadata
654
     *
655
     * @throws Exception|InvalidConfigException|Throwable
656
     *
657
     * @return array all unique indexes for the given table.
658
     */
659 1
    public function findUniqueIndexes(TableSchema $table): array
660
    {
661 1
        $uniqueIndexes = [];
662 1
        $pdo = $this->db->getActivePDO();
663
664
        /** @var array{indexname: string, columnname: string} $row */
665 1
        foreach ($this->getUniqueIndexInformation($table) as $row) {
666 1
            if ($pdo !== null && $pdo->getAttribute(PDO::ATTR_CASE) === PDO::CASE_UPPER) {
667 1
                $row = array_change_key_case($row, CASE_LOWER);
668
            }
669
670 1
            $column = $row['columnname'];
671
672 1
            if (!empty($column) && $column[0] === '"') {
673
                /**
674
                 * postgres will quote names that are not lowercase-only.
675
                 *
676
                 * {@see https://github.com/yiisoft/yii2/issues/10613}
677
                 */
678 1
                $column = substr($column, 1, -1);
679
            }
680
681 1
            $uniqueIndexes[$row['indexname']][] = $column;
682
        }
683
684 1
        return $uniqueIndexes;
685
    }
686
687
    /**
688
     * Collects the metadata of table columns.
689
     *
690
     * @param TableSchema $table the table metadata.
691
     *
692
     * @throws Exception|InvalidConfigException|JsonException|Throwable
693
     *
694
     * @return bool whether the table exists in the database.
695
     */
696 104
    protected function findColumns(TableSchema $table): bool
697
    {
698 104
        $tableName = $table->getName();
699 104
        $schemaName = $table->getSchemaName();
700 104
        $orIdentity = '';
701
702
        /** @var mixed */
703 104
        $tableName = $this->db->getQuoter()->quoteValue($tableName);
704
705 104
        if ($schemaName !== null) {
706
            /** @var mixed */
707 104
            $schemaName = $this->db->getQuoter()->quoteValue($schemaName);
708
        }
709
710 104
        if (version_compare($this->db->getServerVersion(), '12.0', '>=')) {
711 104
            $orIdentity = 'OR a.attidentity != \'\'';
712
        }
713
714 104
        $sql = <<<SQL
715
        SELECT
716
            d.nspname AS table_schema,
717
            c.relname AS table_name,
718
            a.attname AS column_name,
719
            COALESCE(td.typname, tb.typname, t.typname) AS data_type,
720
            COALESCE(td.typtype, tb.typtype, t.typtype) AS type_type,
721
            a.attlen AS character_maximum_length,
722
            pg_catalog.col_description(c.oid, a.attnum) AS column_comment,
723
            a.atttypmod AS modifier,
724
            a.attnotnull = false AS is_nullable,
725
            CAST(pg_get_expr(ad.adbin, ad.adrelid) AS varchar) AS column_default,
726
            coalesce(pg_get_expr(ad.adbin, ad.adrelid) ~ 'nextval',false) $orIdentity AS is_autoinc,
727
            pg_get_serial_sequence(quote_ident(d.nspname) || '.' || quote_ident(c.relname), a.attname)
728
            AS sequence_name,
729
            CASE WHEN COALESCE(td.typtype, tb.typtype, t.typtype) = 'e'::char
730
                THEN array_to_string(
731
                    (
732
                        SELECT array_agg(enumlabel)
733
                        FROM pg_enum
734
                        WHERE enumtypid = COALESCE(td.oid, tb.oid, a.atttypid)
735
                    )::varchar[],
736
                ',')
737
                ELSE NULL
738
            END AS enum_values,
739
            CASE atttypid
740
                WHEN 21 /*int2*/ THEN 16
741
                WHEN 23 /*int4*/ THEN 32
742
                WHEN 20 /*int8*/ THEN 64
743
                WHEN 1700 /*numeric*/ THEN
744
                    CASE WHEN atttypmod = -1
745
                        THEN null
746
                        ELSE ((atttypmod - 4) >> 16) & 65535
747
                        END
748
                WHEN 700 /*float4*/ THEN 24 /*FLT_MANT_DIG*/
749
                WHEN 701 /*float8*/ THEN 53 /*DBL_MANT_DIG*/
750
                    ELSE null
751
                    END   AS numeric_precision,
752
            CASE
753
                WHEN atttypid IN (21, 23, 20) THEN 0
754
                WHEN atttypid IN (1700) THEN
755
            CASE
756
                WHEN atttypmod = -1 THEN null
757
                    ELSE (atttypmod - 4) & 65535
758
                    END
759
                    ELSE null
760
                    END AS numeric_scale,
761
                    CAST(
762
                        information_schema._pg_char_max_length(
763
                        information_schema._pg_truetypid(a, t),
764
                        information_schema._pg_truetypmod(a, t)
765
                        ) AS numeric
766
                    ) AS size,
767
                    a.attnum = any (ct.conkey) as is_pkey,
768
                    COALESCE(NULLIF(a.attndims, 0), NULLIF(t.typndims, 0), (t.typcategory='A')::int) AS dimension
769
            FROM
770
                pg_class c
771
                LEFT JOIN pg_attribute a ON a.attrelid = c.oid
772
                LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
773
                LEFT JOIN pg_type t ON a.atttypid = t.oid
774
                LEFT JOIN pg_type tb ON (a.attndims > 0 OR t.typcategory='A') AND t.typelem > 0 AND t.typelem = tb.oid
775
                                            OR t.typbasetype > 0 AND t.typbasetype = tb.oid
776
                LEFT JOIN pg_type td ON t.typndims > 0 AND t.typbasetype > 0 AND tb.typelem = td.oid
777
                LEFT JOIN pg_namespace d ON d.oid = c.relnamespace
778
                LEFT JOIN pg_constraint ct ON ct.conrelid = c.oid AND ct.contype = 'p'
779
            WHERE
780
                a.attnum > 0 AND t.typname != '' AND NOT a.attisdropped
781
                AND c.relname = $tableName
782
                AND d.nspname = $schemaName
783
            ORDER BY
784
                a.attnum;
785
        SQL;
786
787 104
        $columns = $this->db->createCommand($sql)->queryAll();
788 104
        $pdo = $this->db->getActivePDO();
789
790 104
        if (empty($columns)) {
791 17
            return false;
792
        }
793
794
        /** @var array $column */
795 98
        foreach ($columns as $column) {
796 98
            if ($pdo !== null && $pdo->getAttribute(PDO::ATTR_CASE) === PDO::CASE_UPPER) {
797 1
                $column = array_change_key_case($column, CASE_LOWER);
798
            }
799
800
            /** @psalm-var ColumnArray $column */
801 98
            $loadColumnSchema = $this->loadColumnSchema($column);
802 98
            $table->columns($loadColumnSchema->getName(), $loadColumnSchema);
803
804
            /** @var mixed */
805 98
            $defaultValue = $loadColumnSchema->getDefaultValue();
806
807 98
            if ($loadColumnSchema->isPrimaryKey()) {
808 66
                $table->primaryKey($loadColumnSchema->getName());
809
810 66
                if ($table->getSequenceName() === null) {
811 66
                    $table->sequenceName($loadColumnSchema->getSequenceName());
812
                }
813
814 66
                $loadColumnSchema->defaultValue(null);
815 95
            } elseif ($defaultValue) {
816
                if (
817 57
                    is_string($defaultValue) &&
818 57
                    in_array(
819 57
                        $loadColumnSchema->getType(),
820 57
                        [self::TYPE_TIMESTAMP, self::TYPE_DATE, self::TYPE_TIME],
821
                        true
822
                    ) &&
823 30
                    in_array(
824 30
                        strtoupper($defaultValue),
825 30
                        ['NOW()', 'CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME'],
826
                        true
827
                    )
828
                ) {
829 28
                    $loadColumnSchema->defaultValue(new Expression($defaultValue));
830 57
                } elseif ($loadColumnSchema->getType() === 'boolean') {
831 53
                    $loadColumnSchema->defaultValue(($defaultValue  === 'true'));
832 33
                } elseif (is_string($defaultValue) && preg_match("/^B'(.*?)'::/", $defaultValue, $matches)) {
833
                    $loadColumnSchema->defaultValue(bindec($matches[1]));
834 33
                } elseif (is_string($defaultValue) && preg_match("/^'(\d+)'::\"bit\"$/", $defaultValue, $matches)) {
835 28
                    $loadColumnSchema->defaultValue(bindec($matches[1]));
836 33
                } elseif (is_string($defaultValue) && preg_match("/^'(.*?)'::/", $defaultValue, $matches)) {
837 30
                    $loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($matches[1]));
838
                } elseif (
839 31
                    is_string($defaultValue) &&
840 31
                    preg_match('/^(\()?(.*?)(?(1)\))(?:::.+)?$/', $defaultValue, $matches)
841
                ) {
842 31
                    if ($matches[2] === 'NULL') {
843 5
                        $loadColumnSchema->defaultValue(null);
844
                    } else {
845 31
                        $loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($matches[2]));
846
                    }
847
                } else {
848
                    $loadColumnSchema->defaultValue($loadColumnSchema->phpTypecast($defaultValue));
849
                }
850
            }
851
        }
852
853 98
        return true;
854
    }
855
856
    /**
857
     * Loads the column information into a {@see ColumnSchema} object.
858
     *
859
     * @psalm-param array{
860
     *   table_schema: string,
861
     *   table_name: string,
862
     *   column_name: string,
863
     *   data_type: string,
864
     *   type_type: string|null,
865
     *   character_maximum_length: int,
866
     *   column_comment: string|null,
867
     *   modifier: int,
868
     *   is_nullable: bool,
869
     *   column_default: mixed,
870
     *   is_autoinc: bool,
871
     *   sequence_name: string|null,
872
     *   enum_values: array<array-key, float|int|string>|string|null,
873
     *   numeric_precision: int|null,
874
     *   numeric_scale: int|null,
875
     *   size: string|null,
876
     *   is_pkey: bool|null,
877
     *   dimension: int
878
     * } $info column information.
879
     *
880
     * @return ColumnSchema the column schema object.
881
     */
882 98
    protected function loadColumnSchema(array $info): ColumnSchema
883
    {
884 98
        $column = $this->createColumnSchema();
885 98
        $column->allowNull($info['is_nullable']);
886 98
        $column->autoIncrement($info['is_autoinc']);
887 98
        $column->comment($info['column_comment']);
888 98
        $column->dbType($info['data_type']);
889 98
        $column->defaultValue($info['column_default']);
890 98
        $column->enumValues(($info['enum_values'] !== null)
891 98
            ? explode(',', str_replace(["''"], ["'"], $info['enum_values'])) : null);
892 98
        $column->unsigned(false); // has no meaning in PG
893 98
        $column->primaryKey((bool) $info['is_pkey']);
894 98
        $column->name($info['column_name']);
895 98
        $column->precision($info['numeric_precision']);
896 98
        $column->scale($info['numeric_scale']);
897 98
        $column->size($info['size'] === null ? null : (int) $info['size']);
898 98
        $column->dimension($info['dimension']);
899
900
        /**
901
         * pg_get_serial_sequence() doesn't track DEFAULT value change. GENERATED BY IDENTITY columns always have null
902
         * default value.
903
         *
904
         * @var mixed $defaultValue
905
         */
906 98
        $defaultValue = $column->getDefaultValue();
907 98
        $sequenceName = $info['sequence_name'] ?? null;
908
909
        if (
910 98
            isset($defaultValue) &&
911 98
            is_string($defaultValue) &&
912 98
            preg_match("/nextval\\('\"?\\w+\"?\.?\"?\\w+\"?'(::regclass)?\\)/", $defaultValue) === 1
913
        ) {
914 61
            $column->sequenceName(preg_replace(
915 61
                ['/nextval/', '/::/', '/regclass/', '/\'\)/', '/\(\'/'],
916
                '',
917
                $defaultValue
918
            ));
919 96
        } elseif ($sequenceName !== null) {
920 5
            $column->sequenceName($this->resolveTableName($sequenceName)->getFullName());
921
        }
922
923 98
        if (isset($this->typeMap[$column->getDbType()])) {
924 98
            $column->type($this->typeMap[$column->getDbType()]);
925
        } else {
926
            $column->type(self::TYPE_STRING);
927
        }
928
929 98
        $column->phpType($this->getColumnPhpType($column));
930
931 98
        return $column;
932
    }
933
934
    /**
935
     * Loads multiple types of constraints and returns the specified ones.
936
     *
937
     * @param string $tableName table name.
938
     * @param string $returnType return type:
939
     * - primaryKey
940
     * - foreignKeys
941
     * - uniques
942
     * - checks
943
     *
944
     * @throws Exception|InvalidConfigException|Throwable
945
     *
946
     * @return array|Constraint|null (CheckConstraint|Constraint|ForeignKeyConstraint)[]|Constraint|null constraints.
947
     */
948 61
    private function loadTableConstraints(string $tableName, string $returnType): array|Constraint|null
949
    {
950 61
        $sql = <<<SQL
951
        SELECT
952
            "c"."conname" AS "name",
953
            "a"."attname" AS "column_name",
954
            "c"."contype" AS "type",
955
            "ftcns"."nspname" AS "foreign_table_schema",
956
            "ftc"."relname" AS "foreign_table_name",
957
            "fa"."attname" AS "foreign_column_name",
958
            "c"."confupdtype" AS "on_update",
959
            "c"."confdeltype" AS "on_delete",
960
            pg_get_constraintdef("c"."oid") AS "check_expr"
961
        FROM "pg_class" AS "tc"
962
        INNER JOIN "pg_namespace" AS "tcns"
963
            ON "tcns"."oid" = "tc"."relnamespace"
964
        INNER JOIN "pg_constraint" AS "c"
965
            ON "c"."conrelid" = "tc"."oid"
966
        INNER JOIN "pg_attribute" AS "a"
967
            ON "a"."attrelid" = "c"."conrelid" AND "a"."attnum" = ANY ("c"."conkey")
968
        LEFT JOIN "pg_class" AS "ftc"
969
            ON "ftc"."oid" = "c"."confrelid"
970
        LEFT JOIN "pg_namespace" AS "ftcns"
971
            ON "ftcns"."oid" = "ftc"."relnamespace"
972
        LEFT JOIN "pg_attribute" "fa"
973
            ON "fa"."attrelid" = "c"."confrelid" AND "fa"."attnum" = ANY ("c"."confkey")
974
        WHERE "tcns"."nspname" = :schemaName AND "tc"."relname" = :tableName
975
        ORDER BY "a"."attnum" ASC, "fa"."attnum" ASC
976
        SQL;
977
978
        /** @var array<array-key, string> $actionTypes */
979 61
        $actionTypes = [
980
            'a' => 'NO ACTION',
981
            'r' => 'RESTRICT',
982
            'c' => 'CASCADE',
983
            'n' => 'SET NULL',
984
            'd' => 'SET DEFAULT',
985
        ];
986
987 61
        $resolvedName = $this->resolveTableName($tableName);
988
989 61
        $constraints = $this->db->createCommand($sql, [
990 61
            ':schemaName' => $resolvedName->getSchemaName(),
991 61
            ':tableName' => $resolvedName->getName(),
992 61
        ])->queryAll();
993
994
        /** @var array<array-key, array> $constraints */
995 61
        $constraints = $this->normalizePdoRowKeyCase($constraints, true);
996 61
        $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
997
998 61
        $result = [
999
            'primaryKey' => null,
1000
            'foreignKeys' => [],
1001
            'uniques' => [],
1002
            'checks' => [],
1003
        ];
1004
1005
        /**
1006
         * @var string $type
1007
         * @var array $names
1008
         */
1009 61
        foreach ($constraints as $type => $names) {
1010
            /**
1011
             * @psalm-var object|string|null $name
1012
             * @psalm-var ConstraintArray $constraint
1013
             */
1014 61
            foreach ($names as $name => $constraint) {
1015 61
                switch ($type) {
1016 61
                    case 'p':
1017 46
                        $ct = (new Constraint())
1018 46
                            ->name($name)
1019 46
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
1020
1021 46
                        $result['primaryKey'] = $ct;
1022 46
                        break;
1023 59
                    case 'f':
1024 13
                        $onDelete = $actionTypes[$constraint[0]['on_delete']] ?? null;
1025 13
                        $onUpdate = $actionTypes[$constraint[0]['on_update']] ?? null;
1026
1027 13
                        $fk = (new ForeignKeyConstraint())
1028 13
                            ->name($name)
1029 13
                            ->columnNames(array_values(
1030 13
                                array_unique(ArrayHelper::getColumn($constraint, 'column_name'))
1031
                            ))
1032 13
                            ->foreignSchemaName($constraint[0]['foreign_table_schema'])
1033 13
                            ->foreignTableName($constraint[0]['foreign_table_name'])
1034 13
                            ->foreignColumnNames(array_values(
1035 13
                                array_unique(ArrayHelper::getColumn($constraint, 'foreign_column_name'))
1036
                            ))
1037 13
                            ->onDelete($onDelete)
1038 13
                            ->onUpdate($onUpdate);
1039
1040 13
                        $result['foreignKeys'][] = $fk;
1041 13
                        break;
1042 47
                    case 'u':
1043 46
                        $ct = (new Constraint())
1044 46
                            ->name($name)
1045 46
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'));
1046
1047 46
                        $result['uniques'][] = $ct;
1048 46
                        break;
1049 10
                    case 'c':
1050 10
                        $ck = (new CheckConstraint())
1051 10
                            ->name($name)
1052 10
                            ->columnNames(ArrayHelper::getColumn($constraint, 'column_name'))
1053 10
                            ->expression($constraint[0]['check_expr']);
1054
1055 10
                        $result['checks'][] = $ck;
1056 10
                        break;
1057
                }
1058
            }
1059
        }
1060
1061 61
        foreach ($result as $type => $data) {
1062 61
            $this->setTableMetadata($tableName, $type, $data);
1063
        }
1064
1065 61
        return $result[$returnType];
1066
    }
1067
1068
    /**
1069
     * Creates a column schema for the database.
1070
     *
1071
     * This method may be overridden by child classes to create a DBMS-specific column schema.
1072
     *
1073
     * @return ColumnSchema column schema instance.
1074
     */
1075 98
    private function createColumnSchema(): ColumnSchema
1076
    {
1077 98
        return new ColumnSchema();
1078
    }
1079
1080
    /**
1081
     * Create a column schema builder instance giving the type and value precision.
1082
     *
1083
     * This method may be overridden by child classes to create a DBMS-specific column schema builder.
1084
     *
1085
     * @param string $type type of the column. See {@see ColumnSchemaBuilder::$type}.
1086
     * @param array|int|string|null $length length or precision of the column. See {@see ColumnSchemaBuilder::$length}.
1087
     *
1088
     * @return ColumnSchemaBuilder column schema builder instance
1089
     *
1090
     * @psalm-param int|string|string[]|null $length
1091
     */
1092 4
    public function createColumnSchemaBuilder(string $type, int|string|array|null $length = null): ColumnSchemaBuilder
1093
    {
1094 4
        return new ColumnSchemaBuilder($type, $length);
1095
    }
1096
1097 1
    public function rollBackSavepoint(string $name): void
1098
    {
1099 1
        $this->db->createCommand("ROLLBACK TO SAVEPOINT $name")->execute();
1100
    }
1101
1102 2
    public function setTransactionIsolationLevel(string $level): void
1103
    {
1104 2
        $this->db->createCommand("SET TRANSACTION ISOLATION LEVEL $level")->execute();
1105
    }
1106
1107
    /**
1108
     * Returns the actual name of a given table name.
1109
     *
1110
     * This method will strip off curly brackets from the given table name and replace the percentage character '%' with
1111
     * {@see ConnectionInterface::tablePrefix}.
1112
     *
1113
     * @param string $name the table name to be converted.
1114
     *
1115
     * @return string the real name of the given table name.
1116
     */
1117 164
    public function getRawTableName(string $name): string
1118
    {
1119 164
        if (str_contains($name, '{{')) {
1120 23
            $name = preg_replace('/{{(.*?)}}/', '\1', $name);
1121
1122 23
            return str_replace('%', $this->db->getTablePrefix(), $name);
1123
        }
1124
1125 164
        return $name;
1126
    }
1127
1128
    /**
1129
     * Returns the cache key for the specified table name.
1130
     *
1131
     * @param string $name the table name.
1132
     *
1133
     * @return array the cache key.
1134
     */
1135 164
    protected function getCacheKey(string $name): array
1136
    {
1137
        return [
1138 164
            __CLASS__,
1139 164
            $this->db->getDriver()->getDsn(),
1140 164
            $this->db->getDriver()->getUsername(),
1141 164
            $this->getRawTableName($name),
1142
        ];
1143
    }
1144
1145
    /**
1146
     * Returns the cache tag name.
1147
     *
1148
     * This allows {@see refresh()} to invalidate all cached table schemas.
1149
     *
1150
     * @return string the cache tag name.
1151
     */
1152 164
    protected function getCacheTag(): string
1153
    {
1154 164
        return md5(serialize([
1155
            __CLASS__,
1156 164
            $this->db->getDriver()->getDsn(),
1157 164
            $this->db->getDriver()->getUsername(),
1158
        ]));
1159
    }
1160
1161
    /**
1162
     * @return bool whether this DBMS supports [savepoint](http://en.wikipedia.org/wiki/Savepoint).
1163
     */
1164 2
    public function supportsSavepoint(): bool
1165
    {
1166 2
        return $this->db->isSavepointEnabled();
1167
    }
1168
1169
    /**
1170
     * Changes row's array key case to lower if PDO one is set to uppercase.
1171
     *
1172
     * @param array $row row's array or an array of row's arrays.
1173
     * @param bool $multiple whether multiple rows or a single row passed.
1174
     *
1175
     * @throws Exception
1176
     *
1177
     * @return array normalized row or rows.
1178
     */
1179 71
    protected function normalizePdoRowKeyCase(array $row, bool $multiple): array
1180
    {
1181 71
        if ($this->db->getActivePDO()?->getAttribute(PDO::ATTR_CASE) !== PDO::CASE_UPPER) {
1182 55
            return $row;
1183
        }
1184
1185 16
        if ($multiple) {
1186 16
            return array_map(static function (array $row) {
1187 15
                return array_change_key_case($row, CASE_LOWER);
1188
            }, $row);
1189
        }
1190
1191
        return array_change_key_case($row, CASE_LOWER);
1192
    }
1193
1194
    /**
1195
     * Returns the ID of the last inserted row or sequence value.
1196
     *
1197
     * @param string $sequenceName name of the sequence object (required by some DBMS)
1198
     *
1199
     * @throws InvalidCallException if the DB connection is not active
1200
     *
1201
     * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object
1202
     *
1203
     * @see http://www.php.net/manual/en/function.PDO-lastInsertId.php
1204
     */
1205 3
    public function getLastInsertID(string $sequenceName = ''): string
1206
    {
1207 3
        $pdo = $this->db->getPDO();
1208
1209 3
        if ($this->db->isActive() && $pdo instanceof PDO) {
1210 3
            return $pdo->lastInsertId(
1211 3
                $sequenceName === '' ? null : $this->db->getQuoter()->quoteTableName($sequenceName)
1212
            );
1213
        }
1214
1215
        throw new InvalidCallException('DB Connection is not active.');
1216
    }
1217
1218
    /**
1219
     * Creates a new savepoint.
1220
     *
1221
     * @param string $name the savepoint name
1222
     *
1223
     * @throws Exception|InvalidConfigException|Throwable
1224
     */
1225 1
    public function createSavepoint(string $name): void
1226
    {
1227 1
        $this->db->createCommand("SAVEPOINT $name")->execute();
1228
    }
1229
1230
    /**
1231
     * @throws Exception|InvalidConfigException|Throwable
1232
     */
1233
    public function releaseSavepoint(string $name): void
1234
    {
1235
        $this->db->createCommand("RELEASE SAVEPOINT $name")->execute();
1236
    }
1237
1238
    /**
1239
     * @throws Exception|InvalidConfigException|Throwable
1240
     */
1241 1
    public function getViewNames(string $schema = '', bool $refresh = false): array
1242
    {
1243 1
        if (!isset($this->viewNames[$schema]) || $refresh) {
1244 1
            $this->viewNames[$schema] = $this->findViewNames($schema);
1245
        }
1246
1247 1
        return is_array($this->viewNames[$schema]) ? $this->viewNames[$schema] : [];
1248
    }
1249
}
1250