mirror of
https://github.com/ThisTine/Snip.git
synced 2026-08-18 23:18:47 +07:00
99 lines
2.7 KiB
Go
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
|
|
}
|