package migrations import ( "context" "embed" "fmt" "io/fs" "sort" "github.com/jackc/pgx/v5/pgxpool" ) //go:embed *.sql var files embed.FS func Apply(ctx context.Context, pool *pgxpool.Pool) error { conn, err := pool.Acquire(ctx) if err != nil { return fmt.Errorf("acquire migration connection: %w", err) } defer conn.Release() if _, err = conn.Exec(ctx, `SELECT pg_advisory_lock(hashtext('yovision-bell-migrations'))`); err != nil { return fmt.Errorf("lock migrations: %w", err) } defer func() { _, _ = conn.Exec(context.Background(), `SELECT pg_advisory_unlock(hashtext('yovision-bell-migrations'))`) }() if _, err := conn.Exec(ctx, `CREATE TABLE IF NOT EXISTS bell_schema_migrations (name text PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now())`); err != nil { return fmt.Errorf("create migration ledger: %w", err) } entries, err := fs.ReadDir(files, ".") if err != nil { return fmt.Errorf("read embedded migrations: %w", err) } sort.Slice(entries, func(i, j int) bool { return entries[i].Name() < entries[j].Name() }) for _, entry := range entries { if entry.IsDir() || len(entry.Name()) < 4 || entry.Name()[len(entry.Name())-4:] != ".sql" { continue } var applied bool if err := conn.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM bell_schema_migrations WHERE name=$1)`, entry.Name()).Scan(&applied); err != nil { return err } if applied { continue } sqlBytes, err := files.ReadFile(entry.Name()) if err != nil { return err } tx, err := conn.Begin(ctx) if err != nil { return err } if _, err = tx.Exec(ctx, string(sqlBytes)); err == nil { _, err = tx.Exec(ctx, `INSERT INTO bell_schema_migrations(name) VALUES($1)`, entry.Name()) } if err != nil { _ = tx.Rollback(ctx) return fmt.Errorf("apply migration %s: %w", entry.Name(), err) } if err := tx.Commit(ctx); err != nil { return fmt.Errorf("commit migration %s: %w", entry.Name(), err) } } return nil }