feat: harden loose-offline sync for user JWT, schema, and console ops
Enable Binding-scoped agent push/pull, empty-table schema ensure, SyncPage inspect/drop-table, default module import, and agent-bound publish docs from the 宇恒联调意见. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
184
platform/internal/dbsync/schema.go
Normal file
184
platform/internal/dbsync/schema.go
Normal file
@@ -0,0 +1,184 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user