package dbsync import ( "context" "fmt" "strings" ) // SchemaTableSpec 本机 → 线上:空表/表结构 ensure 请求中的一张表。 type SchemaTableSpec struct { Table string `json:"table"` PKColumn string `json:"pk_column,omitempty"` Columns []string `json:"columns"` OnlineDBID string `json:"online_db_id,omitempty"` } // SchemaEnsureRequest agent 批量 ensure 空表到线上 A。 type SchemaEnsureRequest struct { OnlineDBID string `json:"online_db_id"` Tables []SchemaTableSpec `json:"tables"` } // SchemaEnsureItemResult 单表 ensure 结果。 type SchemaEnsureItemResult struct { Table string `json:"table"` OK bool `json:"ok"` Created bool `json:"created"` // true=本次新建;false=已存在跳过 Message string `json:"message,omitempty"` } // SchemaEnsureResult 批量 ensure 响应。 type SchemaEnsureResult struct { OK bool `json:"ok"` Results []SchemaEnsureItemResult `json:"results"` Message string `json:"message,omitempty"` } // SchemaDescribeRequest 从线上 A 拉取表结构(供本机建空表)。 type SchemaDescribeRequest struct { OnlineDBID string `json:"online_db_id"` Tables []string `json:"tables,omitempty"` // 空=全部业务表 } // SchemaTableDesc 线上表结构描述。 type SchemaTableDesc struct { Name string `json:"name"` PKColumn string `json:"pk_column"` Columns []string `json:"columns"` RowCount int64 `json:"row_count"` } // SchemaDescribeResult 表结构列表。 type SchemaDescribeResult struct { OK bool `json:"ok"` Tables []SchemaTableDesc `json:"tables"` Message string `json:"message,omitempty"` } const MaxSchemaEnsureTables = 100 // EnsureSchemasOnRemote 在线上 A 为每张表 CREATE IF NOT EXISTS(空表也建)。 func EnsureSchemasOnRemote(ctx context.Context, ch *Channel, req SchemaEnsureRequest) (*SchemaEnsureResult, error) { if ch == nil { return nil, fmt.Errorf("channel is nil") } if len(req.Tables) == 0 { return nil, fmt.Errorf("tables required") } if len(req.Tables) > MaxSchemaEnsureTables { return nil, fmt.Errorf("tables limit %d", MaxSchemaEnsureTables) } db, err := AcquireRemote(ch.Remote.Driver, ch.Remote.DSN) if err != nil { return nil, wrapOpenRemote(err) } if err := EnsureMeta(ctx, db, ch.Remote.Driver); err != nil { return nil, Retryablef("ensure meta: %v", err) } out := &SchemaEnsureResult{OK: true, Results: make([]SchemaEnsureItemResult, 0, len(req.Tables))} for _, spec := range req.Tables { table := strings.TrimSpace(spec.Table) item := SchemaEnsureItemResult{Table: table} if table == "" { item.Message = "table required" out.OK = false out.Results = append(out.Results, item) continue } if !tableInChannel(ch, table) { item.Message = "table rejected" out.OK = false out.Results = append(out.Results, item) continue } pkCol := strings.TrimSpace(spec.PKColumn) if pkCol == "" && ch.PKColumns != nil && strings.TrimSpace(ch.PKColumns[table]) != "" { pkCol = ch.PKColumns[table] } if pkCol == "" { pkCol = "id" } existed, err := tableExists(ctx, db, ch.Remote.Driver, table) if err != nil { return nil, Retryablef("table exists: %v", err) } if err := EnsureTableFromColumns(ctx, db, ch.Remote.Driver, table, pkCol, spec.Columns); err != nil { item.Message = err.Error() out.OK = false out.Results = append(out.Results, item) continue } item.OK = true item.Created = !existed if existed { item.Message = "already exists" } else { item.Message = "created" } out.Results = append(out.Results, item) } out.Message = "empty tables ensured on online A; client should also create missing local tables from schema describe" return out, nil } // DescribeSchemasFromRemote 列出线上 A 业务表结构(含空表),供本机 CREATE IF NOT EXISTS。 func DescribeSchemasFromRemote(ctx context.Context, ch *Channel, req SchemaDescribeRequest) (*SchemaDescribeResult, error) { if ch == nil { return nil, fmt.Errorf("channel is nil") } db, err := AcquireRemote(ch.Remote.Driver, ch.Remote.DSN) if err != nil { return nil, wrapOpenRemote(err) } if err := EnsureMeta(ctx, db, ch.Remote.Driver); err != nil { return nil, Retryablef("ensure meta: %v", err) } want := map[string]struct{}{} for _, t := range req.Tables { t = strings.TrimSpace(t) if t != "" { want[t] = struct{}{} } } names, err := ListTables(ctx, db, ch.Remote.Driver) if err != nil { return nil, Retryablef("list tables: %v", err) } out := &SchemaDescribeResult{OK: true, Tables: make([]SchemaTableDesc, 0, len(names))} for _, name := range names { if len(want) > 0 { if _, ok := want[name]; !ok { continue } } if !tableInChannel(ch, name) { continue } pkCol := "id" if ch.PKColumns != nil && strings.TrimSpace(ch.PKColumns[name]) != "" { pkCol = ch.PKColumns[name] } cols, err := listColumns(ctx, db, ch.Remote.Driver, name) if err != nil { return nil, Retryablef("columns %s: %v", name, err) } n, _ := countRows(ctx, db, ch.Remote.Driver, name) out.Tables = append(out.Tables, SchemaTableDesc{ Name: name, PKColumn: pkCol, Columns: cols, RowCount: n, }) } if len(out.Tables) == 0 { out.Message = "online A has no business tables yet; push or ensure-schema first" } else { out.Message = "use columns to CREATE TABLE IF NOT EXISTS on local B (including empty tables)" } return out, nil }