Skip to content

fix(models): add explicit ondelete rules to all foreign keys#79

Open
tongshu2023 wants to merge 1 commit into
zijinz456:mainfrom
tongshu2023:claude/issue-36-fk-ondelete
Open

fix(models): add explicit ondelete rules to all foreign keys#79
tongshu2023 wants to merge 1 commit into
zijinz456:mainfrom
tongshu2023:claude/issue-36-fk-ondelete

Conversation

@tongshu2023

Copy link
Copy Markdown

Summary

Closes #36.

An audit of apps/api/models/ found 22 ForeignKey definitions without an explicit ondelete. Deleting a user or course left orphaned rows (memories, practice results, usage events, scrape sources, content trees) in the database indefinitely. All foreign keys now carry an explicit rule, with a matching Alembic migration.

Policy

CASCADE — owned child rows die with their parent:

Table.column Parent
courses.user_id user
course_content_tree.course_id / parent_id course / parent node (subtree, matches the ORM cascade="all, delete-orphan")
conversation_memories.user_id user (named in the issue)
generated_assets.user_id user
ingestion_jobs.user_id user
practice_problems.course_id course
practice_results.problem_id / user_id problem / user (named in the issue)
scrape_sources.user_id / course_id user / course
auth_sessions.user_id user
study_plans.user_id / course_id user / course
usage_events.user_id user

SET NULL — soft references that should be preserved:

Table.column Why
conversation_memories.course_id user-level memories survive course deletion
generated_assets.course_id assets preserved (named in the issue)
assignments.source_ingestion_id assignments outlive their source job
practice_problems.content_node_id soft curriculum link
practice_problems.parent_problem_id derived problems survive parent deletion
scrape_sources.last_ingestion_id points only at the latest job
usage_events.course_id preserve analytics/billing history

Migration

20260610_0024_fk_ondelete_rules.py (revises 2870051cd576, single head verified):

  • Postgres: defensively drops the reflected constraint by its real name and recreates it with an explicit fk_<table>_<col>_<reftable> name and the new rule; inspector-guarded per table/column like the house pattern (20260303_0012).
  • SQLite local mode: returns early — env.py skips Alembic for sqlite URLs and the schema comes from Base.metadata.create_all() + PRAGMA foreign_keys=ON, so the model-level changes take effect there directly.
  • downgrade() restores the constraints without ondelete.

Note: the Postgres path follows the inspector-driven house pattern but was validated locally against SQLite only (no PG instance here); a staging alembic upgrade head before merge would be a sensible extra check.

Tests

tests/test_fk_cascade.py (4 new) on real in-memory SQLite with PRAGMA foreign_keys=ON, using Core DELETE to bypass ORM-level cascades and exercise the DB rules themselves:

  • deleting a user cascades to courses, memories, practice results, chat sessions, and usage events
  • deleting a course nulls generated_assets.course_id, conversation_memories.course_id, usage_events.course_id
  • deleting a content-tree parent removes the subtree
  • deleting a problem removes its results

Local results: test_fk_cascade.py + test_loom.py + test_services.py90 passed; full suite (tests/ --ignore=tests/e2e) → 983 passed, 4 failed, 5 skipped — the same 4 failures reproduce on unmodified main (local proxy / missing camoufox / sandbox backend environment issues), zero new failures.

Acceptance criteria

  • All ForeignKey definitions have explicit ondelete rules
  • Alembic migration created and tested
  • Deleting a user cascades to their memories, practice results, chat sessions
  • Existing tests pass

🤖 Generated with Claude Code

…56#36)

Audit of apps/api/models/ found 22 ForeignKey definitions without an
explicit ondelete. All are now explicit, with a matching Alembic
migration (20260610_0024) that drops and recreates the constraints on
Postgres (SQLite local mode gets the schema from create_all + PRAGMA
foreign_keys=ON).

CASCADE (owned child rows die with their parent):
- courses.user_id; course_content_tree.course_id / parent_id (subtree)
- conversation_memories.user_id; generated_assets.user_id
- ingestion_jobs.user_id; practice_problems.course_id
- practice_results.problem_id / user_id
- scrape_sources.user_id / course_id; auth_sessions.user_id
- study_plans.user_id / course_id; usage_events.user_id

SET NULL (soft references that should survive parent deletion):
- generated_assets.course_id (assets persist after course delete)
- conversation_memories.course_id; usage_events.course_id (analytics)
- assignments.source_ingestion_id; scrape_sources.last_ingestion_id
- practice_problems.content_node_id / parent_problem_id

Adds tests/test_fk_cascade.py exercising DB-level cascade and SET NULL
behavior via Core DELETEs on a real SQLite database with FK enforcement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add foreign key constraints and cascade delete rules

1 participant