-
Notifications
You must be signed in to change notification settings - Fork 2
fix: atomic NOT EXISTS click guard in SlugRepository.remove #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
63d2ab3
568f9e0
8f0aaf0
1e48c8e
f98e5b1
14f5f03
e7686db
4039bbf
94643de
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -114,22 +114,31 @@ export class SlugRepository { | |
| // Lifetime guard: never drop a slug that has recorded any click, so | ||
| // analytics rows are not orphaned. Filter options would mask historical | ||
| // bot traffic and let real history be deleted. | ||
| const row = await db.prepare(`SELECT ${slugSelect()} FROM slugs s WHERE slug = ?`).bind(slug).first<Slug>(); | ||
| // findByValue is used (rather than a bare db.prepare) so the pre-read | ||
| // is a named static method that tests can spy on. | ||
| const row = await SlugRepository.findByValue(db, slug); | ||
| if (!row) return false; | ||
|
|
||
| if (!row.is_custom) return false; | ||
|
|
||
| if (row.click_count > 0) return false; | ||
|
|
||
| // Primary handover and delete run in one transactional batch. | ||
| // NOT EXISTS re-checks atomically that no click arrived since the pre-read: | ||
| // FK cascade is not active (no PRAGMA foreign_keys), so a click arriving | ||
| // in the window would otherwise orphan its clicks row. | ||
| const statements = []; | ||
|
DennisAlund marked this conversation as resolved.
Outdated
|
||
| 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), | ||
| ); | ||
|
Comment on lines
150
to
159
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in e7686db. The handover UPDATE now carries AND EXISTS (SELECT 1 FROM slugs WHERE slug = ? AND is_primary = 1) on the slug being removed, so it only promotes the random slug when this slug still holds primary inside the transaction. A concurrent setPrimary that moved primary to another custom slug now leaves the handover a no-op, and the delete still fires. Added a regression test that drives a stale pre-read (is_primary = 1) against a real setPrimary to a second custom slug; it fails on the old code with two primaries and passes now. Verified both guards on the UPDATE (click NOT EXISTS and still-primary EXISTS) are independently load-bearing. |
||
| } | ||
| 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; | ||
|
DennisAlund marked this conversation as resolved.
Outdated
DennisAlund marked this conversation as resolved.
|
||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.