fix(18): WR-02 derive seed flag from DB write, not a stale pre-flight SELECT

The seed handler computed seeded from a pre-flight SELECT then returned
seeded:!alreadySet. Under a genuine concurrent race both requests can
SELECT the empty table, both enter the insert branch, and both return
seeded:true though only one row was actually written. Replace the
SELECT + conditional onDuplicateKeyUpdate with a single INSERT IGNORE
and derive seeded from affectedRows (1 = inserted, 0 = ignored/existing
row preserved, D-03). On MariaDB onDuplicateKeyUpdate(value=value)
reports affectedRows 1 for both insert and no-op, so it cannot
distinguish them; INSERT IGNORE can. timezone is bound via a
parameterized sql template and is already IANA-validated by zod. Adds a
test asserting seeded:false for a directly-pre-inserted row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lucas Berger
2026-06-15 07:41:52 -04:00
co-authored by Claude Opus 4.8
parent 692fe2ad9a
commit 93217b58fe
2 changed files with 49 additions and 23 deletions
+18 -23
View File
@@ -241,34 +241,29 @@ adminRouter.put('/config/timezone', zValidator('json', timezoneSchema), async (c
// explicit choice.
//
// Always returns 200 with { ok: true, seeded: <bool> }.
// Does NOT use onDuplicateKeyUpdate — an explicit SELECT + conditional INSERT
// ensures the existing value is never overwritten (D-03).
// Uses a single INSERT IGNORE: when the household_timezone row already exists the
// insert is silently ignored (existing value untouched — D-03 no-overwrite) and
// cannot 500 on the PK constraint under a concurrent seed/PUT. `seeded` is derived
// from the result's affectedRows so it reflects what the DB actually did, accurately
// even under a genuine concurrent race (WR-02).
// ---------------------------------------------------------------------------
adminRouter.post('/config/timezone/seed', zValidator('json', timezoneSchema), async (c) => {
const { timezone } = c.req.valid('json');
const [existing] = await db
.select({ value: appConfig.value })
.from(appConfig)
.where(eq(appConfig.key, 'household_timezone'))
.limit(1);
// WR-02: always run the INSERT and let the DB be the source of truth, instead of a
// pre-flight SELECT whose result could be stale under a concurrent race (two racers
// both observing an empty table and both returning seeded:true). INSERT IGNORE on
// MariaDB reports affectedRows === 1 for a real insert and 0 when the row already
// exists (ignored, value preserved — D-03), so deriving `seeded` from affectedRows
// is accurate: only the racer whose INSERT actually wrote the row gets seeded:true.
// `timezone` is interpolated via drizzle's parameterized sql template (bound param,
// not string concatenation) and is already validated as an IANA zone by timezoneSchema.
const result = (await db.execute(
sql`INSERT IGNORE INTO ${appConfig} (${sql.identifier('key')}, ${sql.identifier('value')}) VALUES ('household_timezone', ${timezone})`,
)) as unknown as [{ affectedRows: number }, unknown];
const alreadySet = existing?.value != null;
const seeded = result[0].affectedRows === 1;
// WR-02: Use onDuplicateKeyUpdate with a no-op (`set: { value: sql`value` }`)
// so a concurrent seed or a seed racing a PUT cannot 500 on the PK constraint.
// The no-op preserves the existing value (D-03 no-overwrite). We always INSERT
// here and let the DB determine whether a row was inserted or not; `seeded` still
// reflects the pre-flight SELECT so the caller gets the correct flag even in the
// concurrent race (the winner observes alreadySet=false → seeded:true; the loser
// observes alreadySet=true → seeded:false and the INSERT is a no-op).
if (!alreadySet) {
await db
.insert(appConfig)
.values({ key: 'household_timezone', value: timezone })
.onDuplicateKeyUpdate({ set: { value: sql`value` } });
}
return c.json({ ok: true, seeded: !alreadySet }, 200);
return c.json({ ok: true, seeded }, 200);
});