AbstractPostgreSQLPlatformTestCase.php 32.7 KB
Newer Older
1 2
<?php

3
namespace Doctrine\DBAL\Tests\Platforms;
4

5
use Doctrine\DBAL\Platforms\AbstractPlatform;
6
use Doctrine\DBAL\Platforms\PostgreSQL94Platform;
7
use Doctrine\DBAL\Schema\Column;
Sergei Morozov's avatar
Sergei Morozov committed
8
use Doctrine\DBAL\Schema\ColumnDiff;
9
use Doctrine\DBAL\Schema\Comparator;
Sergei Morozov's avatar
Sergei Morozov committed
10 11
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
use Doctrine\DBAL\Schema\Sequence;
12 13
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\TableDiff;
14
use Doctrine\DBAL\TransactionIsolationLevel;
15
use Doctrine\DBAL\Types\Type;
16
use UnexpectedValueException;
17

Sergei Morozov's avatar
Sergei Morozov committed
18
use function sprintf;
19

20
abstract class AbstractPostgreSQLPlatformTestCase extends AbstractPlatformTestCase
21
{
22
    /** @var PostgreSQL94Platform */
Grégoire Paris's avatar
Grégoire Paris committed
23 24
    protected $platform;

25
    /** @return PostgreSQL94Platform */
26
    abstract public function createPlatform(): AbstractPlatform;
27

28
    public function getGenerateTableSql(): string
29 30 31 32
    {
        return 'CREATE TABLE test (id SERIAL NOT NULL, test VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))';
    }

33 34 35
    /**
     * {@inheritDoc}
     */
36
    public function getGenerateTableWithMultiColumnUniqueIndexSql(): array
37
    {
Sergei Morozov's avatar
Sergei Morozov committed
38
        return [
39
            'CREATE TABLE test (foo VARCHAR(255) DEFAULT NULL, bar VARCHAR(255) DEFAULT NULL)',
Sergei Morozov's avatar
Sergei Morozov committed
40 41
            'CREATE UNIQUE INDEX UNIQ_D87F7E0C8C73652176FF8CAA ON test (foo, bar)',
        ];
42 43
    }

44 45 46
    /**
     * {@inheritDoc}
     */
47
    public function getGenerateAlterTableSql(): array
48
    {
Sergei Morozov's avatar
Sergei Morozov committed
49
        return [
50 51 52 53 54 55 56 57 58
            'ALTER TABLE mytable ADD quota INT DEFAULT NULL',
            'ALTER TABLE mytable DROP foo',
            'ALTER TABLE mytable ALTER bar TYPE VARCHAR(255)',
            "ALTER TABLE mytable ALTER bar SET DEFAULT 'def'",
            'ALTER TABLE mytable ALTER bar SET NOT NULL',
            'ALTER TABLE mytable ALTER bloo TYPE BOOLEAN',
            "ALTER TABLE mytable ALTER bloo SET DEFAULT 'false'",
            'ALTER TABLE mytable ALTER bloo SET NOT NULL',
            'ALTER TABLE mytable RENAME TO userlist',
Sergei Morozov's avatar
Sergei Morozov committed
59
        ];
60 61
    }

62
    public function getGenerateIndexSql(): string
63 64 65 66
    {
        return 'CREATE INDEX my_idx ON mytable (user_name, last_login)';
    }

67
    protected function getGenerateForeignKeySql(): string
68
    {
Sergei Morozov's avatar
Sergei Morozov committed
69 70
        return 'ALTER TABLE test ADD FOREIGN KEY (fk_name_id)'
            . ' REFERENCES other_table (id) NOT DEFERRABLE INITIALLY IMMEDIATE';
71 72
    }

73
    public function testGeneratesForeignKeySqlForNonStandardOptions(): void
74
    {
Sergei Morozov's avatar
Sergei Morozov committed
75 76 77 78 79 80
        $foreignKey = new ForeignKeyConstraint(
            ['foreign_id'],
            'my_table',
            ['id'],
            'my_fk',
            ['onDelete' => 'CASCADE']
81
        );
82
        self::assertEquals(
Sergei Morozov's avatar
Sergei Morozov committed
83 84
            'CONSTRAINT my_fk FOREIGN KEY (foreign_id)'
                . ' REFERENCES my_table (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE',
Sergei Morozov's avatar
Sergei Morozov committed
85
            $this->platform->getForeignKeyDeclarationSQL($foreignKey)
86 87
        );

Sergei Morozov's avatar
Sergei Morozov committed
88 89 90 91 92 93
        $foreignKey = new ForeignKeyConstraint(
            ['foreign_id'],
            'my_table',
            ['id'],
            'my_fk',
            ['match' => 'full']
94
        );
95
        self::assertEquals(
Sergei Morozov's avatar
Sergei Morozov committed
96 97
            'CONSTRAINT my_fk FOREIGN KEY (foreign_id)'
                . ' REFERENCES my_table (id) MATCH full NOT DEFERRABLE INITIALLY IMMEDIATE',
Sergei Morozov's avatar
Sergei Morozov committed
98
            $this->platform->getForeignKeyDeclarationSQL($foreignKey)
99 100
        );

Sergei Morozov's avatar
Sergei Morozov committed
101 102 103 104 105 106
        $foreignKey = new ForeignKeyConstraint(
            ['foreign_id'],
            'my_table',
            ['id'],
            'my_fk',
            ['deferrable' => true]
107
        );
108
        self::assertEquals(
Sergei Morozov's avatar
Sergei Morozov committed
109 110
            'CONSTRAINT my_fk FOREIGN KEY (foreign_id)'
                . ' REFERENCES my_table (id) DEFERRABLE INITIALLY IMMEDIATE',
Sergei Morozov's avatar
Sergei Morozov committed
111
            $this->platform->getForeignKeyDeclarationSQL($foreignKey)
112 113
        );

Sergei Morozov's avatar
Sergei Morozov committed
114 115 116 117 118 119
        $foreignKey = new ForeignKeyConstraint(
            ['foreign_id'],
            'my_table',
            ['id'],
            'my_fk',
            ['deferred' => true]
120
        );
121
        self::assertEquals(
Sergei Morozov's avatar
Sergei Morozov committed
122 123
            'CONSTRAINT my_fk FOREIGN KEY (foreign_id)'
                . ' REFERENCES my_table (id) NOT DEFERRABLE INITIALLY DEFERRED',
Sergei Morozov's avatar
Sergei Morozov committed
124
            $this->platform->getForeignKeyDeclarationSQL($foreignKey)
125 126
        );

Sergei Morozov's avatar
Sergei Morozov committed
127 128 129 130 131 132
        $foreignKey = new ForeignKeyConstraint(
            ['foreign_id'],
            'my_table',
            ['id'],
            'my_fk',
            ['feferred' => true]
133
        );
134
        self::assertEquals(
Sergei Morozov's avatar
Sergei Morozov committed
135 136
            'CONSTRAINT my_fk FOREIGN KEY (foreign_id)'
                . ' REFERENCES my_table (id) NOT DEFERRABLE INITIALLY DEFERRED',
Sergei Morozov's avatar
Sergei Morozov committed
137
            $this->platform->getForeignKeyDeclarationSQL($foreignKey)
138 139
        );

Sergei Morozov's avatar
Sergei Morozov committed
140 141 142 143 144 145
        $foreignKey = new ForeignKeyConstraint(
            ['foreign_id'],
            'my_table',
            ['id'],
            'my_fk',
            ['deferrable' => true, 'deferred' => true, 'match' => 'full']
146
        );
147
        self::assertEquals(
Sergei Morozov's avatar
Sergei Morozov committed
148 149
            'CONSTRAINT my_fk FOREIGN KEY (foreign_id)'
                . ' REFERENCES my_table (id) MATCH full DEFERRABLE INITIALLY DEFERRED',
Sergei Morozov's avatar
Sergei Morozov committed
150
            $this->platform->getForeignKeyDeclarationSQL($foreignKey)
151 152 153
        );
    }

154
    public function testGeneratesSqlSnippets(): void
155
    {
156 157
        self::assertEquals('SIMILAR TO', $this->platform->getRegexpExpression());
        self::assertEquals('"', $this->platform->getIdentifierQuoteCharacter());
Sergei Morozov's avatar
Sergei Morozov committed
158 159 160 161 162 163

        self::assertEquals(
            'column1 || column2 || column3',
            $this->platform->getConcatExpression('column1', 'column2', 'column3')
        );

164 165
        self::assertEquals('SUBSTRING(column FROM 5)', $this->platform->getSubstringExpression('column', 5));
        self::assertEquals('SUBSTRING(column FROM 1 FOR 5)', $this->platform->getSubstringExpression('column', 1, 5));
166 167
    }

168
    public function testGeneratesTransactionCommands(): void
169
    {
170
        self::assertEquals(
171
            'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL READ UNCOMMITTED',
Sergei Morozov's avatar
Sergei Morozov committed
172
            $this->platform->getSetTransactionIsolationSQL(TransactionIsolationLevel::READ_UNCOMMITTED)
173
        );
174
        self::assertEquals(
175
            'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL READ COMMITTED',
Sergei Morozov's avatar
Sergei Morozov committed
176
            $this->platform->getSetTransactionIsolationSQL(TransactionIsolationLevel::READ_COMMITTED)
177
        );
178
        self::assertEquals(
179
            'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ',
Sergei Morozov's avatar
Sergei Morozov committed
180
            $this->platform->getSetTransactionIsolationSQL(TransactionIsolationLevel::REPEATABLE_READ)
181
        );
182
        self::assertEquals(
183
            'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE',
Sergei Morozov's avatar
Sergei Morozov committed
184
            $this->platform->getSetTransactionIsolationSQL(TransactionIsolationLevel::SERIALIZABLE)
185 186 187
        );
    }

188
    public function testGeneratesDDLSnippets(): void
189
    {
Sergei Morozov's avatar
Sergei Morozov committed
190 191 192
        self::assertEquals('CREATE DATABASE foobar', $this->platform->getCreateDatabaseSQL('foobar'));
        self::assertEquals('DROP DATABASE foobar', $this->platform->getDropDatabaseSQL('foobar'));
        self::assertEquals('DROP TABLE foobar', $this->platform->getDropTableSQL('foobar'));
193 194
    }

195
    public function testGenerateTableWithAutoincrement(): void
196
    {
Sergei Morozov's avatar
Sergei Morozov committed
197
        $table  = new Table('autoinc_table');
198 199 200
        $column = $table->addColumn('id', 'integer');
        $column->setAutoincrement(true);

Sergei Morozov's avatar
Sergei Morozov committed
201 202 203 204
        self::assertEquals(
            ['CREATE TABLE autoinc_table (id SERIAL NOT NULL)'],
            $this->platform->getCreateTableSQL($table)
        );
205 206
    }

Sergei Morozov's avatar
Sergei Morozov committed
207
    /**
208
     * @return mixed[][]
Sergei Morozov's avatar
Sergei Morozov committed
209
     */
210
    public static function serialTypes(): iterable
211 212 213 214 215 216 217 218 219 220
    {
        return [
            ['integer', 'SERIAL'],
            ['bigint', 'BIGSERIAL'],
        ];
    }

    /**
     * @dataProvider serialTypes
     */
221
    public function testGenerateTableWithAutoincrementDoesNotSetDefault(string $type, string $definition): void
222
    {
Sergei Morozov's avatar
Sergei Morozov committed
223
        $table  = new Table('autoinc_table_notnull');
224
        $column = $table->addColumn('id', $type);
225 226
        $column->setAutoincrement(true);
        $column->setNotnull(false);
227

Sergei Morozov's avatar
Sergei Morozov committed
228
        $sql = $this->platform->getCreateTableSQL($table);
229

Sergei Morozov's avatar
Sergei Morozov committed
230
        self::assertEquals([sprintf('CREATE TABLE autoinc_table_notnull (id %s)', $definition)], $sql);
231 232 233 234 235
    }

    /**
     * @dataProvider serialTypes
     */
236
    public function testCreateTableWithAutoincrementAndNotNullAddsConstraint(string $type, string $definition): void
237
    {
Sergei Morozov's avatar
Sergei Morozov committed
238
        $table  = new Table('autoinc_table_notnull_enabled');
239
        $column = $table->addColumn('id', $type);
240 241
        $column->setAutoincrement(true);
        $column->setNotnull(true);
242

Sergei Morozov's avatar
Sergei Morozov committed
243
        $sql = $this->platform->getCreateTableSQL($table);
244

Sergei Morozov's avatar
Sergei Morozov committed
245
        self::assertEquals([sprintf('CREATE TABLE autoinc_table_notnull_enabled (id %s NOT NULL)', $definition)], $sql);
246 247 248 249 250
    }

    /**
     * @dataProvider serialTypes
     */
251
    public function testGetDefaultValueDeclarationSQLIgnoresTheDefaultKeyWhenTheFieldIsSerial(string $type): void
252
    {
Sergei Morozov's avatar
Sergei Morozov committed
253
        $sql = $this->platform->getDefaultValueDeclarationSQL(
254 255 256 257 258 259 260 261 262 263
            [
                'autoincrement' => true,
                'type'          => Type::getType($type),
                'default'       => 1,
            ]
        );

        self::assertSame('', $sql);
    }

264
    public function testGeneratesTypeDeclarationForIntegers(): void
265
    {
266
        self::assertEquals(
267
            'INT',
Sergei Morozov's avatar
Sergei Morozov committed
268
            $this->platform->getIntegerTypeDeclarationSQL([])
269
        );
270
        self::assertEquals(
271
            'SERIAL',
Sergei Morozov's avatar
Sergei Morozov committed
272
            $this->platform->getIntegerTypeDeclarationSQL(['autoincrement' => true])
Sergei Morozov's avatar
Sergei Morozov committed
273
        );
274
        self::assertEquals(
275
            'SERIAL',
Sergei Morozov's avatar
Sergei Morozov committed
276
            $this->platform->getIntegerTypeDeclarationSQL(
Sergei Morozov's avatar
Sergei Morozov committed
277 278 279
                ['autoincrement' => true, 'primary' => true]
            )
        );
280 281
    }

282
    public function testGeneratesTypeDeclarationForStrings(): void
283
    {
284
        self::assertEquals(
285
            'CHAR(10)',
Sergei Morozov's avatar
Sergei Morozov committed
286
            $this->platform->getVarcharTypeDeclarationSQL(
Sergei Morozov's avatar
Sergei Morozov committed
287 288
                ['length' => 10, 'fixed' => true]
            )
289
        );
290
        self::assertEquals(
291
            'VARCHAR(50)',
292
            $this->platform->getVarcharTypeDeclarationSQL(['length' => 50])
293
        );
294
        self::assertEquals(
295
            'VARCHAR(255)',
296
            $this->platform->getVarcharTypeDeclarationSQL([])
297 298 299
        );
    }

300
    public function getGenerateUniqueIndexSql(): string
301 302 303 304
    {
        return 'CREATE UNIQUE INDEX index_name ON test (test, test2)';
    }

305
    public function testGeneratesSequenceSqlCommands(): void
306
    {
Sergei Morozov's avatar
Sergei Morozov committed
307
        $sequence = new Sequence('myseq', 20, 1);
308
        self::assertEquals(
309
            'CREATE SEQUENCE myseq INCREMENT BY 20 MINVALUE 1 START 1',
Sergei Morozov's avatar
Sergei Morozov committed
310
            $this->platform->getCreateSequenceSQL($sequence)
311
        );
312
        self::assertEquals(
313
            'DROP SEQUENCE myseq CASCADE',
Sergei Morozov's avatar
Sergei Morozov committed
314
            $this->platform->getDropSequenceSQL('myseq')
315
        );
316
        self::assertEquals(
317
            "SELECT NEXTVAL('myseq')",
Sergei Morozov's avatar
Sergei Morozov committed
318
            $this->platform->getSequenceNextValSQL('myseq')
319 320 321
        );
    }

322
    public function testDoesNotPreferIdentityColumns(): void
323
    {
Sergei Morozov's avatar
Sergei Morozov committed
324
        self::assertFalse($this->platform->prefersIdentityColumns());
325 326
    }

327
    public function testPrefersSequences(): void
328
    {
Sergei Morozov's avatar
Sergei Morozov committed
329
        self::assertTrue($this->platform->prefersSequences());
330 331
    }

332
    public function testSupportsIdentityColumns(): void
333
    {
Sergei Morozov's avatar
Sergei Morozov committed
334
        self::assertTrue($this->platform->supportsIdentityColumns());
335 336
    }

337
    public function testSupportsSavePoints(): void
338
    {
Sergei Morozov's avatar
Sergei Morozov committed
339
        self::assertTrue($this->platform->supportsSavepoints());
340 341
    }

342
    public function testSupportsSequences(): void
343
    {
Sergei Morozov's avatar
Sergei Morozov committed
344
        self::assertTrue($this->platform->supportsSequences());
345 346
    }

347
    protected function supportsCommentOnStatement(): bool
348 349 350 351
    {
        return true;
    }

352
    public function testModifyLimitQuery(): void
353
    {
Sergei Morozov's avatar
Sergei Morozov committed
354
        $sql = $this->platform->modifyLimitQuery('SELECT * FROM user', 10, 0);
355
        self::assertEquals('SELECT * FROM user LIMIT 10', $sql);
356 357
    }

358
    public function testModifyLimitQueryWithEmptyOffset(): void
359
    {
Sergei Morozov's avatar
Sergei Morozov committed
360
        $sql = $this->platform->modifyLimitQuery('SELECT * FROM user', 10);
361
        self::assertEquals('SELECT * FROM user LIMIT 10', $sql);
362 363
    }

364 365 366
    /**
     * {@inheritDoc}
     */
367
    public function getCreateTableColumnCommentsSQL(): array
368
    {
Sergei Morozov's avatar
Sergei Morozov committed
369 370
        return [
            'CREATE TABLE test (id INT NOT NULL, PRIMARY KEY(id))',
371
            "COMMENT ON COLUMN test.id IS 'This is a comment'",
Sergei Morozov's avatar
Sergei Morozov committed
372
        ];
373 374
    }

375 376 377
    /**
     * {@inheritDoc}
     */
378
    public function getAlterTableColumnCommentsSQL(): array
379
    {
Sergei Morozov's avatar
Sergei Morozov committed
380 381
        return [
            'ALTER TABLE mytable ADD quota INT NOT NULL',
382
            "COMMENT ON COLUMN mytable.quota IS 'A comment'",
Sergei Morozov's avatar
Sergei Morozov committed
383
            'COMMENT ON COLUMN mytable.foo IS NULL',
384
            "COMMENT ON COLUMN mytable.baz IS 'B comment'",
Sergei Morozov's avatar
Sergei Morozov committed
385
        ];
386 387
    }

388 389 390
    /**
     * {@inheritDoc}
     */
391
    public function getCreateTableColumnTypeCommentsSQL(): array
392
    {
Sergei Morozov's avatar
Sergei Morozov committed
393 394 395 396
        return [
            'CREATE TABLE test (id INT NOT NULL, data TEXT NOT NULL, PRIMARY KEY(id))',
            "COMMENT ON COLUMN test.data IS '(DC2Type:array)'",
        ];
397 398
    }

399 400 401
    /**
     * {@inheritDoc}
     */
402
    protected function getQuotedColumnInPrimaryKeySQL(): array
403
    {
Sergei Morozov's avatar
Sergei Morozov committed
404
        return ['CREATE TABLE "quoted" ("create" VARCHAR(255) NOT NULL, PRIMARY KEY("create"))'];
405 406
    }

407 408 409
    /**
     * {@inheritDoc}
     */
410
    protected function getQuotedColumnInIndexSQL(): array
411
    {
Sergei Morozov's avatar
Sergei Morozov committed
412
        return [
413 414
            'CREATE TABLE "quoted" ("create" VARCHAR(255) NOT NULL)',
            'CREATE INDEX IDX_22660D028FD6E0FB ON "quoted" ("create")',
Sergei Morozov's avatar
Sergei Morozov committed
415
        ];
416 417
    }

418 419 420
    /**
     * {@inheritDoc}
     */
421
    protected function getQuotedNameInIndexSQL(): array
Markus Fasselt's avatar
Markus Fasselt committed
422
    {
Sergei Morozov's avatar
Sergei Morozov committed
423
        return [
Markus Fasselt's avatar
Markus Fasselt committed
424 425
            'CREATE TABLE test (column1 VARCHAR(255) NOT NULL)',
            'CREATE INDEX "key" ON test (column1)',
Sergei Morozov's avatar
Sergei Morozov committed
426
        ];
Markus Fasselt's avatar
Markus Fasselt committed
427 428
    }

429 430 431
    /**
     * {@inheritDoc}
     */
432
    protected function getQuotedColumnInForeignKeySQL(): array
433
    {
Sergei Morozov's avatar
Sergei Morozov committed
434
        return [
Sergei Morozov's avatar
Sergei Morozov committed
435 436 437 438 439 440 441 442
            'CREATE TABLE "quoted" ("create" VARCHAR(255) NOT NULL, '
            . 'foo VARCHAR(255) NOT NULL, "bar" VARCHAR(255) NOT NULL)',
            'ALTER TABLE "quoted" ADD CONSTRAINT FK_WITH_RESERVED_KEYWORD FOREIGN KEY ("create", foo, "bar")'
                . ' REFERENCES "foreign" ("create", bar, "foo-bar") NOT DEFERRABLE INITIALLY IMMEDIATE',
            'ALTER TABLE "quoted" ADD CONSTRAINT FK_WITH_NON_RESERVED_KEYWORD FOREIGN KEY ("create", foo, "bar")'
                . ' REFERENCES foo ("create", bar, "foo-bar") NOT DEFERRABLE INITIALLY IMMEDIATE',
            'ALTER TABLE "quoted" ADD CONSTRAINT FK_WITH_INTENDED_QUOTATION FOREIGN KEY ("create", foo, "bar")'
                . ' REFERENCES "foo-bar" ("create", bar, "foo-bar") NOT DEFERRABLE INITIALLY IMMEDIATE',
Sergei Morozov's avatar
Sergei Morozov committed
443
        ];
444 445 446
    }

    /**
447
     * @param string|bool $databaseValue
Sergei Morozov's avatar
Sergei Morozov committed
448 449
     *
     * @dataProvider pgBooleanProvider
450
     */
451 452
    public function testConvertBooleanAsLiteralStrings(
        $databaseValue,
453 454 455
        string $preparedStatementValue,
        ?int $integerValue,
        ?bool $booleanValue
456
    ): void {
457 458
        $platform = $this->createPlatform();

459
        self::assertEquals($preparedStatementValue, $platform->convertBooleans($databaseValue));
460 461
    }

462
    public function testConvertBooleanAsLiteralIntegers(): void
463 464 465 466
    {
        $platform = $this->createPlatform();
        $platform->setUseBooleanTrueFalseStrings(false);

467 468
        self::assertEquals(1, $platform->convertBooleans(true));
        self::assertEquals(1, $platform->convertBooleans('1'));
469

470 471
        self::assertEquals(0, $platform->convertBooleans(false));
        self::assertEquals(0, $platform->convertBooleans('0'));
472 473 474
    }

    /**
475
     * @param string|bool $databaseValue
Sergei Morozov's avatar
Sergei Morozov committed
476 477
     *
     * @dataProvider pgBooleanProvider
478
     */
479 480
    public function testConvertBooleanAsDatabaseValueStrings(
        $databaseValue,
481 482 483
        string $preparedStatementValue,
        ?int $integerValue,
        ?bool $booleanValue
484
    ): void {
485 486
        $platform = $this->createPlatform();

487
        self::assertSame($integerValue, $platform->convertBooleansToDatabaseValue($booleanValue));
488 489
    }

490
    public function testConvertBooleanAsDatabaseValueIntegers(): void
491 492 493 494
    {
        $platform = $this->createPlatform();
        $platform->setUseBooleanTrueFalseStrings(false);

495 496
        self::assertSame(1, $platform->convertBooleansToDatabaseValue(true));
        self::assertSame(0, $platform->convertBooleansToDatabaseValue(false));
497
    }
498 499

    /**
500
     * @param string|bool $databaseValue
Sergei Morozov's avatar
Sergei Morozov committed
501 502
     *
     * @dataProvider pgBooleanProvider
503
     */
Sergei Morozov's avatar
Sergei Morozov committed
504 505 506 507 508 509
    public function testConvertFromBoolean(
        $databaseValue,
        string $prepareStatementValue,
        ?int $integerValue,
        ?bool $booleanValue
    ): void {
510
        $platform = $this->createPlatform();
511

512
        self::assertSame($booleanValue, $platform->convertFromBoolean($databaseValue));
513
    }
514

515
    public function testThrowsExceptionWithInvalidBooleanLiteral(): void
516
    {
517 518 519 520 521 522
        $platform = $this->createPlatform();

        $this->expectException(UnexpectedValueException::class);
        $this->expectExceptionMessage("Unrecognized boolean literal 'my-bool'");

        $platform->convertBooleansToDatabaseValue('my-bool');
523 524
    }

525
    public function testGetCreateSchemaSQL(): void
526 527
    {
        $schemaName = 'schema';
Sergei Morozov's avatar
Sergei Morozov committed
528
        $sql        = $this->platform->getCreateSchemaSQL($schemaName);
529
        self::assertEquals('CREATE SCHEMA ' . $schemaName, $sql);
530 531
    }

532
    public function testAlterDecimalPrecisionScale(): void
533 534 535
    {
        $table = new Table('mytable');
        $table->addColumn('dfoo1', 'decimal');
Sergei Morozov's avatar
Sergei Morozov committed
536 537 538
        $table->addColumn('dfoo2', 'decimal', ['precision' => 10, 'scale' => 6]);
        $table->addColumn('dfoo3', 'decimal', ['precision' => 10, 'scale' => 6]);
        $table->addColumn('dfoo4', 'decimal', ['precision' => 10, 'scale' => 6]);
539

Sergei Morozov's avatar
Sergei Morozov committed
540
        $tableDiff            = new TableDiff('mytable');
541 542
        $tableDiff->fromTable = $table;

Sergei Morozov's avatar
Sergei Morozov committed
543 544 545 546 547 548
        $tableDiff->changedColumns['dloo1'] = new ColumnDiff(
            'dloo1',
            new Column(
                'dloo1',
                Type::getType('decimal'),
                ['precision' => 16, 'scale' => 6]
549
            ),
Sergei Morozov's avatar
Sergei Morozov committed
550 551 552 553 554 555 556 557
            ['precision']
        );
        $tableDiff->changedColumns['dloo2'] = new ColumnDiff(
            'dloo2',
            new Column(
                'dloo2',
                Type::getType('decimal'),
                ['precision' => 10, 'scale' => 4]
558
            ),
Sergei Morozov's avatar
Sergei Morozov committed
559 560 561 562 563 564 565 566
            ['scale']
        );
        $tableDiff->changedColumns['dloo3'] = new ColumnDiff(
            'dloo3',
            new Column(
                'dloo3',
                Type::getType('decimal'),
                ['precision' => 10, 'scale' => 6]
567
            ),
Sergei Morozov's avatar
Sergei Morozov committed
568 569 570 571 572 573 574 575
            []
        );
        $tableDiff->changedColumns['dloo4'] = new ColumnDiff(
            'dloo4',
            new Column(
                'dloo4',
                Type::getType('decimal'),
                ['precision' => 16, 'scale' => 8]
576
            ),
Sergei Morozov's avatar
Sergei Morozov committed
577
            ['precision', 'scale']
578 579
        );

Sergei Morozov's avatar
Sergei Morozov committed
580
        $sql = $this->platform->getAlterTableSQL($tableDiff);
581

Sergei Morozov's avatar
Sergei Morozov committed
582
        $expectedSql = [
583 584 585
            'ALTER TABLE mytable ALTER dloo1 TYPE NUMERIC(16, 6)',
            'ALTER TABLE mytable ALTER dloo2 TYPE NUMERIC(10, 4)',
            'ALTER TABLE mytable ALTER dloo4 TYPE NUMERIC(16, 8)',
Sergei Morozov's avatar
Sergei Morozov committed
586
        ];
587

588
        self::assertEquals($expectedSql, $sql);
589 590
    }

591
    public function testDroppingConstraintsBeforeColumns(): void
592 593 594
    {
        $newTable = new Table('mytable');
        $newTable->addColumn('id', 'integer');
Sergei Morozov's avatar
Sergei Morozov committed
595
        $newTable->setPrimaryKey(['id']);
596 597 598

        $oldTable = clone $newTable;
        $oldTable->addColumn('parent_id', 'integer');
599
        $oldTable->addForeignKeyConstraint('mytable', ['parent_id'], ['id']);
600

Sergei Morozov's avatar
Sergei Morozov committed
601 602
        $comparator = new Comparator();
        $tableDiff  = $comparator->diffTable($oldTable, $newTable);
603

Sergei Morozov's avatar
Sergei Morozov committed
604
        $sql = $this->platform->getAlterTableSQL($tableDiff);
605

Sergei Morozov's avatar
Sergei Morozov committed
606
        $expectedSql = [
607 608 609
            'ALTER TABLE mytable DROP CONSTRAINT FK_6B2BD609727ACA70',
            'DROP INDEX IDX_6B2BD609727ACA70',
            'ALTER TABLE mytable DROP parent_id',
Sergei Morozov's avatar
Sergei Morozov committed
610
        ];
611

612
        self::assertEquals($expectedSql, $sql);
613 614
    }

615
    public function testUsesSequenceEmulatedIdentityColumns(): void
616
    {
Sergei Morozov's avatar
Sergei Morozov committed
617
        self::assertTrue($this->platform->usesSequenceEmulatedIdentityColumns());
618 619
    }

620
    public function testReturnsIdentitySequenceName(): void
621
    {
Sergei Morozov's avatar
Sergei Morozov committed
622
        self::assertSame('mytable_mycolumn_seq', $this->platform->getIdentitySequenceName('mytable', 'mycolumn'));
623 624 625 626 627
    }

    /**
     * @dataProvider dataCreateSequenceWithCache
     */
628
    public function testCreateSequenceWithCache(int $cacheSize, string $expectedSql): void
629
    {
Sergei Morozov's avatar
Sergei Morozov committed
630
        $sequence = new Sequence('foo', 1, 1, $cacheSize);
631
        self::assertStringContainsString($expectedSql, $this->platform->getCreateSequenceSQL($sequence));
632 633
    }

634 635 636
    /**
     * @return mixed[][]
     */
637
    public static function dataCreateSequenceWithCache(): iterable
638
    {
Sergei Morozov's avatar
Sergei Morozov committed
639 640 641
        return [
            [3, 'CACHE 3'],
        ];
642 643
    }

644
    protected function getBinaryDefaultLength(): int
645 646 647 648
    {
        return 0;
    }

649
    protected function getBinaryMaxLength(): int
650 651 652 653
    {
        return 0;
    }

654
    public function testReturnsBinaryTypeDeclarationSQL(): void
655
    {
Sergei Morozov's avatar
Sergei Morozov committed
656 657 658
        self::assertSame('BYTEA', $this->platform->getBinaryTypeDeclarationSQL([]));
        self::assertSame('BYTEA', $this->platform->getBinaryTypeDeclarationSQL(['length' => 0]));
        self::assertSame('BYTEA', $this->platform->getBinaryTypeDeclarationSQL(['length' => 9999999]));
659

Sergei Morozov's avatar
Sergei Morozov committed
660 661 662
        self::assertSame('BYTEA', $this->platform->getBinaryTypeDeclarationSQL(['fixed' => true]));
        self::assertSame('BYTEA', $this->platform->getBinaryTypeDeclarationSQL(['fixed' => true, 'length' => 0]));
        self::assertSame('BYTEA', $this->platform->getBinaryTypeDeclarationSQL(['fixed' => true, 'length' => 9999999]));
663 664
    }

665
    public function testDoesNotPropagateUnnecessaryTableAlterationOnBinaryType(): void
666 667 668
    {
        $table1 = new Table('mytable');
        $table1->addColumn('column_varbinary', 'binary');
Sergei Morozov's avatar
Sergei Morozov committed
669
        $table1->addColumn('column_binary', 'binary', ['fixed' => true]);
670 671 672
        $table1->addColumn('column_blob', 'blob');

        $table2 = new Table('mytable');
Sergei Morozov's avatar
Sergei Morozov committed
673
        $table2->addColumn('column_varbinary', 'binary', ['fixed' => true]);
674 675 676 677 678 679 680 681
        $table2->addColumn('column_binary', 'binary');
        $table2->addColumn('column_blob', 'binary');

        $comparator = new Comparator();

        // VARBINARY -> BINARY
        // BINARY    -> VARBINARY
        // BLOB      -> VARBINARY
Sergei Morozov's avatar
Sergei Morozov committed
682
        self::assertEmpty($this->platform->getAlterTableSQL($comparator->diffTable($table1, $table2)));
683 684

        $table2 = new Table('mytable');
Sergei Morozov's avatar
Sergei Morozov committed
685
        $table2->addColumn('column_varbinary', 'binary', ['length' => 42]);
686
        $table2->addColumn('column_binary', 'blob');
Sergei Morozov's avatar
Sergei Morozov committed
687
        $table2->addColumn('column_blob', 'binary', ['length' => 11, 'fixed' => true]);
688 689 690 691

        // VARBINARY -> VARBINARY with changed length
        // BINARY    -> BLOB
        // BLOB      -> BINARY
Sergei Morozov's avatar
Sergei Morozov committed
692
        self::assertEmpty($this->platform->getAlterTableSQL($comparator->diffTable($table1, $table2)));
693 694 695

        $table2 = new Table('mytable');
        $table2->addColumn('column_varbinary', 'blob');
Sergei Morozov's avatar
Sergei Morozov committed
696
        $table2->addColumn('column_binary', 'binary', ['length' => 42, 'fixed' => true]);
697 698 699 700 701
        $table2->addColumn('column_blob', 'blob');

        // VARBINARY -> BLOB
        // BINARY    -> BINARY with changed length
        // BLOB      -> BLOB
Sergei Morozov's avatar
Sergei Morozov committed
702
        self::assertEmpty($this->platform->getAlterTableSQL($comparator->diffTable($table1, $table2)));
703 704 705
    }

    /**
706
     * {@inheritDoc}
707
     */
708
    protected function getAlterTableRenameIndexSQL(): array
709
    {
Sergei Morozov's avatar
Sergei Morozov committed
710
        return ['ALTER INDEX idx_foo RENAME TO idx_bar'];
711 712 713
    }

    /**
714
     * {@inheritDoc}
715
     */
716
    protected function getQuotedAlterTableRenameIndexSQL(): array
717
    {
Sergei Morozov's avatar
Sergei Morozov committed
718
        return [
719 720
            'ALTER INDEX "create" RENAME TO "select"',
            'ALTER INDEX "foo" RENAME TO "bar"',
Sergei Morozov's avatar
Sergei Morozov committed
721
        ];
722
    }
723

724 725
    /**
     * PostgreSQL boolean strings provider
Sergei Morozov's avatar
Sergei Morozov committed
726
     *
727
     * @return mixed[][]
728
     */
729
    public static function pgBooleanProvider(): iterable
730
    {
Sergei Morozov's avatar
Sergei Morozov committed
731
        return [
732
            // Database value, prepared statement value, boolean integer value, boolean value.
Sergei Morozov's avatar
Sergei Morozov committed
733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750
            [true, 'true', 1, true],
            ['t', 'true', 1, true],
            ['true', 'true', 1, true],
            ['y', 'true', 1, true],
            ['yes', 'true', 1, true],
            ['on', 'true', 1, true],
            ['1', 'true', 1, true],

            [false, 'false', 0, false],
            ['f', 'false', 0, false],
            ['false', 'false', 0, false],
            [ 'n', 'false', 0, false],
            ['no', 'false', 0, false],
            ['off', 'false', 0, false],
            ['0', 'false', 0, false],

            [null, 'NULL', null, null],
        ];
751 752
    }

753 754 755
    /**
     * {@inheritdoc}
     */
756
    protected function getQuotedAlterTableRenameColumnSQL(): array
757
    {
Sergei Morozov's avatar
Sergei Morozov committed
758
        return [
759 760 761 762 763 764 765 766 767
            'ALTER TABLE mytable RENAME COLUMN unquoted1 TO unquoted',
            'ALTER TABLE mytable RENAME COLUMN unquoted2 TO "where"',
            'ALTER TABLE mytable RENAME COLUMN unquoted3 TO "foo"',
            'ALTER TABLE mytable RENAME COLUMN "create" TO reserved_keyword',
            'ALTER TABLE mytable RENAME COLUMN "table" TO "from"',
            'ALTER TABLE mytable RENAME COLUMN "select" TO "bar"',
            'ALTER TABLE mytable RENAME COLUMN quoted1 TO quoted',
            'ALTER TABLE mytable RENAME COLUMN quoted2 TO "and"',
            'ALTER TABLE mytable RENAME COLUMN quoted3 TO "baz"',
Sergei Morozov's avatar
Sergei Morozov committed
768
        ];
769
    }
770

771 772 773
    /**
     * {@inheritdoc}
     */
774
    protected function getQuotedAlterTableChangeColumnLengthSQL(): array
775
    {
Sergei Morozov's avatar
Sergei Morozov committed
776
        return [
777 778 779 780 781 782
            'ALTER TABLE mytable ALTER unquoted1 TYPE VARCHAR(255)',
            'ALTER TABLE mytable ALTER unquoted2 TYPE VARCHAR(255)',
            'ALTER TABLE mytable ALTER unquoted3 TYPE VARCHAR(255)',
            'ALTER TABLE mytable ALTER "create" TYPE VARCHAR(255)',
            'ALTER TABLE mytable ALTER "table" TYPE VARCHAR(255)',
            'ALTER TABLE mytable ALTER "select" TYPE VARCHAR(255)',
Sergei Morozov's avatar
Sergei Morozov committed
783
        ];
784 785
    }

786
    /**
787
     * {@inheritDoc}
788
     */
789
    protected function getAlterTableRenameIndexInSchemaSQL(): array
790
    {
Sergei Morozov's avatar
Sergei Morozov committed
791
        return ['ALTER INDEX myschema.idx_foo RENAME TO idx_bar'];
792 793 794
    }

    /**
795
     * {@inheritDoc}
796
     */
797
    protected function getQuotedAlterTableRenameIndexInSchemaSQL(): array
798
    {
Sergei Morozov's avatar
Sergei Morozov committed
799
        return [
800 801
            'ALTER INDEX "schema"."create" RENAME TO "select"',
            'ALTER INDEX "schema"."foo" RENAME TO "bar"',
Sergei Morozov's avatar
Sergei Morozov committed
802
        ];
803
    }
804

805
    protected function getQuotesDropForeignKeySQL(): string
806 807 808 809
    {
        return 'ALTER TABLE "table" DROP CONSTRAINT "select"';
    }

810
    public function testGetNullCommentOnColumnSQL(): void
811
    {
812
        self::assertEquals(
Sergei Morozov's avatar
Sergei Morozov committed
813
            'COMMENT ON COLUMN mytable.id IS NULL',
Sergei Morozov's avatar
Sergei Morozov committed
814
            $this->platform->getCommentOnColumnSQL('mytable', 'id', null)
815 816
        );
    }
817

818
    public function testReturnsGuidTypeDeclarationSQL(): void
819
    {
Sergei Morozov's avatar
Sergei Morozov committed
820
        self::assertSame('UUID', $this->platform->getGuidTypeDeclarationSQL([]));
821
    }
822 823 824 825

    /**
     * {@inheritdoc}
     */
826
    public function getAlterTableRenameColumnSQL(): array
827
    {
Sergei Morozov's avatar
Sergei Morozov committed
828
        return ['ALTER TABLE foo RENAME COLUMN bar TO baz'];
829
    }
830 831 832 833

    /**
     * {@inheritdoc}
     */
834
    protected function getQuotesTableIdentifiersInAlterTableSQL(): array
835
    {
Sergei Morozov's avatar
Sergei Morozov committed
836
        return [
837 838 839 840 841 842 843 844 845 846 847
            'ALTER TABLE "foo" DROP CONSTRAINT fk1',
            'ALTER TABLE "foo" DROP CONSTRAINT fk2',
            'ALTER TABLE "foo" ADD bloo INT NOT NULL',
            'ALTER TABLE "foo" DROP baz',
            'ALTER TABLE "foo" ALTER bar DROP NOT NULL',
            'ALTER TABLE "foo" RENAME COLUMN id TO war',
            'ALTER TABLE "foo" RENAME TO "table"',
            'ALTER TABLE "table" ADD CONSTRAINT fk_add FOREIGN KEY (fk3) REFERENCES fk_table (id) NOT DEFERRABLE ' .
            'INITIALLY IMMEDIATE',
            'ALTER TABLE "table" ADD CONSTRAINT fk2 FOREIGN KEY (fk2) REFERENCES fk_table2 (id) NOT DEFERRABLE ' .
            'INITIALLY IMMEDIATE',
Sergei Morozov's avatar
Sergei Morozov committed
848
        ];
849
    }
850 851 852 853

    /**
     * {@inheritdoc}
     */
854
    protected function getCommentOnColumnSQL(): array
855
    {
Sergei Morozov's avatar
Sergei Morozov committed
856
        return [
857 858 859
            'COMMENT ON COLUMN foo.bar IS \'comment\'',
            'COMMENT ON COLUMN "Foo"."BAR" IS \'comment\'',
            'COMMENT ON COLUMN "select"."from" IS \'comment\'',
Sergei Morozov's avatar
Sergei Morozov committed
860
        ];
861 862
    }

863
    public function testAltersTableColumnCommentWithExplicitlyQuotedIdentifiers(): void
864
    {
Sergei Morozov's avatar
Sergei Morozov committed
865 866
        $table1 = new Table('"foo"', [new Column('"bar"', Type::getType('integer'))]);
        $table2 = new Table('"foo"', [new Column('"bar"', Type::getType('integer'), ['comment' => 'baz'])]);
867 868 869 870 871

        $comparator = new Comparator();

        $tableDiff = $comparator->diffTable($table1, $table2);

Sergei Morozov's avatar
Sergei Morozov committed
872
        self::assertInstanceOf(TableDiff::class, $tableDiff);
873
        self::assertSame(
Sergei Morozov's avatar
Sergei Morozov committed
874
            ['COMMENT ON COLUMN "foo"."bar" IS \'baz\''],
Sergei Morozov's avatar
Sergei Morozov committed
875
            $this->platform->getAlterTableSQL($tableDiff)
876 877
        );
    }
878

879
    public function testAltersTableColumnCommentIfRequiredByType(): void
880 881 882 883 884 885 886 887
    {
        $table1 = new Table('"foo"', [new Column('"bar"', Type::getType('datetime'))]);
        $table2 = new Table('"foo"', [new Column('"bar"', Type::getType('datetime_immutable'))]);

        $comparator = new Comparator();

        $tableDiff = $comparator->diffTable($table1, $table2);

888 889
        self::assertInstanceOf('Doctrine\DBAL\Schema\TableDiff', $tableDiff);
        self::assertSame(
890 891 892 893 894 895 896 897 898
            [
                'ALTER TABLE "foo" ALTER "bar" TYPE TIMESTAMP(0) WITHOUT TIME ZONE',
                'ALTER TABLE "foo" ALTER "bar" DROP DEFAULT',
                'COMMENT ON COLUMN "foo"."bar" IS \'(DC2Type:datetime_immutable)\'',
            ],
            $this->platform->getAlterTableSQL($tableDiff)
        );
    }

899
    protected function getQuotesReservedKeywordInUniqueConstraintDeclarationSQL(): string
900 901 902 903
    {
        return 'CONSTRAINT "select" UNIQUE (foo)';
    }

904
    protected function getQuotesReservedKeywordInIndexDeclarationSQL(): string
905 906 907
    {
        return 'INDEX "select" (foo)';
    }
908

909
    protected function getQuotesReservedKeywordInTruncateTableSQL(): string
910 911 912 913
    {
        return 'TRUNCATE "select"';
    }

914
    /**
915
     * {@inheritdoc}
916
     */
917
    protected function getAlterStringToFixedStringSQL(): array
918
    {
Sergei Morozov's avatar
Sergei Morozov committed
919
        return ['ALTER TABLE mytable ALTER name TYPE CHAR(2)'];
920
    }
921 922 923 924

    /**
     * {@inheritdoc}
     */
925
    protected function getGeneratesAlterTableRenameIndexUsedByForeignKeySQL(): array
926
    {
Sergei Morozov's avatar
Sergei Morozov committed
927
        return ['ALTER INDEX idx_foo RENAME TO idx_foo_renamed'];
928
    }
Steve Müller's avatar
Steve Müller committed
929

930
    public function testInitializesTsvectorTypeMapping(): void
Steve Müller's avatar
Steve Müller committed
931
    {
Sergei Morozov's avatar
Sergei Morozov committed
932 933
        self::assertTrue($this->platform->hasDoctrineTypeMappingFor('tsvector'));
        self::assertEquals('text', $this->platform->getDoctrineTypeMapping('tsvector'));
Steve Müller's avatar
Steve Müller committed
934
    }
935

936
    public function testReturnsDisallowDatabaseConnectionsSQL(): void
937
    {
938
        self::assertSame(
939
            "UPDATE pg_database SET datallowconn = 'false' WHERE datname = 'foo'",
Sergei Morozov's avatar
Sergei Morozov committed
940
            $this->platform->getDisallowDatabaseConnectionsSQL('foo')
941 942 943
        );
    }

944
    public function testReturnsCloseActiveDatabaseConnectionsSQL(): void
945
    {
946
        self::assertSame(
947
            "SELECT pg_terminate_backend(procpid) FROM pg_stat_activity WHERE datname = 'foo'",
Sergei Morozov's avatar
Sergei Morozov committed
948
            $this->platform->getCloseActiveDatabaseConnectionsSQL('foo')
949 950
        );
    }
951

952
    public function testQuotesTableNameInListTableForeignKeysSQL(): void
953
    {
954 955 956 957
        self::assertStringContainsStringIgnoringCase(
            "'Foo''Bar\\'",
            $this->platform->getListTableForeignKeysSQL("Foo'Bar\\")
        );
958 959
    }

960
    public function testQuotesSchemaNameInListTableForeignKeysSQL(): void
961
    {
962
        self::assertStringContainsStringIgnoringCase(
963
            "'Foo''Bar\\'",
964
            $this->platform->getListTableForeignKeysSQL("Foo'Bar\\.baz_table")
965 966 967
        );
    }

968
    public function testQuotesTableNameInListTableConstraintsSQL(): void
969
    {
970 971 972 973
        self::assertStringContainsStringIgnoringCase(
            "'Foo''Bar\\'",
            $this->platform->getListTableConstraintsSQL("Foo'Bar\\")
        );
974 975
    }

976
    public function testQuotesTableNameInListTableIndexesSQL(): void
977
    {
978 979 980 981
        self::assertStringContainsStringIgnoringCase(
            "'Foo''Bar\\'",
            $this->platform->getListTableIndexesSQL("Foo'Bar\\")
        );
982 983
    }

984
    public function testQuotesSchemaNameInListTableIndexesSQL(): void
985
    {
986
        self::assertStringContainsStringIgnoringCase(
987
            "'Foo''Bar\\'",
988
            $this->platform->getListTableIndexesSQL("Foo'Bar\\.baz_table")
989 990 991
        );
    }

992
    public function testQuotesTableNameInListTableColumnsSQL(): void
993
    {
994 995 996 997
        self::assertStringContainsStringIgnoringCase(
            "'Foo''Bar\\'",
            $this->platform->getListTableColumnsSQL("Foo'Bar\\")
        );
998 999
    }

1000
    public function testQuotesSchemaNameInListTableColumnsSQL(): void
1001
    {
1002
        self::assertStringContainsStringIgnoringCase(
1003
            "'Foo''Bar\\'",
1004
            $this->platform->getListTableColumnsSQL("Foo'Bar\\.baz_table")
1005 1006 1007
        );
    }

1008
    public function testQuotesDatabaseNameInCloseActiveDatabaseConnectionsSQL(): void
1009
    {
1010
        self::assertStringContainsStringIgnoringCase(
1011
            "'Foo''Bar\\'",
1012
            $this->platform->getCloseActiveDatabaseConnectionsSQL("Foo'Bar\\")
1013 1014
        );
    }
1015
}