Failed Conditions
Push — master ( 01c22b...e42c1f )
by Marco
79:13 queued 10s
created

getAdvancedForeignKeyOptionsSQL()   A

Complexity

Conditions 6
Paths 8

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 6

Importance

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

1138
            '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...
1139
            'jsonb'            => Type::JSON,
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::JSON has been deprecated: Use {@see DefaultTypes::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

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