Completed
Pull Request — master (#1393)
by
unknown
03:00
created

PostgresAdapter::disconnect()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 3
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 2
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
    protected function getChangeTableInstructions(Table $table, array $newOptions)
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 ((isset($newOptions['id']) || isset($newOptions['primary_key']))
285 1
            && !empty($primaryKey['constraint']))
286
        {
287
            $sql = sprintf(
288
                'DROP CONSTRAINT %s',
289
                $this->quoteColumnName($primaryKey['constraint'])
290 1
            );
291
            $instructions->addAlter($sql);
292 1
        }
293 1
294
        // Set the default primary key and add associated column
295 View Code Duplication
        if (isset($newOptions['id']) && $newOptions['id'] !== false) {
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...
296
            if ($newOptions['id'] === true) {
297
                $newOptions['primary_key'] = 'id';
298 1
            } else if (is_string($newOptions['id'])) {
299
                // Handle id => "field_name" to support AUTO_INCREMENT
300 1
                $newOptions['primary_key'] = $newOptions['id'];
301 1
            } else {
302 1
                throw new \InvalidArgumentException(sprintf(
303 1
                    "Invalid value for option 'id': %s",
304
                    json_encode($newOptions['id'])
305 1
                ));
306 1
            }
307
308
            if ($this->hasColumn($table->getName(), $newOptions['primary_key'])) {
309
                throw new \RuntimeException(sprintf(
310
                    "Tried to create primary key column %s for table %s, but that column already exists",
311 9
                    $this->quoteColumnName($newOptions['primary_key']),
312
                    $this->quoteTableName($table->getName())
313 9
                ));
314 9
            }
315
316
            $column = new Column();
317
            $column
318 9
                ->setName($newOptions['primary_key'])
319
                ->setType('integer')
320 9
                ->setIdentity(true);
321 9
            $instructions->merge($this->getAddColumnInstructions($table, $column));
322
        }
323 9
324 9
        // Add the primary key(s)
325 9 View Code Duplication
        if (isset($newOptions['primary_key']) && $newOptions['primary_key'] !== false) {
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...
326 9
            $sql = sprintf(
327 9
                'ADD CONSTRAINT %s PRIMARY KEY (',
328 9
                $this->quoteColumnName($parts['table'] . '_pkey')
329 9
            );
330 9
            if (is_string($newOptions['primary_key'])) { // handle primary_key => 'id'
331 9
                $sql .= $this->quoteColumnName($newOptions['primary_key']);
332
            } else if (is_array($newOptions['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...
333 9
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $newOptions['primary_key']));
334 1
            } else {
335 1
                throw new \InvalidArgumentException(sprintf(
336
                    "Invalid value for option 'primary_key': %s",
337 9
                    json_encode($newOptions['primary_key'])
338 5
                ));
339 5
            }
340 9
            $sql .= ')';
341 9
            $instructions->addAlter($sql);
342 9
        }
343
344
        // Process table comments
345
        if (array_key_exists('comment', $newOptions)) {
346
            // passing 'null' is to remove table comment
347
            $newComment = ($newOptions['comment'] !== null)
348 24
                ? $this->getConnection()->quote($newOptions['comment'])
349
                : 'NULL';
350 24
            $sql = sprintf(
351
                'COMMENT ON TABLE %s IS %s',
352
                $this->quoteTableName($table->getName()),
353 24
                $newComment
354 24
            );
355 24
            $instructions->addPostStep($sql);
356
        }
357 24
358
        return $instructions;
359 24
    }
360 24
361
    /**
362
     * {@inheritdoc}
363
     */
364 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...
365
    {
366 18
        $sql = sprintf(
367
            'ALTER TABLE %s RENAME TO %s',
368 18
            $this->quoteTableName($tableName),
369 18
            $this->quoteColumnName($newTableName)
370 18
        );
371 18
372 18
        return new AlterInstructions([], [$sql]);
373 18
    }
374
375 18
    /**
376 18
     * {@inheritdoc}
377
     */
378
    protected function getDropTableInstructions($tableName)
379
    {
380
        $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
381 3
382
        return new AlterInstructions([], [$sql]);
383 3
    }
384
385
    /**
386 3
     * {@inheritdoc}
387 3
     */
388
    public function truncateTable($tableName)
389 3
    {
390 3
        $sql = sprintf(
391 3
            'TRUNCATE TABLE %s',
392 1
            $this->quoteTableName($tableName)
393
        );
394 2
395 2
        $this->execute($sql);
396 2
    }
397 2
398 2
    /**
399 2
     * {@inheritdoc}
400 2
     */
401 2
    public function getColumns($tableName)
402 2
    {
403
        $parts = $this->getSchemaName($tableName);
404
        $columns = [];
405
        $sql = sprintf(
406
            "SELECT column_name, data_type, udt_name, is_identity, is_nullable,
407 5
             column_default, character_maximum_length, numeric_precision, numeric_scale,
408
             datetime_precision
409
             FROM information_schema.columns
410
             WHERE table_schema = %s AND table_name = %s",
411 5
            $this->getConnection()->quote($parts['schema']),
412 5
            $this->getConnection()->quote($parts['table'])
413 5
        );
414 5
        $columnsInfo = $this->fetchAll($sql);
415 5
416 5
        foreach ($columnsInfo as $columnInfo) {
417
            $isUserDefined = strtoupper(trim($columnInfo['data_type'])) === 'USER-DEFINED';
418 5
419 5
            if ($isUserDefined) {
420
                $columnType = Literal::from($columnInfo['udt_name']);
421 5
            } else {
422 5
                $columnType = $this->getPhinxType($columnInfo['data_type']);
423
            }
424 5
425 5
            // If the default value begins with a ' or looks like a function mark it as literal
426 5
            if (isset($columnInfo['column_default'][0]) && $columnInfo['column_default'][0] === "'") {
427 5
                if (preg_match('/^\'(.*)\'::[^:]+$/', $columnInfo['column_default'], $match)) {
428 5
                    // '' 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...
429 5
                    $columnDefault = preg_replace('/[\'\\\\]\'/', "'", $match[1]);
430 2
                } else {
431 2
                    $columnDefault = Literal::from($columnInfo['column_default']);
432 4
                }
433
            } elseif (preg_match('/^\D[a-z_\d]*\(.*\)$/', $columnInfo['column_default'])) {
434 5
                $columnDefault = Literal::from($columnInfo['column_default']);
435 5
            } else {
436
                $columnDefault = $columnInfo['column_default'];
437 1
            }
438 1
439 1
            $column = new Column();
440 1
            $column->setName($columnInfo['column_name'])
441 1
                   ->setType($columnType)
442 1
                   ->setNull($columnInfo['is_nullable'] === 'YES')
443 1
                   ->setDefault($columnDefault)
444 1
                   ->setIdentity($columnInfo['is_identity'] === 'YES')
445 1
                   ->setScale($columnInfo['numeric_scale']);
446
447 4
            if (preg_match('/\bwith time zone$/', $columnInfo['data_type'])) {
448 4
                $column->setTimezone(true);
449 4
            }
450 4
451 4
            if (isset($columnInfo['character_maximum_length'])) {
452 4
                $column->setLimit($columnInfo['character_maximum_length']);
453 4
            }
454
455
            if (in_array($columnType, [static::PHINX_TYPE_TIME, static::PHINX_TYPE_DATETIME])) {
456 5
                $column->setPrecision($columnInfo['datetime_precision']);
457 1
            } else {
458 1
                $column->setPrecision($columnInfo['numeric_precision']);
459 1
            }
460 1
461 1
            $columns[] = $column;
462 1
        }
463 1
464 1
        return $columns;
465 1
    }
466
467
    /**
468 5
     * {@inheritdoc}
469 2
     */
470 2
    public function hasColumn($tableName, $columnName)
471 2
    {
472 5
        $parts = $this->getSchemaName($tableName);
473
        $sql = sprintf(
474
            "SELECT count(*)
475
            FROM information_schema.columns
476
            WHERE table_schema = %s AND table_name = %s AND column_name = %s",
477 1
            $this->getConnection()->quote($parts['schema']),
478
            $this->getConnection()->quote($parts['table']),
479 1
            $this->getConnection()->quote($columnName)
480 1
        );
481 1
482 1
        $result = $this->fetchRow($sql);
483 1
484 1
        return $result['count'] > 0;
485 1
    }
486 1
487
    /**
488
     * {@inheritdoc}
489
     */
490
    protected function getAddColumnInstructions(Table $table, Column $column)
491
    {
492
        $instructions = new AlterInstructions();
493
        $instructions->addAlter(sprintf(
494 9
            'ADD %s %s',
495
            $this->quoteColumnName($column->getName()),
496 9
            $this->getColumnSqlDefinition($column)
497
        ));
498
499
        if ($column->getComment()) {
500
            $instructions->addPostStep($this->getColumnCommentSqlDefinition($column, $table->getName()));
501
        }
502
503
        return $instructions;
504
    }
505
506
    /**
507
     * {@inheritdoc}
508
     */
509
    protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName)
510
    {
511
        $parts = $this->getSchemaName($tableName);
512
        $sql = sprintf(
513
            "SELECT CASE WHEN COUNT(*) > 0 THEN 1 ELSE 0 END AS column_exists
514 9
             FROM information_schema.columns
515 9
             WHERE table_schema = %s AND table_name = %s AND column_name = %s",
516 9
            $this->getConnection()->quote($parts['schema']),
517 9
            $this->getConnection()->quote($parts['table']),
518 9
            $this->getConnection()->quote($columnName)
519 9
        );
520 9
521 9
        $result = $this->fetchRow($sql);
522 9
        if (!(bool)$result['column_exists']) {
523
            throw new \InvalidArgumentException("The specified column does not exist: $columnName");
524
        }
525
526
        $instructions = new AlterInstructions();
527
        $instructions->addPostStep(
528 9
            sprintf(
529
                'ALTER TABLE %s RENAME COLUMN %s TO %s',
530 9
                $tableName,
531 4
                $this->quoteColumnName($columnName),
532 4
                $this->quoteColumnName($newColumnName)
533 9
            )
534 9
        );
535 9
536 9
        return $instructions;
537 9
    }
538
539 8
    /**
540 8
     * {@inheritdoc}
541
     */
542
    protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn)
543
    {
544
        $instructions = new AlterInstructions();
545
546 1
        $sql = sprintf(
547
            'ALTER COLUMN %s TYPE %s',
548 1
            $this->quoteColumnName($columnName),
549 1
            $this->getColumnSqlDefinition($newColumn)
550 1
        );
551 1
        //
552
        //NULL and DEFAULT cannot be set while changing column type
553
        $sql = preg_replace('/ NOT NULL/', '', $sql);
554
        $sql = preg_replace('/ NULL/', '', $sql);
555
        //If it is set, DEFAULT is the last definition
556
        $sql = preg_replace('/DEFAULT .*/', '', $sql);
557
558
        $instructions->addAlter($sql);
559
560 2
        // process null
561
        $sql = sprintf(
562 2
            'ALTER COLUMN %s',
563 2
            $this->quoteColumnName($columnName)
564 2
        );
565
566
        if ($newColumn->isNull()) {
567
            $sql .= ' DROP NOT NULL';
568
        } else {
569 1
            $sql .= ' SET NOT NULL';
570
        }
571 1
572 1
        $instructions->addAlter($sql);
573 1
574
        if (!is_null($newColumn->getDefault())) {
575 1
            $instructions->addAlter(sprintf(
576 1
                'ALTER COLUMN %s SET %s',
577
                $this->quoteColumnName($columnName),
578 1
                $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...
579 1
            ));
580 1
        } else {
581 1
            //drop default
582 1
            $instructions->addAlter(sprintf(
583 1
                'ALTER COLUMN %s DROP DEFAULT',
584 1
                $this->quoteColumnName($columnName)
585 1
            ));
586 1
        }
587
588 1
        // rename column
589
        if ($columnName !== $newColumn->getName()) {
590
            $instructions->addPostStep(sprintf(
591
                'ALTER TABLE %s RENAME COLUMN %s TO %s',
592
                $this->quoteTableName($tableName),
593
                $this->quoteColumnName($columnName),
594
                $this->quoteColumnName($newColumn->getName())
595
            ));
596 1
        }
597
598 1
        // change column comment if needed
599 1
        if ($newColumn->getComment()) {
600
            $instructions->addPostStep($this->getColumnCommentSqlDefinition($newColumn, $tableName));
601 1
        }
602 1
603 1
        return $instructions;
604
    }
605
606
    /**
607
     * {@inheritdoc}
608 3
     */
609
    protected function getDropColumnInstructions($tableName, $columnName)
610 3
    {
611 1
        $alter = sprintf(
612 1
            'DROP COLUMN %s',
613 3
            $this->quoteColumnName($columnName)
614 3
        );
615
616
        return new AlterInstructions([$alter]);
617
    }
618
619
    /**
620 3
     * Get an array of indexes from a particular table.
621 3
     *
622 3
     * @param string $tableName Table Name
623 3
     * @return array
624
     */
625 1
    protected function getIndexes($tableName)
626 1
    {
627
        $parts = $this->getSchemaName($tableName);
628
629
        $indexes = [];
630
        $sql = sprintf(
631
            "SELECT
632
                i.relname AS index_name,
633
                a.attname AS column_name
634
            FROM
635
                pg_class t,
636 3
                pg_class i,
637
                pg_index ix,
638 3
                pg_attribute a,
639 3
                pg_namespace nsp
640
            WHERE
641
                t.oid = ix.indrelid
642
                AND i.oid = ix.indexrelid
643
                AND a.attrelid = t.oid
644
                AND a.attnum = ANY(ix.indkey)
645
                AND t.relnamespace = nsp.oid
646
                AND nsp.nspname = %s
647
                AND t.relkind = 'r'
648
                AND t.relname = %s
649
            ORDER BY
650 3
                t.relname,
651
                i.relname",
652 3
            $this->getConnection()->quote($parts['schema']),
653 3
            $this->getConnection()->quote($parts['table'])
654 3
        );
655 3
        $rows = $this->fetchAll($sql);
656 3 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...
657 3
            if (!isset($indexes[$row['index_name']])) {
658 3
                $indexes[$row['index_name']] = ['columns' => []];
659 3
            }
660
            $indexes[$row['index_name']]['columns'][] = $row['column_name'];
661
        }
662
663
        return $indexes;
664
    }
665 2
666
    /**
667 2
     * {@inheritdoc}
668 2
     */
669 2 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...
670 2
    {
671 2
        if (is_string($columns)) {
672 2
            $columns = [$columns];
673 2
        }
674
        $indexes = $this->getIndexes($tableName);
675
        foreach ($indexes as $index) {
676
            if (array_diff($index['columns'], $columns) === array_diff($columns, $index['columns'])) {
677
                return true;
678 1
            }
679
        }
680 1
681
        return false;
682
    }
683
684 1
    /**
685 1
     * {@inheritdoc}
686 1
     */
687 1 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...
688 1
    {
689
        $indexes = $this->getIndexes($tableName);
690 1
        foreach ($indexes as $name => $index) {
691 1
            if ($name === $indexName) {
692 1
                return true;
693 1
            }
694 1
        }
695
696
        return false;
697
    }
698
699
    /**
700
     * {@inheritdoc}
701 1
     */
702 1
    protected function getAddIndexInstructions(Table $table, Index $index)
703
    {
704 1
        $instructions = new AlterInstructions();
705
        $instructions->addPostStep($this->getIndexSqlDefinition($index, $table->getName()));
706 1
707 1
        return $instructions;
708 1
    }
709 1
710
    /**
711 1
     * {@inheritdoc}
712
     */
713
    protected function getDropIndexByColumnsInstructions($tableName, $columns)
714
    {
715
        $parts = $this->getSchemaName($tableName);
716 68
717
        if (is_string($columns)) {
718
            $columns = [$columns]; // str to array
719 68
        }
720 14
721
        $indexes = $this->getIndexes($tableName);
722 1
        foreach ($indexes as $indexName => $index) {
723
            $a = array_diff($columns, $index['columns']);
724 1
            if (empty($a)) {
725
                return new AlterInstructions([], [sprintf(
726 14
                    'DROP INDEX IF EXISTS %s',
727 68
                    '"' . ($parts['schema'] . '".' . $this->quoteColumnName($indexName))
728 68
                )]);
729 68
            }
730 68
        }
731 68
732 68
        throw new \InvalidArgumentException(sprintf(
733 68
            "The specified index on columns '%s' does not exist",
734 68
            implode(',', $columns)
735 68
        ));
736 68
    }
737 68
738 68
    /**
739 2
     * {@inheritdoc}
740 68
     */
741 68
    protected function getDropIndexByNameInstructions($tableName, $indexName)
742 68
    {
743
        $parts = $this->getSchemaName($tableName);
744 68
745 68
        $sql = sprintf(
746 68
            'DROP INDEX IF EXISTS %s',
747 1
            '"' . ($parts['schema'] . '".' . $this->quoteColumnName($indexName))
748 68
        );
749 68
750 68
        return new AlterInstructions([], [$sql]);
751 15
    }
752 15
753 1
    /**
754
     * {@inheritdoc}
755
     */
756 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...
757
    {
758 14
        $primaryKey = $this->getPrimaryKey($tableName);
759
760
        if (empty($primaryKey)) {
761 14
            return false;
762
        }
763
764 14
        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...
765
            return ($primaryKey['constraint'] === $constraint);
766
        } else {
767 14
            if (is_string($columns)) {
768
                $columns = [$columns]; // str to array
769
            }
770 14
            $missingColumns = array_diff($columns, $primaryKey['columns']);
771 14
            return empty($missingColumns);
772 13
        }
773
    }
774
775 1
    /**
776 14
     * Get the primary key from a particular table.
777
     *
778
     * @param string $tableName Table Name
779
     * @return array
780
     */
781
    public function getPrimaryKey($tableName)
782
    {
783
        $parts = $this->getSchemaName($tableName);
784
        $rows = $this->fetchAll(sprintf(
785 10
            "SELECT
786
                    tc.constraint_name,
787
                    kcu.column_name
788 10
                FROM information_schema.table_constraints AS tc
789 10
                JOIN information_schema.key_column_usage AS kcu
790 6
                    ON tc.constraint_name = kcu.constraint_name
791 10
                WHERE constraint_type = 'PRIMARY KEY'
792 10
                    AND tc.table_schema = %s
793
                    AND tc.table_name = %s
794 10
                ORDER BY kcu.position_in_unique_constraint",
795 2
            $this->getConnection()->quote($parts['schema']),
796 10
            $this->getConnection()->quote($parts['table'])
797
        ));
798 10
799
        $primaryKey = [
800 10
            'columns' => [],
801
        ];
802 1
        foreach ($rows as $row) {
803
            $primaryKey['constraint'] = $row['constraint_name'];
804 1
            $primaryKey['columns'][] = $row['column_name'];
805 10
        }
806 10
        return $primaryKey;
807 10
    }
808 9
809 5
    /**
810 5
     * {@inheritdoc}
811 3
     */
812 4 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...
813 4
    {
814 2
        if (is_string($columns)) {
815 4
            $columns = [$columns]; // str to array
816 4
        }
817 2
        $foreignKeys = $this->getForeignKeys($tableName);
818 4
        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...
819 1
            if (isset($foreignKeys[$constraint])) {
820
                return !empty($foreignKeys[$constraint]);
821 4
            }
822 4
823 4
            return false;
824 4
        } else {
825 3
            foreach ($foreignKeys as $key) {
826 4
                $a = array_diff($columns, $key['columns']);
827 2
                if (empty($a)) {
828 4
                    return true;
829 4
                }
830 4
            }
831 4
832 3
            return false;
833 3
        }
834 3
    }
835 3
836 1
    /**
837 1
     * Get an array of foreign keys from a particular table.
838
     *
839
     * @param string $tableName Table Name
840
     * @return array
841
     */
842
    protected function getForeignKeys($tableName)
843
    {
844
        $parts = $this->getSchemaName($tableName);
845
        $foreignKeys = [];
846
        $rows = $this->fetchAll(sprintf(
847
            "SELECT
848
                    tc.constraint_name,
849
                    tc.table_name, kcu.column_name,
850
                    ccu.table_name AS referenced_table_name,
851
                    ccu.column_name AS referenced_column_name
852 1
                FROM
853
                    information_schema.table_constraints AS tc
854 1
                    JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
855 1
                    JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
856 1
                WHERE constraint_type = 'FOREIGN KEY' AND tc.table_schema = %s AND tc.table_name = %s
857
                ORDER BY kcu.position_in_unique_constraint",
858
            $this->getConnection()->quote($parts['schema']),
859
            $this->getConnection()->quote($parts['table'])
860
        ));
861 2
        foreach ($rows as $row) {
862
            $foreignKeys[$row['constraint_name']]['table'] = $row['table_name'];
863 2
            $foreignKeys[$row['constraint_name']]['columns'][] = $row['column_name'];
864 2
            $foreignKeys[$row['constraint_name']]['referenced_table'] = $row['referenced_table_name'];
865 2
            $foreignKeys[$row['constraint_name']]['referenced_columns'][] = $row['referenced_column_name'];
866
        }
867
868
        return $foreignKeys;
869
    }
870
871 1
    /**
872
     * {@inheritdoc}
873 1
     */
874 1 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...
875 1
    {
876 1
        $alter = sprintf(
877
            'ADD %s',
878
            $this->getForeignKeySqlDefinition($foreignKey, $table->getName())
879
        );
880
881
        return new AlterInstructions([$alter]);
882
    }
883
884 68
    /**
885
     * {@inheritdoc}
886 68
     */
887 4
    protected function getDropForeignKeyInstructions($tableName, $constraint)
888 68
    {
889 68
        $alter = sprintf(
890 68
            'DROP CONSTRAINT %s',
891 68
            $this->quoteColumnName($constraint)
892
        );
893
894
        return new AlterInstructions([$alter]);
895
    }
896
897
    /**
898
     * {@inheritdoc}
899
     */
900 68
    protected function getDropForeignKeyByColumnsInstructions($tableName, $columns)
901
    {
902 68
        $instructions = new AlterInstructions();
903 68
904 50
        $parts = $this->getSchemaName($tableName);
905 50
        $sql = "SELECT c.CONSTRAINT_NAME
906 68
                FROM (
907 68
                    SELECT CONSTRAINT_NAME, array_agg(COLUMN_NAME::varchar) as columns
908
                    FROM information_schema.KEY_COLUMN_USAGE
909 68
                    WHERE TABLE_SCHEMA = %s
910 1
                    AND TABLE_NAME IS NOT NULL
911 1
                    AND TABLE_NAME = %s
912 1
                    AND POSITION_IN_UNIQUE_CONSTRAINT IS NOT NULL
913 1
                    GROUP BY CONSTRAINT_NAME
914 1
                ) c
915 68
                WHERE
916
                    ARRAY[%s]::varchar[] <@ c.columns AND
917
                    ARRAY[%s]::varchar[] @> c.columns";
918
919
        $array = [];
920
        foreach ($columns as $col) {
921
            $array[] = "'$col'";
922 68
        }
923 68
924 68
        $rows = $this->fetchAll(sprintf(
925 68
            $sql,
926 68
            $this->getConnection()->quote($parts['schema']),
927
            $this->getConnection()->quote($parts['table']),
928
            implode(',', $array),
929 68
            implode(',', $array)
930 68
        ));
931 68
932 68
        foreach ($rows as $row) {
933 1
            $newInstr = $this->getDropForeignKeyInstructions($tableName, $row['constraint_name']);
934 1
            $instructions->merge($newInstr);
935
        }
936
937 68
        return $instructions;
938
    }
939 68
940 68
    /**
941 68
     * {@inheritdoc}
942
     */
943 68
    public function getSqlType($type, $limit = null)
944
    {
945
        switch ($type) {
946
            case static::PHINX_TYPE_TEXT:
947
            case static::PHINX_TYPE_TIME:
948
            case static::PHINX_TYPE_DATE:
949
            case static::PHINX_TYPE_BOOLEAN:
950
            case static::PHINX_TYPE_JSON:
951
            case static::PHINX_TYPE_JSONB:
952
            case static::PHINX_TYPE_UUID:
953 6
            case static::PHINX_TYPE_CIDR:
954
            case static::PHINX_TYPE_INET:
955
            case static::PHINX_TYPE_MACADDR:
956 6
            case static::PHINX_TYPE_TIMESTAMP:
957 6
                return ['name' => $type];
958 6
            case static::PHINX_TYPE_INTEGER:
959
                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...
960 6
                    return [
961 6
                        'name' => 'smallint',
962 6
                        'limit' => static::INT_SMALL
963 6
                    ];
964
                }
965 6
966
                return ['name' => $type];
967
            case static::PHINX_TYPE_DECIMAL:
968
                return ['name' => $type, 'precision' => 18, 'scale' => 0];
969
            case static::PHINX_TYPE_STRING:
970
                return ['name' => 'character varying', 'limit' => 255];
971
            case static::PHINX_TYPE_CHAR:
972
                return ['name' => 'character', 'limit' => 255];
973
            case static::PHINX_TYPE_BIG_INTEGER:
974
                return ['name' => 'bigint'];
975 7
            case static::PHINX_TYPE_FLOAT:
976
                return ['name' => 'real'];
977 7
            case static::PHINX_TYPE_DATETIME:
978 3
                return ['name' => 'timestamp'];
979 3
            case static::PHINX_TYPE_BLOB:
980 5
            case static::PHINX_TYPE_BINARY:
981 5
                return ['name' => 'bytea'];
982
            case static::PHINX_TYPE_INTERVAL:
983
                return ['name' => 'interval'];
984 5
            // Geospatial database types
985
            // Spatial storage in Postgres is done via the PostGIS extension,
986 7
            // which enables the use of the "geography" type in combination
987 7
            // with SRID 4326.
988 7
            case static::PHINX_TYPE_GEOMETRY:
989 7
                return ['name' => 'geography', 'type' => 'geometry', 'srid' => 4326];
990 7
            case static::PHINX_TYPE_POINT:
991 7
                return ['name' => 'geography', 'type' => 'point', 'srid' => 4326];
992 7
            case static::PHINX_TYPE_LINESTRING:
993 7
                return ['name' => 'geography', 'type' => 'linestring', 'srid' => 4326];
994
            case static::PHINX_TYPE_POLYGON:
995
                return ['name' => 'geography', 'type' => 'polygon', 'srid' => 4326];
996
            default:
997
                if ($this->isArrayType($type)) {
998
                    return ['name' => $type];
999
                }
1000
                // Return array type
1001
                throw new \RuntimeException('The type: "' . $type . '" is not supported');
1002
        }
1003 3
    }
1004
1005 3
    /**
1006 3
     * Returns Phinx type by SQL type
1007 3
     *
1008 3
     * @param string $sqlType SQL type
1009
     * @returns string Phinx type
1010
     */
1011 3
    public function getPhinxType($sqlType)
1012
    {
1013
        switch ($sqlType) {
1014 3
            case 'character varying':
1015
            case 'varchar':
1016
                return static::PHINX_TYPE_STRING;
1017
            case 'character':
1018
            case 'char':
1019
                return static::PHINX_TYPE_CHAR;
1020 68
            case 'text':
1021
                return static::PHINX_TYPE_TEXT;
1022
            case 'json':
1023 68
                return static::PHINX_TYPE_JSON;
1024 67
            case 'jsonb':
1025 67
                return static::PHINX_TYPE_JSONB;
1026
            case 'smallint':
1027 68
                return [
1028
                    'name' => 'smallint',
1029 68
                    'limit' => static::INT_SMALL
1030 68
                ];
1031
            case 'int':
1032
            case 'int4':
1033
            case 'integer':
1034
                return static::PHINX_TYPE_INTEGER;
1035
            case 'decimal':
1036
            case 'numeric':
1037
                return static::PHINX_TYPE_DECIMAL;
1038 68
            case 'bigint':
1039
            case 'int8':
1040 68
                return static::PHINX_TYPE_BIG_INTEGER;
1041 68
            case 'real':
1042 68
            case 'float4':
1043
                return static::PHINX_TYPE_FLOAT;
1044
            case 'bytea':
1045
                return static::PHINX_TYPE_BINARY;
1046
            case 'interval':
1047
                return static::PHINX_TYPE_INTERVAL;
1048
            case 'time':
1049
            case 'timetz':
1050 68
            case 'time with time zone':
1051
            case 'time without time zone':
1052 68
                return static::PHINX_TYPE_TIME;
1053
            case 'date':
1054
                return static::PHINX_TYPE_DATE;
1055 68
            case 'timestamp':
1056
            case 'timestamptz':
1057 68
            case 'timestamp with time zone':
1058 68
            case 'timestamp without time zone':
1059 68
                return static::PHINX_TYPE_DATETIME;
1060
            case 'bool':
1061
            case 'boolean':
1062
                return static::PHINX_TYPE_BOOLEAN;
1063
            case 'uuid':
1064
                return static::PHINX_TYPE_UUID;
1065
            case 'cidr':
1066
                return static::PHINX_TYPE_CIDR;
1067
            case 'inet':
1068 68
                return static::PHINX_TYPE_INET;
1069
            case 'macaddr':
1070 68
                return static::PHINX_TYPE_MACADDR;
1071 68
            default:
1072 68
                throw new \RuntimeException('The PostgreSQL type: "' . $sqlType . '" is not supported');
1073
        }
1074
    }
1075
1076
    /**
1077
     * {@inheritdoc}
1078
     */
1079 68
    public function createDatabase($name, $options = [])
1080
    {
1081 68
        $charset = isset($options['charset']) ? $options['charset'] : 'utf8';
1082 68
        $this->execute(sprintf("CREATE DATABASE %s WITH ENCODING = '%s'", $name, $charset));
1083 68
    }
1084 68
1085
    /**
1086
     * {@inheritdoc}
1087
     */
1088
    public function hasDatabase($name)
1089
    {
1090
        $sql = sprintf("SELECT count(*) FROM pg_database WHERE datname = '%s'", $name);
1091 68
        $result = $this->fetchRow($sql);
1092
1093
        return $result['count'] > 0;
1094
    }
1095 68
1096 68
    /**
1097 68
     * {@inheritdoc}
1098 68
     */
1099 68
    public function dropDatabase($name)
1100 68
    {
1101 68
        $this->disconnect();
1102
        $this->execute(sprintf('DROP DATABASE IF EXISTS %s', $name));
1103
        $this->connect();
1104
    }
1105
1106
    /**
1107 73
     * Get the defintion for a `DEFAULT` statement.
1108
     *
1109 73
     * @param mixed $default default value
1110
     * @param string $columnType column type added
1111
     * @return string
1112
     */
1113 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...
1114
    {
1115 73
        if (is_string($default) && 'CURRENT_TIMESTAMP' !== $default) {
1116
            $default = $this->getConnection()->quote($default);
1117
        } elseif (is_bool($default)) {
1118 73
            $default = $this->castToBool($default);
1119
        } elseif ($columnType === static::PHINX_TYPE_BOOLEAN) {
1120
            $default = $this->castToBool((bool)$default);
1121
        }
1122
1123
        return isset($default) ? 'DEFAULT ' . $default : '';
1124
    }
1125
1126
    /**
1127 14
     * Gets the PostgreSQL Column Definition for a Column object.
1128
     *
1129 14
     * @param \Phinx\Db\Table\Column $column Column
1130 1
     * @return string
1131
     */
1132
    protected function getColumnSqlDefinition(Column $column)
1133 13
    {
1134 13
        $buffer = [];
1135
        if ($column->isIdentity()) {
1136
            $buffer[] = $column->getType() == 'biginteger' ? 'BIGSERIAL' : 'SERIAL';
1137
        } elseif ($column->getType() instanceof Literal) {
1138
            $buffer[] = (string)$column->getType();
1139
        } else {
1140
            $sqlType = $this->getSqlType($column->getType(), $column->getLimit());
1141
            $buffer[] = strtoupper($sqlType['name']);
1142 68
1143
            // integers cant have limits in postgres
1144 68
            if (static::PHINX_TYPE_DECIMAL === $sqlType['name'] && ($column->getPrecision() || $column->getScale())) {
1145 68
                $buffer[] = sprintf(
1146
                    '(%s, %s)',
1147
                    $column->getPrecision() ?: $sqlType['precision'],
1148
                    $column->getScale() ?: $sqlType['scale']
1149
                );
1150
            } elseif (in_array($sqlType['name'], ['geography'])) {
1151 68
                // geography type must be written with geometry type and srid, like this: geography(POLYGON,4326)
1152
                $buffer[] = sprintf(
1153 68
                    '(%s,%s)',
1154
                    strtoupper($sqlType['type']),
1155
                    $sqlType['srid']
1156
                );
1157
            } elseif (!in_array($sqlType['name'], ['integer', 'smallint', 'bigint', 'boolean'])) {
1158
                if ($column->getLimit() || isset($sqlType['limit'])) {
1159
                    $buffer[] = sprintf('(%s)', $column->getLimit() ?: $sqlType['limit']);
1160
                }
1161
            }
1162
1163
            $timeTypes = [
1164
                'time',
1165
                'timestamp',
1166
            ];
1167
1168
            if (in_array($sqlType['name'], $timeTypes) && is_numeric($column->getPrecision())) {
1169
                $buffer[] = sprintf('(%s)', $column->getPrecision());
1170
            }
1171
1172
            if (in_array($sqlType['name'], $timeTypes) && $column->isTimezone()) {
1173
                $buffer[] = strtoupper('with time zone');
1174
            }
1175
        }
1176
1177
        $buffer[] = $column->isNull() ? 'NULL' : 'NOT NULL';
1178
1179
        if (!is_null($column->getDefault())) {
1180
            $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...
1181
        }
1182
1183
        return implode(' ', $buffer);
1184
    }
1185
1186
    /**
1187
     * Gets the PostgreSQL Column Comment Definition for a column object.
1188
     *
1189
     * @param \Phinx\Db\Table\Column $column Column
1190
     * @param string $tableName Table name
1191
     * @return string
1192
     */
1193
    protected function getColumnCommentSqlDefinition(Column $column, $tableName)
1194
    {
1195
        // passing 'null' is to remove column comment
1196
        $comment = (strcasecmp($column->getComment(), 'NULL') !== 0)
1197
                 ? $this->getConnection()->quote($column->getComment())
1198
                 : 'NULL';
1199
1200
        return sprintf(
1201
            'COMMENT ON COLUMN %s.%s IS %s;',
1202
            $this->quoteTableName($tableName),
1203
            $this->quoteColumnName($column->getName()),
1204
            $comment
1205
        );
1206
    }
1207
1208
    /**
1209
     * Gets the PostgreSQL Index Definition for an Index object.
1210
     *
1211
     * @param \Phinx\Db\Table\Index  $index Index
1212
     * @param string $tableName Table name
1213
     * @return string
1214
     */
1215
    protected function getIndexSqlDefinition(Index $index, $tableName)
1216
    {
1217
        $parts = $this->getSchemaName($tableName);
1218
1219 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...
1220
            $indexName = $index->getName();
1221
        } else {
1222
            $columnNames = $index->getColumns();
1223
            $indexName = sprintf('%s_%s', $parts['table'], implode('_', $columnNames));
1224
        }
1225
        $def = sprintf(
1226
            "CREATE %s INDEX %s ON %s (%s);",
1227
            ($index->getType() === Index::UNIQUE ? 'UNIQUE' : ''),
1228
            $this->quoteColumnName($indexName),
1229
            $this->quoteTableName($tableName),
1230
            implode(',', array_map([$this, 'quoteColumnName'], $index->getColumns()))
1231
        );
1232
1233
        return $def;
1234
    }
1235
1236
    /**
1237
     * Gets the MySQL Foreign Key Definition for an ForeignKey object.
1238
     *
1239
     * @param \Phinx\Db\Table\ForeignKey $foreignKey
1240
     * @param string     $tableName  Table name
1241
     * @return string
1242
     */
1243
    protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, $tableName)
1244
    {
1245
        $parts = $this->getSchemaName($tableName);
1246
1247
        $constraintName = $foreignKey->getConstraint() ?: ($parts['table'] . '_' . implode('_', $foreignKey->getColumns()) . '_fkey');
1248
        $def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName) .
0 ignored issues
show
Bug introduced by
It seems like $constraintName defined by $foreignKey->getConstrai...getColumns()) . '_fkey' on line 1247 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...
1249
        ' FOREIGN KEY ("' . implode('", "', $foreignKey->getColumns()) . '")' .
1250
        " REFERENCES {$this->quoteTableName($foreignKey->getReferencedTable()->getName())} (\"" .
1251
        implode('", "', $foreignKey->getReferencedColumns()) . '")';
1252
        if ($foreignKey->getOnDelete()) {
1253
            $def .= " ON DELETE {$foreignKey->getOnDelete()}";
1254
        }
1255
        if ($foreignKey->getOnUpdate()) {
1256
            $def .= " ON UPDATE {$foreignKey->getOnUpdate()}";
1257
        }
1258
1259
        return $def;
1260
    }
1261
1262
    /**
1263
     * {@inheritdoc}
1264
     */
1265
    public function createSchemaTable()
1266
    {
1267
        // Create the public/custom schema if it doesn't already exist
1268
        if ($this->hasSchema($this->getGlobalSchemaName()) === false) {
1269
            $this->createSchema($this->getGlobalSchemaName());
1270
        }
1271
1272
        $this->fetchAll(sprintf('SET search_path TO %s', $this->getGlobalSchemaName()));
1273
1274
        parent::createSchemaTable();
1275
    }
1276
1277
    /**
1278
     * Creates the specified schema.
1279
     *
1280
     * @param  string $schemaName Schema Name
1281
     * @return void
1282
     */
1283
    public function createSchema($schemaName = 'public')
1284
    {
1285
        // from postgres 9.3 we can use "CREATE SCHEMA IF NOT EXISTS schema_name"
1286
        $sql = sprintf('CREATE SCHEMA %s;', $this->quoteSchemaName($schemaName));
1287
        $this->execute($sql);
1288
    }
1289
1290
    /**
1291
     * Checks to see if a schema exists.
1292
     *
1293
     * @param string $schemaName Schema Name
1294
     * @return bool
1295
     */
1296
    public function hasSchema($schemaName)
1297
    {
1298
        $sql = sprintf(
1299
            "SELECT count(*)
1300
             FROM pg_namespace
1301
             WHERE nspname = %s",
1302
            $this->getConnection()->quote($schemaName)
1303
        );
1304
        $result = $this->fetchRow($sql);
1305
1306
        return $result['count'] > 0;
1307
    }
1308
1309
    /**
1310
     * Drops the specified schema table.
1311
     *
1312
     * @param string $schemaName Schema name
1313
     * @return void
1314
     */
1315
    public function dropSchema($schemaName)
1316
    {
1317
        $sql = sprintf("DROP SCHEMA IF EXISTS %s CASCADE;", $this->quoteSchemaName($schemaName));
1318
        $this->execute($sql);
1319
    }
1320
1321
    /**
1322
     * Drops all schemas.
1323
     *
1324
     * @return void
1325
     */
1326
    public function dropAllSchemas()
1327
    {
1328
        foreach ($this->getAllSchemas() as $schema) {
1329
            $this->dropSchema($schema);
1330
        }
1331
    }
1332
1333
    /**
1334
     * Returns schemas.
1335
     *
1336
     * @return array
1337
     */
1338
    public function getAllSchemas()
1339
    {
1340
        $sql = "SELECT schema_name
1341
                FROM information_schema.schemata
1342
                WHERE schema_name <> 'information_schema' AND schema_name !~ '^pg_'";
1343
        $items = $this->fetchAll($sql);
1344
        $schemaNames = [];
1345
        foreach ($items as $item) {
1346
            $schemaNames[] = $item['schema_name'];
1347
        }
1348
1349
        return $schemaNames;
1350
    }
1351
1352
    /**
1353
     * {@inheritdoc}
1354
     */
1355
    public function getColumnTypes()
1356
    {
1357
        return array_merge(parent::getColumnTypes(), ['json', 'jsonb', 'cidr', 'inet', 'macaddr', 'interval']);
1358
    }
1359
1360
    /**
1361
     * {@inheritdoc}
1362
     */
1363
    public function isValidColumnType(Column $column)
1364
    {
1365
        // If not a standard column type, maybe it is array type?
1366
        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...
1367
    }
1368
1369
    /**
1370
     * Check if the given column is an array of a valid type.
1371
     *
1372
     * @param  string $columnType
1373
     * @return bool
1374
     */
1375
    protected function isArrayType($columnType)
1376
    {
1377
        if (!preg_match('/^([a-z]+)(?:\[\]){1,}$/', $columnType, $matches)) {
1378
            return false;
1379
        }
1380
1381
        $baseType = $matches[1];
1382
1383
        return in_array($baseType, $this->getColumnTypes());
1384
    }
1385
1386
    /**
1387
     * @param string $tableName Table name
1388
     * @return array
1389
     */
1390
    private function getSchemaName($tableName)
1391
    {
1392
        $schema = $this->getGlobalSchemaName();
1393
        $table = $tableName;
1394
        if (false !== strpos($tableName, '.')) {
1395
            list($schema, $table) = explode('.', $tableName);
1396
        }
1397
1398
        return [
1399
            'schema' => $schema,
1400
            'table' => $table,
1401
        ];
1402
    }
1403
1404
    /**
1405
     * Gets the schema name.
1406
     *
1407
     * @return string
1408
     */
1409
    private function getGlobalSchemaName()
1410
    {
1411
        $options = $this->getOptions();
1412
1413
        return empty($options['schema']) ? 'public' : $options['schema'];
1414
    }
1415
1416
    /**
1417
     * {@inheritdoc}
1418
     */
1419
    public function castToBool($value)
1420
    {
1421
        return (bool)$value ? 'TRUE' : 'FALSE';
1422
    }
1423
1424
    /**
1425
     * {@inheritDoc}
1426
     *
1427
     */
1428 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...
1429
    {
1430
        $options = $this->getOptions();
1431
        $options = [
1432
            'username' => $options['user'],
1433
            'password' => $options['pass'],
1434
            'database' => $options['name'],
1435
            'quoteIdentifiers' => true,
1436
        ] + $options;
1437
1438
        $driver = new PostgresDriver($options);
1439
1440
        if (method_exists($driver, 'setConnection')) {
1441
            $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...
1442
        } else {
1443
            $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...
1444
        }
1445
1446
        return new Connection(['driver' => $driver] + $options);
1447
    }
1448
}
1449