diff --git a/src/Platforms/MySQL/CharsetMetadataProvider.php b/src/Platforms/MySQL/CharsetMetadataProvider.php new file mode 100644 index 00000000000..86d9d01d43d --- /dev/null +++ b/src/Platforms/MySQL/CharsetMetadataProvider.php @@ -0,0 +1,13 @@ + */ + private array $cache = []; + + public function __construct(private CharsetMetadataProvider $charsetMetadataProvider) + { + } + + public function getDefaultCharsetCollation(string $charset): ?string + { + if (array_key_exists($charset, $this->cache)) { + return $this->cache[$charset]; + } + + return $this->cache[$charset] = $this->charsetMetadataProvider->getDefaultCharsetCollation($charset); + } +} diff --git a/src/Platforms/MySQL/CharsetMetadataProvider/ConnectionCharsetMetadataProvider.php b/src/Platforms/MySQL/CharsetMetadataProvider/ConnectionCharsetMetadataProvider.php new file mode 100644 index 00000000000..0c743ccdf33 --- /dev/null +++ b/src/Platforms/MySQL/CharsetMetadataProvider/ConnectionCharsetMetadataProvider.php @@ -0,0 +1,41 @@ +connection->fetchOne( + <<<'SQL' + SELECT DEFAULT_COLLATE_NAME + FROM information_schema.CHARACTER_SETS + WHERE CHARACTER_SET_NAME = ?; + SQL + , + [$charset] + ); + + if ($collation !== false) { + return $collation; + } + + return null; + } +} diff --git a/src/Platforms/MySQL/Comparator.php b/src/Platforms/MySQL/Comparator.php index 9c9dacca770..6064465cb5f 100644 --- a/src/Platforms/MySQL/Comparator.php +++ b/src/Platforms/MySQL/Comparator.php @@ -10,7 +10,6 @@ use Doctrine\DBAL\Schema\TableDiff; use function array_diff_assoc; -use function array_intersect_key; /** * Compares schemas in the context of MySQL platform. @@ -21,32 +20,54 @@ */ class Comparator extends BaseComparator { + private CharsetMetadataProvider $charsetMetadataProvider; + private CollationMetadataProvider $collationMetadataProvider; + private DefaultTableOptions $defaultTableOptions; + /** * @internal The comparator can be only instantiated by a schema manager. */ - public function __construct(AbstractMySQLPlatform $platform, CollationMetadataProvider $collationMetadataProvider) - { + public function __construct( + AbstractMySQLPlatform $platform, + CharsetMetadataProvider $charsetMetadataProvider, + CollationMetadataProvider $collationMetadataProvider, + DefaultTableOptions $defaultTableOptions + ) { parent::__construct($platform); + $this->charsetMetadataProvider = $charsetMetadataProvider; $this->collationMetadataProvider = $collationMetadataProvider; + $this->defaultTableOptions = $defaultTableOptions; } public function diffTable(Table $fromTable, Table $toTable): ?TableDiff { return parent::diffTable( - $this->normalizeColumns($fromTable), - $this->normalizeColumns($toTable) + $this->normalizeTable($fromTable), + $this->normalizeTable($toTable) ); } - private function normalizeColumns(Table $table): Table + private function normalizeTable(Table $table): Table { - $tableOptions = array_intersect_key($table->getOptions(), [ - 'charset' => null, - 'collation' => null, - ]); + $charset = $table->getOption('charset'); + $collation = $table->getOption('collation'); + + if ($charset === null && $collation !== null) { + $charset = $this->collationMetadataProvider->getCollationCharset($collation); + } elseif ($charset !== null && $collation === null) { + $collation = $this->charsetMetadataProvider->getDefaultCharsetCollation($charset); + } elseif ($charset === null && $collation === null) { + $charset = $this->defaultTableOptions->getCharset(); + $collation = $this->defaultTableOptions->getCollation(); + } + + $tableOptions = [ + 'charset' => $charset, + 'collation' => $collation, + ]; $table = clone $table; @@ -69,16 +90,14 @@ private function normalizeColumns(Table $table): Table /** * @param array $options * - * @return array + * @return array */ private function normalizeOptions(array $options): array { - if (isset($options['collation']) && ! isset($options['charset'])) { - $charset = $this->collationMetadataProvider->getCollationCharset($options['collation']); - - if ($charset !== null) { - $options['charset'] = $charset; - } + if (isset($options['charset']) && ! isset($options['collation'])) { + $options['collation'] = $this->charsetMetadataProvider->getDefaultCharsetCollation($options['charset']); + } elseif (isset($options['collation']) && ! isset($options['charset'])) { + $options['charset'] = $this->collationMetadataProvider->getCollationCharset($options['collation']); } return $options; diff --git a/src/Platforms/MySQL/DefaultTableOptions.php b/src/Platforms/MySQL/DefaultTableOptions.php new file mode 100644 index 00000000000..47d72fd799d --- /dev/null +++ b/src/Platforms/MySQL/DefaultTableOptions.php @@ -0,0 +1,25 @@ +charset; + } + + public function getCollation(): string + { + return $this->collation; + } +} diff --git a/src/Schema/MySQLSchemaManager.php b/src/Schema/MySQLSchemaManager.php index f5835486d8c..7d7823b18b1 100644 --- a/src/Schema/MySQLSchemaManager.php +++ b/src/Schema/MySQLSchemaManager.php @@ -4,11 +4,15 @@ namespace Doctrine\DBAL\Schema; +use Doctrine\DBAL\Exception; use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; use Doctrine\DBAL\Platforms\MariaDBPlatform; use Doctrine\DBAL\Platforms\MySQL; +use Doctrine\DBAL\Platforms\MySQL\CharsetMetadataProvider\CachingCharsetMetadataProvider; +use Doctrine\DBAL\Platforms\MySQL\CharsetMetadataProvider\ConnectionCharsetMetadataProvider; use Doctrine\DBAL\Platforms\MySQL\CollationMetadataProvider\CachingCollationMetadataProvider; use Doctrine\DBAL\Platforms\MySQL\CollationMetadataProvider\ConnectionCollationMetadataProvider; +use Doctrine\DBAL\Platforms\MySQL\DefaultTableOptions; use Doctrine\DBAL\Result; use Doctrine\DBAL\Types\Type; @@ -52,6 +56,8 @@ class MySQLSchemaManager extends AbstractSchemaManager "''" => "'", ]; + private ?DefaultTableOptions $defaultTableOptions = null; + /** * {@inheritdoc} */ @@ -315,13 +321,20 @@ protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey) ); } + /** + * @throws Exception + */ public function createComparator(): Comparator { return new MySQL\Comparator( $this->_platform, + new CachingCharsetMetadataProvider( + new ConnectionCharsetMetadataProvider($this->_conn) + ), new CachingCollationMetadataProvider( new ConnectionCollationMetadataProvider($this->_conn) - ) + ), + $this->getDefaultTableOptions() ); } @@ -510,4 +523,22 @@ private function parseCreateOptions(?string $string): array return $options; } + + /** + * @throws Exception + */ + private function getDefaultTableOptions(): DefaultTableOptions + { + if ($this->defaultTableOptions === null) { + $row = $this->_conn->fetchNumeric( + 'SELECT @@character_set_database, @@collation_database', + ); + + assert($row !== false); + + $this->defaultTableOptions = new DefaultTableOptions(...$row); + } + + return $this->defaultTableOptions; + } } diff --git a/src/Schema/Table.php b/src/Schema/Table.php index 70e8d69bf43..646e6445451 100644 --- a/src/Schema/Table.php +++ b/src/Schema/Table.php @@ -598,7 +598,7 @@ public function hasOption(string $name): bool public function getOption(string $name): mixed { - return $this->_options[$name]; + return $this->_options[$name] ?? null; } /** diff --git a/tests/Functional/Schema/MySQL/ComparatorTest.php b/tests/Functional/Schema/MySQL/ComparatorTest.php index d3775126abd..8b17a4d739c 100644 --- a/tests/Functional/Schema/MySQL/ComparatorTest.php +++ b/tests/Functional/Schema/MySQL/ComparatorTest.php @@ -136,13 +136,20 @@ public function testChangeColumnCollation(): void ComparatorTestUtils::assertDiffNotEmpty($this->connection, $this->comparator, $table); } - public function testImplicitColumnCharset(): void + /** + * @param array $tableOptions + * @param array $columnOptions + * + * @dataProvider tableAndColumnOptionsProvider + */ + public function testTableAndColumnOptions(array $tableOptions, array $columnOptions): void { - $table = new Table('comparator_test'); + $table = new Table('comparator_test', [], [], [], [], $tableOptions); $table->addColumn('name', Types::STRING, [ 'length' => 32, - 'platformOptions' => ['collation' => 'ascii_general_ci'], + 'platformOptions' => $columnOptions, ]); + $this->dropAndCreateTable($table); self::assertNull(ComparatorTestUtils::diffFromActualToDesiredTable( @@ -158,6 +165,27 @@ public function testImplicitColumnCharset(): void )); } + /** + * @return iterable,array}> + */ + public static function tableAndColumnOptionsProvider(): iterable + { + yield "Column collation explicitly set to its table's default" => [ + [], + ['collation' => 'utf8mb4_unicode_ci'], + ]; + + yield "Column charset implicitly set to a value matching its table's charset" => [ + ['charset' => 'utf8mb4'], + ['collation' => 'utf8mb4_unicode_ci'], + ]; + + yield "Column collation reset to the collation's default matching its table's charset" => [ + ['collation' => 'utf8mb4_unicode_ci'], + ['charset' => 'utf8mb4'], + ]; + } + /** * @return array{Table,Column} * diff --git a/tests/Functional/Schema/MySQLSchemaManagerTest.php b/tests/Functional/Schema/MySQLSchemaManagerTest.php index 9342ad5a7a3..25d0918dd8a 100644 --- a/tests/Functional/Schema/MySQLSchemaManagerTest.php +++ b/tests/Functional/Schema/MySQLSchemaManagerTest.php @@ -10,7 +10,6 @@ use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Platforms\MariaDBPlatform; -use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Table; use Doctrine\DBAL\Tests\Functional\Schema\MySQL\PointType; use Doctrine\DBAL\Tests\TestUtil; @@ -231,23 +230,21 @@ public function testColumnCharsetChange(): void ->setLength(100) ->setNotnull(true) ->setPlatformOption('charset', 'utf8'); + $this->dropAndCreateTable($table); $diffTable = clone $table; $diffTable->getColumn('col_string')->setPlatformOption('charset', 'ascii'); - $fromSchema = new Schema([$table]); - $toSchema = new Schema([$diffTable]); - $diff = $this->schemaManager->createComparator() - ->compareSchemas($fromSchema, $toSchema) - ->toSql( - $this->connection->getDatabasePlatform() - ); + ->diffTable($table, $diffTable); + self::assertNotNull($diff); + $this->schemaManager->alterTable($diff); - self::assertContains( - 'ALTER TABLE test_column_charset_change CHANGE col_string' - . ' col_string VARCHAR(100) CHARACTER SET ascii NOT NULL', - $diff + self::assertEquals( + 'ascii', + $this->schemaManager->listTableDetails('test_column_charset_change') + ->getColumn('col_string') + ->getPlatformOption('charset') ); } diff --git a/tests/Platforms/AbstractMySQLPlatformTestCase.php b/tests/Platforms/AbstractMySQLPlatformTestCase.php index cb1aed88c65..336457e725c 100644 --- a/tests/Platforms/AbstractMySQLPlatformTestCase.php +++ b/tests/Platforms/AbstractMySQLPlatformTestCase.php @@ -7,7 +7,9 @@ use Doctrine\DBAL\Exception\ColumnLengthRequired; use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; use Doctrine\DBAL\Platforms\MySQL; +use Doctrine\DBAL\Platforms\MySQL\CharsetMetadataProvider; use Doctrine\DBAL\Platforms\MySQL\CollationMetadataProvider; +use Doctrine\DBAL\Platforms\MySQL\DefaultTableOptions; use Doctrine\DBAL\Schema\Comparator; use Doctrine\DBAL\Schema\Index; use Doctrine\DBAL\Schema\Table; @@ -808,7 +810,9 @@ protected function createComparator(): Comparator { return new MySQL\Comparator( $this->platform, - $this->createStub(CollationMetadataProvider::class) + $this->createStub(CharsetMetadataProvider::class), + $this->createStub(CollationMetadataProvider::class), + new DefaultTableOptions('utf8mb4', 'utf8mb4_general_ci') ); } } diff --git a/tests/Platforms/MySQL/ComparatorTest.php b/tests/Platforms/MySQL/ComparatorTest.php index 136e57b6d93..16d20d2b655 100644 --- a/tests/Platforms/MySQL/ComparatorTest.php +++ b/tests/Platforms/MySQL/ComparatorTest.php @@ -4,8 +4,10 @@ namespace Doctrine\DBAL\Tests\Platforms\MySQL; +use Doctrine\DBAL\Platforms\MySQL\CharsetMetadataProvider; use Doctrine\DBAL\Platforms\MySQL\CollationMetadataProvider; use Doctrine\DBAL\Platforms\MySQL\Comparator; +use Doctrine\DBAL\Platforms\MySQL\DefaultTableOptions; use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Tests\Schema\ComparatorTest as BaseComparatorTest; @@ -15,7 +17,9 @@ protected function setUp(): void { $this->comparator = new Comparator( new MySQLPlatform(), - $this->createStub(CollationMetadataProvider::class) + $this->createStub(CharsetMetadataProvider::class), + $this->createStub(CollationMetadataProvider::class), + new DefaultTableOptions('utf8mb4', 'utf8mb4_general_ci') ); } } diff --git a/tests/Schema/Platforms/MySQLSchemaTest.php b/tests/Schema/Platforms/MySQLSchemaTest.php index 14c50b9c389..087743c55a6 100644 --- a/tests/Schema/Platforms/MySQLSchemaTest.php +++ b/tests/Schema/Platforms/MySQLSchemaTest.php @@ -6,7 +6,9 @@ use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Platforms\MySQL; +use Doctrine\DBAL\Platforms\MySQL\CharsetMetadataProvider; use Doctrine\DBAL\Platforms\MySQL\CollationMetadataProvider; +use Doctrine\DBAL\Platforms\MySQL\DefaultTableOptions; use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Schema\Comparator; use Doctrine\DBAL\Schema\Table; @@ -91,7 +93,9 @@ private function createComparator(): Comparator { return new MySQL\Comparator( new MySQLPlatform(), - $this->createStub(CollationMetadataProvider::class) + $this->createStub(CharsetMetadataProvider::class), + $this->createStub(CollationMetadataProvider::class), + new DefaultTableOptions('utf8mb4', 'utf8mb4_general_ci') ); } }