Completed
Pull Request — master (#3781)
by Yuriy
73:53 queued 08:58
created

PostgreSqlPlatform::convertSingleBooleanValue()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 6

Importance

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

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

1127
            'json'             => /** @scrutinizer ignore-deprecated */ Type::JSON,

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
1128
            'jsonb'            => Type::JSON,
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::JSON has been deprecated: Use {@see Types::JSON} instead. ( Ignorable by Annotation )

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

1128
            'jsonb'            => /** @scrutinizer ignore-deprecated */ Type::JSON,

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
1129
            'money'            => 'decimal',
1130
            'numeric'          => 'decimal',
1131
            'serial'           => 'integer',
1132
            'serial4'          => 'integer',
1133
            'serial8'          => 'bigint',
1134
            'real'             => 'float',
1135
            'smallint'         => 'smallint',
1136
            'text'             => 'text',
1137
            'time'             => 'time',
1138
            'timestamp'        => 'datetime',
1139
            'timestamptz'      => 'datetimetz',
1140
            'timetz'           => 'time',
1141
            'tsvector'         => 'text',
1142
            'uuid'             => 'guid',
1143
            'varchar'          => 'string',
1144
            'year'             => 'date',
1145
            '_varchar'         => 'string',
1146
        ];
1147
    }
1148
1149
    /**
1150
     * {@inheritdoc}
1151
     */
1152
    public function hasNativeJsonType() : bool
1153
    {
1154
        return true;
1155
    }
1156
1157
    /**
1158
     * {@inheritDoc}
1159
     */
1160
    protected function getReservedKeywordsClass() : string
1161
    {
1162
        return Keywords\PostgreSQLKeywords::class;
1163
    }
1164
1165 13531
    /**
1166
     * {@inheritDoc}
1167
     */
1168
    public function getBlobTypeDeclarationSQL(array $field) : string
1169
    {
1170 13704
        return 'BYTEA';
1171
    }
1172 13704
1173
    /**
1174
     * {@inheritdoc}
1175
     */
1176
    public function getDefaultValueDeclarationSQL(array $field) : string
1177
    {
1178 12675
        if ($this->isSerialField($field)) {
1179
            return '';
1180 12675
        }
1181
1182
        return parent::getDefaultValueDeclarationSQL($field);
1183
    }
1184
1185
    /**
1186 12675
     * {@inheritdoc}
1187
     */
1188 12675
    public function supportsColumnCollation() : bool
1189
    {
1190
        return true;
1191
    }
1192
1193
    /**
1194 2656
     * {@inheritdoc}
1195
     */
1196 2656
    public function getColumnCollationDeclarationSQL(string $collation) : string
1197
    {
1198
        return 'COLLATE ' . $this->quoteSingleIdentifier($collation);
1199
    }
1200
1201
    /**
1202 13239
     * {@inheritdoc}
1203
     */
1204 13239
    public function getJsonTypeDeclarationSQL(array $field) : string
1205
    {
1206
        if (! empty($field['jsonb'])) {
1207
            return 'JSONB';
1208
        }
1209
1210 14031
        return 'JSON';
1211
    }
1212 14031
1213 13706
    /**
1214
     * @param mixed[] $field
1215
     */
1216 13007
    private function isSerialField(array $field) : bool
1217
    {
1218
        return isset($field['type'], $field['autoincrement'])
1219
            && $field['autoincrement'] === true
1220
            && $this->isNumericType($field['type']);
1221
    }
1222 14031
1223
    /**
1224 14031
     * Check whether the type of a column is changed in a way that invalidates the default value for the column
1225 14031
     */
1226 14031
    private function typeChangeBreaksDefaultValue(ColumnDiff $columnDiff) : bool
1227
    {
1228
        if ($columnDiff->fromColumn === null) {
1229
            return $columnDiff->hasChanged('type');
1230
        }
1231
1232 12891
        $oldTypeIsNumeric = $this->isNumericType($columnDiff->fromColumn->getType());
1233
        $newTypeIsNumeric = $this->isNumericType($columnDiff->column->getType());
1234 12891
1235 11675
        // default should not be changed when switching between numeric types and the default comes from a sequence
1236
        return $columnDiff->hasChanged('type')
1237
            && ! ($oldTypeIsNumeric && $newTypeIsNumeric && $columnDiff->column->getAutoincrement());
1238 12679
    }
1239 12679
1240
    private function isNumericType(Type $type) : bool
1241
    {
1242 12679
        return $type instanceof IntegerType || $type instanceof BigIntType;
1243 12679
    }
1244
1245
    private function getOldColumnComment(ColumnDiff $columnDiff) : ?string
1246 13746
    {
1247
        if ($columnDiff->fromColumn === null) {
1248 13746
            return null;
1249
        }
1250
1251 12901
        return $this->getColumnComment($columnDiff->fromColumn);
1252
    }
1253 12901
1254
    public function getListTableMetadataSQL(string $table, ?string $schema = null) : string
1255
    {
1256 4529
        if ($schema !== null) {
1257
            $table = $schema . '.' . $table;
1258 4529
        }
1259
1260
        return sprintf(
1261
            <<<'SQL'
1262 4529
SELECT obj_description(%s::regclass) AS table_comment;
1263
SQL
1264
            ,
1265
            $this->quoteStringLiteral($table)
1266
        );
1267 4529
    }
1268
}
1269