Files
Snip/backend/internal/adapter/postgres/db.go
T
2026-06-15 21:25:57 +07:00

99 lines
2.7 KiB
Go

// 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
}