fix(core): follow merged tombstones in the position lookup
A merged child still holds its (parent_id, position) slot under the unique index, so skipping tombstones made every retry collide. Follow the tombstone to the surviving work instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
437ca143e5
commit
50c115b4c7
3 files changed
+22
-4
No files matched your search
@@ -4,6 +4,7 @@ import type { Db } from "@sttracker/db/db";
|
||||
import { newId, nowIso } from "@sttracker/db/ids";
|
||||
import {
|
||||
createWorkIn,
|
||||
finalWorkId,
|
||||
findWorkByExternalIdsIn,
|
||||
normalizePosition,
|
||||
resolveAliasIn,
|
||||
@@ -85,12 +86,13 @@ async function resolve(k: K, userId: string | null, hint: WorkHint, actor: strin
|
||||
.select("id")
|
||||
.where("parent_id", "=", parentId)
|
||||
.where("position", "=", position)
|
||||
.where("merged_into_id", "is", null)
|
||||
.where("owner_user_id", "is", null)
|
||||
.executeTakeFirst();
|
||||
if (child) {
|
||||
await updateWorkFromHintIn(k, child.id, hint, actor);
|
||||
return { workId: child.id, created: false, draft: false };
|
||||
// A merged tombstone still holds its (parent_id, position) slot: follow it to the surviving work.
|
||||
const workId = await finalWorkId(k, child.id);
|
||||
await updateWorkFromHintIn(k, workId, hint, actor);
|
||||
return { workId, created: false, draft: false };
|
||||
}
|
||||
return { workId: await createWorkIn(k, { hint, parentId }, actor), created: true, draft: false };
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Db } from "@sttracker/db/db";
|
||||
import { newId, nowIso } from "@sttracker/db/ids";
|
||||
import { forEachDialect } from "@sttracker/db/testing";
|
||||
import { fuzzyScore, matchWork } from "../src/matching.ts";
|
||||
import { createWork, getWork } from "../src/works.ts";
|
||||
import { adminMergeWorks, createWork, getWork } from "../src/works.ts";
|
||||
|
||||
async function addUser(db: Db): Promise<string> {
|
||||
const id = newId();
|
||||
@@ -145,4 +145,19 @@ forEachDialect("matching", (getDb) => {
|
||||
await db.k.insertInto("work_aliases").values({ id: newId(), user_id: u, from_work_id: shared, to_work_id: priv }).execute();
|
||||
expect(await matchWork(db, u, { title: "Dune", mediaType: "book", externalIds: { isbn13: "1" } }, "p")).toEqual({ workId: priv, created: false, draft: true });
|
||||
});
|
||||
|
||||
test("position lookup follows a merged tombstone instead of colliding with it", async () => {
|
||||
const db = getDb();
|
||||
const u = await addUser(db);
|
||||
await db.k.updateTable("users").set({ role: "admin" }).where("id", "=", u).execute();
|
||||
const e1 = await matchWork(db, u, chain(), "p");
|
||||
const h2 = chain();
|
||||
h2.title = "Episode 2";
|
||||
h2.position = { season: 1, episode: 2 };
|
||||
const e2 = await matchWork(db, u, h2, "p");
|
||||
await adminMergeWorks(db, u, e1.workId, e2.workId);
|
||||
const before = (await db.k.selectFrom("works").select("id").execute()).length;
|
||||
expect(await matchWork(db, u, chain(), "p")).toEqual({ workId: e2.workId, created: false, draft: false });
|
||||
expect(await db.k.selectFrom("works").select("id").execute()).toHaveLength(before);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user