FM-041 — Strategy Goals Bucketed Into Wrong Quarter
Neon HTTP fetch bypasses pg type parser. timestamp-without-tz returned Invalid Date → JSON null → fallback to current quarter.
| Field | Value |
|---|---|
| Severity | SEV-2 (Strategy app data displayed wrong + PATCH 500s) |
| Category | Database / Date Handling |
| Date | 2026-05-04 |
| Status | Archived |
Symptom
Section titled “Symptom”Adding a goal to any non-current quarter column (“Q3 2026”, “Q4 2026”) rendered the goal in current quarter (“Q2 2026”). Dragging between lanes returned HTTP 500 with value.toISOString is not a function.
Root cause (read bug)
Section titled “Root cause (read bug)”Neon’s HTTP fetch transport (neonConfig.poolQueryViaFetch = true) bypasses the binary wire protocol — the OID 1114 type parser for timestamp without time zone is never invoked. targetDate came back as Invalid Date, JSON-serialized to null, and the client’s getQuarterFromDate(null) fell back to current quarter.
Root cause (drag bug)
Section titled “Root cause (drag bug)”PATCH /api/goals/:id accepted the body as-is. Clients sent targetDate as an ISO string (because JSON.stringify(Date) produces a string); Drizzle’s PgTimestamp.mapToDriverValue then called value.toISOString() on the string and threw.
- Write side: Strict coerced PATCH body schema using
z.coerce.date()for date fields. Route.safeParses and returns 400 on failure. - Read side: Repository derives
quarterandtargetDateIsoin Postgres viaEXTRACT(QUARTER FROM target_date)::intandto_char(target_date, ...). Client readsgoal.quarterfirst (server source of truth). getQuarterFromDatereturnsnumber | null— silent fallbacks were the bug.
Prevention
Section titled “Prevention”- Derive in SQL, not JS, when reading
timestamp without time zone— Neon HTTP path is a known gap - Every PATCH route accepting dates must use
z.coerce.date()— Drizzle’smode:"date"requires Date objects - Don’t silently fall back to “current” — null should surface as null, not as a default