diff --git a/packages/client-common/__tests__/integration/select_query_binding.test.ts b/packages/client-common/__tests__/integration/select_query_binding.test.ts index 21d327a..2872a56 100644 --- a/packages/client-common/__tests__/integration/select_query_binding.test.ts +++ b/packages/client-common/__tests__/integration/select_query_binding.test.ts @@ -1,4 +1,5 @@ import type { QueryParams } from '@clickhouse/client-common' +import { TupleParam } from '@clickhouse/client-common' import { type ClickHouseClient } from '@clickhouse/client-common' import { createTestClient } from '../utils' @@ -92,6 +93,85 @@ describe('select with query binding', () => { ]) }) + it('handles tuples in a parametrized query', async () => { + const rs = await client.query({ + query: 'SELECT {var: Tuple(Int32, String, String, String)} AS result', + format: 'JSONEachRow', + query_params: { + var: new TupleParam([42, 'foo', "foo_'_bar", 'foo_\t_bar']), + }, + }) + expect(await rs.json()).toEqual([ + { + result: [42, 'foo', "foo_'_bar", 'foo_\t_bar'], + }, + ]) + }) + + it('handles arrays of tuples in a parametrized query', async () => { + const rs = await client.query({ + query: + 'SELECT {var: Array(Tuple(Int32, String, String, String))} AS result', + format: 'JSONEachRow', + query_params: { + var: [new TupleParam([42, 'foo', "foo_'_bar", 'foo_\t_bar'])], + }, + }) + expect(await rs.json()).toEqual([ + { + result: [[42, 'foo', "foo_'_bar", 'foo_\t_bar']], + }, + ]) + }) + + it('handles maps with tuples in a parametrized query', async () => { + const rs = await client.query({ + query: + 'SELECT {var: Map(Int32, Tuple(Int32, String, String, String))} AS result', + format: 'JSONEachRow', + query_params: { + var: new Map([ + [42, new TupleParam([144, 'foo', "foo_'_bar", 'foo_\t_bar'])], + ]), + }, + }) + expect(await rs.json()).toEqual([ + { + result: { + 42: [144, 'foo', "foo_'_bar", 'foo_\t_bar'], + }, + }, + ]) + }) + + it('handles maps with nested arrays in a parametrized query', async () => { + const rs = await client.query({ + query: 'SELECT {var: Map(Int32, Array(Array(Int32)))} AS result', + format: 'JSONEachRow', + query_params: { + var: new Map([ + [ + 42, + [ + [1, 2, 3], + [4, 5], + ], + ], + ]), + }, + }) + expect(await rs.json()).toEqual([ + { + result: { + 42: [ + [1, 2, 3], + [4, 5], + ], + }, + }, + ]) + }) + describe('Date(Time)', () => { it('handles Date in a parameterized query', async () => { const rs = await client.query({ @@ -201,7 +281,7 @@ describe('select with query binding', () => { expect(response).toBe('"co\'nca\'t"\n') }) - it('handles an object a parameterized query', async () => { + it('handles an object as a map a parameterized query', async () => { const rs = await client.query({ query: 'SELECT mapKeys({obj: Map(String, UInt32)})', format: 'CSV', @@ -235,6 +315,7 @@ describe('select with query binding', () => { bar = 1, qaz = 2, } + const rs = await client.query({ query: 'SELECT * FROM system.numbers WHERE number = {filter: Int64} LIMIT 1', @@ -253,6 +334,7 @@ describe('select with query binding', () => { foo = 'foo', bar = 'bar', } + const rs = await client.query({ query: 'SELECT concat({str1: String},{str2: String})', format: 'TabSeparated', @@ -284,8 +366,10 @@ describe('select with query binding', () => { await expectAsync( client.query({ query: ` - SELECT * FROM system.numbers - WHERE number > {min_limit: UInt64} LIMIT 3 + SELECT * + FROM system.numbers + WHERE number > {min_limit: UInt64} + LIMIT 3 `, }), ).toBeRejectedWith( diff --git a/packages/client-common/src/client.ts b/packages/client-common/src/client.ts index 83481b1..627efa2 100644 --- a/packages/client-common/src/client.ts +++ b/packages/client-common/src/client.ts @@ -32,7 +32,7 @@ export interface BaseQueryParams { * @default undefined (no override) */ session_id?: string /** A specific list of roles to use for this query. - * If it is not set, {@link BaseClickHouseClientConfigOptions.roles} will be used. + * If it is not set, {@link BaseClickHouseClientConfigOptions.role} will be used. * @default undefined (no override) */ role?: string | Array /** When defined, overrides the credentials from the {@link BaseClickHouseClientConfigOptions.username} diff --git a/packages/client-common/src/data_formatter/format_query_params.ts b/packages/client-common/src/data_formatter/format_query_params.ts index 3ff611c..5f866b1 100644 --- a/packages/client-common/src/data_formatter/format_query_params.ts +++ b/packages/client-common/src/data_formatter/format_query_params.ts @@ -1,3 +1,7 @@ +export class TupleParam { + constructor(public readonly values: any[]) {} +} + export function formatQueryParams( value: any, wrapStringInQuotes = false, @@ -36,8 +40,7 @@ export function formatQueryParams( } if (Array.isArray(value)) { - const formatted = value.map((v) => formatQueryParams(v, true)) - return `[${formatted.join(',')}]` + return `[${value.map((v) => formatQueryParams(v, true)).join(',')}]` } if (value instanceof Date) { @@ -51,6 +54,21 @@ export function formatQueryParams( : `${unixTimestamp}.${milliseconds.toString().padStart(3, '0')}` } + if (value instanceof TupleParam) { + return `(${value.values.map((v) => formatQueryParams(v, true)).join(',')})` + } + + if (value instanceof Map) { + const formatted: string[] = [] + for (const [key, val] of value) { + formatted.push( + `${formatQueryParams(key, true)}:${formatQueryParams(val, true)}`, + ) + } + return `{${formatted.join(',')}}` + } + + // This is only useful for simple maps where the keys are strings if (typeof value === 'object') { const formatted: string[] = [] for (const [key, val] of Object.entries(value)) { diff --git a/packages/client-common/src/data_formatter/index.ts b/packages/client-common/src/data_formatter/index.ts index 8f880b3..c34515d 100644 --- a/packages/client-common/src/data_formatter/index.ts +++ b/packages/client-common/src/data_formatter/index.ts @@ -1,3 +1,3 @@ export * from './formatter' -export { formatQueryParams } from './format_query_params' +export { TupleParam, formatQueryParams } from './format_query_params' export { formatQuerySettings } from './format_query_settings' diff --git a/packages/client-common/src/index.ts b/packages/client-common/src/index.ts index 3bbc262..7852756 100644 --- a/packages/client-common/src/index.ts +++ b/packages/client-common/src/index.ts @@ -36,6 +36,7 @@ export type { SingleDocumentJSONFormats, RecordsJSONFormats, } from './data_formatter' +export { TupleParam } from './data_formatter' export { ClickHouseError } from './error' export { ClickHouseLogLevel, diff --git a/packages/client-common/src/utils/url.ts b/packages/client-common/src/utils/url.ts index 690ee62..70f6a86 100644 --- a/packages/client-common/src/utils/url.ts +++ b/packages/client-common/src/utils/url.ts @@ -56,7 +56,8 @@ export function toSearchParams({ if (query_params !== undefined) { for (const [key, value] of Object.entries(query_params)) { - params.set(`param_${key}`, formatQueryParams(value)) + const formattedParam = formatQueryParams(value) + params.set(`param_${key}`, formattedParam) } } diff --git a/packages/client-node/src/index.ts b/packages/client-node/src/index.ts index 68d4a30..c3544aa 100644 --- a/packages/client-node/src/index.ts +++ b/packages/client-node/src/index.ts @@ -63,4 +63,5 @@ export { type ProgressRow, isProgressRow, type RowOrProgress, + TupleParam, } from '@clickhouse/client-common' diff --git a/packages/client-web/src/index.ts b/packages/client-web/src/index.ts index 27503be..021566a 100644 --- a/packages/client-web/src/index.ts +++ b/packages/client-web/src/index.ts @@ -62,4 +62,5 @@ export { type ProgressRow, isProgressRow, type RowOrProgress, + TupleParam, } from '@clickhouse/client-common'