feat: add SQL template literal API for ergonomic query parameterization#659
feat: add SQL template literal API for ergonomic query parameterization#659Claude wants to merge 5 commits into
Conversation
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>
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>
|
|
| } | ||
|
|
||
| // Check for plain objects (treated as maps with string keys) | ||
| if (typeof value === 'object' && value !== null) { |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
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/identifierhelpers with JS→ClickHouse type inference and nested-template parameter renaming. - Extends
ClickHouseClient.query()to acceptSQLTemplatein addition to existingQueryParams. - 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.
| 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' | ||
| } |
| if (typeof value === 'bigint') { | ||
| return 'Int64' | ||
| } |
| // Infer from the first element | ||
| const elementType = inferClickHouseType(value[0]) |
| 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})` | ||
| } |
| 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>> { |
| 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}`) |
| throw new Error( | ||
| `Cannot infer ClickHouse type for value: ${String(value)} (type: ${typeof value})`, | ||
| ) |
| * 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}` | ||
| * ) |
| - **[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' |
Summary
Implements a SQL template literal API (
sqltagged 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:
Implementation:
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)ClickHouseClient.query()acceptingSQLTemplateobjects alongside existingQueryParamsidentifier()helper for table/column names using ClickHouse'sIdentifiertypeTesting:
Note: Marked as experimental in CHANGELOG. Existing
query_paramsAPI unchanged and fully supported.Checklist