Files
digital-psychology/apps/api/internal/db/db.go
T
jackyu66gitandCursor 0f320e040b feat: P1 profile/portrait API and freeze local-dev environment
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>
2026-08-02 16:24:57 +08:00

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
}