Failed Conditions
Push — master ( 24dbc4...1eba78 )
by Sergei
31:31 queued 31:22
created

usesSequenceEmulatedIdentityColumns()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\DBAL\Platforms;
6
7
use Doctrine\DBAL\Schema\ColumnDiff;
8
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
9
use Doctrine\DBAL\Schema\Identifier;
10
use Doctrine\DBAL\Schema\Index;
11
use Doctrine\DBAL\Schema\Sequence;
12
use Doctrine\DBAL\Schema\TableDiff;
13
use Doctrine\DBAL\Types\BigIntType;
14
use Doctrine\DBAL\Types\BinaryType;
15
use Doctrine\DBAL\Types\BlobType;
16
use Doctrine\DBAL\Types\IntegerType;
17
use Doctrine\DBAL\Types\Type;
18
use UnexpectedValueException;
19
use function array_diff;
20
use function array_merge;
21
use function array_unique;
22
use function array_values;
23
use function count;
24
use function explode;
25
use function implode;
26
use function in_array;
27
use function is_array;
28
use function is_bool;
29
use function is_numeric;
30
use function is_string;
31
use function sprintf;
32
use function strpos;
33
use function strtolower;
34
use function trim;
35
36
/**
37
 * Provides the behavior, features and SQL dialect of the PostgreSQL 9.4+ database platform.
38
 */
39
class PostgreSQL94Platform extends AbstractPlatform
40
{
41
    /** @var bool */
42
    private $useBooleanTrueFalseStrings = true;
43
44
    /** @var string[][] PostgreSQL booleans literals */
45
    private $booleanLiterals = [
46
        'true' => [
47
            't',
48
            'true',
49
            'y',
50
            'yes',
51
            'on',
52
            '1',
53
        ],
54
        'false' => [
55
            'f',
56
            'false',
57
            'n',
58
            'no',
59
            'off',
60
            '0',
61
        ],
62
    ];
63
64
    /**
65
     * PostgreSQL has different behavior with some drivers
66
     * with regard to how booleans have to be handled.
67
     *
68
     * Enables use of 'true'/'false' or otherwise 1 and 0 instead.
69
     */
70
    public function setUseBooleanTrueFalseStrings(bool $flag) : void
71
    {
72
        $this->useBooleanTrueFalseStrings = $flag;
73
    }
74
75
    public function getNowExpression() : string
76
    {
77
        return 'LOCALTIMESTAMP(0)';
78
    }
79
80
    public function getRegexpExpression() : string
81
    {
82
        return 'SIMILAR TO';
83
    }
84
85
    public function getLocateExpression(string $string, string $substring, ?string $start = null) : string
86
    {
87
        if ($start !== null) {
88
            $string = $this->getSubstringExpression($string, $start);
89
90
            return 'CASE WHEN (POSITION(' . $substring . ' IN ' . $string . ') = 0) THEN 0 ELSE (POSITION(' . $substring . ' IN ' . $string . ') + ' . $start . ' - 1) END';
91
        }
92
93
        return sprintf('POSITION(%s IN %s)', $substring, $string);
94
    }
95
96
    protected function getDateArithmeticIntervalExpression(string $date, string $operator, string $interval, string $unit) : string
97
    {
98
        if ($unit === DateIntervalUnit::QUARTER) {
99
            $interval = $this->multiplyInterval($interval, 3);
100
            $unit     = DateIntervalUnit::MONTH;
101
        }
102
103
        return '(' . $date . ' ' . $operator . ' (' . $interval . " || ' " . $unit . "')::interval)";
104
    }
105
106
    public function getDateDiffExpression(string $date1, string $date2) : string
107
    {
108
        return '(DATE(' . $date1 . ')-DATE(' . $date2 . '))';
109
    }
110
111
    public function getCurrentDatabaseExpression() : string
112
    {
113
        return 'CURRENT_DATABASE()';
114
    }
115
116
    public function supportsSequences() : bool
117
    {
118
        return true;
119
    }
120
121
    public function supportsSchemas() : bool
122
    {
123
        return true;
124
    }
125
126
    public function getDefaultSchemaName() : string
127
    {
128
        return 'public';
129
    }
130
131
    public function supportsIdentityColumns() : bool
132
    {
133
        return true;
134
    }
135
136
    public function supportsPartialIndexes() : bool
137
    {
138
        return true;
139
    }
140
141
    public function usesSequenceEmulatedIdentityColumns() : bool
142
    {
143
        return true;
144
    }
145
146
    public function getIdentitySequenceName(string $tableName, string $columnName) : string
147
    {
148
        return $tableName . '_' . $columnName . '_seq';
149
    }
150
151
    public function supportsCommentOnStatement() : bool
152
    {
153
        return true;
154
    }
155
156
    public function prefersSequences() : bool
157
    {
158
        return true;
159
    }
160
161
    public function hasNativeGuidType() : bool
162
    {
163
        return true;
164
    }
165
166
    public function getListDatabasesSQL() : string
167
    {
168
        return 'SELECT datname FROM pg_database';
169
    }
170
171
    public function getListNamespacesSQL() : string
172
    {
173
        return "SELECT schema_name AS nspname
174
                FROM   information_schema.schemata
175
                WHERE  schema_name NOT LIKE 'pg\_%'
176
                AND    schema_name != 'information_schema'";
177
    }
178
179
    public function getListSequencesSQL(string $database) : string
180
    {
181
        return "SELECT sequence_name AS relname,
182
                       sequence_schema AS schemaname
183
                FROM   information_schema.sequences
184
                WHERE  sequence_schema NOT LIKE 'pg\_%'
185
                AND    sequence_schema != 'information_schema'";
186
    }
187
188
    public function getListTablesSQL() : string
189
    {
190
        return "SELECT quote_ident(table_name) AS table_name,
191
                       table_schema AS schema_name
192
                FROM   information_schema.tables
193
                WHERE  table_schema NOT LIKE 'pg\_%'
194
                AND    table_schema != 'information_schema'
195
                AND    table_name != 'geometry_columns'
196
                AND    table_name != 'spatial_ref_sys'
197
                AND    table_type != 'VIEW'";
198
    }
199
200
    public function getListViewsSQL(string $database) : string
201
    {
202
        return 'SELECT quote_ident(table_name) AS viewname,
203
                       table_schema AS schemaname,
204
                       view_definition AS definition
205
                FROM   information_schema.views
206
                WHERE  view_definition IS NOT NULL';
207
    }
208
209
    public function getListTableForeignKeysSQL(string $table, ?string $database = null) : string
210
    {
211
        return 'SELECT quote_ident(r.conname) as conname, pg_catalog.pg_get_constraintdef(r.oid, true) as condef
212
                  FROM pg_catalog.pg_constraint r
213
                  WHERE r.conrelid =
214
                  (
215
                      SELECT c.oid
216
                      FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n
217
                      WHERE ' . $this->getTableWhereClause($table) . " AND n.oid = c.relnamespace
218
                  )
219
                  AND r.contype = 'f'";
220
    }
221
222
    public function getCreateViewSQL(string $name, string $sql) : string
223
    {
224
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
225
    }
226
227
    public function getDropViewSQL(string $name) : string
228
    {
229
        return 'DROP VIEW ' . $name;
230
    }
231
232
    public function getListTableConstraintsSQL(string $table) : string
233
    {
234
        $table = new Identifier($table);
235
        $table = $this->quoteStringLiteral($table->getName());
236
237
        return sprintf(
238
            <<<'SQL'
239
SELECT
240
    quote_ident(relname) as relname
241
FROM
242
    pg_class
243
WHERE oid IN (
244
    SELECT indexrelid
245
    FROM pg_index, pg_class
246
    WHERE pg_class.relname = %s
247
        AND pg_class.oid = pg_index.indrelid
248
        AND (indisunique = 't' OR indisprimary = 't')
249
    )
250
SQL
251
            ,
252
            $table
253
        );
254
    }
255
256
    /**
257
     * {@inheritDoc}
258
     *
259
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
260
     */
261
    public function getListTableIndexesSQL(string $table, ?string $currentDatabase = null) : string
262
    {
263
        return 'SELECT quote_ident(relname) as relname, pg_index.indisunique, pg_index.indisprimary,
264
                       pg_index.indkey, pg_index.indrelid,
265
                       pg_get_expr(indpred, indrelid) AS where
266
                 FROM pg_class, pg_index
267
                 WHERE oid IN (
268
                    SELECT indexrelid
269
                    FROM pg_index si, pg_class sc, pg_namespace sn
270
                    WHERE ' . $this->getTableWhereClause($table, 'sc', 'sn') . ' AND sc.oid=si.indrelid AND sc.relnamespace = sn.oid
271
                 ) AND pg_index.indexrelid = oid';
272
    }
273
274
    private function getTableWhereClause(string $table, string $classAlias = 'c', string $namespaceAlias = 'n') : string
275
    {
276
        $whereClause = $namespaceAlias . ".nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') AND ";
277
        if (strpos($table, '.') !== false) {
278
            [$schema, $table] = explode('.', $table);
279
            $schema           = $this->quoteStringLiteral($schema);
280
        } else {
281
            $schema = 'ANY(current_schemas(false))';
282
        }
283
284
        $table = new Identifier($table);
285
        $table = $this->quoteStringLiteral($table->getName());
286
287
        return $whereClause . sprintf(
288
            '%s.relname = %s AND %s.nspname = %s',
289
            $classAlias,
290
            $table,
291
            $namespaceAlias,
292
            $schema
293
        );
294
    }
295
296
    public function getListTableColumnsSQL(string $table, ?string $database = null) : string
297
    {
298
        return "SELECT
299
                    a.attnum,
300
                    quote_ident(a.attname) AS field,
301
                    t.typname AS type,
302
                    format_type(a.atttypid, a.atttypmod) AS complete_type,
303
                    (SELECT tc.collcollate FROM pg_catalog.pg_collation tc WHERE tc.oid = a.attcollation) AS collation,
304
                    (SELECT t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type,
305
                    (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM
306
                      pg_catalog.pg_type t2 WHERE t2.typtype = 'd' AND t2.oid = a.atttypid) AS domain_complete_type,
307
                    a.attnotnull AS isnotnull,
308
                    (SELECT 't'
309
                     FROM pg_index
310
                     WHERE c.oid = pg_index.indrelid
311
                        AND pg_index.indkey[0] = a.attnum
312
                        AND pg_index.indisprimary = 't'
313
                    ) AS pri,
314
                    (SELECT pg_get_expr(adbin, adrelid)
315
                     FROM pg_attrdef
316
                     WHERE c.oid = pg_attrdef.adrelid
317
                        AND pg_attrdef.adnum=a.attnum
318
                    ) AS default,
319
                    (SELECT pg_description.description
320
                        FROM pg_description WHERE pg_description.objoid = c.oid AND a.attnum = pg_description.objsubid
321
                    ) AS comment
322
                    FROM pg_attribute a, pg_class c, pg_type t, pg_namespace n
323
                    WHERE " . $this->getTableWhereClause($table, 'c', 'n') . '
324
                        AND a.attnum > 0
325
                        AND a.attrelid = c.oid
326
                        AND a.atttypid = t.oid
327
                        AND n.oid = c.relnamespace
328
                    ORDER BY a.attnum';
329
    }
330
331
    public function getCreateDatabaseSQL(string $database) : string
332
    {
333
        return 'CREATE DATABASE ' . $database;
334
    }
335
336
    /**
337
     * Returns the SQL statement for disallowing new connections on the given database.
338
     *
339
     * This is useful to force DROP DATABASE operations which could fail because of active connections.
340
     *
341
     * @param string $database The name of the database to disallow new connections for.
342
     */
343
    public function getDisallowDatabaseConnectionsSQL(string $database) : string
344
    {
345
        return "UPDATE pg_database SET datallowconn = 'false' WHERE datname = " . $this->quoteStringLiteral($database);
346
    }
347
348
    /**
349
     * Returns the SQL statement for closing currently active connections on the given database.
350
     *
351
     * This is useful to force DROP DATABASE operations which could fail because of active connections.
352
     *
353
     * @param string $database The name of the database to close currently active connections for.
354
     */
355
    public function getCloseActiveDatabaseConnectionsSQL(string $database) : string
356
    {
357
        return 'SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '
358
            . $this->quoteStringLiteral($database);
359
    }
360
361
    public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey) : string
362
    {
363
        $query = '';
364
365
        if ($foreignKey->hasOption('match')) {
366
            $query .= ' MATCH ' . $foreignKey->getOption('match');
367
        }
368
369
        $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey);
370
371
        if ($foreignKey->hasOption('deferrable') && $foreignKey->getOption('deferrable') !== false) {
372
            $query .= ' DEFERRABLE';
373
        } else {
374
            $query .= ' NOT DEFERRABLE';
375
        }
376
377
        if ($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false) {
378
            $query .= ' INITIALLY DEFERRED';
379
        } else {
380
            $query .= ' INITIALLY IMMEDIATE';
381
        }
382
383
        return $query;
384
    }
385
386
    /**
387
     * {@inheritDoc}
388
     */
389
    public function getAlterTableSQL(TableDiff $diff) : array
390
    {
391
        $sql         = [];
392
        $commentsSQL = [];
393
        $columnSql   = [];
394
395
        foreach ($diff->addedColumns as $column) {
396
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
397
                continue;
398
            }
399
400
            $query = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray());
401
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
402
403
            $comment = $this->getColumnComment($column);
404
405
            if ($comment === '') {
406
                continue;
407
            }
408
409
            $commentsSQL[] = $this->getCommentOnColumnSQL(
410
                $diff->getName($this)->getQuotedName($this),
411
                $column->getQuotedName($this),
412
                $comment
413
            );
414
        }
415
416
        foreach ($diff->removedColumns as $column) {
417
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
418
                continue;
419
            }
420
421
            $query = 'DROP ' . $column->getQuotedName($this);
422
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
423
        }
424
425
        foreach ($diff->changedColumns as $columnDiff) {
426
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
427
                continue;
428
            }
429
430
            if ($this->isUnchangedBinaryColumn($columnDiff)) {
431
                continue;
432
            }
433
434
            $oldColumnName = $columnDiff->getOldColumnName()->getQuotedName($this);
435
            $column        = $columnDiff->column;
436
437
            if ($columnDiff->hasChanged('type') || $columnDiff->hasChanged('precision') || $columnDiff->hasChanged('scale') || $columnDiff->hasChanged('fixed')) {
438
                $type = $column->getType();
439
440
                // SERIAL/BIGSERIAL are not "real" types and we can't alter a column to that type
441
                $columnDefinition                  = $column->toArray();
442
                $columnDefinition['autoincrement'] = false;
443
444
                // here was a server version check before, but DBAL API does not support this anymore.
445
                $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $type->getSQLDeclaration($columnDefinition, $this);
446
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
447
            }
448
449
            if ($columnDiff->hasChanged('default') || $this->typeChangeBreaksDefaultValue($columnDiff)) {
450
                $defaultClause = $column->getDefault() === null
451
                    ? ' DROP DEFAULT'
452
                    : ' SET' . $this->getDefaultValueDeclarationSQL($column->toArray());
453
                $query         = 'ALTER ' . $oldColumnName . $defaultClause;
454
                $sql[]         = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
455
            }
456
457
            if ($columnDiff->hasChanged('notnull')) {
458
                $query = 'ALTER ' . $oldColumnName . ' ' . ($column->getNotnull() ? 'SET' : 'DROP') . ' NOT NULL';
459
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
460
            }
461
462
            if ($columnDiff->hasChanged('autoincrement')) {
463
                if ($column->getAutoincrement()) {
464
                    // add autoincrement
465
                    $seqName = $this->getIdentitySequenceName($diff->name, $oldColumnName);
466
467
                    $sql[] = 'CREATE SEQUENCE ' . $seqName;
468
                    $sql[] = "SELECT setval('" . $seqName . "', (SELECT MAX(" . $oldColumnName . ') FROM ' . $diff->getName($this)->getQuotedName($this) . '))';
469
                    $query = 'ALTER ' . $oldColumnName . " SET DEFAULT nextval('" . $seqName . "')";
470
                    $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
471
                } else {
472
                    // Drop autoincrement, but do NOT drop the sequence. It might be re-used by other tables or have
473
                    $query = 'ALTER ' . $oldColumnName . ' DROP DEFAULT';
474
                    $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
475
                }
476
            }
477
478
            $newComment = $this->getColumnComment($column);
479
            $oldComment = $this->getOldColumnComment($columnDiff);
480
481
            if ($columnDiff->hasChanged('comment') || ($columnDiff->fromColumn !== null && $oldComment !== $newComment)) {
482
                $commentsSQL[] = $this->getCommentOnColumnSQL(
483
                    $diff->getName($this)->getQuotedName($this),
484
                    $column->getQuotedName($this),
485
                    $newComment
486
                );
487
            }
488
489
            if (! $columnDiff->hasChanged('length')) {
490
                continue;
491
            }
492
493
            $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $column->getType()->getSQLDeclaration($column->toArray(), $this);
494
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
495
        }
496
497
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
498
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
499
                continue;
500
            }
501
502
            $oldColumnName = new Identifier($oldColumnName);
503
504
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) .
505
                ' RENAME COLUMN ' . $oldColumnName->getQuotedName($this) . ' TO ' . $column->getQuotedName($this);
506
        }
507
508
        $tableSql = [];
509
510
        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
511
            $sql = array_merge($sql, $commentsSQL);
512
513
            $newName = $diff->getNewName();
514
515
            if ($newName !== null) {
516
                $sql[] = sprintf(
517
                    'ALTER TABLE %s RENAME TO %s',
518
                    $diff->getName($this)->getQuotedName($this),
519
                    $newName->getQuotedName($this)
520
                );
521
            }
522
523
            $sql = array_merge(
524
                $this->getPreAlterTableIndexForeignKeySQL($diff),
525
                $sql,
526
                $this->getPostAlterTableIndexForeignKeySQL($diff)
527
            );
528
        }
529
530
        return array_merge($sql, $tableSql, $columnSql);
531
    }
532
533
    /**
534
     * Checks whether a given column diff is a logically unchanged binary type column.
535
     *
536
     * Used to determine whether a column alteration for a binary type column can be skipped.
537
     * Doctrine's {@link \Doctrine\DBAL\Types\BinaryType} and {@link \Doctrine\DBAL\Types\BlobType}
538
     * are mapped to the same database column type on this platform as this platform
539
     * does not have a native VARBINARY/BINARY column type. Therefore the {@link \Doctrine\DBAL\Schema\Comparator}
540
     * might detect differences for binary type columns which do not have to be propagated
541
     * to database as there actually is no difference at database level.
542
     *
543
     * @param ColumnDiff $columnDiff The column diff to check against.
544
     *
545
     * @return bool True if the given column diff is an unchanged binary type column, false otherwise.
546
     */
547
    private function isUnchangedBinaryColumn(ColumnDiff $columnDiff) : bool
548
    {
549
        $columnType = $columnDiff->column->getType();
550
551
        if (! $columnType instanceof BinaryType && ! $columnType instanceof BlobType) {
552
            return false;
553
        }
554
555
        $fromColumn = $columnDiff->fromColumn;
556
557
        if ($fromColumn !== null) {
558
            $fromColumnType = $fromColumn->getType();
559
560
            if (! $fromColumnType instanceof BinaryType && ! $fromColumnType instanceof BlobType) {
561
                return false;
562
            }
563
564
            return count(array_diff($columnDiff->changedProperties, ['type', 'length', 'fixed'])) === 0;
565
        }
566
567
        if ($columnDiff->hasChanged('type')) {
568
            return false;
569
        }
570
571
        return count(array_diff($columnDiff->changedProperties, ['length', 'fixed'])) === 0;
572
    }
573
574
    /**
575
     * {@inheritdoc}
576
     */
577
    protected function getRenameIndexSQL(string $oldIndexName, Index $index, string $tableName) : array
578
    {
579
        if (strpos($tableName, '.') !== false) {
580
            [$schema]     = explode('.', $tableName);
581
            $oldIndexName = $schema . '.' . $oldIndexName;
582
        }
583
584
        return ['ALTER INDEX ' . $oldIndexName . ' RENAME TO ' . $index->getQuotedName($this)];
585
    }
586
587
    public function getCreateSequenceSQL(Sequence $sequence) : string
588
    {
589
        return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) .
590
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
591
            ' MINVALUE ' . $sequence->getInitialValue() .
592
            ' START ' . $sequence->getInitialValue() .
593
            $this->getSequenceCacheSQL($sequence);
594
    }
595
596
    public function getAlterSequenceSQL(Sequence $sequence) : string
597
    {
598
        return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) .
599
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
600
            $this->getSequenceCacheSQL($sequence);
601
    }
602
603
    /**
604
     * Cache definition for sequences
605
     */
606
    private function getSequenceCacheSQL(Sequence $sequence) : string
607
    {
608
        if ($sequence->getCache() > 1) {
609
            return ' CACHE ' . $sequence->getCache();
610
        }
611
612
        return '';
613
    }
614
615
    /**
616
     * {@inheritDoc}
617
     */
618
    public function getDropSequenceSQL($sequence) : string
619
    {
620
        if ($sequence instanceof Sequence) {
621
            $sequence = $sequence->getQuotedName($this);
622
        }
623
624
        return 'DROP SEQUENCE ' . $sequence . ' CASCADE';
625
    }
626
627
    public function getCreateSchemaSQL(string $schemaName) : string
628
    {
629
        return 'CREATE SCHEMA ' . $schemaName;
630
    }
631
632
    /**
633
     * {@inheritDoc}
634
     */
635
    public function getDropForeignKeySQL($foreignKey, $table) : string
636
    {
637
        return $this->getDropConstraintSQL($foreignKey, $table);
638
    }
639
640
    /**
641
     * {@inheritDoc}
642
     */
643
    protected function _getCreateTableSQL(string $tableName, array $columns, array $options = []) : array
644
    {
645
        $queryFields = $this->getColumnDeclarationListSQL($columns);
646
647
        if (isset($options['primary']) && ! empty($options['primary'])) {
648
            $keyColumns   = array_unique(array_values($options['primary']));
649
            $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')';
650
        }
651
652
        $query = 'CREATE TABLE ' . $tableName . ' (' . $queryFields . ')';
653
654
        $sql = [$query];
655
656
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
657
            foreach ($options['indexes'] as $index) {
658
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
659
            }
660
        }
661
662
        if (isset($options['foreignKeys'])) {
663
            foreach ((array) $options['foreignKeys'] as $definition) {
664
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
665
            }
666
        }
667
668
        return $sql;
669
    }
670
671
    /**
672
     * Converts a single boolean value.
673
     *
674
     * First converts the value to its native PHP boolean type
675
     * and passes it to the given callback function to be reconverted
676
     * into any custom representation.
677
     *
678
     * @param mixed    $value    The value to convert.
679
     * @param callable $callback The callback function to use for converting the real boolean value.
680
     *
681
     * @return mixed
682
     *
683
     * @throws UnexpectedValueException
684
     */
685
    private function convertSingleBooleanValue($value, callable $callback)
686
    {
687
        if ($value === null) {
688
            return $callback(null);
689
        }
690
691
        if (is_bool($value) || is_numeric($value)) {
692
            return $callback((bool) $value);
693
        }
694
695
        if (! is_string($value)) {
696
            return $callback(true);
697
        }
698
699
        /**
700
         * Better safe than sorry: http://php.net/in_array#106319
701
         */
702
        if (in_array(strtolower(trim($value)), $this->booleanLiterals['false'], true)) {
703
            return $callback(false);
704
        }
705
706
        if (in_array(strtolower(trim($value)), $this->booleanLiterals['true'], true)) {
707
            return $callback(true);
708
        }
709
710
        throw new UnexpectedValueException(sprintf(
711
            'Unrecognized boolean literal, %s given.',
712
            $value
713
        ));
714
    }
715
716
    /**
717
     * Converts one or multiple boolean values.
718
     *
719
     * First converts the value(s) to their native PHP boolean type
720
     * and passes them to the given callback function to be reconverted
721
     * into any custom representation.
722
     *
723
     * @param mixed    $item     The value(s) to convert.
724
     * @param callable $callback The callback function to use for converting the real boolean value(s).
725
     *
726
     * @return mixed
727
     */
728
    private function doConvertBooleans($item, callable $callback)
729
    {
730
        if (is_array($item)) {
731
            foreach ($item as $key => $value) {
732
                $item[$key] = $this->convertSingleBooleanValue($value, $callback);
733
            }
734
735
            return $item;
736
        }
737
738
        return $this->convertSingleBooleanValue($item, $callback);
739
    }
740
741
    /**
742
     * {@inheritDoc}
743
     *
744
     * Postgres wants boolean values converted to the strings 'true'/'false'.
745
     */
746
    public function convertBooleans($item)
747
    {
748
        if (! $this->useBooleanTrueFalseStrings) {
749
            return parent::convertBooleans($item);
750
        }
751
752
        return $this->doConvertBooleans(
753
            $item,
754
            static function ($boolean) : string {
755
                if ($boolean === null) {
756
                    return 'NULL';
757
                }
758
759
                return $boolean === true ? 'true' : 'false';
760
            }
761
        );
762
    }
763
764
    /**
765
     * {@inheritDoc}
766
     */
767
    public function convertBooleansToDatabaseValue($item)
768
    {
769
        if (! $this->useBooleanTrueFalseStrings) {
770
            return parent::convertBooleansToDatabaseValue($item);
771
        }
772
773
        return $this->doConvertBooleans(
774
            $item,
775
            static function ($boolean) : ?int {
776
                return $boolean === null ? null : (int) $boolean;
777
            }
778
        );
779
    }
780
781
    /**
782
     * {@inheritDoc}
783
     */
784
    public function convertFromBoolean($item) : ?bool
785
    {
786
        if (in_array($item, $this->booleanLiterals['false'], true)) {
787
            return false;
788
        }
789
790
        return parent::convertFromBoolean($item);
791
    }
792
793
    public function getSequenceNextValSQL(string $sequenceName) : string
794
    {
795
        return "SELECT NEXTVAL('" . $sequenceName . "')";
796
    }
797
798
    public function getSetTransactionIsolationSQL(int $level) : string
799
    {
800
        return 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL '
801
            . $this->_getTransactionIsolationLevelSQL($level);
802
    }
803
804
    /**
805
     * {@inheritDoc}
806
     */
807
    public function getBooleanTypeDeclarationSQL(array $columnDef) : string
808
    {
809
        return 'BOOLEAN';
810
    }
811
812
    /**
813
     * {@inheritDoc}
814
     */
815
    public function getIntegerTypeDeclarationSQL(array $columnDef) : string
816
    {
817
        if (! empty($columnDef['autoincrement'])) {
818
            return 'SERIAL';
819
        }
820
821
        return 'INT';
822
    }
823
824
    /**
825
     * {@inheritDoc}
826
     */
827
    public function getBigIntTypeDeclarationSQL(array $columnDef) : string
828
    {
829
        if (! empty($columnDef['autoincrement'])) {
830
            return 'BIGSERIAL';
831
        }
832
833
        return 'BIGINT';
834
    }
835
836
    /**
837
     * {@inheritDoc}
838
     */
839
    public function getSmallIntTypeDeclarationSQL(array $columnDef) : string
840
    {
841
        if (! empty($columnDef['autoincrement'])) {
842
            return 'SMALLSERIAL';
843
        }
844
845
        return 'SMALLINT';
846
    }
847
848
    /**
849
     * {@inheritDoc}
850
     */
851
    public function getGuidTypeDeclarationSQL(array $column) : string
852
    {
853
        return 'UUID';
854
    }
855
856
    /**
857
     * {@inheritDoc}
858
     */
859
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) : string
860
    {
861
        return 'TIMESTAMP(0) WITHOUT TIME ZONE';
862
    }
863
864
    /**
865
     * {@inheritDoc}
866
     */
867
    public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration) : string
868
    {
869
        return 'TIMESTAMP(0) WITH TIME ZONE';
870
    }
871
872
    /**
873
     * {@inheritDoc}
874
     */
875
    public function getDateTypeDeclarationSQL(array $fieldDeclaration) : string
876
    {
877
        return 'DATE';
878
    }
879
880
    /**
881
     * {@inheritDoc}
882
     */
883
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration) : string
884
    {
885
        return 'TIME(0) WITHOUT TIME ZONE';
886
    }
887
888
    /**
889
     * {@inheritDoc}
890
     */
891
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) : string
892
    {
893
        return '';
894
    }
895
896
    protected function getVarcharTypeDeclarationSQLSnippet(?int $length) : string
897
    {
898
        $sql = 'VARCHAR';
899
900
        if ($length !== null) {
901
            $sql .= sprintf('(%d)', $length);
902
        }
903
904
        return $sql;
905
    }
906
907
    protected function getBinaryTypeDeclarationSQLSnippet(?int $length) : string
908
    {
909
        return 'BYTEA';
910
    }
911
912
    protected function getVarbinaryTypeDeclarationSQLSnippet(?int $length) : string
913
    {
914
        return 'BYTEA';
915
    }
916
917
    /**
918
     * {@inheritDoc}
919
     */
920
    public function getClobTypeDeclarationSQL(array $field) : string
921
    {
922
        return 'TEXT';
923
    }
924
925
    public function getName() : string
926
    {
927
        return 'postgresql';
928
    }
929
930
    /**
931
     * {@inheritDoc}
932
     *
933
     * PostgreSQL returns all column names in SQL result sets in lowercase.
934
     */
935
    public function getSQLResultCasing(string $column) : string
936
    {
937
        return strtolower($column);
938
    }
939
940
    public function getDateTimeTzFormatString() : string
941
    {
942
        return 'Y-m-d H:i:sO';
943
    }
944
945
    public function getEmptyIdentityInsertSQL(string $tableName, string $identifierColumnName) : string
946
    {
947
        return 'INSERT INTO ' . $tableName . ' (' . $identifierColumnName . ') VALUES (DEFAULT)';
948
    }
949
950
    public function getTruncateTableSQL(string $tableName, bool $cascade = false) : string
951
    {
952
        $tableIdentifier = new Identifier($tableName);
953
        $sql             = 'TRUNCATE ' . $tableIdentifier->getQuotedName($this);
954
955
        if ($cascade) {
956
            $sql .= ' CASCADE';
957
        }
958
959
        return $sql;
960
    }
961
962
    public function getReadLockSQL() : string
963
    {
964
        return 'FOR SHARE';
965
    }
966
967
    protected function initializeDoctrineTypeMappings() : void
968
    {
969
        $this->doctrineTypeMapping = [
970
            'bigint'           => 'bigint',
971
            'bigserial'        => 'bigint',
972
            'bool'             => 'boolean',
973
            'boolean'          => 'boolean',
974
            'bpchar'           => 'string',
975
            'bytea'            => 'blob',
976
            'char'             => 'string',
977
            'date'             => 'date',
978
            'datetime'         => 'datetime',
979
            'decimal'          => 'decimal',
980
            'double'           => 'float',
981
            'double precision' => 'float',
982
            'float'            => 'float',
983
            'float4'           => 'float',
984
            'float8'           => 'float',
985
            'inet'             => 'string',
986
            'int'              => 'integer',
987
            'int2'             => 'smallint',
988
            'int4'             => 'integer',
989
            'int8'             => 'bigint',
990
            'integer'          => 'integer',
991
            'interval'         => 'string',
992
            'json'             => 'json',
993
            'jsonb'            => 'json',
994
            'money'            => 'decimal',
995
            'numeric'          => 'decimal',
996
            'serial'           => 'integer',
997
            'serial4'          => 'integer',
998
            'serial8'          => 'bigint',
999
            'real'             => 'float',
1000
            'smallint'         => 'smallint',
1001
            'text'             => 'text',
1002
            'time'             => 'time',
1003
            'timestamp'        => 'datetime',
1004
            'timestamptz'      => 'datetimetz',
1005
            'timetz'           => 'time',
1006
            'tsvector'         => 'text',
1007
            'uuid'             => 'guid',
1008
            'varchar'          => 'string',
1009
            'year'             => 'date',
1010
            '_varchar'         => 'string',
1011
        ];
1012
    }
1013
1014
    public function hasNativeJsonType() : bool
1015
    {
1016
        return true;
1017
    }
1018
1019
    protected function getReservedKeywordsClass() : string
1020
    {
1021
        return Keywords\PostgreSQL94Keywords::class;
1022
    }
1023
1024
    /**
1025
     * {@inheritDoc}
1026
     */
1027
    public function getBlobTypeDeclarationSQL(array $field) : string
1028
    {
1029
        return 'BYTEA';
1030
    }
1031
1032
    /**
1033
     * {@inheritdoc}
1034
     */
1035
    public function getDefaultValueDeclarationSQL(array $field) : string
1036
    {
1037
        if ($this->isSerialField($field)) {
1038
            return '';
1039
        }
1040
1041
        return parent::getDefaultValueDeclarationSQL($field);
1042
    }
1043
1044
    public function supportsColumnCollation() : bool
1045
    {
1046
        return true;
1047
    }
1048
1049
    public function getColumnCollationDeclarationSQL(string $collation) : string
1050
    {
1051
        return 'COLLATE ' . $this->quoteSingleIdentifier($collation);
1052
    }
1053
1054
    /**
1055
     * {@inheritdoc}
1056
     */
1057
    public function getJsonTypeDeclarationSQL(array $field) : string
1058
    {
1059
        if (! empty($field['jsonb'])) {
1060
            return 'JSONB';
1061
        }
1062
1063
        return 'JSON';
1064
    }
1065
1066
    /**
1067
     * @param mixed[] $field
1068
     */
1069
    private function isSerialField(array $field) : bool
1070
    {
1071
        return isset($field['type'], $field['autoincrement'])
1072
            && $field['autoincrement'] === true
1073
            && $this->isNumericType($field['type']);
1074
    }
1075
1076
    /**
1077
     * Check whether the type of a column is changed in a way that invalidates the default value for the column
1078
     */
1079
    private function typeChangeBreaksDefaultValue(ColumnDiff $columnDiff) : bool
1080
    {
1081
        if ($columnDiff->fromColumn === null) {
1082
            return $columnDiff->hasChanged('type');
1083
        }
1084
1085
        $oldTypeIsNumeric = $this->isNumericType($columnDiff->fromColumn->getType());
1086
        $newTypeIsNumeric = $this->isNumericType($columnDiff->column->getType());
1087
1088
        // default should not be changed when switching between numeric types and the default comes from a sequence
1089
        return $columnDiff->hasChanged('type')
1090
            && ! ($oldTypeIsNumeric && $newTypeIsNumeric && $columnDiff->column->getAutoincrement());
1091
    }
1092
1093
    private function isNumericType(Type $type) : bool
1094
    {
1095
        return $type instanceof IntegerType || $type instanceof BigIntType;
1096
    }
1097
1098
    private function getOldColumnComment(ColumnDiff $columnDiff) : ?string
1099
    {
1100
        if ($columnDiff->fromColumn === null) {
1101
            return null;
1102
        }
1103
1104
        return $this->getColumnComment($columnDiff->fromColumn);
1105
    }
1106
1107
    public function getListTableMetadataSQL(string $table, ?string $schema = null) : string
1108
    {
1109
        if ($schema !== null) {
1110
            $table = $schema . '.' . $table;
1111
        }
1112
1113
        return sprintf(
1114
            <<<'SQL'
1115
SELECT obj_description(%s::regclass) AS table_comment;
1116
SQL
1117
            ,
1118
            $this->quoteStringLiteral($table)
1119
        );
1120
    }
1121
}
1122