fix: atomic NOT EXISTS click guard in SlugRepository.remove#14
Open
DennisAlund wants to merge 1 commit into
Open
fix: atomic NOT EXISTS click guard in SlugRepository.remove#14DennisAlund wants to merge 1 commit into
DennisAlund wants to merge 1 commit into
Conversation
The previous code checked click_count > 0 via a pre-read SELECT and then ran the batch DELETE unconditionally. A click arriving between the pre-read and the DELETE would be orphaned: FK cascade is not active (no PRAGMA foreign_keys in D1), so the clicks row would reference a deleted slug with no cascade cleanup. LinkRepository.delete already guards against this race by embedding AND NOT EXISTS (SELECT 1 FROM clicks ...) inside the transactional batch. SlugRepository.remove was left unguarded by the same 0.35.3 hardening pass. Fix: add the same NOT EXISTS re-check to the DELETE statement and check the result (meta.changes) to detect when the guard blocks the delete. Use SlugRepository.findByValue for the pre-read so the method is spyable in tests. Regression test: mocks findByValue to return click_count=0 while an actual click row exists in the DB; asserts remove() returns false and both the slug and click records survive. https://claude.ai/code/session_01RCqBLUKrf78eW9EdrqD9Mi
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
shrtnr | 63d2ab3 | Jun 12 2026, 07:19 PM |
There was a problem hiding this comment.
Pull request overview
This PR hardens SlugRepository.remove against a race where a click can arrive between the pre-read and the transactional delete, preventing orphaned clicks rows when FK cascade is inactive in D1.
Changes:
- Refactors the pre-read in
SlugRepository.removeto callSlugRepository.findByValue(enabling spying in tests). - Adds an atomic
NOT EXISTS (SELECT 1 FROM clicks ...)guard to the slug DELETE inside the transactional batch and returnsfalsewhen the guard blocks deletion. - Adds a regression test that simulates the race window by spying on
findByValueto reportclick_count: 0while a click row exists at DELETE time.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/db/slug-repository.ts |
Adds an atomic NOT EXISTS guard to prevent deleting slugs that have click history under concurrent load. |
src/__tests__/repository/slug-repository.test.ts |
Adds a regression test for the race window between the pre-read and transactional delete. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
130
to
+142
| const statements = []; | ||
| if (row.is_primary) { | ||
| statements.push( | ||
| db.prepare("UPDATE slugs SET is_primary = 1 WHERE link_id = ? AND is_custom = 0").bind(row.link_id), | ||
| ); | ||
| } | ||
| statements.push(db.prepare("DELETE FROM slugs WHERE slug = ?").bind(slug)); | ||
| await db.batch(statements); | ||
| return true; | ||
| statements.push( | ||
| db.prepare("DELETE FROM slugs WHERE slug = ? AND NOT EXISTS (SELECT 1 FROM clicks WHERE slug = ?)") | ||
| .bind(slug, slug), | ||
| ); | ||
| const results = await db.batch(statements); | ||
| const deleteResult = results[results.length - 1]; | ||
| return (deleteResult.meta.changes ?? 0) > 0; |
Comment on lines
+237
to
+242
| expect(removed).toBe(false); | ||
| const clickRow = await env.DB.prepare("SELECT 1 FROM clicks WHERE slug = 'rmrace-c'").first(); | ||
| const slugRow = await env.DB.prepare("SELECT 1 FROM slugs WHERE slug = 'rmrace-c'").first(); | ||
| expect(clickRow).not.toBeNull(); | ||
| expect(slugRow).not.toBeNull(); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Weekly defect review. One confirmed new bug found; all other candidates were refuted or pre-existing.
What was wrong
SlugRepository.removecheckedclick_count > 0via a pre-read SELECT, then ran the batch DELETE unconditionally. A click arriving between the pre-read and the DELETE would produce an orphanedclicksrow:PRAGMA foreign_keysis not set anywhere in the codebase (confirmed by search), so D1's FK cascade is inactive andclicks.slug ON DELETE CASCADEnever fires.LinkRepository.deletewas hardened against the same race in PR #13 by embeddingAND NOT EXISTS (SELECT 1 FROM clicks ...)inside the transactional batch.SlugRepository.removewas left unguarded by that pass.The method's own comment states: "never drop a slug that has recorded any click, so analytics rows are not orphaned" — the pre-read alone does not enforce this under concurrent load.
The fix
AND NOT EXISTS (SELECT 1 FROM clicks WHERE slug = ?)to the DELETE inside the batch, matching the pattern used inLinkRepository.delete.deleteResult.meta.changesand returnfalsewhen the guard blocks the delete (the service layer already maps this to 409 "Slug has click history").SlugRepository.findByValue(an existing public static method) instead of an inlinedb.prepare, making it spyable for the race-condition test.The regression test
src/__tests__/repository/slug-repository.test.ts— "guard holds when a click lands between the pre-read and the batch delete":findByValueto returnclick_count: 0(simulating the race window where the pre-read predates the click).removeand asserts it returnsfalse.On the old code (no NOT EXISTS), the DELETE would fire and
removewould returntrue, failing the assertion. On the new code the guard blocks the delete.Test suite: 933 tests, all pass.
https://claude.ai/code/session_01RCqBLUKrf78eW9EdrqD9Mi
Generated by Claude Code