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); } }; });