Skip to content

Merge 3.7.x into 4.0.x #5959

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 13, 2023
8 changes: 7 additions & 1 deletion src/Driver/AbstractMySQLDriver.php
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
77 changes: 77 additions & 0 deletions src/Platforms/MariaDB1043Platform.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Platforms;

use Doctrine\DBAL\Types\JsonType;

/**
* 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
Original file line number Diff line number Diff line change
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
7 changes: 5 additions & 2 deletions src/Schema/MySQLSchemaManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 .= <<<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 +364,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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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.');
}
}
21 changes: 21 additions & 0 deletions tests/Functional/Schema/MySQLSchemaManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down
Loading