PostgresAdapter::getGlobalSchemaName()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 5
c 0
b 0
f 0
ccs 0
cts 0
cp 0
rs 10
cc 2
nc 2
nop 0
crap 6
1
<?php
2
3
/**
4
 * MIT License
5
 * For full license information, please view the LICENSE file that was distributed with this source code.
6
 */
7
8
namespace Phinx\Db\Adapter;
9
10
use Cake\Database\Connection;
11
use Cake\Database\Driver\Postgres as PostgresDriver;
12
use InvalidArgumentException;
13
use PDO;
14
use PDOException;
15
use Phinx\Db\Table\Column;
16
use Phinx\Db\Table\ForeignKey;
17
use Phinx\Db\Table\Index;
18
use Phinx\Db\Table\Table;
19
use Phinx\Db\Util\AlterInstructions;
20
use Phinx\Util\Literal;
21
use RuntimeException;
22
23
class PostgresAdapter extends PdoAdapter
24
{
25
    /**
26
     * @var string[]
27
     */
28
    protected static $specificColumnTypes = [
29
        self::PHINX_TYPE_JSON,
30
        self::PHINX_TYPE_JSONB,
31
        self::PHINX_TYPE_CIDR,
32
        self::PHINX_TYPE_INET,
33
        self::PHINX_TYPE_MACADDR,
34
        self::PHINX_TYPE_INTERVAL,
35
        self::PHINX_TYPE_BINARYUUID,
36
    ];
37
38
    private const GIN_INDEX_TYPE = 'gin';
39
40
    /**
41
     * Columns with comments
42
     *
43
     * @var \Phinx\Db\Table\Column[]
44
     */
45
    protected $columnsWithComments = [];
46
47
    /**
48
     * {@inheritDoc}
49
     *
50 68
     * @throws \RuntimeException
51
     * @throws \InvalidArgumentException
52 68
     * @return void
53 68
     */
54
    public function connect()
55
    {
56
        if ($this->connection === null) {
57
            if (!class_exists('PDO') || !in_array('pgsql', PDO::getAvailableDrivers(), true)) {
58
                // @codeCoverageIgnoreStart
59 68
                throw new RuntimeException('You need to enable the PDO_Pgsql extension for Phinx to run properly.');
60 68
                // @codeCoverageIgnoreEnd
61
            }
62
63 68
            $options = $this->getOptions();
64 68
65 68
            $dsn = 'pgsql:dbname=' . $options['name'];
66 1
67
            if (isset($options['host'])) {
68
                $dsn .= ';host=' . $options['host'];
69
            }
70 68
71 68
            // if custom port is specified use it
72 1
            if (isset($options['port'])) {
73 1
                $dsn .= ';port=' . $options['port'];
74 1
            }
75 1
76
            $driverOptions = [];
77
78 68
            // use custom data fetch mode
79 68
            if (!empty($options['fetch_mode'])) {
80 68
                $driverOptions[PDO::ATTR_DEFAULT_FETCH_MODE] = constant('\PDO::FETCH_' . strtoupper($options['fetch_mode']));
81
            }
82
83
            // pass \PDO::ATTR_PERSISTENT to driver options instead of useless setting it after instantiation
84
            if (isset($options['attr_persistent'])) {
85 68
                $driverOptions[PDO::ATTR_PERSISTENT] = $options['attr_persistent'];
86
            }
87 68
88 68
            $db = $this->createPdoConnection($dsn, $options['user'] ?? null, $options['pass'] ?? null, $driverOptions);
89
90
            try {
91
                if (isset($options['schema'])) {
92
                    $db->exec('SET search_path TO ' . $this->quoteSchemaName($options['schema']));
93
                }
94
            } catch (PDOException $exception) {
95
                throw new InvalidArgumentException(
96
                    sprintf('Schema does not exists: %s', $options['schema']),
97
                    $exception->getCode(),
98
                    $exception
99
                );
100
            }
101
102
            $this->setConnection($db);
103
        }
104
    }
105
106
    /**
107
     * @inheritDoc
108
     */
109
    public function disconnect()
110
    {
111
        $this->connection = null;
112
    }
113
114
    /**
115
     * @inheritDoc
116
     */
117
    public function hasTransactions()
118
    {
119
        return true;
120
    }
121
122
    /**
123
     * @inheritDoc
124
     */
125
    public function beginTransaction()
126
    {
127
        $this->execute('BEGIN');
128 68
    }
129
130 68
    /**
131
     * @inheritDoc
132
     */
133
    public function commitTransaction()
134
    {
135
        $this->execute('COMMIT');
136 68
    }
137
138 68
    /**
139
     * @inheritDoc
140
     */
141
    public function rollbackTransaction()
142
    {
143
        $this->execute('ROLLBACK');
144 68
    }
145
146 68
    /**
147
     * Quotes a schema name for use in a query.
148
     *
149
     * @param string $schemaName Schema Name
150
     * @return string
151
     */
152 68
    public function quoteSchemaName($schemaName)
153
    {
154 68
        return $this->quoteColumnName($schemaName);
155 68
    }
156
157
    /**
158
     * @inheritDoc
159 68
     */
160 68
    public function quoteTableName($tableName)
161 68
    {
162 68
        $parts = $this->getSchemaName($tableName);
163 68
164
        return $this->quoteSchemaName($parts['schema']) . '.' . $this->quoteColumnName($parts['table']);
165 68
    }
166
167
    /**
168
     * @inheritDoc
169
     */
170
    public function quoteColumnName($columnName)
171 68
    {
172
        return '"' . $columnName . '"';
173 68
    }
174
175
    /**
176 68
     * @inheritDoc
177 68
     */
178 48
    public function hasTable($tableName)
179 48
    {
180 48
        if ($this->hasCreatedTable($tableName)) {
181 48
            return true;
182
        }
183 48
184 48
        $parts = $this->getSchemaName($tableName);
185 68
        $result = $this->getConnection()->query(
186
            sprintf(
187 2
                'SELECT *
188 2
                FROM information_schema.tables
189 2
                WHERE table_schema = %s
190 2
                AND table_name = %s',
191
                $this->getConnection()->quote($parts['schema']),
192 2
                $this->getConnection()->quote($parts['table'])
193 2
            )
194 2
        );
195
196
        return $result->rowCount() === 1;
197 68
    }
198 68
199
    /**
200 68
     * @inheritDoc
201 68
     */
202 68
    public function createTable(Table $table, array $columns = [], array $indexes = [])
203
    {
204
        $queries = [];
205 68
206 6
        $options = $table->getOptions();
207 6
        $parts = $this->getSchemaName($table->getName());
208 68
209
         // Add the default primary key
210
        if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) {
211 68
            $options['id'] = 'id';
212 68
        }
213 68
214 68
        if (isset($options['id']) && is_string($options['id'])) {
215 68
            // Handle id => "field_name" to support AUTO_INCREMENT
216 68
            $column = new Column();
217
            $column->setName($options['id'])
218
                   ->setType('integer')
219 1
                   ->setIdentity(true);
220 1
221 1
            array_unshift($columns, $column);
222 1
            if (isset($options['primary_key']) && (array)$options['id'] !== (array)$options['primary_key']) {
223 1
                throw new InvalidArgumentException('You cannot enable an auto incrementing ID field and a primary key');
224 1
            }
225 1
            $options['primary_key'] = $options['id'];
226 1
        }
227 1
228 1
        // TODO - process table options like collation etc
229 68
        $sql = 'CREATE TABLE ';
230 68
        $sql .= $this->quoteTableName($table->getName()) . ' (';
231 2
232
        $this->columnsWithComments = [];
233
        foreach ($columns as $column) {
234
            $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column) . ', ';
235 68
236 68
            // set column comments, if needed
237 1
            if ($column->getComment()) {
238 1
                $this->columnsWithComments[] = $column;
239 1
            }
240 1
        }
241
242 68
         // set the primary key(s)
243
        if (isset($options['primary_key'])) {
244
            $sql = rtrim($sql);
245 68
            $sql .= sprintf(' CONSTRAINT %s PRIMARY KEY (', $this->quoteColumnName($parts['table'] . '_pkey'));
246 6
            if (is_string($options['primary_key'])) { // handle primary_key => 'id'
247 6
                $sql .= $this->quoteColumnName($options['primary_key']);
248 6
            } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id')
249 6
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key']));
250
            }
251
            $sql .= ')';
252
        } else {
253 68
            $sql = rtrim($sql, ', '); // no primary keys
254 68
        }
255 5
256 5
        $sql .= ')';
257 5
        $queries[] = $sql;
258 5
259
        // process column comments
260
        if (!empty($this->columnsWithComments)) {
261 68
            foreach ($this->columnsWithComments as $column) {
262
                $queries[] = $this->getColumnCommentSqlDefinition($column, $table->getName());
263
            }
264 68
        }
265 1
266 1
        // set the indexes
267 1
        if (!empty($indexes)) {
268 1
            foreach ($indexes as $index) {
269 1
                $queries[] = $this->getIndexSqlDefinition($index, $table->getName());
270 1
            }
271 1
        }
272 68
273
        // process table comments
274
        if (isset($options['comment'])) {
275
            $queries[] = sprintf(
276
                'COMMENT ON TABLE %s IS %s',
277 1
                $this->quoteTableName($table->getName()),
278
                $this->getConnection()->quote($options['comment'])
279 1
            );
280 1
        }
281 1
282 1
        foreach ($queries as $query) {
283 1
            $this->execute($query);
284 1
        }
285 1
286
        $this->addCreatedTable($table->getName());
287
    }
288
289
    /**
290 1
     * {@inheritDoc}
291
     *
292 1
     * @throws \InvalidArgumentException
293 1
     */
294
    protected function getChangePrimaryKeyInstructions(Table $table, $newColumns)
295
    {
296
        $parts = $this->getSchemaName($table->getName());
297
298 1
        $instructions = new AlterInstructions();
299
300 1
        // Drop the existing primary key
301 1
        $primaryKey = $this->getPrimaryKey($table->getName());
302 1
        if (!empty($primaryKey['constraint'])) {
303 1
            $sql = sprintf(
304
                'DROP CONSTRAINT %s',
305 1
                $this->quoteColumnName($primaryKey['constraint'])
306 1
            );
307
            $instructions->addAlter($sql);
308
        }
309
310
        // Add the new primary key
311 9
        if (!empty($newColumns)) {
312
            $sql = sprintf(
313 9
                'ADD CONSTRAINT %s PRIMARY KEY (',
314 9
                $this->quoteColumnName($parts['table'] . '_pkey')
315
            );
316
            if (is_string($newColumns)) { // handle primary_key => 'id'
317
                $sql .= $this->quoteColumnName($newColumns);
318 9
            } elseif (is_array($newColumns)) { // handle primary_key => array('tag_id', 'resource_id')
319
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $newColumns));
320 9
            } else {
321 9
                throw new InvalidArgumentException(sprintf(
322
                    'Invalid value for primary key: %s',
323 9
                    json_encode($newColumns)
324 9
                ));
325 9
            }
326 9
            $sql .= ')';
327 9
            $instructions->addAlter($sql);
328 9
        }
329 9
330 9
        return $instructions;
331 9
    }
332
333 9
    /**
334 1
     * @inheritDoc
335 1
     */
336
    protected function getChangeCommentInstructions(Table $table, $newComment)
337 9
    {
338 5
        $instructions = new AlterInstructions();
339 5
340 9
        // passing 'null' is to remove table comment
341 9
        $newComment = $newComment !== null
342 9
            ? $this->getConnection()->quote($newComment)
343
            : 'NULL';
344
        $sql = sprintf(
345
            'COMMENT ON TABLE %s IS %s',
346
            $this->quoteTableName($table->getName()),
347
            $newComment
348 24
        );
349
        $instructions->addPostStep($sql);
350 24
351
        return $instructions;
352
    }
353 24
354 24
    /**
355 24
     * @inheritDoc
356
     */
357 24
    protected function getRenameTableInstructions($tableName, $newTableName)
358
    {
359 24
        $this->updateCreatedTableName($tableName, $newTableName);
360 24
        $sql = sprintf(
361
            'ALTER TABLE %s RENAME TO %s',
362
            $this->quoteTableName($tableName),
363
            $this->quoteColumnName($newTableName)
364
        );
365
366 18
        return new AlterInstructions([], [$sql]);
367
    }
368 18
369 18
    /**
370 18
     * @inheritDoc
371 18
     */
372 18
    protected function getDropTableInstructions($tableName)
373 18
    {
374
        $this->removeCreatedTable($tableName);
375 18
        $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
376 18
377
        return new AlterInstructions([], [$sql]);
378
    }
379
380
    /**
381 3
     * @inheritDoc
382
     */
383 3
    public function truncateTable($tableName)
384
    {
385
        $sql = sprintf(
386 3
            'TRUNCATE TABLE %s RESTART IDENTITY',
387 3
            $this->quoteTableName($tableName)
388
        );
389 3
390 3
        $this->execute($sql);
391 3
    }
392 1
393
    /**
394 2
     * @inheritDoc
395 2
     */
396 2
    public function getColumns($tableName)
397 2
    {
398 2
        $parts = $this->getSchemaName($tableName);
399 2
        $columns = [];
400 2
        $sql = sprintf(
401 2
            'SELECT column_name, data_type, udt_name, is_identity, is_nullable,
402 2
             column_default, character_maximum_length, numeric_precision, numeric_scale,
403
             datetime_precision
404
             FROM information_schema.columns
405
             WHERE table_schema = %s AND table_name = %s
406
             ORDER BY ordinal_position',
407 5
            $this->getConnection()->quote($parts['schema']),
408
            $this->getConnection()->quote($parts['table'])
409
        );
410
        $columnsInfo = $this->fetchAll($sql);
411 5
412 5
        foreach ($columnsInfo as $columnInfo) {
413 5
            $isUserDefined = strtoupper(trim($columnInfo['data_type'])) === 'USER-DEFINED';
414 5
415 5
            if ($isUserDefined) {
416 5
                $columnType = Literal::from($columnInfo['udt_name']);
417
            } else {
418 5
                $columnType = $this->getPhinxType($columnInfo['data_type']);
419 5
            }
420
421 5
            // If the default value begins with a ' or looks like a function mark it as literal
422 5
            if (isset($columnInfo['column_default'][0]) && $columnInfo['column_default'][0] === "'") {
423
                if (preg_match('/^\'(.*)\'::[^:]+$/', $columnInfo['column_default'], $match)) {
424 5
                    // '' and \' are replaced with a single '
425 5
                    $columnDefault = preg_replace('/[\'\\\\]\'/', "'", $match[1]);
426 5
                } else {
427 5
                    $columnDefault = Literal::from($columnInfo['column_default']);
428 5
                }
429 5
            } elseif (preg_match('/^\D[a-z_\d]*\(.*\)$/', $columnInfo['column_default'])) {
430 2
                $columnDefault = Literal::from($columnInfo['column_default']);
431 2
            } else {
432 4
                $columnDefault = $columnInfo['column_default'];
433
            }
434 5
435 5
            $column = new Column();
436
            $column->setName($columnInfo['column_name'])
437 1
                   ->setType($columnType)
438 1
                   ->setNull($columnInfo['is_nullable'] === 'YES')
439 1
                   ->setDefault($columnDefault)
440 1
                   ->setIdentity($columnInfo['is_identity'] === 'YES')
441 1
                   ->setScale($columnInfo['numeric_scale']);
442 1
443 1
            if (preg_match('/\bwith time zone$/', $columnInfo['data_type'])) {
444 1
                $column->setTimezone(true);
445 1
            }
446
447 4
            if (isset($columnInfo['character_maximum_length'])) {
448 4
                $column->setLimit($columnInfo['character_maximum_length']);
449 4
            }
450 4
451 4
            if (in_array($columnType, [static::PHINX_TYPE_TIME, static::PHINX_TYPE_DATETIME], true)) {
452 4
                $column->setPrecision($columnInfo['datetime_precision']);
453 4
            } elseif (
454
                !in_array($columnType, [
455
                    self::PHINX_TYPE_SMALL_INTEGER,
456 5
                    self::PHINX_TYPE_INTEGER,
457 1
                    self::PHINX_TYPE_BIG_INTEGER,
458 1
                ], true)
459 1
            ) {
460 1
                $column->setPrecision($columnInfo['numeric_precision']);
461 1
            }
462 1
            $columns[] = $column;
463 1
        }
464 1
465 1
        return $columns;
466
    }
467
468 5
    /**
469 2
     * @inheritDoc
470 2
     */
471 2
    public function hasColumn($tableName, $columnName)
472 5
    {
473
        $parts = $this->getSchemaName($tableName);
474
        $sql = sprintf(
475
            'SELECT count(*)
476
            FROM information_schema.columns
477 1
            WHERE table_schema = %s AND table_name = %s AND column_name = %s',
478
            $this->getConnection()->quote($parts['schema']),
479 1
            $this->getConnection()->quote($parts['table']),
480 1
            $this->getConnection()->quote($columnName)
481 1
        );
482 1
483 1
        $result = $this->fetchRow($sql);
484 1
485 1
        return $result['count'] > 0;
486 1
    }
487
488
    /**
489
     * @inheritDoc
490
     */
491
    protected function getAddColumnInstructions(Table $table, Column $column)
492
    {
493
        $instructions = new AlterInstructions();
494 9
        $instructions->addAlter(sprintf(
495
            'ADD %s %s',
496 9
            $this->quoteColumnName($column->getName()),
497
            $this->getColumnSqlDefinition($column)
498
        ));
499
500
        if ($column->getComment()) {
501
            $instructions->addPostStep($this->getColumnCommentSqlDefinition($column, $table->getName()));
502
        }
503
504
        return $instructions;
505
    }
506
507
    /**
508
     * {@inheritDoc}
509
     *
510
     * @throws \InvalidArgumentException
511
     */
512
    protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName)
513
    {
514 9
        $parts = $this->getSchemaName($tableName);
515 9
        $sql = sprintf(
516 9
            'SELECT CASE WHEN COUNT(*) > 0 THEN 1 ELSE 0 END AS column_exists
517 9
             FROM information_schema.columns
518 9
             WHERE table_schema = %s AND table_name = %s AND column_name = %s',
519 9
            $this->getConnection()->quote($parts['schema']),
520 9
            $this->getConnection()->quote($parts['table']),
521 9
            $this->getConnection()->quote($columnName)
522 9
        );
523
524
        $result = $this->fetchRow($sql);
525
        if (!(bool)$result['column_exists']) {
526
            throw new InvalidArgumentException("The specified column does not exist: $columnName");
527
        }
528 9
529
        $instructions = new AlterInstructions();
530 9
        $instructions->addPostStep(
531 4
            sprintf(
532 4
                'ALTER TABLE %s RENAME COLUMN %s TO %s',
533 9
                $this->quoteTableName($tableName),
534 9
                $this->quoteColumnName($columnName),
535 9
                $this->quoteColumnName($newColumnName)
536 9
            )
537 9
        );
538
539 8
        return $instructions;
540 8
    }
541
542
    /**
543
     * @inheritDoc
544
     */
545
    protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn)
546 1
    {
547
        $quotedColumnName = $this->quoteColumnName($columnName);
548 1
        $instructions = new AlterInstructions();
549 1
        if ($newColumn->getType() === 'boolean') {
550 1
            $sql = sprintf('ALTER COLUMN %s DROP DEFAULT', $quotedColumnName);
551 1
            $instructions->addAlter($sql);
552
        }
553
        $sql = sprintf(
554
            'ALTER COLUMN %s TYPE %s',
555
            $quotedColumnName,
556
            $this->getColumnSqlDefinition($newColumn)
557
        );
558
        if (in_array($newColumn->getType(), ['smallinteger', 'integer', 'biginteger'], true)) {
559
            $sql .= sprintf(
560 2
                ' USING (%s::bigint)',
561
                $quotedColumnName
562 2
            );
563 2
        }
564 2
        if ($newColumn->getType() === 'uuid') {
565
            $sql .= sprintf(
566
                ' USING (%s::uuid)',
567
                $quotedColumnName
568
            );
569 1
        }
570
        //NULL and DEFAULT cannot be set while changing column type
571 1
        $sql = preg_replace('/ NOT NULL/', '', $sql);
572 1
        $sql = preg_replace('/ NULL/', '', $sql);
573 1
        //If it is set, DEFAULT is the last definition
574
        $sql = preg_replace('/DEFAULT .*/', '', $sql);
575 1
        if ($newColumn->getType() === 'boolean') {
576 1
            $sql .= sprintf(
577
                ' USING (CASE WHEN %s IS NULL THEN NULL WHEN %s::int=0 THEN FALSE ELSE TRUE END)',
578 1
                $quotedColumnName,
579 1
                $quotedColumnName
580 1
            );
581 1
        }
582 1
        $instructions->addAlter($sql);
583 1
584 1
        // process null
585 1
        $sql = sprintf(
586 1
            'ALTER COLUMN %s',
587
            $quotedColumnName
588 1
        );
589
590
        if ($newColumn->isNull()) {
591
            $sql .= ' DROP NOT NULL';
592
        } else {
593
            $sql .= ' SET NOT NULL';
594
        }
595
596 1
        $instructions->addAlter($sql);
597
598 1
        if ($newColumn->getDefault() !== null) {
599 1
            $instructions->addAlter(sprintf(
600
                'ALTER COLUMN %s SET %s',
601 1
                $quotedColumnName,
602 1
                $this->getDefaultValueDefinition($newColumn->getDefault(), $newColumn->getType())
603 1
            ));
604
        } else {
605
            //drop default
606
            $instructions->addAlter(sprintf(
607
                'ALTER COLUMN %s DROP DEFAULT',
608 3
                $quotedColumnName
609
            ));
610 3
        }
611 1
612 1
        // rename column
613 3
        if ($columnName !== $newColumn->getName()) {
614 3
            $instructions->addPostStep(sprintf(
615
                'ALTER TABLE %s RENAME COLUMN %s TO %s',
616
                $this->quoteTableName($tableName),
617
                $quotedColumnName,
618
                $this->quoteColumnName($newColumn->getName())
619
            ));
620 3
        }
621 3
622 3
        // change column comment if needed
623 3
        if ($newColumn->getComment()) {
624
            $instructions->addPostStep($this->getColumnCommentSqlDefinition($newColumn, $tableName));
625 1
        }
626 1
627
        return $instructions;
628
    }
629
630
    /**
631
     * @inheritDoc
632
     */
633
    protected function getDropColumnInstructions($tableName, $columnName)
634
    {
635
        $alter = sprintf(
636 3
            'DROP COLUMN %s',
637
            $this->quoteColumnName($columnName)
638 3
        );
639 3
640
        return new AlterInstructions([$alter]);
641
    }
642
643
    /**
644
     * Get an array of indexes from a particular table.
645
     *
646
     * @param string $tableName Table name
647
     * @return array
648
     */
649
    protected function getIndexes($tableName)
650 3
    {
651
        $parts = $this->getSchemaName($tableName);
652 3
653 3
        $indexes = [];
654 3
        $sql = sprintf(
655 3
            "SELECT
656 3
                i.relname AS index_name,
657 3
                a.attname AS column_name
658 3
            FROM
659 3
                pg_class t,
660
                pg_class i,
661
                pg_index ix,
662
                pg_attribute a,
663
                pg_namespace nsp
664
            WHERE
665 2
                t.oid = ix.indrelid
666
                AND i.oid = ix.indexrelid
667 2
                AND a.attrelid = t.oid
668 2
                AND a.attnum = ANY(ix.indkey)
669 2
                AND t.relnamespace = nsp.oid
670 2
                AND nsp.nspname = %s
671 2
                AND t.relkind = 'r'
672 2
                AND t.relname = %s
673 2
            ORDER BY
674
                t.relname,
675
                i.relname",
676
            $this->getConnection()->quote($parts['schema']),
677
            $this->getConnection()->quote($parts['table'])
678 1
        );
679
        $rows = $this->fetchAll($sql);
680 1
        foreach ($rows as $row) {
681
            if (!isset($indexes[$row['index_name']])) {
682
                $indexes[$row['index_name']] = ['columns' => []];
683
            }
684 1
            $indexes[$row['index_name']]['columns'][] = $row['column_name'];
685 1
        }
686 1
687 1
        return $indexes;
688 1
    }
689
690 1
    /**
691 1
     * @inheritDoc
692 1
     */
693 1
    public function hasIndex($tableName, $columns)
694 1
    {
695
        if (is_string($columns)) {
696
            $columns = [$columns];
697
        }
698
        $indexes = $this->getIndexes($tableName);
699
        foreach ($indexes as $index) {
700
            if (array_diff($index['columns'], $columns) === array_diff($columns, $index['columns'])) {
701 1
                return true;
702 1
            }
703
        }
704 1
705
        return false;
706 1
    }
707 1
708 1
    /**
709 1
     * @inheritDoc
710
     */
711 1
    public function hasIndexByName($tableName, $indexName)
712
    {
713
        $indexes = $this->getIndexes($tableName);
714
        foreach ($indexes as $name => $index) {
715
            if ($name === $indexName) {
716 68
                return true;
717
            }
718
        }
719 68
720 14
        return false;
721
    }
722 1
723
    /**
724 1
     * @inheritDoc
725
     */
726 14
    protected function getAddIndexInstructions(Table $table, Index $index)
727 68
    {
728 68
        $instructions = new AlterInstructions();
729 68
        $instructions->addPostStep($this->getIndexSqlDefinition($index, $table->getName()));
730 68
731 68
        return $instructions;
732 68
    }
733 68
734 68
    /**
735 68
     * {@inheritDoc}
736 68
     *
737 68
     * @throws \InvalidArgumentException
738 68
     */
739 2
    protected function getDropIndexByColumnsInstructions($tableName, $columns)
740 68
    {
741 68
        $parts = $this->getSchemaName($tableName);
742 68
743
        if (is_string($columns)) {
744 68
            $columns = [$columns]; // str to array
745 68
        }
746 68
747 1
        $indexes = $this->getIndexes($tableName);
748 68
        foreach ($indexes as $indexName => $index) {
749 68
            $a = array_diff($columns, $index['columns']);
750 68
            if (empty($a)) {
751 15
                return new AlterInstructions([], [sprintf(
752 15
                    'DROP INDEX IF EXISTS %s',
753 1
                    '"' . ($parts['schema'] . '".' . $this->quoteColumnName($indexName))
754
                )]);
755
            }
756
        }
757
758 14
        throw new InvalidArgumentException(sprintf(
759
            "The specified index on columns '%s' does not exist",
760
            implode(',', $columns)
761 14
        ));
762
    }
763
764 14
    /**
765
     * @inheritDoc
766
     */
767 14
    protected function getDropIndexByNameInstructions($tableName, $indexName)
768
    {
769
        $parts = $this->getSchemaName($tableName);
770 14
771 14
        $sql = sprintf(
772 13
            'DROP INDEX IF EXISTS %s',
773
            '"' . ($parts['schema'] . '".' . $this->quoteColumnName($indexName))
774
        );
775 1
776 14
        return new AlterInstructions([], [$sql]);
777
    }
778
779
    /**
780
     * @inheritDoc
781
     */
782
    public function hasPrimaryKey($tableName, $columns, $constraint = null)
783
    {
784
        $primaryKey = $this->getPrimaryKey($tableName);
785 10
786
        if (empty($primaryKey)) {
787
            return false;
788 10
        }
789 10
790 6
        if ($constraint) {
791 10
            return $primaryKey['constraint'] === $constraint;
792 10
        } else {
793
            if (is_string($columns)) {
794 10
                $columns = [$columns]; // str to array
795 2
            }
796 10
            $missingColumns = array_diff($columns, $primaryKey['columns']);
797
798 10
            return empty($missingColumns);
799
        }
800 10
    }
801
802 1
    /**
803
     * Get the primary key from a particular table.
804 1
     *
805 10
     * @param string $tableName Table name
806 10
     * @return array
807 10
     */
808 9
    public function getPrimaryKey($tableName)
809 5
    {
810 5
        $parts = $this->getSchemaName($tableName);
811 3
        $rows = $this->fetchAll(sprintf(
812 4
            "SELECT
813 4
                    tc.constraint_name,
814 2
                    kcu.column_name
815 4
                FROM information_schema.table_constraints AS tc
816 4
                JOIN information_schema.key_column_usage AS kcu
817 2
                    ON tc.constraint_name = kcu.constraint_name
818 4
                WHERE constraint_type = 'PRIMARY KEY'
819 1
                    AND tc.table_schema = %s
820
                    AND tc.table_name = %s
821 4
                ORDER BY kcu.position_in_unique_constraint",
822 4
            $this->getConnection()->quote($parts['schema']),
823 4
            $this->getConnection()->quote($parts['table'])
824 4
        ));
825 3
826 4
        $primaryKey = [
827 2
            'columns' => [],
828 4
        ];
829 4
        foreach ($rows as $row) {
830 4
            $primaryKey['constraint'] = $row['constraint_name'];
831 4
            $primaryKey['columns'][] = $row['column_name'];
832 3
        }
833 3
834 3
        return $primaryKey;
835 3
    }
836 1
837 1
    /**
838
     * @inheritDoc
839
     */
840
    public function hasForeignKey($tableName, $columns, $constraint = null)
841
    {
842
        if (is_string($columns)) {
843
            $columns = [$columns]; // str to array
844
        }
845
        $foreignKeys = $this->getForeignKeys($tableName);
846
        if ($constraint) {
847
            if (isset($foreignKeys[$constraint])) {
848
                return !empty($foreignKeys[$constraint]);
849
            }
850
851
            return false;
852 1
        }
853
854 1
        foreach ($foreignKeys as $key) {
855 1
            $a = array_diff($columns, $key['columns']);
856 1
            if (empty($a)) {
857
                return true;
858
            }
859
        }
860
861 2
        return false;
862
    }
863 2
864 2
    /**
865 2
     * Get an array of foreign keys from a particular table.
866
     *
867
     * @param string $tableName Table name
868
     * @return array
869
     */
870
    protected function getForeignKeys($tableName)
871 1
    {
872
        $parts = $this->getSchemaName($tableName);
873 1
        $foreignKeys = [];
874 1
        $rows = $this->fetchAll(sprintf(
875 1
            "SELECT
876 1
                    tc.constraint_name,
877
                    tc.table_name, kcu.column_name,
878
                    ccu.table_name AS referenced_table_name,
879
                    ccu.column_name AS referenced_column_name
880
                FROM
881
                    information_schema.table_constraints AS tc
882
                    JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
883
                    JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
884 68
                WHERE constraint_type = 'FOREIGN KEY' AND tc.table_schema = %s AND tc.table_name = %s
885
                ORDER BY kcu.position_in_unique_constraint",
886 68
            $this->getConnection()->quote($parts['schema']),
887 4
            $this->getConnection()->quote($parts['table'])
888 68
        ));
889 68
        foreach ($rows as $row) {
890 68
            $foreignKeys[$row['constraint_name']]['table'] = $row['table_name'];
891 68
            $foreignKeys[$row['constraint_name']]['columns'][] = $row['column_name'];
892
            $foreignKeys[$row['constraint_name']]['referenced_table'] = $row['referenced_table_name'];
893
            $foreignKeys[$row['constraint_name']]['referenced_columns'][] = $row['referenced_column_name'];
894
        }
895
896
        return $foreignKeys;
897
    }
898
899
    /**
900 68
     * @inheritDoc
901
     */
902 68
    protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey)
903 68
    {
904 50
        $alter = sprintf(
905 50
            'ADD %s',
906 68
            $this->getForeignKeySqlDefinition($foreignKey, $table->getName())
907 68
        );
908
909 68
        return new AlterInstructions([$alter]);
910 1
    }
911 1
912 1
    /**
913 1
     * @inheritDoc
914 1
     */
915 68
    protected function getDropForeignKeyInstructions($tableName, $constraint)
916
    {
917
        $alter = sprintf(
918
            'DROP CONSTRAINT %s',
919
            $this->quoteColumnName($constraint)
920
        );
921
922 68
        return new AlterInstructions([$alter]);
923 68
    }
924 68
925 68
    /**
926 68
     * @inheritDoc
927
     */
928
    protected function getDropForeignKeyByColumnsInstructions($tableName, $columns)
929 68
    {
930 68
        $instructions = new AlterInstructions();
931 68
932 68
        $parts = $this->getSchemaName($tableName);
933 1
        $sql = 'SELECT c.CONSTRAINT_NAME
934 1
                FROM (
935
                    SELECT CONSTRAINT_NAME, array_agg(COLUMN_NAME::varchar) as columns
936
                    FROM information_schema.KEY_COLUMN_USAGE
937 68
                    WHERE TABLE_SCHEMA = %s
938
                    AND TABLE_NAME IS NOT NULL
939 68
                    AND TABLE_NAME = %s
940 68
                    AND POSITION_IN_UNIQUE_CONSTRAINT IS NOT NULL
941 68
                    GROUP BY CONSTRAINT_NAME
942
                ) c
943 68
                WHERE
944
                    ARRAY[%s]::varchar[] <@ c.columns AND
945
                    ARRAY[%s]::varchar[] @> c.columns';
946
947
        $array = [];
948
        foreach ($columns as $col) {
949
            $array[] = "'$col'";
950
        }
951
952
        $rows = $this->fetchAll(sprintf(
953 6
            $sql,
954
            $this->getConnection()->quote($parts['schema']),
955
            $this->getConnection()->quote($parts['table']),
956 6
            implode(',', $array),
957 6
            implode(',', $array)
958 6
        ));
959
960 6
        foreach ($rows as $row) {
961 6
            $newInstr = $this->getDropForeignKeyInstructions($tableName, $row['constraint_name']);
962 6
            $instructions->merge($newInstr);
963 6
        }
964
965 6
        return $instructions;
966
    }
967
968
    /**
969
     * {@inheritDoc}
970
     *
971
     * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException
972
     */
973
    public function getSqlType($type, $limit = null)
974
    {
975 7
        switch ($type) {
976
            case static::PHINX_TYPE_TEXT:
977 7
            case static::PHINX_TYPE_TIME:
978 3
            case static::PHINX_TYPE_DATE:
979 3
            case static::PHINX_TYPE_BOOLEAN:
980 5
            case static::PHINX_TYPE_JSON:
981 5
            case static::PHINX_TYPE_JSONB:
982
            case static::PHINX_TYPE_UUID:
983
            case static::PHINX_TYPE_CIDR:
984 5
            case static::PHINX_TYPE_INET:
985
            case static::PHINX_TYPE_MACADDR:
986 7
            case static::PHINX_TYPE_TIMESTAMP:
987 7
            case static::PHINX_TYPE_INTEGER:
988 7
                return ['name' => $type];
989 7
            case static::PHINX_TYPE_TINY_INTEGER:
990 7
                return ['name' => 'smallint'];
991 7
            case static::PHINX_TYPE_SMALL_INTEGER:
992 7
                return ['name' => 'smallint'];
993 7
            case static::PHINX_TYPE_DECIMAL:
994
                return ['name' => $type, 'precision' => 18, 'scale' => 0];
995
            case static::PHINX_TYPE_DOUBLE:
996
                return ['name' => 'double precision'];
997
            case static::PHINX_TYPE_STRING:
998
                return ['name' => 'character varying', 'limit' => 255];
999
            case static::PHINX_TYPE_CHAR:
1000
                return ['name' => 'character', 'limit' => 255];
1001
            case static::PHINX_TYPE_BIG_INTEGER:
1002
                return ['name' => 'bigint'];
1003 3
            case static::PHINX_TYPE_FLOAT:
1004
                return ['name' => 'real'];
1005 3
            case static::PHINX_TYPE_DATETIME:
1006 3
                return ['name' => 'timestamp'];
1007 3
            case static::PHINX_TYPE_BINARYUUID:
1008 3
                return ['name' => 'uuid'];
1009
            case static::PHINX_TYPE_BLOB:
1010
            case static::PHINX_TYPE_BINARY:
1011 3
                return ['name' => 'bytea'];
1012
            case static::PHINX_TYPE_INTERVAL:
1013
                return ['name' => 'interval'];
1014 3
            // Geospatial database types
1015
            // Spatial storage in Postgres is done via the PostGIS extension,
1016
            // which enables the use of the "geography" type in combination
1017
            // with SRID 4326.
1018
            case static::PHINX_TYPE_GEOMETRY:
1019
                return ['name' => 'geography', 'type' => 'geometry', 'srid' => 4326];
1020 68
            case static::PHINX_TYPE_POINT:
1021
                return ['name' => 'geography', 'type' => 'point', 'srid' => 4326];
1022
            case static::PHINX_TYPE_LINESTRING:
1023 68
                return ['name' => 'geography', 'type' => 'linestring', 'srid' => 4326];
1024 67
            case static::PHINX_TYPE_POLYGON:
1025 67
                return ['name' => 'geography', 'type' => 'polygon', 'srid' => 4326];
1026
            default:
1027 68
                if ($this->isArrayType($type)) {
1028
                    return ['name' => $type];
1029 68
                }
1030 68
                // Return array type
1031
                throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not supported by Postgresql.');
1032
        }
1033
    }
1034
1035
    /**
1036
     * Returns Phinx type by SQL type
1037
     *
1038 68
     * @param string $sqlType SQL type
1039
     * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException
1040 68
     * @return string Phinx type
1041 68
     */
1042 68
    public function getPhinxType($sqlType)
1043
    {
1044
        switch ($sqlType) {
1045
            case 'character varying':
1046
            case 'varchar':
1047
                return static::PHINX_TYPE_STRING;
1048
            case 'character':
1049
            case 'char':
1050 68
                return static::PHINX_TYPE_CHAR;
1051
            case 'text':
1052 68
                return static::PHINX_TYPE_TEXT;
1053
            case 'json':
1054
                return static::PHINX_TYPE_JSON;
1055 68
            case 'jsonb':
1056
                return static::PHINX_TYPE_JSONB;
1057 68
            case 'smallint':
1058 68
                return static::PHINX_TYPE_SMALL_INTEGER;
1059 68
            case 'int':
1060
            case 'int4':
1061
            case 'integer':
1062
                return static::PHINX_TYPE_INTEGER;
1063
            case 'decimal':
1064
            case 'numeric':
1065
                return static::PHINX_TYPE_DECIMAL;
1066
            case 'bigint':
1067
            case 'int8':
1068 68
                return static::PHINX_TYPE_BIG_INTEGER;
1069
            case 'real':
1070 68
            case 'float4':
1071 68
                return static::PHINX_TYPE_FLOAT;
1072 68
            case 'double precision':
1073
                return static::PHINX_TYPE_DOUBLE;
1074
            case 'bytea':
1075
                return static::PHINX_TYPE_BINARY;
1076
            case 'interval':
1077
                return static::PHINX_TYPE_INTERVAL;
1078
            case 'time':
1079 68
            case 'timetz':
1080
            case 'time with time zone':
1081 68
            case 'time without time zone':
1082 68
                return static::PHINX_TYPE_TIME;
1083 68
            case 'date':
1084 68
                return static::PHINX_TYPE_DATE;
1085
            case 'timestamp':
1086
            case 'timestamptz':
1087
            case 'timestamp with time zone':
1088
            case 'timestamp without time zone':
1089
                return static::PHINX_TYPE_DATETIME;
1090
            case 'bool':
1091 68
            case 'boolean':
1092
                return static::PHINX_TYPE_BOOLEAN;
1093
            case 'uuid':
1094
                return static::PHINX_TYPE_UUID;
1095 68
            case 'cidr':
1096 68
                return static::PHINX_TYPE_CIDR;
1097 68
            case 'inet':
1098 68
                return static::PHINX_TYPE_INET;
1099 68
            case 'macaddr':
1100 68
                return static::PHINX_TYPE_MACADDR;
1101 68
            default:
1102
                throw new UnsupportedColumnTypeException('Column type "' . $sqlType . '" is not supported by Postgresql.');
1103
        }
1104
    }
1105
1106
    /**
1107 73
     * @inheritDoc
1108
     */
1109 73
    public function createDatabase($name, $options = [])
1110
    {
1111
        $charset = $options['charset'] ?? 'utf8';
1112
        $this->execute(sprintf("CREATE DATABASE %s WITH ENCODING = '%s'", $name, $charset));
1113
    }
1114
1115 73
    /**
1116
     * @inheritDoc
1117
     */
1118 73
    public function hasDatabase($name)
1119
    {
1120
        $sql = sprintf("SELECT count(*) FROM pg_database WHERE datname = '%s'", $name);
1121
        $result = $this->fetchRow($sql);
1122
1123
        return $result['count'] > 0;
1124
    }
1125
1126
    /**
1127 14
     * @inheritDoc
1128
     */
1129 14
    public function dropDatabase($name)
1130 1
    {
1131
        $this->disconnect();
1132
        $this->execute(sprintf('DROP DATABASE IF EXISTS %s', $name));
1133 13
        $this->createdTables = [];
1134 13
        $this->connect();
1135
    }
1136
1137
    /**
1138
     * Get the defintion for a `DEFAULT` statement.
1139
     *
1140
     * @param mixed $default default value
1141
     * @param string|null $columnType column type added
1142 68
     * @return string
1143
     */
1144 68
    protected function getDefaultValueDefinition($default, $columnType = null)
1145 68
    {
1146
        if (is_string($default) && $default !== 'CURRENT_TIMESTAMP') {
1147
            $default = $this->getConnection()->quote($default);
1148
        } elseif (is_bool($default)) {
1149
            $default = $this->castToBool($default);
1150
        } elseif ($columnType === static::PHINX_TYPE_BOOLEAN) {
1151 68
            $default = $this->castToBool((bool)$default);
1152
        }
1153 68
1154
        return isset($default) ? 'DEFAULT ' . $default : '';
1155
    }
1156
1157
    /**
1158
     * Gets the PostgreSQL Column Definition for a Column object.
1159
     *
1160
     * @param \Phinx\Db\Table\Column $column Column
1161
     * @return string
1162
     */
1163
    protected function getColumnSqlDefinition(Column $column)
1164
    {
1165
        $buffer = [];
1166
        if ($column->isIdentity()) {
1167
            $buffer[] = $column->getType() === 'biginteger' ? 'BIGSERIAL' : 'SERIAL';
1168
        } elseif ($column->getType() instanceof Literal) {
1169
            $buffer[] = (string)$column->getType();
1170
        } else {
1171
            $sqlType = $this->getSqlType($column->getType(), $column->getLimit());
1172
            $buffer[] = strtoupper($sqlType['name']);
1173
1174
            // integers cant have limits in postgres
1175
            if ($sqlType['name'] === static::PHINX_TYPE_DECIMAL && ($column->getPrecision() || $column->getScale())) {
1176
                $buffer[] = sprintf(
1177
                    '(%s, %s)',
1178
                    $column->getPrecision() ?: $sqlType['precision'],
1179
                    $column->getScale() ?: $sqlType['scale']
1180
                );
1181
            } elseif ($sqlType['name'] === self::PHINX_TYPE_GEOMETRY) {
1182
                // geography type must be written with geometry type and srid, like this: geography(POLYGON,4326)
1183
                $buffer[] = sprintf(
1184
                    '(%s,%s)',
1185
                    strtoupper($sqlType['type']),
1186
                    $column->getSrid() ?: $sqlType['srid']
1187
                );
1188
            } elseif (in_array($sqlType['name'], [self::PHINX_TYPE_TIME, self::PHINX_TYPE_TIMESTAMP], true)) {
1189
                if (is_numeric($column->getPrecision())) {
0 ignored issues
show
introduced by
The condition is_numeric($column->getPrecision()) is always true.
Loading history...
1190
                    $buffer[] = sprintf('(%s)', $column->getPrecision());
1191
                }
1192
1193
                if ($column->isTimezone()) {
1194
                    $buffer[] = strtoupper('with time zone');
1195
                }
1196
            } elseif (
1197
                !in_array($column->getType(), [
1198
                    self::PHINX_TYPE_TINY_INTEGER,
1199
                    self::PHINX_TYPE_SMALL_INTEGER,
1200
                    self::PHINX_TYPE_INTEGER,
1201
                    self::PHINX_TYPE_BIG_INTEGER,
1202
                    self::PHINX_TYPE_BOOLEAN,
1203
                    self::PHINX_TYPE_TEXT,
1204
                    self::PHINX_TYPE_BINARY,
1205
                ], true)
1206
            ) {
1207
                if ($column->getLimit() || isset($sqlType['limit'])) {
1208
                    $buffer[] = sprintf('(%s)', $column->getLimit() ?: $sqlType['limit']);
1209
                }
1210
            }
1211
        }
1212
1213
        $buffer[] = $column->isNull() ? 'NULL' : 'NOT NULL';
1214
1215
        if ($column->getDefault() !== null) {
1216
            $buffer[] = $this->getDefaultValueDefinition($column->getDefault(), $column->getType());
1217
        }
1218
1219
        return implode(' ', $buffer);
1220
    }
1221
1222
    /**
1223
     * Gets the PostgreSQL Column Comment Definition for a column object.
1224
     *
1225
     * @param \Phinx\Db\Table\Column $column Column
1226
     * @param string $tableName Table name
1227
     * @return string
1228
     */
1229
    protected function getColumnCommentSqlDefinition(Column $column, $tableName)
1230
    {
1231
        // passing 'null' is to remove column comment
1232
        $comment = strcasecmp($column->getComment(), 'NULL') !== 0
1233
                 ? $this->getConnection()->quote($column->getComment())
1234
                 : 'NULL';
1235
1236
        return sprintf(
1237
            'COMMENT ON COLUMN %s.%s IS %s;',
1238
            $this->quoteTableName($tableName),
1239
            $this->quoteColumnName($column->getName()),
1240
            $comment
1241
        );
1242
    }
1243
1244
    /**
1245
     * Gets the PostgreSQL Index Definition for an Index object.
1246
     *
1247
     * @param \Phinx\Db\Table\Index $index Index
1248
     * @param string $tableName Table name
1249
     * @return string
1250
     */
1251
    protected function getIndexSqlDefinition(Index $index, $tableName)
1252
    {
1253
        $parts = $this->getSchemaName($tableName);
1254
        $columnNames = $index->getColumns();
1255
1256
        if (is_string($index->getName())) {
1257
            $indexName = $index->getName();
1258
        } else {
1259
            $indexName = sprintf('%s_%s', $parts['table'], implode('_', $columnNames));
1260
        }
1261
1262
        $order = $index->getOrder() ?? [];
1263
        $columnNames = array_map(function ($columnName) use ($order) {
1264
            $ret = '"' . $columnName . '"';
1265
            if (isset($order[$columnName])) {
1266
                $ret .= ' ' . $order[$columnName];
1267
            }
1268
1269
            return $ret;
1270
        }, $columnNames);
1271
1272
        $includedColumns = $index->getInclude() ? sprintf('INCLUDE ("%s")', implode('","', $index->getInclude())) : '';
1273
1274
        $createIndexSentence = 'CREATE %s INDEX %s ON %s ';
1275
        if ($index->getType() === self::GIN_INDEX_TYPE) {
1276
            $createIndexSentence .= ' USING ' . $index->getType() . '(%s) %s;';
1277
        } else {
1278
            $createIndexSentence .= '(%s) %s;';
1279
        }
1280
1281
        return sprintf(
1282
            $createIndexSentence,
1283
            ($index->getType() === Index::UNIQUE ? 'UNIQUE' : ''),
1284
            $this->quoteColumnName($indexName),
1285
            $this->quoteTableName($tableName),
1286
            implode(',', $columnNames),
1287
            $includedColumns
1288
        );
1289
    }
1290
1291
    /**
1292
     * Gets the MySQL Foreign Key Definition for an ForeignKey object.
1293
     *
1294
     * @param \Phinx\Db\Table\ForeignKey $foreignKey Foreign key
1295
     * @param string $tableName Table name
1296
     * @return string
1297
     */
1298
    protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, $tableName)
1299
    {
1300
        $parts = $this->getSchemaName($tableName);
1301
1302
        $constraintName = $foreignKey->getConstraint() ?: ($parts['table'] . '_' . implode('_', $foreignKey->getColumns()) . '_fkey');
1303
        $def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName) .
1304
        ' FOREIGN KEY ("' . implode('", "', $foreignKey->getColumns()) . '")' .
1305
        " REFERENCES {$this->quoteTableName($foreignKey->getReferencedTable()->getName())} (\"" .
1306
        implode('", "', $foreignKey->getReferencedColumns()) . '")';
1307
        if ($foreignKey->getOnDelete()) {
1308
            $def .= " ON DELETE {$foreignKey->getOnDelete()}";
1309
        }
1310
        if ($foreignKey->getOnUpdate()) {
1311
            $def .= " ON UPDATE {$foreignKey->getOnUpdate()}";
1312
        }
1313
1314
        return $def;
1315
    }
1316
1317
    /**
1318
     * @inheritDoc
1319
     */
1320
    public function createSchemaTable()
1321
    {
1322
        // Create the public/custom schema if it doesn't already exist
1323
        if ($this->hasSchema($this->getGlobalSchemaName()) === false) {
1324
            $this->createSchema($this->getGlobalSchemaName());
1325
        }
1326
1327
        $this->setSearchPath();
1328
1329
        parent::createSchemaTable();
1330
    }
1331
1332
    /**
1333
     * @inheritDoc
1334
     */
1335
    public function getVersions()
1336
    {
1337
        $this->setSearchPath();
1338
1339
        return parent::getVersions();
1340
    }
1341
1342
    /**
1343
     * @inheritDoc
1344
     */
1345
    public function getVersionLog()
1346
    {
1347
        $this->setSearchPath();
1348
1349
        return parent::getVersionLog();
1350
    }
1351
1352
    /**
1353
     * Creates the specified schema.
1354
     *
1355
     * @param string $schemaName Schema Name
1356
     * @return void
1357
     */
1358
    public function createSchema($schemaName = 'public')
1359
    {
1360
        // from postgres 9.3 we can use "CREATE SCHEMA IF NOT EXISTS schema_name"
1361
        $sql = sprintf('CREATE SCHEMA IF NOT EXISTS %s', $this->quoteSchemaName($schemaName));
1362
        $this->execute($sql);
1363
    }
1364
1365
    /**
1366
     * Checks to see if a schema exists.
1367
     *
1368
     * @param string $schemaName Schema Name
1369
     * @return bool
1370
     */
1371
    public function hasSchema($schemaName)
1372
    {
1373
        $sql = sprintf(
1374
            'SELECT count(*)
1375
             FROM pg_namespace
1376
             WHERE nspname = %s',
1377
            $this->getConnection()->quote($schemaName)
1378
        );
1379
        $result = $this->fetchRow($sql);
1380
1381
        return $result['count'] > 0;
1382
    }
1383
1384
    /**
1385
     * Drops the specified schema table.
1386
     *
1387
     * @param string $schemaName Schema name
1388
     * @return void
1389
     */
1390
    public function dropSchema($schemaName)
1391
    {
1392
        $sql = sprintf('DROP SCHEMA IF EXISTS %s CASCADE', $this->quoteSchemaName($schemaName));
1393
        $this->execute($sql);
1394
1395
        foreach ($this->createdTables as $idx => $createdTable) {
1396
            if ($this->getSchemaName($createdTable)['schema'] === $this->quoteSchemaName($schemaName)) {
1397
                unset($this->createdTables[$idx]);
1398
            }
1399
        }
1400
    }
1401
1402
    /**
1403
     * Drops all schemas.
1404
     *
1405
     * @return void
1406
     */
1407
    public function dropAllSchemas()
1408
    {
1409
        foreach ($this->getAllSchemas() as $schema) {
1410
            $this->dropSchema($schema);
1411
        }
1412
    }
1413
1414
    /**
1415
     * Returns schemas.
1416
     *
1417
     * @return array
1418
     */
1419
    public function getAllSchemas()
1420
    {
1421
        $sql = "SELECT schema_name
1422
                FROM information_schema.schemata
1423
                WHERE schema_name <> 'information_schema' AND schema_name !~ '^pg_'";
1424
        $items = $this->fetchAll($sql);
1425
        $schemaNames = [];
1426
        foreach ($items as $item) {
1427
            $schemaNames[] = $item['schema_name'];
1428
        }
1429
1430
        return $schemaNames;
1431
    }
1432
1433
    /**
1434
     * @inheritDoc
1435
     */
1436
    public function getColumnTypes()
1437
    {
1438
        return array_merge(parent::getColumnTypes(), static::$specificColumnTypes);
1439
    }
1440
1441
    /**
1442
     * @inheritDoc
1443
     */
1444
    public function isValidColumnType(Column $column)
1445
    {
1446
        // If not a standard column type, maybe it is array type?
1447
        return parent::isValidColumnType($column) || $this->isArrayType($column->getType());
1448
    }
1449
1450
    /**
1451
     * Check if the given column is an array of a valid type.
1452
     *
1453
     * @param string $columnType Column type
1454
     * @return bool
1455
     */
1456
    protected function isArrayType($columnType)
1457
    {
1458
        if (!preg_match('/^([a-z]+)(?:\[\]){1,}$/', $columnType, $matches)) {
1459
            return false;
1460
        }
1461
1462
        $baseType = $matches[1];
1463
1464
        return in_array($baseType, $this->getColumnTypes(), true);
1465
    }
1466
1467
    /**
1468
     * @param string $tableName Table name
1469
     * @return array
1470
     */
1471
    protected function getSchemaName($tableName)
1472
    {
1473
        $schema = $this->getGlobalSchemaName();
1474
        $table = $tableName;
1475
        if (strpos($tableName, '.') !== false) {
1476
            [$schema, $table] = explode('.', $tableName);
1477
        }
1478
1479
        return [
1480
            'schema' => $schema,
1481
            'table' => $table,
1482
        ];
1483
    }
1484
1485
    /**
1486
     * Gets the schema name.
1487
     *
1488
     * @return string
1489
     */
1490
    protected function getGlobalSchemaName()
1491
    {
1492
        $options = $this->getOptions();
1493
1494
        return empty($options['schema']) ? 'public' : $options['schema'];
1495
    }
1496
1497
    /**
1498
     * @inheritDoc
1499
     */
1500
    public function castToBool($value)
1501
    {
1502
        return (bool)$value ? 'TRUE' : 'FALSE';
1503
    }
1504
1505
    /**
1506
     * @inheritDoc
1507
     */
1508
    public function getDecoratedConnection()
1509
    {
1510
        $options = $this->getOptions();
1511
        $options = [
1512
            'username' => $options['user'] ?? null,
1513
            'password' => $options['pass'] ?? null,
1514
            'database' => $options['name'],
1515
            'quoteIdentifiers' => true,
1516
        ] + $options;
1517
1518
        $driver = new PostgresDriver($options);
1519
1520
        $driver->setConnection($this->connection);
1521
1522
        return new Connection(['driver' => $driver] + $options);
1523
    }
1524
1525
    /**
1526
     * Sets search path of schemas to look through for a table
1527
     *
1528
     * @return void
1529
     */
1530
    public function setSearchPath()
1531
    {
1532
        $this->execute(
1533
            sprintf(
1534
                'SET search_path TO %s,"$user",public',
1535
                $this->quoteSchemaName($this->getGlobalSchemaName())
1536
            )
1537
        );
1538
    }
1539
}
1540