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 24, 2021
1 parent d318863 commit 26f64f4
Show file tree
Hide file tree
Showing 35 changed files with 1,057 additions and 337 deletions.
7 changes: 7 additions & 0 deletions UPGRADE.md
Expand Up @@ -8,6 +8,13 @@ awareness about deprecated code.

# Upgrade to 3.2

## Deprecated schema comparison APIs that don't account for the current database connection and the database platform

1. Instantiation of the `Comparator` class outside the DBAL is deprecated. Use `SchemaManager::createComparator()`
to create the comparator specific to the current database connection and the database platform.
2. The `Schema::getMigrateFromSql()` and `::getMigrateToSql()` methods are deprecated. Compare the schemas using the
connection-aware comparator and produce the SQL by passing the resulting diff to the target platform.

## Deprecated reference from `ForeignKeyConstraint` to its local (referencing) `Table`.

Reference from `ForeignKeyConstraint` to its local (referencing) `Table` is deprecated as well as the following methods:
Expand Down
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>
6 changes: 6 additions & 0 deletions psalm.xml.dist
Expand Up @@ -125,6 +125,10 @@
<referencedMethod name="Doctrine\DBAL\Schema\ForeignKeyConstraint::getLocalTable"/>
<referencedMethod name="Doctrine\DBAL\Schema\ForeignKeyConstraint::getLocalTableName"/>
<referencedMethod name="Doctrine\DBAL\Schema\ForeignKeyConstraint::setLocalTable"/>
<!--
TODO: remove in 4.0.0
-->
<referencedMethod name="Doctrine\DBAL\Schema\Schema::getMigrateToSql"/>
</errorLevel>
</DeprecatedMethod>
<DeprecatedProperty>
Expand Down Expand Up @@ -317,6 +321,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
61 changes: 49 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 @@ -3893,6 +3883,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 All @@ -3905,4 +3916,30 @@ protected function getLikeWildcardCharacters(): string
{
return '%_';
}

/**
* Compares the definitions of the given columns in the context of this platform.
*
* @throws Exception
*/
public function columnsEqual(Column $column1, Column $column2): bool
{
if (
$this->getColumnDeclarationSQL('', $this->columnToArray($column1))
!== $this->getColumnDeclarationSQL('', $this->columnToArray($column2))
) {
return false;
}

// If the platform supports inline comments, all comparison is already done above
if ($this->supportsInlineColumnComments()) {
return true;
}

if ($column1->getComment() !== $column2->getComment()) {
return false;
}

return $column1->getType() === $column2->getType();
}
}
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);
}
}
}
10 changes: 10 additions & 0 deletions src/Platforms/SQLServer2012Platform.php
Expand Up @@ -1625,6 +1625,16 @@ public function getColumnDeclarationSQL($name, array $column)
return $name . ' ' . $columnDef;
}

public function columnsEqual(Column $column1, Column $column2): bool
{
if (! parent::columnsEqual($column1, $column2)) {
return false;
}

return $this->getDefaultValueDeclarationSQL($column1->toArray())
=== $this->getDefaultValueDeclarationSQL($column2->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 26f64f4

Please sign in to comment.