Completed
Pull Request — master (#1193)
by Dmitriy
01:57
created

PostgresAdapter::getIndexSqlDefinition()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 23
Code Lines 16

Duplication

Lines 7
Ratio 30.43 %

Code Coverage

Tests 7
CRAP Score 4

Importance

Changes 0
Metric Value
dl 7
loc 23
ccs 7
cts 7
cp 1
rs 8.7972
c 0
b 0
f 0
cc 4
eloc 16
nc 3
nop 2
crap 4
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 Phinx\Db\Table;
32
use Phinx\Db\Table\Column;
33
use Phinx\Db\Table\ForeignKey;
34
use Phinx\Db\Table\Index;
35
use Phinx\Util\Literal;
36
37
class PostgresAdapter extends PdoAdapter implements AdapterInterface
38
{
39
    const INT_SMALL = 65535;
40
41
    /**
42
     * Columns with comments
43
     *
44
     * @var array
45
     */
46
    protected $columnsWithComments = [];
47
48
    /**
49
     * {@inheritdoc}
50 68
     */
51
    public function connect()
52 68
    {
53 68
        if ($this->connection === null) {
54
            if (!class_exists('PDO') || !in_array('pgsql', \PDO::getAvailableDrivers(), true)) {
55
                // @codeCoverageIgnoreStart
56
                throw new \RuntimeException('You need to enable the PDO_Pgsql extension for Phinx to run properly.');
57
                // @codeCoverageIgnoreEnd
58
            }
59 68
60 68
            $db = null;
61
            $options = $this->getOptions();
62
63 68
            // if port is specified use it, otherwise use the PostgreSQL default
64 68 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...
65 68
                $dsn = 'pgsql:host=' . $options['host'] . ';port=' . $options['port'] . ';dbname=' . $options['name'];
66 1
            } else {
67
                $dsn = 'pgsql:host=' . $options['host'] . ';dbname=' . $options['name'];
68
            }
69
70 68
            try {
71 68
                $db = new \PDO($dsn, $options['user'], $options['pass'], [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]);
72 1
            } catch (\PDOException $exception) {
73 1
                throw new \InvalidArgumentException(sprintf(
74 1
                    'There was a problem connecting to the database: %s',
75 1
                    $exception->getMessage()
76
                ), $exception->getCode(), $exception);
77
            }
78 68
79 68
            try {
80 68
                if (isset($options['schema'])) {
81
                    $db->exec('SET search_path TO ' . $options['schema']);
82
                }
83
            } catch (\PDOException $exception) {
84
                throw new \InvalidArgumentException(
85 68
                    sprintf('Schema does not exists: %s', $options['schema']),
86
                    $exception->getCode(),
87 68
                    $exception
88 68
                );
89
            }
90
91
            $this->setConnection($db);
92
        }
93
    }
94
95
    /**
96
     * {@inheritdoc}
97
     */
98
    public function disconnect()
99
    {
100
        $this->connection = null;
101
    }
102
103
    /**
104
     * {@inheritdoc}
105
     */
106
    public function hasTransactions()
107
    {
108
        return true;
109
    }
110
111
    /**
112
     * {@inheritdoc}
113
     */
114
    public function beginTransaction()
115
    {
116
        $this->execute('BEGIN');
117
    }
118
119
    /**
120
     * {@inheritdoc}
121
     */
122
    public function commitTransaction()
123
    {
124
        $this->execute('COMMIT');
125
    }
126
127
    /**
128 68
     * {@inheritdoc}
129
     */
130 68
    public function rollbackTransaction()
131
    {
132
        $this->execute('ROLLBACK');
133
    }
134
135
    /**
136 68
     * Quotes a schema name for use in a query.
137
     *
138 68
     * @param string $schemaName Schema Name
139
     * @return string
140
     */
141
    public function quoteSchemaName($schemaName)
142
    {
143
        return $this->quoteColumnName($schemaName);
144 68
    }
145
146 68
    /**
147
     * {@inheritdoc}
148
     */
149
    public function quoteTableName($tableName)
150
    {
151
        $parts = $this->getSchemaName($tableName);
152 68
153
        return $this->quoteSchemaName($parts['schema']) . '.' . $this->quoteColumnName($parts['table']);
154 68
    }
155 68
156
    /**
157
     * {@inheritdoc}
158
     */
159 68
    public function quoteColumnName($columnName)
160 68
    {
161 68
        return '"' . $columnName . '"';
162 68
    }
163 68
164
    /**
165 68
     * {@inheritdoc}
166
     */
167
    public function hasTable($tableName)
168
    {
169
        $parts = $this->getSchemaName($tableName);
170
        $result = $this->getConnection()->query(
171 68
            sprintf(
172
                'SELECT *
173 68
                FROM information_schema.tables
174
                WHERE table_schema = %s
175
                AND table_name = %s',
176 68
                $this->getConnection()->quote($parts['schema']),
177 68
                $this->getConnection()->quote($parts['table'])
178 48
            )
179 48
        );
180 48
181 48
        return $result->rowCount() === 1;
182
    }
183 48
184 48
    /**
185 68
     * {@inheritdoc}
186
     */
187 2
    public function createTable(Table $table)
188 2
    {
189 2
        $options = $table->getOptions();
190 2
        $parts = $this->getSchemaName($table->getName());
191
192 2
         // Add the default primary key
193 2
        $columns = $table->getPendingColumns();
194 2 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...
195
            $column = new Column();
196
            $column->setName('id')
197 68
                   ->setType('integer')
198 68
                   ->setIdentity(true);
199
200 68
            array_unshift($columns, $column);
201 68
            $options['primary_key'] = 'id';
202 68
        } elseif (isset($options['id']) && is_string($options['id'])) {
203
            // Handle id => "field_name" to support AUTO_INCREMENT
204
            $column = new Column();
205 68
            $column->setName($options['id'])
206 6
                   ->setType('integer')
207 6
                   ->setIdentity(true);
208 68
209
            array_unshift($columns, $column);
210
            $options['primary_key'] = $options['id'];
211 68
        }
212 68
213 68
        // TODO - process table options like collation etc
214 68
        $sql = 'CREATE TABLE ';
215 68
        $sql .= $this->quoteTableName($table->getName()) . ' (';
216 68
217
        $this->columnsWithComments = [];
218 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...
219 1
            $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column) . ', ';
220 1
221 1
            // set column comments, if needed
222 1
            if ($column->getComment()) {
223 1
                $this->columnsWithComments[] = $column;
224 1
            }
225 1
        }
226 1
227 1
         // set the primary key(s)
228 1 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...
229 68
            $sql = rtrim($sql);
230 68
            $sql .= sprintf(' CONSTRAINT %s PRIMARY KEY (', $this->quoteColumnName($parts['table'] . '_pkey'));
231 2
            if (is_string($options['primary_key'])) { // handle primary_key => 'id'
232
                $sql .= $this->quoteColumnName($options['primary_key']);
233
            } 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...
234
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key']));
235 68
            }
236 68
            $sql .= ')';
237 1
        } else {
238 1
            $sql = rtrim($sql, ', '); // no primary keys
239 1
        }
240 1
241
        // set the foreign keys
242 68
        $foreignKeys = $table->getForeignKeys();
243
        if (!empty($foreignKeys)) {
244
            foreach ($foreignKeys as $foreignKey) {
245 68
                $sql .= ', ' . $this->getForeignKeySqlDefinition($foreignKey, $table->getName());
246 6
            }
247 6
        }
248 6
249 6
        $sql .= ');';
250
251
        // process column comments
252
        if (!empty($this->columnsWithComments)) {
253 68
            foreach ($this->columnsWithComments as $column) {
254 68
                $sql .= $this->getColumnCommentSqlDefinition($column, $table->getName());
255 5
            }
256 5
        }
257 5
258 5
        // set the indexes
259
        $indexes = $table->getIndexes();
260
        if (!empty($indexes)) {
261 68
            foreach ($indexes as $index) {
262
                $sql .= $this->getIndexSqlDefinition($index, $table->getName());
263
            }
264 68
        }
265 1
266 1
        // execute the sql
267 1
        $this->execute($sql);
268 1
269 1
        // process table comments
270 1
        if (isset($options['comment'])) {
271 1
            $sql = sprintf(
272 68
                'COMMENT ON TABLE %s IS %s',
273
                $this->quoteTableName($table->getName()),
274
                $this->getConnection()->quote($options['comment'])
275
            );
276
            $this->execute($sql);
277 1
        }
278
    }
279 1
280 1
    /**
281 1
     * {@inheritdoc}
282 1
     */
283 1
    public function renameTable($tableName, $newTableName)
284 1
    {
285 1
        $sql = sprintf(
286
            'ALTER TABLE %s RENAME TO %s',
287
            $this->quoteTableName($tableName),
288
            $this->quoteColumnName($newTableName)
289
        );
290 1
        $this->execute($sql);
291
    }
292 1
293 1
    /**
294
     * {@inheritdoc}
295
     */
296
    public function dropTable($tableName)
297
    {
298 1
        $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($tableName)));
299
    }
300 1
301 1
    /**
302 1
     * {@inheritdoc}
303 1
     */
304
    public function truncateTable($tableName)
305 1
    {
306 1
        $sql = sprintf(
307
            'TRUNCATE TABLE %s',
308
            $this->quoteTableName($tableName)
309
        );
310
311 9
        $this->execute($sql);
312
    }
313 9
314 9
    /**
315
     * {@inheritdoc}
316
     */
317
    public function getColumns($tableName)
318 9
    {
319
        $parts   = $this->getSchemaName($tableName);
320 9
        $columns = [];
321 9
        $sql     = sprintf(
322
            "SELECT column_name, data_type, udt_name, is_identity, is_nullable,
323 9
             column_default, character_maximum_length, numeric_precision, numeric_scale,
324 9
             datetime_precision
325 9
             FROM information_schema.columns
326 9
             WHERE table_schema = %s AND table_name = %s",
327 9
            $this->getConnection()->quote($parts['schema']),
328 9
            $this->getConnection()->quote($parts['table'])
329 9
        );
330 9
        $columnsInfo = $this->fetchAll($sql);
331 9
332
        foreach ($columnsInfo as $columnInfo) {
333 9
            $isUserDefined = strtoupper(trim($columnInfo['data_type'])) === 'USER-DEFINED';
334 1
335 1
            if ($isUserDefined) {
336
                $columnType = Literal::from($columnInfo['udt_name']);
337 9
            } else {
338 5
                $columnType = $this->getPhinxType($columnInfo['data_type']);
339 5
            }
340 9
341 9
            // If the default value begins with a ' or looks like a function mark it as literal
342 9
            if (isset($columnInfo['column_default'][0]) && $columnInfo['column_default'][0] === "'") {
343
                if (preg_match('/^\'(.*)\'::[^:]+$/', $columnInfo['column_default'], $match)) {
344
                    // '' 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...
345
                    $columnDefault = preg_replace('/[\'\\\\]\'/', "'", $match[1]);
346
                } else {
347
                    $columnDefault = Literal::from($columnInfo['column_default']);
348 24
                }
349
            } elseif (preg_match('/^\D[a-z_\d]*\(.*\)$/', $columnInfo['column_default'])) {
350 24
                $columnDefault = Literal::from($columnInfo['column_default']);
351
            } else {
352
                $columnDefault = $columnInfo['column_default'];
353 24
            }
354 24
355 24
            $column = new Column();
356
            $column->setName($columnInfo['column_name'])
357 24
                   ->setType($columnType)
358
                   ->setNull($columnInfo['is_nullable'] === 'YES')
359 24
                   ->setDefault($columnDefault)
360 24
                   ->setIdentity($columnInfo['is_identity'] === 'YES')
361
                   ->setScale($columnInfo['numeric_scale']);
362
363
            if (preg_match('/\bwith time zone$/', $columnInfo['data_type'])) {
364
                $column->setTimezone(true);
365
            }
366 18
367
            if (isset($columnInfo['character_maximum_length'])) {
368 18
                $column->setLimit($columnInfo['character_maximum_length']);
369 18
            }
370 18
371 18
            $phinxType = $this->getPhinxType($columnInfo['data_type']);
372 18
            if (in_array($phinxType, [static::PHINX_TYPE_TIME, static::PHINX_TYPE_DATETIME])) {
373 18
                $column->setPrecision($columnInfo['datetime_precision']);
374
            } else {
375 18
                $column->setPrecision($columnInfo['numeric_precision']);
376 18
            }
377
378
            $columns[] = $column;
379
        }
380
381 3
        return $columns;
382
    }
383 3
384
    /**
385
     * {@inheritdoc}
386 3
     */
387 3
    public function hasColumn($tableName, $columnName)
388
    {
389 3
        $parts = $this->getSchemaName($tableName);
390 3
        $sql = sprintf(
391 3
            "SELECT count(*)
392 1
            FROM information_schema.columns
393
            WHERE table_schema = %s AND table_name = %s AND column_name = %s",
394 2
            $this->getConnection()->quote($parts['schema']),
395 2
            $this->getConnection()->quote($parts['table']),
396 2
            $this->getConnection()->quote($columnName)
397 2
        );
398 2
399 2
        $result = $this->fetchRow($sql);
400 2
401 2
        return $result['count'] > 0;
402 2
    }
403
404
    /**
405
     * {@inheritdoc}
406
     */
407 5 View Code Duplication
    public function addColumn(Table $table, Column $column)
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...
408
    {
409
        $sql = sprintf(
410
            'ALTER TABLE %s ADD %s %s;',
411 5
            $this->quoteTableName($table->getName()),
412 5
            $this->quoteColumnName($column->getName()),
413 5
            $this->getColumnSqlDefinition($column)
414 5
        );
415 5
416 5
        if ($column->getComment()) {
417
            $sql .= $this->getColumnCommentSqlDefinition($column, $table->getName());
418 5
        }
419 5
420
        $this->execute($sql);
421 5
    }
422 5
423
    /**
424 5
     * {@inheritdoc}
425 5
     */
426 5
    public function renameColumn($tableName, $columnName, $newColumnName)
427 5
    {
428 5
        $parts = $this->getSchemaName($tableName);
429 5
        $sql = sprintf(
430 2
            "SELECT CASE WHEN COUNT(*) > 0 THEN 1 ELSE 0 END AS column_exists
431 2
             FROM information_schema.columns
432 4
             WHERE table_schema = %s AND table_name = %s AND column_name = %s",
433
            $this->getConnection()->quote($parts['schema']),
434 5
            $this->getConnection()->quote($parts['table']),
435 5
            $this->getConnection()->quote($columnName)
436
        );
437 1
        $result = $this->fetchRow($sql);
438 1
        if (!(bool)$result['column_exists']) {
439 1
            throw new \InvalidArgumentException("The specified column does not exist: $columnName");
440 1
        }
441 1
        $this->execute(
442 1
            sprintf(
443 1
                'ALTER TABLE %s RENAME COLUMN %s TO %s',
444 1
                $this->quoteTableName($tableName),
445 1
                $this->quoteColumnName($columnName),
446
                $this->quoteColumnName($newColumnName)
447 4
            )
448 4
        );
449 4
    }
450 4
451 4
    /**
452 4
     * {@inheritdoc}
453 4
     */
454
    public function changeColumn($tableName, $columnName, Column $newColumn)
455
    {
456 5
        // TODO - is it possible to merge these 3 queries into less?
457 1
        // change data type
458 1
        $sql = sprintf(
459 1
            'ALTER TABLE %s ALTER COLUMN %s TYPE %s',
460 1
            $this->quoteTableName($tableName),
461 1
            $this->quoteColumnName($columnName),
462 1
            $this->getColumnSqlDefinition($newColumn)
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
        $this->execute($sql);
470 2
        // process null
471 2
        $sql = sprintf(
472 5
            'ALTER TABLE %s ALTER COLUMN %s',
473
            $this->quoteTableName($tableName),
474
            $this->quoteColumnName($columnName)
475
        );
476
        if ($newColumn->isNull()) {
477 1
            $sql .= ' DROP NOT NULL';
478
        } else {
479 1
            $sql .= ' SET NOT NULL';
480 1
        }
481 1
        $this->execute($sql);
482 1
        if (!is_null($newColumn->getDefault())) {
483 1
            //change default
484 1
            $this->execute(
485 1
                sprintf(
486 1
                    'ALTER TABLE %s ALTER COLUMN %s SET %s',
487
                    $this->quoteTableName($tableName),
488
                    $this->quoteColumnName($columnName),
489
                    $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...
490
                )
491
            );
492
        } else {
493
            //drop default
494 9
            $this->execute(
495
                sprintf(
496 9
                    'ALTER TABLE %s ALTER COLUMN %s DROP DEFAULT',
497
                    $this->quoteTableName($tableName),
498
                    $this->quoteColumnName($columnName)
499
                )
500
            );
501
        }
502
        // rename column
503
        if ($columnName !== $newColumn->getName()) {
504
            $this->execute(
505
                sprintf(
506
                    'ALTER TABLE %s RENAME COLUMN %s TO %s',
507
                    $this->quoteTableName($tableName),
508
                    $this->quoteColumnName($columnName),
509
                    $this->quoteColumnName($newColumn->getName())
510
                )
511
            );
512
        }
513
514 9
        // change column comment if needed
515 9
        if ($newColumn->getComment()) {
516 9
            $sql = $this->getColumnCommentSqlDefinition($newColumn, $tableName);
517 9
            $this->execute($sql);
518 9
        }
519 9
    }
520 9
521 9
    /**
522 9
     * {@inheritdoc}
523
     */
524
    public function dropColumn($tableName, $columnName)
525
    {
526
        $this->execute(
527
            sprintf(
528 9
                'ALTER TABLE %s DROP COLUMN %s',
529
                $this->quoteTableName($tableName),
530 9
                $this->quoteColumnName($columnName)
531 4
            )
532 4
        );
533 9
    }
534 9
535 9
    /**
536 9
     * Get an array of indexes from a particular table.
537 9
     *
538
     * @param string $tableName Table Name
539 8
     * @return array
540 8
     */
541
    protected function getIndexes($tableName)
542
    {
543
        $parts = $this->getSchemaName($tableName);
544
545
        $indexes = [];
546 1
        $sql = sprintf(
547
            "SELECT
548 1
                i.relname AS index_name,
549 1
                a.attname AS column_name
550 1
            FROM
551 1
                pg_class t,
552
                pg_class i,
553
                pg_index ix,
554
                pg_attribute a,
555
                pg_namespace nsp
556
            WHERE
557
                t.oid = ix.indrelid
558
                AND i.oid = ix.indexrelid
559
                AND a.attrelid = t.oid
560 2
                AND a.attnum = ANY(ix.indkey)
561
                AND t.relnamespace = nsp.oid
562 2
                AND nsp.nspname = %s
563 2
                AND t.relkind = 'r'
564 2
                AND t.relname = %s
565
            ORDER BY
566
                t.relname,
567
                i.relname",
568
            $this->getConnection()->quote($parts['schema']),
569 1
            $this->getConnection()->quote($parts['table'])
570
        );
571 1
        $rows = $this->fetchAll($sql);
572 1 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...
573 1
            if (!isset($indexes[$row['index_name']])) {
574
                $indexes[$row['index_name']] = ['columns' => []];
575 1
            }
576 1
            $indexes[$row['index_name']]['columns'][] = $row['column_name'];
577
        }
578 1
579 1
        return $indexes;
580 1
    }
581 1
582 1
    /**
583 1
     * {@inheritdoc}
584 1
     */
585 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...
586 1
    {
587
        if (is_string($columns)) {
588 1
            $columns = [$columns];
589
        }
590
        $indexes = $this->getIndexes($tableName);
591
        foreach ($indexes as $index) {
592
            if (array_diff($index['columns'], $columns) === array_diff($columns, $index['columns'])) {
593
                return true;
594
            }
595
        }
596 1
597
        return false;
598 1
    }
599 1
600
    /**
601 1
     * {@inheritdoc}
602 1
     */
603 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...
604
    {
605
        $indexes = $this->getIndexes($tableName);
606
        foreach ($indexes as $name => $index) {
607
            if ($name === $indexName) {
608 3
                return true;
609
            }
610 3
        }
611 1
612 1
        return false;
613 3
    }
614 3
615
    /**
616
     * {@inheritdoc}
617
     */
618
    public function addIndex(Table $table, Index $index)
619
    {
620 3
        $sql = $this->getIndexSqlDefinition($index, $table->getName());
621 3
        $this->execute($sql);
622 3
    }
623 3
624
    /**
625 1
     * {@inheritdoc}
626 1
     */
627
    public function dropIndex($tableName, $columns)
628
    {
629
        $parts = $this->getSchemaName($tableName);
630
631
        if (is_string($columns)) {
632
            $columns = [$columns]; // str to array
633
        }
634
635
        $indexes = $this->getIndexes($tableName);
636 3
        foreach ($indexes as $indexName => $index) {
637
            $a = array_diff($columns, $index['columns']);
638 3
            if (empty($a)) {
639 3
                $this->execute(
640
                    sprintf(
641
                        'DROP INDEX IF EXISTS %s',
642
                        '"' . ($parts['schema'] . '".' . $this->quoteColumnName($indexName))
643
                    )
644
                );
645
646
                return;
647
            }
648
        }
649
    }
650 3
651
    /**
652 3
     * {@inheritdoc}
653 3
     */
654 3
    public function dropIndexByName($tableName, $indexName)
655 3
    {
656 3
        $parts = $this->getSchemaName($tableName);
657 3
658 3
        $sql = sprintf(
659 3
            'DROP INDEX IF EXISTS %s',
660
            '"' . ($parts['schema'] . '".' . $this->quoteColumnName($indexName))
661
        );
662
        $this->execute($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
    public function addForeignKey(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
        $sql = sprintf(
733 68
            'ALTER TABLE %s ADD %s',
734 68
            $this->quoteTableName($table->getName()),
735 68
            $this->getForeignKeySqlDefinition($foreignKey, $table->getName())
736 68
        );
737 68
        $this->execute($sql);
738 68
    }
739 2
740 68
    /**
741 68
     * {@inheritdoc}
742 68
     */
743
    public function dropForeignKey($tableName, $columns, $constraint = null)
744 68
    {
745 68
        if (is_string($columns)) {
746 68
            $columns = [$columns]; // str to array
747 1
        }
748 68
749 68
        $parts = $this->getSchemaName($tableName);
750 68
751 15
        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...
752 15
            $this->execute(
753 1
                sprintf(
754
                    'ALTER TABLE %s DROP CONSTRAINT %s',
755
                    $this->quoteTableName($tableName),
756
                    $this->quoteColumnName($constraint)
757
                )
758 14
            );
759
        } else {
760
            foreach ($columns as $column) {
761 14
                $rows = $this->fetchAll(sprintf(
762
                    "SELECT CONSTRAINT_NAME
763
                      FROM information_schema.KEY_COLUMN_USAGE
764 14
                      WHERE TABLE_SCHEMA = %s
765
                        AND TABLE_NAME IS NOT NULL
766
                        AND TABLE_NAME = %s
767 14
                        AND COLUMN_NAME = %s
768
                      ORDER BY POSITION_IN_UNIQUE_CONSTRAINT",
769
                    $this->getConnection()->quote($parts['schema']),
770 14
                    $this->getConnection()->quote($parts['table']),
771 14
                    $this->getConnection()->quote($column)
772 13
                ));
773
774
                foreach ($rows as $row) {
775 1
                    $this->dropForeignKey($tableName, $columns, $row['constraint_name']);
776 14
                }
777
            }
778
        }
779
    }
780
781
    /**
782
     * {@inheritdoc}
783
     */
784
    public function getSqlType($type, $limit = null)
785 10
    {
786
        switch ($type) {
787
            case static::PHINX_TYPE_TEXT:
788 10
            case static::PHINX_TYPE_TIME:
789 10
            case static::PHINX_TYPE_DATE:
790 6
            case static::PHINX_TYPE_BOOLEAN:
791 10
            case static::PHINX_TYPE_JSON:
792 10
            case static::PHINX_TYPE_JSONB:
793
            case static::PHINX_TYPE_UUID:
794 10
            case static::PHINX_TYPE_CIDR:
795 2
            case static::PHINX_TYPE_INET:
796 10
            case static::PHINX_TYPE_MACADDR:
797
            case static::PHINX_TYPE_TIMESTAMP:
798 10
                return ['name' => $type];
799
            case static::PHINX_TYPE_INTEGER:
800 10
                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...
801
                    return [
802 1
                        'name' => 'smallint',
803
                        'limit' => static::INT_SMALL
804 1
                    ];
805 10
                }
806 10
807 10
                return ['name' => $type];
808 9
            case static::PHINX_TYPE_DECIMAL:
809 5
                return ['name' => $type, 'precision' => 18, 'scale' => 0];
810 5
            case static::PHINX_TYPE_STRING:
811 3
                return ['name' => 'character varying', 'limit' => 255];
812 4
            case static::PHINX_TYPE_CHAR:
813 4
                return ['name' => 'character', 'limit' => 255];
814 2
            case static::PHINX_TYPE_BIG_INTEGER:
815 4
                return ['name' => 'bigint'];
816 4
            case static::PHINX_TYPE_FLOAT:
817 2
                return ['name' => 'real'];
818 4
            case static::PHINX_TYPE_DATETIME:
819 1
                return ['name' => 'timestamp'];
820
            case static::PHINX_TYPE_BLOB:
821 4
            case static::PHINX_TYPE_BINARY:
822 4
                return ['name' => 'bytea'];
823 4
            case static::PHINX_TYPE_INTERVAL:
824 4
                return ['name' => 'interval'];
825 3
            // Geospatial database types
826 4
            // Spatial storage in Postgres is done via the PostGIS extension,
827 2
            // which enables the use of the "geography" type in combination
828 4
            // with SRID 4326.
829 4
            case static::PHINX_TYPE_GEOMETRY:
830 4
                return ['name' => 'geography', 'type' => 'geometry', 'srid' => 4326];
831 4
            case static::PHINX_TYPE_POINT:
832 3
                return ['name' => 'geography', 'type' => 'point', 'srid' => 4326];
833 3
            case static::PHINX_TYPE_LINESTRING:
834 3
                return ['name' => 'geography', 'type' => 'linestring', 'srid' => 4326];
835 3
            case static::PHINX_TYPE_POLYGON:
836 1
                return ['name' => 'geography', 'type' => 'polygon', 'srid' => 4326];
837 1
            default:
838
                if ($this->isArrayType($type)) {
839
                    return ['name' => $type];
840
                }
841
                // Return array type
842
                throw new \RuntimeException('The type: "' . $type . '" is not supported');
843
        }
844
    }
845
846
    /**
847
     * Returns Phinx type by SQL type
848
     *
849
     * @param string $sqlType SQL type
850
     * @returns string Phinx type
851
     */
852 1
    public function getPhinxType($sqlType)
853
    {
854 1
        switch ($sqlType) {
855 1
            case 'character varying':
856 1
            case 'varchar':
857
                return static::PHINX_TYPE_STRING;
858
            case 'character':
859
            case 'char':
860
                return static::PHINX_TYPE_CHAR;
861 2
            case 'text':
862
                return static::PHINX_TYPE_TEXT;
863 2
            case 'json':
864 2
                return static::PHINX_TYPE_JSON;
865 2
            case 'jsonb':
866
                return static::PHINX_TYPE_JSONB;
867
            case 'smallint':
868
                return [
869
                    'name' => 'smallint',
870
                    'limit' => static::INT_SMALL
871 1
                ];
872
            case 'int':
873 1
            case 'int4':
874 1
            case 'integer':
875 1
                return static::PHINX_TYPE_INTEGER;
876 1
            case 'decimal':
877
            case 'numeric':
878
                return static::PHINX_TYPE_DECIMAL;
879
            case 'bigint':
880
            case 'int8':
881
                return static::PHINX_TYPE_BIG_INTEGER;
882
            case 'real':
883
            case 'float4':
884 68
                return static::PHINX_TYPE_FLOAT;
885
            case 'bytea':
886 68
                return static::PHINX_TYPE_BINARY;
887 4
            case 'interval':
888 68
                return static::PHINX_TYPE_INTERVAL;
889 68
            case 'time':
890 68
            case 'timetz':
891 68
            case 'time with time zone':
892
            case 'time without time zone':
893
                return static::PHINX_TYPE_TIME;
894
            case 'date':
895
                return static::PHINX_TYPE_DATE;
896
            case 'timestamp':
897
            case 'timestamptz':
898
            case 'timestamp with time zone':
899
            case 'timestamp without time zone':
900 68
                return static::PHINX_TYPE_DATETIME;
901
            case 'bool':
902 68
            case 'boolean':
903 68
                return static::PHINX_TYPE_BOOLEAN;
904 50
            case 'uuid':
905 50
                return static::PHINX_TYPE_UUID;
906 68
            case 'cidr':
907 68
                return static::PHINX_TYPE_CIDR;
908
            case 'inet':
909 68
                return static::PHINX_TYPE_INET;
910 1
            case 'macaddr':
911 1
                return static::PHINX_TYPE_MACADDR;
912 1
            default:
913 1
                throw new \RuntimeException('The PostgreSQL type: "' . $sqlType . '" is not supported');
914 1
        }
915 68
    }
916
917
    /**
918
     * {@inheritdoc}
919
     */
920
    public function createDatabase($name, $options = [])
921
    {
922 68
        $charset = isset($options['charset']) ? $options['charset'] : 'utf8';
923 68
        $this->execute(sprintf("CREATE DATABASE %s WITH ENCODING = '%s'", $name, $charset));
924 68
    }
925 68
926 68
    /**
927
     * {@inheritdoc}
928
     */
929 68
    public function hasDatabase($name)
930 68
    {
931 68
        $sql = sprintf("SELECT count(*) FROM pg_database WHERE datname = '%s'", $name);
932 68
        $result = $this->fetchRow($sql);
933 1
934 1
        return $result['count'] > 0;
935
    }
936
937 68
    /**
938
     * {@inheritdoc}
939 68
     */
940 68
    public function dropDatabase($name)
941 68
    {
942
        $this->disconnect();
943 68
        $this->execute(sprintf('DROP DATABASE IF EXISTS %s', $name));
944
        $this->connect();
945
    }
946
947
    /**
948
     * Get the defintion for a `DEFAULT` statement.
949
     *
950
     * @param mixed $default default value
951
     * @param string $columnType column type added
952
     * @return string
953 6
     */
954 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...
955
    {
956 6
        if (is_string($default) && 'CURRENT_TIMESTAMP' !== $default) {
957 6
            $default = $this->getConnection()->quote($default);
958 6
        } elseif (is_bool($default)) {
959
            $default = $this->castToBool($default);
960 6
        } elseif ($columnType === static::PHINX_TYPE_BOOLEAN) {
961 6
            $default = $this->castToBool((bool)$default);
962 6
        }
963 6
964
        return isset($default) ? 'DEFAULT ' . $default : '';
965 6
    }
966
967
    /**
968
     * Gets the PostgreSQL Column Definition for a Column object.
969
     *
970
     * @param \Phinx\Db\Table\Column $column Column
971
     * @return string
972
     */
973
    protected function getColumnSqlDefinition(Column $column)
974
    {
975 7
        $buffer = [];
976
        if ($column->isIdentity()) {
977 7
            $buffer[] = $column->getType() == 'biginteger' ? 'BIGSERIAL' : 'SERIAL';
978 3
        } elseif ($column->getType() instanceof Literal) {
979 3
            $buffer[] = (string)$column->getType();
980 5
        } else {
981 5
            $sqlType = $this->getSqlType($column->getType(), $column->getLimit());
982
            $buffer[] = strtoupper($sqlType['name']);
983
984 5
            // integers cant have limits in postgres
985
            if (static::PHINX_TYPE_DECIMAL === $sqlType['name'] && ($column->getPrecision() || $column->getScale())) {
986 7
                $buffer[] = sprintf(
987 7
                    '(%s, %s)',
988 7
                    $column->getPrecision() ?: $sqlType['precision'],
989 7
                    $column->getScale() ?: $sqlType['scale']
990 7
                );
991 7
            } elseif (in_array($sqlType['name'], ['geography'])) {
992 7
                // geography type must be written with geometry type and srid, like this: geography(POLYGON,4326)
993 7
                $buffer[] = sprintf(
994
                    '(%s,%s)',
995
                    strtoupper($sqlType['type']),
996
                    $sqlType['srid']
997
                );
998
            } elseif (!in_array($sqlType['name'], ['integer', 'smallint', 'bigint', 'boolean'])) {
999
                if ($column->getLimit() || isset($sqlType['limit'])) {
1000
                    $buffer[] = sprintf('(%s)', $column->getLimit() ?: $sqlType['limit']);
1001
                }
1002
            }
1003 3
1004
            $timeTypes = [
1005 3
                'time',
1006 3
                'timestamp',
1007 3
            ];
1008 3
1009
            if (in_array($sqlType['name'], $timeTypes) && is_numeric($column->getPrecision())) {
1010
                $buffer[] = sprintf('(%s)', $column->getPrecision());
1011 3
            }
1012
1013
            if (in_array($sqlType['name'], $timeTypes) && $column->isTimezone()) {
1014 3
                $buffer[] = strtoupper('with time zone');
1015
            }
1016
        }
1017
1018
        $buffer[] = $column->isNull() ? 'NULL' : 'NOT NULL';
1019
1020 68
        if (!is_null($column->getDefault())) {
1021
            $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...
1022
        }
1023 68
1024 67
        return implode(' ', $buffer);
1025 67
    }
1026
1027 68
    /**
1028
     * Gets the PostgreSQL Column Comment Definition for a column object.
1029 68
     *
1030 68
     * @param \Phinx\Db\Table\Column $column Column
1031
     * @param string $tableName Table name
1032
     * @return string
1033
     */
1034
    protected function getColumnCommentSqlDefinition(Column $column, $tableName)
1035
    {
1036
        // passing 'null' is to remove column comment
1037
        $comment = (strcasecmp($column->getComment(), 'NULL') !== 0)
1038 68
                 ? $this->getConnection()->quote($column->getComment())
1039
                 : 'NULL';
1040 68
1041 68
        return sprintf(
1042 68
            'COMMENT ON COLUMN %s.%s IS %s;',
1043
            $this->quoteTableName($tableName),
1044
            $this->quoteColumnName($column->getName()),
1045
            $comment
1046
        );
1047
    }
1048
1049
    /**
1050 68
     * Gets the PostgreSQL Index Definition for an Index object.
1051
     *
1052 68
     * @param \Phinx\Db\Table\Index  $index Index
1053
     * @param string $tableName Table name
1054
     * @return string
1055 68
     */
1056
    protected function getIndexSqlDefinition(Index $index, $tableName)
1057 68
    {
1058 68
        $parts = $this->getSchemaName($tableName);
1059 68
1060
        if (is_string($index->getName())) {
1061
            $indexName = $index->getName();
1062 View Code Duplication
        } else {
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...
1063
            $columnNames = $index->getColumns();
1064
            if (is_string($columnNames)) {
1065
                $columnNames = [$columnNames];
1066
            }
1067
            $indexName = sprintf('%s_%s', $parts['table'], implode('_', $columnNames));
1068 68
        }
1069
        $def = sprintf(
1070 68
            "CREATE %s INDEX %s ON %s (%s);",
1071 68
            ($index->getType() === Index::UNIQUE ? 'UNIQUE' : ''),
1072 68
            $this->quoteColumnName($indexName),
1073
            $this->quoteTableName($tableName),
1074
            implode(',', array_map([$this, 'quoteColumnName'], $index->getColumns()))
1075
        );
1076
1077
        return $def;
1078
    }
1079 68
1080
    /**
1081 68
     * Gets the MySQL Foreign Key Definition for an ForeignKey object.
1082 68
     *
1083 68
     * @param \Phinx\Db\Table\ForeignKey $foreignKey
1084 68
     * @param string     $tableName  Table name
1085
     * @return string
1086
     */
1087
    protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, $tableName)
1088
    {
1089
        $parts = $this->getSchemaName($tableName);
1090
1091 68
        $constraintName = $foreignKey->getConstraint() ?: ($parts['table'] . '_' . implode('_', $foreignKey->getColumns()) . '_fkey');
1092
        $def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName)
0 ignored issues
show
Bug introduced by
It seems like $constraintName defined by $foreignKey->getConstrai...getColumns()) . '_fkey' on line 1091 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...
1093
            . ' FOREIGN KEY ("'
1094
            . implode('", "', $foreignKey->getColumns())
1095 68
            . '")'
1096 68
            . " REFERENCES {$this->quoteTableName($foreignKey->getReferencedTable()->getName())} (\""
1097 68
            . implode('", "', $foreignKey->getReferencedColumns())
1098 68
            . '")';
1099 68
        if ($foreignKey->getOnDelete()) {
1100 68
            $def .= " ON DELETE {$foreignKey->getOnDelete()}";
1101 68
        }
1102
        if ($foreignKey->getOnUpdate()) {
1103
            $def .= " ON UPDATE {$foreignKey->getOnUpdate()}";
1104
        }
1105
1106
        return $def;
1107 73
    }
1108
1109 73
    /**
1110
     * {@inheritdoc}
1111
     */
1112
    public function createSchemaTable()
1113
    {
1114
        // Create the public/custom schema if it doesn't already exist
1115 73
        if ($this->hasSchema($this->getGlobalSchemaName()) === false) {
1116
            $this->createSchema($this->getGlobalSchemaName());
1117
        }
1118 73
1119
        $this->fetchAll(sprintf('SET search_path TO %s', $this->getGlobalSchemaName()));
1120
1121
        parent::createSchemaTable();
1122
    }
1123
1124
    /**
1125
     * Creates the specified schema.
1126
     *
1127 14
     * @param  string $schemaName Schema Name
1128
     * @return void
1129 14
     */
1130 1
    public function createSchema($schemaName = 'public')
1131
    {
1132
        $sql = sprintf('CREATE SCHEMA %s;', $this->quoteSchemaName($schemaName)); // from postgres 9.3 we can use "CREATE SCHEMA IF NOT EXISTS schema_name"
1133 13
        $this->execute($sql);
1134 13
    }
1135
1136
    /**
1137
     * Checks to see if a schema exists.
1138
     *
1139
     * @param string $schemaName Schema Name
1140
     * @return bool
1141
     */
1142 68
    public function hasSchema($schemaName)
1143
    {
1144 68
        $sql = sprintf(
1145 68
            "SELECT count(*)
1146
             FROM pg_namespace
1147
             WHERE nspname = %s",
1148
            $this->getConnection()->quote($schemaName)
1149
        );
1150
        $result = $this->fetchRow($sql);
1151 68
1152
        return $result['count'] > 0;
1153 68
    }
1154
1155
    /**
1156
     * Drops the specified schema table.
1157
     *
1158
     * @param string $schemaName Schema name
1159
     * @return void
1160
     */
1161
    public function dropSchema($schemaName)
1162
    {
1163
        $sql = sprintf("DROP SCHEMA IF EXISTS %s CASCADE;", $this->quoteSchemaName($schemaName));
1164
        $this->execute($sql);
1165
    }
1166
1167
    /**
1168
     * Drops all schemas.
1169
     *
1170
     * @return void
1171
     */
1172
    public function dropAllSchemas()
1173
    {
1174
        foreach ($this->getAllSchemas() as $schema) {
1175
            $this->dropSchema($schema);
1176
        }
1177
    }
1178
1179
    /**
1180
     * Returns schemas.
1181
     *
1182
     * @return array
1183
     */
1184
    public function getAllSchemas()
1185
    {
1186
        $sql = "SELECT schema_name
1187
                FROM information_schema.schemata
1188
                WHERE schema_name <> 'information_schema' AND schema_name !~ '^pg_'";
1189
        $items = $this->fetchAll($sql);
1190
        $schemaNames = [];
1191
        foreach ($items as $item) {
1192
            $schemaNames[] = $item['schema_name'];
1193
        }
1194
1195
        return $schemaNames;
1196
    }
1197
1198
    /**
1199
     * {@inheritdoc}
1200
     */
1201
    public function getColumnTypes()
1202
    {
1203
        return array_merge(parent::getColumnTypes(), ['json', 'jsonb', 'cidr', 'inet', 'macaddr', 'interval']);
1204
    }
1205
1206
    /**
1207
     * {@inheritdoc}
1208
     */
1209
    public function isValidColumnType(Column $column)
1210
    {
1211
        // If not a standard column type, maybe it is array type?
1212
        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...
1213
    }
1214
1215
    /**
1216
     * Check if the given column is an array of a valid type.
1217
     *
1218
     * @param  string $columnType
1219
     * @return bool
1220
     */
1221
    protected function isArrayType($columnType)
1222
    {
1223
        if (!preg_match('/^([a-z]+)(?:\[\]){1,}$/', $columnType, $matches)) {
1224
            return false;
1225
        }
1226
1227
        $baseType = $matches[1];
1228
1229
        return in_array($baseType, $this->getColumnTypes());
1230
    }
1231
1232
    /**
1233
     * @param string $tableName
1234
     * @return array
1235
     */
1236
    private function getSchemaName($tableName)
1237
    {
1238
        $schema = $this->getGlobalSchemaName();
1239
        $table = $tableName;
1240
        if (false !== strpos($tableName, '.')) {
1241
            list($schema, $table) = explode('.', $tableName);
1242
        }
1243
1244
        return [
1245
            'schema' => $schema,
1246
            'table' => $table,
1247
        ];
1248
    }
1249
1250
    /**
1251
     * Gets the schema name.
1252
     *
1253
     * @return string
1254
     */
1255
    private function getGlobalSchemaName()
1256
    {
1257
        $options = $this->getOptions();
1258
1259
        return empty($options['schema']) ? 'public' : $options['schema'];
1260
    }
1261
1262
    /**
1263
     * {@inheritdoc}
1264
     */
1265
    public function castToBool($value)
1266
    {
1267
        return (bool)$value ? 'TRUE' : 'FALSE';
1268
    }
1269
}
1270