Completed
Pull Request — develop (#3445)
by Evgeniy
64:46
created

PostgreSqlPlatform::escapeStringForObject()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 2
1
<?php
2
3
namespace Doctrine\DBAL\Platforms;
4
5
use Doctrine\DBAL\Schema\Column;
6
use Doctrine\DBAL\Schema\ColumnDiff;
7
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
8
use Doctrine\DBAL\Schema\Identifier;
9
use Doctrine\DBAL\Schema\Index;
10
use Doctrine\DBAL\Schema\Sequence;
11
use Doctrine\DBAL\Schema\TableDiff;
12
use Doctrine\DBAL\Types\BigIntType;
13
use Doctrine\DBAL\Types\BinaryType;
14
use Doctrine\DBAL\Types\BlobType;
15
use Doctrine\DBAL\Types\IntegerType;
16
use Doctrine\DBAL\Types\Type;
17
use UnexpectedValueException;
18
use function array_diff;
19
use function array_merge;
20
use function array_unique;
21
use function array_values;
22
use function count;
23
use function explode;
24
use function implode;
25
use function in_array;
26
use function is_array;
27
use function is_bool;
28
use function is_numeric;
29
use function is_string;
30
use function pg_escape_bytea;
31
use function sprintf;
32
use function strpos;
33
use function strtolower;
34
use function trim;
35
36
/**
37
 * Provides the behavior, features and SQL dialect of the PostgreSQL 9.4+ database platform.
38
 *
39
 * @todo   Rename: PostgreSQLPlatform
40
 */
41
class PostgreSqlPlatform extends AbstractPlatform
42
{
43
    /** @var bool */
44
    private $useBooleanTrueFalseStrings = true;
45
46
    /** @var string[][] PostgreSQL booleans literals */
47
    private $booleanLiterals = [
48
        'true' => [
49
            't',
50
            'true',
51
            'y',
52
            'yes',
53
            'on',
54
            '1',
55
        ],
56
        'false' => [
57
            'f',
58
            'false',
59
            'n',
60
            'no',
61
            'off',
62
            '0',
63
        ],
64
    ];
65
66
    /**
67
     * PostgreSQL has different behavior with some drivers
68
     * with regard to how booleans have to be handled.
69
     *
70
     * Enables use of 'true'/'false' or otherwise 1 and 0 instead.
71
     *
72
     * @param bool $flag
73 138
     */
74
    public function setUseBooleanTrueFalseStrings($flag)
75 138
    {
76 138
        $this->useBooleanTrueFalseStrings = (bool) $flag;
77
    }
78
79
    /**
80
     * {@inheritDoc}
81 75
     */
82
    public function getSubstringExpression($value, $from, $length = null)
83 75
    {
84 75
        if ($length === null) {
85
            return 'SUBSTRING(' . $value . ' FROM ' . $from . ')';
86
        }
87 69
88
        return 'SUBSTRING(' . $value . ' FROM ' . $from . ' FOR ' . $length . ')';
89
    }
90
91
    /**
92
     * {@inheritDoc}
93
     */
94
    public function getNowExpression()
95
    {
96
        return 'LOCALTIMESTAMP(0)';
97
    }
98
99
    /**
100
     * {@inheritDoc}
101 69
     */
102
    public function getRegexpExpression()
103 69
    {
104
        return 'SIMILAR TO';
105
    }
106
107
    /**
108
     * {@inheritDoc}
109 6
     */
110
    public function getLocateExpression($str, $substr, $startPos = false)
111 6
    {
112 6
        if ($startPos !== false) {
113
            $str = $this->getSubstringExpression($str, $startPos);
114 6
115
            return 'CASE WHEN (POSITION(' . $substr . ' IN ' . $str . ') = 0) THEN 0 ELSE (POSITION(' . $substr . ' IN ' . $str . ') + ' . ($startPos-1) . ') END';
116
        }
117 6
118
        return 'POSITION(' . $substr . ' IN ' . $str . ')';
119
    }
120
121
    /**
122
     * {@inheritdoc}
123 6
     */
124
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
125 6
    {
126 6
        if ($unit === DateIntervalUnit::QUARTER) {
127 6
            $interval *= 3;
128
            $unit      = DateIntervalUnit::MONTH;
129
        }
130 6
131
        return '(' . $date . ' ' . $operator . ' (' . $interval . " || ' " . $unit . "')::interval)";
132
    }
133
134
    /**
135
     * {@inheritDoc}
136 18
     */
137
    public function getDateDiffExpression($date1, $date2)
138 18
    {
139
        return '(DATE(' . $date1 . ')-DATE(' . $date2 . '))';
140
    }
141
142
    /**
143
     * {@inheritDoc}
144 111
     */
145
    public function supportsSequences()
146 111
    {
147
        return true;
148
    }
149
150
    /**
151
     * {@inheritDoc}
152 24
     */
153
    public function supportsSchemas()
154 24
    {
155
        return true;
156
    }
157
158
    /**
159
     * {@inheritdoc}
160 6
     */
161
    public function getDefaultSchemaName()
162 6
    {
163
        return 'public';
164
    }
165
166
    /**
167
     * {@inheritDoc}
168 87
     */
169
    public function supportsIdentityColumns()
170 87
    {
171
        return true;
172
    }
173
174
    /**
175
     * {@inheritdoc}
176 660
     */
177
    public function supportsPartialIndexes()
178 660
    {
179
        return true;
180
    }
181
182
    /**
183
     * {@inheritdoc}
184 75
     */
185
    public function usesSequenceEmulatedIdentityColumns()
186 75
    {
187
        return true;
188
    }
189
190
    /**
191
     * {@inheritdoc}
192 81
     */
193
    public function getIdentitySequenceName($tableName, $columnName)
194 81
    {
195
        return $tableName . '_' . $columnName . '_seq';
196
    }
197
198
    /**
199
     * {@inheritDoc}
200 1993
     */
201
    public function supportsCommentOnStatement()
202 1993
    {
203
        return true;
204
    }
205
206
    /**
207
     * {@inheritDoc}
208 69
     */
209
    public function prefersSequences()
210 69
    {
211
        return true;
212
    }
213
214
    /**
215
     * {@inheritDoc}
216 3498
     */
217
    public function hasNativeGuidType()
218 3498
    {
219
        return true;
220
    }
221
222
    /**
223
     * {@inheritDoc}
224 12
     */
225
    public function getListDatabasesSQL()
226 12
    {
227
        return 'SELECT datname FROM pg_database';
228
    }
229
230
    /**
231
     * {@inheritDoc}
232 12
     */
233
    public function getListNamespacesSQL()
234 12
    {
235
        return "SELECT schema_name AS nspname
236
                FROM   information_schema.schemata
237
                WHERE  schema_name NOT LIKE 'pg\_%'
238
                AND    schema_name != 'information_schema'";
239
    }
240
241
    /**
242
     * {@inheritDoc}
243 24
     */
244
    public function getListSequencesSQL($database)
245 24
    {
246
        return "SELECT sequence_name AS relname,
247
                       sequence_schema AS schemaname
248
                FROM   information_schema.sequences
249
                WHERE  sequence_schema NOT LIKE 'pg\_%'
250
                AND    sequence_schema != 'information_schema'";
251
    }
252
253
    /**
254
     * {@inheritDoc}
255 526
     */
256
    public function getListTablesSQL()
257 526
    {
258
        return "SELECT quote_ident(table_name) AS table_name,
259
                       table_schema AS schema_name
260
                FROM   information_schema.tables
261
                WHERE  table_schema NOT LIKE 'pg\_%'
262
                AND    table_schema != 'information_schema'
263
                AND    table_name != 'geometry_columns'
264
                AND    table_name != 'spatial_ref_sys'
265
                AND    table_type != 'VIEW'";
266
    }
267
268
    /**
269
     * {@inheritDoc}
270 6
     */
271
    public function getListViewsSQL($database)
272 6
    {
273
        return 'SELECT quote_ident(table_name) AS viewname,
274
                       table_schema AS schemaname,
275
                       view_definition AS definition
276
                FROM   information_schema.views
277
                WHERE  view_definition IS NOT NULL';
278
    }
279
280
    /**
281
     * {@inheritDoc}
282 402
     */
283
    public function getListTableForeignKeysSQL($table, $database = null)
0 ignored issues
show
Unused Code introduced by
The parameter $database is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

283
    public function getListTableForeignKeysSQL($table, /** @scrutinizer ignore-unused */ $database = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
284
    {
285
        return 'SELECT quote_ident(r.conname) as conname, pg_catalog.pg_get_constraintdef(r.oid, true) as condef
286
                  FROM pg_catalog.pg_constraint r
287
                  WHERE r.conrelid =
288
                  (
289
                      SELECT c.oid
290 402
                      FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n
291
                      WHERE ' . $this->getTableWhereClause($table) . " AND n.oid = c.relnamespace
292
                  )
293
                  AND r.contype = 'f'";
294
    }
295
296
    /**
297
     * {@inheritDoc}
298 12
     */
299
    public function getCreateViewSQL($name, $sql)
300 12
    {
301
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
302
    }
303
304
    /**
305
     * {@inheritDoc}
306 12
     */
307
    public function getDropViewSQL($name)
308 12
    {
309
        return 'DROP VIEW ' . $name;
310
    }
311
312
    /**
313
     * {@inheritDoc}
314 69
     */
315
    public function getListTableConstraintsSQL($table)
316 69
    {
317 69
        $table = new Identifier($table);
318
        $table = $this->quoteStringLiteral($table->getName());
319 69
320
        return sprintf(
321 69
            <<<'SQL'
322
SELECT
323
    quote_ident(relname) as relname
324
FROM
325
    pg_class
326
WHERE oid IN (
327
    SELECT indexrelid
328
    FROM pg_index, pg_class
329
    WHERE pg_class.relname = %s
330
        AND pg_class.oid = pg_index.indrelid
331
        AND (indisunique = 't' OR indisprimary = 't')
332
    )
333
SQL
334 69
            ,
335
            $table
336
        );
337
    }
338
339
    /**
340
     * {@inheritDoc}
341
     *
342
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
343 414
     */
344
    public function getListTableIndexesSQL($table, $currentDatabase = null)
345
    {
346
        return 'SELECT quote_ident(relname) as relname, pg_index.indisunique, pg_index.indisprimary,
347
                       pg_index.indkey, pg_index.indrelid,
348
                       pg_get_expr(indpred, indrelid) AS where
349
                 FROM pg_class, pg_index
350
                 WHERE oid IN (
351
                    SELECT indexrelid
352 414
                    FROM pg_index si, pg_class sc, pg_namespace sn
353
                    WHERE ' . $this->getTableWhereClause($table, 'sc', 'sn') . ' AND sc.oid=si.indrelid AND sc.relnamespace = sn.oid
354
                 ) AND pg_index.indexrelid = oid';
355
    }
356
357
    /**
358
     * @param string $table
359
     * @param string $classAlias
360
     * @param string $namespaceAlias
361
     *
362
     * @return string
363 796
     */
364
    private function getTableWhereClause($table, $classAlias = 'c', $namespaceAlias = 'n')
365 796
    {
366 796
        $whereClause = $namespaceAlias . ".nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') AND ";
367 237
        if (strpos($table, '.') !== false) {
368 237
            [$schema, $table] = explode('.', $table);
369
            $schema           = $this->quoteStringLiteral($schema);
370 583
        } else {
371
            $schema = "ANY(string_to_array((select replace(replace(setting,'\"\$user\"',user),' ','') from pg_catalog.pg_settings where name = 'search_path'),','))";
372
        }
373 796
374 796
        $table = new Identifier($table);
375
        $table = $this->quoteStringLiteral($table->getName());
376 796
377 796
        return $whereClause . sprintf(
378 796
            '%s.relname = %s AND %s.nspname = %s',
379 796
            $classAlias,
380 796
            $table,
381 796
            $namespaceAlias,
382
            $schema
383
        );
384
    }
385
386
    /**
387
     * {@inheritDoc}
388 484
     */
389
    public function getListTableColumnsSQL($table, $database = null)
390
    {
391
        return "SELECT
392
                    a.attnum,
393
                    quote_ident(a.attname) AS field,
394
                    t.typname AS type,
395
                    format_type(a.atttypid, a.atttypmod) AS complete_type,
396
                    (SELECT tc.collcollate FROM pg_catalog.pg_collation tc WHERE tc.oid = a.attcollation) AS collation,
397
                    (SELECT t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type,
398
                    (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM
399
                      pg_catalog.pg_type t2 WHERE t2.typtype = 'd' AND t2.oid = a.atttypid) AS domain_complete_type,
400
                    a.attnotnull AS isnotnull,
401
                    (SELECT 't'
402
                     FROM pg_index
403
                     WHERE c.oid = pg_index.indrelid
404
                        AND pg_index.indkey[0] = a.attnum
405
                        AND pg_index.indisprimary = 't'
406
                    ) AS pri,
407
                    (SELECT pg_get_expr(adbin, adrelid)
408
                     FROM pg_attrdef
409
                     WHERE c.oid = pg_attrdef.adrelid
410
                        AND pg_attrdef.adnum=a.attnum
411
                    ) AS default,
412
                    (SELECT pg_description.description
413
                        FROM pg_description WHERE pg_description.objoid = c.oid AND a.attnum = pg_description.objsubid
414
                    ) AS comment
415 484
                    FROM pg_attribute a, pg_class c, pg_type t, pg_namespace n
416
                    WHERE " . $this->getTableWhereClause($table, 'c', 'n') . '
417
                        AND a.attnum > 0
418
                        AND a.attrelid = c.oid
419
                        AND a.atttypid = t.oid
420
                        AND n.oid = c.relnamespace
421
                    ORDER BY a.attnum';
422
    }
423
424
    /**
425
     * {@inheritDoc}
426 81
     */
427
    public function getCreateDatabaseSQL($name)
428 81
    {
429
        return 'CREATE DATABASE ' . $name;
430
    }
431
432
    /**
433
     * Returns the SQL statement for disallowing new connections on the given database.
434
     *
435
     * This is useful to force DROP DATABASE operations which could fail because of active connections.
436
     *
437
     * @param string $database The name of the database to disallow new connections for.
438
     *
439
     * @return string
440 75
     */
441
    public function getDisallowDatabaseConnectionsSQL($database)
442 75
    {
443
        return "UPDATE pg_database SET datallowconn = 'false' WHERE datname = " . $this->quoteStringLiteral($database);
444
    }
445
446
    /**
447
     * Returns the SQL statement for closing currently active connections on the given database.
448
     *
449
     * This is useful to force DROP DATABASE operations which could fail because of active connections.
450
     *
451
     * @param string $database The name of the database to close currently active connections for.
452
     *
453
     * @return string
454 144
     */
455
    public function getCloseActiveDatabaseConnectionsSQL($database)
456
    {
457 144
        return 'SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '
458
            . $this->quoteStringLiteral($database);
459
    }
460
461
    /**
462
     * {@inheritDoc}
463 435
     */
464
    public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey)
465 435
    {
466
        $query = '';
467 435
468 69
        if ($foreignKey->hasOption('match')) {
469
            $query .= ' MATCH ' . $foreignKey->getOption('match');
470
        }
471 435
472
        $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey);
473 435
474 69
        if ($foreignKey->hasOption('deferrable') && $foreignKey->getOption('deferrable') !== false) {
475
            $query .= ' DEFERRABLE';
476 435
        } else {
477
            $query .= ' NOT DEFERRABLE';
478
        }
479 435
480 435
        if (($foreignKey->hasOption('feferred') && $foreignKey->getOption('feferred') !== false)
481
            || ($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false)
482 69
        ) {
483
            $query .= ' INITIALLY DEFERRED';
484 435
        } else {
485
            $query .= ' INITIALLY IMMEDIATE';
486
        }
487 435
488
        return $query;
489
    }
490
491
    /**
492
     * {@inheritDoc}
493 1425
     */
494
    public function getAlterTableSQL(TableDiff $diff)
495 1425
    {
496 1425
        $sql         = [];
497 1425
        $commentsSQL = [];
498
        $columnSql   = [];
499 1425
500 282
        foreach ($diff->addedColumns as $column) {
501
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
502
                continue;
503
            }
504 282
505 282
            $query = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray());
506
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
507 282
508
            $comment = $this->getColumnComment($column);
509 282
510 213
            if ($comment === null || $comment === '') {
511
                continue;
512
            }
513 69
514 69
            $commentsSQL[] = $this->getCommentOnColumnSQL(
515 69
                $diff->getName($this)->getQuotedName($this),
516 69
                $column->getQuotedName($this),
517
                $comment
518
            );
519
        }
520 1425
521 282
        foreach ($diff->removedColumns as $column) {
522
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
523
                continue;
524
            }
525 282
526 282
            $query = 'DROP ' . $column->getQuotedName($this);
527
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
528
        }
529 1425
530
        foreach ($diff->changedColumns as $columnDiff) {
531 855
            /** @var $columnDiff \Doctrine\DBAL\Schema\ColumnDiff */
532
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
533
                continue;
534
            }
535 855
536 69
            if ($this->isUnchangedBinaryColumn($columnDiff)) {
537
                continue;
538
            }
539 786
540 786
            $oldColumnName = $columnDiff->getOldColumnName()->getQuotedName($this);
541
            $column        = $columnDiff->column;
542 786
543 363
            if ($columnDiff->hasChanged('type') || $columnDiff->hasChanged('precision') || $columnDiff->hasChanged('scale') || $columnDiff->hasChanged('fixed')) {
544
                $type = $column->getType();
545
546 363
                // SERIAL/BIGSERIAL are not "real" types and we can't alter a column to that type
547 363
                $columnDefinition                  = $column->toArray();
548
                $columnDefinition['autoincrement'] = false;
549
550 363
                // here was a server version check before, but DBAL API does not support this anymore.
551 363
                $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $type->getSQLDeclaration($columnDefinition, $this);
552
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
553
            }
554 786
555 219
            if ($columnDiff->hasChanged('default') || $this->typeChangeBreaksDefaultValue($columnDiff)) {
556 144
                $defaultClause = $column->getDefault() === null
557 219
                    ? ' DROP DEFAULT'
558 219
                    : ' SET' . $this->getDefaultValueDeclarationSQL($column->toArray());
559 219
                $query         = 'ALTER ' . $oldColumnName . $defaultClause;
560
                $sql[]         = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
561
            }
562 786
563 138
            if ($columnDiff->hasChanged('notnull')) {
564 138
                $query = 'ALTER ' . $oldColumnName . ' ' . ($column->getNotnull() ? 'SET' : 'DROP') . ' NOT NULL';
565
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
566
            }
567 786
568 12
            if ($columnDiff->hasChanged('autoincrement')) {
569
                if ($column->getAutoincrement()) {
570 6
                    // add autoincrement
571
                    $seqName = $this->getIdentitySequenceName($diff->name, $oldColumnName);
572 6
573 6
                    $sql[] = 'CREATE SEQUENCE ' . $seqName;
574 6
                    $sql[] = "SELECT setval('" . $seqName . "', (SELECT MAX(" . $oldColumnName . ') FROM ' . $diff->getName($this)->getQuotedName($this) . '))';
575 6
                    $query = 'ALTER ' . $oldColumnName . " SET DEFAULT nextval('" . $seqName . "')";
576
                    $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
577
                } else {
578 6
                    // Drop autoincrement, but do NOT drop the sequence. It might be re-used by other tables or have
579 6
                    $query = 'ALTER ' . $oldColumnName . ' DROP DEFAULT';
580
                    $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
581
                }
582
            }
583 786
584 786
            $newComment = $this->getColumnComment($column);
585
            $oldComment = $this->getOldColumnComment($columnDiff);
586 786
587 267
            if ($columnDiff->hasChanged('comment') || ($columnDiff->fromColumn !== null && $oldComment !== $newComment)) {
588 267
                $commentsSQL[] = $this->getCommentOnColumnSQL(
589 267
                    $diff->getName($this)->getQuotedName($this),
590 267
                    $column->getQuotedName($this),
591
                    $newComment
592
                );
593
            }
594 786
595 717
            if (! $columnDiff->hasChanged('length')) {
596
                continue;
597
            }
598 69
599 69
            $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $column->getType()->getSQLDeclaration($column->toArray(), $this);
600
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
601
        }
602 1425
603 282
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
604
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
605
                continue;
606
            }
607 282
608
            $oldColumnName = new Identifier($oldColumnName);
609 282
610 282
            $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) .
611
                ' RENAME COLUMN ' . $oldColumnName->getQuotedName($this) . ' TO ' . $column->getQuotedName($this);
612
        }
613 1425
614
        $tableSql = [];
615 1425
616 1425
        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
617
            $sql = array_merge($sql, $commentsSQL);
618 1425
619 138
            if ($diff->newName !== false) {
620
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' RENAME TO ' . $diff->getNewName()->getQuotedName($this);
621
            }
622 1425
623 1425
            $sql = array_merge(
624 1425
                $this->getPreAlterTableIndexForeignKeySQL($diff),
625 1425
                $sql,
626
                $this->getPostAlterTableIndexForeignKeySQL($diff)
627
            );
628
        }
629 1425
630
        return array_merge($sql, $tableSql, $columnSql);
631
    }
632
633
    /**
634
     * Checks whether a given column diff is a logically unchanged binary type column.
635
     *
636
     * Used to determine whether a column alteration for a binary type column can be skipped.
637
     * Doctrine's {@link \Doctrine\DBAL\Types\BinaryType} and {@link \Doctrine\DBAL\Types\BlobType}
638
     * are mapped to the same database column type on this platform as this platform
639
     * does not have a native VARBINARY/BINARY column type. Therefore the {@link \Doctrine\DBAL\Schema\Comparator}
640
     * might detect differences for binary type columns which do not have to be propagated
641
     * to database as there actually is no difference at database level.
642
     *
643
     * @param ColumnDiff $columnDiff The column diff to check against.
644
     *
645
     * @return bool True if the given column diff is an unchanged binary type column, false otherwise.
646 855
     */
647
    private function isUnchangedBinaryColumn(ColumnDiff $columnDiff)
648 855
    {
649
        $columnType = $columnDiff->column->getType();
650 855
651 786
        if (! $columnType instanceof BinaryType && ! $columnType instanceof BlobType) {
652
            return false;
653
        }
654 69
655
        $fromColumn = $columnDiff->fromColumn instanceof Column ? $columnDiff->fromColumn : null;
0 ignored issues
show
introduced by
$columnDiff->fromColumn is always a sub-type of Doctrine\DBAL\Schema\Column.
Loading history...
656 69
657 69
        if ($fromColumn) {
0 ignored issues
show
introduced by
$fromColumn is of type Doctrine\DBAL\Schema\Column, thus it always evaluated to true.
Loading history...
658
            $fromColumnType = $fromColumn->getType();
659 69
660
            if (! $fromColumnType instanceof BinaryType && ! $fromColumnType instanceof BlobType) {
661
                return false;
662
            }
663 69
664
            return count(array_diff($columnDiff->changedProperties, ['type', 'length', 'fixed'])) === 0;
665
        }
666
667
        if ($columnDiff->hasChanged('type')) {
668
            return false;
669
        }
670
671
        return count(array_diff($columnDiff->changedProperties, ['length', 'fixed'])) === 0;
672
    }
673
674
    /**
675
     * {@inheritdoc}
676 357
     */
677
    protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
678 357
    {
679 138
        if (strpos($tableName, '.') !== false) {
680 138
            [$schema]     = explode('.', $tableName);
681
            $oldIndexName = $schema . '.' . $oldIndexName;
682
        }
683 357
684
        return ['ALTER INDEX ' . $oldIndexName . ' RENAME TO ' . $index->getQuotedName($this)];
685
    }
686
687
    /**
688
     * {@inheritdoc}
689 806
     */
690
    public function getCommentOnColumnSQL($tableName, $columnName, $comment)
691 806
    {
692 806
        $tableName  = new Identifier($tableName);
693 806
        $columnName = new Identifier($columnName);
694
        $comment    = $comment === null ? 'NULL' : $this->quoteStringLiteral($comment);
695 806
696 806
        return sprintf(
697 806
            'COMMENT ON COLUMN %s.%s IS %s',
698 806
            $tableName->getQuotedName($this),
699 806
            $columnName->getQuotedName($this),
700
            $comment
701
        );
702
    }
703
704
    /**
705
     * {@inheritDoc}
706 168
     */
707
    public function getCreateSequenceSQL(Sequence $sequence)
708 168
    {
709 168
        return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) .
710 168
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
711 168
            ' MINVALUE ' . $sequence->getInitialValue() .
712 168
            ' START ' . $sequence->getInitialValue() .
713
            $this->getSequenceCacheSQL($sequence);
714
    }
715
716
    /**
717
     * {@inheritDoc}
718
     */
719
    public function getAlterSequenceSQL(Sequence $sequence)
720
    {
721
        return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) .
722
            ' INCREMENT BY ' . $sequence->getAllocationSize() .
723
            $this->getSequenceCacheSQL($sequence);
724
    }
725
726
    /**
727
     * Cache definition for sequences
728
     *
729
     * @return string
730 168
     */
731
    private function getSequenceCacheSQL(Sequence $sequence)
732 168
    {
733 69
        if ($sequence->getCache() > 1) {
734
            return ' CACHE ' . $sequence->getCache();
735
        }
736 99
737
        return '';
738
    }
739
740
    /**
741
     * {@inheritDoc}
742 81
     */
743
    public function getDropSequenceSQL($sequence)
744 81
    {
745
        if ($sequence instanceof Sequence) {
746
            $sequence = $sequence->getQuotedName($this);
747
        }
748 81
749
        return 'DROP SEQUENCE ' . $sequence . ' CASCADE';
750
    }
751
752
    /**
753
     * {@inheritDoc}
754 81
     */
755
    public function getCreateSchemaSQL($schemaName)
756 81
    {
757
        return 'CREATE SCHEMA ' . $schemaName;
758
    }
759
760
    /**
761
     * {@inheritDoc}
762 213
     */
763
    public function getDropForeignKeySQL($foreignKey, $table)
764 213
    {
765
        return $this->getDropConstraintSQL($foreignKey, $table);
766
    }
767
768
    /**
769
     * {@inheritDoc}
770 1924
     */
771
    protected function _getCreateTableSQL($tableName, array $columns, array $options = [])
772 1924
    {
773
        $queryFields = $this->getColumnDeclarationListSQL($columns);
774 1924
775 888
        if (isset($options['primary']) && ! empty($options['primary'])) {
776 888
            $keyColumns   = array_unique(array_values($options['primary']));
777
            $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')';
778
        }
779 1924
780
        $query = 'CREATE TABLE ' . $tableName . ' (' . $queryFields . ')';
781 1924
782
        $sql = [$query];
783 1924
784 309
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
785 309
            foreach ($options['indexes'] as $index) {
786
                $sql[] = $this->getCreateIndexSQL($index, $tableName);
787
            }
788
        }
789 1924
790 985
        if (isset($options['foreignKeys'])) {
791 141
            foreach ((array) $options['foreignKeys'] as $definition) {
792
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
793
            }
794
        }
795 1924
796
        return $sql;
797
    }
798
799
    /**
800
     * Converts a single boolean value.
801
     *
802
     * First converts the value to its native PHP boolean type
803
     * and passes it to the given callback function to be reconverted
804
     * into any custom representation.
805
     *
806
     * @param mixed    $value    The value to convert.
807
     * @param callable $callback The callback function to use for converting the real boolean value.
808
     *
809
     * @return mixed
810
     *
811
     * @throws UnexpectedValueException
812 2268
     */
813
    private function convertSingleBooleanValue($value, $callback)
814 2268
    {
815 150
        if ($value === null) {
816
            return $callback(null);
817
        }
818 2118
819 1353
        if (is_bool($value) || is_numeric($value)) {
820
            return $callback($value ? true : false);
821
        }
822 765
823
        if (! is_string($value)) {
824
            return $callback(true);
825
        }
826
827
        /**
828
         * Better safe than sorry: http://php.net/in_array#106319
829 765
         */
830 351
        if (in_array(strtolower(trim($value)), $this->booleanLiterals['false'], true)) {
831
            return $callback(false);
832
        }
833 414
834 345
        if (in_array(strtolower(trim($value)), $this->booleanLiterals['true'], true)) {
835
            return $callback(true);
836
        }
837 69
838
        throw new UnexpectedValueException("Unrecognized boolean literal '${value}'");
839
    }
840
841
    /**
842
     * Converts one or multiple boolean values.
843
     *
844
     * First converts the value(s) to their native PHP boolean type
845
     * and passes them to the given callback function to be reconverted
846
     * into any custom representation.
847
     *
848
     * @param mixed    $item     The value(s) to convert.
849
     * @param callable $callback The callback function to use for converting the real boolean value(s).
850
     *
851
     * @return mixed
852 2172
     */
853
    private function doConvertBooleans($item, $callback)
854 2268
    {
855
        if (is_array($item)) {
856
            foreach ($item as $key => $value) {
857
                $item[$key] = $this->convertSingleBooleanValue($value, $callback);
858
            }
859
860
            return $item;
861
        }
862 2268
863
        return $this->convertSingleBooleanValue($item, $callback);
864
    }
865
866
    /**
867
     * {@inheritDoc}
868
     *
869
     * Postgres wants boolean values converted to the strings 'true'/'false'.
870 1194
     */
871
    public function convertBooleans($item)
872 1248
    {
873 138
        if (! $this->useBooleanTrueFalseStrings) {
874
            return parent::convertBooleans($item);
875
        }
876 1110
877 1110
        return $this->doConvertBooleans(
878
            $item,
879 1110
            static function ($boolean) {
880 69
                if ($boolean === null) {
881
                    return 'NULL';
882
                }
883 1041
884 1110
                return $boolean === true ? 'true' : 'false';
885
            }
886
        );
887
    }
888
889
    /**
890
     * {@inheritDoc}
891 1176
     */
892
    public function convertBooleansToDatabaseValue($item)
893 1227
    {
894 69
        if (! $this->useBooleanTrueFalseStrings) {
895
            return parent::convertBooleansToDatabaseValue($item);
896
        }
897 1158
898 1158
        return $this->doConvertBooleans(
899
            $item,
900 1089
            static function ($boolean) {
901 1158
                return $boolean === null ? null : (int) $boolean;
902
            }
903
        );
904
    }
905
906
    /**
907
     * {@inheritDoc}
908 1002
     */
909
    public function convertFromBoolean($item)
910 1047
    {
911 414
        if (in_array(strtolower($item), $this->booleanLiterals['false'], true)) {
912
            return false;
913
        }
914 633
915
        return parent::convertFromBoolean($item);
916
    }
917
918
    /**
919
     * {@inheritDoc}
920 72
     */
921
    public function getSequenceNextValSQL($sequenceName)
922 75
    {
923
        return "SELECT NEXTVAL('" . $sequenceName . "')";
924
    }
925
926
    /**
927
     * {@inheritDoc}
928 66
     */
929
    public function getSetTransactionIsolationSQL($level)
930
    {
931 69
        return 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL '
932
            . $this->_getTransactionIsolationLevelSQL($level);
933
    }
934
935
    /**
936
     * {@inheritDoc}
937 162
     */
938
    public function getBooleanTypeDeclarationSQL(array $field)
939 165
    {
940
        return 'BOOLEAN';
941
    }
942
943
    /**
944
     * {@inheritDoc}
945 1626
     */
946
    public function getIntegerTypeDeclarationSQL(array $field)
947 1659
    {
948 513
        if (! empty($field['autoincrement'])) {
949
            return 'SERIAL';
950
        }
951 1335
952
        return 'INT';
953
    }
954
955
    /**
956
     * {@inheritDoc}
957 252
     */
958
    public function getBigIntTypeDeclarationSQL(array $field)
959 258
    {
960 156
        if (! empty($field['autoincrement'])) {
961
            return 'BIGSERIAL';
962
        }
963 102
964
        return 'BIGINT';
965
    }
966
967
    /**
968
     * {@inheritDoc}
969 78
     */
970
    public function getSmallIntTypeDeclarationSQL(array $field)
971 81
    {
972 69
        if (! empty($field['autoincrement'])) {
973
            return 'SMALLSERIAL';
974
        }
975 81
976
        return 'SMALLINT';
977
    }
978
979
    /**
980
     * {@inheritDoc}
981 66
     */
982
    public function getGuidTypeDeclarationSQL(array $field)
983 69
    {
984
        return 'UUID';
985
    }
986
987
    /**
988
     * {@inheritDoc}
989 198
     */
990
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
991 201
    {
992
        return 'TIMESTAMP(0) WITHOUT TIME ZONE';
993
    }
994
995
    /**
996
     * {@inheritDoc}
997 90
     */
998
    public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration)
999 90
    {
1000
        return 'TIMESTAMP(0) WITH TIME ZONE';
1001
    }
1002
1003
    /**
1004
     * {@inheritDoc}
1005 108
     */
1006
    public function getDateTypeDeclarationSQL(array $fieldDeclaration)
1007 108
    {
1008
        return 'DATE';
1009
    }
1010
1011
    /**
1012
     * {@inheritDoc}
1013 108
     */
1014
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration)
1015 108
    {
1016
        return 'TIME(0) WITHOUT TIME ZONE';
1017
    }
1018
1019
    /**
1020
     * {@inheritDoc}
1021
     */
1022
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
1023
    {
1024
        return '';
1025
    }
1026
1027
    /**
1028
     * {@inheritDoc}
1029 1254
     */
1030
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
1031 1290
    {
1032 1290
        return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)')
1033
            : ($length ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)');
1034
    }
1035
1036
    /**
1037
     * {@inheritdoc}
1038 78
     */
1039
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
1040 81
    {
1041
        return 'BYTEA';
1042
    }
1043
1044
    /**
1045
     * {@inheritDoc}
1046 348
     */
1047
    public function getClobTypeDeclarationSQL(array $field)
1048 351
    {
1049
        return 'TEXT';
1050
    }
1051
1052
    /**
1053
     * {@inheritDoc}
1054 754
     */
1055
    public function getObjectTypeDeclarationSQL(array $field)
1056 760
    {
1057
        return 'BYTEA';
1058
    }
1059
1060
    /**
1061
     * {@inheritDoc}
1062
     */
1063
    public function getName()
1064
    {
1065
        return 'postgresql';
1066
    }
1067
1068
    /**
1069
     * {@inheritDoc}
1070
     *
1071
     * PostgreSQL returns all column names in SQL result sets in lowercase.
1072 6
     */
1073
    public function getSQLResultCasing($column)
1074 6
    {
1075
        return strtolower($column);
1076
    }
1077
1078
    /**
1079
     * {@inheritDoc}
1080 6
     */
1081
    public function getDateTimeTzFormatString()
1082 6
    {
1083
        return 'Y-m-d H:i:sO';
1084
    }
1085
1086
    /**
1087
     * {@inheritDoc}
1088 120
     */
1089
    public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
1090 123
    {
1091 123
        return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (DEFAULT)';
1092
    }
1093 123
1094
    /**
1095
     * {@inheritDoc}
1096
     */
1097 123
    public function getTruncateTableSQL($tableName, $cascade = false)
1098
    {
1099
        $tableIdentifier = new Identifier($tableName);
1100
        $sql             = 'TRUNCATE ' . $tableIdentifier->getQuotedName($this);
1101
1102
        if ($cascade) {
1103
            $sql .= ' CASCADE';
1104
        }
1105
1106
        return $sql;
1107
    }
1108
1109
    /**
1110
     * {@inheritDoc}
1111 402
     */
1112
    public function getReadLockSQL()
1113 420
    {
1114 114
        return 'FOR SHARE';
1115 114
    }
1116 114
1117 114
    /**
1118 114
     * {@inheritDoc}
1119 114
     */
1120 114
    protected function initializeDoctrineTypeMappings()
1121 114
    {
1122 114
        $this->doctrineTypeMapping = [
1123 114
            'bigint'           => 'bigint',
1124 114
            'bigserial'        => 'bigint',
1125 114
            'bool'             => 'boolean',
1126 114
            'boolean'          => 'boolean',
1127 114
            'bpchar'           => 'string',
1128 114
            'bytea'            => 'blob',
1129 114
            'char'             => 'string',
1130 114
            'date'             => 'date',
1131 114
            'datetime'         => 'datetime',
1132 114
            'decimal'          => 'decimal',
1133 114
            'double'           => 'float',
1134 114
            'double precision' => 'float',
1135 114
            'float'            => 'float',
1136 114
            'float4'           => 'float',
1137 114
            'float8'           => 'float',
1138 114
            'inet'             => 'string',
1139 114
            'int'              => 'integer',
1140 114
            'int2'             => 'smallint',
1141 114
            'int4'             => 'integer',
1142 114
            'int8'             => 'bigint',
1143 114
            'integer'          => 'integer',
1144 114
            'interval'         => 'string',
1145 114
            'json'             => Type::JSON,
1146 114
            'money'            => 'decimal',
1147 114
            'numeric'          => 'decimal',
1148 114
            'serial'           => 'integer',
1149 114
            'serial4'          => 'integer',
1150 114
            'serial8'          => 'bigint',
1151 114
            'real'             => 'float',
1152 114
            'smallint'         => 'smallint',
1153 114
            'text'             => 'text',
1154
            'time'             => 'time',
1155 420
            'timestamp'        => 'datetime',
1156
            'timestamptz'      => 'datetimetz',
1157
            'timetz'           => 'time',
1158
            'tsvector'         => 'text',
1159
            'uuid'             => 'guid',
1160 1254
            'varchar'          => 'string',
1161
            'year'             => 'date',
1162 1290
            '_varchar'         => 'string',
1163
        ];
1164
    }
1165
1166
    /**
1167
     * {@inheritDoc}
1168 66
     */
1169
    public function getVarcharMaxLength()
1170 69
    {
1171
        return 65535;
1172
    }
1173
1174
    /**
1175
     * {@inheritdoc}
1176 138
     */
1177
    public function getBinaryMaxLength()
1178 144
    {
1179
        return 0;
1180
    }
1181
1182
    /**
1183
     * {@inheritdoc}
1184 3438
     */
1185
    public function getBinaryDefaultLength()
1186 3591
    {
1187
        return 0;
1188
    }
1189
1190
    /**
1191
     * {@inheritdoc}
1192 1152
     */
1193
    public function hasNativeJsonType()
1194 1204
    {
1195
        return true;
1196
    }
1197
1198
    /**
1199
     * {@inheritDoc}
1200 48
     */
1201
    protected function getReservedKeywordsClass()
1202 48
    {
1203
        return Keywords\PostgreSQLKeywords::class;
1204
    }
1205
1206
    /**
1207
     * {@inheritDoc}
1208 2542
     */
1209
    public function getBlobTypeDeclarationSQL(array $field)
1210 2614
    {
1211 738
        return 'BYTEA';
1212
    }
1213
1214 2071
    /**
1215
     * {@inheritdoc}
1216
     */
1217
    public function getDefaultValueDeclarationSQL($field)
1218
    {
1219
        if ($this->isSerialField($field)) {
1220
            return '';
1221
        }
1222
1223
        return parent::getDefaultValueDeclarationSQL($field);
1224
    }
1225
1226
    /**
1227
     * {@inheritdoc}
1228 66
     */
1229
    public function supportsColumnCollation()
1230 69
    {
1231
        return true;
1232
    }
1233
1234
    /**
1235
     * {@inheritdoc}
1236 38
     */
1237
    public function getColumnCollationDeclarationSQL($collation)
1238 39
    {
1239
        return 'COLLATE ' . $this->quoteSingleIdentifier($collation);
1240
    }
1241
1242
    /**
1243
     * {@inheritdoc}
1244 2542
     */
1245
    public function getJsonTypeDeclarationSQL(array $field)
1246 2614
    {
1247
        return 'JSON';
1248
    }
1249
1250
    /**
1251
     * @param mixed[] $field
1252
     */
1253 684
    private function isSerialField(array $field) : bool
1254
    {
1255 711
        return $field['autoincrement'] ?? false === true && isset($field['type'])
1256 345
            && $this->isNumericType($field['type']);
1257
    }
1258
1259 366
    /**
1260 366
     * Check whether the type of a column is changed in a way that invalidates the default value for the column
1261
     */
1262
    private function typeChangeBreaksDefaultValue(ColumnDiff $columnDiff) : bool
1263 366
    {
1264 366
        if (! $columnDiff->fromColumn) {
1265
            return $columnDiff->hasChanged('type');
1266
        }
1267 354
1268
        $oldTypeIsNumeric = $this->isNumericType($columnDiff->fromColumn->getType());
1269 366
        $newTypeIsNumeric = $this->isNumericType($columnDiff->column->getType());
1270
1271
        // default should not be changed when switching between numeric types and the default comes from a sequence
1272 756
        return $columnDiff->hasChanged('type')
1273
            && ! ($oldTypeIsNumeric && $newTypeIsNumeric && $columnDiff->column->getAutoincrement());
1274 786
    }
1275
1276
    private function isNumericType(Type $type) : bool
1277
    {
1278
        return $type instanceof IntegerType || $type instanceof BigIntType;
1279
    }
1280
1281
    private function getOldColumnComment(ColumnDiff $columnDiff) : ?string
1282
    {
1283
        return $columnDiff->fromColumn ? $this->getColumnComment($columnDiff->fromColumn) : null;
1284
    }
1285
1286
    /**
1287
     * {@inheritDoc}
1288
     */
1289
    public function escapeStringForObject($data)
1290
    {
1291
        return pg_escape_bytea($data);
0 ignored issues
show
Bug introduced by
The call to pg_escape_bytea() has too few arguments starting with data. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1291
        return /** @scrutinizer ignore-call */ pg_escape_bytea($data);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1292
    }
1293
}
1294