package sync import ( "GoNavi-Wails/internal/connection" "GoNavi-Wails/internal/db" "fmt" "strings" ) // SyncObjectRef identifies one tabular object without overloading a connection's // Database field (Oracle, for example, stores the service name there). type SyncObjectRef struct { Catalog string `json:"catalog,omitempty"` Database string `json:"database,omitempty"` Schema string `json:"schema,omitempty"` Name string `json:"name"` } // SyncObjectMapping is orthogonal to the legacy Tables field. When Mappings is // empty the engine preserves the legacy identity table/column behavior. type SyncObjectMapping struct { ID string `json:"id,omitempty"` Source SyncObjectRef `json:"source"` Target SyncObjectRef `json:"target"` KeyColumns []string `json:"keyColumns,omitempty"` Filter string `json:"filter,omitempty"` Columns []SyncColumnMapping `json:"columns,omitempty"` } // SyncColumnMapping maps and optionally transforms one source field. A mapping // with an empty Source must provide Default and acts as a generated field. type SyncColumnMapping struct { Source string `json:"source,omitempty"` Target string `json:"target,omitempty"` Drop bool `json:"drop,omitempty"` Default *SyncDefaultValue `json:"default,omitempty"` Transforms []SyncValueTransform `json:"transforms,omitempty"` } // SyncDefaultValue stores a durable, typed literal. When defaults to missing // and null. Supported When values are missing, null and empty. type SyncDefaultValue struct { When []string `json:"when,omitempty"` ValueType string `json:"valueType,omitempty"` Value string `json:"value,omitempty"` } // SyncValueTransform is deliberately declarative. Arbitrary SQL or executable // expressions are not accepted by CompileProjection. type SyncValueTransform struct { Type string `json:"type"` Args map[string]string `json:"args,omitempty"` } func hasExplicitSyncMappings(config SyncConfig) bool { return len(config.Mappings) > 0 } func normalizeMappedSyncTables(config SyncConfig) SyncConfig { if len(config.Mappings) == 0 || len(config.Tables) > 0 { return config } config.Tables = make([]string, 0, len(config.Mappings)) for _, mapping := range config.Mappings { config.Tables = append(config.Tables, syncObjectRefIdentifier(mapping.Source)) } return config } func validateSyncMappings(config SyncConfig) error { if len(config.Mappings) == 0 { return nil } if hasSourceQuery(config) { return validateSourceQueryMappings(config) } if isRedisToMongoKeyspacePair(config) || isMongoToRedisKeyspacePair(config) { return fmt.Errorf("Redis 与 MongoDB 键空间迁移尚未接入对象和字段映射") } if classifyMigrationDataModel(resolveMigrationDBType(config.SourceConfig)) == MigrationDataModelDocument || classifyMigrationDataModel(resolveMigrationDBType(config.SourceConfig)) == MigrationDataModelKeyValue || classifyMigrationDataModel(resolveMigrationDBType(config.TargetConfig)) == MigrationDataModelDocument || classifyMigrationDataModel(resolveMigrationDBType(config.TargetConfig)) == MigrationDataModelKeyValue { return fmt.Errorf("当前映射执行底座仅支持表格式数据源和目标端") } content := strings.ToLower(strings.TrimSpace(config.Content)) if content == "schema" || content == "both" { return fmt.Errorf("对象和字段映射当前仅支持数据同步,尚未接入结构同步") } if config.AutoAddColumns || config.CreateIndexes || normalizeTargetTableStrategy(config.TargetTableStrategy) != "existing_only" { return fmt.Errorf("对象和字段映射当前要求目标表已存在,且不支持自动补字段、建表或创建索引") } seenIDs := make(map[string]struct{}, len(config.Mappings)) seenSources := make(map[string]struct{}, len(config.Mappings)) for index, mapping := range config.Mappings { source := strings.TrimSpace(syncObjectRefIdentifier(mapping.Source)) target := strings.TrimSpace(syncObjectRefIdentifier(mapping.Target)) if source == "" { return fmt.Errorf("第 %d 个对象映射缺少源对象名称", index+1) } if target == "" { return fmt.Errorf("对象映射 %s 缺少目标对象名称", source) } if strings.TrimSpace(mapping.Source.Catalog) != "" || strings.TrimSpace(mapping.Source.Database) != "" || strings.TrimSpace(mapping.Target.Catalog) != "" || strings.TrimSpace(mapping.Target.Database) != "" { return fmt.Errorf("对象映射 %s 尚不支持逐对象 catalog/database 覆盖,请在连接配置中选择数据库", source) } if strings.TrimSpace(mapping.Filter) != "" { return fmt.Errorf("对象映射 %s 的源过滤条件尚未接入执行引擎,已拒绝执行", source) } if id := strings.TrimSpace(mapping.ID); id != "" { key := strings.ToLower(id) if _, exists := seenIDs[key]; exists { return fmt.Errorf("对象映射 ID 重复:%s", id) } seenIDs[key] = struct{}{} } sourceKey := strings.ToLower(source) if _, exists := seenSources[sourceKey]; exists { return fmt.Errorf("源对象被重复映射:%s", source) } seenSources[sourceKey] = struct{}{} projection, err := CompileProjection(mapping) if err != nil { return err } seenKeys := make(map[string]struct{}, len(mapping.KeyColumns)) for _, keyColumn := range mapping.KeyColumns { keyColumn = strings.TrimSpace(keyColumn) if keyColumn == "" { return fmt.Errorf("对象映射 %s 的 keyColumns 不能包含空字段", source) } key := strings.ToLower(keyColumn) if _, duplicate := seenKeys[key]; duplicate { return fmt.Errorf("对象映射 %s 的 keyColumns 字段重复:%s", source, keyColumn) } seenKeys[key] = struct{}{} if targetColumn, ok := projection.TargetColumn(keyColumn); !ok || strings.TrimSpace(targetColumn) == "" { return fmt.Errorf("对象映射 %s 的稳定 key %s 未唯一映射到目标字段", source, keyColumn) } } } for _, tableName := range config.Tables { if _, err := explicitSyncMappingForTable(config, tableName); err != nil { return err } } return nil } func validateSourceQueryMappings(config SyncConfig) error { if len(config.Mappings) != 1 { return fmt.Errorf("SQL 结果同步要求恰好一个对象映射") } mapping := config.Mappings[0] source := strings.TrimSpace(syncObjectRefIdentifier(mapping.Source)) target := strings.TrimSpace(syncObjectRefIdentifier(mapping.Target)) if source == "" { return fmt.Errorf("SQL 结果映射缺少合成源对象名称") } if target == "" { return fmt.Errorf("SQL 结果映射 %s 缺少目标对象名称", source) } if strings.TrimSpace(mapping.Filter) != "" { return fmt.Errorf("SQL 结果映射 %s 不支持额外源过滤条件", source) } if strings.TrimSpace(mapping.Source.Catalog) != "" || strings.TrimSpace(mapping.Source.Database) != "" || strings.TrimSpace(mapping.Target.Catalog) != "" || strings.TrimSpace(mapping.Target.Database) != "" { return fmt.Errorf("SQL 结果映射 %s 尚不支持逐对象 catalog/database 覆盖", source) } content := strings.ToLower(strings.TrimSpace(config.Content)) if content != "" && content != "data" { return fmt.Errorf("SQL 结果对象映射仅支持数据同步") } if config.AutoAddColumns || config.CreateIndexes || normalizeTargetTableStrategy(config.TargetTableStrategy) != "existing_only" { return fmt.Errorf("SQL 结果对象映射要求目标表已存在,且不支持自动补字段、建表或创建索引") } projection, err := CompileProjection(mapping) if err != nil { return err } seenKeys := make(map[string]struct{}, len(mapping.KeyColumns)) for _, sourceKey := range mapping.KeyColumns { sourceKey = strings.TrimSpace(sourceKey) if sourceKey == "" { return fmt.Errorf("SQL 结果映射 %s 的 keyColumns 不能包含空字段", source) } key := strings.ToLower(sourceKey) if _, duplicate := seenKeys[key]; duplicate { return fmt.Errorf("SQL 结果映射 %s 的 keyColumns 字段重复:%s", source, sourceKey) } seenKeys[key] = struct{}{} if targetKey, ok := projection.TargetColumn(sourceKey); !ok || strings.TrimSpace(targetKey) == "" { return fmt.Errorf("SQL 结果映射 %s 的稳定 key %s 未唯一映射到目标字段", source, sourceKey) } } return nil } func sourceQueryMapping(config SyncConfig) (SyncObjectMapping, bool, error) { if len(config.Mappings) == 0 { return SyncObjectMapping{}, false, nil } if err := validateSourceQueryMappings(config); err != nil { return SyncObjectMapping{}, false, err } return config.Mappings[0], true, nil } func explicitSyncMappingForTable(config SyncConfig, tableName string) (SyncObjectMapping, error) { if len(config.Mappings) == 0 { return SyncObjectMapping{}, fmt.Errorf("同步配置未提供显式对象映射") } raw := strings.TrimSpace(tableName) last := lastSyncTableIdentifier(raw) matches := make([]SyncObjectMapping, 0, 1) for _, mapping := range config.Mappings { identifier := strings.TrimSpace(syncObjectRefIdentifier(mapping.Source)) if strings.EqualFold(identifier, raw) || (mapping.Source.Schema != "" && !strings.Contains(raw, ".") && strings.EqualFold(mapping.Source.Name, last)) || (mapping.Source.Schema == "" && strings.EqualFold(mapping.Source.Name, last)) { matches = append(matches, mapping) } } if len(matches) == 0 { return SyncObjectMapping{}, fmt.Errorf("表 %s 缺少对象映射", tableName) } if len(matches) > 1 { return SyncObjectMapping{}, fmt.Errorf("表 %s 匹配到多个对象映射", tableName) } return matches[0], nil } func syncObjectRefIdentifier(ref SyncObjectRef) string { name := strings.TrimSpace(ref.Name) if name == "" { return "" } schema := strings.TrimSpace(ref.Schema) if schema == "" { return name } return schema + "." + name } func projectionForSyncTable(config SyncConfig, tableName string) (*CompiledProjection, error) { if !hasExplicitSyncMappings(config) { return CompileProjection(SyncObjectMapping{}) } mapping, err := explicitSyncMappingForTable(config, tableName) if err != nil { return nil, err } return CompileProjection(mapping) } func projectSyncRows(projection *CompiledProjection, rows []map[string]interface{}) ([]map[string]interface{}, error) { if projection == nil { return nil, fmt.Errorf("字段投影尚未编译") } projected := make([]map[string]interface{}, 0, len(rows)) for _, row := range rows { mapped, err := projection.Project(row) if err != nil { return nil, err } projected = append(projected, mapped) } return projected, nil } func explicitSyncKeyColumnsForTable(config SyncConfig, tableName string, sourceColumns []connection.ColumnDefinition) ([]string, bool, error) { if !hasExplicitSyncMappings(config) { return nil, false, nil } mapping, err := explicitSyncMappingForTable(config, tableName) if err != nil { return nil, false, err } if len(mapping.KeyColumns) == 0 { return nil, false, nil } projection, err := CompileProjection(mapping) if err != nil { return nil, false, err } available := make(map[string]string, len(sourceColumns)) for _, column := range sourceColumns { name := strings.TrimSpace(column.Name) if name != "" { available[strings.ToLower(name)] = name } } keys := make([]string, 0, len(mapping.KeyColumns)) seen := make(map[string]struct{}, len(mapping.KeyColumns)) for _, configured := range mapping.KeyColumns { key := strings.ToLower(strings.TrimSpace(configured)) canonical, exists := available[key] if !exists { return nil, false, fmt.Errorf("对象映射 %s 的稳定 key 字段 %s 不存在", syncObjectRefIdentifier(mapping.Source), configured) } if _, duplicate := seen[key]; duplicate { return nil, false, fmt.Errorf("对象映射稳定 key 字段重复:%s", configured) } seen[key] = struct{}{} if targetColumn, ok := projection.TargetColumn(canonical); !ok || strings.TrimSpace(targetColumn) == "" { return nil, false, fmt.Errorf("对象映射稳定 key %s 未唯一映射到目标字段", canonical) } keys = append(keys, canonical) } return keys, true, nil } func syncKeyColumnsForTable(config SyncConfig, tableName string, sourceColumns []connection.ColumnDefinition) ([]string, error) { if keys, explicit, err := explicitSyncKeyColumnsForTable(config, tableName, sourceColumns); err != nil { return nil, err } else if explicit { return keys, nil } keys := make([]string, 0, 2) for _, column := range sourceColumns { if strings.EqualFold(strings.TrimSpace(column.Key), "PRI") || strings.EqualFold(strings.TrimSpace(column.Key), "PK") { keys = append(keys, strings.TrimSpace(column.Name)) } } return keys, nil } func buildMappedExistingTargetPlan(config SyncConfig, tableName string, sourceDB db.Database, targetDB db.Database) (SchemaMigrationPlan, []connection.ColumnDefinition, []connection.ColumnDefinition, error) { mapping, err := explicitSyncMappingForTable(config, tableName) if err != nil { return SchemaMigrationPlan{}, nil, nil, err } projection, err := CompileProjection(mapping) if err != nil { return SchemaMigrationPlan{}, nil, nil, err } sourceType := resolveMigrationDBType(config.SourceConfig) targetType := resolveMigrationDBType(config.TargetConfig) sourceSchema, sourceTable := normalizeSyncSourceSchemaAndTable(config, tableName) if value := strings.TrimSpace(mapping.Source.Schema); value != "" { sourceSchema = value } if value := strings.TrimSpace(mapping.Source.Name); value != "" { sourceTable = value } targetSchema, targetTable := normalizeSyncTargetSchemaAndTable(config, tableName) if value := strings.TrimSpace(mapping.Target.Schema); value != "" { targetSchema = value } if value := strings.TrimSpace(mapping.Target.Name); value != "" { targetTable = value } plan := SchemaMigrationPlan{ SourceSchema: sourceSchema, SourceTable: sourceTable, TargetSchema: targetSchema, TargetTable: targetTable, SourceQueryTable: qualifiedNameForQuery(sourceType, sourceSchema, sourceTable, syncObjectRefIdentifier(SyncObjectRef{Schema: sourceSchema, Name: sourceTable})), TargetQueryTable: qualifiedNameForQuery(targetType, targetSchema, targetTable, syncObjectRefIdentifier(SyncObjectRef{Schema: targetSchema, Name: targetTable})), PlannedAction: "使用对象和字段映射导入已有目标表", } sourceCols, sourceExists, err := inspectTableColumns(sourceDB, sourceSchema, sourceTable) if err != nil { return plan, nil, nil, syncWrapDetailError("data_sync.backend.error.source_table_columns_failed", err) } if !sourceExists { return plan, nil, nil, syncTextError("data_sync.backend.error.source_table_missing_or_no_columns", map[string]any{"table": tableName}) } if err := projection.ValidateSourceColumns(sourceCols); err != nil { return plan, sourceCols, nil, err } targetCols, targetExists, err := inspectTableColumns(targetDB, targetSchema, targetTable) if err != nil { return plan, sourceCols, nil, syncWrapDetailError("data_sync.backend.error.target_table_columns_failed", err) } plan.TargetTableExists = targetExists if !targetExists { plan.PlannedAction = "映射目标表不存在,需先创建目标表" plan.Warnings = append(plan.Warnings, "对象和字段映射当前仅支持写入已有目标表") return plan, sourceCols, targetCols, nil } missing := projection.MissingTargetColumns(targetCols, sourceCols) if len(missing) > 0 { return plan, sourceCols, targetCols, fmt.Errorf("映射目标表缺少字段 %d 个:%s", len(missing), strings.Join(missing, ", ")) } return plan, sourceCols, targetCols, nil }