Skip to content

Commit a46044f

Browse files
committed
Merge branch '3.7.x' into 4.0.x
* 3.7.x: Trigger a runtime deprecation for Connection::executeUpdate() MariaDb1043. Detect the need to migrate JSON columns to native JSON. MariaDb. Test that comparator ignores collation for JSON columns. AbstractMySQLDriver. Use MariaDb1043Platform where applicable. Add MariaDb1043Platform using JSON as json column type.
2 parents b4d73f4 + c982b9e commit a46044f

12 files changed

+516
-6
lines changed

src/Driver/AbstractMySQLDriver.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Doctrine\DBAL\Driver\API\MySQL\ExceptionConverter;
99
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
1010
use Doctrine\DBAL\Platforms\Exception\InvalidPlatformVersion;
11+
use Doctrine\DBAL\Platforms\MariaDB1043Platform;
1112
use Doctrine\DBAL\Platforms\MariaDB1052Platform;
1213
use Doctrine\DBAL\Platforms\MariaDBPlatform;
1314
use Doctrine\DBAL\Platforms\MySQL80Platform;
@@ -32,10 +33,15 @@ public function getDatabasePlatform(ServerVersionProvider $versionProvider): Abs
3233
{
3334
$version = $versionProvider->getServerVersion();
3435
if (stripos($version, 'mariadb') !== false) {
35-
if (version_compare($this->getMariaDbMysqlVersionNumber($version), '10.5.2', '>=')) {
36+
$mariaDbVersion = $this->getMariaDbMysqlVersionNumber($version);
37+
if (version_compare($mariaDbVersion, '10.5.2', '>=')) {
3638
return new MariaDB1052Platform();
3739
}
3840

41+
if (version_compare($mariaDbVersion, '10.4.3', '>=')) {
42+
return new MariaDB1043Platform();
43+
}
44+
3945
return new MariaDBPlatform();
4046
}
4147

src/Platforms/AbstractMySQLPlatform.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,18 @@ public function supportsColumnCollation(): bool
206206
return true;
207207
}
208208

209+
/**
210+
* The SQL snippets required to elucidate a column type
211+
*
212+
* Returns an array of the form [column type SELECT snippet, additional JOIN statement snippet]
213+
*
214+
* @return array{string, string}
215+
*/
216+
public function getColumnTypeSQLSnippets(string $tableAlias = 'c'): array
217+
{
218+
return [$tableAlias . '.COLUMN_TYPE', ''];
219+
}
220+
209221
/**
210222
* {@inheritDoc}
211223
*/

src/Platforms/MariaDB1043Platform.php

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\DBAL\Platforms;
6+
7+
use Doctrine\DBAL\Types\JsonType;
8+
9+
/**
10+
* Provides the behavior, features and SQL dialect of the MariaDB 10.4 (10.4.6 GA) database platform.
11+
*
12+
* Extend deprecated MariaDb1027Platform to ensure correct functions used in MySQLSchemaManager which
13+
* tests for MariaDb1027Platform not MariaDBPlatform.
14+
*/
15+
class MariaDB1043Platform extends MariaDBPlatform
16+
{
17+
/**
18+
* Use JSON rather than LONGTEXT for json columns. Since it is not a true native type, do not override
19+
* hasNativeJsonType() so the DC2Type comment will still be set.
20+
*
21+
* {@inheritdoc}
22+
*/
23+
public function getJsonTypeDeclarationSQL(array $column): string
24+
{
25+
return 'JSON';
26+
}
27+
28+
/**
29+
* Generate SQL snippets to reverse the aliasing of JSON to LONGTEXT.
30+
*
31+
* MariaDb aliases columns specified as JSON to LONGTEXT and sets a CHECK constraint to ensure the column
32+
* is valid json. This function generates the SQL snippets which reverse this aliasing i.e. report a column
33+
* as JSON where it was originally specified as such instead of LONGTEXT.
34+
*
35+
* The CHECK constraints are stored in information_schema.CHECK_CONSTRAINTS so JOIN that table.
36+
*
37+
* @return array{string, string}
38+
*/
39+
public function getColumnTypeSQLSnippets(string $tableAlias = 'c'): array
40+
{
41+
if ($this->getJsonTypeDeclarationSQL([]) !== 'JSON') {
42+
return parent::getColumnTypeSQLSnippets($tableAlias);
43+
}
44+
45+
$columnTypeSQL = <<<SQL
46+
IF(
47+
x.CHECK_CLAUSE IS NOT NULL AND $tableAlias.COLUMN_TYPE = 'longtext',
48+
'json',
49+
$tableAlias.COLUMN_TYPE
50+
)
51+
SQL;
52+
53+
$joinCheckConstraintSQL = <<<SQL
54+
LEFT JOIN information_schema.CHECK_CONSTRAINTS x
55+
ON (
56+
$tableAlias.TABLE_SCHEMA = x.CONSTRAINT_SCHEMA
57+
AND $tableAlias.TABLE_NAME = x.TABLE_NAME
58+
AND x.CHECK_CLAUSE = CONCAT('json_valid(`', $tableAlias.COLUMN_NAME , '`)')
59+
)
60+
SQL;
61+
62+
return [$columnTypeSQL, $joinCheckConstraintSQL];
63+
}
64+
65+
/** {@inheritDoc} */
66+
public function getColumnDeclarationSQL(string $name, array $column): string
67+
{
68+
// MariaDb forces column collation to utf8mb4_bin where the column was declared as JSON so ignore
69+
// collation and character set for json columns as attempting to set them can cause an error.
70+
if ($this->getJsonTypeDeclarationSQL([]) === 'JSON' && ($column['type'] ?? null) instanceof JsonType) {
71+
unset($column['collation']);
72+
unset($column['charset']);
73+
}
74+
75+
return parent::getColumnDeclarationSQL($name, $column);
76+
}
77+
}

src/Platforms/MariaDB1052Platform.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
*
1313
* Note: Should not be used with versions prior to 10.5.2.
1414
*/
15-
class MariaDB1052Platform extends MariaDBPlatform
15+
class MariaDB1052Platform extends MariaDB1043Platform
1616
{
1717
/**
1818
* {@inheritdoc}

src/Schema/MySQLSchemaManager.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ protected function _getPortableTableColumnDefinition(array $tableColumn): Column
132132
$scale = 0;
133133
$precision = null;
134134

135-
$type = $this->platform->getDoctrineTypeMapping($dbType);
135+
$type = $origType = $this->platform->getDoctrineTypeMapping($dbType);
136136

137137
switch ($dbType) {
138138
case 'char':
@@ -343,15 +343,17 @@ protected function selectTableNames(string $databaseName): Result
343343

344344
protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result
345345
{
346+
[$columnTypeSQL, $joinCheckConstraintSQL] = $this->platform->getColumnTypeSQLSnippets();
347+
346348
$sql = 'SELECT';
347349

348350
if ($tableName === null) {
349351
$sql .= ' c.TABLE_NAME,';
350352
}
351353

352-
$sql .= <<<'SQL'
354+
$sql .= <<<SQL
353355
c.COLUMN_NAME AS field,
354-
c.COLUMN_TYPE AS type,
356+
$columnTypeSQL AS type,
355357
c.IS_NULLABLE AS `null`,
356358
c.COLUMN_KEY AS `key`,
357359
c.COLUMN_DEFAULT AS `default`,
@@ -362,6 +364,7 @@ protected function selectTableColumns(string $databaseName, ?string $tableName =
362364
FROM information_schema.COLUMNS c
363365
INNER JOIN information_schema.TABLES t
364366
ON t.TABLE_NAME = c.TABLE_NAME
367+
$joinCheckConstraintSQL
365368
SQL;
366369

367370
// The schema name is passed multiple times as a literal in the WHERE clause instead of using a JOIN condition

tests/Functional/Schema/MySQL/ComparatorTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Doctrine\DBAL\Exception;
88
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
99
use Doctrine\DBAL\Platforms\AbstractPlatform;
10+
use Doctrine\DBAL\Platforms\MariaDB1043Platform;
1011
use Doctrine\DBAL\Schema\AbstractSchemaManager;
1112
use Doctrine\DBAL\Schema\Column;
1213
use Doctrine\DBAL\Schema\Comparator;
@@ -174,6 +175,24 @@ public static function tableAndColumnOptionsProvider(): iterable
174175
];
175176
}
176177

178+
public function testMariaDb1043NativeJsonUpgradeDetected(): void
179+
{
180+
if (! $this->platform instanceof MariaDB1043Platform) {
181+
self::markTestSkipped();
182+
}
183+
184+
$table = new Table('mariadb_json_upgrade');
185+
186+
$table->addColumn('json_col', 'json');
187+
$this->dropAndCreateTable($table);
188+
189+
// Revert column to old LONGTEXT declaration
190+
$sql = 'ALTER TABLE mariadb_json_upgrade CHANGE json_col json_col LONGTEXT NOT NULL COMMENT \'(DC2Type:json)\'';
191+
$this->connection->executeStatement($sql);
192+
193+
ComparatorTestUtils::assertDiffNotEmpty($this->connection, $this->comparator, $table);
194+
}
195+
177196
/**
178197
* @return array{Table,Column}
179198
*
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\DBAL\Tests\Functional\Schema\MySQL;
6+
7+
use Doctrine\DBAL\Platforms\AbstractPlatform;
8+
use Doctrine\DBAL\Platforms\MariaDB1043Platform;
9+
use Doctrine\DBAL\Schema\AbstractSchemaManager;
10+
use Doctrine\DBAL\Schema\Comparator;
11+
use Doctrine\DBAL\Schema\Table;
12+
use Doctrine\DBAL\Tests\FunctionalTestCase;
13+
use Iterator;
14+
15+
use function array_filter;
16+
17+
/**
18+
* Tests that character set and collation are ignored for columns declared as native JSON in MySQL and
19+
* MariaDb and cannot be changed.
20+
*/
21+
final class JsonCollationTest extends FunctionalTestCase
22+
{
23+
private AbstractPlatform $platform;
24+
25+
private AbstractSchemaManager $schemaManager;
26+
27+
private Comparator $comparator;
28+
29+
protected function setUp(): void
30+
{
31+
$this->platform = $this->connection->getDatabasePlatform();
32+
33+
if (! $this->platform instanceof MariaDB1043Platform) {
34+
self::markTestSkipped();
35+
}
36+
37+
$this->schemaManager = $this->connection->createSchemaManager();
38+
$this->comparator = $this->schemaManager->createComparator();
39+
}
40+
41+
/**
42+
* Generates a number of tables comprising only json columns. The tables are identical but for character
43+
* set and collation.
44+
*
45+
* @return Iterator<array{Table}>
46+
*/
47+
public function tableProvider(): iterable
48+
{
49+
$tables = [
50+
[
51+
'name' => 'mariadb_json_column_comparator_test',
52+
'columns' => [
53+
['name' => 'json_1', 'charset' => 'latin1', 'collation' => 'latin1_swedish_ci'],
54+
['name' => 'json_2', 'charset' => 'utf8', 'collation' => 'utf8_general_ci'],
55+
['name' => 'json_3'],
56+
],
57+
'charset' => 'latin1',
58+
'collation' => 'latin1_swedish_ci',
59+
],
60+
[
61+
'name' => 'mariadb_json_column_comparator_test',
62+
'columns' => [
63+
['name' => 'json_1', 'charset' => 'latin1', 'collation' => 'latin1_swedish_ci'],
64+
['name' => 'json_2', 'charset' => 'utf8', 'collation' => 'utf8_general_ci'],
65+
['name' => 'json_3'],
66+
],
67+
],
68+
[
69+
'name' => 'mariadb_json_column_comparator_test',
70+
'columns' => [
71+
['name' => 'json_1', 'charset' => 'utf8mb4', 'collation' => 'utf8mb4_bin'],
72+
['name' => 'json_2', 'charset' => 'utf8mb4', 'collation' => 'utf8mb4_bin'],
73+
['name' => 'json_3', 'charset' => 'utf8mb4', 'collation' => 'utf8mb4_general_ci'],
74+
],
75+
],
76+
[
77+
'name' => 'mariadb_json_column_comparator_test',
78+
'columns' => [
79+
['name' => 'json_1'],
80+
['name' => 'json_2'],
81+
['name' => 'json_3'],
82+
],
83+
],
84+
];
85+
86+
foreach ($tables as $table) {
87+
yield [$this->setUpTable(
88+
$table['name'],
89+
$table['columns'],
90+
$table['charset'] ?? null,
91+
$table['collation'] ?? null,
92+
),
93+
];
94+
}
95+
}
96+
97+
/** @param array{name: string, type?: string, charset?: string, collation?: string}[] $columns */
98+
private function setUpTable(string $name, array $columns, ?string $charset = null, ?string $collation = null): Table
99+
{
100+
$tableOptions = array_filter(['charset' => $charset, 'collation' => $collation]);
101+
102+
$table = new Table($name, [], [], [], [], $tableOptions);
103+
104+
foreach ($columns as $column) {
105+
if (! isset($column['charset']) || ! isset($column['collation'])) {
106+
$table->addColumn($column['name'], $column['type'] ?? 'json');
107+
} else {
108+
$table->addColumn($column['name'], $column['type'] ?? 'json')
109+
->setPlatformOption('charset', $column['charset'])
110+
->setPlatformOption('collation', $column['collation']);
111+
}
112+
}
113+
114+
return $table;
115+
}
116+
117+
/** @dataProvider tableProvider */
118+
public function testJsonColumnComparison(Table $table): void
119+
{
120+
$this->dropAndCreateTable($table);
121+
122+
$onlineTable = $this->schemaManager->introspectTable('mariadb_json_column_comparator_test');
123+
$diff = $this->comparator->compareTables($table, $onlineTable);
124+
125+
self::assertTrue($diff->isEmpty(), 'Tables should be identical.');
126+
127+
$originalTable = clone $table;
128+
129+
$table->getColumn('json_1')
130+
->setPlatformOption('charset', 'utf8')
131+
->setPlatformOption('collation', 'utf8_general_ci');
132+
133+
$diff = $this->comparator->compareTables($table, $onlineTable);
134+
self::assertTrue($diff->isEmpty(), 'Tables should be unchanged after attempted collation change.');
135+
136+
$diff = $this->comparator->compareTables($table, $originalTable);
137+
self::assertTrue($diff->isEmpty(), 'Tables should be unchanged after attempted collation change.');
138+
}
139+
}

tests/Functional/Schema/MySQLSchemaManagerTest.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
use Doctrine\DBAL\Types\BlobType;
1717
use Doctrine\DBAL\Types\Type;
1818

19+
use function array_keys;
20+
1921
class MySQLSchemaManagerTest extends SchemaManagerFunctionalTestCase
2022
{
2123
public static function setUpBeforeClass(): void
@@ -532,6 +534,25 @@ public function testEnsureTableWithoutOptionsAreReflectedInMetadata(): void
532534
self::assertEquals([], $onlineTable->getOption('create_options'));
533535
}
534536

537+
public function testColumnIntrospection(): void
538+
{
539+
$table = new Table('test_column_introspection');
540+
541+
$doctrineTypes = array_keys(Type::getTypesMap());
542+
543+
foreach ($doctrineTypes as $type) {
544+
$table->addColumn('col_' . $type, $type, ['length' => 8, 'precision' => 8, 'scale' => 2]);
545+
}
546+
547+
$this->dropAndCreateTable($table);
548+
549+
$onlineTable = $this->schemaManager->introspectTable('test_column_introspection');
550+
551+
$diff = $this->schemaManager->createComparator()->compareTables($table, $onlineTable);
552+
553+
self::assertTrue($diff->isEmpty(), 'Tables should be identical.');
554+
}
555+
535556
public function testListTableColumnsThrowsDatabaseRequired(): void
536557
{
537558
$params = TestUtil::getConnectionParams();

0 commit comments

Comments
 (0)