📄 Comparator.php
📁 Path: /home/skhata/chandrasons.com/vendor/doctrine/dbal/src/Schema/Comparator.php
📊 Size: 15.36 KB
🔒 Perm: 0644
📝 MIME: text/x-php
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Schema;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\Deprecations\Deprecation;
use function array_map;
use function assert;
use function count;
use function strtolower;
/**
* Compares two Schemas and return an instance of SchemaDiff.
*/
class Comparator
{
/** @internal The comparator can be only instantiated by a schema manager. */
public function __construct(
private readonly AbstractPlatform $platform,
private readonly ComparatorConfig $config = new ComparatorConfig(),
) {
}
/**
* Returns the differences between the schemas.
*/
public function compareSchemas(Schema $oldSchema, Schema $newSchema): SchemaDiff
{
$createdSchemas = [];
$droppedSchemas = [];
$createdTables = [];
$alteredTables = [];
$droppedTables = [];
$createdSequences = [];
$alteredSequences = [];
$droppedSequences = [];
foreach ($newSchema->getNamespaces() as $newNamespace) {
if ($oldSchema->hasNamespace($newNamespace)) {
continue;
}
$createdSchemas[] = $newNamespace;
}
foreach ($oldSchema->getNamespaces() as $oldNamespace) {
if ($newSchema->hasNamespace($oldNamespace)) {
continue;
}
$droppedSchemas[] = $oldNamespace;
}
foreach ($newSchema->getTables() as $newTable) {
$newTableName = $newTable->getShortestName($newSchema->getName());
if (! $oldSchema->hasTable($newTableName)) {
$createdTables[] = $newSchema->getTable($newTableName);
} else {
$tableDiff = $this->compareTables(
$oldSchema->getTable($newTableName),
$newSchema->getTable($newTableName),
);
if (! $tableDiff->isEmpty()) {
$alteredTables[] = $tableDiff;
}
}
}
// Check if there are tables removed
foreach ($oldSchema->getTables() as $oldTable) {
$oldTableName = $oldTable->getShortestName($oldSchema->getName());
$oldTable = $oldSchema->getTable($oldTableName);
if ($newSchema->hasTable($oldTableName)) {
continue;
}
$droppedTables[] = $oldTable;
}
foreach ($newSchema->getSequences() as $newSequence) {
$newSequenceName = $newSequence->getShortestName($newSchema->getName());
if (! $oldSchema->hasSequence($newSequenceName)) {
if (! $this->isAutoIncrementSequenceInSchema($oldSchema, $newSequence)) {
$createdSequences[] = $newSequence;
}
} else {
if ($this->diffSequence($newSequence, $oldSchema->getSequence($newSequenceName))) {
$alteredSequences[] = $newSchema->getSequence($newSequenceName);
}
}
}
foreach ($oldSchema->getSequences() as $oldSequence) {
if ($this->isAutoIncrementSequenceInSchema($newSchema, $oldSequence)) {
continue;
}
$oldSequenceName = $oldSequence->getShortestName($oldSchema->getName());
if ($newSchema->hasSequence($oldSequenceName)) {
continue;
}
$droppedSequences[] = $oldSequence;
}
return new SchemaDiff(
$createdSchemas,
$droppedSchemas,
$createdTables,
$alteredTables,
$droppedTables,
$createdSequences,
$alteredSequences,
$droppedSequences,
);
}
private function isAutoIncrementSequenceInSchema(Schema $schema, Sequence $sequence): bool
{
foreach ($schema->getTables() as $table) {
if ($sequence->isAutoIncrementsFor($table)) {
return true;
}
}
return false;
}
public function diffSequence(Sequence $sequence1, Sequence $sequence2): bool
{
if ($sequence1->getAllocationSize() !== $sequence2->getAllocationSize()) {
return true;
}
return $sequence1->getInitialValue() !== $sequence2->getInitialValue();
}
/**
* Compares the tables and returns the difference between them.
*/
public function compareTables(Table $oldTable, Table $newTable): TableDiff
{
$shouldReportModifiedIndexes = $this->config->getReportModifiedIndexes();
if ($shouldReportModifiedIndexes) {
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/6890',
'Detection of modified indexes is deprecated. Please disable it by configuring the comparator'
. ' using ComparatorConfig::withReportModifiedIndexes(false).',
);
}
$addedColumns = [];
$modifiedColumns = [];
$droppedColumns = [];
$addedIndexes = [];
$modifiedIndexes = [];
$droppedIndexes = [];
$renamedIndexes = [];
$addedForeignKeys = [];
$droppedForeignKeys = [];
$oldColumns = $oldTable->getColumns();
$newColumns = $newTable->getColumns();
// See if all the columns in the old table exist in the new table
foreach ($newColumns as $newColumn) {
$newColumnName = strtolower($newColumn->getName());
if ($oldTable->hasColumn($newColumnName)) {
continue;
}
$addedColumns[$newColumnName] = $newColumn;
}
// See if there are any removed columns in the new table
foreach ($oldColumns as $oldColumn) {
$oldColumnName = strtolower($oldColumn->getName());
// See if column is removed in the new table.
if (! $newTable->hasColumn($oldColumnName)) {
$droppedColumns[$oldColumnName] = $oldColumn;
continue;
}
$newColumn = $newTable->getColumn($oldColumnName);
if ($this->columnsEqual($oldColumn, $newColumn)) {
continue;
}
$modifiedColumns[$oldColumnName] = new ColumnDiff($oldColumn, $newColumn);
}
$renamedColumnNames = $newTable->getRenamedColumns();
foreach ($addedColumns as $addedColumnName => $addedColumn) {
if (! isset($renamedColumnNames[$addedColumn->getName()])) {
continue;
}
$removedColumnName = strtolower($renamedColumnNames[$addedColumn->getName()]);
// Explicitly renamed columns need to be diffed, because their types can also have changed
$modifiedColumns[$removedColumnName] = new ColumnDiff(
$droppedColumns[$removedColumnName],
$addedColumn,
);
unset(
$addedColumns[$addedColumnName],
$droppedColumns[$removedColumnName],
);
}
if ($this->config->getDetectRenamedColumns()) {
$this->detectRenamedColumns($modifiedColumns, $addedColumns, $droppedColumns);
}
$oldIndexes = $oldTable->getIndexes();
$newIndexes = $newTable->getIndexes();
// See if all the indexes from the old table exist in the new one
foreach ($newIndexes as $newIndex) {
$newIndexName = $newIndex->getName();
if (($newIndex->isPrimary() && $oldTable->getPrimaryKey() !== null) || $oldTable->hasIndex($newIndexName)) {
continue;
}
$addedIndexes[] = $newIndex;
}
// See if there are any removed indexes in the new table
foreach ($oldIndexes as $oldIndex) {
$oldIndexName = $oldIndex->getName();
// See if the index is removed in the new table.
if (
($oldIndex->isPrimary() && $newTable->getPrimaryKey() === null) ||
! $oldIndex->isPrimary() && ! $newTable->hasIndex($oldIndexName)
) {
$droppedIndexes[] = $oldIndex;
continue;
}
// See if index has changed in the new table.
$newIndex = $oldIndex->isPrimary() ? $newTable->getPrimaryKey() : $newTable->getIndex($oldIndexName);
assert($newIndex instanceof Index);
if (! $this->diffIndex($oldIndex, $newIndex)) {
continue;
}
if ($shouldReportModifiedIndexes) {
$modifiedIndexes[] = $newIndex;
} else {
$droppedIndexes[] = $oldIndex;
$addedIndexes[] = $newIndex;
}
}
if ($this->config->getDetectRenamedIndexes()) {
$renamedIndexes = $this->detectRenamedIndexes($addedIndexes, $droppedIndexes);
}
$oldForeignKeys = $oldTable->getForeignKeys();
$newForeignKeys = $newTable->getForeignKeys();
foreach ($oldForeignKeys as $oldKey => $oldForeignKey) {
foreach ($newForeignKeys as $newKey => $newForeignKey) {
if ($this->diffForeignKey($oldForeignKey, $newForeignKey) === false) {
unset($oldForeignKeys[$oldKey], $newForeignKeys[$newKey]);
} else {
if (strtolower($oldForeignKey->getName()) === strtolower($newForeignKey->getName())) {
$droppedForeignKeys[$oldKey] = $oldForeignKey;
$addedForeignKeys[$newKey] = $newForeignKey;
unset($oldForeignKeys[$oldKey], $newForeignKeys[$newKey]);
}
}
}
}
foreach ($oldForeignKeys as $oldForeignKey) {
$droppedForeignKeys[] = $oldForeignKey;
}
foreach ($newForeignKeys as $newForeignKey) {
$addedForeignKeys[] = $newForeignKey;
}
return new TableDiff(
$oldTable,
addedColumns: $addedColumns,
changedColumns: $modifiedColumns,
droppedColumns: $droppedColumns,
addedIndexes: $addedIndexes,
modifiedIndexes: $modifiedIndexes,
droppedIndexes: $droppedIndexes,
renamedIndexes: $renamedIndexes,
addedForeignKeys: $addedForeignKeys,
droppedForeignKeys: $droppedForeignKeys,
);
}
/**
* Try to find columns that only changed their name, rename operations maybe cheaper than add/drop
* however ambiguities between different possibilities should not lead to renaming at all.
*
* @param array<string,ColumnDiff> $modifiedColumns
* @param array<string,Column> $addedColumns
* @param array<string,Column> $removedColumns
*/
private function detectRenamedColumns(array &$modifiedColumns, array &$addedColumns, array &$removedColumns): void
{
/** @var array<string, array<array<Column>>> $candidatesByName */
$candidatesByName = [];
foreach ($addedColumns as $addedColumnName => $addedColumn) {
foreach ($removedColumns as $removedColumn) {
if (! $this->columnsEqual($addedColumn, $removedColumn)) {
continue;
}
$candidatesByName[$addedColumnName][] = [$removedColumn, $addedColumn];
}
}
foreach ($candidatesByName as $addedColumnName => $candidates) {
if (count($candidates) !== 1) {
continue;
}
[$oldColumn, $newColumn] = $candidates[0];
$oldColumnName = strtolower($oldColumn->getName());
if (isset($modifiedColumns[$oldColumnName])) {
continue;
}
$modifiedColumns[$oldColumnName] = new ColumnDiff(
$oldColumn,
$newColumn,
);
unset(
$addedColumns[$addedColumnName],
$removedColumns[$oldColumnName],
);
}
}
/**
* Try to find indexes that only changed their name, rename operations maybe cheaper than add/drop
* however ambiguities between different possibilities should not lead to renaming at all.
*
* @param array<Index> $addedIndexes
* @param array<Index> $removedIndexes
*
* @return array<string,Index>
*/
private function detectRenamedIndexes(array &$addedIndexes, array &$removedIndexes): array
{
$candidatesByName = [];
// Gather possible rename candidates by comparing each added and removed index based on semantics.
foreach ($addedIndexes as $addedIndexKey => $addedIndex) {
foreach ($removedIndexes as $removedIndexKey => $removedIndex) {
if ($this->diffIndex($addedIndex, $removedIndex)) {
continue;
}
$candidatesByName[$addedIndex->getName()][] = [$removedIndexKey, $addedIndexKey];
}
}
$renamedIndexes = [];
foreach ($candidatesByName as $candidates) {
// If the current rename candidate contains exactly one semantically equal index,
// we can safely rename it.
// Otherwise, it is unclear if a rename action is really intended,
// therefore we let those ambiguous indexes be added/dropped.
if (count($candidates) !== 1) {
continue;
}
[$removedIndexKey, $addedIndexKey] = $candidates[0];
$removedIndex = $removedIndexes[$removedIndexKey];
$removedIndexName = strtolower($removedIndex->getName());
if (isset($renamedIndexes[$removedIndexName])) {
continue;
}
$addedIndex = $addedIndexes[$addedIndexKey];
$renamedIndexes[$removedIndexName] = $addedIndex;
unset(
$addedIndexes[$addedIndexKey],
$removedIndexes[$removedIndexKey],
);
}
return $renamedIndexes;
}
protected function diffForeignKey(ForeignKeyConstraint $key1, ForeignKeyConstraint $key2): bool
{
if (
array_map('strtolower', $key1->getUnquotedLocalColumns())
!== array_map('strtolower', $key2->getUnquotedLocalColumns())
) {
return true;
}
if (
array_map('strtolower', $key1->getUnquotedForeignColumns())
!== array_map('strtolower', $key2->getUnquotedForeignColumns())
) {
return true;
}
if ($key1->getUnqualifiedForeignTableName() !== $key2->getUnqualifiedForeignTableName()) {
return true;
}
if ($key1->onUpdate() !== $key2->onUpdate()) {
return true;
}
return $key1->onDelete() !== $key2->onDelete();
}
/**
* Compares the definitions of the given columns
*/
protected function columnsEqual(Column $column1, Column $column2): bool
{
return $this->platform->columnsEqual($column1, $column2);
}
/**
* Finds the difference between the indexes $index1 and $index2.
*
* Compares $index1 with $index2 and returns true if there are any
* differences or false in case there are no differences.
*/
protected function diffIndex(Index $index1, Index $index2): bool
{
return ! ($index1->isFulfilledBy($index2) && $index2->isFulfilledBy($index1));
}
}