diff --git a/CHANGELOG.md b/CHANGELOG.md index 44fe38bc..3f616092 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 ) ## Bug fixes diff --git a/packages/client-common/__tests__/unit/format_query_params.test.ts b/packages/client-common/__tests__/unit/format_query_params.test.ts index cedf0e56..d69161e4 100644 --- a/packages/client-common/__tests__/unit/format_query_params.test.ts +++ b/packages/client-common/__tests__/unit/format_query_params.test.ts @@ -1,4 +1,4 @@ -import { formatQueryParams } from '@clickhouse/client-common' +import { formatQueryParams, TupleParam } from '@clickhouse/client-common' describe('formatQueryParams', () => { it('formats null', () => { @@ -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)}") + }) }) 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 0e270a20..6efc6ea3 100644 --- a/packages/client-common/src/data_formatter/format_query_params.ts +++ b/packages/client-common/src/data_formatter/format_query_params.ts @@ -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' @@ -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++) { @@ -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(',')}]` @@ -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(',')})` @@ -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, })}`, ) }