From cf461404c0fb47b5941d5a660e57ee1d365ec260 Mon Sep 17 00:00:00 2001 From: Jose Cruz Date: Sun, 21 Jun 2026 23:52:51 -0400 Subject: [PATCH 01/14] chore(db): scaffold FK-integrity migration with audit references (#179, #180) --- backend/db/migration_fk_integrity.sql | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 backend/db/migration_fk_integrity.sql diff --git a/backend/db/migration_fk_integrity.sql b/backend/db/migration_fk_integrity.sql new file mode 100644 index 00000000..66487a9b --- /dev/null +++ b/backend/db/migration_fk_integrity.sql @@ -0,0 +1,13 @@ +-- Migration: foreign-key integrity for graph_edges + notes +-- Run once in the Supabase SQL editor (idempotent — safe to re-run). +-- +-- Closes the orphan-row gaps where a user_id/course_id is a bare TEXT column +-- with no REFERENCES, inconsistent with every sibling table: +-- #179 graph_edges.user_id -> users(id) +-- #180 notes.user_id -> users(id) +-- #180 notes.course_id -> courses(id) +-- +-- Each constraint is added behind the same pg_constraint guard the codebase +-- already uses in migration_gradebook.sql, because Postgres has no +-- ADD CONSTRAINT IF NOT EXISTS. Pre-existing orphan rows are deleted first so +-- the ALTER TABLE can validate. From db92cf3de9ac15d12f9a3246cf27034e80c5568e Mon Sep 17 00:00:00 2001 From: Jose Cruz Date: Sun, 21 Jun 2026 23:52:58 -0400 Subject: [PATCH 02/14] fix(db): delete orphan graph_edges before adding user_id FK (#179) --- backend/db/migration_fk_integrity.sql | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/db/migration_fk_integrity.sql b/backend/db/migration_fk_integrity.sql index 66487a9b..7026b2f7 100644 --- a/backend/db/migration_fk_integrity.sql +++ b/backend/db/migration_fk_integrity.sql @@ -11,3 +11,8 @@ -- already uses in migration_gradebook.sql, because Postgres has no -- ADD CONSTRAINT IF NOT EXISTS. Pre-existing orphan rows are deleted first so -- the ALTER TABLE can validate. + +-- #179 graph_edges.user_id: remove edges whose user_id has no users row, then +-- add the FK other learning tables already enforce. +DELETE FROM graph_edges + WHERE user_id NOT IN (SELECT id FROM users); From f8a14516a36736b18424ab2a69314b7293994a47 Mon Sep 17 00:00:00 2001 From: Jose Cruz Date: Sun, 21 Jun 2026 23:53:04 -0400 Subject: [PATCH 03/14] fix(db): add graph_edges.user_id FK to users(id), idempotently guarded (#179) --- backend/db/migration_fk_integrity.sql | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/backend/db/migration_fk_integrity.sql b/backend/db/migration_fk_integrity.sql index 7026b2f7..269589e2 100644 --- a/backend/db/migration_fk_integrity.sql +++ b/backend/db/migration_fk_integrity.sql @@ -16,3 +16,14 @@ -- add the FK other learning tables already enforce. DELETE FROM graph_edges WHERE user_id NOT IN (SELECT id FROM users); + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'graph_edges_user_id_fkey' + ) THEN + ALTER TABLE graph_edges + ADD CONSTRAINT graph_edges_user_id_fkey + FOREIGN KEY (user_id) REFERENCES users(id); + END IF; +END $$; From f6b761b643b1c69dbab3e3f2cbeb8dfe2f040949 Mon Sep 17 00:00:00 2001 From: Jose Cruz Date: Sun, 21 Jun 2026 23:53:12 -0400 Subject: [PATCH 04/14] fix(db): delete orphan notes before adding user/course FKs (#180) --- backend/db/migration_fk_integrity.sql | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/backend/db/migration_fk_integrity.sql b/backend/db/migration_fk_integrity.sql index 269589e2..386fd825 100644 --- a/backend/db/migration_fk_integrity.sql +++ b/backend/db/migration_fk_integrity.sql @@ -27,3 +27,10 @@ BEGIN FOREIGN KEY (user_id) REFERENCES users(id); END IF; END $$; + +-- #180 notes.user_id / notes.course_id: notes is core user data but both +-- columns are bare TEXT. Remove rows pointing at a non-existent user or course +-- (e.g. notes left dangling after a course delete) before adding the FKs. +DELETE FROM notes + WHERE user_id NOT IN (SELECT id FROM users) + OR course_id NOT IN (SELECT id FROM courses); From 5ea6a392d1a18585bac4d1032786ccad836cd46e Mon Sep 17 00:00:00 2001 From: Jose Cruz Date: Sun, 21 Jun 2026 23:53:18 -0400 Subject: [PATCH 05/14] fix(db): add notes.user_id FK to users(id), idempotently guarded (#180) --- backend/db/migration_fk_integrity.sql | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/backend/db/migration_fk_integrity.sql b/backend/db/migration_fk_integrity.sql index 386fd825..20a7e630 100644 --- a/backend/db/migration_fk_integrity.sql +++ b/backend/db/migration_fk_integrity.sql @@ -34,3 +34,14 @@ END $$; DELETE FROM notes WHERE user_id NOT IN (SELECT id FROM users) OR course_id NOT IN (SELECT id FROM courses); + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'notes_user_id_fkey' + ) THEN + ALTER TABLE notes + ADD CONSTRAINT notes_user_id_fkey + FOREIGN KEY (user_id) REFERENCES users(id); + END IF; +END $$; From aef9ff0d6b621e7ed1bfafccc0eea03f3a625d0b Mon Sep 17 00:00:00 2001 From: Jose Cruz Date: Sun, 21 Jun 2026 23:53:24 -0400 Subject: [PATCH 06/14] fix(db): add notes.course_id FK to courses(id), idempotently guarded (#180) --- backend/db/migration_fk_integrity.sql | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/backend/db/migration_fk_integrity.sql b/backend/db/migration_fk_integrity.sql index 20a7e630..de6014cf 100644 --- a/backend/db/migration_fk_integrity.sql +++ b/backend/db/migration_fk_integrity.sql @@ -45,3 +45,14 @@ BEGIN FOREIGN KEY (user_id) REFERENCES users(id); END IF; END $$; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'notes_course_id_fkey' + ) THEN + ALTER TABLE notes + ADD CONSTRAINT notes_course_id_fkey + FOREIGN KEY (course_id) REFERENCES courses(id); + END IF; +END $$; From 41c3d866224595ccdb2b069a6640bdca37768951 Mon Sep 17 00:00:00 2001 From: Jose Cruz Date: Sun, 21 Jun 2026 23:53:37 -0400 Subject: [PATCH 07/14] fix(db): add REFERENCES users(id) to graph_edges.user_id in schema (#179) --- backend/db/supabase_schema.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/db/supabase_schema.sql b/backend/db/supabase_schema.sql index 13f687dd..6e18bbc3 100644 --- a/backend/db/supabase_schema.sql +++ b/backend/db/supabase_schema.sql @@ -94,7 +94,7 @@ CREATE INDEX IF NOT EXISTS idx_graph_nodes_user_course ON graph_nodes(user_id, c -- Knowledge graph edges CREATE TABLE IF NOT EXISTS graph_edges ( id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, + user_id TEXT NOT NULL REFERENCES users(id), -- #179 source_node_id TEXT NOT NULL REFERENCES graph_nodes(id), target_node_id TEXT NOT NULL REFERENCES graph_nodes(id), strength DOUBLE PRECISION DEFAULT 0.5, From 3bfb2b54efc63d4e83b968245957976c65e52ab5 Mon Sep 17 00:00:00 2001 From: Jose Cruz Date: Sun, 21 Jun 2026 23:53:53 -0400 Subject: [PATCH 08/14] fix(db): add REFERENCES to notes.user_id/course_id in schema (#180) --- backend/db/supabase_schema.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/db/supabase_schema.sql b/backend/db/supabase_schema.sql index 6e18bbc3..5b3e6f44 100644 --- a/backend/db/supabase_schema.sql +++ b/backend/db/supabase_schema.sql @@ -495,8 +495,8 @@ CREATE TABLE IF NOT EXISTS user_cosmetics ( CREATE TABLE notes ( id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - course_id TEXT NOT NULL, + user_id TEXT NOT NULL REFERENCES users(id), -- #180 + course_id TEXT NOT NULL REFERENCES courses(id), -- #180 title TEXT, body TEXT, tags TEXT[] NOT NULL DEFAULT '{}', From 4c319d8481450115c3efbf77f6d3306da186f56c Mon Sep 17 00:00:00 2001 From: Jose Cruz Date: Sun, 21 Jun 2026 23:54:11 -0400 Subject: [PATCH 09/14] =?UTF-8?q?docs(db):=20clarify=20notes=20FK=20ration?= =?UTF-8?q?ale=20=E2=80=94=20only=20the=20graph=5Fnode=20link=20stays=20so?= =?UTF-8?q?ft=20(#180)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/db/supabase_schema.sql | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/db/supabase_schema.sql b/backend/db/supabase_schema.sql index 5b3e6f44..215b6094 100644 --- a/backend/db/supabase_schema.sql +++ b/backend/db/supabase_schema.sql @@ -487,11 +487,13 @@ CREATE TABLE IF NOT EXISTS user_cosmetics ( -- filters work for tag-based search. last_summary is the cached output of -- the most recent /summarize action; null until the user runs it. -- +-- notes.user_id / notes.course_id DO carry hard FKs (#180) — they are core +-- user data and must not dangle past a user/course delete. +-- -- note_concepts is a junction table linking notes <-> graph_nodes. -- ON DELETE CASCADE on note_id ensures deleting a note cleans up its --- links. The graph_node FK is intentionally NOT a hard FK because --- graph_nodes uses TEXT ids managed by application code (no enforced FK --- pattern elsewhere in this codebase — see graph_edges.source_node_id). +-- links. Only the note_concepts.concept_node_id link is intentionally NOT a +-- hard FK, because graph_nodes uses TEXT ids managed by application code. CREATE TABLE notes ( id TEXT PRIMARY KEY, From 75d3729733dfe1655c9efecfc951f7e447a46ace Mon Sep 17 00:00:00 2001 From: Jose Cruz Date: Sun, 21 Jun 2026 23:54:34 -0400 Subject: [PATCH 10/14] test(db): drift-guard FK-integrity migration constraints + orphan cleanup --- backend/tests/test_fk_integrity_migration.py | 47 ++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 backend/tests/test_fk_integrity_migration.py diff --git a/backend/tests/test_fk_integrity_migration.py b/backend/tests/test_fk_integrity_migration.py new file mode 100644 index 00000000..5337e963 --- /dev/null +++ b/backend/tests/test_fk_integrity_migration.py @@ -0,0 +1,47 @@ +""" +Drift guard for the FK-integrity migration (#179, #180). + +No live Postgres in the unit suite, so we assert the textual invariants that +matter: the forward migration adds each constraint behind the pg_constraint +guard (re-runnable) and cleans orphans first, and the canonical schema declares +the same REFERENCES inline so a fresh database is born with the FKs. +""" +import os + +_DB = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "db") + +CONSTRAINTS = ( + "graph_edges_user_id_fkey", + "notes_user_id_fkey", + "notes_course_id_fkey", +) + + +def _read(name: str) -> str: + with open(os.path.join(_DB, name), encoding="utf-8") as fh: + return fh.read() + + +def test_migration_adds_each_constraint_behind_a_guard(): + sql = _read("migration_fk_integrity.sql") + for name in CONSTRAINTS: + assert name in sql, f"{name} missing from migration" + # Every ADD CONSTRAINT must be inside an IF NOT EXISTS pg_constraint guard. + assert sql.count("IF NOT EXISTS") >= len(CONSTRAINTS) + assert "pg_constraint" in sql + + +def test_migration_cleans_orphans_before_altering(): + sql = _read("migration_fk_integrity.sql") + # Orphan deletes must precede the ALTER TABLE that validates the FK. + assert "DELETE FROM graph_edges" in sql + assert "DELETE FROM notes" in sql + assert sql.index("DELETE FROM graph_edges") < sql.index("graph_edges_user_id_fkey") + assert sql.index("DELETE FROM notes") < sql.index("notes_user_id_fkey") + + +def test_schema_declares_inline_references(): + sql = _read("supabase_schema.sql") + assert "user_id TEXT NOT NULL REFERENCES users(id)" in sql # graph_edges + assert "user_id TEXT NOT NULL REFERENCES users(id)" in sql # notes + assert "course_id TEXT NOT NULL REFERENCES courses(id)" in sql # notes From 1ba451fd302fb479785950acf9a90e14d5ddd46e Mon Sep 17 00:00:00 2001 From: Jose Cruz Date: Wed, 24 Jun 2026 00:36:22 -0400 Subject: [PATCH 11/14] fix(db): add idx_graph_edges_user_id on FK referencing column (#179) Postgres does not auto-index the referencing side of a foreign key, and sibling tables index this access path. Add the index in both the migration and the canonical schema. --- backend/db/migration_fk_integrity.sql | 4 ++++ backend/db/supabase_schema.sql | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/backend/db/migration_fk_integrity.sql b/backend/db/migration_fk_integrity.sql index de6014cf..176f8623 100644 --- a/backend/db/migration_fk_integrity.sql +++ b/backend/db/migration_fk_integrity.sql @@ -28,6 +28,10 @@ BEGIN END IF; END $$; +-- Index the FK referencing column: Postgres does not auto-index the +-- referencing side of a foreign key, and sibling tables index this path. +CREATE INDEX IF NOT EXISTS idx_graph_edges_user_id ON graph_edges(user_id); + -- #180 notes.user_id / notes.course_id: notes is core user data but both -- columns are bare TEXT. Remove rows pointing at a non-existent user or course -- (e.g. notes left dangling after a course delete) before adding the FKs. diff --git a/backend/db/supabase_schema.sql b/backend/db/supabase_schema.sql index 215b6094..4ee217d4 100644 --- a/backend/db/supabase_schema.sql +++ b/backend/db/supabase_schema.sql @@ -102,6 +102,10 @@ CREATE TABLE IF NOT EXISTS graph_edges ( relationship_type TEXT DEFAULT 'related' ); +-- Index the FK referencing column: Postgres does not auto-index the +-- referencing side of a foreign key, and sibling tables index this path. +CREATE INDEX IF NOT EXISTS idx_graph_edges_user_id ON graph_edges(user_id); + -- Learning sessions CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY, From cc1ec58c7cadfe65b3665ed1e139b43fa7598489 Mon Sep 17 00:00:00 2001 From: Jose Cruz Date: Wed, 24 Jun 2026 00:37:04 -0400 Subject: [PATCH 12/14] docs(db): document actual ON DELETE NO ACTION/RESTRICT semantics of the new FKs (#179, #180) The FKs have no ON DELETE clause, so they default to RESTRICT (block hard delete) rather than cascading cleanup. Correct the schema comment that implied cleanup-on-delete, and document the real semantics at the graph_edges/notes FK sites and in the migration header. --- backend/db/migration_fk_integrity.sql | 8 ++++++++ backend/db/supabase_schema.sql | 13 +++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/backend/db/migration_fk_integrity.sql b/backend/db/migration_fk_integrity.sql index 176f8623..51e53c45 100644 --- a/backend/db/migration_fk_integrity.sql +++ b/backend/db/migration_fk_integrity.sql @@ -11,6 +11,14 @@ -- already uses in migration_gradebook.sql, because Postgres has no -- ADD CONSTRAINT IF NOT EXISTS. Pre-existing orphan rows are deleted first so -- the ALTER TABLE can validate. +-- +-- ON DELETE semantics: these FKs have no ON DELETE clause, so they default to +-- NO ACTION (RESTRICT). A referenced users/courses row cannot be hard-deleted +-- while a graph_edges/notes row still points at it. This guarantees no orphans +-- but does NOT cascade-delete dependents. Today nothing hard-deletes +-- users/courses (delete_account is a soft delete; delete_course only removes +-- the user_courses enrollment row), so RESTRICT never actually fires. Switch +-- to ON DELETE CASCADE (and add a hard-delete path) if cleanup is ever wanted. -- #179 graph_edges.user_id: remove edges whose user_id has no users row, then -- add the FK other learning tables already enforce. diff --git a/backend/db/supabase_schema.sql b/backend/db/supabase_schema.sql index 4ee217d4..dbf2ccd4 100644 --- a/backend/db/supabase_schema.sql +++ b/backend/db/supabase_schema.sql @@ -92,6 +92,9 @@ CREATE TABLE IF NOT EXISTS graph_nodes ( CREATE INDEX IF NOT EXISTS idx_graph_nodes_user_course ON graph_nodes(user_id, course_id); -- Knowledge graph edges +-- graph_edges.user_id carries a hard FK (#179) with no ON DELETE clause, so it +-- defaults to NO ACTION (RESTRICT): a users row cannot be hard-deleted while +-- edges still reference it. This prevents orphaned edges; it does not cascade. CREATE TABLE IF NOT EXISTS graph_edges ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id), -- #179 @@ -491,8 +494,14 @@ CREATE TABLE IF NOT EXISTS user_cosmetics ( -- filters work for tag-based search. last_summary is the cached output of -- the most recent /summarize action; null until the user runs it. -- --- notes.user_id / notes.course_id DO carry hard FKs (#180) — they are core --- user data and must not dangle past a user/course delete. +-- notes.user_id / notes.course_id carry hard FKs (#180) — they are core user +-- data. The FKs have no ON DELETE clause, so they default to NO ACTION +-- (RESTRICT): a user or course row cannot be hard-deleted while notes still +-- reference it. This guarantees notes never become orphaned, but it does NOT +-- cascade-delete them. Today nothing hard-deletes users/courses +-- (delete_account is a soft delete; delete_course only removes the +-- user_courses enrollment row), so RESTRICT never fires. If cleanup-on-delete +-- is ever wanted, switch these to ON DELETE CASCADE and add a hard-delete path. -- -- note_concepts is a junction table linking notes <-> graph_nodes. -- ON DELETE CASCADE on note_id ensures deleting a note cleans up its From 1a20cd832643319e0afceda83388f700a3ae271d Mon Sep 17 00:00:00 2001 From: Jose Cruz Date: Wed, 24 Jun 2026 10:40:54 -0400 Subject: [PATCH 13/14] fix(db): move FK-integrity DDL into numbered migration 0020 (#179, #180) main restructured db/ into ordered migrations applied by migrate.py, deleting the flat migration_*.sql files. Move the FK-integrity DDL into migrations/0020_fk_integrity.sql so existing databases get the graph_edges/notes FK constraints. The DDL is already idempotent (pg_constraint-guarded ADD CONSTRAINT, orphan DELETEs first, CREATE INDEX IF NOT EXISTS), so it is also a no-op on fresh DBs that already have the inline FKs from 0001_baseline_schema. --- .../0020_fk_integrity.sql} | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) rename backend/db/{migration_fk_integrity.sql => migrations/0020_fk_integrity.sql} (73%) diff --git a/backend/db/migration_fk_integrity.sql b/backend/db/migrations/0020_fk_integrity.sql similarity index 73% rename from backend/db/migration_fk_integrity.sql rename to backend/db/migrations/0020_fk_integrity.sql index 51e53c45..ad4f5b5b 100644 --- a/backend/db/migration_fk_integrity.sql +++ b/backend/db/migrations/0020_fk_integrity.sql @@ -1,16 +1,19 @@ --- Migration: foreign-key integrity for graph_edges + notes --- Run once in the Supabase SQL editor (idempotent — safe to re-run). +-- Migration: foreign-key integrity for graph_edges + notes (#179, #180) -- --- Closes the orphan-row gaps where a user_id/course_id is a bare TEXT column --- with no REFERENCES, inconsistent with every sibling table: +-- Backfills, on already-migrated databases, the FK constraints that fresh +-- databases now get inline from 0001_baseline_schema.sql. graph_edges.user_id +-- and notes.user_id / notes.course_id historically shipped as bare TEXT columns +-- with no REFERENCES, inconsistent with every sibling learning table: -- #179 graph_edges.user_id -> users(id) -- #180 notes.user_id -> users(id) -- #180 notes.course_id -> courses(id) -- --- Each constraint is added behind the same pg_constraint guard the codebase --- already uses in migration_gradebook.sql, because Postgres has no --- ADD CONSTRAINT IF NOT EXISTS. Pre-existing orphan rows are deleted first so --- the ALTER TABLE can validate. +-- migrate.py wraps each migration in a single transaction, so this is plain +-- (non-CONCURRENT) DDL. Each constraint is added behind a pg_constraint guard +-- because Postgres has no ADD CONSTRAINT IF NOT EXISTS, which also makes this +-- migration a no-op on fresh databases that already have the inline FKs from +-- the baseline. Pre-existing orphan rows are deleted first so the ALTER TABLE +-- can validate. -- -- ON DELETE semantics: these FKs have no ON DELETE clause, so they default to -- NO ACTION (RESTRICT). A referenced users/courses row cannot be hard-deleted From 4f8655e584e8f069c86c91f1a40857567b2b7c8d Mon Sep 17 00:00:00 2001 From: Jose Cruz Date: Wed, 24 Jun 2026 10:41:01 -0400 Subject: [PATCH 14/14] test(db): point FK-integrity drift guard at the canonical migration files supabase_schema.sql was deleted when main restructured db/. Assert the FK DDL invariants against migrations/0020_fk_integrity.sql and the inline REFERENCES against migrations/0001_baseline_schema.sql instead, preserving the original verification intent (guarded ADD CONSTRAINT, orphan cleanup ordering, inline FKs on fresh DBs). Also assert the idx_graph_edges_user_id index this PR adds. --- backend/tests/test_fk_integrity_migration.py | 35 +++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/backend/tests/test_fk_integrity_migration.py b/backend/tests/test_fk_integrity_migration.py index 5337e963..e37b7e88 100644 --- a/backend/tests/test_fk_integrity_migration.py +++ b/backend/tests/test_fk_integrity_migration.py @@ -2,13 +2,23 @@ Drift guard for the FK-integrity migration (#179, #180). No live Postgres in the unit suite, so we assert the textual invariants that -matter: the forward migration adds each constraint behind the pg_constraint -guard (re-runnable) and cleans orphans first, and the canonical schema declares -the same REFERENCES inline so a fresh database is born with the FKs. +matter: the numbered migration adds each constraint behind the pg_constraint +guard (re-runnable) and cleans orphans first, and the canonical baseline schema +declares the same REFERENCES inline so a fresh database is born with the FKs. + +After main restructured db/ into ordered migrations applied by migrate.py, the +FK DDL lives in migrations/0020_fk_integrity.sql (for already-migrated DBs) and +the inline REFERENCES live in migrations/0001_baseline_schema.sql (for fresh +DBs). The old flat db/supabase_schema.sql / migration_*.sql files were deleted. """ import os -_DB = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "db") +_MIGRATIONS = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "db", "migrations" +) + +FK_MIGRATION = "0020_fk_integrity.sql" +BASELINE = "0001_baseline_schema.sql" CONSTRAINTS = ( "graph_edges_user_id_fkey", @@ -18,12 +28,12 @@ def _read(name: str) -> str: - with open(os.path.join(_DB, name), encoding="utf-8") as fh: + with open(os.path.join(_MIGRATIONS, name), encoding="utf-8") as fh: return fh.read() def test_migration_adds_each_constraint_behind_a_guard(): - sql = _read("migration_fk_integrity.sql") + sql = _read(FK_MIGRATION) for name in CONSTRAINTS: assert name in sql, f"{name} missing from migration" # Every ADD CONSTRAINT must be inside an IF NOT EXISTS pg_constraint guard. @@ -32,7 +42,7 @@ def test_migration_adds_each_constraint_behind_a_guard(): def test_migration_cleans_orphans_before_altering(): - sql = _read("migration_fk_integrity.sql") + sql = _read(FK_MIGRATION) # Orphan deletes must precede the ALTER TABLE that validates the FK. assert "DELETE FROM graph_edges" in sql assert "DELETE FROM notes" in sql @@ -40,8 +50,15 @@ def test_migration_cleans_orphans_before_altering(): assert sql.index("DELETE FROM notes") < sql.index("notes_user_id_fkey") -def test_schema_declares_inline_references(): - sql = _read("supabase_schema.sql") +def test_migration_indexes_the_referencing_column(): + # graph_edges.user_id needs an index on the FK referencing side (#179). + sql = _read(FK_MIGRATION) + assert "CREATE INDEX IF NOT EXISTS idx_graph_edges_user_id" in sql + + +def test_baseline_declares_inline_references(): + # Fresh databases must be born with the FKs, declared inline in the baseline. + sql = _read(BASELINE) assert "user_id TEXT NOT NULL REFERENCES users(id)" in sql # graph_edges assert "user_id TEXT NOT NULL REFERENCES users(id)" in sql # notes assert "course_id TEXT NOT NULL REFERENCES courses(id)" in sql # notes