CREATE TABLE users ( id text PRIMARY KEY, wechat_identity text NOT NULL UNIQUE, created_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE sessions ( token_hash text PRIMARY KEY, user_id text NOT NULL REFERENCES users(id), expires_at timestamptz NOT NULL ); CREATE INDEX sessions_expiry ON sessions(expires_at); CREATE TABLE projects ( id text PRIMARY KEY, user_id text NOT NULL REFERENCES users(id), name text NOT NULL CHECK (char_length(btrim(name)) BETWEEN 1 AND 40), archived boolean NOT NULL DEFAULT false, created_at timestamptz NOT NULL DEFAULT now(), position bigint GENERATED ALWAYS AS IDENTITY, UNIQUE (user_id, id) ); CREATE INDEX projects_owner ON projects(user_id, position); CREATE TABLE records ( id text PRIMARY KEY, user_id text NOT NULL REFERENCES users(id), project_id text NOT NULL, practice_date date NOT NULL CHECK (practice_date >= DATE '0001-01-01'), minutes integer NOT NULL CHECK (minutes > 0), note text NOT NULL DEFAULT '' CHECK (char_length(note) <= 2000), created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), FOREIGN KEY (user_id, project_id) REFERENCES projects(user_id, id) ON DELETE RESTRICT ); CREATE INDEX records_owner_date ON records(user_id, practice_date DESC, id DESC); CREATE INDEX records_owner_updated ON records(user_id, updated_at DESC, id DESC); CREATE INDEX records_project ON records(user_id, project_id); -- Keep submission receipts after a record is deleted so a delayed retry cannot resurrect it. CREATE TABLE submissions ( user_id text NOT NULL REFERENCES users(id), request_id text NOT NULL, payload_hash text NOT NULL, record_id text NOT NULL, PRIMARY KEY (user_id, request_id) );