diff --git a/src/Driver/AbstractMySQLDriver.php b/src/Driver/AbstractMySQLDriver.php index a70afdaf744..4d7ae843680 100644 --- a/src/Driver/AbstractMySQLDriver.php +++ b/src/Driver/AbstractMySQLDriver.php @@ -8,6 +8,7 @@ use Doctrine\DBAL\Driver\API\MySQL\ExceptionConverter; use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; use Doctrine\DBAL\Platforms\Exception\InvalidPlatformVersion; +use Doctrine\DBAL\Platforms\MariaDB1043Platform; use Doctrine\DBAL\Platforms\MariaDB1052Platform; use Doctrine\DBAL\Platforms\MariaDBPlatform; use Doctrine\DBAL\Platforms\MySQL80Platform; @@ -32,10 +33,15 @@ public function getDatabasePlatform(ServerVersionProvider $versionProvider): Abs { $version = $versionProvider->getServerVersion(); if (stripos($version, 'mariadb') !== false) { - if (version_compare($this->getMariaDbMysqlVersionNumber($version), '10.5.2', '>=')) { + $mariaDbVersion = $this->getMariaDbMysqlVersionNumber($version); + if (version_compare($mariaDbVersion, '10.5.2', '>=')) { return new MariaDB1052Platform(); } + if (version_compare($mariaDbVersion, '10.4.3', '>=')) { + return new MariaDB1043Platform(); + } + return new MariaDBPlatform(); } diff --git a/src/Platforms/AbstractMySQLPlatform.php b/src/Platforms/AbstractMySQLPlatform.php index ca3e6c9cb01..ab9820abffd 100644 --- a/src/Platforms/AbstractMySQLPlatform.php +++ b/src/Platforms/AbstractMySQLPlatform.php @@ -206,6 +206,18 @@ public function supportsColumnCollation(): bool return true; } + /** + * The SQL snippets required to elucidate a column type + * + * Returns an array of the form [column type SELECT snippet, additional JOIN statement snippet] + * + * @return array{string, string} + */ + public function getColumnTypeSQLSnippets(string $tableAlias = 'c'): array + { + return [$tableAlias . '.COLUMN_TYPE', '']; + } + /** * {@inheritDoc} */ diff --git a/src/Platforms/MariaDB1043Platform.php b/src/Platforms/MariaDB1043Platform.php new file mode 100644 index 00000000000..30f72ea36fa --- /dev/null +++ b/src/Platforms/MariaDB1043Platform.php @@ -0,0 +1,77 @@ +getJsonTypeDeclarationSQL([]) !== 'JSON') { + return parent::getColumnTypeSQLSnippets($tableAlias); + } + + $columnTypeSQL = <<getJsonTypeDeclarationSQL([]) === 'JSON' && ($column['type'] ?? null) instanceof JsonType) { + unset($column['collation']); + unset($column['charset']); + } + + return parent::getColumnDeclarationSQL($name, $column); + } +} diff --git a/src/Platforms/MariaDB1052Platform.php b/src/Platforms/MariaDB1052Platform.php index 2e6c4a5b61f..2bff90c44f2 100644 --- a/src/Platforms/MariaDB1052Platform.php +++ b/src/Platforms/MariaDB1052Platform.php @@ -12,7 +12,7 @@ * * Note: Should not be used with versions prior to 10.5.2. */ -class MariaDB1052Platform extends MariaDBPlatform +class MariaDB1052Platform extends MariaDB1043Platform { /** * {@inheritdoc} diff --git a/src/Schema/MySQLSchemaManager.php b/src/Schema/MySQLSchemaManager.php index 47b17eacf3b..17f9cb6ba90 100644 --- a/src/Schema/MySQLSchemaManager.php +++ b/src/Schema/MySQLSchemaManager.php @@ -343,15 +343,17 @@ protected function selectTableNames(string $databaseName): Result protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result { + [$columnTypeSQL, $joinCheckConstraintSQL] = $this->platform->getColumnTypeSQLSnippets(); + $sql = 'SELECT'; if ($tableName === null) { $sql .= ' c.TABLE_NAME,'; } - $sql .= <<<'SQL' + $sql .= <<platform instanceof MariaDB1043Platform) { + self::markTestSkipped(); + } + + $table = new Table('mariadb_json_upgrade'); + + $table->addColumn('json_col', 'json'); + $this->dropAndCreateTable($table); + + // Revert column to old LONGTEXT declaration + $sql = 'ALTER TABLE mariadb_json_upgrade CHANGE json_col json_col LONGTEXT NOT NULL COMMENT \'(DC2Type:json)\''; + $this->connection->executeStatement($sql); + + ComparatorTestUtils::assertDiffNotEmpty($this->connection, $this->comparator, $table); + } + /** * @return array{Table,Column} * diff --git a/tests/Functional/Schema/MySQL/JsonCollationTest.php b/tests/Functional/Schema/MySQL/JsonCollationTest.php new file mode 100644 index 00000000000..854f64a064e --- /dev/null +++ b/tests/Functional/Schema/MySQL/JsonCollationTest.php @@ -0,0 +1,139 @@ +platform = $this->connection->getDatabasePlatform(); + + if (! $this->platform instanceof MariaDB1043Platform) { + self::markTestSkipped(); + } + + $this->schemaManager = $this->connection->createSchemaManager(); + $this->comparator = $this->schemaManager->createComparator(); + } + + /** + * Generates a number of tables comprising only json columns. The tables are identical but for character + * set and collation. + * + * @return Iterator + */ + public function tableProvider(): iterable + { + $tables = [ + [ + 'name' => 'mariadb_json_column_comparator_test', + 'columns' => [ + ['name' => 'json_1', 'charset' => 'latin1', 'collation' => 'latin1_swedish_ci'], + ['name' => 'json_2', 'charset' => 'utf8', 'collation' => 'utf8_general_ci'], + ['name' => 'json_3'], + ], + 'charset' => 'latin1', + 'collation' => 'latin1_swedish_ci', + ], + [ + 'name' => 'mariadb_json_column_comparator_test', + 'columns' => [ + ['name' => 'json_1', 'charset' => 'latin1', 'collation' => 'latin1_swedish_ci'], + ['name' => 'json_2', 'charset' => 'utf8', 'collation' => 'utf8_general_ci'], + ['name' => 'json_3'], + ], + ], + [ + 'name' => 'mariadb_json_column_comparator_test', + 'columns' => [ + ['name' => 'json_1', 'charset' => 'utf8mb4', 'collation' => 'utf8mb4_bin'], + ['name' => 'json_2', 'charset' => 'utf8mb4', 'collation' => 'utf8mb4_bin'], + ['name' => 'json_3', 'charset' => 'utf8mb4', 'collation' => 'utf8mb4_general_ci'], + ], + ], + [ + 'name' => 'mariadb_json_column_comparator_test', + 'columns' => [ + ['name' => 'json_1'], + ['name' => 'json_2'], + ['name' => 'json_3'], + ], + ], + ]; + + foreach ($tables as $table) { + yield [$this->setUpTable( + $table['name'], + $table['columns'], + $table['charset'] ?? null, + $table['collation'] ?? null, + ), + ]; + } + } + + /** @param array{name: string, type?: string, charset?: string, collation?: string}[] $columns */ + private function setUpTable(string $name, array $columns, ?string $charset = null, ?string $collation = null): Table + { + $tableOptions = array_filter(['charset' => $charset, 'collation' => $collation]); + + $table = new Table($name, [], [], [], [], $tableOptions); + + foreach ($columns as $column) { + if (! isset($column['charset']) || ! isset($column['collation'])) { + $table->addColumn($column['name'], $column['type'] ?? 'json'); + } else { + $table->addColumn($column['name'], $column['type'] ?? 'json') + ->setPlatformOption('charset', $column['charset']) + ->setPlatformOption('collation', $column['collation']); + } + } + + return $table; + } + + /** @dataProvider tableProvider */ + public function testJsonColumnComparison(Table $table): void + { + $this->dropAndCreateTable($table); + + $onlineTable = $this->schemaManager->introspectTable('mariadb_json_column_comparator_test'); + $diff = $this->comparator->compareTables($table, $onlineTable); + + self::assertTrue($diff->isEmpty(), 'Tables should be identical.'); + + $originalTable = clone $table; + + $table->getColumn('json_1') + ->setPlatformOption('charset', 'utf8') + ->setPlatformOption('collation', 'utf8_general_ci'); + + $diff = $this->comparator->compareTables($table, $onlineTable); + self::assertTrue($diff->isEmpty(), 'Tables should be unchanged after attempted collation change.'); + + $diff = $this->comparator->compareTables($table, $originalTable); + self::assertTrue($diff->isEmpty(), 'Tables should be unchanged after attempted collation change.'); + } +} diff --git a/tests/Functional/Schema/MySQLSchemaManagerTest.php b/tests/Functional/Schema/MySQLSchemaManagerTest.php index 9115a890682..9c89f0db257 100644 --- a/tests/Functional/Schema/MySQLSchemaManagerTest.php +++ b/tests/Functional/Schema/MySQLSchemaManagerTest.php @@ -16,6 +16,8 @@ use Doctrine\DBAL\Types\BlobType; use Doctrine\DBAL\Types\Type; +use function array_keys; + class MySQLSchemaManagerTest extends SchemaManagerFunctionalTestCase { public static function setUpBeforeClass(): void @@ -532,6 +534,25 @@ public function testEnsureTableWithoutOptionsAreReflectedInMetadata(): void self::assertEquals([], $onlineTable->getOption('create_options')); } + public function testColumnIntrospection(): void + { + $table = new Table('test_column_introspection'); + + $doctrineTypes = array_keys(Type::getTypesMap()); + + foreach ($doctrineTypes as $type) { + $table->addColumn('col_' . $type, $type, ['length' => 8, 'precision' => 8, 'scale' => 2]); + } + + $this->dropAndCreateTable($table); + + $onlineTable = $this->schemaManager->introspectTable('test_column_introspection'); + + $diff = $this->schemaManager->createComparator()->compareTables($table, $onlineTable); + + self::assertTrue($diff->isEmpty(), 'Tables should be identical.'); + } + public function testListTableColumnsThrowsDatabaseRequired(): void { $params = TestUtil::getConnectionParams(); diff --git a/tests/Functional/Types/JsonTest.php b/tests/Functional/Types/JsonTest.php new file mode 100644 index 00000000000..feb431b7924 --- /dev/null +++ b/tests/Functional/Types/JsonTest.php @@ -0,0 +1,96 @@ +addColumn('id', 'integer'); + + $table->addColumn('val', 'json'); + $table->setPrimaryKey(['id']); + + $this->dropAndCreateTable($table); + } + + public function testInsertAndSelect(): void + { + $id1 = 1; + $id2 = 2; + + $value1 = [ + 'firstKey' => 'firstVal', + 'secondKey' => 'secondVal', + 'nestedKey' => [ + 'nestedKey1' => 'nestedVal1', + 'nestedKey2' => 2, + ], + ]; + $value2 = json_decode('{"key1":"Val1","key2":2,"key3":"Val3"}', true); + + $this->insert($id1, $value1); + $this->insert($id2, $value2); + + $res1 = $this->select($id1); + $res2 = $this->select($id2); + + // The returned arrays are not guaranteed to be in the same order so sort them + ksort($value1); + ksort($value2); + ksort($res1); + ksort($res2); + + self::assertSame($value1, $res1); + self::assertSame($value2, $res2); + } + + /** @param array $value */ + private function insert(int $id, array $value): void + { + $result = $this->connection->insert('json_test_table', [ + 'id' => $id, + 'val' => $value, + ], [ + ParameterType::INTEGER, + Type::getType('json'), + ]); + + self::assertSame(1, $result); + } + + /** @return array */ + private function select(int $id): array + { + $value = $this->connection->fetchOne( + 'SELECT val FROM json_test_table WHERE id = ?', + [$id], + [ParameterType::INTEGER], + ); + + if (is_resource($value)) { + $value = stream_get_contents($value); + } + + self::assertIsString($value); + + $value = json_decode($value, true); + + self::assertIsArray($value); + + return $value; + } +} diff --git a/tests/Platforms/MariaDB1043PlatformTest.php b/tests/Platforms/MariaDB1043PlatformTest.php new file mode 100644 index 00000000000..81320272f60 --- /dev/null +++ b/tests/Platforms/MariaDB1043PlatformTest.php @@ -0,0 +1,39 @@ +platform->getJsonTypeDeclarationSQL([])); + } + + public function testInitializesJsonTypeMapping(): void + { + self::assertTrue($this->platform->hasDoctrineTypeMappingFor('json')); + self::assertSame(Types::JSON, $this->platform->getDoctrineTypeMapping('json')); + } + + public function testIgnoresDifferenceInDefaultValuesForUnsupportedColumnTypes(): void + { + self::markTestSkipped('MariaDb1043Platform supports default values for BLOB and TEXT columns'); + } +} diff --git a/tests/Platforms/MariaDB1052PlatformTest.php b/tests/Platforms/MariaDB1052PlatformTest.php index e41fe003de8..365df99d8dc 100644 --- a/tests/Platforms/MariaDB1052PlatformTest.php +++ b/tests/Platforms/MariaDB1052PlatformTest.php @@ -7,7 +7,7 @@ use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Platforms\MariaDB1052Platform; -class MariaDB1052PlatformTest extends MariaDBPlatformTest +class MariaDB1052PlatformTest extends MariaDB1043PlatformTest { public function createPlatform(): AbstractPlatform { diff --git a/tests/Platforms/MySQL/MariaDBJsonComparatorTest.php b/tests/Platforms/MySQL/MariaDBJsonComparatorTest.php new file mode 100644 index 00000000000..45a2bc8b3c4 --- /dev/null +++ b/tests/Platforms/MySQL/MariaDBJsonComparatorTest.php @@ -0,0 +1,98 @@ +comparator = new Comparator( + new MariaDB1043Platform(), + new class implements CharsetMetadataProvider { + public function getDefaultCharsetCollation(string $charset): ?string + { + return null; + } + }, + new class implements CollationMetadataProvider { + public function getCollationCharset(string $collation): ?string + { + return null; + } + }, + new DefaultTableOptions('utf8mb4', 'utf8mb4_general_ci'), + ); + + // TableA has collation set at table level and various column collations + $this->tables['A'] = new Table( + 'foo', + [], + [], + [], + [], + ['charset' => 'latin1', 'collation' => 'latin1_swedish_ci'], + ); + + $this->tables['A']->addColumn('json_1', 'json')->setPlatformOption('collation', 'latin1_swedish_ci'); + $this->tables['A']->addColumn('json_2', 'json')->setPlatformOption('collation', 'utf8_general_ci'); + $this->tables['A']->addColumn('json_3', 'json'); + + // TableB has no table-level collation and various column collations + $this->tables['B'] = new Table('foo'); + $this->tables['B']->addColumn('json_1', 'json')->setPlatformOption('collation', 'latin1_swedish_ci'); + $this->tables['B']->addColumn('json_2', 'json')->setPlatformOption('collation', 'utf8_general_ci'); + $this->tables['B']->addColumn('json_3', 'json'); + + // Table C has no table-level collation and column collations as MariaDb would return for columns declared + // as JSON + $this->tables['C'] = new Table('foo'); + $this->tables['C']->addColumn('json_1', 'json')->setPlatformOption('collation', 'utf8mb4_bin'); + $this->tables['C']->addColumn('json_2', 'json')->setPlatformOption('collation', 'utf8mb4_bin'); + $this->tables['C']->addColumn('json_3', 'json')->setPlatformOption('collation', 'utf8mb4_bin'); + + // Table D has no table or column collations set + $this->tables['D'] = new Table('foo'); + $this->tables['D']->addColumn('json_1', 'json'); + $this->tables['D']->addColumn('json_2', 'json'); + $this->tables['D']->addColumn('json_3', 'json'); + } + + /** @return array{string, string}[] */ + public static function providerTableComparisons(): iterable + { + return [ + ['A', 'B'], + ['A', 'C'], + ['A', 'D'], + ['B', 'C'], + ['B', 'D'], + ['C', 'D'], + ]; + } + + /** @dataProvider providerTableComparisons */ + public function testJsonColumnComparison(string $table1, string $table2): void + { + self::assertTrue( + $this->comparator->compareTables($this->tables[$table1], $this->tables[$table2])->isEmpty(), + sprintf('Tables %s and %s should be identical', $table1, $table2), + ); + } +}