Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements to the Tuple and Map parameter binding #359

Merged
merged 1 commit into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion packages/client-common/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>
/** When defined, overrides the credentials from the {@link BaseClickHouseClientConfigOptions.username}
Expand Down
22 changes: 20 additions & 2 deletions packages/client-common/src/data_formatter/format_query_params.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export class TupleParam {
constructor(public readonly values: any[]) {}
}

export function formatQueryParams(
value: any,
wrapStringInQuotes = false,
Expand Down Expand Up @@ -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) {
Expand All @@ -51,6 +54,21 @@ export function formatQueryParams(
: `${unixTimestamp}.${milliseconds.toString().padStart(3, '0')}`
}

if (value instanceof TupleParam) {
mshustov marked this conversation as resolved.
Show resolved Hide resolved
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)) {
Expand Down
2 changes: 1 addition & 1 deletion packages/client-common/src/data_formatter/index.ts
Original file line number Diff line number Diff line change
@@ -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'
1 change: 1 addition & 0 deletions packages/client-common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export type {
SingleDocumentJSONFormats,
RecordsJSONFormats,
} from './data_formatter'
export { TupleParam } from './data_formatter'
export { ClickHouseError } from './error'
export {
ClickHouseLogLevel,
Expand Down
3 changes: 2 additions & 1 deletion packages/client-common/src/utils/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/client-node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,5 @@ export {
type ProgressRow,
isProgressRow,
type RowOrProgress,
TupleParam,
} from '@clickhouse/client-common'
1 change: 1 addition & 0 deletions packages/client-web/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,5 @@ export {
type ProgressRow,
isProgressRow,
type RowOrProgress,
TupleParam,
} from '@clickhouse/client-common'