From a226c2a705bce1da93ba73f19b8bc456ed50ab89 Mon Sep 17 00:00:00 2001 From: Onyx2406 Date: Sun, 8 Mar 2026 13:11:08 +0530 Subject: [PATCH 01/28] Fix ResultSet.json() race condition on JSONEachRow streams When calling json() on a JSONEachRow result, the method first checks _stream.readableEnded and then calls stream() which checks readableEnded again. If the stream ends between these two checks (common with fast/small responses), stream() throws "Stream has been already consumed" even though this is the first consumption call. Fix by introducing a _consumed boolean flag that is set synchronously when any consumption method (text/json/stream) is called. This eliminates the race window between the two readableEnded checks. The fix splits stream() into a public method (with consumption check) and a private _streamImpl() (without check) that json() calls internally after already marking as consumed. This matches the pattern used by the web client's ResultSet which uses isAlreadyConsumed boolean instead of readableEnded. Fixes #575 --- .../__tests__/unit/node_result_set.test.ts | 17 ++++++++ packages/client-node/src/result_set.ts | 40 +++++++++++++------ 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/packages/client-node/__tests__/unit/node_result_set.test.ts b/packages/client-node/__tests__/unit/node_result_set.test.ts index 7fbf4b00c..977c0c9c1 100644 --- a/packages/client-node/__tests__/unit/node_result_set.test.ts +++ b/packages/client-node/__tests__/unit/node_result_set.test.ts @@ -61,6 +61,23 @@ describe('[Node.js] ResultSet', () => { await expect(rs.text()).rejects.toEqual(err) }) + // Regression test for https://github.com/ClickHouse/clickhouse-js/issues/575 + // json() on JSONEachRow calls stream() internally. If the underlying stream + // ends between json()'s readableEnded check and stream()'s readableEnded check, + // the old code would throw "Stream has been already consumed" on first call. + // The fix uses a boolean flag instead of readableEnded for consumption tracking. + it('should not throw "already consumed" on json() when stream ends quickly', async () => { + // Use a stream that ends immediately (single synchronous chunk) + const rs = makeResultSet( + Readable.from([Buffer.from('{"n":1}\n')]), + ) + // Yield to event loop to allow the stream to potentially end + await new Promise((r) => setImmediate(r)) + // This should NOT throw "Stream has been already consumed" + const result = await rs.json() + expect(result).toEqual([{ n: 1 }]) + }) + it('should be able to call Row.text and Row.json multiple times', async () => { const rs = makeResultSet( Stream.Readable.from([Buffer.from('{"foo":"bar"}\n')]), diff --git a/packages/client-node/src/result_set.ts b/packages/client-node/src/result_set.ts index d78bdd5b2..0905cd24c 100644 --- a/packages/client-node/src/result_set.ts +++ b/packages/client-node/src/result_set.ts @@ -67,6 +67,7 @@ export class ResultSet< private readonly exceptionTag: string | undefined = undefined private readonly log_error: (error: Error) => void private readonly jsonHandling: JSONHandling + private _consumed = false constructor( private _stream: Stream.Readable, @@ -91,23 +92,33 @@ export class ResultSet< } } - /** See {@link BaseResultSet.text}. */ - async text(): Promise { - if (this._stream.readableEnded) { + /** + * Mark the result set as consumed and throw if it was already consumed. + * Uses a boolean flag instead of checking `readableEnded` to avoid a race + * condition where the stream's 'end' event fires between two separate + * `readableEnded` checks (e.g. when `json()` calls `stream()` internally + * for JSONEachRow). See: https://github.com/ClickHouse/clickhouse-js/issues/575 + */ + private markAsConsumed(): void { + if (this._consumed || this._stream.readableEnded) { throw Error(streamAlreadyConsumedMessage) } + this._consumed = true + } + + /** See {@link BaseResultSet.text}. */ + async text(): Promise { + this.markAsConsumed() return (await getAsText(this._stream)).toString() } /** See {@link BaseResultSet.json}. */ async json(): Promise> { - if (this._stream.readableEnded) { - throw Error(streamAlreadyConsumedMessage) - } + this.markAsConsumed() // JSONEachRow, etc. if (isStreamableJSONFamily(this.format as DataFormat)) { const result: T[] = [] - const stream = this.stream() + const stream = this._streamImpl() for await (const rows of stream) { for (const row of rows) { result.push(row.json() as T) @@ -126,12 +137,15 @@ export class ResultSet< /** See {@link BaseResultSet.stream}. */ stream(): ResultStream[]>> { - // If the underlying stream has already ended by calling `text` or `json`, - // Stream.pipeline will create a new empty stream - // but without "readableEnded" flag set to true - if (this._stream.readableEnded) { - throw Error(streamAlreadyConsumedMessage) - } + this.markAsConsumed() + return this._streamImpl() + } + + /** + * Internal stream implementation that skips the consumption check. + * Used by `json()` which has already called `markAsConsumed()`. + */ + private _streamImpl(): ResultStream[]>> { validateStreamFormat(this.format) From 45c89254c783cc60c1ec3649c61a8b8e3ac029a6 Mon Sep 17 00:00:00 2001 From: Onyx2406 Date: Sun, 8 Mar 2026 13:14:04 +0530 Subject: [PATCH 02/28] Fix: don't check readableEnded in markAsConsumed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit readableEnded=true just means the 'end' event fired, NOT that someone already consumed the data. For fast/small responses, the stream can end before json() is even called, making readableEnded=true while data is still buffered and available. Checking readableEnded would reject the first consumption call — exactly the bug reported in #575. Only use the _consumed boolean flag, which tracks actual consumption by our code, not stream lifecycle events. --- packages/client-node/src/result_set.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/client-node/src/result_set.ts b/packages/client-node/src/result_set.ts index 0905cd24c..da2304982 100644 --- a/packages/client-node/src/result_set.ts +++ b/packages/client-node/src/result_set.ts @@ -98,9 +98,14 @@ export class ResultSet< * condition where the stream's 'end' event fires between two separate * `readableEnded` checks (e.g. when `json()` calls `stream()` internally * for JSONEachRow). See: https://github.com/ClickHouse/clickhouse-js/issues/575 + * + * We intentionally do NOT check `readableEnded` here. A stream can have + * `readableEnded=true` (the 'end' event fired) while its data is still + * buffered and available for reading. Checking readableEnded would falsely + * reject the first consumption call for fast/small responses. */ private markAsConsumed(): void { - if (this._consumed || this._stream.readableEnded) { + if (this._consumed) { throw Error(streamAlreadyConsumedMessage) } this._consumed = true From fa4494189b51451e7a701bc1fdb37ef1c917dffb Mon Sep 17 00:00:00 2001 From: Onyx2406 Date: Sun, 8 Mar 2026 13:22:22 +0530 Subject: [PATCH 03/28] Fix stream() marking consumed before format validation, improve test Address Copilot review feedback: 1. Move validateStreamFormat() before markAsConsumed() in stream(). Previously, if the format was invalid, the ResultSet was permanently marked as consumed even though nothing was actually read, preventing a subsequent text() call from working. 2. Make regression test deterministic by overriding readableEnded to always return true, simulating a fast response. The old code would throw on this; the new code only checks the _consumed flag. --- .../__tests__/unit/node_result_set.test.ts | 28 +++++++++++-------- packages/client-node/src/result_set.ts | 10 ++++--- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/packages/client-node/__tests__/unit/node_result_set.test.ts b/packages/client-node/__tests__/unit/node_result_set.test.ts index 977c0c9c1..fc02786a3 100644 --- a/packages/client-node/__tests__/unit/node_result_set.test.ts +++ b/packages/client-node/__tests__/unit/node_result_set.test.ts @@ -62,18 +62,22 @@ describe('[Node.js] ResultSet', () => { }) // Regression test for https://github.com/ClickHouse/clickhouse-js/issues/575 - // json() on JSONEachRow calls stream() internally. If the underlying stream - // ends between json()'s readableEnded check and stream()'s readableEnded check, - // the old code would throw "Stream has been already consumed" on first call. - // The fix uses a boolean flag instead of readableEnded for consumption tracking. - it('should not throw "already consumed" on json() when stream ends quickly', async () => { - // Use a stream that ends immediately (single synchronous chunk) - const rs = makeResultSet( - Readable.from([Buffer.from('{"n":1}\n')]), - ) - // Yield to event loop to allow the stream to potentially end - await new Promise((r) => setImmediate(r)) - // This should NOT throw "Stream has been already consumed" + // The old code used readableEnded to track consumption, which could become + // true before json() is called (for fast/small responses). The fix uses a + // _consumed boolean flag that only our code controls. + it('should succeed on json() even if readableEnded is already true', async () => { + const stream = Readable.from([Buffer.from('{"n":1}\n')]) + + // Force readableEnded=true to deterministically simulate a fast response + // that has already ended before json() is called. + Object.defineProperty(stream, 'readableEnded', { + get: () => true, + configurable: true, + }) + + const rs = makeResultSet(stream) + // Old code would throw "Stream has been already consumed" here + // because it checked readableEnded. New code only checks _consumed. const result = await rs.json() expect(result).toEqual([{ n: 1 }]) }) diff --git a/packages/client-node/src/result_set.ts b/packages/client-node/src/result_set.ts index da2304982..8409a593f 100644 --- a/packages/client-node/src/result_set.ts +++ b/packages/client-node/src/result_set.ts @@ -142,18 +142,20 @@ export class ResultSet< /** See {@link BaseResultSet.stream}. */ stream(): ResultStream[]>> { + // Validate format before marking as consumed, so that if the format is + // invalid, the ResultSet can still be consumed via text() afterwards. + validateStreamFormat(this.format) this.markAsConsumed() return this._streamImpl() } /** - * Internal stream implementation that skips the consumption check. - * Used by `json()` which has already called `markAsConsumed()`. + * Internal stream implementation that skips the consumption check + * and format validation. Used by `json()` which has already called + * `markAsConsumed()` and validated the format via `isStreamableJSONFamily`. */ private _streamImpl(): ResultStream[]>> { - validateStreamFormat(this.format) - const incompleteChunks: Buffer[] = [] const logError = this.log_error const exceptionTag = this.exceptionTag From 8034819144b2e0521d7483b425cd48a61bd3b17b Mon Sep 17 00:00:00 2001 From: Onyx2406 Date: Thu, 19 Mar 2026 23:45:54 +0530 Subject: [PATCH 04/28] Move markAsConsumed into consumption branches in json() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address reviewer feedback from peter-leonov-ch: Move `markAsConsumed()` from the top of `json()` into each branch that actually consumes the stream (streamable JSON and non-streamable JSON). The unsupported-format path (CSV, etc.) no longer marks the ResultSet as consumed, preserving the ability to call `text()` afterwards — matching the pre-PR exception semantics. Add tests verifying: - json() on CSV throws without marking consumed, text() still works - stream() on non-streamable format throws, text() still works --- .../__tests__/unit/node_result_set.test.ts | 26 +++++++++++++++++++ packages/client-node/src/result_set.ts | 4 ++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/client-node/__tests__/unit/node_result_set.test.ts b/packages/client-node/__tests__/unit/node_result_set.test.ts index fc02786a3..9b5c0e1c8 100644 --- a/packages/client-node/__tests__/unit/node_result_set.test.ts +++ b/packages/client-node/__tests__/unit/node_result_set.test.ts @@ -82,6 +82,32 @@ describe('[Node.js] ResultSet', () => { expect(result).toEqual([{ n: 1 }]) }) + // Verify that calling json() on a non-JSON format (e.g. CSV) does not + // permanently mark the ResultSet as consumed — text() should still work. + it('should allow text() after json() throws for unsupported format', async () => { + const rs = makeResultSet( + Stream.Readable.from([Buffer.from('1,"foo"\n')]), + 'CSV', + ) + await expect(rs.json()).rejects.toThrow('Cannot decode CSV as JSON') + // ResultSet should NOT be consumed — text() should still work + const text = await rs.text() + expect(text).toEqual('1,"foo"\n') + }) + + // Verify that calling stream() on a non-streamable format does not + // permanently mark the ResultSet as consumed — text() should still work. + it('should allow text() after stream() throws for invalid format', async () => { + const rs = makeResultSet( + Stream.Readable.from([Buffer.from('{"data":[1,2,3]}')]), + 'JSON', + ) + expect(() => rs.stream()).toThrow() + // ResultSet should NOT be consumed — text() should still work + const text = await rs.text() + expect(text).toEqual('{"data":[1,2,3]}') + }) + it('should be able to call Row.text and Row.json multiple times', async () => { const rs = makeResultSet( Stream.Readable.from([Buffer.from('{"foo":"bar"}\n')]), diff --git a/packages/client-node/src/result_set.ts b/packages/client-node/src/result_set.ts index 8409a593f..bb97c5f68 100644 --- a/packages/client-node/src/result_set.ts +++ b/packages/client-node/src/result_set.ts @@ -119,9 +119,9 @@ export class ResultSet< /** See {@link BaseResultSet.json}. */ async json(): Promise> { - this.markAsConsumed() // JSONEachRow, etc. if (isStreamableJSONFamily(this.format as DataFormat)) { + this.markAsConsumed() const result: T[] = [] const stream = this._streamImpl() for await (const rows of stream) { @@ -133,10 +133,12 @@ export class ResultSet< } // JSON, JSONObjectEachRow, etc. if (isNotStreamableJSONFamily(this.format as DataFormat)) { + this.markAsConsumed() const text = await getAsText(this._stream) return this.jsonHandling.parse(text) } // should not be called for CSV, etc. + // Do NOT mark as consumed here — the caller can still use text() instead. throw new Error(`Cannot decode ${this.format} as JSON`) } From a26ec2e6db46c5145f4533a5ea55212855dc108c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 13:26:49 +0000 Subject: [PATCH 05/28] ci: skip cloud tests for PRs from forks --- .github/workflows/tests.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2d3779452..6e4bc4624 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -337,6 +337,8 @@ jobs: node .scripts/export-coverage-metrics.mjs node-integration-tests-cloud: + # Cloud secrets are not available for PRs from forks; skip the job in that case. + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} timeout-minutes: 5 runs-on: ubuntu-latest strategy: @@ -371,6 +373,8 @@ jobs: node .scripts/export-coverage-metrics.mjs web-integration-tests-cloud: + # Cloud secrets are not available for PRs from forks; skip the job in that case. + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} timeout-minutes: 5 runs-on: ubuntu-latest strategy: @@ -506,7 +510,13 @@ jobs: 'web-integration-tests-cloud', 'web-codecov-upload', ] + # Run even if some needed jobs were skipped (e.g. cloud jobs on PRs from forks), + # but still fail if any of them failed or were cancelled. + if: ${{ always() }} runs-on: ubuntu-latest steps: + - name: Fail if any needed job failed or was cancelled + if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} + run: exit 1 - name: All tests passed run: echo "All tests passed! 🎉" From f5ebc5ff74bf4b3b6362cb7604bab666f14099ec Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 28 May 2026 16:03:25 +0000 Subject: [PATCH 06/28] chore: bump version to 1.20.0 --- package-lock.json | 12 ++++++------ packages/client-common/package.json | 2 +- packages/client-common/src/version.ts | 2 +- packages/client-node/package.json | 4 ++-- packages/client-node/src/version.ts | 2 +- packages/client-web/package.json | 4 ++-- packages/client-web/src/version.ts | 2 +- tests/clickhouse-test-runner/package.json | 2 +- 8 files changed, 15 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 04b927e24..9f8d7f5c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7838,16 +7838,16 @@ }, "packages/client-common": { "name": "@clickhouse/client-common", - "version": "1.19.0", + "version": "1.20.0", "license": "Apache-2.0", "devDependencies": {} }, "packages/client-node": { "name": "@clickhouse/client", - "version": "1.19.0", + "version": "1.20.0", "license": "Apache-2.0", "dependencies": { - "@clickhouse/client-common": "1.19.0" + "@clickhouse/client-common": "1.20.0" }, "devDependencies": { "simdjson": "^0.9.2" @@ -7858,15 +7858,15 @@ }, "packages/client-web": { "name": "@clickhouse/client-web", - "version": "1.19.0", + "version": "1.20.0", "license": "Apache-2.0", "dependencies": { - "@clickhouse/client-common": "1.19.0" + "@clickhouse/client-common": "1.20.0" } }, "tests/clickhouse-test-runner": { "name": "@clickhouse/clickhouse-test-runner", - "version": "1.19.0", + "version": "1.20.0", "dependencies": { "@clickhouse/client": "*" }, diff --git a/packages/client-common/package.json b/packages/client-common/package.json index 679376539..6e0b2516d 100644 --- a/packages/client-common/package.json +++ b/packages/client-common/package.json @@ -2,7 +2,7 @@ "name": "@clickhouse/client-common", "description": "Official JS client for ClickHouse DB - common types", "homepage": "https://clickhouse.com", - "version": "1.19.0", + "version": "1.20.0", "license": "Apache-2.0", "keywords": [ "clickhouse", diff --git a/packages/client-common/src/version.ts b/packages/client-common/src/version.ts index cbb22fa1d..885bab02a 100644 --- a/packages/client-common/src/version.ts +++ b/packages/client-common/src/version.ts @@ -1 +1 @@ -export default '1.19.0' +export default '1.20.0' diff --git a/packages/client-node/package.json b/packages/client-node/package.json index 54e89b8a8..3f265cc08 100644 --- a/packages/client-node/package.json +++ b/packages/client-node/package.json @@ -2,7 +2,7 @@ "name": "@clickhouse/client", "description": "Official JS client for ClickHouse DB - Node.js implementation", "homepage": "https://clickhouse.com", - "version": "1.19.0", + "version": "1.20.0", "license": "Apache-2.0", "keywords": [ "clickhouse", @@ -44,7 +44,7 @@ "build": "rm -rf dist; tsc" }, "dependencies": { - "@clickhouse/client-common": "1.19.0" + "@clickhouse/client-common": "1.20.0" }, "devDependencies": { "simdjson": "^0.9.2" diff --git a/packages/client-node/src/version.ts b/packages/client-node/src/version.ts index cbb22fa1d..885bab02a 100644 --- a/packages/client-node/src/version.ts +++ b/packages/client-node/src/version.ts @@ -1 +1 @@ -export default '1.19.0' +export default '1.20.0' diff --git a/packages/client-web/package.json b/packages/client-web/package.json index 1811bbcb5..026596758 100644 --- a/packages/client-web/package.json +++ b/packages/client-web/package.json @@ -2,7 +2,7 @@ "name": "@clickhouse/client-web", "description": "Official JS client for ClickHouse DB - Web API implementation", "homepage": "https://clickhouse.com", - "version": "1.19.0", + "version": "1.20.0", "license": "Apache-2.0", "keywords": [ "clickhouse", @@ -31,6 +31,6 @@ "build": "rm -rf dist; tsc" }, "dependencies": { - "@clickhouse/client-common": "1.19.0" + "@clickhouse/client-common": "1.20.0" } } diff --git a/packages/client-web/src/version.ts b/packages/client-web/src/version.ts index cbb22fa1d..885bab02a 100644 --- a/packages/client-web/src/version.ts +++ b/packages/client-web/src/version.ts @@ -1 +1 @@ -export default '1.19.0' +export default '1.20.0' diff --git a/tests/clickhouse-test-runner/package.json b/tests/clickhouse-test-runner/package.json index 9a0de798b..e7a667887 100644 --- a/tests/clickhouse-test-runner/package.json +++ b/tests/clickhouse-test-runner/package.json @@ -1,7 +1,7 @@ { "name": "@clickhouse/clickhouse-test-runner", "private": true, - "version": "0.0.0", + "version": "1.20.0", "description": "Node.js port of ClickHouse/clickhouse-java tests/clickhouse-client harness", "engines": { "node": ">=20.19.0" From 457b5a47b88131b60fbb28a76cff5663f9aaef8a Mon Sep 17 00:00:00 2001 From: Peter Leonov Date: Fri, 29 May 2026 12:26:34 +0200 Subject: [PATCH 07/28] unwrap to avoid indirection in test results --- .../integration/node_select_streaming.test.ts | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/client-node/__tests__/integration/node_select_streaming.test.ts b/packages/client-node/__tests__/integration/node_select_streaming.test.ts index 14e3d8e26..1bf133b1c 100644 --- a/packages/client-node/__tests__/integration/node_select_streaming.test.ts +++ b/packages/client-node/__tests__/integration/node_select_streaming.test.ts @@ -13,11 +13,6 @@ describe('[Node.js] SELECT streaming', () => { }) describe('consume the response only once', () => { - async function assertAlreadyConsumed$(fn: () => Promise) { - await expect(fn()).rejects.toMatchObject({ - message: 'Stream has been already consumed', - }) - } function assertAlreadyConsumed(fn: () => T) { expect(fn).toThrow('Stream has been already consumed') } @@ -28,8 +23,12 @@ describe('[Node.js] SELECT streaming', () => { }) expect(await rs.json()).toEqual([{ number: '0' }]) // wrap in a func to avoid changing inner "this" - await assertAlreadyConsumed$(() => rs.json()) - await assertAlreadyConsumed$(() => rs.text()) + await expect(() => rs.json()).rejects.toMatchObject({ + message: 'Stream has been already consumed', + }) + await expect(() => rs.text()).rejects.toMatchObject({ + message: 'Stream has been already consumed', + }) assertAlreadyConsumed(() => rs.stream()) }) @@ -40,8 +39,12 @@ describe('[Node.js] SELECT streaming', () => { }) expect(await rs.text()).toEqual('0\n') // wrap in a func to avoid changing inner "this" - await assertAlreadyConsumed$(() => rs.json()) - await assertAlreadyConsumed$(() => rs.text()) + await expect(() => rs.json()).rejects.toMatchObject({ + message: 'Stream has been already consumed', + }) + await expect(() => rs.text()).rejects.toMatchObject({ + message: 'Stream has been already consumed', + }) assertAlreadyConsumed(() => rs.stream()) }) @@ -58,9 +61,15 @@ describe('[Node.js] SELECT streaming', () => { } expect(result).toEqual('0') // wrap in a func to avoid changing inner "this" - await assertAlreadyConsumed$(() => rs.json()) - await assertAlreadyConsumed$(() => rs.text()) - assertAlreadyConsumed(() => rs.stream()) + await expect(() => rs.json()).rejects.toMatchObject({ + message: 'Stream has been already consumed', + }) + await expect(() => rs.text()).rejects.toMatchObject({ + message: 'Stream has been already consumed', + }) + await expect(() => rs.stream()).rejects.toMatchObject({ + message: 'Stream has been already consumed', + }) }) }) From b6e1397345ac08670cddd1b158f3ce63c8bc5a52 Mon Sep 17 00:00:00 2001 From: Peter Leonov Date: Fri, 29 May 2026 12:30:28 +0200 Subject: [PATCH 08/28] revert for testing --- packages/client-node/src/result_set.ts | 51 +++++++------------------- 1 file changed, 14 insertions(+), 37 deletions(-) diff --git a/packages/client-node/src/result_set.ts b/packages/client-node/src/result_set.ts index bb97c5f68..d78bdd5b2 100644 --- a/packages/client-node/src/result_set.ts +++ b/packages/client-node/src/result_set.ts @@ -67,7 +67,6 @@ export class ResultSet< private readonly exceptionTag: string | undefined = undefined private readonly log_error: (error: Error) => void private readonly jsonHandling: JSONHandling - private _consumed = false constructor( private _stream: Stream.Readable, @@ -92,38 +91,23 @@ export class ResultSet< } } - /** - * Mark the result set as consumed and throw if it was already consumed. - * Uses a boolean flag instead of checking `readableEnded` to avoid a race - * condition where the stream's 'end' event fires between two separate - * `readableEnded` checks (e.g. when `json()` calls `stream()` internally - * for JSONEachRow). See: https://github.com/ClickHouse/clickhouse-js/issues/575 - * - * We intentionally do NOT check `readableEnded` here. A stream can have - * `readableEnded=true` (the 'end' event fired) while its data is still - * buffered and available for reading. Checking readableEnded would falsely - * reject the first consumption call for fast/small responses. - */ - private markAsConsumed(): void { - if (this._consumed) { - throw Error(streamAlreadyConsumedMessage) - } - this._consumed = true - } - /** See {@link BaseResultSet.text}. */ async text(): Promise { - this.markAsConsumed() + if (this._stream.readableEnded) { + throw Error(streamAlreadyConsumedMessage) + } return (await getAsText(this._stream)).toString() } /** See {@link BaseResultSet.json}. */ async json(): Promise> { + if (this._stream.readableEnded) { + throw Error(streamAlreadyConsumedMessage) + } // JSONEachRow, etc. if (isStreamableJSONFamily(this.format as DataFormat)) { - this.markAsConsumed() const result: T[] = [] - const stream = this._streamImpl() + const stream = this.stream() for await (const rows of stream) { for (const row of rows) { result.push(row.json() as T) @@ -133,30 +117,23 @@ export class ResultSet< } // JSON, JSONObjectEachRow, etc. if (isNotStreamableJSONFamily(this.format as DataFormat)) { - this.markAsConsumed() const text = await getAsText(this._stream) return this.jsonHandling.parse(text) } // should not be called for CSV, etc. - // Do NOT mark as consumed here — the caller can still use text() instead. throw new Error(`Cannot decode ${this.format} as JSON`) } /** See {@link BaseResultSet.stream}. */ stream(): ResultStream[]>> { - // Validate format before marking as consumed, so that if the format is - // invalid, the ResultSet can still be consumed via text() afterwards. - validateStreamFormat(this.format) - this.markAsConsumed() - return this._streamImpl() - } + // If the underlying stream has already ended by calling `text` or `json`, + // Stream.pipeline will create a new empty stream + // but without "readableEnded" flag set to true + if (this._stream.readableEnded) { + throw Error(streamAlreadyConsumedMessage) + } - /** - * Internal stream implementation that skips the consumption check - * and format validation. Used by `json()` which has already called - * `markAsConsumed()` and validated the format via `isStreamableJSONFamily`. - */ - private _streamImpl(): ResultStream[]>> { + validateStreamFormat(this.format) const incompleteChunks: Buffer[] = [] const logError = this.log_error From 01c6cf4833441487fc967028c071a6636596b5ce Mon Sep 17 00:00:00 2001 From: Peter Leonov Date: Fri, 29 May 2026 12:38:01 +0200 Subject: [PATCH 09/28] construct the error --- packages/client-node/src/result_set.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client-node/src/result_set.ts b/packages/client-node/src/result_set.ts index d78bdd5b2..2cca0ca29 100644 --- a/packages/client-node/src/result_set.ts +++ b/packages/client-node/src/result_set.ts @@ -130,7 +130,7 @@ export class ResultSet< // Stream.pipeline will create a new empty stream // but without "readableEnded" flag set to true if (this._stream.readableEnded) { - throw Error(streamAlreadyConsumedMessage) + throw new Error(streamAlreadyConsumedMessage) } validateStreamFormat(this.format) From f260259ba1f306e7c9c77f72891b32fbd16cbd71 Mon Sep 17 00:00:00 2001 From: Peter Leonov Date: Fri, 29 May 2026 12:39:14 +0200 Subject: [PATCH 10/28] use proper matchers --- .../integration/node_select_streaming.test.ts | 56 ++++++++++--------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/packages/client-node/__tests__/integration/node_select_streaming.test.ts b/packages/client-node/__tests__/integration/node_select_streaming.test.ts index 1bf133b1c..eb32bac70 100644 --- a/packages/client-node/__tests__/integration/node_select_streaming.test.ts +++ b/packages/client-node/__tests__/integration/node_select_streaming.test.ts @@ -23,13 +23,15 @@ describe('[Node.js] SELECT streaming', () => { }) expect(await rs.json()).toEqual([{ number: '0' }]) // wrap in a func to avoid changing inner "this" - await expect(() => rs.json()).rejects.toMatchObject({ - message: 'Stream has been already consumed', - }) - await expect(() => rs.text()).rejects.toMatchObject({ - message: 'Stream has been already consumed', - }) - assertAlreadyConsumed(() => rs.stream()) + await expect(async () => rs.json()).rejects.toThrow( + /Stream has been already consumed/, + ) + await expect(async () => rs.text()).rejects.toThrow( + /Stream has been already consumed/, + ) + await expect(async () => rs.stream()).rejects.toThrow( + /Stream has been already consumed/, + ) }) it('should consume a text response only once', async () => { @@ -39,13 +41,15 @@ describe('[Node.js] SELECT streaming', () => { }) expect(await rs.text()).toEqual('0\n') // wrap in a func to avoid changing inner "this" - await expect(() => rs.json()).rejects.toMatchObject({ - message: 'Stream has been already consumed', - }) - await expect(() => rs.text()).rejects.toMatchObject({ - message: 'Stream has been already consumed', - }) - assertAlreadyConsumed(() => rs.stream()) + await expect(async () => rs.json()).rejects.toThrow( + /Stream has been already consumed/, + ) + await expect(async () => rs.text()).rejects.toThrow( + /Stream has been already consumed/, + ) + await expect(async () => rs.stream()).rejects.toThrow( + /Stream has been already consumed/, + ) }) it('should consume a stream response only once', async () => { @@ -61,15 +65,15 @@ describe('[Node.js] SELECT streaming', () => { } expect(result).toEqual('0') // wrap in a func to avoid changing inner "this" - await expect(() => rs.json()).rejects.toMatchObject({ - message: 'Stream has been already consumed', - }) - await expect(() => rs.text()).rejects.toMatchObject({ - message: 'Stream has been already consumed', - }) - await expect(() => rs.stream()).rejects.toMatchObject({ - message: 'Stream has been already consumed', - }) + await expect(async () => rs.json()).rejects.toThrow( + /Stream has been already consumed/, + ) + await expect(async () => rs.text()).rejects.toThrow( + /Stream has been already consumed/, + ) + await expect(async () => rs.stream()).rejects.toThrow( + /Stream has been already consumed/, + ) }) }) @@ -80,9 +84,9 @@ describe('[Node.js] SELECT streaming', () => { format: 'JSON', }) try { - await expect(async () => result.stream()).rejects.toMatchObject({ - message: expect.stringContaining('JSON format is not streamable'), - }) + await expect(async () => result.stream()).rejects.toThrow( + /JSON format is not streamable/, + ) } finally { result.close() } From 0452839c1e054a684d71e83e677bdb7365f769c7 Mon Sep 17 00:00:00 2001 From: Peter Leonov Date: Fri, 29 May 2026 13:49:53 +0200 Subject: [PATCH 11/28] extra --- packages/client-node/src/result_set.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client-node/src/result_set.ts b/packages/client-node/src/result_set.ts index 2cca0ca29..bd506140d 100644 --- a/packages/client-node/src/result_set.ts +++ b/packages/client-node/src/result_set.ts @@ -96,7 +96,7 @@ export class ResultSet< if (this._stream.readableEnded) { throw Error(streamAlreadyConsumedMessage) } - return (await getAsText(this._stream)).toString() + return await getAsText(this._stream) } /** See {@link BaseResultSet.json}. */ From 8fa0c9d6c3a3688571742c127bfd9efc20e98b6b Mon Sep 17 00:00:00 2001 From: Peter Leonov Date: Fri, 29 May 2026 13:59:47 +0200 Subject: [PATCH 12/28] explain expectations --- packages/client-node/src/result_set.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/client-node/src/result_set.ts b/packages/client-node/src/result_set.ts index bd506140d..951bb7023 100644 --- a/packages/client-node/src/result_set.ts +++ b/packages/client-node/src/result_set.ts @@ -69,6 +69,12 @@ export class ResultSet< private readonly jsonHandling: JSONHandling constructor( + /** + * The stream of the response body. + * + * It is expected that the stream is passed directly from the response of the HTTP request + * and has not been consumed or altered yet. + */ private _stream: Stream.Readable, private readonly format: Format, public readonly query_id: string, From 5c28df879ca42cab78ae65a9e155b8f792d71423 Mon Sep 17 00:00:00 2001 From: Peter Leonov Date: Fri, 29 May 2026 17:17:52 +0200 Subject: [PATCH 13/28] shield the stream though consume() method --- packages/client-node/src/result_set.ts | 35 +++++++++++++------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/packages/client-node/src/result_set.ts b/packages/client-node/src/result_set.ts index 951bb7023..c38e8289c 100644 --- a/packages/client-node/src/result_set.ts +++ b/packages/client-node/src/result_set.ts @@ -67,6 +67,7 @@ export class ResultSet< private readonly exceptionTag: string | undefined = undefined private readonly log_error: (error: Error) => void private readonly jsonHandling: JSONHandling + private _consumed = false constructor( /** @@ -97,33 +98,38 @@ export class ResultSet< } } + private consume() { + if (this._consumed) { + throw new Error(streamAlreadyConsumedMessage) + } + this._consumed = true + return this._stream + } + /** See {@link BaseResultSet.text}. */ async text(): Promise { - if (this._stream.readableEnded) { - throw Error(streamAlreadyConsumedMessage) - } - return await getAsText(this._stream) + return await getAsText(this.consume()) } /** See {@link BaseResultSet.json}. */ async json(): Promise> { - if (this._stream.readableEnded) { - throw Error(streamAlreadyConsumedMessage) - } // JSONEachRow, etc. if (isStreamableJSONFamily(this.format as DataFormat)) { const result: T[] = [] + // Using the stream() instead of _stream directly to leverage the existing logic + // for handling incomplete chunks and exception tags. + // TODO: consider using stream() for all formats to unify the logic and error handling. const stream = this.stream() for await (const rows of stream) { for (const row of rows) { result.push(row.json() as T) } } - return result as any + return result as ResultJSONType } // JSON, JSONObjectEachRow, etc. if (isNotStreamableJSONFamily(this.format as DataFormat)) { - const text = await getAsText(this._stream) + const text = await getAsText(this.consume()) return this.jsonHandling.parse(text) } // should not be called for CSV, etc. @@ -132,13 +138,6 @@ export class ResultSet< /** See {@link BaseResultSet.stream}. */ stream(): ResultStream[]>> { - // If the underlying stream has already ended by calling `text` or `json`, - // Stream.pipeline will create a new empty stream - // but without "readableEnded" flag set to true - if (this._stream.readableEnded) { - throw new Error(streamAlreadyConsumedMessage) - } - validateStreamFormat(this.format) const incompleteChunks: Buffer[] = [] @@ -203,7 +202,7 @@ export class ResultSet< }) const pipeline = Stream.pipeline( - this._stream, + this.consume(), toRows, function pipelineCb(err) { if ( @@ -220,7 +219,7 @@ export class ResultSet< /** See {@link BaseResultSet.close}. */ close() { - this._stream.destroy(new Error(resultSetClosedMessage)) + this.consume().destroy(new Error(resultSetClosedMessage)) } /** From cbe0d13cdb8c0904e6e4c9b2e46bfab89c35526c Mon Sep 17 00:00:00 2001 From: Peter Leonov Date: Fri, 29 May 2026 17:22:54 +0200 Subject: [PATCH 14/28] adapt the tests --- .../integration/node_select_streaming.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/client-node/__tests__/integration/node_select_streaming.test.ts b/packages/client-node/__tests__/integration/node_select_streaming.test.ts index eb32bac70..ecd24f36c 100644 --- a/packages/client-node/__tests__/integration/node_select_streaming.test.ts +++ b/packages/client-node/__tests__/integration/node_select_streaming.test.ts @@ -37,9 +37,9 @@ describe('[Node.js] SELECT streaming', () => { it('should consume a text response only once', async () => { const rs = await client.query({ query: 'SELECT * FROM system.numbers LIMIT 1', - format: 'TabSeparated', + format: 'JSONEachRow', }) - expect(await rs.text()).toEqual('0\n') + expect(await rs.text()).toEqual('{"number":"0"}\n') // wrap in a func to avoid changing inner "this" await expect(async () => rs.json()).rejects.toThrow( /Stream has been already consumed/, @@ -55,7 +55,7 @@ describe('[Node.js] SELECT streaming', () => { it('should consume a stream response only once', async () => { const rs = await client.query({ query: 'SELECT * FROM system.numbers LIMIT 1', - format: 'TabSeparated', + format: 'JSONEachRow', }) let result = '' for await (const rows of rs.stream()) { @@ -63,7 +63,7 @@ describe('[Node.js] SELECT streaming', () => { result += row.text }) } - expect(result).toEqual('0') + expect(result).toEqual('{"number":"0"}') // wrap in a func to avoid changing inner "this" await expect(async () => rs.json()).rejects.toThrow( /Stream has been already consumed/, @@ -77,7 +77,7 @@ describe('[Node.js] SELECT streaming', () => { }) }) - describe('select result asStream()', () => { + describe('select result as stream()', () => { it('throws an exception if format is not stream-able', async () => { const result = await client.query({ query: 'SELECT number FROM system.numbers LIMIT 5', From fc4b8d8e878803f20e8ae5cb8d0cd921dcfa350a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 20:23:35 +0000 Subject: [PATCH 15/28] ci: add copilot-setup-steps using medium-runner-ubuntu-x64 --- .github/workflows/copilot-setup-steps.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/copilot-setup-steps.yml diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 000000000..ae7beb2e5 --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,18 @@ +name: "Copilot Setup Steps" + +# Automatically run the setup steps when they are changed to allow for easy validation, +# and allow manual testing through the repository's "Actions" tab. +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + pull_request: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. + copilot-setup-steps: + runs-on: medium-runner-ubuntu-x64 + permissions: {} From 23255c54d25960b099c0792f0853f2ebedbf2bb0 Mon Sep 17 00:00:00 2001 From: Peter Leonov Date: Fri, 29 May 2026 22:44:53 +0200 Subject: [PATCH 16/28] for the future --- .../node_stream_error_handling.test.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/client-node/__tests__/integration/node_stream_error_handling.test.ts b/packages/client-node/__tests__/integration/node_stream_error_handling.test.ts index ce01af2b7..50b059049 100644 --- a/packages/client-node/__tests__/integration/node_stream_error_handling.test.ts +++ b/packages/client-node/__tests__/integration/node_stream_error_handling.test.ts @@ -74,4 +74,40 @@ describe('[Node.js] Stream error handling', () => { assertError(caughtError) }) + + it.skip('with .json()', async ({ skip }) => { + if (!(await isClickHouseVersionAtLeast(client, 25, 11))) { + skip() + } + + let caughtError: ClickHouseError | null = null + + try { + const queryParams = streamErrorQueryParams() + const rs = await client.query(queryParams) + await rs.json() + } catch (err) { + caughtError = err as ClickHouseError + } + + assertError(caughtError) + }) + + it.skip('with .text()', async ({ skip }) => { + if (!(await isClickHouseVersionAtLeast(client, 25, 11))) { + skip() + } + + let caughtError: ClickHouseError | null = null + + try { + const queryParams = streamErrorQueryParams() + const rs = await client.query(queryParams) + await rs.text() + } catch (err) { + caughtError = err as ClickHouseError + } + + assertError(caughtError) + }) }) From 97181db0530fa0ffe5bf82db3594a6e6093f9210 Mon Sep 17 00:00:00 2001 From: Peter Leonov Date: Fri, 29 May 2026 23:08:17 +0200 Subject: [PATCH 17/28] Update packages/client-node/src/result_set.ts facepalm.jpg --- packages/client-node/src/result_set.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client-node/src/result_set.ts b/packages/client-node/src/result_set.ts index c38e8289c..5f426275a 100644 --- a/packages/client-node/src/result_set.ts +++ b/packages/client-node/src/result_set.ts @@ -219,7 +219,7 @@ export class ResultSet< /** See {@link BaseResultSet.close}. */ close() { - this.consume().destroy(new Error(resultSetClosedMessage)) + this._stream.destroy(new Error(resultSetClosedMessage)) } /** From a0c85265b40dfd2283664465280054fc2b953013 Mon Sep 17 00:00:00 2001 From: Peter Leonov Date: Fri, 29 May 2026 22:50:45 +0200 Subject: [PATCH 18/28] unwrap --- .../integration/node_select_streaming.test.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/client-node/__tests__/integration/node_select_streaming.test.ts b/packages/client-node/__tests__/integration/node_select_streaming.test.ts index ecd24f36c..6142c3860 100644 --- a/packages/client-node/__tests__/integration/node_select_streaming.test.ts +++ b/packages/client-node/__tests__/integration/node_select_streaming.test.ts @@ -23,13 +23,13 @@ describe('[Node.js] SELECT streaming', () => { }) expect(await rs.json()).toEqual([{ number: '0' }]) // wrap in a func to avoid changing inner "this" - await expect(async () => rs.json()).rejects.toThrow( + await expect(rs.json()).rejects.toThrow( /Stream has been already consumed/, ) - await expect(async () => rs.text()).rejects.toThrow( + await expect(rs.text()).rejects.toThrow( /Stream has been already consumed/, ) - await expect(async () => rs.stream()).rejects.toThrow( + await expect(rs.stream()).rejects.toThrow( /Stream has been already consumed/, ) }) @@ -41,13 +41,13 @@ describe('[Node.js] SELECT streaming', () => { }) expect(await rs.text()).toEqual('{"number":"0"}\n') // wrap in a func to avoid changing inner "this" - await expect(async () => rs.json()).rejects.toThrow( + await expect(rs.json()).rejects.toThrow( /Stream has been already consumed/, ) - await expect(async () => rs.text()).rejects.toThrow( + await expect(rs.text()).rejects.toThrow( /Stream has been already consumed/, ) - await expect(async () => rs.stream()).rejects.toThrow( + await expect(rs.stream()).rejects.toThrow( /Stream has been already consumed/, ) }) @@ -65,13 +65,13 @@ describe('[Node.js] SELECT streaming', () => { } expect(result).toEqual('{"number":"0"}') // wrap in a func to avoid changing inner "this" - await expect(async () => rs.json()).rejects.toThrow( + await expect(rs.json()).rejects.toThrow( /Stream has been already consumed/, ) - await expect(async () => rs.text()).rejects.toThrow( + await expect(rs.text()).rejects.toThrow( /Stream has been already consumed/, ) - await expect(async () => rs.stream()).rejects.toThrow( + await expect(rs.stream()).rejects.toThrow( /Stream has been already consumed/, ) }) @@ -84,7 +84,7 @@ describe('[Node.js] SELECT streaming', () => { format: 'JSON', }) try { - await expect(async () => result.stream()).rejects.toThrow( + await expect(result.stream()).rejects.toThrow( /JSON format is not streamable/, ) } finally { From 24e4a0577df6a9a7b4e5f2398e3c7a066aa1d37e Mon Sep 17 00:00:00 2001 From: Peter Leonov Date: Fri, 29 May 2026 23:16:39 +0200 Subject: [PATCH 19/28] wrap :) --- .../__tests__/integration/node_select_streaming.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/client-node/__tests__/integration/node_select_streaming.test.ts b/packages/client-node/__tests__/integration/node_select_streaming.test.ts index 6142c3860..4c61a6e0f 100644 --- a/packages/client-node/__tests__/integration/node_select_streaming.test.ts +++ b/packages/client-node/__tests__/integration/node_select_streaming.test.ts @@ -29,7 +29,7 @@ describe('[Node.js] SELECT streaming', () => { await expect(rs.text()).rejects.toThrow( /Stream has been already consumed/, ) - await expect(rs.stream()).rejects.toThrow( + await expect(async () => rs.stream()).rejects.toThrow( /Stream has been already consumed/, ) }) @@ -47,7 +47,7 @@ describe('[Node.js] SELECT streaming', () => { await expect(rs.text()).rejects.toThrow( /Stream has been already consumed/, ) - await expect(rs.stream()).rejects.toThrow( + await expect(async () => rs.stream()).rejects.toThrow( /Stream has been already consumed/, ) }) @@ -71,7 +71,7 @@ describe('[Node.js] SELECT streaming', () => { await expect(rs.text()).rejects.toThrow( /Stream has been already consumed/, ) - await expect(rs.stream()).rejects.toThrow( + await expect(async () => rs.stream()).rejects.toThrow( /Stream has been already consumed/, ) }) @@ -84,7 +84,7 @@ describe('[Node.js] SELECT streaming', () => { format: 'JSON', }) try { - await expect(result.stream()).rejects.toThrow( + await expect(async () => result.stream()).rejects.toThrow( /JSON format is not streamable/, ) } finally { From 40c1d6036f07df342e2f5b03ba824e4da971d3f5 Mon Sep 17 00:00:00 2001 From: Peter Leonov Date: Fri, 29 May 2026 23:23:39 +0200 Subject: [PATCH 20/28] Update .github/workflows/copilot-setup-steps.yml --- .github/workflows/copilot-setup-steps.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index ae7beb2e5..ef00f25e9 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -1,4 +1,5 @@ name: "Copilot Setup Steps" +permissions: {} # Automatically run the setup steps when they are changed to allow for easy validation, # and allow manual testing through the repository's "Actions" tab. From 0d06d197543d5392d92f13ee83e0e79361220874 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 23:55:59 +0200 Subject: [PATCH 21/28] ci(publish): gate npm publish jobs on npm-publish environment (#771) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Restrict npm publishing to a protected GitHub environment so release approvals/secrets can be gated centrally. - Add `environment: npm-publish` to the `head` and `latest` jobs in `.github/workflows/publish.yml`. - `e2e` is left unscoped — it only installs from the public registry. - No other changes needed: OIDC auth and `id-token: write` permissions remain at the job level. ## Checklist - [x] A human-readable description of the changes was provided to include in CHANGELOG CHANGELOG: Publish jobs now run under the `npm-publish` GitHub environment to enforce release protection rules. Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Peter Leonov --- .github/workflows/publish.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 71f9241e9..2fe3d8451 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -44,6 +44,7 @@ jobs: head: if: github.ref == 'refs/heads/release' && github.event_name == 'push' runs-on: ubuntu-latest + environment: npm-publish outputs: version: ${{ steps.version.outputs.version }} steps: @@ -81,6 +82,7 @@ jobs: latest: if: github.ref == 'refs/heads/release' && github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest + environment: npm-publish permissions: contents: write # Required to push the release git tag id-token: write # Required for npm OIDC authentication and provenance From ecccb2f69c541f84dad982c9c381b84e6897f1c5 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 30 May 2026 00:11:45 +0200 Subject: [PATCH 22/28] Fix splitQueries to skip SQL comments in upstream test runner (#762) ## Summary Upstream test `03262_system_functions_should_not_fill_query_log_functions` failed with `Syntax error (Multi-statements are not allowed)`. The `.sql` file begins with a `--` comment ending in `test's purpose.`; `splitQueries` treated the apostrophe as the start of a string literal, so subsequent `;` separators were ignored and all three statements were sent as one. - `tests/clickhouse-test-runner/src/split-queries.ts`: when not inside a quoted string, skip `-- ...` line comments (to end of line) and `/* ... */` block comments verbatim, so apostrophes/semicolons inside comments don't affect splitting. - `tests/clickhouse-test-runner/__tests__/split-queries.test.ts`: added cases for apostrophes/semicolons embedded in both line and block comments. ```sql -- defeat the test's purpose. SELECT 1; SYSTEM FLUSH LOGS query_log; SELECT 2; ``` Previously split into 1 statement; now correctly split into 3. ## Checklist - [x] Unit and integration tests covering the common scenarios were added - [x] A human-readable description of the changes was provided to include in CHANGELOG Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Peter Leonov --- .../__tests__/split-queries.test.ts | 18 ++++++++++++++++ .../src/split-queries.ts | 21 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/tests/clickhouse-test-runner/__tests__/split-queries.test.ts b/tests/clickhouse-test-runner/__tests__/split-queries.test.ts index 1cc72a946..189cfbb4e 100644 --- a/tests/clickhouse-test-runner/__tests__/split-queries.test.ts +++ b/tests/clickhouse-test-runner/__tests__/split-queries.test.ts @@ -56,4 +56,22 @@ describe('splitQueries', () => { 'SELECT 2', ]) }) + + it('ignores apostrophes and semicolons inside line comments', () => { + const sql = + "-- defeat the test's purpose; really\nSELECT 1;\nSYSTEM FLUSH LOGS query_log;\nSELECT 2" + expect(splitQueries(sql)).toEqual([ + "-- defeat the test's purpose; really\nSELECT 1", + 'SYSTEM FLUSH LOGS query_log', + 'SELECT 2', + ]) + }) + + it('ignores apostrophes and semicolons inside block comments', () => { + const sql = "/* it's a; trap */ SELECT 1; SELECT 2" + expect(splitQueries(sql)).toEqual([ + "/* it's a; trap */ SELECT 1", + 'SELECT 2', + ]) + }) }) diff --git a/tests/clickhouse-test-runner/src/split-queries.ts b/tests/clickhouse-test-runner/src/split-queries.ts index afe09b1f8..029d46009 100644 --- a/tests/clickhouse-test-runner/src/split-queries.ts +++ b/tests/clickhouse-test-runner/src/split-queries.ts @@ -22,6 +22,27 @@ export function splitQueries(sql: string): string[] { continue } + // Skip SQL comments when not inside a quoted string so that apostrophes + // or semicolons embedded in comments do not affect statement splitting. + if (!inSingleQuote && !inDoubleQuote && !inBacktick) { + // Line comment: -- ... until end of line + if (ch === '-' && sql.charAt(i + 1) === '-') { + const newlineIdx = sql.indexOf('\n', i + 2) + const end = newlineIdx === -1 ? sql.length : newlineIdx + current += sql.slice(i, end) + i = end - 1 + continue + } + // Block comment: /* ... */ + if (ch === '/' && sql.charAt(i + 1) === '*') { + const closeIdx = sql.indexOf('*/', i + 2) + const end = closeIdx === -1 ? sql.length : closeIdx + 2 + current += sql.slice(i, end) + i = end - 1 + continue + } + } + if (!inDoubleQuote && !inBacktick && ch === "'") { inSingleQuote = !inSingleQuote current += ch From 297045c5148c121e60feb3647a1d59f5e439ff08 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 30 May 2026 00:22:55 +0200 Subject: [PATCH 23/28] docs: add 1.20.0 changelog entry (#773) ## Summary Document user-facing changes shipped since `1.19.0` under a new `1.20.0` section in `CHANGELOG.md`. - **Bug Fixes** - (Node.js) `ResultSet.json()` / `stream()` race on `JSONEachRow` where a fast response ending between `readableEnded` checks threw `Stream has been already consumed`. Consumption is now funneled through a single `consume()` path that marks the result set consumed in the appropriate branches, after format validation (#603). - **Improvements** - Re-export `ResponseHeaders` from `@clickhouse/client` and `@clickhouse/client-web`, continuing the move toward making `@clickhouse/client-common` internal-only (#758). Other commits in the `1.19.0..HEAD` range are CI/infra/test-only (publish env gating, copilot-setup-steps, fork cloud-test skip, test runner `splitQueries` fix, version bump, test-comment typo) and intentionally omitted. ## Checklist - [x] A human-readable description of the changes was provided to include in CHANGELOG Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5de493b45..f54bb5333 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# 1.20.0 + +## Bug Fixes + +- (Node.js only) Fixed a race condition in `ResultSet.json()` and `ResultSet.stream()` on `JSONEachRow` (and other streamable) result sets where calling `json()` on a fast/small response could throw `Stream has been already consumed` if the underlying stream ended between internal `readableEnded` checks. The consumption guard has been hardened: the stream is now shielded through a single `consume()` path that marks the result set as consumed in the appropriate branches, after format validation, so a successful `json()` call no longer races against the stream finishing. ([#603]) + +## Improvements + +- Re-exported the `ResponseHeaders` type from `@clickhouse/client` and `@clickhouse/client-web`. Previously this type was only available from `@clickhouse/client-common`; it is now part of the public re-export surface of both flavored packages, alongside the other commonly used types. This is part of an ongoing effort to make `@clickhouse/client-common` an internal-only package so downstream consumers can depend solely on `@clickhouse/client` or `@clickhouse/client-web`. ([#758]) + +[#603]: https://github.com/ClickHouse/clickhouse-js/pull/603 +[#758]: https://github.com/ClickHouse/clickhouse-js/pull/758 + # 1.19.0 ## Breaking Changes From 2762d0f2b6d40f5edf17ef7187a1d9f8f5d86520 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 30 May 2026 00:57:54 +0200 Subject: [PATCH 24/28] Pin Playwright to ^1.60.0 to fix CI install hang (#774) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary `playwright install` hangs indefinitely on Playwright `< 1.60.0` (microsoft/playwright#40998), causing GitHub Actions to cancel jobs during the Chromium download. Playwright was only present transitively as a peer dependency of `@vitest/browser-playwright`, resolving to `1.57.0` (root) and `1.59.1` (web example) — both affected. Changes: - **Pin Playwright** — add `playwright: "^1.60.0"` as a direct devDependency in `package.json` and `examples/web/package.json`, giving explicit version control over the previously transitive peer dep. - **Lockfiles** — regenerate `package-lock.json` and `examples/web/package-lock.json`; `playwright`/`playwright-core` now resolve to `1.60.0`. - **CI workflows** — no change needed; `.github/workflows/*` invoke `npx playwright install` with no pinned version, so they pick up the new resolved version automatically. No test logic or other dependencies were modified. ## Checklist - [x] A human-readable description of the changes was provided to include in CHANGELOG Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- examples/web/package-lock.json | 18 ++++++++---------- examples/web/package.json | 1 + package-lock.json | 18 ++++++++---------- package.json | 1 + 4 files changed, 18 insertions(+), 20 deletions(-) diff --git a/examples/web/package-lock.json b/examples/web/package-lock.json index ab45b3c99..0764982b4 100644 --- a/examples/web/package-lock.json +++ b/examples/web/package-lock.json @@ -17,6 +17,7 @@ "eslint-config-prettier": "^10.1.8", "eslint-plugin-expect-type": "^0.6.2", "eslint-plugin-prettier": "^5.5.4", + "playwright": "^1.60.0", "tsx": "^4.21.0", "typescript": "^5.9.3", "typescript-eslint": "^8.46.4", @@ -2959,14 +2960,13 @@ } }, "node_modules/playwright": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", - "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { - "playwright-core": "1.59.1" + "playwright-core": "1.60.0" }, "bin": { "playwright": "cli.js" @@ -2979,12 +2979,11 @@ } }, "node_modules/playwright-core": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", - "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "playwright-core": "cli.js" }, @@ -3003,7 +3002,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } diff --git a/examples/web/package.json b/examples/web/package.json index 25c385d75..3e35f2da6 100644 --- a/examples/web/package.json +++ b/examples/web/package.json @@ -25,6 +25,7 @@ "eslint-config-prettier": "^10.1.8", "eslint-plugin-expect-type": "^0.6.2", "eslint-plugin-prettier": "^5.5.4", + "playwright": "^1.60.0", "tsx": "^4.21.0", "typescript": "^5.9.3", "typescript-eslint": "^8.46.4", diff --git a/package-lock.json b/package-lock.json index 9f8d7f5c8..b8006e1cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "jsonwebtoken": "^9.0.3", "lint-staged": "^16.4.0", "parquet-wasm": "0.7.1", + "playwright": "^1.60.0", "prettier": "3.8.1", "split2": "^4.2.0", "typescript": "^5.9.3", @@ -6592,14 +6593,13 @@ } }, "node_modules/playwright": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", - "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { - "playwright-core": "1.57.0" + "playwright-core": "1.60.0" }, "bin": { "playwright": "cli.js" @@ -6612,12 +6612,11 @@ } }, "node_modules/playwright-core": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", - "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "playwright-core": "cli.js" }, @@ -6636,7 +6635,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } diff --git a/package.json b/package.json index ab83b0bc1..c17c63018 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "jsonwebtoken": "^9.0.3", "lint-staged": "^16.4.0", "parquet-wasm": "0.7.1", + "playwright": "^1.60.0", "prettier": "3.8.1", "split2": "^4.2.0", "typescript": "^5.9.3", From 89a48727a846ec1a6dd1a9348f6ea65554c60860 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 30 May 2026 01:13:50 +0200 Subject: [PATCH 25/28] Refactoring test client to eliminate ClickHouse dependency (#775) Pull request created by AI Agent Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- packages/client-common/__tests__/README.md | 13 ++++ .../__tests__/unit/client.test.ts | 9 +++ .../client-common/__tests__/utils/client.ts | 63 +++++++++---------- .../client-common/__tests__/utils/index.ts | 1 + .../__tests__/utils/simple_client.ts | 47 ++++++++++++++ .../__tests__/unit/node_client.test.ts | 9 +++ .../__tests__/utils/simple_node_client.ts | 12 ++++ .../__tests__/unit/web_client.test.ts | 9 +++ .../__tests__/utils/simple_web_client.ts | 12 ++++ 9 files changed, 142 insertions(+), 33 deletions(-) create mode 100644 packages/client-common/__tests__/utils/simple_client.ts create mode 100644 packages/client-node/__tests__/utils/simple_node_client.ts create mode 100644 packages/client-web/__tests__/utils/simple_web_client.ts diff --git a/packages/client-common/__tests__/README.md b/packages/client-common/__tests__/README.md index 2626153d3..6904ce75a 100644 --- a/packages/client-common/__tests__/README.md +++ b/packages/client-common/__tests__/README.md @@ -2,3 +2,16 @@ This folder contains unit and integration test scenarios that we expect to be compatible to every connection, as well as the shared utilities for effective tests writing. + +#### Test client utilities + +For integration tests that need a running ClickHouse instance, use `createTestClient()` (or the platform +wrappers `createNodeTestClient()` / `createWebTestClient()`). These connect to the configured test environment +(local single node, local cluster, or cloud), apply environment-specific settings, and rely on the shared +`beforeAll` initializer registered in `utils/client.ts`. + +For unit tests that must be runnable **without** a reachable ClickHouse instance, use `createSimpleTestClient()` +from `utils/simple_client.ts` (or the platform wrappers `createSimpleNodeTestClient()` / +`createSimpleWebTestClient()`). This factory lives in a side-effect-free module: importing it never registers the +shared `beforeAll` test-environment initializer and it does not read any connection details from the environment, +so no ClickHouse server is required as long as the test does not issue an actual request. diff --git a/packages/client-common/__tests__/unit/client.test.ts b/packages/client-common/__tests__/unit/client.test.ts index e9c8e2679..68ff44ce4 100644 --- a/packages/client-common/__tests__/unit/client.test.ts +++ b/packages/client-common/__tests__/unit/client.test.ts @@ -1,6 +1,7 @@ import { vi, describe, it, expect } from 'vitest' import { sleep } from '../utils/sleep' import { ClickHouseClient } from '../../src/client' +import { createSimpleTestClient } from '../utils/simple_client' function isAwaitUsingStatementSupported(): boolean { try { @@ -27,6 +28,14 @@ function mockImpl(): any { } describe('client', () => { + it('createSimpleTestClient creates a client without requiring ClickHouse', async () => { + // Imported from the side-effect-free `simple_client` module, so it does not + // register the shared `beforeAll` test-env init and needs no ClickHouse. + const client = createSimpleTestClient() + expect(client).toBeDefined() + await client.close() + }) + it.skipIf(!isAwaitUsingStatementSupported())( 'closes the client when used with using statement', async () => { diff --git a/packages/client-common/__tests__/utils/client.ts b/packages/client-common/__tests__/utils/client.ts index 3bad67544..b0ae31fce 100644 --- a/packages/client-common/__tests__/utils/client.ts +++ b/packages/client-common/__tests__/utils/client.ts @@ -1,13 +1,13 @@ /* eslint @typescript-eslint/no-var-requires: 0 */ import { beforeAll } from 'vitest' import { - ClickHouseLogLevel, type BaseClickHouseClientConfigOptions, type ClickHouseClient, type ClickHouseSettings, } from '@clickhouse/client-common' import { EnvKeys, getFromEnv } from './env' import { guid } from './guid' +import { createSimpleTestClient, getTestLogConfig } from './simple_client' import { getClickHouseTestEnvironment, isCloudTestEnv, @@ -15,34 +15,41 @@ import { SKIP_INIT, TestEnv, } from './test_env' -import { TestLogger } from './test_logger' -let databaseName: string -beforeAll(async () => { - if (SKIP_INIT) { - // it will be skipped for unit tests that don't require DB setup - console.log('\nSkipping test environment initialization') - return - } +export { createSimpleTestClient } - console.log( - `\nTest environment: ${getClickHouseTestEnvironment()}, database: ${ - databaseName ?? 'default' - }`, - ) - const initClient = createTestClient({ - request_timeout: 10_000, +let databaseName: string +// Only register the shared test-environment initializer when it is actually +// needed. Skipping the registration entirely (instead of returning early from +// the hook) ensures that importing this module never couples a test suite to a +// reachable ClickHouse instance when init is skipped. +if (!SKIP_INIT) { + beforeAll(async () => { + console.log( + `\nTest environment: ${getClickHouseTestEnvironment()}, database: ${ + databaseName ?? 'default' + }`, + ) + const initClient = createTestClient({ + request_timeout: 10_000, + }) + if (isCloudTestEnv() && databaseName === undefined) { + await wakeUpPing(initClient) + databaseName = await createRandomDatabase(initClient) + } + await initClient.close() }) - if (isCloudTestEnv() && databaseName === undefined) { - await wakeUpPing(initClient) - databaseName = await createRandomDatabase(initClient) - } - await initClient.close() -}) +} export function createTestClient( config: BaseClickHouseClientConfigOptions = {}, ): ClickHouseClient { + // When the shared test-environment init is skipped, there is no ClickHouse + // instance to talk to; fall back to a client that requires no server. + if (SKIP_INIT) { + return createSimpleTestClient(config) + } + const env = getClickHouseTestEnvironment() const clickHouseSettings: ClickHouseSettings = { // (U)Int64 are not quoted by default since 25.8 @@ -55,17 +62,7 @@ export function createTestClient( } // Allow to override `insert_quorum` if necessary Object.assign(clickHouseSettings, config?.clickhouse_settings || {}) - const level = - config.log?.level ?? - (!process.env.LOG_LEVEL || process.env.LOG_LEVEL === 'undefined' - ? undefined - : ClickHouseLogLevel[ - process.env.LOG_LEVEL as keyof typeof ClickHouseLogLevel - ]) - const log: BaseClickHouseClientConfigOptions['log'] = { - LoggerClass: TestLogger, - level, - } + const log = getTestLogConfig(config) if (isCloudTestEnv()) { return (globalThis as any).environmentSpecificCreateClient({ diff --git a/packages/client-common/__tests__/utils/index.ts b/packages/client-common/__tests__/utils/index.ts index f15f8a367..fa599e873 100644 --- a/packages/client-common/__tests__/utils/index.ts +++ b/packages/client-common/__tests__/utils/index.ts @@ -1,6 +1,7 @@ export { TestLogger } from './test_logger' export { createTestClient, + createSimpleTestClient, createRandomDatabase, createTable, getTestDatabaseName, diff --git a/packages/client-common/__tests__/utils/simple_client.ts b/packages/client-common/__tests__/utils/simple_client.ts new file mode 100644 index 000000000..b73eeac4d --- /dev/null +++ b/packages/client-common/__tests__/utils/simple_client.ts @@ -0,0 +1,47 @@ +import { + ClickHouseLogLevel, + type BaseClickHouseClientConfigOptions, + type ClickHouseClient, +} from '@clickhouse/client-common' +import { TestLogger } from './test_logger' + +/** + * Resolves the test logger configuration based on the provided config and the + * `LOG_LEVEL` environment variable. Shared between {@link createSimpleTestClient} + * and the environment-aware `createTestClient`. + */ +export function getTestLogConfig( + config: BaseClickHouseClientConfigOptions = {}, +): BaseClickHouseClientConfigOptions['log'] { + const level = + config.log?.level ?? + (!process.env.LOG_LEVEL || process.env.LOG_LEVEL === 'undefined' + ? undefined + : ClickHouseLogLevel[ + process.env.LOG_LEVEL as keyof typeof ClickHouseLogLevel + ]) + return { + LoggerClass: TestLogger, + level, + } +} + +/** + * Creates a test client that does NOT require a running ClickHouse instance. + * + * Unlike `createTestClient`, this factory lives in its own module that does not + * register the shared `beforeAll` test-environment initializer and does not read + * any ClickHouse connection details from the environment. Importing it therefore + * never pulls in the shared test-env init, which makes it safe to use from unit + * tests that must be runnable without a reachable ClickHouse instance. + * + * No network request is performed unless the test explicitly issues one. + */ +export function createSimpleTestClient( + config: BaseClickHouseClientConfigOptions = {}, +): ClickHouseClient { + return (globalThis as any).environmentSpecificCreateClient({ + log: getTestLogConfig(config), + ...config, + }) as ClickHouseClient +} diff --git a/packages/client-node/__tests__/unit/node_client.test.ts b/packages/client-node/__tests__/unit/node_client.test.ts index 7906bed01..e10bf9376 100644 --- a/packages/client-node/__tests__/unit/node_client.test.ts +++ b/packages/client-node/__tests__/unit/node_client.test.ts @@ -16,8 +16,17 @@ import { } from '../../src/connection' import { sleep } from '../utils/sleep' import { isAwaitUsingStatementSupported } from '../utils/feature_detection' +import { createSimpleNodeTestClient } from '../utils/simple_node_client' describe('[Node.js] createClient', () => { + it('createSimpleNodeTestClient creates a client without requiring ClickHouse', async () => { + // Imported from the side-effect-free `simple_node_client` module, so it does + // not register the shared `beforeAll` test-env init and needs no ClickHouse. + const client = createSimpleNodeTestClient() + expect(client).toBeDefined() + await client.close() + }) + it('throws on incorrect "url" config value', () => { expect(() => createClient({ url: 'foobar' })).toThrow( expect.objectContaining({ diff --git a/packages/client-node/__tests__/utils/simple_node_client.ts b/packages/client-node/__tests__/utils/simple_node_client.ts new file mode 100644 index 000000000..a84de9eb7 --- /dev/null +++ b/packages/client-node/__tests__/utils/simple_node_client.ts @@ -0,0 +1,12 @@ +// Import directly from the side-effect-free module (not from `@test/utils`) +// so that creating a simple client never registers the shared `beforeAll` +// test-environment initializer and stays runnable without ClickHouse. +import { createSimpleTestClient } from '@test/utils/simple_client' +import type Stream from 'stream' +import type { ClickHouseClient, ClickHouseClientConfigOptions } from '../../src' + +export function createSimpleNodeTestClient( + config: ClickHouseClientConfigOptions = {}, +): ClickHouseClient { + return createSimpleTestClient(config) as ClickHouseClient +} diff --git a/packages/client-web/__tests__/unit/web_client.test.ts b/packages/client-web/__tests__/unit/web_client.test.ts index 527a9bd52..b6455319f 100644 --- a/packages/client-web/__tests__/unit/web_client.test.ts +++ b/packages/client-web/__tests__/unit/web_client.test.ts @@ -3,8 +3,17 @@ import type { BaseClickHouseClientConfigOptions } from '@clickhouse/client-commo import { createClient } from '../../src' import { isAwaitUsingStatementSupported } from '../utils/feature_detection' import { sleep } from '../utils/sleep' +import { createSimpleWebTestClient } from '../utils/simple_web_client' describe('[Web] createClient', () => { + it('createSimpleWebTestClient creates a client without requiring ClickHouse', async () => { + // Imported from the side-effect-free `simple_web_client` module, so it does + // not register the shared `beforeAll` test-env init and needs no ClickHouse. + const client = createSimpleWebTestClient() + expect(client).toBeDefined() + await client.close() + }) + it('throws on incorrect "url" config value', () => { expect(() => createClient({ url: 'foo' })).toThrow( expect.objectContaining({ diff --git a/packages/client-web/__tests__/utils/simple_web_client.ts b/packages/client-web/__tests__/utils/simple_web_client.ts new file mode 100644 index 000000000..a318da2f8 --- /dev/null +++ b/packages/client-web/__tests__/utils/simple_web_client.ts @@ -0,0 +1,12 @@ +// Import directly from the side-effect-free module (not from `@test/utils`) +// so that creating a simple client never registers the shared `beforeAll` +// test-environment initializer and stays runnable without ClickHouse. +import { createSimpleTestClient } from '@test/utils/simple_client' +import type { ClickHouseClientConfigOptions } from '../../src' +import type { WebClickHouseClient } from '../../src/client' + +export function createSimpleWebTestClient( + config: ClickHouseClientConfigOptions = {}, +): WebClickHouseClient { + return createSimpleTestClient(config) as unknown as WebClickHouseClient +} From 373d1953684ffe83a8a374fb5759383c6bebbc85 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 30 May 2026 01:46:07 +0200 Subject: [PATCH 26/28] Move ResponseHeaders re-export note from 1.20.0 to 1.19.0 changelog (#784) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 1.20.0 changelog entry claimed `ResponseHeaders` was newly re-exported from `@clickhouse/client` and `@clickhouse/client-web`, but those re-exports already exist on the current entrypoints and are not modified by this release — the note belongs to 1.19.0. ## Summary - Removed the `ResponseHeaders` re-export bullet from the 1.20.0 section of `CHANGELOG.md`; 1.20.0 now lists only the `ResultSet.json()`/`stream()` race-condition fix that is actually shipped here. - Added the same bullet under 1.19.0 (where the re-export was introduced), along with its `[#758]` link reference. ## Checklist - [x] A human-readable description of the changes was provided to include in CHANGELOG Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f54bb5333..171dc0cdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,15 +4,16 @@ - (Node.js only) Fixed a race condition in `ResultSet.json()` and `ResultSet.stream()` on `JSONEachRow` (and other streamable) result sets where calling `json()` on a fast/small response could throw `Stream has been already consumed` if the underlying stream ended between internal `readableEnded` checks. The consumption guard has been hardened: the stream is now shielded through a single `consume()` path that marks the result set as consumed in the appropriate branches, after format validation, so a successful `json()` call no longer races against the stream finishing. ([#603]) +[#603]: https://github.com/ClickHouse/clickhouse-js/pull/603 + +# 1.19.0 + ## Improvements - Re-exported the `ResponseHeaders` type from `@clickhouse/client` and `@clickhouse/client-web`. Previously this type was only available from `@clickhouse/client-common`; it is now part of the public re-export surface of both flavored packages, alongside the other commonly used types. This is part of an ongoing effort to make `@clickhouse/client-common` an internal-only package so downstream consumers can depend solely on `@clickhouse/client` or `@clickhouse/client-web`. ([#758]) -[#603]: https://github.com/ClickHouse/clickhouse-js/pull/603 [#758]: https://github.com/ClickHouse/clickhouse-js/pull/758 -# 1.19.0 - ## Breaking Changes - **Enum type parsing now correctly unescapes backslash escape sequences in enum names.** Previously, `parseEnumType` returned enum names with raw escape sequences (e.g., `f\'` instead of `f'`). Now it properly decodes escape sequences including `\'` (single quote), `\\` (backslash), `\n` (newline), `\t` (tab), and `\r` (carriage return). This matches the behavior of ClickHouse string literals and ensures consistency with how the client encodes strings when sending data to the server. If you were relying on the previous incorrect behavior where backslash escape sequences were preserved in enum names, you will need to update your code to handle properly unescaped values. From f4c4ec193c34896957ec15c7e9aa2ba4fec60aa3 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 31 May 2026 20:40:57 +0200 Subject: [PATCH 27/28] upstream-sql-tests: disable 5 allowlist entries failing on released ClickHouse (#787) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Reran the upstream SQL test harness after merging `main`. All 3152 allowlisted tests pass against `clickhouse:head`; against `clickhouse:latest` (26.5.1.882) 5 tests fail because upstream test files on master reference server features not yet shipped in the released image. These are upstream-vs-server version skew, not client bugs, so they are commented out in `upstream-allowlist.txt` per the file's documented "remove or comment out with a reason" policy. - **Setting / column not in released CH** - `03701_limit_by_in_order` — uses setting `query_plan_push_limit_by_into_sort` - `03643_system_instrumentation_no_suspicious_lowcardinality` — uses `system.instrumentation.arguments` - **Join-optimizer reference output drift between master and `latest`** - `01031_pmj_new_any_semi_join` - `01031_semi_anti_join` - `03211_convert_outer_join_to_inner_join_anti_join` Each entry is preserved (commented out) with a one-line reason so it can be re-enabled once the released server catches up. ## Checklist - [x] A human-readable description of the changes was provided to include in CHANGELOG Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- .../clickhouse-test-runner/upstream-allowlist.txt | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/clickhouse-test-runner/upstream-allowlist.txt b/tests/clickhouse-test-runner/upstream-allowlist.txt index 0c5be6436..6a3a5feb4 100644 --- a/tests/clickhouse-test-runner/upstream-allowlist.txt +++ b/tests/clickhouse-test-runner/upstream-allowlist.txt @@ -671,8 +671,10 @@ 01029_early_constant_folding 01030_concatenate_equal_fixed_strings 01030_final_mark_empty_primary_key -01031_pmj_new_any_semi_join -01031_semi_anti_join +# 01031_pmj_new_any_semi_join: reference output drifts on released CH (`latest`) vs upstream master due to join optimizer changes; passes on `head`. +# 01031_pmj_new_any_semi_join +# 01031_semi_anti_join: same upstream version-skew as above (join optimizer reference drift on `latest`). +# 01031_semi_anti_join 01032_cityHash64_for_UUID 01032_cityHash64_for_decimal 01034_order_by_pk_prefix @@ -2269,7 +2271,8 @@ 03210_lag_lead_inframe_types 03210_nested_short_circuit_functions_bug 03210_variant_with_aggregate_function_type -03211_convert_outer_join_to_inner_join_anti_join +# 03211_convert_outer_join_to_inner_join_anti_join: reference output drifts on released CH (`latest`) vs upstream master due to join optimizer changes; passes on `head`. +# 03211_convert_outer_join_to_inner_join_anti_join 03213_array_element_msan 03213_denseRank_percentRank_alias 03214_join_on_tuple_comparison_elimination_bug @@ -2687,7 +2690,8 @@ 03641_json_array_of_float_and_bool 03642_column_ttl_sparse 03643_paste_join_disable_filter_pushdown -03643_system_instrumentation_no_suspicious_lowcardinality +# 03643_system_instrumentation_no_suspicious_lowcardinality: requires `system.instrumentation.arguments` column not yet in released CH (`latest`); passes on `head`. +# 03643_system_instrumentation_no_suspicious_lowcardinality 03644_explain_indices 03644_join_order_mixed_comma_and_left 03644_min_level_for_wide_part @@ -2712,7 +2716,8 @@ 03699_reverse_utf8 03700_vertical_format_pretty_print_json 03701_distinct_but_no_group_by_projection_table_use_check -03701_limit_by_in_order +# 03701_limit_by_in_order: uses setting `query_plan_push_limit_by_into_sort` not present in released CH (`latest`); passes on `head`. +# 03701_limit_by_in_order 03702_encode_decode_memory_usage 03702_json_datetime_format_settings 03702_optimize_inverse_dictionary_lookup_composite_and_layouts From 7d0627f91f4c0f2268a6ece100fbbddfdf2a5237 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 31 May 2026 21:08:09 +0200 Subject: [PATCH 28/28] Soft-deprecate runtime value re-exports from @clickhouse/client-common (#783) ## Summary First step toward deprecating `@clickhouse/client-common` so the platform packages can evolve their class hierarchies and types without being constrained by a shared base. Runtime values re-exported from `@clickhouse/client-common` are annotated with `@deprecated` JSDoc, while platform packages remain the recommended non-deprecated import path. Type-only exports are untouched. This PR also includes a follow-up fix from review feedback: the `ClickHouseClient` deprecation message in `packages/client-common/src/index.ts` was corrected to avoid implying a runtime import from `@clickhouse/client-web` (which exports `ClickHouseClient` as a type). The guidance now points to: - runtime imports from `@clickhouse/client` - type-only Web usage via `import type { ClickHouseClient } from '@clickhouse/client-web'` ### CHANGELOG entry Importing runtime values (`ClickHouseError`, `ClickHouseLogLevel`, `parseError`, `SettingsMap`, `TupleParam`, `isRow`, `isProgressRow`, `isException`, `parseColumnType`, `SimpleColumnTypes`, `defaultJSONHandling`, and the format-name constants) from `@clickhouse/client-common` is now deprecated; import them from `@clickhouse/client` or `@clickhouse/client-web` instead. For `ClickHouseClient`, use `@clickhouse/client` for runtime imports and `import type` from `@clickhouse/client-web` in Web projects. No runtime behavior change. ## Checklist - [ ] Unit and integration tests covering the common scenarios were added - [x] A human-readable description of the changes was provided to include in CHANGELOG - [ ] For significant changes, documentation in https://github.com/ClickHouse/clickhouse-docs was updated with further explanations or tutorials --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- .github/workflows/copilot-setup-steps.yml | 2 +- packages/client-common/src/client.ts | 3 +- packages/client-common/src/index.ts | 29 +++++++++- packages/client-node/src/index.ts | 66 +++++++++++++++++------ packages/client-web/src/index.ts | 66 +++++++++++++++++------ 5 files changed, 130 insertions(+), 36 deletions(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index ef00f25e9..7ea12897e 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -1,4 +1,4 @@ -name: "Copilot Setup Steps" +name: 'Copilot Setup Steps' permissions: {} # Automatically run the setup steps when they are changed to allow for easy validation, diff --git a/packages/client-common/src/client.ts b/packages/client-common/src/client.ts index 4c4ccb23f..2c6c14c8a 100644 --- a/packages/client-common/src/client.ts +++ b/packages/client-common/src/client.ts @@ -10,7 +10,8 @@ import type { WithResponseHeaders, DataFormat, } from './index' -import { defaultJSONHandling, DefaultLogger, ClickHouseLogLevel } from './index' +import { defaultJSONHandling } from './parse' +import { DefaultLogger, ClickHouseLogLevel } from './logger' import type { InsertValues, NonEmptyArray, diff --git a/packages/client-common/src/index.ts b/packages/client-common/src/index.ts index 59e039a3a..da15239e4 100644 --- a/packages/client-common/src/index.ts +++ b/packages/client-common/src/index.ts @@ -5,6 +5,7 @@ export { type QueryResult, type ExecParams, type InsertParams, + /** @deprecated Import `ClickHouseClient` from `@clickhouse/client` instead. In Web projects, use `import type { ClickHouseClient } from '@clickhouse/client-web'`. Importing it from `@clickhouse/client-common` is deprecated. */ ClickHouseClient, type CommandParams, type CommandResult, @@ -33,16 +34,29 @@ export type { SingleDocumentJSONFormat, } from './data_formatter' export { + /** @deprecated Import `SupportedJSONFormats` from `@clickhouse/client` (Node.js) or `@clickhouse/client-web` (Web) instead. Importing it from `@clickhouse/client-common` is deprecated. */ SupportedJSONFormats, + /** @deprecated Import `SupportedRawFormats` from `@clickhouse/client` (Node.js) or `@clickhouse/client-web` (Web) instead. Importing it from `@clickhouse/client-common` is deprecated. */ SupportedRawFormats, + /** @deprecated Import `StreamableFormats` from `@clickhouse/client` (Node.js) or `@clickhouse/client-web` (Web) instead. Importing it from `@clickhouse/client-common` is deprecated. */ StreamableFormats, + /** @deprecated Import `StreamableJSONFormats` from `@clickhouse/client` (Node.js) or `@clickhouse/client-web` (Web) instead. Importing it from `@clickhouse/client-common` is deprecated. */ StreamableJSONFormats, + /** @deprecated Import `SingleDocumentJSONFormats` from `@clickhouse/client` (Node.js) or `@clickhouse/client-web` (Web) instead. Importing it from `@clickhouse/client-common` is deprecated. */ SingleDocumentJSONFormats, + /** @deprecated Import `RecordsJSONFormats` from `@clickhouse/client` (Node.js) or `@clickhouse/client-web` (Web) instead. Importing it from `@clickhouse/client-common` is deprecated. */ RecordsJSONFormats, + /** @deprecated Import `TupleParam` from `@clickhouse/client` (Node.js) or `@clickhouse/client-web` (Web) instead. Importing it from `@clickhouse/client-common` is deprecated. */ TupleParam, } from './data_formatter' -export { ClickHouseError, parseError } from './error' export { + /** @deprecated Import `ClickHouseError` from `@clickhouse/client` (Node.js) or `@clickhouse/client-web` (Web) instead. Importing it from `@clickhouse/client-common` is deprecated. */ + ClickHouseError, + /** @deprecated Import `parseError` from `@clickhouse/client` (Node.js) or `@clickhouse/client-web` (Web) instead. Importing it from `@clickhouse/client-common` is deprecated. */ + parseError, +} from './error' +export { + /** @deprecated Import `ClickHouseLogLevel` from `@clickhouse/client` (Node.js) or `@clickhouse/client-web` (Web) instead. Importing it from `@clickhouse/client-common` is deprecated. */ ClickHouseLogLevel, type ErrorLogParams, type WarnLogParams, @@ -63,10 +77,18 @@ export type { ClickHouseJWTAuth, ClickHouseCredentialsAuth, } from './clickhouse_types' -export { isProgressRow, isRow, isException } from './clickhouse_types' +export { + /** @deprecated Import `isProgressRow` from `@clickhouse/client` (Node.js) or `@clickhouse/client-web` (Web) instead. Importing it from `@clickhouse/client-common` is deprecated. */ + isProgressRow, + /** @deprecated Import `isRow` from `@clickhouse/client` (Node.js) or `@clickhouse/client-web` (Web) instead. Importing it from `@clickhouse/client-common` is deprecated. */ + isRow, + /** @deprecated Import `isException` from `@clickhouse/client` (Node.js) or `@clickhouse/client-web` (Web) instead. Importing it from `@clickhouse/client-common` is deprecated. */ + isException, +} from './clickhouse_types' export { type ClickHouseSettings, type MergeTreeSettings, + /** @deprecated Import `SettingsMap` from `@clickhouse/client` (Node.js) or `@clickhouse/client-web` (Web) instead. Importing it from `@clickhouse/client-common` is deprecated. */ SettingsMap, } from './settings' export type { @@ -85,8 +107,11 @@ export type { JSONHandling, } from './parse' export { + /** @deprecated Import `SimpleColumnTypes` from `@clickhouse/client` (Node.js) or `@clickhouse/client-web` (Web) instead. Importing it from `@clickhouse/client-common` is deprecated. */ SimpleColumnTypes, + /** @deprecated Import `parseColumnType` from `@clickhouse/client` (Node.js) or `@clickhouse/client-web` (Web) instead. Importing it from `@clickhouse/client-common` is deprecated. */ parseColumnType, + /** @deprecated Import `defaultJSONHandling` from `@clickhouse/client` (Node.js) or `@clickhouse/client-web` (Web) instead. Importing it from `@clickhouse/client-common` is deprecated. */ defaultJSONHandling, } from './parse' diff --git a/packages/client-node/src/index.ts b/packages/client-node/src/index.ts index a194e389d..5166afecf 100644 --- a/packages/client-node/src/index.ts +++ b/packages/client-node/src/index.ts @@ -38,16 +38,6 @@ export { type BaseResultSet, type PingResult, type ResponseHeaders, - ClickHouseError, - parseError, - ClickHouseLogLevel, - SettingsMap, - SupportedJSONFormats, - SupportedRawFormats, - StreamableFormats, - StreamableJSONFormats, - SingleDocumentJSONFormats, - RecordsJSONFormats, type SimpleColumnType, type ParsedColumnSimple, type ParsedColumnEnum, @@ -60,15 +50,59 @@ export { type ParsedColumnTuple, type ParsedColumnMap, type ParsedColumnType, - parseColumnType, - SimpleColumnTypes, type ProgressRow, - isProgressRow, - isRow, - isException, type RowOrProgress, type ClickHouseAuth, type ClickHouseJWTAuth, type ClickHouseCredentialsAuth, - TupleParam, } from '@clickhouse/client-common' + +/** + * Re-export @clickhouse/client-common runtime values. + * + * These are intentionally re-exported through local bindings (rather than a direct + * `export { ... } from '@clickhouse/client-common'`) so that the `@deprecated` JSDoc tags + * applied to them in `@clickhouse/client-common` are NOT propagated to consumers of this package. + * Importing these values from `@clickhouse/client` is the recommended, non-deprecated path. + */ +import { + ClickHouseError as ClickHouseError_, + parseError as parseError_, + ClickHouseLogLevel as ClickHouseLogLevel_, + SettingsMap as SettingsMap_, + SupportedJSONFormats as SupportedJSONFormats_, + SupportedRawFormats as SupportedRawFormats_, + StreamableFormats as StreamableFormats_, + StreamableJSONFormats as StreamableJSONFormats_, + SingleDocumentJSONFormats as SingleDocumentJSONFormats_, + RecordsJSONFormats as RecordsJSONFormats_, + parseColumnType as parseColumnType_, + SimpleColumnTypes as SimpleColumnTypes_, + isProgressRow as isProgressRow_, + isRow as isRow_, + isException as isException_, + TupleParam as TupleParam_, + defaultJSONHandling as defaultJSONHandling_, +} from '@clickhouse/client-common' + +export const ClickHouseError = ClickHouseError_ +export type ClickHouseError = ClickHouseError_ +export const parseError = parseError_ +export const ClickHouseLogLevel = ClickHouseLogLevel_ +export type ClickHouseLogLevel = ClickHouseLogLevel_ +export const SettingsMap = SettingsMap_ +export type SettingsMap = SettingsMap_ +export const SupportedJSONFormats = SupportedJSONFormats_ +export const SupportedRawFormats = SupportedRawFormats_ +export const StreamableFormats = StreamableFormats_ +export const StreamableJSONFormats = StreamableJSONFormats_ +export const SingleDocumentJSONFormats = SingleDocumentJSONFormats_ +export const RecordsJSONFormats = RecordsJSONFormats_ +export const parseColumnType = parseColumnType_ +export const SimpleColumnTypes = SimpleColumnTypes_ +export const isProgressRow = isProgressRow_ +export const isRow = isRow_ +export const isException = isException_ +export const TupleParam = TupleParam_ +export type TupleParam = TupleParam_ +export const defaultJSONHandling = defaultJSONHandling_ diff --git a/packages/client-web/src/index.ts b/packages/client-web/src/index.ts index 0d10f26ca..e39831c1b 100644 --- a/packages/client-web/src/index.ts +++ b/packages/client-web/src/index.ts @@ -37,16 +37,6 @@ export { type BaseResultSet, type PingResult, type ResponseHeaders, - ClickHouseError, - parseError, - ClickHouseLogLevel, - SettingsMap, - SupportedJSONFormats, - SupportedRawFormats, - StreamableFormats, - StreamableJSONFormats, - SingleDocumentJSONFormats, - RecordsJSONFormats, type SimpleColumnType, type ParsedColumnSimple, type ParsedColumnEnum, @@ -59,15 +49,59 @@ export { type ParsedColumnTuple, type ParsedColumnMap, type ParsedColumnType, - parseColumnType, - SimpleColumnTypes, type ProgressRow, - isProgressRow, - isRow, - isException, type RowOrProgress, type ClickHouseAuth, type ClickHouseJWTAuth, type ClickHouseCredentialsAuth, - TupleParam, } from '@clickhouse/client-common' + +/** + * Re-export @clickhouse/client-common runtime values. + * + * These are intentionally re-exported through local bindings (rather than a direct + * `export { ... } from '@clickhouse/client-common'`) so that the `@deprecated` JSDoc tags + * applied to them in `@clickhouse/client-common` are NOT propagated to consumers of this package. + * Importing these values from `@clickhouse/client-web` is the recommended, non-deprecated path. + */ +import { + ClickHouseError as ClickHouseError_, + parseError as parseError_, + ClickHouseLogLevel as ClickHouseLogLevel_, + SettingsMap as SettingsMap_, + SupportedJSONFormats as SupportedJSONFormats_, + SupportedRawFormats as SupportedRawFormats_, + StreamableFormats as StreamableFormats_, + StreamableJSONFormats as StreamableJSONFormats_, + SingleDocumentJSONFormats as SingleDocumentJSONFormats_, + RecordsJSONFormats as RecordsJSONFormats_, + parseColumnType as parseColumnType_, + SimpleColumnTypes as SimpleColumnTypes_, + isProgressRow as isProgressRow_, + isRow as isRow_, + isException as isException_, + TupleParam as TupleParam_, + defaultJSONHandling as defaultJSONHandling_, +} from '@clickhouse/client-common' + +export const ClickHouseError = ClickHouseError_ +export type ClickHouseError = ClickHouseError_ +export const parseError = parseError_ +export const ClickHouseLogLevel = ClickHouseLogLevel_ +export type ClickHouseLogLevel = ClickHouseLogLevel_ +export const SettingsMap = SettingsMap_ +export type SettingsMap = SettingsMap_ +export const SupportedJSONFormats = SupportedJSONFormats_ +export const SupportedRawFormats = SupportedRawFormats_ +export const StreamableFormats = StreamableFormats_ +export const StreamableJSONFormats = StreamableJSONFormats_ +export const SingleDocumentJSONFormats = SingleDocumentJSONFormats_ +export const RecordsJSONFormats = RecordsJSONFormats_ +export const parseColumnType = parseColumnType_ +export const SimpleColumnTypes = SimpleColumnTypes_ +export const isProgressRow = isProgressRow_ +export const isRow = isRow_ +export const isException = isException_ +export const TupleParam = TupleParam_ +export type TupleParam = TupleParam_ +export const defaultJSONHandling = defaultJSONHandling_