mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-10 17:43:15 +08:00
- 同步引擎新增查询结果集同步分支,支持单目标表差异分析、预览与执行 - 数据同步工作台增加 SQL 结果集模式,并补充目标表与查询校验 - 补充后端同步链路与前端请求构造回归测试,并更新 backlog 记录 Fixes #321
178 lines
5.2 KiB
Go
178 lines
5.2 KiB
Go
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)
|
|
}
|
|
}
|