Files
zheromiku dcdc8d1ddd feat: 知识治理平台(含已验证发布/回滚契约)
全栈知识治理平台首次入库(P1-P10 + 发布链路模拟验证闭环):

- 治理核心:源目录/变更集/人工审核/发布批次/发布执行(fail-closed 闸口)
- 测试通道:WeKnora 测试 KB 上传/轮询/验收;poll 完成时写入
  parser_generation/index_generation,修复 manual/seed 路径发布闸口永久拦截
- 发布适配器 v071(本地 source=测试角色 / 云端 target=生产角色 模拟验证通过):
  * ES 导出/导入 mapping 自适应(.keyword 子字段、可选 tenant_id)
  * OSS locator 支持嵌套 scheme;chunks.seq_id 由目标序列分配
  * bundle 归档同身份重试原子替换;verifier 按 bundle chunk_type 计数
  * 回滚 job payload 支持 prior_execution_id(strict 解码)
- 三通道发布+回滚演练实测:publish 4/4 verified,检索命中,
  rollback 4/4 rolled_back,目标端 PG is_enabled=false、ES is_enabled=false
- 前端 A·Data-console 风格全站统一,120 用例通过
- 门禁:go vet / go test 全包通过,gofmt 干净
2026-09-30 10:54:06 +08:00

141 lines
4.1 KiB
Go

package postgres
import (
"context"
"embed"
"fmt"
"io/fs"
"sort"
"strconv"
"strings"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
//go:embed migrations/*.sql
var migrationFS embed.FS
type migration struct {
Version int64
UpSQL string
DownSQL string
}
func loadMigrations() ([]migration, error) {
paths, err := fs.Glob(migrationFS, "migrations/*.up.sql")
if err != nil {
return nil, fmt.Errorf("glob migrations: %w", err)
}
result := make([]migration, 0, len(paths))
for _, path := range paths {
base := strings.TrimPrefix(path, "migrations/")
versionText, _, ok := strings.Cut(base, "_")
if !ok {
return nil, fmt.Errorf("invalid migration filename %q", path)
}
version, err := strconv.ParseInt(versionText, 10, 64)
if err != nil {
return nil, fmt.Errorf("parse migration version %q: %w", path, err)
}
up, err := migrationFS.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read up migration %q: %w", path, err)
}
downPath := strings.TrimSuffix(path, ".up.sql") + ".down.sql"
down, err := migrationFS.ReadFile(downPath)
if err != nil {
return nil, fmt.Errorf("read down migration %q: %w", downPath, err)
}
result = append(result, migration{Version: version, UpSQL: string(up), DownSQL: string(down)})
}
sort.Slice(result, func(i, j int) bool { return result[i].Version < result[j].Version })
return result, nil
}
const migrationLockID int64 = 7346729101
func ApplyMigrations(ctx context.Context, pool *pgxpool.Pool) error {
if _, err := pool.Exec(ctx, "SELECT pg_advisory_lock($1)", migrationLockID); err != nil {
return fmt.Errorf("lock migrations: %w", err)
}
defer func() { _, _ = pool.Exec(context.Background(), "SELECT pg_advisory_unlock($1)", migrationLockID) }()
if _, err := pool.Exec(ctx, `CREATE TABLE IF NOT EXISTS schema_migrations (
version BIGINT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)`); err != nil {
return fmt.Errorf("create migration ledger: %w", err)
}
migrations, err := loadMigrations()
if err != nil {
return err
}
for _, item := range migrations {
var applied bool
if err := pool.QueryRow(ctx, "SELECT EXISTS (SELECT 1 FROM schema_migrations WHERE version = $1)", item.Version).Scan(&applied); err != nil {
return fmt.Errorf("check migration %d: %w", item.Version, err)
}
if applied {
continue
}
tx, err := pool.BeginTx(ctx, pgx.TxOptions{})
if err != nil {
return fmt.Errorf("begin migration %d: %w", item.Version, err)
}
if _, err := tx.Exec(ctx, item.UpSQL); err != nil {
_ = tx.Rollback(ctx)
return fmt.Errorf("apply migration %d: %w", item.Version, err)
}
if _, err := tx.Exec(ctx, "INSERT INTO schema_migrations (version) VALUES ($1)", item.Version); err != nil {
_ = tx.Rollback(ctx)
return fmt.Errorf("record migration %d: %w", item.Version, err)
}
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("commit migration %d: %w", item.Version, err)
}
}
return nil
}
func RollbackLatestMigration(ctx context.Context, pool *pgxpool.Pool) error {
migrations, err := loadMigrations()
if err != nil {
return err
}
var version int64
if err := pool.QueryRow(ctx, "SELECT version FROM schema_migrations ORDER BY version DESC LIMIT 1").Scan(&version); err != nil {
if err == pgx.ErrNoRows {
return nil
}
return fmt.Errorf("read latest migration: %w", err)
}
var selected *migration
for i := range migrations {
if migrations[i].Version == version {
selected = &migrations[i]
break
}
}
if selected == nil {
return fmt.Errorf("missing down migration for version %d", version)
}
tx, err := pool.BeginTx(ctx, pgx.TxOptions{})
if err != nil {
return fmt.Errorf("begin rollback %d: %w", version, err)
}
if _, err := tx.Exec(ctx, selected.DownSQL); err != nil {
_ = tx.Rollback(ctx)
return fmt.Errorf("rollback migration %d: %w", version, err)
}
if _, err := tx.Exec(ctx, "DELETE FROM schema_migrations WHERE version = $1", version); err != nil {
_ = tx.Rollback(ctx)
return fmt.Errorf("delete migration ledger %d: %w", version, err)
}
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("commit rollback %d: %w", version, err)
}
return nil
}