Subversion-Projekte lars-tiefland.laravel_shop

Revision

Zur aktuellen Revision | Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
namespace Doctrine\DBAL\Platforms;
4
 
5
use Doctrine\DBAL\Connection;
6
use Doctrine\DBAL\Schema\Column;
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\PostgreSQLSchemaManager;
12
use Doctrine\DBAL\Schema\Sequence;
13
use Doctrine\DBAL\Schema\TableDiff;
14
use Doctrine\DBAL\Types\BinaryType;
15
use Doctrine\DBAL\Types\BlobType;
16
use Doctrine\Deprecations\Deprecation;
17
use UnexpectedValueException;
18
 
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 database platform of the oldest supported version.
38
 */
39
class PostgreSQLPlatform extends AbstractPlatform
40
{
41
    private bool $useBooleanTrueFalseStrings = true;
42
 
43
    /** @var string[][] PostgreSQL booleans literals */
44
    private array $booleanLiterals = [
45
        'true' => [
46
            't',
47
            'true',
48
            'y',
49
            'yes',
50
            'on',
51
            '1',
52
        ],
53
        'false' => [
54
            'f',
55
            'false',
56
            'n',
57
            'no',
58
            'off',
59
            '0',
60
        ],
61
    ];
62
 
63
    /**
64
     * PostgreSQL has different behavior with some drivers
65
     * with regard to how booleans have to be handled.
66
     *
67
     * Enables use of 'true'/'false' or otherwise 1 and 0 instead.
68
     *
69
     * @param bool $flag
70
     *
71
     * @return void
72
     */
73
    public function setUseBooleanTrueFalseStrings($flag)
74
    {
75
        $this->useBooleanTrueFalseStrings = (bool) $flag;
76
    }
77
 
78
    /**
79
     * {@inheritDoc}
80
     */
81
    public function getSubstringExpression($string, $start, $length = null)
82
    {
83
        if ($length === null) {
84
            return 'SUBSTRING(' . $string . ' FROM ' . $start . ')';
85
        }
86
 
87
        return 'SUBSTRING(' . $string . ' FROM ' . $start . ' FOR ' . $length . ')';
88
    }
89
 
90
    /**
91
     * {@inheritDoc}
92
     *
93
     * @deprecated Generate dates within the application.
94
     */
95
    public function getNowExpression()
96
    {
97
        Deprecation::trigger(
98
            'doctrine/dbal',
99
            'https://github.com/doctrine/dbal/pull/4753',
100
            'PostgreSQLPlatform::getNowExpression() is deprecated. Generate dates within the application.',
101
        );
102
 
103
        return 'LOCALTIMESTAMP(0)';
104
    }
105
 
106
    /**
107
     * {@inheritDoc}
108
     */
109
    public function getRegexpExpression()
110
    {
111
        return 'SIMILAR TO';
112
    }
113
 
114
    /**
115
     * {@inheritDoc}
116
     */
117
    public function getLocateExpression($str, $substr, $startPos = false)
118
    {
119
        if ($startPos !== false) {
120
            $str = $this->getSubstringExpression($str, $startPos);
121
 
122
            return 'CASE WHEN (POSITION(' . $substr . ' IN ' . $str . ') = 0) THEN 0'
123
                . ' ELSE (POSITION(' . $substr . ' IN ' . $str . ') + ' . $startPos . ' - 1) END';
124
        }
125
 
126
        return 'POSITION(' . $substr . ' IN ' . $str . ')';
127
    }
128
 
129
    /**
130
     * {@inheritdoc}
131
     */
132
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
133
    {
134
        if ($unit === DateIntervalUnit::QUARTER) {
135
            $interval *= 3;
136
            $unit      = DateIntervalUnit::MONTH;
137
        }
138
 
139
        return '(' . $date . ' ' . $operator . ' (' . $interval . " || ' " . $unit . "')::interval)";
140
    }
141
 
142
    /**
143
     * {@inheritDoc}
144
     */
145
    public function getDateDiffExpression($date1, $date2)
146
    {
147
        return '(DATE(' . $date1 . ')-DATE(' . $date2 . '))';
148
    }
149
 
150
    public function getCurrentDatabaseExpression(): string
151
    {
152
        return 'CURRENT_DATABASE()';
153
    }
154
 
155
    /**
156
     * {@inheritDoc}
157
     */
158
    public function supportsSequences()
159
    {
160
        return true;
161
    }
162
 
163
    /**
164
     * {@inheritDoc}
165
     */
166
    public function supportsSchemas()
167
    {
168
        return true;
169
    }
170
 
171
    /**
172
     * {@inheritdoc}
173
     *
174
     * @deprecated
175
     */
176
    public function getDefaultSchemaName()
177
    {
178
        Deprecation::trigger(
179
            'doctrine/dbal',
180
            'https://github.com/doctrine/dbal/pull/5513',
181
            '%s is deprecated.',
182
            __METHOD__,
183
        );
184
 
185
        return 'public';
186
    }
187
 
188
    /**
189
     * {@inheritDoc}
190
     */
191
    public function supportsIdentityColumns()
192
    {
193
        return true;
194
    }
195
 
196
    /**
197
     * {@inheritdoc}
198
     *
199
     * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy.
200
     */
201
    public function supportsPartialIndexes()
202
    {
203
        return true;
204
    }
205
 
206
    /**
207
     * {@inheritdoc}
208
     *
209
     * @deprecated
210
     */
211
    public function usesSequenceEmulatedIdentityColumns()
212
    {
213
        Deprecation::trigger(
214
            'doctrine/dbal',
215
            'https://github.com/doctrine/dbal/pull/5513',
216
            '%s is deprecated.',
217
            __METHOD__,
218
        );
219
 
220
        return true;
221
    }
222
 
223
    /**
224
     * {@inheritdoc}
225
     *
226
     * @deprecated
227
     */
228
    public function getIdentitySequenceName($tableName, $columnName)
229
    {
230
        Deprecation::triggerIfCalledFromOutside(
231
            'doctrine/dbal',
232
            'https://github.com/doctrine/dbal/pull/5513',
233
            '%s is deprecated.',
234
            __METHOD__,
235
        );
236
 
237
        return $tableName . '_' . $columnName . '_seq';
238
    }
239
 
240
    /**
241
     * {@inheritDoc}
242
     *
243
     * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy.
244
     */
245
    public function supportsCommentOnStatement()
246
    {
247
        return true;
248
    }
249
 
250
    /**
251
     * {@inheritDoc}
252
     *
253
     * @deprecated
254
     */
255
    public function hasNativeGuidType()
256
    {
257
        Deprecation::triggerIfCalledFromOutside(
258
            'doctrine/dbal',
259
            'https://github.com/doctrine/dbal/pull/5509',
260
            '%s is deprecated.',
261
            __METHOD__,
262
        );
263
 
264
        return true;
265
    }
266
 
267
    /**
268
     * {@inheritDoc}
269
     *
270
     * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy.
271
     */
272
    public function getListDatabasesSQL()
273
    {
274
        return 'SELECT datname FROM pg_database';
275
    }
276
 
277
    /**
278
     * {@inheritDoc}
279
     *
280
     * @deprecated Use {@see PostgreSQLSchemaManager::listSchemaNames()} instead.
281
     */
282
    public function getListNamespacesSQL()
283
    {
284
        Deprecation::triggerIfCalledFromOutside(
285
            'doctrine/dbal',
286
            'https://github.com/doctrine/dbal/issues/4503',
287
            'PostgreSQLPlatform::getListNamespacesSQL() is deprecated,'
288
                . ' use PostgreSQLSchemaManager::listSchemaNames() instead.',
289
        );
290
 
291
        return "SELECT schema_name AS nspname
292
                FROM   information_schema.schemata
293
                WHERE  schema_name NOT LIKE 'pg\_%'
294
                AND    schema_name != 'information_schema'";
295
    }
296
 
297
    /**
298
     * {@inheritDoc}
299
     *
300
     * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy.
301
     */
302
    public function getListSequencesSQL($database)
303
    {
304
        return 'SELECT sequence_name AS relname,
305
                       sequence_schema AS schemaname,
306
                       minimum_value AS min_value,
307
                       increment AS increment_by
308
                FROM   information_schema.sequences
309
                WHERE  sequence_catalog = ' . $this->quoteStringLiteral($database) . "
310
                AND    sequence_schema NOT LIKE 'pg\_%'
311
                AND    sequence_schema != 'information_schema'";
312
    }
313
 
314
    /**
315
     * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon.
316
     *
317
     * {@inheritDoc}
318
     */
319
    public function getListTablesSQL()
320
    {
321
        return "SELECT quote_ident(table_name) AS table_name,
322
                       table_schema AS schema_name
323
                FROM   information_schema.tables
324
                WHERE  table_schema NOT LIKE 'pg\_%'
325
                AND    table_schema != 'information_schema'
326
                AND    table_name != 'geometry_columns'
327
                AND    table_name != 'spatial_ref_sys'
328
                AND    table_type != 'VIEW'";
329
    }
330
 
331
    /**
332
     * {@inheritDoc}
333
     *
334
     * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy.
335
     */
336
    public function getListViewsSQL($database)
337
    {
338
        return 'SELECT quote_ident(table_name) AS viewname,
339
                       table_schema AS schemaname,
340
                       view_definition AS definition
341
                FROM   information_schema.views
342
                WHERE  view_definition IS NOT NULL';
343
    }
344
 
345
    /**
346
     * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon.
347
     *
348
     * @param string      $table
349
     * @param string|null $database
350
     *
351
     * @return string
352
     */
353
    public function getListTableForeignKeysSQL($table, $database = null)
354
    {
355
        return 'SELECT quote_ident(r.conname) as conname, pg_catalog.pg_get_constraintdef(r.oid, true) as condef
356
                  FROM pg_catalog.pg_constraint r
357
                  WHERE r.conrelid =
358
                  (
359
                      SELECT c.oid
360
                      FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n
361
                      WHERE ' . $this->getTableWhereClause($table) . " AND n.oid = c.relnamespace
362
                  )
363
                  AND r.contype = 'f'";
364
    }
365
 
366
    /**
367
     * @deprecated
368
     *
369
     * {@inheritDoc}
370
     */
371
    public function getListTableConstraintsSQL($table)
372
    {
373
        $table = new Identifier($table);
374
        $table = $this->quoteStringLiteral($table->getName());
375
 
376
        return sprintf(
377
            <<<'SQL'
378
SELECT
379
    quote_ident(relname) as relname
380
FROM
381
    pg_class
382
WHERE oid IN (
383
    SELECT indexrelid
384
    FROM pg_index, pg_class
385
    WHERE pg_class.relname = %s
386
        AND pg_class.oid = pg_index.indrelid
387
        AND (indisunique = 't' OR indisprimary = 't')
388
    )
389
SQL
390
            ,
391
            $table,
392
        );
393
    }
394
 
395
    /**
396
     * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon.
397
     *
398
     * {@inheritDoc}
399
     *
400
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
401
     */
402
    public function getListTableIndexesSQL($table, $database = null)
403
    {
404
        return 'SELECT quote_ident(relname) as relname, pg_index.indisunique, pg_index.indisprimary,
405
                       pg_index.indkey, pg_index.indrelid,
406
                       pg_get_expr(indpred, indrelid) AS where
407
                 FROM pg_class, pg_index
408
                 WHERE oid IN (
409
                    SELECT indexrelid
410
                    FROM pg_index si, pg_class sc, pg_namespace sn
411
                    WHERE ' . $this->getTableWhereClause($table, 'sc', 'sn') . '
412
                    AND sc.oid=si.indrelid AND sc.relnamespace = sn.oid
413
                 ) AND pg_index.indexrelid = oid';
414
    }
415
 
416
    /**
417
     * @param string $table
418
     * @param string $classAlias
419
     * @param string $namespaceAlias
420
     */
421
    private function getTableWhereClause($table, $classAlias = 'c', $namespaceAlias = 'n'): string
422
    {
423
        $whereClause = $namespaceAlias . ".nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') AND ";
424
        if (strpos($table, '.') !== false) {
425
            [$schema, $table] = explode('.', $table);
426
            $schema           = $this->quoteStringLiteral($schema);
427
        } else {
428
            $schema = 'ANY(current_schemas(false))';
429
        }
430
 
431
        $table = new Identifier($table);
432
        $table = $this->quoteStringLiteral($table->getName());
433
 
434
        return $whereClause . sprintf(
435
            '%s.relname = %s AND %s.nspname = %s',
436
            $classAlias,
437
            $table,
438
            $namespaceAlias,
439
            $schema,
440
        );
441
    }
442
 
443
    /**
444
     * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon.
445
     *
446
     * {@inheritDoc}
447
     */
448
    public function getListTableColumnsSQL($table, $database = null)
449
    {
450
        return "SELECT
451
                    a.attnum,
452
                    quote_ident(a.attname) AS field,
453
                    t.typname AS type,
454
                    format_type(a.atttypid, a.atttypmod) AS complete_type,
455
                    (SELECT tc.collcollate FROM pg_catalog.pg_collation tc WHERE tc.oid = a.attcollation) AS collation,
456
                    (SELECT t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type,
457
                    (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM
458
                      pg_catalog.pg_type t2 WHERE t2.typtype = 'd' AND t2.oid = a.atttypid) AS domain_complete_type,
459
                    a.attnotnull AS isnotnull,
460
                    (SELECT 't'
461
                     FROM pg_index
462
                     WHERE c.oid = pg_index.indrelid
463
                        AND pg_index.indkey[0] = a.attnum
464
                        AND pg_index.indisprimary = 't'
465
                    ) AS pri,
466
                    (SELECT pg_get_expr(adbin, adrelid)
467
                     FROM pg_attrdef
468
                     WHERE c.oid = pg_attrdef.adrelid
469
                        AND pg_attrdef.adnum=a.attnum
470
                    ) AS default,
471
                    (SELECT pg_description.description
472
                        FROM pg_description WHERE pg_description.objoid = c.oid AND a.attnum = pg_description.objsubid
473
                    ) AS comment
474
                    FROM pg_attribute a, pg_class c, pg_type t, pg_namespace n
475
                    WHERE " . $this->getTableWhereClause($table, 'c', 'n') . '
476
                        AND a.attnum > 0
477
                        AND a.attrelid = c.oid
478
                        AND a.atttypid = t.oid
479
                        AND n.oid = c.relnamespace
480
                    ORDER BY a.attnum';
481
    }
482
 
483
    /**
484
     * {@inheritDoc}
485
     *
486
     * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy.
487
     */
488
    public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey)
489
    {
490
        $query = '';
491
 
492
        if ($foreignKey->hasOption('match')) {
493
            $query .= ' MATCH ' . $foreignKey->getOption('match');
494
        }
495
 
496
        $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey);
497
 
498
        if ($foreignKey->hasOption('deferrable') && $foreignKey->getOption('deferrable') !== false) {
499
            $query .= ' DEFERRABLE';
500
        } else {
501
            $query .= ' NOT DEFERRABLE';
502
        }
503
 
504
        if (
505
            ($foreignKey->hasOption('feferred') && $foreignKey->getOption('feferred') !== false)
506
            || ($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false)
507
        ) {
508
            $query .= ' INITIALLY DEFERRED';
509
        } else {
510
            $query .= ' INITIALLY IMMEDIATE';
511
        }
512
 
513
        return $query;
514
    }
515
 
516
    /**
517
     * {@inheritDoc}
518
     */
519
    public function getAlterTableSQL(TableDiff $diff)
520
    {
521
        $sql         = [];
522
        $commentsSQL = [];
523
        $columnSql   = [];
524
 
525
        $table = $diff->getOldTable() ?? $diff->getName($this);
526
 
527
        $tableNameSQL = $table->getQuotedName($this);
528
 
529
        foreach ($diff->getAddedColumns() as $addedColumn) {
530
            if ($this->onSchemaAlterTableAddColumn($addedColumn, $diff, $columnSql)) {
531
                continue;
532
            }
533
 
534
            $query = 'ADD ' . $this->getColumnDeclarationSQL(
535
                $addedColumn->getQuotedName($this),
536
                $addedColumn->toArray(),
537
            );
538
 
539
            $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query;
540
 
541
            $comment = $this->getColumnComment($addedColumn);
542
 
543
            if ($comment === null || $comment === '') {
544
                continue;
545
            }
546
 
547
            $commentsSQL[] = $this->getCommentOnColumnSQL(
548
                $tableNameSQL,
549
                $addedColumn->getQuotedName($this),
550
                $comment,
551
            );
552
        }
553
 
554
        foreach ($diff->getDroppedColumns() as $droppedColumn) {
555
            if ($this->onSchemaAlterTableRemoveColumn($droppedColumn, $diff, $columnSql)) {
556
                continue;
557
            }
558
 
559
            $query = 'DROP ' . $droppedColumn->getQuotedName($this);
560
            $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query;
561
        }
562
 
563
        foreach ($diff->getModifiedColumns() as $columnDiff) {
564
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
565
                continue;
566
            }
567
 
568
            if ($this->isUnchangedBinaryColumn($columnDiff)) {
569
                continue;
570
            }
571
 
572
            $oldColumn = $columnDiff->getOldColumn() ?? $columnDiff->getOldColumnName();
573
            $newColumn = $columnDiff->getNewColumn();
574
 
575
            $oldColumnName = $oldColumn->getQuotedName($this);
576
 
577
            if (
578
                $columnDiff->hasTypeChanged()
579
                || $columnDiff->hasPrecisionChanged()
580
                || $columnDiff->hasScaleChanged()
581
                || $columnDiff->hasFixedChanged()
582
            ) {
583
                $type = $newColumn->getType();
584
 
585
                // SERIAL/BIGSERIAL are not "real" types and we can't alter a column to that type
586
                $columnDefinition                  = $newColumn->toArray();
587
                $columnDefinition['autoincrement'] = false;
588
 
589
                // here was a server version check before, but DBAL API does not support this anymore.
590
                $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $type->getSQLDeclaration($columnDefinition, $this);
591
                $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query;
592
            }
593
 
594
            if ($columnDiff->hasDefaultChanged()) {
595
                $defaultClause = $newColumn->getDefault() === null
596
                    ? ' DROP DEFAULT'
597
                    : ' SET' . $this->getDefaultValueDeclarationSQL($newColumn->toArray());
598
 
599
                $query = 'ALTER ' . $oldColumnName . $defaultClause;
600
                $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query;
601
            }
602
 
603
            if ($columnDiff->hasNotNullChanged()) {
604
                $query = 'ALTER ' . $oldColumnName . ' ' . ($newColumn->getNotnull() ? 'SET' : 'DROP') . ' NOT NULL';
605
                $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query;
606
            }
607
 
608
            if ($columnDiff->hasAutoIncrementChanged()) {
609
                if ($newColumn->getAutoincrement()) {
610
                    // add autoincrement
611
                    $seqName = $this->getIdentitySequenceName(
612
                        $table->getName(),
613
                        $oldColumnName,
614
                    );
615
 
616
                    $sql[] = 'CREATE SEQUENCE ' . $seqName;
617
                    $sql[] = "SELECT setval('" . $seqName . "', (SELECT MAX(" . $oldColumnName . ') FROM '
618
                        . $tableNameSQL . '))';
619
                    $query = 'ALTER ' . $oldColumnName . " SET DEFAULT nextval('" . $seqName . "')";
620
                } else {
621
                    // Drop autoincrement, but do NOT drop the sequence. It might be re-used by other tables or have
622
                    $query = 'ALTER ' . $oldColumnName . ' DROP DEFAULT';
623
                }
624
 
625
                $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query;
626
            }
627
 
628
            $oldComment = $this->getOldColumnComment($columnDiff);
629
            $newComment = $this->getColumnComment($newColumn);
630
 
631
            if (
632
                $columnDiff->hasCommentChanged()
633
                || ($columnDiff->getOldColumn() !== null && $oldComment !== $newComment)
634
            ) {
635
                $commentsSQL[] = $this->getCommentOnColumnSQL(
636
                    $tableNameSQL,
637
                    $newColumn->getQuotedName($this),
638
                    $newComment,
639
                );
640
            }
641
 
642
            if (! $columnDiff->hasLengthChanged()) {
643
                continue;
644
            }
645
 
646
            $query = 'ALTER ' . $oldColumnName . ' TYPE '
647
                . $newColumn->getType()->getSQLDeclaration($newColumn->toArray(), $this);
648
            $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query;
649
        }
650
 
651
        foreach ($diff->getRenamedColumns() as $oldColumnName => $column) {
652
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
653
                continue;
654
            }
655
 
656
            $oldColumnName = new Identifier($oldColumnName);
657
 
658
            $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' RENAME COLUMN ' . $oldColumnName->getQuotedName($this)
659
                . ' TO ' . $column->getQuotedName($this);
660
        }
661
 
662
        $tableSql = [];
663
 
664
        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
665
            $sql = array_merge($sql, $commentsSQL);
666
 
667
            $newName = $diff->getNewName();
668
 
669
            if ($newName !== false) {
670
                Deprecation::trigger(
671
                    'doctrine/dbal',
672
                    'https://github.com/doctrine/dbal/pull/5663',
673
                    'Generation of "rename table" SQL using %s is deprecated. Use getRenameTableSQL() instead.',
674
                    __METHOD__,
675
                );
676
 
677
                $sql[] = sprintf(
678
                    'ALTER TABLE %s RENAME TO %s',
679
                    $tableNameSQL,
680
                    $newName->getQuotedName($this),
681
                );
682
            }
683
 
684
            $sql = array_merge(
685
                $this->getPreAlterTableIndexForeignKeySQL($diff),
686
                $sql,
687
                $this->getPostAlterTableIndexForeignKeySQL($diff),
688
            );
689
        }
690
 
691
        return array_merge($sql, $tableSql, $columnSql);
692
    }
693
 
694
    /**
695
     * Checks whether a given column diff is a logically unchanged binary type column.
696
     *
697
     * Used to determine whether a column alteration for a binary type column can be skipped.
698
     * Doctrine's {@see BinaryType} and {@see BlobType} are mapped to the same database column type on this platform
699
     * as this platform does not have a native VARBINARY/BINARY column type. Therefore the comparator
700
     * might detect differences for binary type columns which do not have to be propagated
701
     * to database as there actually is no difference at database level.
702
     */
703
    private function isUnchangedBinaryColumn(ColumnDiff $columnDiff): bool
704
    {
705
        $newColumnType = $columnDiff->getNewColumn()->getType();
706
 
707
        if (! $newColumnType instanceof BinaryType && ! $newColumnType instanceof BlobType) {
708
            return false;
709
        }
710
 
711
        $oldColumn = $columnDiff->getOldColumn() instanceof Column ? $columnDiff->getOldColumn() : null;
712
 
713
        if ($oldColumn !== null) {
714
            $oldColumnType = $oldColumn->getType();
715
 
716
            if (! $oldColumnType instanceof BinaryType && ! $oldColumnType instanceof BlobType) {
717
                return false;
718
            }
719
 
720
            return count(array_diff($columnDiff->changedProperties, ['type', 'length', 'fixed'])) === 0;
721
        }
722
 
723
        if ($columnDiff->hasTypeChanged()) {
724
            return false;
725
        }
726
 
727
        return count(array_diff($columnDiff->changedProperties, ['length', 'fixed'])) === 0;
728
    }
729
 
730
    /**
731
     * {@inheritdoc}
732
     */
733
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
734
    {
735
        if (strpos($tableName, '.') !== false) {
736
            [$schema]     = explode('.', $tableName);
737
            $oldIndexName = $schema . '.' . $oldIndexName;
738
        }
739
 
740
        return ['ALTER INDEX ' . $oldIndexName . ' RENAME TO ' . $index->getQuotedName($this)];
741
    }
742
 
743
    /**
744
     * {@inheritdoc}
745
     *
746
     * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy.
747
     */
748
    public function getCommentOnColumnSQL($tableName, $columnName, $comment)
749
    {
750
        $tableName  = new Identifier($tableName);
751
        $columnName = new Identifier($columnName);
752
        $comment    = $comment === null ? 'NULL' : $this->quoteStringLiteral($comment);
753
 
754
        return sprintf(
755
            'COMMENT ON COLUMN %s.%s IS %s',
756
            $tableName->getQuotedName($this),
757
            $columnName->getQuotedName($this),
758
            $comment,
759
        );
760
    }
761
 
762
    /**
763
     * {@inheritDoc}
764
     */
765
    public function getCreateSequenceSQL(Sequence $sequence)
766
    {
767
        return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) .
768
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
769
            ' MINVALUE ' . $sequence->getInitialValue() .
770
            ' START ' . $sequence->getInitialValue() .
771
            $this->getSequenceCacheSQL($sequence);
772
    }
773
 
774
    /**
775
     * {@inheritDoc}
776
     */
777
    public function getAlterSequenceSQL(Sequence $sequence)
778
    {
779
        return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) .
780
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
781
            $this->getSequenceCacheSQL($sequence);
782
    }
783
 
784
    /**
785
     * Cache definition for sequences
786
     */
787
    private function getSequenceCacheSQL(Sequence $sequence): string
788
    {
789
        if ($sequence->getCache() > 1) {
790
            return ' CACHE ' . $sequence->getCache();
791
        }
792
 
793
        return '';
794
    }
795
 
796
    /**
797
     * {@inheritDoc}
798
     */
799
    public function getDropSequenceSQL($sequence)
800
    {
801
        return parent::getDropSequenceSQL($sequence) . ' CASCADE';
802
    }
803
 
804
    /**
805
     * {@inheritDoc}
806
     */
807
    public function getDropForeignKeySQL($foreignKey, $table)
808
    {
809
        return $this->getDropConstraintSQL($foreignKey, $table);
810
    }
811
 
812
    /**
813
     * {@inheritDoc}
814
     */
815
    protected function _getCreateTableSQL($name, array $columns, array $options = [])
816
    {
817
        $queryFields = $this->getColumnDeclarationListSQL($columns);
818
 
819
        if (isset($options['primary']) && ! empty($options['primary'])) {
820
            $keyColumns   = array_unique(array_values($options['primary']));
821
            $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')';
822
        }
823
 
824
        $query = 'CREATE TABLE ' . $name . ' (' . $queryFields . ')';
825
 
826
        $sql = [$query];
827
 
828
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
829
            foreach ($options['indexes'] as $index) {
830
                $sql[] = $this->getCreateIndexSQL($index, $name);
831
            }
832
        }
833
 
834
        if (isset($options['uniqueConstraints'])) {
835
            foreach ($options['uniqueConstraints'] as $uniqueConstraint) {
836
                $sql[] = $this->getCreateConstraintSQL($uniqueConstraint, $name);
837
            }
838
        }
839
 
840
        if (isset($options['foreignKeys'])) {
841
            foreach ($options['foreignKeys'] as $definition) {
842
                $sql[] = $this->getCreateForeignKeySQL($definition, $name);
843
            }
844
        }
845
 
846
        return $sql;
847
    }
848
 
849
    /**
850
     * Converts a single boolean value.
851
     *
852
     * First converts the value to its native PHP boolean type
853
     * and passes it to the given callback function to be reconverted
854
     * into any custom representation.
855
     *
856
     * @param mixed    $value    The value to convert.
857
     * @param callable $callback The callback function to use for converting the real boolean value.
858
     *
859
     * @return mixed
860
     *
861
     * @throws UnexpectedValueException
862
     */
863
    private function convertSingleBooleanValue($value, $callback)
864
    {
865
        if ($value === null) {
866
            return $callback(null);
867
        }
868
 
869
        if (is_bool($value) || is_numeric($value)) {
870
            return $callback((bool) $value);
871
        }
872
 
873
        if (! is_string($value)) {
874
            return $callback(true);
875
        }
876
 
877
        /**
878
         * Better safe than sorry: http://php.net/in_array#106319
879
         */
880
        if (in_array(strtolower(trim($value)), $this->booleanLiterals['false'], true)) {
881
            return $callback(false);
882
        }
883
 
884
        if (in_array(strtolower(trim($value)), $this->booleanLiterals['true'], true)) {
885
            return $callback(true);
886
        }
887
 
888
        throw new UnexpectedValueException(sprintf("Unrecognized boolean literal '%s'", $value));
889
    }
890
 
891
    /**
892
     * Converts one or multiple boolean values.
893
     *
894
     * First converts the value(s) to their native PHP boolean type
895
     * and passes them to the given callback function to be reconverted
896
     * into any custom representation.
897
     *
898
     * @param mixed    $item     The value(s) to convert.
899
     * @param callable $callback The callback function to use for converting the real boolean value(s).
900
     *
901
     * @return mixed
902
     */
903
    private function doConvertBooleans($item, $callback)
904
    {
905
        if (is_array($item)) {
906
            foreach ($item as $key => $value) {
907
                $item[$key] = $this->convertSingleBooleanValue($value, $callback);
908
            }
909
 
910
            return $item;
911
        }
912
 
913
        return $this->convertSingleBooleanValue($item, $callback);
914
    }
915
 
916
    /**
917
     * {@inheritDoc}
918
     *
919
     * Postgres wants boolean values converted to the strings 'true'/'false'.
920
     */
921
    public function convertBooleans($item)
922
    {
923
        if (! $this->useBooleanTrueFalseStrings) {
924
            return parent::convertBooleans($item);
925
        }
926
 
927
        return $this->doConvertBooleans(
928
            $item,
929
            /** @param mixed $value */
930
            static function ($value) {
931
                if ($value === null) {
932
                    return 'NULL';
933
                }
934
 
935
                return $value === true ? 'true' : 'false';
936
            },
937
        );
938
    }
939
 
940
    /**
941
     * {@inheritDoc}
942
     */
943
    public function convertBooleansToDatabaseValue($item)
944
    {
945
        if (! $this->useBooleanTrueFalseStrings) {
946
            return parent::convertBooleansToDatabaseValue($item);
947
        }
948
 
949
        return $this->doConvertBooleans(
950
            $item,
951
            /** @param mixed $value */
952
            static function ($value): ?int {
953
                return $value === null ? null : (int) $value;
954
            },
955
        );
956
    }
957
 
958
    /**
959
     * {@inheritDoc}
960
     */
961
    public function convertFromBoolean($item)
962
    {
963
        if ($item !== null && in_array(strtolower($item), $this->booleanLiterals['false'], true)) {
964
            return false;
965
        }
966
 
967
        return parent::convertFromBoolean($item);
968
    }
969
 
970
    /**
971
     * {@inheritDoc}
972
     */
973
    public function getSequenceNextValSQL($sequence)
974
    {
975
        return "SELECT NEXTVAL('" . $sequence . "')";
976
    }
977
 
978
    /**
979
     * {@inheritDoc}
980
     */
981
    public function getSetTransactionIsolationSQL($level)
982
    {
983
        return 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL '
984
            . $this->_getTransactionIsolationLevelSQL($level);
985
    }
986
 
987
    /**
988
     * {@inheritDoc}
989
     */
990
    public function getBooleanTypeDeclarationSQL(array $column)
991
    {
992
        return 'BOOLEAN';
993
    }
994
 
995
    /**
996
     * {@inheritDoc}
997
     */
998
    public function getIntegerTypeDeclarationSQL(array $column)
999
    {
1000
        if (! empty($column['autoincrement'])) {
1001
            return 'SERIAL';
1002
        }
1003
 
1004
        return 'INT';
1005
    }
1006
 
1007
    /**
1008
     * {@inheritDoc}
1009
     */
1010
    public function getBigIntTypeDeclarationSQL(array $column)
1011
    {
1012
        if (! empty($column['autoincrement'])) {
1013
            return 'BIGSERIAL';
1014
        }
1015
 
1016
        return 'BIGINT';
1017
    }
1018
 
1019
    /**
1020
     * {@inheritDoc}
1021
     */
1022
    public function getSmallIntTypeDeclarationSQL(array $column)
1023
    {
1024
        if (! empty($column['autoincrement'])) {
1025
            return 'SMALLSERIAL';
1026
        }
1027
 
1028
        return 'SMALLINT';
1029
    }
1030
 
1031
    /**
1032
     * {@inheritDoc}
1033
     */
1034
    public function getGuidTypeDeclarationSQL(array $column)
1035
    {
1036
        return 'UUID';
1037
    }
1038
 
1039
    /**
1040
     * {@inheritDoc}
1041
     */
1042
    public function getDateTimeTypeDeclarationSQL(array $column)
1043
    {
1044
        return 'TIMESTAMP(0) WITHOUT TIME ZONE';
1045
    }
1046
 
1047
    /**
1048
     * {@inheritDoc}
1049
     */
1050
    public function getDateTimeTzTypeDeclarationSQL(array $column)
1051
    {
1052
        return 'TIMESTAMP(0) WITH TIME ZONE';
1053
    }
1054
 
1055
    /**
1056
     * {@inheritDoc}
1057
     */
1058
    public function getDateTypeDeclarationSQL(array $column)
1059
    {
1060
        return 'DATE';
1061
    }
1062
 
1063
    /**
1064
     * {@inheritDoc}
1065
     */
1066
    public function getTimeTypeDeclarationSQL(array $column)
1067
    {
1068
        return 'TIME(0) WITHOUT TIME ZONE';
1069
    }
1070
 
1071
    /**
1072
     * {@inheritDoc}
1073
     */
1074
    protected function _getCommonIntegerTypeDeclarationSQL(array $column)
1075
    {
1076
        return '';
1077
    }
1078
 
1079
    /**
1080
     * {@inheritDoc}
1081
     */
1082
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1083
    {
1084
        return $fixed ? ($length > 0 ? 'CHAR(' . $length . ')' : 'CHAR(255)')
1085
            : ($length > 0 ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)');
1086
    }
1087
 
1088
    /**
1089
     * {@inheritdoc}
1090
     */
1091
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
1092
    {
1093
        return 'BYTEA';
1094
    }
1095
 
1096
    /**
1097
     * {@inheritDoc}
1098
     */
1099
    public function getClobTypeDeclarationSQL(array $column)
1100
    {
1101
        return 'TEXT';
1102
    }
1103
 
1104
    /**
1105
     * {@inheritDoc}
1106
     */
1107
    public function getName()
1108
    {
1109
        Deprecation::triggerIfCalledFromOutside(
1110
            'doctrine/dbal',
1111
            'https://github.com/doctrine/dbal/issues/4749',
1112
            'PostgreSQLPlatform::getName() is deprecated. Identify platforms by their class.',
1113
        );
1114
 
1115
        return 'postgresql';
1116
    }
1117
 
1118
    /**
1119
     * {@inheritDoc}
1120
     */
1121
    public function getDateTimeTzFormatString()
1122
    {
1123
        return 'Y-m-d H:i:sO';
1124
    }
1125
 
1126
    /**
1127
     * {@inheritDoc}
1128
     */
1129
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
1130
    {
1131
        return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (DEFAULT)';
1132
    }
1133
 
1134
    /**
1135
     * {@inheritDoc}
1136
     */
1137
    public function getTruncateTableSQL($tableName, $cascade = false)
1138
    {
1139
        $tableIdentifier = new Identifier($tableName);
1140
        $sql             = 'TRUNCATE ' . $tableIdentifier->getQuotedName($this);
1141
 
1142
        if ($cascade) {
1143
            $sql .= ' CASCADE';
1144
        }
1145
 
1146
        return $sql;
1147
    }
1148
 
1149
    /**
1150
     * {@inheritDoc}
1151
     */
1152
    public function getReadLockSQL()
1153
    {
1154
        return 'FOR SHARE';
1155
    }
1156
 
1157
    /**
1158
     * {@inheritDoc}
1159
     */
1160
    protected function initializeDoctrineTypeMappings()
1161
    {
1162
        $this->doctrineTypeMapping = [
1163
            'bigint'           => 'bigint',
1164
            'bigserial'        => 'bigint',
1165
            'bool'             => 'boolean',
1166
            'boolean'          => 'boolean',
1167
            'bpchar'           => 'string',
1168
            'bytea'            => 'blob',
1169
            'char'             => 'string',
1170
            'date'             => 'date',
1171
            'datetime'         => 'datetime',
1172
            'decimal'          => 'decimal',
1173
            'double'           => 'float',
1174
            'double precision' => 'float',
1175
            'float'            => 'float',
1176
            'float4'           => 'float',
1177
            'float8'           => 'float',
1178
            'inet'             => 'string',
1179
            'int'              => 'integer',
1180
            'int2'             => 'smallint',
1181
            'int4'             => 'integer',
1182
            'int8'             => 'bigint',
1183
            'integer'          => 'integer',
1184
            'interval'         => 'string',
1185
            'json'             => 'json',
1186
            'jsonb'            => 'json',
1187
            'money'            => 'decimal',
1188
            'numeric'          => 'decimal',
1189
            'serial'           => 'integer',
1190
            'serial4'          => 'integer',
1191
            'serial8'          => 'bigint',
1192
            'real'             => 'float',
1193
            'smallint'         => 'smallint',
1194
            'text'             => 'text',
1195
            'time'             => 'time',
1196
            'timestamp'        => 'datetime',
1197
            'timestamptz'      => 'datetimetz',
1198
            'timetz'           => 'time',
1199
            'tsvector'         => 'text',
1200
            'uuid'             => 'guid',
1201
            'varchar'          => 'string',
1202
            'year'             => 'date',
1203
            '_varchar'         => 'string',
1204
        ];
1205
    }
1206
 
1207
    /**
1208
     * {@inheritDoc}
1209
     *
1210
     * @deprecated
1211
     */
1212
    public function getVarcharMaxLength()
1213
    {
1214
        Deprecation::triggerIfCalledFromOutside(
1215
            'doctrine/dbal',
1216
            'https://github.com/doctrine/dbal/issues/3263',
1217
            'PostgreSQLPlatform::getVarcharMaxLength() is deprecated.',
1218
        );
1219
 
1220
        return 65535;
1221
    }
1222
 
1223
    /**
1224
     * {@inheritdoc}
1225
     */
1226
    public function getBinaryMaxLength()
1227
    {
1228
        Deprecation::triggerIfCalledFromOutside(
1229
            'doctrine/dbal',
1230
            'https://github.com/doctrine/dbal/issues/3263',
1231
            'PostgreSQLPlatform::getBinaryMaxLength() is deprecated.',
1232
        );
1233
 
1234
        return 0;
1235
    }
1236
 
1237
    /**
1238
     * {@inheritdoc}
1239
     *
1240
     * @deprecated
1241
     */
1242
    public function getBinaryDefaultLength()
1243
    {
1244
        Deprecation::trigger(
1245
            'doctrine/dbal',
1246
            'https://github.com/doctrine/dbal/issues/3263',
1247
            'Relying on the default binary column length is deprecated, specify the length explicitly.',
1248
        );
1249
 
1250
        return 0;
1251
    }
1252
 
1253
    /**
1254
     * {@inheritdoc}
1255
     *
1256
     * @deprecated
1257
     */
1258
    public function hasNativeJsonType()
1259
    {
1260
        Deprecation::triggerIfCalledFromOutside(
1261
            'doctrine/dbal',
1262
            'https://github.com/doctrine/dbal/pull/5509',
1263
            '%s is deprecated.',
1264
            __METHOD__,
1265
        );
1266
 
1267
        return true;
1268
    }
1269
 
1270
    /**
1271
     * {@inheritDoc}
1272
     *
1273
     * @deprecated Implement {@see createReservedKeywordsList()} instead.
1274
     */
1275
    protected function getReservedKeywordsClass()
1276
    {
1277
        Deprecation::triggerIfCalledFromOutside(
1278
            'doctrine/dbal',
1279
            'https://github.com/doctrine/dbal/issues/4510',
1280
            'PostgreSQLPlatform::getReservedKeywordsClass() is deprecated,'
1281
                . ' use PostgreSQLPlatform::createReservedKeywordsList() instead.',
1282
        );
1283
 
1284
        return Keywords\PostgreSQL94Keywords::class;
1285
    }
1286
 
1287
    /**
1288
     * {@inheritDoc}
1289
     */
1290
    public function getBlobTypeDeclarationSQL(array $column)
1291
    {
1292
        return 'BYTEA';
1293
    }
1294
 
1295
    /**
1296
     * {@inheritdoc}
1297
     *
1298
     * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy.
1299
     */
1300
    public function getDefaultValueDeclarationSQL($column)
1301
    {
1302
        if (isset($column['autoincrement']) && $column['autoincrement'] === true) {
1303
            return '';
1304
        }
1305
 
1306
        return parent::getDefaultValueDeclarationSQL($column);
1307
    }
1308
 
1309
    /**
1310
     * {@inheritdoc}
1311
     *
1312
     * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy.
1313
     */
1314
    public function supportsColumnCollation()
1315
    {
1316
        return true;
1317
    }
1318
 
1319
    /**
1320
     * {@inheritdoc}
1321
     */
1322
    public function getJsonTypeDeclarationSQL(array $column)
1323
    {
1324
        if (! empty($column['jsonb'])) {
1325
            return 'JSONB';
1326
        }
1327
 
1328
        return 'JSON';
1329
    }
1330
 
1331
    private function getOldColumnComment(ColumnDiff $columnDiff): ?string
1332
    {
1333
        $oldColumn = $columnDiff->getOldColumn();
1334
 
1335
        if ($oldColumn !== null) {
1336
            return $this->getColumnComment($oldColumn);
1337
        }
1338
 
1339
        return null;
1340
    }
1341
 
1342
    /** @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. */
1343
    public function getListTableMetadataSQL(string $table, ?string $schema = null): string
1344
    {
1345
        if ($schema !== null) {
1346
            $table = $schema . '.' . $table;
1347
        }
1348
 
1349
        return sprintf(
1350
            <<<'SQL'
1351
SELECT obj_description(%s::regclass) AS table_comment;
1352
SQL
1353
            ,
1354
            $this->quoteStringLiteral($table),
1355
        );
1356
    }
1357
 
1358
    public function createSchemaManager(Connection $connection): PostgreSQLSchemaManager
1359
    {
1360
        return new PostgreSQLSchemaManager($connection, $this);
1361
    }
1362
}