Completed
Pull Request — master (#1390)
by
unknown
02:54
created

PostgresAdapter::getAddForeignKeyInstructions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9

Duplication

Lines 9
Ratio 100 %

Code Coverage

Tests 9
CRAP Score 1

Importance

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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