Skip to content

Commit 45c357d

Browse files
authored
Improvements to the Tuple and Map parameter binding (#359)
1 parent 64ea1eb commit 45c357d

File tree

8 files changed

+114
-8
lines changed

8 files changed

+114
-8
lines changed

packages/client-common/__tests__/integration/select_query_binding.test.ts

+87-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { QueryParams } from '@clickhouse/client-common'
2+
import { TupleParam } from '@clickhouse/client-common'
23
import { type ClickHouseClient } from '@clickhouse/client-common'
34
import { createTestClient } from '../utils'
45

@@ -92,6 +93,85 @@ describe('select with query binding', () => {
9293
])
9394
})
9495

96+
it('handles tuples in a parametrized query', async () => {
97+
const rs = await client.query({
98+
query: 'SELECT {var: Tuple(Int32, String, String, String)} AS result',
99+
format: 'JSONEachRow',
100+
query_params: {
101+
var: new TupleParam([42, 'foo', "foo_'_bar", 'foo_\t_bar']),
102+
},
103+
})
104+
expect(await rs.json()).toEqual([
105+
{
106+
result: [42, 'foo', "foo_'_bar", 'foo_\t_bar'],
107+
},
108+
])
109+
})
110+
111+
it('handles arrays of tuples in a parametrized query', async () => {
112+
const rs = await client.query({
113+
query:
114+
'SELECT {var: Array(Tuple(Int32, String, String, String))} AS result',
115+
format: 'JSONEachRow',
116+
query_params: {
117+
var: [new TupleParam([42, 'foo', "foo_'_bar", 'foo_\t_bar'])],
118+
},
119+
})
120+
expect(await rs.json()).toEqual([
121+
{
122+
result: [[42, 'foo', "foo_'_bar", 'foo_\t_bar']],
123+
},
124+
])
125+
})
126+
127+
it('handles maps with tuples in a parametrized query', async () => {
128+
const rs = await client.query({
129+
query:
130+
'SELECT {var: Map(Int32, Tuple(Int32, String, String, String))} AS result',
131+
format: 'JSONEachRow',
132+
query_params: {
133+
var: new Map([
134+
[42, new TupleParam([144, 'foo', "foo_'_bar", 'foo_\t_bar'])],
135+
]),
136+
},
137+
})
138+
expect(await rs.json()).toEqual([
139+
{
140+
result: {
141+
42: [144, 'foo', "foo_'_bar", 'foo_\t_bar'],
142+
},
143+
},
144+
])
145+
})
146+
147+
it('handles maps with nested arrays in a parametrized query', async () => {
148+
const rs = await client.query({
149+
query: 'SELECT {var: Map(Int32, Array(Array(Int32)))} AS result',
150+
format: 'JSONEachRow',
151+
query_params: {
152+
var: new Map([
153+
[
154+
42,
155+
[
156+
[1, 2, 3],
157+
[4, 5],
158+
],
159+
],
160+
]),
161+
},
162+
})
163+
expect(await rs.json()).toEqual([
164+
{
165+
result: {
166+
42: [
167+
[1, 2, 3],
168+
[4, 5],
169+
],
170+
},
171+
},
172+
])
173+
})
174+
95175
describe('Date(Time)', () => {
96176
it('handles Date in a parameterized query', async () => {
97177
const rs = await client.query({
@@ -201,7 +281,7 @@ describe('select with query binding', () => {
201281
expect(response).toBe('"co\'nca\'t"\n')
202282
})
203283

204-
it('handles an object a parameterized query', async () => {
284+
it('handles an object as a map a parameterized query', async () => {
205285
const rs = await client.query({
206286
query: 'SELECT mapKeys({obj: Map(String, UInt32)})',
207287
format: 'CSV',
@@ -235,6 +315,7 @@ describe('select with query binding', () => {
235315
bar = 1,
236316
qaz = 2,
237317
}
318+
238319
const rs = await client.query({
239320
query:
240321
'SELECT * FROM system.numbers WHERE number = {filter: Int64} LIMIT 1',
@@ -253,6 +334,7 @@ describe('select with query binding', () => {
253334
foo = 'foo',
254335
bar = 'bar',
255336
}
337+
256338
const rs = await client.query({
257339
query: 'SELECT concat({str1: String},{str2: String})',
258340
format: 'TabSeparated',
@@ -284,8 +366,10 @@ describe('select with query binding', () => {
284366
await expectAsync(
285367
client.query({
286368
query: `
287-
SELECT * FROM system.numbers
288-
WHERE number > {min_limit: UInt64} LIMIT 3
369+
SELECT *
370+
FROM system.numbers
371+
WHERE number > {min_limit: UInt64}
372+
LIMIT 3
289373
`,
290374
}),
291375
).toBeRejectedWith(

packages/client-common/src/client.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export interface BaseQueryParams {
3232
* @default undefined (no override) */
3333
session_id?: string
3434
/** A specific list of roles to use for this query.
35-
* If it is not set, {@link BaseClickHouseClientConfigOptions.roles} will be used.
35+
* If it is not set, {@link BaseClickHouseClientConfigOptions.role} will be used.
3636
* @default undefined (no override) */
3737
role?: string | Array<string>
3838
/** When defined, overrides the credentials from the {@link BaseClickHouseClientConfigOptions.username}

packages/client-common/src/data_formatter/format_query_params.ts

+20-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
export class TupleParam {
2+
constructor(public readonly values: any[]) {}
3+
}
4+
15
export function formatQueryParams(
26
value: any,
37
wrapStringInQuotes = false,
@@ -36,8 +40,7 @@ export function formatQueryParams(
3640
}
3741

3842
if (Array.isArray(value)) {
39-
const formatted = value.map((v) => formatQueryParams(v, true))
40-
return `[${formatted.join(',')}]`
43+
return `[${value.map((v) => formatQueryParams(v, true)).join(',')}]`
4144
}
4245

4346
if (value instanceof Date) {
@@ -51,6 +54,21 @@ export function formatQueryParams(
5154
: `${unixTimestamp}.${milliseconds.toString().padStart(3, '0')}`
5255
}
5356

57+
if (value instanceof TupleParam) {
58+
return `(${value.values.map((v) => formatQueryParams(v, true)).join(',')})`
59+
}
60+
61+
if (value instanceof Map) {
62+
const formatted: string[] = []
63+
for (const [key, val] of value) {
64+
formatted.push(
65+
`${formatQueryParams(key, true)}:${formatQueryParams(val, true)}`,
66+
)
67+
}
68+
return `{${formatted.join(',')}}`
69+
}
70+
71+
// This is only useful for simple maps where the keys are strings
5472
if (typeof value === 'object') {
5573
const formatted: string[] = []
5674
for (const [key, val] of Object.entries(value)) {
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export * from './formatter'
2-
export { formatQueryParams } from './format_query_params'
2+
export { TupleParam, formatQueryParams } from './format_query_params'
33
export { formatQuerySettings } from './format_query_settings'

packages/client-common/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export type {
3636
SingleDocumentJSONFormats,
3737
RecordsJSONFormats,
3838
} from './data_formatter'
39+
export { TupleParam } from './data_formatter'
3940
export { ClickHouseError } from './error'
4041
export {
4142
ClickHouseLogLevel,

packages/client-common/src/utils/url.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ export function toSearchParams({
5656

5757
if (query_params !== undefined) {
5858
for (const [key, value] of Object.entries(query_params)) {
59-
params.set(`param_${key}`, formatQueryParams(value))
59+
const formattedParam = formatQueryParams(value)
60+
params.set(`param_${key}`, formattedParam)
6061
}
6162
}
6263

packages/client-node/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,5 @@ export {
6363
type ProgressRow,
6464
isProgressRow,
6565
type RowOrProgress,
66+
TupleParam,
6667
} from '@clickhouse/client-common'

packages/client-web/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,5 @@ export {
6262
type ProgressRow,
6363
isProgressRow,
6464
type RowOrProgress,
65+
TupleParam,
6566
} from '@clickhouse/client-common'

0 commit comments

Comments
 (0)