§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
@@ -0,0 +1,95 @@
"""§22.13 / migration 028 — the slug-keyed PK/UNIQUE rebuild that activates
project #2. Proves two projects can hold the same slug, that (project_id, slug)
is still unique within a project, that the rebuilt FK is composite + enforced,
and that the no-foreign-keys migration runner left no dangling references."""
from __future__ import annotations
import sqlite3
import tempfile
from pathlib import Path
import pytest
from app import db
class _Cfg:
def __init__(self, path):
self.database_path = path
def _fresh_db():
d = tempfile.mkdtemp()
path = Path(d) / "t.db"
db.run_migrations(_Cfg(str(path)))
return db.connect(str(path))
def _seed_two_projects(conn):
for pid in ("default", "ecomm"):
conn.execute(
"INSERT OR IGNORE INTO projects (id, name, type, content_repo, visibility, initial_state) "
"VALUES (?, ?, 'document', ?, 'public', 'super-draft')",
(pid, pid.title(), pid + "-content"),
)
def test_same_slug_coexists_across_projects():
conn = _fresh_db()
_seed_two_projects(conn)
for pid in ("default", "ecomm"):
conn.execute(
"INSERT INTO cached_rfcs (slug, title, state, project_id) "
"VALUES ('intro', 'Intro', 'active', ?)",
(pid,),
)
rows = conn.execute(
"SELECT project_id FROM cached_rfcs WHERE slug = 'intro' ORDER BY project_id"
).fetchall()
assert [r["project_id"] for r in rows] == ["default", "ecomm"]
def test_slug_still_unique_within_a_project():
conn = _fresh_db()
_seed_two_projects(conn)
conn.execute(
"INSERT INTO cached_rfcs (slug, title, state, project_id) "
"VALUES ('intro', 'Intro', 'active', 'default')"
)
with pytest.raises(sqlite3.IntegrityError):
conn.execute(
"INSERT INTO cached_rfcs (slug, title, state, project_id) "
"VALUES ('intro', 'Dup', 'active', 'default')"
)
def test_rfc_collaborators_composite_fk_enforced():
conn = _fresh_db()
_seed_two_projects(conn)
conn.execute("INSERT INTO users (id, gitea_login, display_name, role) VALUES (1, 'a', 'A', 'contributor')")
conn.execute(
"INSERT INTO cached_rfcs (slug, title, state, project_id) "
"VALUES ('intro', 'Intro', 'active', 'ecomm')"
)
# Matching (project_id, slug) — FK holds.
conn.execute(
"INSERT INTO rfc_collaborators (rfc_slug, user_id, role_in_rfc, project_id) "
"VALUES ('intro', 1, 'contributor', 'ecomm')"
)
# Same slug but a project with no such entry — composite FK must reject.
with pytest.raises(sqlite3.IntegrityError):
conn.execute(
"INSERT INTO rfc_collaborators (rfc_slug, user_id, role_in_rfc, project_id) "
"VALUES ('intro', 1, 'contributor', 'default')"
)
def test_stars_unique_now_scoped_by_project():
conn = _fresh_db()
_seed_two_projects(conn)
conn.execute("INSERT INTO users (id, gitea_login, display_name, role) VALUES (1, 'a', 'A', 'contributor')")
# Same (user, slug) under two projects coexist; a duplicate within one rejects.
conn.execute("INSERT INTO stars (user_id, rfc_slug, project_id) VALUES (1, 'intro', 'default')")
conn.execute("INSERT INTO stars (user_id, rfc_slug, project_id) VALUES (1, 'intro', 'ecomm')")
with pytest.raises(sqlite3.IntegrityError):
conn.execute("INSERT INTO stars (user_id, rfc_slug, project_id) VALUES (1, 'intro', 'default')")