Completed
Pull Request — master (#3445)
by Evgeniy
62:43
created

PostgreSqlPlatform::getListSequencesSQL()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 1
cts 1
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
namespace Doctrine\DBAL\Platforms;
4
5
use Doctrine\DBAL\Schema\Column;
6
use Doctrine\DBAL\Schema\ColumnDiff;
7
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
8
use Doctrine\DBAL\Schema\Identifier;
9
use Doctrine\DBAL\Schema\Index;
10
use Doctrine\DBAL\Schema\Sequence;
11
use Doctrine\DBAL\Schema\TableDiff;
12
use Doctrine\DBAL\Types\BigIntType;
13
use Doctrine\DBAL\Types\BinaryType;
14
use Doctrine\DBAL\Types\BlobType;
15
use Doctrine\DBAL\Types\IntegerType;
16
use Doctrine\DBAL\Types\Type;
17
use UnexpectedValueException;
18
use function array_diff;
19
use function array_merge;
20
use function array_unique;
21
use function array_values;
22
use function count;
23
use function explode;
24
use function implode;
25
use function in_array;
26
use function is_array;
27
use function is_bool;
28
use function is_numeric;
29
use function is_string;
30
use function pg_escape_bytea;
31
use function sprintf;
32
use function strpos;
33
use function strtolower;
34
use function trim;
35
36
/**
37
 * PostgreSqlPlatform.
38
 *
39
 * @todo   Rename: PostgreSQLPlatform
40
 */
41
class PostgreSqlPlatform extends AbstractPlatform
42
{
43
    /** @var bool */
44
    private $useBooleanTrueFalseStrings = true;
45
46
    /** @var string[][] PostgreSQL booleans literals */
47
    private $booleanLiterals = [
48
        'true' => [
49
            't',
50
            'true',
51
            'y',
52
            'yes',
53
            'on',
54
            '1',
55
        ],
56
        'false' => [
57
            'f',
58
            'false',
59
            'n',
60
            'no',
61
            'off',
62
            '0',
63
        ],
64
    ];
65
66
    /**
67
     * PostgreSQL has different behavior with some drivers
68
     * with regard to how booleans have to be handled.
69
     *
70
     * Enables use of 'true'/'false' or otherwise 1 and 0 instead.
71
     *
72
     * @param bool $flag
73 270
     */
74
    public function setUseBooleanTrueFalseStrings($flag)
75 270
    {
76 270
        $this->useBooleanTrueFalseStrings = (bool) $flag;
77
    }
78
79
    /**
80
     * {@inheritDoc}
81 142
     */
82
    public function getSubstringExpression($value, $from, $length = null)
83 142
    {
84 142
        if ($length === null) {
85
            return 'SUBSTRING(' . $value . ' FROM ' . $from . ')';
86
        }
87 135
88
        return 'SUBSTRING(' . $value . ' FROM ' . $from . ' FOR ' . $length . ')';
89
    }
90
91
    /**
92
     * {@inheritDoc}
93
     */
94
    public function getNowExpression()
95
    {
96
        return 'LOCALTIMESTAMP(0)';
97
    }
98
99
    /**
100
     * {@inheritDoc}
101 135
     */
102
    public function getRegexpExpression()
103 135
    {
104
        return 'SIMILAR TO';
105
    }
106
107
    /**
108
     * {@inheritDoc}
109 7
     */
110
    public function getLocateExpression($str, $substr, $startPos = false)
111 7
    {
112 7
        if ($startPos !== false) {
113
            $str = $this->getSubstringExpression($str, $startPos);
114 7
115
            return 'CASE WHEN (POSITION(' . $substr . ' IN ' . $str . ') = 0) THEN 0 ELSE (POSITION(' . $substr . ' IN ' . $str . ') + ' . ($startPos-1) . ') END';
116
        }
117 7
118
        return 'POSITION(' . $substr . ' IN ' . $str . ')';
119
    }
120
121
    /**
122
     * {@inheritdoc}
123 7
     */
124
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
125 7
    {
126 7
        if ($unit === DateIntervalUnit::QUARTER) {
127 7
            $interval *= 3;
128
            $unit      = DateIntervalUnit::MONTH;
129
        }
130 7
131
        return '(' . $date . ' ' . $operator . ' (' . $interval . " || ' " . $unit . "')::interval)";
132
    }
133
134
    /**
135
     * {@inheritDoc}
136 21
     */
137
    public function getDateDiffExpression($date1, $date2)
138 21
    {
139
        return '(DATE(' . $date1 . ')-DATE(' . $date2 . '))';
140
    }
141
142
    /**
143
     * {@inheritDoc}
144 184
     */
145
    public function supportsSequences()
146 184
    {
147
        return true;
148
    }
149
150
    /**
151
     * {@inheritDoc}
152 28
     */
153
    public function supportsSchemas()
154 28
    {
155
        return true;
156
    }
157
158
    /**
159
     * {@inheritdoc}
160 7
     */
161
    public function getDefaultSchemaName()
162 7
    {
163
        return 'public';
164
    }
165
166
    /**
167
     * {@inheritDoc}
168 156
     */
169
    public function supportsIdentityColumns()
170 156
    {
171
        return true;
172
    }
173
174
    /**
175
     * {@inheritdoc}
176 1260
     */
177
    public function supportsPartialIndexes()
178 1260
    {
179
        return true;
180
    }
181
182
    /**
183
     * {@inheritdoc}
184 142
     */
185
    public function usesSequenceEmulatedIdentityColumns()
186 142
    {
187
        return true;
188
    }
189
190
    /**
191
     * {@inheritdoc}
192 149
     */
193
    public function getIdentitySequenceName($tableName, $columnName)
194 149
    {
195
        return $tableName . '_' . $columnName . '_seq';
196
    }
197
198
    /**
199
     * {@inheritDoc}
200 3175
     */
201
    public function supportsCommentOnStatement()
202 3175
    {
203
        return true;
204
    }
205
206
    /**
207
     * {@inheritDoc}
208 135
     */
209
    public function prefersSequences()
210 135
    {
211
        return true;
212
    }
213
214
    /**
215
     * {@inheritDoc}
216 6833
     */
217
    public function hasNativeGuidType()
218 6833
    {
219
        return true;
220
    }
221
222
    /**
223
     * {@inheritDoc}
224 14
     */
225
    public function getListDatabasesSQL()
226 14
    {
227
        return 'SELECT datname FROM pg_database';
228
    }
229
230
    /**
231
     * {@inheritDoc}
232 14
     */
233
    public function getListNamespacesSQL()
234 14
    {
235
        return "SELECT schema_name AS nspname
236
                FROM   information_schema.schemata
237
                WHERE  schema_name NOT LIKE 'pg\_%'
238
                AND    schema_name != 'information_schema'";
239
    }
240
241
    /**
242
     * {@inheritDoc}
243 30
     */
244
    public function getListSequencesSQL($database)
245 30
    {
246
        return "SELECT sequence_name AS relname,
247
                       sequence_schema AS schemaname
248
                FROM   information_schema.sequences
249
                WHERE  sequence_schema NOT LIKE 'pg\_%'
250
                AND    sequence_schema != 'information_schema'";
251
    }
252
253
    /**
254
     * {@inheritDoc}
255 612
     */
256
    public function getListTablesSQL()
257 612
    {
258
        return "SELECT quote_ident(table_name) AS table_name,
259
                       table_schema AS schema_name
260
                FROM   information_schema.tables
261
                WHERE  table_schema NOT LIKE 'pg\_%'
262
                AND    table_schema != 'information_schema'
263
                AND    table_name != 'geometry_columns'
264
                AND    table_name != 'spatial_ref_sys'
265
                AND    table_type != 'VIEW'";
266
    }
267
268
    /**
269
     * {@inheritDoc}
270 7
     */
271
    public function getListViewsSQL($database)
272 7
    {
273
        return 'SELECT quote_ident(table_name) AS viewname,
274
                       table_schema AS schemaname,
275
                       view_definition AS definition
276
                FROM   information_schema.views
277
                WHERE  view_definition IS NOT NULL';
278
    }
279
280
    /**
281
     * {@inheritDoc}
282 585
     */
283
    public function getListTableForeignKeysSQL($table, $database = null)
0 ignored issues
show
Unused Code introduced by
The parameter $database is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

283
    public function getListTableForeignKeysSQL($table, /** @scrutinizer ignore-unused */ $database = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
284
    {
285
        return 'SELECT quote_ident(r.conname) as conname, pg_catalog.pg_get_constraintdef(r.oid, true) as condef
286
                  FROM pg_catalog.pg_constraint r
287
                  WHERE r.conrelid =
288
                  (
289
                      SELECT c.oid
290 585
                      FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n
291
                      WHERE ' . $this->getTableWhereClause($table) . " AND n.oid = c.relnamespace
292
                  )
293
                  AND r.contype = 'f'";
294
    }
295
296
    /**
297
     * {@inheritDoc}
298 14
     */
299
    public function getCreateViewSQL($name, $sql)
300 14
    {
301
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
302
    }
303
304
    /**
305
     * {@inheritDoc}
306 14
     */
307
    public function getDropViewSQL($name)
308 14
    {
309
        return 'DROP VIEW ' . $name;
310
    }
311
312
    /**
313
     * {@inheritDoc}
314 135
     */
315
    public function getListTableConstraintsSQL($table)
316 135
    {
317 135
        $table = new Identifier($table);
318
        $table = $this->quoteStringLiteral($table->getName());
319 135
320
        return sprintf(
321 135
            <<<'SQL'
322
SELECT
323
    quote_ident(relname) as relname
324
FROM
325
    pg_class
326
WHERE oid IN (
327
    SELECT indexrelid
328
    FROM pg_index, pg_class
329
    WHERE pg_class.relname = %s
330
        AND pg_class.oid = pg_index.indrelid
331
        AND (indisunique = 't' OR indisprimary = 't')
332
    )
333
SQL
334 135
            ,
335
            $table
336
        );
337
    }
338
339
    /**
340
     * {@inheritDoc}
341
     *
342
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
343 599
     */
344
    public function getListTableIndexesSQL($table, $currentDatabase = null)
345
    {
346
        return 'SELECT quote_ident(relname) as relname, pg_index.indisunique, pg_index.indisprimary,
347
                       pg_index.indkey, pg_index.indrelid,
348
                       pg_get_expr(indpred, indrelid) AS where
349
                 FROM pg_class, pg_index
350
                 WHERE oid IN (
351
                    SELECT indexrelid
352 599
                    FROM pg_index si, pg_class sc, pg_namespace sn
353
                    WHERE ' . $this->getTableWhereClause($table, 'sc', 'sn') . ' AND sc.oid=si.indrelid AND sc.relnamespace = sn.oid
354
                 ) AND pg_index.indexrelid = oid';
355
    }
356
357
    /**
358
     * @param string $table
359
     * @param string $classAlias
360
     * @param string $namespaceAlias
361
     *
362
     * @return string
363 1261
     */
364
    private function getTableWhereClause($table, $classAlias = 'c', $namespaceAlias = 'n')
365 1261
    {
366 1261
        $whereClause = $namespaceAlias . ".nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') AND ";
367 440
        if (strpos($table, '.') !== false) {
368 440
            [$schema, $table] = explode('.', $table);
369
            $schema           = $this->quoteStringLiteral($schema);
370 849
        } else {
371
            $schema = "ANY(string_to_array((select replace(replace(setting,'\"\$user\"',user),' ','') from pg_catalog.pg_settings where name = 'search_path'),','))";
372
        }
373 1261
374 1261
        $table = new Identifier($table);
375
        $table = $this->quoteStringLiteral($table->getName());
376 1261
377 1261
        return $whereClause . sprintf(
378 1261
            '%s.relname = %s AND %s.nspname = %s',
379 1261
            $classAlias,
380 1261
            $table,
381 1261
            $namespaceAlias,
382
            $schema
383
        );
384
    }
385
386
    /**
387
     * {@inheritDoc}
388 679
     */
389
    public function getListTableColumnsSQL($table, $database = null)
390
    {
391
        return "SELECT
392
                    a.attnum,
393
                    quote_ident(a.attname) AS field,
394
                    t.typname AS type,
395
                    format_type(a.atttypid, a.atttypmod) AS complete_type,
396
                    (SELECT t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type,
397
                    (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM
398
                      pg_catalog.pg_type t2 WHERE t2.typtype = 'd' AND t2.oid = a.atttypid) AS domain_complete_type,
399
                    a.attnotnull AS isnotnull,
400
                    (SELECT 't'
401
                     FROM pg_index
402
                     WHERE c.oid = pg_index.indrelid
403
                        AND pg_index.indkey[0] = a.attnum
404
                        AND pg_index.indisprimary = 't'
405
                    ) AS pri,
406
                    (SELECT pg_get_expr(adbin, adrelid)
407
                     FROM pg_attrdef
408
                     WHERE c.oid = pg_attrdef.adrelid
409
                        AND pg_attrdef.adnum=a.attnum
410
                    ) AS default,
411
                    (SELECT pg_description.description
412
                        FROM pg_description WHERE pg_description.objoid = c.oid AND a.attnum = pg_description.objsubid
413
                    ) AS comment
414 679
                    FROM pg_attribute a, pg_class c, pg_type t, pg_namespace n
415
                    WHERE " . $this->getTableWhereClause($table, 'c', 'n') . '
416
                        AND a.attnum > 0
417
                        AND a.attrelid = c.oid
418
                        AND a.atttypid = t.oid
419
                        AND n.oid = c.relnamespace
420
                    ORDER BY a.attnum';
421
    }
422
423
    /**
424
     * {@inheritDoc}
425 149
     */
426
    public function getCreateDatabaseSQL($name)
427 149
    {
428
        return 'CREATE DATABASE ' . $name;
429
    }
430
431
    /**
432
     * Returns the SQL statement for disallowing new connections on the given database.
433
     *
434
     * This is useful to force DROP DATABASE operations which could fail because of active connections.
435
     *
436
     * @param string $database The name of the database to disallow new connections for.
437
     *
438
     * @return string
439 142
     */
440
    public function getDisallowDatabaseConnectionsSQL($database)
441 142
    {
442
        return "UPDATE pg_database SET datallowconn = 'false' WHERE datname = " . $this->quoteStringLiteral($database);
443
    }
444
445
    /**
446
     * Returns the SQL statement for closing currently active connections on the given database.
447
     *
448
     * This is useful to force DROP DATABASE operations which could fail because of active connections.
449
     *
450
     * @param string $database The name of the database to close currently active connections for.
451
     *
452
     * @return string
453 108
     */
454
    public function getCloseActiveDatabaseConnectionsSQL($database)
455
    {
456 108
        return 'SELECT pg_terminate_backend(procpid) FROM pg_stat_activity WHERE datname = '
457
            . $this->quoteStringLiteral($database);
458
    }
459
460
    /**
461
     * {@inheritDoc}
462 780
     */
463
    public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey)
464 780
    {
465
        $query = '';
466 780
467 135
        if ($foreignKey->hasOption('match')) {
468
            $query .= ' MATCH ' . $foreignKey->getOption('match');
469
        }
470 780
471
        $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey);
472 780
473 135
        if ($foreignKey->hasOption('deferrable') && $foreignKey->getOption('deferrable') !== false) {
474
            $query .= ' DEFERRABLE';
475 780
        } else {
476
            $query .= ' NOT DEFERRABLE';
477
        }
478 780
479 780
        if (($foreignKey->hasOption('feferred') && $foreignKey->getOption('feferred') !== false)
480
            || ($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false)
481 135
        ) {
482
            $query .= ' INITIALLY DEFERRED';
483 780
        } else {
484
            $query .= ' INITIALLY IMMEDIATE';
485
        }
486 780
487
        return $query;
488
    }
489
490
    /**
491
     * {@inheritDoc}
492 2698
     */
493
    public function getAlterTableSQL(TableDiff $diff)
494 2698
    {
495 2698
        $sql         = [];
496 2698
        $commentsSQL = [];
497
        $columnSql   = [];
498 2698
499 547
        foreach ($diff->addedColumns as $column) {
500
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
501
                continue;
502
            }
503 547
504 547
            $query = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray());
505
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
506 547
507
            $comment = $this->getColumnComment($column);
508 547
509 412
            if ($comment === null || $comment === '') {
510
                continue;
511
            }
512 135
513 135
            $commentsSQL[] = $this->getCommentOnColumnSQL(
514 135
                $diff->getName($this)->getQuotedName($this),
515 135
                $column->getQuotedName($this),
516
                $comment
517
            );
518
        }
519 2698
520 547
        foreach ($diff->removedColumns as $column) {
521
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
522
                continue;
523
            }
524 547
525 547
            $query = 'DROP ' . $column->getQuotedName($this);
526
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
527
        }
528 2698
529
        foreach ($diff->changedColumns as $columnDiff) {
530 1597
            /** @var $columnDiff \Doctrine\DBAL\Schema\ColumnDiff */
531
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
532
                continue;
533
            }
534 1597
535 135
            if ($this->isUnchangedBinaryColumn($columnDiff)) {
536
                continue;
537
            }
538 1462
539 1462
            $oldColumnName = $columnDiff->getOldColumnName()->getQuotedName($this);
540
            $column        = $columnDiff->column;
541 1462
542 696
            if ($columnDiff->hasChanged('type') || $columnDiff->hasChanged('precision') || $columnDiff->hasChanged('scale') || $columnDiff->hasChanged('fixed')) {
543
                $type = $column->getType();
544
545 696
                // SERIAL/BIGSERIAL are not "real" types and we can't alter a column to that type
546 696
                $columnDefinition                  = $column->toArray();
547
                $columnDefinition['autoincrement'] = false;
548
549 696
                // here was a server version check before, but DBAL API does not support this anymore.
550 696
                $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $type->getSQLDeclaration($columnDefinition, $this);
551
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
552
            }
553 1462
554 419
            if ($columnDiff->hasChanged('default') || $this->typeChangeBreaksDefaultValue($columnDiff)) {
555 277
                $defaultClause = $column->getDefault() === null
556 419
                    ? ' DROP DEFAULT'
557 419
                    : ' SET' . $this->getDefaultValueDeclarationSQL($column->toArray());
558 419
                $query         = 'ALTER ' . $oldColumnName . $defaultClause;
559
                $sql[]         = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
560
            }
561 1462
562 270
            if ($columnDiff->hasChanged('notnull')) {
563 270
                $query = 'ALTER ' . $oldColumnName . ' ' . ($column->getNotnull() ? 'SET' : 'DROP') . ' NOT NULL';
564
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
565
            }
566 1462
567 14
            if ($columnDiff->hasChanged('autoincrement')) {
568
                if ($column->getAutoincrement()) {
569 7
                    // add autoincrement
570
                    $seqName = $this->getIdentitySequenceName($diff->name, $oldColumnName);
571 7
572 7
                    $sql[] = 'CREATE SEQUENCE ' . $seqName;
573 7
                    $sql[] = "SELECT setval('" . $seqName . "', (SELECT MAX(" . $oldColumnName . ') FROM ' . $diff->getName($this)->getQuotedName($this) . '))';
574 7
                    $query = 'ALTER ' . $oldColumnName . " SET DEFAULT nextval('" . $seqName . "')";
575
                    $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
576
                } else {
577 7
                    // Drop autoincrement, but do NOT drop the sequence. It might be re-used by other tables or have
578 7
                    $query = 'ALTER ' . $oldColumnName . ' DROP DEFAULT';
579
                    $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
580
                }
581
            }
582 1462
583 1462
            $newComment = $this->getColumnComment($column);
584
            $oldComment = $this->getOldColumnComment($columnDiff);
585 1462
586 475
            if ($columnDiff->hasChanged('comment') || ($columnDiff->fromColumn !== null && $oldComment !== $newComment)) {
587 475
                $commentsSQL[] = $this->getCommentOnColumnSQL(
588 475
                    $diff->getName($this)->getQuotedName($this),
589 475
                    $column->getQuotedName($this),
590
                    $newComment
591
                );
592
            }
593 1462
594 1327
            if (! $columnDiff->hasChanged('length')) {
595
                continue;
596
            }
597 135
598 135
            $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $column->getType()->getSQLDeclaration($column->toArray(), $this);
599
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
600
        }
601 2698
602 547
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
603
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
604
                continue;
605
            }
606 547
607
            $oldColumnName = new Identifier($oldColumnName);
608 547
609 547
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) .
610
                ' RENAME COLUMN ' . $oldColumnName->getQuotedName($this) . ' TO ' . $column->getQuotedName($this);
611
        }
612 2698
613
        $tableSql = [];
614 2698
615 2698
        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
616
            $sql = array_merge($sql, $commentsSQL);
617 2698
618 270
            $newName = $diff->getNewName();
619
620
            if ($newName !== false) {
621 2698
                $sql[] = sprintf(
622 2698
                    'ALTER TABLE %s RENAME TO %s',
623 2698
                    $diff->getName($this)->getQuotedName($this),
624 2698
                    $newName->getQuotedName($this)
625
                );
626
            }
627
628 2698
            $sql = array_merge(
629
                $this->getPreAlterTableIndexForeignKeySQL($diff),
630
                $sql,
631
                $this->getPostAlterTableIndexForeignKeySQL($diff)
632
            );
633
        }
634
635
        return array_merge($sql, $tableSql, $columnSql);
636
    }
637
638
    /**
639
     * Checks whether a given column diff is a logically unchanged binary type column.
640
     *
641
     * Used to determine whether a column alteration for a binary type column can be skipped.
642
     * Doctrine's {@link \Doctrine\DBAL\Types\BinaryType} and {@link \Doctrine\DBAL\Types\BlobType}
643
     * are mapped to the same database column type on this platform as this platform
644
     * does not have a native VARBINARY/BINARY column type. Therefore the {@link \Doctrine\DBAL\Schema\Comparator}
645 1597
     * might detect differences for binary type columns which do not have to be propagated
646
     * to database as there actually is no difference at database level.
647 1597
     *
648
     * @param ColumnDiff $columnDiff The column diff to check against.
649 1597
     *
650 1462
     * @return bool True if the given column diff is an unchanged binary type column, false otherwise.
651
     */
652
    private function isUnchangedBinaryColumn(ColumnDiff $columnDiff)
653 135
    {
654
        $columnType = $columnDiff->column->getType();
655 135
656 135
        if (! $columnType instanceof BinaryType && ! $columnType instanceof BlobType) {
657
            return false;
658 135
        }
659
660
        $fromColumn = $columnDiff->fromColumn instanceof Column ? $columnDiff->fromColumn : null;
661
662 135
        if ($fromColumn) {
663
            $fromColumnType = $fromColumn->getType();
664
665
            if (! $fromColumnType instanceof BinaryType && ! $fromColumnType instanceof BlobType) {
666
                return false;
667
            }
668
669
            return count(array_diff($columnDiff->changedProperties, ['type', 'length', 'fixed'])) === 0;
670
        }
671
672
        if ($columnDiff->hasChanged('type')) {
673
            return false;
674
        }
675 689
676
        return count(array_diff($columnDiff->changedProperties, ['length', 'fixed'])) === 0;
677 689
    }
678 270
679 270
    /**
680
     * {@inheritdoc}
681
     */
682 689
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
683
    {
684
        if (strpos($tableName, '.') !== false) {
685
            [$schema]     = explode('.', $tableName);
686
            $oldIndexName = $schema . '.' . $oldIndexName;
687
        }
688 1430
689
        return ['ALTER INDEX ' . $oldIndexName . ' RENAME TO ' . $index->getQuotedName($this)];
690 1430
    }
691 1430
692 1430
    /**
693
     * {@inheritdoc}
694 1430
     */
695 1430
    public function getCommentOnColumnSQL($tableName, $columnName, $comment)
696 1430
    {
697 1430
        $tableName  = new Identifier($tableName);
698 1430
        $columnName = new Identifier($columnName);
699
        $comment    = $comment === null ? 'NULL' : $this->quoteStringLiteral($comment);
700
701
        return sprintf(
702
            'COMMENT ON COLUMN %s.%s IS %s',
703
            $tableName->getQuotedName($this),
704
            $columnName->getQuotedName($this),
705 305
            $comment
706
        );
707 305
    }
708 305
709 305
    /**
710 305
     * {@inheritDoc}
711 305
     */
712
    public function getCreateSequenceSQL(Sequence $sequence)
713
    {
714
        return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) .
715
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
716
            ' MINVALUE ' . $sequence->getInitialValue() .
717
            ' START ' . $sequence->getInitialValue() .
718
            $this->getSequenceCacheSQL($sequence);
719
    }
720
721
    /**
722
     * {@inheritDoc}
723
     */
724
    public function getAlterSequenceSQL(Sequence $sequence)
725
    {
726
        return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) .
727
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
728
            $this->getSequenceCacheSQL($sequence);
729 305
    }
730
731 305
    /**
732 135
     * Cache definition for sequences
733
     *
734
     * @return string
735 170
     */
736
    private function getSequenceCacheSQL(Sequence $sequence)
737
    {
738
        if ($sequence->getCache() > 1) {
739
            return ' CACHE ' . $sequence->getCache();
740
        }
741 149
742
        return '';
743 149
    }
744
745
    /**
746
     * {@inheritDoc}
747 149
     */
748
    public function getDropSequenceSQL($sequence)
749
    {
750
        if ($sequence instanceof Sequence) {
751
            $sequence = $sequence->getQuotedName($this);
752
        }
753 149
754
        return 'DROP SEQUENCE ' . $sequence . ' CASCADE';
755 149
    }
756
757
    /**
758
     * {@inheritDoc}
759
     */
760
    public function getCreateSchemaSQL($schemaName)
761 412
    {
762
        return 'CREATE SCHEMA ' . $schemaName;
763 412
    }
764
765
    /**
766
     * {@inheritDoc}
767
     */
768
    public function getDropForeignKeySQL($foreignKey, $table)
769 3040
    {
770
        return $this->getDropConstraintSQL($foreignKey, $table);
771 3040
    }
772
773 3040
    /**
774 1261
     * {@inheritDoc}
775 1261
     */
776
    protected function _getCreateTableSQL($tableName, array $columns, array $options = [])
777
    {
778 3040
        $queryFields = $this->getColumnDeclarationListSQL($columns);
779
780 3040
        if (isset($options['primary']) && ! empty($options['primary'])) {
781
            $keyColumns   = array_unique(array_values($options['primary']));
782 3040
            $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')';
783 524
        }
784 524
785
        $query = 'CREATE TABLE ' . $tableName . ' (' . $queryFields . ')';
786
787
        $sql = [$query];
788 3040
789 1209
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
790 219
            foreach ($options['indexes'] as $index) {
791
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
792
            }
793
        }
794 3040
795
        if (isset($options['foreignKeys'])) {
796
            foreach ((array) $options['foreignKeys'] as $definition) {
797
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
798
            }
799
        }
800
801
        return $sql;
802
    }
803
804
    /**
805
     * Converts a single boolean value.
806
     *
807
     * First converts the value to its native PHP boolean type
808
     * and passes it to the given callback function to be reconverted
809
     * into any custom representation.
810
     *
811 4397
     * @param mixed    $value    The value to convert.
812
     * @param callable $callback The callback function to use for converting the real boolean value.
813 4397
     *
814 284
     * @return mixed
815
     *
816
     * @throws UnexpectedValueException
817 4113
     */
818 2621
    private function convertSingleBooleanValue($value, $callback)
819
    {
820
        if ($value === null) {
821 1492
            return $callback(null);
822
        }
823
824
        if (is_bool($value) || is_numeric($value)) {
825
            return $callback($value ? true : false);
826
        }
827
828 1492
        if (! is_string($value)) {
829 682
            return $callback(true);
830
        }
831
832 810
        /**
833 675
         * Better safe than sorry: http://php.net/in_array#106319
834
         */
835
        if (in_array(strtolower(trim($value)), $this->booleanLiterals['false'], true)) {
836 135
            return $callback(false);
837
        }
838
839
        if (in_array(strtolower(trim($value)), $this->booleanLiterals['true'], true)) {
840
            return $callback(true);
841
        }
842
843
        throw new UnexpectedValueException("Unrecognized boolean literal '${value}'");
844
    }
845
846
    /**
847
     * Converts one or multiple boolean values.
848
     *
849
     * First converts the value(s) to their native PHP boolean type
850
     * and passes them to the given callback function to be reconverted
851 4077
     * into any custom representation.
852
     *
853 4397
     * @param mixed    $item     The value(s) to convert.
854
     * @param callable $callback The callback function to use for converting the real boolean value(s).
855
     *
856
     * @return mixed
857
     */
858
    private function doConvertBooleans($item, $callback)
859
    {
860
        if (is_array($item)) {
861 4397
            foreach ($item as $key => $value) {
862
                $item[$key] = $this->convertSingleBooleanValue($value, $callback);
863
            }
864
865
            return $item;
866
        }
867
868
        return $this->convertSingleBooleanValue($item, $callback);
869 2264
    }
870
871 2444
    /**
872 270
     * {@inheritDoc}
873
     *
874
     * Postgres wants boolean values converted to the strings 'true'/'false'.
875 2174
     */
876 2174
    public function convertBooleans($item)
877
    {
878 2174
        if (! $this->useBooleanTrueFalseStrings) {
879 135
            return parent::convertBooleans($item);
880
        }
881
882 2039
        return $this->doConvertBooleans(
883 2174
            $item,
884
            static function ($boolean) {
885
                if ($boolean === null) {
886
                    return 'NULL';
887
                }
888
889
                return $boolean === true ? 'true' : 'false';
890 2188
            }
891
        );
892 2358
    }
893 135
894
    /**
895
     * {@inheritDoc}
896 2223
     */
897 2223
    public function convertBooleansToDatabaseValue($item)
898
    {
899 2088
        if (! $this->useBooleanTrueFalseStrings) {
900 2223
            return parent::convertBooleansToDatabaseValue($item);
901
        }
902
903
        return $this->doConvertBooleans(
904
            $item,
905
            static function ($boolean) {
906
                return $boolean === null ? null : (int) $boolean;
907 1889
            }
908
        );
909 2039
    }
910 810
911
    /**
912
     * {@inheritDoc}
913 1229
     */
914
    public function convertFromBoolean($item)
915
    {
916
        if (in_array(strtolower($item), $this->booleanLiterals['false'], true)) {
917
            return false;
918
        }
919 132
920
        return parent::convertFromBoolean($item);
921 142
    }
922
923
    /**
924
     * {@inheritDoc}
925
     */
926
    public function getSequenceNextValSQL($sequenceName)
927 125
    {
928
        return "SELECT NEXTVAL('" . $sequenceName . "')";
929
    }
930 135
931
    /**
932
     * {@inheritDoc}
933
     */
934
    public function getSetTransactionIsolationSQL($level)
935
    {
936 244
        return 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL '
937
            . $this->_getTransactionIsolationLevelSQL($level);
938 254
    }
939
940
    /**
941
     * {@inheritDoc}
942
     */
943
    public function getBooleanTypeDeclarationSQL(array $field)
944 2425
    {
945
        return 'BOOLEAN';
946 2535
    }
947 871
948
    /**
949
     * {@inheritDoc}
950 1939
     */
951
    public function getIntegerTypeDeclarationSQL(array $field)
952
    {
953
        if (! empty($field['autoincrement'])) {
954
            return 'SERIAL';
955
        }
956 390
957
        return 'INT';
958 410
    }
959 291
960
    /**
961
     * {@inheritDoc}
962 119
     */
963
    public function getBigIntTypeDeclarationSQL(array $field)
964
    {
965
        if (! empty($field['autoincrement'])) {
966
            return 'BIGSERIAL';
967
        }
968 89
969
        return 'BIGINT';
970 95
    }
971
972
    /**
973
     * {@inheritDoc}
974
     */
975
    public function getSmallIntTypeDeclarationSQL(array $field)
976 125
    {
977
        return 'SMALLINT';
978 135
    }
979
980
    /**
981
     * {@inheritDoc}
982
     */
983
    public function getGuidTypeDeclarationSQL(array $field)
984 279
    {
985
        return 'UUID';
986 289
    }
987
988
    /**
989
     * {@inheritDoc}
990
     */
991
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
992 105
    {
993
        return 'TIMESTAMP(0) WITHOUT TIME ZONE';
994 105
    }
995
996
    /**
997
     * {@inheritDoc}
998
     */
999
    public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration)
1000 126
    {
1001
        return 'TIMESTAMP(0) WITH TIME ZONE';
1002 126
    }
1003
1004
    /**
1005
     * {@inheritDoc}
1006
     */
1007
    public function getDateTypeDeclarationSQL(array $fieldDeclaration)
1008 126
    {
1009
        return 'DATE';
1010 126
    }
1011
1012
    /**
1013
     * {@inheritDoc}
1014
     */
1015
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration)
1016
    {
1017
        return 'TIME(0) WITHOUT TIME ZONE';
1018
    }
1019
1020
    /**
1021
     * {@inheritDoc}
1022
     *
1023
     * @deprecated Use application-generated UUIDs instead
1024
     */
1025
    public function getGuidExpression()
1026
    {
1027
        return 'UUID_GENERATE_V4()';
1028
    }
1029
1030
    /**
1031
     * {@inheritDoc}
1032
     */
1033
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
1034 2078
    {
1035
        return '';
1036 2200
    }
1037 2200
1038
    /**
1039
     * {@inheritDoc}
1040
     */
1041
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1042
    {
1043 132
        return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)')
1044
            : ($length ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)');
1045 142
    }
1046
1047
    /**
1048
     * {@inheritdoc}
1049
     */
1050
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
1051 490
    {
1052
        return 'BYTEA';
1053 504
    }
1054
1055
    /**
1056
     * {@inheritDoc}
1057
     */
1058
    public function getClobTypeDeclarationSQL(array $field)
1059 974
    {
1060
        return 'TEXT';
1061 994
    }
1062
1063
    /**
1064
     * {@inheritDoc}
1065
     */
1066
    public function getObjectTypeDeclarationSQL(array $field)
1067
    {
1068
        return 'BYTEA';
1069
    }
1070
1071
    /**
1072
     * {@inheritDoc}
1073
     */
1074
    public function getName()
1075
    {
1076
        return 'postgresql';
1077 7
    }
1078
1079 7
    /**
1080
     * {@inheritDoc}
1081
     *
1082
     * PostgreSQL returns all column names in SQL result sets in lowercase.
1083
     */
1084
    public function getSQLResultCasing($column)
1085 7
    {
1086
        return strtolower($column);
1087 7
    }
1088
1089
    /**
1090
     * {@inheritDoc}
1091
     */
1092
    public function getDateTimeTzFormatString()
1093 188
    {
1094
        return 'Y-m-d H:i:sO';
1095 198
    }
1096 198
1097
    /**
1098 198
     * {@inheritDoc}
1099
     */
1100
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
1101
    {
1102 198
        return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (DEFAULT)';
1103
    }
1104
1105
    /**
1106
     * {@inheritDoc}
1107
     */
1108
    public function getTruncateTableSQL($tableName, $cascade = false)
1109
    {
1110
        $tableIdentifier = new Identifier($tableName);
1111
        $sql             = 'TRUNCATE ' . $tableIdentifier->getQuotedName($this);
1112
1113
        if ($cascade) {
1114
            $sql .= ' CASCADE';
1115
        }
1116 707
1117
        return $sql;
1118 763
    }
1119
1120
    /**
1121
     * {@inheritDoc}
1122
     */
1123
    public function getReadLockSQL()
1124
    {
1125
        return 'FOR SHARE';
1126
    }
1127
1128
    /**
1129
     * {@inheritDoc}
1130
     */
1131
    protected function initializeDoctrineTypeMappings()
1132
    {
1133
        $this->doctrineTypeMapping = [
1134
            'smallint'      => 'smallint',
1135
            'int2'          => 'smallint',
1136
            'serial'        => 'integer',
1137
            'serial4'       => 'integer',
1138
            'int'           => 'integer',
1139
            'int4'          => 'integer',
1140
            'integer'       => 'integer',
1141
            'bigserial'     => 'bigint',
1142
            'serial8'       => 'bigint',
1143
            'bigint'        => 'bigint',
1144
            'int8'          => 'bigint',
1145
            'bool'          => 'boolean',
1146
            'boolean'       => 'boolean',
1147
            'text'          => 'text',
1148
            'tsvector'      => 'text',
1149
            'varchar'       => 'string',
1150
            'interval'      => 'string',
1151
            '_varchar'      => 'string',
1152
            'char'          => 'string',
1153
            'bpchar'        => 'string',
1154
            'inet'          => 'string',
1155
            'date'          => 'date',
1156
            'datetime'      => 'datetime',
1157
            'timestamp'     => 'datetime',
1158
            'timestamptz'   => 'datetimetz',
1159 763
            'time'          => 'time',
1160
            'timetz'        => 'time',
1161
            'float'         => 'float',
1162
            'float4'        => 'float',
1163
            'float8'        => 'float',
1164 2078
            'double'        => 'float',
1165
            'double precision' => 'float',
1166 2200
            'real'          => 'float',
1167
            'decimal'       => 'decimal',
1168
            'money'         => 'decimal',
1169
            'numeric'       => 'decimal',
1170
            'year'          => 'date',
1171
            'uuid'          => 'guid',
1172 264
            'bytea'         => 'blob',
1173
        ];
1174 284
    }
1175
1176
    /**
1177
     * {@inheritDoc}
1178
     */
1179
    public function getVarcharMaxLength()
1180 257
    {
1181
        return 65535;
1182 277
    }
1183
1184
    /**
1185
     * {@inheritdoc}
1186
     */
1187
    public function getBinaryMaxLength()
1188 1300
    {
1189
        return 0;
1190 1404
    }
1191
1192
    /**
1193
     * {@inheritdoc}
1194
     */
1195
    public function getBinaryDefaultLength()
1196 188
    {
1197
        return 0;
1198 198
    }
1199
1200
    /**
1201
     * {@inheritDoc}
1202
     */
1203
    protected function getReservedKeywordsClass()
1204 4148
    {
1205
        return Keywords\PostgreSQLKeywords::class;
1206 4390
    }
1207 1297
1208
    /**
1209
     * {@inheritDoc}
1210 3375
     */
1211
    public function getBlobTypeDeclarationSQL(array $field)
1212
    {
1213
        return 'BYTEA';
1214
    }
1215
1216 4148
    /**
1217
     * {@inheritdoc}
1218 4390
     */
1219
    public function getDefaultValueDeclarationSQL($field)
1220
    {
1221
        if ($this->isSerialField($field)) {
1222
            return '';
1223
        }
1224
1225 1230
        return parent::getDefaultValueDeclarationSQL($field);
1226
    }
1227 1320
1228 675
    /**
1229
     * @param mixed[] $field
1230
     */
1231 645
    private function isSerialField(array $field) : bool
1232 645
    {
1233
        return isset($field['type'], $field['autoincrement'])
1234
            && $field['autoincrement'] === true
1235 645
            && $this->isNumericType($field['type']);
1236 645
    }
1237
1238
    /**
1239 605
     * Check whether the type of a column is changed in a way that invalidates the default value for the column
1240
     */
1241 645
    private function typeChangeBreaksDefaultValue(ColumnDiff $columnDiff) : bool
1242
    {
1243
        if (! $columnDiff->fromColumn) {
1244 1362
            return $columnDiff->hasChanged('type');
1245
        }
1246 1462
1247
        $oldTypeIsNumeric = $this->isNumericType($columnDiff->fromColumn->getType());
1248
        $newTypeIsNumeric = $this->isNumericType($columnDiff->column->getType());
1249
1250
        // default should not be changed when switching between numeric types and the default comes from a sequence
1251
        return $columnDiff->hasChanged('type')
1252
            && ! ($oldTypeIsNumeric && $newTypeIsNumeric && $columnDiff->column->getAutoincrement());
1253
    }
1254
1255
    private function isNumericType(Type $type) : bool
1256
    {
1257
        return $type instanceof IntegerType || $type instanceof BigIntType;
1258
    }
1259
1260
    private function getOldColumnComment(ColumnDiff $columnDiff) : ?string
1261
    {
1262
        return $columnDiff->fromColumn ? $this->getColumnComment($columnDiff->fromColumn) : null;
1263
    }
1264
1265
    /**
1266
     * {@inheritDoc}
1267
     */
1268
    public function escapeStringForObject($data)
1269
    {
1270
        return pg_escape_bytea($data);
0 ignored issues
show
Bug introduced by
The call to pg_escape_bytea() has too few arguments starting with data. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1270
        return /** @scrutinizer ignore-call */ pg_escape_bytea($data);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1271
    }
1272
}
1273