Skip to content

feat: add SQL template literal API for ergonomic query parameterization#659

Open
Claude wants to merge 5 commits into
mainfrom
claude/propose-api-template-literals
Open

feat: add SQL template literal API for ergonomic query parameterization#659
Claude wants to merge 5 commits into
mainfrom
claude/propose-api-template-literals

Conversation

@Claude
Copy link
Copy Markdown
Contributor

@Claude Claude AI commented Apr 17, 2026

Summary

Implements a SQL template literal API (sql tagged template function) as an ergonomic alternative to ClickHouse's native {name: Type} parameter syntax. Addresses the lack of industry-standard parameter placeholders ($1, ?, :name) by providing JavaScript template literals with automatic type inference and built-in SQL injection protection.

Usage:

import { sql, identifier } from '@clickhouse/client'

// Before: verbose type annotations required
await client.query({
  query: 'SELECT * FROM {table: Identifier} WHERE name = {name: String} AND age > {age: Int32}',
  query_params: { table: 'users', name: 'Alice', age: 30 }
})

// After: automatic type inference, familiar syntax
const tableName = 'users'
const userName = 'Alice'
const age = 30
await client.query(
  sql`SELECT * FROM ${identifier(tableName)} WHERE name = ${userName} AND age > ${age}`
)

// Composable SQL fragments
const whereClause = sql`status = ${'active'} AND verified = ${true}`
await client.query(sql`SELECT * FROM users WHERE ${whereClause}`)

Implementation:

  • Core API (packages/client-common/src/sql_template.ts): Tagged template function with automatic JS→ClickHouse type mapping (string→String, number→Int32/Float64, Date→DateTime, arrays, tuples, maps)
  • Client integration: Method overload on ClickHouseClient.query() accepting SQLTemplate objects alongside existing QueryParams
  • Composability: Nested templates supported with automatic parameter renaming to avoid conflicts
  • Type safety: identifier() helper for table/column names using ClickHouse's Identifier type

Testing:

  • 40 unit tests covering type inference, template parsing, composability, edge cases
  • 24 integration tests verifying end-to-end functionality with live ClickHouse server

Note: Marked as experimental in CHANGELOG. Existing query_params API unchanged and fully supported.

Checklist

  • Unit and integration tests covering the common scenarios were added
  • A human-readable description of the changes was provided to include in CHANGELOG
  • For significant changes, documentation in https://github.com/ClickHouse/clickhouse-docs was updated with further explanations or tutorials

Claude AI and others added 5 commits April 17, 2026 08:55
Agent-Logs-Url: https://github.com/ClickHouse/clickhouse-js/sessions/9d9fab50-6c38-45a6-bf53-e8e8dbddbdd1

Co-authored-by: peter-leonov-ch <209667683+peter-leonov-ch@users.noreply.github.com>
Agent-Logs-Url: https://github.com/ClickHouse/clickhouse-js/sessions/9d9fab50-6c38-45a6-bf53-e8e8dbddbdd1

Co-authored-by: peter-leonov-ch <209667683+peter-leonov-ch@users.noreply.github.com>
Agent-Logs-Url: https://github.com/ClickHouse/clickhouse-js/sessions/9d9fab50-6c38-45a6-bf53-e8e8dbddbdd1

Co-authored-by: peter-leonov-ch <209667683+peter-leonov-ch@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 17, 2026 11:48
@Claude Claude AI review requested due to automatic review settings April 17, 2026 11:49
@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

}

// Check for plain objects (treated as maps with string keys)
if (typeof value === 'object' && value !== null) {
Comment thread packages/client-common/__tests__/integration/sql_template.test.ts Dismissed
Comment thread packages/client-common/__tests__/unit/sql_template.test.ts Dismissed
Comment thread packages/client-common/__tests__/integration/sql_template.test.ts Dismissed
@peter-leonov-ch peter-leonov-ch marked this pull request as ready for review May 5, 2026 11:34
Copilot AI review requested due to automatic review settings May 5, 2026 11:34
@codecov
Copy link
Copy Markdown

codecov Bot commented May 5, 2026

Codecov Report

❌ Patch coverage is 95.12195% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
packages/client-common/src/sql_template.ts 94.80% 2 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an ergonomic sql tagged template literal API to @clickhouse/client-common for building ClickHouse queries with automatically inferred {name: Type} placeholders, including support for nested/composable fragments and identifier-safe interpolation.

Changes:

  • Introduces SQLTemplate + sql/identifier helpers with JS→ClickHouse type inference and nested-template parameter renaming.
  • Extends ClickHouseClient.query() to accept SQLTemplate in addition to existing QueryParams.
  • Adds unit + integration test suites for the new API and documents it in CHANGELOG.md (experimental).

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
packages/client-common/src/sql_template.ts New core SQL template literal implementation, type inference, composability support
packages/client-common/src/index.ts Re-exports SQLTemplate and sql-template helpers from client-common
packages/client-common/src/client.ts Adds query() overload to accept SQLTemplate and converts it to existing query params
packages/client-common/tests/unit/sql_template.test.ts Unit tests for parsing, inference, identifiers, and nested templates
packages/client-common/tests/integration/sql_template.test.ts End-to-end tests against ClickHouse verifying behavior across types and composition
CHANGELOG.md Adds an “Unreleased” entry describing the experimental SQL template literal API

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +84 to +96
if (typeof value === 'number') {
// Detect if it's an integer or float
// Use Int32 for integers and Float64 for floats by default
if (Number.isInteger(value)) {
// Check if it fits in Int32 range
if (value >= -2147483648 && value <= 2147483647) {
return 'Int32'
}
// For larger integers, use Int64
return 'Int64'
}
return 'Float64'
}
Comment on lines +98 to +100
if (typeof value === 'bigint') {
return 'Int64'
}
Comment on lines +112 to +113
// Infer from the first element
const elementType = inferClickHouseType(value[0])
Comment on lines +125 to +150
if (value instanceof Map) {
if (value.size === 0) {
throw new Error(
'Cannot infer ClickHouse type from empty Map. Please provide at least one entry or use an explicit type hint.',
)
}
// Infer from the first entry
const [k, v] = value.entries().next().value as [unknown, unknown]
const keyType = inferClickHouseType(k)
const valueType = inferClickHouseType(v)
return `Map(${keyType}, ${valueType})`
}

// Check for plain objects (treated as maps with string keys)
if (typeof value === 'object' && value !== null) {
const entries = Object.entries(value)
if (entries.length === 0) {
throw new Error(
'Cannot infer ClickHouse type from empty object. Please provide at least one property or use an explicit type hint.',
)
}
// Infer from the first entry
const [, v] = entries[0]
const valueType = inferClickHouseType(v)
return `Map(String, ${valueType})`
}
Comment on lines +253 to 260
async query<Format extends DataFormat = 'JSON'>(
template: SQLTemplate,
format?: Format,
): Promise<QueryResult<Stream, Format>>
async query<Format extends DataFormat = 'JSON'>(
paramsOrTemplate: QueryParamsWithFormat<Format> | SQLTemplate,
format?: Format,
): Promise<QueryResult<Stream, Format>> {
Comment thread CHANGELOG.md
Comment on lines +15 to +32
const result = await client.query(
sql`SELECT * FROM users WHERE name = ${userName} AND age = ${age}`,
)

// Table and column identifiers
const tableName = 'users'
const columnName = 'name'
const result = await client.query(
sql`SELECT ${identifier(columnName)} FROM ${identifier(tableName)}`,
)

// Works with all supported types: arrays, tuples, maps, dates, etc.
const ids = [1, 2, 3]
const result = await client.query(sql`SELECT * FROM users WHERE id IN ${ids}`)

// Composable SQL fragments
const whereClause = sql`status = ${'active'} AND role = ${'admin'}`
const result = await client.query(sql`SELECT * FROM users WHERE ${whereClause}`)
Comment on lines +152 to +154
throw new Error(
`Cannot infer ClickHouse type for value: ${String(value)} (type: ${typeof value})`,
)
Comment on lines +241 to +247
* import { sql, identifier } from '@clickhouse/client'
*
* const tableName = 'users'
* const userName = 'Alice'
* const result = await client.query(
* sql`SELECT * FROM ${identifier(tableName)} WHERE name = ${userName}`
* )
Comment thread CHANGELOG.md
- **[Experimental]** Added SQL template literal API (`sql` tagged template function) for safe query parameterization with automatic type inference. This provides a more ergonomic alternative to ClickHouse's native `{name: Type}` parameter syntax, with familiar JavaScript template literal syntax and built-in SQL injection protection.

```ts
import { createClient, sql, identifier } from '@clickhouse/client'
*
* @example
* ```typescript
* import { sql, identifier } from '@clickhouse/client'
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.

4 participants