From 109cf8993134442449cd09968c3401c707402b19 Mon Sep 17 00:00:00 2001 From: Pieter Develtere Date: Sat, 11 Jul 2026 13:12:13 +0200 Subject: [PATCH] Add Temporal support to SqlString.escape Escaping a Temporal value (e.g. Temporal.Instant) currently falls through to objectToValues and produces broken SQL. Teach escape() about Temporal, mirroring the existing Date handling. - Instant / ZonedDateTime: absolute times, delegated to dateToString so the timeZone argument behaves exactly as it does for Date (ms precision). - PlainDateTime / PlainDate / PlainTime: wall-clock values, emitted verbatim as DATETIME / DATE / TIME literals (timeZone ignored). - Other Temporal types fall back to their ISO string. Temporal types are detected via Object.prototype.toString (Symbol.toStringTag), so the Temporal global is never referenced and the `node >= 0.6` engines range is unaffected. Adds tests (skipped when Temporal is unavailable) and README docs. Co-Authored-By: Claude Opus 4.8 --- README.md | 6 +++++ lib/SqlString.js | 22 ++++++++++++++++ test/unit/test-SqlString.js | 52 +++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+) diff --git a/README.md b/README.md index 5a9934f..da4bac0 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,12 @@ Different value types are escaped differently, here is how: * Numbers are left untouched * Booleans are converted to `true` / `false` * Date objects are converted to `'YYYY-mm-dd HH:ii:ss'` strings +* `Temporal.Instant` and `Temporal.ZonedDateTime` are treated as absolute times + and converted to `'YYYY-mm-dd HH:ii:ss.sss'` strings, honoring the `timeZone` + option just like `Date` +* `Temporal.PlainDateTime`, `Temporal.PlainDate` and `Temporal.PlainTime` are + wall-clock values and are converted verbatim to `DATETIME` / `DATE` / `TIME` + literals, ignoring `timeZone` * Buffers are converted to hex strings, e.g. `X'0fa5'` * Strings are safely escaped * Arrays are turned into list, e.g. `['a', 'b']` turns into `'a', 'b'` diff --git a/lib/SqlString.js b/lib/SqlString.js index 8206dad..4ed6ddb 100644 --- a/lib/SqlString.js +++ b/lib/SqlString.js @@ -42,6 +42,8 @@ SqlString.escape = function escape(val, stringifyObjects, timeZone) { case 'object': if (Object.prototype.toString.call(val) === '[object Date]') { return SqlString.dateToString(val, timeZone || 'local'); + } else if (Object.prototype.toString.call(val).indexOf('[object Temporal.') === 0) { + return SqlString.temporalToString(val, timeZone); } else if (Array.isArray(val)) { return SqlString.arrayToList(val, timeZone); } else if (Buffer.isBuffer(val)) { @@ -163,6 +165,26 @@ SqlString.dateToString = function dateToString(date, timeZone) { return escapeString(str); }; +SqlString.temporalToString = function temporalToString(temporal, timeZone) { + switch (Object.prototype.toString.call(temporal)) { + // Absolute points in time. Handled exactly like a Date so the `timeZone` + // argument keeps the same meaning (millisecond precision, matching Date). + case '[object Temporal.Instant]': + case '[object Temporal.ZonedDateTime]': + return SqlString.dateToString(new Date(temporal.epochMilliseconds), timeZone || 'local'); + // Wall-clock values that carry no time zone. Emit them verbatim as the + // matching MySQL DATE / TIME / DATETIME literal, ignoring `timeZone`. + case '[object Temporal.PlainDateTime]': + case '[object Temporal.PlainDate]': + case '[object Temporal.PlainTime]': + return escapeString(temporal.toString().replace('T', ' ')); + // Any other Temporal type (Duration, PlainYearMonth, PlainMonthDay, ...) + // has no dedicated MySQL type; fall back to its ISO string. + default: + return escapeString(temporal.toString()); + } +}; + SqlString.bufferToString = function bufferToString(buffer) { return 'X' + escapeString(buffer.toString('hex')); }; diff --git a/test/unit/test-SqlString.js b/test/unit/test-SqlString.js index 580aa4e..2be2076 100644 --- a/test/unit/test-SqlString.js +++ b/test/unit/test-SqlString.js @@ -1,3 +1,5 @@ +/* global Temporal */ + var assert = require('assert'); var SqlString = require('../../'); var test = require('utest'); @@ -231,6 +233,56 @@ test('SqlString.escape', { assert.strictEqual(string, "'" + expected + "'"); }, + 'Temporal.Instant is escaped like a Date': function() { + if (typeof Temporal === 'undefined') return; + var expected = '2012-05-07 11:42:03.002'; + var instant = Temporal.Instant.from('2012-05-07T11:42:03.002Z'); + var string = SqlString.escape(instant, false, 'Z'); + + assert.strictEqual(string, "'" + expected + "'"); + }, + + 'Temporal.Instant honors the time zone argument': function() { + if (typeof Temporal === 'undefined') return; + var expected = '2012-05-07 13:42:03.002'; + var instant = Temporal.Instant.from('2012-05-07T11:42:03.002Z'); + var string = SqlString.escape(instant, false, '+0200'); + + assert.strictEqual(string, "'" + expected + "'"); + }, + + 'Temporal.ZonedDateTime is escaped as an absolute time': function() { + if (typeof Temporal === 'undefined') return; + var expected = '2012-05-07 11:42:03.002'; + var zoned = Temporal.Instant.from('2012-05-07T11:42:03.002Z').toZonedDateTimeISO('+02:00'); + var string = SqlString.escape(zoned, false, 'Z'); + + assert.strictEqual(string, "'" + expected + "'"); + }, + + 'Temporal.PlainDateTime is escaped ignoring the time zone': function() { + if (typeof Temporal === 'undefined') return; + var expected = '2012-05-07 11:42:03.002'; + var plain = Temporal.PlainDateTime.from('2012-05-07T11:42:03.002'); + var string = SqlString.escape(plain, false, '+0200'); + + assert.strictEqual(string, "'" + expected + "'"); + }, + + 'Temporal.PlainDate is escaped as a DATE literal': function() { + if (typeof Temporal === 'undefined') return; + var string = SqlString.escape(Temporal.PlainDate.from('2012-05-07')); + + assert.strictEqual(string, "'2012-05-07'"); + }, + + 'Temporal.PlainTime is escaped as a TIME literal': function() { + if (typeof Temporal === 'undefined') return; + var string = SqlString.escape(Temporal.PlainTime.from('11:42:03')); + + assert.strictEqual(string, "'11:42:03'"); + }, + 'buffers are converted to hex': function() { var buffer = new Buffer([0, 1, 254, 255]); var string = SqlString.escape(buffer);