Skip to content
Open
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
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# 1.12.2

## Bug fixes

- Fixed boolean value formatting in query parameters. Boolean values within `Array`, `Tuple`, and `Map` types are now correctly formatted as `TRUE`/`FALSE` instead of `1`/`0` to ensure proper type compatibility with ClickHouse.

# 1.12.1

## Improvements
Expand Down Expand Up @@ -198,7 +204,7 @@ A minor release to allow further investigation regarding uncaught error issues w

## New features

- Added optional `real_time_microseconds` field to the `ClickHouseSummary` interface (see https://github.com/ClickHouse/ClickHouse/pull/69032)
- Added optional `real_time_microseconds` field to the `ClickHouseSummary` interface (see <https://github.com/ClickHouse/ClickHouse/pull/69032>)

## Bug fixes

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { formatQueryParams } from '@clickhouse/client-common'
import { formatQueryParams, TupleParam } from '@clickhouse/client-common'

describe('formatQueryParams', () => {
it('formats null', () => {
Expand Down Expand Up @@ -158,4 +158,76 @@ describe('formatQueryParams', () => {
}),
).toBe("{'name':'custom','id':42,'params':{'refs':[44]}}")
})

it('formats booleans in arrays as TRUE/FALSE', () => {
expect(formatQueryParams({ value: [true, false] })).toBe('[TRUE,FALSE]')
expect(formatQueryParams({ value: [true] })).toBe('[TRUE]')
expect(formatQueryParams({ value: [false] })).toBe('[FALSE]')
})

it('formats booleans in nested arrays as TRUE/FALSE', () => {
expect(
formatQueryParams({
value: [
[true, false],
[false, true],
],
}),
).toBe('[[TRUE,FALSE],[FALSE,TRUE]]')
expect(
formatQueryParams({
value: [[[true]], [[false]]],
}),
).toBe('[[[TRUE]],[[FALSE]]]')
})

it('formats booleans in arrays with mixed types', () => {
expect(formatQueryParams({ value: [1, true, 'test', false, null] })).toBe(
"[1,TRUE,'test',FALSE,NULL]",
)
})

it('formats booleans in tuples as TRUE/FALSE', () => {
expect(
formatQueryParams({
value: new TupleParam([true, false]),
}),
).toBe('(TRUE,FALSE)')
expect(
formatQueryParams({
value: new TupleParam([1, true, 'test', false]),
}),
).toBe("(1,TRUE,'test',FALSE)")
})

it('formats booleans in nested tuples as TRUE/FALSE', () => {
expect(
formatQueryParams({
value: new TupleParam([new TupleParam([true, false]), true]),
}),
).toBe('((TRUE,FALSE),TRUE)')
})

it('formats booleans in objects (Maps) as TRUE/FALSE', () => {
expect(
formatQueryParams({
value: {
isActive: true,
isDeleted: false,
},
}),
).toBe("{'isActive':TRUE,'isDeleted':FALSE}")
})

it('formats booleans in nested structures', () => {
expect(
formatQueryParams({
value: {
name: 'test',
flags: [true, false],
tuple: new TupleParam([false, true]),
},
}),
).toBe("{'name':'test','flags':[TRUE,FALSE],'tuple':(FALSE,TRUE)}")
})
})
33 changes: 28 additions & 5 deletions packages/client-common/src/data_formatter/format_query_params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ export function formatQueryParams({
wrapStringInQuotes,
printNullAsKeyword,
}: FormatQueryParamsOptions): string {
return formatQueryParamsInternal({
value,
wrapStringInQuotes,
printNullAsKeyword,
isInArrayOrTuple: false,
})
}

function formatQueryParamsInternal({
value,
wrapStringInQuotes,
printNullAsKeyword,
isInArrayOrTuple,
}: FormatQueryParamsOptions & { isInArrayOrTuple: boolean }): string {
if (value === null || value === undefined) {
if (printNullAsKeyword) return 'NULL'
return '\\N'
Expand All @@ -16,7 +30,12 @@ export function formatQueryParams({
if (value === Number.NEGATIVE_INFINITY) return '-inf'

if (typeof value === 'number') return String(value)
if (typeof value === 'boolean') return value ? '1' : '0'
if (typeof value === 'boolean') {
if (isInArrayOrTuple) {
return value ? 'TRUE' : 'FALSE'
}
return value ? '1' : '0'
}
if (typeof value === 'string') {
let result = ''
for (let i = 0; i < value.length; i++) {
Expand Down Expand Up @@ -46,10 +65,11 @@ export function formatQueryParams({
if (Array.isArray(value)) {
return `[${value
.map((v) =>
formatQueryParams({
formatQueryParamsInternal({
value: v,
wrapStringInQuotes: true,
printNullAsKeyword: true,
isInArrayOrTuple: true,
}),
)
.join(',')}]`
Expand All @@ -70,10 +90,11 @@ export function formatQueryParams({
if (value instanceof TupleParam) {
return `(${value.values
.map((v) =>
formatQueryParams({
formatQueryParamsInternal({
value: v,
wrapStringInQuotes: true,
printNullAsKeyword: true,
isInArrayOrTuple: true,
}),
)
.join(',')})`
Expand All @@ -98,14 +119,16 @@ function formatObjectLikeParam(
const formatted: string[] = []
for (const [key, val] of entries) {
formatted.push(
`${formatQueryParams({
`${formatQueryParamsInternal({
value: key,
wrapStringInQuotes: true,
printNullAsKeyword: true,
})}:${formatQueryParams({
isInArrayOrTuple: true,
})}:${formatQueryParamsInternal({
value: val,
wrapStringInQuotes: true,
printNullAsKeyword: true,
isInArrayOrTuple: true,
})}`,
)
}
Expand Down