feat(slice-1): platform/db migration runner + migration 0001 (INV-7, §6.3 schema)
Forward-only, fail-stop, advisory-lock-guarded psycopg runner; migration 0001 lays the account/auth_code/storefront/membership schema. Tests prove migrate-from-empty and idempotent re-migrate (INV-1 partial) and the INV-4 many-capable shape. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
-- 0001_init.sql — SD-0001 §6.3 data model. Forward-only and fail-stop (INV-7):
|
||||
-- do NOT edit this file once it has merged/applied; add a new numbered migration.
|
||||
|
||||
-- account — a person's identity on the platform. Email is the canonical key (INV-2);
|
||||
-- the address is stored already-normalized (lowercased) by the service layer, so a
|
||||
-- plain unique index enforces one-account-per-email. Data minimization (OHM): email
|
||||
-- and nothing else (§6.3).
|
||||
CREATE TABLE account (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
email TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE UNIQUE INDEX account_email_key ON account (email); -- INV-2
|
||||
|
||||
-- auth_code — one-time codes, handled like secrets (INV-3): only the hash is stored;
|
||||
-- keyed by email (the code is issued before the account may exist — sign-up and
|
||||
-- log-in converge, PUC-3).
|
||||
CREATE TABLE auth_code (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
email TEXT NOT NULL,
|
||||
code_hash TEXT NOT NULL,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
attempts INTEGER NOT NULL DEFAULT 0,
|
||||
consumed_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX auth_code_email_idx ON auth_code (email);
|
||||
|
||||
-- storefront — a merchant's business presence. Name is always populated (a blank
|
||||
-- entry gets a generated default at the service layer, §6.3); "unnamed" is never NULL.
|
||||
CREATE TABLE storefront (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- storefront_membership — account <-> storefront ownership. INV-4: deliberately a
|
||||
-- many-to-many shape (composite PK, no UNIQUE(account_id)) so many-storefronts-per-
|
||||
-- account stays open; the one-storefront rule is a deletable service-layer guard, not
|
||||
-- a schema law. INV-5: every storefront-scoped row carries its storefront_id. `role`
|
||||
-- is a single-value enum today; it is the seam the staff/permissions model attaches to.
|
||||
CREATE TABLE storefront_membership (
|
||||
account_id BIGINT NOT NULL REFERENCES account (id),
|
||||
storefront_id BIGINT NOT NULL REFERENCES storefront (id),
|
||||
role TEXT NOT NULL DEFAULT 'owner' CHECK (role IN ('owner')),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (account_id, storefront_id)
|
||||
);
|
||||
Reference in New Issue
Block a user