Completed
Push — develop ( e7b6c1...8dba78 )
by Marco
22s queued 13s
created

PostgreSqlPlatform::getSubstringExpression()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 3
crap 2
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 sprintf;
31
use function strpos;
32
use function strtolower;
33
use function trim;
34
35
/**
36
 * Provides the behavior, features and SQL dialect of the PostgreSQL 9.4+ database platform.
37
 *
38
 * @todo   Rename: PostgreSQLPlatform
39
 */
40
class PostgreSqlPlatform extends AbstractPlatform
41
{
42
    /** @var bool */
43
    private $useBooleanTrueFalseStrings = true;
44
45
    /** @var string[][] PostgreSQL booleans literals */
46
    private $booleanLiterals = [
47
        'true' => [
48
            't',
49
            'true',
50
            'y',
51
            'yes',
52
            'on',
53
            '1',
54
        ],
55
        'false' => [
56
            'f',
57
            'false',
58
            'n',
59
            'no',
60
            'off',
61
            '0',
62
        ],
63
    ];
64
65
    /**
66
     * PostgreSQL has different behavior with some drivers
67
     * with regard to how booleans have to be handled.
68
     *
69
     * Enables use of 'true'/'false' or otherwise 1 and 0 instead.
70
     *
71
     * @param bool $flag
72
     */
73 9414
    public function setUseBooleanTrueFalseStrings($flag)
74
    {
75 9414
        $this->useBooleanTrueFalseStrings = (bool) $flag;
76 9414
    }
77
78
    /**
79
     * {@inheritDoc}
80
     */
81 11229
    public function getNowExpression()
82
    {
83 11229
        return 'LOCALTIMESTAMP(0)';
84 11229
    }
85
86
    /**
87 10035
     * {@inheritDoc}
88
     */
89
    public function getRegexpExpression()
90
    {
91
        return 'SIMILAR TO';
92
    }
93
94
    /**
95
     * {@inheritDoc}
96
     */
97
    public function getLocateExpression(string $string, string $substring, ?string $start = null) : string
98
    {
99
        if ($start !== null) {
100
            $string = $this->getSubstringExpression($string, $start);
101 10035
102
            return 'CASE WHEN (POSITION(' . $substring . ' IN ' . $string . ') = 0) THEN 0 ELSE (POSITION(' . $substring . ' IN ' . $string . ') + ' . $start . ' - 1) END';
103 10035
        }
104
105
        return sprintf('POSITION(%s IN %s)', $substring, $string);
106
    }
107
108
    /**
109 3702
     * {@inheritdoc}
110
     */
111 3702
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
112 3702
    {
113
        if ($unit === DateIntervalUnit::QUARTER) {
114 3702
            $interval *= 3;
115
            $unit      = DateIntervalUnit::MONTH;
116
        }
117 3702
118
        return '(' . $date . ' ' . $operator . ' (' . $interval . " || ' " . $unit . "')::interval)";
119
    }
120
121
    /**
122
     * {@inheritDoc}
123 3708
     */
124
    public function getDateDiffExpression($date1, $date2)
125 3708
    {
126 3708
        return '(DATE(' . $date1 . ')-DATE(' . $date2 . '))';
127 3708
    }
128
129
    /**
130 3708
     * {@inheritDoc}
131
     */
132
    public function supportsSequences()
133
    {
134
        return true;
135
    }
136 3528
137
    /**
138 3528
     * {@inheritDoc}
139
     */
140
    public function supportsSchemas()
141
    {
142
        return true;
143
    }
144 10529
145
    /**
146 10529
     * {@inheritdoc}
147
     */
148
    public function getDefaultSchemaName()
149
    {
150
        return 'public';
151
    }
152 3236
153
    /**
154 3236
     * {@inheritDoc}
155
     */
156
    public function supportsIdentityColumns()
157
    {
158
        return true;
159
    }
160 3086
161
    /**
162 3086
     * {@inheritdoc}
163
     */
164
    public function supportsPartialIndexes()
165
    {
166
        return true;
167
    }
168 10445
169
    /**
170 10445
     * {@inheritdoc}
171
     */
172
    public function usesSequenceEmulatedIdentityColumns()
173
    {
174
        return true;
175
    }
176 11340
177
    /**
178 11340
     * {@inheritdoc}
179
     */
180
    public function getIdentitySequenceName($tableName, $columnName)
181
    {
182
        return $tableName . '_' . $columnName . '_seq';
183
    }
184 9141
185
    /**
186 9141
     * {@inheritDoc}
187
     */
188
    public function supportsCommentOnStatement()
189
    {
190
        return true;
191
    }
192 9939
193
    /**
194 9939
     * {@inheritDoc}
195
     */
196
    public function prefersSequences()
197
    {
198
        return true;
199
    }
200 11277
201
    /**
202 11277
     * {@inheritDoc}
203
     */
204
    public function hasNativeGuidType()
205
    {
206
        return true;
207
    }
208 9723
209
    /**
210 9723
     * {@inheritDoc}
211
     */
212
    public function getListDatabasesSQL()
213
    {
214
        return 'SELECT datname FROM pg_database';
215
    }
216 11382
217
    /**
218 11382
     * {@inheritDoc}
219
     */
220
    public function getListNamespacesSQL()
221
    {
222
        return "SELECT schema_name AS nspname
223
                FROM   information_schema.schemata
224 3260
                WHERE  schema_name NOT LIKE 'pg\_%'
225
                AND    schema_name != 'information_schema'";
226 3260
    }
227
228
    /**
229
     * {@inheritDoc}
230
     */
231
    public function getListSequencesSQL($database)
232 3236
    {
233
        return "SELECT sequence_name AS relname,
234 3236
                       sequence_schema AS schemaname
235
                FROM   information_schema.sequences
236
                WHERE  sequence_schema NOT LIKE 'pg\_%'
237
                AND    sequence_schema != 'information_schema'";
238
    }
239
240
    /**
241
     * {@inheritDoc}
242
     */
243 2169
    public function getListTablesSQL()
244
    {
245 2169
        return "SELECT quote_ident(table_name) AS table_name,
246
                       table_schema AS schema_name
247
                FROM   information_schema.tables
248
                WHERE  table_schema NOT LIKE 'pg\_%'
249
                AND    table_schema != 'information_schema'
250
                AND    table_name != 'geometry_columns'
251
                AND    table_name != 'spatial_ref_sys'
252
                AND    table_type != 'VIEW'";
253
    }
254
255 3576
    /**
256
     * {@inheritDoc}
257 3576
     */
258
    public function getListViewsSQL($database)
259
    {
260
        return 'SELECT quote_ident(table_name) AS viewname,
261
                       table_schema AS schemaname,
262
                       view_definition AS definition
263
                FROM   information_schema.views
264
                WHERE  view_definition IS NOT NULL';
265
    }
266
267
    /**
268
     * {@inheritDoc}
269
     */
270 3158
    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

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