Skip to content

Commit

Permalink
Platform-aware schema comparison
Browse files Browse the repository at this point in the history
Co-authored-by: Martin Auswöger <martin@auswoeger.com>
  • Loading branch information
morozov and ausi committed Aug 22, 2021
1 parent d318863 commit 8fe95db
Show file tree
Hide file tree
Showing 32 changed files with 1,005 additions and 343 deletions.
5 changes: 5 additions & 0 deletions phpcs.xml.dist
Expand Up @@ -129,4 +129,9 @@
<rule ref="Squiz.PHP.LowercasePHPFunctions">
<exclude-pattern>src/Driver/SQLSrv/Statement.php</exclude-pattern>
</rule>

<!-- See https://github.com/squizlabs/PHP_CodeSniffer/issues/3035 -->
<rule ref="Generic.CodeAnalysis.UselessOverridingMethod.Found">
<exclude-pattern>src/Platforms/*/Comparator.php</exclude-pattern>
</rule>
</ruleset>
2 changes: 2 additions & 0 deletions psalm.xml.dist
Expand Up @@ -317,6 +317,8 @@
<!-- See https://github.com/doctrine/dbal/pull/3562 -->
<file name="src/Schema/AbstractSchemaManager.php"/>
<file name="src/Schema/SqliteSchemaManager.php"/>
<!-- See https://github.com/doctrine/dbal/pull/3498 -->
<file name="tests/Platforms/AbstractMySQLPlatformTestCase.php"/>
</errorLevel>
</TooManyArguments>
<TypeDoesNotContainType>
Expand Down
45 changes: 33 additions & 12 deletions src/Platforms/AbstractPlatform.php
Expand Up @@ -1787,23 +1787,13 @@ public function getCreateTableSQL(Table $table, $createFlags = self::CREATE_INDE
}
}

$name = $column->getQuotedName($this);

$columnData = array_merge($column->toArray(), [
'name' => $name,
'version' => $column->hasPlatformOption('version') ? $column->getPlatformOption('version') : false,
'comment' => $this->getColumnComment($column),
]);

if ($columnData['type'] instanceof Types\StringType && $columnData['length'] === null) {
$columnData['length'] = 255;
}
$columnData = $this->columnToArray($column);

if (in_array($column->getName(), $options['primary'], true)) {
$columnData['primary'] = true;
}

$columns[$name] = $columnData;
$columns[$columnData['name']] = $columnData;
}

if ($this->_eventManager !== null && $this->_eventManager->hasListeners(Events::onSchemaCreateTable)) {
Expand Down Expand Up @@ -2477,6 +2467,16 @@ public function getColumnDeclarationSQL($name, array $column)
return $name . ' ' . $declaration;
}

/**
* @internal
*
* @throws Exception
*/
public function getColumnComparisonString(Column $column): string
{
return $this->getColumnDeclarationSQL('', $this->columnToArray($column));
}

/**
* Returns the SQL snippet that declares a floating point column of arbitrary precision.
*
Expand Down Expand Up @@ -3893,6 +3893,27 @@ final public function escapeStringForLike(string $inputString, string $escapeCha
);
}

/**
* @return array<string,mixed> An associative array with the name of the properties
* of the column being declared as array indexes.
*/
private function columnToArray(Column $column): array
{
$name = $column->getQuotedName($this);

$columnData = array_merge($column->toArray(), [
'name' => $name,
'version' => $column->hasPlatformOption('version') ? $column->getPlatformOption('version') : false,
'comment' => $this->getColumnComment($column),
]);

if ($columnData['type'] instanceof Types\StringType && $columnData['length'] === null) {
$columnData['length'] = 255;
}

return $columnData;
}

/**
* @internal
*/
Expand Down
66 changes: 66 additions & 0 deletions src/Platforms/MySQL/Comparator.php
@@ -0,0 +1,66 @@
<?php

namespace Doctrine\DBAL\Platforms\MySQL;

use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Comparator as BaseComparator;
use Doctrine\DBAL\Schema\Table;

use function array_diff_assoc;
use function array_intersect_key;

/**
* Compares schemas in the context of MySQL platform.
*
* In MySQL, unless specified explicitly, the column's character set and collation are inherited from its containing
* table. So during comparison, an omitted value and the value that matches the default value of table in the
* desired schema must be considered equal.
*/
class Comparator extends BaseComparator
{
/**
* @internal The comparator can be only instantiated by a schema manager.
*/
public function __construct(MySQLPlatform $platform)
{
parent::__construct($platform);
}

/**
* {@inheritDoc}
*/
public function diffTable(Table $fromTable, Table $toTable)
{
$defaults = array_intersect_key($fromTable->getOptions(), [
'charset' => null,
'collation' => null,
]);

if ($defaults !== []) {
$fromTable = clone $fromTable;
$toTable = clone $toTable;

$this->normalizeColumns($fromTable, $defaults);
$this->normalizeColumns($toTable, $defaults);
}

return parent::diffTable($fromTable, $toTable);
}

/**
* @param array<string,mixed> $defaults
*/
private function normalizeColumns(Table $table, array $defaults): void
{
foreach ($table->getColumns() as $column) {
$options = $column->getPlatformOptions();
$diff = array_diff_assoc($options, $defaults);

if ($diff === $options) {
continue;
}

$column->setPlatformOptions($diff);
}
}
}
11 changes: 9 additions & 2 deletions src/Platforms/MySQLPlatform.php
Expand Up @@ -370,8 +370,15 @@ public function getListTableMetadataSQL(string $table, ?string $database = null)
{
return sprintf(
<<<'SQL'
SELECT ENGINE, AUTO_INCREMENT, TABLE_COLLATION, TABLE_COMMENT, CREATE_OPTIONS
FROM information_schema.TABLES
SELECT t.ENGINE,
t.AUTO_INCREMENT,
t.TABLE_COMMENT,
t.CREATE_OPTIONS,
t.TABLE_COLLATION,
ccsa.CHARACTER_SET_NAME
FROM information_schema.TABLES t
INNER JOIN information_schema.`COLLATION_CHARACTER_SET_APPLICABILITY` ccsa
ON ccsa.COLLATION_NAME = t.TABLE_COLLATION
WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_SCHEMA = %s AND TABLE_NAME = %s
SQL
,
Expand Down
56 changes: 56 additions & 0 deletions src/Platforms/SQLServer/Comparator.php
@@ -0,0 +1,56 @@
<?php

namespace Doctrine\DBAL\Platforms\SQLServer;

use Doctrine\DBAL\Platforms\SQLServer2012Platform;
use Doctrine\DBAL\Schema\Comparator as BaseComparator;
use Doctrine\DBAL\Schema\Table;

/**
* Compares schemas in the context of SQL Server platform.
*
* @link https://docs.microsoft.com/en-us/sql/t-sql/statements/collations?view=sql-server-ver15
*/
class Comparator extends BaseComparator
{
/** @var string */
private $databaseCollation;

/**
* @internal The comparator can be only instantiated by a schema manager.
*/
public function __construct(SQLServer2012Platform $platform, string $databaseCollation)
{
parent::__construct($platform);

$this->databaseCollation = $databaseCollation;
}

/**
* {@inheritDoc}
*/
public function diffTable(Table $fromTable, Table $toTable)
{
$fromTable = clone $fromTable;
$toTable = clone $toTable;

$this->normalizeColumns($fromTable);
$this->normalizeColumns($toTable);

return parent::diffTable($fromTable, $toTable);
}

private function normalizeColumns(Table $table): void
{
foreach ($table->getColumns() as $column) {
$options = $column->getPlatformOptions();

if (! isset($options['collation']) || $options['collation'] !== $this->databaseCollation) {
continue;
}

unset($options['collation']);
$column->setPlatformOptions($options);
}
}
}
5 changes: 5 additions & 0 deletions src/Platforms/SQLServer2012Platform.php
Expand Up @@ -1625,6 +1625,11 @@ public function getColumnDeclarationSQL($name, array $column)
return $name . ' ' . $columnDef;
}

public function getColumnComparisonString(Column $column): string
{
return parent::getColumnComparisonString($column) . $this->getDefaultValueDeclarationSQL($column->toArray());
}

protected function getLikeWildcardCharacters(): string
{
return parent::getLikeWildcardCharacters() . '[]^';
Expand Down
53 changes: 53 additions & 0 deletions src/Platforms/SQLite/Comparator.php
@@ -0,0 +1,53 @@
<?php

namespace Doctrine\DBAL\Platforms\SQLite;

use Doctrine\DBAL\Platforms\SqlitePlatform;
use Doctrine\DBAL\Schema\Comparator as BaseComparator;
use Doctrine\DBAL\Schema\Table;

use function strcasecmp;

/**
* Compares schemas in the context of SQLite platform.
*
* BINARY is the default column collation and should be ignored if specified explicitly.
*/
class Comparator extends BaseComparator
{
/**
* @internal The comparator can be only instantiated by a schema manager.
*/
public function __construct(SqlitePlatform $platform)
{
parent::__construct($platform);
}

/**
* {@inheritDoc}
*/
public function diffTable(Table $fromTable, Table $toTable)
{
$fromTable = clone $fromTable;
$toTable = clone $toTable;

$this->normalizeColumns($fromTable);
$this->normalizeColumns($toTable);

return parent::diffTable($fromTable, $toTable);
}

private function normalizeColumns(Table $table): void
{
foreach ($table->getColumns() as $column) {
$options = $column->getPlatformOptions();

if (! isset($options['collation']) || strcasecmp($options['collation'], 'binary') !== 0) {
continue;
}

unset($options['collation']);
$column->setPlatformOptions($options);
}
}
}
8 changes: 7 additions & 1 deletion src/Schema/AbstractSchemaManager.php
Expand Up @@ -699,7 +699,8 @@ public function alterSchema(SchemaDiff $schemaDiff): void
*/
public function migrateSchema(Schema $toSchema): void
{
$schemaDiff = (new Comparator())->compareSchemas($this->createSchema(), $toSchema);
$schemaDiff = $this->createComparator()
->compareSchemas($this->createSchema(), $toSchema);

$this->alterSchema($schemaDiff);
}
Expand Down Expand Up @@ -1249,4 +1250,9 @@ public function removeDoctrineTypeFromComment($comment, $type)

return str_replace('(DC2Type:' . $type . ')', '', $comment);
}

public function createComparator(): Comparator
{
return new Comparator($this->getDatabasePlatform());
}
}

0 comments on commit 8fe95db

Please sign in to comment.