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

1128
            '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...
1129
            '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

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