postgres-js driver: transparent serializer override breaks JS Date params in raw sql`` templates
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 78/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Quiet
- Tech stack
- postgres, typescript
- Domain
- databases
Research direction
Start in drizzle-orm/src/postgres-js/driver.ts, then run the minimal raw sql template reproduction with a Date parameter against postgres-js. Check the parser and serializer overrides for the listed OIDs; done means inbound parsing remains transparent while the Date parameter reaches postgres-js as a valid serialized value without the Bind() TypeError.
Written by the indexing model from the issue text.
Description
Bug
drizzle() for the postgres-js driver overrides postgres-js's outbound timestamptz serializer with a transparent passthrough, breaking every ${jsDate} parameter passed through a raw sql\`template. The override is intended to defeat the inbound parser (so reads return raw text for Drizzle to convert itself), but the same loop also rebinds the outbound serializer for OIDs1184/1082/1083/1114/1182/1185/1115/1231`.
drizzle-orm/src/postgres-js/driver.ts:29-36:
const transparentParser = (val: any) => val;
// Override postgres.js default date parsers: https://github.com/porsager/postgres/discussions/761
for (const type of ['1184', '1082', '1083', '1114', '1182', '1185', '1115', '1231']) {
client.options.parsers[type as any] = transparentParser;
client.options.serializers[type as any] = transparentParser; // ← also rebinds outbound serializer
}
The native postgres-js serializer for OID 1184 is x => (x instanceof Date ? x : new Date(x)).toISOString() (porsager/postgres/src/types.js:31). After Drizzle's override, the passthrough returns the JS Date instance unchanged. Then postgres-js's Bind() flow at connection.js:959-964 calls b.str(date), which invokes Buffer.byteLength(date), which throws:
TypeError [ERR_INVALID_ARG_TYPE]: The "string" argument must be of type string or an instance of Buffer or ArrayBuffer. Received an instance of Date
at Buffer.byteLength (node:buffer:850:11)
at reset.str (.../postgres/cjs/src/bytes.js:22:27)
at .../postgres/cjs/src/connection.js:964:16
at Bind (.../postgres/cjs/src/connection.js:954:16)
This surfaces as DrizzleQueryError: Failed query: … to the caller, with the actual TypeError hidden on .cause.
Minimal repro
const postgres = require('postgres');
const { drizzle } = require('drizzle-orm/postgres-js');
const { sql } = require('drizzle-orm');
const client = postgres({ /* connection */ });
const db = drizzle(client);
await db.execute(sql`
INSERT INTO some_table (ts_col) VALUES (${new Date()})
`);
// → DrizzleQueryError → .cause = TypeError ERR_INVALID_ARG_TYPE
The same INSERT via client.unsafe(sqlString, [new Date()]) (raw postgres-js, no drizzle()) works because the native serializer is intact.
What does work (and why this bug is easy to miss)
db.insert(table).values({ ts_col: new Date() })— the column-aware encoder converts the Date through Drizzle's typed path, not through the raw param flow that hits postgres-js'sBind(). So users of the typed query builder don't hit this.sql\… ${new Date().toISOString()}`` — pre-stringifying defeats the transparent serializer (it passes the string through unchanged, and PG parses the ISO 8601 literal).sql\… NOW()`` — uses PG's server-side timestamp, no parameter.
The bug only surfaces for users mixing the raw sql\`template (often for cross-table UPSERTs orON CONFLICTclauses that Drizzle's typed builder doesn't model cleanly) with JSDate` interpolation.
Suggested fix
The cited porsager/postgres#761 is about Drizzle wanting raw text from PG so it can do its own parsing. The fix is to override only parsers, not serializers:
const transparentParser = (val: any) => val;
for (const type of ['1184', '1082', '1083', '1114', '1182', '1185', '1115', '1231']) {
client.options.parsers[type as any] = transparentParser;
- client.options.serializers[type as any] = transparentParser;
}
client.options.serializers['114'] = transparentParser;
client.options.serializers['3802'] = transparentParser;
Or, if there's a reason to also rebind the serializer (e.g., to defeat postgres-js converting a Date back via the round-trip), provide a serializer that actually serializes:
+ const dateSerializer = (val: any) => (val instanceof Date ? val.toISOString() : val);
for (const type of ['1184', '1082', '1083', '1114', '1182', '1185', '1115', '1231']) {
client.options.parsers[type as any] = transparentParser;
- client.options.serializers[type as any] = transparentParser;
+ client.options.serializers[type as any] = dateSerializer;
}
Either change preserves Drizzle's intent (custom inbound date handling) without breaking outbound writes.
Real-world impact
A WXYC backend job (library-identity-consumer) hit this on its first prod run on 2026-05-20: 14,405 / 14,405 UPSERTs failed because last_verified_at was passed as ${new Date()} through a sql\`raw template containing anON CONFLICT … DO UPDATEclause. Diagnosis cost an afternoon — theDrizzleQueryError.messageonly contains the SQL + params and the actual TypeError is hidden on.cause. The local workaround was ${new Date().toISOString()}`, but the trap is easy to fall into for anyone writing raw SQL with date parameters.
Environment
drizzle-orm0.44.x (latest at time of report)postgres3.4.9- Node 24 (also reproduces on Node 20+)
Related
- porsager/postgres#761 — the discussion cited in the source comment
- Drizzle docs don't currently mention this constraint; raw
sql\`` templates with Date parameters are a documented pattern
- Dominant language
- TypeScript
- Stars
- 35.8k
- Forks
- 1.6k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 4
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from drizzle-team/drizzle-orm
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
drizzle-team/drizzle-orm#6287 ·
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
drizzle-team/drizzle-orm#6229 ·
-
bug drizzle/kit
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
drizzle-team/drizzle-orm#6214 ·
-
bug bug/fixed-in-beta drizzle/kit
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
drizzle-team/drizzle-orm#6192 · 3 comments ·
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
drizzle-team/drizzle-orm#6162 · 3 comments · 1 reaction ·
All issues in drizzle-team/drizzle-orm
Similar issues
-
calcite-components needs triage refactor
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Esri/calcite-design-system#15203 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 91/100
-
community first-timers-only good first issue hacktoberfest help wanted low hanging fruit up-for-grabs
Difficulty 1/5 Under an hour Newbie friendliness 95/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Automattic/studio#4908 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 90/100