Files
MyGoNavi/internal/app/methods_result_diff.go

307 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import (
"fmt"
"strings"
"GoNavi-Wails/internal/connection"
"GoNavi-Wails/internal/resultdiff"
)
// ResultDiffStartRequest 前端启动比对请求(含连接配置)。
type ResultDiffStartRequest struct {
JobID string `json:"jobId,omitempty"`
Config connection.ConnectionConfig `json:"config"`
Database string `json:"database"`
Left resultdiff.DatasetSpec `json:"left"`
Right resultdiff.DatasetSpec `json:"right"`
KeyColumns []string `json:"keyColumns"`
CompareColumns []string `json:"compareColumns,omitempty"`
IgnoreColumns []string `json:"ignoreColumns,omitempty"`
Options resultdiff.CompareOptions `json:"options"`
MaxRowsPerSide int `json:"maxRowsPerSide,omitempty"`
IncludeSameRows bool `json:"includeSameRows,omitempty"`
}
// ResultDiffStart 启动结果集比对。
// left/right mode=sql 时在服务端执行查询并全量装载mode=rows 时创建会话等待 UploadChunk。
func (a *App) ResultDiffStart(req ResultDiffStartRequest) connection.QueryResult {
if a == nil || a.resultDiffManager == nil {
return connection.QueryResult{Success: false, Message: defaultAppText("result_diff.backend.error.manager_unavailable", nil)}
}
if len(req.KeyColumns) == 0 {
return connection.QueryResult{Success: false, Message: a.appText("result_diff.backend.error.key_columns_required", nil)}
}
leftMode := normalizeDatasetMode(req.Left.Mode)
rightMode := normalizeDatasetMode(req.Right.Mode)
if leftMode == "" || rightMode == "" {
return connection.QueryResult{Success: false, Message: a.appText("result_diff.backend.error.invalid_dataset_mode", nil)}
}
startReq := resultdiff.StartRequest{
JobID: req.JobID,
Database: req.Database,
Left: req.Left,
Right: req.Right,
KeyColumns: req.KeyColumns,
CompareColumns: req.CompareColumns,
IgnoreColumns: req.IgnoreColumns,
Options: req.Options,
MaxRowsPerSide: req.MaxRowsPerSide,
IncludeSameRows: req.IncludeSameRows,
}
session := a.resultDiffManager.Create(startReq)
// 纯 rows 模式:仅创建会话,等待分块上传后 ResultDiffCompute
if leftMode == resultdiff.DatasetModeRows && rightMode == resultdiff.DatasetModeRows {
// 若请求内已内嵌 rows直接装载
if len(req.Left.Rows) > 0 || len(req.Right.Rows) > 0 {
if err := session.SetLoaded(req.Left.Columns, req.Left.Rows, req.Right.Columns, req.Right.Rows); err != nil {
a.resultDiffManager.Close(session.ID)
return connection.QueryResult{Success: false, Message: err.Error()}
}
summary, err := session.Compute()
if err != nil {
a.resultDiffManager.Close(session.ID)
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{
Success: true,
Message: a.appText("result_diff.backend.result.ready", nil),
Data: resultdiff.StartResult{JobID: session.ID, Summary: summary},
}
}
return connection.QueryResult{
Success: true,
Message: a.appText("result_diff.backend.result.session_created", nil),
Data: resultdiff.StartResult{JobID: session.ID},
}
}
// 需要 SQL 装载时解析连接
resolved, err := a.resolveConnectionSecrets(req.Config)
if err != nil {
a.resultDiffManager.Close(session.ID)
return connection.QueryResult{
Success: false,
Message: a.appText("result_diff.backend.error.resolve_secret_failed", map[string]any{"detail": err.Error()}),
}
}
var leftCols, rightCols []string
var leftRows, rightRows []map[string]interface{}
if leftMode == resultdiff.DatasetModeSQL {
leftRows, leftCols, err = a.loadResultDiffSQL(resolved, req.Database, req.Left.SQL, session.MaxRowsPerSide)
if err != nil {
a.resultDiffManager.Close(session.ID)
return connection.QueryResult{
Success: false,
Message: a.appText("result_diff.backend.error.load_left_failed", map[string]any{"detail": err.Error()}),
}
}
} else {
leftCols = req.Left.Columns
leftRows = req.Left.Rows
}
if rightMode == resultdiff.DatasetModeSQL {
rightRows, rightCols, err = a.loadResultDiffSQL(resolved, req.Database, req.Right.SQL, session.MaxRowsPerSide)
if err != nil {
a.resultDiffManager.Close(session.ID)
return connection.QueryResult{
Success: false,
Message: a.appText("result_diff.backend.error.load_right_failed", map[string]any{"detail": err.Error()}),
}
}
} else {
rightCols = req.Right.Columns
rightRows = req.Right.Rows
}
// 混合模式rows 侧可能稍后上传
if leftMode == resultdiff.DatasetModeRows && len(leftRows) == 0 {
// 先装 rightleft 等待 upload
_ = session.SetLoaded(nil, nil, rightCols, rightRows)
// SetLoaded 会把两侧都 mark done — 不适合混合。改用 Append。
// 重新创建更简单:
a.resultDiffManager.Close(session.ID)
session = a.resultDiffManager.Create(startReq)
if err := session.AppendRows("right", rightCols, rightRows, true); err != nil {
a.resultDiffManager.Close(session.ID)
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{
Success: true,
Message: a.appText("result_diff.backend.result.session_created", nil),
Data: resultdiff.StartResult{JobID: session.ID},
}
}
if rightMode == resultdiff.DatasetModeRows && len(rightRows) == 0 {
a.resultDiffManager.Close(session.ID)
session = a.resultDiffManager.Create(startReq)
if err := session.AppendRows("left", leftCols, leftRows, true); err != nil {
a.resultDiffManager.Close(session.ID)
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{
Success: true,
Message: a.appText("result_diff.backend.result.session_created", nil),
Data: resultdiff.StartResult{JobID: session.ID},
}
}
if err := session.SetLoaded(leftCols, leftRows, rightCols, rightRows); err != nil {
a.resultDiffManager.Close(session.ID)
return connection.QueryResult{Success: false, Message: err.Error()}
}
summary, err := session.Compute()
if err != nil {
a.resultDiffManager.Close(session.ID)
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{
Success: true,
Message: a.appText("result_diff.backend.result.ready", nil),
Data: resultdiff.StartResult{JobID: session.ID, Summary: summary},
}
}
// ResultDiffUploadChunk 快照模式分块上传行。
func (a *App) ResultDiffUploadChunk(req resultdiff.UploadChunkRequest) connection.QueryResult {
if a == nil || a.resultDiffManager == nil {
return connection.QueryResult{Success: false, Message: defaultAppText("result_diff.backend.error.manager_unavailable", nil)}
}
session, err := a.resultDiffManager.Get(req.JobID)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if err := session.AppendRows(req.Side, req.Columns, req.Rows, req.Done); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Message: a.appText("result_diff.backend.result.chunk_accepted", nil)}
}
// ResultDiffCompute 在两侧 upload 完成后计算 diff。
func (a *App) ResultDiffCompute(jobID string) connection.QueryResult {
if a == nil || a.resultDiffManager == nil {
return connection.QueryResult{Success: false, Message: defaultAppText("result_diff.backend.error.manager_unavailable", nil)}
}
session, err := a.resultDiffManager.Get(jobID)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
summary, err := session.Compute()
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{
Success: true,
Message: a.appText("result_diff.backend.result.ready", nil),
Data: resultdiff.StartResult{JobID: session.ID, Summary: summary},
}
}
// ResultDiffPage 分页获取 diff 行。
func (a *App) ResultDiffPage(req resultdiff.PageRequest) connection.QueryResult {
if a == nil || a.resultDiffManager == nil {
return connection.QueryResult{Success: false, Message: defaultAppText("result_diff.backend.error.manager_unavailable", nil)}
}
session, err := a.resultDiffManager.Get(req.JobID)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
page, err := session.Page(req)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Data: page}
}
// ResultDiffClose 释放 diff 会话内存。
func (a *App) ResultDiffClose(jobID string) connection.QueryResult {
if a == nil || a.resultDiffManager == nil {
return connection.QueryResult{Success: false, Message: defaultAppText("result_diff.backend.error.manager_unavailable", nil)}
}
a.resultDiffManager.Close(jobID)
return connection.QueryResult{Success: true, Message: a.appText("result_diff.backend.result.closed", nil)}
}
func normalizeDatasetMode(mode resultdiff.DatasetMode) resultdiff.DatasetMode {
switch strings.ToLower(strings.TrimSpace(string(mode))) {
case "sql":
return resultdiff.DatasetModeSQL
case "rows":
return resultdiff.DatasetModeRows
default:
return ""
}
}
func (a *App) loadResultDiffSQL(config connection.ConnectionConfig, database, sqlText string, maxRows int) ([]map[string]interface{}, []string, error) {
sqlText = strings.TrimSpace(sqlText)
if sqlText == "" {
return nil, nil, fmt.Errorf("%s", a.appText("result_diff.backend.error.sql_required", nil))
}
if maxRows <= 0 {
maxRows = resultdiff.DefaultMaxRowsPerSide
}
// 复用 DBQuery 路径(含只读校验、连接缓存)
result := a.DBQuery(config, database, sqlText)
if !result.Success {
return nil, nil, fmt.Errorf("%s", result.Message)
}
rows, cols, err := extractQueryRows(result)
if err != nil {
return nil, nil, err
}
if len(rows) > maxRows {
return nil, nil, fmt.Errorf("%s", a.appText("result_diff.backend.error.row_limit_exceeded", map[string]any{
"count": len(rows),
"maxRows": maxRows,
}))
}
return rows, cols, nil
}
func extractQueryRows(result connection.QueryResult) ([]map[string]interface{}, []string, error) {
cols := result.Fields
switch data := result.Data.(type) {
case []map[string]interface{}:
if len(cols) == 0 && len(data) > 0 {
for k := range data[0] {
cols = append(cols, k)
}
}
return data, cols, nil
case []interface{}:
rows := make([]map[string]interface{}, 0, len(data))
for _, item := range data {
row, ok := item.(map[string]interface{})
if !ok {
continue
}
rows = append(rows, row)
}
if len(cols) == 0 && len(rows) > 0 {
for k := range rows[0] {
cols = append(cols, k)
}
}
return rows, cols, nil
case map[string]interface{}:
// affectedRows 等非结果集
if _, ok := data["affectedRows"]; ok {
return nil, nil, fmt.Errorf("query did not return a result set")
}
return []map[string]interface{}{data}, cols, nil
case nil:
return []map[string]interface{}{}, cols, nil
default:
return nil, nil, fmt.Errorf("unsupported query result type %T", result.Data)
}
}