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

PostgresAdapter::getDropColumnInstructions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1.0156

Importance

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