Add host-first environment contracts (Local vs CI vs Prod), deps-only compose, and the Profile → Portrait → deep-access mock payment slice with device identity and auto-migrate on API startup. Co-authored-by: Cursor <cursoragent@cursor.com>
78 lines
1.8 KiB
Go
78 lines
1.8 KiB
Go
// Package db opens Postgres and runs SQL migrations.
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// Connect opens a pgx pool using DATABASE_URL-style DSN.
|
|
func Connect(ctx context.Context, databaseURL string) (*pgxpool.Pool, error) {
|
|
pool, err := pgxpool.New(ctx, databaseURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("pgxpool: %w", err)
|
|
}
|
|
if err := pool.Ping(ctx); err != nil {
|
|
pool.Close()
|
|
return nil, fmt.Errorf("ping: %w", err)
|
|
}
|
|
return pool, nil
|
|
}
|
|
|
|
// Migrate applies *.up.sql files under dir that are not yet recorded.
|
|
func Migrate(ctx context.Context, pool *pgxpool.Pool, dir string) error {
|
|
if _, err := pool.Exec(ctx, `
|
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
version text PRIMARY KEY,
|
|
applied_at timestamptz NOT NULL DEFAULT now()
|
|
)`); err != nil {
|
|
return fmt.Errorf("schema_migrations: %w", err)
|
|
}
|
|
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return fmt.Errorf("read migrations: %w", err)
|
|
}
|
|
var ups []string
|
|
for _, e := range entries {
|
|
name := e.Name()
|
|
if strings.HasSuffix(name, ".up.sql") {
|
|
ups = append(ups, name)
|
|
}
|
|
}
|
|
sort.Strings(ups)
|
|
|
|
for _, name := range ups {
|
|
version := strings.TrimSuffix(name, ".up.sql")
|
|
var exists bool
|
|
if err := pool.QueryRow(ctx,
|
|
`SELECT EXISTS(SELECT 1 FROM schema_migrations WHERE version=$1)`, version,
|
|
).Scan(&exists); err != nil {
|
|
return err
|
|
}
|
|
if exists {
|
|
continue
|
|
}
|
|
body, err := os.ReadFile(filepath.Join(dir, name))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// DDL (incl. CREATE EXTENSION) may not run inside a transaction.
|
|
if _, err := pool.Exec(ctx, string(body)); err != nil {
|
|
return fmt.Errorf("migrate %s: %w", name, err)
|
|
}
|
|
if _, err := pool.Exec(ctx,
|
|
`INSERT INTO schema_migrations(version) VALUES ($1)`, version,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|