← Files ClickHouseARCHIVED FILE

skills/clickhouse-js-node-coding/reference/query-parameters.md

5.42 KB · Sep 30, 2026 · 22:50 UTC

↓ Download file

# Query Parameter Binding

> **Applies to:** all versions. NULL parameter binding fixed in `0.0.16`.
> Special-character (tab/newline/quote/backslash) binding `>= 0.3.1`.
> `TupleParam` and JS `Map` parameters `>= 1.9.0`. Boolean formatting in
> `Array`/`Tuple`/`Map` parameters fixed in `>= 1.13.0`. `BigInt` query
> parameters `>= 1.15.0`.

## Answer checklist

When the user passes user-controlled values into SQL:

- Use ClickHouse `{name: Type}` placeholders and a `query_params` object.
- **Your response must explicitly name template-literal / string
  interpolation of user input as a SQL injection risk** — even when the
  user only asked "how do I bind values" and did not mention security.
  This is non-negotiable: the security framing is part of the right
  answer, not an optional aside.
- Do not suggest PostgreSQL/MySQL-style `$1`, `?`, or `:name` placeholders.
- Pick the placeholder type to match the ClickHouse column type (`String`,
  `Date`, `DateTime`, `Nullable(T)`, etc.).

## Syntax: `{name: Type}`

ClickHouse uses `{name: Type}` placeholders — **not** `$1`, `?`, or `:name`.

```ts
await client.query({
  query: "SELECT plus({a: Int32}, {b: Int32})",
  format: "JSONEachRow",
  query_params: { a: 10, b: 20 },
});
```

The `Type` must be a valid ClickHouse type (`Int32`, `String`, `Date`,
`Array(UInt32)`, `Tuple(Int32, String)`, `Map(K, V)`, `Nullable(T)`, etc.).

## ⚠️ Never use template literals for user values

Interpolating user input into the SQL string bypasses server-side escaping
and opens the door to SQL injection:

```ts
const userId = req.params.id;

// ❌ Dangerous — never do this with user-controlled values
await client.query({ query: `SELECT * FROM users WHERE id = ${userId}` });

// ✓ Safe — parameterized
await client.query({
  query: "SELECT * FROM users WHERE id = {id: UInt32}",
  query_params: { id: userId },
});
```

This is the most common mistake for users coming from PostgreSQL/MySQL. Call
it out explicitly when the user shows template-literal interpolation.

## Common types

```ts
import { TupleParam } from "@clickhouse/client";

await client.query({
  query: `
    SELECT
      {var_int: Int32}                     AS var_int,
      {var_float: Float32}                 AS var_float,
      {var_str: String}                    AS var_str,
      {var_array: Array(Int32)}            AS var_array,
      {var_tuple: Tuple(Int32, String)}    AS var_tuple,
      {var_map: Map(Int, Array(String))}   AS var_map,
      {var_date: Date}                     AS var_date,
      {var_datetime: DateTime}             AS var_datetime,
      {var_datetime64_3: DateTime64(3)}    AS var_datetime64_3,
      {var_datetime64_9: DateTime64(9)}    AS var_datetime64_9,
      {var_decimal: Decimal(9, 2)}         AS var_decimal,
      {var_uuid: UUID}                     AS var_uuid,
      {var_ipv4: IPv4}                     AS var_ipv4,
      {var_null: Nullable(String)}         AS var_null
  `,
  format: "JSONEachRow",
  query_params: {
    var_int: 10,
    var_float: "10.557",
    var_str: "hello",
    var_array: [42, 144],
    var_tuple: new TupleParam([42, "foo"]), // >= 1.9.0
    var_map: new Map([
      [42, ["a", "b"]],
      [144, ["c", "d"]],
    ]), // >= 1.9.0
    var_date: "2022-01-01",
    var_datetime: "2022-01-01 12:34:56", // or a Date
    var_datetime64_3: "2022-01-01 12:34:56.789", // or a Date
    var_datetime64_9: "2022-01-01 12:34:56.123456789", // string for ns precision
    var_decimal: "123.45", // string to avoid precision loss
    var_uuid: "01234567-89ab-cdef-0123-456789abcdef",
    var_ipv4: "192.168.0.1",
    var_null: null, // fixed in 0.0.16
  },
});
```

### Type-by-type tips

- **Decimals** — pass as strings to avoid JS number precision loss.
- **`DateTime64(>3)`** — pass as a string; JS `Date` only has millisecond
  precision and will lose sub-millisecond digits.
- **`DateTime64`** — strings can also be UNIX timestamps, including
  fractional ones (e.g., `'1651490755.123456789'`).
- **`BigInt`** — supported in `query_params` since `>= 1.15.0`. On older
  clients, pass as a string.
- **`Tuple(...)`** — wrap in `new TupleParam([...])` (`>= 1.9.0`); on older
  clients, build the literal manually as a string.
- **`Map(K, V)`** — pass a JS `Map` (`>= 1.9.0`); on older clients, build
  it manually.
- **`Nullable(T)`** — pass `null` directly (`>= 0.0.16`).

## Special characters in string parameters (`>= 0.3.1`)

Tabs, newlines, carriage returns, single quotes, and backslashes are
escaped automatically by the client — just pass the JS string as-is:

```ts
await client.query({
  query: `
    SELECT
      'foo_\t_bar'  = {tab: String}             AS has_tab,
      'foo_\n_bar'  = {newline: String}         AS has_newline,
      'foo_\\'_bar' = {single_quote: String}    AS has_single_quote,
      'foo_\\_bar'  = {backslash: String}       AS has_backslash
  `,
  format: "JSONEachRow",
  query_params: {
    tab: "foo_\t_bar",
    newline: "foo_\n_bar",
    single_quote: "foo_'_bar",
    backslash: "foo_\\_bar",
  },
});
```

## Common pitfalls

- **`$1` / `?` / `:name` placeholders.** None work — use `{name: Type}`.
- **Forgetting the type in the placeholder.** `{id}` is a syntax error;
  it must be `{id: UInt32}`.
- **Stringifying tuples/maps manually on `>= 1.9.0`.** Use `TupleParam`
  and `Map` — both serialize correctly and respect special characters.
- **Boolean array/tuple/map elements before `1.13.0`.** Boolean formatting
  was fixed in 1.13.0 — earlier versions may misformat them.

SHA-256: cbd30281f736cf93304355c05da9e4e88f5b22cb46d51e6e0d45752701150004