Passed
Branch dev (d14b82)
by Wilmer
12:57
created

SchemaPDOPgsql::findColumns()   D

Complexity

Conditions 23
Paths 96

Size

Total Lines 155
Code Lines 70

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 45
CRAP Score 23.0408

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