Skip to content

Commit d0f0b37

Browse files
Fix Array(Date) query param binding: serialize container Dates as quoted date strings (#947)
## Description Fixes #946. `formatQueryParams` serialized a JS `Date` as a bare Unix-seconds integer (e.g. `1683244800`) in **every** context. Inside a container that becomes `[1683244800]`, which ClickHouse accepts for `Array(DateTime)` but **rejects** for `Array(Date)`/`Array(Date32)`: ``` Code: 27. DB::Exception: Cannot parse input: expected '\'' before: '1683244800]': value [1683244800] cannot be parsed as Array(Date) for query parameter 'l'. (CANNOT_PARSE_INPUT_ASSERTION_FAILED) ``` The `Date` branch now mirrors the existing `boolean` handling and uses the `isInArrayOrTuple` flag: inside arrays, tuples, and maps a `Date` is emitted as a quoted UTC `'YYYY-MM-DD'` string. Verified against ClickHouse 26.5 that this is the **single** representation the element parser accepts for **all** temporal element types — `Array(Date)`, `Array(Date32)`, `Array(DateTime)`, and `Array(DateTime64)` (bare int, quoted int, and full datetime strings are all rejected by `Array(Date)`). Scalar `Date`/`DateTime`/`DateTime64` binding is **unchanged** (scalar still uses the bare Unix-timestamp form that scalar `DateTime` relies on). ### Behavioral note (deliberate tradeoff) Because a query parameter is type-agnostic on the client side (the formatter can't see whether the target column is `Array(Date)` or `Array(DateTime)`), a `Date` used inside `Array(DateTime)`/`Array(DateTime64)` is now bound at **day precision** — the time-of-day is dropped (`2022-05-02 13:25:55` → `2022-05-02 00:00:00`). Date-only is the only encoding `Array(Date)` accepts, so this is unavoidable for a type-blind formatter; an integration test pins this behavior. Scalar `DateTime`/`DateTime64` are untouched and keep full precision. This is the JS analog of a cross-client class also fixed in clickhouse-java (#2898) and reported for clickhouse-connect (#188). ## Changes - `packages/client-common/src/data_formatter/format_query_params.ts`: in the `value instanceof Date` branch, when `isInArrayOrTuple` is true, return `'${value.toISOString().slice(0, 10)}'`. (This file is the single shared source; `client-node`/`client-web` symlink it, so both clients get the fix.) ## Test - **Unit** (`format_query_params.test.ts`): `Date` in an array, nested array, tuple, and object value all render as quoted date strings; time-of-day/millis are dropped; a `Date` composes with other element types in a mixed array. Existing scalar `Date` tests are retained as the contrast that pins the unchanged scalar output. - **Integration** (`select_query_binding.test.ts`): binding `[Date, Date]` to `Array(Date)` round-trips to `["2023-05-05","2021-01-02"]` (fails on `main` with the parse error above); a `Date` bound to `Array(DateTime)` returns `2022-05-02 00:00:00`, documenting the day-precision tradeoff and guarding that `Array(DateTime)` no longer errors. Verified the unit tests fail on unpatched code with the exact bug (`expected '[1659081134]' to be '['2022-07-29']'`) and pass with the fix; full unit suite (389 passed) and the `select_query_binding` integration suite (31 passed) are green; `prettier`, `eslint`, and `tsc` are clean. ## Pre-PR validation gate - [x] Deterministic repro confirmed (server rejects `[<unix>]` for `Array(Date)`; new tests fail on `main`) - [x] Root cause documented above - [x] Fix targets the root cause (container element encoding) - [x] Test fails without fix, passes with fix - [x] No existing tests broken; no existing tests weakened - [x] Fix on the live runtime path (proven by an end-to-end integration test through `client.query`) - [x] Convention compliance verified per `AGENTS.md` / `CONTRIBUTING.md` - [ ] CHANGELOG entries (added in a follow-up commit on this branch) --------- Co-authored-by: Polyglot AI <293096396+polyglotAI-bot@users.noreply.github.com>
1 parent 3bccc81 commit d0f0b37

5 files changed

Lines changed: 101 additions & 0 deletions

File tree

packages/client-common/__tests__/integration/select_query_binding.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,35 @@ describe("select with query binding", () => {
268268
const response = await rs.text();
269269
expect(response).toBe('"2022-05-02 13:25:55.123456789"\n');
270270
});
271+
272+
it("handles Array(Date) in a parameterized query", async () => {
273+
const rs = await client.query({
274+
query: "SELECT {dates: Array(Date)} AS dates",
275+
format: "JSONEachRow",
276+
query_params: {
277+
dates: [
278+
new Date(Date.UTC(2023, 4, 5)),
279+
new Date(Date.UTC(2021, 0, 2)),
280+
],
281+
},
282+
});
283+
284+
expect(await rs.json()).toEqual([
285+
{ dates: ["2023-05-05", "2021-01-02"] },
286+
]);
287+
});
288+
289+
it("binds a Date inside Array(DateTime) at day precision (time is dropped)", async () => {
290+
const rs = await client.query({
291+
query: "SELECT {dates: Array(DateTime)} AS dates",
292+
format: "JSONEachRow",
293+
query_params: {
294+
dates: [new Date(Date.UTC(2022, 4, 2, 13, 25, 55))],
295+
},
296+
});
297+
298+
expect(await rs.json()).toEqual([{ dates: ["2022-05-02 00:00:00"] }]);
299+
});
271300
});
272301

273302
it("handles an array of strings in a parameterized query", async () => {

packages/client-common/__tests__/unit/format_query_params.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,4 +249,52 @@ describe("formatQueryParams", () => {
249249
}),
250250
).toBe("{'name':'test','flags':[TRUE,FALSE],'tuple':(FALSE,TRUE)}");
251251
});
252+
253+
it("formats a Date inside an array as a quoted date string", () => {
254+
expect(
255+
formatQueryParams({
256+
value: [new Date(Date.UTC(2022, 6, 29, 7, 52, 14))],
257+
}),
258+
).toBe("['2022-07-29']");
259+
});
260+
261+
it("formats a Date inside a nested array as a quoted date string", () => {
262+
expect(
263+
formatQueryParams({
264+
value: [[new Date(Date.UTC(2023, 4, 5))]],
265+
}),
266+
).toBe("[['2023-05-05']]");
267+
});
268+
269+
it("formats a Date inside a tuple as a quoted date string", () => {
270+
expect(
271+
formatQueryParams({
272+
value: new TupleParam([new Date(Date.UTC(2023, 4, 5))]),
273+
}),
274+
).toBe("('2023-05-05')");
275+
});
276+
277+
it("formats a Date inside an object value as a quoted date string", () => {
278+
expect(
279+
formatQueryParams({
280+
value: { d: new Date(Date.UTC(2023, 4, 5)) },
281+
}),
282+
).toBe("{'d':'2023-05-05'}");
283+
});
284+
285+
it("uses the UTC date and drops the time for a Date inside an array", () => {
286+
expect(
287+
formatQueryParams({
288+
value: [new Date(Date.UTC(2022, 6, 29, 23, 59, 59, 999))],
289+
}),
290+
).toBe("['2022-07-29']");
291+
});
292+
293+
it("formats a Date alongside other types inside an array", () => {
294+
expect(
295+
formatQueryParams({
296+
value: [new Date(Date.UTC(2023, 4, 5)), "foo", 42, null],
297+
}),
298+
).toBe("['2023-05-05','foo',42,NULL]");
299+
});
252300
});

packages/client-common/src/data_formatter/format_query_params.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,14 @@ function formatQueryParamsInternal({
8080
}
8181

8282
if (value instanceof Date) {
83+
if (isInArrayOrTuple) {
84+
// Inside a container each element is parsed by the element type's own
85+
// parser: Array(Date)/Array(Date32) reject a bare Unix timestamp and only
86+
// accept a quoted date string. A quoted UTC 'YYYY-MM-DD' is the single
87+
// representation accepted by every temporal element type (Date, Date32,
88+
// DateTime, DateTime64).
89+
return `'${value.toISOString().slice(0, 10)}'`;
90+
}
8391
// The ClickHouse server parses numbers as time-zone-agnostic Unix timestamps
8492
const unixTimestamp = Math.floor(value.getTime() / 1000)
8593
.toString()

packages/client-node/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
# 1.23.2
2+
3+
## Bug fixes
4+
5+
- Fixed `Array(Date)` / `Array(Date32)` query-parameter binding (and other temporal element types nested in arrays, tuples, and maps). A JS `Date` inside a container was serialized as a bare Unix timestamp (e.g. `[1683244800]`), which the server's `Array(Date)` element parser rejects (`CANNOT_PARSE_INPUT_ASSERTION_FAILED`). Container-nested `Date` values are now emitted as a quoted UTC date string (e.g. `['2023-05-05']`), the one encoding every temporal element type accepts. Note: a `Date` used inside `Array(DateTime)` / `Array(DateTime64)` is now bound at day precision (the time-of-day is dropped), since date-only is the only form `Array(Date)` accepts; scalar `Date` / `DateTime` binding is unchanged. ([#947])
6+
7+
[#947]: https://github.com/ClickHouse/clickhouse-js/pull/947
8+
19
# 1.23.1
210

311
## Bug Fixes

packages/client-web/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
# 1.23.2
2+
3+
## Bug fixes
4+
5+
- Fixed `Array(Date)` / `Array(Date32)` query-parameter binding (and other temporal element types nested in arrays, tuples, and maps). A JS `Date` inside a container was serialized as a bare Unix timestamp (e.g. `[1683244800]`), which the server's `Array(Date)` element parser rejects (`CANNOT_PARSE_INPUT_ASSERTION_FAILED`). Container-nested `Date` values are now emitted as a quoted UTC date string (e.g. `['2023-05-05']`), the one encoding every temporal element type accepts. Note: a `Date` used inside `Array(DateTime)` / `Array(DateTime64)` is now bound at day precision (the time-of-day is dropped), since date-only is the only form `Array(Date)` accepts; scalar `Date` / `DateTime` binding is unchanged. ([#947])
6+
7+
[#947]: https://github.com/ClickHouse/clickhouse-js/pull/947
8+
19
# 1.23.1
210

311
## Bug Fixes

0 commit comments

Comments
 (0)