feat: use postgres

This commit is contained in:
Ilia Denisov
2026-04-26 20:34:39 +02:00
committed by GitHub
parent 48b0056b49
commit fe829285a6
365 changed files with 29223 additions and 24049 deletions
@@ -0,0 +1,169 @@
-- +goose Up
-- accounts holds the editable source-of-truth user-account state.
-- email and user_name remain UNIQUE for both live and soft-deleted records:
-- emails are never reassigned to a fresh user_id after DeleteUser, and
-- user_name is immutable for the lifetime of the account.
CREATE TABLE accounts (
user_id text PRIMARY KEY,
email text NOT NULL,
user_name text NOT NULL,
display_name text NOT NULL DEFAULT '',
preferred_language text NOT NULL,
time_zone text NOT NULL,
declared_country text,
created_at timestamptz NOT NULL,
updated_at timestamptz NOT NULL,
deleted_at timestamptz,
CONSTRAINT accounts_email_unique UNIQUE (email),
CONSTRAINT accounts_user_name_unique UNIQUE (user_name)
);
-- Newest-first listing index used by the trusted admin user-list surface.
CREATE INDEX accounts_listing_idx
ON accounts (created_at DESC, user_id DESC);
-- Reverse-lookup index for the optional declared-country filter; the partial
-- predicate keeps the index small while declared_country is mostly NULL.
CREATE INDEX accounts_declared_country_idx
ON accounts (declared_country)
WHERE declared_country IS NOT NULL;
-- blocked_emails persists pre-user blocked-email subjects that may exist
-- before any user account exists, plus the blocked subjects produced by
-- BlockByUserID/BlockByEmail. resolved_user_id is populated when the block
-- corresponds to an existing or formerly existing account.
CREATE TABLE blocked_emails (
email text PRIMARY KEY,
reason_code text NOT NULL,
blocked_at timestamptz NOT NULL,
actor_type text,
actor_id text,
resolved_user_id text
);
-- entitlement_records stores the immutable history of entitlement periods.
-- Each row represents one segment that was current at some point; closed
-- segments carry closed_* metadata.
CREATE TABLE entitlement_records (
record_id text PRIMARY KEY,
user_id text NOT NULL REFERENCES accounts(user_id),
plan_code text NOT NULL,
source text NOT NULL,
actor_type text NOT NULL,
actor_id text,
reason_code text NOT NULL,
starts_at timestamptz NOT NULL,
ends_at timestamptz,
created_at timestamptz NOT NULL,
closed_at timestamptz,
closed_by_type text,
closed_by_id text,
closed_reason_code text
);
CREATE INDEX entitlement_records_user_idx
ON entitlement_records (user_id, created_at DESC);
-- entitlement_snapshots stores the read-optimized current entitlement state.
-- Exactly one row per user_id; updated atomically together with history rows
-- by EntitlementLifecycleStore operations.
CREATE TABLE entitlement_snapshots (
user_id text PRIMARY KEY REFERENCES accounts(user_id),
plan_code text NOT NULL,
is_paid boolean NOT NULL,
starts_at timestamptz NOT NULL,
ends_at timestamptz,
source text NOT NULL,
actor_type text NOT NULL,
actor_id text,
reason_code text NOT NULL,
updated_at timestamptz NOT NULL
);
-- Coarse free-versus-paid filter used by the admin listing surface.
CREATE INDEX entitlement_snapshots_paid_state_idx
ON entitlement_snapshots (is_paid, plan_code);
-- Finite paid-expiry filter; partial predicate keeps the index limited to
-- finite paid plans (paid_monthly, paid_yearly).
CREATE INDEX entitlement_snapshots_paid_expiry_idx
ON entitlement_snapshots (ends_at)
WHERE is_paid AND ends_at IS NOT NULL;
-- sanction_records stores the immutable history of sanction mutations.
-- A row may carry removed_at + removed_* fields once the sanction is lifted.
CREATE TABLE sanction_records (
record_id text PRIMARY KEY,
user_id text NOT NULL REFERENCES accounts(user_id),
sanction_code text NOT NULL,
scope text NOT NULL,
reason_code text NOT NULL,
actor_type text NOT NULL,
actor_id text,
applied_at timestamptz NOT NULL,
expires_at timestamptz,
removed_at timestamptz,
removed_by_type text,
removed_by_id text,
removed_reason_code text
);
CREATE INDEX sanction_records_user_idx
ON sanction_records (user_id, applied_at DESC);
-- sanction_active stores the at-most-one active record per (user_id,
-- sanction_code). It is maintained by PolicyLifecycleStore in the same
-- transaction as the corresponding sanction_records mutation.
CREATE TABLE sanction_active (
user_id text NOT NULL REFERENCES accounts(user_id),
sanction_code text NOT NULL,
record_id text NOT NULL REFERENCES sanction_records(record_id),
PRIMARY KEY (user_id, sanction_code)
);
CREATE INDEX sanction_active_code_idx
ON sanction_active (sanction_code);
-- limit_records mirrors sanction_records for user-specific limit overrides.
CREATE TABLE limit_records (
record_id text PRIMARY KEY,
user_id text NOT NULL REFERENCES accounts(user_id),
limit_code text NOT NULL,
value integer NOT NULL,
reason_code text NOT NULL,
actor_type text NOT NULL,
actor_id text,
applied_at timestamptz NOT NULL,
expires_at timestamptz,
removed_at timestamptz,
removed_by_type text,
removed_by_id text,
removed_reason_code text
);
CREATE INDEX limit_records_user_idx
ON limit_records (user_id, applied_at DESC);
-- limit_active mirrors sanction_active for user-specific limits. value is
-- denormalised so the admin listing predicate can read it without joining
-- the full history.
CREATE TABLE limit_active (
user_id text NOT NULL REFERENCES accounts(user_id),
limit_code text NOT NULL,
record_id text NOT NULL REFERENCES limit_records(record_id),
value integer NOT NULL,
PRIMARY KEY (user_id, limit_code)
);
CREATE INDEX limit_active_code_idx
ON limit_active (limit_code);
-- +goose Down
DROP TABLE IF EXISTS limit_active;
DROP TABLE IF EXISTS limit_records;
DROP TABLE IF EXISTS sanction_active;
DROP TABLE IF EXISTS sanction_records;
DROP TABLE IF EXISTS entitlement_snapshots;
DROP TABLE IF EXISTS entitlement_records;
DROP TABLE IF EXISTS blocked_emails;
DROP TABLE IF EXISTS accounts;
@@ -0,0 +1,19 @@
// Package migrations exposes the embedded goose migration files used by
// User Service to provision its `user` schema in PostgreSQL.
//
// The embedded filesystem is consumed by `pkg/postgres.RunMigrations`
// during user-service startup and by `cmd/jetgen` when regenerating the
// `internal/adapters/postgres/jet/` code against a transient PostgreSQL
// instance.
package migrations
import "embed"
//go:embed *.sql
var fs embed.FS
// FS returns the embedded filesystem containing every numbered goose
// migration shipped with User Service.
func FS() embed.FS {
return fs
}