fix: replace \d with [0-9] in all PostgreSQL regex patterns
PostgreSQL POSIX regex on the server does not support \d shorthand, causing SUBSTRING to return NULL and every entry to get sequence 1. Replaced all \d with [0-9] in: - allocateSequenceNumber (new + old format queries) - recalculateSequence (new + old format queries) - debug-sequences endpoint (GET + POST queries) Also added samples field to debug GET for raw number diagnostics, and POST now handles old-format entries (BTG→B mapping) with ON CONFLICT GREATEST for proper counter merging. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Debug endpoint for registry sequence counters.
|
||||
*
|
||||
* GET — Show all sequence counters + actual max from entries
|
||||
* GET — Show all sequence counters + actual max from entries + sample numbers
|
||||
* POST — Reset all counters to match actual entries (fixes stale counters)
|
||||
*
|
||||
* Auth: NextAuth session only.
|
||||
@@ -22,18 +22,32 @@ export async function GET() {
|
||||
Array<{ company: string; year: number; type: string; lastSeq: number }>
|
||||
>`SELECT company, year, type, "lastSeq" FROM "RegistrySequence" ORDER BY company, year, type`;
|
||||
|
||||
// Get actual max sequences from entries (current format: B-2026-00001)
|
||||
// Sample: show actual number values from entries (for debugging regex issues)
|
||||
const samples = await prisma.$queryRawUnsafe<
|
||||
Array<{ key: string; num: string }>
|
||||
>(`
|
||||
SELECT key,
|
||||
SUBSTRING(value::text FROM '"number":"([^"]+)"') AS num
|
||||
FROM "KeyValueStore"
|
||||
WHERE namespace = 'registratura'
|
||||
AND key LIKE 'entry:%'
|
||||
ORDER BY key
|
||||
LIMIT 20
|
||||
`);
|
||||
|
||||
// Get actual max sequences from entries — current format: B-2026-00001
|
||||
// Use [0-9] instead of \d for PostgreSQL POSIX regex compatibility
|
||||
const actuals = await prisma.$queryRawUnsafe<
|
||||
Array<{ prefix: string; maxSeq: number; count: number }>
|
||||
>(`
|
||||
SELECT
|
||||
SUBSTRING(value::text FROM '"number":"([A-Z]-\\d{4})-') AS prefix,
|
||||
MAX(CAST(SUBSTRING(value::text FROM '"number":"[A-Z]-\\d{4}-(\\d{5})"') AS INTEGER)) AS "maxSeq",
|
||||
SUBSTRING(value::text FROM '"number":"([A-Z]-[0-9]{4})-') AS prefix,
|
||||
MAX(CAST(SUBSTRING(value::text FROM '"number":"[A-Z]-[0-9]{4}-([0-9]{5})"') AS INTEGER)) AS "maxSeq",
|
||||
COUNT(*)::int AS count
|
||||
FROM "KeyValueStore"
|
||||
WHERE namespace = 'registratura'
|
||||
AND key LIKE 'entry:%'
|
||||
AND value::text ~ '"number":"[A-Z]-\\d{4}-\\d{5}"'
|
||||
AND value::text ~ '"number":"[A-Z]-[0-9]{4}-[0-9]{5}"'
|
||||
GROUP BY prefix
|
||||
ORDER BY prefix
|
||||
`);
|
||||
@@ -43,19 +57,20 @@ export async function GET() {
|
||||
Array<{ prefix: string; maxSeq: number; count: number }>
|
||||
>(`
|
||||
SELECT
|
||||
SUBSTRING(value::text FROM '"number":"([A-Z]{3}-\\d{4})-') AS prefix,
|
||||
MAX(CAST(SUBSTRING(value::text FROM '"number":"[A-Z]{3}-\\d{4}-(?:IN|OUT|INT)-(\\d{5})"') AS INTEGER)) AS "maxSeq",
|
||||
SUBSTRING(value::text FROM '"number":"([A-Z]{3}-[0-9]{4})-') AS prefix,
|
||||
MAX(CAST(SUBSTRING(value::text FROM '"number":"[A-Z]{3}-[0-9]{4}-(?:IN|OUT|INT)-([0-9]{5})"') AS INTEGER)) AS "maxSeq",
|
||||
COUNT(*)::int AS count
|
||||
FROM "KeyValueStore"
|
||||
WHERE namespace = 'registratura'
|
||||
AND key LIKE 'entry:%'
|
||||
AND value::text ~ '"number":"[A-Z]{3}-\\d{4}-(IN|OUT|INT)-\\d{5}"'
|
||||
AND value::text ~ '"number":"[A-Z]{3}-[0-9]{4}-(IN|OUT|INT)-[0-9]{5}"'
|
||||
GROUP BY prefix
|
||||
ORDER BY prefix
|
||||
`);
|
||||
|
||||
return NextResponse.json({
|
||||
counters,
|
||||
samples,
|
||||
currentFormatEntries: actuals,
|
||||
oldFormatEntries: oldFormatActuals,
|
||||
note: "POST to this endpoint to reset all counters to match actual entries",
|
||||
@@ -68,31 +83,59 @@ export async function POST() {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Delete ALL old counters (including old-format BTG/SDT/USW/GRP and IN/OUT types)
|
||||
// Delete ALL old counters
|
||||
const deleted = await prisma.$executeRaw`DELETE FROM "RegistrySequence"`;
|
||||
|
||||
// Re-create counters from actual entries in current format (B-2026-00001)
|
||||
// Re-create counters from current format entries (B-2026-00001)
|
||||
const insertedNew = await prisma.$executeRawUnsafe(`
|
||||
INSERT INTO "RegistrySequence" (id, company, year, type, "lastSeq", "createdAt", "updatedAt")
|
||||
SELECT
|
||||
gen_random_uuid()::text,
|
||||
SUBSTRING(value::text FROM '"number":"([A-Z])-') AS company,
|
||||
CAST(SUBSTRING(value::text FROM '"number":"[A-Z]-(\\d{4})-') AS INTEGER) AS year,
|
||||
CAST(SUBSTRING(value::text FROM '"number":"[A-Z]-([0-9]{4})-') AS INTEGER) AS year,
|
||||
'SEQ' AS type,
|
||||
MAX(CAST(SUBSTRING(value::text FROM '"number":"[A-Z]-\\d{4}-(\\d{5})"') AS INTEGER)) AS "lastSeq",
|
||||
MAX(CAST(SUBSTRING(value::text FROM '"number":"[A-Z]-[0-9]{4}-([0-9]{5})"') AS INTEGER)) AS "lastSeq",
|
||||
NOW(),
|
||||
NOW()
|
||||
FROM "KeyValueStore"
|
||||
WHERE namespace = 'registratura'
|
||||
AND key LIKE 'entry:%'
|
||||
AND value::text ~ '"number":"[A-Z]-\\d{4}-\\d{5}"'
|
||||
AND value::text ~ '"number":"[A-Z]-[0-9]{4}-[0-9]{5}"'
|
||||
GROUP BY company, year, type
|
||||
`);
|
||||
|
||||
// Also handle old-format entries (BTG→B, USW→U, SDT→S, GRP→G)
|
||||
const insertedOld = await prisma.$executeRawUnsafe(`
|
||||
INSERT INTO "RegistrySequence" (id, company, year, type, "lastSeq", "createdAt", "updatedAt")
|
||||
SELECT
|
||||
gen_random_uuid()::text,
|
||||
CASE SUBSTRING(value::text FROM '"number":"([A-Z]{3})-')
|
||||
WHEN 'BTG' THEN 'B'
|
||||
WHEN 'USW' THEN 'U'
|
||||
WHEN 'SDT' THEN 'S'
|
||||
WHEN 'GRP' THEN 'G'
|
||||
ELSE SUBSTRING(value::text FROM '"number":"([A-Z]{3})-')
|
||||
END AS company,
|
||||
CAST(SUBSTRING(value::text FROM '"number":"[A-Z]{3}-([0-9]{4})-') AS INTEGER) AS year,
|
||||
'SEQ' AS type,
|
||||
MAX(CAST(SUBSTRING(value::text FROM '"number":"[A-Z]{3}-[0-9]{4}-(?:IN|OUT|INT)-([0-9]{5})"') AS INTEGER)) AS "lastSeq",
|
||||
NOW(),
|
||||
NOW()
|
||||
FROM "KeyValueStore"
|
||||
WHERE namespace = 'registratura'
|
||||
AND key LIKE 'entry:%'
|
||||
AND value::text ~ '"number":"[A-Z]{3}-[0-9]{4}-(IN|OUT|INT)-[0-9]{5}"'
|
||||
GROUP BY company, year, type
|
||||
ON CONFLICT (company, year, type)
|
||||
DO UPDATE SET "lastSeq" = GREATEST("RegistrySequence"."lastSeq", EXCLUDED."lastSeq"),
|
||||
"updatedAt" = NOW()
|
||||
`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
deletedCounters: deleted,
|
||||
recreatedCounters: insertedNew,
|
||||
message: "All sequence counters reset. Old-format counters (BTG/SDT/USW/GRP, IN/OUT) removed.",
|
||||
recreatedFromNewFormat: insertedNew,
|
||||
recreatedFromOldFormat: insertedOld,
|
||||
message: "All counters reset from actual entries (both old and new format).",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -257,8 +257,9 @@ export async function allocateSequenceNumber(
|
||||
const oldPrefix = OLD_COMPANY_PREFIX[companyPrefix] ?? "";
|
||||
|
||||
// New format: B-2026-00001
|
||||
// NOTE: Use [0-9] instead of \d — PostgreSQL POSIX regex may not support \d
|
||||
const newLike = `%"number":"${companyPrefix}-${yr}-%"%`;
|
||||
const newRegex = `${companyPrefix}-${yr}-(\\d{5})`;
|
||||
const newRegex = `${companyPrefix}-${yr}-([0-9]{5})`;
|
||||
const newMaxRows = await tx.$queryRaw<Array<{ maxSeq: number | null }>>`
|
||||
SELECT MAX(
|
||||
CAST(SUBSTRING(value::text FROM ${newRegex}) AS INTEGER)
|
||||
@@ -274,7 +275,7 @@ export async function allocateSequenceNumber(
|
||||
let oldMax = 0;
|
||||
if (oldPrefix) {
|
||||
const oldLike = `%"number":"${oldPrefix}-${yr}-%"%`;
|
||||
const oldRegex = `${oldPrefix}-${yr}-(?:IN|OUT|INT)-(\\d{5})`;
|
||||
const oldRegex = `${oldPrefix}-${yr}-(?:IN|OUT|INT)-([0-9]{5})`;
|
||||
const oldMaxRows = await tx.$queryRaw<Array<{ maxSeq: number | null }>>`
|
||||
SELECT MAX(
|
||||
CAST(SUBSTRING(value::text FROM ${oldRegex}) AS INTEGER)
|
||||
@@ -333,8 +334,9 @@ export async function recalculateSequence(
|
||||
const seqType = "SEQ";
|
||||
|
||||
// Find max from new format (B-2026-00001)
|
||||
// NOTE: Use [0-9] instead of \d — PostgreSQL POSIX regex may not support \d
|
||||
const newLike = `%"number":"${companyPrefix}-${yr}-%"%`;
|
||||
const newRegex = `${companyPrefix}-${yr}-(\\d{5})`;
|
||||
const newRegex = `${companyPrefix}-${yr}-([0-9]{5})`;
|
||||
const newRows = await prisma.$queryRaw<Array<{ maxSeq: number | null }>>`
|
||||
SELECT MAX(
|
||||
CAST(SUBSTRING(value::text FROM ${newRegex}) AS INTEGER)
|
||||
@@ -349,7 +351,7 @@ export async function recalculateSequence(
|
||||
// Also check old format (BTG-2026-OUT-00001)
|
||||
if (oldPrefix) {
|
||||
const oldLike = `%"number":"${oldPrefix}-${yr}-%"%`;
|
||||
const oldRegex = `${oldPrefix}-${yr}-(?:IN|OUT|INT)-(\\d{5})`;
|
||||
const oldRegex = `${oldPrefix}-${yr}-(?:IN|OUT|INT)-([0-9]{5})`;
|
||||
const oldRows = await prisma.$queryRaw<Array<{ maxSeq: number | null }>>`
|
||||
SELECT MAX(
|
||||
CAST(SUBSTRING(value::text FROM ${oldRegex}) AS INTEGER)
|
||||
|
||||
Reference in New Issue
Block a user