Passed
Pull Request — master (#1941)
by
unknown
05:30 queued 02:20
created

PostgresAdapter   F

Complexity

Total Complexity 239

Size/Duplication

Total Lines 1503
Duplicated Lines 0 %

Test Coverage

Coverage 92.21%

Importance

Changes 7
Bugs 0 Features 0
Metric Value
wmc 239
eloc 693
c 7
b 0
f 0
dl 0
loc 1503
ccs 568
cts 616
cp 0.9221
rs 1.907

61 Methods

Rating   Name   Duplication   Size   Complexity  
A commitTransaction() 0 3 1
A getAddColumnInstructions() 0 14 2
A quoteSchemaName() 0 3 1
A getChangePrimaryKeyInstructions() 0 37 5
A disconnect() 0 3 1
A hasColumn() 0 15 1
A hasTable() 0 19 2
A getRenameTableInstructions() 0 10 1
A truncateTable() 0 8 1
B connect() 0 44 9
A getDropTableInstructions() 0 6 1
A hasTransactions() 0 3 1
B getColumns() 0 70 11
A getRenameColumnInstructions() 0 28 2
A rollbackTransaction() 0 3 1
A quoteColumnName() 0 3 1
A quoteTableName() 0 5 1
A beginTransaction() 0 3 1
A getChangeCommentInstructions() 0 16 2
F createTable() 0 85 19
A getDropColumnInstructions() 0 8 1
A getVersions() 0 5 1
A getAddIndexInstructions() 0 6 1
A createSchema() 0 5 1
A getColumnTypes() 0 3 1
A getColumnCommentSqlDefinition() 0 12 2
D getColumnSqlDefinition() 0 57 20
B getChangeColumnInstructions() 0 70 6
A hasSchema() 0 11 1
A getDropIndexByNameInstructions() 0 10 1
A getIndexes() 0 39 3
A getDropForeignKeyByColumnsInstructions() 0 38 3
A getDropForeignKeyInstructions() 0 8 1
A hasIndex() 0 13 4
A getAddForeignKeyInstructions() 0 8 1
A getForeignKeys() 0 27 2
A getPrimaryKey() 0 27 2
A createSchemaTable() 0 10 2
A hasForeignKey() 0 22 6
A setSearchPath() 0 6 1
A hasPrimaryKey() 0 17 4
D getPhinxType() 0 61 36
A getGlobalSchemaName() 0 5 2
A getForeignKeySqlDefinition() 0 17 4
A createDatabase() 0 4 1
A dropAllSchemas() 0 4 2
A hasIndexByName() 0 10 3
A getSchemaName() 0 11 2
A dropDatabase() 0 6 1
A isValidColumnType() 0 4 2
A getVersionLog() 0 5 1
A isArrayType() 0 9 2
A getDefaultValueDefinition() 0 11 6
A castToBool() 0 3 2
A dropSchema() 0 8 3
A getDecoratedConnection() 0 15 1
A getIndexSqlDefinition() 0 30 5
A getDropIndexByColumnsInstructions() 0 22 4
D getSqlType() 0 59 31
A getAllSchemas() 0 12 2
A hasDatabase() 0 6 1

How to fix   Complexity   

Complex Class

Complex classes like PostgresAdapter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PostgresAdapter, and based on these observations, apply Extract Interface, too.

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