Skip to content

Commit d052b75

Browse files
minor #61582 [DoctrineBridge] Use a single table in isSameDatabaseChecker (GromNaN)
This PR was squashed before being merged into the 7.4 branch. Discussion ---------- [DoctrineBridge] Use a single table in isSameDatabaseChecker | Q | A | ------------- | --- | Branch? | 7.4 | Bug fix? | yes | New feature? | yes | Deprecations? | no | Issues | Fix #54348 | License | MIT The `isSameDatabaseChecker` feature of the SchemaListener detect if 2 DBAL connections are connected to the same server and database by creating a temporary table with one connection and checking if the table exists using the other connection. Creating a temporary table with a random name cause an issue to configure permissions, as an exhaustive list of table names can be required. I propose to modify the implementation to always use the same table name, but insert a row with a random key with one connection and try to delete it with the other connection. The table is dropped only if it is empty, to prevent concurrence issues. Commits ------- e2cc570 [DoctrineBridge] Use a single table in isSameDatabaseChecker
2 parents c8de3de + e2cc570 commit d052b75

File tree

3 files changed

+81
-11
lines changed

3 files changed

+81
-11
lines changed

src/Symfony/Bridge/Doctrine/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Deprecate `UniqueEntity::getRequiredOptions()` and `UniqueEntity::getDefaultOption()`
8+
* Use a single table named `_schema_subscriber_check` in schema listeners to detect same database connections
89

910
7.3
1011
---

src/Symfony/Bridge/Doctrine/SchemaListener/AbstractSchemaListener.php

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
namespace Symfony\Bridge\Doctrine\SchemaListener;
1313

1414
use Doctrine\DBAL\Connection;
15+
use Doctrine\DBAL\Exception\ConnectionException;
16+
use Doctrine\DBAL\Exception\DatabaseObjectExistsException;
1517
use Doctrine\DBAL\Exception\DatabaseObjectNotFoundException;
16-
use Doctrine\DBAL\Exception\TableNotFoundException;
1718
use Doctrine\DBAL\Schema\Name\Identifier;
1819
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
1920
use Doctrine\DBAL\Schema\PrimaryKeyConstraint;
@@ -29,32 +30,47 @@ protected function getIsSameDatabaseChecker(Connection $connection): \Closure
2930
{
3031
return static function (\Closure $exec) use ($connection): bool {
3132
$schemaManager = method_exists($connection, 'createSchemaManager') ? $connection->createSchemaManager() : $connection->getSchemaManager();
32-
$checkTable = 'schema_subscriber_check_'.bin2hex(random_bytes(7));
33-
$table = new Table($checkTable);
33+
$key = bin2hex(random_bytes(7));
34+
$table = new Table('_schema_subscriber_check');
3435
$table->addColumn('id', Types::INTEGER)
3536
->setAutoincrement(true)
3637
->setNotnull(true);
38+
$table->addColumn('key', Types::STRING)
39+
->setLength(14)
40+
->setNotNull(true)
41+
;
3742

3843
if (class_exists(PrimaryKeyConstraint::class)) {
3944
$table->addPrimaryKeyConstraint(new PrimaryKeyConstraint(null, [new UnqualifiedName(Identifier::unquoted('id'))], true));
4045
} else {
4146
$table->setPrimaryKey(['id']);
4247
}
4348

44-
$schemaManager->createTable($table);
49+
try {
50+
$schemaManager->createTable($table);
51+
} catch (DatabaseObjectExistsException) {
52+
}
53+
54+
$connection->executeStatement('INSERT INTO _schema_subscriber_check (key) VALUES (:key)', ['key' => $key], ['key' => Types::STRING]);
4555

4656
try {
47-
$exec(\sprintf('DROP TABLE %s', $checkTable));
48-
} catch (\Exception) {
49-
// ignore
57+
$exec('DELETE FROM _schema_subscriber_check WHERE key == :key', ['key' => $key], ['key' => Types::STRING]);
58+
} catch (DatabaseObjectNotFoundException|ConnectionException) {
5059
}
5160

5261
try {
53-
$schemaManager->dropTable($checkTable);
62+
$rowCount = $connection->executeStatement('DELETE FROM _schema_subscriber_check WHERE key == :key', ['key' => $key], ['key' => Types::STRING]);
63+
64+
return 0 === $rowCount;
65+
} finally {
66+
[$count] = $connection->executeQuery('SELECT count(id) FROM _schema_subscriber_check')->fetchOne();
5467

55-
return false;
56-
} catch (DatabaseObjectNotFoundException) {
57-
return true;
68+
if (!$count) {
69+
try {
70+
$schemaManager->dropTable('_schema_subscriber_check');
71+
} catch (DatabaseObjectNotFoundException) {
72+
}
73+
}
5874
}
5975
};
6076
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace SchemaListener;
13+
14+
use Doctrine\DBAL\Connection;
15+
use Doctrine\DBAL\DriverManager;
16+
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
17+
use PHPUnit\Framework\TestCase;
18+
use Symfony\Bridge\Doctrine\SchemaListener\AbstractSchemaListener;
19+
20+
#[RequiresPhpExtension('pdo_sqlite')]
21+
class AbstractSchemaListenerTest extends TestCase
22+
{
23+
public function testSameDatabaseChecker()
24+
{
25+
$connectionParams = [
26+
'dbname' => ':memory:',
27+
'driver' => 'pdo_sqlite',
28+
];
29+
// Create two distinct in-memory SQLite databases
30+
$connection1 = DriverManager::getConnection($connectionParams);
31+
$connection2 = DriverManager::getConnection($connectionParams);
32+
33+
self::assertTrue($this->getIsSameDatabaseChecker($connection1)($connection1->executeStatement(...)));
34+
self::assertFalse($this->getIsSameDatabaseChecker($connection1)($connection2->executeStatement(...)));
35+
36+
$remainingTables = $connection1->executeQuery('SELECT name FROM sqlite_schema WHERE name <> "sqlite_sequence"')->fetchFirstColumn();
37+
self::assertSame([], $remainingTables, 'Temporary table was dropped');
38+
}
39+
40+
private function getIsSameDatabaseChecker(Connection $connection): \Closure
41+
{
42+
return (new class extends AbstractSchemaListener {
43+
public function postGenerateSchema($event): void
44+
{
45+
}
46+
47+
public function getIsSameDatabaseChecker(Connection $connection): \Closure
48+
{
49+
return parent::getIsSameDatabaseChecker($connection);
50+
}
51+
})->getIsSameDatabaseChecker($connection);
52+
}
53+
}

0 commit comments

Comments
 (0)