// Package postgres implements the repository ports on top of pgx/pgxpool. package postgres import ( "context" "github.com/jackc/pgx/v5/pgxpool" ) const schema = ` CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, email TEXT NOT NULL, name TEXT NOT NULL, provider TEXT NOT NULL, subject TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (provider, subject) ); CREATE SEQUENCE IF NOT EXISTS link_code_seq; CREATE TABLE IF NOT EXISTS links ( id TEXT PRIMARY KEY, code TEXT UNIQUE NOT NULL, long_url TEXT NOT NULL, mode TEXT NOT NULL, has_pin BOOLEAN NOT NULL DEFAULT false, pin_hash TEXT NOT NULL DEFAULT '', owner_id TEXT, total_clicks BIGINT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS links_owner_idx ON links (owner_id); CREATE TABLE IF NOT EXISTS click_daily ( code TEXT NOT NULL, day DATE NOT NULL, count BIGINT NOT NULL DEFAULT 0, PRIMARY KEY (code, day) ); ` // migrationLockKey is an arbitrary, stable key for the advisory lock that // serializes schema creation across concurrently-starting services. const migrationLockKey = 947213 // Migrate opens a short-lived connection pool, runs the schema migrations, and // closes the pool. Safe to call concurrently: an advisory lock serialises DDL // across multiple callers. All statements use IF NOT EXISTS so re-runs are // idempotent and no data is lost. func Migrate(ctx context.Context, dsn string) error { pool, err := pgxpool.New(ctx, dsn) if err != nil { return err } defer pool.Close() if err := pool.Ping(ctx); err != nil { return err } return migrate(ctx, pool) } // Connect opens a pool and applies the schema (idempotent migrations). Because // all three commands may boot at once and each runs migrations, the DDL is // guarded by a session advisory lock so concurrent `CREATE ... IF NOT EXISTS` // can't race on the Postgres catalog. func Connect(ctx context.Context, dsn string) (*pgxpool.Pool, error) { pool, err := pgxpool.New(ctx, dsn) if err != nil { return nil, err } if err := pool.Ping(ctx); err != nil { pool.Close() return nil, err } if err := migrate(ctx, pool); err != nil { pool.Close() return nil, err } return pool, nil } func migrate(ctx context.Context, pool *pgxpool.Pool) error { conn, err := pool.Acquire(ctx) // advisory lock is session-scoped → one conn if err != nil { return err } defer conn.Release() if _, err := conn.Exec(ctx, "SELECT pg_advisory_lock($1)", migrationLockKey); err != nil { return err } defer conn.Exec(ctx, "SELECT pg_advisory_unlock($1)", migrationLockKey) _, err = conn.Exec(ctx, schema) return err }