Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 148 | Details | Vergleich mit vorheriger | 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}
621 lars 960
     *
961
     * @param T $item
962
     *
963
     * @return (T is null ? null : bool)
964
     *
965
     * @template T
148 lars 966
     */
967
    public function convertFromBoolean($item)
968
    {
969
        if ($item !== null && in_array(strtolower($item), $this->booleanLiterals['false'], true)) {
970
            return false;
971
        }
972
 
973
        return parent::convertFromBoolean($item);
974
    }
975
 
976
    /**
977
     * {@inheritDoc}
978
     */
979
    public function getSequenceNextValSQL($sequence)
980
    {
981
        return "SELECT NEXTVAL('" . $sequence . "')";
982
    }
983
 
984
    /**
985
     * {@inheritDoc}
986
     */
987
    public function getSetTransactionIsolationSQL($level)
988
    {
989
        return 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL '
990
            . $this->_getTransactionIsolationLevelSQL($level);
991
    }
992
 
993
    /**
994
     * {@inheritDoc}
995
     */
996
    public function getBooleanTypeDeclarationSQL(array $column)
997
    {
998
        return 'BOOLEAN';
999
    }
1000
 
1001
    /**
1002
     * {@inheritDoc}
1003
     */
1004
    public function getIntegerTypeDeclarationSQL(array $column)
1005
    {
1006
        if (! empty($column['autoincrement'])) {
1007
            return 'SERIAL';
1008
        }
1009
 
1010
        return 'INT';
1011
    }
1012
 
1013
    /**
1014
     * {@inheritDoc}
1015
     */
1016
    public function getBigIntTypeDeclarationSQL(array $column)
1017
    {
1018
        if (! empty($column['autoincrement'])) {
1019
            return 'BIGSERIAL';
1020
        }
1021
 
1022
        return 'BIGINT';
1023
    }
1024
 
1025
    /**
1026
     * {@inheritDoc}
1027
     */
1028
    public function getSmallIntTypeDeclarationSQL(array $column)
1029
    {
1030
        if (! empty($column['autoincrement'])) {
1031
            return 'SMALLSERIAL';
1032
        }
1033
 
1034
        return 'SMALLINT';
1035
    }
1036
 
1037
    /**
1038
     * {@inheritDoc}
1039
     */
1040
    public function getGuidTypeDeclarationSQL(array $column)
1041
    {
1042
        return 'UUID';
1043
    }
1044
 
1045
    /**
1046
     * {@inheritDoc}
1047
     */
1048
    public function getDateTimeTypeDeclarationSQL(array $column)
1049
    {
1050
        return 'TIMESTAMP(0) WITHOUT TIME ZONE';
1051
    }
1052
 
1053
    /**
1054
     * {@inheritDoc}
1055
     */
1056
    public function getDateTimeTzTypeDeclarationSQL(array $column)
1057
    {
1058
        return 'TIMESTAMP(0) WITH TIME ZONE';
1059
    }
1060
 
1061
    /**
1062
     * {@inheritDoc}
1063
     */
1064
    public function getDateTypeDeclarationSQL(array $column)
1065
    {
1066
        return 'DATE';
1067
    }
1068
 
1069
    /**
1070
     * {@inheritDoc}
1071
     */
1072
    public function getTimeTypeDeclarationSQL(array $column)
1073
    {
1074
        return 'TIME(0) WITHOUT TIME ZONE';
1075
    }
1076
 
1077
    /**
1078
     * {@inheritDoc}
1079
     */
1080
    protected function _getCommonIntegerTypeDeclarationSQL(array $column)
1081
    {
1082
        return '';
1083
    }
1084
 
1085
    /**
1086
     * {@inheritDoc}
1087
     */
1088
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1089
    {
1090
        return $fixed ? ($length > 0 ? 'CHAR(' . $length . ')' : 'CHAR(255)')
1091
            : ($length > 0 ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)');
1092
    }
1093
 
1094
    /**
1095
     * {@inheritdoc}
1096
     */
1097
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
1098
    {
1099
        return 'BYTEA';
1100
    }
1101
 
1102
    /**
1103
     * {@inheritDoc}
1104
     */
1105
    public function getClobTypeDeclarationSQL(array $column)
1106
    {
1107
        return 'TEXT';
1108
    }
1109
 
1110
    /**
1111
     * {@inheritDoc}
1112
     */
1113
    public function getName()
1114
    {
1115
        Deprecation::triggerIfCalledFromOutside(
1116
            'doctrine/dbal',
1117
            'https://github.com/doctrine/dbal/issues/4749',
1118
            'PostgreSQLPlatform::getName() is deprecated. Identify platforms by their class.',
1119
        );
1120
 
1121
        return 'postgresql';
1122
    }
1123
 
1124
    /**
1125
     * {@inheritDoc}
1126
     */
1127
    public function getDateTimeTzFormatString()
1128
    {
1129
        return 'Y-m-d H:i:sO';
1130
    }
1131
 
1132
    /**
1133
     * {@inheritDoc}
1134
     */
1135
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
1136
    {
1137
        return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (DEFAULT)';
1138
    }
1139
 
1140
    /**
1141
     * {@inheritDoc}
1142
     */
1143
    public function getTruncateTableSQL($tableName, $cascade = false)
1144
    {
1145
        $tableIdentifier = new Identifier($tableName);
1146
        $sql             = 'TRUNCATE ' . $tableIdentifier->getQuotedName($this);
1147
 
1148
        if ($cascade) {
1149
            $sql .= ' CASCADE';
1150
        }
1151
 
1152
        return $sql;
1153
    }
1154
 
1155
    /**
1156
     * {@inheritDoc}
1157
     */
1158
    public function getReadLockSQL()
1159
    {
1160
        return 'FOR SHARE';
1161
    }
1162
 
1163
    /**
1164
     * {@inheritDoc}
1165
     */
1166
    protected function initializeDoctrineTypeMappings()
1167
    {
1168
        $this->doctrineTypeMapping = [
1169
            'bigint'           => 'bigint',
1170
            'bigserial'        => 'bigint',
1171
            'bool'             => 'boolean',
1172
            'boolean'          => 'boolean',
1173
            'bpchar'           => 'string',
1174
            'bytea'            => 'blob',
1175
            'char'             => 'string',
1176
            'date'             => 'date',
1177
            'datetime'         => 'datetime',
1178
            'decimal'          => 'decimal',
1179
            'double'           => 'float',
1180
            'double precision' => 'float',
1181
            'float'            => 'float',
1182
            'float4'           => 'float',
1183
            'float8'           => 'float',
1184
            'inet'             => 'string',
1185
            'int'              => 'integer',
1186
            'int2'             => 'smallint',
1187
            'int4'             => 'integer',
1188
            'int8'             => 'bigint',
1189
            'integer'          => 'integer',
1190
            'interval'         => 'string',
1191
            'json'             => 'json',
1192
            'jsonb'            => 'json',
1193
            'money'            => 'decimal',
1194
            'numeric'          => 'decimal',
1195
            'serial'           => 'integer',
1196
            'serial4'          => 'integer',
1197
            'serial8'          => 'bigint',
1198
            'real'             => 'float',
1199
            'smallint'         => 'smallint',
1200
            'text'             => 'text',
1201
            'time'             => 'time',
1202
            'timestamp'        => 'datetime',
1203
            'timestamptz'      => 'datetimetz',
1204
            'timetz'           => 'time',
1205
            'tsvector'         => 'text',
1206
            'uuid'             => 'guid',
1207
            'varchar'          => 'string',
1208
            'year'             => 'date',
1209
            '_varchar'         => 'string',
1210
        ];
1211
    }
1212
 
1213
    /**
1214
     * {@inheritDoc}
1215
     *
1216
     * @deprecated
1217
     */
1218
    public function getVarcharMaxLength()
1219
    {
1220
        Deprecation::triggerIfCalledFromOutside(
1221
            'doctrine/dbal',
1222
            'https://github.com/doctrine/dbal/issues/3263',
1223
            'PostgreSQLPlatform::getVarcharMaxLength() is deprecated.',
1224
        );
1225
 
1226
        return 65535;
1227
    }
1228
 
1229
    /**
1230
     * {@inheritdoc}
1231
     */
1232
    public function getBinaryMaxLength()
1233
    {
1234
        Deprecation::triggerIfCalledFromOutside(
1235
            'doctrine/dbal',
1236
            'https://github.com/doctrine/dbal/issues/3263',
1237
            'PostgreSQLPlatform::getBinaryMaxLength() is deprecated.',
1238
        );
1239
 
1240
        return 0;
1241
    }
1242
 
1243
    /**
1244
     * {@inheritdoc}
1245
     *
1246
     * @deprecated
1247
     */
1248
    public function getBinaryDefaultLength()
1249
    {
1250
        Deprecation::trigger(
1251
            'doctrine/dbal',
1252
            'https://github.com/doctrine/dbal/issues/3263',
1253
            'Relying on the default binary column length is deprecated, specify the length explicitly.',
1254
        );
1255
 
1256
        return 0;
1257
    }
1258
 
1259
    /**
1260
     * {@inheritdoc}
1261
     *
1262
     * @deprecated
1263
     */
1264
    public function hasNativeJsonType()
1265
    {
1266
        Deprecation::triggerIfCalledFromOutside(
1267
            'doctrine/dbal',
1268
            'https://github.com/doctrine/dbal/pull/5509',
1269
            '%s is deprecated.',
1270
            __METHOD__,
1271
        );
1272
 
1273
        return true;
1274
    }
1275
 
1276
    /**
1277
     * {@inheritDoc}
1278
     *
1279
     * @deprecated Implement {@see createReservedKeywordsList()} instead.
1280
     */
1281
    protected function getReservedKeywordsClass()
1282
    {
1283
        Deprecation::triggerIfCalledFromOutside(
1284
            'doctrine/dbal',
1285
            'https://github.com/doctrine/dbal/issues/4510',
1286
            'PostgreSQLPlatform::getReservedKeywordsClass() is deprecated,'
1287
                . ' use PostgreSQLPlatform::createReservedKeywordsList() instead.',
1288
        );
1289
 
1290
        return Keywords\PostgreSQL94Keywords::class;
1291
    }
1292
 
1293
    /**
1294
     * {@inheritDoc}
1295
     */
1296
    public function getBlobTypeDeclarationSQL(array $column)
1297
    {
1298
        return 'BYTEA';
1299
    }
1300
 
1301
    /**
1302
     * {@inheritdoc}
1303
     *
1304
     * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy.
1305
     */
1306
    public function getDefaultValueDeclarationSQL($column)
1307
    {
1308
        if (isset($column['autoincrement']) && $column['autoincrement'] === true) {
1309
            return '';
1310
        }
1311
 
1312
        return parent::getDefaultValueDeclarationSQL($column);
1313
    }
1314
 
1315
    /**
1316
     * {@inheritdoc}
1317
     *
1318
     * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy.
1319
     */
1320
    public function supportsColumnCollation()
1321
    {
1322
        return true;
1323
    }
1324
 
1325
    /**
1326
     * {@inheritdoc}
1327
     */
1328
    public function getJsonTypeDeclarationSQL(array $column)
1329
    {
1330
        if (! empty($column['jsonb'])) {
1331
            return 'JSONB';
1332
        }
1333
 
1334
        return 'JSON';
1335
    }
1336
 
1337
    private function getOldColumnComment(ColumnDiff $columnDiff): ?string
1338
    {
1339
        $oldColumn = $columnDiff->getOldColumn();
1340
 
1341
        if ($oldColumn !== null) {
1342
            return $this->getColumnComment($oldColumn);
1343
        }
1344
 
1345
        return null;
1346
    }
1347
 
1348
    /** @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. */
1349
    public function getListTableMetadataSQL(string $table, ?string $schema = null): string
1350
    {
1351
        if ($schema !== null) {
1352
            $table = $schema . '.' . $table;
1353
        }
1354
 
1355
        return sprintf(
1356
            <<<'SQL'
1357
SELECT obj_description(%s::regclass) AS table_comment;
1358
SQL
1359
            ,
1360
            $this->quoteStringLiteral($table),
1361
        );
1362
    }
1363
 
1364
    public function createSchemaManager(Connection $connection): PostgreSQLSchemaManager
1365
    {
1366
        return new PostgreSQLSchemaManager($connection, $this);
1367
    }
1368
}