- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
416 lines
15 KiB
PHP
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);
|
|
}
|
|
};
|
|
});
|