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

PostgresAdapter::hasTable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 16
ccs 8
cts 8
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 8
nc 1
nop 1
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 Phinx\Db\Table;
32
use Phinx\Db\Table\Column;
33
use Phinx\Db\Table\ForeignKey;
34
use Phinx\Db\Table\Index;
35
36
class PostgresAdapter extends PdoAdapter implements AdapterInterface
37
{
38
    const INT_SMALL = 65535;
39
40
    /**
41
     * Columns with comments
42
     *
43
     * @var array
44
     */
45
    protected $columnsWithComments = [];
46
47
    /**
48
     * {@inheritdoc}
49
     */
50 68
    public function connect()
51
    {
52 68
        if ($this->connection === null) {
53 68
            if (!class_exists('PDO') || !in_array('pgsql', \PDO::getAvailableDrivers(), true)) {
54
                // @codeCoverageIgnoreStart
55
                throw new \RuntimeException('You need to enable the PDO_Pgsql extension for Phinx to run properly.');
56
                // @codeCoverageIgnoreEnd
57
            }
58
59 68
            $db = null;
60 68
            $options = $this->getOptions();
61
62
            // if port is specified use it, otherwise use the PostgreSQL default
63 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...
64 68
                $dsn = 'pgsql:host=' . $options['host'] . ';port=' . $options['port'] . ';dbname=' . $options['name'];
65 68
            } else {
66 1
                $dsn = 'pgsql:host=' . $options['host'] . ';dbname=' . $options['name'];
67
            }
68
69
            try {
70 68
                $db = new \PDO($dsn, $options['user'], $options['pass'], [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]);
71 68
            } catch (\PDOException $exception) {
72 1
                throw new \InvalidArgumentException(sprintf(
73 1
                    'There was a problem connecting to the database: %s',
74 1
                    $exception->getMessage()
75 1
                ));
76
            }
77
78 68
            $this->setConnection($db);
79 68
        }
80 68
    }
81
82
    /**
83
     * {@inheritdoc}
84
     */
85 68
    public function disconnect()
86
    {
87 68
        $this->connection = null;
88 68
    }
89
90
    /**
91
     * {@inheritdoc}
92
     */
93
    public function hasTransactions()
94
    {
95
        return true;
96
    }
97
98
    /**
99
     * {@inheritdoc}
100
     */
101
    public function beginTransaction()
102
    {
103
        $this->execute('BEGIN');
104
    }
105
106
    /**
107
     * {@inheritdoc}
108
     */
109
    public function commitTransaction()
110
    {
111
        $this->execute('COMMIT');
112
    }
113
114
    /**
115
     * {@inheritdoc}
116
     */
117
    public function rollbackTransaction()
118
    {
119
        $this->execute('ROLLBACK');
120
    }
121
122
    /**
123
     * Quotes a schema name for use in a query.
124
     *
125
     * @param string $schemaName Schema Name
126
     * @return string
127
     */
128 68
    public function quoteSchemaName($schemaName)
129
    {
130 68
        return $this->quoteColumnName($schemaName);
131
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136 68
    public function quoteTableName($tableName)
137
    {
138 68
        $parts = $this->getSchemaName($tableName);
139
140
        return $this->quoteSchemaName($parts['schema']) . '.' . $this->quoteColumnName($parts['table']);
141
    }
142
143
    /**
144 68
     * {@inheritdoc}
145
     */
146 68
    public function quoteColumnName($columnName)
147
    {
148
        return '"'. $columnName . '"';
149
    }
150
151
    /**
152 68
     * {@inheritdoc}
153
     */
154 68
    public function hasTable($tableName)
155 68
    {
156
        $parts = $this->getSchemaName($tableName);
157
        $result = $this->getConnection()->query(
158
            sprintf(
159 68
                'SELECT *
160 68
                FROM information_schema.tables
161 68
                WHERE table_schema = %s
162 68
                AND table_name = %s',
163 68
                $this->getConnection()->quote($parts['schema']),
164
                $this->getConnection()->quote($parts['table'])
165 68
            )
166
        );
167
168
        return $result->rowCount() === 1;
169
    }
170
171 68
    /**
172
     * {@inheritdoc}
173 68
     */
174
    public function createTable(Table $table)
175
    {
176 68
        $options = $table->getOptions();
177 68
        $parts = $this->getSchemaName($table->getName());
178 48
179 48
         // Add the default primary key
180 48
        $columns = $table->getPendingColumns();
181 48 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...
182
            $column = new Column();
183 48
            $column->setName('id')
184 48
                   ->setType('integer')
185 68
                   ->setIdentity(true);
186
187 2
            array_unshift($columns, $column);
188 2
            $options['primary_key'] = 'id';
189 2
        } elseif (isset($options['id']) && is_string($options['id'])) {
190 2
            // Handle id => "field_name" to support AUTO_INCREMENT
191
            $column = new Column();
192 2
            $column->setName($options['id'])
193 2
                   ->setType('integer')
194 2
                   ->setIdentity(true);
195
196
            array_unshift($columns, $column);
197 68
            $options['primary_key'] = $options['id'];
198 68
        }
199
200 68
        // TODO - process table options like collation etc
201 68
        $sql = 'CREATE TABLE ';
202 68
        $sql .= $this->quoteTableName($table->getName()) . ' (';
203
204
        $this->columnsWithComments = [];
205 68 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...
206 6
            $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column) . ', ';
207 6
208 68
            // set column comments, if needed
209
            if ($column->getComment()) {
210
                $this->columnsWithComments[] = $column;
211 68
            }
212 68
        }
213 68
214 68
         // set the primary key(s)
215 68
        if (isset($options['primary_key'])) {
216 68
            $sql = rtrim($sql);
217
            $sql .= sprintf(' CONSTRAINT %s PRIMARY KEY (', $this->quoteColumnName($parts['table'] . '_pkey'));
218
            if (is_string($options['primary_key'])) { // handle primary_key => 'id'
219 1
                $sql .= $this->quoteColumnName($options['primary_key']);
220 1
            } 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...
221 1
                // PHP 5.4 will allow access of $this, so we can call quoteColumnName() directly in the anonymous function,
222 1
                // but for now just hard-code the adapter quotes
223 1
                $sql .= implode(
224 1
                    ',',
225 1
                    array_map(
226 1
                        function ($v) {
227 1
                            return '"' . $v . '"';
228 1
                        },
229 68
                        $options['primary_key']
230 68
                    )
231 2
                );
232
            }
233
            $sql .= ')';
234
        } else {
235 68
            $sql = substr(rtrim($sql), 0, -1); // no primary keys
236 68
        }
237 1
238 1
        // set the foreign keys
239 1
        $foreignKeys = $table->getForeignKeys();
240 1
        if (!empty($foreignKeys)) {
241
            foreach ($foreignKeys as $foreignKey) {
242 68
                $sql .= ', ' . $this->getForeignKeySqlDefinition($foreignKey, $table->getName());
243
            }
244
        }
245 68
246 6
        $sql .= ');';
247 6
248 6
        // process column comments
249 6
        if (!empty($this->columnsWithComments)) {
250
            foreach ($this->columnsWithComments as $column) {
251
                $sql .= $this->getColumnCommentSqlDefinition($column, $table->getName());
252
            }
253 68
        }
254 68
255 5
        // set the indexes
256 5
        $indexes = $table->getIndexes();
257 5
        if (!empty($indexes)) {
258 5
            foreach ($indexes as $index) {
259
                $sql .= $this->getIndexSqlDefinition($index, $table->getName());
260
            }
261 68
        }
262
263
        // execute the sql
264 68
        $this->execute($sql);
265 1
266 1
        // process table comments
267 1
        if (isset($options['comment'])) {
268 1
            $sql = sprintf(
269 1
                'COMMENT ON TABLE %s IS %s',
270 1
                $this->quoteTableName($table->getName()),
271 1
                $this->getConnection()->quote($options['comment'])
272 68
            );
273
            $this->execute($sql);
274
        }
275
    }
276
277 1
    /**
278
     * {@inheritdoc}
279 1
     */
280 1
    public function renameTable($tableName, $newTableName)
281 1
    {
282 1
        $sql = sprintf(
283 1
            'ALTER TABLE %s RENAME TO %s',
284 1
            $this->quoteTableName($tableName),
285 1
            $this->quoteColumnName($newTableName)
286
        );
287
        $this->execute($sql);
288
    }
289
290 1
    /**
291
     * {@inheritdoc}
292 1
     */
293 1
    public function dropTable($tableName)
294
    {
295
        $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($tableName)));
296
    }
297
298 1
    /**
299
     * {@inheritdoc}
300 1
     */
301 1
    public function truncateTable($tableName)
302 1
    {
303 1
        $sql = sprintf(
304
            'TRUNCATE TABLE %s',
305 1
            $this->quoteTableName($tableName)
306 1
        );
307
308
        $this->execute($sql);
309
    }
310
311 9
    /**
312
     * {@inheritdoc}
313 9
     */
314 9
    public function getColumns($tableName)
315
    {
316
        $parts = $this->getSchemaName($tableName);
317
        $columns = [];
318 9
        $sql = sprintf(
319
            "SELECT column_name, data_type, is_identity, is_nullable,
320 9
             column_default, character_maximum_length, numeric_precision, numeric_scale
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
            $column = new Column();
330 9
            $column->setName($columnInfo['column_name'])
331 9
                   ->setType($this->getPhinxType($columnInfo['data_type']))
0 ignored issues
show
Bug introduced by
It seems like $this->getPhinxType($columnInfo['data_type']) targeting Phinx\Db\Adapter\PostgresAdapter::getPhinxType() can also be of type array<string,string|inte...ng","limit":"integer"}> or null; however, Phinx\Db\Table\Column::setType() 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...
332
                   ->setNull($columnInfo['is_nullable'] === 'YES')
333 9
                   ->setDefault($columnInfo['column_default'])
334 1
                   ->setIdentity($columnInfo['is_identity'] === 'YES')
335 1
                   ->setPrecision($columnInfo['numeric_precision'])
336
                   ->setScale($columnInfo['numeric_scale']);
337 9
338 5
            if (preg_match('/\bwith time zone$/', $columnInfo['data_type'])) {
339 5
                $column->setTimezone(true);
340 9
            }
341 9
342 9
            if (isset($columnInfo['character_maximum_length'])) {
343
                $column->setLimit($columnInfo['character_maximum_length']);
344
            }
345
            $columns[] = $column;
346
        }
347
348 24
        return $columns;
349
    }
350 24
351
    /**
352
     * {@inheritdoc}
353 24
     */
354 24
    public function hasColumn($tableName, $columnName)
355 24
    {
356
        $parts = $this->getSchemaName($tableName);
357 24
        $sql = sprintf(
358
            "SELECT count(*)
359 24
            FROM information_schema.columns
360 24
            WHERE table_schema = %s AND table_name = %s AND column_name = %s",
361
            $this->getConnection()->quote($parts['schema']),
362
            $this->getConnection()->quote($parts['table']),
363
            $this->getConnection()->quote($columnName)
364
        );
365
366 18
        $result = $this->fetchRow($sql);
367
368 18
        return $result['count'] > 0;
369 18
    }
370 18
371 18
    /**
372 18
     * {@inheritdoc}
373 18
     */
374 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...
375 18
    {
376 18
        $sql = sprintf(
377
            'ALTER TABLE %s ADD %s %s',
378
            $this->quoteTableName($table->getName()),
379
            $this->quoteColumnName($column->getName()),
380
            $this->getColumnSqlDefinition($column)
381 3
        );
382
383 3
        $this->execute($sql);
384
    }
385
386 3
    /**
387 3
     * {@inheritdoc}
388
     */
389 3
    public function renameColumn($tableName, $columnName, $newColumnName)
390 3
    {
391 3
        $parts = $this->getSchemaName($tableName);
392 1
        $sql = sprintf(
393
            "SELECT CASE WHEN COUNT(*) > 0 THEN 1 ELSE 0 END AS column_exists
394 2
             FROM information_schema.columns
395 2
             WHERE table_schema = %s AND table_name = %s AND column_name = %s",
396 2
            $this->getConnection()->quote($parts['schema']),
397 2
            $this->getConnection()->quote($parts['table']),
398 2
            $this->getConnection()->quote($columnName)
399 2
        );
400 2
        $result = $this->fetchRow($sql);
401 2
        if (!(bool)$result['column_exists']) {
402 2
            throw new \InvalidArgumentException("The specified column does not exist: $columnName");
403
        }
404
        $this->execute(
405
            sprintf(
406
                'ALTER TABLE %s RENAME COLUMN %s TO %s',
407 5
                $this->quoteTableName($tableName),
408
                $this->quoteColumnName($columnName),
409
                $this->quoteColumnName($newColumnName)
410
            )
411 5
        );
412 5
    }
413 5
414 5
    /**
415 5
     * {@inheritdoc}
416 5
     */
417
    public function changeColumn($tableName, $columnName, Column $newColumn)
418 5
    {
419 5
        // TODO - is it possible to merge these 3 queries into less?
420
        // change data type
421 5
        $sql = sprintf(
422 5
            'ALTER TABLE %s ALTER COLUMN %s TYPE %s',
423
            $this->quoteTableName($tableName),
424 5
            $this->quoteColumnName($columnName),
425 5
            $this->getColumnSqlDefinition($newColumn)
426 5
        );
427 5
        //NULL and DEFAULT cannot be set while changing column type
428 5
        $sql = preg_replace('/ NOT NULL/', '', $sql);
429 5
        $sql = preg_replace('/ NULL/', '', $sql);
430 2
        //If it is set, DEFAULT is the last definition
431 2
        $sql = preg_replace('/DEFAULT .*/', '', $sql);
432 4
        $this->execute($sql);
433
        // process null
434 5
        $sql = sprintf(
435 5
            'ALTER TABLE %s ALTER COLUMN %s',
436
            $this->quoteTableName($tableName),
437 1
            $this->quoteColumnName($columnName)
438 1
        );
439 1
        if ($newColumn->isNull()) {
440 1
            $sql .= ' DROP NOT NULL';
441 1
        } else {
442 1
            $sql .= ' SET NOT NULL';
443 1
        }
444 1
        $this->execute($sql);
445 1
        if (!is_null($newColumn->getDefault())) {
446
            //change default
447 4
            $this->execute(
448 4
                sprintf(
449 4
                    'ALTER TABLE %s ALTER COLUMN %s SET %s',
450 4
                    $this->quoteTableName($tableName),
451 4
                    $this->quoteColumnName($columnName),
452 4
                    $this->getDefaultValueDefinition($newColumn->getDefault())
453 4
                )
454
            );
455
        } else {
456 5
            //drop default
457 1
            $this->execute(
458 1
                sprintf(
459 1
                    'ALTER TABLE %s ALTER COLUMN %s DROP DEFAULT',
460 1
                    $this->quoteTableName($tableName),
461 1
                    $this->quoteColumnName($columnName)
462 1
                )
463 1
            );
464 1
        }
465 1
        // rename column
466
        if ($columnName !== $newColumn->getName()) {
467
            $this->execute(
468 5
                sprintf(
469 2
                    'ALTER TABLE %s RENAME COLUMN %s TO %s',
470 2
                    $this->quoteTableName($tableName),
471 2
                    $this->quoteColumnName($columnName),
472 5
                    $this->quoteColumnName($newColumn->getName())
473
                )
474
            );
475
        }
476
477 1
        // change column comment if needed
478
        if ($newColumn->getComment()) {
479 1
            $sql = $this->getColumnCommentSqlDefinition($newColumn, $tableName);
480 1
            $this->execute($sql);
481 1
        }
482 1
    }
483 1
484 1
    /**
485 1
     * {@inheritdoc}
486 1
     */
487
    public function dropColumn($tableName, $columnName)
488
    {
489
        $this->execute(
490
            sprintf(
491
                'ALTER TABLE %s DROP COLUMN %s',
492
                $this->quoteTableName($tableName),
493
                $this->quoteColumnName($columnName)
494 9
            )
495
        );
496 9
    }
497
498
    /**
499
     * Get an array of indexes from a particular table.
500
     *
501
     * @param string $tableName Table Name
502
     * @return array
503
     */
504
    protected function getIndexes($tableName)
505
    {
506
        $parts = $this->getSchemaName($tableName);
507
508
        $indexes = [];
509
        $sql = sprintf(
510
            "SELECT
511
                i.relname AS index_name,
512
                a.attname AS column_name
513
            FROM
514 9
                pg_class t,
515 9
                pg_class i,
516 9
                pg_index ix,
517 9
                pg_attribute a,
518 9
                pg_namespace nsp
519 9
            WHERE
520 9
                t.oid = ix.indrelid
521 9
                AND i.oid = ix.indexrelid
522 9
                AND a.attrelid = t.oid
523
                AND a.attnum = ANY(ix.indkey)
524
                AND t.relnamespace = nsp.oid
525
                AND nsp.nspname = %s
526
                AND t.relkind = 'r'
527
                AND t.relname = %s
528 9
            ORDER BY
529
                t.relname,
530 9
                i.relname",
531 4
            $this->getConnection()->quote($parts['schema']),
532 4
            $this->getConnection()->quote($parts['table'])
533 9
        );
534 9
        $rows = $this->fetchAll($sql);
535 9 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...
536 9
            if (!isset($indexes[$row['index_name']])) {
537 9
                $indexes[$row['index_name']] = ['columns' => []];
538
            }
539 8
            $indexes[$row['index_name']]['columns'][] = $row['column_name'];
540 8
        }
541
542
        return $indexes;
543
    }
544
545
    /**
546 1
     * {@inheritdoc}
547
     */
548 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...
549 1
    {
550 1
        if (is_string($columns)) {
551 1
            $columns = [$columns];
552
        }
553
        $indexes = $this->getIndexes($tableName);
554
        foreach ($indexes as $index) {
555
            if (array_diff($index['columns'], $columns) === array_diff($columns, $index['columns'])) {
556
                return true;
557
            }
558
        }
559
560 2
        return false;
561
    }
562 2
563 2
    /**
564 2
     * {@inheritdoc}
565
     */
566 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...
567
    {
568
        $indexes = $this->getIndexes($tableName);
569 1
        foreach ($indexes as $name => $index) {
570
            if ($name === $indexName) {
571 1
                return true;
572 1
            }
573 1
        }
574
575 1
        return false;
576 1
    }
577
578 1
    /**
579 1
     * {@inheritdoc}
580 1
     */
581 1
    public function addIndex(Table $table, Index $index)
582 1
    {
583 1
        $sql = $this->getIndexSqlDefinition($index, $table->getName());
584 1
        $this->execute($sql);
585 1
    }
586 1
587
    /**
588 1
     * {@inheritdoc}
589
     */
590
    public function dropIndex($tableName, $columns)
591
    {
592
        $parts = $this->getSchemaName($tableName);
593
594
        if (is_string($columns)) {
595
            $columns = [$columns]; // str to array
596 1
        }
597
598 1
        $indexes = $this->getIndexes($tableName);
599 1
        foreach ($indexes as $indexName => $index) {
600
            $a = array_diff($columns, $index['columns']);
601 1
            if (empty($a)) {
602 1
                $this->execute(
603 1
                    sprintf(
604
                        'DROP INDEX IF EXISTS %s',
605
                        '"' . ($parts['schema'] . '".' . $this->quoteColumnName($indexName))
606
                    )
607
                );
608 3
609
                return;
610 3
            }
611 1
        }
612 1
    }
613 3
614 3
    /**
615
     * {@inheritdoc}
616
     */
617
    public function dropIndexByName($tableName, $indexName)
618
    {
619
        $parts = $this->getSchemaName($tableName);
620 3
621 3
        $sql = sprintf(
622 3
            'DROP INDEX IF EXISTS %s',
623 3
            '"' . ($parts['schema'] . '".' . $this->quoteColumnName($indexName))
624
        );
625 1
        $this->execute($sql);
626 1
    }
627
628
    /**
629
     * {@inheritdoc}
630
     */
631 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...
632
    {
633
        if (is_string($columns)) {
634
            $columns = [$columns]; // str to array
635
        }
636 3
        $foreignKeys = $this->getForeignKeys($tableName);
637
        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...
638 3
            if (isset($foreignKeys[$constraint])) {
639 3
                return !empty($foreignKeys[$constraint]);
640
            }
641
642
            return false;
643
        } else {
644
            foreach ($foreignKeys as $key) {
645
                $a = array_diff($columns, $key['columns']);
646
                if (empty($a)) {
647
                    return true;
648
                }
649
            }
650 3
651
            return false;
652 3
        }
653 3
    }
654 3
655 3
    /**
656 3
     * Get an array of foreign keys from a particular table.
657 3
     *
658 3
     * @param string $tableName Table Name
659 3
     * @return array
660
     */
661
    protected function getForeignKeys($tableName)
662
    {
663
        $parts = $this->getSchemaName($tableName);
664
        $foreignKeys = [];
665 2
        $rows = $this->fetchAll(sprintf(
666
            "SELECT
667 2
                    tc.constraint_name,
668 2
                    tc.table_name, kcu.column_name,
669 2
                    ccu.table_name AS referenced_table_name,
670 2
                    ccu.column_name AS referenced_column_name
671 2
                FROM
672 2
                    information_schema.table_constraints AS tc
673 2
                    JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
674
                    JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
675
                WHERE constraint_type = 'FOREIGN KEY' AND tc.table_schema = %s AND tc.table_name = %s
676
                ORDER BY kcu.position_in_unique_constraint",
677
            $this->getConnection()->quote($parts['schema']),
678 1
            $this->getConnection()->quote($parts['table'])
679
        ));
680 1
        foreach ($rows as $row) {
681
            $foreignKeys[$row['constraint_name']]['table'] = $row['table_name'];
682
            $foreignKeys[$row['constraint_name']]['columns'][] = $row['column_name'];
683
            $foreignKeys[$row['constraint_name']]['referenced_table'] = $row['referenced_table_name'];
684 1
            $foreignKeys[$row['constraint_name']]['referenced_columns'][] = $row['referenced_column_name'];
685 1
        }
686 1
687 1
        return $foreignKeys;
688 1
    }
689
690 1
    /**
691 1
     * {@inheritdoc}
692 1
     */
693 1 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...
694 1
    {
695
        $sql = sprintf(
696
            'ALTER TABLE %s ADD %s',
697
            $this->quoteTableName($table->getName()),
698
            $this->getForeignKeySqlDefinition($foreignKey, $table->getName())
699
        );
700
        $this->execute($sql);
701 1
    }
702 1
703
    /**
704 1
     * {@inheritdoc}
705
     */
706 1
    public function dropForeignKey($tableName, $columns, $constraint = null)
707 1
    {
708 1
        if (is_string($columns)) {
709 1
            $columns = [$columns]; // str to array
710
        }
711 1
712
        $parts = $this->getSchemaName($tableName);
713
714
        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...
715
            $this->execute(
716 68
                sprintf(
717
                    'ALTER TABLE %s DROP CONSTRAINT %s',
718
                    $this->quoteTableName($tableName),
719 68
                    $this->quoteColumnName($constraint)
720 14
                )
721
            );
722 1
        } else {
723
            foreach ($columns as $column) {
724 1
                $rows = $this->fetchAll(sprintf(
725
                    "SELECT CONSTRAINT_NAME
726 14
                      FROM information_schema.KEY_COLUMN_USAGE
727 68
                      WHERE TABLE_SCHEMA = %s
728 68
                        AND TABLE_NAME IS NOT NULL
729 68
                        AND TABLE_NAME = %s
730 68
                        AND COLUMN_NAME = %s
731 68
                      ORDER BY POSITION_IN_UNIQUE_CONSTRAINT",
732 68
                    $this->getConnection()->quote($parts['schema']),
733 68
                    $this->getConnection()->quote($parts['table']),
734 68
                    $this->getConnection()->quote($column)
735 68
                ));
736 68
737 68
                foreach ($rows as $row) {
738 68
                    $this->dropForeignKey($tableName, $columns, $row['constraint_name']);
739 2
                }
740 68
            }
741 68
        }
742 68
    }
743
744 68
    /**
745 68
     * {@inheritdoc}
746 68
     */
747 1
    public function getSqlType($type, $limit = null)
748 68
    {
749 68
        switch ($type) {
750 68
            case static::PHINX_TYPE_INTEGER:
751 15
                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...
752 15
                    return [
753 1
                        'name' => 'smallint',
754
                        'limit' => static::INT_SMALL
755
                    ];
756
                }
757
758 14
                return ['name' => $type];
759
            case static::PHINX_TYPE_TEXT:
760
            case static::PHINX_TYPE_TIME:
761 14
            case static::PHINX_TYPE_DATE:
762
            case static::PHINX_TYPE_BOOLEAN:
763
            case static::PHINX_TYPE_JSON:
764 14
            case static::PHINX_TYPE_JSONB:
765
            case static::PHINX_TYPE_UUID:
766
            case static::PHINX_TYPE_CIDR:
767 14
            case static::PHINX_TYPE_INET:
768
            case static::PHINX_TYPE_MACADDR:
769
                return ['name' => $type];
770 14
            case static::PHINX_TYPE_DECIMAL:
771 14
                return ['name' => $type, 'precision' => 18, 'scale' => 0];
772 13
            case static::PHINX_TYPE_STRING:
773
                return ['name' => 'character varying', 'limit' => 255];
774
            case static::PHINX_TYPE_CHAR:
775 1
                return ['name' => 'character', 'limit' => 255];
776 14
            case static::PHINX_TYPE_BIG_INTEGER:
777
                return ['name' => 'bigint'];
778
            case static::PHINX_TYPE_FLOAT:
779
                return ['name' => 'real'];
780
            case static::PHINX_TYPE_DATETIME:
781
            case static::PHINX_TYPE_TIMESTAMP:
782
                return ['name' => 'timestamp'];
783
            case static::PHINX_TYPE_BLOB:
784
            case static::PHINX_TYPE_BINARY:
785 10
                return ['name' => 'bytea'];
786
            // Geospatial database types
787
            // Spatial storage in Postgres is done via the PostGIS extension,
788 10
            // which enables the use of the "geography" type in combination
789 10
            // with SRID 4326.
790 6
            case static::PHINX_TYPE_GEOMETRY:
791 10
                return ['name' => 'geography', 'type' => 'geometry', 'srid' => 4326];
792 10
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
793
            case static::PHINX_TYPE_POINT:
794 10
                return ['name' => 'geography', 'type' => 'point', 'srid' => 4326];
795 2
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
796 10
            case static::PHINX_TYPE_LINESTRING:
797
                return ['name' => 'geography', 'type' => 'linestring', 'srid' => 4326];
798 10
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
799
            case static::PHINX_TYPE_POLYGON:
800 10
                return ['name' => 'geography', 'type' => 'polygon', 'srid' => 4326];
801
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
802 1
            default:
803
                if ($this->isArrayType($type)) {
804 1
                    return ['name' => $type];
805 10
                }
806 10
                // Return array type
807 10
                throw new \RuntimeException('The type: "' . $type . '" is not supported');
808 9
        }
809 5
    }
810 5
811 3
    /**
812 4
     * Returns Phinx type by SQL type
813 4
     *
814 2
     * @param string $sqlType SQL type
815 4
     * @returns string Phinx type
816 4
     */
817 2
    public function getPhinxType($sqlType)
818 4
    {
819 1
        switch ($sqlType) {
820
            case 'character varying':
821 4
            case 'varchar':
822 4
                return static::PHINX_TYPE_STRING;
823 4
            case 'character':
824 4
            case 'char':
825 3
                return static::PHINX_TYPE_CHAR;
826 4
            case 'text':
827 2
                return static::PHINX_TYPE_TEXT;
828 4
            case 'json':
829 4
                return static::PHINX_TYPE_JSON;
830 4
            case 'jsonb':
831 4
                return static::PHINX_TYPE_JSONB;
832 3
            case 'smallint':
833 3
                return [
834 3
                    'name' => 'smallint',
835 3
                    'limit' => static::INT_SMALL
836 1
                ];
837 1
            case 'int':
838
            case 'int4':
839
            case 'integer':
840
                return static::PHINX_TYPE_INTEGER;
841
            case 'decimal':
842
            case 'numeric':
843
                return static::PHINX_TYPE_DECIMAL;
844
            case 'bigint':
845
            case 'int8':
846
                return static::PHINX_TYPE_BIG_INTEGER;
847
            case 'real':
848
            case 'float4':
849
                return static::PHINX_TYPE_FLOAT;
850
            case 'bytea':
851
                return static::PHINX_TYPE_BINARY;
852 1
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
853
            case 'time':
854 1
            case 'timetz':
855 1
            case 'time with time zone':
856 1
            case 'time without time zone':
857
                return static::PHINX_TYPE_TIME;
858
            case 'date':
859
                return static::PHINX_TYPE_DATE;
860
            case 'timestamp':
861 2
            case 'timestamptz':
862
            case 'timestamp with time zone':
863 2
            case 'timestamp without time zone':
864 2
                return static::PHINX_TYPE_DATETIME;
865 2
            case 'bool':
866
            case 'boolean':
867
                return static::PHINX_TYPE_BOOLEAN;
868
            case 'uuid':
869
                return static::PHINX_TYPE_UUID;
870
            case 'cidr':
871 1
                return static::PHINX_TYPE_CIDR;
872
            case 'inet':
873 1
                return static::PHINX_TYPE_INET;
874 1
            case 'macaddr':
875 1
                return static::PHINX_TYPE_MACADDR;
876 1
            default:
877
                throw new \RuntimeException('The PostgreSQL type: "' . $sqlType . '" is not supported');
878
        }
879
    }
880
881
    /**
882
     * {@inheritdoc}
883
     */
884 68
    public function createDatabase($name, $options = [])
885
    {
886 68
        $charset = isset($options['charset']) ? $options['charset'] : 'utf8';
887 4
        $this->execute(sprintf("CREATE DATABASE %s WITH ENCODING = '%s'", $name, $charset));
888 68
    }
889 68
890 68
    /**
891 68
     * {@inheritdoc}
892
     */
893
    public function hasDatabase($name)
894
    {
895
        $sql = sprintf("SELECT count(*) FROM pg_database WHERE datname = '%s'", $name);
896
        $result = $this->fetchRow($sql);
897
898
        return $result['count'] > 0;
899
    }
900 68
901
    /**
902 68
     * {@inheritdoc}
903 68
     */
904 50
    public function dropDatabase($name)
905 50
    {
906 68
        $this->disconnect();
907 68
        $this->execute(sprintf('DROP DATABASE IF EXISTS %s', $name));
908
        $this->connect();
909 68
    }
910 1
911 1
    /**
912 1
     * Get the defintion for a `DEFAULT` statement.
913 1
     *
914 1
     * @param  mixed $default
915 68
     * @return string
916
     */
917 View Code Duplication
    protected function getDefaultValueDefinition($default)
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...
918
    {
919
        if (is_string($default) && 'CURRENT_TIMESTAMP' !== $default) {
920
            $default = $this->getConnection()->quote($default);
921
        } elseif (is_bool($default)) {
922 68
            $default = $this->castToBool($default);
923 68
        }
924 68
925 68
        return isset($default) ? 'DEFAULT ' . $default : '';
926 68
    }
927
928
    /**
929 68
     * Gets the PostgreSQL Column Definition for a Column object.
930 68
     *
931 68
     * @param \Phinx\Db\Table\Column $column Column
932 68
     * @return string
933 1
     */
934 1
    protected function getColumnSqlDefinition(Column $column)
935
    {
936
        $buffer = [];
937 68
        if ($column->isIdentity()) {
938
            $buffer[] = $column->getType() == 'biginteger' ? 'BIGSERIAL' : 'SERIAL';
939 68
        } else {
940 68
            $sqlType = $this->getSqlType($column->getType(), $column->getLimit());
941 68
            $buffer[] = strtoupper($sqlType['name']);
942
            // integers cant have limits in postgres
943 68
            if (static::PHINX_TYPE_DECIMAL === $sqlType['name'] && ($column->getPrecision() || $column->getScale())) {
944
                $buffer[] = sprintf(
945
                    '(%s, %s)',
946
                    $column->getPrecision() ? $column->getPrecision() : $sqlType['precision'],
947
                    $column->getScale() ? $column->getScale() : $sqlType['scale']
948
                );
949
            } elseif (in_array($sqlType['name'], ['geography'])) {
950
                // geography type must be written with geometry type and srid, like this: geography(POLYGON,4326)
951
                $buffer[] = sprintf(
952
                    '(%s,%s)',
953 6
                    strtoupper($sqlType['type']),
954
                    $sqlType['srid']
955
                );
956 6
            } elseif (!in_array($sqlType['name'], ['integer', 'smallint', 'bigint'])) {
957 6
                if ($column->getLimit() || isset($sqlType['limit'])) {
958 6
                    $buffer[] = sprintf('(%s)', $column->getLimit() ? $column->getLimit() : $sqlType['limit']);
959
                }
960 6
            }
961 6
962 6
            $timeTypes = [
963 6
                'time',
964
                'timestamp',
965 6
            ];
966
            if (in_array($sqlType['name'], $timeTypes) && $column->isTimezone()) {
967
                $buffer[] = strtoupper('with time zone');
968
            }
969
        }
970
971
        $buffer[] = $column->isNull() ? 'NULL' : 'NOT NULL';
972
973
        if (!is_null($column->getDefault())) {
974
            $buffer[] = $this->getDefaultValueDefinition($column->getDefault());
975 7
        }
976
977 7
        return implode(' ', $buffer);
978 3
    }
979 3
980 5
    /**
981 5
     * Gets the PostgreSQL Column Comment Definition for a column object.
982
     *
983
     * @param \Phinx\Db\Table\Column $column Column
984 5
     * @param string $tableName Table name
985
     * @return string
986 7
     */
987 7
    protected function getColumnCommentSqlDefinition(Column $column, $tableName)
988 7
    {
989 7
        // passing 'null' is to remove column comment
990 7
        $comment = (strcasecmp($column->getComment(), 'NULL') !== 0)
991 7
                 ? $this->getConnection()->quote($column->getComment())
992 7
                 : 'NULL';
993 7
994
        return sprintf(
995
            'COMMENT ON COLUMN %s.%s IS %s;',
996
            $this->quoteTableName($tableName),
997
            $this->quoteColumnName($column->getName()),
998
            $comment
999
        );
1000
    }
1001
1002
    /**
1003 3
     * Gets the PostgreSQL Index Definition for an Index object.
1004
     *
1005 3
     * @param \Phinx\Db\Table\Index  $index Index
1006 3
     * @param string $tableName Table name
1007 3
     * @return string
1008 3
     */
1009
    protected function getIndexSqlDefinition(Index $index, $tableName)
1010
    {
1011 3
        $parts = $this->getSchemaName($tableName);
1012
1013
        if (is_string($index->getName())) {
1014 3
            $indexName = $index->getName();
1015 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...
1016
            $columnNames = $index->getColumns();
1017
            if (is_string($columnNames)) {
1018
                $columnNames = [$columnNames];
1019
            }
1020 68
            $indexName = sprintf('%s_%s', $parts['table'], implode('_', $columnNames));
1021
        }
1022
        $def = sprintf(
1023 68
            "CREATE %s INDEX %s ON %s (%s);",
1024 67
            ($index->getType() === Index::UNIQUE ? 'UNIQUE' : ''),
1025 67
            $this->quoteColumnName($indexName),
1026
            $this->quoteTableName($tableName),
1027 68
            implode(',', array_map([$this, 'quoteColumnName'], $index->getColumns()))
1028
        );
1029 68
1030 68
        return $def;
1031
    }
1032
1033
    /**
1034
     * Gets the MySQL Foreign Key Definition for an ForeignKey object.
1035
     *
1036
     * @param \Phinx\Db\Table\ForeignKey $foreignKey
1037
     * @param string     $tableName  Table name
1038 68
     * @return string
1039
     */
1040 68
    protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, $tableName)
1041 68
    {
1042 68
        $parts = $this->getSchemaName($tableName);
1043
1044
        $constraintName = $foreignKey->getConstraint()
1045
            ? $foreignKey->getConstraint()
1046
            : ($parts['table'] . '_' . implode('_', $foreignKey->getColumns()) . '_fkey');
1047
        $def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName)
0 ignored issues
show
Bug introduced by
It seems like $constraintName defined by $foreignKey->getConstrai...getColumns()) . '_fkey' on line 1044 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...
1048
            . ' FOREIGN KEY ("'
1049
            . implode('", "', $foreignKey->getColumns())
1050 68
            . '")'
1051
            . " REFERENCES {$this->quoteTableName($foreignKey->getReferencedTable()->getName())} (\""
1052 68
            . implode('", "', $foreignKey->getReferencedColumns())
1053
            . '")';
1054
        if ($foreignKey->getOnDelete()) {
1055 68
            $def .= " ON DELETE {$foreignKey->getOnDelete()}";
1056
        }
1057 68
        if ($foreignKey->getOnUpdate()) {
1058 68
            $def .= " ON UPDATE {$foreignKey->getOnUpdate()}";
1059 68
        }
1060
1061
        return $def;
1062
    }
1063
1064
    /**
1065
     * {@inheritdoc}
1066
     */
1067
    public function createSchemaTable()
1068 68
    {
1069
        // Create the public/custom schema if it doesn't already exist
1070 68
        if ($this->hasSchema($this->getGlobalSchemaName()) === false) {
1071 68
            $this->createSchema($this->getGlobalSchemaName());
1072 68
        }
1073
1074
        $this->fetchAll(sprintf('SET search_path TO %s', $this->getGlobalSchemaName()));
1075
1076
        parent::createSchemaTable();
1077
    }
1078
1079 68
    /**
1080
     * Creates the specified schema.
1081 68
     *
1082 68
     * @param  string $schemaName Schema Name
1083 68
     * @return void
1084 68
     */
1085
    public function createSchema($schemaName = 'public')
1086
    {
1087
        $sql = sprintf('CREATE SCHEMA %s;', $this->quoteSchemaName($schemaName)); // from postgres 9.3 we can use "CREATE SCHEMA IF NOT EXISTS schema_name"
1088
        $this->execute($sql);
1089
    }
1090
1091 68
    /**
1092
     * Checks to see if a schema exists.
1093
     *
1094
     * @param string $schemaName Schema Name
1095 68
     * @return bool
1096 68
     */
1097 68
    public function hasSchema($schemaName)
1098 68
    {
1099 68
        $sql = sprintf(
1100 68
            "SELECT count(*)
1101 68
             FROM pg_namespace
1102
             WHERE nspname = %s",
1103
            $this->getConnection()->quote($schemaName)
1104
        );
1105
        $result = $this->fetchRow($sql);
1106
1107 73
        return $result['count'] > 0;
1108
    }
1109 73
1110
    /**
1111
     * Drops the specified schema table.
1112
     *
1113
     * @param string $schemaName Schema name
1114
     * @return void
1115 73
     */
1116
    public function dropSchema($schemaName)
1117
    {
1118 73
        $sql = sprintf("DROP SCHEMA IF EXISTS %s CASCADE;", $this->quoteSchemaName($schemaName));
1119
        $this->execute($sql);
1120
    }
1121
1122
    /**
1123
     * Drops all schemas.
1124
     *
1125
     * @return void
1126
     */
1127 14
    public function dropAllSchemas()
1128
    {
1129 14
        foreach ($this->getAllSchemas() as $schema) {
1130 1
            $this->dropSchema($schema);
1131
        }
1132
    }
1133 13
1134 13
    /**
1135
     * Returns schemas.
1136
     *
1137
     * @return array
1138
     */
1139
    public function getAllSchemas()
1140
    {
1141
        $sql = "SELECT schema_name
1142 68
                FROM information_schema.schemata
1143
                WHERE schema_name <> 'information_schema' AND schema_name !~ '^pg_'";
1144 68
        $items = $this->fetchAll($sql);
1145 68
        $schemaNames = [];
1146
        foreach ($items as $item) {
1147
            $schemaNames[] = $item['schema_name'];
1148
        }
1149
1150
        return $schemaNames;
1151 68
    }
1152
1153 68
    /**
1154
     * {@inheritdoc}
1155
     */
1156
    public function getColumnTypes()
1157
    {
1158
        return array_merge(parent::getColumnTypes(), ['json', 'jsonb', 'cidr', 'inet', 'macaddr']);
1159
    }
1160
1161
    /**
1162
     * {@inheritdoc}
1163
     */
1164
    public function isValidColumnType(Column $column)
1165
    {
1166
        // If not a standard column type, maybe it is array type?
1167
        return (parent::isValidColumnType($column) || $this->isArrayType($column->getType()));
1168
    }
1169
1170
    /**
1171
     * Check if the given column is an array of a valid type.
1172
     *
1173
     * @param  string $columnType
1174
     * @return bool
1175
     */
1176
    protected function isArrayType($columnType)
1177
    {
1178
        if (!preg_match('/^([a-z]+)(?:\[\]){1,}$/', $columnType, $matches)) {
1179
            return false;
1180
        }
1181
1182
        $baseType = $matches[1];
1183
1184
        return in_array($baseType, $this->getColumnTypes());
1185
    }
1186
1187
    /**
1188
     * @param  string $tableName
1189
     * @return array
1190
     */
1191
    private function getSchemaName($tableName)
1192
    {
1193
        $schema = $this->getGlobalSchemaName();
1194
        $table = $tableName;
1195
        if (false !== strpos($tableName, '.')) {
1196
            list($schema, $table) = explode('.', $tableName);
1197
        }
1198
1199
        return [
1200
            'schema' => $schema,
1201
            'table'  => $table,
1202
        ];
1203
    }
1204
1205
    /**
1206
     * Gets the schema name.
1207
     *
1208
     * @return string
1209
     */
1210
    private function getGlobalSchemaName()
1211
    {
1212
        $options = $this->getOptions();
1213
1214
        return empty($options['schema']) ? 'public' : $options['schema'];
1215
    }
1216
1217
    /**
1218
     * {@inheritdoc}
1219
     */
1220
    public function castToBool($value)
1221
    {
1222
        return (bool)$value ? 'TRUE' : 'FALSE';
1223
    }
1224
}
1225