Skip to content

Scalar {p:Date} / {p:Date32} query params serialize JS Date as a Unix timestamp, rejected by server #955

Description

@claude

Description

When a JavaScript Date object is bound to a scalar Date or Date32 server-side query parameter ({name:Date} / {name:Date32}), the client serializes it as a bare Unix-seconds timestamp. ClickHouse's parameter parser accepts a numeric timestamp for DateTime/DateTime64, but rejects it for Date/Date32, which expect a YYYY-MM-DD date string. The query fails with BAD_QUERY_PARAMETER (code 457).

The offending path is in packages/client-common/src/data_formatter/format_query_params.ts. For a top-level Date value (isInArrayOrTuple === false), it returns a Unix timestamp:

if (value instanceof Date) {
  if (isInArrayOrTuple) {
    // container elements are correctly emitted as quoted 'YYYY-MM-DD'
    return `'${value.toISOString().slice(0, 10)}'`;
  }
  // scalar path: Unix timestamp -- OK for DateTime/DateTime64, WRONG for Date/Date32
  const unixTimestamp = Math.floor(value.getTime() / 1000).toString().padStart(10, "0");
  ...
}

The Array(Date) container case was already fixed in #947 (elements emitted as quoted 'YYYY-MM-DD'), but the scalar Date/Date32 case still emits a Unix timestamp.

This is the clickhouse-js analog of ClickHouse/clickhouse-go#1927 (case 1). Cases 1b/1c (Array(Bool), Array(Date)), 2a/2b (string escaping / TSV newline) and 3 (timezone) from that report do not reproduce here — I verified them against the server and they are already handled correctly (arrays use TRUE/FALSE and quoted dates; strings are escaped for the escaped-TSV format; timestamps are timezone-agnostic Unix seconds).

ClickHouse server version

Verified against 26.6.1.1193 (local, reachable over HTTP).

Reproduction

Client-level test (Node client, against http://localhost:8123):

import { createClient } from "@clickhouse/client";

const client = createClient({ url: "http://localhost:8123" });
const d = new Date(Date.UTC(2026, 4, 15)); // 2026-05-15T00:00:00Z

async function run(label: string, query: string, params: Record<string, unknown>) {
  try {
    const rs = await client.query({ query, query_params: params, format: "JSONEachRow" });
    console.log(`${label}\n    OK -> ${JSON.stringify(await rs.json())}`);
  } catch (e: any) {
    console.log(`${label}\n    ERROR: ${e.message?.split("\n")[0]}`);
  }
}

await run("{p:Date}     scalar", "SELECT {p:Date} AS v",     { p: d });
await run("{p:Date32}   scalar", "SELECT {p:Date32} AS v",   { p: d });
await run("{p:DateTime} scalar (control)", "SELECT {p:DateTime} AS v", { p: d });
await client.close();

Expected: all three succeed (a value valid for the declared type should round-trip).

Actual: the two Date/Date32 calls fail; only DateTime succeeds.

{p:Date}     scalar
    ERROR: code: 457, ... Value 1778457600 cannot be parsed as Date ... only 8 of 10 bytes was parsed: 17784576
{p:Date32}   scalar
    ERROR: code: 457, ... Value 1778457600 cannot be parsed as Date32 ...
{p:DateTime} scalar (control)
    OK -> [{"v":"..."}]

Direct verification against the server

The HTTP transport forwards formatQueryParams({ value }) verbatim as param_<name> (packages/client-common/src/utils/url.ts). Sending the exact value the formatter emits for the Date object above (1778457600) reproduces the failure without the client:

$ curl -s --get 'http://localhost:8123/?query=SELECT%20{p:Date}' --data-urlencode 'param_p=1778457600'
Code: 457. DB::Exception: Value 1778457600 cannot be parsed as Date for query parameter 'p'
  because it isn't parsed completely: only 8 of 10 bytes was parsed: 17784576. (BAD_QUERY_PARAMETER)

$ curl -s --get 'http://localhost:8123/?query=SELECT%20{p:Date}' --data-urlencode 'param_p=2026-05-15'
2026-05-15   # a quoted/plain date string is what Date expects

Suggested fix

In format_query_params.ts, the scalar Date branch cannot know the declared parameter type, so it must emit a representation accepted by every temporal type. A quoted 'YYYY-MM-DD HH:MM:SS[.fff]' string (or, matching the already-fixed container path, distinguishing Date vs DateTime) is accepted by Date, Date32, DateTime and DateTime64 alike, whereas a bare Unix timestamp is only accepted by the DateTime family. Aligning the scalar path with the container fix (which emits a quoted date string) would resolve Date/Date32 while preserving DateTime behavior.

Link

Relayed from ClickHouse/clickhouse-go#1927 (case 1). Related prior fix for the container case: #947.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions