Files
Agent-System/internal/db/migrate.go
T
agent-bot 686dede802
CI / go-vet (pull_request) Successful in 19s
CI / short-tests-report (pull_request) Failing after 35s
CI / backup-restore-drill (pull_request) Successful in 27s
CI / web (pull_request) Successful in 32s
feat(db): embedded goose migrations fallback (U10a first slice)
- deploy/postgres (package dbassets) embeds the full goose chain;
  RunMigrations prefers the on-disk dir (dev behavior unchanged) and falls
  back to the embedded FS when the source tree is absent (single-binary
  deployments per plan U10/KTD16)
- goose SetBaseFS global state reset after use
- TestRunMigrationsEmbeddedFallback verified against a throwaway isolated
  pgvector container: nonexistent dir -> embedded chain -> version 78,
  migration gate count agrees
2026-09-29 07:07:58 +00:00

128 lines
3.0 KiB
Go

package db
import (
"context"
"database/sql"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"chat-agent-system/deploy/postgres"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/jackc/pgx/v5/stdlib"
"github.com/pressly/goose/v3"
)
const schemaTable = "goose_db_version"
// RunMigrations 执行 goose 迁移链。
func RunMigrations(ctx context.Context, pool *pgxpool.Pool, migrationsDir string) error {
if pool == nil {
return fmt.Errorf("db migrate: pool required")
}
absDir, err := filepath.Abs(migrationsDir)
if err != nil {
return err
}
useDisk := true
if _, err := os.Stat(absDir); err != nil {
useDisk = false
}
db := stdlib.OpenDB(*pool.Config().ConnConfig)
defer db.Close()
if err := goose.SetDialect("postgres"); err != nil {
return err
}
goose.SetTableName(schemaTable)
if useDisk {
if err := goose.UpContext(ctx, db, absDir); err != nil {
return fmt.Errorf("db migrate up: %w", err)
}
return nil
}
// U10a:源码目录不存在(单文件部署)时回退内嵌迁移链。
// goose SetBaseFS 是全局状态;用后复位,避免污染后续磁盘调用。
goose.SetBaseFS(dbassets.MigrationsFS)
defer goose.SetBaseFS(nil)
if err := goose.UpContext(ctx, db, dbassets.MigrationsDir); err != nil {
return fmt.Errorf("db migrate up (embedded): %w", err)
}
return nil
}
// CurrentVersion 返回当前 schema 版本号。
func CurrentVersion(ctx context.Context, pool *pgxpool.Pool) (int64, error) {
if pool == nil {
return 0, fmt.Errorf("db migrate: pool required")
}
var version int64
err := pool.QueryRow(ctx, fmt.Sprintf(`
SELECT COALESCE(MAX(version_id), 0)::bigint FROM %s
`, schemaTable)).Scan(&version)
if err != nil {
// 表不存在时视为未迁移
if strings.Contains(err.Error(), "does not exist") {
return 0, nil
}
return 0, err
}
return version, nil
}
// ExpectedMigrationCount 统计 migrations 目录下 .sql 文件数量。
func ExpectedMigrationCount(migrationsDir string) (int, error) {
entries, err := os.ReadDir(migrationsDir)
if err != nil {
return 0, err
}
count := 0
for _, e := range entries {
if !e.IsDir() && strings.HasSuffix(e.Name(), ".sql") {
count++
}
}
return count, nil
}
// ResolveMigrationsDir 定位 goose 迁移目录。
func ResolveMigrationsDir() string {
candidates := []string{
"deploy/postgres/goose",
"../deploy/postgres/goose",
"../../deploy/postgres/goose",
}
for _, c := range candidates {
if fi, err := os.Stat(c); err == nil && fi.IsDir() {
return c
}
}
return "deploy/postgres/goose"
}
// OpenSQLDB 从 pgx pool 打开 database/sql(测试辅助)。
func OpenSQLDB(pool *pgxpool.Pool) *sql.DB {
return stdlib.OpenDB(*pool.Config().ConnConfig)
}
// ListMigrationFiles 按名称排序列出迁移文件(测试辅助)。
func ListMigrationFiles(dir string) ([]string, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
var files []string
for _, e := range entries {
if !e.IsDir() && strings.HasSuffix(e.Name(), ".sql") {
files = append(files, e.Name())
}
}
sort.Strings(files)
return files, nil
}