fix(housekeeping): lock poll questions by primary key

This commit is contained in:
Simo committed 2026-09-02 18:54:58 +02:00
1 parent 6e16b948e6
commit e9680775f9
2 files changed
+70 -23

No files matched your search

@@ -15,9 +15,15 @@ const sqlMocks = vi.hoisted(() => ({
}));
const database = vi.hoisted(() => {
type SelectedResult =
| Record<string, unknown>
| readonly Record<string, unknown>[];
let selected: Record<string, unknown> = { id: 7, title: "Existing" };
let selectedQueue: Record<string, unknown>[] = [];
let selectedQueue: SelectedResult[] = [];
let selectedTable: string | undefined;
let selectedCondition:
| { operator: string; column: unknown; value: unknown }
| undefined;
let lockedTables: Array<readonly [string | undefined, string]> = [];
let mutationSteps: string[] = [];
const insertedValues = vi.fn(async (_values: Record<string, unknown>) => [
@@ -29,16 +35,33 @@ const database = vi.hoisted(() => {
values,
}));
const update = vi.fn(() => ({ set }));
const nextSelected = () => [selectedQueue.shift() ?? selected];
const nextSelected = () => {
const result = selectedQueue.shift() ?? selected;
return Array.isArray(result) ? [...result] : [result];
};
const limit = vi.fn(async () => nextSelected());
const lock = vi.fn(async (strength: string) => {
lockedTables.push([selectedTable, strength]);
mutationSteps.push(`lock:${selectedTable ?? "unknown"}`);
mutationSteps.push(
`lock:${selectedTable ?? "unknown"}:${selectedCondition?.operator ?? "none"}:${String(selectedCondition?.column ?? "none")}:${String(selectedCondition?.value ?? "none")}`,
);
return nextSelected();
});
const where = vi.fn(() => ({ for: lock, limit }));
const orderBy = vi.fn(async (column: unknown) => {
mutationSteps.push(
`read:${selectedTable ?? "unknown"}:${selectedCondition?.operator ?? "none"}:${String(selectedCondition?.column ?? "none")}:orderBy:${String(column)}`,
);
return nextSelected();
});
const where = vi.fn(
(condition: { operator: string; column: unknown; value: unknown }) => {
selectedCondition = condition;
return { for: lock, limit, orderBy };
},
);
const from = vi.fn((table: unknown) => {
selectedTable = (table as { tableName?: string }).tableName;
selectedCondition = undefined;
return { where };
});
const select = vi.fn(() => ({ from }));
@@ -75,10 +98,11 @@ const database = vi.hoisted(() => {
selected = value;
selectedQueue = [];
selectedTable = undefined;
selectedCondition = undefined;
lockedTables = [];
mutationSteps = [];
},
queueSelected(...values: Record<string, unknown>[]) {
queueSelected(...values: SelectedResult[]) {
selectedQueue = [...values];
},
};
@@ -86,8 +110,16 @@ const database = vi.hoisted(() => {
vi.mock("drizzle-orm", () => ({
count: vi.fn(() => "count"),
eq: vi.fn(),
inArray: vi.fn(),
eq: vi.fn((column: unknown, value: unknown) => ({
operator: "eq",
column,
value,
})),
inArray: vi.fn((column: unknown, value: unknown) => ({
operator: "inArray",
column,
value,
})),
sql: Object.assign(sqlMocks.tagged, {
join: sqlMocks.join,
raw: sqlMocks.raw,
@@ -173,17 +205,22 @@ describe("Content database mutation runtime partial updates", () => {
]);
});
it("deletes poll votes, questions, and the poll in child-first order", async () => {
database.selected({
id: 7,
title: "Existing",
description: null,
status: "draft",
showResults: 1,
multipleChoice: 0,
startsAt: null,
endsAt: null,
});
it("locks poll questions by ascending primary key before child-first deletion", async () => {
database.queueSelected(
{
id: 7,
title: "Existing",
description: null,
status: "draft",
showResults: 1,
multipleChoice: 0,
startsAt: null,
endsAt: null,
},
[{ id: 4 }, { id: 9 }],
{ id: 4 },
{ id: 9 },
);
await executeContentDatabaseMutation(
"poll.change",
@@ -200,10 +237,13 @@ describe("Content database mutation runtime partial updates", () => {
expect(database.lockedTables()).toEqual([
["WebsitePoll", "update"],
["WebsitePollQuestion", "update"],
["WebsitePollQuestion", "update"],
]);
expect(database.mutationSteps()).toEqual([
"lock:WebsitePoll",
"lock:WebsitePollQuestion",
"lock:WebsitePoll:eq:id:7",
"read:WebsitePollQuestion:eq:pollId:orderBy:id",
"lock:WebsitePollQuestion:eq:id:4",
"lock:WebsitePollQuestion:eq:id:9",
"delete:WebsitePollVote",
"delete:WebsitePollQuestion",
"delete:WebsitePoll",
@@ -241,7 +281,7 @@ describe("Content database mutation runtime partial updates", () => {
["WebsitePollQuestion", "update"],
]);
expect(database.mutationSteps()).toEqual([
"lock:WebsitePollQuestion",
"lock:WebsitePollQuestion:eq:id:7",
"delete:WebsitePollVote",
"delete:WebsitePollQuestion",
]);
@@ -635,11 +635,18 @@ async function pollChange(
.for("update");
if (!existing) throw notFound();
if (action === "delete") {
await connection
const questionIds = await connection
.select({ id: WebsitePollQuestion.id })
.from(WebsitePollQuestion)
.where(eq(WebsitePollQuestion.pollId, id))
.for("update");
.orderBy(WebsitePollQuestion.id);
for (const question of questionIds) {
await connection
.select({ id: WebsitePollQuestion.id })
.from(WebsitePollQuestion)
.where(eq(WebsitePollQuestion.id, question.id))
.for("update");
}
await connection
.delete(WebsitePollVote)
.where(