Passed
Pull Request — dev (#109)
by Wilmer
03:08
created

SchemaPDOPgsql::findConstraints()   B

Complexity

Conditions 8
Paths 36

Size

Total Lines 93
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 8.0368

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 48
c 1
b 0
f 0
nc 36
nop 1
dl 0
loc 93
ccs 22
cts 24
cp 0.9167
crap 8.0368
rs 7.8901

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

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