Uh oh!
There was an error while loading. Please reload this page.
fix(graph): UNIQUE constraints + idempotent writes for node/edge dedup (#181, #195) - #249
fix(graph): UNIQUE constraints + idempotent writes for node/edge dedup (#181, #195)#249Jose-Gael-Cruz-Lopez wants to merge 22 commits into
Conversation
Deploying with |
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs | frontend | 260f952 | Jun 22 2026, 04:03 AM |
Warning Review limit reached
More reviews will be available in 40 minutes and 36 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits. 🚦 How do rate limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Deploying with |
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs | frontend-staging | 46fb5ac | Commit Preview URL Branch Preview URL | Jun 24 2026, 02:52 PM |
main restructured DB into ordered migrations/ applied by migrate.py and removed the flat migration_dedup_unique.sql path. Relocate the node/edge dedup + UNIQUE index DDL to migrations/0021_graph_dedup_constraints.sql, preserving dedup-before-index ordering and the BEGIN/COMMIT wrapper. Fresh DBs already get the two indexes inline from 0001_baseline_schema.sql.
The old flat migration_dedup_unique.sql and supabase_schema.sql were removed by main's DB restructure. Point the drift guard at the files that exist: migrations/0021_graph_dedup_constraints.sql (back-fill) and migrations/0001_baseline_schema.sql (fresh-DB inline). Preserve original intent: normalized expression, NULLS NOT DISTINCT, dedup-before-index order, plus transaction-wrap and edge on_conflict-column checks.
Graph-node and graph-edge dedup was best-effort in app code (a select-then-insert race in
apply_graph_update) plus a manualdb/dedup_nodes.pycleanup. Two concurrent updates (two tabs, chat+quiz) both miss the existence check and write duplicates. This makes duplicates impossible at the DB level and turns the writes idempotent.DB —
backend/db/migration_dedup_unique.sql(new)(user_id, lower(concept_name), course_id)— keeping the strongest row and repointing/removing dependent edges,quiz_attempts,quiz_context(same policy asdedup_nodes.py) — thenCREATE UNIQUE INDEX … (user_id, lower(concept_name), course_id) NULLS NOT DISTINCT.lower(concept_name)matches the app's_normalize_concept;NULLS NOT DISTINCT(PG15+) makes course-less duplicates collide too.(user_id, source_node_id, target_node_id)thenCREATE UNIQUE INDEX idx_graph_edges_unique.supabase_schema.sql.App —
backend/services/graph_service.py409(concurrent insert won the race): it refetches and resolves the winning row so the rest of the batch still links. Non-409 errors still propagate.inserttoupsert(on_conflict="user_id,source_node_id,target_node_id").dedup_nodes.pydocstring updated — it's now a one-time backfill, not an ongoing safeguard.Tests
test_graph_dedup_constraints.py— 409-recovery resolves the winner; non-409 propagates; edges go throughupsertwith the righton_conflict.test_dedup_unique_migration.py— drift guard on both indexes + dedup-before-index ordering.Verification
ruff check .clean; gated suite green (+6 new). No live Postgres in CI, so theNULLS NOT DISTINCTbuild is left for a reviewer with DB access.Closes#181, #195.