-- P2 competitive short-post variants. 022/023/024 exist. The runner keys -- migrations by filename; 025_archive_search.sql belongs to another slice. ALTER TABLE polls ADD COLUMN kind text NOT NULL DEFAULT 'yes_no' CHECK (kind IN ('yes_no', 'variant_selection')), ADD COLUMN winner_option_id uuid, ADD COLUMN draft_revision bigint, ADD COLUMN content_kind text NOT NULL DEFAULT 'short_post' CHECK (content_kind IN ('short_post', 'document')), ADD COLUMN eligible_user_ids uuid[]; -- document is a schema seam only; P2 accepts short_post exclusively. ALTER TABLE polls DROP CONSTRAINT polls_state_check; ALTER TABLE polls ADD CONSTRAINT polls_state_check CHECK (state IN ('open', 'passed', 'failed', 'vetoed', 'quorum_unreachable', 'cancelled', 'stale')); ALTER TABLE polls DROP CONSTRAINT polls_close_reason_check; ALTER TABLE polls ADD CONSTRAINT polls_close_reason_check CHECK (close_reason IN ('quorum_early', 'timebox', 'veto', 'cancelled', 'quorum_unreachable', 'reset_on_change')); CREATE TABLE poll_options ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE ON UPDATE RESTRICT, poll_id uuid NOT NULL, seq integer NOT NULL CHECK (seq > 0), -- Future document revision chains can anchor here. Short posts use the -- poll's numeric draft_revision plus this immutable text snapshot. draft_revision_id uuid, text text NOT NULL CHECK (char_length(text) BETWEEN 1 AND 5000), submitted_by uuid NOT NULL REFERENCES users(id) ON DELETE NO ACTION ON UPDATE RESTRICT, submitted_at timestamptz NOT NULL DEFAULT clock_timestamp(), UNIQUE (poll_id, seq), UNIQUE (id, poll_id, workspace_id), FOREIGN KEY (poll_id, workspace_id) REFERENCES polls(id, workspace_id) ON DELETE CASCADE ON UPDATE RESTRICT ); CREATE TRIGGER trg_poll_options_ws_immutable BEFORE UPDATE ON poll_options FOR EACH ROW EXECUTE FUNCTION reject_workspace_change(); -- Artifacts cannot be edited or individually removed, even by SQL callers. -- Cascading workspace/poll retention remains possible. CREATE FUNCTION chatgov_option_immutable() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF TG_OP = 'UPDATE' OR pg_trigger_depth() = 1 THEN RAISE EXCEPTION 'poll options are immutable' USING ERRCODE = '55000'; END IF; RETURN OLD; END $$; CREATE TRIGGER trg_poll_options_immutable BEFORE UPDATE OR DELETE ON poll_options FOR EACH ROW EXECUTE FUNCTION chatgov_option_immutable(); ALTER TABLE poll_options ENABLE ROW LEVEL SECURITY; ALTER TABLE poll_options FORCE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON poll_options USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid) WITH CHECK (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid); GRANT SELECT, INSERT, UPDATE, DELETE ON poll_options TO sp_app; ALTER TABLE poll_votes ADD COLUMN option_id uuid; ALTER TABLE poll_votes ADD CONSTRAINT poll_votes_option_fk FOREIGN KEY (option_id, poll_id, workspace_id) REFERENCES poll_options(id, poll_id, workspace_id) ON DELETE NO ACTION ON UPDATE RESTRICT; ALTER TABLE poll_votes ADD CONSTRAINT poll_votes_option_ballot_check CHECK (option_id IS NULL OR ballot = 'yes'); ALTER TABLE polls ADD CONSTRAINT polls_winner_option_fk FOREIGN KEY (winner_option_id, id, workspace_id) REFERENCES poll_options(id, poll_id, workspace_id) ON DELETE NO ACTION ON UPDATE RESTRICT; ALTER TABLE polls ADD CONSTRAINT polls_winner_state_check CHECK (winner_option_id IS NULL OR (kind = 'variant_selection' AND state = 'passed')); ALTER TABLE poll_votes ADD COLUMN voter_identity_id uuid REFERENCES chat_identities(id) ON DELETE NO ACTION ON UPDATE RESTRICT; ALTER TABLE polls ADD COLUMN final_evaluation jsonb; -- Narrow expiry discovery follows the publishing due-schedule house pattern: -- returns references only, never ballots/content. Mutations still use RLS. CREATE FUNCTION chatgov_due_polls() RETURNS TABLE(workspace_id uuid, id uuid) LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$ SELECT p.workspace_id, p.id FROM polls p WHERE p.state = 'open' AND p.closes_at <= now() ORDER BY p.closes_at LIMIT 500 $$; REVOKE ALL ON FUNCTION chatgov_due_polls() FROM PUBLIC; GRANT EXECUTE ON FUNCTION chatgov_due_polls() TO sp_app;