From 7bb252c528d810584bcb46a56cff3d26ebf392ff Mon Sep 17 00:00:00 2001 From: Janpeter Visser <30029041+madhura68@users.noreply.github.com> Date: Thu, 14 May 2026 10:46:47 +0000 Subject: [PATCH] fix(ideas): make nextIdeaCode self-correcting against counter drift (#200) Fixes a P2002 unique constraint crash on (user_id, code) when idea_code_counter on the User is behind the actual codes in the ideas table (e.g. after direct DB inserts during development). After incrementing the counter the function now queries MAX(CAST(SUBSTRING(code FROM 6) AS INTEGER)) via raw SQL and takes max(counter, maxExisting + 1) as the next code. String MAX was not safe above IDEA-999, hence the numeric cast. If the counter lagged it is updated in-place to stay in sync. No schema change, no migration, no changes outside idea-code-server.ts. Co-authored-by: Claude Sonnet 4.6 --- lib/idea-code-server.ts | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/lib/idea-code-server.ts b/lib/idea-code-server.ts index 9f26aed..819d4ed 100644 --- a/lib/idea-code-server.ts +++ b/lib/idea-code-server.ts @@ -7,6 +7,11 @@ // Geen aparte $transaction nodig voor enkelvoudige update — de update is // atomisch op één rij. Voor combineren met een idea.create wordt // nextIdeaCode aangeroepen binnen de bredere $transaction van de caller. +// +// Self-correcting: na de increment wordt de numerieke MAX van bestaande codes +// opgevraagd via raw SQL (geen string-vergelijking — die faalt boven IDEA-999). +// Als de counter achterloopt (bijv. na directe DB-inserts tijdens development) +// wordt nextN = MAX+1 gebruikt en de counter direct bijgewerkt. import { prisma } from '@/lib/prisma' import { formatIdeaCode } from '@/lib/idea-code' @@ -17,10 +22,34 @@ export async function nextIdeaCode( userId: string, client: Prisma.TransactionClient | typeof prisma = prisma, ): Promise { + // Increment counter — acquires Postgres row lock, serializes concurrent calls. const u = await client.user.update({ where: { id: userId }, data: { idea_code_counter: { increment: 1 } }, select: { idea_code_counter: true }, }) - return formatIdeaCode(u.idea_code_counter) + + // Numeric MAX guards against counter drift (e.g. ideas inserted directly in + // DB without updating the counter). String MAX mis-sorts "IDEA-1000" < + // "IDEA-999", so we cast to INTEGER in SQL. + const rows = await (client as Prisma.TransactionClient).$queryRaw< + [{ max_n: number | bigint | null }] + >` + SELECT MAX(CAST(SUBSTRING(code FROM 6) AS INTEGER)) AS max_n + FROM ideas + WHERE user_id = ${userId} + ` + const maxExisting = rows[0].max_n !== null ? Number(rows[0].max_n) : 0 + const nextN = Math.max(u.idea_code_counter, maxExisting + 1) + + // Re-sync counter forward if it was behind the actual max. + if (nextN !== u.idea_code_counter) { + await client.user.update({ + where: { id: userId }, + data: { idea_code_counter: nextN }, + select: { id: true }, + }) + } + + return formatIdeaCode(nextN) }