From cbad7a669aeee51e443c49d8b6041b040d29e654 Mon Sep 17 00:00:00 2001 From: Mohamed MAACHE Date: Fri, 3 Jul 2026 20:16:05 +0200 Subject: [PATCH] fix(pg): deprecate invalid Date query params instead of serializing NaN string new Date(undefined) (and any Date whose .getTime() is NaN) was serialized by prepareValue() as the literal string "0NaN-NaN-NaNTNaN:NaN:NaN.NaN+NaN:NaN", which Postgres cannot meaningfully parse. Per the maintainer-approved direction in #3318 (charmander/brianc, 2026-04-16), this follows the staged deprecation used elsewhere in this codebase (see client.js's nodeUtils.deprecate() notices targeting pg@9.0): invalid dates now serialize to NULL and emit a DeprecationWarning in pg@8, and will throw an error in pg@9. Also fixes a latent bug this surfaced in arrayString(): it called escapeElement(prepareValue(item)) without checking whether prepareValue's *result* was null, which crashed once prepareValue could return null for a non-null input (an invalid Date). It now treats a null prepared value the same as a null item. Fixes #3318 Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/pg/lib/utils.js | 25 ++++++++++++++++++- packages/pg/test/unit/utils-tests.js | 36 ++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/packages/pg/lib/utils.js b/packages/pg/lib/utils.js index 638b43970..b0e23a355 100644 --- a/packages/pg/lib/utils.js +++ b/packages/pg/lib/utils.js @@ -3,6 +3,17 @@ const defaults = require('./defaults') const { isDate } = require('util/types') +const nodeUtils = require('util') + +// see https://github.com/brianc/node-postgres/issues/3318 +// invalid Date objects (e.g. new Date(undefined), whose .getTime() is NaN) used to be +// serialized as the meaningless string '0NaN-NaN-NaNTNaN:NaN:NaN.NaN+NaN:NaN', which +// Postgres cannot parse. This is deprecated in pg@8 (serialized as NULL, with a warning) +// and will throw an error in pg@9. +const invalidDateDeprecationNotice = nodeUtils.deprecate( + () => {}, + 'Passing an invalid Date (i.e. one whose .getTime() is NaN) as a query parameter is deprecated and will throw an error in pg@9.0. It currently serializes to NULL. See https://github.com/brianc/node-postgres/issues/3318' +) function escapeElement(elementRepresentation) { const escaped = elementRepresentation.replace(/\\/g, '\\\\').replace(/"/g, '\\"') @@ -30,7 +41,15 @@ function arrayString(val) { } result += '\\\\x' + item.toString('hex') } else { - result += escapeElement(prepareValue(item)) + // prepareValue can itself return null (e.g. an invalid Date, see #3318), + // so re-check for null on its result rather than assuming item's nullness + // implies the prepared value's nullness. + const preparedItem = prepareValue(item) + if (preparedItem == null) { + result += 'NULL' + } else { + result += escapeElement(preparedItem) + } } } result += '}' @@ -54,6 +73,10 @@ const prepareValue = function (val, seen) { return Buffer.from(val.buffer, val.byteOffset, val.byteLength) } if (isDate(val)) { + if (isNaN(val.getTime())) { + invalidDateDeprecationNotice() + return null + } if (defaults.parseInputDatesAsUTC) { return dateToStringUTC(val) } else { diff --git a/packages/pg/test/unit/utils-tests.js b/packages/pg/test/unit/utils-tests.js index 5f75f6c2d..d30cef7d3 100644 --- a/packages/pg/test/unit/utils-tests.js +++ b/packages/pg/test/unit/utils-tests.js @@ -89,6 +89,42 @@ test('prepareValues: 1 BC date prepared properly', function () { helper.resetTimezoneOffset() }) +test('prepareValues: invalid date never serializes to a NaN string and warns of deprecation', async function () { + // GIVEN an invalid Date param (.getTime() is NaN) + const invalidDate = new Date(undefined) + assert.ok(isNaN(invalidDate.getTime()), 'test setup: date must be invalid') + + let sawDeprecationWarning = false + const onWarning = (warning) => { + if (warning.name === 'DeprecationWarning' && /invalid date/i.test(warning.message)) { + sawDeprecationWarning = true + } + } + process.on('warning', onWarning) + + let out + try { + // WHEN it is serialized by the parameter path + out = utils.prepareValue(invalidDate) + // deprecation warnings are emitted asynchronously (process.emitWarning queues a + // nextTick/immediate task) - give the event loop a turn before asserting on it + await new Promise((resolve) => setImmediate(resolve)) + } finally { + process.removeListener('warning', onWarning) + } + + // THEN it never produces the "0NaN-NaN-NaNTNaN:NaN:NaN.NaN+NaN:NaN" garbage string + assert.notStrictEqual(out, '0NaN-NaN-NaNTNaN:NaN:NaN.NaN+NaN:NaN') + // AND it emits the blessed pg@8 deprecation warning (pg@9 will throw instead, see #3318) + assert.ok(sawDeprecationWarning, 'expected a DeprecationWarning to be emitted for an invalid date') +}) + +test('prepareValues: invalid date array element never serializes to a NaN string', function () { + const invalidDate = new Date('not a real date') + const out = utils.prepareValue([invalidDate]) + assert.ok(!/NaN/.test(out), 'expected no NaN components in array-serialized invalid date, got: ' + out) +}) + test('prepareValues: undefined prepared properly', function () { const out = utils.prepareValue(void 0) assert.strictEqual(out, null)