- 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
128 lines
3.0 KiB
Go
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
|
|
}
|