-- migrate:no-foreign-keys -- -- §22.13 / §22.4 — fold project_id into the slug-keyed PRIMARY KEY / UNIQUE -- constraints that migration 026 deliberately left global, so a *second* -- project can hold an entry with the same slug as the first. M1 (026) added -- project_id additively (no rebuild); this is the rebuild that activates -- project #2, enumerated in 026's header. -- -- SQLite can't ALTER a PK/UNIQUE in place, so each table is rebuilt by the -- official procedure: create `__new` with the new constraint, copy, DROP -- the live table, rename `__new` -> ``, recreate its indexes. FK -- enforcement is OFF for the whole file (the `migrate:no-foreign-keys` marker -- above tells the runner to toggle it and run foreign_key_check after). -- -- DROP-the-live-table (rather than rename-live-to-__old) is deliberate: SQLite -- rewrites child FK references when you RENAME a *referenced* table, so we drop -- the old cached_rfcs (allowed with FK off) and rename the temp in. cached_rfcs -- is rebuilt FIRST so rfc_collaborators / contribution_requests can re-point -- their FK at its new composite (project_id, slug) key. -- ── cached_rfcs: PRIMARY KEY (slug) -> (project_id, slug) ────────────────── CREATE TABLE cached_rfcs__new ( slug TEXT NOT NULL, title TEXT NOT NULL, state TEXT NOT NULL CHECK (state IN ('super-draft', 'active', 'withdrawn', 'retired')), rfc_id TEXT, repo TEXT, proposed_by TEXT, proposed_at TEXT, graduated_at TEXT, graduated_by TEXT, owners_json TEXT NOT NULL DEFAULT '[]', arbiters_json TEXT NOT NULL DEFAULT '[]', tags_json TEXT NOT NULL DEFAULT '[]', body TEXT, body_sha TEXT, last_main_commit_at TEXT, last_entry_commit_at TEXT, updated_at TEXT NOT NULL DEFAULT (datetime('now')), models_json TEXT, funder_login TEXT, proposed_use_case TEXT, project_id TEXT NOT NULL DEFAULT 'default', unreviewed INTEGER NOT NULL DEFAULT 0, reviewed_at TEXT, reviewed_by TEXT, PRIMARY KEY (project_id, slug) ); INSERT INTO cached_rfcs__new SELECT * FROM cached_rfcs; DROP TABLE cached_rfcs; ALTER TABLE cached_rfcs__new RENAME TO cached_rfcs; CREATE INDEX idx_cached_rfcs_state ON cached_rfcs (state); CREATE INDEX idx_cached_rfcs_last_active ON cached_rfcs ( COALESCE(last_main_commit_at, last_entry_commit_at) DESC ); CREATE INDEX idx_cached_rfcs_project ON cached_rfcs(project_id); -- ── rfc_invitations: FK rfc_slug -> cached_rfcs(slug) becomes composite ──── -- (no key change of its own, but its single-column FK to cached_rfcs is now a -- mismatch against the composite PK, so it must be rebuilt too). Rebuilt after -- cached_rfcs (its FK target) and before the two tables that FK rfc_invitations. CREATE TABLE rfc_invitations__new ( id INTEGER PRIMARY KEY AUTOINCREMENT, rfc_slug TEXT NOT NULL, inviter_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, invitee_email TEXT NOT NULL, role_in_rfc TEXT NOT NULL CHECK (role_in_rfc IN ('contributor', 'discussant')), status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'revoked', 'expired')), token TEXT NOT NULL, expires_at TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')), accepted_at TEXT, accepted_by_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, project_id TEXT NOT NULL DEFAULT 'default', FOREIGN KEY (project_id, rfc_slug) REFERENCES cached_rfcs(project_id, slug) ON DELETE CASCADE ); INSERT INTO rfc_invitations__new SELECT * FROM rfc_invitations; DROP TABLE rfc_invitations; ALTER TABLE rfc_invitations__new RENAME TO rfc_invitations; CREATE UNIQUE INDEX idx_rfc_invitations_token ON rfc_invitations (token); CREATE INDEX idx_rfc_invitations_rfc_status ON rfc_invitations (rfc_slug, status); CREATE INDEX idx_rfc_invitations_email_status ON rfc_invitations (invitee_email, status); -- ── cached_branches: UNIQUE (rfc_slug, branch_name) -> +project_id ───────── CREATE TABLE cached_branches__new ( id INTEGER PRIMARY KEY AUTOINCREMENT, rfc_slug TEXT NOT NULL, branch_name TEXT NOT NULL, head_sha TEXT, state TEXT NOT NULL DEFAULT 'open' CHECK (state IN ('open', 'closed', 'deleted')), pinned INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), last_commit_at TEXT, closed_at TEXT, project_id TEXT NOT NULL DEFAULT 'default', UNIQUE (project_id, rfc_slug, branch_name) ); INSERT INTO cached_branches__new SELECT * FROM cached_branches; DROP TABLE cached_branches; ALTER TABLE cached_branches__new RENAME TO cached_branches; CREATE INDEX idx_cached_branches_rfc ON cached_branches (rfc_slug, state); -- ── branch_visibility: UNIQUE (rfc_slug, branch_name) -> +project_id ─────── CREATE TABLE branch_visibility__new ( id INTEGER PRIMARY KEY AUTOINCREMENT, rfc_slug TEXT NOT NULL, branch_name TEXT NOT NULL, read_public INTEGER NOT NULL DEFAULT 1, contribute_mode TEXT NOT NULL DEFAULT 'just-me' CHECK (contribute_mode IN ('just-me', 'specific', 'any-contributor')), project_id TEXT NOT NULL DEFAULT 'default', UNIQUE (project_id, rfc_slug, branch_name) ); INSERT INTO branch_visibility__new SELECT * FROM branch_visibility; DROP TABLE branch_visibility; ALTER TABLE branch_visibility__new RENAME TO branch_visibility; -- ── branch_contribute_grants: UNIQUE (rfc_slug, branch_name, grantee) -> +pid CREATE TABLE branch_contribute_grants__new ( id INTEGER PRIMARY KEY AUTOINCREMENT, rfc_slug TEXT NOT NULL, branch_name TEXT NOT NULL, grantee_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, granted_by INTEGER NOT NULL REFERENCES users(id) ON DELETE SET NULL, granted_at TEXT NOT NULL DEFAULT (datetime('now')), project_id TEXT NOT NULL DEFAULT 'default', UNIQUE (project_id, rfc_slug, branch_name, grantee_user_id) ); INSERT INTO branch_contribute_grants__new SELECT * FROM branch_contribute_grants; DROP TABLE branch_contribute_grants; ALTER TABLE branch_contribute_grants__new RENAME TO branch_contribute_grants; CREATE INDEX idx_grants_lookup ON branch_contribute_grants (rfc_slug, branch_name); CREATE INDEX idx_grants_grantee ON branch_contribute_grants (grantee_user_id); -- ── stars: UNIQUE (user_id, rfc_slug) -> +project_id ─────────────────────── CREATE TABLE stars__new ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, rfc_slug TEXT NOT NULL, starred_at TEXT NOT NULL DEFAULT (datetime('now')), project_id TEXT NOT NULL DEFAULT 'default', UNIQUE (project_id, user_id, rfc_slug) ); INSERT INTO stars__new SELECT * FROM stars; DROP TABLE stars; ALTER TABLE stars__new RENAME TO stars; CREATE INDEX idx_stars_user ON stars (user_id); CREATE INDEX idx_stars_rfc ON stars (rfc_slug); -- ── watches: UNIQUE (user_id, rfc_slug) -> +project_id ───────────────────── CREATE TABLE watches__new ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, rfc_slug TEXT NOT NULL, state TEXT NOT NULL CHECK (state IN ('watching', 'following', 'muted')), set_by TEXT NOT NULL CHECK (set_by IN ('auto', 'explicit')), set_at TEXT NOT NULL DEFAULT (datetime('now')), last_participation_at TEXT, project_id TEXT NOT NULL DEFAULT 'default', UNIQUE (project_id, user_id, rfc_slug) ); INSERT INTO watches__new SELECT * FROM watches; DROP TABLE watches; ALTER TABLE watches__new RENAME TO watches; CREATE INDEX idx_watches_user ON watches (user_id); CREATE INDEX idx_watches_rfc ON watches (rfc_slug); CREATE INDEX idx_watches_decay ON watches (state, last_participation_at); -- ── pr_seen: UNIQUE (user_id, rfc_slug, pr_number) -> +project_id ────────── CREATE TABLE pr_seen__new ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, rfc_slug TEXT NOT NULL, pr_number INTEGER NOT NULL, last_seen_commit_sha TEXT, last_seen_message_id INTEGER REFERENCES thread_messages(id) ON DELETE SET NULL, seen_at TEXT NOT NULL DEFAULT (datetime('now')), project_id TEXT NOT NULL DEFAULT 'default', UNIQUE (project_id, user_id, rfc_slug, pr_number) ); INSERT INTO pr_seen__new SELECT * FROM pr_seen; DROP TABLE pr_seen; ALTER TABLE pr_seen__new RENAME TO pr_seen; -- ── branch_chat_seen: UNIQUE (user_id, rfc_slug, branch_name) -> +project_id CREATE TABLE branch_chat_seen__new ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, rfc_slug TEXT NOT NULL, branch_name TEXT NOT NULL, last_seen_message_id INTEGER REFERENCES thread_messages(id) ON DELETE SET NULL, seen_at TEXT NOT NULL DEFAULT (datetime('now')), project_id TEXT NOT NULL DEFAULT 'default', UNIQUE (project_id, user_id, rfc_slug, branch_name) ); INSERT INTO branch_chat_seen__new SELECT * FROM branch_chat_seen; DROP TABLE branch_chat_seen; ALTER TABLE branch_chat_seen__new RENAME TO branch_chat_seen; -- ── funder_consents: PRIMARY KEY (user_id, rfc_slug) -> +project_id ──────── CREATE TABLE funder_consents__new ( user_id INTEGER NOT NULL, rfc_slug TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')), project_id TEXT NOT NULL DEFAULT 'default', PRIMARY KEY (project_id, user_id, rfc_slug), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); INSERT INTO funder_consents__new SELECT * FROM funder_consents; DROP TABLE funder_consents; ALTER TABLE funder_consents__new RENAME TO funder_consents; CREATE INDEX idx_funder_consents_slug ON funder_consents (rfc_slug); -- ── rfc_collaborators: UNIQUE idx (rfc_slug, user_id) -> +project_id; -- FK rfc_slug -> cached_rfcs(slug) becomes composite (project_id, rfc_slug) CREATE TABLE rfc_collaborators__new ( id INTEGER PRIMARY KEY AUTOINCREMENT, rfc_slug TEXT NOT NULL, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, role_in_rfc TEXT NOT NULL CHECK (role_in_rfc IN ('contributor', 'discussant')), invitation_id INTEGER REFERENCES rfc_invitations(id) ON DELETE SET NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')), project_id TEXT NOT NULL DEFAULT 'default', FOREIGN KEY (project_id, rfc_slug) REFERENCES cached_rfcs(project_id, slug) ON DELETE CASCADE ); INSERT INTO rfc_collaborators__new SELECT * FROM rfc_collaborators; DROP TABLE rfc_collaborators; ALTER TABLE rfc_collaborators__new RENAME TO rfc_collaborators; CREATE UNIQUE INDEX idx_rfc_collaborators_unique ON rfc_collaborators (project_id, rfc_slug, user_id); CREATE INDEX idx_rfc_collaborators_user ON rfc_collaborators (user_id); -- ── contribution_requests: UNIQUE idx (rfc_slug, requester) WHERE pending -- -> +project_id; FK rfc_slug -> cached_rfcs(slug) becomes composite. CREATE TABLE contribution_requests__new ( id INTEGER PRIMARY KEY AUTOINCREMENT, rfc_slug TEXT NOT NULL, requester_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, matched_term TEXT NOT NULL, who_i_am TEXT NOT NULL, why TEXT NOT NULL, use_case TEXT, status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'declined')), created_at TEXT NOT NULL DEFAULT (datetime('now')), decided_at TEXT, decided_by_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, invitation_id INTEGER REFERENCES rfc_invitations(id) ON DELETE SET NULL, notification_id INTEGER REFERENCES notifications(id) ON DELETE SET NULL, project_id TEXT NOT NULL DEFAULT 'default', FOREIGN KEY (project_id, rfc_slug) REFERENCES cached_rfcs(project_id, slug) ON DELETE CASCADE ); INSERT INTO contribution_requests__new SELECT * FROM contribution_requests; DROP TABLE contribution_requests; ALTER TABLE contribution_requests__new RENAME TO contribution_requests; CREATE INDEX idx_contribution_requests_rfc ON contribution_requests(rfc_slug, status); CREATE INDEX idx_contribution_requests_requester ON contribution_requests(requester_user_id, status); CREATE UNIQUE INDEX idx_contribution_requests_one_open ON contribution_requests(project_id, rfc_slug, requester_user_id) WHERE status = 'pending'; -- ── proposed_use_cases: UNIQUE (scope, pr_number) -> +project_id ─────────── CREATE TABLE proposed_use_cases__new ( id INTEGER PRIMARY KEY AUTOINCREMENT, scope TEXT NOT NULL CHECK (scope IN ('rfc', 'pr')), rfc_slug TEXT NOT NULL, pr_number INTEGER NOT NULL, use_case TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')), project_id TEXT NOT NULL DEFAULT 'default', UNIQUE (project_id, scope, pr_number) ); INSERT INTO proposed_use_cases__new SELECT * FROM proposed_use_cases; DROP TABLE proposed_use_cases; ALTER TABLE proposed_use_cases__new RENAME TO proposed_use_cases; CREATE INDEX idx_proposed_use_cases_lookup ON proposed_use_cases (scope, pr_number); CREATE INDEX idx_proposed_use_cases_slug ON proposed_use_cases (scope, rfc_slug);