Completed
Push — master ( e33650...15989c )
by José
02:42
created

PostgresAdapter::getChangePrimaryKeyInstructions()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 38

Duplication

Lines 38
Ratio 100 %

Code Coverage

Tests 20
CRAP Score 5

Importance

Changes 0
Metric Value
dl 38
loc 38
rs 9.0008
c 0
b 0
f 0
ccs 20
cts 20
cp 1
cc 5
nc 8
nop 2
crap 5
1
<?php
2
/**
3
 * Phinx
4
 *
5
 * (The MIT license)
6
 * Copyright (c) 2015 Rob Morgan
7
 *
8
 * Permission is hereby granted, free of charge, to any person obtaining a copy
9
 * of this software and associated * documentation files (the "Software"), to
10
 * deal in the Software without restriction, including without limitation the
11
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
12
 * sell copies of the Software, and to permit persons to whom the Software is
13
 * furnished to do so, subject to the following conditions:
14
 *
15
 * The above copyright notice and this permission notice shall be included in
16
 * all copies or substantial portions of the Software.
17
 *
18
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
24
 * IN THE SOFTWARE.
25
 *
26
 * @package    Phinx
27
 * @subpackage Phinx\Db\Adapter
28
 */
29
namespace Phinx\Db\Adapter;
30
31
use Cake\Database\Connection;
32
use Cake\Database\Driver\Postgres as PostgresDriver;
33
use Phinx\Db\Table\Column;
34
use Phinx\Db\Table\ForeignKey;
35
use Phinx\Db\Table\Index;
36
use Phinx\Db\Table\Table;
37
use Phinx\Db\Util\AlterInstructions;
38
use Phinx\Util\Literal;
39
40
class PostgresAdapter extends PdoAdapter implements AdapterInterface
41
{
42
    const INT_SMALL = 65535;
43
44
    /**
45
     * Columns with comments
46
     *
47
     * @var array
48
     */
49
    protected $columnsWithComments = [];
50 68
51
    /**
52 68
     * {@inheritdoc}
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
            $db = null;
64 68
            $options = $this->getOptions();
65 68
66 1
            // if port is specified use it, otherwise use the PostgreSQL default
67 View Code Duplication
            if (isset($options['port'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
68
                $dsn = 'pgsql:host=' . $options['host'] . ';port=' . $options['port'] . ';dbname=' . $options['name'];
69
            } else {
70 68
                $dsn = 'pgsql:host=' . $options['host'] . ';dbname=' . $options['name'];
71 68
            }
72 1
73 1
            try {
74 1
                $db = new \PDO($dsn, $options['user'], $options['pass'], [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]);
75 1
            } catch (\PDOException $exception) {
76
                throw new \InvalidArgumentException(sprintf(
77
                    'There was a problem connecting to the database: %s',
78 68
                    $exception->getMessage()
79 68
                ), $exception->getCode(), $exception);
80 68
            }
81
82
            try {
83
                if (isset($options['schema'])) {
84
                    $db->exec('SET search_path TO ' . $options['schema']);
85 68
                }
86
            } catch (\PDOException $exception) {
87 68
                throw new \InvalidArgumentException(
88 68
                    sprintf('Schema does not exists: %s', $options['schema']),
89
                    $exception->getCode(),
90
                    $exception
91
                );
92
            }
93
94
            $this->setConnection($db);
95
        }
96
    }
97
98
    /**
99
     * {@inheritdoc}
100
     */
101
    public function disconnect()
102
    {
103
        $this->connection = null;
104
    }
105
106
    /**
107
     * {@inheritdoc}
108
     */
109
    public function hasTransactions()
110
    {
111
        return true;
112
    }
113
114
    /**
115
     * {@inheritdoc}
116
     */
117
    public function beginTransaction()
118
    {
119
        $this->execute('BEGIN');
120
    }
121
122
    /**
123
     * {@inheritdoc}
124
     */
125
    public function commitTransaction()
126
    {
127
        $this->execute('COMMIT');
128 68
    }
129
130 68
    /**
131
     * {@inheritdoc}
132
     */
133
    public function rollbackTransaction()
134
    {
135
        $this->execute('ROLLBACK');
136 68
    }
137
138 68
    /**
139
     * Quotes a schema name for use in a query.
140
     *
141
     * @param string $schemaName Schema Name
142
     * @return string
143
     */
144 68
    public function quoteSchemaName($schemaName)
145
    {
146 68
        return $this->quoteColumnName($schemaName);
147
    }
148
149
    /**
150
     * {@inheritdoc}
151
     */
152 68
    public function quoteTableName($tableName)
153
    {
154 68
        $parts = $this->getSchemaName($tableName);
155 68
156
        return $this->quoteSchemaName($parts['schema']) . '.' . $this->quoteColumnName($parts['table']);
157
    }
158
159 68
    /**
160 68
     * {@inheritdoc}
161 68
     */
162 68
    public function quoteColumnName($columnName)
163 68
    {
164
        return '"' . $columnName . '"';
165 68
    }
166
167
    /**
168
     * {@inheritdoc}
169
     */
170
    public function hasTable($tableName)
171 68
    {
172
        $parts = $this->getSchemaName($tableName);
173 68
        $result = $this->getConnection()->query(
174
            sprintf(
175
                'SELECT *
176 68
                FROM information_schema.tables
177 68
                WHERE table_schema = %s
178 48
                AND table_name = %s',
179 48
                $this->getConnection()->quote($parts['schema']),
180 48
                $this->getConnection()->quote($parts['table'])
181 48
            )
182
        );
183 48
184 48
        return $result->rowCount() === 1;
185 68
    }
186
187 2
    /**
188 2
     * {@inheritdoc}
189 2
     */
190 2
    public function createTable(Table $table, array $columns = [], array $indexes = [])
191
    {
192 2
        $options = $table->getOptions();
193 2
        $parts = $this->getSchemaName($table->getName());
194 2
195
         // Add the default primary key
196 View Code Duplication
        if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
197 68
            $column = new Column();
198 68
            $column->setName('id')
199
                   ->setType('integer')
200 68
                   ->setIdentity(true);
201 68
202 68
            array_unshift($columns, $column);
203
            $options['primary_key'] = 'id';
204
        } elseif (isset($options['id']) && is_string($options['id'])) {
205 68
            // Handle id => "field_name" to support AUTO_INCREMENT
206 6
            $column = new Column();
207 6
            $column->setName($options['id'])
208 68
                   ->setType('integer')
209
                   ->setIdentity(true);
210
211 68
            array_unshift($columns, $column);
212 68
            $options['primary_key'] = $options['id'];
213 68
        }
214 68
215 68
        // TODO - process table options like collation etc
216 68
        $sql = 'CREATE TABLE ';
217
        $sql .= $this->quoteTableName($table->getName()) . ' (';
218
219 1
        $this->columnsWithComments = [];
220 1 View Code Duplication
        foreach ($columns as $column) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
221 1
            $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column) . ', ';
222 1
223 1
            // set column comments, if needed
224 1
            if ($column->getComment()) {
225 1
                $this->columnsWithComments[] = $column;
226 1
            }
227 1
        }
228 1
229 68
         // set the primary key(s)
230 68 View Code Duplication
        if (isset($options['primary_key'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
231 2
            $sql = rtrim($sql);
232
            $sql .= sprintf(' CONSTRAINT %s PRIMARY KEY (', $this->quoteColumnName($parts['table'] . '_pkey'));
233
            if (is_string($options['primary_key'])) { // handle primary_key => 'id'
234
                $sql .= $this->quoteColumnName($options['primary_key']);
235 68
            } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id')
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
236 68
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key']));
237 1
            }
238 1
            $sql .= ')';
239 1
        } else {
240 1
            $sql = rtrim($sql, ', '); // no primary keys
241
        }
242 68
243
        $sql .= ');';
244
245 68
        // process column comments
246 6
        if (!empty($this->columnsWithComments)) {
247 6
            foreach ($this->columnsWithComments as $column) {
248 6
                $sql .= $this->getColumnCommentSqlDefinition($column, $table->getName());
249 6
            }
250
        }
251
252
        // set the indexes
253 68
        if (!empty($indexes)) {
254 68
            foreach ($indexes as $index) {
255 5
                $sql .= $this->getIndexSqlDefinition($index, $table->getName());
256 5
            }
257 5
        }
258 5
259
        // execute the sql
260
        $this->execute($sql);
261 68
262
        // process table comments
263
        if (isset($options['comment'])) {
264 68
            $sql = sprintf(
265 1
                'COMMENT ON TABLE %s IS %s',
266 1
                $this->quoteTableName($table->getName()),
267 1
                $this->getConnection()->quote($options['comment'])
268 1
            );
269 1
            $this->execute($sql);
270 1
        }
271 1
    }
272 68
273
    /**
274
     * {@inheritdoc}
275
     */
276 View Code Duplication
    protected function getChangePrimaryKeyInstructions(Table $table, $newColumns)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
277 1
    {
278
        $parts = $this->getSchemaName($table->getName());
279 1
280 1
        $instructions = new AlterInstructions();
281 1
282 1
        // Drop the existing primary key
283 1
        $primaryKey = $this->getPrimaryKey($table->getName());
284 1
        if (!empty($primaryKey['constraint'])) {
285 1
            $sql = sprintf(
286
                'DROP CONSTRAINT %s',
287
                $this->quoteColumnName($primaryKey['constraint'])
288
            );
289
            $instructions->addAlter($sql);
290 1
        }
291
292 1
        // Add the new primary key
293 1
        if (!empty($newColumns)) {
294
            $sql = sprintf(
295
                'ADD CONSTRAINT %s PRIMARY KEY (',
296
                $this->quoteColumnName($parts['table'] . '_pkey')
297
            );
298 1
            if (is_string($newColumns)) { // handle primary_key => 'id'
299
                $sql .= $this->quoteColumnName($newColumns);
300 1
            } elseif (is_array($newColumns)) { // handle primary_key => array('tag_id', 'resource_id')
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
301 1
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $newColumns));
302 1
            } else {
303 1
                throw new \InvalidArgumentException(sprintf(
304
                    "Invalid value for primary key: %s",
305 1
                    json_encode($newColumns)
306 1
                ));
307
            }
308
            $sql .= ')';
309
            $instructions->addAlter($sql);
310
        }
311 9
312
        return $instructions;
313 9
    }
314 9
315
    /**
316
     * {@inheritdoc}
317
     */
318 9
    protected function getChangeCommentInstructions(Table $table, $newComment)
319
    {
320 9
        $instructions = new AlterInstructions();
321 9
322
        // passing 'null' is to remove table comment
323 9
        $newComment = ($newComment !== null)
324 9
            ? $this->getConnection()->quote($newComment)
325 9
            : 'NULL';
326 9
        $sql = sprintf(
327 9
            'COMMENT ON TABLE %s IS %s',
328 9
            $this->quoteTableName($table->getName()),
329 9
            $newComment
330 9
        );
331 9
        $instructions->addPostStep($sql);
332
333 9
        return $instructions;
334 1
    }
335 1
336
    /**
337 9
     * {@inheritdoc}
338 5
     */
339 5 View Code Duplication
    protected function getRenameTableInstructions($tableName, $newTableName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
340 9
    {
341 9
        $sql = sprintf(
342 9
            'ALTER TABLE %s RENAME TO %s',
343
            $this->quoteTableName($tableName),
344
            $this->quoteColumnName($newTableName)
345
        );
346
347
        return new AlterInstructions([], [$sql]);
348 24
    }
349
350 24
    /**
351
     * {@inheritdoc}
352
     */
353 24
    protected function getDropTableInstructions($tableName)
354 24
    {
355 24
        $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
356
357 24
        return new AlterInstructions([], [$sql]);
358
    }
359 24
360 24
    /**
361
     * {@inheritdoc}
362
     */
363
    public function truncateTable($tableName)
364
    {
365
        $sql = sprintf(
366 18
            'TRUNCATE TABLE %s',
367
            $this->quoteTableName($tableName)
368 18
        );
369 18
370 18
        $this->execute($sql);
371 18
    }
372 18
373 18
    /**
374
     * {@inheritdoc}
375 18
     */
376 18
    public function getColumns($tableName)
377
    {
378
        $parts = $this->getSchemaName($tableName);
379
        $columns = [];
380
        $sql = sprintf(
381 3
            "SELECT column_name, data_type, udt_name, is_identity, is_nullable,
382
             column_default, character_maximum_length, numeric_precision, numeric_scale,
383 3
             datetime_precision
384
             FROM information_schema.columns
385
             WHERE table_schema = %s AND table_name = %s",
386 3
            $this->getConnection()->quote($parts['schema']),
387 3
            $this->getConnection()->quote($parts['table'])
388
        );
389 3
        $columnsInfo = $this->fetchAll($sql);
390 3
391 3
        foreach ($columnsInfo as $columnInfo) {
392 1
            $isUserDefined = strtoupper(trim($columnInfo['data_type'])) === 'USER-DEFINED';
393
394 2
            if ($isUserDefined) {
395 2
                $columnType = Literal::from($columnInfo['udt_name']);
396 2
            } else {
397 2
                $columnType = $this->getPhinxType($columnInfo['data_type']);
398 2
            }
399 2
400 2
            // If the default value begins with a ' or looks like a function mark it as literal
401 2
            if (isset($columnInfo['column_default'][0]) && $columnInfo['column_default'][0] === "'") {
402 2
                if (preg_match('/^\'(.*)\'::[^:]+$/', $columnInfo['column_default'], $match)) {
403
                    // '' and \' are replaced with a single '
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
404
                    $columnDefault = preg_replace('/[\'\\\\]\'/', "'", $match[1]);
405
                } else {
406
                    $columnDefault = Literal::from($columnInfo['column_default']);
407 5
                }
408
            } elseif (preg_match('/^\D[a-z_\d]*\(.*\)$/', $columnInfo['column_default'])) {
409
                $columnDefault = Literal::from($columnInfo['column_default']);
410
            } else {
411 5
                $columnDefault = $columnInfo['column_default'];
412 5
            }
413 5
414 5
            $column = new Column();
415 5
            $column->setName($columnInfo['column_name'])
416 5
                   ->setType($columnType)
417
                   ->setNull($columnInfo['is_nullable'] === 'YES')
418 5
                   ->setDefault($columnDefault)
419 5
                   ->setIdentity($columnInfo['is_identity'] === 'YES')
420
                   ->setScale($columnInfo['numeric_scale']);
421 5
422 5
            if (preg_match('/\bwith time zone$/', $columnInfo['data_type'])) {
423
                $column->setTimezone(true);
424 5
            }
425 5
426 5
            if (isset($columnInfo['character_maximum_length'])) {
427 5
                $column->setLimit($columnInfo['character_maximum_length']);
428 5
            }
429 5
430 2
            if (in_array($columnType, [static::PHINX_TYPE_TIME, static::PHINX_TYPE_DATETIME])) {
431 2
                $column->setPrecision($columnInfo['datetime_precision']);
432 4
            } else {
433
                $column->setPrecision($columnInfo['numeric_precision']);
434 5
            }
435 5
436
            $columns[] = $column;
437 1
        }
438 1
439 1
        return $columns;
440 1
    }
441 1
442 1
    /**
443 1
     * {@inheritdoc}
444 1
     */
445 1
    public function hasColumn($tableName, $columnName)
446
    {
447 4
        $parts = $this->getSchemaName($tableName);
448 4
        $sql = sprintf(
449 4
            "SELECT count(*)
450 4
            FROM information_schema.columns
451 4
            WHERE table_schema = %s AND table_name = %s AND column_name = %s",
452 4
            $this->getConnection()->quote($parts['schema']),
453 4
            $this->getConnection()->quote($parts['table']),
454
            $this->getConnection()->quote($columnName)
455
        );
456 5
457 1
        $result = $this->fetchRow($sql);
458 1
459 1
        return $result['count'] > 0;
460 1
    }
461 1
462 1
    /**
463 1
     * {@inheritdoc}
464 1
     */
465 1
    protected function getAddColumnInstructions(Table $table, Column $column)
466
    {
467
        $instructions = new AlterInstructions();
468 5
        $instructions->addAlter(sprintf(
469 2
            'ADD %s %s',
470 2
            $this->quoteColumnName($column->getName()),
471 2
            $this->getColumnSqlDefinition($column)
472 5
        ));
473
474
        if ($column->getComment()) {
475
            $instructions->addPostStep($this->getColumnCommentSqlDefinition($column, $table->getName()));
476
        }
477 1
478
        return $instructions;
479 1
    }
480 1
481 1
    /**
482 1
     * {@inheritdoc}
483 1
     */
484 1
    protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName)
485 1
    {
486 1
        $parts = $this->getSchemaName($tableName);
487
        $sql = sprintf(
488
            "SELECT CASE WHEN COUNT(*) > 0 THEN 1 ELSE 0 END AS column_exists
489
             FROM information_schema.columns
490
             WHERE table_schema = %s AND table_name = %s AND column_name = %s",
491
            $this->getConnection()->quote($parts['schema']),
492
            $this->getConnection()->quote($parts['table']),
493
            $this->getConnection()->quote($columnName)
494 9
        );
495
496 9
        $result = $this->fetchRow($sql);
497
        if (!(bool)$result['column_exists']) {
498
            throw new \InvalidArgumentException("The specified column does not exist: $columnName");
499
        }
500
501
        $instructions = new AlterInstructions();
502
        $instructions->addPostStep(
503
            sprintf(
504
                'ALTER TABLE %s RENAME COLUMN %s TO %s',
505
                $tableName,
506
                $this->quoteColumnName($columnName),
507
                $this->quoteColumnName($newColumnName)
508
            )
509
        );
510
511
        return $instructions;
512
    }
513
514 9
    /**
515 9
     * {@inheritdoc}
516 9
     */
517 9
    protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn)
518 9
    {
519 9
        $instructions = new AlterInstructions();
520 9
521 9
        $sql = sprintf(
522 9
            'ALTER COLUMN %s TYPE %s',
523
            $this->quoteColumnName($columnName),
524
            $this->getColumnSqlDefinition($newColumn)
525
        );
526
        //
527
        //NULL and DEFAULT cannot be set while changing column type
528 9
        $sql = preg_replace('/ NOT NULL/', '', $sql);
529
        $sql = preg_replace('/ NULL/', '', $sql);
530 9
        //If it is set, DEFAULT is the last definition
531 4
        $sql = preg_replace('/DEFAULT .*/', '', $sql);
532 4
533 9
        $instructions->addAlter($sql);
534 9
535 9
        // process null
536 9
        $sql = sprintf(
537 9
            'ALTER COLUMN %s',
538
            $this->quoteColumnName($columnName)
539 8
        );
540 8
541
        if ($newColumn->isNull()) {
542
            $sql .= ' DROP NOT NULL';
543
        } else {
544
            $sql .= ' SET NOT NULL';
545
        }
546 1
547
        $instructions->addAlter($sql);
548 1
549 1
        if (!is_null($newColumn->getDefault())) {
550 1
            $instructions->addAlter(sprintf(
551 1
                'ALTER COLUMN %s SET %s',
552
                $this->quoteColumnName($columnName),
553
                $this->getDefaultValueDefinition($newColumn->getDefault(), $newColumn->getType())
0 ignored issues
show
Bug introduced by
It seems like $newColumn->getType() targeting Phinx\Db\Table\Column::getType() can also be of type object<Phinx\Util\Literal>; however, Phinx\Db\Adapter\Postgre...efaultValueDefinition() does only seem to accept string|null, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
554
            ));
555
        } else {
556
            //drop default
557
            $instructions->addAlter(sprintf(
558
                'ALTER COLUMN %s DROP DEFAULT',
559
                $this->quoteColumnName($columnName)
560 2
            ));
561
        }
562 2
563 2
        // rename column
564 2
        if ($columnName !== $newColumn->getName()) {
565
            $instructions->addPostStep(sprintf(
566
                'ALTER TABLE %s RENAME COLUMN %s TO %s',
567
                $this->quoteTableName($tableName),
568
                $this->quoteColumnName($columnName),
569 1
                $this->quoteColumnName($newColumn->getName())
570
            ));
571 1
        }
572 1
573 1
        // change column comment if needed
574
        if ($newColumn->getComment()) {
575 1
            $instructions->addPostStep($this->getColumnCommentSqlDefinition($newColumn, $tableName));
576 1
        }
577
578 1
        return $instructions;
579 1
    }
580 1
581 1
    /**
582 1
     * {@inheritdoc}
583 1
     */
584 1
    protected function getDropColumnInstructions($tableName, $columnName)
585 1
    {
586 1
        $alter = sprintf(
587
            'DROP COLUMN %s',
588 1
            $this->quoteColumnName($columnName)
589
        );
590
591
        return new AlterInstructions([$alter]);
592
    }
593
594
    /**
595
     * Get an array of indexes from a particular table.
596 1
     *
597
     * @param string $tableName Table Name
598 1
     * @return array
599 1
     */
600
    protected function getIndexes($tableName)
601 1
    {
602 1
        $parts = $this->getSchemaName($tableName);
603 1
604
        $indexes = [];
605
        $sql = sprintf(
606
            "SELECT
607
                i.relname AS index_name,
608 3
                a.attname AS column_name
609
            FROM
610 3
                pg_class t,
611 1
                pg_class i,
612 1
                pg_index ix,
613 3
                pg_attribute a,
614 3
                pg_namespace nsp
615
            WHERE
616
                t.oid = ix.indrelid
617
                AND i.oid = ix.indexrelid
618
                AND a.attrelid = t.oid
619
                AND a.attnum = ANY(ix.indkey)
620 3
                AND t.relnamespace = nsp.oid
621 3
                AND nsp.nspname = %s
622 3
                AND t.relkind = 'r'
623 3
                AND t.relname = %s
624
            ORDER BY
625 1
                t.relname,
626 1
                i.relname",
627
            $this->getConnection()->quote($parts['schema']),
628
            $this->getConnection()->quote($parts['table'])
629
        );
630
        $rows = $this->fetchAll($sql);
631 View Code Duplication
        foreach ($rows as $row) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
632
            if (!isset($indexes[$row['index_name']])) {
633
                $indexes[$row['index_name']] = ['columns' => []];
634
            }
635
            $indexes[$row['index_name']]['columns'][] = $row['column_name'];
636 3
        }
637
638 3
        return $indexes;
639 3
    }
640
641
    /**
642
     * {@inheritdoc}
643
     */
644 View Code Duplication
    public function hasIndex($tableName, $columns)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
645
    {
646
        if (is_string($columns)) {
647
            $columns = [$columns];
648
        }
649
        $indexes = $this->getIndexes($tableName);
650 3
        foreach ($indexes as $index) {
651
            if (array_diff($index['columns'], $columns) === array_diff($columns, $index['columns'])) {
652 3
                return true;
653 3
            }
654 3
        }
655 3
656 3
        return false;
657 3
    }
658 3
659 3
    /**
660
     * {@inheritdoc}
661
     */
662 View Code Duplication
    public function hasIndexByName($tableName, $indexName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
663
    {
664
        $indexes = $this->getIndexes($tableName);
665 2
        foreach ($indexes as $name => $index) {
666
            if ($name === $indexName) {
667 2
                return true;
668 2
            }
669 2
        }
670 2
671 2
        return false;
672 2
    }
673 2
674
    /**
675
     * {@inheritdoc}
676
     */
677
    protected function getAddIndexInstructions(Table $table, Index $index)
678 1
    {
679
        $instructions = new AlterInstructions();
680 1
        $instructions->addPostStep($this->getIndexSqlDefinition($index, $table->getName()));
681
682
        return $instructions;
683
    }
684 1
685 1
    /**
686 1
     * {@inheritdoc}
687 1
     */
688 1
    protected function getDropIndexByColumnsInstructions($tableName, $columns)
689
    {
690 1
        $parts = $this->getSchemaName($tableName);
691 1
692 1
        if (is_string($columns)) {
693 1
            $columns = [$columns]; // str to array
694 1
        }
695
696
        $indexes = $this->getIndexes($tableName);
697
        foreach ($indexes as $indexName => $index) {
698
            $a = array_diff($columns, $index['columns']);
699
            if (empty($a)) {
700
                return new AlterInstructions([], [sprintf(
701 1
                    'DROP INDEX IF EXISTS %s',
702 1
                    '"' . ($parts['schema'] . '".' . $this->quoteColumnName($indexName))
703
                )]);
704 1
            }
705
        }
706 1
707 1
        throw new \InvalidArgumentException(sprintf(
708 1
            "The specified index on columns '%s' does not exist",
709 1
            implode(',', $columns)
710
        ));
711 1
    }
712
713
    /**
714
     * {@inheritdoc}
715
     */
716 68
    protected function getDropIndexByNameInstructions($tableName, $indexName)
717
    {
718
        $parts = $this->getSchemaName($tableName);
719 68
720 14
        $sql = sprintf(
721
            'DROP INDEX IF EXISTS %s',
722 1
            '"' . ($parts['schema'] . '".' . $this->quoteColumnName($indexName))
723
        );
724 1
725
        return new AlterInstructions([], [$sql]);
726 14
    }
727 68
728 68
    /**
729 68
     * {@inheritdoc}
730 68
     */
731 68 View Code Duplication
    public function hasPrimaryKey($tableName, $columns, $constraint = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
732 68
    {
733 68
        $primaryKey = $this->getPrimaryKey($tableName);
734 68
735 68
        if (empty($primaryKey)) {
736 68
            return false;
737 68
        }
738 68
739 2
        if ($constraint) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $constraint of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
740 68
            return ($primaryKey['constraint'] === $constraint);
741 68
        } else {
742 68
            if (is_string($columns)) {
743
                $columns = [$columns]; // str to array
744 68
            }
745 68
            $missingColumns = array_diff($columns, $primaryKey['columns']);
746 68
747 1
            return empty($missingColumns);
748 68
        }
749 68
    }
750 68
751 15
    /**
752 15
     * Get the primary key from a particular table.
753 1
     *
754
     * @param string $tableName Table Name
755
     * @return array
756
     */
757
    public function getPrimaryKey($tableName)
758 14
    {
759
        $parts = $this->getSchemaName($tableName);
760
        $rows = $this->fetchAll(sprintf(
761 14
            "SELECT
762
                    tc.constraint_name,
763
                    kcu.column_name
764 14
                FROM information_schema.table_constraints AS tc
765
                JOIN information_schema.key_column_usage AS kcu
766
                    ON tc.constraint_name = kcu.constraint_name
767 14
                WHERE constraint_type = 'PRIMARY KEY'
768
                    AND tc.table_schema = %s
769
                    AND tc.table_name = %s
770 14
                ORDER BY kcu.position_in_unique_constraint",
771 14
            $this->getConnection()->quote($parts['schema']),
772 13
            $this->getConnection()->quote($parts['table'])
773
        ));
774
775 1
        $primaryKey = [
776 14
            'columns' => [],
777
        ];
778
        foreach ($rows as $row) {
779
            $primaryKey['constraint'] = $row['constraint_name'];
780
            $primaryKey['columns'][] = $row['column_name'];
781
        }
782
783
        return $primaryKey;
784
    }
785 10
786
    /**
787
     * {@inheritdoc}
788 10
     */
789 10 View Code Duplication
    public function hasForeignKey($tableName, $columns, $constraint = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
790 6
    {
791 10
        if (is_string($columns)) {
792 10
            $columns = [$columns]; // str to array
793
        }
794 10
        $foreignKeys = $this->getForeignKeys($tableName);
795 2
        if ($constraint) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $constraint of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
796 10
            if (isset($foreignKeys[$constraint])) {
797
                return !empty($foreignKeys[$constraint]);
798 10
            }
799
800 10
            return false;
801
        } else {
802 1
            foreach ($foreignKeys as $key) {
803
                $a = array_diff($columns, $key['columns']);
804 1
                if (empty($a)) {
805 10
                    return true;
806 10
                }
807 10
            }
808 9
809 5
            return false;
810 5
        }
811 3
    }
812 4
813 4
    /**
814 2
     * Get an array of foreign keys from a particular table.
815 4
     *
816 4
     * @param string $tableName Table Name
817 2
     * @return array
818 4
     */
819 1
    protected function getForeignKeys($tableName)
820
    {
821 4
        $parts = $this->getSchemaName($tableName);
822 4
        $foreignKeys = [];
823 4
        $rows = $this->fetchAll(sprintf(
824 4
            "SELECT
825 3
                    tc.constraint_name,
826 4
                    tc.table_name, kcu.column_name,
827 2
                    ccu.table_name AS referenced_table_name,
828 4
                    ccu.column_name AS referenced_column_name
829 4
                FROM
830 4
                    information_schema.table_constraints AS tc
831 4
                    JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
832 3
                    JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
833 3
                WHERE constraint_type = 'FOREIGN KEY' AND tc.table_schema = %s AND tc.table_name = %s
834 3
                ORDER BY kcu.position_in_unique_constraint",
835 3
            $this->getConnection()->quote($parts['schema']),
836 1
            $this->getConnection()->quote($parts['table'])
837 1
        ));
838
        foreach ($rows as $row) {
839
            $foreignKeys[$row['constraint_name']]['table'] = $row['table_name'];
840
            $foreignKeys[$row['constraint_name']]['columns'][] = $row['column_name'];
841
            $foreignKeys[$row['constraint_name']]['referenced_table'] = $row['referenced_table_name'];
842
            $foreignKeys[$row['constraint_name']]['referenced_columns'][] = $row['referenced_column_name'];
843
        }
844
845
        return $foreignKeys;
846
    }
847
848
    /**
849
     * {@inheritdoc}
850
     */
851 View Code Duplication
    protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
852 1
    {
853
        $alter = sprintf(
854 1
            'ADD %s',
855 1
            $this->getForeignKeySqlDefinition($foreignKey, $table->getName())
856 1
        );
857
858
        return new AlterInstructions([$alter]);
859
    }
860
861 2
    /**
862
     * {@inheritdoc}
863 2
     */
864 2
    protected function getDropForeignKeyInstructions($tableName, $constraint)
865 2
    {
866
        $alter = sprintf(
867
            'DROP CONSTRAINT %s',
868
            $this->quoteColumnName($constraint)
869
        );
870
871 1
        return new AlterInstructions([$alter]);
872
    }
873 1
874 1
    /**
875 1
     * {@inheritdoc}
876 1
     */
877
    protected function getDropForeignKeyByColumnsInstructions($tableName, $columns)
878
    {
879
        $instructions = new AlterInstructions();
880
881
        $parts = $this->getSchemaName($tableName);
882
        $sql = "SELECT c.CONSTRAINT_NAME
883
                FROM (
884 68
                    SELECT CONSTRAINT_NAME, array_agg(COLUMN_NAME::varchar) as columns
885
                    FROM information_schema.KEY_COLUMN_USAGE
886 68
                    WHERE TABLE_SCHEMA = %s
887 4
                    AND TABLE_NAME IS NOT NULL
888 68
                    AND TABLE_NAME = %s
889 68
                    AND POSITION_IN_UNIQUE_CONSTRAINT IS NOT NULL
890 68
                    GROUP BY CONSTRAINT_NAME
891 68
                ) c
892
                WHERE
893
                    ARRAY[%s]::varchar[] <@ c.columns AND
894
                    ARRAY[%s]::varchar[] @> c.columns";
895
896
        $array = [];
897
        foreach ($columns as $col) {
898
            $array[] = "'$col'";
899
        }
900 68
901
        $rows = $this->fetchAll(sprintf(
902 68
            $sql,
903 68
            $this->getConnection()->quote($parts['schema']),
904 50
            $this->getConnection()->quote($parts['table']),
905 50
            implode(',', $array),
906 68
            implode(',', $array)
907 68
        ));
908
909 68
        foreach ($rows as $row) {
910 1
            $newInstr = $this->getDropForeignKeyInstructions($tableName, $row['constraint_name']);
911 1
            $instructions->merge($newInstr);
912 1
        }
913 1
914 1
        return $instructions;
915 68
    }
916
917
    /**
918
     * {@inheritdoc}
919
     */
920
    public function getSqlType($type, $limit = null)
921
    {
922 68
        switch ($type) {
923 68
            case static::PHINX_TYPE_TEXT:
924 68
            case static::PHINX_TYPE_TIME:
925 68
            case static::PHINX_TYPE_DATE:
926 68
            case static::PHINX_TYPE_BOOLEAN:
927
            case static::PHINX_TYPE_JSON:
928
            case static::PHINX_TYPE_JSONB:
929 68
            case static::PHINX_TYPE_UUID:
930 68
            case static::PHINX_TYPE_CIDR:
931 68
            case static::PHINX_TYPE_INET:
932 68
            case static::PHINX_TYPE_MACADDR:
933 1
            case static::PHINX_TYPE_TIMESTAMP:
934 1
                return ['name' => $type];
935
            case static::PHINX_TYPE_INTEGER:
936
                if ($limit && $limit == static::INT_SMALL) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $limit of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
937 68
                    return [
938
                        'name' => 'smallint',
939 68
                        'limit' => static::INT_SMALL
940 68
                    ];
941 68
                }
942
943 68
                return ['name' => $type];
944
            case static::PHINX_TYPE_DECIMAL:
945
                return ['name' => $type, 'precision' => 18, 'scale' => 0];
946
            case static::PHINX_TYPE_STRING:
947
                return ['name' => 'character varying', 'limit' => 255];
948
            case static::PHINX_TYPE_CHAR:
949
                return ['name' => 'character', 'limit' => 255];
950
            case static::PHINX_TYPE_BIG_INTEGER:
951
                return ['name' => 'bigint'];
952
            case static::PHINX_TYPE_FLOAT:
953 6
                return ['name' => 'real'];
954
            case static::PHINX_TYPE_DATETIME:
955
                return ['name' => 'timestamp'];
956 6
            case static::PHINX_TYPE_BLOB:
957 6
            case static::PHINX_TYPE_BINARY:
958 6
                return ['name' => 'bytea'];
959
            case static::PHINX_TYPE_INTERVAL:
960 6
                return ['name' => 'interval'];
961 6
            // Geospatial database types
962 6
            // Spatial storage in Postgres is done via the PostGIS extension,
963 6
            // which enables the use of the "geography" type in combination
964
            // with SRID 4326.
965 6
            case static::PHINX_TYPE_GEOMETRY:
966
                return ['name' => 'geography', 'type' => 'geometry', 'srid' => 4326];
967
            case static::PHINX_TYPE_POINT:
968
                return ['name' => 'geography', 'type' => 'point', 'srid' => 4326];
969
            case static::PHINX_TYPE_LINESTRING:
970
                return ['name' => 'geography', 'type' => 'linestring', 'srid' => 4326];
971
            case static::PHINX_TYPE_POLYGON:
972
                return ['name' => 'geography', 'type' => 'polygon', 'srid' => 4326];
973
            default:
974
                if ($this->isArrayType($type)) {
975 7
                    return ['name' => $type];
976
                }
977 7
                // Return array type
978 3
                throw new \RuntimeException('The type: "' . $type . '" is not supported');
979 3
        }
980 5
    }
981 5
982
    /**
983
     * Returns Phinx type by SQL type
984 5
     *
985
     * @param string $sqlType SQL type
986 7
     * @returns string Phinx type
987 7
     */
988 7
    public function getPhinxType($sqlType)
989 7
    {
990 7
        switch ($sqlType) {
991 7
            case 'character varying':
992 7
            case 'varchar':
993 7
                return static::PHINX_TYPE_STRING;
994
            case 'character':
995
            case 'char':
996
                return static::PHINX_TYPE_CHAR;
997
            case 'text':
998
                return static::PHINX_TYPE_TEXT;
999
            case 'json':
1000
                return static::PHINX_TYPE_JSON;
1001
            case 'jsonb':
1002
                return static::PHINX_TYPE_JSONB;
1003 3
            case 'smallint':
1004
                return [
1005 3
                    'name' => 'smallint',
1006 3
                    'limit' => static::INT_SMALL
1007 3
                ];
1008 3
            case 'int':
1009
            case 'int4':
1010
            case 'integer':
1011 3
                return static::PHINX_TYPE_INTEGER;
1012
            case 'decimal':
1013
            case 'numeric':
1014 3
                return static::PHINX_TYPE_DECIMAL;
1015
            case 'bigint':
1016
            case 'int8':
1017
                return static::PHINX_TYPE_BIG_INTEGER;
1018
            case 'real':
1019
            case 'float4':
1020 68
                return static::PHINX_TYPE_FLOAT;
1021
            case 'bytea':
1022
                return static::PHINX_TYPE_BINARY;
1023 68
            case 'interval':
1024 67
                return static::PHINX_TYPE_INTERVAL;
1025 67
            case 'time':
1026
            case 'timetz':
1027 68
            case 'time with time zone':
1028
            case 'time without time zone':
1029 68
                return static::PHINX_TYPE_TIME;
1030 68
            case 'date':
1031
                return static::PHINX_TYPE_DATE;
1032
            case 'timestamp':
1033
            case 'timestamptz':
1034
            case 'timestamp with time zone':
1035
            case 'timestamp without time zone':
1036
                return static::PHINX_TYPE_DATETIME;
1037
            case 'bool':
1038 68
            case 'boolean':
1039
                return static::PHINX_TYPE_BOOLEAN;
1040 68
            case 'uuid':
1041 68
                return static::PHINX_TYPE_UUID;
1042 68
            case 'cidr':
1043
                return static::PHINX_TYPE_CIDR;
1044
            case 'inet':
1045
                return static::PHINX_TYPE_INET;
1046
            case 'macaddr':
1047
                return static::PHINX_TYPE_MACADDR;
1048
            default:
1049
                throw new \RuntimeException('The PostgreSQL type: "' . $sqlType . '" is not supported');
1050 68
        }
1051
    }
1052 68
1053
    /**
1054
     * {@inheritdoc}
1055 68
     */
1056
    public function createDatabase($name, $options = [])
1057 68
    {
1058 68
        $charset = isset($options['charset']) ? $options['charset'] : 'utf8';
1059 68
        $this->execute(sprintf("CREATE DATABASE %s WITH ENCODING = '%s'", $name, $charset));
1060
    }
1061
1062
    /**
1063
     * {@inheritdoc}
1064
     */
1065
    public function hasDatabase($name)
1066
    {
1067
        $sql = sprintf("SELECT count(*) FROM pg_database WHERE datname = '%s'", $name);
1068 68
        $result = $this->fetchRow($sql);
1069
1070 68
        return $result['count'] > 0;
1071 68
    }
1072 68
1073
    /**
1074
     * {@inheritdoc}
1075
     */
1076
    public function dropDatabase($name)
1077
    {
1078
        $this->disconnect();
1079 68
        $this->execute(sprintf('DROP DATABASE IF EXISTS %s', $name));
1080
        $this->connect();
1081 68
    }
1082 68
1083 68
    /**
1084 68
     * Get the defintion for a `DEFAULT` statement.
1085
     *
1086
     * @param mixed $default default value
1087
     * @param string $columnType column type added
1088
     * @return string
1089
     */
1090 View Code Duplication
    protected function getDefaultValueDefinition($default, $columnType = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1091 68
    {
1092
        if (is_string($default) && 'CURRENT_TIMESTAMP' !== $default) {
1093
            $default = $this->getConnection()->quote($default);
1094
        } elseif (is_bool($default)) {
1095 68
            $default = $this->castToBool($default);
1096 68
        } elseif ($columnType === static::PHINX_TYPE_BOOLEAN) {
1097 68
            $default = $this->castToBool((bool)$default);
1098 68
        }
1099 68
1100 68
        return isset($default) ? 'DEFAULT ' . $default : '';
1101 68
    }
1102
1103
    /**
1104
     * Gets the PostgreSQL Column Definition for a Column object.
1105
     *
1106
     * @param \Phinx\Db\Table\Column $column Column
1107 73
     * @return string
1108
     */
1109 73
    protected function getColumnSqlDefinition(Column $column)
1110
    {
1111
        $buffer = [];
1112
        if ($column->isIdentity()) {
1113
            $buffer[] = $column->getType() == 'biginteger' ? 'BIGSERIAL' : 'SERIAL';
1114
        } elseif ($column->getType() instanceof Literal) {
1115 73
            $buffer[] = (string)$column->getType();
1116
        } else {
1117
            $sqlType = $this->getSqlType($column->getType(), $column->getLimit());
1118 73
            $buffer[] = strtoupper($sqlType['name']);
1119
1120
            // integers cant have limits in postgres
1121
            if (static::PHINX_TYPE_DECIMAL === $sqlType['name'] && ($column->getPrecision() || $column->getScale())) {
1122
                $buffer[] = sprintf(
1123
                    '(%s, %s)',
1124
                    $column->getPrecision() ?: $sqlType['precision'],
1125
                    $column->getScale() ?: $sqlType['scale']
1126
                );
1127 14
            } elseif (in_array($sqlType['name'], ['geography'])) {
1128
                // geography type must be written with geometry type and srid, like this: geography(POLYGON,4326)
1129 14
                $buffer[] = sprintf(
1130 1
                    '(%s,%s)',
1131
                    strtoupper($sqlType['type']),
1132
                    $sqlType['srid']
1133 13
                );
1134 13
            } elseif (!in_array($sqlType['name'], ['integer', 'smallint', 'bigint', 'boolean'])) {
1135
                if ($column->getLimit() || isset($sqlType['limit'])) {
1136
                    $buffer[] = sprintf('(%s)', $column->getLimit() ?: $sqlType['limit']);
1137
                }
1138
            }
1139
1140
            $timeTypes = [
1141
                'time',
1142 68
                'timestamp',
1143
            ];
1144 68
1145 68
            if (in_array($sqlType['name'], $timeTypes) && is_numeric($column->getPrecision())) {
1146
                $buffer[] = sprintf('(%s)', $column->getPrecision());
1147
            }
1148
1149
            if (in_array($sqlType['name'], $timeTypes) && $column->isTimezone()) {
1150
                $buffer[] = strtoupper('with time zone');
1151 68
            }
1152
        }
1153 68
1154
        $buffer[] = $column->isNull() ? 'NULL' : 'NOT NULL';
1155
1156
        if (!is_null($column->getDefault())) {
1157
            $buffer[] = $this->getDefaultValueDefinition($column->getDefault(), $column->getType());
0 ignored issues
show
Bug introduced by
It seems like $column->getType() targeting Phinx\Db\Table\Column::getType() can also be of type object<Phinx\Util\Literal>; however, Phinx\Db\Adapter\Postgre...efaultValueDefinition() does only seem to accept string|null, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1158
        }
1159
1160
        return implode(' ', $buffer);
1161
    }
1162
1163
    /**
1164
     * Gets the PostgreSQL Column Comment Definition for a column object.
1165
     *
1166
     * @param \Phinx\Db\Table\Column $column Column
1167
     * @param string $tableName Table name
1168
     * @return string
1169
     */
1170
    protected function getColumnCommentSqlDefinition(Column $column, $tableName)
1171
    {
1172
        // passing 'null' is to remove column comment
1173
        $comment = (strcasecmp($column->getComment(), 'NULL') !== 0)
1174
                 ? $this->getConnection()->quote($column->getComment())
1175
                 : 'NULL';
1176
1177
        return sprintf(
1178
            'COMMENT ON COLUMN %s.%s IS %s;',
1179
            $this->quoteTableName($tableName),
1180
            $this->quoteColumnName($column->getName()),
1181
            $comment
1182
        );
1183
    }
1184
1185
    /**
1186
     * Gets the PostgreSQL Index Definition for an Index object.
1187
     *
1188
     * @param \Phinx\Db\Table\Index  $index Index
1189
     * @param string $tableName Table name
1190
     * @return string
1191
     */
1192
    protected function getIndexSqlDefinition(Index $index, $tableName)
1193
    {
1194
        $parts = $this->getSchemaName($tableName);
1195
1196 View Code Duplication
        if (is_string($index->getName())) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1197
            $indexName = $index->getName();
1198
        } else {
1199
            $columnNames = $index->getColumns();
1200
            $indexName = sprintf('%s_%s', $parts['table'], implode('_', $columnNames));
1201
        }
1202
        $def = sprintf(
1203
            "CREATE %s INDEX %s ON %s (%s);",
1204
            ($index->getType() === Index::UNIQUE ? 'UNIQUE' : ''),
1205
            $this->quoteColumnName($indexName),
1206
            $this->quoteTableName($tableName),
1207
            implode(',', array_map([$this, 'quoteColumnName'], $index->getColumns()))
1208
        );
1209
1210
        return $def;
1211
    }
1212
1213
    /**
1214
     * Gets the MySQL Foreign Key Definition for an ForeignKey object.
1215
     *
1216
     * @param \Phinx\Db\Table\ForeignKey $foreignKey
1217
     * @param string     $tableName  Table name
1218
     * @return string
1219
     */
1220
    protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, $tableName)
1221
    {
1222
        $parts = $this->getSchemaName($tableName);
1223
1224
        $constraintName = $foreignKey->getConstraint() ?: ($parts['table'] . '_' . implode('_', $foreignKey->getColumns()) . '_fkey');
1225
        $def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName) .
0 ignored issues
show
Bug introduced by
It seems like $constraintName defined by $foreignKey->getConstrai...getColumns()) . '_fkey' on line 1224 can also be of type boolean; however, Phinx\Db\Adapter\Postgre...pter::quoteColumnName() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1226
        ' FOREIGN KEY ("' . implode('", "', $foreignKey->getColumns()) . '")' .
1227
        " REFERENCES {$this->quoteTableName($foreignKey->getReferencedTable()->getName())} (\"" .
1228
        implode('", "', $foreignKey->getReferencedColumns()) . '")';
1229
        if ($foreignKey->getOnDelete()) {
1230
            $def .= " ON DELETE {$foreignKey->getOnDelete()}";
1231
        }
1232
        if ($foreignKey->getOnUpdate()) {
1233
            $def .= " ON UPDATE {$foreignKey->getOnUpdate()}";
1234
        }
1235
1236
        return $def;
1237
    }
1238
1239
    /**
1240
     * {@inheritdoc}
1241
     */
1242
    public function createSchemaTable()
1243
    {
1244
        // Create the public/custom schema if it doesn't already exist
1245
        if ($this->hasSchema($this->getGlobalSchemaName()) === false) {
1246
            $this->createSchema($this->getGlobalSchemaName());
1247
        }
1248
1249
        $this->fetchAll(sprintf('SET search_path TO %s', $this->getGlobalSchemaName()));
1250
1251
        parent::createSchemaTable();
1252
    }
1253
1254
    /**
1255
     * Creates the specified schema.
1256
     *
1257
     * @param  string $schemaName Schema Name
1258
     * @return void
1259
     */
1260
    public function createSchema($schemaName = 'public')
1261
    {
1262
        // from postgres 9.3 we can use "CREATE SCHEMA IF NOT EXISTS schema_name"
1263
        $sql = sprintf('CREATE SCHEMA %s;', $this->quoteSchemaName($schemaName));
1264
        $this->execute($sql);
1265
    }
1266
1267
    /**
1268
     * Checks to see if a schema exists.
1269
     *
1270
     * @param string $schemaName Schema Name
1271
     * @return bool
1272
     */
1273
    public function hasSchema($schemaName)
1274
    {
1275
        $sql = sprintf(
1276
            "SELECT count(*)
1277
             FROM pg_namespace
1278
             WHERE nspname = %s",
1279
            $this->getConnection()->quote($schemaName)
1280
        );
1281
        $result = $this->fetchRow($sql);
1282
1283
        return $result['count'] > 0;
1284
    }
1285
1286
    /**
1287
     * Drops the specified schema table.
1288
     *
1289
     * @param string $schemaName Schema name
1290
     * @return void
1291
     */
1292
    public function dropSchema($schemaName)
1293
    {
1294
        $sql = sprintf("DROP SCHEMA IF EXISTS %s CASCADE;", $this->quoteSchemaName($schemaName));
1295
        $this->execute($sql);
1296
    }
1297
1298
    /**
1299
     * Drops all schemas.
1300
     *
1301
     * @return void
1302
     */
1303
    public function dropAllSchemas()
1304
    {
1305
        foreach ($this->getAllSchemas() as $schema) {
1306
            $this->dropSchema($schema);
1307
        }
1308
    }
1309
1310
    /**
1311
     * Returns schemas.
1312
     *
1313
     * @return array
1314
     */
1315
    public function getAllSchemas()
1316
    {
1317
        $sql = "SELECT schema_name
1318
                FROM information_schema.schemata
1319
                WHERE schema_name <> 'information_schema' AND schema_name !~ '^pg_'";
1320
        $items = $this->fetchAll($sql);
1321
        $schemaNames = [];
1322
        foreach ($items as $item) {
1323
            $schemaNames[] = $item['schema_name'];
1324
        }
1325
1326
        return $schemaNames;
1327
    }
1328
1329
    /**
1330
     * {@inheritdoc}
1331
     */
1332
    public function getColumnTypes()
1333
    {
1334
        return array_merge(parent::getColumnTypes(), ['json', 'jsonb', 'cidr', 'inet', 'macaddr', 'interval']);
1335
    }
1336
1337
    /**
1338
     * {@inheritdoc}
1339
     */
1340
    public function isValidColumnType(Column $column)
1341
    {
1342
        // If not a standard column type, maybe it is array type?
1343
        return (parent::isValidColumnType($column) || $this->isArrayType($column->getType()));
0 ignored issues
show
Bug introduced by
It seems like $column->getType() targeting Phinx\Db\Table\Column::getType() can also be of type object<Phinx\Util\Literal>; however, Phinx\Db\Adapter\PostgresAdapter::isArrayType() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1344
    }
1345
1346
    /**
1347
     * Check if the given column is an array of a valid type.
1348
     *
1349
     * @param  string $columnType
1350
     * @return bool
1351
     */
1352
    protected function isArrayType($columnType)
1353
    {
1354
        if (!preg_match('/^([a-z]+)(?:\[\]){1,}$/', $columnType, $matches)) {
1355
            return false;
1356
        }
1357
1358
        $baseType = $matches[1];
1359
1360
        return in_array($baseType, $this->getColumnTypes());
1361
    }
1362
1363
    /**
1364
     * @param string $tableName Table name
1365
     * @return array
1366
     */
1367
    private function getSchemaName($tableName)
1368
    {
1369
        $schema = $this->getGlobalSchemaName();
1370
        $table = $tableName;
1371
        if (false !== strpos($tableName, '.')) {
1372
            list($schema, $table) = explode('.', $tableName);
1373
        }
1374
1375
        return [
1376
            'schema' => $schema,
1377
            'table' => $table,
1378
        ];
1379
    }
1380
1381
    /**
1382
     * Gets the schema name.
1383
     *
1384
     * @return string
1385
     */
1386
    private function getGlobalSchemaName()
1387
    {
1388
        $options = $this->getOptions();
1389
1390
        return empty($options['schema']) ? 'public' : $options['schema'];
1391
    }
1392
1393
    /**
1394
     * {@inheritdoc}
1395
     */
1396
    public function castToBool($value)
1397
    {
1398
        return (bool)$value ? 'TRUE' : 'FALSE';
1399
    }
1400
1401
    /**
1402
     * {@inheritDoc}
1403
     *
1404
     */
1405 View Code Duplication
    public function getDecoratedConnection()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1406
    {
1407
        $options = $this->getOptions();
1408
        $options = [
1409
            'username' => $options['user'],
1410
            'password' => $options['pass'],
1411
            'database' => $options['name'],
1412
            'quoteIdentifiers' => true,
1413
        ] + $options;
1414
1415
        $driver = new PostgresDriver($options);
1416
1417
        if (method_exists($driver, 'setConnection')) {
1418
            $driver->setConnection($this->connection);
0 ignored issues
show
Bug introduced by
It seems like $this->connection can be null; however, setConnection() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1419
        } else {
1420
            $driver->connection($this->connection);
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Database\Driver::connection() has been deprecated with message: 3.6.0 Use getConnection()/setConnection() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1421
        }
1422
1423
        return new Connection(['driver' => $driver] + $options);
1424
    }
1425
}
1426