§22 M3-backend Plan B (1/2): slug-keyed PK rebuilds activate project #2 (v0.36.0)

Lands the table rebuilds migration 026 deferred until a second project exists,
shipped separately from per-project serving for migration hygiene. No behavior
change (deployments stay single-project 'default').

- migration 028_project_scoped_keys.sql: folds project_id into the slug-keyed
  PK/UNIQUE of 13 tables (cached_rfcs PK (slug)->(project_id,slug); the
  UNIQUE/PK on cached_branches, branch_visibility, branch_contribute_grants,
  stars, watches, pr_seen, branch_chat_seen, funder_consents, rfc_collaborators,
  contribution_requests, proposed_use_cases gain project_id) and makes the FKs
  to cached_rfcs on rfc_invitations/rfc_collaborators/contribution_requests
  composite (project_id, rfc_slug) -> cached_rfcs(project_id, slug).
- db.run_migrations: a `-- migrate:no-foreign-keys` migration runs with
  PRAGMA foreign_keys toggled OFF around it (required by SQLite's table-rebuild
  procedure) + foreign_key_check after, failing loudly on dangling refs.
- ON CONFLICT upsert targets for the rebuilt tables gain project_id so they
  match the new composite indexes (value still defaults to 'default').
- test_migration_028_project_scoped_keys.py: proves two-project same-slug
  coexistence, within-project uniqueness, composite-FK enforcement. 442 pass.
- design doc §2 marked shipped.

Per docs/superpowers/specs/2026-06-04-m3-backend-planb-design.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-04 06:30:37 -07:00
parent d5213fc2da
commit a117fbb521
13 changed files with 464 additions and 15 deletions
+2 -2
View File
@@ -742,7 +742,7 @@ def make_router(
"""
INSERT INTO branch_visibility (rfc_slug, branch_name, read_public, contribute_mode)
VALUES (?, ?, ?, ?)
ON CONFLICT(rfc_slug, branch_name) DO UPDATE SET
ON CONFLICT(project_id, rfc_slug, branch_name) DO UPDATE SET
read_public = excluded.read_public,
contribute_mode = excluded.contribute_mode
""",
@@ -896,7 +896,7 @@ def make_router(
"""
INSERT INTO branch_chat_seen (user_id, rfc_slug, branch_name, last_seen_message_id, seen_at)
VALUES (?, ?, ?, ?, datetime('now'))
ON CONFLICT(user_id, rfc_slug, branch_name) DO UPDATE SET
ON CONFLICT(project_id, user_id, rfc_slug, branch_name) DO UPDATE SET
last_seen_message_id = excluded.last_seen_message_id,
seen_at = excluded.seen_at
""",