Completed
Pull Request — master (#3711)
by
unknown
65:53
created

PostgreSqlPlatform::isCaseFoldedForUnquoted()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

1139
            '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...
1140
            '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

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