Completed
Push — develop ( a59880...a5109c )
by Sergei
112:22 queued 47:20
created

PostgreSqlPlatform::getGuidExpression()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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