5759c1862e
M0-M7 已完成:核心网关(身份/RBAC/TOTP/OIDC/SAML/Provider/配额/路由/内容策略/审计/定价)+ 资源市场(MCP/Skills/数字员工)。 含 22 个 PostgreSQL 迁移、管理端/门户端前端源码、OpenAPI 契约、部署 compose。 Co-Authored-By: Claude <noreply@anthropic.com>
114 lines
3.4 KiB
Go
114 lines
3.4 KiB
Go
package migrate
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
const advisoryLockID int64 = 6720240805
|
|
|
|
type Migration struct {
|
|
Version string
|
|
Filename string
|
|
SQL string
|
|
Checksum string
|
|
}
|
|
|
|
func Load(directory string) ([]Migration, error) {
|
|
entries, err := os.ReadDir(directory)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read migration directory: %w", err)
|
|
}
|
|
var filenames []string
|
|
for _, entry := range entries {
|
|
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".sql") {
|
|
filenames = append(filenames, entry.Name())
|
|
}
|
|
}
|
|
sort.Strings(filenames)
|
|
migrations := make([]Migration, 0, len(filenames))
|
|
for _, filename := range filenames {
|
|
body, err := os.ReadFile(filepath.Join(directory, filename))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read migration %s: %w", filename, err)
|
|
}
|
|
version, _, ok := strings.Cut(filename, "_")
|
|
if !ok || version == "" {
|
|
return nil, fmt.Errorf("migration %s must start with a version and underscore", filename)
|
|
}
|
|
digest := sha256.Sum256(body)
|
|
migrations = append(migrations, Migration{
|
|
Version: version, Filename: filename, SQL: string(body), Checksum: hex.EncodeToString(digest[:]),
|
|
})
|
|
}
|
|
return migrations, nil
|
|
}
|
|
|
|
func Apply(ctx context.Context, pool *pgxpool.Pool, migrations []Migration) error {
|
|
connection, err := pool.Acquire(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("acquire migration connection: %w", err)
|
|
}
|
|
defer connection.Release()
|
|
if _, err := connection.Exec(ctx, "SELECT pg_advisory_lock($1)", advisoryLockID); err != nil {
|
|
return fmt.Errorf("lock migrations: %w", err)
|
|
}
|
|
defer func() { _, _ = connection.Exec(context.Background(), "SELECT pg_advisory_unlock($1)", advisoryLockID) }()
|
|
|
|
if _, err := connection.Exec(ctx, `
|
|
CREATE SCHEMA IF NOT EXISTS gateway;
|
|
CREATE TABLE IF NOT EXISTS gateway.schema_migrations (
|
|
version text PRIMARY KEY,
|
|
filename text NOT NULL,
|
|
checksum text NOT NULL,
|
|
applied_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
|
)`); err != nil {
|
|
return fmt.Errorf("initialize migration table: %w", err)
|
|
}
|
|
|
|
for _, migration := range migrations {
|
|
var existingChecksum string
|
|
err := connection.QueryRow(ctx,
|
|
"SELECT checksum FROM gateway.schema_migrations WHERE version = $1", migration.Version,
|
|
).Scan(&existingChecksum)
|
|
if err == nil {
|
|
if existingChecksum != migration.Checksum {
|
|
return fmt.Errorf("migration %s checksum changed after application", migration.Filename)
|
|
}
|
|
continue
|
|
}
|
|
if !errors.Is(err, pgx.ErrNoRows) {
|
|
return fmt.Errorf("check migration %s: %w", migration.Filename, err)
|
|
}
|
|
transaction, err := connection.Begin(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("begin migration %s: %w", migration.Filename, err)
|
|
}
|
|
if _, err := transaction.Exec(ctx, migration.SQL); err != nil {
|
|
_ = transaction.Rollback(ctx)
|
|
return fmt.Errorf("apply migration %s: %w", migration.Filename, err)
|
|
}
|
|
if _, err := transaction.Exec(ctx,
|
|
"INSERT INTO gateway.schema_migrations (version, filename, checksum) VALUES ($1, $2, $3)",
|
|
migration.Version, migration.Filename, migration.Checksum,
|
|
); err != nil {
|
|
_ = transaction.Rollback(ctx)
|
|
return fmt.Errorf("record migration %s: %w", migration.Filename, err)
|
|
}
|
|
if err := transaction.Commit(ctx); err != nil {
|
|
return fmt.Errorf("commit migration %s: %w", migration.Filename, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|