From b7b4870947e49ef26c2054cbc89078bf6fe949c9 Mon Sep 17 00:00:00 2001 From: Colin Knox <124388865+cgknx@users.noreply.github.com> Date: Thu, 9 Feb 2023 16:07:03 +0000 Subject: [PATCH 1/5] Add MariaDb1043Platform using JSON as json column type. MariaDb aliases columns specified as JSON to LONGTEXT. Since version 10.4.3, MariaDb adds a CHECK constraint to JSON columns to ensure they contain valid JSON and sets the column collation to utf8mb4_bin. Simply setting JSON as the column type in the platform results in introspection failures as the database reports LONGTEXT (not JSON) with changed collation. Therefore, inverse the aliasing where the relevant check constraint exists and ignore collation changes for JSON columns in the relevant database versions. The main functional changes are in: - MariaDB1043Platform->getListTableColumnsSQL() and - MySQLSchemaManager->selectTableColumns() MariaDb1043Platform extends MariaDb1027Platform rather than MariaDBPlatform to ensure the platform checks in MySQLSchemaManager (based on MariaDb1027Platform) continue to work as expected. The alternative approach of adding the CHECK constraint to the SQL for the column is not robust as MariaDb syntax seems to require the CHECK constraint to be the final part of the column declaration and it was not obvious how to guarantee this (specifically, appending the CHECK constraint to the output of getJsonTypeDeclarationSQL() did not work). New public methods: MariaDBPlatform->getColumnTypeSQLSnippets() generates SQL snippets to reverse the JSON to LONGTEXT aliasing for JSON-specified columns. It is also used in MySQLSchemaManager. It still checks that json columns are JSON data type so that switching the getJsonTypeDeclarationSQL to LONGTEXT is all that is necessary to revert to old behaviour which is helpful for testing. Overridden methods in MariaDb1043Platform: MariaDb1043Platform->getJsonTypeDeclarationSQL(). To return JSON. MariaDBPlatform->getListTableColumnsSQL(). To reverse the JSON to LONGTEXT aliasing carried out by MariaDb. MariaDBPlatform->getColumnDeclarationSQL(). To unset collation and charset (used by the comparator). New test cases: 1. Types/JsonTest. Test storage and retrieval of json data. 2. Platforms/MariaDb1043PlatformTest. A clone of MariaDb1027Platform test. 3. Schema/MySQLSchemaManagerTest->testColumnIntrospection(). Ensures introspected table matches original table. Tests all doctrine types not just json. Based on failure highlighted in pull request #5100. For previous discussion on this topic, see: https://github.com/doctrine/dbal/pull/5100 https://github.com/doctrine/dbal/issues/3202 Further background at: https://mariadb.com/kb/en/json-data-type/ https://mariadb.com/kb/en/information-schema-check_constraints-table/ https://jira.mariadb.org/browse/MDEV-13916 Notes regarding JsonTest.php: 1. Some platforms return json as a stream so convert streams to strings. 2. 'json_table' is a reserved word in MySQL (and a function name in Oracle SQL). Use json_test_table instead. 3. Some platforms reorder json arrays so the insert order will not necessarily match the select order. Resort the arrays before testing for equality. --- src/Platforms/AbstractMySQLPlatform.php | 14 ++- src/Platforms/MariaDb1043Platform.php | 114 ++++++++++++++++++ src/Platforms/MariaDb1052Platform.php | 2 +- src/Schema/MySQLSchemaManager.php | 7 +- .../Schema/MySQLSchemaManagerTest.php | 21 ++++ tests/Functional/Types/JsonTest.php | 96 +++++++++++++++ tests/Platforms/MariaDb1043PlatformTest.php | 42 +++++++ tests/Platforms/MariaDb1052PlatformTest.php | 2 +- 8 files changed, 293 insertions(+), 5 deletions(-) create mode 100644 src/Platforms/MariaDb1043Platform.php create mode 100644 tests/Functional/Types/JsonTest.php create mode 100644 tests/Platforms/MariaDb1043PlatformTest.php diff --git a/src/Platforms/AbstractMySQLPlatform.php b/src/Platforms/AbstractMySQLPlatform.php index cdbdbec78b9..503881a6c4a 100644 --- a/src/Platforms/AbstractMySQLPlatform.php +++ b/src/Platforms/AbstractMySQLPlatform.php @@ -396,6 +396,18 @@ public function getListTableColumnsSQL($table, $database = null) ' ORDER BY ORDINAL_POSITION ASC'; } + /** + * 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', '']; + } + /** @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. */ public function getListTableMetadataSQL(string $table, ?string $database = null): string { @@ -1390,7 +1402,7 @@ public function supportsColumnLengthIndexes(): bool return true; } - private function getDatabaseNameSQL(?string $databaseName): string + protected function getDatabaseNameSQL(?string $databaseName): string { if ($databaseName !== null) { return $this->quoteStringLiteral($databaseName); diff --git a/src/Platforms/MariaDb1043Platform.php b/src/Platforms/MariaDb1043Platform.php new file mode 100644 index 00000000000..a9b7b1de5ba --- /dev/null +++ b/src/Platforms/MariaDb1043Platform.php @@ -0,0 +1,114 @@ +getColumnTypeSQLSnippets(); + + return sprintf( + <<getDatabaseNameSQL($database), + $this->quoteStringLiteral($table), + ); + } + + /** + * Generate SQL snippets to reverse the aliasing of JSON to LONGTEXT. + * + * MariaDb aliases columns specified as JSON to LONGTEXT and sets a CHECK constraint to ensure the column + * is valid json. This function generates the SQL snippets which reverse this aliasing i.e. report a column + * as JSON where it was originally specified as such instead of LONGTEXT. + * + * The CHECK constraints are stored in information_schema.CHECK_CONSTRAINTS so JOIN that table. + * + * @return array{string, string} + */ + public function getColumnTypeSQLSnippets(string $tableAlias = 'c'): array + { + if ($this->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 b4a3a20cf73..b25fb1af9e6 100644 --- a/src/Platforms/MariaDb1052Platform.php +++ b/src/Platforms/MariaDb1052Platform.php @@ -10,7 +10,7 @@ * * Note: Should not be used with versions prior to 10.5.2. */ -class MariaDb1052Platform extends MariaDb1027Platform +class MariaDb1052Platform extends MariaDb1043Platform { /** * {@inheritdoc} diff --git a/src/Schema/MySQLSchemaManager.php b/src/Schema/MySQLSchemaManager.php index f2305098ab4..5b43ab07463 100644 --- a/src/Schema/MySQLSchemaManager.php +++ b/src/Schema/MySQLSchemaManager.php @@ -405,15 +405,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 .= <<getOption('comment')); 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); + } + + $this->dropAndCreateTable($table); + + $onlineTable = $this->schemaManager->introspectTable('test_column_introspection'); + + $diff = $this->schemaManager->createComparator()->diffTable($table, $onlineTable); + + self::assertFalse($diff, 'Tables should be identical.'); + } } 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..e3c8a1aed16 --- /dev/null +++ b/tests/Platforms/MariaDb1043PlatformTest.php @@ -0,0 +1,42 @@ +platform->hasNativeJsonType()); + } + + /** + * From MariaDB 10.2.7, JSON type is an alias to LONGTEXT however from 10.4.3 setting a column + * as JSON adds additional functionality so use JSON. + * + * @link https://mariadb.com/kb/en/library/json-data-type/ + */ + public function testReturnsJsonTypeDeclarationSQL(): void + { + self::assertSame('JSON', $this->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 72847db531e..f740afc7b97 100644 --- a/tests/Platforms/MariaDb1052PlatformTest.php +++ b/tests/Platforms/MariaDb1052PlatformTest.php @@ -5,7 +5,7 @@ use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Platforms\MariaDb1052Platform; -class MariaDb1052PlatformTest extends MariaDb1027PlatformTest +class MariaDb1052PlatformTest extends MariaDb1043PlatformTest { public function createPlatform(): AbstractPlatform { From 666317a7ea46070339508fd0fbde766db249f74c Mon Sep 17 00:00:00 2001 From: Colin Knox <124388865+cgknx@users.noreply.github.com> Date: Thu, 9 Feb 2023 16:08:55 +0000 Subject: [PATCH 2/5] AbstractMySQLDriver. Use MariaDb1043Platform where applicable. Select MariaDb1043Platform where MariaDb database is at version 10.4.3 or later. --- src/Driver/AbstractMySQLDriver.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Driver/AbstractMySQLDriver.php b/src/Driver/AbstractMySQLDriver.php index b603dd9ba0f..5f1a45fd7e6 100644 --- a/src/Driver/AbstractMySQLDriver.php +++ b/src/Driver/AbstractMySQLDriver.php @@ -9,6 +9,7 @@ use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Platforms\MariaDb1027Platform; +use Doctrine\DBAL\Platforms\MariaDb1043Platform; use Doctrine\DBAL\Platforms\MariaDb1052Platform; use Doctrine\DBAL\Platforms\MySQL57Platform; use Doctrine\DBAL\Platforms\MySQL80Platform; @@ -35,12 +36,17 @@ abstract class AbstractMySQLDriver implements VersionAwarePlatformDriver public function createDatabasePlatformForVersion($version) { $mariadb = stripos($version, 'mariadb') !== false; + if ($mariadb) { $mariaDbVersion = $this->getMariaDbMysqlVersionNumber($version); if (version_compare($mariaDbVersion, '10.5.2', '>=')) { return new MariaDb1052Platform(); } + if (version_compare($mariaDbVersion, '10.4.3', '>=')) { + return new MariaDb1043Platform(); + } + if (version_compare($mariaDbVersion, '10.2.7', '>=')) { return new MariaDb1027Platform(); } From 0c9526d63b9fd0cde6a280705c814725a5ef27f4 Mon Sep 17 00:00:00 2001 From: Colin Knox <124388865+cgknx@users.noreply.github.com> Date: Tue, 28 Feb 2023 15:52:13 +0000 Subject: [PATCH 3/5] MariaDb. Test that comparator ignores collation for JSON columns. MariaDb sets character set to utf8mb4 and collation to utf8mb4_bin for columns declared as JSON but still reports them as LONGTEXT with CHARACTER SET and COLLATE so comparator needs to ignore character set and collation for columns declared as JSON. Attempting to set CHARACTER SET or COLLATE for values other than utf8mb4 and utf8mb4_bin causes a database error on MariaDb. Test that character set and collation specifications are not passed to the database and are ignored by the comparator. --- .../Schema/MySQL/JsonCollationTest.php | 137 ++++++++++++++++++ .../MySQL/MariaDbJsonComparatorTest.php | 87 +++++++++++ 2 files changed, 224 insertions(+) create mode 100644 tests/Functional/Schema/MySQL/JsonCollationTest.php create mode 100644 tests/Platforms/MySQL/MariaDbJsonComparatorTest.php diff --git a/tests/Functional/Schema/MySQL/JsonCollationTest.php b/tests/Functional/Schema/MySQL/JsonCollationTest.php new file mode 100644 index 00000000000..e7f1903e23f --- /dev/null +++ b/tests/Functional/Schema/MySQL/JsonCollationTest.php @@ -0,0 +1,137 @@ +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); + + $this->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); + $this->assertTrue($diff->isEmpty(), 'Tables should be unchanged after attempted collation change.'); + + $diff = $this->comparator->compareTables($table, $originalTable); + $this->assertTrue($diff->isEmpty(), 'Tables should be unchanged after attempted collation change.'); + } +} diff --git a/tests/Platforms/MySQL/MariaDbJsonComparatorTest.php b/tests/Platforms/MySQL/MariaDbJsonComparatorTest.php new file mode 100644 index 00000000000..83dafaca6c4 --- /dev/null +++ b/tests/Platforms/MySQL/MariaDbJsonComparatorTest.php @@ -0,0 +1,87 @@ +comparator = new Comparator( + new MariaDb1043Platform(), + new class implements CollationMetadataProvider { + public function getCollationCharset(string $collation): ?string + { + return null; + } + }, + ); + + // 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 + { + $this->assertTrue( + $this->comparator->compareTables($this->tables[$table1], $this->tables[$table2])->isEmpty(), + sprintf('Tables %s and %s should be identical', $table1, $table2), + ); + } +} From 04c3330dcf55a232dede0b467394b35ee46e520a Mon Sep 17 00:00:00 2001 From: Colin Knox <124388865+cgknx@users.noreply.github.com> Date: Sat, 4 Mar 2023 13:54:07 +0000 Subject: [PATCH 4/5] MariaDb1043. Detect the need to migrate JSON columns to native JSON. MariaDb JSON columns are marked as json with a DC2Type comment hint. The schema manager maps such columns to a json doctrine type which matches the metadata whether the column was declared as LONGTEXT (by the previous MariaDb1027 platform) or JSON. This means the comparator will not detect that a column should be upgraded from LONGTEXT to native JSON and no migration would be generated. Therefore, during column introspection check that the data type of the introspected columns match that which should be set for any doctrine types inferred from DC2Type comments. Set a flag (a platformOption for the relevant column) where it does not. Check whether the flag is set when the comparator diffs the expected and actual tables and mark a column type difference where the flag is set so a migration is generated. --- src/Platforms/AbstractPlatform.php | 15 ++++++++ src/Schema/MySQLSchemaManager.php | 34 ++++++++++++++++++- .../Schema/MySQL/ComparatorTest.php | 19 +++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/Platforms/AbstractPlatform.php b/src/Platforms/AbstractPlatform.php index 28e0782e8a9..d4d719471c5 100644 --- a/src/Platforms/AbstractPlatform.php +++ b/src/Platforms/AbstractPlatform.php @@ -4626,6 +4626,10 @@ public function columnsEqual(Column $column1, Column $column2): bool return false; } + if (! $this->columnDeclarationsMatch($column1, $column2)) { + return false; + } + // If the platform supports inline comments, all comparison is already done above if ($this->supportsInlineColumnComments()) { return true; @@ -4638,6 +4642,17 @@ public function columnsEqual(Column $column1, Column $column2): bool return $column1->getType() === $column2->getType(); } + /** + * Whether the database data type matches that expected for the doctrine type for the given colunms. + */ + private function columnDeclarationsMatch(Column $column1, Column $column2): bool + { + return ! ( + $column1->hasPlatformOption('declarationMismatch') || + $column2->hasPlatformOption('declarationMismatch') + ); + } + /** * Creates the schema manager that can be used to inspect and change the underlying * database schema according to the dialect of the platform. diff --git a/src/Schema/MySQLSchemaManager.php b/src/Schema/MySQLSchemaManager.php index 5b43ab07463..f5cf0f87e18 100644 --- a/src/Schema/MySQLSchemaManager.php +++ b/src/Schema/MySQLSchemaManager.php @@ -183,7 +183,7 @@ protected function _getPortableTableColumnDefinition($tableColumn) $scale = null; $precision = null; - $type = $this->_platform->getDoctrineTypeMapping($dbType); + $type = $origType = $this->_platform->getDoctrineTypeMapping($dbType); // In cases where not connected to a database DESCRIBE $table does not return 'Comment' if (isset($tableColumn['comment'])) { @@ -191,6 +191,12 @@ protected function _getPortableTableColumnDefinition($tableColumn) $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type); } + // Check underlying database type where doctrine type is inferred from DC2Type comment + // and set a flag if it is not as expected. + if ($origType !== $type && $this->expectedDbType($type, $tableColumn) !== $dbType) { + $tableColumn['declarationMismatch'] = true; + } + switch ($dbType) { case 'char': case 'binary': @@ -286,9 +292,35 @@ protected function _getPortableTableColumnDefinition($tableColumn) $column->setPlatformOption('collation', $tableColumn['collation']); } + if (isset($tableColumn['declarationMismatch'])) { + $column->setPlatformOption('declarationMismatch', $tableColumn['declarationMismatch']); + } + return $column; } + /** + * Returns the database data type for a given doctrine type and column + * + * Note that for data types that depend on length where length is not part of the column definition + * and therefore the $tableColumn['length'] will not be set, for example TEXT (which could be LONGTEXT, + * MEDIUMTEXT) or BLOB (LONGBLOB or TINYBLOB), the expectedDbType cannot be inferred exactly, merely + * the default type. + * + * This method is intended to be used to determine underlying database type where doctrine type is + * inferred from a DC2Type comment. + * + * @param mixed[] $tableColumn + */ + private function expectedDbType(string $type, array $tableColumn): string + { + $_type = Type::getType($type); + $expectedDbType = strtolower($_type->getSQLDeclaration($tableColumn, $this->_platform)); + $expectedDbType = strtok($expectedDbType, '(), '); + + return $expectedDbType === false ? '' : $expectedDbType; + } + /** * Return Doctrine/Mysql-compatible column default values for MariaDB 10.2.7+ servers. * diff --git a/tests/Functional/Schema/MySQL/ComparatorTest.php b/tests/Functional/Schema/MySQL/ComparatorTest.php index 8665e50ec47..d799d0ac3b7 100644 --- a/tests/Functional/Schema/MySQL/ComparatorTest.php +++ b/tests/Functional/Schema/MySQL/ComparatorTest.php @@ -5,6 +5,7 @@ use Doctrine\DBAL\Exception; use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Platforms\MariaDb1043Platform; use Doctrine\DBAL\Schema\AbstractSchemaManager; use Doctrine\DBAL\Schema\Column; use Doctrine\DBAL\Schema\Comparator; @@ -147,6 +148,24 @@ public function testImplicitColumnCharset(): void )); } + public function testMariaDb1043NativeJsonUpgradeDetected(): void + { + if (! $this->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} * From e10f00b3d44d1f0145338e96f833c0a54c841ec0 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Mon, 6 Mar 2023 15:08:39 +0100 Subject: [PATCH 5/5] Trigger a runtime deprecation for Connection::executeUpdate() --- src/Connection.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Connection.php b/src/Connection.php index 70caf82287c..7a583f56113 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -1940,13 +1940,20 @@ private function handleDriverException( /** * BC layer for a wide-spread use-case of old DBAL APIs * - * @deprecated This API is deprecated and will be removed after 2022 + * @deprecated Use {@see executeStatement()} instead * * @param array $params The query parameters * @param array $types The parameter types */ public function executeUpdate(string $sql, array $params = [], array $types = []): int { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4163', + '%s is deprecated, please use executeStatement() instead.', + __METHOD__, + ); + return $this->executeStatement($sql, $params, $types); }