ExceptionTest.php 14.9 KB
Newer Older
1
<?php
Sergei Morozov's avatar
Sergei Morozov committed
2

3 4
namespace Doctrine\Tests\DBAL\Functional;

5
use Doctrine\DBAL\Driver\ExceptionConverterDriver;
6
use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
Sergei Morozov's avatar
Sergei Morozov committed
7
use Doctrine\DBAL\DriverManager;
Luís Cobucci's avatar
Luís Cobucci committed
8
use Doctrine\DBAL\Exception;
9 10 11 12
use Doctrine\DBAL\Platforms\DrizzlePlatform;
use Doctrine\DBAL\Platforms\MySqlPlatform;
use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
use Doctrine\DBAL\Platforms\SqlitePlatform;
Sergei Morozov's avatar
Sergei Morozov committed
13
use Doctrine\DBAL\Schema\Schema;
14
use Doctrine\DBAL\Schema\Table;
Sergei Morozov's avatar
Sergei Morozov committed
15 16
use Doctrine\Tests\DbalFunctionalTestCase;
use Throwable;
17

18
use function array_merge;
19
use function assert;
20
use function chmod;
21
use function exec;
22
use function file_exists;
23 24
use function posix_geteuid;
use function posix_getpwuid;
25 26 27 28
use function sprintf;
use function sys_get_temp_dir;
use function touch;
use function unlink;
29
use function version_compare;
30

Grégoire Paris's avatar
Grégoire Paris committed
31
use const PHP_OS;
32

Sergei Morozov's avatar
Sergei Morozov committed
33
class ExceptionTest extends DbalFunctionalTestCase
34
{
35
    protected function setUp(): void
36 37 38
    {
        parent::setUp();

Sergei Morozov's avatar
Sergei Morozov committed
39
        if ($this->connection->getDriver() instanceof ExceptionConverterDriver) {
Sergei Morozov's avatar
Sergei Morozov committed
40
            return;
41
        }
Sergei Morozov's avatar
Sergei Morozov committed
42 43

        $this->markTestSkipped('Driver does not support special exception handling.');
44 45
    }

46
    public function testPrimaryConstraintViolationException(): void
47
    {
Sergei Morozov's avatar
Sergei Morozov committed
48 49 50
        $table = new Table('duplicatekey_table');
        $table->addColumn('id', 'integer', []);
        $table->setPrimaryKey(['id']);
51

Sergei Morozov's avatar
Sergei Morozov committed
52
        $this->connection->getSchemaManager()->createTable($table);
53

Sergei Morozov's avatar
Sergei Morozov committed
54
        $this->connection->insert('duplicatekey_table', ['id' => 1]);
55

Luís Cobucci's avatar
Luís Cobucci committed
56
        $this->expectException(Exception\UniqueConstraintViolationException::class);
Sergei Morozov's avatar
Sergei Morozov committed
57
        $this->connection->insert('duplicatekey_table', ['id' => 1]);
58
    }
59

60
    public function testTableNotFoundException(): void
61
    {
Sergei Morozov's avatar
Sergei Morozov committed
62
        $sql = 'SELECT * FROM unknown_table';
63

Luís Cobucci's avatar
Luís Cobucci committed
64
        $this->expectException(Exception\TableNotFoundException::class);
Sergei Morozov's avatar
Sergei Morozov committed
65
        $this->connection->executeQuery($sql);
66
    }
67

68
    public function testTableExistsException(): void
69
    {
Sergei Morozov's avatar
Sergei Morozov committed
70
        $schemaManager = $this->connection->getSchemaManager();
Sergei Morozov's avatar
Sergei Morozov committed
71 72 73
        $table         = new Table('alreadyexist_table');
        $table->addColumn('id', 'integer', []);
        $table->setPrimaryKey(['id']);
74

Luís Cobucci's avatar
Luís Cobucci committed
75
        $this->expectException(Exception\TableExistsException::class);
76 77
        $schemaManager->createTable($table);
        $schemaManager->createTable($table);
78
    }
79

80
    public function testForeignKeyConstraintViolationExceptionOnInsert(): void
81
    {
82 83
        if ($this->connection->getDatabasePlatform()->getName() === 'sqlite') {
            $this->connection->exec('PRAGMA foreign_keys=ON');
84 85
        }

86
        $this->setUpForeignKeyConstraintViolationExceptionTest();
87

88
        try {
Sergei Morozov's avatar
Sergei Morozov committed
89 90
            $this->connection->insert('constraint_error_table', ['id' => 1]);
            $this->connection->insert('owning_table', ['id' => 1, 'constraint_id' => 1]);
Sergei Morozov's avatar
Sergei Morozov committed
91
        } catch (Throwable $exception) {
92 93 94 95
            $this->tearDownForeignKeyConstraintViolationExceptionTest();

            throw $exception;
        }
96

Luís Cobucci's avatar
Luís Cobucci committed
97
        $this->expectException(Exception\ForeignKeyConstraintViolationException::class);
98 99

        try {
Sergei Morozov's avatar
Sergei Morozov committed
100
            $this->connection->insert('owning_table', ['id' => 2, 'constraint_id' => 2]);
Luís Cobucci's avatar
Luís Cobucci committed
101
        } catch (Exception\ForeignKeyConstraintViolationException $exception) {
102 103
            $this->tearDownForeignKeyConstraintViolationExceptionTest();

104
            throw $exception;
Sergei Morozov's avatar
Sergei Morozov committed
105
        } catch (Throwable $exception) {
106 107
            $this->tearDownForeignKeyConstraintViolationExceptionTest();

108 109
            throw $exception;
        }
110

111 112 113
        $this->tearDownForeignKeyConstraintViolationExceptionTest();
    }

114
    public function testForeignKeyConstraintViolationExceptionOnUpdate(): void
115
    {
Sergei Morozov's avatar
Sergei Morozov committed
116
        if (! $this->connection->getDatabasePlatform()->supportsForeignKeyConstraints()) {
Sergei Morozov's avatar
Sergei Morozov committed
117
            $this->markTestSkipped('Only fails on platforms with foreign key constraints.');
118 119
        }

120 121
        $this->setUpForeignKeyConstraintViolationExceptionTest();

122
        try {
Sergei Morozov's avatar
Sergei Morozov committed
123 124
            $this->connection->insert('constraint_error_table', ['id' => 1]);
            $this->connection->insert('owning_table', ['id' => 1, 'constraint_id' => 1]);
Sergei Morozov's avatar
Sergei Morozov committed
125
        } catch (Throwable $exception) {
126 127 128 129
            $this->tearDownForeignKeyConstraintViolationExceptionTest();

            throw $exception;
        }
130

Luís Cobucci's avatar
Luís Cobucci committed
131
        $this->expectException(Exception\ForeignKeyConstraintViolationException::class);
132 133

        try {
Sergei Morozov's avatar
Sergei Morozov committed
134
            $this->connection->update('constraint_error_table', ['id' => 2], ['id' => 1]);
Luís Cobucci's avatar
Luís Cobucci committed
135
        } catch (Exception\ForeignKeyConstraintViolationException $exception) {
136 137
            $this->tearDownForeignKeyConstraintViolationExceptionTest();

138
            throw $exception;
Sergei Morozov's avatar
Sergei Morozov committed
139
        } catch (Throwable $exception) {
140 141
            $this->tearDownForeignKeyConstraintViolationExceptionTest();

142 143
            throw $exception;
        }
144 145 146 147

        $this->tearDownForeignKeyConstraintViolationExceptionTest();
    }

148
    public function testForeignKeyConstraintViolationExceptionOnDelete(): void
149
    {
Sergei Morozov's avatar
Sergei Morozov committed
150
        if (! $this->connection->getDatabasePlatform()->supportsForeignKeyConstraints()) {
Sergei Morozov's avatar
Sergei Morozov committed
151
            $this->markTestSkipped('Only fails on platforms with foreign key constraints.');
152 153 154 155
        }

        $this->setUpForeignKeyConstraintViolationExceptionTest();

156
        try {
Sergei Morozov's avatar
Sergei Morozov committed
157 158
            $this->connection->insert('constraint_error_table', ['id' => 1]);
            $this->connection->insert('owning_table', ['id' => 1, 'constraint_id' => 1]);
Sergei Morozov's avatar
Sergei Morozov committed
159
        } catch (Throwable $exception) {
160 161 162 163
            $this->tearDownForeignKeyConstraintViolationExceptionTest();

            throw $exception;
        }
164

Luís Cobucci's avatar
Luís Cobucci committed
165
        $this->expectException(Exception\ForeignKeyConstraintViolationException::class);
166 167

        try {
Sergei Morozov's avatar
Sergei Morozov committed
168
            $this->connection->delete('constraint_error_table', ['id' => 1]);
Luís Cobucci's avatar
Luís Cobucci committed
169
        } catch (Exception\ForeignKeyConstraintViolationException $exception) {
170 171
            $this->tearDownForeignKeyConstraintViolationExceptionTest();

172
            throw $exception;
Sergei Morozov's avatar
Sergei Morozov committed
173
        } catch (Throwable $exception) {
174 175
            $this->tearDownForeignKeyConstraintViolationExceptionTest();

176 177
            throw $exception;
        }
178 179 180 181

        $this->tearDownForeignKeyConstraintViolationExceptionTest();
    }

182
    public function testForeignKeyConstraintViolationExceptionOnTruncate(): void
183
    {
Sergei Morozov's avatar
Sergei Morozov committed
184
        $platform = $this->connection->getDatabasePlatform();
185

Sergei Morozov's avatar
Sergei Morozov committed
186 187
        if (! $platform->supportsForeignKeyConstraints()) {
            $this->markTestSkipped('Only fails on platforms with foreign key constraints.');
188 189 190 191
        }

        $this->setUpForeignKeyConstraintViolationExceptionTest();

192
        try {
Sergei Morozov's avatar
Sergei Morozov committed
193 194
            $this->connection->insert('constraint_error_table', ['id' => 1]);
            $this->connection->insert('owning_table', ['id' => 1, 'constraint_id' => 1]);
Sergei Morozov's avatar
Sergei Morozov committed
195
        } catch (Throwable $exception) {
196 197 198 199
            $this->tearDownForeignKeyConstraintViolationExceptionTest();

            throw $exception;
        }
200

Luís Cobucci's avatar
Luís Cobucci committed
201
        $this->expectException(Exception\ForeignKeyConstraintViolationException::class);
202 203

        try {
Sergei Morozov's avatar
Sergei Morozov committed
204
            $this->connection->executeUpdate($platform->getTruncateTableSQL('constraint_error_table'));
Luís Cobucci's avatar
Luís Cobucci committed
205
        } catch (Exception\ForeignKeyConstraintViolationException $exception) {
206 207
            $this->tearDownForeignKeyConstraintViolationExceptionTest();

208
            throw $exception;
Sergei Morozov's avatar
Sergei Morozov committed
209
        } catch (Throwable $exception) {
210 211
            $this->tearDownForeignKeyConstraintViolationExceptionTest();

212 213
            throw $exception;
        }
214 215

        $this->tearDownForeignKeyConstraintViolationExceptionTest();
216
    }
217

218
    public function testNotNullConstraintViolationException(): void
219
    {
Sergei Morozov's avatar
Sergei Morozov committed
220
        $schema = new Schema();
221

Sergei Morozov's avatar
Sergei Morozov committed
222 223 224 225
        $table = $schema->createTable('notnull_table');
        $table->addColumn('id', 'integer', []);
        $table->addColumn('value', 'integer', ['notnull' => true]);
        $table->setPrimaryKey(['id']);
226

Sergei Morozov's avatar
Sergei Morozov committed
227 228
        foreach ($schema->toSql($this->connection->getDatabasePlatform()) as $sql) {
            $this->connection->exec($sql);
229 230
        }

Luís Cobucci's avatar
Luís Cobucci committed
231
        $this->expectException(Exception\NotNullConstraintViolationException::class);
Sergei Morozov's avatar
Sergei Morozov committed
232
        $this->connection->insert('notnull_table', ['id' => 1, 'value' => null]);
233
    }
234

235
    public function testInvalidFieldNameException(): void
236
    {
Sergei Morozov's avatar
Sergei Morozov committed
237
        $schema = new Schema();
238

239
        $table = $schema->createTable('bad_columnname_table');
Sergei Morozov's avatar
Sergei Morozov committed
240
        $table->addColumn('id', 'integer', []);
241

Sergei Morozov's avatar
Sergei Morozov committed
242 243
        foreach ($schema->toSql($this->connection->getDatabasePlatform()) as $sql) {
            $this->connection->exec($sql);
244 245
        }

Luís Cobucci's avatar
Luís Cobucci committed
246
        $this->expectException(Exception\InvalidFieldNameException::class);
247
        $this->connection->insert('bad_columnname_table', ['name' => 5]);
248 249
    }

250
    public function testNonUniqueFieldNameException(): void
251
    {
Sergei Morozov's avatar
Sergei Morozov committed
252
        $schema = new Schema();
253

Sergei Morozov's avatar
Sergei Morozov committed
254
        $table = $schema->createTable('ambiguous_list_table');
255 256
        $table->addColumn('id', 'integer');

Sergei Morozov's avatar
Sergei Morozov committed
257
        $table2 = $schema->createTable('ambiguous_list_table_2');
258 259
        $table2->addColumn('id', 'integer');

Sergei Morozov's avatar
Sergei Morozov committed
260 261
        foreach ($schema->toSql($this->connection->getDatabasePlatform()) as $sql) {
            $this->connection->exec($sql);
262 263 264
        }

        $sql = 'SELECT id FROM ambiguous_list_table, ambiguous_list_table_2';
Luís Cobucci's avatar
Luís Cobucci committed
265
        $this->expectException(Exception\NonUniqueFieldNameException::class);
Sergei Morozov's avatar
Sergei Morozov committed
266
        $this->connection->executeQuery($sql);
267 268
    }

269
    public function testUniqueConstraintViolationException(): void
270
    {
Sergei Morozov's avatar
Sergei Morozov committed
271
        $schema = new Schema();
272

273
        $table = $schema->createTable('unique_column_table');
274
        $table->addColumn('id', 'integer');
Sergei Morozov's avatar
Sergei Morozov committed
275
        $table->addUniqueIndex(['id']);
276

Sergei Morozov's avatar
Sergei Morozov committed
277 278
        foreach ($schema->toSql($this->connection->getDatabasePlatform()) as $sql) {
            $this->connection->exec($sql);
279
        }
280

281
        $this->connection->insert('unique_column_table', ['id' => 5]);
Luís Cobucci's avatar
Luís Cobucci committed
282
        $this->expectException(Exception\UniqueConstraintViolationException::class);
283
        $this->connection->insert('unique_column_table', ['id' => 5]);
284 285
    }

286
    public function testSyntaxErrorException(): void
287
    {
Sergei Morozov's avatar
Sergei Morozov committed
288 289 290
        $table = new Table('syntax_error_table');
        $table->addColumn('id', 'integer', []);
        $table->setPrimaryKey(['id']);
291

Sergei Morozov's avatar
Sergei Morozov committed
292
        $this->connection->getSchemaManager()->createTable($table);
293 294

        $sql = 'SELECT id FRO syntax_error_table';
Luís Cobucci's avatar
Luís Cobucci committed
295
        $this->expectException(Exception\SyntaxErrorException::class);
Sergei Morozov's avatar
Sergei Morozov committed
296
        $this->connection->executeQuery($sql);
297 298
    }

299
    public function testConnectionExceptionSqLite(): void
300
    {
301
        if (! ($this->connection->getDatabasePlatform() instanceof SqlitePlatform)) {
Sergei Morozov's avatar
Sergei Morozov committed
302
            $this->markTestSkipped('Only fails this way on sqlite');
303 304
        }

305 306 307
        // mode 0 is considered read-only on Windows
        $mode = PHP_OS === 'Linux' ? 0444 : 0000;

Sergei Morozov's avatar
Sergei Morozov committed
308
        $filename = sprintf('%s/%s', sys_get_temp_dir(), 'doctrine_failed_connection_' . $mode . '.db');
309 310

        if (file_exists($filename)) {
311
            $this->cleanupReadOnlyFile($filename);
312
        }
313 314 315 316

        touch($filename);
        chmod($filename, $mode);

317 318 319 320
        if ($this->isLinuxRoot()) {
            exec(sprintf('chattr +i %s', $filename));
        }

Sergei Morozov's avatar
Sergei Morozov committed
321
        $params = [
322 323
            'driver' => 'pdo_sqlite',
            'path'   => $filename,
Sergei Morozov's avatar
Sergei Morozov committed
324 325
        ];
        $conn   = DriverManager::getConnection($params);
326

Sergei Morozov's avatar
Sergei Morozov committed
327 328
        $schema = new Schema();
        $table  = $schema->createTable('no_connection');
329 330
        $table->addColumn('id', 'integer');

331
        $this->expectException(Exception\ReadOnlyException::class);
332 333
        $this->expectExceptionMessage(
            <<<EOT
334
An exception occurred while executing 'CREATE TABLE no_connection (id INTEGER NOT NULL)':
335

336 337 338 339 340 341 342 343 344 345 346
SQLSTATE[HY000]: General error: 8 attempt to write a readonly database
EOT
        );

        try {
            foreach ($schema->toSql($conn->getDatabasePlatform()) as $sql) {
                $conn->exec($sql);
            }
        } finally {
            $this->cleanupReadOnlyFile($filename);
        }
347 348
    }

349
    /**
350 351
     * @param array<string, mixed> $params
     *
352 353
     * @dataProvider getConnectionParams
     */
354
    public function testConnectionException(array $params): void
355
    {
356 357 358
        $platform = $this->connection->getDatabasePlatform();

        if ($platform instanceof SqlitePlatform) {
Sergei Morozov's avatar
Sergei Morozov committed
359
            $this->markTestSkipped('Only skipped if platform is not sqlite');
360 361
        }

362
        if ($platform instanceof DrizzlePlatform) {
Sergei Morozov's avatar
Sergei Morozov committed
363
            $this->markTestSkipped('Drizzle does not always support authentication');
364 365
        }

366
        if ($platform instanceof PostgreSqlPlatform && isset($params['password'])) {
Sergei Morozov's avatar
Sergei Morozov committed
367
            $this->markTestSkipped('Does not work on Travis');
Benjamin Eberlei's avatar
Benjamin Eberlei committed
368 369
        }

370 371 372 373 374 375 376 377 378
        if ($platform instanceof MySqlPlatform && isset($params['user'])) {
            $wrappedConnection = $this->connection->getWrappedConnection();
            assert($wrappedConnection instanceof ServerInfoAwareConnection);

            if (version_compare($wrappedConnection->getServerVersion(), '8', '>=')) {
                $this->markTestIncomplete('PHP currently does not completely support MySQL 8');
            }
        }

Sergei Morozov's avatar
Sergei Morozov committed
379
        $defaultParams = $this->connection->getParams();
Sergei Morozov's avatar
Sergei Morozov committed
380
        $params        = array_merge($defaultParams, $params);
381

Sergei Morozov's avatar
Sergei Morozov committed
382
        $conn = DriverManager::getConnection($params);
383

Sergei Morozov's avatar
Sergei Morozov committed
384 385
        $schema = new Schema();
        $table  = $schema->createTable('no_connection');
386 387
        $table->addColumn('id', 'integer');

Luís Cobucci's avatar
Luís Cobucci committed
388
        $this->expectException(Exception\ConnectionException::class);
389

jeroendedauw's avatar
jeroendedauw committed
390
        foreach ($schema->toSql($conn->getDatabasePlatform()) as $sql) {
391
            $conn->exec($sql);
392
        }
393 394
    }

395 396 397
    /**
     * @return array<int, array<int, mixed>>
     */
398
    public static function getConnectionParams(): iterable
399
    {
Sergei Morozov's avatar
Sergei Morozov committed
400 401 402 403 404
        return [
            [['user' => 'not_existing']],
            [['password' => 'really_not']],
            [['host' => 'localnope']],
        ];
405
    }
406

407
    private function setUpForeignKeyConstraintViolationExceptionTest(): void
408
    {
Sergei Morozov's avatar
Sergei Morozov committed
409
        $schemaManager = $this->connection->getSchemaManager();
410

Sergei Morozov's avatar
Sergei Morozov committed
411 412 413
        $table = new Table('constraint_error_table');
        $table->addColumn('id', 'integer', []);
        $table->setPrimaryKey(['id']);
414

Sergei Morozov's avatar
Sergei Morozov committed
415 416 417 418 419
        $owningTable = new Table('owning_table');
        $owningTable->addColumn('id', 'integer', []);
        $owningTable->addColumn('constraint_id', 'integer', []);
        $owningTable->setPrimaryKey(['id']);
        $owningTable->addForeignKeyConstraint($table, ['constraint_id'], ['id']);
420 421 422 423 424

        $schemaManager->createTable($table);
        $schemaManager->createTable($owningTable);
    }

425
    private function tearDownForeignKeyConstraintViolationExceptionTest(): void
426
    {
Sergei Morozov's avatar
Sergei Morozov committed
427
        $schemaManager = $this->connection->getSchemaManager();
428 429 430 431

        $schemaManager->dropTable('owning_table');
        $schemaManager->dropTable('constraint_error_table');
    }
432

433
    private function isLinuxRoot(): bool
434 435 436 437
    {
        return PHP_OS === 'Linux' && posix_getpwuid(posix_geteuid())['name'] === 'root';
    }

438
    private function cleanupReadOnlyFile(string $filename): void
439 440 441 442 443 444 445 446
    {
        if ($this->isLinuxRoot()) {
            exec(sprintf('chattr -i %s', $filename));
        }

        chmod($filename, 0200); // make the file writable again, so it can be removed on Windows
        unlink($filename);
    }
447
}