Skip to content

Bind Tuple parameters and integer Map keys, matching @clickhouse/client#75

Open
dfrankland wants to merge 1 commit into
chdb-io:mainfrom
dfrankland:fix/tuple-param-binding
Open

Bind Tuple parameters and integer Map keys, matching @clickhouse/client#75
dfrankland wants to merge 1 commit into
chdb-io:mainfrom
dfrankland:fix/tuple-param-binding

Conversation

@dfrankland

@dfrankland dfrankland commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

What

serializeValue (the JS-value serializer behind formatParamValue, used by the queryBindAsync {name:Type} server-side binding path) mishandled two compound-type parameter shapes, so @clickhouse/client code passing them through ChdbConnection failed with engine parse errors:

  • A @clickhouse/client-common TupleParam fell through to the generic-object branch and serialized as the map literal {'values':[...]}, which the engine rejects for a Tuple(...) param.
  • Map keys were force-stringified and quoted ({'42':...}), which the engine rejects for Map(Int32, ...).

Why

chdb-node is a byte-compatible @clickhouse/client façade. The reference client serializes a TupleParam as the positional form (v1, v2, …) and a Map key by its JS type (numbers unquoted); chdb-node's divergent output was a serialization bug, not an engine limitation. These are exactly the compound-type divergences called out as out-of-scope in #68's "Scope / non-goals".

The diff

   if (Array.isArray(value)) return serializeArray(value)

+  if (isTupleParam(value)) return serializeTuple(value.values)
+
   if (value instanceof Map) {
     const parts: string[] = []
     for (const [k, v] of value) {
-      parts.push(`${escapeStringLiteral(String(k))}:${serializeValue(v)}`)
+      // key by JS type: numeric → `42`, string → `'k'`
+      parts.push(`${serializeValue(k)}:${serializeValue(v)}`)
     }
     return `{${parts.join(',')}}`
   }

TupleParam is detected by shape (constructor name + values array), not instanceof: the instance a caller passes originates from their @clickhouse/client(-common) copy, which under pnpm/nested installs may resolve to a different module instance than ours, so instanceof against our imported class would spuriously miss it. Both fixes live in serializeValue, so they apply at any nesting depth (Array(Tuple(...)), Map(K, Tuple(...))), mirroring the reference client's recursion.

Verification

Confirmed end-to-end against the native chdb_query_with_params path:

param placeholder result
new TupleParam(['main*','^main.*$']) Tuple(String, String) ('main*','^main.*$')
[TupleParam, TupleParam] Array(Tuple(String, String)) rows parse correctly
new Map([[42, 'v']]) Map(Int32, String) {42:'v'} (was {'42':'v'} → rejected)

Parity suite

Removes the 7 tuple/map entries from tests/clickhouse-js/skip_list.json. Verified against the upstream select_query_binding.test.ts (@clickhouse/client client-1.23.0, ChdbConnection injected, :memory:) — all 7 previously-skipped cases now pass:

✓ … handles tuples in a parametrized query
✓ … handles arrays of tuples in a parametrized query
✓ … handles maps with tuples in a parametrized query
✓ … handles maps with nested arrays in a parametrized query
✓ … handles maps with nullable values in a parametrized query
✓ … Nested boolean types > handles boolean in a tuple
✓ … Nested boolean types > handles boolean in a mixed nested structure

Tests

  • test/v3/serialize.test.ts: tuple literal, Array(Tuple), numeric-key Map, and a guard that a plain { values: [...] } object still serializes as a map.
  • test/v3/querybind.test.ts: real-engine round-trips for Tuple(String, String) and Array(Tuple(String, String)).
  • Full test/v3 suite green: 454 passed, 11 skipped.

🤖 Generated with Claude Code

Note

Bind Tuple parameters and integer Map keys in query serialization, matching @clickhouse/client

  • Adds TupleParam support to serializeValue in serialize.ts: objects matching the TupleParam shape (constructor name 'TupleParam' with an array .values) are rendered as SQL tuple literals, e.g. ('a','b').
  • Changes Map key serialization to call serializeValue(k) instead of always quoting keys as strings, so numeric keys render unquoted.
  • Removes tuple-related entries from the integration test skip list in skip_list.json as these cases now pass.
  • Behavioral Change: Map keys that are numeric will no longer be quoted in serialized SQL output.

Macroscope summarized a0e5360.

`serializeValue` (the JS-value serializer behind `formatParamValue`, used by
the `queryBindAsync` server-side binding path) mishandled two compound-type
parameter shapes, so `@clickhouse/client` code passing them through
`ChdbConnection` failed with engine parse errors:

- A `@clickhouse/client-common` `TupleParam` fell through to the generic-object
  branch and serialized as the map literal `{'values':[...]}`, which the engine
  rejects for a `Tuple(...)` param. It now serializes as the positional tuple
  literal `(a,b,c)`. Detected by shape (constructor name + `values` array)
  rather than `instanceof`: the instance a caller passes comes from their own
  `@clickhouse/client(-common)` copy, which under pnpm/nested installs may be a
  different module instance than ours, so `instanceof` would spuriously miss it.

- `Map` keys were force-stringified and quoted (`{'42':...}`), which the engine
  rejects for `Map(Int32, ...)`. Keys are now serialized by their JS type
  (numeric keys unquoted, string keys quoted), matching
  `@clickhouse/client-common`'s formatter.

Verified against the real engine: added unit + `queryBind` round-trip tests,
and confirmed the upstream `select_query_binding.test.ts` tuple/map cases now
pass, so their entries are removed from the clickhouse-js parity skip list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant