diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 000000000..7ea12897e --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,19 @@ +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. +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: {} 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 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! 🎉" diff --git a/CHANGELOG.md b/CHANGELOG.md index 5de493b45..171dc0cdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ +# 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]) + +[#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]) + +[#758]: https://github.com/ClickHouse/clickhouse-js/pull/758 + ## 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. 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 04b927e24..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" } @@ -7838,16 +7836,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 +7856,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/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", 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-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/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-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/__tests__/integration/node_select_streaming.test.ts b/packages/client-node/__tests__/integration/node_select_streaming.test.ts index 14e3d8e26..4c61a6e0f 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,27 +23,39 @@ 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()) - assertAlreadyConsumed(() => rs.stream()) + await expect(rs.json()).rejects.toThrow( + /Stream has been already consumed/, + ) + await expect(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 () => { 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 assertAlreadyConsumed$(() => rs.json()) - await assertAlreadyConsumed$(() => rs.text()) - assertAlreadyConsumed(() => rs.stream()) + await expect(rs.json()).rejects.toThrow( + /Stream has been already consumed/, + ) + await expect(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 () => { 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()) { @@ -56,24 +63,30 @@ 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 assertAlreadyConsumed$(() => rs.json()) - await assertAlreadyConsumed$(() => rs.text()) - assertAlreadyConsumed(() => rs.stream()) + await expect(rs.json()).rejects.toThrow( + /Stream has been already consumed/, + ) + await expect(rs.text()).rejects.toThrow( + /Stream has been already consumed/, + ) + await expect(async () => rs.stream()).rejects.toThrow( + /Stream has been already consumed/, + ) }) }) - 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', 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() } 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) + }) }) 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__/unit/node_result_set.test.ts b/packages/client-node/__tests__/unit/node_result_set.test.ts index 7fbf4b00c..9b5c0e1c8 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,53 @@ describe('[Node.js] ResultSet', () => { await expect(rs.text()).rejects.toEqual(err) }) + // Regression test for https://github.com/ClickHouse/clickhouse-js/issues/575 + // 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 }]) + }) + + // 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/__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-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/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-node/src/result_set.ts b/packages/client-node/src/result_set.ts index d78bdd5b2..5f426275a 100644 --- a/packages/client-node/src/result_set.ts +++ b/packages/client-node/src/result_set.ts @@ -67,8 +67,15 @@ export class ResultSet< private readonly exceptionTag: string | undefined = undefined private readonly log_error: (error: Error) => void private readonly jsonHandling: JSONHandling + private _consumed = false 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, @@ -91,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)).toString() + 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. @@ -126,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 Error(streamAlreadyConsumedMessage) - } - validateStreamFormat(this.format) const incompleteChunks: Buffer[] = [] @@ -197,7 +202,7 @@ export class ResultSet< }) const pipeline = Stream.pipeline( - this._stream, + this.consume(), toRows, function pipelineCb(err) { if ( 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/__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 +} 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/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_ 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/__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/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" 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 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