mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-05 13:27:55 +08:00
✨ feat(sync): 新增 SQL 结果集数据同步能力
- 同步引擎新增查询结果集同步分支,支持单目标表差异分析、预览与执行 - 数据同步工作台增加 SQL 结果集模式,并补充目标表与查询校验 - 补充后端同步链路与前端请求构造回归测试,并更新 backlog 记录 Fixes #321
This commit is contained in:
177
internal/sync/source_query_sync_test.go
Normal file
177
internal/sync/source_query_sync_test.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package sync
|
||||
|
||||
import (
|
||||
"GoNavi-Wails/internal/connection"
|
||||
"GoNavi-Wails/internal/db"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type fakeQuerySyncTargetDB struct {
|
||||
fakeMigrationDB
|
||||
appliedTable string
|
||||
appliedChanges connection.ChangeSet
|
||||
}
|
||||
|
||||
func (f *fakeQuerySyncTargetDB) ApplyChanges(tableName string, changes connection.ChangeSet) error {
|
||||
f.appliedTable = tableName
|
||||
f.appliedChanges = changes
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ db.BatchApplier = (*fakeQuerySyncTargetDB)(nil)
|
||||
|
||||
func TestAnalyze_SourceQueryUsesQueryResultAsSourceDataset(t *testing.T) {
|
||||
sourceDB := &fakeMigrationDB{
|
||||
columns: map[string][]connection.ColumnDefinition{
|
||||
"app.users": {
|
||||
{Name: "id", Type: "bigint", Nullable: "NO", Key: "PRI"},
|
||||
{Name: "name", Type: "varchar(64)", Nullable: "YES"},
|
||||
},
|
||||
},
|
||||
queryData: map[string][]map[string]interface{}{
|
||||
"SELECT id, name FROM active_users": {
|
||||
{"id": 1, "name": "Alice New"},
|
||||
{"id": 2, "name": "Bob"},
|
||||
},
|
||||
},
|
||||
}
|
||||
targetDB := &fakeQuerySyncTargetDB{
|
||||
fakeMigrationDB: fakeMigrationDB{
|
||||
columns: map[string][]connection.ColumnDefinition{
|
||||
"app.users": {
|
||||
{Name: "id", Type: "bigint", Nullable: "NO", Key: "PRI"},
|
||||
{Name: "name", Type: "varchar(64)", Nullable: "YES"},
|
||||
},
|
||||
},
|
||||
queryData: map[string][]map[string]interface{}{
|
||||
"SELECT * FROM `app`.`users`": {
|
||||
{"id": 1, "name": "Alice Old"},
|
||||
{"id": 3, "name": "Carol"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
oldFactory := newSyncDatabase
|
||||
defer func() { newSyncDatabase = oldFactory }()
|
||||
callCount := 0
|
||||
newSyncDatabase = func(dbType string) (db.Database, error) {
|
||||
callCount++
|
||||
if callCount == 1 {
|
||||
return sourceDB, nil
|
||||
}
|
||||
return targetDB, nil
|
||||
}
|
||||
|
||||
engine := NewSyncEngine(Reporter{})
|
||||
result := engine.Analyze(SyncConfig{
|
||||
SourceConfig: connection.ConnectionConfig{Type: "mysql", Database: "app"},
|
||||
TargetConfig: connection.ConnectionConfig{Type: "mysql", Database: "app"},
|
||||
Tables: []string{"users"},
|
||||
Mode: "insert_update",
|
||||
SourceQuery: "SELECT id, name FROM active_users",
|
||||
})
|
||||
|
||||
if !result.Success {
|
||||
t.Fatalf("Analyze 返回失败: %+v", result)
|
||||
}
|
||||
if len(result.Tables) != 1 {
|
||||
t.Fatalf("expected one table summary, got %d", len(result.Tables))
|
||||
}
|
||||
|
||||
summary := result.Tables[0]
|
||||
if summary.PKColumn != "id" {
|
||||
t.Fatalf("expected PKColumn=id, got %q", summary.PKColumn)
|
||||
}
|
||||
if !summary.CanSync {
|
||||
t.Fatalf("expected summary can sync, got %+v", summary)
|
||||
}
|
||||
if summary.Inserts != 1 || summary.Updates != 1 || summary.Deletes != 1 {
|
||||
t.Fatalf("unexpected diff summary: %+v", summary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunSync_SourceQueryAppliesDiffAgainstTargetTable(t *testing.T) {
|
||||
sourceDB := &fakeMigrationDB{
|
||||
columns: map[string][]connection.ColumnDefinition{
|
||||
"app.users": {
|
||||
{Name: "id", Type: "bigint", Nullable: "NO", Key: "PRI"},
|
||||
{Name: "name", Type: "varchar(64)", Nullable: "YES"},
|
||||
},
|
||||
},
|
||||
queryData: map[string][]map[string]interface{}{
|
||||
"SELECT id, name FROM active_users": {
|
||||
{"id": 1, "name": "Alice New"},
|
||||
{"id": 2, "name": "Bob"},
|
||||
},
|
||||
},
|
||||
}
|
||||
targetDB := &fakeQuerySyncTargetDB{
|
||||
fakeMigrationDB: fakeMigrationDB{
|
||||
columns: map[string][]connection.ColumnDefinition{
|
||||
"app.users": {
|
||||
{Name: "id", Type: "bigint", Nullable: "NO", Key: "PRI"},
|
||||
{Name: "name", Type: "varchar(64)", Nullable: "YES"},
|
||||
},
|
||||
},
|
||||
queryData: map[string][]map[string]interface{}{
|
||||
"SELECT * FROM `app`.`users`": {
|
||||
{"id": 1, "name": "Alice Old"},
|
||||
{"id": 3, "name": "Carol"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
oldFactory := newSyncDatabase
|
||||
defer func() { newSyncDatabase = oldFactory }()
|
||||
callCount := 0
|
||||
newSyncDatabase = func(dbType string) (db.Database, error) {
|
||||
callCount++
|
||||
if callCount == 1 {
|
||||
return sourceDB, nil
|
||||
}
|
||||
return targetDB, nil
|
||||
}
|
||||
|
||||
engine := NewSyncEngine(Reporter{})
|
||||
result := engine.RunSync(SyncConfig{
|
||||
SourceConfig: connection.ConnectionConfig{Type: "mysql", Database: "app"},
|
||||
TargetConfig: connection.ConnectionConfig{Type: "mysql", Database: "app"},
|
||||
Tables: []string{"users"},
|
||||
Mode: "insert_update",
|
||||
SourceQuery: "SELECT id, name FROM active_users",
|
||||
TableOptions: map[string]TableOptions{
|
||||
"users": {Insert: true, Update: true, Delete: true},
|
||||
},
|
||||
})
|
||||
|
||||
if !result.Success {
|
||||
t.Fatalf("RunSync 返回失败: %+v", result)
|
||||
}
|
||||
if result.TablesSynced != 1 || result.RowsInserted != 1 || result.RowsUpdated != 1 || result.RowsDeleted != 1 {
|
||||
t.Fatalf("unexpected sync result: %+v", result)
|
||||
}
|
||||
if targetDB.appliedTable != "users" {
|
||||
t.Fatalf("expected applied table users, got %q", targetDB.appliedTable)
|
||||
}
|
||||
|
||||
wantInserts := []map[string]interface{}{{"id": 2, "name": "Bob"}}
|
||||
if !reflect.DeepEqual(targetDB.appliedChanges.Inserts, wantInserts) {
|
||||
t.Fatalf("unexpected inserts: got=%v want=%v", targetDB.appliedChanges.Inserts, wantInserts)
|
||||
}
|
||||
|
||||
wantUpdates := []connection.UpdateRow{{
|
||||
Keys: map[string]interface{}{"id": 1},
|
||||
Values: map[string]interface{}{"name": "Alice New"},
|
||||
}}
|
||||
if !reflect.DeepEqual(targetDB.appliedChanges.Updates, wantUpdates) {
|
||||
t.Fatalf("unexpected updates: got=%v want=%v", targetDB.appliedChanges.Updates, wantUpdates)
|
||||
}
|
||||
|
||||
wantDeletes := []map[string]interface{}{{"id": 3}}
|
||||
if !reflect.DeepEqual(targetDB.appliedChanges.Deletes, wantDeletes) {
|
||||
t.Fatalf("unexpected deletes: got=%v want=%v", targetDB.appliedChanges.Deletes, wantDeletes)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user