Skip to content

Commit f547b22

Browse files
disnetclaude
andcommitted
fix: collision-safe url_normalized backfill in saved export
The keyset index from migration 0058 changes the implicit row order of backfillUrlNormalized's unordered SELECT, so a different one of two same-URL duplicate saves gets keyed. exportNativeSaves then processed the still-null duplicate first and its UPDATE collided with the already-keyed row on the unique (user_did, url_normalized) index, failing backing-enable.spec.ts. - Make the export's url_normalized UPDATE collision-safe (NOT EXISTS guard), matching the documented intent of backfillUrlNormalized; the loop's alreadyBacked guard still collapses the duplicate to a single member. - Order backfillUrlNormalized's SELECT by id so which duplicate gets keyed is deterministic instead of depending on the query planner's index choice. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b2f2489 commit f547b22

1 file changed

Lines changed: 13 additions & 4 deletions

File tree

backend/src/services/backing/enable.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,9 @@ export async function disableBacking(env: Env, session: Session): Promise<void>
137137
*/
138138
export async function backfillUrlNormalized(env: Env, did: string): Promise<number> {
139139
const rows = await env.DB.prepare(
140-
`SELECT id, url, url_normalized FROM saved_articles WHERE user_did = ? AND url != ''`
140+
// ORDER BY id so which of two same-URL duplicates gets keyed is deterministic
141+
// (the lowest id wins) rather than depending on the query planner's index choice.
142+
`SELECT id, url, url_normalized FROM saved_articles WHERE user_did = ? AND url != '' ORDER BY id`
141143
)
142144
.bind(did)
143145
.all<{ id: number; url: string; url_normalized: string | null }>();
@@ -267,10 +269,17 @@ export async function exportNativeSaves(
267269
canonicalAtUri,
268270
});
269271
await env.DB.batch([
270-
// Backfill the join key on the enrichment row (no-op if already set).
272+
// Backfill the join key on the enrichment row. No-op if already set, and
273+
// collision-safe: if a DIFFERENT same-URL row already holds this key (a
274+
// pre-existing native duplicate), leave this row untouched rather than
275+
// tripping the unique (user_did, url_normalized) index. The loop's
276+
// alreadyBacked guard still collapses the duplicate to a single member.
271277
env.DB.prepare(
272-
`UPDATE saved_articles SET url_normalized = ? WHERE id = ? AND url_normalized IS NULL`
273-
).bind(normalized, row.id),
278+
`UPDATE saved_articles SET url_normalized = ? WHERE id = ? AND url_normalized IS NULL
279+
AND NOT EXISTS (
280+
SELECT 1 FROM saved_articles WHERE user_did = ? AND url_normalized = ?
281+
)`
282+
).bind(normalized, row.id, session.did, normalized),
274283
env.DB.prepare(
275284
`INSERT INTO backed_collection_members
276285
(user_did, external_collection, url_normalized, url, external_provider, external_item_uri, external_link_uri, metadata)

0 commit comments

Comments
 (0)