feat: add schema fingerprint and Z10d alter; refresh sync coop docs

Ship ensure ADD COLUMN and fingerprint API for Yuheng reconcile, and rewrite §0.2–0.3 cooperation checklist in the联调意见.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
whm
2026-08-05 18:16:55 +08:00
parent cb76824e94
commit 4fae7ae718
5 changed files with 420 additions and 72 deletions

View File

@@ -37,8 +37,8 @@ func EnsureTableFromRow(ctx context.Context, db *sql.DB, driver Driver, table, p
return EnsureTableFromColumns(ctx, db, driver, table, pkCol, cols)
}
// EnsureTableFromColumns 表不存在时按列名建空表(全部 TEXT指定 PK已存在则幂等跳过。
// 用于空表结构同步:本机有空表 → 线上也建同名空表(无需 outbox 行)
// EnsureTableFromColumns 按列名建空表(全部 TEXT指定 PK
// 已存在时补齐缺失列ADD COLUMN解决「先按 _row_id 建表、后 push 注入 id」导致的缺列 503
func EnsureTableFromColumns(ctx context.Context, db *sql.DB, driver Driver, table, pkCol string, columns []string) error {
table = strings.TrimSpace(table)
pkCol = strings.TrimSpace(pkCol)
@@ -55,27 +55,57 @@ func EnsureTableFromColumns(ctx context.Context, db *sql.DB, driver Driver, tabl
if err != nil {
return err
}
if exists {
return nil
}
want := make([]string, 0, len(columns)+1)
seen := map[string]struct{}{}
defs := make([]string, 0, len(columns)+1)
defs = append(defs, fmt.Sprintf("%s TEXT PRIMARY KEY", quoteIdent(driver, pkCol)))
seen[pkCol] = struct{}{}
for _, c := range columns {
add := func(c string) {
c = strings.TrimSpace(c)
if c == "" {
continue
return
}
if _, ok := seen[c]; ok {
continue
return
}
seen[c] = struct{}{}
defs = append(defs, fmt.Sprintf("%s TEXT", quoteIdent(driver, c)))
want = append(want, c)
}
if len(defs) == 0 {
add(pkCol)
for _, c := range columns {
add(c)
}
if len(want) == 0 {
return fmt.Errorf("columns required")
}
if exists {
have, err := listColumns(ctx, db, driver, table)
if err != nil {
return err
}
haveSet := map[string]struct{}{}
for _, c := range have {
haveSet[c] = struct{}{}
}
for _, c := range want {
if _, ok := haveSet[c]; ok {
continue
}
ddl := fmt.Sprintf(`ALTER TABLE %s ADD COLUMN %s TEXT`, quoteIdent(driver, table), quoteIdent(driver, c))
if _, err := db.ExecContext(ctx, ddl); err != nil {
// SQLite 无 IF NOT EXISTS并发下可能已存在
if !strings.Contains(strings.ToLower(err.Error()), "duplicate column") {
return err
}
}
}
return nil
}
defs := make([]string, 0, len(want))
defs = append(defs, fmt.Sprintf("%s TEXT PRIMARY KEY", quoteIdent(driver, pkCol)))
for _, c := range want {
if c == pkCol {
continue
}
defs = append(defs, fmt.Sprintf("%s TEXT", quoteIdent(driver, c)))
}
ddl := fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s (%s)`, quoteIdent(driver, table), strings.Join(defs, ", "))
_, err = db.ExecContext(ctx, ddl)
return err