Skip to content

Commit fb2b723

Browse files
committed
Fix: #6146 - utf8/utf8mb3 alias causes redundant ALTER TABLE changes
1 parent cbd0e9a commit fb2b723

14 files changed

+314
-8
lines changed

src/Platforms/AbstractMySQLPlatform.php

+10
Original file line numberDiff line numberDiff line change
@@ -839,4 +839,14 @@ private function indexAssetsByLowerCaseName(array $assets): array
839839

840840
return $result;
841841
}
842+
843+
/**
844+
* INFORMATION_SCHEMA.CHARACTER_SETS will contain either 'utf8' or 'utf8mb3' but not both.
845+
*/
846+
public function informationSchemaUsesUtf8mb3(Connection $connection): bool
847+
{
848+
$sql = "SELECT character_set_name FROM INFORMATION_SCHEMA.CHARACTER_SETS WHERE character_set_name = 'utf8mb3'";
849+
850+
return $connection->fetchOne($sql) === 'utf8mb3';
851+
}
842852
}

src/Platforms/MySQL/CharsetMetadataProvider.php

+2
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@
77
/** @internal */
88
interface CharsetMetadataProvider
99
{
10+
public function normalizeCharset(string $charset): string;
11+
1012
public function getDefaultCharsetCollation(string $charset): ?string;
1113
}

src/Platforms/MySQL/CharsetMetadataProvider/CachingCharsetMetadataProvider.php

+5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ public function __construct(private readonly CharsetMetadataProvider $charsetMet
1818
{
1919
}
2020

21+
public function normalizeCharset(string $charset): string
22+
{
23+
return $this->charsetMetadataProvider->normalizeCharset($charset);
24+
}
25+
2126
public function getDefaultCharsetCollation(string $charset): ?string
2227
{
2328
if (array_key_exists($charset, $this->cache)) {

src/Platforms/MySQL/CharsetMetadataProvider/ConnectionCharsetMetadataProvider.php

+16-1
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,28 @@
1111
/** @internal */
1212
final class ConnectionCharsetMetadataProvider implements CharsetMetadataProvider
1313
{
14-
public function __construct(private readonly Connection $connection)
14+
public function __construct(private readonly Connection $connection, private bool $useUtf8mb3)
1515
{
1616
}
1717

18+
public function normalizeCharset(string $charset): string
19+
{
20+
if ($this->useUtf8mb3 && $charset === 'utf8') {
21+
return 'utf8mb3';
22+
}
23+
24+
if (! $this->useUtf8mb3 && $charset === 'utf8mb3') {
25+
return 'utf8';
26+
}
27+
28+
return $charset;
29+
}
30+
1831
/** @throws Exception */
1932
public function getDefaultCharsetCollation(string $charset): ?string
2033
{
34+
$charset = $this->normalizeCharset($charset);
35+
2136
$collation = $this->connection->fetchOne(
2237
<<<'SQL'
2338
SELECT DEFAULT_COLLATE_NAME

src/Platforms/MySQL/CollationMetadataProvider.php

+2
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@
77
/** @internal */
88
interface CollationMetadataProvider
99
{
10+
public function normalizeCollation(string $collation): string;
11+
1012
public function getCollationCharset(string $collation): ?string;
1113
}

src/Platforms/MySQL/CollationMetadataProvider/CachingCollationMetadataProvider.php

+5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ public function __construct(private readonly CollationMetadataProvider $collatio
1818
{
1919
}
2020

21+
public function normalizeCollation(string $collation): string
22+
{
23+
return $this->collationMetadataProvider->normalizeCollation($collation);
24+
}
25+
2126
public function getCollationCharset(string $collation): ?string
2227
{
2328
if (array_key_exists($collation, $this->cache)) {

src/Platforms/MySQL/CollationMetadataProvider/ConnectionCollationMetadataProvider.php

+19-1
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,34 @@
88
use Doctrine\DBAL\Exception;
99
use Doctrine\DBAL\Platforms\MySQL\CollationMetadataProvider;
1010

11+
use function str_starts_with;
12+
use function substr;
13+
1114
/** @internal */
1215
final class ConnectionCollationMetadataProvider implements CollationMetadataProvider
1316
{
14-
public function __construct(private readonly Connection $connection)
17+
public function __construct(private readonly Connection $connection, private bool $useUtf8mb3)
1518
{
1619
}
1720

21+
public function normalizeCollation(string $collation): string
22+
{
23+
if ($this->useUtf8mb3 && str_starts_with($collation, 'utf8_')) {
24+
return 'utf8mb3' . substr($collation, 4);
25+
}
26+
27+
if (! $this->useUtf8mb3 && str_starts_with($collation, 'utf8mb3_')) {
28+
return 'utf8' . substr($collation, 7);
29+
}
30+
31+
return $collation;
32+
}
33+
1834
/** @throws Exception */
1935
public function getCollationCharset(string $collation): ?string
2036
{
37+
$collation = $this->normalizeCollation($collation);
38+
2139
$charset = $this->connection->fetchOne(
2240
<<<'SQL'
2341
SELECT CHARACTER_SET_NAME

src/Platforms/MySQL/Comparator.php

+14-4
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,20 @@ private function normalizeTable(Table $table): Table
8282
*/
8383
private function normalizeOptions(array $options): array
8484
{
85-
if (isset($options['charset']) && ! isset($options['collation'])) {
86-
$options['collation'] = $this->charsetMetadataProvider->getDefaultCharsetCollation($options['charset']);
87-
} elseif (isset($options['collation']) && ! isset($options['charset'])) {
88-
$options['charset'] = $this->collationMetadataProvider->getCollationCharset($options['collation']);
85+
if (isset($options['charset'])) {
86+
$options['charset'] = $this->charsetMetadataProvider->normalizeCharset($options['charset']);
87+
88+
if (! isset($options['collation'])) {
89+
$options['collation'] = $this->charsetMetadataProvider->getDefaultCharsetCollation($options['charset']);
90+
}
91+
}
92+
93+
if (isset($options['collation'])) {
94+
$options['collation'] = $this->collationMetadataProvider->normalizeCollation($options['collation']);
95+
96+
if (! isset($options['charset'])) {
97+
$options['charset'] = $this->collationMetadataProvider->getCollationCharset($options['collation']);
98+
}
8999
}
90100

91101
return $options;

src/Schema/MySQLSchemaManager.php

+4-2
Original file line numberDiff line numberDiff line change
@@ -316,13 +316,15 @@ protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey)
316316
/** @throws Exception */
317317
public function createComparator(): Comparator
318318
{
319+
$useUtf8mb3 = $this->platform->informationSchemaUsesUtf8mb3($this->connection);
320+
319321
return new MySQL\Comparator(
320322
$this->platform,
321323
new CachingCharsetMetadataProvider(
322-
new ConnectionCharsetMetadataProvider($this->connection),
324+
new ConnectionCharsetMetadataProvider($this->connection, $useUtf8mb3),
323325
),
324326
new CachingCollationMetadataProvider(
325-
new ConnectionCollationMetadataProvider($this->connection),
327+
new ConnectionCollationMetadataProvider($this->connection, $useUtf8mb3),
326328
),
327329
$this->getDefaultTableOptions(),
328330
);
+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\DBAL\Tests\Functional\Driver;
6+
7+
use Doctrine\DBAL\Schema\Column;
8+
use Doctrine\DBAL\Schema\Schema;
9+
use Doctrine\DBAL\Schema\Table;
10+
use Doctrine\DBAL\Tests\FunctionalTestCase;
11+
use Doctrine\DBAL\Tests\TestUtil;
12+
use Doctrine\DBAL\Types\StringType;
13+
use PHPUnit\Framework\Attributes\DataProvider;
14+
15+
class DBAL6146Test extends FunctionalTestCase
16+
{
17+
protected function setUp(): void
18+
{
19+
parent::setUp();
20+
21+
if (TestUtil::isDriverOneOf('pdo_mysql', 'mysqli')) {
22+
return;
23+
}
24+
25+
self::markTestSkipped('This test requires the pdo_mysql or the mysqli driver.');
26+
}
27+
28+
/** @return iterable<array{array<string,string>,array<string,string>}> */
29+
public static function equivalentCharsetAndCollationProvider(): iterable
30+
{
31+
yield [[], []];
32+
yield [['charset' => 'utf8'], ['charset' => 'utf8']];
33+
yield [['charset' => 'utf8'], ['charset' => 'utf8mb3']];
34+
yield [['charset' => 'utf8mb3'], ['charset' => 'utf8']];
35+
yield [['charset' => 'utf8mb3'], ['charset' => 'utf8mb3']];
36+
yield [['collation' => 'utf8_unicode_ci'], ['collation' => 'utf8_unicode_ci']];
37+
yield [['collation' => 'utf8_unicode_ci'], ['collation' => 'utf8mb3_unicode_ci']];
38+
yield [['collation' => 'utf8mb3_unicode_ci'], ['collation' => 'utf8mb3_unicode_ci']];
39+
yield [['collation' => 'utf8mb3_unicode_ci'], ['collation' => 'utf8_unicode_ci']];
40+
yield [
41+
['charset' => 'utf8', 'collation' => 'utf8_unicode_ci'],
42+
['charset' => 'utf8', 'collation' => 'utf8_unicode_ci'],
43+
];
44+
45+
yield [
46+
['charset' => 'utf8', 'collation' => 'utf8_unicode_ci'],
47+
['charset' => 'utf8mb3', 'collation' => 'utf8mb3_unicode_ci'],
48+
];
49+
50+
yield [
51+
['charset' => 'utf8mb3', 'collation' => 'utf8mb3_unicode_ci'],
52+
['charset' => 'utf8', 'collation' => 'utf8_unicode_ci'],
53+
];
54+
55+
yield [
56+
['charset' => 'utf8mb3', 'collation' => 'utf8mb3_unicode_ci'],
57+
['charset' => 'utf8mb3', 'collation' => 'utf8mb3_unicode_ci'],
58+
];
59+
}
60+
61+
/**
62+
* @param array<string,string> $options1
63+
* @param array<string,string> $options2
64+
*/
65+
#[DataProvider('equivalentCharsetAndCollationProvider')]
66+
public function testThereAreNoRedundantAlterTableStatements(array $options1, array $options2): void
67+
{
68+
$column1 = new Column('bar', new StringType(), ['length' => 32, 'platformOptions' => $options1]);
69+
$table1 = new Table(name: 'foo6146', columns: [$column1]);
70+
71+
$column2 = new Column('bar', new StringType(), ['length' => 32, 'platformOptions' => $options2]);
72+
$table2 = new Table(name: 'foo6146', columns: [$column2]);
73+
74+
$this->dropAndCreateTable($table1);
75+
76+
$schemaManager = $this->connection->createSchemaManager();
77+
$oldSchema = $schemaManager->introspectSchema();
78+
$newSchema = new Schema([$table2]);
79+
$comparator = $schemaManager->createComparator();
80+
$schemaDiff = $comparator->compareSchemas($oldSchema, $newSchema);
81+
$alteredTables = $schemaDiff->getAlteredTables();
82+
83+
self::assertEmpty($alteredTables);
84+
}
85+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\DBAL\Tests\Platforms\MySQL\CollationMetadataProvider;
6+
7+
use Doctrine\DBAL\Connection;
8+
use Doctrine\DBAL\Platforms\MySQL\CharsetMetadataProvider\ConnectionCharsetMetadataProvider;
9+
use PHPUnit\Framework\TestCase;
10+
11+
class ConnectionCharsetMetadataProviderTest extends TestCase
12+
{
13+
public function testNormalizeCharset(): void
14+
{
15+
$connection = $this->createMock(Connection::class);
16+
17+
$utf8Provider = new ConnectionCharsetMetadataProvider($connection, false);
18+
19+
self::assertSame('utf8', $utf8Provider->normalizeCharset('utf8'));
20+
self::assertSame('utf8', $utf8Provider->normalizeCharset('utf8mb3'));
21+
self::assertSame('foobar', $utf8Provider->normalizeCharset('foobar'));
22+
23+
$utf8mb3Provider = new ConnectionCharsetMetadataProvider($connection, true);
24+
25+
self::assertSame('utf8mb3', $utf8mb3Provider->normalizeCharset('utf8'));
26+
self::assertSame('utf8mb3', $utf8mb3Provider->normalizeCharset('utf8mb3'));
27+
self::assertSame('foobar', $utf8mb3Provider->normalizeCharset('foobar'));
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\DBAL\Tests\Platforms\MySQL\CollationMetadataProvider;
6+
7+
use Doctrine\DBAL\Connection;
8+
use Doctrine\DBAL\Platforms\MySQL\CollationMetadataProvider\ConnectionCollationMetadataProvider;
9+
use PHPUnit\Framework\TestCase;
10+
11+
class ConnectionCollationMetadataProviderTest extends TestCase
12+
{
13+
public function testNormalizeCcollation(): void
14+
{
15+
$connection = $this->createMock(Connection::class);
16+
17+
$utf8Provider = new ConnectionCollationMetadataProvider($connection, false);
18+
19+
self::assertSame('utf8_unicode_ci', $utf8Provider->normalizeCollation('utf8_unicode_ci'));
20+
self::assertSame('utf8_unicode_ci', $utf8Provider->normalizeCollation('utf8mb3_unicode_ci'));
21+
self::assertSame('foobar_unicode_ci', $utf8Provider->normalizeCollation('foobar_unicode_ci'));
22+
23+
$utf8mb3Provider = new ConnectionCollationMetadataProvider($connection, true);
24+
25+
self::assertSame('utf8mb3_unicode_ci', $utf8mb3Provider->normalizeCollation('utf8_unicode_ci'));
26+
self::assertSame('utf8mb3_unicode_ci', $utf8mb3Provider->normalizeCollation('utf8mb3_unicode_ci'));
27+
self::assertSame('foobar_unicode_ci', $utf8mb3Provider->normalizeCollation('foobar_unicode_ci'));
28+
}
29+
}

0 commit comments

Comments
 (0)