Skip to content

Commit

Permalink
Merge branch '3.7.x' into 4.0.x
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
derrabus committed Mar 6, 2023
2 parents b4d73f4 + c982b9e commit 809ec96
Show file tree
Hide file tree
Showing 12 changed files with 540 additions and 6 deletions.
8 changes: 7 additions & 1 deletion src/Driver/AbstractMySQLDriver.php
Expand Up @@ -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;
Expand All @@ -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();
}

Expand Down
12 changes: 12 additions & 0 deletions src/Platforms/AbstractMySQLPlatform.php
Expand Up @@ -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}
*/
Expand Down
79 changes: 79 additions & 0 deletions src/Platforms/MariaDB1043Platform.php
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Platforms;

use Doctrine\DBAL\Types\JsonType;

use function sprintf;

Check failure on line 9 in src/Platforms/MariaDB1043Platform.php

View workflow job for this annotation

GitHub Actions / Coding Standards / Coding Standards (8.2)

Type sprintf is not used in this file.

/**
* Provides the behavior, features and SQL dialect of the MariaDB 10.4 (10.4.6 GA) database platform.
*
* Extend deprecated MariaDb1027Platform to ensure correct functions used in MySQLSchemaManager which
* tests for MariaDb1027Platform not MariaDBPlatform.
*/
class MariaDB1043Platform extends MariaDBPlatform
{
/**
* Use JSON rather than LONGTEXT for json columns. Since it is not a true native type, do not override
* hasNativeJsonType() so the DC2Type comment will still be set.
*
* {@inheritdoc}
*/
public function getJsonTypeDeclarationSQL(array $column): string
{
return 'JSON';
}

/**
* 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 = <<<SQL
IF(
x.CHECK_CLAUSE IS NOT NULL AND $tableAlias.COLUMN_TYPE = 'longtext',
'json',
$tableAlias.COLUMN_TYPE
)
SQL;

$joinCheckConstraintSQL = <<<SQL
LEFT JOIN information_schema.CHECK_CONSTRAINTS x
ON (
$tableAlias.TABLE_SCHEMA = x.CONSTRAINT_SCHEMA
AND $tableAlias.TABLE_NAME = x.TABLE_NAME
AND x.CHECK_CLAUSE = CONCAT('json_valid(`', $tableAlias.COLUMN_NAME , '`)')
)
SQL;

return [$columnTypeSQL, $joinCheckConstraintSQL];
}

/** {@inheritDoc} */
public function getColumnDeclarationSQL(string $name, array $column): string
{
// MariaDb forces column collation to utf8mb4_bin where the column was declared as JSON so ignore
// collation and character set for json columns as attempting to set them can cause an error.
if ($this->getJsonTypeDeclarationSQL([]) === 'JSON' && ($column['type'] ?? null) instanceof JsonType) {
unset($column['collation']);
unset($column['charset']);
}

return parent::getColumnDeclarationSQL($name, $column);
}
}
2 changes: 1 addition & 1 deletion src/Platforms/MariaDB1052Platform.php
Expand Up @@ -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}
Expand Down
31 changes: 28 additions & 3 deletions src/Schema/MySQLSchemaManager.php
Expand Up @@ -132,7 +132,7 @@ protected function _getPortableTableColumnDefinition(array $tableColumn): Column
$scale = 0;
$precision = null;

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

switch ($dbType) {
case 'char':
Expand Down Expand Up @@ -228,6 +228,28 @@ protected function _getPortableTableColumnDefinition(array $tableColumn): Column
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

Check failure on line 244 in src/Schema/MySQLSchemaManager.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (8.2)

Method Doctrine\DBAL\Schema\MySQLSchemaManager::expectedDbType() is unused.
{
$_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.
*
Expand Down Expand Up @@ -343,15 +365,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 .= <<<SQL
c.COLUMN_NAME AS field,
c.COLUMN_TYPE AS type,
$columnTypeSQL AS type,
c.IS_NULLABLE AS `null`,
c.COLUMN_KEY AS `key`,
c.COLUMN_DEFAULT AS `default`,
Expand All @@ -362,6 +386,7 @@ protected function selectTableColumns(string $databaseName, ?string $tableName =
FROM information_schema.COLUMNS c
INNER JOIN information_schema.TABLES t
ON t.TABLE_NAME = c.TABLE_NAME
$joinCheckConstraintSQL
SQL;

// The schema name is passed multiple times as a literal in the WHERE clause instead of using a JOIN condition
Expand Down
19 changes: 19 additions & 0 deletions tests/Functional/Schema/MySQL/ComparatorTest.php
Expand Up @@ -7,6 +7,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;
Expand Down Expand Up @@ -174,6 +175,24 @@ public static function tableAndColumnOptionsProvider(): iterable
];
}

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}
*
Expand Down
139 changes: 139 additions & 0 deletions tests/Functional/Schema/MySQL/JsonCollationTest.php
@@ -0,0 +1,139 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Tests\Functional\Schema\MySQL;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\MariaDB1043Platform;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
use Doctrine\DBAL\Schema\Comparator;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Tests\FunctionalTestCase;
use Iterator;

use function array_filter;

/**
* Tests that character set and collation are ignored for columns declared as native JSON in MySQL and
* MariaDb and cannot be changed.
*/
final class JsonCollationTest extends FunctionalTestCase
{
private AbstractPlatform $platform;

private AbstractSchemaManager $schemaManager;

private Comparator $comparator;

protected function setUp(): void
{
$this->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<array{Table}>
*/
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.');
}
}

0 comments on commit 809ec96

Please sign in to comment.