Files
michaelschiemer/tests/Framework/Database/Schema/Comparison/SchemaComparatorTest.php
Michael Schiemer 55a330b223 Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
2025-08-11 20:13:26 +02:00

416 lines
15 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Framework\Database\Schema\Comparison;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\ResultInterface;
use App\Framework\Database\Schema\Comparison\SchemaComparator;
use App\Framework\Database\Schema\Comparison\SchemaDifference;
use Mockery;
beforeEach(function () {
$this->sourceConnection = Mockery::mock(ConnectionInterface::class);
$this->targetConnection = Mockery::mock(ConnectionInterface::class);
$this->comparator = new SchemaComparator($this->sourceConnection, $this->targetConnection);
});
afterEach(function () {
Mockery::close();
});
test('compares schemas with different tables', function () {
// Mock source tables
$sourceTables = Mockery::mock(ResultInterface::class);
$sourceTables->shouldReceive('fetchAll')->andReturn([
['table_name' => 'users'],
['table_name' => 'posts'],
['table_name' => 'comments'],
]);
// Mock target tables
$targetTables = Mockery::mock(ResultInterface::class);
$targetTables->shouldReceive('fetchAll')->andReturn([
['table_name' => 'users'],
['table_name' => 'posts'],
['table_name' => 'categories'], // Different table
]);
// Mock table structure queries
$this->sourceConnection->shouldReceive('getPdo->getAttribute')
->with(\PDO::ATTR_DRIVER_NAME)
->andReturn('pgsql');
$this->targetConnection->shouldReceive('getPdo->getAttribute')
->with(\PDO::ATTR_DRIVER_NAME)
->andReturn('pgsql');
$this->sourceConnection->shouldReceive('query')
->with(Mockery::pattern('/information_schema.tables/'), [])
->andReturn($sourceTables);
$this->targetConnection->shouldReceive('query')
->with(Mockery::pattern('/information_schema.tables/'), [])
->andReturn($targetTables);
// Mock column queries for each table
$this->mockTableStructure('users', true, true);
$this->mockTableStructure('posts', true, true);
$this->mockTableStructure('comments', true, false);
$this->mockTableStructure('categories', false, true);
$difference = $this->comparator->compare();
expect($difference)->toBeInstanceOf(SchemaDifference::class);
expect($difference->hasDifferences())->toBeTrue();
expect($difference->missingTables)->toHaveCount(1);
expect($difference->extraTables)->toHaveCount(1);
expect(array_keys($difference->missingTables))->toBe(['comments']);
expect(array_keys($difference->extraTables))->toBe(['categories']);
});
test('compares schemas with identical tables', function () {
// Mock source tables
$sourceTables = Mockery::mock(ResultInterface::class);
$sourceTables->shouldReceive('fetchAll')->andReturn([
['table_name' => 'users'],
['table_name' => 'posts'],
]);
// Mock target tables
$targetTables = Mockery::mock(ResultInterface::class);
$targetTables->shouldReceive('fetchAll')->andReturn([
['table_name' => 'users'],
['table_name' => 'posts'],
]);
// Mock table structure queries
$this->sourceConnection->shouldReceive('getPdo->getAttribute')
->with(\PDO::ATTR_DRIVER_NAME)
->andReturn('pgsql');
$this->targetConnection->shouldReceive('getPdo->getAttribute')
->with(\PDO::ATTR_DRIVER_NAME)
->andReturn('pgsql');
$this->sourceConnection->shouldReceive('query')
->with(Mockery::pattern('/information_schema.tables/'), [])
->andReturn($sourceTables);
$this->targetConnection->shouldReceive('query')
->with(Mockery::pattern('/information_schema.tables/'), [])
->andReturn($targetTables);
// Mock identical column structure for each table
$this->mockIdenticalTableStructure('users');
$this->mockIdenticalTableStructure('posts');
$difference = $this->comparator->compare();
expect($difference)->toBeInstanceOf(SchemaDifference::class);
expect($difference->hasDifferences())->toBeFalse();
expect($difference->missingTables)->toBeEmpty();
expect($difference->extraTables)->toBeEmpty();
expect($difference->tableDifferences)->toBeEmpty();
});
test('compares schemas with different column definitions', function () {
// Mock source tables
$sourceTables = Mockery::mock(ResultInterface::class);
$sourceTables->shouldReceive('fetchAll')->andReturn([
['table_name' => 'users'],
]);
// Mock target tables
$targetTables = Mockery::mock(ResultInterface::class);
$targetTables->shouldReceive('fetchAll')->andReturn([
['table_name' => 'users'],
]);
// Mock table structure queries
$this->sourceConnection->shouldReceive('getPdo->getAttribute')
->with(\PDO::ATTR_DRIVER_NAME)
->andReturn('pgsql');
$this->targetConnection->shouldReceive('getPdo->getAttribute')
->with(\PDO::ATTR_DRIVER_NAME)
->andReturn('pgsql');
$this->sourceConnection->shouldReceive('query')
->with(Mockery::pattern('/information_schema.tables/'), [])
->andReturn($sourceTables);
$this->targetConnection->shouldReceive('query')
->with(Mockery::pattern('/information_schema.tables/'), [])
->andReturn($targetTables);
// Mock source columns
$sourceColumns = Mockery::mock(ResultInterface::class);
$sourceColumns->shouldReceive('fetchAll')->andReturn([
[
'column_name' => 'id',
'data_type' => 'integer',
'is_nullable' => 'NO',
'column_default' => 'nextval(\'users_id_seq\'::regclass)',
],
[
'column_name' => 'name',
'data_type' => 'character varying',
'is_nullable' => 'NO',
'column_default' => null,
],
[
'column_name' => 'email',
'data_type' => 'character varying',
'is_nullable' => 'NO',
'column_default' => null,
],
]);
// Mock target columns with differences
$targetColumns = Mockery::mock(ResultInterface::class);
$targetColumns->shouldReceive('fetchAll')->andReturn([
[
'column_name' => 'id',
'data_type' => 'integer',
'is_nullable' => 'NO',
'column_default' => 'nextval(\'users_id_seq\'::regclass)',
],
[
'column_name' => 'name',
'data_type' => 'character varying',
'is_nullable' => 'YES', // Changed to nullable
'column_default' => null,
],
[
'column_name' => 'username', // Different column
'data_type' => 'character varying',
'is_nullable' => 'NO',
'column_default' => null,
],
]);
$this->sourceConnection->shouldReceive('query')
->with(Mockery::pattern('/information_schema.columns.*users/'), [])
->andReturn($sourceColumns);
$this->targetConnection->shouldReceive('query')
->with(Mockery::pattern('/information_schema.columns.*users/'), [])
->andReturn($targetColumns);
// Mock empty indexes and foreign keys
$this->mockEmptyIndexes('users');
$this->mockEmptyForeignKeys('users');
$difference = $this->comparator->compare();
expect($difference)->toBeInstanceOf(SchemaDifference::class);
expect($difference->hasDifferences())->toBeTrue();
expect($difference->tableDifferences)->toHaveCount(1);
expect($difference->tableDifferences['users']->missingColumns)->toHaveKey('email');
expect($difference->tableDifferences['users']->extraColumns)->toHaveKey('username');
expect($difference->tableDifferences['users']->modifiedColumns)->toHaveKey('name');
});
test('compares schemas with different indexes', function () {
// Mock source tables
$sourceTables = Mockery::mock(ResultInterface::class);
$sourceTables->shouldReceive('fetchAll')->andReturn([
['table_name' => 'users'],
]);
// Mock target tables
$targetTables = Mockery::mock(ResultInterface::class);
$targetTables->shouldReceive('fetchAll')->andReturn([
['table_name' => 'users'],
]);
// Mock table structure queries
$this->sourceConnection->shouldReceive('getPdo->getAttribute')
->with(\PDO::ATTR_DRIVER_NAME)
->andReturn('pgsql');
$this->targetConnection->shouldReceive('getPdo->getAttribute')
->with(\PDO::ATTR_DRIVER_NAME)
->andReturn('pgsql');
$this->sourceConnection->shouldReceive('query')
->with(Mockery::pattern('/information_schema.tables/'), [])
->andReturn($sourceTables);
$this->targetConnection->shouldReceive('query')
->with(Mockery::pattern('/information_schema.tables/'), [])
->andReturn($targetTables);
// Mock identical columns
$this->mockIdenticalColumns('users');
// Mock source indexes
$sourceIndexes = Mockery::mock(ResultInterface::class);
$sourceIndexes->shouldReceive('fetchAll')->andReturn([
[
'indexname' => 'users_pkey',
'indexdef' => 'CREATE UNIQUE INDEX users_pkey ON users USING btree (id)',
],
[
'indexname' => 'users_email_idx',
'indexdef' => 'CREATE UNIQUE INDEX users_email_idx ON users USING btree (email)',
],
]);
// Mock target indexes with differences
$targetIndexes = Mockery::mock(ResultInterface::class);
$targetIndexes->shouldReceive('fetchAll')->andReturn([
[
'indexname' => 'users_pkey',
'indexdef' => 'CREATE UNIQUE INDEX users_pkey ON users USING btree (id)',
],
[
'indexname' => 'users_name_idx', // Different index
'indexdef' => 'CREATE INDEX users_name_idx ON users USING btree (name)',
],
]);
$this->sourceConnection->shouldReceive('query')
->with(Mockery::pattern('/pg_indexes.*users/'), [])
->andReturn($sourceIndexes);
$this->targetConnection->shouldReceive('query')
->with(Mockery::pattern('/pg_indexes.*users/'), [])
->andReturn($targetIndexes);
// Mock empty foreign keys
$this->mockEmptyForeignKeys('users');
$difference = $this->comparator->compare();
expect($difference)->toBeInstanceOf(SchemaDifference::class);
expect($difference->hasDifferences())->toBeTrue();
expect($difference->tableDifferences)->toHaveCount(1);
expect($difference->tableDifferences['users']->missingIndexes)->toHaveKey('users_email_idx');
expect($difference->tableDifferences['users']->extraIndexes)->toHaveKey('users_name_idx');
});
// Helper functions for tests
beforeEach(function () {
// Define helper methods in the test context
$this->mockTableStructure = function (string $tableName, bool $inSource, bool $inTarget): void {
if ($inSource) {
$sourceColumns = Mockery::mock(ResultInterface::class);
$sourceColumns->shouldReceive('fetchAll')->andReturn([
[
'column_name' => 'id',
'data_type' => 'integer',
'is_nullable' => 'NO',
'column_default' => 'nextval(\'' . $tableName . '_id_seq\'::regclass)',
],
[
'column_name' => 'name',
'data_type' => 'character varying',
'is_nullable' => 'NO',
'column_default' => null,
],
]);
$this->sourceConnection->shouldReceive('query')
->with(Mockery::pattern('/information_schema.columns.*' . $tableName . '/'), [])
->andReturn($sourceColumns);
$this->mockEmptyIndexes($tableName, true, false);
$this->mockEmptyForeignKeys($tableName, true, false);
}
if ($inTarget) {
$targetColumns = Mockery::mock(ResultInterface::class);
$targetColumns->shouldReceive('fetchAll')->andReturn([
[
'column_name' => 'id',
'data_type' => 'integer',
'is_nullable' => 'NO',
'column_default' => 'nextval(\'' . $tableName . '_id_seq\'::regclass)',
],
[
'column_name' => 'name',
'data_type' => 'character varying',
'is_nullable' => 'NO',
'column_default' => null,
],
]);
$this->targetConnection->shouldReceive('query')
->with(Mockery::pattern('/information_schema.columns.*' . $tableName . '/'), [])
->andReturn($targetColumns);
$this->mockEmptyIndexes($tableName, false, true);
$this->mockEmptyForeignKeys($tableName, false, true);
}
};
$this->mockIdenticalTableStructure = function (string $tableName): void {
$this->mockIdenticalColumns($tableName);
$this->mockEmptyIndexes($tableName);
$this->mockEmptyForeignKeys($tableName);
};
$this->mockIdenticalColumns = function (string $tableName): void {
$columns = Mockery::mock(ResultInterface::class);
$columns->shouldReceive('fetchAll')->andReturn([
[
'column_name' => 'id',
'data_type' => 'integer',
'is_nullable' => 'NO',
'column_default' => 'nextval(\'' . $tableName . '_id_seq\'::regclass)',
],
[
'column_name' => 'name',
'data_type' => 'character varying',
'is_nullable' => 'NO',
'column_default' => null,
],
]);
$this->sourceConnection->shouldReceive('query')
->with(Mockery::pattern('/information_schema.columns.*' . $tableName . '/'), [])
->andReturn($columns);
$this->targetConnection->shouldReceive('query')
->with(Mockery::pattern('/information_schema.columns.*' . $tableName . '/'), [])
->andReturn($columns);
};
$this->mockEmptyIndexes = function (string $tableName, bool $source = true, bool $target = true): void {
$emptyIndexes = Mockery::mock(ResultInterface::class);
$emptyIndexes->shouldReceive('fetchAll')->andReturn([]);
if ($source) {
$this->sourceConnection->shouldReceive('query')
->with(Mockery::pattern('/pg_indexes.*' . $tableName . '/'), [])
->andReturn($emptyIndexes);
}
if ($target) {
$this->targetConnection->shouldReceive('query')
->with(Mockery::pattern('/pg_indexes.*' . $tableName . '/'), [])
->andReturn($emptyIndexes);
}
};
$this->mockEmptyForeignKeys = function (string $tableName, bool $source = true, bool $target = true): void {
$emptyForeignKeys = Mockery::mock(ResultInterface::class);
$emptyForeignKeys->shouldReceive('fetchAll')->andReturn([]);
if ($source) {
$this->sourceConnection->shouldReceive('query')
->with(Mockery::pattern('/information_schema.table_constraints.*' . $tableName . '/'), [])
->andReturn($emptyForeignKeys);
}
if ($target) {
$this->targetConnection->shouldReceive('query')
->with(Mockery::pattern('/information_schema.table_constraints.*' . $tableName . '/'), [])
->andReturn($emptyForeignKeys);
}
};
});