From 3fea961500088870c5cc3637b299c50cd4040f8a Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 08:32:33 +0200 Subject: [PATCH 01/20] Disable @typescript-eslint/ban-ts-comment globally (#808) ## Summary The `@typescript-eslint/ban-ts-comment` rule was only disabled for test files via `testFilesOverrides`, leaving it enabled for source files. This disables it globally. - **`eslint.config.base.mjs`**: add `'@typescript-eslint/ban-ts-comment': 'off'` to the shared `typescriptEslintConfig` rules. ## Checklist - [ ] 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> --- eslint.config.base.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint.config.base.mjs b/eslint.config.base.mjs index 7735b344..ffe45771 100644 --- a/eslint.config.base.mjs +++ b/eslint.config.base.mjs @@ -23,6 +23,7 @@ export function typescriptEslintConfig(root) { '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/consistent-type-imports': 'warn', '@typescript-eslint/array-type': 'off', + '@typescript-eslint/ban-ts-comment': 'off', }, } // TypeScript-ESLint recommended rules with type checking } @@ -34,7 +35,6 @@ export function testFilesOverrides() { rules: { '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/ban-ts-comment': 'off', 'no-constant-condition': 'off', 'no-console': 'off', }, From 63b17fe2cc2b1b55942b4bde4f3e43aa4937ed0f Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 09:13:14 +0000 Subject: [PATCH 02/20] Enable additional TypeScript strictness flags (excluding exactOptionalPropertyTypes) (#793) ## Summary Enable additional TypeScript strictness options not already covered by `strict` in `tsconfig.base.json` and fix the resulting errors. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Peter Leonov --- .../client-common/src/parse/column_types.ts | 18 +++++++++++++----- packages/client-node/src/client.ts | 2 +- packages/client-web/src/client.ts | 2 +- tests/clickhouse-test-runner/src/log.ts | 2 +- tsconfig.base.json | 5 +++++ 5 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/client-common/src/parse/column_types.ts b/packages/client-common/src/parse/column_types.ts index 6545d7b2..a62cba38 100644 --- a/packages/client-common/src/parse/column_types.ts +++ b/packages/client-common/src/parse/column_types.ts @@ -247,8 +247,10 @@ export function parseDecimalType({ }, ) } + // SAFETY: `split` length is checked to be exactly 2 above + const [precisionStr, scaleStr] = split as [string, string] let intSize: DecimalParams['intSize'] = 32 - const precision = parseInt(split[0], 10) + const precision = parseInt(precisionStr, 10) if (Number.isNaN(precision) || precision < 1 || precision > 76) { throw new ColumnTypeParseError('Invalid Decimal precision', { columnType, @@ -256,7 +258,7 @@ export function parseDecimalType({ precision, }) } - const scale = parseInt(split[1], 10) + const scale = parseInt(scaleStr, 10) if (Number.isNaN(scale) || scale < 0 || scale > precision) { throw new ColumnTypeParseError('Invalid Decimal scale', { columnType, @@ -415,7 +417,10 @@ export function parseEnumType({ const values: ParsedColumnEnum['values'] = {} for (let i = 0; i < names.length; i++) { - values[indices[i]] = names[i] + const idx = indices[i] + const name = names[i] + // SAFETY: `names.length !== indices.length` is checked and throws above, so both indexed values are always defined. + values[idx!] = name! } return { type: 'Enum', @@ -467,7 +472,10 @@ export function parseMapType({ }) } columnType = columnType.slice(MapPrefix.length, -1) - const [keyType, valueType] = getElementsTypes({ columnType, sourceType }, 2) + const [keyType, valueType] = getElementsTypes( + { columnType, sourceType }, + 2, + ) as [string, string, ...string[]] const key = parseColumnType(keyType) if ( key.type === 'DateTime64' || @@ -604,7 +612,7 @@ export function parseDateTime64Type({ sourceType, }) } - const precision = parseInt(columnType[DateTime64Prefix.length], 10) + const precision = parseInt(columnType[DateTime64Prefix.length] ?? '', 10) if (Number.isNaN(precision) || precision < 0 || precision > 9) { throw new ColumnTypeParseError('Invalid DateTime64 precision', { columnType, diff --git a/packages/client-node/src/client.ts b/packages/client-node/src/client.ts index 9dcc61d2..d4b4576e 100644 --- a/packages/client-node/src/client.ts +++ b/packages/client-node/src/client.ts @@ -19,7 +19,7 @@ export type QueryResult = export class NodeClickHouseClient extends ClickHouseClient { /** See {@link ClickHouseClient.query}. */ - query( + override query( params: QueryParamsWithFormat, ): Promise> { return super.query(params) as Promise> diff --git a/packages/client-web/src/client.ts b/packages/client-web/src/client.ts index a10ea7ab..6dc757f5 100644 --- a/packages/client-web/src/client.ts +++ b/packages/client-web/src/client.ts @@ -54,7 +54,7 @@ export type WebClickHouseClient = Omit< class WebClickHouseClientImpl extends ClickHouseClient { /** See {@link ClickHouseClient.query}. */ - query( + override query( params: QueryParamsWithFormat, ): Promise> { return super.query(params) as Promise> diff --git a/tests/clickhouse-test-runner/src/log.ts b/tests/clickhouse-test-runner/src/log.ts index 47fb8700..ed347c58 100644 --- a/tests/clickhouse-test-runner/src/log.ts +++ b/tests/clickhouse-test-runner/src/log.ts @@ -6,7 +6,7 @@ const DEFAULT_LOG_PATH = '/tmp/clickhouse-client-cli.log' const FALLBACK_LOG_FILENAME = 'clickhouse-client-cli.log' export function resolveLogPath(): string { - const fromEnv = process.env.CLICKHOUSE_CLIENT_CLI_LOG + const fromEnv = process.env['CLICKHOUSE_CLIENT_CLI_LOG'] if (fromEnv !== undefined && fromEnv.trim().length > 0) { return fromEnv } diff --git a/tsconfig.base.json b/tsconfig.base.json index 27a6014e..7f6a7f1d 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -7,6 +7,11 @@ "pretty": true, "noEmitOnError": true, "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "allowUnusedLabels": false, + "allowUnreachableCode": false, "resolveJsonModule": true, "removeComments": false, "sourceMap": true, From 0f17ce0240908363559d92b2d89baff85cd9e5c3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:15:18 +0200 Subject: [PATCH 03/20] chore: bump version to 1.21.0 (#809) Bumps version from `1.20.0` to `1.21.0` (minor bump). Co-authored-by: github-actions[bot] --- 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 3c1a553c..b026e830 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9638,16 +9638,16 @@ }, "packages/client-common": { "name": "@clickhouse/client-common", - "version": "1.20.0", + "version": "1.21.0", "license": "Apache-2.0", "devDependencies": {} }, "packages/client-node": { "name": "@clickhouse/client", - "version": "1.20.0", + "version": "1.21.0", "license": "Apache-2.0", "dependencies": { - "@clickhouse/client-common": "1.20.0" + "@clickhouse/client-common": "1.21.0" }, "devDependencies": { "simdjson": "^0.9.2" @@ -9658,15 +9658,15 @@ }, "packages/client-web": { "name": "@clickhouse/client-web", - "version": "1.20.0", + "version": "1.21.0", "license": "Apache-2.0", "dependencies": { - "@clickhouse/client-common": "1.20.0" + "@clickhouse/client-common": "1.21.0" } }, "tests/clickhouse-test-runner": { "name": "@clickhouse/clickhouse-test-runner", - "version": "1.20.0", + "version": "1.21.0", "dependencies": { "@clickhouse/client": "*" }, diff --git a/packages/client-common/package.json b/packages/client-common/package.json index 6e0b2516..0e3193de 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.20.0", + "version": "1.21.0", "license": "Apache-2.0", "keywords": [ "clickhouse", diff --git a/packages/client-common/src/version.ts b/packages/client-common/src/version.ts index 885bab02..fab0915d 100644 --- a/packages/client-common/src/version.ts +++ b/packages/client-common/src/version.ts @@ -1 +1 @@ -export default '1.20.0' +export default '1.21.0' diff --git a/packages/client-node/package.json b/packages/client-node/package.json index 3f265cc0..288e297f 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.20.0", + "version": "1.21.0", "license": "Apache-2.0", "keywords": [ "clickhouse", @@ -44,7 +44,7 @@ "build": "rm -rf dist; tsc" }, "dependencies": { - "@clickhouse/client-common": "1.20.0" + "@clickhouse/client-common": "1.21.0" }, "devDependencies": { "simdjson": "^0.9.2" diff --git a/packages/client-node/src/version.ts b/packages/client-node/src/version.ts index 885bab02..fab0915d 100644 --- a/packages/client-node/src/version.ts +++ b/packages/client-node/src/version.ts @@ -1 +1 @@ -export default '1.20.0' +export default '1.21.0' diff --git a/packages/client-web/package.json b/packages/client-web/package.json index 02659675..cebd0851 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.20.0", + "version": "1.21.0", "license": "Apache-2.0", "keywords": [ "clickhouse", @@ -31,6 +31,6 @@ "build": "rm -rf dist; tsc" }, "dependencies": { - "@clickhouse/client-common": "1.20.0" + "@clickhouse/client-common": "1.21.0" } } diff --git a/packages/client-web/src/version.ts b/packages/client-web/src/version.ts index 885bab02..fab0915d 100644 --- a/packages/client-web/src/version.ts +++ b/packages/client-web/src/version.ts @@ -1 +1 @@ -export default '1.20.0' +export default '1.21.0' diff --git a/tests/clickhouse-test-runner/package.json b/tests/clickhouse-test-runner/package.json index e7a66788..91a19144 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": "1.20.0", + "version": "1.21.0", "description": "Node.js port of ClickHouse/clickhouse-java tests/clickhouse-client harness", "engines": { "node": ">=20.19.0" From ceb8d82ec4917f10eb988ddefc02895c156f45a4 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:03:38 +0000 Subject: [PATCH 04/20] Make TypeScript sources compatible with --erasableSyntaxOnly (#768) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Removes all TypeScript-only syntax extensions that emit runtime code, so the project can be built/typechecked with `--erasableSyntaxOnly` (and consumed by runtimes like Node's native TS stripping). The option is enabled in `tsconfig.base.json` to lock this in. - **Enums → `as const` object + type alias.** Preserves `Foo.Bar` member access and runtime values. Applied to internal/test enums (`TestEnv`, `TestConnectionType`, local test enums in `select_query_binding.test.ts`). - **Public `ClickHouseLogLevel` → `as const` object that mimics the TS-compiled enum shape.** The object contains both forward (`TRACE: 0`, …) and reverse (`0: 'TRACE'`, …) entries — the same runtime layout `tsc` emits for a numeric `enum` — so both `ClickHouseLogLevel.TRACE` and `ClickHouseLogLevel[0]` continue to work for downstream TS consumers. The exported type is the literal numeric union (`0 | 1 | 2 | 3 | 4 | 127`), matching the old enum's value type. - **Constructor parameter properties → explicit field declarations + assignments.** Applied to `TupleParam`, `SettingsMap`, `LogWriter`, `WebConnection`, web/node `ResultSet`, `NodeBaseConnection`, `SocketPool`, and the `BufferReader` test helper. - **`tsconfig.base.json`**: `"erasableSyntaxOnly": true`. Example of the enum transform — call sites such as `ClickHouseLogLevel.TRACE` and reverse lookups like `ClickHouseLogLevel[0]` continue to work: ```ts export const ClickHouseLogLevel = { TRACE: 0, DEBUG: 1, INFO: 2, WARN: 3, ERROR: 4, OFF: 127, 0: 'TRACE', 1: 'DEBUG', 2: 'INFO', 3: 'WARN', 4: 'ERROR', 127: 'OFF', } as const export type ClickHouseLogLevel = 0 | 1 | 2 | 3 | 4 | 127 ``` No namespaces or `import = require(...)` exist in the tree, so nothing else needed to change. ## Checklist - [x] A human-readable description of the changes was provided to include in CHANGELOG ### CHANGELOG - Internal: TypeScript sources are now compatible with the `--erasableSyntaxOnly` compiler option. `ClickHouseLogLevel` is now exported as a `const` object plus a union type instead of a TS `enum`, but keeps the same bidirectional runtime shape (both `Name → number` and `number → Name` entries) that `tsc` emits for numeric enums, so existing usage like `ClickHouseLogLevel.TRACE` and `ClickHouseLogLevel[0]` is unchanged. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] Co-authored-by: Peter Leonov Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/instructions/review.instructions.md | 4 +- CHANGELOG.md | 4 ++ .../integration/select_query_binding.test.ts | 22 +++++----- .../__tests__/utils/test_connection_type.ts | 10 +++-- .../client-common/__tests__/utils/test_env.ts | 11 ++--- packages/client-common/src/config.ts | 2 +- .../src/data_formatter/format_query_params.ts | 5 ++- packages/client-common/src/logger.ts | 43 +++++++++++++------ packages/client-common/src/settings.ts | 5 ++- .../node_stream_row_binary_select.test.ts | 5 ++- .../src/connection/node_base_connection.ts | 9 ++-- .../client-node/src/connection/socket_pool.ts | 17 ++++++-- packages/client-node/src/result_set.ts | 24 +++++++---- .../src/connection/web_connection.ts | 4 +- packages/client-web/src/result_set.ts | 12 ++++-- tsconfig.base.json | 3 +- 16 files changed, 119 insertions(+), 61 deletions(-) diff --git a/.github/instructions/review.instructions.md b/.github/instructions/review.instructions.md index 7550ddf8..5a6756db 100644 --- a/.github/instructions/review.instructions.md +++ b/.github/instructions/review.instructions.md @@ -1,6 +1,6 @@ --- -applyTo: "**" -excludeAgent: "cloud-agent" +applyTo: '**' +excludeAgent: 'cloud-agent' --- ## UX diff --git a/CHANGELOG.md b/CHANGELOG.md index bec4844e..1ddeee08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 1.20.0 +## Migration Notes + +- TypeScript: `ClickHouseLogLevel` is now exported as a literal numeric union type (`0 | 1 | 2 | 3 | 4 | 127`) instead of a TypeScript `enum` type. If you were assigning arbitrary `number` values to `ClickHouseLogLevel`, you may need to narrow/cast those values during migration. + ## 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]) diff --git a/packages/client-common/__tests__/integration/select_query_binding.test.ts b/packages/client-common/__tests__/integration/select_query_binding.test.ts index 9cba3c7e..569f415c 100644 --- a/packages/client-common/__tests__/integration/select_query_binding.test.ts +++ b/packages/client-common/__tests__/integration/select_query_binding.test.ts @@ -341,12 +341,12 @@ describe('select with query binding', () => { }) describe('Enum', () => { - it('should accept numeric enums in a parametrized query', async () => { - enum MyEnum { - foo = 0, - bar = 1, - qaz = 2, - } + it('should accept numeric enum-like values in a parametrized query', async () => { + const MyEnum = { + foo: 0, + bar: 1, + qaz: 2, + } as const const rs = await client.query({ query: @@ -361,11 +361,11 @@ describe('select with query binding', () => { expect(response).toBe('2\n') }) - it('should accept numeric enums in a parametrized query', async () => { - enum MyEnum { - foo = 'foo', - bar = 'bar', - } + it('should accept string enum-like values in a parametrized query', async () => { + const MyEnum = { + foo: 'foo', + bar: 'bar', + } as const const rs = await client.query({ query: 'SELECT concat({str1: String},{str2: String})', diff --git a/packages/client-common/__tests__/utils/test_connection_type.ts b/packages/client-common/__tests__/utils/test_connection_type.ts index 409ba627..9a0fad79 100644 --- a/packages/client-common/__tests__/utils/test_connection_type.ts +++ b/packages/client-common/__tests__/utils/test_connection_type.ts @@ -1,7 +1,9 @@ -export enum TestConnectionType { - Node = 'node', - Browser = 'browser', -} +export const TestConnectionType = { + Node: 'node', + Browser: 'browser', +} as const +export type TestConnectionType = + (typeof TestConnectionType)[keyof typeof TestConnectionType] export function getTestConnectionType(): TestConnectionType { let connectionType switch (process.env['CLICKHOUSE_TEST_CONNECTION_TYPE']) { diff --git a/packages/client-common/__tests__/utils/test_env.ts b/packages/client-common/__tests__/utils/test_env.ts index 92dad408..9deff646 100644 --- a/packages/client-common/__tests__/utils/test_env.ts +++ b/packages/client-common/__tests__/utils/test_env.ts @@ -1,8 +1,9 @@ -export enum TestEnv { - Cloud = 'cloud', - LocalSingleNode = 'local_single_node', - LocalCluster = 'local_cluster', -} +export const TestEnv = { + Cloud: 'cloud', + LocalSingleNode: 'local_single_node', + LocalCluster: 'local_cluster', +} as const +export type TestEnv = (typeof TestEnv)[keyof typeof TestEnv] export function getClickHouseTestEnvironment(): TestEnv { let env diff --git a/packages/client-common/src/config.ts b/packages/client-common/src/config.ts index eefe7ea8..70229ea4 100644 --- a/packages/client-common/src/config.ts +++ b/packages/client-common/src/config.ts @@ -455,7 +455,7 @@ export function loadConfigOptionsFromURL( key, value, enumObject: ClickHouseLogLevel, - }) + }) as ClickHouseLogLevel break case 'keep_alive_enabled': if (config.keep_alive === undefined) { diff --git a/packages/client-common/src/data_formatter/format_query_params.ts b/packages/client-common/src/data_formatter/format_query_params.ts index 16041155..13d7faed 100644 --- a/packages/client-common/src/data_formatter/format_query_params.ts +++ b/packages/client-common/src/data_formatter/format_query_params.ts @@ -1,5 +1,8 @@ export class TupleParam { - constructor(public readonly values: readonly unknown[]) {} + readonly values: readonly unknown[] + constructor(values: readonly unknown[]) { + this.values = values + } } export function formatQueryParams({ diff --git a/packages/client-common/src/logger.ts b/packages/client-common/src/logger.ts index 84868fd1..4b608525 100644 --- a/packages/client-common/src/logger.ts +++ b/packages/client-common/src/logger.ts @@ -76,11 +76,13 @@ export type LogWriterParams = Omit< > & { module?: string } export class LogWriter { - constructor( - private readonly logger: Logger, - private readonly module: string, - private readonly logLevel: ClickHouseLogLevel, - ) { + private readonly logger: Logger + private readonly module: string + private readonly logLevel: ClickHouseLogLevel + constructor(logger: Logger, module: string, logLevel: ClickHouseLogLevel) { + this.logger = logger + this.module = module + this.logLevel = logLevel this.info({ message: `Log level is set to ${ClickHouseLogLevel[this.logLevel]}`, }) @@ -132,32 +134,47 @@ export class LogWriter { } } -export enum ClickHouseLogLevel { +/** + * Mimics the runtime shape of a numeric TypeScript `enum`: an object with both + * forward (`Name -> number`) and reverse (`number -> Name`) mappings. This + * preserves backwards compatibility with the previous `enum ClickHouseLogLevel` + * — both `ClickHouseLogLevel.TRACE` (returns `0`) and `ClickHouseLogLevel[0]` + * (returns `'TRACE'`) continue to work — while staying compatible with the + * `--erasableSyntaxOnly` TypeScript option (no TS `enum` declaration). + */ +export const ClickHouseLogLevel = { /** * A fine-grained debugging event. Might produce a lot of logs, so use with caution. */ - TRACE = 0, + TRACE: 0, /** * A debugging event. Useful for debugging, but generally not needed in production. Includes technical values that might require redacting. */ - DEBUG = 1, + DEBUG: 1, /** * An informational event. Indicates that an event happened. */ - INFO = 2, + INFO: 2, /** * A warning event. Not an error, but is likely more important than an informational event. Addressing should help prevent potential issues. */ - WARN = 3, + WARN: 3, /** * An error event. Something went wrong. */ - ERROR = 4, + ERROR: 4, /** * Logging is turned off. */ - OFF = 127, -} + OFF: 127, + 0: 'TRACE', + 1: 'DEBUG', + 2: 'INFO', + 3: 'WARN', + 4: 'ERROR', + 127: 'OFF', +} as const +export type ClickHouseLogLevel = 0 | 1 | 2 | 3 | 4 | 127 function formatMessage({ level, diff --git a/packages/client-common/src/settings.ts b/packages/client-common/src/settings.ts index 7352adcf..2f91e535 100644 --- a/packages/client-common/src/settings.ts +++ b/packages/client-common/src/settings.ts @@ -1949,7 +1949,10 @@ type URI = string type Map = SettingsMap export class SettingsMap { - private constructor(private readonly record: Record) {} + private readonly record: Record + private constructor(record: Record) { + this.record = record + } toString(): string { return `{${Object.entries(this.record) diff --git a/packages/client-node/__tests__/integration/node_stream_row_binary_select.test.ts b/packages/client-node/__tests__/integration/node_stream_row_binary_select.test.ts index 6152f496..af2bc638 100644 --- a/packages/client-node/__tests__/integration/node_stream_row_binary_select.test.ts +++ b/packages/client-node/__tests__/integration/node_stream_row_binary_select.test.ts @@ -60,7 +60,10 @@ describe('[Node.js] stream RowBinary select', () => { class BufferReader { private offset = 0 - constructor(private readonly buf: Buffer) {} + private readonly buf: Buffer + constructor(buf: Buffer) { + this.buf = buf + } eof(): boolean { return this.offset >= this.buf.length diff --git a/packages/client-node/src/connection/node_base_connection.ts b/packages/client-node/src/connection/node_base_connection.ts index fd0a143e..f3140672 100644 --- a/packages/client-node/src/connection/node_base_connection.ts +++ b/packages/client-node/src/connection/node_base_connection.ts @@ -73,11 +73,12 @@ export abstract class NodeBaseConnection implements Connection private readonly connectionId: string = crypto.randomUUID() private readonly socketPool: SocketPool + protected readonly params: NodeConnectionParams + protected readonly agent: Http.Agent - protected constructor( - protected readonly params: NodeConnectionParams, - protected readonly agent: Http.Agent, - ) { + protected constructor(params: NodeConnectionParams, agent: Http.Agent) { + this.params = params + this.agent = agent this.socketPool = new SocketPool( this.connectionId, this.params, diff --git a/packages/client-node/src/connection/socket_pool.ts b/packages/client-node/src/connection/socket_pool.ts index d87182ee..c6b14043 100644 --- a/packages/client-node/src/connection/socket_pool.ts +++ b/packages/client-node/src/connection/socket_pool.ts @@ -76,12 +76,21 @@ export class SocketPool { return `${this.connectionId}:${this.socketCounter}` } + private readonly connectionId: string + private readonly params: NodeConnectionParams + private readonly createClientRequest: CreateClientRequest + private readonly agent: Http.Agent + constructor( - private readonly connectionId: string, - private readonly params: NodeConnectionParams, - private readonly createClientRequest: CreateClientRequest, - private readonly agent: Http.Agent, + connectionId: string, + params: NodeConnectionParams, + createClientRequest: CreateClientRequest, + agent: Http.Agent, ) { + this.connectionId = connectionId + this.params = params + this.createClientRequest = createClientRequest + this.agent = agent this.jsonHandling = params.json ?? { parse: JSON.parse, stringify: JSON.stringify, diff --git a/packages/client-node/src/result_set.ts b/packages/client-node/src/result_set.ts index 5f426275..f015c5ed 100644 --- a/packages/client-node/src/result_set.ts +++ b/packages/client-node/src/result_set.ts @@ -68,21 +68,27 @@ export class ResultSet< private readonly log_error: (error: Error) => void private readonly jsonHandling: JSONHandling private _consumed = false + /** + * 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 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, + _stream: Stream.Readable, + format: Format, + query_id: string, log_error?: (error: Error) => void, _response_headers?: ResponseHeaders, jsonHandling?: JSONHandling, ) { + this._stream = _stream + this.format = format + this.query_id = query_id this.jsonHandling = { ...defaultJSONHandling, ...jsonHandling, diff --git a/packages/client-web/src/connection/web_connection.ts b/packages/client-web/src/connection/web_connection.ts index aff24483..9c4d4f7c 100644 --- a/packages/client-web/src/connection/web_connection.ts +++ b/packages/client-web/src/connection/web_connection.ts @@ -34,7 +34,9 @@ export type WebConnectionParams = ConnectionParams & { export class WebConnection implements Connection { private readonly defaultAuthHeader: string - constructor(private readonly params: WebConnectionParams) { + private readonly params: WebConnectionParams + constructor(params: WebConnectionParams) { + this.params = params if (params.auth.type === 'JWT') { this.defaultAuthHeader = `Bearer ${params.auth.access_token}` } else if (params.auth.type === 'Credentials') { diff --git a/packages/client-web/src/result_set.ts b/packages/client-web/src/result_set.ts index 901be8df..8c2af20e 100644 --- a/packages/client-web/src/result_set.ts +++ b/packages/client-web/src/result_set.ts @@ -28,17 +28,23 @@ export class ResultSet< private readonly exceptionTag: string | undefined = undefined private isAlreadyConsumed = false private readonly jsonHandling: JSONHandling + private _stream: ReadableStream + private readonly format: Format + public readonly query_id: string constructor( - private _stream: ReadableStream, - private readonly format: Format, - public readonly query_id: string, + _stream: ReadableStream, + format: Format, + query_id: string, _response_headers?: ResponseHeaders, jsonHandling: JSONHandling = { parse: JSON.parse, stringify: JSON.stringify, }, ) { + this._stream = _stream + this.format = format + this.query_id = query_id this.response_headers = _response_headers !== undefined ? Object.freeze(_response_headers) : {} this.exceptionTag = this.response_headers['x-clickhouse-exception-tag'] as diff --git a/tsconfig.base.json b/tsconfig.base.json index 7f6a7f1d..2c524f25 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -20,7 +20,8 @@ "forceConsistentCasingInFileNames": true, "skipLibCheck": false, "esModuleInterop": true, - "importHelpers": false + "importHelpers": false, + "erasableSyntaxOnly": true }, "exclude": ["node_modules"] } From c7942ce089543ef39a3b20e99a64da07ae16045c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:08:28 +0200 Subject: [PATCH 05/20] Bump actions/checkout from 6.0.2 to 6.0.3 in /.github/workflows (#815) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.2 to 6.0.3.
Release notes

Sourced from actions/checkout's releases.

v6.0.3

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v6...v6.0.3

Changelog

Sourced from actions/checkout's changelog.

Changelog

v6.0.3

v6.0.2

v6.0.1

v6.0.0

v5.0.1

v5.0.0

v4.3.1

v4.3.0

v4.2.2

v4.2.1

v4.2.0

v4.1.7

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=6.0.2&new-version=6.0.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/bump-version.yml | 2 +- .github/workflows/clean-up.yml | 2 +- .github/workflows/e2e-skills.yml | 2 +- .github/workflows/examples.yml | 4 ++-- .github/workflows/publish.yml | 6 +++--- .github/workflows/scorecard.yml | 2 +- .github/workflows/tests-bun.yml | 4 ++-- .github/workflows/tests-node.yml | 14 +++++++------- .github/workflows/tests-web.yml | 10 +++++----- .github/workflows/upstream-sql-tests.yml | 4 ++-- 10 files changed, 25 insertions(+), 25 deletions(-) diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index 1787b8ef..d2c169b4 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -25,7 +25,7 @@ jobs: pull-requests: write steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: main diff --git a/.github/workflows/clean-up.yml b/.github/workflows/clean-up.yml index a8391d24..32a58284 100644 --- a/.github/workflows/clean-up.yml +++ b/.github/workflows/clean-up.yml @@ -19,7 +19,7 @@ jobs: if: always() runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup NodeJS uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 diff --git a/.github/workflows/e2e-skills.yml b/.github/workflows/e2e-skills.yml index da067fa4..24749dc8 100644 --- a/.github/workflows/e2e-skills.yml +++ b/.github/workflows/e2e-skills.yml @@ -16,7 +16,7 @@ jobs: skills-packaging: runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup Node.js uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index f9ccd680..d3511690 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -41,7 +41,7 @@ jobs: run: working-directory: examples/${{ matrix.package }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup NodeJS uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 @@ -72,7 +72,7 @@ jobs: run: working-directory: examples/${{ matrix.package }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Start ClickHouse (version - ${{ matrix.clickhouse }}) in Docker uses: isbang/compose-action@3846bcd61da338e9eaaf83e7ed0234a12b099b72 # v2.4.2 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2fe3d845..aa841c22 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -49,7 +49,7 @@ jobs: version: ${{ steps.version.outputs.version }} steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup Node.js uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 @@ -90,7 +90,7 @@ jobs: version: ${{ steps.version.outputs.version }} steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup Node.js uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 @@ -149,7 +149,7 @@ jobs: env: PUBLISHED_VERSION: ${{ needs.head.outputs.version || needs.latest.outputs.version }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup NodeJS ${{ matrix.node }} uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index cf4a23b8..03960653 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -45,7 +45,7 @@ jobs: steps: - name: 'Checkout code' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/tests-bun.yml b/.github/workflows/tests-bun.yml index 73932b43..8a86f9d7 100644 --- a/.github/workflows/tests-bun.yml +++ b/.github/workflows/tests-bun.yml @@ -25,7 +25,7 @@ jobs: timeout-minutes: 5 runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 @@ -42,7 +42,7 @@ jobs: timeout-minutes: 5 runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 diff --git a/.github/workflows/tests-node.yml b/.github/workflows/tests-node.yml index 058dbe2c..3da72836 100644 --- a/.github/workflows/tests-node.yml +++ b/.github/workflows/tests-node.yml @@ -36,7 +36,7 @@ jobs: timeout-minutes: 5 runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup NodeJS uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 @@ -67,7 +67,7 @@ jobs: matrix: node: [20, 22, 24] steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup NodeJS ${{ matrix.node }} uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 @@ -96,7 +96,7 @@ jobs: matrix: node: [20, 22, 24] steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup NodeJS ${{ matrix.node }} uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 @@ -148,7 +148,7 @@ jobs: clickhouse: 25.10 log_level: undefined steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Start ClickHouse (version - ${{ matrix.clickhouse }}) in Docker uses: isbang/compose-action@3846bcd61da338e9eaaf83e7ed0234a12b099b72 # v2.4.2 @@ -193,7 +193,7 @@ jobs: clickhouse: [head, latest] steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Start ClickHouse cluster (version - ${{ matrix.clickhouse }}) in Docker uses: isbang/compose-action@3846bcd61da338e9eaaf83e7ed0234a12b099b72 # v2.4.2 @@ -233,7 +233,7 @@ jobs: node: [20, 22, 24] steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup NodeJS ${{ matrix.node }} uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 @@ -267,7 +267,7 @@ jobs: matrix: clickhouse: [head, latest] steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 diff --git a/.github/workflows/tests-web.yml b/.github/workflows/tests-web.yml index 75bdd0a6..3e2c9e3f 100644 --- a/.github/workflows/tests-web.yml +++ b/.github/workflows/tests-web.yml @@ -40,7 +40,7 @@ jobs: matrix: browser: [chromium, firefox] # We're not testing in WebKit atm steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup NodeJS uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 @@ -73,7 +73,7 @@ jobs: browser: [chromium, firefox] # We're not testing in WebKit atm clickhouse: [head, latest] steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Start ClickHouse (version - ${{ matrix.clickhouse }}) in Docker uses: isbang/compose-action@3846bcd61da338e9eaaf83e7ed0234a12b099b72 # v2.4.2 @@ -114,7 +114,7 @@ jobs: browser: [chromium, firefox] # We're not testing in WebKit atm clickhouse: [head, latest] steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Start ClickHouse cluster (version - ${{ matrix.clickhouse }}) in Docker uses: isbang/compose-action@3846bcd61da338e9eaaf83e7ed0234a12b099b72 # v2.4.2 @@ -156,7 +156,7 @@ jobs: matrix: browser: [chromium, firefox] # We're not testing in WebKit atm steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Setup NodeJS uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 @@ -192,7 +192,7 @@ jobs: matrix: clickhouse: [head, latest] steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 diff --git a/.github/workflows/upstream-sql-tests.yml b/.github/workflows/upstream-sql-tests.yml index 90778f0d..3d521ee4 100644 --- a/.github/workflows/upstream-sql-tests.yml +++ b/.github/workflows/upstream-sql-tests.yml @@ -45,10 +45,10 @@ jobs: shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] steps: - name: Checkout clickhouse-js - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Checkout ClickHouse upstream - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: repository: ${{ env.UPSTREAM_REPO }} ref: ${{ github.event.inputs.upstream_ref || 'master' }} From 15088379476ff2660d7943b7279faedb8dd76110 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 19:09:59 +0200 Subject: [PATCH 06/20] Bump dev dependencies and fix TypeScript 6/examples CI (#816) ## Summary Bumps the dev-dependencies group with 16 updates, including ESLint 10 and TypeScript 6, and adds the minimal follow-up fixes needed to keep CI green after the upgrade. In particular, this PR: - updates the dev dependency set from the original Renovate change - makes the workspace package `tsconfig.json` files compatible with TypeScript 6 by setting explicit `rootDir` values and enabling the required disposable libs for `Symbol.asyncDispose` - makes the ClickHouse Cloud schema/deployment examples skip cleanly when cloud credentials are not provided, so the examples CI jobs do not fail in environments without `CLICKHOUSE_CLOUD_URL` / `CLICKHOUSE_CLOUD_PASSWORD` Human-readable CHANGELOG note: - Build/examples: make the workspace packages compatible with TypeScript 6 and skip ClickHouse Cloud examples when cloud credentials are unavailable in CI. ## 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 --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Peter Leonov --- eslint.config.base.mjs | 1 + package-lock.json | 4644 ++++----------------- package.json | 28 +- packages/client-common/tsconfig.json | 2 + packages/client-node/tsconfig.json | 1 + packages/client-web/tsconfig.json | 2 + tests/clickhouse-test-runner/package.json | 14 +- 7 files changed, 814 insertions(+), 3878 deletions(-) diff --git a/eslint.config.base.mjs b/eslint.config.base.mjs index ffe45771..e49d430b 100644 --- a/eslint.config.base.mjs +++ b/eslint.config.base.mjs @@ -24,6 +24,7 @@ export function typescriptEslintConfig(root) { '@typescript-eslint/consistent-type-imports': 'warn', '@typescript-eslint/array-type': 'off', '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', }, } // TypeScript-ESLint recommended rules with type checking } diff --git a/package-lock.json b/package-lock.json index b026e830..146ff430 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,36 +14,36 @@ ], "devDependencies": { "@eslint/js": "^10.0.1", - "@faker-js/faker": "^10.3.0", - "@opentelemetry/api": "^1.9.0", + "@faker-js/faker": "^10.4.0", + "@opentelemetry/api": "^1.9.1", "@opentelemetry/auto-instrumentations-node": "^0.76.0", - "@opentelemetry/context-zone": "^2.6.0", - "@opentelemetry/exporter-trace-otlp-proto": "^0.213.0", - "@opentelemetry/instrumentation-document-load": "^0.58.0", - "@opentelemetry/instrumentation-fetch": "^0.213.0", + "@opentelemetry/context-zone": "^2.7.1", + "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0", + "@opentelemetry/instrumentation-document-load": "^0.63.0", + "@opentelemetry/instrumentation-fetch": "^0.218.0", "@opentelemetry/sdk-trace-web": "^2.5.1", "@types/jsonwebtoken": "^9.0.10", - "@types/node": "25.5.0", + "@types/node": "25.9.2", "@types/split2": "^4.2.3", "@types/uuid": "^11.0.0", "@vitest/browser-playwright": "^4.1.8", "@vitest/coverage-istanbul": "^4.1.0", "@vitest/coverage-v8": "^4.1.0", "apache-arrow": "^21.0.0", - "eslint": "^10.2.0", + "eslint": "^10.4.1", "eslint-config-prettier": "^10.1.8", "eslint-plugin-expect-type": "^0.6.2", - "eslint-plugin-prettier": "^5.5.5", + "eslint-plugin-prettier": "^5.5.6", "husky": "^9.1.7", "jsonwebtoken": "^9.0.3", - "lint-staged": "^16.4.0", + "lint-staged": "^17.0.7", "parquet-wasm": "0.7.1", "playwright": "^1.60.0", - "prettier": "3.8.1", + "prettier": "3.8.3", "split2": "^4.2.0", - "typescript": "^5.9.3", - "typescript-eslint": "^8.57.0", - "uuid": "^13.0.0", + "typescript": "^6.0.3", + "typescript-eslint": "^8.60.1", + "uuid": "^14.0.0", "vitest": "^4.0.16" }, "engines": { @@ -420,13 +420,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.23.4", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.4.tgz", - "integrity": "sha512-lf19F24LSMfF8weXvW5QEtnLqW70u7kgit5e9PSx0MsHAFclGd1T9ynvWEMDT1w5J4Qt54tomGeAhdoAku1Xow==", + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^3.0.4", + "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" }, @@ -435,22 +435,22 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.4.tgz", - "integrity": "sha512-jJhqiY3wPMlWWO3370M86CPJ7pt8GmEwSLglMfQhjXal07RCvhmU0as4IuUEW5SJeunfItiEetHmSxCCe9lDBg==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.2.0" + "@eslint/core": "^1.2.1" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/core": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.0.tgz", - "integrity": "sha512-8FTGbNzTvmSlc4cZBaShkC6YvFMG0riksYWRFKXztqVdXaQbcZLXlFbSpC05s70sGEsXAw0qwhx69JiW7hQS7A==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -460,85 +460,6 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.14.0", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.5", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@eslint/js": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", @@ -561,9 +482,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.4.tgz", - "integrity": "sha512-55lO/7+Yp0ISKRP0PsPtNTeNGapXaO085aELZmWCVc5SH3jfrqpuU6YgOdIxMS99ZHkQN1cXKE+cdIqwww9ptw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -571,13 +492,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.0.tgz", - "integrity": "sha512-ejvBr8MQCbVsWNZnCwDXjUKq40MDmHalq7cJ6e9s/qzTUFIIo/afzt1Vui9T97FM/V/pN4YsFVoed5NIa96RDg==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.2.0", + "@eslint/core": "^1.2.1", "levn": "^0.4.1" }, "engines": { @@ -585,9 +506,9 @@ } }, "node_modules/@faker-js/faker": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.3.0.tgz", - "integrity": "sha512-It0Sne6P3szg7JIi6CgKbvTZoMjxBZhcv91ZrqrNuaZQfB5WoqYYbzCUOq89YR+VY8juY9M1vDWmDDa2TzfXCw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.4.0.tgz", + "integrity": "sha512-sDBWI3yLy8EcDzgobvJTWq1MJYzAkQdpjXuPukga9wXonhpMRvd1Izuo2Qgwey2OiEoRIBr35RMU9HJRoOHzpw==", "dev": true, "funding": [ { @@ -777,9 +698,9 @@ } }, "node_modules/@opentelemetry/api": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", - "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", "dev": true, "license": "Apache-2.0", "engines": { @@ -787,9 +708,9 @@ } }, "node_modules/@opentelemetry/api-logs": { - "version": "0.213.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.213.0.tgz", - "integrity": "sha512-zRM5/Qj6G84Ej3F1yt33xBVY/3tnMxtL1fiDIxYbDWYaZ/eudVw3/PBiZ8G7JwUxXxjW8gU4g6LnOyfGKYHYgw==", + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", + "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -863,37 +784,6 @@ "@opentelemetry/core": "^2.0.0" } }, - "node_modules/@opentelemetry/auto-instrumentations-node/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/auto-instrumentations-node/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, "node_modules/@opentelemetry/configuration": { "version": "0.218.0", "resolved": "https://registry.npmjs.org/@opentelemetry/configuration/-/configuration-0.218.0.tgz", @@ -925,13 +815,13 @@ } }, "node_modules/@opentelemetry/context-zone": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-zone/-/context-zone-2.6.0.tgz", - "integrity": "sha512-uOgV184sK+YHtKekt4JqRHNV93Z9vo13cdWAMBih9UG0xm8WCWCfbhW94NL17V9u9t1R7c/ivaTutA2nwiN9PA==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-zone/-/context-zone-2.7.1.tgz", + "integrity": "sha512-B42kO3zIMVbJ+wj5nlSkDvLF8cJY+7wDKLomHp10GL00nvUnhY67UQ/soZQgKR4dvPf8zTKbcONDsOiJLyRuXw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/context-zone-peer-dep": "2.6.0", + "@opentelemetry/context-zone-peer-dep": "2.7.1", "zone.js": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^0.14.0 || ^0.15.0 || ^0.16.0" }, "engines": { @@ -939,9 +829,9 @@ } }, "node_modules/@opentelemetry/context-zone-peer-dep": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-zone-peer-dep/-/context-zone-peer-dep-2.6.0.tgz", - "integrity": "sha512-cFSbc+3Osyo3nBmdteNarJaI3GqTRn0YgcEJWt/PkgazLqfwMifPIoSBLpLCZQzGtkTbdef2htgE7Tw6qe/bkw==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-zone-peer-dep/-/context-zone-peer-dep-2.7.1.tgz", + "integrity": "sha512-QPLvl82Ds+W9Tjz0s4b8UDUK9YkCb3pvaur4JQdgHe+eph6Ii20NbiC+wsdnBtG17DTPhmZcFvWMcQXZFBgeVw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -989,28 +879,18 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/otlp-exporter-base": { + "node_modules/@opentelemetry/exporter-logs-otlp-http": { "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.218.0.tgz", - "integrity": "sha512-ZwqpkNL5W7RyGJPDZ9g06DvKp8KFTWPJPN12anpMQYSKpTSU0z3EIZuPq9vPGpS8siFyOqDYDAuCwlNO9FqgbA==", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.218.0.tgz", + "integrity": "sha512-Qx+4rpVHzgg89dawcWRHyt+XRXeLnhFz/qBtvggmjkcgPUdr+NAB0/u/eIPA8yAeJV0J80Vz43JZCh/XFvZFGw==", "dev": true, "license": "Apache-2.0", "dependencies": { + "@opentelemetry/api-logs": "0.218.0", "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-transformer": "0.218.0" + "@opentelemetry/otlp-exporter-base": "0.218.0", + "@opentelemetry/otlp-transformer": "0.218.0", + "@opentelemetry/sdk-logs": "0.218.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1019,18 +899,19 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/otlp-transformer": { + "node_modules/@opentelemetry/exporter-logs-otlp-proto": { "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.218.0.tgz", - "integrity": "sha512-CFaKH87WAzjuJ4awowTTLzUvMfaRfiOFG5+qm5S5ncyalRtN4ecQ+YmuANJSCrVPuvZFEkUgKhBPBndxi3rHsQ==", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.218.0.tgz", + "integrity": "sha512-1/noQNsp9gXD75HPzgjBrcF1+XTtry7pFAUfxVEJgg7mPv2AawKQuYkhMmJ8qjxz4Ubc3Y8bwvfxevXsKTq4cg==", "dev": true, "license": "Apache-2.0", "dependencies": { "@opentelemetry/api-logs": "0.218.0", "@opentelemetry/core": "2.7.1", + "@opentelemetry/otlp-exporter-base": "0.218.0", + "@opentelemetry/otlp-transformer": "0.218.0", "@opentelemetry/resources": "2.7.1", "@opentelemetry/sdk-logs": "0.218.0", - "@opentelemetry/sdk-metrics": "2.7.1", "@opentelemetry/sdk-trace-base": "2.7.1" }, "engines": { @@ -1040,72 +921,62 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/sdk-logs": { + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.218.0.tgz", - "integrity": "sha512-QvnNdugatFTVCJXH0Mcu7GOOJSylA9j127kIezOE4YwTI4YbowRons2K4WZTv5FMS8T4q9P0NdaRHdkSmeAIag==", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.218.0.tgz", + "integrity": "sha512-YapQ9vNMX0NSZF6LK5pWAFfjpJleV2O9uYWfYGeb/5F1Kb9rPGK8tZDMJFa/sOksgdFuflDvYuA0B4qjDB4fjQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.218.0", + "@grpc/grpc-js": "^1.14.3", "@opentelemetry/core": "2.7.1", + "@opentelemetry/exporter-metrics-otlp-http": "0.218.0", + "@opentelemetry/otlp-exporter-base": "0.218.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.218.0", + "@opentelemetry/otlp-transformer": "0.218.0", "@opentelemetry/resources": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.1.tgz", - "integrity": "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1" + "@opentelemetry/sdk-metrics": "2.7.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", - "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", + "node_modules/@opentelemetry/exporter-metrics-otlp-http": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.218.0.tgz", + "integrity": "sha512-bV7d2OuMpZu2+gAaxUAhzfZ0h3WVZk8ETQUEE3DNSntbTaMpuITjtm8I0rNyHFdm7Ax57K6ty7SgFXlBmOLIvQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.7.1", + "@opentelemetry/otlp-exporter-base": "0.218.0", + "@opentelemetry/otlp-transformer": "0.218.0", "@opentelemetry/resources": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@opentelemetry/sdk-metrics": "2.7.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-logs-otlp-http": { + "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.218.0.tgz", - "integrity": "sha512-Qx+4rpVHzgg89dawcWRHyt+XRXeLnhFz/qBtvggmjkcgPUdr+NAB0/u/eIPA8yAeJV0J80Vz43JZCh/XFvZFGw==", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.218.0.tgz", + "integrity": "sha512-ubLddKjWULhla9YZRCj/rTBeppjJYE4e9w0icx5mTu3eFhWjQzbV75NYjXuIlEG+NJsBl6d+sTFw5Qu+oej4oQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.218.0", "@opentelemetry/core": "2.7.1", + "@opentelemetry/exporter-metrics-otlp-http": "0.218.0", "@opentelemetry/otlp-exporter-base": "0.218.0", "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/sdk-logs": "0.218.0" + "@opentelemetry/resources": "2.7.1", + "@opentelemetry/sdk-metrics": "2.7.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1114,28 +985,17 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/otlp-exporter-base": { + "node_modules/@opentelemetry/exporter-prometheus": { "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.218.0.tgz", - "integrity": "sha512-ZwqpkNL5W7RyGJPDZ9g06DvKp8KFTWPJPN12anpMQYSKpTSU0z3EIZuPq9vPGpS8siFyOqDYDAuCwlNO9FqgbA==", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.218.0.tgz", + "integrity": "sha512-RT5oEyu1kddZJ1vt7/BUo5wV+P7hpNAESsR3dUd3+8deHuX7gWNoCOZn+SfDT+hJHlIJ5h/AxiCLXIrutswDJg==", "dev": true, "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-transformer": "0.218.0" + "@opentelemetry/resources": "2.7.1", + "@opentelemetry/sdk-metrics": "2.7.1", + "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1144,18 +1004,19 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/otlp-transformer": { + "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.218.0.tgz", - "integrity": "sha512-CFaKH87WAzjuJ4awowTTLzUvMfaRfiOFG5+qm5S5ncyalRtN4ecQ+YmuANJSCrVPuvZFEkUgKhBPBndxi3rHsQ==", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.218.0.tgz", + "integrity": "sha512-3fXxVQEj9TNAFaCi79JeFKfeLd0sDtInaR3gaZDVlzNSPHtz8PZuCV34JKWjD4XXzT20IdMe8IpX6mRVNDA4Tw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.218.0", + "@grpc/grpc-js": "^1.14.3", "@opentelemetry/core": "2.7.1", + "@opentelemetry/otlp-exporter-base": "0.218.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.218.0", + "@opentelemetry/otlp-transformer": "0.218.0", "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-logs": "0.218.0", - "@opentelemetry/sdk-metrics": "2.7.1", "@opentelemetry/sdk-trace-base": "2.7.1" }, "engines": { @@ -1165,74 +1026,75 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/sdk-logs": { + "node_modules/@opentelemetry/exporter-trace-otlp-http": { "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.218.0.tgz", - "integrity": "sha512-QvnNdugatFTVCJXH0Mcu7GOOJSylA9j127kIezOE4YwTI4YbowRons2K4WZTv5FMS8T4q9P0NdaRHdkSmeAIag==", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.218.0.tgz", + "integrity": "sha512-8dqezsmPhtKitIK/eTipZhYl9EX2/gNQ5zUMhaz3uxEURwfkNf8IPvo6yNfrzbxdtpAOybS/+h7wmIWYqFSpiw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.218.0", "@opentelemetry/core": "2.7.1", + "@opentelemetry/otlp-exporter-base": "0.218.0", + "@opentelemetry/otlp-transformer": "0.218.0", "@opentelemetry/resources": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@opentelemetry/sdk-trace-base": "2.7.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.1.tgz", - "integrity": "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==", + "node_modules/@opentelemetry/exporter-trace-otlp-proto": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.218.0.tgz", + "integrity": "sha512-r1Msf8SNLRmwh9J6XQ5uh82D7CdDWMNHnPB7LAVHjzut0TkSeKc5KcIvr4SvHvfk/xwN5gxC+VLKQ1k0o8PSPw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1" + "@opentelemetry/otlp-exporter-base": "0.218.0", + "@opentelemetry/otlp-transformer": "0.218.0", + "@opentelemetry/resources": "2.7.1", + "@opentelemetry/sdk-trace-base": "2.7.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/sdk-trace-base": { + "node_modules/@opentelemetry/exporter-zipkin": { "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", - "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.7.1.tgz", + "integrity": "sha512-mfsD9bKAxcKrh5+y08TPodvClBO0CznBE3p79YAGnO81WI4LrdsGA65T53e4iTSbCalW4WaUpkbeJcbpyIUHfg==", "dev": true, "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", + "@opentelemetry/sdk-trace-base": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "@opentelemetry/api": "^1.0.0" } }, - "node_modules/@opentelemetry/exporter-logs-otlp-proto": { + "node_modules/@opentelemetry/instrumentation": { "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.218.0.tgz", - "integrity": "sha512-1/noQNsp9gXD75HPzgjBrcF1+XTtry7pFAUfxVEJgg7mPv2AawKQuYkhMmJ8qjxz4Ubc3Y8bwvfxevXsKTq4cg==", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", + "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", "dev": true, "license": "Apache-2.0", "dependencies": { "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-logs": "0.218.0", - "@opentelemetry/sdk-trace-base": "2.7.1" + "import-in-the-middle": "^3.0.0", + "require-in-the-middle": "^8.0.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1241,2406 +1103,16 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.218.0.tgz", - "integrity": "sha512-ZwqpkNL5W7RyGJPDZ9g06DvKp8KFTWPJPN12anpMQYSKpTSU0z3EIZuPq9vPGpS8siFyOqDYDAuCwlNO9FqgbA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-transformer": "0.218.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/otlp-transformer": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.218.0.tgz", - "integrity": "sha512-CFaKH87WAzjuJ4awowTTLzUvMfaRfiOFG5+qm5S5ncyalRtN4ecQ+YmuANJSCrVPuvZFEkUgKhBPBndxi3rHsQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-logs": "0.218.0", - "@opentelemetry/sdk-metrics": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/sdk-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.218.0.tgz", - "integrity": "sha512-QvnNdugatFTVCJXH0Mcu7GOOJSylA9j127kIezOE4YwTI4YbowRons2K4WZTv5FMS8T4q9P0NdaRHdkSmeAIag==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.1.tgz", - "integrity": "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", - "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.218.0.tgz", - "integrity": "sha512-YapQ9vNMX0NSZF6LK5pWAFfjpJleV2O9uYWfYGeb/5F1Kb9rPGK8tZDMJFa/sOksgdFuflDvYuA0B4qjDB4fjQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/exporter-metrics-otlp-http": "0.218.0", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-metrics": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.218.0.tgz", - "integrity": "sha512-ZwqpkNL5W7RyGJPDZ9g06DvKp8KFTWPJPN12anpMQYSKpTSU0z3EIZuPq9vPGpS8siFyOqDYDAuCwlNO9FqgbA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-transformer": "0.218.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/otlp-transformer": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.218.0.tgz", - "integrity": "sha512-CFaKH87WAzjuJ4awowTTLzUvMfaRfiOFG5+qm5S5ncyalRtN4ecQ+YmuANJSCrVPuvZFEkUgKhBPBndxi3rHsQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-logs": "0.218.0", - "@opentelemetry/sdk-metrics": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/sdk-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.218.0.tgz", - "integrity": "sha512-QvnNdugatFTVCJXH0Mcu7GOOJSylA9j127kIezOE4YwTI4YbowRons2K4WZTv5FMS8T4q9P0NdaRHdkSmeAIag==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.1.tgz", - "integrity": "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", - "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-http": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.218.0.tgz", - "integrity": "sha512-bV7d2OuMpZu2+gAaxUAhzfZ0h3WVZk8ETQUEE3DNSntbTaMpuITjtm8I0rNyHFdm7Ax57K6ty7SgFXlBmOLIvQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-metrics": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.218.0.tgz", - "integrity": "sha512-ZwqpkNL5W7RyGJPDZ9g06DvKp8KFTWPJPN12anpMQYSKpTSU0z3EIZuPq9vPGpS8siFyOqDYDAuCwlNO9FqgbA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-transformer": "0.218.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/otlp-transformer": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.218.0.tgz", - "integrity": "sha512-CFaKH87WAzjuJ4awowTTLzUvMfaRfiOFG5+qm5S5ncyalRtN4ecQ+YmuANJSCrVPuvZFEkUgKhBPBndxi3rHsQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-logs": "0.218.0", - "@opentelemetry/sdk-metrics": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.218.0.tgz", - "integrity": "sha512-QvnNdugatFTVCJXH0Mcu7GOOJSylA9j127kIezOE4YwTI4YbowRons2K4WZTv5FMS8T4q9P0NdaRHdkSmeAIag==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.1.tgz", - "integrity": "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", - "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.218.0.tgz", - "integrity": "sha512-ubLddKjWULhla9YZRCj/rTBeppjJYE4e9w0icx5mTu3eFhWjQzbV75NYjXuIlEG+NJsBl6d+sTFw5Qu+oej4oQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/exporter-metrics-otlp-http": "0.218.0", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-metrics": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.218.0.tgz", - "integrity": "sha512-ZwqpkNL5W7RyGJPDZ9g06DvKp8KFTWPJPN12anpMQYSKpTSU0z3EIZuPq9vPGpS8siFyOqDYDAuCwlNO9FqgbA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-transformer": "0.218.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/otlp-transformer": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.218.0.tgz", - "integrity": "sha512-CFaKH87WAzjuJ4awowTTLzUvMfaRfiOFG5+qm5S5ncyalRtN4ecQ+YmuANJSCrVPuvZFEkUgKhBPBndxi3rHsQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-logs": "0.218.0", - "@opentelemetry/sdk-metrics": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/sdk-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.218.0.tgz", - "integrity": "sha512-QvnNdugatFTVCJXH0Mcu7GOOJSylA9j127kIezOE4YwTI4YbowRons2K4WZTv5FMS8T4q9P0NdaRHdkSmeAIag==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.1.tgz", - "integrity": "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", - "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-prometheus": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.218.0.tgz", - "integrity": "sha512-RT5oEyu1kddZJ1vt7/BUo5wV+P7hpNAESsR3dUd3+8deHuX7gWNoCOZn+SfDT+hJHlIJ5h/AxiCLXIrutswDJg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-metrics": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.1.tgz", - "integrity": "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.218.0.tgz", - "integrity": "sha512-3fXxVQEj9TNAFaCi79JeFKfeLd0sDtInaR3gaZDVlzNSPHtz8PZuCV34JKWjD4XXzT20IdMe8IpX6mRVNDA4Tw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.218.0.tgz", - "integrity": "sha512-ZwqpkNL5W7RyGJPDZ9g06DvKp8KFTWPJPN12anpMQYSKpTSU0z3EIZuPq9vPGpS8siFyOqDYDAuCwlNO9FqgbA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-transformer": "0.218.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/otlp-transformer": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.218.0.tgz", - "integrity": "sha512-CFaKH87WAzjuJ4awowTTLzUvMfaRfiOFG5+qm5S5ncyalRtN4ecQ+YmuANJSCrVPuvZFEkUgKhBPBndxi3rHsQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-logs": "0.218.0", - "@opentelemetry/sdk-metrics": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/sdk-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.218.0.tgz", - "integrity": "sha512-QvnNdugatFTVCJXH0Mcu7GOOJSylA9j127kIezOE4YwTI4YbowRons2K4WZTv5FMS8T4q9P0NdaRHdkSmeAIag==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.1.tgz", - "integrity": "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", - "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-http": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.218.0.tgz", - "integrity": "sha512-8dqezsmPhtKitIK/eTipZhYl9EX2/gNQ5zUMhaz3uxEURwfkNf8IPvo6yNfrzbxdtpAOybS/+h7wmIWYqFSpiw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.218.0.tgz", - "integrity": "sha512-ZwqpkNL5W7RyGJPDZ9g06DvKp8KFTWPJPN12anpMQYSKpTSU0z3EIZuPq9vPGpS8siFyOqDYDAuCwlNO9FqgbA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-transformer": "0.218.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/otlp-transformer": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.218.0.tgz", - "integrity": "sha512-CFaKH87WAzjuJ4awowTTLzUvMfaRfiOFG5+qm5S5ncyalRtN4ecQ+YmuANJSCrVPuvZFEkUgKhBPBndxi3rHsQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-logs": "0.218.0", - "@opentelemetry/sdk-metrics": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/sdk-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.218.0.tgz", - "integrity": "sha512-QvnNdugatFTVCJXH0Mcu7GOOJSylA9j127kIezOE4YwTI4YbowRons2K4WZTv5FMS8T4q9P0NdaRHdkSmeAIag==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.1.tgz", - "integrity": "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", - "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto": { - "version": "0.213.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.213.0.tgz", - "integrity": "sha512-six3vPq3sL+ge1iZOfKEg+RHuFQhGb8ZTdlvD234w/0gi8ty/qKD46qoGpKvM3amy5yYunWBKiFBW47WaVS26w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.6.0", - "@opentelemetry/otlp-exporter-base": "0.213.0", - "@opentelemetry/otlp-transformer": "0.213.0", - "@opentelemetry/resources": "2.6.0", - "@opentelemetry/sdk-trace-base": "2.6.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/core": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.0.tgz", - "integrity": "sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/resources": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.0.tgz", - "integrity": "sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.6.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-zipkin": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.7.1.tgz", - "integrity": "sha512-mfsD9bKAxcKrh5+y08TPodvClBO0CznBE3p79YAGnO81WI4LrdsGA65T53e4iTSbCalW4WaUpkbeJcbpyIUHfg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.0.0" - } - }, - "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", - "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/instrumentation": { - "version": "0.213.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.213.0.tgz", - "integrity": "sha512-3i9NdkET/KvQomeh7UaR/F4r9P25Rx6ooALlWXPIjypcEOUxksCmVu0zA70NBJWlrMW1rPr/LRidFAflLI+s/w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.213.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-amqplib": { - "version": "0.65.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.65.0.tgz", - "integrity": "sha512-fF7fNHA59n3y23ROfst2EbSxmP+L3E+snZO6aMU4w4xD84mfejAivspIAsqa9arX5HZlBK6dslHz5dWGNp5D0A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.218.0", - "@opentelemetry/semantic-conventions": "^1.33.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-amqplib/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-amqplib/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-aws-lambda": { - "version": "0.70.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-lambda/-/instrumentation-aws-lambda-0.70.0.tgz", - "integrity": "sha512-HT74cQxi/iiVEz5dRdNdfGCFzPFbkxSiwHfFPHDwkRcr1JKQqI6hm8qeXEvEiJ+36xIU1KkQMDfeThJ1ifnUiA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.218.0", - "@opentelemetry/semantic-conventions": "^1.27.0", - "@types/aws-lambda": "^8.10.155" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-aws-lambda/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-aws-lambda/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-aws-sdk": { - "version": "0.73.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.73.0.tgz", - "integrity": "sha512-0INPkHbR6o4J3psE+ncwWaE7qtDpb2p+i+qfV82cfwYLCXavYCGosBZ/S4pOErDVJYIyQVIsNAHhaUgaL313SQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.218.0", - "@opentelemetry/semantic-conventions": "^1.34.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-aws-sdk/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-aws-sdk/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-bunyan": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-bunyan/-/instrumentation-bunyan-0.63.0.tgz", - "integrity": "sha512-z0xPSZ62d3I7sG2sUTyQ5/ES1RdESP2eOETiMLY9gPSp+HZwbsAyj7T/2sdZKYD+O2ajRHZEil+DBoUolf1ocQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "^0.218.0", - "@opentelemetry/instrumentation": "^0.218.0", - "@types/bunyan": "1.8.11" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-bunyan/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-bunyan/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-cassandra-driver": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cassandra-driver/-/instrumentation-cassandra-driver-0.63.0.tgz", - "integrity": "sha512-jnVTOr3h/46UDalEwJ4ITux8UWwHmnsOik5WFs3JB/UrUj8Wad5eI+KpOEBuOUeOfPB9sce11qgVw3WXU2r+hg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.218.0", - "@opentelemetry/semantic-conventions": "^1.37.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-cassandra-driver/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-cassandra-driver/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-connect": { - "version": "0.61.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.61.0.tgz", - "integrity": "sha512-ZTQ0W3Lb7GJsOd+72cG8FJQKA5DqYfELJGLmChrJIezRSLfJIfofwKEGLX5rMtFJmwckpichQkBZWjid5dvnVQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.218.0", - "@opentelemetry/semantic-conventions": "^1.27.0", - "@types/connect": "3.4.38" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-connect/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-connect/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-cucumber": { - "version": "0.34.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cucumber/-/instrumentation-cucumber-0.34.0.tgz", - "integrity": "sha512-VK63Cm8osAdsSZpULPk+qnNktQUJzmnIOv2wuh79fV41WuTM38uOFC3s978/24pDkSljhN4EYCbPRLrAhXfKSA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.218.0", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-cucumber/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-cucumber/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-dataloader": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.35.0.tgz", - "integrity": "sha512-6x6UPP0tLzrdj15PIEN3qgp/WCcESCavHJfkIKoyLmy4UjGLF1KgEUMyD74xhbKGo426uvMbhvCgZC0ye8nO/A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.218.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-dataloader/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-dataloader/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-dns": { - "version": "0.61.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dns/-/instrumentation-dns-0.61.0.tgz", - "integrity": "sha512-5D8xFaw9GXq9ZIOAvG7NPDivFfZWFAekLGFn1B7ppyhuAYBVHGybFpx4Q9BV1Uup3yzCdiD78KhyH7c3dKOYSw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.218.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-dns/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-dns/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-document-load": { - "version": "0.58.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-document-load/-/instrumentation-document-load-0.58.0.tgz", - "integrity": "sha512-szTyKkwz3BgHhlUZgQwmj3d9Xz6ZvYJAL/8wehsQyW9HAsfxfWVCaBTfnv/HmkX+zXtXSiwi3W4//WndJsiNsA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.213.0", - "@opentelemetry/sdk-trace-web": "^2.0.0", - "@opentelemetry/semantic-conventions": "^1.23.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-express": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.66.0.tgz", - "integrity": "sha512-G1xTh5M5shklMgIyUXWDjU2BakulKtcISaM4U5TyanvO7R4xbB3iC7YQ8QKegLXaOs81Ku8RlcIcbYRrz/82wQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.218.0", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-express/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-express/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-fetch": { - "version": "0.213.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fetch/-/instrumentation-fetch-0.213.0.tgz", - "integrity": "sha512-A9Gr/iQ4bjQ4m6FOKierMOmI1/MGcRetmG7Y+/SrgV9aefT9/Fn4hFWHwbgZ4dATkEJQ5DIBXVn1sENOe/uQyg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.6.0", - "@opentelemetry/instrumentation": "0.213.0", - "@opentelemetry/sdk-trace-web": "2.6.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-fetch/node_modules/@opentelemetry/core": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.0.tgz", - "integrity": "sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/instrumentation-fs": { - "version": "0.37.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.37.0.tgz", - "integrity": "sha512-5mxhFuwAK0FFvisUdvuywaZ9ySMZ15HfbN6IpLn0gwRh9s1/QBcpLznQ/A15cZs1QFtBJ+JXIHdwY7WOD0c4Eg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.218.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-fs/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-fs/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-generic-pool": { - "version": "0.61.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.61.0.tgz", - "integrity": "sha512-tvp5PWnGRPHY/kz9Kg1IRLBL0qUAxMSNG623f+ZGEsvnCVEjr3tFyw1JGQzM+B3eZKkO+Dp/LYrtOSfb69D5lA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.218.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-generic-pool/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-generic-pool/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-graphql": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.66.0.tgz", - "integrity": "sha512-D4PN1tStj6rnOdofnt2xINJjtT1k2ockzaODrn76VEBZeqJ3QsEvKFfunB0EFAohO4xswVp14VAVmKNnGzA1Dw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.218.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-graphql/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-graphql/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-grpc": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-grpc/-/instrumentation-grpc-0.218.0.tgz", - "integrity": "sha512-kcDCNrC7IWNXEKQriGrwuh5jjbMFU5exOQzU9ufEY9UkACNcgYIdOd7XpX3IqZ3UPSnZyZtlwgfsbC5SNlEDbA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "0.218.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-grpc/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-grpc/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-hapi": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.64.0.tgz", - "integrity": "sha512-PCHgCICCDz7p9BgCU9gQz2smbqu4V4P8QtWJ7DLjL3bmzSdrgy6EGvecDg1YuhjBsoN08SR+y36hgdHkqCgrzQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.218.0", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-hapi/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-hapi/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-http": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.218.0.tgz", - "integrity": "sha512-x9djaqdzpT8WAboep1H9nCAQ1E+MMsm08TNfA02TqM3bNNddZeiim+E3KMWVQFaX6JpUy7V0nm/wfN/K2Em+Zw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/instrumentation": "0.218.0", - "@opentelemetry/semantic-conventions": "^1.29.0", - "forwarded-parse": "2.1.2" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-ioredis": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.66.0.tgz", - "integrity": "sha512-UfTAcaBKCzLUZ9opvfOLV4bH46XiNFqUsKykfPCIefDIxJ1iUYtMOucNaiZ+/kjQdPy5i6Ef5tk2IAjxol4X1w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.218.0", - "@opentelemetry/redis-common": "^0.38.3", - "@opentelemetry/semantic-conventions": "^1.33.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-ioredis/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-ioredis/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-kafkajs": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.27.0.tgz", - "integrity": "sha512-kl/C2AU4KZGHlMZD12nMFXcMjxSHvu5Q0UPSQ6IJeBfCadYuWgW+sWIa2JZVK/A0qRYm2cncekJyeBHQDyfUUg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.218.0", - "@opentelemetry/semantic-conventions": "^1.30.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-kafkajs/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-kafkajs/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-knex": { - "version": "0.62.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.62.0.tgz", - "integrity": "sha512-XgfhCAWwSqA0YnwaEKdpvQMavc90D3R65frhLCO9JNl867EulNps9tm6pjGIg+GiYuewn00gEzW4HQ5btgYxGQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.218.0", - "@opentelemetry/semantic-conventions": "^1.33.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-knex/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-knex/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-koa": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.66.0.tgz", - "integrity": "sha512-04x/z21WTMEfy3lUSr4aTj8WsTN3OZF901hJ+ciOwdwf7AK8UJTpZCXw6KQ3G4Vag56q1HoMihCONeWZLeld1g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.218.0", - "@opentelemetry/semantic-conventions": "^1.36.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.9.0" - } - }, - "node_modules/@opentelemetry/instrumentation-koa/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-koa/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-lru-memoizer": { - "version": "0.62.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.62.0.tgz", - "integrity": "sha512-AlGKIdk6ZT7WmIozfUb2LjOcI3AhQrvAXKX0zi1cVcnw2QlRbVYyV5GTa2Th9ebuczVfWPaoPrmZw61zCp/czw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.218.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-lru-memoizer/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-lru-memoizer/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-memcached": { - "version": "0.61.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-memcached/-/instrumentation-memcached-0.61.0.tgz", - "integrity": "sha512-qiCR9Wovf5AHzn6g+LXhvwMmv2I6zhHz2I2tEHZMmBuD8c18bkJzGFxHoSBlxdApRT+SW13r9472dDMm4BRjgQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.218.0", - "@opentelemetry/semantic-conventions": "^1.33.0", - "@types/memcached": "^2.2.6" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-memcached/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-memcached/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mongodb": { - "version": "0.71.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.71.0.tgz", - "integrity": "sha512-6rwfVjAUY69CKkyGqzL+F5X7Nzw0+Ke9pOxk9xUPJpy8vracZxuQYF7rWu02sV1xOgi4u52449SuVhD+zaSiIA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.218.0", - "@opentelemetry/semantic-conventions": "^1.33.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mongodb/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mongodb/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mongoose": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.64.0.tgz", - "integrity": "sha512-iCIqeUaERN8Uc5Rrtg4zvQ6d7z5JQ5iUmbnr/JHYPxAidDowmRc8/wDMJeMKRfLPTj336Zu0ec7rH/ak/4N9vw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.218.0", - "@opentelemetry/semantic-conventions": "^1.33.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mongoose/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mongoose/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mysql": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.64.0.tgz", - "integrity": "sha512-W1w76AJkP7i0uzzAe7nsCMWq4+EMSA550f1lAmxDPdQC5FnreNbRIm/tod2OS9gVrYvRrQXNkFmZJKGo4kzCnw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.218.0", - "@opentelemetry/semantic-conventions": "^1.33.0", - "@types/mysql": "2.15.27" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mysql/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mysql/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mysql2": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.64.0.tgz", - "integrity": "sha512-yTu0mYh/qJPSE86VmNLQww5uugDyvCS2KJIPfPtIk2ufoEUoHPsV6Iynnvmz588Moq04aBLxfTa/EtE4A2ykWA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.218.0", - "@opentelemetry/semantic-conventions": "^1.33.0", - "@opentelemetry/sql-common": "^0.41.2" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mysql2/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mysql2/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-nestjs-core": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.64.0.tgz", - "integrity": "sha512-PW1ArxryMwF8/IXq1nzlQs7tmr/fWd1tf71AHevZT3Fm0hW7jRX9JEfYgIAcKDvmbqcJEr5K1224NEimrRPbuQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.218.0", - "@opentelemetry/semantic-conventions": "^1.30.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-nestjs-core/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-nestjs-core/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-net": { - "version": "0.62.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-net/-/instrumentation-net-0.62.0.tgz", - "integrity": "sha512-Gt2kzpACpmIad+q3LQqe8UNHuoVvdLuFpB6SN/A6xLPKNllb+ksPUYQhj1kXdZOpcFZNGKDXHyN+TUCVCk1TRw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.218.0", - "@opentelemetry/semantic-conventions": "^1.33.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-net/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-net/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-openai": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-openai/-/instrumentation-openai-0.16.0.tgz", - "integrity": "sha512-I0KKybyqqFOxSBgYKQNdR/EF3LvzSaAUT7Y75xkjbgscY+V8UWDpUbY68POLhUC3SKMlGvZmrTSxcQ+Y0vRhNw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "^0.218.0", - "@opentelemetry/instrumentation": "^0.218.0", - "@opentelemetry/semantic-conventions": "^1.36.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-openai/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-openai/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-oracledb": { - "version": "0.43.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-oracledb/-/instrumentation-oracledb-0.43.0.tgz", - "integrity": "sha512-7Z4kOOdnrHX4S5gCeWhnnpWQwEd7weRjDhJA1nSrwTYtAcVWNjk5wsMKHBCTDCN0uJtA9T6PouZ+AKRYiS1Rrg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.218.0", - "@opentelemetry/semantic-conventions": "^1.34.0", - "@types/oracledb": "6.5.2" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-oracledb/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-oracledb/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-pg": { - "version": "0.70.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.70.0.tgz", - "integrity": "sha512-g8WXwwOUXfjiEmATwjB/33QKE2AkIpNe4KIuJJh4djtXgCL0Wne+AzAfjuDIAspGvO1txQp8ibKsLd3SBmcvJA==", + "node_modules/@opentelemetry/instrumentation-amqplib": { + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.65.0.tgz", + "integrity": "sha512-fF7fNHA59n3y23ROfst2EbSxmP+L3E+snZO6aMU4w4xD84mfejAivspIAsqa9arX5HZlBK6dslHz5dWGNp5D0A==", "dev": true, "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.218.0", - "@opentelemetry/semantic-conventions": "^1.34.0", - "@opentelemetry/sql-common": "^0.41.2", - "@types/pg": "8.15.6", - "@types/pg-pool": "2.0.7" + "@opentelemetry/semantic-conventions": "^1.33.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3649,29 +1121,16 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-pg/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-pg/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", + "node_modules/@opentelemetry/instrumentation-aws-lambda": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-lambda/-/instrumentation-aws-lambda-0.70.0.tgz", + "integrity": "sha512-HT74cQxi/iiVEz5dRdNdfGCFzPFbkxSiwHfFPHDwkRcr1JKQqI6hm8qeXEvEiJ+36xIU1KkQMDfeThJ1ifnUiA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/aws-lambda": "^8.10.155" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3680,96 +1139,16 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-pino": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pino/-/instrumentation-pino-0.64.0.tgz", - "integrity": "sha512-+vDL7tZMZjkp8BpYMx/cL2/HWGsNUqKcRmAIIEaQu/6F44oM6xGDMCSqMKHdKCsH1+WW52EYdHbWkVGTF0KVsQ==", + "node_modules/@opentelemetry/instrumentation-aws-sdk": { + "version": "0.73.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.73.0.tgz", + "integrity": "sha512-0INPkHbR6o4J3psE+ncwWaE7qtDpb2p+i+qfV82cfwYLCXavYCGosBZ/S4pOErDVJYIyQVIsNAHhaUgaL313SQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "^0.218.0", "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.218.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-pino/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-pino/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-redis": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.66.0.tgz", - "integrity": "sha512-bVShkag6vP2VQO0cpA8CHjOohWbKNYLyjiwGkOnSAwou1TPc6pf9DssFUxwqN2XF1J4oqP0LVSvN9kZUzMecfA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { "@opentelemetry/instrumentation": "^0.218.0", - "@opentelemetry/redis-common": "^0.38.3", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-redis/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-redis/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" + "@opentelemetry/semantic-conventions": "^1.34.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3778,47 +1157,16 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-restify": { + "node_modules/@opentelemetry/instrumentation-bunyan": { "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-restify/-/instrumentation-restify-0.63.0.tgz", - "integrity": "sha512-Z73YxZpt0Y56uRu2pRWOjO5wXHvZqF46K4czoKRTGlUifzzFmUZxyOeAAECACuMRSLZmZ394WJin0MDgU9iW9w==", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-bunyan/-/instrumentation-bunyan-0.63.0.tgz", + "integrity": "sha512-z0xPSZ62d3I7sG2sUTyQ5/ES1RdESP2eOETiMLY9gPSp+HZwbsAyj7T/2sdZKYD+O2ajRHZEil+DBoUolf1ocQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "^2.0.0", + "@opentelemetry/api-logs": "^0.218.0", "@opentelemetry/instrumentation": "^0.218.0", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-restify/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-restify/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" + "@types/bunyan": "1.8.11" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3827,111 +1175,15 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-router": { - "version": "0.62.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-router/-/instrumentation-router-0.62.0.tgz", - "integrity": "sha512-0w8ok7GbXtYvX7TtLp72qQJKNyI7lD72Fy2NsNKIcQAv6TqGox5javFyXrIrCAtZHCONePxeAwAYj1Qd9si9OQ==", + "node_modules/@opentelemetry/instrumentation-cassandra-driver": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cassandra-driver/-/instrumentation-cassandra-driver-0.63.0.tgz", + "integrity": "sha512-jnVTOr3h/46UDalEwJ4ITux8UWwHmnsOik5WFs3JB/UrUj8Wad5eI+KpOEBuOUeOfPB9sce11qgVw3WXU2r+hg==", "dev": true, "license": "Apache-2.0", "dependencies": { "@opentelemetry/instrumentation": "^0.218.0", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-router/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-router/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-runtime-node": { - "version": "0.31.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-runtime-node/-/instrumentation-runtime-node-0.31.0.tgz", - "integrity": "sha512-HkLsuEfUDahFiL/xFtEqJDMp7sp8ynOtA045bJi9nAH8CrPvljPW5SgJQb2mQqEYJQopbWYZ2lPqQEfj7bYgJg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "^0.218.0", - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.218.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-runtime-node/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-runtime-node/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-socket.io": { - "version": "0.65.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-socket.io/-/instrumentation-socket.io-0.65.0.tgz", - "integrity": "sha512-dNvIbD40h0z69stQ9cIeAWRyy5WyQM1a1XnFthekc/oi/ipX4E6oYJBM4X2xKBxjZMTjdV5VshLoNeYMSBsnjw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.218.0" + "@opentelemetry/semantic-conventions": "^1.37.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3940,47 +1192,50 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-socket.io/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", + "node_modules/@opentelemetry/instrumentation-connect": { + "version": "0.61.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.61.0.tgz", + "integrity": "sha512-ZTQ0W3Lb7GJsOd+72cG8FJQKA5DqYfELJGLmChrJIezRSLfJIfofwKEGLX5rMtFJmwckpichQkBZWjid5dvnVQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/connect": "3.4.38" }, "engines": { - "node": ">=8.0.0" + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-socket.io/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", + "node_modules/@opentelemetry/instrumentation-cucumber": { + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cucumber/-/instrumentation-cucumber-0.34.0.tgz", + "integrity": "sha512-VK63Cm8osAdsSZpULPk+qnNktQUJzmnIOv2wuh79fV41WuTM38uOFC3s978/24pDkSljhN4EYCbPRLrAhXfKSA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": "^1.0.0" } }, - "node_modules/@opentelemetry/instrumentation-tedious": { - "version": "0.37.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.37.0.tgz", - "integrity": "sha512-cGLF46UsgeI1334atJxLO36yQlV7WXKg35Mp+e2NXo2vOTfIZTVqoKOzExVOTOwT4AQjfGVEDxyq5wXybUYXIA==", + "node_modules/@opentelemetry/instrumentation-dataloader": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.35.0.tgz", + "integrity": "sha512-6x6UPP0tLzrdj15PIEN3qgp/WCcESCavHJfkIKoyLmy4UjGLF1KgEUMyD74xhbKGo426uvMbhvCgZC0ye8nO/A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.218.0", - "@opentelemetry/semantic-conventions": "^1.33.0", - "@types/tedious": "^4.0.14" + "@opentelemetry/instrumentation": "^0.218.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3989,29 +1244,33 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-tedious/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", + "node_modules/@opentelemetry/instrumentation-dns": { + "version": "0.61.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dns/-/instrumentation-dns-0.61.0.tgz", + "integrity": "sha512-5D8xFaw9GXq9ZIOAvG7NPDivFfZWFAekLGFn1B7ppyhuAYBVHGybFpx4Q9BV1Uup3yzCdiD78KhyH7c3dKOYSw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/instrumentation": "^0.218.0" }, "engines": { - "node": ">=8.0.0" + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-tedious/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", + "node_modules/@opentelemetry/instrumentation-document-load": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-document-load/-/instrumentation-document-load-0.63.0.tgz", + "integrity": "sha512-eVQ2IcDeWFMLgnmeVGvgGSMIdomrEat56fDfA9jXYExgr/1xClO0MpHKDDfInGV2o6STtzarcrsBgwrnV8cm7w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/sdk-trace-web": "^2.0.0", + "@opentelemetry/semantic-conventions": "^1.23.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4020,47 +1279,52 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-undici": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.28.0.tgz", - "integrity": "sha512-7nh4Gw7PhYtQm82FIJtWUhx6iZQJj0bdkKe2RQb3XNIyxu0o9rM1J5Xt083SsG2tCbQZpX9/mlDxhTrK1Z/lVQ==", + "node_modules/@opentelemetry/instrumentation-express": { + "version": "0.66.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.66.0.tgz", + "integrity": "sha512-G1xTh5M5shklMgIyUXWDjU2BakulKtcISaM4U5TyanvO7R4xbB3iC7YQ8QKegLXaOs81Ku8RlcIcbYRrz/82wQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.218.0", - "@opentelemetry/semantic-conventions": "^1.24.0" + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.7.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-undici/node_modules/@opentelemetry/api-logs": { + "node_modules/@opentelemetry/instrumentation-fetch": { "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fetch/-/instrumentation-fetch-0.218.0.tgz", + "integrity": "sha512-eP/Y5hDupb+6MwZSaMw4ZdsDz8YgfJbJ4Ta86BMVeOmI2EArwXcd0v1nfNIvUzXTPi7nakidwqeuUa3FwRwECg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/core": "2.7.1", + "@opentelemetry/instrumentation": "0.218.0", + "@opentelemetry/sdk-trace-web": "2.7.1", + "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { - "node": ">=8.0.0" + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-undici/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", + "node_modules/@opentelemetry/instrumentation-fs": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.37.0.tgz", + "integrity": "sha512-5mxhFuwAK0FFvisUdvuywaZ9ySMZ15HfbN6IpLn0gwRh9s1/QBcpLznQ/A15cZs1QFtBJ+JXIHdwY7WOD0c4Eg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.218.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4069,14 +1333,13 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-winston": { - "version": "0.62.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-winston/-/instrumentation-winston-0.62.0.tgz", - "integrity": "sha512-pr1U9ZV4RRy23qMVrRzebfxwDWjp44xA7sC0PAdeW9v4HDcfOr0ejdTJmIsBGvhkNHPBajfieaIF9b6/9wjErA==", + "node_modules/@opentelemetry/instrumentation-generic-pool": { + "version": "0.61.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.61.0.tgz", + "integrity": "sha512-tvp5PWnGRPHY/kz9Kg1IRLBL0qUAxMSNG623f+ZGEsvnCVEjr3tFyw1JGQzM+B3eZKkO+Dp/LYrtOSfb69D5lA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "^0.218.0", "@opentelemetry/instrumentation": "^0.218.0" }, "engines": { @@ -4086,29 +1349,31 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-winston/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", + "node_modules/@opentelemetry/instrumentation-graphql": { + "version": "0.66.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.66.0.tgz", + "integrity": "sha512-D4PN1tStj6rnOdofnt2xINJjtT1k2ockzaODrn76VEBZeqJ3QsEvKFfunB0EFAohO4xswVp14VAVmKNnGzA1Dw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/instrumentation": "^0.218.0" }, "engines": { - "node": ">=8.0.0" + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-winston/node_modules/@opentelemetry/instrumentation": { + "node_modules/@opentelemetry/instrumentation-grpc": { "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-grpc/-/instrumentation-grpc-0.218.0.tgz", + "integrity": "sha512-kcDCNrC7IWNXEKQriGrwuh5jjbMFU5exOQzU9ufEY9UkACNcgYIdOd7XpX3IqZ3UPSnZyZtlwgfsbC5SNlEDbA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" + "@opentelemetry/instrumentation": "0.218.0", + "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4117,15 +1382,16 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.213.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.213.0.tgz", - "integrity": "sha512-MegxAP1/n09Ob2dQvY5NBDVjAFkZRuKtWKxYev1R2M8hrsgXzQGkaMgoEKeUOyQ0FUyYcO29UOnYdQWmWa0PXg==", + "node_modules/@opentelemetry/instrumentation-hapi": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.64.0.tgz", + "integrity": "sha512-PCHgCICCDz7p9BgCU9gQz2smbqu4V4P8QtWJ7DLjL3bmzSdrgy6EGvecDg1YuhjBsoN08SR+y36hgdHkqCgrzQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.0", - "@opentelemetry/otlp-transformer": "0.213.0" + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4134,33 +1400,35 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/core": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.0.tgz", - "integrity": "sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==", + "node_modules/@opentelemetry/instrumentation-http": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.218.0.tgz", + "integrity": "sha512-x9djaqdzpT8WAboep1H9nCAQ1E+MMsm08TNfA02TqM3bNNddZeiim+E3KMWVQFaX6JpUy7V0nm/wfN/K2Em+Zw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" + "@opentelemetry/core": "2.7.1", + "@opentelemetry/instrumentation": "0.218.0", + "@opentelemetry/semantic-conventions": "^1.29.0", + "forwarded-parse": "2.1.2" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.218.0.tgz", - "integrity": "sha512-H/lCGJ536N98VpYJOaWTQOkv4Dx6TnmStK6Rqfu1W7KkFbPAx04hjdYEMZF/YbnHzPUSIK4kM6OE2GKGBTpV9A==", + "node_modules/@opentelemetry/instrumentation-ioredis": { + "version": "0.66.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.66.0.tgz", + "integrity": "sha512-UfTAcaBKCzLUZ9opvfOLV4bH46XiNFqUsKykfPCIefDIxJ1iUYtMOucNaiZ+/kjQdPy5i6Ef5tk2IAjxol4X1w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0" + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/redis-common": "^0.38.3", + "@opentelemetry/semantic-conventions": "^1.33.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4169,28 +1437,32 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", + "node_modules/@opentelemetry/instrumentation-kafkajs": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.27.0.tgz", + "integrity": "sha512-kl/C2AU4KZGHlMZD12nMFXcMjxSHvu5Q0UPSQ6IJeBfCadYuWgW+sWIa2JZVK/A0qRYm2cncekJyeBHQDyfUUg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.30.0" }, "engines": { - "node": ">=8.0.0" + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.218.0.tgz", - "integrity": "sha512-ZwqpkNL5W7RyGJPDZ9g06DvKp8KFTWPJPN12anpMQYSKpTSU0z3EIZuPq9vPGpS8siFyOqDYDAuCwlNO9FqgbA==", + "node_modules/@opentelemetry/instrumentation-knex": { + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.62.0.tgz", + "integrity": "sha512-XgfhCAWwSqA0YnwaEKdpvQMavc90D3R65frhLCO9JNl867EulNps9tm6pjGIg+GiYuewn00gEzW4HQ5btgYxGQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-transformer": "0.218.0" + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.33.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4199,95 +1471,85 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/otlp-transformer": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.218.0.tgz", - "integrity": "sha512-CFaKH87WAzjuJ4awowTTLzUvMfaRfiOFG5+qm5S5ncyalRtN4ecQ+YmuANJSCrVPuvZFEkUgKhBPBndxi3rHsQ==", + "node_modules/@opentelemetry/instrumentation-koa": { + "version": "0.66.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.66.0.tgz", + "integrity": "sha512-04x/z21WTMEfy3lUSr4aTj8WsTN3OZF901hJ+ciOwdwf7AK8UJTpZCXw6KQ3G4Vag56q1HoMihCONeWZLeld1g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-logs": "0.218.0", - "@opentelemetry/sdk-metrics": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.36.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": "^1.9.0" } }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/sdk-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.218.0.tgz", - "integrity": "sha512-QvnNdugatFTVCJXH0Mcu7GOOJSylA9j127kIezOE4YwTI4YbowRons2K4WZTv5FMS8T4q9P0NdaRHdkSmeAIag==", + "node_modules/@opentelemetry/instrumentation-lru-memoizer": { + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.62.0.tgz", + "integrity": "sha512-AlGKIdk6ZT7WmIozfUb2LjOcI3AhQrvAXKX0zi1cVcnw2QlRbVYyV5GTa2Th9ebuczVfWPaoPrmZw61zCp/czw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@opentelemetry/instrumentation": "^0.218.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.1.tgz", - "integrity": "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==", + "node_modules/@opentelemetry/instrumentation-memcached": { + "version": "0.61.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-memcached/-/instrumentation-memcached-0.61.0.tgz", + "integrity": "sha512-qiCR9Wovf5AHzn6g+LXhvwMmv2I6zhHz2I2tEHZMmBuD8c18bkJzGFxHoSBlxdApRT+SW13r9472dDMm4BRjgQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1" + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.33.0", + "@types/memcached": "^2.2.6" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", - "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", + "node_modules/@opentelemetry/instrumentation-mongodb": { + "version": "0.71.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.71.0.tgz", + "integrity": "sha512-6rwfVjAUY69CKkyGqzL+F5X7Nzw0+Ke9pOxk9xUPJpy8vracZxuQYF7rWu02sV1xOgi4u52449SuVhD+zaSiIA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.33.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-transformer": { - "version": "0.213.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.213.0.tgz", - "integrity": "sha512-RSuAlxFFPjeK4d5Y6ps8L2WhaQI6CXWllIjvo5nkAlBpmq2XdYWEBGiAbOF4nDs8CX4QblJDv5BbMUft3sEfDw==", + "node_modules/@opentelemetry/instrumentation-mongoose": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.64.0.tgz", + "integrity": "sha512-iCIqeUaERN8Uc5Rrtg4zvQ6d7z5JQ5iUmbnr/JHYPxAidDowmRc8/wDMJeMKRfLPTj336Zu0ec7rH/ak/4N9vw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.213.0", - "@opentelemetry/core": "2.6.0", - "@opentelemetry/resources": "2.6.0", - "@opentelemetry/sdk-logs": "0.213.0", - "@opentelemetry/sdk-metrics": "2.6.0", - "@opentelemetry/sdk-trace-base": "2.6.0", - "protobufjs": "^7.0.0" + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.33.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4296,480 +1558,486 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/core": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.0.tgz", - "integrity": "sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==", + "node_modules/@opentelemetry/instrumentation-mysql": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.64.0.tgz", + "integrity": "sha512-W1w76AJkP7i0uzzAe7nsCMWq4+EMSA550f1lAmxDPdQC5FnreNbRIm/tod2OS9gVrYvRrQXNkFmZJKGo4kzCnw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.33.0", + "@types/mysql": "2.15.27" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.0.tgz", - "integrity": "sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ==", + "node_modules/@opentelemetry/instrumentation-mysql2": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.64.0.tgz", + "integrity": "sha512-yTu0mYh/qJPSE86VmNLQww5uugDyvCS2KJIPfPtIk2ufoEUoHPsV6Iynnvmz588Moq04aBLxfTa/EtE4A2ykWA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.0", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.33.0", + "@opentelemetry/sql-common": "^0.41.2" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/propagator-b3": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.7.1.tgz", - "integrity": "sha512-RJid6E2CKyeGfKBzXKF21ejabGMHypFkPAh3qZ+NvI+SGjuIye79t3PmiqcDgtRzdKH6ynXzbfslQ8DfpRUg2A==", + "node_modules/@opentelemetry/instrumentation-nestjs-core": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.64.0.tgz", + "integrity": "sha512-PW1ArxryMwF8/IXq1nzlQs7tmr/fWd1tf71AHevZT3Fm0hW7jRX9JEfYgIAcKDvmbqcJEr5K1224NEimrRPbuQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1" + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.30.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/propagator-jaeger": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.7.1.tgz", - "integrity": "sha512-KMjVBHzP4N60bOzxja76M1F1hZZ43lGPga5ix+mkv9+kk1nx9SbkxSvJsMbuVUxdPQmsPTqGShmhN8ulrMOg6Q==", + "node_modules/@opentelemetry/instrumentation-net": { + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-net/-/instrumentation-net-0.62.0.tgz", + "integrity": "sha512-Gt2kzpACpmIad+q3LQqe8UNHuoVvdLuFpB6SN/A6xLPKNllb+ksPUYQhj1kXdZOpcFZNGKDXHyN+TUCVCk1TRw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1" + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.33.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/redis-common": { - "version": "0.38.3", - "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.38.3.tgz", - "integrity": "sha512-VCghU1JYs/4gP6Gqf/xro9MEsZ7LrMv2uONVsaESKL38ZOB9BqnI98FfS23wjMnHlpuE+TTaWSoAVNpTwYXzjw==", + "node_modules/@opentelemetry/instrumentation-openai": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-openai/-/instrumentation-openai-0.16.0.tgz", + "integrity": "sha512-I0KKybyqqFOxSBgYKQNdR/EF3LvzSaAUT7Y75xkjbgscY+V8UWDpUbY68POLhUC3SKMlGvZmrTSxcQ+Y0vRhNw==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.218.0", + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.36.0" + }, "engines": { "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/resource-detector-alibaba-cloud": { - "version": "0.33.8", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-alibaba-cloud/-/resource-detector-alibaba-cloud-0.33.8.tgz", - "integrity": "sha512-RnSB/uxkElny0/WBFEtIG2HRG0cpSNTRdE+YSB7Poa+uljK+ddCacEZYz/PMgZh+cs586XstJQxdyjz0jtcAug==", + "node_modules/@opentelemetry/instrumentation-oracledb": { + "version": "0.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-oracledb/-/instrumentation-oracledb-0.43.0.tgz", + "integrity": "sha512-7Z4kOOdnrHX4S5gCeWhnnpWQwEd7weRjDhJA1nSrwTYtAcVWNjk5wsMKHBCTDCN0uJtA9T6PouZ+AKRYiS1Rrg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/resources": "^2.0.0" + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.34.0", + "@types/oracledb": "6.5.2" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.0.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/resource-detector-aws": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-aws/-/resource-detector-aws-2.18.0.tgz", - "integrity": "sha512-wyMM4UoRuHvI2KjqnTzvyW8Yv7MKRGA+I78Xti6gTEw7hBhqXU1SRo+f9KrsQfeeiOn+TkDuvxavuaAQbD3i6g==", + "node_modules/@opentelemetry/instrumentation-pg": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.70.0.tgz", + "integrity": "sha512-g8WXwwOUXfjiEmATwjB/33QKE2AkIpNe4KIuJJh4djtXgCL0Wne+AzAfjuDIAspGvO1txQp8ibKsLd3SBmcvJA==", "dev": true, "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", - "@opentelemetry/resources": "^2.0.0", - "@opentelemetry/semantic-conventions": "^1.27.0" + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.34.0", + "@opentelemetry/sql-common": "^0.41.2", + "@types/pg": "8.15.6", + "@types/pg-pool": "2.0.7" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.0.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/resource-detector-azure": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-azure/-/resource-detector-azure-0.26.0.tgz", - "integrity": "sha512-7KxF7mlwI2nKja/iEdwPqOaS0QAJbhT9ye4DeYZnXdOS/4phfonk5nSmyGDBYhBL7J30MPL91oZNuGYRKXZAXA==", + "node_modules/@opentelemetry/instrumentation-pino": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pino/-/instrumentation-pino-0.64.0.tgz", + "integrity": "sha512-+vDL7tZMZjkp8BpYMx/cL2/HWGsNUqKcRmAIIEaQu/6F44oM6xGDMCSqMKHdKCsH1+WW52EYdHbWkVGTF0KVsQ==", "dev": true, "license": "Apache-2.0", "dependencies": { + "@opentelemetry/api-logs": "^0.218.0", "@opentelemetry/core": "^2.0.0", - "@opentelemetry/resources": "^2.0.0", - "@opentelemetry/semantic-conventions": "^1.37.0" + "@opentelemetry/instrumentation": "^0.218.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.0.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/resource-detector-container": { - "version": "0.8.9", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-container/-/resource-detector-container-0.8.9.tgz", - "integrity": "sha512-Xd2C4HjW9hl75iqZT7tQNy2yRBUqNucq2O9+e0FJRNkbiItInYVMzc0S0KDXcx/vZBwNmlrKS3R0uLCU9ULsGA==", + "node_modules/@opentelemetry/instrumentation-redis": { + "version": "0.66.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.66.0.tgz", + "integrity": "sha512-bVShkag6vP2VQO0cpA8CHjOohWbKNYLyjiwGkOnSAwou1TPc6pf9DssFUxwqN2XF1J4oqP0LVSvN9kZUzMecfA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/resources": "^2.0.0" + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/redis-common": "^0.38.3", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.0.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/resource-detector-gcp": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-gcp/-/resource-detector-gcp-0.53.0.tgz", - "integrity": "sha512-RCV31v23ZwZfYR3LPkuORHTHIOvfm3hZBT7hAzSO0+oAIrG/Dm0ld5tV4lYNO05GjI7sHQdRcbSqzEYAvQcQuw==", + "node_modules/@opentelemetry/instrumentation-restify": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-restify/-/instrumentation-restify-0.63.0.tgz", + "integrity": "sha512-Z73YxZpt0Y56uRu2pRWOjO5wXHvZqF46K4czoKRTGlUifzzFmUZxyOeAAECACuMRSLZmZ394WJin0MDgU9iW9w==", "dev": true, "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.0.0", - "@opentelemetry/resources": "^2.0.0", - "gcp-metadata": "^8.0.0" + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.0.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/resources": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", - "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", + "node_modules/@opentelemetry/instrumentation-router": { + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-router/-/instrumentation-router-0.62.0.tgz", + "integrity": "sha512-0w8ok7GbXtYvX7TtLp72qQJKNyI7lD72Fy2NsNKIcQAv6TqGox5javFyXrIrCAtZHCONePxeAwAYj1Qd9si9OQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/sdk-logs": { - "version": "0.213.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.213.0.tgz", - "integrity": "sha512-00xlU3GZXo3kXKve4DLdrAL0NAFUaZ9appU/mn00S/5kSUdAvyYsORaDUfR04Mp2CLagAOhrzfUvYozY/EZX2g==", + "node_modules/@opentelemetry/instrumentation-runtime-node": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-runtime-node/-/instrumentation-runtime-node-0.31.0.tgz", + "integrity": "sha512-HkLsuEfUDahFiL/xFtEqJDMp7sp8ynOtA045bJi9nAH8CrPvljPW5SgJQb2mQqEYJQopbWYZ2lPqQEfj7bYgJg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.213.0", - "@opentelemetry/core": "2.6.0", - "@opentelemetry/resources": "2.6.0", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@opentelemetry/api-logs": "^0.218.0", + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.218.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/core": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.0.tgz", - "integrity": "sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==", + "node_modules/@opentelemetry/instrumentation-socket.io": { + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-socket.io/-/instrumentation-socket.io-0.65.0.tgz", + "integrity": "sha512-dNvIbD40h0z69stQ9cIeAWRyy5WyQM1a1XnFthekc/oi/ipX4E6oYJBM4X2xKBxjZMTjdV5VshLoNeYMSBsnjw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" + "@opentelemetry/instrumentation": "^0.218.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.0.tgz", - "integrity": "sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ==", + "node_modules/@opentelemetry/instrumentation-tedious": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.37.0.tgz", + "integrity": "sha512-cGLF46UsgeI1334atJxLO36yQlV7WXKg35Mp+e2NXo2vOTfIZTVqoKOzExVOTOwT4AQjfGVEDxyq5wXybUYXIA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.0", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.33.0", + "@types/tedious": "^4.0.14" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/sdk-metrics": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.6.0.tgz", - "integrity": "sha512-CicxWZxX6z35HR83jl+PLgtFgUrKRQ9LCXyxgenMnz5A1lgYWfAog7VtdOvGkJYyQgMNPhXQwkYrDLujk7z1Iw==", + "node_modules/@opentelemetry/instrumentation-undici": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.28.0.tgz", + "integrity": "sha512-7nh4Gw7PhYtQm82FIJtWUhx6iZQJj0bdkKe2RQb3XNIyxu0o9rM1J5Xt083SsG2tCbQZpX9/mlDxhTrK1Z/lVQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.0", - "@opentelemetry/resources": "2.6.0" + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.24.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" + "@opentelemetry/api": "^1.7.0" } }, - "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/core": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.0.tgz", - "integrity": "sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==", + "node_modules/@opentelemetry/instrumentation-winston": { + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-winston/-/instrumentation-winston-0.62.0.tgz", + "integrity": "sha512-pr1U9ZV4RRy23qMVrRzebfxwDWjp44xA7sC0PAdeW9v4HDcfOr0ejdTJmIsBGvhkNHPBajfieaIF9b6/9wjErA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" + "@opentelemetry/api-logs": "^0.218.0", + "@opentelemetry/instrumentation": "^0.218.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.0.tgz", - "integrity": "sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ==", + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.218.0.tgz", + "integrity": "sha512-ZwqpkNL5W7RyGJPDZ9g06DvKp8KFTWPJPN12anpMQYSKpTSU0z3EIZuPq9vPGpS8siFyOqDYDAuCwlNO9FqgbA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.0", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@opentelemetry/core": "2.7.1", + "@opentelemetry/otlp-transformer": "0.218.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/sdk-node": { + "node_modules/@opentelemetry/otlp-grpc-exporter-base": { "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.218.0.tgz", - "integrity": "sha512-tPMjHrLV5gsfNdYqoRHjeGbCAZBXXD9c1Qo/2ut7VwnUABDNh76xNxrT0SEhkIIJuCN45bbN1vZnYL1gY0IkOg==", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.218.0.tgz", + "integrity": "sha512-H/lCGJ536N98VpYJOaWTQOkv4Dx6TnmStK6Rqfu1W7KkFbPAx04hjdYEMZF/YbnHzPUSIK4kM6OE2GKGBTpV9A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/configuration": "0.218.0", - "@opentelemetry/context-async-hooks": "2.7.1", + "@grpc/grpc-js": "^1.14.3", "@opentelemetry/core": "2.7.1", - "@opentelemetry/exporter-logs-otlp-grpc": "0.218.0", - "@opentelemetry/exporter-logs-otlp-http": "0.218.0", - "@opentelemetry/exporter-logs-otlp-proto": "0.218.0", - "@opentelemetry/exporter-metrics-otlp-grpc": "0.218.0", - "@opentelemetry/exporter-metrics-otlp-http": "0.218.0", - "@opentelemetry/exporter-metrics-otlp-proto": "0.218.0", - "@opentelemetry/exporter-prometheus": "0.218.0", - "@opentelemetry/exporter-trace-otlp-grpc": "0.218.0", - "@opentelemetry/exporter-trace-otlp-http": "0.218.0", - "@opentelemetry/exporter-trace-otlp-proto": "0.218.0", - "@opentelemetry/exporter-zipkin": "2.7.1", - "@opentelemetry/instrumentation": "0.218.0", "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/propagator-b3": "2.7.1", - "@opentelemetry/propagator-jaeger": "2.7.1", + "@opentelemetry/otlp-transformer": "0.218.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.218.0.tgz", + "integrity": "sha512-CFaKH87WAzjuJ4awowTTLzUvMfaRfiOFG5+qm5S5ncyalRtN4ecQ+YmuANJSCrVPuvZFEkUgKhBPBndxi3rHsQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.218.0", + "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/sdk-logs": "0.218.0", "@opentelemetry/sdk-metrics": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1", - "@opentelemetry/sdk-trace-node": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@opentelemetry/sdk-trace-base": "2.7.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", + "node_modules/@opentelemetry/propagator-b3": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.7.1.tgz", + "integrity": "sha512-RJid6E2CKyeGfKBzXKF21ejabGMHypFkPAh3qZ+NvI+SGjuIye79t3PmiqcDgtRzdKH6ynXzbfslQ8DfpRUg2A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/core": "2.7.1" }, "engines": { - "node": ">=8.0.0" + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/exporter-trace-otlp-proto": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.218.0.tgz", - "integrity": "sha512-r1Msf8SNLRmwh9J6XQ5uh82D7CdDWMNHnPB7LAVHjzut0TkSeKc5KcIvr4SvHvfk/xwN5gxC+VLKQ1k0o8PSPw==", + "node_modules/@opentelemetry/propagator-jaeger": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.7.1.tgz", + "integrity": "sha512-KMjVBHzP4N60bOzxja76M1F1hZZ43lGPga5ix+mkv9+kk1nx9SbkxSvJsMbuVUxdPQmsPTqGShmhN8ulrMOg6Q==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" + "@opentelemetry/core": "2.7.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", + "node_modules/@opentelemetry/redis-common": { + "version": "0.38.3", + "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.38.3.tgz", + "integrity": "sha512-VCghU1JYs/4gP6Gqf/xro9MEsZ7LrMv2uONVsaESKL38ZOB9BqnI98FfS23wjMnHlpuE+TTaWSoAVNpTwYXzjw==", "dev": true, "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, "engines": { "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.218.0.tgz", - "integrity": "sha512-ZwqpkNL5W7RyGJPDZ9g06DvKp8KFTWPJPN12anpMQYSKpTSU0z3EIZuPq9vPGpS8siFyOqDYDAuCwlNO9FqgbA==", + "node_modules/@opentelemetry/resource-detector-alibaba-cloud": { + "version": "0.33.8", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-alibaba-cloud/-/resource-detector-alibaba-cloud-0.33.8.tgz", + "integrity": "sha512-RnSB/uxkElny0/WBFEtIG2HRG0cpSNTRdE+YSB7Poa+uljK+ddCacEZYz/PMgZh+cs586XstJQxdyjz0jtcAug==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-transformer": "0.218.0" + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": "^1.0.0" } }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/otlp-transformer": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.218.0.tgz", - "integrity": "sha512-CFaKH87WAzjuJ4awowTTLzUvMfaRfiOFG5+qm5S5ncyalRtN4ecQ+YmuANJSCrVPuvZFEkUgKhBPBndxi3rHsQ==", + "node_modules/@opentelemetry/resource-detector-aws": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-aws/-/resource-detector-aws-2.18.0.tgz", + "integrity": "sha512-wyMM4UoRuHvI2KjqnTzvyW8Yv7MKRGA+I78Xti6gTEw7hBhqXU1SRo+f9KrsQfeeiOn+TkDuvxavuaAQbD3i6g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-logs": "0.218.0", - "@opentelemetry/sdk-metrics": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": "^1.0.0" } }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.218.0.tgz", - "integrity": "sha512-QvnNdugatFTVCJXH0Mcu7GOOJSylA9j127kIezOE4YwTI4YbowRons2K4WZTv5FMS8T4q9P0NdaRHdkSmeAIag==", + "node_modules/@opentelemetry/resource-detector-azure": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-azure/-/resource-detector-azure-0.26.0.tgz", + "integrity": "sha512-7KxF7mlwI2nKja/iEdwPqOaS0QAJbhT9ye4DeYZnXdOS/4phfonk5nSmyGDBYhBL7J30MPL91oZNuGYRKXZAXA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/semantic-conventions": "^1.37.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.10.0" + "@opentelemetry/api": "^1.0.0" } }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.1.tgz", - "integrity": "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==", + "node_modules/@opentelemetry/resource-detector-container": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-container/-/resource-detector-container-0.8.9.tgz", + "integrity": "sha512-Xd2C4HjW9hl75iqZT7tQNy2yRBUqNucq2O9+e0FJRNkbiItInYVMzc0S0KDXcx/vZBwNmlrKS3R0uLCU9ULsGA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1" + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" + "@opentelemetry/api": "^1.0.0" } }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", - "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", + "node_modules/@opentelemetry/resource-detector-gcp": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-gcp/-/resource-detector-gcp-0.53.0.tgz", + "integrity": "sha512-RCV31v23ZwZfYR3LPkuORHTHIOvfm3hZBT7hAzSO0+oAIrG/Dm0ld5tV4lYNO05GjI7sHQdRcbSqzEYAvQcQuw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0", + "gcp-metadata": "^8.0.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "@opentelemetry/api": "^1.0.0" } }, - "node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.0.tgz", - "integrity": "sha512-g/OZVkqlxllgFM7qMKqbPV9c1DUPhQ7d4n3pgZFcrnrNft9eJXZM2TNHTPYREJBrtNdRytYyvwjgL5geDKl3EQ==", + "node_modules/@opentelemetry/resources": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", + "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.0", - "@opentelemetry/resources": "2.6.0", + "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4779,58 +2047,83 @@ "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/core": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.0.tgz", - "integrity": "sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==", + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.218.0.tgz", + "integrity": "sha512-QvnNdugatFTVCJXH0Mcu7GOOJSylA9j127kIezOE4YwTI4YbowRons2K4WZTv5FMS8T4q9P0NdaRHdkSmeAIag==", "dev": true, "license": "Apache-2.0", "dependencies": { + "@opentelemetry/api-logs": "0.218.0", + "@opentelemetry/core": "2.7.1", + "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/resources": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.0.tgz", - "integrity": "sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ==", + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.1.tgz", + "integrity": "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.0", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@opentelemetry/core": "2.7.1", + "@opentelemetry/resources": "2.7.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-trace-node": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.7.1.tgz", - "integrity": "sha512-pCpQxU68lV+I9s9svqMyVu5iHdDDUnqUpSxqwyCU8A9ejEsSnMPCbearwsUO4yk08ZJzAIUCFuReMdVQvHrdvg==", + "node_modules/@opentelemetry/sdk-node": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.218.0.tgz", + "integrity": "sha512-tPMjHrLV5gsfNdYqoRHjeGbCAZBXXD9c1Qo/2ut7VwnUABDNh76xNxrT0SEhkIIJuCN45bbN1vZnYL1gY0IkOg==", "dev": true, "license": "Apache-2.0", "dependencies": { + "@opentelemetry/api-logs": "0.218.0", + "@opentelemetry/configuration": "0.218.0", "@opentelemetry/context-async-hooks": "2.7.1", "@opentelemetry/core": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" + "@opentelemetry/exporter-logs-otlp-grpc": "0.218.0", + "@opentelemetry/exporter-logs-otlp-http": "0.218.0", + "@opentelemetry/exporter-logs-otlp-proto": "0.218.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "0.218.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.218.0", + "@opentelemetry/exporter-metrics-otlp-proto": "0.218.0", + "@opentelemetry/exporter-prometheus": "0.218.0", + "@opentelemetry/exporter-trace-otlp-grpc": "0.218.0", + "@opentelemetry/exporter-trace-otlp-http": "0.218.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.218.0", + "@opentelemetry/exporter-zipkin": "2.7.1", + "@opentelemetry/instrumentation": "0.218.0", + "@opentelemetry/otlp-exporter-base": "0.218.0", + "@opentelemetry/propagator-b3": "2.7.1", + "@opentelemetry/propagator-jaeger": "2.7.1", + "@opentelemetry/resources": "2.7.1", + "@opentelemetry/sdk-logs": "0.218.0", + "@opentelemetry/sdk-metrics": "2.7.1", + "@opentelemetry/sdk-trace-base": "2.7.1", + "@opentelemetry/sdk-trace-node": "2.7.1", + "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/sdk-trace-base": { + "node_modules/@opentelemetry/sdk-trace-base": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", @@ -4848,15 +2141,16 @@ "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-trace-web": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-web/-/sdk-trace-web-2.6.0.tgz", - "integrity": "sha512-xyYmLFatwUeYnB7NtQ2Ydl9Y8uiblN+EDo5YEjnk7ZRMhGFyt1wgPqb8EYvATLuDiRVtxid1fJsL6RH1fCQMIA==", + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.7.1.tgz", + "integrity": "sha512-pCpQxU68lV+I9s9svqMyVu5iHdDDUnqUpSxqwyCU8A9ejEsSnMPCbearwsUO4yk08ZJzAIUCFuReMdVQvHrdvg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.0", - "@opentelemetry/sdk-trace-base": "2.6.0" + "@opentelemetry/context-async-hooks": "2.7.1", + "@opentelemetry/core": "2.7.1", + "@opentelemetry/sdk-trace-base": "2.7.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4865,14 +2159,15 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-trace-web/node_modules/@opentelemetry/core": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.0.tgz", - "integrity": "sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==", + "node_modules/@opentelemetry/sdk-trace-web": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-web/-/sdk-trace-web-2.7.1.tgz", + "integrity": "sha512-K806OouCSOjMd8Nr7+ZCq3QT22tdAzzS/7h8vprfiKjkgFQ99/dvwU8d12WJANA6D5Qtme65hyBAqAu9CkQuxQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" + "@opentelemetry/core": "2.7.1", + "@opentelemetry/sdk-trace-base": "2.7.1" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4918,13 +2213,13 @@ } }, "node_modules/@pkgr/core": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", - "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", + "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", "dev": true, "license": "MIT", "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + "node": "^14.18.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/pkgr" @@ -5103,9 +2398,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5123,9 +2415,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5143,9 +2432,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5163,9 +2449,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5183,9 +2466,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5203,9 +2483,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5439,19 +2716,19 @@ } }, "node_modules/@types/node": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", - "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "version": "25.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.2.tgz", + "integrity": "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.18.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@types/node/node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "dev": true, "license": "MIT" }, @@ -5519,20 +2796,20 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", - "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz", + "integrity": "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.57.0", - "@typescript-eslint/type-utils": "8.57.0", - "@typescript-eslint/utils": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/type-utils": "8.60.1", + "@typescript-eslint/utils": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5542,9 +2819,9 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.57.0", + "@typescript-eslint/parser": "^8.60.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { @@ -5558,16 +2835,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz", - "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.1.tgz", + "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", "debug": "^4.4.3" }, "engines": { @@ -5579,18 +2856,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz", - "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", + "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.0", - "@typescript-eslint/types": "^8.57.0", + "@typescript-eslint/tsconfig-utils": "^8.60.1", + "@typescript-eslint/types": "^8.60.1", "debug": "^4.4.3" }, "engines": { @@ -5601,18 +2878,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz", - "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz", + "integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0" + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5623,9 +2900,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz", - "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", + "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", "dev": true, "license": "MIT", "engines": { @@ -5636,21 +2913,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz", - "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz", + "integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0", - "@typescript-eslint/utils": "8.57.0", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/utils": "8.60.1", "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5661,13 +2938,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz", - "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz", + "integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==", "dev": true, "license": "MIT", "engines": { @@ -5679,21 +2956,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz", - "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", + "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.57.0", - "@typescript-eslint/tsconfig-utils": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", + "@typescript-eslint/project-service": "8.60.1", + "@typescript-eslint/tsconfig-utils": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5703,20 +2980,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz", - "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz", + "integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0" + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5727,17 +3004,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz", - "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz", + "integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/types": "8.60.1", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -6025,9 +3302,9 @@ } }, "node_modules/ansi-escapes": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", - "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", "dev": true, "license": "MIT", "dependencies": { @@ -6100,13 +3377,6 @@ "undici-types": "~7.16.0" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, "node_modules/array-back": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz", @@ -6146,13 +3416,6 @@ "dev": true, "license": "MIT" }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, "node_modules/baseline-browser-mapping": { "version": "2.10.8", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz", @@ -6240,16 +3503,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001779", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001779.tgz", @@ -6338,14 +3591,14 @@ } }, "node_modules/cli-truncate": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", - "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", "dev": true, "license": "MIT", "dependencies": { - "slice-ansi": "^7.1.0", - "string-width": "^8.0.0" + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" }, "engines": { "node": ">=20" @@ -6355,14 +3608,14 @@ } }, "node_modules/cli-truncate/node_modules/string-width": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", - "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", "dev": true, "license": "MIT", "dependencies": { - "get-east-asian-width": "^1.3.0", - "strip-ansi": "^7.1.0" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { "node": ">=20" @@ -6429,13 +3682,6 @@ "dev": true, "license": "MIT" }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, "node_modules/command-line-args": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-6.0.1.tgz", @@ -6476,23 +3722,6 @@ "node": ">=12.20.0" } }, - "node_modules/commander": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -6628,18 +3857,18 @@ } }, "node_modules/eslint": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.0.tgz", - "integrity": "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.1.tgz", + "integrity": "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.4", - "@eslint/config-helpers": "^0.5.4", - "@eslint/core": "^1.2.0", - "@eslint/plugin-kit": "^0.7.0", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -6720,14 +3949,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.5.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", - "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.6.tgz", + "integrity": "sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==", "dev": true, "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.1", - "synckit": "^0.11.12" + "synckit": "^0.11.13" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -6856,6 +4085,13 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -7102,9 +4338,9 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", - "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", "dev": true, "license": "MIT", "engines": { @@ -7137,20 +4373,7 @@ "is-glob": "^4.0.3" }, "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=10.13.0" } }, "node_modules/google-logging-utils": { @@ -7227,23 +4450,6 @@ "node": ">= 4" } }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/import-in-the-middle": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.0.1.tgz", @@ -7362,19 +4568,6 @@ "dev": true, "license": "MIT" }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -7667,9 +4860,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -7691,9 +4881,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -7715,9 +4902,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -7739,9 +4923,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -7798,45 +4979,45 @@ } }, "node_modules/lint-staged": { - "version": "16.4.0", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.4.0.tgz", - "integrity": "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==", + "version": "17.0.7", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.0.7.tgz", + "integrity": "sha512-JrSobt+tW3rH8IOMi8tDZd3foorM5yPEkLD/V2NxobgHrFfHWGee4MOLVuZeScgxftEwbHrPHIFA/ZL+nUJeuA==", "dev": true, "license": "MIT", "dependencies": { - "commander": "^14.0.3", - "listr2": "^9.0.5", - "picomatch": "^4.0.3", + "listr2": "^10.2.1", + "picomatch": "^4.0.4", "string-argv": "^0.3.2", - "tinyexec": "^1.0.4", - "yaml": "^2.8.2" + "tinyexec": "^1.2.4" }, "bin": { "lint-staged": "bin/lint-staged.js" }, "engines": { - "node": ">=20.17" + "node": ">=22.22.1" }, "funding": { "url": "https://opencollective.com/lint-staged" + }, + "optionalDependencies": { + "yaml": "^2.9.0" } }, "node_modules/listr2": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", - "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-10.2.1.tgz", + "integrity": "sha512-7I5knELsJKTUjXG+A6BkKAiGkW1i25fNa/xlUl9hFtk15WbE9jndA89xu5FzQKrY5llajE1hfZZFMILXkDHk/Q==", "dev": true, "license": "MIT", "dependencies": { - "cli-truncate": "^5.0.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", + "cli-truncate": "^5.2.0", + "eventemitter3": "^5.0.4", "log-update": "^6.1.0", "rfdc": "^1.4.1", - "wrap-ansi": "^9.0.0" + "wrap-ansi": "^10.0.0" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.13.0" } }, "node_modules/listr2/node_modules/ansi-styles": { @@ -7852,51 +5033,36 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/listr2/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/listr2/node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true, - "license": "MIT" - }, "node_modules/listr2/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/listr2/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.0.tgz", + "integrity": "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" + "ansi-styles": "^6.2.3", + "string-width": "^8.2.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" @@ -7967,13 +5133,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -8021,6 +5180,23 @@ "dev": true, "license": "MIT" }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/log-update/node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -8322,19 +5498,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/parquet-wasm": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/parquet-wasm/-/parquet-wasm-0.7.1.tgz", @@ -8563,9 +5726,9 @@ } }, "node_modules/prettier": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true, "license": "MIT", "bin": { @@ -8650,16 +5813,6 @@ "node": ">=9.3.0 || >=8.10.0 <9.0.0" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -8832,17 +5985,17 @@ } }, "node_modules/slice-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", - "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/chalk/slice-ansi?sponsor=1" @@ -8954,13 +6107,13 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -8969,19 +6122,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8996,13 +6136,13 @@ } }, "node_modules/synckit": { - "version": "0.11.12", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", - "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.2.9" + "@pkgr/core": "^0.3.6" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -9033,9 +6173,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", - "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", "dev": true, "license": "MIT", "engines": { @@ -9098,9 +6238,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -9131,9 +6271,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -9145,16 +6285,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.0.tgz", - "integrity": "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.60.1.tgz", + "integrity": "sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.57.0", - "@typescript-eslint/parser": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0", - "@typescript-eslint/utils": "8.57.0" + "@typescript-eslint/eslint-plugin": "8.60.1", + "@typescript-eslint/parser": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/utils": "8.60.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -9165,7 +6305,7 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/typical": { @@ -9237,9 +6377,9 @@ } }, "node_modules/uuid": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz", - "integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", "dev": true, "funding": [ "https://github.com/sponsors/broofa", @@ -9630,9 +6770,9 @@ } }, "node_modules/zone.js": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.16.1.tgz", - "integrity": "sha512-dpvY17vxYIW3+bNrP0ClUlaiY0CiIRK3tnoLaGoQsQcY9/I/NpzIWQ7tQNhbV7LacQMpCII6wVzuL3tuWOyfuA==", + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.16.2.tgz", + "integrity": "sha512-Eky7p2Z1Ig3NnbfodSPoARCjKBSTFMnE/ACsP1L/XJEfY4SdOFce19BsUCWVwL6K5ABZFy5J3bjcMWffX+YM3Q==", "dev": true, "license": "MIT" }, @@ -9674,228 +6814,18 @@ "clickhouse-js-test-runner": "dist/main.js" }, "devDependencies": { - "@eslint/js": "^9.39.4", - "@types/node": "25.5.0", - "eslint": "^9.39.4", - "eslint-plugin-prettier": "^5.5.5", - "prettier": "3.8.1", - "typescript": "^5.9.3", - "typescript-eslint": "^8.57.0", + "@eslint/js": "^10.0.1", + "@types/node": "25.9.2", + "eslint": "^10.4.1", + "eslint-plugin-prettier": "^5.5.6", + "prettier": "3.8.3", + "typescript": "^6.0.3", + "typescript-eslint": "^8.60.1", "vitest": "^4.0.16" }, "engines": { "node": ">=20.19.0" } - }, - "tests/clickhouse-test-runner/node_modules/@eslint/config-array": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", - "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.5" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "tests/clickhouse-test-runner/node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "tests/clickhouse-test-runner/node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "tests/clickhouse-test-runner/node_modules/@eslint/js": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "tests/clickhouse-test-runner/node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "tests/clickhouse-test-runner/node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "tests/clickhouse-test-runner/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "tests/clickhouse-test-runner/node_modules/eslint": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.2", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.5", - "@eslint/js": "9.39.4", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.5", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "tests/clickhouse-test-runner/node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "tests/clickhouse-test-runner/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "tests/clickhouse-test-runner/node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "tests/clickhouse-test-runner/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } } } } diff --git a/package.json b/package.json index c39f3589..cff71baa 100644 --- a/package.json +++ b/package.json @@ -49,36 +49,36 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", - "@faker-js/faker": "^10.3.0", - "@opentelemetry/api": "^1.9.0", + "@faker-js/faker": "^10.4.0", + "@opentelemetry/api": "^1.9.1", "@opentelemetry/auto-instrumentations-node": "^0.76.0", - "@opentelemetry/context-zone": "^2.6.0", - "@opentelemetry/exporter-trace-otlp-proto": "^0.213.0", - "@opentelemetry/instrumentation-document-load": "^0.58.0", - "@opentelemetry/instrumentation-fetch": "^0.213.0", + "@opentelemetry/context-zone": "^2.7.1", + "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0", + "@opentelemetry/instrumentation-document-load": "^0.63.0", + "@opentelemetry/instrumentation-fetch": "^0.218.0", "@opentelemetry/sdk-trace-web": "^2.5.1", "@types/jsonwebtoken": "^9.0.10", - "@types/node": "25.5.0", + "@types/node": "25.9.2", "@types/split2": "^4.2.3", "@types/uuid": "^11.0.0", "@vitest/browser-playwright": "^4.1.8", "@vitest/coverage-istanbul": "^4.1.0", "@vitest/coverage-v8": "^4.1.0", "apache-arrow": "^21.0.0", - "eslint": "^10.2.0", + "eslint": "^10.4.1", "eslint-config-prettier": "^10.1.8", "eslint-plugin-expect-type": "^0.6.2", - "eslint-plugin-prettier": "^5.5.5", + "eslint-plugin-prettier": "^5.5.6", "husky": "^9.1.7", "jsonwebtoken": "^9.0.3", - "lint-staged": "^16.4.0", + "lint-staged": "^17.0.7", "parquet-wasm": "0.7.1", "playwright": "^1.60.0", - "prettier": "3.8.1", + "prettier": "3.8.3", "split2": "^4.2.0", - "typescript": "^5.9.3", - "typescript-eslint": "^8.57.0", - "uuid": "^13.0.0", + "typescript": "^6.0.3", + "typescript-eslint": "^8.60.1", + "uuid": "^14.0.0", "vitest": "^4.0.16" }, "workspaces": [ diff --git a/packages/client-common/tsconfig.json b/packages/client-common/tsconfig.json index 7e518f58..378ea626 100644 --- a/packages/client-common/tsconfig.json +++ b/packages/client-common/tsconfig.json @@ -2,6 +2,8 @@ "extends": "../../tsconfig.base.json", "include": ["./src/**/*.ts"], "compilerOptions": { + "rootDir": "./src", + "lib": ["ES2022", "ESNext.Disposable", "DOM"], "outDir": "./dist" } } diff --git a/packages/client-node/tsconfig.json b/packages/client-node/tsconfig.json index 40e5ca43..05de665e 100644 --- a/packages/client-node/tsconfig.json +++ b/packages/client-node/tsconfig.json @@ -3,6 +3,7 @@ "include": ["./src/**/*.ts"], "compilerOptions": { "types": ["node"], + "rootDir": "./src", "outDir": "./dist" } } diff --git a/packages/client-web/tsconfig.json b/packages/client-web/tsconfig.json index 7e518f58..378ea626 100644 --- a/packages/client-web/tsconfig.json +++ b/packages/client-web/tsconfig.json @@ -2,6 +2,8 @@ "extends": "../../tsconfig.base.json", "include": ["./src/**/*.ts"], "compilerOptions": { + "rootDir": "./src", + "lib": ["ES2022", "ESNext.Disposable", "DOM"], "outDir": "./dist" } } diff --git a/tests/clickhouse-test-runner/package.json b/tests/clickhouse-test-runner/package.json index 91a19144..f6723b0d 100644 --- a/tests/clickhouse-test-runner/package.json +++ b/tests/clickhouse-test-runner/package.json @@ -22,13 +22,13 @@ "@clickhouse/client": "*" }, "devDependencies": { - "@eslint/js": "^9.39.4", - "@types/node": "25.5.0", - "eslint": "^9.39.4", - "eslint-plugin-prettier": "^5.5.5", - "prettier": "3.8.1", - "typescript": "^5.9.3", - "typescript-eslint": "^8.57.0", + "@eslint/js": "^10.0.1", + "@types/node": "25.9.2", + "eslint": "^10.4.1", + "eslint-plugin-prettier": "^5.5.6", + "prettier": "3.8.3", + "typescript": "^6.0.3", + "typescript-eslint": "^8.60.1", "vitest": "^4.0.16" } } From 1806c5ea848f66762da1ec907076e99c30c9d9ec Mon Sep 17 00:00:00 2001 From: Peter Leonov Date: Wed, 10 Jun 2026 19:32:33 +0200 Subject: [PATCH 07/20] Use default prettier settings (#810) ## Summary Stick to the industry defaults as LLMs produce the code. Otherwise any code that comes in needs to be reformatted, especially subtrees from other repos. --- .github/ISSUE_TEMPLATE/bug_report.md | 4 +- .github/ISSUE_TEMPLATE/feature_request.md | 4 +- .github/ISSUE_TEMPLATE/question.md | 4 +- .github/dependabot.yml | 32 +- .github/instructions/review.instructions.md | 4 +- .github/workflows/bump-version.yml | 4 +- .github/workflows/clean-up.yml | 6 +- .github/workflows/copilot-setup-steps.yml | 2 +- .github/workflows/e2e-skills.yml | 2 +- .github/workflows/examples.yml | 30 +- .github/workflows/github-export-otel.yml | 4 +- .github/workflows/publish.yml | 26 +- .github/workflows/scorecard.yml | 26 +- .github/workflows/tests-bun.yml | 16 +- .github/workflows/tests-node.yml | 52 +- .github/workflows/tests-web.yml | 48 +- .github/workflows/upstream-sql-tests.yml | 26 +- .prettierrc | 5 +- .scripts/cleanup_old_databases.mjs | 60 +- .scripts/export-coverage-metrics.mjs | 281 +-- .scripts/generate_cloud_jwt.ts | 8 +- AGENTS.md | 8 +- CHANGELOG.md | 180 +- CONTRIBUTING.md | 1 - README.md | 22 +- benchmarks/common/handlers.ts | 8 +- benchmarks/common/index.ts | 2 +- benchmarks/formats/json.ts | 74 +- benchmarks/leaks/memory_leak_arrays.ts | 80 +- benchmarks/leaks/memory_leak_brown.ts | 68 +- .../leaks/memory_leak_random_integers.ts | 72 +- benchmarks/leaks/shared.ts | 102 +- docker-compose.yml | 56 +- docs/howto/keep_alive_timeout.md | 6 +- docs/howto/long_running_queries.md | 44 +- docs/socket_hang_up_econnreset.md | 16 +- eslint.config.base.mjs | 40 +- examples/node/coding/array_json_each_row.ts | 26 +- examples/node/coding/async_insert.ts | 44 +- examples/node/coding/clickhouse_settings.ts | 16 +- examples/node/coding/custom_json_handling.ts | 38 +- .../node/coding/default_format_setting.ts | 14 +- examples/node/coding/dynamic_variant_json.ts | 28 +- .../coding/insert_data_formats_overview.ts | 152 +- examples/node/coding/insert_decimals.ts | 40 +- .../node/coding/insert_ephemeral_columns.ts | 30 +- .../node/coding/insert_exclude_columns.ts | 30 +- examples/node/coding/insert_from_select.ts | 18 +- .../node/coding/insert_into_different_db.ts | 32 +- examples/node/coding/insert_js_dates.ts | 24 +- .../node/coding/insert_specific_columns.ts | 30 +- .../node/coding/insert_uuid_into_uint128.ts | 64 +- .../coding/insert_values_and_functions.ts | 60 +- examples/node/coding/ping_existing_host.ts | 18 +- .../node/coding/ping_non_existing_host.ts | 26 +- examples/node/coding/qbit.ts | 58 +- .../coding/query_with_parameter_binding.ts | 36 +- ...ry_with_parameter_binding_special_chars.ts | 20 +- .../coding/select_data_formats_overview.ts | 92 +- examples/node/coding/select_json_each_row.ts | 18 +- .../node/coding/select_json_with_metadata.ts | 16 +- .../coding/session_id_and_temporary_tables.ts | 22 +- .../node/coding/session_level_commands.ts | 24 +- examples/node/coding/time_time64.ts | 62 +- examples/node/coding/url_configuration.ts | 36 +- examples/node/eslint.config.mjs | 42 +- examples/node/performance/async_insert.ts | 44 +- .../async_insert_without_waiting.ts | 95 +- .../insert_arbitrary_format_stream.ts | 32 +- .../performance/insert_file_stream_csv.ts | 34 +- .../performance/insert_file_stream_ndjson.ts | 44 +- .../performance/insert_file_stream_parquet.ts | 34 +- .../node/performance/insert_from_select.ts | 18 +- .../insert_streaming_backpressure_simple.ts | 102 +- .../insert_streaming_with_backpressure.ts | 250 +- .../select_json_each_row_with_progress.ts | 86 +- .../performance/select_parquet_as_file.ts | 24 +- .../select_streaming_json_each_row.ts | 36 +- ...elect_streaming_json_each_row_for_await.ts | 18 +- .../select_streaming_text_line_by_line.ts | 36 +- .../stream_created_from_array_raw.ts | 26 +- .../create_table_cloud.ts | 20 +- .../create_table_on_premise_cluster.ts | 10 +- .../create_table_single_node.ts | 8 +- .../insert_ephemeral_columns.ts | 30 +- .../insert_exclude_columns.ts | 30 +- .../url_configuration.ts | 36 +- examples/node/security/basic_tls.ts | 20 +- examples/node/security/mutual_tls.ts | 22 +- .../security/query_with_parameter_binding.ts | 36 +- ...ry_with_parameter_binding_special_chars.ts | 20 +- examples/node/security/read_only_user.ts | 102 +- examples/node/security/role.ts | 66 +- .../node/troubleshooting/abort_request.ts | 24 +- examples/node/troubleshooting/cancel_query.ts | 26 +- .../troubleshooting/custom_json_handling.ts | 38 +- .../long_running_queries_cancel_request.ts | 88 +- .../long_running_queries_progress_headers.ts | 28 +- .../troubleshooting/ping_non_existing_host.ts | 26 +- examples/node/troubleshooting/ping_timeout.ts | 48 +- .../node/troubleshooting/read_only_user.ts | 102 +- examples/node/vitest.config.ts | 46 +- examples/node/vitest.setup.ts | 16 +- examples/web/coding/array_json_each_row.ts | 26 +- examples/web/coding/async_insert.ts | 42 +- examples/web/coding/clickhouse_settings.ts | 16 +- examples/web/coding/custom_json_handling.ts | 38 +- examples/web/coding/default_format_setting.ts | 14 +- examples/web/coding/dynamic_variant_json.ts | 28 +- .../coding/insert_data_formats_overview.ts | 152 +- examples/web/coding/insert_decimals.ts | 40 +- .../web/coding/insert_ephemeral_columns.ts | 30 +- examples/web/coding/insert_exclude_columns.ts | 30 +- examples/web/coding/insert_from_select.ts | 18 +- .../web/coding/insert_into_different_db.ts | 32 +- examples/web/coding/insert_js_dates.ts | 24 +- .../web/coding/insert_specific_columns.ts | 30 +- .../web/coding/insert_uuid_into_uint128.ts | 68 +- .../web/coding/insert_values_and_functions.ts | 66 +- examples/web/coding/ping_existing_host.ts | 16 +- examples/web/coding/ping_non_existing_host.ts | 18 +- examples/web/coding/qbit.ts | 58 +- .../coding/query_with_parameter_binding.ts | 36 +- ...ry_with_parameter_binding_special_chars.ts | 20 +- .../coding/select_data_formats_overview.ts | 92 +- examples/web/coding/select_json_each_row.ts | 18 +- .../web/coding/select_json_with_metadata.ts | 16 +- .../coding/session_id_and_temporary_tables.ts | 20 +- examples/web/coding/session_level_commands.ts | 22 +- examples/web/coding/time_time64.ts | 62 +- examples/web/coding/url_configuration.ts | 34 +- examples/web/eslint.config.mjs | 46 +- examples/web/global.d.ts | 6 +- .../select_streaming_json_each_row.ts | 28 +- .../create_table_cloud.ts | 16 +- .../create_table_on_premise_cluster.ts | 12 +- .../create_table_single_node.ts | 8 +- .../insert_ephemeral_columns.ts | 30 +- .../insert_exclude_columns.ts | 30 +- .../url_configuration.ts | 34 +- .../security/query_with_parameter_binding.ts | 36 +- ...ry_with_parameter_binding_special_chars.ts | 20 +- examples/web/security/read_only_user.ts | 100 +- examples/web/security/role.ts | 66 +- examples/web/troubleshooting/abort_request.ts | 24 +- examples/web/troubleshooting/cancel_query.ts | 24 +- .../troubleshooting/custom_json_handling.ts | 38 +- .../long_running_queries_progress_headers.ts | 28 +- .../troubleshooting/ping_non_existing_host.ts | 18 +- .../web/troubleshooting/read_only_user.ts | 100 +- examples/web/vitest.config.ts | 52 +- examples/web/vitest.setup.ts | 10 +- .../__tests__/fixtures/read_only_user.ts | 42 +- .../__tests__/fixtures/simple_table.ts | 36 +- .../__tests__/fixtures/stream_errors.ts | 22 +- .../__tests__/fixtures/table_with_fields.ts | 16 +- .../__tests__/fixtures/test_data.ts | 30 +- .../integration/abort_request.test.ts | 142 +- .../__tests__/integration/auth.test.ts | 116 +- .../integration/clickhouse_settings.test.ts | 74 +- .../__tests__/integration/config.test.ts | 42 +- .../__tests__/integration/data_types.test.ts | 690 +++--- .../__tests__/integration/date_time.test.ts | 174 +- .../integration/error_parsing.test.ts | 68 +- .../integration/exec_and_command.test.ts | 174 +- .../__tests__/integration/insert.test.ts | 168 +- .../insert_specific_columns.test.ts | 236 +- .../integration/multiple_clients.test.ts | 92 +- .../__tests__/integration/ping.test.ts | 26 +- .../__tests__/integration/query_log.test.ts | 88 +- .../integration/read_only_user.test.ts | 88 +- .../integration/request_compression.test.ts | 46 +- .../integration/response_compression.test.ts | 32 +- .../__tests__/integration/role.test.ts | 306 +-- .../__tests__/integration/select.test.ts | 236 +- .../integration/select_query_binding.test.ts | 474 ++-- .../integration/select_result.test.ts | 74 +- .../__tests__/integration/session.test.ts | 76 +- .../__tests__/integration/totals.test.ts | 90 +- .../__tests__/unit/clickhouse_types.test.ts | 78 +- .../__tests__/unit/client.test.ts | 56 +- .../__tests__/unit/config.test.ts | 1264 +++++----- .../__tests__/unit/error.test.ts | 218 +- .../unit/format_query_params.test.ts | 198 +- .../unit/format_query_settings.test.ts | 48 +- .../__tests__/unit/parse_column_types.test.ts | 64 +- .../unit/parse_column_types_array.test.ts | 224 +- .../unit/parse_column_types_datetime.test.ts | 109 +- .../unit/parse_column_types_decimal.test.ts | 98 +- .../unit/parse_column_types_enum.test.ts | 110 +- .../unit/parse_column_types_map.test.ts | 46 +- .../unit/parse_column_types_nullable.test.ts | 212 +- .../unit/parse_column_types_tuple.test.ts | 150 +- .../__tests__/unit/stream_utils.test.ts | 68 +- .../__tests__/unit/to_search_params.test.ts | 166 +- .../__tests__/unit/transform_url.test.ts | 104 +- .../client-common/__tests__/utils/client.ts | 102 +- .../client-common/__tests__/utils/datasets.ts | 24 +- .../client-common/__tests__/utils/env.test.ts | 124 +- packages/client-common/__tests__/utils/env.ts | 40 +- .../client-common/__tests__/utils/guid.ts | 8 +- .../client-common/__tests__/utils/index.ts | 18 +- .../__tests__/utils/native_columns.ts | 72 +- .../__tests__/utils/parametrized.ts | 24 +- .../__tests__/utils/permutations.ts | 4 +- .../client-common/__tests__/utils/random.ts | 6 +- .../__tests__/utils/server_version.ts | 56 +- .../__tests__/utils/simple_client.ts | 14 +- .../client-common/__tests__/utils/sleep.ts | 4 +- .../__tests__/utils/test_connection_type.ts | 34 +- .../client-common/__tests__/utils/test_env.ts | 56 +- .../__tests__/utils/test_logger.ts | 24 +- packages/client-common/eslint.config.mjs | 28 +- .../client-common/src/clickhouse_types.ts | 90 +- packages/client-common/src/client.ts | 225 +- packages/client-common/src/config.ts | 402 ++-- packages/client-common/src/connection.ts | 104 +- .../src/data_formatter/format_query_params.ts | 94 +- .../data_formatter/format_query_settings.ts | 12 +- .../src/data_formatter/formatter.ts | 96 +- .../client-common/src/data_formatter/index.ts | 6 +- packages/client-common/src/error/error.ts | 48 +- packages/client-common/src/error/index.ts | 2 +- packages/client-common/src/index.ts | 40 +- packages/client-common/src/logger.ts | 128 +- .../client-common/src/parse/column_types.ts | 554 ++--- packages/client-common/src/parse/index.ts | 4 +- .../client-common/src/parse/json_handling.ts | 6 +- packages/client-common/src/result.ts | 32 +- packages/client-common/src/settings.ts | 2116 ++++++++--------- packages/client-common/src/ts_utils.ts | 2 +- .../client-common/src/utils/connection.ts | 34 +- packages/client-common/src/utils/index.ts | 8 +- packages/client-common/src/utils/sleep.ts | 4 +- packages/client-common/src/utils/stream.ts | 34 +- packages/client-common/src/utils/url.ts | 60 +- packages/client-common/src/version.ts | 2 +- .../integration/node_abort_request.test.ts | 192 +- .../__tests__/integration/node_client.test.ts | 266 +-- .../integration/node_command.test.ts | 72 +- .../integration/node_compression.test.ts | 94 +- .../node_custom_http_agent.test.ts | 42 +- .../node_eager_socket_destroy.test.ts | 126 +- .../integration/node_errors_parsing.test.ts | 22 +- .../__tests__/integration/node_exec.test.ts | 256 +- .../__tests__/integration/node_insert.test.ts | 74 +- .../integration/node_jwt_auth.test.ts | 68 +- .../integration/node_keep_alive.test.ts | 150 +- .../node_keep_alive_header.test.ts | 190 +- .../integration/node_logger_support.test.ts | 130 +- .../node_max_open_connections.test.ts | 112 +- .../integration/node_multiple_clients.test.ts | 66 +- .../__tests__/integration/node_ping.test.ts | 66 +- .../node_query_format_types.test.ts | 470 ++-- .../node_response_headers_cap.test.ts | 214 +- .../node_response_headers_cap_client.test.ts | 154 +- .../integration/node_select_streaming.test.ts | 332 +-- .../integration/node_socket_handling.test.ts | 392 +-- .../node_stream_error_handling.test.ts | 120 +- .../node_stream_json_compact_each_row.test.ts | 268 +-- .../node_stream_json_each_row.test.ts | 72 +- ...stream_json_each_row_with_progress.test.ts | 348 +-- .../node_stream_json_insert.test.ts | 108 +- .../node_stream_raw_formats.test.ts | 358 +-- .../node_stream_row_binary.test.ts | 94 +- .../node_stream_row_binary_select.test.ts | 114 +- .../integration/node_streaming_e2e.test.ts | 224 +- .../integration/node_summary.test.ts | 58 +- .../client-node/__tests__/tls/tls.test.ts | 224 +- .../__tests__/unit/node_client.test.ts | 180 +- .../__tests__/unit/node_client_query.test.ts | 126 +- .../__tests__/unit/node_config.test.ts | 268 +-- .../__tests__/unit/node_connection.test.ts | 494 ++-- .../unit/node_connection_compression.test.ts | 242 +- .../unit/node_create_connection.test.ts | 82 +- .../unit/node_custom_agent_connection.test.ts | 342 +-- .../unit/node_default_logger.test.ts | 186 +- .../__tests__/unit/node_getAsText.test.ts | 72 +- .../unit/node_http_connection.test.ts | 104 +- .../unit/node_https_connection.test.ts | 252 +- .../__tests__/unit/node_result_set.test.ts | 260 +- .../unit/node_result_set_extra.test.ts | 160 +- .../__tests__/unit/node_stream.test.ts | 134 +- .../unit/node_stream_internal.test.ts | 156 +- .../unit/node_stream_internal_trace.test.ts | 222 +- .../__tests__/unit/node_user_agent.test.ts | 38 +- .../unit/node_values_encoder.test.ts | 340 +-- .../client-node/__tests__/utils/assert.ts | 18 +- .../__tests__/utils/feature_detection.ts | 12 +- .../client-node/__tests__/utils/http_stubs.ts | 96 +- packages/client-node/__tests__/utils/jwt.ts | 22 +- .../__tests__/utils/node_client.ts | 11 +- .../__tests__/utils/simple_node_client.ts | 11 +- packages/client-node/__tests__/utils/sleep.ts | 4 +- .../client-node/__tests__/utils/stream.ts | 6 +- packages/client-node/eslint.config.mjs | 32 +- packages/client-node/src/client.ts | 20 +- packages/client-node/src/config.ts | 90 +- .../client-node/src/connection/compression.ts | 30 +- .../src/connection/create_connection.ts | 42 +- packages/client-node/src/connection/index.ts | 8 +- .../src/connection/node_base_connection.ts | 348 +-- .../node_custom_agent_connection.ts | 30 +- .../src/connection/node_http_connection.ts | 18 +- .../src/connection/node_https_connection.ts | 60 +- .../client-node/src/connection/socket_pool.ts | 382 +-- packages/client-node/src/connection/stream.ts | 130 +- packages/client-node/src/index.ts | 56 +- packages/client-node/src/result_set.ts | 176 +- packages/client-node/src/utils/encoder.ts | 34 +- packages/client-node/src/utils/index.ts | 8 +- packages/client-node/src/utils/process.ts | 2 +- packages/client-node/src/utils/runtime.ts | 10 +- packages/client-node/src/utils/stream.ts | 40 +- packages/client-node/src/utils/user_agent.ts | 6 +- packages/client-node/src/version.ts | 2 +- .../integration/web_abort_request.test.ts | 110 +- .../__tests__/integration/web_client.test.ts | 154 +- .../integration/web_error_parsing.test.ts | 22 +- .../__tests__/integration/web_exec.test.ts | 80 +- .../__tests__/integration/web_ping.test.ts | 54 +- .../integration/web_select_streaming.test.ts | 416 ++-- .../web_stream_error_handling.test.ts | 50 +- .../__tests__/jwt/web_jwt_auth.test.ts | 68 +- .../__tests__/unit/node_getAsText.test.ts | 76 +- .../__tests__/unit/web_client.test.ts | 72 +- .../__tests__/unit/web_result_set.test.ts | 150 +- .../__tests__/utils/feature_detection.ts | 12 +- .../__tests__/utils/simple_web_client.ts | 8 +- packages/client-web/__tests__/utils/sleep.ts | 4 +- .../client-web/__tests__/utils/web_client.ts | 8 +- packages/client-web/eslint.config.mjs | 28 +- packages/client-web/src/client.ts | 34 +- packages/client-web/src/config.ts | 16 +- packages/client-web/src/connection/index.ts | 2 +- .../src/connection/web_connection.ts | 166 +- packages/client-web/src/index.ts | 54 +- packages/client-web/src/result_set.ts | 148 +- packages/client-web/src/utils/encoder.ts | 30 +- packages/client-web/src/utils/index.ts | 4 +- packages/client-web/src/utils/stream.ts | 34 +- packages/client-web/src/version.ts | 2 +- .../reference/async-insert.md | 24 +- .../reference/client-configuration.md | 32 +- .../reference/custom-json.md | 62 +- .../reference/data-types.md | 130 +- .../reference/insert-columns.md | 46 +- .../reference/insert-formats.md | 60 +- .../reference/insert-values.md | 70 +- .../reference/ping.md | 48 +- .../reference/query-parameters.md | 54 +- .../reference/select-formats.md | 42 +- .../reference/sessions.md | 54 +- .../reference/compression.md | 4 +- .../reference/data-types.md | 48 +- .../reference/logging.md | 10 +- .../reference/proxy-pathname.md | 14 +- .../reference/query-format-clause.md | 12 +- .../reference/query-params.md | 60 +- .../reference/readonly-users.md | 8 +- .../reference/socket-hangup.md | 40 +- .../reference/tls.md | 48 +- .../__tests__/args.test.ts | 298 +-- .../__tests__/extract-from-config.test.ts | 66 +- .../__tests__/log.test.ts | 80 +- .../__tests__/split-queries.test.ts | 111 +- .../clickhouse-test-runner/eslint.config.mjs | 12 +- tests/clickhouse-test-runner/src/args.ts | 310 +-- .../src/backends/client.ts | 56 +- .../src/extract-from-config.ts | 28 +- tests/clickhouse-test-runner/src/log.ts | 36 +- tests/clickhouse-test-runner/src/main.ts | 100 +- tests/clickhouse-test-runner/src/settings.ts | 302 +-- .../src/split-queries.ts | 90 +- tests/clickhouse-test-runner/vitest.config.ts | 6 +- tests/e2e/install/src/index.ts | 30 +- tests/e2e/skills/check.js | 140 +- vitest.node.config.ts | 94 +- vitest.node.otel.js | 32 +- vitest.node.setup.ts | 4 +- vitest.web.config.ts | 114 +- vitest.web.otel.js | 42 +- vitest.web.setup.ts | 6 +- 383 files changed, 16590 insertions(+), 16537 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 423c1b00..e927a1eb 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,9 +1,9 @@ --- name: Bug report about: Create a report to help us improve -title: '' +title: "" labels: bug -assignees: '' +assignees: "" --- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index eafc7cb3..2d47db6f 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,9 +1,9 @@ --- name: Feature request about: Suggest an idea for the client -title: '' +title: "" labels: enhancement -assignees: '' +assignees: "" --- diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index f5197f58..7808da45 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -1,9 +1,9 @@ --- name: Question about: Ask a question about the client -title: '' +title: "" labels: question -assignees: '' +assignees: "" --- > Make sure to check the [documentation](https://clickhouse.com/docs/en/integrations/language-clients/javascript) first. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c272e963..e21c3cc1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,26 +1,26 @@ # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - - package-ecosystem: 'github-actions' - directory: '.github/workflows' + - package-ecosystem: "github-actions" + directory: ".github/workflows" schedule: - interval: 'weekly' - day: 'monday' + interval: "weekly" + day: "monday" groups: workflows: - dependency-type: 'development' - - package-ecosystem: 'npm' - directory: '/' + dependency-type: "development" + - package-ecosystem: "npm" + directory: "/" schedule: - interval: 'weekly' - day: 'monday' + interval: "weekly" + day: "monday" groups: dev-dependencies: - dependency-type: 'development' + dependency-type: "development" ignore: - - dependency-name: '@opentelemetry/auto-instrumentations-node' - versions: ['0.70.0'] - - dependency-name: '@types/node' - versions: ['25.3.0'] - - dependency-name: 'typescript-eslint' - versions: ['8.56.0'] + - dependency-name: "@opentelemetry/auto-instrumentations-node" + versions: ["0.70.0"] + - dependency-name: "@types/node" + versions: ["25.3.0"] + - dependency-name: "typescript-eslint" + versions: ["8.56.0"] diff --git a/.github/instructions/review.instructions.md b/.github/instructions/review.instructions.md index 5a6756db..7550ddf8 100644 --- a/.github/instructions/review.instructions.md +++ b/.github/instructions/review.instructions.md @@ -1,6 +1,6 @@ --- -applyTo: '**' -excludeAgent: 'cloud-agent' +applyTo: "**" +excludeAgent: "cloud-agent" --- ## UX diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index d2c169b4..8f76be39 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -1,10 +1,10 @@ -name: 'bump-version' +name: "bump-version" on: workflow_dispatch: inputs: bump_type: - description: 'Version bump type' + description: "Version bump type" required: true type: choice options: diff --git a/.github/workflows/clean-up.yml b/.github/workflows/clean-up.yml index 32a58284..4cc29c4e 100644 --- a/.github/workflows/clean-up.yml +++ b/.github/workflows/clean-up.yml @@ -1,14 +1,14 @@ -name: 'misc' +name: "misc" permissions: {} on: workflow_dispatch: push: schedule: - - cron: '0 10 * * *' + - cron: "0 10 * * *" concurrency: - group: '${{ github.workflow }}-${{ github.ref }}' + group: "${{ github.workflow }}-${{ github.ref }}" cancel-in-progress: true jobs: diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 7ea12897..ef00f25e 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/.github/workflows/e2e-skills.yml b/.github/workflows/e2e-skills.yml index 24749dc8..1ad18db0 100644 --- a/.github/workflows/e2e-skills.yml +++ b/.github/workflows/e2e-skills.yml @@ -1,4 +1,4 @@ -name: 'Skills E2E' +name: "Skills E2E" permissions: {} on: diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index d3511690..8303d502 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -1,4 +1,4 @@ -name: 'examples' +name: "examples" permissions: {} on: @@ -8,26 +8,26 @@ on: - main - release paths-ignore: - - '**/*.md' - - 'LICENSE' - - 'benchmarks/**' + - "**/*.md" + - "LICENSE" + - "benchmarks/**" pull_request: paths-ignore: - - '**/*.md' - - 'LICENSE' - - 'benchmarks/**' + - "**/*.md" + - "LICENSE" + - "benchmarks/**" concurrency: - group: '${{ github.workflow }}-${{ github.ref }}' + group: "${{ github.workflow }}-${{ github.ref }}" cancel-in-progress: true env: OTEL_SERVICE_NAME: vitest OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }} - OTEL_EXPORTER_OTLP_HEADERS: 'authorization=${{ secrets.OTEL_EXPORTER_OTLP_API_KEY }}' - OTEL_RESOURCE_ATTRIBUTES: 'service.namespace=clickhouse-js,deployment.environment=ci' - VITEST_OTEL_ENABLED: 'true' - VITEST_COVERAGE: 'true' + OTEL_EXPORTER_OTLP_HEADERS: "authorization=${{ secrets.OTEL_EXPORTER_OTLP_API_KEY }}" + OTEL_RESOURCE_ATTRIBUTES: "service.namespace=clickhouse-js,deployment.environment=ci" + VITEST_OTEL_ENABLED: "true" + VITEST_COVERAGE: "true" jobs: code-quality: @@ -79,8 +79,8 @@ jobs: env: CLICKHOUSE_VERSION: ${{ matrix.clickhouse }} with: - compose-file: 'docker-compose.yml' - down-flags: '--volumes' + compose-file: "docker-compose.yml" + down-flags: "--volumes" - name: Setup NodeJS uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 @@ -113,7 +113,7 @@ jobs: npm run run-examples success: - needs: ['code-quality', 'run-examples'] + needs: ["code-quality", "run-examples"] runs-on: ubuntu-latest steps: - name: All examples checks passed diff --git a/.github/workflows/github-export-otel.yml b/.github/workflows/github-export-otel.yml index 644ba1ce..a26da23c 100644 --- a/.github/workflows/github-export-otel.yml +++ b/.github/workflows/github-export-otel.yml @@ -23,8 +23,8 @@ jobs: env: OTEL_SERVICE_NAME: github-actions OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }} - OTEL_EXPORTER_OTLP_HEADERS: 'authorization=${{ secrets.OTEL_EXPORTER_OTLP_API_KEY }}' - OTEL_RESOURCE_ATTRIBUTES: 'service.namespace=clickhouse-js' + OTEL_EXPORTER_OTLP_HEADERS: "authorization=${{ secrets.OTEL_EXPORTER_OTLP_API_KEY }}" + OTEL_RESOURCE_ATTRIBUTES: "service.namespace=clickhouse-js" with: # Required for collecting workflow data GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index aa841c22..63743dba 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,4 +1,4 @@ -name: 'publish' +name: "publish" # As NPM only supports a single workflow for publishing packages, # this workflow is both triggered on push to the `release` branch and manually. @@ -30,15 +30,15 @@ on: # common package, so any change under packages/** triggers an # all-or-nothing publish of every package. paths: - - 'packages/**' - - 'package.json' - - 'package-lock.json' - - 'tsconfig.base.json' - - 'README.md' - - 'LICENSE' - - 'skills/**' - - '.scripts/update_version.sh' - - '.github/workflows/publish.yml' + - "packages/**" + - "package.json" + - "package-lock.json" + - "tsconfig.base.json" + - "README.md" + - "LICENSE" + - "skills/**" + - ".scripts/update_version.sh" + - ".github/workflows/publish.yml" jobs: head: @@ -55,7 +55,7 @@ jobs: uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: 24 - registry-url: 'https://registry.npmjs.org' + registry-url: "https://registry.npmjs.org" - name: Install dependencies run: npm ci @@ -96,7 +96,7 @@ jobs: uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: 24 - registry-url: 'https://registry.npmjs.org' + registry-url: "https://registry.npmjs.org" - name: Install dependencies run: npm ci @@ -155,7 +155,7 @@ jobs: uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: ${{ matrix.node }} - registry-url: 'https://registry.npmjs.org' + registry-url: "https://registry.npmjs.org" - name: Wait for ${{ needs.head.outputs.version || needs.latest.outputs.version }} to be available on npm run: | diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 03960653..f4770a6f 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -10,21 +10,21 @@ on: # To guarantee Maintained check is occasionally updated. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained schedule: - - cron: '43 12 * * 6' + - cron: "43 12 * * 6" push: branches: - main paths-ignore: - - '**/*.md' - - 'LICENSE' - - 'benchmarks/**' - - 'examples/**' + - "**/*.md" + - "LICENSE" + - "benchmarks/**" + - "examples/**" pull_request: paths-ignore: - - '**/*.md' - - 'LICENSE' - - 'benchmarks/**' - - 'examples/**' + - "**/*.md" + - "LICENSE" + - "benchmarks/**" + - "examples/**" workflow_dispatch: # Declare default permissions as read only. @@ -44,12 +44,12 @@ jobs: # actions: read steps: - - name: 'Checkout code' + - name: "Checkout code" uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - - name: 'Run analysis' + - name: "Run analysis" uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 with: results_file: results.sarif @@ -71,7 +71,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - - name: 'Upload artifact' + - name: "Upload artifact" uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: SARIF file @@ -80,7 +80,7 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - - name: 'Upload to code-scanning' + - name: "Upload to code-scanning" uses: github/codeql-action/upload-sarif@v3 with: sarif_file: results.sarif diff --git a/.github/workflows/tests-bun.yml b/.github/workflows/tests-bun.yml index 8a86f9d7..b464da77 100644 --- a/.github/workflows/tests-bun.yml +++ b/.github/workflows/tests-bun.yml @@ -1,4 +1,4 @@ -name: 'bun' +name: "bun" permissions: {} on: @@ -7,17 +7,17 @@ on: branches: - main paths-ignore: - - '**/*.md' - - 'LICENSE' - - 'benchmarks/**' + - "**/*.md" + - "LICENSE" + - "benchmarks/**" pull_request: paths-ignore: - - '**/*.md' - - 'LICENSE' - - 'benchmarks/**' + - "**/*.md" + - "LICENSE" + - "benchmarks/**" concurrency: - group: '${{ github.workflow }}-${{ github.ref }}' + group: "${{ github.workflow }}-${{ github.ref }}" cancel-in-progress: true jobs: diff --git a/.github/workflows/tests-node.yml b/.github/workflows/tests-node.yml index 3da72836..a4091268 100644 --- a/.github/workflows/tests-node.yml +++ b/.github/workflows/tests-node.yml @@ -1,4 +1,4 @@ -name: 'node' +name: "node" permissions: {} on: @@ -7,29 +7,29 @@ on: branches: - main paths-ignore: - - '**/*.md' - - 'LICENSE' - - 'benchmarks/**' + - "**/*.md" + - "LICENSE" + - "benchmarks/**" pull_request: paths-ignore: - - '**/*.md' - - 'LICENSE' - - 'benchmarks/**' + - "**/*.md" + - "LICENSE" + - "benchmarks/**" schedule: - - cron: '0 9 * * *' + - cron: "0 9 * * *" concurrency: - group: '${{ github.workflow }}-${{ github.ref }}' + group: "${{ github.workflow }}-${{ github.ref }}" cancel-in-progress: true env: OTEL_SERVICE_NAME: vitest OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }} - OTEL_EXPORTER_OTLP_HEADERS: 'authorization=${{ secrets.OTEL_EXPORTER_OTLP_API_KEY }}' - OTEL_RESOURCE_ATTRIBUTES: 'service.namespace=clickhouse-js,deployment.environment=ci' - VITEST_OTEL_ENABLED: 'true' - VITEST_COVERAGE: 'true' + OTEL_EXPORTER_OTLP_HEADERS: "authorization=${{ secrets.OTEL_EXPORTER_OTLP_API_KEY }}" + OTEL_RESOURCE_ATTRIBUTES: "service.namespace=clickhouse-js,deployment.environment=ci" + VITEST_OTEL_ENABLED: "true" + VITEST_COVERAGE: "true" jobs: code-quality: @@ -155,8 +155,8 @@ jobs: env: CLICKHOUSE_VERSION: ${{ matrix.clickhouse }} with: - compose-file: 'docker-compose.yml' - down-flags: '--volumes' + compose-file: "docker-compose.yml" + down-flags: "--volumes" - name: Setup NodeJS ${{ matrix.node }} uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 @@ -200,8 +200,8 @@ jobs: env: CLICKHOUSE_VERSION: ${{ matrix.clickhouse }} with: - compose-file: 'docker-compose.yml' - down-flags: '--volumes' + compose-file: "docker-compose.yml" + down-flags: "--volumes" - name: Setup NodeJS ${{ matrix.node }} uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 @@ -276,8 +276,8 @@ jobs: env: CLICKHOUSE_VERSION: ${{ matrix.clickhouse }} with: - compose-file: 'docker-compose.yml' - down-flags: '--volumes' + compose-file: "docker-compose.yml" + down-flags: "--volumes" - name: Setup NodeJS uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 @@ -309,13 +309,13 @@ jobs: success: needs: [ - 'code-quality', - 'common-unit-tests', - 'unit-tests', - 'integration-tests-local-single-node', - 'integration-tests-local-cluster', - 'integration-tests-cloud', - 'codecov-upload', + "code-quality", + "common-unit-tests", + "unit-tests", + "integration-tests-local-single-node", + "integration-tests-local-cluster", + "integration-tests-cloud", + "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. diff --git a/.github/workflows/tests-web.yml b/.github/workflows/tests-web.yml index 3e2c9e3f..7c7381c5 100644 --- a/.github/workflows/tests-web.yml +++ b/.github/workflows/tests-web.yml @@ -1,4 +1,4 @@ -name: 'web' +name: "web" permissions: {} on: @@ -7,29 +7,29 @@ on: branches: - main paths-ignore: - - '**/*.md' - - 'LICENSE' - - 'benchmarks/**' + - "**/*.md" + - "LICENSE" + - "benchmarks/**" pull_request: paths-ignore: - - '**/*.md' - - 'LICENSE' - - 'benchmarks/**' + - "**/*.md" + - "LICENSE" + - "benchmarks/**" schedule: - - cron: '0 9 * * *' + - cron: "0 9 * * *" concurrency: - group: '${{ github.workflow }}-${{ github.ref }}' + group: "${{ github.workflow }}-${{ github.ref }}" cancel-in-progress: true env: OTEL_SERVICE_NAME: vitest OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }} - OTEL_EXPORTER_OTLP_HEADERS: 'authorization=${{ secrets.OTEL_EXPORTER_OTLP_API_KEY }}' - OTEL_RESOURCE_ATTRIBUTES: 'service.namespace=clickhouse-js,deployment.environment=ci' - VITEST_OTEL_ENABLED: 'true' - VITEST_COVERAGE: 'true' + OTEL_EXPORTER_OTLP_HEADERS: "authorization=${{ secrets.OTEL_EXPORTER_OTLP_API_KEY }}" + OTEL_RESOURCE_ATTRIBUTES: "service.namespace=clickhouse-js,deployment.environment=ci" + VITEST_OTEL_ENABLED: "true" + VITEST_COVERAGE: "true" jobs: common-unit-tests: @@ -80,8 +80,8 @@ jobs: env: CLICKHOUSE_VERSION: ${{ matrix.clickhouse }} with: - compose-file: 'docker-compose.yml' - down-flags: '--volumes' + compose-file: "docker-compose.yml" + down-flags: "--volumes" - name: Setup NodeJS uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 @@ -121,8 +121,8 @@ jobs: env: CLICKHOUSE_VERSION: ${{ matrix.clickhouse }} with: - compose-file: 'docker-compose.yml' - down-flags: '--volumes' + compose-file: "docker-compose.yml" + down-flags: "--volumes" - name: Setup NodeJS uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 @@ -201,8 +201,8 @@ jobs: env: CLICKHOUSE_VERSION: ${{ matrix.clickhouse }} with: - compose-file: 'docker-compose.yml' - down-flags: '--volumes' + compose-file: "docker-compose.yml" + down-flags: "--volumes" - name: Setup NodeJS uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 @@ -235,11 +235,11 @@ jobs: success: needs: [ - 'common-unit-tests', - 'all-tests-local-single-node', - 'integration-tests-local-cluster', - 'integration-tests-cloud', - 'codecov-upload', + "common-unit-tests", + "all-tests-local-single-node", + "integration-tests-local-cluster", + "integration-tests-cloud", + "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. diff --git a/.github/workflows/upstream-sql-tests.yml b/.github/workflows/upstream-sql-tests.yml index 3d521ee4..5c410df6 100644 --- a/.github/workflows/upstream-sql-tests.yml +++ b/.github/workflows/upstream-sql-tests.yml @@ -1,4 +1,4 @@ -name: 'upstream-sql-tests' +name: "upstream-sql-tests" permissions: {} @@ -6,29 +6,29 @@ on: workflow_dispatch: inputs: upstream_ref: - description: 'ClickHouse/ClickHouse ref to check out' + description: "ClickHouse/ClickHouse ref to check out" required: false - default: 'master' + default: "master" type: string schedule: - - cron: '0 5 * * *' + - cron: "0 5 * * *" push: branches: - main paths: - - 'tests/clickhouse-test-runner/**' - - '.github/workflows/upstream-sql-tests.yml' + - "tests/clickhouse-test-runner/**" + - ".github/workflows/upstream-sql-tests.yml" pull_request: paths: - - 'tests/clickhouse-test-runner/**' - - '.github/workflows/upstream-sql-tests.yml' + - "tests/clickhouse-test-runner/**" + - ".github/workflows/upstream-sql-tests.yml" concurrency: - group: '${{ github.workflow }}-${{ github.ref }}' + group: "${{ github.workflow }}-${{ github.ref }}" cancel-in-progress: true env: - UPSTREAM_REPO: 'ClickHouse/ClickHouse' + UPSTREAM_REPO: "ClickHouse/ClickHouse" jobs: upstream-sql-tests: @@ -70,7 +70,7 @@ jobs: - name: Setup Python uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: - python-version: '3.12' + python-version: "3.12" - name: Install Python dependencies for upstream clickhouse-test run: | @@ -82,8 +82,8 @@ jobs: env: CLICKHOUSE_VERSION: ${{ matrix.clickhouse }} with: - compose-file: 'docker-compose.yml' - down-flags: '--volumes' + compose-file: "docker-compose.yml" + down-flags: "--volumes" - name: Build test runner run: | diff --git a/.prettierrc b/.prettierrc index fd496a82..0967ef42 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1 @@ -{ - "singleQuote": true, - "semi": false -} +{} diff --git a/.scripts/cleanup_old_databases.mjs b/.scripts/cleanup_old_databases.mjs index 354f2434..93341f1a 100644 --- a/.scripts/cleanup_old_databases.mjs +++ b/.scripts/cleanup_old_databases.mjs @@ -7,78 +7,78 @@ * Integrations tests take around 1 minute to run, * so we set TTL to 10 minutes by default to give some buffer. */ -const TTL_MINUTES = process.env.TTL_MINUTES || 10 -const PREFIX = process.env.PREFIX || 'clickhousejs_' -const CLICKHOUSE_CLOUD_HOST = process.env.CLICKHOUSE_CLOUD_HOST -const CLICKHOUSE_CLOUD_PASSWORD = process.env.CLICKHOUSE_CLOUD_PASSWORD +const TTL_MINUTES = process.env.TTL_MINUTES || 10; +const PREFIX = process.env.PREFIX || "clickhousejs_"; +const CLICKHOUSE_CLOUD_HOST = process.env.CLICKHOUSE_CLOUD_HOST; +const CLICKHOUSE_CLOUD_PASSWORD = process.env.CLICKHOUSE_CLOUD_PASSWORD; // Executes query using HTTP interface async function executeQuery(query) { const r = await fetch( `https://${CLICKHOUSE_CLOUD_HOST}/?query=${encodeURIComponent(query)}`, { - method: 'POST', + method: "POST", headers: { - Authorization: `Basic ${Buffer.from(`default:${CLICKHOUSE_CLOUD_PASSWORD}`).toString('base64')}`, + Authorization: `Basic ${Buffer.from(`default:${CLICKHOUSE_CLOUD_PASSWORD}`).toString("base64")}`, }, }, - ) + ); if (!r.ok) { try { - const errorText = await r.text() + const errorText = await r.text(); throw new Error( `Query failed: ${r.status} ${r.statusText} - ${errorText}`, - ) + ); } catch (e) { throw new Error( `Query failed: ${r.status} ${r.statusText} - Unable to read error details`, - ) + ); } } - return r + return r; } // Main script -console.log(`🔍 Searching for databases matching "${PREFIX}%"...\n`) +console.log(`🔍 Searching for databases matching "${PREFIX}%"...\n`); // Query for databases -const query = `SELECT name FROM system.databases WHERE name LIKE '${PREFIX}%' FORMAT JSON` +const query = `SELECT name FROM system.databases WHERE name LIKE '${PREFIX}%' FORMAT JSON`; -const result = await executeQuery(query) -const { data } = await result.json() +const result = await executeQuery(query); +const { data } = await result.json(); if (data.length === 0) { - console.log(`✅ No databases found matching "${PREFIX}%"`) - process.exit(0) + console.log(`✅ No databases found matching "${PREFIX}%"`); + process.exit(0); } -console.log(`Found ${data.length} database(s):`) -data.forEach((row) => console.log(` - ${row.name}`)) -console.log() +console.log(`Found ${data.length} database(s):`); +data.forEach((row) => console.log(` - ${row.name}`)); +console.log(); // Shuffle the list to avoid dropping the same databases first every time // and also allow for more efficient parallel dropping in case there // are many databases to clean up. -data.sort(() => Math.random() - 0.5) +data.sort(() => Math.random() - 0.5); // Drop each database -let droppedCount = 0 +let droppedCount = 0; for (const { name } of data) { try { - const timestamp = new Date(Number(name.split('__').pop())) + const timestamp = new Date(Number(name.split("__").pop())); if (Date.now() - timestamp.getTime() < TTL_MINUTES * 60 * 1000) { console.log( `⏳ Skipping ${name} (created at ${timestamp.toISOString()}) - TTL not expired`, - ) - continue + ); + continue; } - await executeQuery(`DROP DATABASE IF EXISTS ${name}`) - console.log(`✅ Successfully dropped: ${name}`) - droppedCount++ + await executeQuery(`DROP DATABASE IF EXISTS ${name}`); + console.log(`✅ Successfully dropped: ${name}`); + droppedCount++; } catch (error) { - console.error(`❌ Failed to drop ${name}: ${error.message}`) + console.error(`❌ Failed to drop ${name}: ${error.message}`); } } -console.log(`\n🎉 Cleanup completed! Dropped ${droppedCount} database(s).`) +console.log(`\n🎉 Cleanup completed! Dropped ${droppedCount} database(s).`); diff --git a/.scripts/export-coverage-metrics.mjs b/.scripts/export-coverage-metrics.mjs index 6c65a62d..cbdae26b 100755 --- a/.scripts/export-coverage-metrics.mjs +++ b/.scripts/export-coverage-metrics.mjs @@ -13,28 +13,28 @@ * - Branch coverage percentage per file */ -import { readFileSync } from 'fs' -import { join } from 'path' +import { readFileSync } from "fs"; +import { join } from "path"; import { MeterProvider, PeriodicExportingMetricReader, -} from '@opentelemetry/sdk-metrics' -import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto' -import { resourceFromAttributes } from '@opentelemetry/resources' +} from "@opentelemetry/sdk-metrics"; +import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto"; +import { resourceFromAttributes } from "@opentelemetry/resources"; import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, -} from '@opentelemetry/semantic-conventions' +} from "@opentelemetry/semantic-conventions"; // Parse lcov.info file format function parseLcov(content) { - const files = [] - let currentFile = null + const files = []; + let currentFile = null; - for (const line of content.split('\n')) { - const trimmed = line.trim() + for (const line of content.split("\n")) { + const trimmed = line.trim(); - if (trimmed.startsWith('SF:')) { + if (trimmed.startsWith("SF:")) { // Source File - start of a new file entry currentFile = { path: trimmed.substring(3), @@ -44,267 +44,270 @@ function parseLcov(content) { functionsHit: 0, branchesFound: 0, branchesHit: 0, - } - } else if (trimmed.startsWith('LF:')) { + }; + } else if (trimmed.startsWith("LF:")) { // Lines Found - currentFile.linesFound = parseInt(trimmed.substring(3), 10) - } else if (trimmed.startsWith('LH:')) { + currentFile.linesFound = parseInt(trimmed.substring(3), 10); + } else if (trimmed.startsWith("LH:")) { // Lines Hit - currentFile.linesHit = parseInt(trimmed.substring(3), 10) - } else if (trimmed.startsWith('FNF:')) { + currentFile.linesHit = parseInt(trimmed.substring(3), 10); + } else if (trimmed.startsWith("FNF:")) { // Functions Found - currentFile.functionsFound = parseInt(trimmed.substring(4), 10) - } else if (trimmed.startsWith('FNH:')) { + currentFile.functionsFound = parseInt(trimmed.substring(4), 10); + } else if (trimmed.startsWith("FNH:")) { // Functions Hit - currentFile.functionsHit = parseInt(trimmed.substring(4), 10) - } else if (trimmed.startsWith('BRF:')) { + currentFile.functionsHit = parseInt(trimmed.substring(4), 10); + } else if (trimmed.startsWith("BRF:")) { // Branches Found - currentFile.branchesFound = parseInt(trimmed.substring(4), 10) - } else if (trimmed.startsWith('BRH:')) { + currentFile.branchesFound = parseInt(trimmed.substring(4), 10); + } else if (trimmed.startsWith("BRH:")) { // Branches Hit - currentFile.branchesHit = parseInt(trimmed.substring(4), 10) - } else if (trimmed === 'end_of_record') { + currentFile.branchesHit = parseInt(trimmed.substring(4), 10); + } else if (trimmed === "end_of_record") { // End of current file record if (currentFile) { - files.push(currentFile) - currentFile = null + files.push(currentFile); + currentFile = null; } } } - return files + return files; } // Calculate coverage percentage function calculatePercentage(hit, found) { - if (found === 0) return 100 // No code to cover = 100% - return (hit / found) * 100 + if (found === 0) return 100; // No code to cover = 100% + return (hit / found) * 100; } // Main function async function exportCoverageMetrics() { // Get coverage file path from args or use default const coverageFile = - process.argv[2] || join(process.cwd(), 'coverage', 'lcov.info') + process.argv[2] || join(process.cwd(), "coverage", "lcov.info"); - console.log(`Reading coverage from: ${coverageFile}`) + console.log(`Reading coverage from: ${coverageFile}`); // Read and parse coverage file - let content + let content; try { - content = readFileSync(coverageFile, 'utf-8') + content = readFileSync(coverageFile, "utf-8"); } catch (error) { - console.error(`Error reading coverage file: ${error.message}`) - process.exit(1) + console.error(`Error reading coverage file: ${error.message}`); + process.exit(1); } - const files = parseLcov(content) - console.log(`Parsed coverage for ${files.length} files`) + const files = parseLcov(content); + console.log(`Parsed coverage for ${files.length} files`); // Log coverage summary - console.log('\nCoverage Summary:') - console.log('=================') + console.log("\nCoverage Summary:"); + console.log("================="); let totalLines = 0, - totalLinesHit = 0 + totalLinesHit = 0; let totalFunctions = 0, - totalFunctionsHit = 0 + totalFunctionsHit = 0; let totalBranches = 0, - totalBranchesHit = 0 + totalBranchesHit = 0; files.forEach((file) => { - totalLines += file.linesFound - totalLinesHit += file.linesHit - totalFunctions += file.functionsFound - totalFunctionsHit += file.functionsHit - totalBranches += file.branchesFound - totalBranchesHit += file.branchesHit - }) + totalLines += file.linesFound; + totalLinesHit += file.linesHit; + totalFunctions += file.functionsFound; + totalFunctionsHit += file.functionsHit; + totalBranches += file.branchesFound; + totalBranchesHit += file.branchesHit; + }); console.log( `Total Line Coverage: ${calculatePercentage(totalLinesHit, totalLines).toFixed(2)}%`, - ) + ); console.log( `Total Function Coverage: ${calculatePercentage(totalFunctionsHit, totalFunctions).toFixed(2)}%`, - ) + ); console.log( `Total Branch Coverage: ${calculatePercentage(totalBranchesHit, totalBranches).toFixed(2)}%`, - ) + ); // Setup OpenTelemetry const resource = resourceFromAttributes({ - [ATTR_SERVICE_NAME]: 'test-coverage', + [ATTR_SERVICE_NAME]: "test-coverage", [ATTR_SERVICE_VERSION]: process.env.GITHUB_SHA?.substring(0, 7), - 'ci.run.id': process.env.GITHUB_RUN_ID, - 'ci.job.name': process.env.GITHUB_JOB, - 'ci.workflow': process.env.GITHUB_WORKFLOW, - 'coverage.exporter': 'vitest-lcov', - 'coverage.report.name': process.env.COVERAGE_REPORT_NAME || 'default', - }) - - const metricExporter = new OTLPMetricExporter() + "ci.run.id": process.env.GITHUB_RUN_ID, + "ci.job.name": process.env.GITHUB_JOB, + "ci.workflow": process.env.GITHUB_WORKFLOW, + "coverage.exporter": "vitest-lcov", + "coverage.report.name": process.env.COVERAGE_REPORT_NAME || "default", + }); + + const metricExporter = new OTLPMetricExporter(); const metricReader = new PeriodicExportingMetricReader({ exporter: metricExporter, exportIntervalMillis: 1000, // Exports immediately below - }) + }); const meterProvider = new MeterProvider({ resource, readers: [metricReader], - }) + }); - const meter = meterProvider.getMeter('coverage-exporter', '1.0.0') + const meter = meterProvider.getMeter("coverage-exporter", "1.0.0"); // Create observable gauges for each metric type const lineCoverageGauge = meter.createObservableGauge( - 'code.coverage.line.percentage', + "code.coverage.line.percentage", { - description: 'Line coverage percentage per file', - unit: '%', + description: "Line coverage percentage per file", + unit: "%", }, - ) + ); const functionCoverageGauge = meter.createObservableGauge( - 'code.coverage.function.percentage', + "code.coverage.function.percentage", { - description: 'Function coverage percentage per file', - unit: '%', + description: "Function coverage percentage per file", + unit: "%", }, - ) + ); const branchCoverageGauge = meter.createObservableGauge( - 'code.coverage.branch.percentage', + "code.coverage.branch.percentage", { - description: 'Branch coverage percentage per file', - unit: '%', + description: "Branch coverage percentage per file", + unit: "%", }, - ) + ); const totalLinesGauge = meter.createObservableGauge( - 'code.coverage.line.total', + "code.coverage.line.total", { - description: 'Total lines per file', - unit: 'lines', + description: "Total lines per file", + unit: "lines", }, - ) + ); const coveredLinesGauge = meter.createObservableGauge( - 'code.coverage.line.covered', + "code.coverage.line.covered", { - description: 'Covered lines per file', - unit: 'lines', + description: "Covered lines per file", + unit: "lines", }, - ) + ); const totalLinesCoverageGauge = meter.createObservableGauge( - 'code.coverage.line.total.percentage', + "code.coverage.line.total.percentage", { - description: 'Total line coverage percentage', - unit: '%', + description: "Total line coverage percentage", + unit: "%", }, - ) + ); const totalFunctionsCoverageGauge = meter.createObservableGauge( - 'code.coverage.function.total.percentage', + "code.coverage.function.total.percentage", { - description: 'Total function coverage percentage', - unit: '%', + description: "Total function coverage percentage", + unit: "%", }, - ) + ); const totalBranchesCoverageGauge = meter.createObservableGauge( - 'code.coverage.branch.total.percentage', + "code.coverage.branch.total.percentage", { - description: 'Total branch coverage percentage', - unit: '%', + description: "Total branch coverage percentage", + unit: "%", }, - ) + ); const totalFileCountGauge = meter.createObservableGauge( - 'code.coverage.file.count', + "code.coverage.file.count", { - description: 'Total number of files covered', - unit: 'files', + description: "Total number of files covered", + unit: "files", }, - ) + ); // Register callbacks to observe metrics lineCoverageGauge.addCallback((observableResult) => { files.forEach((file) => { - const percentage = calculatePercentage(file.linesHit, file.linesFound) - observableResult.observe(percentage, { file: file.path }) - }) - }) + const percentage = calculatePercentage(file.linesHit, file.linesFound); + observableResult.observe(percentage, { file: file.path }); + }); + }); functionCoverageGauge.addCallback((observableResult) => { files.forEach((file) => { const percentage = calculatePercentage( file.functionsHit, file.functionsFound, - ) - observableResult.observe(percentage, { file: file.path }) - }) - }) + ); + observableResult.observe(percentage, { file: file.path }); + }); + }); branchCoverageGauge.addCallback((observableResult) => { files.forEach((file) => { const percentage = calculatePercentage( file.branchesHit, file.branchesFound, - ) - observableResult.observe(percentage, { file: file.path }) - }) - }) + ); + observableResult.observe(percentage, { file: file.path }); + }); + }); totalLinesGauge.addCallback((observableResult) => { files.forEach((file) => { - observableResult.observe(file.linesFound, { file: file.path }) - }) - }) + observableResult.observe(file.linesFound, { file: file.path }); + }); + }); coveredLinesGauge.addCallback((observableResult) => { files.forEach((file) => { - observableResult.observe(file.linesHit, { file: file.path }) - }) - }) + observableResult.observe(file.linesHit, { file: file.path }); + }); + }); totalLinesCoverageGauge.addCallback((observableResult) => { - const totalPercentage = calculatePercentage(totalLinesHit, totalLines) - observableResult.observe(totalPercentage, {}) - }) + const totalPercentage = calculatePercentage(totalLinesHit, totalLines); + observableResult.observe(totalPercentage, {}); + }); totalFunctionsCoverageGauge.addCallback((observableResult) => { const totalPercentage = calculatePercentage( totalFunctionsHit, totalFunctions, - ) - observableResult.observe(totalPercentage, {}) - }) + ); + observableResult.observe(totalPercentage, {}); + }); totalBranchesCoverageGauge.addCallback((observableResult) => { - const totalPercentage = calculatePercentage(totalBranchesHit, totalBranches) - observableResult.observe(totalPercentage, {}) - }) + const totalPercentage = calculatePercentage( + totalBranchesHit, + totalBranches, + ); + observableResult.observe(totalPercentage, {}); + }); totalFileCountGauge.addCallback((observableResult) => { - observableResult.observe(files.length, {}) - }) + observableResult.observe(files.length, {}); + }); - metricReader.collect() // Trigger immediate collection of metrics + metricReader.collect(); // Trigger immediate collection of metrics // Force metrics export - console.log('\nExporting metrics to OpenTelemetry collector...') - await metricReader.forceFlush() + console.log("\nExporting metrics to OpenTelemetry collector..."); + await metricReader.forceFlush(); - console.log('Metrics exported successfully!') + console.log("Metrics exported successfully!"); // Shutdown - await meterProvider.shutdown() - process.exit(0) + await meterProvider.shutdown(); + process.exit(0); } // Run the script exportCoverageMetrics().catch(async (error) => { - console.error('Error exporting coverage metrics:', error) - process.exit(1) -}) + console.error("Error exporting coverage metrics:", error); + process.exit(1); +}); diff --git a/.scripts/generate_cloud_jwt.ts b/.scripts/generate_cloud_jwt.ts index 3f3137c8..2cf9ae87 100644 --- a/.scripts/generate_cloud_jwt.ts +++ b/.scripts/generate_cloud_jwt.ts @@ -1,7 +1,7 @@ -import { makeJWT } from '../packages/client-node/__tests__/utils/jwt' +import { makeJWT } from "../packages/client-node/__tests__/utils/jwt"; /** Used to generate a JWT token for web testing (can't use `jsonwebtoken` library directly there) * See `package.json` -> `scripts` -> `test:web:integration:cloud:jwt` */ -;(() => { - console.log(makeJWT()) -})() +(() => { + console.log(makeJWT()); +})(); diff --git a/AGENTS.md b/AGENTS.md index 6c27db09..ee3ab526 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,8 +7,8 @@ ```ts if (log_level <= ClickHouseLogLevel.WARN) { log_writer.warn({ - message: 'Example log message', - }) + message: "Example log message", + }); } ``` @@ -18,8 +18,8 @@ if (some_condition) { log_writer.warn({ message: - 'Example log message with suggestions for users. For more information, see https://github.com/ClickHouse/clickhouse-js/blob/main/docs/socket_hang_up_econnreset.md', - }) + "Example log message with suggestions for users. For more information, see https://github.com/ClickHouse/clickhouse-js/blob/main/docs/socket_hang_up_econnreset.md", + }); } ``` diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ddeee08..38736d0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,14 +29,14 @@ Example: parseEnumType({ columnType: "Enum8('f\\'' = 1)", sourceType: "Enum8('f\\'' = 1)", -}) +}); // returned: { values: { 1: "f\\'" } } // with backslash // After (correct): parseEnumType({ columnType: "Enum8('f\\'' = 1)", sourceType: "Enum8('f\\'' = 1)", -}) +}); // returns: { values: { 1: "f'" } } // unescaped ``` @@ -52,9 +52,9 @@ const client = createClient({ max_response_headers_size: 1024 * 1024, // accept up to 1 MiB of response headers clickhouse_settings: { send_progress_in_http_headers: 1, - http_headers_progress_interval_ms: '110000', + http_headers_progress_interval_ms: "110000", }, -}) +}); ``` - The `@clickhouse/client` npm package now ships embedded AI-agent skills, `clickhouse-js-node-coding` and `clickhouse-js-node-troubleshooting`, under `node_modules/@clickhouse/client/skills/`. These skills are also declared in the `agents.skills` field of the package manifest for discovery tools that scan `node_modules`. This allows agentic coding tools to load focused, Node-client-specific coding and troubleshooting guidance without any additional setup. ([#682]) @@ -78,7 +78,7 @@ const client = createClient({ idle_socket_ttl: 2500, eagerly_destroy_stale_sockets: true, }, -}) +}); ``` - Added auto-detection and warning when `request_timeout` is high (> 60 seconds) but progress headers are not configured. Long-running queries may fail with socket hang-up errors if they exceed the load balancer idle timeout. The client now warns users to enable `send_progress_in_http_headers` and `http_headers_progress_interval_ms` settings to prevent such issues. @@ -88,16 +88,16 @@ const client = createClient({ const client = createClient({ request_timeout: 120_000, // 120 seconds // send_progress_in_http_headers is not configured -}) +}); // ✓ Properly configured to avoid load balancer timeouts const client = createClient({ request_timeout: 400_000, clickhouse_settings: { send_progress_in_http_headers: 1, - http_headers_progress_interval_ms: '110000', // ~10s below LB timeout + http_headers_progress_interval_ms: "110000", // ~10s below LB timeout }, -}) +}); ``` # 1.18.2 @@ -119,7 +119,7 @@ const client = createClient({ log: { level: ClickHouseLogLevel.WARN, // to see the warning logs }, -}) +}); for (let i = 0; i < 1000; i++) { await client.ping({ @@ -127,14 +127,14 @@ for (let i = 0; i < 1000; i++) { // which might be configured differently on the server side // and have different timeout settings. select: true, - }) + }); // Wait long enough to let the server close the idle connection, // but not too long to let the client remove it from the pool, // in other words try to hit the scenario when the race condition // happens between the server closing the connection and the client // trying to reuse it. - await sleep(SERVER_KEEP_ALIVE_TIMEOUT_MS - 100) + await sleep(SERVER_KEEP_ALIVE_TIMEOUT_MS - 100); } ``` @@ -168,7 +168,7 @@ const client = createClient({ log: { level: ClickHouseLogLevel.WARN, // default is now ClickHouseLogLevel.WARN instead of ClickHouseLogLevel.OFF }, -}) +}); ``` - Logging is now lazy, which means that the log messages will only be constructed if the log level is appropriate for the message. This can improve performance in cases where constructing the log message is expensive, and the log level is set to ignore such messages. See `ClickHouseLogLevel` enum for the complete list of log levels. ([#520]) @@ -179,7 +179,7 @@ const client = createClient({ log: { level: ClickHouseLogLevel.TRACE, // to log everything available down to the network level events }, -}) +}); ``` - Enhanced the logging of the HTTP request / socket lifecycle with additional trace messages and context such as Connection ID (UUID) and Request ID and Socket ID that embed the connection ID for ease of tracing the logs of a particular request across the connection lifecycle. To enable such logs, set the `log.level` config option to `ClickHouseLogLevel.TRACE`. ([#567]) @@ -404,8 +404,8 @@ A minor release to allow further investigation regarding uncaught error issues w ```ts const client = createClient({ // ... - access_token: '', - }) + access_token: "", + }); ``` Access token can also be configured via the URL params, e.g., `https://host:port?access_token=...`. @@ -456,12 +456,12 @@ A minor release to allow further investigation regarding uncaught error issues w ```ts for (const type of [ - 'Int32', - 'Array(Nullable(String))', + "Int32", + "Array(Nullable(String))", `Map(Int32, DateTime64(9, 'UTC'))`, ]) { - console.log(`##### Source ClickHouse type: ${type}`) - console.log(parseColumnType(type)) + console.log(`##### Source ClickHouse type: ${type}`); + console.log(parseColumnType(type)); } ``` @@ -546,10 +546,10 @@ A minor release to allow further investigation regarding uncaught error issues w ```ts const rs = await client.query({ - query: 'SELECT * FROM system.numbers LIMIT 1', - format: 'JSONEachRow', - }) - console.log(rs.response_headers['content-type']) + query: "SELECT * FROM system.numbers LIMIT 1", + format: "JSONEachRow", + }); + console.log(rs.response_headers["content-type"]); ``` This will print: `application/x-ndjson; charset=UTF-8`. It can be used in a similar way with the other methods. @@ -576,19 +576,19 @@ A minor release to allow further investigation regarding uncaught error issues w ```ts const agent = new https.Agent({ - ca: fs.readFileSync('./ca.crt'), - }) + ca: fs.readFileSync("./ca.crt"), + }); const client = createClient({ - url: 'https://server.clickhouseconnect.test:8443', + url: "https://server.clickhouseconnect.test:8443", http_agent: agent, // With a custom HTTPS agent, the client won't use the default HTTPS connection implementation; the headers should be provided manually http_headers: { - 'X-ClickHouse-User': 'default', - 'X-ClickHouse-Key': '', + "X-ClickHouse-User": "default", + "X-ClickHouse-Key": "", }, // Authorization header conflicts with the TLS headers; disable it. set_basic_auth_header: false, - }) + }); ``` NB: It is currently not possible to set the `set_basic_auth_header` option via the URL params. @@ -678,83 +678,83 @@ Complete reference: By default, `T` (which represents `JSONType`) is still `unknown`. However, considering `JSONObjectsEachRow` example: prior to 1.0.0, you had to specify the entire type hint, including the shape of the data, manually: ```ts -type Data = { foo: string } +type Data = { foo: string }; const resultSet = await client.query({ - query: 'SELECT * FROM my_table', - format: 'JSONObjectsEachRow', -}) + query: "SELECT * FROM my_table", + format: "JSONObjectsEachRow", +}); // pre-1.0.0, `resultOld` has type Record -const resultOld = resultSet.json>() +const resultOld = resultSet.json>(); // const resultOld = resultSet.json() // incorrect! The type hint should've been `Record` here. // 1.0.0, `resultNew` also has type Record; client inferred that it has to be a Record from the format literal. -const resultNew = resultSet.json() +const resultNew = resultSet.json(); ``` This is even more handy in case of streaming on the Node.js platform: ```ts const resultSet = await client.query({ - query: 'SELECT * FROM my_table', - format: 'JSONEachRow', -}) + query: "SELECT * FROM my_table", + format: "JSONEachRow", +}); // pre-1.0.0 // `streamOld` was just a regular Node.js Stream.Readable -const streamOld = resultSet.stream() +const streamOld = resultSet.stream(); // `rows` were `any`, needed an explicit type hint -streamNew.on('data', (rows: Row[]) => { +streamNew.on("data", (rows: Row[]) => { rows.forEach((row) => { // without an explicit type hint to `rows`, calling `forEach` and other array methods resulted in TS compiler errors - const t = row.text - const j = row.json() // `j` needed a type hint here, otherwise, it's `unknown` - }) -}) + const t = row.text; + const j = row.json(); // `j` needed a type hint here, otherwise, it's `unknown` + }); +}); // 1.0.0 // `streamNew` is now StreamReadable (Node.js Stream.Readable with a bit more type hints); // type hint for the further `json` calls can be added here (and removed from the `json` calls) -const streamNew = resultSet.stream() +const streamNew = resultSet.stream(); // `rows` are inferred as an Array> instead of `any` -streamNew.on('data', (rows) => { +streamNew.on("data", (rows) => { // `row` is inferred as Row rows.forEach((row) => { // no explicit type hints required, you can use `forEach` straight away and TS compiler will be happy - const t = row.text - const j = row.json() // `j` will be of type Data - }) -}) + const t = row.text; + const j = row.json(); // `j` will be of type Data + }); +}); // async iterator now also has type hints // similarly to the `on(data)` example above, `rows` are inferred as Array> for await (const rows of streamNew) { // `row` is inferred as Row rows.forEach((row) => { - const t = row.text - const j = row.json() // `j` will be of type Data - }) + const t = row.text; + const j = row.json(); // `j` will be of type Data + }); } ``` Calling `ResultSet.stream` is not allowed for certain data formats, such as `JSON` and `JSONObjectsEachRow` (unlike `JSONEachRow` and the rest of `JSON*EachRow`, these formats return a single object). In these cases, the client throws an error. However, it was previously not reflected on the type level; now, calling `stream` on these formats will result in a TS compiler error. For example: ```ts -const resultSet = await client.query('SELECT * FROM table', { - format: 'JSON', -}) -const stream = resultSet.stream() // `stream` is `never` +const resultSet = await client.query("SELECT * FROM table", { + format: "JSON", +}); +const stream = resultSet.stream(); // `stream` is `never` ``` Calling `ResultSet.json` also does not make sense on `CSV` and similar "raw" formats, and the client throws. Again, now, it is typed properly: ```ts -const resultSet = await client.query('SELECT * FROM table', { - format: 'CSV', -}) +const resultSet = await client.query("SELECT * FROM table", { + format: "CSV", +}); // `json` is `never`; same if you stream CSV, and call `Row.json` - it will be `never`, too. -const json = resultSet.json() +const json = resultSet.json(); ``` Currently, there is one known limitation: as the general shape of the data and the methods allowed for calling are inferred from the format literal, there might be situations where it will fail to do so, for example: @@ -764,10 +764,10 @@ Currently, there is one known limitation: as the general shape of the data and t async function runQuery( queryParams: QueryParams, ): Promise> { - const resultSet = await client.query(queryParams) + const resultSet = await client.query(queryParams); // type hint here will provide a union of all known shapes instead of a specific one // inferred shapes: Data[] | ResponseJSON | Record - return resultSet.json() + return resultSet.json(); } ``` @@ -779,10 +779,10 @@ async function runQuery( ): Promise> { const resultSet = await client.query({ ...queryParams, - format: 'JSONObjectsEachRow', - }) + format: "JSONObjectsEachRow", + }); // TS understands that it is a Record now - return resultSet.json() + return resultSet.json(); } ``` @@ -818,7 +818,7 @@ createClient({ async_insert: 1, wait_for_async_insert: 1, }, -}) +}); ``` Note: boolean values for `clickhouse_settings` should be passed as `1`/`0` in the URL. @@ -828,9 +828,9 @@ Note: boolean values for `clickhouse_settings` should be passed as `1`/`0` in th ```ts createClient({ http_headers: { - 'x-clickhouse-auth': 'foobar', + "x-clickhouse-auth": "foobar", }, -}) +}); ``` **Important: URL will _always_ overwrite the hardcoded values and a warning will be logged in this case.** @@ -905,10 +905,10 @@ See [0.3.0](#030-nodejs-only). ```ts const client = createClient({ additional_headers: { - 'X-ClickHouse-User': 'clickhouse_user', - 'X-ClickHouse-Key': 'clickhouse_password', + "X-ClickHouse-User": "clickhouse_user", + "X-ClickHouse-Key": "clickhouse_password", }, -}) +}); ``` ## 0.2.8 (Common, Node.js, Web) @@ -919,8 +919,8 @@ const client = createClient({ Keep-Alive setting **is now enabled by default** for the Web version. ```ts -import { createClient } from '@clickhouse/client-web' -const client = createClient({ keep_alive: { enabled: true } }) +import { createClient } from "@clickhouse/client-web"; +const client = createClient({ keep_alive: { enabled: true } }); ``` - (Node.js & Web) It is now possible to either specify a list of columns to insert the data into or a list of excluded columns: @@ -928,19 +928,19 @@ const client = createClient({ keep_alive: { enabled: true } }) ```ts // Generated query: INSERT INTO mytable (message) FORMAT JSONEachRow await client.insert({ - table: 'mytable', - format: 'JSONEachRow', - values: [{ message: 'foo' }], - columns: ['message'], -}) + table: "mytable", + format: "JSONEachRow", + values: [{ message: "foo" }], + columns: ["message"], +}); // Generated query: INSERT INTO mytable (* EXCEPT (message)) FORMAT JSONEachRow await client.insert({ - table: 'mytable', - format: 'JSONEachRow', + table: "mytable", + format: "JSONEachRow", values: [{ id: 42 }], - columns: { except: ['message'] }, -}) + columns: { except: ["message"] }, +}); ``` See also the new examples: @@ -1062,7 +1062,7 @@ const client = createClient({ keep_alive: { enabled: false, }, -}) +}); ``` #### Retry on expired socket @@ -1077,7 +1077,7 @@ const client = createClient({ socket_ttl: 2500, retry_on_expired_socket: true, }, -}) +}); ``` ## 0.1.0 @@ -1097,16 +1097,16 @@ Example: ```ts // incorrect: stream is not consumed and not destroyed, request will be timed out eventually -await client.exec('CREATE TABLE foo (id String) ENGINE Memory') +await client.exec("CREATE TABLE foo (id String) ENGINE Memory"); // correct: stream does not contain any information and just destroyed const { stream } = await client.exec( - 'CREATE TABLE foo (id String) ENGINE Memory', -) -stream.destroy() + "CREATE TABLE foo (id String) ENGINE Memory", +); +stream.destroy(); // correct: same as exec + stream.destroy() -await client.command('CREATE TABLE foo (id String) ENGINE Memory') +await client.command("CREATE TABLE foo (id String) ENGINE Memory"); ``` ### Bug fixes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 781c9751..56b34a88 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -143,7 +143,6 @@ npm run test:node:integration:tls Used when `CLICKHOUSE_TEST_ENVIRONMENT` is set to `local_cluster`. - Run the tests (Node.js): ```bash diff --git a/README.md b/README.md index 9cc94a42..b49f1e2e 100644 --- a/README.md +++ b/README.md @@ -84,23 +84,23 @@ The client may work with older versions too; however, this is best-effort suppor ## Quick start ```ts -import { createClient } from '@clickhouse/client' // or '@clickhouse/client-web' +import { createClient } from "@clickhouse/client"; // or '@clickhouse/client-web' const client = createClient({ - url: process.env.CLICKHOUSE_URL ?? 'http://localhost:8123', - username: process.env.CLICKHOUSE_USER ?? 'default', - password: process.env.CLICKHOUSE_PASSWORD ?? '', -}) + url: process.env.CLICKHOUSE_URL ?? "http://localhost:8123", + username: process.env.CLICKHOUSE_USER ?? "default", + password: process.env.CLICKHOUSE_PASSWORD ?? "", +}); const resultSet = await client.query({ - query: 'SELECT * FROM system.tables', - format: 'JSONEachRow', -}) + query: "SELECT * FROM system.tables", + format: "JSONEachRow", +}); -const tables = await resultSet.json() -console.log(tables) +const tables = await resultSet.json(); +console.log(tables); -await client.close() +await client.close(); ``` See more examples in the [examples directory](./examples). diff --git a/benchmarks/common/handlers.ts b/benchmarks/common/handlers.ts index b4ca22a1..f117d7a2 100644 --- a/benchmarks/common/handlers.ts +++ b/benchmarks/common/handlers.ts @@ -1,9 +1,9 @@ export function attachExceptionHandlers() { - process.on('uncaughtException', (err) => logAndQuit(err)) - process.on('unhandledRejection', (err) => logAndQuit(err)) + process.on("uncaughtException", (err) => logAndQuit(err)); + process.on("unhandledRejection", (err) => logAndQuit(err)); function logAndQuit(err: unknown) { - console.error(err) - process.exit(1) + console.error(err); + process.exit(1); } } diff --git a/benchmarks/common/index.ts b/benchmarks/common/index.ts index 0848023a..b55af20e 100644 --- a/benchmarks/common/index.ts +++ b/benchmarks/common/index.ts @@ -1 +1 @@ -export * from './handlers' +export * from "./handlers"; diff --git a/benchmarks/formats/json.ts b/benchmarks/formats/json.ts index 872cd865..9e34747a 100644 --- a/benchmarks/formats/json.ts +++ b/benchmarks/formats/json.ts @@ -1,5 +1,5 @@ -import { createClient } from '@clickhouse/client' -import { attachExceptionHandlers } from '../common' +import { createClient } from "@clickhouse/client"; +import { attachExceptionHandlers } from "../common"; /* Large strings table definition: @@ -22,87 +22,87 @@ Large strings table definition: FROM numbers(100000); */ -const WarmupIterations = 3 -const BenchmarkIterations = 10 +const WarmupIterations = 3; +const BenchmarkIterations = 10; -const largeStringsQuery = `SELECT * FROM large_strings ORDER BY id ASC LIMIT 50000` -const cellTowersQuery = `SELECT * FROM cell_towers ORDER BY (radio, mcc, net, created) ASC LIMIT 200000` -const queries = [largeStringsQuery, cellTowersQuery] +const largeStringsQuery = `SELECT * FROM large_strings ORDER BY id ASC LIMIT 50000`; +const cellTowersQuery = `SELECT * FROM cell_towers ORDER BY (radio, mcc, net, created) ASC LIMIT 200000`; +const queries = [largeStringsQuery, cellTowersQuery]; -const formats = ['JSONEachRow'] as const +const formats = ["JSONEachRow"] as const; void (async () => { const client = createClient({ - url: 'http://localhost:8123', + url: "http://localhost:8123", compression: { request: false, response: false, }, - }) + }); - type TotalPerQuery = Record + type TotalPerQuery = Record; const results: Record<(typeof formats)[number], TotalPerQuery> = { JSONEachRow: {}, - } + }; async function benchmarkJSON( format: (typeof formats)[number], query: string, keepResults: boolean, ) { - const start = +new Date() + const start = +new Date(); const rs = await client.query({ query, format, - }) - await rs.json() // discard the result - const elapsed = +new Date() - start + }); + await rs.json(); // discard the result + const elapsed = +new Date() - start; if (keepResults) { - const current = results[format][query] ?? 0 - results[format][query] = current + elapsed + const current = results[format][query] ?? 0; + results[format][query] = current + elapsed; } - logResult(format, query, elapsed) + logResult(format, query, elapsed); } - attachExceptionHandlers() - process.on('SIGINT', closeAndExit) - process.on('SIGINT', closeAndExit) + attachExceptionHandlers(); + process.on("SIGINT", closeAndExit); + process.on("SIGINT", closeAndExit); - console.log('Warmup') + console.log("Warmup"); for (let i = 0; i < WarmupIterations; i++) { - await runQueries(false) + await runQueries(false); } - console.log('Benchmarking') + console.log("Benchmarking"); for (let i = 0; i < BenchmarkIterations; i++) { - await runQueries(true) + await runQueries(true); } - console.log('Results:', results) - console.log('Average results:') + console.log("Results:", results); + console.log("Average results:"); for (const format of formats) { for (const query of queries) { - const avg = Math.floor(results[format][query] / BenchmarkIterations) - logResult(format, query, avg) + const avg = Math.floor(results[format][query] / BenchmarkIterations); + logResult(format, query, avg); } } - await closeAndExit() + await closeAndExit(); function logResult(format: string, query: string, elapsed: number) { - const elapsedStr = elapsed.toString(10) + ' ms' + const elapsedStr = elapsed.toString(10) + " ms"; console.log( `[${elapsedStr.padEnd(10)}][${format.padEnd(18)}][${query.padEnd(80)}]`, - ) + ); } async function runQueries(keepResults: boolean) { for (const query of queries) { for (const format of formats) { - await benchmarkJSON(format, query, keepResults) + await benchmarkJSON(format, query, keepResults); } } } async function closeAndExit() { - await client.close() - process.exit(0) + await client.close(); + process.exit(0); } -})() +})(); diff --git a/benchmarks/leaks/memory_leak_arrays.ts b/benchmarks/leaks/memory_leak_arrays.ts index 113a7a5f..f335f1d7 100644 --- a/benchmarks/leaks/memory_leak_arrays.ts +++ b/benchmarks/leaks/memory_leak_arrays.ts @@ -1,7 +1,7 @@ -import { createClient } from '@clickhouse/client' -import { randomInt } from 'crypto' -import { v4 as uuid_v4 } from 'uuid' -import { attachExceptionHandlers } from '../common' +import { createClient } from "@clickhouse/client"; +import { randomInt } from "crypto"; +import { v4 as uuid_v4 } from "uuid"; +import { attachExceptionHandlers } from "../common"; import { getMemoryUsageInMegabytes, logFinalMemoryUsage, @@ -9,11 +9,11 @@ import { logMemoryUsageOnIteration, randomArray, randomStr, -} from './shared' +} from "./shared"; const program = async () => { - const client = createClient({}) - const tableName = `memory_leak_arrays_${uuid_v4().replace(/-/g, '')}` + const client = createClient({}); + const tableName = `memory_leak_arrays_${uuid_v4().replace(/-/g, "")}`; await client.command({ query: ` @@ -29,71 +29,71 @@ const program = async () => { clickhouse_settings: { wait_end_of_query: 1, }, - }) + }); - console.info(`Created table ${tableName}`) + console.info(`Created table ${tableName}`); - console.log() + console.log(); console.log( `Batch size: ${BATCH_SIZE}, iterations count: ${ITERATIONS}, ` + `logging memory usage every ${LOG_INTERVAL} iterations`, - ) + ); - console.log() - console.log('Initial memory usage:') - const initialMemoryUsage = getMemoryUsageInMegabytes() - logMemoryUsage(initialMemoryUsage) - let prevMemoryUsage = initialMemoryUsage + console.log(); + console.log("Initial memory usage:"); + const initialMemoryUsage = getMemoryUsageInMegabytes(); + logMemoryUsage(initialMemoryUsage); + let prevMemoryUsage = initialMemoryUsage; for (let i = 1; i <= ITERATIONS; i++) { - const values = makeRows() + const values = makeRows(); await client.insert({ table: tableName, - format: 'JSONEachRow', + format: "JSONEachRow", values, - }) + }); if (i % LOG_INTERVAL === 0) { - const currentMemoryUsage = getMemoryUsageInMegabytes() + const currentMemoryUsage = getMemoryUsageInMegabytes(); logMemoryUsageOnIteration({ iteration: i, prevMemoryUsage, currentMemoryUsage, - }) - prevMemoryUsage = currentMemoryUsage + }); + prevMemoryUsage = currentMemoryUsage; } } - logFinalMemoryUsage(initialMemoryUsage) - process.exit(0) -} + logFinalMemoryUsage(initialMemoryUsage); + process.exit(0); +}; function makeRows(): Row[] { - const batch = new Array(BATCH_SIZE) + const batch = new Array(BATCH_SIZE); for (let i = 0; i < BATCH_SIZE; i++) { - const data: Row['data'] = randomArray(randomInt(5, 10), randomStr) - const data2: Row['data2'] = {} + const data: Row["data"] = randomArray(randomInt(5, 10), randomStr); + const data2: Row["data2"] = {}; for (let i = 0; i < randomInt(5, 10); i++) { - data2[randomStr()] = randomArray(randomInt(5, 10), randomStr) + data2[randomStr()] = randomArray(randomInt(5, 10), randomStr); } const row: Row = { id: randomInt(1, 1000), data, data2, - } - batch.push(row) + }; + batch.push(row); } - return batch + return batch; } interface Row { - id: number - data: string[] - data2: Record + id: number; + data: string[]; + data2: Record; } -attachExceptionHandlers() -const ITERATIONS = +(process.env['ITERATIONS'] || 1000) -const BATCH_SIZE = +(process.env['BATCH_SIZE'] || 1000) -const LOG_INTERVAL = +(process.env['LOG_INTERVAL'] || 100) +attachExceptionHandlers(); +const ITERATIONS = +(process.env["ITERATIONS"] || 1000); +const BATCH_SIZE = +(process.env["BATCH_SIZE"] || 1000); +const LOG_INTERVAL = +(process.env["LOG_INTERVAL"] || 100); -void program() +void program(); diff --git a/benchmarks/leaks/memory_leak_brown.ts b/benchmarks/leaks/memory_leak_brown.ts index fb417bd7..487f1c9b 100644 --- a/benchmarks/leaks/memory_leak_brown.ts +++ b/benchmarks/leaks/memory_leak_brown.ts @@ -1,18 +1,18 @@ -import { createClient } from '@clickhouse/client' -import Fs from 'fs' -import Path from 'path' -import { v4 as uuid_v4 } from 'uuid' -import { attachExceptionHandlers } from '../common' +import { createClient } from "@clickhouse/client"; +import Fs from "fs"; +import Path from "path"; +import { v4 as uuid_v4 } from "uuid"; +import { attachExceptionHandlers } from "../common"; import { getMemoryUsageInMegabytes, logFinalMemoryUsage, logMemoryUsage, logMemoryUsageDiff, -} from './shared' +} from "./shared"; const program = async () => { - const client = createClient({}) - const tableName = `memory_leak_test_brown_${uuid_v4().replace(/-/g, '')}` + const client = createClient({}); + const tableName = `memory_leak_test_brown_${uuid_v4().replace(/-/g, "")}`; await client.command({ query: ` @@ -45,44 +45,44 @@ const program = async () => { clickhouse_settings: { wait_end_of_query: 1, }, - }) + }); - console.info(`Created table ${tableName}`) + console.info(`Created table ${tableName}`); - console.log() - console.log('Initial memory usage:') - const initialMemoryUsage = getMemoryUsageInMegabytes() - logMemoryUsage(initialMemoryUsage) - let prevMemoryUsage = initialMemoryUsage + console.log(); + console.log("Initial memory usage:"); + const initialMemoryUsage = getMemoryUsageInMegabytes(); + logMemoryUsage(initialMemoryUsage); + let prevMemoryUsage = initialMemoryUsage; setInterval(() => { - console.log() - console.log('Current memory usage:') - const currentMemoryUsage = getMemoryUsageInMegabytes() - logMemoryUsage(currentMemoryUsage) - console.log('Diff from previous measurement:') + console.log(); + console.log("Current memory usage:"); + const currentMemoryUsage = getMemoryUsageInMegabytes(); + logMemoryUsage(currentMemoryUsage); + console.log("Diff from previous measurement:"); logMemoryUsageDiff({ previous: prevMemoryUsage, current: currentMemoryUsage, - }) - prevMemoryUsage = currentMemoryUsage - }, 1000) + }); + prevMemoryUsage = currentMemoryUsage; + }, 1000); - console.time('insert') + console.time("insert"); const filename = Path.resolve( process.cwd(), - 'benchmarks/leaks/input/mgbench1.csv', - ) + "benchmarks/leaks/input/mgbench1.csv", + ); await client.insert({ table: tableName, values: Fs.createReadStream(filename), - format: 'CSVWithNames', - }) - console.timeEnd('insert') + format: "CSVWithNames", + }); + console.timeEnd("insert"); - logFinalMemoryUsage(initialMemoryUsage) - process.exit(0) -} + logFinalMemoryUsage(initialMemoryUsage); + process.exit(0); +}; -attachExceptionHandlers() -void program() +attachExceptionHandlers(); +void program(); diff --git a/benchmarks/leaks/memory_leak_random_integers.ts b/benchmarks/leaks/memory_leak_random_integers.ts index 06751410..d309822c 100644 --- a/benchmarks/leaks/memory_leak_random_integers.ts +++ b/benchmarks/leaks/memory_leak_random_integers.ts @@ -1,18 +1,18 @@ -import { createClient } from '@clickhouse/client' -import { randomInt } from 'crypto' -import Stream from 'stream' -import { v4 as uuid_v4 } from 'uuid' -import { attachExceptionHandlers } from '../common' +import { createClient } from "@clickhouse/client"; +import { randomInt } from "crypto"; +import Stream from "stream"; +import { v4 as uuid_v4 } from "uuid"; +import { attachExceptionHandlers } from "../common"; import { getMemoryUsageInMegabytes, logFinalMemoryUsage, logMemoryUsage, logMemoryUsageOnIteration, -} from './shared' +} from "./shared"; const program = async () => { - const client = createClient({}) - const tableName = `memory_leak_random_integers_${uuid_v4().replace(/-/g, '')}` + const client = createClient({}); + const tableName = `memory_leak_random_integers_${uuid_v4().replace(/-/g, "")}`; await client.command({ query: ` @@ -24,61 +24,61 @@ const program = async () => { clickhouse_settings: { wait_end_of_query: 1, }, - }) + }); - console.info(`Created table ${tableName}`) + console.info(`Created table ${tableName}`); - console.log() + console.log(); console.log( `Batch size: ${BATCH_SIZE}, iterations count: ${ITERATIONS}, ` + `logging memory usage every ${LOG_INTERVAL} iterations`, - ) + ); - console.log() - console.log('Initial memory usage:') - const initialMemoryUsage = getMemoryUsageInMegabytes() - logMemoryUsage(initialMemoryUsage) - let prevMemoryUsage = initialMemoryUsage + console.log(); + console.log("Initial memory usage:"); + const initialMemoryUsage = getMemoryUsageInMegabytes(); + logMemoryUsage(initialMemoryUsage); + let prevMemoryUsage = initialMemoryUsage; for (let i = 1; i <= ITERATIONS; i++) { - const stream = makeRowsStream() + const stream = makeRowsStream(); await client.insert({ table: tableName, values: stream, - format: 'TabSeparated', - }) + format: "TabSeparated", + }); if (i % LOG_INTERVAL === 0) { - const currentMemoryUsage = getMemoryUsageInMegabytes() + const currentMemoryUsage = getMemoryUsageInMegabytes(); logMemoryUsageOnIteration({ iteration: i, prevMemoryUsage, currentMemoryUsage, - }) - prevMemoryUsage = currentMemoryUsage + }); + prevMemoryUsage = currentMemoryUsage; } } - logFinalMemoryUsage(initialMemoryUsage) - process.exit(0) -} + logFinalMemoryUsage(initialMemoryUsage); + process.exit(0); +}; function makeRowsStream() { - let i = 0 + let i = 0; async function* gen() { while (true) { if (i >= BATCH_SIZE) { - return + return; } - yield `${randomInt(1, 1000)}\t${randomInt(1, 1000)}\n` - i++ + yield `${randomInt(1, 1000)}\t${randomInt(1, 1000)}\n`; + i++; } } - return Stream.Readable.from(gen(), { objectMode: false }) + return Stream.Readable.from(gen(), { objectMode: false }); } -attachExceptionHandlers() -const ITERATIONS = +(process.env['ITERATIONS'] || 10000) -const BATCH_SIZE = +(process.env['BATCH_SIZE'] || 10000) -const LOG_INTERVAL = +(process.env['LOG_INTERVAL'] || 1000) +attachExceptionHandlers(); +const ITERATIONS = +(process.env["ITERATIONS"] || 10000); +const BATCH_SIZE = +(process.env["BATCH_SIZE"] || 10000); +const LOG_INTERVAL = +(process.env["LOG_INTERVAL"] || 1000); -void program() +void program(); diff --git a/benchmarks/leaks/shared.ts b/benchmarks/leaks/shared.ts index f46bf6b8..5d346107 100644 --- a/benchmarks/leaks/shared.ts +++ b/benchmarks/leaks/shared.ts @@ -1,73 +1,81 @@ -import { memoryUsage } from 'process' +import { memoryUsage } from "process"; export interface MemoryUsage { - rss: number - heapTotal: number - heapUsed: number - external: number - arrayBuffers: number + rss: number; + heapTotal: number; + heapUsed: number; + external: number; + arrayBuffers: number; } export function getMemoryUsageInMegabytes(): MemoryUsage { - const mu = memoryUsage() + const mu = memoryUsage(); for (const key in mu) { - const k = key as keyof MemoryUsage - mu[k] = mu[k] / (1024 * 1024) // Bytes -> Megabytes + const k = key as keyof MemoryUsage; + mu[k] = mu[k] / (1024 * 1024); // Bytes -> Megabytes } - return mu + return mu; } export function logMemoryUsage(mu: MemoryUsage) { - console.log('-------------------------------------------------------------') + console.log("-------------------------------------------------------------"); for (const key in mu) { - const k = key as keyof MemoryUsage - console.log(`${k}: ${mu[k].toFixed(2)} MB`) + const k = key as keyof MemoryUsage; + console.log(`${k}: ${mu[k].toFixed(2)} MB`); } - console.log('-------------------------------------------------------------') + console.log("-------------------------------------------------------------"); } export function logMemoryUsageDiff({ previous, current, }: { - previous: MemoryUsage - current: MemoryUsage + previous: MemoryUsage; + current: MemoryUsage; }) { for (const key in previous) { - const k = key as keyof MemoryUsage - const diff = current[k] - previous[k] + const k = key as keyof MemoryUsage; + const diff = current[k] - previous[k]; console.log( `${k}: ${diff > 0 ? `+${diff.toFixed(2)}` : diff.toFixed(2)} MB`, - ) + ); } } export function logFinalMemoryUsage(initialMemoryUsage: MemoryUsage) { - console.log() - console.log('=============================================================') - console.log('Final diff between start and end of the test (before GC call)') - console.log('=============================================================') - const finalMemoryUsageBeforeGC = getMemoryUsageInMegabytes() - logMemoryUsage(finalMemoryUsageBeforeGC) + console.log(); + console.log("============================================================="); + console.log("Final diff between start and end of the test (before GC call)"); + console.log("============================================================="); + const finalMemoryUsageBeforeGC = getMemoryUsageInMegabytes(); + logMemoryUsage(finalMemoryUsageBeforeGC); logMemoryUsageDiff({ previous: initialMemoryUsage, current: finalMemoryUsageBeforeGC, - }) + }); if (global.gc) { - global.gc() - console.log() - console.log('=============================================================') - console.log('Final diff between start and end of the test (after GC call)') - console.log('=============================================================') - const finalMemoryUsageAfterGC = getMemoryUsageInMegabytes() - logMemoryUsage(finalMemoryUsageAfterGC) + global.gc(); + console.log(); + console.log( + "=============================================================", + ); + console.log( + "Final diff between start and end of the test (after GC call)", + ); + console.log( + "=============================================================", + ); + const finalMemoryUsageAfterGC = getMemoryUsageInMegabytes(); + logMemoryUsage(finalMemoryUsageAfterGC); logMemoryUsageDiff({ previous: initialMemoryUsage, current: finalMemoryUsageAfterGC, - }) + }); } else { - console.log('GC is not exposed. Re-run the test with --expose_gc node flag') + console.log( + "GC is not exposed. Re-run the test with --expose_gc node flag", + ); } } @@ -76,26 +84,26 @@ export function logMemoryUsageOnIteration({ iteration, prevMemoryUsage, }: { - iteration: number - prevMemoryUsage: MemoryUsage - currentMemoryUsage: MemoryUsage + iteration: number; + prevMemoryUsage: MemoryUsage; + currentMemoryUsage: MemoryUsage; }) { - console.log() - console.log('=============================================================') - console.log(`${iteration} iterations`) - console.log('=============================================================') - console.log('Current memory usage:') - logMemoryUsage(currentMemoryUsage) + console.log(); + console.log("============================================================="); + console.log(`${iteration} iterations`); + console.log("============================================================="); + console.log("Current memory usage:"); + logMemoryUsage(currentMemoryUsage); logMemoryUsageDiff({ previous: prevMemoryUsage, current: currentMemoryUsage, - }) + }); } export function randomStr() { - return Math.random().toString(36).slice(2) + return Math.random().toString(36).slice(2); } export function randomArray(size: number, generator: () => T): T[] { - return [...Array(size).keys()].map(() => generator()) + return [...Array(size).keys()].map(() => generator()); } diff --git a/docker-compose.yml b/docker-compose.yml index f812a635..78eccb84 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,41 +15,41 @@ # nginx (cluster HTTP entrypoint, round-robin load balancer): 8127 services: clickhouse: - image: 'clickhouse/clickhouse-server:${CLICKHOUSE_VERSION-head}' - container_name: 'clickhouse-js-clickhouse-server' + image: "clickhouse/clickhouse-server:${CLICKHOUSE_VERSION-head}" + container_name: "clickhouse-js-clickhouse-server" environment: CLICKHOUSE_SKIP_USER_SETUP: 1 ports: - - '8123:8123' - - '9000:9000' + - "8123:8123" + - "9000:9000" ulimits: nofile: soft: 262144 hard: 262144 volumes: - - './.docker/clickhouse/single_node/config.xml:/etc/clickhouse-server/config.xml' - - './.docker/clickhouse/users.xml:/etc/clickhouse-server/users.xml' + - "./.docker/clickhouse/single_node/config.xml:/etc/clickhouse-server/config.xml" + - "./.docker/clickhouse/users.xml:/etc/clickhouse-server/users.xml" clickhouse_tls: build: context: ./ dockerfile: .docker/clickhouse/single_node_tls/Dockerfile - container_name: 'clickhouse-js-clickhouse-server-tls' + container_name: "clickhouse-js-clickhouse-server-tls" environment: CLICKHOUSE_SKIP_USER_SETUP: 1 ports: - - '8443:8443' - - '9440:9440' + - "8443:8443" + - "9440:9440" ulimits: nofile: soft: 262144 hard: 262144 volumes: - - './.docker/clickhouse/single_node_tls/config.xml:/etc/clickhouse-server/config.xml' - - './.docker/clickhouse/single_node_tls/users.xml:/etc/clickhouse-server/users.xml' + - "./.docker/clickhouse/single_node_tls/config.xml:/etc/clickhouse-server/config.xml" + - "./.docker/clickhouse/single_node_tls/users.xml:/etc/clickhouse-server/users.xml" clickhouse1: - image: 'clickhouse/clickhouse-server:${CLICKHOUSE_VERSION-head}' + image: "clickhouse/clickhouse-server:${CLICKHOUSE_VERSION-head}" ulimits: nofile: soft: 262144 @@ -59,16 +59,16 @@ services: environment: CLICKHOUSE_SKIP_USER_SETUP: 1 ports: - - '8124:8123' - - '9100:9000' - - '9181:9181' + - "8124:8123" + - "9100:9000" + - "9181:9181" volumes: - - './.docker/clickhouse/cluster/server1_config.xml:/etc/clickhouse-server/config.xml' - - './.docker/clickhouse/cluster/server1_macros.xml:/etc/clickhouse-server/config.d/macros.xml' - - './.docker/clickhouse/users.xml:/etc/clickhouse-server/users.xml' + - "./.docker/clickhouse/cluster/server1_config.xml:/etc/clickhouse-server/config.xml" + - "./.docker/clickhouse/cluster/server1_macros.xml:/etc/clickhouse-server/config.d/macros.xml" + - "./.docker/clickhouse/users.xml:/etc/clickhouse-server/users.xml" clickhouse2: - image: 'clickhouse/clickhouse-server:${CLICKHOUSE_VERSION-head}' + image: "clickhouse/clickhouse-server:${CLICKHOUSE_VERSION-head}" ulimits: nofile: soft: 262144 @@ -78,20 +78,20 @@ services: environment: CLICKHOUSE_SKIP_USER_SETUP: 1 ports: - - '8125:8123' - - '9101:9000' - - '9182:9181' + - "8125:8123" + - "9101:9000" + - "9182:9181" volumes: - - './.docker/clickhouse/cluster/server2_config.xml:/etc/clickhouse-server/config.xml' - - './.docker/clickhouse/cluster/server2_macros.xml:/etc/clickhouse-server/config.d/macros.xml' - - './.docker/clickhouse/users.xml:/etc/clickhouse-server/users.xml' + - "./.docker/clickhouse/cluster/server2_config.xml:/etc/clickhouse-server/config.xml" + - "./.docker/clickhouse/cluster/server2_macros.xml:/etc/clickhouse-server/config.d/macros.xml" + - "./.docker/clickhouse/users.xml:/etc/clickhouse-server/users.xml" # Using Nginx as a cluster entrypoint and a round-robin load balancer for HTTP requests nginx: - image: 'nginx:1.23.1-alpine' + image: "nginx:1.23.1-alpine" hostname: nginx ports: - - '8127:8123' + - "8127:8123" volumes: - - './.docker/nginx/local.conf:/etc/nginx/conf.d/local.conf' + - "./.docker/nginx/local.conf:/etc/nginx/conf.d/local.conf" container_name: clickhouse-js-nginx diff --git a/docs/howto/keep_alive_timeout.md b/docs/howto/keep_alive_timeout.md index a0fe7abc..0e66e020 100644 --- a/docs/howto/keep_alive_timeout.md +++ b/docs/howto/keep_alive_timeout.md @@ -15,7 +15,7 @@ This happens when `keep_alive.idle_socket_ttl` (client-side) is greater than the ```ts const client = createClient({ log: { level: ClickHouseLogLevel.TRACE }, -}) +}); ``` Look for two log entries: @@ -54,7 +54,7 @@ const client = createClient({ keep_alive: { idle_socket_ttl: 2500, // ms; server timeout is 3000 ms → safe margin }, -}) +}); ``` A margin of 500–1000 ms is recommended to account for clock skew and event-loop delays. @@ -75,7 +75,7 @@ const client = createClient({ keep_alive: { eagerly_destroy_stale_sockets: true, }, -}) +}); ``` When enabled and the client detects that an idle socket has exceeded the server timeout, it will be destroyed immediately. This can help prevent `ECONNRESET` errors on the next request that tries to reuse that socket. You can check the logs for messages about destroying idle sockets: diff --git a/docs/howto/long_running_queries.md b/docs/howto/long_running_queries.md index 7351dbfd..47bf1994 100644 --- a/docs/howto/long_running_queries.md +++ b/docs/howto/long_running_queries.md @@ -48,9 +48,9 @@ The default value for `http_headers_progress_interval_ms` is defined by how ofte > max_response_headers_size: 1024 * 1024, // 1 MiB > clickhouse_settings: { > send_progress_in_http_headers: 1, -> http_headers_progress_interval_ms: '110000', +> http_headers_progress_interval_ms: "110000", > }, -> }) +> }); > ``` > > The Web client uses `fetch` and is not subject to this limit. @@ -62,7 +62,7 @@ The default value for `http_headers_progress_interval_ms` is defined by how ofte **Step 3.** Configure the client: ```ts -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; const client = createClient({ // Allow up to 400s for the query to complete (adjust to your estimate). @@ -72,9 +72,9 @@ const client = createClient({ send_progress_in_http_headers: 1, // Send headers every 110s — just under the assumed 120s LB idle timeout. // Must be a string because UInt64 can exceed Number.MAX_SAFE_INTEGER. - http_headers_progress_interval_ms: '110000', + http_headers_progress_interval_ms: "110000", }, -}) +}); ``` **Step 4.** Execute the query normally: @@ -82,7 +82,7 @@ const client = createClient({ ```ts await client.command({ query: `INSERT INTO my_table SELECT * FROM source_table`, -}) +}); ``` The client will now receive periodic header frames from ClickHouse, keeping the LB idle timer reset. @@ -100,14 +100,14 @@ This reduces the window of exposure to network errors from "the entire query dur **Step 1.** Generate a `query_id` on the client side so you can track the query later: ```ts -import * as crypto from 'crypto' -const queryId = crypto.randomUUID() +import * as crypto from "crypto"; +const queryId = crypto.randomUUID(); ``` **Step 2.** Start the long-running command but **do not await it yet**. Attach an `AbortController` so you can drop the HTTP connection without cancelling the server-side query: ```ts -const abortController = new AbortController() +const abortController = new AbortController(); const commandPromise = client .command({ @@ -116,12 +116,12 @@ const commandPromise = client abort_signal: abortController.signal, }) .catch((err) => { - if (err instanceof Error && err.message.includes('abort')) { + if (err instanceof Error && err.message.includes("abort")) { // Expected — we aborted the request intentionally. } else { - throw err + throw err; } - }) + }); ``` **Step 3.** Poll `system.query_log` until the query appears (meaning the server has registered it): @@ -134,18 +134,18 @@ async function checkQueryExists(client, queryId) { FROM system.query_log WHERE query_id = '${queryId}' `, - format: 'JSONEachRow', - }) - const [row] = await rs.json() - return row?.exists !== 0 + format: "JSONEachRow", + }); + const [row] = await rs.json(); + return row?.exists !== 0; } ``` **Step 4.** Once the query is confirmed to exist on the server, abort the HTTP request: ```ts -abortController.abort() -await commandPromise // resolves immediately after abort +abortController.abort(); +await commandPromise; // resolves immediately after abort ``` If the query never appears after a reasonable number of polls, treat it as a failure and handle accordingly. @@ -161,10 +161,10 @@ async function checkCompletedQuery(client, queryId) { WHERE query_id = '${queryId}' AND type != 'QueryStart' LIMIT 1 `, - format: 'JSONEachRow', - }) - const [row] = await rs.json() - return row?.type === 'QueryFinish' + format: "JSONEachRow", + }); + const [row] = await rs.json(); + return row?.type === "QueryFinish"; } ``` diff --git a/docs/socket_hang_up_econnreset.md b/docs/socket_hang_up_econnreset.md index b952af3d..5c061308 100644 --- a/docs/socket_hang_up_econnreset.md +++ b/docs/socket_hang_up_econnreset.md @@ -7,7 +7,7 @@ If you're experiencing `socket hang up` and / or `ECONNRESET` errors even when u ```ts const client = createClient({ log: { level: ClickHouseLogLevel.WARN }, - }) + }); ``` - Make sure that the desired configuration is applied to the correct client instance. If you have multiple client instances in your application, double-check that the one you're using for queries has the correct `keep_alive.idle_socket_ttl` value. @@ -25,9 +25,9 @@ If you're experiencing `socket hang up` and / or `ECONNRESET` errors even when u * In this case, we assume that the LB has idle connection timeout of 120s, so we set 110s as a "safe" value. */ clickhouse_settings: { send_progress_in_http_headers: 1, - http_headers_progress_interval_ms: '110000', // UInt64, should be passed as a string + http_headers_progress_interval_ms: "110000", // UInt64, should be passed as a string }, - }) + }); ``` Keep in mind, however, that the total size of the received headers has a 16KB limit in recent Node.js versions; after a certain amount of progress headers received, which was around 70-80 in our tests, an exception will be thrown. @@ -41,7 +41,7 @@ If you're experiencing `socket hang up` and / or `ECONNRESET` errors even when u keep_alive: { enabled: false, }, - }) + }); ``` - Rule out potential issues with the rest of the network stack including Node.js itself by running a simple command-line test with the same ClickHouse instance and the same network path (i.e. from the same machine or network segment, e.g. a Kubernetes pod), for example, using `curl`: @@ -55,13 +55,13 @@ If you're experiencing `socket hang up` and / or `ECONNRESET` errors even when u - To test the connection with plain Node.js functionality, you can try to create a simple HTTP request to the ClickHouse server using the built-in `fetch` API: ```ts - const response = await fetch('?query=SELECT+1', { - method: 'POST', + const response = await fetch("?query=SELECT+1", { + method: "POST", headers: { Authorization: - 'Basic ' + Buffer.from(':').toString('base64'), + "Basic " + Buffer.from(":").toString("base64"), }, - }) + }); ``` - In some cases the application code or the framework adapters can add a preemptive `ping()` before the actual query execution, which can lead to a situation where the `ping()` request is successful, but the subsequent query request fails with a "socket hang up" error due to the same underlying issue with idle connections. If you see that pattern in the logs, try to check if there is an option to disable preemptive pings in your framework or application code. This should also help with reducing the probability of getting rate limited by any of the intermediate network components. diff --git a/eslint.config.base.mjs b/eslint.config.base.mjs index e49d430b..41bc9360 100644 --- a/eslint.config.base.mjs +++ b/eslint.config.base.mjs @@ -1,9 +1,9 @@ -import pluginPrettier from 'eslint-plugin-prettier' -import pluginExpectType from 'eslint-plugin-expect-type/configs/recommended' +import pluginPrettier from "eslint-plugin-prettier"; +import pluginExpectType from "eslint-plugin-expect-type/configs/recommended"; export function typescriptEslintConfig(root) { return { - files: ['**/*.ts'], + files: ["**/*.ts"], languageOptions: { parserOptions: { projectService: true, @@ -12,32 +12,32 @@ export function typescriptEslintConfig(root) { }, plugins: { prettier: pluginPrettier, - 'expect-type': pluginExpectType, + "expect-type": pluginExpectType, }, rules: { - 'prettier/prettier': 'error', - '@typescript-eslint/no-floating-promises': 'error', - eqeqeq: 'error', - 'no-console': 'error', + "prettier/prettier": "error", + "@typescript-eslint/no-floating-promises": "error", + eqeqeq: "error", + "no-console": "error", // Keep some rules relaxed until addressed in dedicated PRs - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/consistent-type-imports': 'warn', - '@typescript-eslint/array-type': 'off', - '@typescript-eslint/ban-ts-comment': 'off', - '@typescript-eslint/no-non-null-assertion': 'off', + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/consistent-type-imports": "warn", + "@typescript-eslint/array-type": "off", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-non-null-assertion": "off", }, - } // TypeScript-ESLint recommended rules with type checking + }; // TypeScript-ESLint recommended rules with type checking } export function testFilesOverrides() { // Test files overrides return { - files: ['./**/__tests__/**/*.ts'], + files: ["./**/__tests__/**/*.ts"], rules: { - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-non-null-assertion': 'off', - 'no-constant-condition': 'off', - 'no-console': 'off', + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "no-constant-condition": "off", + "no-console": "off", }, - } + }; } diff --git a/examples/node/coding/array_json_each_row.ts b/examples/node/coding/array_json_each_row.ts index 32de5fdb..deb54251 100644 --- a/examples/node/coding/array_json_each_row.ts +++ b/examples/node/coding/array_json_each_row.ts @@ -1,13 +1,13 @@ -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; // Inserting and selecting an array of JS objects using the `JSONEachRow` format. // This is the most common shape for app code: pass `values` as `Array>` // where each object's keys match the table's column names. -const tableName = 'array_json_each_row' -const client = createClient() +const tableName = "array_json_each_row"; +const client = createClient(); await client.command({ query: `DROP TABLE IF EXISTS ${tableName}`, -}) +}); await client.command({ query: ` CREATE TABLE ${tableName} @@ -15,19 +15,19 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); await client.insert({ table: tableName, // structure should match the desired format, JSONEachRow in this example values: [ - { id: 42, name: 'foo' }, - { id: 42, name: 'bar' }, + { id: 42, name: "foo" }, + { id: 42, name: "bar" }, ], - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); const rows = await client.query({ query: `SELECT * FROM ${tableName}`, - format: 'JSONEachRow', -}) -console.info(await rows.json()) -await client.close() + format: "JSONEachRow", +}); +console.info(await rows.json()); +await client.close(); diff --git a/examples/node/coding/async_insert.ts b/examples/node/coding/async_insert.ts index e6e25992..10286f98 100644 --- a/examples/node/coding/async_insert.ts +++ b/examples/node/coding/async_insert.ts @@ -1,11 +1,11 @@ -import { createClient, ClickHouseError } from '@clickhouse/client' +import { createClient, ClickHouseError } from "@clickhouse/client"; // This example demonstrates how to use asynchronous inserts, avoiding client side batching of the incoming data. // Suitable for ClickHouse Cloud, too. // See https://clickhouse.com/docs/en/optimize/asynchronous-inserts const client = createClient({ - url: process.env['CLICKHOUSE_URL'], // defaults to 'http://localhost:8123' - password: process.env['CLICKHOUSE_PASSWORD'], // defaults to an empty string + url: process.env["CLICKHOUSE_URL"], // defaults to 'http://localhost:8123' + password: process.env["CLICKHOUSE_PASSWORD"], // defaults to an empty string max_open_connections: 10, clickhouse_settings: { // https://clickhouse.com/docs/en/operations/settings/settings#async_insert @@ -13,14 +13,14 @@ const client = createClient({ // https://clickhouse.com/docs/en/operations/settings/settings#wait_for_async_insert wait_for_async_insert: 1, // https://clickhouse.com/docs/en/operations/settings/settings#async_insert_max_data_size - async_insert_max_data_size: '1000000', + async_insert_max_data_size: "1000000", // https://clickhouse.com/docs/en/operations/settings/settings#async_insert_busy_timeout_ms async_insert_busy_timeout_ms: 1000, }, -}) +}); // Create the table if necessary -const table = 'async_insert_example' +const table = "async_insert_example"; await client.command({ query: ` CREATE OR REPLACE TABLE ${table} @@ -32,9 +32,9 @@ await client.command({ clickhouse_settings: { wait_end_of_query: 1, }, -}) +}); -const start = new Date() +const start = new Date(); // Assume that we can receive multiple insert requests at the same time // (e.g. from parallel HTTP requests in your app or similar). const promises = [...new Array(10)].map(async () => { @@ -48,38 +48,38 @@ const promises = [...new Array(10)].map(async () => { const values = [...new Array(1000).keys()].map(() => ({ id: Math.floor(Math.random() * 100_000) + 1, data: Math.random().toString(36).slice(2), - })) + })); await client .insert({ table, values, - format: 'JSONEachRow', // or other, depends on your data + format: "JSONEachRow", // or other, depends on your data }) .catch((err) => { // Depending on the error, it is possible that the request itself was not processed on the server. if (err instanceof ClickHouseError) { // You could decide what to do with a failed insert based on the error code. // An overview of possible error codes is available in the `system.errors` ClickHouse table. - console.error(`ClickHouse error: ${err.code}. Insert failed:`, err) - return + console.error(`ClickHouse error: ${err.code}. Insert failed:`, err); + return; } // You could implement a proper retry mechanism depending on your application needs; // for the sake of this example, we just log an error. - console.error('Insert failed:', err) - }) -}) -await Promise.all(promises) + console.error("Insert failed:", err); + }); +}); +await Promise.all(promises); // In this example, it should take `async_insert_busy_timeout_ms` milliseconds or a bit more, // as the server will wait for more insert operations, // cause due to small amount of data its internal buffer was not exceeded. -console.log('Inserts took', new Date().getTime() - start.getTime(), 'ms') +console.log("Inserts took", new Date().getTime() - start.getTime(), "ms"); const resultSet = await client.query({ query: `SELECT count(*) AS count FROM ${table}`, - format: 'JSONEachRow', -}) -const [{ count }] = await resultSet.json<{ count: string }>() + format: "JSONEachRow", +}); +const [{ count }] = await resultSet.json<{ count: string }>(); // It is expected to have 10k records in the table. -console.info('Select count result:', count) -await client.close() +console.info("Select count result:", count); +await client.close(); diff --git a/examples/node/coding/clickhouse_settings.ts b/examples/node/coding/clickhouse_settings.ts index c5703b9e..f7a86fe8 100644 --- a/examples/node/coding/clickhouse_settings.ts +++ b/examples/node/coding/clickhouse_settings.ts @@ -1,16 +1,16 @@ // Applying ClickHouse settings on the client or the operation level. // See also: {@link ClickHouseSettings} typings. -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; const client = createClient({ // Settings applied in the client settings will be added to every request. clickhouse_settings: { - date_time_input_format: 'best_effort', + date_time_input_format: "best_effort", }, -}) +}); const rows = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 2', - format: 'JSONEachRow', + query: "SELECT number FROM system.numbers LIMIT 2", + format: "JSONEachRow", /** * Apply these settings only for this query; * overrides the defaults set in the client instance settings. @@ -22,6 +22,6 @@ const rows = await client.query({ // default is 0 since 25.8 output_format_json_quote_64bit_integers: 1, }, -}) -console.info(await rows.json()) -await client.close() +}); +console.info(await rows.json()); +await client.close(); diff --git a/examples/node/coding/custom_json_handling.ts b/examples/node/coding/custom_json_handling.ts index d8440c64..a372ef63 100644 --- a/examples/node/coding/custom_json_handling.ts +++ b/examples/node/coding/custom_json_handling.ts @@ -4,43 +4,43 @@ // a custom serializer before passing it to the JSON.stringify function. // // This example also shows how you can serialize Date objects in a custom way. -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; const valueSerializer = (value: unknown): unknown => { if (value instanceof Date) { // if you would have put this in the `replacer` parameter of JSON.stringify, (e.x: JSON.stringify(obj, replacerFn)) // it would have been an ISO string, but since we are serializing before `stringify`ing, // it will convert it before the `.toJSON()` method has been called - return value.getTime() + return value.getTime(); } - if (typeof value === 'bigint') { - return value.toString() + if (typeof value === "bigint") { + return value.toString(); } if (Array.isArray(value)) { - return value.map(valueSerializer) + return value.map(valueSerializer); } - if (typeof value === 'object' && value !== null) { + if (typeof value === "object" && value !== null) { return Object.fromEntries( Object.entries(value).map(([k, v]) => [k, valueSerializer(v)]), - ) + ); } - return value -} + return value; +}; -const tableName = 'inserts_custom_json_handling' +const tableName = "inserts_custom_json_handling"; const client = createClient({ json: { parse: JSON.parse, stringify: (obj: unknown) => JSON.stringify(valueSerializer(obj)), }, -}) +}); await client.command({ query: `DROP TABLE IF EXISTS ${tableName}`, -}) +}); await client.command({ query: ` CREATE TABLE ${tableName} @@ -48,7 +48,7 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); await client.insert({ table: tableName, values: [ @@ -57,11 +57,11 @@ await client.insert({ dt: new Date(), }, ], - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); const rows = await client.query({ query: `SELECT * FROM ${tableName}`, - format: 'JSONEachRow', -}) -console.info(await rows.json()) -await client.close() + format: "JSONEachRow", +}); +console.info(await rows.json()); +await client.close(); diff --git a/examples/node/coding/default_format_setting.ts b/examples/node/coding/default_format_setting.ts index c402a1c4..c243ce6f 100644 --- a/examples/node/coding/default_format_setting.ts +++ b/examples/node/coding/default_format_setting.ts @@ -1,10 +1,10 @@ -import { createClient, ResultSet } from '@clickhouse/client' +import { createClient, ResultSet } from "@clickhouse/client"; // Using the `default_format` ClickHouse setting with `client.exec` so that the query // does not need an explicit `FORMAT` clause and the response can be wrapped in a // `ResultSet` for typed parsing. Useful when issuing arbitrary SQL via `exec`. -const client = createClient() -const format = 'JSONCompactEachRowWithNamesAndTypes' +const client = createClient(); +const format = "JSONCompactEachRowWithNamesAndTypes"; const { stream, query_id } = await client.exec({ // this query fails without `default_format` setting // as it does not have the FORMAT clause @@ -12,7 +12,7 @@ const { stream, query_id } = await client.exec({ clickhouse_settings: { default_format: format, }, -}) -const rs = new ResultSet(stream, format, query_id) -console.log(await rs.json()) -await client.close() +}); +const rs = new ResultSet(stream, format, query_id); +console.log(await rs.json()); +await client.close(); diff --git a/examples/node/coding/dynamic_variant_json.ts b/examples/node/coding/dynamic_variant_json.ts index e79b4ad2..ac7b2cbc 100644 --- a/examples/node/coding/dynamic_variant_json.ts +++ b/examples/node/coding/dynamic_variant_json.ts @@ -1,6 +1,6 @@ -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; -const tableName = `chjs_dynamic_variant_json` +const tableName = `chjs_dynamic_variant_json`; const client = createClient({ // Since 25.3, all these types are no longer experimental and are enabled by default // However, if you are using an older version of ClickHouse, you might need these settings @@ -16,7 +16,7 @@ const client = createClient({ // https://clickhouse.com/docs/sql-reference/data-types/newjson allow_experimental_json_type: 1, }, -}) +}); await client.command({ query: ` CREATE OR REPLACE TABLE ${tableName} @@ -29,20 +29,20 @@ await client.command({ ENGINE MergeTree ORDER BY id `, -}) +}); // Sample representation in JSONEachRow format const values = [ { id: 1, var: 42, - dynamic: 'foo', + dynamic: "foo", json: { - foo: 'x', + foo: "x", }, }, { id: 2, - var: 'str', + var: "str", // A number will default to Int64; it could be also represented as a string in JSON* family formats // using `output_format_json_quote_64bit_integers` setting (default is 0 since CH 25.8). // See https://clickhouse.com/docs/en/operations/settings/formats#output_format_json_quote_64bit_integers @@ -51,12 +51,12 @@ const values = [ bar: 10, }, }, -] +]; await client.insert({ table: tableName, - format: 'JSONEachRow', + format: "JSONEachRow", values, -}) +}); const rs = await client.query({ query: ` SELECT *, @@ -66,7 +66,7 @@ const rs = await client.query({ dynamicType(json.bar) FROM ${tableName} `, - format: 'JSONEachRow', -}) -console.log(await rs.json()) -await client.close() + format: "JSONEachRow", +}); +console.log(await rs.json()); +await client.close(); diff --git a/examples/node/coding/insert_data_formats_overview.ts b/examples/node/coding/insert_data_formats_overview.ts index f3550de2..2aca3f63 100644 --- a/examples/node/coding/insert_data_formats_overview.ts +++ b/examples/node/coding/insert_data_formats_overview.ts @@ -25,100 +25,100 @@ import { type DataFormat, type InputJSON, type InputJSONObjectEachRow, -} from '@clickhouse/client' +} from "@clickhouse/client"; -const tableName = 'insert_data_formats_overview' -const client = createClient() -await prepareTestTable() +const tableName = "insert_data_formats_overview"; +const client = createClient(); +await prepareTestTable(); // These JSON formats can be streamed as well instead of sending the entire data set at once; // See this example that streams a file: node/insert_file_stream_ndjson.ts -console.log('#### Streamable JSON formats:\n') +console.log("#### Streamable JSON formats:\n"); // All of these formats accept various arrays of objects, depending on the format. -await insertJSON('JSONEachRow', [ - { id: 1, name: 'foo', sku: [1, 2, 3] }, - { id: 2, name: 'bar', sku: [4, 5, 6] }, -]) -await insertJSON('JSONStringsEachRow', [ - { id: '3', name: 'foo', sku: '[1,2,3]' }, - { id: '4', name: 'bar', sku: '[4,5,6]' }, -]) -await insertJSON('JSONCompactEachRow', [ - [5, 'foo', [1, 2, 3]], - [6, 'bar', [4, 5, 6]], -]) -await insertJSON('JSONCompactStringsEachRow', [ - ['7', 'foo', '[1,2,3]'], - ['8', 'bar', '[4,5,6]'], -]) -await insertJSON('JSONCompactEachRowWithNames', [ - ['id', 'name', 'sku'], - [9, 'foo', [1, 2, 3]], - [10, 'bar', [4, 5, 6]], -]) -await insertJSON('JSONCompactEachRowWithNamesAndTypes', [ - ['id', 'name', 'sku'], - ['UInt32', 'String', 'Array(UInt32)'], - [11, 'foo', [1, 2, 3]], - [12, 'bar', [4, 5, 6]], -]) -await insertJSON('JSONCompactStringsEachRowWithNames', [ - ['id', 'name', 'sku'], - ['13', 'foo', '[1,2,3]'], - ['14', 'bar', '[4,5,6]'], -]) -await insertJSON('JSONCompactStringsEachRowWithNamesAndTypes', [ - ['id', 'name', 'sku'], - ['UInt32', 'String', 'Array(UInt32)'], - ['15', 'foo', '[1,2,3]'], - ['16', 'bar', '[4,5,6]'], -]) +await insertJSON("JSONEachRow", [ + { id: 1, name: "foo", sku: [1, 2, 3] }, + { id: 2, name: "bar", sku: [4, 5, 6] }, +]); +await insertJSON("JSONStringsEachRow", [ + { id: "3", name: "foo", sku: "[1,2,3]" }, + { id: "4", name: "bar", sku: "[4,5,6]" }, +]); +await insertJSON("JSONCompactEachRow", [ + [5, "foo", [1, 2, 3]], + [6, "bar", [4, 5, 6]], +]); +await insertJSON("JSONCompactStringsEachRow", [ + ["7", "foo", "[1,2,3]"], + ["8", "bar", "[4,5,6]"], +]); +await insertJSON("JSONCompactEachRowWithNames", [ + ["id", "name", "sku"], + [9, "foo", [1, 2, 3]], + [10, "bar", [4, 5, 6]], +]); +await insertJSON("JSONCompactEachRowWithNamesAndTypes", [ + ["id", "name", "sku"], + ["UInt32", "String", "Array(UInt32)"], + [11, "foo", [1, 2, 3]], + [12, "bar", [4, 5, 6]], +]); +await insertJSON("JSONCompactStringsEachRowWithNames", [ + ["id", "name", "sku"], + ["13", "foo", "[1,2,3]"], + ["14", "bar", "[4,5,6]"], +]); +await insertJSON("JSONCompactStringsEachRowWithNamesAndTypes", [ + ["id", "name", "sku"], + ["UInt32", "String", "Array(UInt32)"], + ["15", "foo", "[1,2,3]"], + ["16", "bar", "[4,5,6]"], +]); // These are single document JSON formats, which are not streamable -console.log('\n#### Single document JSON formats:\n') +console.log("\n#### Single document JSON formats:\n"); // JSON, JSONCompact, JSONColumnsWithMetadata accept the InputJSON shape. // For example: https://clickhouse.com/docs/en/interfaces/formats#json -const meta: InputJSON['meta'] = [ - { name: 'id', type: 'UInt32' }, - { name: 'name', type: 'String' }, - { name: 'sku', type: 'Array(UInt32)' }, -] -await insertJSON('JSON', { +const meta: InputJSON["meta"] = [ + { name: "id", type: "UInt32" }, + { name: "name", type: "String" }, + { name: "sku", type: "Array(UInt32)" }, +]; +await insertJSON("JSON", { meta: [], // not required for JSON format input data: [ - { id: 17, name: 'foo', sku: [1, 2, 3] }, - { id: 18, name: 'bar', sku: [4, 5, 6] }, + { id: 17, name: "foo", sku: [1, 2, 3] }, + { id: 18, name: "bar", sku: [4, 5, 6] }, ], -}) -await insertJSON('JSONCompact', { +}); +await insertJSON("JSONCompact", { meta, data: [ - [19, 'foo', [1, 2, 3]], - [20, 'bar', [4, 5, 6]], + [19, "foo", [1, 2, 3]], + [20, "bar", [4, 5, 6]], ], -}) -await insertJSON('JSONColumnsWithMetadata', { +}); +await insertJSON("JSONColumnsWithMetadata", { meta, data: { id: [21, 22], - name: ['foo', 'bar'], + name: ["foo", "bar"], sku: [ [1, 2, 3], [4, 5, 6], ], }, -}) +}); // JSONObjectEachRow accepts Record (alias: InputJSONObjectEachRow). // See https://clickhouse.com/docs/en/interfaces/formats#jsonobjecteachrow -await insertJSON('JSONObjectEachRow', { - row_1: { id: 23, name: 'foo', sku: [1, 2, 3] }, - row_2: { id: 24, name: 'bar', sku: [4, 5, 6] }, -}) +await insertJSON("JSONObjectEachRow", { + row_1: { id: 23, name: "foo", sku: [1, 2, 3] }, + row_2: { id: 24, name: "bar", sku: [4, 5, 6] }, +}); // Print the inserted data - see that the IDs are matching. -await printInsertedData() -await client.close() +await printInsertedData(); +await client.close(); // Inserting data in different JSON formats async function insertJSON( @@ -130,11 +130,11 @@ async function insertJSON( table: tableName, format: format, values, - }) - console.log(`Successfully inserted data with format ${format}`) + }); + console.log(`Successfully inserted data with format ${format}`); } catch (err) { - console.error(`Failed to insert data with format ${format}, cause:`, err) - throw err + console.error(`Failed to insert data with format ${format}, cause:`, err); + throw err; } } @@ -146,15 +146,15 @@ async function prepareTestTable() { ENGINE MergeTree() ORDER BY (id) `, - }) + }); } async function printInsertedData() { const resultSet = await client.query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, - format: 'JSONEachRow', - }) - const data = await resultSet.json() - console.log('Inserted data:') - console.dir(data, { depth: null }) + format: "JSONEachRow", + }); + const data = await resultSet.json(); + console.log("Inserted data:"); + console.dir(data, { depth: null }); } diff --git a/examples/node/coding/insert_decimals.ts b/examples/node/coding/insert_decimals.ts index 22fc5a50..9e7c69c2 100644 --- a/examples/node/coding/insert_decimals.ts +++ b/examples/node/coding/insert_decimals.ts @@ -1,11 +1,11 @@ -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; // Inserting and reading back values for all four `Decimal(P, S)` widths (32/64/128/256-bit). // Decimal values are passed as strings to avoid floating-point precision loss, and read back // using `toString(decN)` for the same reason. Reach for this when storing money or other // fixed-precision quantities. -const client = createClient() -const tableName = 'insert_decimals_example' +const client = createClient(); +const tableName = "insert_decimals_example"; await client.command({ query: ` CREATE OR REPLACE TABLE ${tableName} @@ -19,27 +19,27 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); const row1 = { id: 1, - dec32: '1234567.89', - dec64: '123456789123456.789', - dec128: '1234567891234567891234567891.1234567891', + dec32: "1234567.89", + dec64: "123456789123456.789", + dec128: "1234567891234567891234567891.1234567891", dec256: - '12345678912345678912345678911234567891234567891234567891.12345678911234567891', -} + "12345678912345678912345678911234567891234567891234567891.12345678911234567891", +}; const row2 = { id: 2, - dec32: '12.01', - dec64: '5000000.405', - dec128: '1.0000000004', - dec256: '42.00000000000000013007', -} + dec32: "12.01", + dec64: "5000000.405", + dec128: "1.0000000004", + dec256: "42.00000000000000013007", +}; await client.insert({ table: tableName, values: [row1, row2], - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); const resultSet = await client.query({ query: ` SELECT toString(dec32) AS decimal32, @@ -48,7 +48,7 @@ const resultSet = await client.query({ toString(dec256) AS decimal256 FROM ${tableName} `, - format: 'JSONEachRow', -}) -console.log('Result:', await resultSet.json()) -await client.close() + format: "JSONEachRow", +}); +console.log("Result:", await resultSet.json()); +await client.close(); diff --git a/examples/node/coding/insert_ephemeral_columns.ts b/examples/node/coding/insert_ephemeral_columns.ts index 7cbc794a..2bc7b6c5 100644 --- a/examples/node/coding/insert_ephemeral_columns.ts +++ b/examples/node/coding/insert_ephemeral_columns.ts @@ -1,8 +1,8 @@ -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; // Ephemeral columns documentation: https://clickhouse.com/docs/en/sql-reference/statements/create/table#ephemeral -const tableName = 'insert_ephemeral_columns' -const client = createClient() +const tableName = "insert_ephemeral_columns"; +const client = createClient(); await client.command({ query: ` @@ -15,31 +15,31 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); await client.insert({ table: tableName, values: [ { - id: '42', - message_default: 'foo', + id: "42", + message_default: "foo", }, { - id: '144', - message_default: 'bar', + id: "144", + message_default: "bar", }, ], - format: 'JSONEachRow', + format: "JSONEachRow", // The name of the ephemeral column has to be specified here // to trigger the default values logic for the rest of the columns - columns: ['id', 'message_default'], -}) + columns: ["id", "message_default"], +}); const rows = await client.query({ query: `SELECT * FROM ${tableName}`, - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); -console.info(await rows.json()) -await client.close() +console.info(await rows.json()); +await client.close(); diff --git a/examples/node/coding/insert_exclude_columns.ts b/examples/node/coding/insert_exclude_columns.ts index 5bc2b52f..889e6f59 100644 --- a/examples/node/coding/insert_exclude_columns.ts +++ b/examples/node/coding/insert_exclude_columns.ts @@ -1,9 +1,9 @@ // Excluding certain columns from the INSERT statement. // For the inverse (specifying the exact columns to insert into), see `insert_specific_columns.ts`. -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; -const tableName = 'insert_exclude_columns' -const client = createClient() +const tableName = "insert_exclude_columns"; +const client = createClient(); await client.command({ query: ` @@ -12,32 +12,32 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); await client.insert({ table: tableName, - values: [{ message: 'bar' }], - format: 'JSONEachRow', + values: [{ message: "bar" }], + format: "JSONEachRow", // `id` column value for this row will be zero columns: { - except: ['id'], + except: ["id"], }, -}) +}); await client.insert({ table: tableName, values: [{ id: 144 }], - format: 'JSONEachRow', + format: "JSONEachRow", // `message` column value for this row will be an empty string columns: { - except: ['message'], + except: ["message"], }, -}) +}); const rows = await client.query({ query: `SELECT * FROM ${tableName} ORDER BY id, message DESC`, - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); -console.info(await rows.json()) -await client.close() +console.info(await rows.json()); +await client.close(); diff --git a/examples/node/coding/insert_from_select.ts b/examples/node/coding/insert_from_select.ts index 742c9241..dc19878c 100644 --- a/examples/node/coding/insert_from_select.ts +++ b/examples/node/coding/insert_from_select.ts @@ -2,10 +2,10 @@ // Demonstrates that `client.command` can run server-side data movement queries // (no client-side rows are sent), and that aggregate states are read back via // `finalizeAggregation`. Inspired by https://github.com/ClickHouse/clickhouse-js/issues/166 -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; -const tableName = 'insert_from_select' -const client = createClient() +const tableName = "insert_from_select"; +const client = createClient(); await client.command({ query: ` CREATE OR REPLACE TABLE ${tableName} @@ -13,15 +13,15 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); await client.command({ query: ` INSERT INTO ${tableName} SELECT '42', quantilesBFloat16State(0.5)(arrayJoin([toFloat32(10), toFloat32(20)]))`, -}) +}); const rows = await client.query({ query: `SELECT finalizeAggregation(data) AS result FROM ${tableName}`, - format: 'JSONEachRow', -}) -console.info(await rows.json()) -await client.close() + format: "JSONEachRow", +}); +console.info(await rows.json()); +await client.close(); diff --git a/examples/node/coding/insert_into_different_db.ts b/examples/node/coding/insert_into_different_db.ts index f668df7e..9ccaf8d6 100644 --- a/examples/node/coding/insert_into_different_db.ts +++ b/examples/node/coding/insert_into_different_db.ts @@ -1,20 +1,20 @@ -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; // Writing to a table that lives in a database other than the client's default `database`. // Pass a fully qualified `database.table` name to `client.insert`/`client.query`/`client.command` // when you need to address a different database without recreating the client. -const dbName = 'clickhouse_js_examples' -const tableName = `test_table_${Math.random().toString(36).slice(2, 8)}` +const dbName = "clickhouse_js_examples"; +const tableName = `test_table_${Math.random().toString(36).slice(2, 8)}`; const client = createClient({ - database: 'system', -}) + database: "system", +}); await client.command({ query: `CREATE DATABASE IF NOT EXISTS ${dbName}`, -}) +}); // Including the database here, as the client is created for "system" -const fullTableName = `${dbName}.${tableName}` +const fullTableName = `${dbName}.${tableName}`; await client.command({ query: ` @@ -23,19 +23,19 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) -console.info(`Created table ${fullTableName}`) +}); +console.info(`Created table ${fullTableName}`); await client.insert({ table: fullTableName, - values: [{ id: 42, message: 'foo' }], - format: 'JSONEachRow', -}) + values: [{ id: 42, message: "foo" }], + format: "JSONEachRow", +}); const rows = await client.query({ query: `SELECT * FROM ${fullTableName} ORDER BY id, message DESC`, - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); -console.info('Result:', await rows.json()) -await client.close() +console.info("Result:", await rows.json()); +await client.close(); diff --git a/examples/node/coding/insert_js_dates.ts b/examples/node/coding/insert_js_dates.ts index 81922d9c..5694e493 100644 --- a/examples/node/coding/insert_js_dates.ts +++ b/examples/node/coding/insert_js_dates.ts @@ -1,8 +1,8 @@ -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; // NB: currently, JS Date objects work only with DateTime* fields -const tableName = 'insert_js_date' -const client = createClient() +const tableName = "insert_js_date"; +const client = createClient(); await client.command({ query: ` CREATE OR REPLACE TABLE ${tableName} @@ -10,24 +10,24 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); await client.insert({ table: tableName, values: [ { - id: '42', + id: "42", dt: new Date(), }, ], clickhouse_settings: { // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z') - date_time_input_format: 'best_effort', + date_time_input_format: "best_effort", }, - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); const rows = await client.query({ query: `SELECT * FROM ${tableName}`, - format: 'JSONEachRow', -}) -console.info(await rows.json()) -await client.close() + format: "JSONEachRow", +}); +console.info(await rows.json()); +await client.close(); diff --git a/examples/node/coding/insert_specific_columns.ts b/examples/node/coding/insert_specific_columns.ts index 24bad7dd..3b73a9d9 100644 --- a/examples/node/coding/insert_specific_columns.ts +++ b/examples/node/coding/insert_specific_columns.ts @@ -1,9 +1,9 @@ // Explicitly specifying a list of columns to insert the data into. // For the inverse (excluding certain columns instead), see `insert_exclude_columns.ts`. -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; -const tableName = 'insert_specific_columns' -const client = createClient() +const tableName = "insert_specific_columns"; +const client = createClient(); await client.command({ query: ` @@ -12,28 +12,28 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); await client.insert({ table: tableName, - values: [{ message: 'foo' }], - format: 'JSONEachRow', + values: [{ message: "foo" }], + format: "JSONEachRow", // `id` column value for this row will be zero - columns: ['message'], -}) + columns: ["message"], +}); await client.insert({ table: tableName, values: [{ id: 42 }], - format: 'JSONEachRow', + format: "JSONEachRow", // `message` column value for this row will be an empty string - columns: ['id'], -}) + columns: ["id"], +}); const rows = await client.query({ query: `SELECT * FROM ${tableName} ORDER BY id, message DESC`, - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); -console.info(await rows.json()) -await client.close() +console.info(await rows.json()); +await client.close(); diff --git a/examples/node/coding/insert_uuid_into_uint128.ts b/examples/node/coding/insert_uuid_into_uint128.ts index a4a60100..04d67fc4 100644 --- a/examples/node/coding/insert_uuid_into_uint128.ts +++ b/examples/node/coding/insert_uuid_into_uint128.ts @@ -1,6 +1,6 @@ -import { strict as assert } from 'node:assert' +import { strict as assert } from "node:assert"; -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; // ClickHouse converts UUID values into UInt128 implicitly only for the `VALUES` // clause. With row-oriented input formats such as `JSONEachRow`, sending a UUID @@ -18,16 +18,16 @@ import { createClient } from '@clickhouse/client' function uuidToUInt128(uuid: string): string { // 8-4-4-4-12 hex digits → 32 hex digits → BigInt → decimal string - return BigInt('0x' + uuid.replace(/-/g, '')).toString() + return BigInt("0x" + uuid.replace(/-/g, "")).toString(); } -const client = createClient() +const client = createClient(); -const uuid = '019982cb-3abf-7e12-9668-c788a9e3639c' -const expectedUInt128 = uuidToUInt128(uuid) +const uuid = "019982cb-3abf-7e12-9668-c788a9e3639c"; +const expectedUInt128 = uuidToUInt128(uuid); // ---- Pattern 1: client-side UUID → UInt128 conversion ---- -const tableName = 'insert_uuid_into_uint128_example' +const tableName = "insert_uuid_into_uint128_example"; await client.command({ query: ` CREATE OR REPLACE TABLE ${tableName} @@ -38,34 +38,34 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); await client.insert({ table: tableName, values: [ { id: expectedUInt128, - description: 'converted from UUID on the client', + description: "converted from UUID on the client", }, ], - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); const converted = await client.query({ // UInt128 values are returned as decimal strings (too wide for a JS number). query: `SELECT toString(id) AS id_uint128, description FROM ${tableName}`, - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); const convertedRows = await converted.json<{ - id_uint128: string - description: string -}>() -console.info('Pattern 1 (client-side conversion):', convertedRows) + id_uint128: string; + description: string; +}>(); +console.info("Pattern 1 (client-side conversion):", convertedRows); // Round-trip assertion: the value SELECTed back must equal the BigInt-derived // UInt128 we sent, proving the server parsed the JSON input correctly. -assert.equal(convertedRows[0].id_uint128, expectedUInt128) +assert.equal(convertedRows[0].id_uint128, expectedUInt128); // ---- Pattern 2: EPHEMERAL UUID column with UInt128 DEFAULT ---- -const ephemeralTableName = 'insert_uuid_into_uint128_ephemeral_example' +const ephemeralTableName = "insert_uuid_into_uint128_ephemeral_example"; await client.command({ query: ` CREATE OR REPLACE TABLE ${ephemeralTableName} @@ -77,31 +77,31 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); await client.insert({ table: ephemeralTableName, values: [ { id_uuid: uuid, - description: 'populated via EPHEMERAL UUID column', + description: "populated via EPHEMERAL UUID column", }, ], - format: 'JSONEachRow', + format: "JSONEachRow", // The ephemeral column must be listed explicitly so that the DEFAULT // expression on `id` is evaluated. - columns: ['id_uuid', 'description'], -}) + columns: ["id_uuid", "description"], +}); const ephemeral = await client.query({ query: `SELECT toString(id) AS id_uint128, description FROM ${ephemeralTableName}`, - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); const ephemeralRows = await ephemeral.json<{ - id_uint128: string - description: string -}>() -console.info('Pattern 2 (EPHEMERAL column):', ephemeralRows) + id_uint128: string; + description: string; +}>(); +console.info("Pattern 2 (EPHEMERAL column):", ephemeralRows); // Both patterns must produce the same UInt128 representation of the UUID. -assert.equal(ephemeralRows[0].id_uint128, expectedUInt128) +assert.equal(ephemeralRows[0].id_uint128, expectedUInt128); -await client.close() +await client.close(); diff --git a/examples/node/coding/insert_values_and_functions.ts b/examples/node/coding/insert_values_and_functions.ts index 32ddda9a..ec4bfffd 100644 --- a/examples/node/coding/insert_values_and_functions.ts +++ b/examples/node/coding/insert_values_and_functions.ts @@ -1,29 +1,29 @@ // An example how to send an INSERT INTO ... VALUES ... query that requires additional functions call. // Inspired by https://github.com/ClickHouse/clickhouse-js/issues/239 -import type { ClickHouseSettings } from '@clickhouse/client' -import { createClient } from '@clickhouse/client' +import type { ClickHouseSettings } from "@clickhouse/client"; +import { createClient } from "@clickhouse/client"; interface Data { - id: string - timestamp: number - email: string - name: string | null + id: string; + timestamp: number; + email: string; + name: string | null; } -const tableName = 'insert_values_and_functions' -const client = createClient() +const tableName = "insert_values_and_functions"; +const client = createClient(); // Recommended for cluster usage to avoid situations where a query processing error occurred after the response code // and HTTP headers were sent to the client, as it might happen before the changes were applied on the server. // See https://clickhouse.com/docs/en/interfaces/http/#response-buffering const commandSettings: ClickHouseSettings = { wait_end_of_query: 1, -} +}; // Prepare an example table await client.command({ query: `DROP TABLE IF EXISTS ${tableName}`, clickhouse_settings: commandSettings, -}) +}); await client.command({ query: ` CREATE TABLE ${tableName} ( @@ -36,45 +36,45 @@ await client.command({ ORDER BY (id) `, clickhouse_settings: commandSettings, -}) +}); // Here we are assuming that we are getting these rows from somewhere... -const rows = getRows(20_000) +const rows = getRows(20_000); // Generate the query and insert the values const insertQuery = ` INSERT INTO ${tableName} (id, timestamp, email, name) VALUES - ${rows.map((r) => toInsertValue(r)).join(',')} -` + ${rows.map((r) => toInsertValue(r)).join(",")} +`; await client.command({ query: insertQuery, clickhouse_settings: commandSettings, -}) +}); // Get a few back and print those rows to check what was inserted const sampleResultSet = await client.query({ query: `SELECT * FROM ${tableName} ORDER BY rand() LIMIT 3`, - format: 'JSONEachRow', -}) -console.info(`Sample inserted rows:`) -const sampleRows = await sampleResultSet.json() + format: "JSONEachRow", +}); +console.info(`Sample inserted rows:`); +const sampleRows = await sampleResultSet.json(); sampleRows.forEach((row) => { - console.info(row) -}) + console.info(row); +}); // Close it during your application graceful shutdown -await client.close() +await client.close(); function getRows(n: number): Data[] { - const now = Date.now() // UNIX timestamp in milliseconds + const now = Date.now(); // UNIX timestamp in milliseconds return [...new Array(n)].map((_, i) => ({ - id: Buffer.from(i.toString(10)).toString('hex'), + id: Buffer.from(i.toString(10)).toString("hex"), timestamp: now - i * 1000, // subtract one second for each row email: `email${i}@example.com`, name: i % 2 === 0 ? `Name${i}` : null, // for every second row it is NULL - })) + })); } // Generates something like: @@ -83,9 +83,9 @@ function getRows(n: number): Data[] { // (unhex('144'), '1623677409123', 'email144@example.com', NULL) // if name is null. function toInsertValue(row: Data): string { - const id = `unhex('${row.id}')` - const timestamp = `'${row.timestamp}'` - const email = `'${row.email}'` - const name = row.name === null ? 'NULL' : `'${row.name}'` - return `(${id}, ${timestamp}, ${email}, ${name})` + const id = `unhex('${row.id}')`; + const timestamp = `'${row.timestamp}'`; + const email = `'${row.email}'`; + const name = row.name === null ? "NULL" : `'${row.name}'`; + return `(${id}, ${timestamp}, ${email}, ${name})`; } diff --git a/examples/node/coding/ping_existing_host.ts b/examples/node/coding/ping_existing_host.ts index fd9ce5bc..3a37e536 100644 --- a/examples/node/coding/ping_existing_host.ts +++ b/examples/node/coding/ping_existing_host.ts @@ -8,19 +8,19 @@ // See also: // - `ping_non_existing_host.ts` - ping against a host that does not exist. // - `../troubleshooting/ping_timeout.ts` - Node.js-only ping timeout example. -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; const client = createClient({ - url: process.env['CLICKHOUSE_URL'], // defaults to 'http://localhost:8123' - password: process.env['CLICKHOUSE_PASSWORD'], // defaults to an empty string -}) -const pingResult = await client.ping() + url: process.env["CLICKHOUSE_URL"], // defaults to 'http://localhost:8123' + password: process.env["CLICKHOUSE_PASSWORD"], // defaults to an empty string +}); +const pingResult = await client.ping(); if (pingResult.success) { - console.log('[ExistingHostPing] Ping to the existing host is successful') + console.log("[ExistingHostPing] Ping to the existing host is successful"); } else { console.error( - '[ExistingHostPing] Ping expected to succeed, but got:', + "[ExistingHostPing] Ping expected to succeed, but got:", pingResult, - ) + ); } -await client.close() +await client.close(); diff --git a/examples/node/coding/ping_non_existing_host.ts b/examples/node/coding/ping_non_existing_host.ts index 401898ef..7e774541 100644 --- a/examples/node/coding/ping_non_existing_host.ts +++ b/examples/node/coding/ping_non_existing_host.ts @@ -7,31 +7,31 @@ // See also: // - `ping_existing_host.ts` - successful ping against an existing host. // - `ping_timeout.ts` - ping that times out. -import type { PingResult } from '@clickhouse/client' -import { createClient } from '@clickhouse/client' +import type { PingResult } from "@clickhouse/client"; +import { createClient } from "@clickhouse/client"; const client = createClient({ - url: 'http://localhost:8100', // non-existing host + url: "http://localhost:8100", // non-existing host request_timeout: 50, // low request_timeout to speed up the example -}) +}); // Ping does not throw an error; instead, { success: false; error: Error } is returned. -const pingResult = await client.ping() +const pingResult = await client.ping(); if (hasConnectionRefusedError(pingResult)) { - console.info('[NonExistingHostPing] Ping connection refused, as expected') + console.info("[NonExistingHostPing] Ping connection refused, as expected"); } else { console.error( - '[NonExistingHostPing] Ping expected to fail with ECONNREFUSED, but got:', + "[NonExistingHostPing] Ping expected to fail with ECONNREFUSED, but got:", pingResult, - ) + ); } -await client.close() +await client.close(); function hasConnectionRefusedError( pingResult: PingResult, -): pingResult is PingResult & { error: { code: 'ECONNREFUSED' } } { +): pingResult is PingResult & { error: { code: "ECONNREFUSED" } } { return ( !pingResult.success && - 'code' in pingResult.error && - pingResult.error.code === 'ECONNREFUSED' - ) + "code" in pingResult.error && + pingResult.error.code === "ECONNREFUSED" + ); } diff --git a/examples/node/coding/qbit.ts b/examples/node/coding/qbit.ts index 43160a10..743b251b 100644 --- a/examples/node/coding/qbit.ts +++ b/examples/node/coding/qbit.ts @@ -1,5 +1,5 @@ -import { createClient } from '@clickhouse/client' -import assert from 'node:assert' +import { createClient } from "@clickhouse/client"; +import assert from "node:assert"; /** * QBit is a column type that stores float vectors in bit-sliced ("transposed") @@ -24,13 +24,13 @@ import assert from 'node:assert' * See https://clickhouse.com/docs/sql-reference/data-types/qbit */ -const tableName = `chjs_qbit` +const tableName = `chjs_qbit`; const client = createClient({ clickhouse_settings: { // Required on ClickHouse 25.10 (experimental); ignored on 26.x where QBit is GA. allow_experimental_qbit_type: 1, }, -}) +}); await client.command({ query: ` @@ -42,7 +42,7 @@ await client.command({ ENGINE MergeTree ORDER BY id `, -}) +}); // Even though QBit is stored internally as a Tuple of FixedString bit planes, // JSON* formats accept (and return) the original Array(Float32) shape. @@ -50,24 +50,24 @@ const values = [ { id: 1, vec: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0] }, { id: 2, vec: [8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0] }, { id: 3, vec: [1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5] }, -] +]; await client.insert({ table: tableName, - format: 'JSONEachRow', + format: "JSONEachRow", values, -}) +}); // Round-trip via JSONEachRow: the vec column comes back as an array of numbers. const rs = await client.query({ query: `SELECT id, vec FROM ${tableName} ORDER BY id`, - format: 'JSONEachRow', -}) -const rows = await rs.json<{ id: number; vec: number[] }>() -console.log('Round-tripped rows:') -console.log(rows) + format: "JSONEachRow", +}); +const rows = await rs.json<{ id: number; vec: number[] }>(); +console.log("Round-tripped rows:"); +console.log(rows); // Even though QBit is stored as a Tuple of FixedString bit planes, JSON* formats // return the original Float32 vectors unchanged. -assert.deepStrictEqual(rows, values) +assert.deepStrictEqual(rows, values); // Approximate vector search via L2DistanceTransposed. // The third argument is the precision in bits: lower = less I/O, less accurate. @@ -82,17 +82,17 @@ const search = await client.query({ ref: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], bits: 16, }, - format: 'JSONEachRow', -}) -const nearest = await search.json<{ id: number; dist: number }>() -console.log('Nearest neighbours of the reference vector:') -console.log(nearest) + format: "JSONEachRow", +}); +const nearest = await search.json<{ id: number; dist: number }>(); +console.log("Nearest neighbours of the reference vector:"); +console.log(nearest); // The reference vector is exactly row #1, so it must be the closest match (dist 0). assert.deepStrictEqual( nearest.map((r) => r.id), [1, 3, 2], -) -assert.strictEqual(nearest[0].dist, 0) +); +assert.strictEqual(nearest[0].dist, 0); // Bit-plane subcolumns (`vec.N`) are exposed as FixedString and therefore are // NOT valid UTF-8. Selecting them directly with a JSON* format would force the @@ -101,18 +101,18 @@ assert.strictEqual(nearest[0].dist, 0) // or read them as hex/base64: const planes = await client.query({ query: `SELECT id, hex(vec.1) AS bit_plane_1_hex FROM ${tableName} ORDER BY id`, - format: 'JSONEachRow', -}) -const planeRows = await planes.json<{ id: number; bit_plane_1_hex: string }>() -console.log('First bit plane per row (hex-encoded to keep JSON UTF-8 safe):') -console.log(planeRows) + format: "JSONEachRow", +}); +const planeRows = await planes.json<{ id: number; bit_plane_1_hex: string }>(); +console.log("First bit plane per row (hex-encoded to keep JSON UTF-8 safe):"); +console.log(planeRows); // hex() yields a UTF-8-safe representation of the raw FixedString bit-plane bytes. assert.deepStrictEqual( planeRows.map((r) => r.id), [1, 2, 3], -) +); for (const row of planeRows) { - assert.match(row.bit_plane_1_hex, /^[0-9A-F]*$/) + assert.match(row.bit_plane_1_hex, /^[0-9A-F]*$/); } -await client.close() +await client.close(); diff --git a/examples/node/coding/query_with_parameter_binding.ts b/examples/node/coding/query_with_parameter_binding.ts index 29f13a25..84c8ad30 100644 --- a/examples/node/coding/query_with_parameter_binding.ts +++ b/examples/node/coding/query_with_parameter_binding.ts @@ -2,9 +2,9 @@ // // For binding parameters that contain special characters (tabs, newlines, quotes, etc.), // see `query_with_parameter_binding_special_chars.ts`. -import { createClient, TupleParam } from '@clickhouse/client' +import { createClient, TupleParam } from "@clickhouse/client"; -const client = createClient() +const client = createClient(); const resultSet = await client.query({ query: ` SELECT @@ -24,31 +24,31 @@ const resultSet = await client.query({ {var_ipv4: IPv4} AS var_ipv4, {var_null: Nullable(String)} AS var_null `, - format: 'JSONEachRow', + format: "JSONEachRow", query_params: { var_int: 10, - var_float: '10.557', + var_float: "10.557", var_str: 20, var_array: [42, 144], - var_tuple: new TupleParam([42, 'foo']), + var_tuple: new TupleParam([42, "foo"]), var_map: new Map([ - [42, ['a', 'b']], - [144, ['c', 'd']], + [42, ["a", "b"]], + [144, ["c", "d"]], ]), - var_date: '2022-01-01', - var_datetime: '2022-01-01 12:34:56', // or a Date object - var_datetime64_3: '2022-01-01 12:34:56.789', // or a Date object + var_date: "2022-01-01", + var_datetime: "2022-01-01 12:34:56", // or a Date object + var_datetime64_3: "2022-01-01 12:34:56.789", // or a Date object // NB: Date object with DateTime64(9) is still possible, // but there will be precision loss, as JS Date has only milliseconds. - var_datetime64_9: '2022-01-01 12:34:56.123456789', + var_datetime64_9: "2022-01-01 12:34:56.123456789", // It is also possible to provide DateTime64 as a timestamp. - var_datetime64_9_ts: '1651490755.123456789', - var_decimal: '123.45', - var_uuid: '01234567-89ab-cdef-0123-456789abcdef', - var_ipv4: '192.168.0.1', + var_datetime64_9_ts: "1651490755.123456789", + var_decimal: "123.45", + var_uuid: "01234567-89ab-cdef-0123-456789abcdef", + var_ipv4: "192.168.0.1", var_null: null, }, -}) -console.info('Result (different data types):', await resultSet.json()) +}); +console.info("Result (different data types):", await resultSet.json()); -await client.close() +await client.close(); diff --git a/examples/node/coding/query_with_parameter_binding_special_chars.ts b/examples/node/coding/query_with_parameter_binding_special_chars.ts index 0a1e048b..974d4add 100644 --- a/examples/node/coding/query_with_parameter_binding_special_chars.ts +++ b/examples/node/coding/query_with_parameter_binding_special_chars.ts @@ -2,9 +2,9 @@ // Available since clickhouse-js 0.3.1. // // For an overview of binding regular values of various data types, see `query_with_parameter_binding.ts`. -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; -const client = createClient() +const client = createClient(); const resultSet = await client.query({ query: ` @@ -14,17 +14,17 @@ const resultSet = await client.query({ 'foo_\r_bar' = {carriage_return: String} AS has_carriage_return, 'foo_\\'_bar' = {single_quote: String} AS has_single_quote, 'foo_\\_bar' = {backslash: String} AS has_backslash`, - format: 'JSONEachRow', + format: "JSONEachRow", query_params: { - tab: 'foo_\t_bar', - newline: 'foo_\n_bar', - carriage_return: 'foo_\r_bar', + tab: "foo_\t_bar", + newline: "foo_\n_bar", + carriage_return: "foo_\r_bar", single_quote: "foo_'_bar", - backslash: 'foo_\\_bar', + backslash: "foo_\\_bar", }, -}) +}); // Should return all 1, as query params will match the strings in the SELECT. -console.info('Result (special characters):', await resultSet.json()) +console.info("Result (special characters):", await resultSet.json()); -await client.close() +await client.close(); diff --git a/examples/node/coding/select_data_formats_overview.ts b/examples/node/coding/select_data_formats_overview.ts index d28f3817..2aea714f 100644 --- a/examples/node/coding/select_data_formats_overview.ts +++ b/examples/node/coding/select_data_formats_overview.ts @@ -10,61 +10,61 @@ // - INSERT formats overview - insert_data_formats_overview.ts // - JSON data streaming example - select_streaming_json_each_row.ts // - Streaming Parquet into a file - node/select_parquet_as_file.ts -import { createClient, type DataFormat } from '@clickhouse/client' +import { createClient, type DataFormat } from "@clickhouse/client"; -const tableName = 'select_data_formats_overview' -const client = createClient() -await prepareTestData() +const tableName = "select_data_formats_overview"; +const client = createClient(); +await prepareTestData(); // These ClickHouse JSON formats can be streamed as well instead of loading the entire result into the app memory; // See this example: node/select_streaming_json_each_row.ts -console.log('#### Streamable JSON formats:\n') -await selectJSON('JSONEachRow') -await selectJSON('JSONStringsEachRow') -await selectJSON('JSONCompactEachRow') -await selectJSON('JSONCompactStringsEachRow') -await selectJSON('JSONCompactEachRowWithNames') -await selectJSON('JSONCompactEachRowWithNamesAndTypes') -await selectJSON('JSONCompactStringsEachRowWithNames') -await selectJSON('JSONCompactStringsEachRowWithNamesAndTypes') +console.log("#### Streamable JSON formats:\n"); +await selectJSON("JSONEachRow"); +await selectJSON("JSONStringsEachRow"); +await selectJSON("JSONCompactEachRow"); +await selectJSON("JSONCompactStringsEachRow"); +await selectJSON("JSONCompactEachRowWithNames"); +await selectJSON("JSONCompactEachRowWithNamesAndTypes"); +await selectJSON("JSONCompactStringsEachRowWithNames"); +await selectJSON("JSONCompactStringsEachRowWithNamesAndTypes"); // These are single document ClickHouse JSON formats, which are not streamable -console.log('\n#### Single document JSON formats:\n') -await selectJSON('JSON') -await selectJSON('JSONStrings') -await selectJSON('JSONCompact') -await selectJSON('JSONCompactStrings') -await selectJSON('JSONColumnsWithMetadata') -await selectJSON('JSONObjectEachRow') +console.log("\n#### Single document JSON formats:\n"); +await selectJSON("JSON"); +await selectJSON("JSONStrings"); +await selectJSON("JSONCompact"); +await selectJSON("JSONCompactStrings"); +await selectJSON("JSONColumnsWithMetadata"); +await selectJSON("JSONObjectEachRow"); // These "raw" ClickHouse formats can be streamed as well instead of loading the entire result into the app memory; // see node/select_streaming_text_line_by_line.ts -console.log('\n#### Raw formats:\n') -await selectText('CSV') -await selectText('CSVWithNames') -await selectText('CSVWithNamesAndTypes') -await selectText('TabSeparated') -await selectText('TabSeparatedRaw') -await selectText('TabSeparatedWithNames') -await selectText('TabSeparatedWithNamesAndTypes') -await selectText('CustomSeparated') -await selectText('CustomSeparatedWithNames') -await selectText('CustomSeparatedWithNamesAndTypes') +console.log("\n#### Raw formats:\n"); +await selectText("CSV"); +await selectText("CSVWithNames"); +await selectText("CSVWithNamesAndTypes"); +await selectText("TabSeparated"); +await selectText("TabSeparatedRaw"); +await selectText("TabSeparatedWithNames"); +await selectText("TabSeparatedWithNamesAndTypes"); +await selectText("CustomSeparated"); +await selectText("CustomSeparatedWithNames"); +await selectText("CustomSeparatedWithNamesAndTypes"); // Parquet can be streamed in and out, too. // See node/select_parquet_as_file.ts, node/insert_file_stream_parquet.ts -await client.close() +await client.close(); // Selecting data in different JSON formats async function selectJSON(format: DataFormat) { const rows = await client.query({ query: `SELECT * FROM ${tableName} LIMIT 10`, // don't use FORMAT clause; specify the format separately format: format, - }) - const data = await rows.json() // get all the data at once - console.log(`Format: ${format}, parsed data:`) - console.dir(data, { depth: null }) // prints the nested arrays, too + }); + const data = await rows.json(); // get all the data at once + console.log(`Format: ${format}, parsed data:`); + console.dir(data, { depth: null }); // prints the nested arrays, too } // Selecting text data in different formats; `.json()` cannot be used here as it does not make sense. @@ -75,12 +75,12 @@ async function selectText(format: DataFormat) { clickhouse_settings: { // This is for CustomSeparated format demo purposes. // See also: https://clickhouse.com/docs/en/interfaces/formats#format-customseparated - format_custom_field_delimiter: ' | ', + format_custom_field_delimiter: " | ", }, - }) - const data = await rows.text() // get all the data at once - console.log(`Format: ${format}, text data:`) - console.log(data) + }); + const data = await rows.text(); // get all the data at once + console.log(`Format: ${format}, text data:`); + console.log(data); } async function prepareTestData() { @@ -91,14 +91,14 @@ async function prepareTestData() { ENGINE MergeTree() ORDER BY (id) `, - }) + }); // See also: INSERT formats overview - insert_data_formats_overview.ts await client.insert({ table: tableName, values: [ - { id: 42, name: 'foo', sku: [1, 2, 3] }, - { id: 43, name: 'bar', sku: [4, 5, 6] }, + { id: 42, name: "foo", sku: [1, 2, 3] }, + { id: 43, name: "bar", sku: [4, 5, 6] }, ], - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); } diff --git a/examples/node/coding/select_json_each_row.ts b/examples/node/coding/select_json_each_row.ts index 4b81c1f0..b0fb098f 100644 --- a/examples/node/coding/select_json_each_row.ts +++ b/examples/node/coding/select_json_each_row.ts @@ -5,17 +5,17 @@ // See also: // - `select_json_with_metadata.ts` for metadata-aware JSON responses. // - `select_data_formats_overview.ts` for a broader format comparison. -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; interface Data { - number: string + number: string; } -const client = createClient() +const client = createClient(); const rows = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 5', - format: 'JSONEachRow', -}) -const result = await rows.json() -result.forEach((row) => console.log(row)) -await client.close() + query: "SELECT number FROM system.numbers LIMIT 5", + format: "JSONEachRow", +}); +const result = await rows.json(); +result.forEach((row) => console.log(row)); +await client.close(); diff --git a/examples/node/coding/select_json_with_metadata.ts b/examples/node/coding/select_json_with_metadata.ts index b0943a2c..9cfe405c 100644 --- a/examples/node/coding/select_json_with_metadata.ts +++ b/examples/node/coding/select_json_with_metadata.ts @@ -5,13 +5,13 @@ // See also: // - `select_json_each_row.ts` for row-by-row JSON output. // - `select_data_formats_overview.ts` for a broader format comparison. -import { createClient, type ResponseJSON } from '@clickhouse/client' +import { createClient, type ResponseJSON } from "@clickhouse/client"; -const client = createClient() +const client = createClient(); const rows = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 2', - format: 'JSON', -}) -const result = await rows.json>() -console.log(result) -await client.close() + query: "SELECT number FROM system.numbers LIMIT 2", + format: "JSON", +}); +const result = await rows.json>(); +console.log(result); +await client.close(); diff --git a/examples/node/coding/session_id_and_temporary_tables.ts b/examples/node/coding/session_id_and_temporary_tables.ts index 17eafc6e..ff3721c6 100644 --- a/examples/node/coding/session_id_and_temporary_tables.ts +++ b/examples/node/coding/session_id_and_temporary_tables.ts @@ -1,24 +1,24 @@ -import { createClient } from '@clickhouse/client' -import * as crypto from 'node:crypto' +import { createClient } from "@clickhouse/client"; +import * as crypto from "node:crypto"; // Using a `session_id` so that a `TEMPORARY TABLE` created on one request is visible on the next. // Temporary tables only exist for the lifetime of the session and are scoped to the node that // served the CREATE — see also `session_level_commands.ts` for caveats behind load balancers. -const tableName = 'temporary_example' +const tableName = "temporary_example"; const client = createClient({ session_id: crypto.randomUUID(), -}) +}); await client.command({ query: `CREATE TEMPORARY TABLE ${tableName} (i Int32)`, -}) +}); await client.insert({ table: tableName, values: [{ i: 42 }, { i: 144 }], - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); const rs = await client.query({ query: `SELECT * FROM ${tableName}`, - format: 'JSONEachRow', -}) -console.info(await rs.json()) -await client.close() + format: "JSONEachRow", +}); +console.info(await rs.json()); +await client.close(); diff --git a/examples/node/coding/session_level_commands.ts b/examples/node/coding/session_level_commands.ts index 43a6ebe1..a6661b2e 100644 --- a/examples/node/coding/session_level_commands.ts +++ b/examples/node/coding/session_level_commands.ts @@ -1,5 +1,5 @@ -import { createClient } from '@clickhouse/client' -import * as crypto from 'node:crypto' +import { createClient } from "@clickhouse/client"; +import * as crypto from "node:crypto"; // Note that session will work as expected ONLY if you are accessing the Node directly. // If there is a load-balancer in front of ClickHouse nodes, the requests might end up on different nodes, @@ -9,30 +9,30 @@ const client = createClient({ // with session_id defined, SET and other session commands // will affect all the consecutive queries session_id: crypto.randomUUID(), -}) +}); await client.command({ query: `SET output_format_json_quote_64bit_integers = 0`, clickhouse_settings: { wait_end_of_query: 1 }, -}) +}); // this query uses output_format_json_quote_64bit_integers = 0 const rows1 = await client.query({ query: `SELECT toInt64(42)`, - format: 'JSONEachRow', -}) -console.log(await rows1.json()) + format: "JSONEachRow", +}); +console.log(await rows1.json()); await client.command({ query: `SET output_format_json_quote_64bit_integers = 1`, clickhouse_settings: { wait_end_of_query: 1 }, -}) +}); // this query uses output_format_json_quote_64bit_integers = 1 const rows2 = await client.query({ query: `SELECT toInt64(144)`, - format: 'JSONEachRow', -}) -console.log(await rows2.json()) + format: "JSONEachRow", +}); +console.log(await rows2.json()); -await client.close() +await client.close(); diff --git a/examples/node/coding/time_time64.ts b/examples/node/coding/time_time64.ts index d2786071..14ca8537 100644 --- a/examples/node/coding/time_time64.ts +++ b/examples/node/coding/time_time64.ts @@ -1,15 +1,15 @@ // See also: // - https://clickhouse.com/docs/sql-reference/data-types/time // - https://clickhouse.com/docs/sql-reference/data-types/time64 -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; -const tableName = `chjs_time_time64` +const tableName = `chjs_time_time64`; const client = createClient({ clickhouse_settings: { // Since ClickHouse 25.6 enable_time_time64_type: 1, }, -}) +}); await client.command({ query: ` CREATE OR REPLACE TABLE ${tableName} @@ -24,50 +24,50 @@ await client.command({ ENGINE MergeTree ORDER BY id `, -}) +}); // Sample representation in JSONEachRow format const values = [ { id: 1, - t: '12:34:56', - t64_0: '12:34:56', - t64_3: '12:34:56.123', - t64_6: '12:34:56.123456', - t64_9: '12:34:56.123456789', + t: "12:34:56", + t64_0: "12:34:56", + t64_3: "12:34:56.123", + t64_6: "12:34:56.123456", + t64_9: "12:34:56.123456789", }, { id: 2, - t: '23:59:59', - t64_0: '23:59:59', - t64_3: '23:59:59.987', - t64_6: '23:59:59.987654', - t64_9: '23:59:59.987654321', + t: "23:59:59", + t64_0: "23:59:59", + t64_3: "23:59:59.987", + t64_6: "23:59:59.987654", + t64_9: "23:59:59.987654321", }, { id: 3, - t: '999:59:59', - t64_0: '999:59:59', - t64_3: '999:59:59.999', - t64_6: '999:59:59.999999', - t64_9: '999:59:59.999999999', + t: "999:59:59", + t64_0: "999:59:59", + t64_3: "999:59:59.999", + t64_6: "999:59:59.999999", + t64_9: "999:59:59.999999999", }, { id: 4, - t: '-999:59:59', - t64_0: '-999:59:59', - t64_3: '-999:59:59.999', - t64_6: '-999:59:59.999999', - t64_9: '-999:59:59.999999999', + t: "-999:59:59", + t64_0: "-999:59:59", + t64_3: "-999:59:59.999", + t64_6: "-999:59:59.999999", + t64_9: "-999:59:59.999999999", }, -] +]; await client.insert({ table: tableName, - format: 'JSONEachRow', + format: "JSONEachRow", values, -}) +}); const rs = await client.query({ query: `SELECT * FROM ${tableName}`, - format: 'JSONEachRow', -}) -console.log(await rs.json()) -await client.close() + format: "JSONEachRow", +}); +console.log(await rs.json()); +await client.close(); diff --git a/examples/node/coding/url_configuration.ts b/examples/node/coding/url_configuration.ts index 4ed39979..6467c1d6 100644 --- a/examples/node/coding/url_configuration.ts +++ b/examples/node/coding/url_configuration.ts @@ -1,4 +1,4 @@ -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; // URL configuration reference with all possible values. // @@ -13,38 +13,38 @@ import { createClient } from '@clickhouse/client' // +----------------------------------------------------------------------------------------------------------+ const url = // basic auth (username:password) + database will be extracted from the URL - 'https://bob:secret@my.host:8124/analytics?' + + "https://bob:secret@my.host:8124/analytics?" + [ // arbitrary string values - 'application=my_analytics_app', - 'session_id=random_session_id', - 'pathname=/my_proxy', // or `my_proxy`, without the leading slash + "application=my_analytics_app", + "session_id=random_session_id", + "pathname=/my_proxy", // or `my_proxy`, without the leading slash // a numeric value - 'request_timeout=60000', + "request_timeout=60000", // a numeric value; max_open_connections is expected to be at least 1 when set explicitly. - 'max_open_connections=10', + "max_open_connections=10", // boolean values can be set as 1/0 or true/false; // sets compression.request = true - 'compression_request=1', + "compression_request=1", // sets compression.response = false - 'compression_response=false', + "compression_response=false", // sets log.level = 'TRACE'; // allowed values: TRACE, DEBUG, INFO, WARN, ERROR, OFF. - 'log_level=TRACE', + "log_level=TRACE", // sets keep_alive.enabled = false - 'keep_alive_enabled=false', + "keep_alive_enabled=false", // (Node.js only) sets keep_alive.idle_socket_ttl = 1500 - 'keep_alive_idle_socket_ttl=1500', + "keep_alive_idle_socket_ttl=1500", // all values prefixed with clickhouse_setting_ will be added to clickhouse_settings // this will set clickhouse_settings.async_insert = 1 - 'clickhouse_setting_async_insert=1', + "clickhouse_setting_async_insert=1", // ch_ is a shorthand for clickhouse_setting_* parameters; works similarly. // this will set clickhouse_settings.wait_for_async_insert = 0 - 'ch_wait_for_async_insert=0', + "ch_wait_for_async_insert=0", // adds a custom HTTP header 'X-CLICKHOUSE-AUTH' with 'secret_header' value to client requests // arbitrary string value - 'http_header_X-CLICKHOUSE-AUTH=secret_header', - ].join('&') + "http_header_X-CLICKHOUSE-AUTH=secret_header", + ].join("&"); /* The URL above is an equivalent to the following configuration object: { @@ -76,6 +76,6 @@ const url = */ const client = createClient({ url, -}) +}); // your queries will go here... -await client.close() +await client.close(); diff --git a/examples/node/eslint.config.mjs b/examples/node/eslint.config.mjs index cb88cd08..b965e9e6 100644 --- a/examples/node/eslint.config.mjs +++ b/examples/node/eslint.config.mjs @@ -1,8 +1,8 @@ -import js from '@eslint/js' -import { defineConfig } from 'eslint/config' -import tseslint from 'typescript-eslint' -import pluginPrettier from 'eslint-plugin-prettier' -import pluginExpectType from 'eslint-plugin-expect-type/configs/recommended' +import js from "@eslint/js"; +import { defineConfig } from "eslint/config"; +import tseslint from "typescript-eslint"; +import pluginPrettier from "eslint-plugin-prettier"; +import pluginExpectType from "eslint-plugin-expect-type/configs/recommended"; export default defineConfig( // Base ESLint recommended rules @@ -11,7 +11,7 @@ export default defineConfig( ...tseslint.configs.strict, ...tseslint.configs.stylistic, { - files: ['**/*.ts'], + files: ["**/*.ts"], languageOptions: { parserOptions: { projectService: true, @@ -20,33 +20,27 @@ export default defineConfig( }, plugins: { prettier: pluginPrettier, - 'expect-type': pluginExpectType, + "expect-type": pluginExpectType, }, rules: { - 'prettier/prettier': 'error', - '@typescript-eslint/no-floating-promises': 'error', - eqeqeq: 'error', - 'no-console': 'error', + "prettier/prettier": "error", + "@typescript-eslint/no-floating-promises": "error", + eqeqeq: "error", + "no-console": "error", // Keep some rules relaxed until addressed in dedicated PRs - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/consistent-type-imports': 'warn', - '@typescript-eslint/array-type': 'off', + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/consistent-type-imports": "warn", + "@typescript-eslint/array-type": "off", }, }, { - files: ['**/*.ts'], + files: ["**/*.ts"], rules: { - 'no-console': 'off', + "no-console": "off", }, }, // Ignore build artifacts and externals { - ignores: [ - 'eslint.config.mjs', - 'coverage', - 'out', - 'dist', - 'node_modules', - ], + ignores: ["eslint.config.mjs", "coverage", "out", "dist", "node_modules"], }, -) +); diff --git a/examples/node/performance/async_insert.ts b/examples/node/performance/async_insert.ts index e6e25992..10286f98 100644 --- a/examples/node/performance/async_insert.ts +++ b/examples/node/performance/async_insert.ts @@ -1,11 +1,11 @@ -import { createClient, ClickHouseError } from '@clickhouse/client' +import { createClient, ClickHouseError } from "@clickhouse/client"; // This example demonstrates how to use asynchronous inserts, avoiding client side batching of the incoming data. // Suitable for ClickHouse Cloud, too. // See https://clickhouse.com/docs/en/optimize/asynchronous-inserts const client = createClient({ - url: process.env['CLICKHOUSE_URL'], // defaults to 'http://localhost:8123' - password: process.env['CLICKHOUSE_PASSWORD'], // defaults to an empty string + url: process.env["CLICKHOUSE_URL"], // defaults to 'http://localhost:8123' + password: process.env["CLICKHOUSE_PASSWORD"], // defaults to an empty string max_open_connections: 10, clickhouse_settings: { // https://clickhouse.com/docs/en/operations/settings/settings#async_insert @@ -13,14 +13,14 @@ const client = createClient({ // https://clickhouse.com/docs/en/operations/settings/settings#wait_for_async_insert wait_for_async_insert: 1, // https://clickhouse.com/docs/en/operations/settings/settings#async_insert_max_data_size - async_insert_max_data_size: '1000000', + async_insert_max_data_size: "1000000", // https://clickhouse.com/docs/en/operations/settings/settings#async_insert_busy_timeout_ms async_insert_busy_timeout_ms: 1000, }, -}) +}); // Create the table if necessary -const table = 'async_insert_example' +const table = "async_insert_example"; await client.command({ query: ` CREATE OR REPLACE TABLE ${table} @@ -32,9 +32,9 @@ await client.command({ clickhouse_settings: { wait_end_of_query: 1, }, -}) +}); -const start = new Date() +const start = new Date(); // Assume that we can receive multiple insert requests at the same time // (e.g. from parallel HTTP requests in your app or similar). const promises = [...new Array(10)].map(async () => { @@ -48,38 +48,38 @@ const promises = [...new Array(10)].map(async () => { const values = [...new Array(1000).keys()].map(() => ({ id: Math.floor(Math.random() * 100_000) + 1, data: Math.random().toString(36).slice(2), - })) + })); await client .insert({ table, values, - format: 'JSONEachRow', // or other, depends on your data + format: "JSONEachRow", // or other, depends on your data }) .catch((err) => { // Depending on the error, it is possible that the request itself was not processed on the server. if (err instanceof ClickHouseError) { // You could decide what to do with a failed insert based on the error code. // An overview of possible error codes is available in the `system.errors` ClickHouse table. - console.error(`ClickHouse error: ${err.code}. Insert failed:`, err) - return + console.error(`ClickHouse error: ${err.code}. Insert failed:`, err); + return; } // You could implement a proper retry mechanism depending on your application needs; // for the sake of this example, we just log an error. - console.error('Insert failed:', err) - }) -}) -await Promise.all(promises) + console.error("Insert failed:", err); + }); +}); +await Promise.all(promises); // In this example, it should take `async_insert_busy_timeout_ms` milliseconds or a bit more, // as the server will wait for more insert operations, // cause due to small amount of data its internal buffer was not exceeded. -console.log('Inserts took', new Date().getTime() - start.getTime(), 'ms') +console.log("Inserts took", new Date().getTime() - start.getTime(), "ms"); const resultSet = await client.query({ query: `SELECT count(*) AS count FROM ${table}`, - format: 'JSONEachRow', -}) -const [{ count }] = await resultSet.json<{ count: string }>() + format: "JSONEachRow", +}); +const [{ count }] = await resultSet.json<{ count: string }>(); // It is expected to have 10k records in the table. -console.info('Select count result:', count) -await client.close() +console.info("Select count result:", count); +await client.close(); diff --git a/examples/node/performance/async_insert_without_waiting.ts b/examples/node/performance/async_insert_without_waiting.ts index 49599c21..c5c9c318 100644 --- a/examples/node/performance/async_insert_without_waiting.ts +++ b/examples/node/performance/async_insert_without_waiting.ts @@ -1,6 +1,6 @@ -import { createClient, ClickHouseError } from '@clickhouse/client' -import { EventEmitter } from 'node:events' -import { setTimeout as sleep } from 'node:timers/promises' +import { createClient, ClickHouseError } from "@clickhouse/client"; +import { EventEmitter } from "node:events"; +import { setTimeout as sleep } from "node:timers/promises"; // This example demonstrates how to use async inserts without waiting for an ack about a successfully written batch. // Run it for some time and observe the number of rows sent and the number of rows written to the table. @@ -9,8 +9,8 @@ import { setTimeout as sleep } from 'node:timers/promises' // that can receive an arbitrarily large or small amount of data at various times. // See https://clickhouse.com/docs/en/optimize/asynchronous-inserts const client = createClient({ - url: process.env['CLICKHOUSE_URL'], // defaults to 'http://localhost:8123' - password: process.env['CLICKHOUSE_PASSWORD'], // defaults to an empty string + url: process.env["CLICKHOUSE_URL"], // defaults to 'http://localhost:8123' + password: process.env["CLICKHOUSE_PASSWORD"], // defaults to an empty string max_open_connections: 10, clickhouse_settings: { // https://clickhouse.com/docs/en/operations/settings/settings#async_insert @@ -20,12 +20,12 @@ const client = createClient({ // insert operations promises will be resolved as soon as the request itself was processed on the server. wait_for_async_insert: 0, // https://clickhouse.com/docs/en/operations/settings/settings#async_insert_max_data_size - async_insert_max_data_size: '1000000', + async_insert_max_data_size: "1000000", // https://clickhouse.com/docs/en/operations/settings/settings#async_insert_busy_timeout_max_ms async_insert_busy_timeout_max_ms: 1000, }, -}) -const tableName = 'async_insert_without_waiting' +}); +const tableName = "async_insert_without_waiting"; await client.command({ query: ` CREATE OR REPLACE TABLE ${tableName} @@ -33,20 +33,20 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); interface Row { - id: number - name: string + id: number; + name: string; } // Assume we have an event listener in our application that periodically receives incoming data, // that we would like to have inserted into ClickHouse. // This emitter is just a simulation for the sake of this example. -let rowsInserted = 0 -const listener = new EventEmitter() +let rowsInserted = 0; +const listener = new EventEmitter(); const asyncInsertOnData = async (rows: Row[]) => { - const start = +new Date() + const start = +new Date(); // Each individual insert operation will be resolved as soon as the request itself was processed on the server. // The data will be batched on the server side. Insert will not wait for an ack about a successfully written batch. // This is the main difference from the `examples/async_insert.ts` example. @@ -54,41 +54,41 @@ const asyncInsertOnData = async (rows: Row[]) => { await client.insert({ table: tableName, values: rows, - format: 'JSONEachRow', - }) - rowsInserted += rows.length - const elapsed = +new Date() - start - console.log(`Insert ${rows.length} rows finished in ${elapsed} ms`) + format: "JSONEachRow", + }); + rowsInserted += rows.length; + const elapsed = +new Date() - start; + console.log(`Insert ${rows.length} rows finished in ${elapsed} ms`); } catch (err) { // Depending on the error, it is possible that the request itself was not processed on the server. if (err instanceof ClickHouseError) { // You could decide what to do with a failed insert based on the error code. // An overview of possible error codes is available in the `system.errors` ClickHouse table. - console.error(`ClickHouse error: ${err.code}. Insert failed:`, err) - return + console.error(`ClickHouse error: ${err.code}. Insert failed:`, err); + return; } // You could implement a proper retry mechanism depending on your application needs; // for the sake of this example, we just log an error. - console.error(`Insert failed:`, err) + console.error(`Insert failed:`, err); } -} -listener.on('data', asyncInsertOnData) +}; +listener.on("data", asyncInsertOnData); // Periodically send a random amount of data to the listener, simulating a real application behavior. -let rowsSent = 0 -let sendRowsHandle: ReturnType | null = null +let rowsSent = 0; +let sendRowsHandle: ReturnType | null = null; const sendRows = () => { - const rowsCount = Math.floor(Math.random() * 100) + 1 + const rowsCount = Math.floor(Math.random() * 100) + 1; const rows = [...new Array(rowsCount)].map((_, i) => ({ id: rowsSent + i, name: `Name ${rowsSent + i}`, - })) - rowsSent += rowsCount - listener.emit('data', rows) + })); + rowsSent += rowsCount; + listener.emit("data", rows); // Send the data at a random interval up to 1000 ms. - sendRowsHandle = setTimeout(sendRows, Math.floor(Math.random() * 900) + 100) -} -sendRows() + sendRowsHandle = setTimeout(sendRows, Math.floor(Math.random() * 900) + 100); +}; +sendRows(); // Periodically check the number of rows inserted so far. // Amount of inserted values will be almost always slightly behind due to async inserts. @@ -96,26 +96,29 @@ const rowsCountHandle = setInterval(async () => { try { const resultSet = await client.query({ query: `SELECT count(*) AS count FROM ${tableName}`, - format: 'JSONEachRow', - }) - const [{ count }] = await resultSet.json<{ count: string }>() + format: "JSONEachRow", + }); + const [{ count }] = await resultSet.json<{ count: string }>(); console.log( - 'Rows inserted so far:', + "Rows inserted so far:", `${rowsInserted};`, - 'written to the table:', + "written to the table:", count, - ) + ); } catch (err) { - console.error('Failed to get the number of rows written to the table:', err) + console.error( + "Failed to get the number of rows written to the table:", + err, + ); } -}, 1000) +}, 1000); -await sleep(15000) // Run the example for 15 seconds +await sleep(15000); // Run the example for 15 seconds if (sendRowsHandle) { - clearTimeout(sendRowsHandle) + clearTimeout(sendRowsHandle); } -clearInterval(rowsCountHandle) -listener.removeListener('data', asyncInsertOnData) +clearInterval(rowsCountHandle); +listener.removeListener("data", asyncInsertOnData); -await client.close() +await client.close(); diff --git a/examples/node/performance/insert_arbitrary_format_stream.ts b/examples/node/performance/insert_arbitrary_format_stream.ts index f489f929..7b82274b 100644 --- a/examples/node/performance/insert_arbitrary_format_stream.ts +++ b/examples/node/performance/insert_arbitrary_format_stream.ts @@ -1,8 +1,8 @@ -import type { ClickHouseClient } from '@clickhouse/client' -import { createClient, drainStream } from '@clickhouse/client' -import Fs from 'node:fs' -import { cwd } from 'node:process' -import Path from 'node:path' +import type { ClickHouseClient } from "@clickhouse/client"; +import { createClient, drainStream } from "@clickhouse/client"; +import Fs from "node:fs"; +import { cwd } from "node:process"; +import Path from "node:path"; /** If a particular format is not supported in the {@link ClickHouseClient.insert} method, there is still a workaround: * you could use the {@link ClickHouseClient.exec} method to insert data in an arbitrary format. @@ -16,29 +16,29 @@ import Path from 'node:path' * Related issue with a question: https://github.com/ClickHouse/clickhouse-js/issues/418 * See also: https://clickhouse.com/docs/interfaces/formats/Avro#inserting-data */ -const client = createClient() -const tableName = 'chjs_avro_stream_insert_demo' -await prepareTable(client, tableName) +const client = createClient(); +const tableName = "chjs_avro_stream_insert_demo"; +await prepareTable(client, tableName); -const avroDataFilePath = Path.resolve(cwd(), './node/resources/data.avro') -const avroStream = Fs.createReadStream(avroDataFilePath) +const avroDataFilePath = Path.resolve(cwd(), "./node/resources/data.avro"); +const avroStream = Fs.createReadStream(avroDataFilePath); // Important #1: remember to add the FORMAT clause here, as `exec` takes a raw query in the arguments! const execResult = await client.exec({ query: `INSERT INTO ${tableName} FORMAT Avro`, values: avroStream, -}) +}); // Important #2: the result stream contains nothing useful for an INSERT query (usually, it is just `Ok.`), // and should be immediately drained to release the underlying connection (i.e., HTTP keep-alive socket). -await drainStream(execResult.stream) +await drainStream(execResult.stream); // Verifying that the data was properly inserted; using `JSONEachRow` output format for convenience const rs = await client.query({ query: `SELECT * FROM ${tableName}`, - format: 'JSONEachRow', -}) -console.log('Inserted data:', await rs.json()) + format: "JSONEachRow", +}); +console.log("Inserted data:", await rs.json()); async function prepareTable(client: ClickHouseClient, tableName: string) { await client.command({ @@ -53,5 +53,5 @@ async function prepareTable(client: ClickHouseClient, tableName: string) { // See https://clickhouse.com/docs/en/interfaces/http/#response-buffering wait_end_of_query: 1, }, - }) + }); } diff --git a/examples/node/performance/insert_file_stream_csv.ts b/examples/node/performance/insert_file_stream_csv.ts index 0faa09dc..dec2418a 100644 --- a/examples/node/performance/insert_file_stream_csv.ts +++ b/examples/node/performance/insert_file_stream_csv.ts @@ -1,13 +1,13 @@ -import { createClient, type Row } from '@clickhouse/client' -import Fs from 'node:fs' -import { cwd } from 'node:process' -import Path from 'node:path' +import { createClient, type Row } from "@clickhouse/client"; +import Fs from "node:fs"; +import { cwd } from "node:process"; +import Path from "node:path"; -const client = createClient() -const tableName = 'insert_file_stream_csv' +const client = createClient(); +const tableName = "insert_file_stream_csv"; await client.command({ query: `DROP TABLE IF EXISTS ${tableName}`, -}) +}); await client.command({ query: ` CREATE TABLE ${tableName} @@ -15,33 +15,33 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); // contains data as 1,"foo","[1,2]"\n2,"bar","[3,4]"\n... -const filename = Path.resolve(cwd(), './node/resources/data.csv') -const fileStream = Fs.createReadStream(filename) +const filename = Path.resolve(cwd(), "./node/resources/data.csv"); +const fileStream = Fs.createReadStream(filename); await client.insert({ table: tableName, values: fileStream, - format: 'CSV', + format: "CSV", clickhouse_settings: { /** See also: https://clickhouse.com/docs/en/interfaces/formats#csv-format-settings. * You could specify these (and other settings) here. */ }, -}) +}); const rs = await client.query({ query: `SELECT * from ${tableName}`, - format: 'CSV', -}) + format: "CSV", +}); for await (const rows of rs.stream()) { // or just `rows.text()` // to consume the entire response at once rows.forEach((row: Row) => { - console.log(row.text) - }) + console.log(row.text); + }); } -await client.close() +await client.close(); diff --git a/examples/node/performance/insert_file_stream_ndjson.ts b/examples/node/performance/insert_file_stream_ndjson.ts index 1ec1a4df..c9ee615c 100644 --- a/examples/node/performance/insert_file_stream_ndjson.ts +++ b/examples/node/performance/insert_file_stream_ndjson.ts @@ -1,59 +1,59 @@ -import type { Row } from '@clickhouse/client' -import { createClient } from '@clickhouse/client' -import Fs from 'node:fs' -import { cwd } from 'node:process' -import Path from 'node:path' -import Readline from 'node:readline' -import { Readable } from 'node:stream' +import type { Row } from "@clickhouse/client"; +import { createClient } from "@clickhouse/client"; +import Fs from "node:fs"; +import { cwd } from "node:process"; +import Path from "node:path"; +import Readline from "node:readline"; +import { Readable } from "node:stream"; -const client = createClient() -const tableName = 'insert_file_stream_ndjson' +const client = createClient(); +const tableName = "insert_file_stream_ndjson"; await client.command({ query: `DROP TABLE IF EXISTS ${tableName}`, -}) +}); await client.command({ query: ` CREATE TABLE ${tableName} (id UInt64) ENGINE MergeTree() ORDER BY (id) `, -}) +}); // contains id as numbers in JSONCompactEachRow format ["0"]\n["0"]\n... // see also: NDJSON format -const filename = Path.resolve(cwd(), './node/resources/data.ndjson') +const filename = Path.resolve(cwd(), "./node/resources/data.ndjson"); // Read the file line by line and parse each line as JSON, then expose the // parsed rows as a Readable stream that the client can consume. const lines = Readline.createInterface({ input: Fs.createReadStream(filename), crlfDelay: Infinity, -}) +}); const fileStream = Readable.from( (async function* () { for await (const line of lines) { - if (line.length > 0) yield JSON.parse(line) as unknown[] + if (line.length > 0) yield JSON.parse(line) as unknown[]; } })(), -) +); await client.insert({ table: tableName, values: fileStream, - format: 'JSONCompactEachRow', -}) + format: "JSONCompactEachRow", +}); const rs = await client.query({ query: `SELECT * from ${tableName}`, - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); for await (const rows of rs.stream()) { // or just `rows.text()` / `rows.json()` // to consume the entire response at once rows.forEach((row: Row) => { - console.log(row.json()) - }) + console.log(row.json()); + }); } -await client.close() +await client.close(); diff --git a/examples/node/performance/insert_file_stream_parquet.ts b/examples/node/performance/insert_file_stream_parquet.ts index 628d1099..878ac590 100644 --- a/examples/node/performance/insert_file_stream_parquet.ts +++ b/examples/node/performance/insert_file_stream_parquet.ts @@ -1,14 +1,14 @@ -import { createClient, type Row } from '@clickhouse/client' -import Fs from 'node:fs' -import { cwd } from 'node:process' -import Path from 'node:path' +import { createClient, type Row } from "@clickhouse/client"; +import Fs from "node:fs"; +import { cwd } from "node:process"; +import Path from "node:path"; /** See also: https://clickhouse.com/docs/en/interfaces/formats#parquet-format-settings */ -const client = createClient() -const tableName = 'insert_file_stream_parquet' +const client = createClient(); +const tableName = "insert_file_stream_parquet"; await client.command({ query: `DROP TABLE IF EXISTS ${tableName}`, -}) +}); await client.command({ query: ` CREATE TABLE ${tableName} @@ -16,10 +16,10 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); -const filename = Path.resolve(cwd(), './node/resources/data.parquet') -const fileStream = Fs.createReadStream(filename) +const filename = Path.resolve(cwd(), "./node/resources/data.parquet"); +const fileStream = Fs.createReadStream(filename); /* @@ -38,24 +38,24 @@ const fileStream = Fs.createReadStream(filename) await client.insert({ table: tableName, values: fileStream, - format: 'Parquet', + format: "Parquet", clickhouse_settings: { /** See also https://clickhouse.com/docs/en/interfaces/formats#parquet-format-settings. * You could specify these (and other settings) here. */ }, -}) +}); const rs = await client.query({ query: `SELECT * from ${tableName}`, - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); for await (const rows of rs.stream()) { // or just `rows.json()` // to consume the entire response at once rows.forEach((row: Row) => { - console.log(row.json()) - }) + console.log(row.json()); + }); } -await client.close() +await client.close(); diff --git a/examples/node/performance/insert_from_select.ts b/examples/node/performance/insert_from_select.ts index 742c9241..dc19878c 100644 --- a/examples/node/performance/insert_from_select.ts +++ b/examples/node/performance/insert_from_select.ts @@ -2,10 +2,10 @@ // Demonstrates that `client.command` can run server-side data movement queries // (no client-side rows are sent), and that aggregate states are read back via // `finalizeAggregation`. Inspired by https://github.com/ClickHouse/clickhouse-js/issues/166 -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; -const tableName = 'insert_from_select' -const client = createClient() +const tableName = "insert_from_select"; +const client = createClient(); await client.command({ query: ` CREATE OR REPLACE TABLE ${tableName} @@ -13,15 +13,15 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); await client.command({ query: ` INSERT INTO ${tableName} SELECT '42', quantilesBFloat16State(0.5)(arrayJoin([toFloat32(10), toFloat32(20)]))`, -}) +}); const rows = await client.query({ query: `SELECT finalizeAggregation(data) AS result FROM ${tableName}`, - format: 'JSONEachRow', -}) -console.info(await rows.json()) -await client.close() + format: "JSONEachRow", +}); +console.info(await rows.json()); +await client.close(); diff --git a/examples/node/performance/insert_streaming_backpressure_simple.ts b/examples/node/performance/insert_streaming_backpressure_simple.ts index 32ccf46f..66fe7845 100644 --- a/examples/node/performance/insert_streaming_backpressure_simple.ts +++ b/examples/node/performance/insert_streaming_backpressure_simple.ts @@ -1,85 +1,85 @@ -import { createClient } from '@clickhouse/client' -import * as Stream from 'node:stream' +import { createClient } from "@clickhouse/client"; +import * as Stream from "node:stream"; interface DataRow { - id: number - name: string - value: number + id: number; + name: string; + value: number; } class SimpleBackpressureStream extends Stream.Readable { - #currentId = 1 - #maxRecords = 0 - #intervalId: NodeJS.Timeout | null = null - #isPaused = false + #currentId = 1; + #maxRecords = 0; + #intervalId: NodeJS.Timeout | null = null; + #isPaused = false; constructor(maxRecords: number) { - super({ objectMode: true, highWaterMark: 5 }) - this.#maxRecords = maxRecords + super({ objectMode: true, highWaterMark: 5 }); + this.#maxRecords = maxRecords; } _read() { if (this.#isPaused) { - console.log('Backpressure relieved - resuming data production') - this.#isPaused = false - this.#startProducing() + console.log("Backpressure relieved - resuming data production"); + this.#isPaused = false; + this.#startProducing(); } } #startProducing() { if (this.#intervalId || this.#currentId > this.#maxRecords) { - return + return; } this.#intervalId = setInterval(() => { if (this.#currentId > this.#maxRecords) { - console.log('All data produced, ending stream') - this.push(null) // End the stream - this.#stopProducing() - return + console.log("All data produced, ending stream"); + this.push(null); // End the stream + this.#stopProducing(); + return; } const data: DataRow = { id: this.#currentId++, name: `Name_${this.#currentId - 1}`, value: Math.random() * 1000, - } - const canContinue = this.push(data) + }; + const canContinue = this.push(data); if (!canContinue) { - console.log('Backpressure detected - pausing data production') - this.#isPaused = true - this.#stopProducing() + console.log("Backpressure detected - pausing data production"); + this.#isPaused = true; + this.#stopProducing(); } else if (this.#currentId % 500 === 0) { - console.log(`Produced ${this.#currentId - 1} records`) + console.log(`Produced ${this.#currentId - 1} records`); } - }, 1) + }, 1); } #stopProducing() { if (this.#intervalId) { - clearInterval(this.#intervalId) - this.#intervalId = null + clearInterval(this.#intervalId); + this.#intervalId = null; } } start() { - this.#startProducing() + this.#startProducing(); } _destroy(error: Error | null, callback: (error?: Error | null) => void) { - this.#stopProducing() - callback(error) + this.#stopProducing(); + callback(error); } } -const tableName = 'simple_streaming_demo' -const client = createClient() +const tableName = "simple_streaming_demo"; +const client = createClient(); // Setup table await client.command({ query: `DROP TABLE IF EXISTS ${tableName}`, -}) +}); await client.command({ query: ` @@ -92,44 +92,44 @@ await client.command({ ENGINE = MergeTree() ORDER BY id `, -}) +}); -const maxRecords = 10000 -console.log('Creating backpressure-aware data stream...') +const maxRecords = 10000; +console.log("Creating backpressure-aware data stream..."); -const dataStream = new SimpleBackpressureStream(maxRecords) +const dataStream = new SimpleBackpressureStream(maxRecords); try { - console.log('Starting streaming insert with backpressure demonstration...') + console.log("Starting streaming insert with backpressure demonstration..."); const insertPromise = client.insert({ table: tableName, values: dataStream, - format: 'JSONEachRow', + format: "JSONEachRow", clickhouse_settings: { // Use async inserts to handle streaming data more efficiently async_insert: 1, wait_for_async_insert: 1, - async_insert_max_data_size: '10485760', // 10MB + async_insert_max_data_size: "10485760", // 10MB async_insert_busy_timeout_ms: 1000, }, - }) + }); - setTimeout(() => dataStream.start(), 100) + setTimeout(() => dataStream.start(), 100); - await insertPromise + await insertPromise; - console.log('Insert completed successfully!') + console.log("Insert completed successfully!"); const result = await client.query({ query: `SELECT count() as total FROM ${tableName}`, - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); - const [{ total }] = await result.json<{ total: string }>() - console.log(`Total records inserted: ${total}`) + const [{ total }] = await result.json<{ total: string }>(); + console.log(`Total records inserted: ${total}`); } catch (error) { - console.error('Insert failed:', error) + console.error("Insert failed:", error); } finally { - await client.close() + await client.close(); } diff --git a/examples/node/performance/insert_streaming_with_backpressure.ts b/examples/node/performance/insert_streaming_with_backpressure.ts index d4fd039a..dcd873f7 100644 --- a/examples/node/performance/insert_streaming_with_backpressure.ts +++ b/examples/node/performance/insert_streaming_with_backpressure.ts @@ -1,21 +1,21 @@ -import { createClient, type Row } from '@clickhouse/client' -import * as Stream from 'node:stream' -import { EventEmitter } from 'node:events' +import { createClient, type Row } from "@clickhouse/client"; +import * as Stream from "node:stream"; +import { EventEmitter } from "node:events"; interface DataRow { - id: number - timestamp: Date - message: string - value: number + id: number; + timestamp: Date; + message: string; + value: number; } class BackpressureAwareDataProducer extends Stream.Readable { - #dataSource: EventEmitter - #streamPaused = false - #pendingData: DataRow[] = [] - #isDestroyed = false - #total = 0 - #shouldEndAfterDrain = false + #dataSource: EventEmitter; + #streamPaused = false; + #pendingData: DataRow[] = []; + #isDestroyed = false; + #total = 0; + #shouldEndAfterDrain = false; constructor(dataSource: EventEmitter, options?: Stream.ReadableOptions) { super({ @@ -24,39 +24,39 @@ class BackpressureAwareDataProducer extends Stream.Readable { // Limit buffering to prevent memory issues highWaterMark: 16, ...options, - }) + }); - this.#dataSource = dataSource - this.#setupDataSourceListeners() + this.#dataSource = dataSource; + this.#setupDataSourceListeners(); } #setupDataSourceListeners() { - this.#dataSource.on('data', this.#handleIncomingData.bind(this)) - this.#dataSource.on('end', this.#handleDataSourceEnd.bind(this)) - this.#dataSource.on('error', this.#handleDataSourceError.bind(this)) + this.#dataSource.on("data", this.#handleIncomingData.bind(this)); + this.#dataSource.on("end", this.#handleDataSourceEnd.bind(this)); + this.#dataSource.on("error", this.#handleDataSourceError.bind(this)); } #handleIncomingData(data: DataRow) { if (this.#isDestroyed) { - return + return; } if (this.#streamPaused) { - this.#pendingData.push(data) - return + this.#pendingData.push(data); + return; } // Try to push the data immediately if (!this.#pushData(data)) { // If push returns false, we're experiencing backpressure // Pause the data source and buffer subsequent data - this.#streamPaused = true + this.#streamPaused = true; } } #pushData(data: DataRow): boolean { if (this.#isDestroyed) { - return false + return false; } // Convert data to JSON object for ClickHouse @@ -65,47 +65,47 @@ class BackpressureAwareDataProducer extends Stream.Readable { timestamp: data.timestamp.toISOString(), message: data.message, value: data.value, - } + }; - const pushed = this.push(jsonData) + const pushed = this.push(jsonData); if (pushed) { - this.#total++ + this.#total++; if (this.#total % 1000 === 0) { - console.log(`Produced ${this.#total} rows`) + console.log(`Produced ${this.#total} rows`); } } - return pushed + return pushed; } #handleDataSourceEnd() { - console.log(`Data source ended. Total produced: ${this.#total} rows`) + console.log(`Data source ended. Total produced: ${this.#total} rows`); // If there's pending data, it will be flushed in _read() // before the final push(null) when the stream is ready if (this.#pendingData.length === 0 && !this.#streamPaused) { - this.push(null) + this.push(null); } else { // Mark that we should end after draining pending data - this.#shouldEndAfterDrain = true + this.#shouldEndAfterDrain = true; } } #handleDataSourceError(error: Error) { - console.error('Data source error:', error) - this.destroy(error) + console.error("Data source error:", error); + this.destroy(error); } // Called when the stream is ready to accept more data (backpressure resolved) _read() { if (this.#streamPaused && this.#pendingData.length > 0) { // Process buffered data when backpressure is resolved - this.#streamPaused = false + this.#streamPaused = false; // Push all pending data, but stop if we hit backpressure again while (this.#pendingData.length > 0 && !this.#streamPaused) { - const data = this.#pendingData.shift() + const data = this.#pendingData.shift(); if (!data || !this.#pushData(data)) { - this.#streamPaused = true - break + this.#streamPaused = true; + break; } } } @@ -116,63 +116,63 @@ class BackpressureAwareDataProducer extends Stream.Readable { this.#pendingData.length === 0 && !this.#streamPaused ) { - this.push(null) + this.push(null); } } _destroy(error: Error | null, callback: (error?: Error | null) => void) { - this.#isDestroyed = true - this.#dataSource.removeAllListeners('data') - this.#dataSource.removeAllListeners('end') - this.#dataSource.removeAllListeners('error') - callback(error) + this.#isDestroyed = true; + this.#dataSource.removeAllListeners("data"); + this.#dataSource.removeAllListeners("end"); + this.#dataSource.removeAllListeners("error"); + callback(error); } get total(): number { - return this.#total + return this.#total; } } // Simulated data source that generates data at varying rates class SimulatedDataSource extends EventEmitter { - #intervalHandle: NodeJS.Timeout | null = null - #burstModeIntervalHandle: NodeJS.Timeout | null = null - #isRunning = false - #total = 0 - #burstMode = false - #maxRows: number | null = null + #intervalHandle: NodeJS.Timeout | null = null; + #burstModeIntervalHandle: NodeJS.Timeout | null = null; + #isRunning = false; + #total = 0; + #burstMode = false; + #maxRows: number | null = null; constructor(maxRows: number | null = null) { - super() - this.#maxRows = maxRows + super(); + this.#maxRows = maxRows; } start() { - if (this.#isRunning) return + if (this.#isRunning) return; - this.#isRunning = true - this.#scheduleNextBatch() + this.#isRunning = true; + this.#scheduleNextBatch(); // Randomly switch between normal and burst modes this.#burstModeIntervalHandle = setInterval(() => { - this.#burstMode = !this.#burstMode - console.log(`Switched to ${this.#burstMode ? 'burst' : 'normal'} mode`) - }, 10000) + this.#burstMode = !this.#burstMode; + console.log(`Switched to ${this.#burstMode ? "burst" : "normal"} mode`); + }, 10000); } #scheduleNextBatch() { - if (!this.#isRunning) return + if (!this.#isRunning) return; // Variable delay to simulate real-world conditions - const delay = this.#burstMode ? 10 : Math.random() * 100 + 50 + const delay = this.#burstMode ? 10 : Math.random() * 100 + 50; const batchSize = this.#burstMode ? Math.floor(Math.random() * 100) + 50 - : Math.floor(Math.random() * 10) + 1 + : Math.floor(Math.random() * 10) + 1; this.#intervalHandle = setTimeout(() => { - this.#generateBatch(batchSize) - this.#scheduleNextBatch() - }, delay) + this.#generateBatch(batchSize); + this.#scheduleNextBatch(); + }, delay); } #generateBatch(size: number) { @@ -180,42 +180,42 @@ class SimulatedDataSource extends EventEmitter { // Stop generating if we've reached the limit if (this.#maxRows && this.#total >= this.#maxRows) { // Schedule stop for next tick to avoid stopping mid-batch - setImmediate(() => this.stop()) - return + setImmediate(() => this.stop()); + return; } - const id = this.#total++ + const id = this.#total++; const data: DataRow = { id, timestamp: new Date(), message: `Message ${id} - ${Math.random().toString(36).substring(7)}`, value: Math.random() * 1000, - } + }; - this.emit('data', data) + this.emit("data", data); } } stop() { - this.#isRunning = false + this.#isRunning = false; if (this.#intervalHandle) { - clearTimeout(this.#intervalHandle) - this.#intervalHandle = null + clearTimeout(this.#intervalHandle); + this.#intervalHandle = null; } if (this.#burstModeIntervalHandle) { - clearInterval(this.#burstModeIntervalHandle) - this.#burstModeIntervalHandle = null + clearInterval(this.#burstModeIntervalHandle); + this.#burstModeIntervalHandle = null; } // Emit 'end' on next tick to ensure all 'data' events are processed first - process.nextTick(() => this.emit('end')) + process.nextTick(() => this.emit("end")); } get total(): number { - return this.#total + return this.#total; } } -const tableName = 'streaming_backpressure_demo' +const tableName = "streaming_backpressure_demo"; const client = createClient({ // Configure client for high-throughput scenarios max_open_connections: 5, @@ -223,13 +223,13 @@ const client = createClient({ request: true, response: true, }, -}) +}); -console.log('Setting up table...') +console.log("Setting up table..."); await client.command({ query: `DROP TABLE IF EXISTS ${tableName}`, -}) +}); await client.command({ query: ` @@ -243,63 +243,63 @@ await client.command({ ENGINE = MergeTree() ORDER BY (id, timestamp) `, -}) +}); // Create data source and producer // For CI: limit the total rows generated based on runtime duration -const maxRows = 5000 // in ~80 seconds -const dataSource = new SimulatedDataSource(maxRows) -const dataProducer = new BackpressureAwareDataProducer(dataSource) +const maxRows = 5000; // in ~80 seconds +const dataSource = new SimulatedDataSource(maxRows); +const dataProducer = new BackpressureAwareDataProducer(dataSource); // Start generating data -console.log('Starting data generation...') -dataSource.start() +console.log("Starting data generation..."); +dataSource.start(); // Handle graceful shutdown -let isCleaningUp = false +let isCleaningUp = false; const cleanup = async () => { - if (isCleaningUp) return - isCleaningUp = true + if (isCleaningUp) return; + isCleaningUp = true; - console.log('\nShutting down gracefully...') - dataSource.stop() + console.log("\nShutting down gracefully..."); + dataSource.stop(); // Wait a bit for any remaining data to be processed - await new Promise((resolve) => setTimeout(resolve, 1000)) + await new Promise((resolve) => setTimeout(resolve, 1000)); - console.log(`Final stats:`) - console.log(`- Generated: ${dataSource.total} rows`) - console.log(`- Produced: ${dataProducer.total} rows`) + console.log(`Final stats:`); + console.log(`- Generated: ${dataSource.total} rows`); + console.log(`- Produced: ${dataProducer.total} rows`); - await client.close() - process.exit(0) -} + await client.close(); + process.exit(0); +}; -process.on('SIGINT', cleanup) -process.on('SIGTERM', cleanup) +process.on("SIGINT", cleanup); +process.on("SIGTERM", cleanup); try { - console.log('Starting streaming insert...') - const startTime = Date.now() + console.log("Starting streaming insert..."); + const startTime = Date.now(); await client.insert({ table: tableName, values: dataProducer, - format: 'JSONEachRow', + format: "JSONEachRow", clickhouse_settings: { // Optimize for streaming inserts async_insert: 1, wait_for_async_insert: 1, - async_insert_max_data_size: '10485760', // 10MB + async_insert_max_data_size: "10485760", // 10MB async_insert_busy_timeout_ms: 1000, }, - }) + }); - const duration = Date.now() - startTime + const duration = Date.now() - startTime; - console.log(`\nInsert completed in ${duration}ms`) - console.log(`Inserted ${dataProducer.total} rows`) - console.log('\nVerifying inserted data...') + console.log(`\nInsert completed in ${duration}ms`); + console.log(`Inserted ${dataProducer.total} rows`); + console.log("\nVerifying inserted data..."); const result = await client.query({ query: ` @@ -310,31 +310,31 @@ try { avg(value) as avg_value FROM ${tableName} `, - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); const stats = await result.json<{ - total_rows: string - min_timestamp: string - max_timestamp: string - avg_value: string - }>() + total_rows: string; + min_timestamp: string; + max_timestamp: string; + avg_value: string; + }>(); - console.log('Verification results:', stats[0]) + console.log("Verification results:", stats[0]); const sampleResult = await client.query({ query: `SELECT * FROM ${tableName} ORDER BY id LIMIT 5`, - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); - console.log('\nSample data:') + console.log("\nSample data:"); for await (const rows of sampleResult.stream()) { rows.forEach((row: Row) => { - console.log(row.json()) - }) + console.log(row.json()); + }); } } catch (error) { - console.error('Insert failed:', error) + console.error("Insert failed:", error); } finally { - await cleanup() + await cleanup(); } diff --git a/examples/node/performance/select_json_each_row_with_progress.ts b/examples/node/performance/select_json_each_row_with_progress.ts index 00139ed6..f9cf3308 100644 --- a/examples/node/performance/select_json_each_row_with_progress.ts +++ b/examples/node/performance/select_json_each_row_with_progress.ts @@ -5,25 +5,25 @@ import { isException, isRow, parseError, -} from '@clickhouse/client' +} from "@clickhouse/client"; /** A few use cases of the `JSONEachRowWithProgress` format with ClickHouse and the Node.js/TypeScript client. * Here, the ResultSet infers the final row type as `{ row: T } | ProgressRow | SpecialEventRow`. */ -const client = createClient() +const client = createClient(); const selectResultSet = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 3', - format: 'JSONEachRowWithProgress', + query: "SELECT number FROM system.numbers LIMIT 3", + format: "JSONEachRowWithProgress", clickhouse_settings: { // in this example, we reduce the block size to 1 to see progress rows more frequently - max_block_size: '1', + max_block_size: "1", }, -}) +}); await processResultSet<{ number: string }>( - 'A simple select query', + "A simple select query", selectResultSet, -) -printLine() +); +printLine(); const aggregationResultSet = await client.query({ query: ` @@ -38,80 +38,80 @@ const aggregationResultSet = await client.query({ ORDER BY ALL LIMIT 10 `, - format: 'JSONEachRowWithProgress', + format: "JSONEachRowWithProgress", clickhouse_settings: { // enables 'rows_before_aggregation' special event row rows_before_aggregation: 1, // enables 'min' and 'max' special event rows extremes: 1, }, -}) +}); await processResultSet<{ k: number; c: string; s: string }>( - 'Aggregation result set with all kinds of special events', + "Aggregation result set with all kinds of special events", aggregationResultSet, -) -printLine() +); +printLine(); const exceptionResultSet = await client.query({ query: `SELECT number, throwIf(number = 3, 'boom') AS foo FROM system.numbers`, - format: 'JSONEachRowWithProgress', + format: "JSONEachRowWithProgress", clickhouse_settings: { // in this example, we reduce the block size to 1 to see progress rows more frequently - max_block_size: '1', + max_block_size: "1", }, -}) +}); try { await processResultSet<{ number: string; foo: 0 }>( - 'An exception in the middle of a stream', + "An exception in the middle of a stream", exceptionResultSet, - ) + ); } catch (err) { - console.error('Caught an error while processing the result set:', err) + console.error("Caught an error while processing the result set:", err); } -await client.close() +await client.close(); async function processResultSet( name: string, - rs: ResultSet<'JSONEachRowWithProgress'>, + rs: ResultSet<"JSONEachRowWithProgress">, ) { - console.log('###', name) - printLine() + console.log("###", name); + printLine(); - let totalRows = 0 - let totalProgressRows = 0 - let totalSpecialEventRows = 0 + let totalRows = 0; + let totalProgressRows = 0; + let totalSpecialEventRows = 0; - const stream = rs.stream() + const stream = rs.stream(); for await (const rows of stream) { for (const row of rows) { - const decodedRow = row.json() + const decodedRow = row.json(); if (isProgressRow(decodedRow)) { - console.log('Got a progress row:', decodedRow) - totalProgressRows++ + console.log("Got a progress row:", decodedRow); + totalProgressRows++; } else if (isRow(decodedRow)) { - totalRows++ - console.log('Got a row:', decodedRow) + totalRows++; + console.log("Got a row:", decodedRow); } else if (isException(decodedRow)) { console.error( - 'Got an exception row:', + "Got an exception row:", decodedRow, - 'which can be parsed as a ClickHouseError instance:\n', + "which can be parsed as a ClickHouseError instance:\n", parseError(decodedRow.exception), - ) + ); } else { - totalSpecialEventRows++ - console.log('Got a special event row:', decodedRow) + totalSpecialEventRows++; + console.log("Got a special event row:", decodedRow); } } } - printLine() - console.log('Total rows:', totalRows) - console.log('Total progress rows:', totalProgressRows) - console.log('Total special event rows:', totalSpecialEventRows) + printLine(); + console.log("Total rows:", totalRows); + console.log("Total progress rows:", totalProgressRows); + console.log("Total special event rows:", totalSpecialEventRows); } function printLine() { - console.log('-------------') + console.log("-------------"); } diff --git a/examples/node/performance/select_parquet_as_file.ts b/examples/node/performance/select_parquet_as_file.ts index 5ae7051e..b497ef8a 100644 --- a/examples/node/performance/select_parquet_as_file.ts +++ b/examples/node/performance/select_parquet_as_file.ts @@ -1,9 +1,9 @@ -import { createClient } from '@clickhouse/client' -import Fs from 'node:fs' -import { cwd } from 'node:process' -import Path from 'node:path' +import { createClient } from "@clickhouse/client"; +import Fs from "node:fs"; +import { cwd } from "node:process"; +import Path from "node:path"; -const client = createClient() +const client = createClient(); const { stream } = await client.exec({ query: `SELECT * from system.numbers LIMIT 10 FORMAT Parquet`, @@ -11,14 +11,14 @@ const { stream } = await client.exec({ /** See also https://clickhouse.com/docs/en/interfaces/formats#parquet-format-settings. * You could specify these (and other settings) here. */ }, -}) +}); -const filename = Path.resolve(cwd(), './node/out.parquet') -const writeStream = Fs.createWriteStream(filename) -stream.pipe(writeStream) +const filename = Path.resolve(cwd(), "./node/out.parquet"); +const writeStream = Fs.createWriteStream(filename); +stream.pipe(writeStream); await new Promise((resolve) => { - stream.on('end', resolve) -}) + stream.on("end", resolve); +}); /* @@ -41,4 +41,4 @@ await new Promise((resolve) => { */ -await client.close() +await client.close(); diff --git a/examples/node/performance/select_streaming_json_each_row.ts b/examples/node/performance/select_streaming_json_each_row.ts index ea276c9d..0103dfcd 100644 --- a/examples/node/performance/select_streaming_json_each_row.ts +++ b/examples/node/performance/select_streaming_json_each_row.ts @@ -1,4 +1,4 @@ -import { createClient, type Row } from '@clickhouse/client' +import { createClient, type Row } from "@clickhouse/client"; /** * Can be used for consuming large datasets for reducing memory overhead, @@ -25,23 +25,23 @@ import { createClient, type Row } from '@clickhouse/client' * The client supports streaming JSON objects with JSONEachRow and other JSON*EachRow formats (see the list above); * it's just that ClickHouse JSON format and a few others are represented as a single object in the response and cannot be streamed by the client. */ -const client = createClient() +const client = createClient(); const rows = await client.query({ - query: 'SELECT number FROM system.numbers_mt LIMIT 5', - format: 'JSONEachRow', // or JSONCompactEachRow, JSONStringsEachRow, etc. -}) -const stream = rows.stream() -stream.on('data', (rows: Row[]) => { + query: "SELECT number FROM system.numbers_mt LIMIT 5", + format: "JSONEachRow", // or JSONCompactEachRow, JSONStringsEachRow, etc. +}); +const stream = rows.stream(); +stream.on("data", (rows: Row[]) => { rows.forEach((row: Row) => { - console.log(row.json()) // or `row.text` to avoid parsing JSON - }) -}) + console.log(row.json()); // or `row.text` to avoid parsing JSON + }); +}); await new Promise((resolve, reject) => { - stream.on('end', () => { - console.log('Completed!') - resolve(0) - }) - stream.on('error', reject) -}) -await client.close() -process.exit(0) + stream.on("end", () => { + console.log("Completed!"); + resolve(0); + }); + stream.on("error", reject); +}); +await client.close(); +process.exit(0); diff --git a/examples/node/performance/select_streaming_json_each_row_for_await.ts b/examples/node/performance/select_streaming_json_each_row_for_await.ts index db2f608c..09f36c80 100644 --- a/examples/node/performance/select_streaming_json_each_row_for_await.ts +++ b/examples/node/performance/select_streaming_json_each_row_for_await.ts @@ -1,4 +1,4 @@ -import { createClient, type Row } from '@clickhouse/client' +import { createClient, type Row } from "@clickhouse/client"; /** * Similar to `select_streaming_text_line_by_line.ts`, but using `for await const` syntax instead of `on(data)`. @@ -6,17 +6,17 @@ import { createClient, type Row } from '@clickhouse/client' * NB (Node.js platform): `for await const` has some overhead (up to 2 times worse) vs the old-school `on(data)` approach. * See the related Node.js issue: https://github.com/nodejs/node/issues/31979 */ -const client = createClient() +const client = createClient(); const rs = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 10', + query: "SELECT number FROM system.numbers LIMIT 10", // See all supported formats for streaming: // https://clickhouse.com/docs/en/integrations/language-clients/javascript#supported-data-formats - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); for await (const rows of rs.stream()) { rows.forEach((row: Row) => { - console.log(row.json()) - }) + console.log(row.json()); + }); } -console.log('Completed!') -await client.close() +console.log("Completed!"); +await client.close(); diff --git a/examples/node/performance/select_streaming_text_line_by_line.ts b/examples/node/performance/select_streaming_text_line_by_line.ts index f0bb06a2..43f99760 100644 --- a/examples/node/performance/select_streaming_text_line_by_line.ts +++ b/examples/node/performance/select_streaming_text_line_by_line.ts @@ -1,4 +1,4 @@ -import { createClient, type Row } from '@clickhouse/client' +import { createClient, type Row } from "@clickhouse/client"; /** * Can be used for consuming large datasets for reducing memory overhead, @@ -24,23 +24,23 @@ import { createClient, type Row } from '@clickhouse/client' * See other supported formats for streaming: * https://clickhouse.com/docs/en/integrations/language-clients/javascript#supported-data-formats */ -const client = createClient() +const client = createClient(); const rows = await client.query({ - query: 'SELECT number, number + 1 FROM system.numbers_mt LIMIT 5', - format: 'CSV', // or TabSeparated, CustomSeparated, etc. -}) -const stream = rows.stream() -stream.on('data', (rows: Row[]) => { + query: "SELECT number, number + 1 FROM system.numbers_mt LIMIT 5", + format: "CSV", // or TabSeparated, CustomSeparated, etc. +}); +const stream = rows.stream(); +stream.on("data", (rows: Row[]) => { rows.forEach((row: Row) => { - console.log(row.text) - }) -}) + console.log(row.text); + }); +}); await new Promise((resolve, reject) => { - stream.on('end', () => { - console.log('Completed!') - resolve(0) - }) - stream.on('error', reject) -}) -await client.close() -process.exit(0) + stream.on("end", () => { + console.log("Completed!"); + resolve(0); + }); + stream.on("error", reject); +}); +await client.close(); +process.exit(0); diff --git a/examples/node/performance/stream_created_from_array_raw.ts b/examples/node/performance/stream_created_from_array_raw.ts index 7dd05407..30aa1c9c 100644 --- a/examples/node/performance/stream_created_from_array_raw.ts +++ b/examples/node/performance/stream_created_from_array_raw.ts @@ -1,16 +1,16 @@ -import { createClient } from '@clickhouse/client' -import Stream from 'node:stream' +import { createClient } from "@clickhouse/client"; +import Stream from "node:stream"; // If your application deals with a string input that can be considered as one of "raw" formats, such as CSV, TabSeparated, etc. // the client will require the input values to be converted into a Stream.Readable instance. // If your input is already a stream, then no conversion is needed; see insert_file_stream_csv.ts for an example. // See all supported formats for streaming: // https://clickhouse.com/docs/en/integrations/language-clients/javascript#supported-data-formats -const tableName = 'insert_stream_created_from_array_raw' -const client = createClient() +const tableName = "insert_stream_created_from_array_raw"; +const client = createClient(); await client.command({ query: `DROP TABLE IF EXISTS ${tableName}`, -}) +}); await client.command({ query: ` CREATE TABLE ${tableName} @@ -18,19 +18,19 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); await client.insert({ table: tableName, // structure should match the desired format, CSV in this example - values: Stream.Readable.from(['42,foo\n43,bar'], { + values: Stream.Readable.from(["42,foo\n43,bar"], { objectMode: false, // required for "raw" family formats }), - format: 'CSV', // or any other desired "raw" format -}) + format: "CSV", // or any other desired "raw" format +}); const rows = await client.query({ query: `SELECT * FROM ${tableName}`, - format: 'CSV', -}) + format: "CSV", +}); // Note that `.json()` call is not possible here due to "raw" format usage -console.info(await rows.text()) -await client.close() +console.info(await rows.text()); +await client.close(); diff --git a/examples/node/schema-and-deployments/create_table_cloud.ts b/examples/node/schema-and-deployments/create_table_cloud.ts index 81e57a3f..a443e10d 100644 --- a/examples/node/schema-and-deployments/create_table_cloud.ts +++ b/examples/node/schema-and-deployments/create_table_cloud.ts @@ -1,16 +1,16 @@ -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; -if (!process.env['CLICKHOUSE_CLOUD_URL']) { - throw new Error('CLICKHOUSE_CLOUD_URL environment variable is not set') +if (!process.env["CLICKHOUSE_CLOUD_URL"]) { + throw new Error("CLICKHOUSE_CLOUD_URL environment variable is not set"); } -if (!process.env['CLICKHOUSE_CLOUD_PASSWORD']) { - throw new Error('CLICKHOUSE_CLOUD_PASSWORD environment variable is not set') +if (!process.env["CLICKHOUSE_CLOUD_PASSWORD"]) { + throw new Error("CLICKHOUSE_CLOUD_PASSWORD environment variable is not set"); } const client = createClient({ - url: process.env['CLICKHOUSE_CLOUD_URL'], - password: process.env['CLICKHOUSE_CLOUD_PASSWORD'], -}) + url: process.env["CLICKHOUSE_CLOUD_URL"], + password: process.env["CLICKHOUSE_CLOUD_PASSWORD"], +}); // Note that ENGINE and ON CLUSTER clauses can be omitted entirely here. // ClickHouse cloud will automatically use ReplicatedMergeTree // with appropriate settings in this case. @@ -27,5 +27,5 @@ await client.command({ clickhouse_settings: { wait_end_of_query: 1, }, -}) -await client.close() +}); +await client.close(); diff --git a/examples/node/schema-and-deployments/create_table_on_premise_cluster.ts b/examples/node/schema-and-deployments/create_table_on_premise_cluster.ts index 48b7acd6..38b93d16 100644 --- a/examples/node/schema-and-deployments/create_table_on_premise_cluster.ts +++ b/examples/node/schema-and-deployments/create_table_on_premise_cluster.ts @@ -1,10 +1,10 @@ -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; // ClickHouse cluster - for example, as defined in our `docker-compose.yml` // (services `clickhouse1`/`clickhouse2` behind the `nginx` round-robin entrypoint on port 8127). const client = createClient({ - url: process.env['CLICKHOUSE_CLUSTER_URL'] ?? 'http://localhost:8127', -}) + url: process.env["CLICKHOUSE_CLUSTER_URL"] ?? "http://localhost:8127", +}); await client.command({ // Sample macro definitions are located in `.docker/clickhouse/cluster/serverN_config.xml` @@ -24,6 +24,6 @@ await client.command({ clickhouse_settings: { wait_end_of_query: 1, }, -}) +}); -await client.close() +await client.close(); diff --git a/examples/node/schema-and-deployments/create_table_single_node.ts b/examples/node/schema-and-deployments/create_table_single_node.ts index 128dfb2f..2cd90c01 100644 --- a/examples/node/schema-and-deployments/create_table_single_node.ts +++ b/examples/node/schema-and-deployments/create_table_single_node.ts @@ -1,7 +1,7 @@ -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; // A single ClickHouse node - for example, as in our `docker-compose.yml` -const client = createClient() +const client = createClient(); await client.command({ query: ` CREATE TABLE IF NOT EXISTS clickhouse_js_create_table_example @@ -9,5 +9,5 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) -await client.close() +}); +await client.close(); diff --git a/examples/node/schema-and-deployments/insert_ephemeral_columns.ts b/examples/node/schema-and-deployments/insert_ephemeral_columns.ts index 7cbc794a..2bc7b6c5 100644 --- a/examples/node/schema-and-deployments/insert_ephemeral_columns.ts +++ b/examples/node/schema-and-deployments/insert_ephemeral_columns.ts @@ -1,8 +1,8 @@ -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; // Ephemeral columns documentation: https://clickhouse.com/docs/en/sql-reference/statements/create/table#ephemeral -const tableName = 'insert_ephemeral_columns' -const client = createClient() +const tableName = "insert_ephemeral_columns"; +const client = createClient(); await client.command({ query: ` @@ -15,31 +15,31 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); await client.insert({ table: tableName, values: [ { - id: '42', - message_default: 'foo', + id: "42", + message_default: "foo", }, { - id: '144', - message_default: 'bar', + id: "144", + message_default: "bar", }, ], - format: 'JSONEachRow', + format: "JSONEachRow", // The name of the ephemeral column has to be specified here // to trigger the default values logic for the rest of the columns - columns: ['id', 'message_default'], -}) + columns: ["id", "message_default"], +}); const rows = await client.query({ query: `SELECT * FROM ${tableName}`, - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); -console.info(await rows.json()) -await client.close() +console.info(await rows.json()); +await client.close(); diff --git a/examples/node/schema-and-deployments/insert_exclude_columns.ts b/examples/node/schema-and-deployments/insert_exclude_columns.ts index 3219ae6c..11cb19a8 100644 --- a/examples/node/schema-and-deployments/insert_exclude_columns.ts +++ b/examples/node/schema-and-deployments/insert_exclude_columns.ts @@ -1,11 +1,11 @@ -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; /** * Excluding certain columns from the INSERT statement. * For the inverse (specifying the exact columns to insert into), see `insert_specific_columns.ts`. */ -const tableName = 'insert_exclude_columns' -const client = createClient() +const tableName = "insert_exclude_columns"; +const client = createClient(); await client.command({ query: ` @@ -14,32 +14,32 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); await client.insert({ table: tableName, - values: [{ message: 'bar' }], - format: 'JSONEachRow', + values: [{ message: "bar" }], + format: "JSONEachRow", // `id` column value for this row will be zero columns: { - except: ['id'], + except: ["id"], }, -}) +}); await client.insert({ table: tableName, values: [{ id: 144 }], - format: 'JSONEachRow', + format: "JSONEachRow", // `message` column value for this row will be an empty string columns: { - except: ['message'], + except: ["message"], }, -}) +}); const rows = await client.query({ query: `SELECT * FROM ${tableName} ORDER BY id, message DESC`, - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); -console.info(await rows.json()) -await client.close() +console.info(await rows.json()); +await client.close(); diff --git a/examples/node/schema-and-deployments/url_configuration.ts b/examples/node/schema-and-deployments/url_configuration.ts index 4ed39979..6467c1d6 100644 --- a/examples/node/schema-and-deployments/url_configuration.ts +++ b/examples/node/schema-and-deployments/url_configuration.ts @@ -1,4 +1,4 @@ -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; // URL configuration reference with all possible values. // @@ -13,38 +13,38 @@ import { createClient } from '@clickhouse/client' // +----------------------------------------------------------------------------------------------------------+ const url = // basic auth (username:password) + database will be extracted from the URL - 'https://bob:secret@my.host:8124/analytics?' + + "https://bob:secret@my.host:8124/analytics?" + [ // arbitrary string values - 'application=my_analytics_app', - 'session_id=random_session_id', - 'pathname=/my_proxy', // or `my_proxy`, without the leading slash + "application=my_analytics_app", + "session_id=random_session_id", + "pathname=/my_proxy", // or `my_proxy`, without the leading slash // a numeric value - 'request_timeout=60000', + "request_timeout=60000", // a numeric value; max_open_connections is expected to be at least 1 when set explicitly. - 'max_open_connections=10', + "max_open_connections=10", // boolean values can be set as 1/0 or true/false; // sets compression.request = true - 'compression_request=1', + "compression_request=1", // sets compression.response = false - 'compression_response=false', + "compression_response=false", // sets log.level = 'TRACE'; // allowed values: TRACE, DEBUG, INFO, WARN, ERROR, OFF. - 'log_level=TRACE', + "log_level=TRACE", // sets keep_alive.enabled = false - 'keep_alive_enabled=false', + "keep_alive_enabled=false", // (Node.js only) sets keep_alive.idle_socket_ttl = 1500 - 'keep_alive_idle_socket_ttl=1500', + "keep_alive_idle_socket_ttl=1500", // all values prefixed with clickhouse_setting_ will be added to clickhouse_settings // this will set clickhouse_settings.async_insert = 1 - 'clickhouse_setting_async_insert=1', + "clickhouse_setting_async_insert=1", // ch_ is a shorthand for clickhouse_setting_* parameters; works similarly. // this will set clickhouse_settings.wait_for_async_insert = 0 - 'ch_wait_for_async_insert=0', + "ch_wait_for_async_insert=0", // adds a custom HTTP header 'X-CLICKHOUSE-AUTH' with 'secret_header' value to client requests // arbitrary string value - 'http_header_X-CLICKHOUSE-AUTH=secret_header', - ].join('&') + "http_header_X-CLICKHOUSE-AUTH=secret_header", + ].join("&"); /* The URL above is an equivalent to the following configuration object: { @@ -76,6 +76,6 @@ const url = */ const client = createClient({ url, -}) +}); // your queries will go here... -await client.close() +await client.close(); diff --git a/examples/node/security/basic_tls.ts b/examples/node/security/basic_tls.ts index 82efc0f6..ab450dfa 100644 --- a/examples/node/security/basic_tls.ts +++ b/examples/node/security/basic_tls.ts @@ -1,17 +1,17 @@ -import { createClient } from '@clickhouse/client' -import fs from 'node:fs' +import { createClient } from "@clickhouse/client"; +import fs from "node:fs"; const client = createClient({ - url: 'https://server.clickhouseconnect.test:8443', + url: "https://server.clickhouseconnect.test:8443", tls: { ca_cert: fs.readFileSync( - '../.docker/clickhouse/single_node_tls/certificates/ca.crt', + "../.docker/clickhouse/single_node_tls/certificates/ca.crt", ), }, -}) +}); const rows = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 2', - format: 'JSONEachRow', -}) -console.info(await rows.json()) -await client.close() + query: "SELECT number FROM system.numbers LIMIT 2", + format: "JSONEachRow", +}); +console.info(await rows.json()); +await client.close(); diff --git a/examples/node/security/mutual_tls.ts b/examples/node/security/mutual_tls.ts index 2fd69368..6098dd0f 100644 --- a/examples/node/security/mutual_tls.ts +++ b/examples/node/security/mutual_tls.ts @@ -1,19 +1,19 @@ -import { createClient } from '@clickhouse/client' -import fs from 'node:fs' +import { createClient } from "@clickhouse/client"; +import fs from "node:fs"; -const certsPath = '../.docker/clickhouse/single_node_tls/certificates' +const certsPath = "../.docker/clickhouse/single_node_tls/certificates"; const client = createClient({ - url: 'https://server.clickhouseconnect.test:8443', - username: 'cert_user', + url: "https://server.clickhouseconnect.test:8443", + username: "cert_user", tls: { ca_cert: fs.readFileSync(`${certsPath}/ca.crt`), cert: fs.readFileSync(`${certsPath}/client.crt`), key: fs.readFileSync(`${certsPath}/client.key`), }, -}) +}); const rows = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 2', - format: 'JSONEachRow', -}) -console.info(await rows.json()) -await client.close() + query: "SELECT number FROM system.numbers LIMIT 2", + format: "JSONEachRow", +}); +console.info(await rows.json()); +await client.close(); diff --git a/examples/node/security/query_with_parameter_binding.ts b/examples/node/security/query_with_parameter_binding.ts index 0c172fee..a2def870 100644 --- a/examples/node/security/query_with_parameter_binding.ts +++ b/examples/node/security/query_with_parameter_binding.ts @@ -1,4 +1,4 @@ -import { createClient, TupleParam } from '@clickhouse/client' +import { createClient, TupleParam } from "@clickhouse/client"; /** * Binding query parameters of various data types. @@ -6,7 +6,7 @@ import { createClient, TupleParam } from '@clickhouse/client' * For binding parameters that contain special characters (tabs, newlines, quotes, etc.), * see `query_with_parameter_binding_special_chars.ts`. */ -const client = createClient() +const client = createClient(); const resultSet = await client.query({ query: ` SELECT @@ -26,31 +26,31 @@ const resultSet = await client.query({ {var_ipv4: IPv4} AS var_ipv4, {var_null: Nullable(String)} AS var_null `, - format: 'JSONEachRow', + format: "JSONEachRow", query_params: { var_int: 10, - var_float: '10.557', + var_float: "10.557", var_str: 20, var_array: [42, 144], - var_tuple: new TupleParam([42, 'foo']), + var_tuple: new TupleParam([42, "foo"]), var_map: new Map([ - [42, ['a', 'b']], - [144, ['c', 'd']], + [42, ["a", "b"]], + [144, ["c", "d"]], ]), - var_date: '2022-01-01', - var_datetime: '2022-01-01 12:34:56', // or a Date object - var_datetime64_3: '2022-01-01 12:34:56.789', // or a Date object + var_date: "2022-01-01", + var_datetime: "2022-01-01 12:34:56", // or a Date object + var_datetime64_3: "2022-01-01 12:34:56.789", // or a Date object // NB: Date object with DateTime64(9) is still possible, // but there will be precision loss, as JS Date has only milliseconds. - var_datetime64_9: '2022-01-01 12:34:56.123456789', + var_datetime64_9: "2022-01-01 12:34:56.123456789", // It is also possible to provide DateTime64 as a timestamp. - var_datetime64_9_ts: '1651490755.123456789', - var_decimal: '123.45', - var_uuid: '01234567-89ab-cdef-0123-456789abcdef', - var_ipv4: '192.168.0.1', + var_datetime64_9_ts: "1651490755.123456789", + var_decimal: "123.45", + var_uuid: "01234567-89ab-cdef-0123-456789abcdef", + var_ipv4: "192.168.0.1", var_null: null, }, -}) -console.info('Result (different data types):', await resultSet.json()) +}); +console.info("Result (different data types):", await resultSet.json()); -await client.close() +await client.close(); diff --git a/examples/node/security/query_with_parameter_binding_special_chars.ts b/examples/node/security/query_with_parameter_binding_special_chars.ts index 5228d9cb..960bd0e3 100644 --- a/examples/node/security/query_with_parameter_binding_special_chars.ts +++ b/examples/node/security/query_with_parameter_binding_special_chars.ts @@ -1,4 +1,4 @@ -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; /** * Binding query parameters that contain special characters (tabs, newlines, quotes, backslashes, etc.). @@ -6,7 +6,7 @@ import { createClient } from '@clickhouse/client' * * For an overview of binding regular values of various data types, see `query_with_parameter_binding.ts`. */ -const client = createClient() +const client = createClient(); const resultSet = await client.query({ query: ` @@ -16,17 +16,17 @@ const resultSet = await client.query({ 'foo_\r_bar' = {carriage_return: String} AS has_carriage_return, 'foo_\\'_bar' = {single_quote: String} AS has_single_quote, 'foo_\\_bar' = {backslash: String} AS has_backslash`, - format: 'JSONEachRow', + format: "JSONEachRow", query_params: { - tab: 'foo_\t_bar', - newline: 'foo_\n_bar', - carriage_return: 'foo_\r_bar', + tab: "foo_\t_bar", + newline: "foo_\n_bar", + carriage_return: "foo_\r_bar", single_quote: "foo_'_bar", - backslash: 'foo_\\_bar', + backslash: "foo_\\_bar", }, -}) +}); // Should return all 1, as query params will match the strings in the SELECT. -console.info('Result (special characters):', await resultSet.json()) +console.info("Result (special characters):", await resultSet.json()); -await client.close() +await client.close(); diff --git a/examples/node/security/read_only_user.ts b/examples/node/security/read_only_user.ts index fe2983ee..0552b903 100644 --- a/examples/node/security/read_only_user.ts +++ b/examples/node/security/read_only_user.ts @@ -1,15 +1,15 @@ -import { createClient } from '@clickhouse/client' -import { randomUUID } from 'node:crypto' +import { createClient } from "@clickhouse/client"; +import { randomUUID } from "node:crypto"; /** * An illustration of limitations and client-specific settings for users created in `READONLY = 1` mode. */ -const defaultClient = createClient() +const defaultClient = createClient(); // using the default (non-read-only) user to create a read-only one for the purposes of the example -const guid = randomUUID().replace(/-/g, '') -const readOnlyUsername = `clickhouse_js_examples_readonly_user_${guid}` -const readOnlyPassword = `${guid}_pwd` +const guid = randomUUID().replace(/-/g, ""); +const readOnlyUsername = `clickhouse_js_examples_readonly_user_${guid}`; +const readOnlyPassword = `${guid}_pwd`; const commands = [ ` CREATE USER ${readOnlyUsername} @@ -22,22 +22,22 @@ const commands = [ ON default.* TO ${readOnlyUsername} `, -] +]; for (const query of commands) { await defaultClient.command({ query, clickhouse_settings: { wait_end_of_query: 1, }, - }) + }); } console.log( `Created user ${readOnlyUsername} with restricted access to the system database`, -) -printSeparator() +); +printSeparator(); // and a test table with some data in there -const testTableName = 'clickhouse_js_examples_readonly_user_test_data' +const testTableName = "clickhouse_js_examples_readonly_user_test_data"; await defaultClient.command({ query: ` CREATE OR REPLACE TABLE ${testTableName} @@ -48,114 +48,114 @@ await defaultClient.command({ clickhouse_settings: { wait_end_of_query: 1, }, -}) +}); await defaultClient.insert({ table: testTableName, values: [ - { id: 12, name: 'foo' }, - { id: 42, name: 'bar' }, + { id: 12, name: "foo" }, + { id: 42, name: "bar" }, ], - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); // Read-only user let readOnlyUserClient = createClient({ username: readOnlyUsername, password: readOnlyPassword, -}) +}); // read-only user cannot insert the data into the table await readOnlyUserClient .insert({ table: testTableName, values: [ - { id: 12, name: 'foo' }, - { id: 42, name: 'bar' }, + { id: 12, name: "foo" }, + { id: 42, name: "bar" }, ], - format: 'JSONEachRow', + format: "JSONEachRow", }) .catch((err) => { console.error( - '[Expected error] Readonly user cannot insert the data into the table. Cause:\n', + "[Expected error] Readonly user cannot insert the data into the table. Cause:\n", err, - ) - }) -printSeparator() + ); + }); +printSeparator(); // ... cannot query from system.users because no grant (system.numbers will still work, though) await readOnlyUserClient .query({ - query: 'SELECT * FROM system.users LIMIT 5', - format: 'JSONEachRow', + query: "SELECT * FROM system.users LIMIT 5", + format: "JSONEachRow", }) .catch((err) => { console.error( - '[Expected error] Cannot query system.users cause it was not granted. Cause:\n', + "[Expected error] Cannot query system.users cause it was not granted. Cause:\n", err, - ) - }) -printSeparator() + ); + }); +printSeparator(); // ... can query the test table since it is granted const rs = await readOnlyUserClient.query({ query: `SELECT * FROM ${testTableName}`, - format: 'JSONEachRow', -}) -console.log('Select result:', await rs.json()) -printSeparator() + format: "JSONEachRow", +}); +console.log("Select result:", await rs.json()); +printSeparator(); // ... cannot use ClickHouse settings -await readOnlyUserClient.close() +await readOnlyUserClient.close(); readOnlyUserClient = createClient({ username: readOnlyUsername, password: readOnlyPassword, clickhouse_settings: { send_progress_in_http_headers: 1, }, -}) +}); await readOnlyUserClient .query({ query: `SELECT * FROM ${testTableName}`, - format: 'JSONEachRow', + format: "JSONEachRow", }) .catch((err) => { console.error( `[Expected error] Cannot modify 'send_progress_in_http_headers' setting in readonly mode. Cause:\n`, err, - ) - }) -printSeparator() + ); + }); +printSeparator(); // ... cannot use response compression. Request compression is still allowed. -await readOnlyUserClient.close() +await readOnlyUserClient.close(); readOnlyUserClient = createClient({ username: readOnlyUsername, password: readOnlyPassword, compression: { response: true, }, -}) +}); await readOnlyUserClient .query({ query: `SELECT * FROM ${testTableName}`, - format: 'JSONEachRow', + format: "JSONEachRow", }) .catch((err) => { console.error( `[Expected error] Cannot enable compression setting in readonly mode. Cause:\n`, err, - ) - }) -printSeparator() + ); + }); +printSeparator(); -console.log('All done!') +console.log("All done!"); -await readOnlyUserClient.close() -await defaultClient.close() +await readOnlyUserClient.close(); +await defaultClient.close(); function printSeparator() { console.log( - '------------------------------------------------------------------------', - ) + "------------------------------------------------------------------------", + ); } diff --git a/examples/node/security/role.ts b/examples/node/security/role.ts index 78f56f70..1c44bb28 100644 --- a/examples/node/security/role.ts +++ b/examples/node/security/role.ts @@ -1,22 +1,22 @@ -import type { ClickHouseError } from '@clickhouse/client' -import { createClient } from '@clickhouse/client' +import type { ClickHouseError } from "@clickhouse/client"; +import { createClient } from "@clickhouse/client"; /** * An example of specifying a role using query parameters * See https://clickhouse.com/docs/en/interfaces/http#setting-role-with-query-parameters */ -const format = 'JSONEachRow' -const username = 'clickhouse_js_role_user' -const password = 'role_user_password' -const table1 = 'clickhouse_js_role_table_1' -const table2 = 'clickhouse_js_role_table_2' +const format = "JSONEachRow"; +const username = "clickhouse_js_role_user"; +const password = "role_user_password"; +const table1 = "clickhouse_js_role_table_1"; +const table2 = "clickhouse_js_role_table_2"; // Create 2 tables, a role for each table allowing SELECT, and a user with access to those roles -const defaultClient = createClient() -await createOrReplaceUser(username, password) -const table1Role = await createTableAndGrantAccess(table1, username) -const table2Role = await createTableAndGrantAccess(table2, username) -await defaultClient.close() +const defaultClient = createClient(); +await createOrReplaceUser(username, password); +const table1Role = await createTableAndGrantAccess(table1, username); +const table2Role = await createTableAndGrantAccess(table2, username); +await defaultClient.close(); // Create a client using a role that only has permission to query table1 const client = createClient({ @@ -25,17 +25,17 @@ const client = createClient({ // This role will be applied to all the queries by default, // unless it is overridden in a specific method call role: table1Role, -}) +}); // Selecting from table1 is allowed using table1Role const resultSet1 = await client.query({ query: `select count(*) from ${table1}`, format, -}) +}); console.log( `Successfully queried from ${table1} using ${table1Role}. Result: `, await resultSet1.json(), -) +); // Selecting from table2 is not allowed using table1Role, // which is set by default in the client instance @@ -45,19 +45,19 @@ await client console.error( `Failed to query from ${table2}, as ${table1Role} does not have sufficient privileges. Expected error:`, e, - ) - }) + ); + }); // Override the client's role to table2Role, allowing a query to table2 const resultSet2 = await client.query({ query: `select count(*) from ${table2}`, role: table2Role, format, -}) +}); console.log( `Successfully queried from ${table2} using ${table2Role}. Result:`, await resultSet2.json(), -) +); // Selecting from table1 is no longer allowed, since table2Role is being used await client @@ -70,40 +70,40 @@ await client console.error( `Failed to query from ${table1}, as ${table2Role} does not have sufficient privileges. Expected error:`, e, - ) - }) + ); + }); // Multiple roles can be specified to allowed querying from either table const resultSet3 = await client.query({ query: `select count(*) from ${table1}`, role: [table1Role, table2Role], format, -}) +}); console.log( `Successfully queried from ${table1} using roles: [${table1Role}, ${table2Role}]. Result:`, await resultSet3.json(), -) +); const resultSet4 = await client.query({ query: `select count(*) from ${table2}`, role: [table1Role, table2Role], format, -}) +}); console.log( `Successfully queried from ${table2} using roles: [${table1Role}, ${table2Role}]. Result: `, await resultSet4.json(), -) +); -await client.close() +await client.close(); async function createOrReplaceUser(username: string, password: string) { await defaultClient.command({ query: `CREATE USER OR REPLACE ${username} IDENTIFIED WITH plaintext_password BY '${password}'`, - }) + }); } async function createTableAndGrantAccess(tableName: string, username: string) { - const role = `${tableName}_role` + const role = `${tableName}_role`; await defaultClient.command({ query: ` @@ -112,13 +112,13 @@ async function createTableAndGrantAccess(tableName: string, username: string) { ENGINE MergeTree() ORDER BY (id) `, - }) + }); - await defaultClient.command({ query: `CREATE ROLE OR REPLACE ${role}` }) + await defaultClient.command({ query: `CREATE ROLE OR REPLACE ${role}` }); await defaultClient.command({ query: `GRANT SELECT ON ${tableName} TO ${role}`, - }) - await defaultClient.command({ query: `GRANT ${role} TO ${username}` }) + }); + await defaultClient.command({ query: `GRANT ${role} TO ${username}` }); - return role + return role; } diff --git a/examples/node/troubleshooting/abort_request.ts b/examples/node/troubleshooting/abort_request.ts index 7227a665..ca3581c8 100644 --- a/examples/node/troubleshooting/abort_request.ts +++ b/examples/node/troubleshooting/abort_request.ts @@ -1,4 +1,4 @@ -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; /** * Cancelling a request in progress. By default, this does not cancel the query on the server, only the request itself. @@ -18,19 +18,19 @@ const client = createClient({ // https://clickhouse.com/docs/operations/settings/settings#cancel_http_readonly_queries_on_client_close cancel_http_readonly_queries_on_client_close: 1, }, -}) -const controller = new AbortController() +}); +const controller = new AbortController(); const selectPromise = client .query({ - query: 'SELECT sleep(3)', - format: 'CSV', + query: "SELECT sleep(3)", + format: "CSV", abort_signal: controller.signal, }) .catch((e: unknown) => { - console.error(e) - console.info('---------------------------------------------------') - console.info('Select was aborted, see above for the error details') - }) -controller.abort() -await selectPromise -await client.close() + console.error(e); + console.info("---------------------------------------------------"); + console.info("Select was aborted, see above for the error details"); + }); +controller.abort(); +await selectPromise; +await client.close(); diff --git a/examples/node/troubleshooting/cancel_query.ts b/examples/node/troubleshooting/cancel_query.ts index 5e240a34..2993f140 100644 --- a/examples/node/troubleshooting/cancel_query.ts +++ b/examples/node/troubleshooting/cancel_query.ts @@ -1,29 +1,29 @@ -import { createClient, ClickHouseError } from '@clickhouse/client' -import * as crypto from 'node:crypto' +import { createClient, ClickHouseError } from "@clickhouse/client"; +import * as crypto from "node:crypto"; /** * An example of cancelling a long-running query on the server side. * See https://clickhouse.com/docs/en/sql-reference/statements/kill */ -const client = createClient() -const query_id = crypto.randomUUID() +const client = createClient(); +const query_id = crypto.randomUUID(); // Assuming a long-running query on the server. This promise is not awaited. const selectPromise = client .query({ - query: 'SELECT * FROM system.numbers', // it will never end, unless it is cancelled. - format: 'JSONEachRow', + query: "SELECT * FROM system.numbers", // it will never end, unless it is cancelled. + format: "JSONEachRow", query_id, // required in this case; should be unique. }) .catch((err: unknown) => { // An overview of possible error codes is available in the `system.errors` ClickHouse table. // In this example, the expected error code is 394 (QUERY_WAS_CANCELLED). - if (err instanceof ClickHouseError && err.code === '394') { - console.error('Got an expected ClickHouse error:', err) + if (err instanceof ClickHouseError && err.code === "394") { + console.error("Got an expected ClickHouse error:", err); } else { - console.error('Unexpected error', err) + console.error("Unexpected error", err); } - }) + }); // Similarly, a mutation can be cancelled. // See also: https://clickhouse.com/docs/en/sql-reference/statements/kill#kill-mutation @@ -32,8 +32,8 @@ await client.command({ clickhouse_settings: { wait_end_of_query: 1, }, -}) +}); // select promise will be rejected and print the error message -await selectPromise -await client.close() +await selectPromise; +await client.close(); diff --git a/examples/node/troubleshooting/custom_json_handling.ts b/examples/node/troubleshooting/custom_json_handling.ts index 61eeb9f0..f6a5ad82 100644 --- a/examples/node/troubleshooting/custom_json_handling.ts +++ b/examples/node/troubleshooting/custom_json_handling.ts @@ -1,4 +1,4 @@ -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; /** * Similar to `insert_js_dates.ts` but testing custom JSON handling @@ -13,36 +13,36 @@ const valueSerializer = (value: unknown): unknown => { // if you would have put this in the `replacer` parameter of JSON.stringify, (e.x: JSON.stringify(obj, replacerFn)) // it would have been an ISO string, but since we are serializing before `stringify`ing, // it will convert it before the `.toJSON()` method has been called - return value.getTime() + return value.getTime(); } - if (typeof value === 'bigint') { - return value.toString() + if (typeof value === "bigint") { + return value.toString(); } if (Array.isArray(value)) { - return value.map(valueSerializer) + return value.map(valueSerializer); } - if (typeof value === 'object' && value !== null) { + if (typeof value === "object" && value !== null) { return Object.fromEntries( Object.entries(value).map(([k, v]) => [k, valueSerializer(v)]), - ) + ); } - return value -} + return value; +}; -const tableName = 'inserts_custom_json_handling' +const tableName = "inserts_custom_json_handling"; const client = createClient({ json: { parse: JSON.parse, stringify: (obj: unknown) => JSON.stringify(valueSerializer(obj)), }, -}) +}); await client.command({ query: `DROP TABLE IF EXISTS ${tableName}`, -}) +}); await client.command({ query: ` CREATE TABLE ${tableName} @@ -50,7 +50,7 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); await client.insert({ table: tableName, values: [ @@ -59,11 +59,11 @@ await client.insert({ dt: new Date(), }, ], - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); const rows = await client.query({ query: `SELECT * FROM ${tableName}`, - format: 'JSONEachRow', -}) -console.info(await rows.json()) -await client.close() + format: "JSONEachRow", +}); +console.info(await rows.json()); +await client.close(); diff --git a/examples/node/troubleshooting/long_running_queries_cancel_request.ts b/examples/node/troubleshooting/long_running_queries_cancel_request.ts index 472864d3..3af3cdb5 100644 --- a/examples/node/troubleshooting/long_running_queries_cancel_request.ts +++ b/examples/node/troubleshooting/long_running_queries_cancel_request.ts @@ -1,6 +1,6 @@ -import { type ClickHouseClient, createClient } from '@clickhouse/client' -import * as crypto from 'node:crypto' -import { setTimeout as sleep } from 'node:timers/promises' +import { type ClickHouseClient, createClient } from "@clickhouse/client"; +import * as crypto from "node:crypto"; +import { setTimeout as sleep } from "node:timers/promises"; /** * If you execute a long-running query without data coming in from the client, @@ -21,9 +21,9 @@ import { setTimeout as sleep } from 'node:timers/promises' */ const client = createClient({ // we don't need any extra settings here. -}) +}); -const tableName = 'long_running_queries_cancel_request' +const tableName = "long_running_queries_cancel_request"; await client.command({ query: ` CREATE OR REPLACE TABLE ${tableName} ( @@ -33,14 +33,14 @@ await client.command({ ENGINE = MergeTree() ORDER BY id `, -}) +}); // Used to cancel the outgoing HTTP request (but not the query itself!). // See more on cancelling the HTTP requests in examples/abort_request.ts. -const abortController = new AbortController() +const abortController = new AbortController(); // IMPORTANT: you HAVE to generate the known query_id on the client side to be able to cancel the query later. -const queryId = crypto.randomUUID() +const queryId = crypto.randomUUID(); // Assuming that this is our long-long running insert. // IMPORTANT: do not wait for the promise to resolve yet, @@ -51,43 +51,43 @@ const longRunningQueryPromise = client.command({ SELECT number, sleepEachRow(1) FROM system.numbers LIMIT 10 `, clickhouse_settings: { - function_sleep_max_microseconds_per_block: '100000000', // 100 seconds per block + function_sleep_max_microseconds_per_block: "100000000", // 100 seconds per block }, abort_signal: abortController.signal, query_id: queryId, -}) +}); // Waiting until the query appears on the server in `system.query_log`. // Once it is there, we can safely cancel the outgoing HTTP request. for (let attempts = 1; ; attempts++) { if (await getQueryStatus(client, queryId)) { - break + break; } - await sleep(1000) + await sleep(1000); if (attempts >= 30) { throw new Error( - 'The query is not received by the server - assuming a failure.', - ) + "The query is not received by the server - assuming a failure.", + ); } } // Simulate the user cancelling the request. setTimeout(() => { - console.info('Aborting the HTTP request...') - abortController.abort() -}, 3000) + console.info("Aborting the HTTP request..."); + abortController.abort(); +}, 3000); try { - await longRunningQueryPromise + await longRunningQueryPromise; } catch (err) { - if (err instanceof Error && err.message.includes('abort')) { + if (err instanceof Error && err.message.includes("abort")) { console.info( - 'The request was aborted, but the query might still be running on the server.', - ) + "The request was aborted, but the query might still be running on the server.", + ); } else { - console.error('Unexpected error occurred during long-running insert.') - await client.close() - throw err + console.error("Unexpected error occurred during long-running insert."); + await client.close(); + throw err; } } @@ -95,39 +95,39 @@ try { // that the query finished successfully and the data is inserted, // even though the client request was cancelled. for (let attempts = 1; ; attempts++) { - if ((await getQueryStatus(client, queryId)) === 'QueryFinish') { - break + if ((await getQueryStatus(client, queryId)) === "QueryFinish") { + break; } - await sleep(1000) + await sleep(1000); if (attempts >= 60) { throw new Error( - 'The query did not finish on the server - assuming a failure.', - ) + "The query did not finish on the server - assuming a failure.", + ); } } // Check the inserted data. const rows = await client.query({ query: `SELECT * FROM ${tableName}`, - format: 'JSONEachRow', -}) -console.info('Inserted data:', await rows.json()) + format: "JSONEachRow", +}); +console.info("Inserted data:", await rows.json()); // Make sure all the resources are released and the process can exit. -await client.close() +await client.close(); interface QueryLogInfo { type: - | 'QueryStart' - | 'QueryFinish' - | 'ExceptionBeforeStart' - | 'ExceptionWhileProcessing' + | "QueryStart" + | "QueryFinish" + | "ExceptionBeforeStart" + | "ExceptionWhileProcessing"; } async function getQueryStatus( client: ClickHouseClient, queryId: string, -): Promise { +): Promise { const resultSet = await client.query({ query: ` SELECT type @@ -136,12 +136,12 @@ async function getQueryStatus( ORDER BY event_time DESC LIMIT 1 `, - format: 'JSONEachRow', - }) - const result = await resultSet.json() - console.log(`[Query ${queryId}] getQueryStatus() result:`, result) + format: "JSONEachRow", + }); + const result = await resultSet.json(); + console.log(`[Query ${queryId}] getQueryStatus() result:`, result); if (result.length === 0) { - return null + return null; } - return result[0].type + return result[0].type; } diff --git a/examples/node/troubleshooting/long_running_queries_progress_headers.ts b/examples/node/troubleshooting/long_running_queries_progress_headers.ts index 3d3d77c4..fdf57860 100644 --- a/examples/node/troubleshooting/long_running_queries_progress_headers.ts +++ b/examples/node/troubleshooting/long_running_queries_progress_headers.ts @@ -1,4 +1,4 @@ -import { type ClickHouseClient, createClient } from '@clickhouse/client' +import { type ClickHouseClient, createClient } from "@clickhouse/client"; /** * If you execute a long-running query without data coming in from the client, @@ -17,7 +17,7 @@ import { type ClickHouseClient, createClient } from '@clickhouse/client' * @see https://clickhouse.com/docs/en/operations/settings/settings#send_progress_in_http_headers * @see https://clickhouse.com/docs/en/interfaces/http */ -const tableName = 'long_running_queries_progress_headers' +const tableName = "long_running_queries_progress_headers"; const client = createClient({ /* Here we assume that: @@ -37,10 +37,10 @@ const client = createClient({ // The interval of sending these progress headers. Here it is less than 120s, // which in this example is assumed to be the LB idle connection timeout. // As it is UInt64 (UInt64 max value > Number.MAX_SAFE_INTEGER), it should be passed as a string. - http_headers_progress_interval_ms: '110000', + http_headers_progress_interval_ms: "110000", }, -}) -await createTestTable(client, tableName) +}); +await createTestTable(client, tableName); // Assuming that this is our long-long running insert, // it should not fail because of LB and the client settings described above. @@ -49,13 +49,13 @@ await client.command({ INSERT INTO ${tableName} SELECT '42', 'foobar' `, -}) +}); const rows = await client.query({ query: `SELECT * FROM ${tableName}`, - format: 'JSONEachRow', -}) -console.info('Inserted data:', await rows.json()) -await client.close() + format: "JSONEachRow", +}); +console.info("Inserted data:", await rows.json()); +await client.close(); async function createTestTable(client: ClickHouseClient, tableName: string) { try { @@ -66,10 +66,10 @@ async function createTestTable(client: ClickHouseClient, tableName: string) { ENGINE MergeTree() ORDER BY (id) `, - }) + }); } catch (err) { - console.error(`Error while creating the table ${tableName}:`, err) - await client.close() - process.exit(1) + console.error(`Error while creating the table ${tableName}:`, err); + await client.close(); + process.exit(1); } } diff --git a/examples/node/troubleshooting/ping_non_existing_host.ts b/examples/node/troubleshooting/ping_non_existing_host.ts index e29dbe77..5612a4bd 100644 --- a/examples/node/troubleshooting/ping_non_existing_host.ts +++ b/examples/node/troubleshooting/ping_non_existing_host.ts @@ -1,5 +1,5 @@ -import type { PingResult } from '@clickhouse/client' -import { createClient } from '@clickhouse/client' +import type { PingResult } from "@clickhouse/client"; +import { createClient } from "@clickhouse/client"; /** * This example assumes that your local port 8100 is free. @@ -13,27 +13,27 @@ import { createClient } from '@clickhouse/client' * - `ping_timeout.ts` - ping that times out. */ const client = createClient({ - url: 'http://localhost:8100', // non-existing host + url: "http://localhost:8100", // non-existing host request_timeout: 50, // low request_timeout to speed up the example -}) +}); // Ping does not throw an error; instead, { success: false; error: Error } is returned. -const pingResult = await client.ping() +const pingResult = await client.ping(); if (hasConnectionRefusedError(pingResult)) { - console.info('[NonExistingHostPing] Ping connection refused, as expected') + console.info("[NonExistingHostPing] Ping connection refused, as expected"); } else { console.error( - '[NonExistingHostPing] Ping expected to fail with ECONNREFUSED, but got:', + "[NonExistingHostPing] Ping expected to fail with ECONNREFUSED, but got:", pingResult, - ) + ); } -await client.close() +await client.close(); function hasConnectionRefusedError( pingResult: PingResult, -): pingResult is PingResult & { error: { code: 'ECONNREFUSED' } } { +): pingResult is PingResult & { error: { code: "ECONNREFUSED" } } { return ( !pingResult.success && - 'code' in pingResult.error && - pingResult.error.code === 'ECONNREFUSED' - ) + "code" in pingResult.error && + pingResult.error.code === "ECONNREFUSED" + ); } diff --git a/examples/node/troubleshooting/ping_timeout.ts b/examples/node/troubleshooting/ping_timeout.ts index 5d7543e2..499ccfe0 100644 --- a/examples/node/troubleshooting/ping_timeout.ts +++ b/examples/node/troubleshooting/ping_timeout.ts @@ -1,6 +1,6 @@ -import type { PingResult } from '@clickhouse/client' -import { createClient } from '@clickhouse/client' -import http from 'node:http' +import type { PingResult } from "@clickhouse/client"; +import { createClient } from "@clickhouse/client"; +import http from "node:http"; /** * Node.js-only example. @@ -20,38 +20,38 @@ import http from 'node:http' * - `ping_existing_host.ts` - successful ping against an existing host. * - `ping_non_existing_host.ts` - ping against a host that does not exist. */ -const server = await startSlowHTTPServer() +const server = await startSlowHTTPServer(); const client = createClient({ - url: 'http://localhost:18123', + url: "http://localhost:18123", request_timeout: 50, // low request_timeout to speed up the example -}) +}); // Ping does not throw an error; instead, { success: false; error: Error } is returned. -const pingResult = await client.ping() -server.close() +const pingResult = await client.ping(); +server.close(); if (hasTimeoutError(pingResult)) { - console.info('[TimeoutPing] Ping timed out, as expected') + console.info("[TimeoutPing] Ping timed out, as expected"); } else { console.error( - '[TimeoutPing] Ping expected to fail with a timeout error, but got:', + "[TimeoutPing] Ping expected to fail with a timeout error, but got:", pingResult, - ) + ); } -await client.close() +await client.close(); // Wait until the server is actually listening before returning; // otherwise the ping below could race and yield ECONNREFUSED instead of a timeout. async function startSlowHTTPServer() { const server = http.createServer(async (_req, res) => { - await new Promise((resolve) => setTimeout(resolve, 100)) - res.write('Ok.') - return res.end() - }) + await new Promise((resolve) => setTimeout(resolve, 100)); + res.write("Ok."); + return res.end(); + }); await new Promise((resolve, reject) => { - server.once('listening', () => resolve()) - server.once('error', reject) - server.listen(18123) - }) - return server + server.once("listening", () => resolve()); + server.once("error", reject); + server.listen(18123); + }); + return server; } function hasTimeoutError( @@ -59,7 +59,7 @@ function hasTimeoutError( ): pingResult is PingResult & { error: Error } { return ( !pingResult.success && - 'message' in pingResult.error && - pingResult.error.message.includes('Timeout error') - ) + "message" in pingResult.error && + pingResult.error.message.includes("Timeout error") + ); } diff --git a/examples/node/troubleshooting/read_only_user.ts b/examples/node/troubleshooting/read_only_user.ts index fe2983ee..0552b903 100644 --- a/examples/node/troubleshooting/read_only_user.ts +++ b/examples/node/troubleshooting/read_only_user.ts @@ -1,15 +1,15 @@ -import { createClient } from '@clickhouse/client' -import { randomUUID } from 'node:crypto' +import { createClient } from "@clickhouse/client"; +import { randomUUID } from "node:crypto"; /** * An illustration of limitations and client-specific settings for users created in `READONLY = 1` mode. */ -const defaultClient = createClient() +const defaultClient = createClient(); // using the default (non-read-only) user to create a read-only one for the purposes of the example -const guid = randomUUID().replace(/-/g, '') -const readOnlyUsername = `clickhouse_js_examples_readonly_user_${guid}` -const readOnlyPassword = `${guid}_pwd` +const guid = randomUUID().replace(/-/g, ""); +const readOnlyUsername = `clickhouse_js_examples_readonly_user_${guid}`; +const readOnlyPassword = `${guid}_pwd`; const commands = [ ` CREATE USER ${readOnlyUsername} @@ -22,22 +22,22 @@ const commands = [ ON default.* TO ${readOnlyUsername} `, -] +]; for (const query of commands) { await defaultClient.command({ query, clickhouse_settings: { wait_end_of_query: 1, }, - }) + }); } console.log( `Created user ${readOnlyUsername} with restricted access to the system database`, -) -printSeparator() +); +printSeparator(); // and a test table with some data in there -const testTableName = 'clickhouse_js_examples_readonly_user_test_data' +const testTableName = "clickhouse_js_examples_readonly_user_test_data"; await defaultClient.command({ query: ` CREATE OR REPLACE TABLE ${testTableName} @@ -48,114 +48,114 @@ await defaultClient.command({ clickhouse_settings: { wait_end_of_query: 1, }, -}) +}); await defaultClient.insert({ table: testTableName, values: [ - { id: 12, name: 'foo' }, - { id: 42, name: 'bar' }, + { id: 12, name: "foo" }, + { id: 42, name: "bar" }, ], - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); // Read-only user let readOnlyUserClient = createClient({ username: readOnlyUsername, password: readOnlyPassword, -}) +}); // read-only user cannot insert the data into the table await readOnlyUserClient .insert({ table: testTableName, values: [ - { id: 12, name: 'foo' }, - { id: 42, name: 'bar' }, + { id: 12, name: "foo" }, + { id: 42, name: "bar" }, ], - format: 'JSONEachRow', + format: "JSONEachRow", }) .catch((err) => { console.error( - '[Expected error] Readonly user cannot insert the data into the table. Cause:\n', + "[Expected error] Readonly user cannot insert the data into the table. Cause:\n", err, - ) - }) -printSeparator() + ); + }); +printSeparator(); // ... cannot query from system.users because no grant (system.numbers will still work, though) await readOnlyUserClient .query({ - query: 'SELECT * FROM system.users LIMIT 5', - format: 'JSONEachRow', + query: "SELECT * FROM system.users LIMIT 5", + format: "JSONEachRow", }) .catch((err) => { console.error( - '[Expected error] Cannot query system.users cause it was not granted. Cause:\n', + "[Expected error] Cannot query system.users cause it was not granted. Cause:\n", err, - ) - }) -printSeparator() + ); + }); +printSeparator(); // ... can query the test table since it is granted const rs = await readOnlyUserClient.query({ query: `SELECT * FROM ${testTableName}`, - format: 'JSONEachRow', -}) -console.log('Select result:', await rs.json()) -printSeparator() + format: "JSONEachRow", +}); +console.log("Select result:", await rs.json()); +printSeparator(); // ... cannot use ClickHouse settings -await readOnlyUserClient.close() +await readOnlyUserClient.close(); readOnlyUserClient = createClient({ username: readOnlyUsername, password: readOnlyPassword, clickhouse_settings: { send_progress_in_http_headers: 1, }, -}) +}); await readOnlyUserClient .query({ query: `SELECT * FROM ${testTableName}`, - format: 'JSONEachRow', + format: "JSONEachRow", }) .catch((err) => { console.error( `[Expected error] Cannot modify 'send_progress_in_http_headers' setting in readonly mode. Cause:\n`, err, - ) - }) -printSeparator() + ); + }); +printSeparator(); // ... cannot use response compression. Request compression is still allowed. -await readOnlyUserClient.close() +await readOnlyUserClient.close(); readOnlyUserClient = createClient({ username: readOnlyUsername, password: readOnlyPassword, compression: { response: true, }, -}) +}); await readOnlyUserClient .query({ query: `SELECT * FROM ${testTableName}`, - format: 'JSONEachRow', + format: "JSONEachRow", }) .catch((err) => { console.error( `[Expected error] Cannot enable compression setting in readonly mode. Cause:\n`, err, - ) - }) -printSeparator() + ); + }); +printSeparator(); -console.log('All done!') +console.log("All done!"); -await readOnlyUserClient.close() -await defaultClient.close() +await readOnlyUserClient.close(); +await defaultClient.close(); function printSeparator() { console.log( - '------------------------------------------------------------------------', - ) + "------------------------------------------------------------------------", + ); } diff --git a/examples/node/vitest.config.ts b/examples/node/vitest.config.ts index 7c357795..8fefe7bb 100644 --- a/examples/node/vitest.config.ts +++ b/examples/node/vitest.config.ts @@ -1,41 +1,41 @@ -import { defineConfig } from 'vitest/config' +import { defineConfig } from "vitest/config"; export default defineConfig({ test: { - name: 'examples-node', - include: ['**/*.ts'], + name: "examples-node", + include: ["**/*.ts"], // Examples are intentionally duplicated across category folders so each // category is a self-contained "skill corpus". To keep CI runtime stable, // each example runs once from its primary location; secondary copies are // excluded below. Keep this list in sync with examples/README.md. exclude: [ - 'node_modules/**', - 'vitest.config.ts', - 'vitest.setup.ts', + "node_modules/**", + "vitest.config.ts", + "vitest.setup.ts", // Duplicates of `coding/` files - 'performance/async_insert.ts', - 'performance/insert_from_select.ts', - 'troubleshooting/ping_non_existing_host.ts', - 'troubleshooting/custom_json_handling.ts', - 'security/query_with_parameter_binding.ts', - 'security/query_with_parameter_binding_special_chars.ts', - 'schema-and-deployments/insert_ephemeral_columns.ts', - 'schema-and-deployments/insert_exclude_columns.ts', - 'schema-and-deployments/url_configuration.ts', + "performance/async_insert.ts", + "performance/insert_from_select.ts", + "troubleshooting/ping_non_existing_host.ts", + "troubleshooting/custom_json_handling.ts", + "security/query_with_parameter_binding.ts", + "security/query_with_parameter_binding_special_chars.ts", + "schema-and-deployments/insert_ephemeral_columns.ts", + "schema-and-deployments/insert_exclude_columns.ts", + "schema-and-deployments/url_configuration.ts", // Duplicate of `security/read_only_user.ts` - 'troubleshooting/read_only_user.ts', + "troubleshooting/read_only_user.ts", ], - setupFiles: ['vitest.setup.ts'], - pool: 'forks', + setupFiles: ["vitest.setup.ts"], + pool: "forks", testTimeout: 60_000, hookTimeout: 60_000, passWithNoTests: true, - reporters: ['verbose'], + reporters: ["verbose"], env: { - CLICKHOUSE_URL: process.env['CLICKHOUSE_URL'] ?? 'http://localhost:8123', - CLICKHOUSE_PASSWORD: process.env['CLICKHOUSE_PASSWORD'] ?? '', + CLICKHOUSE_URL: process.env["CLICKHOUSE_URL"] ?? "http://localhost:8123", + CLICKHOUSE_PASSWORD: process.env["CLICKHOUSE_PASSWORD"] ?? "", CLICKHOUSE_CLUSTER_URL: - process.env['CLICKHOUSE_CLUSTER_URL'] ?? 'http://localhost:8127', + process.env["CLICKHOUSE_CLUSTER_URL"] ?? "http://localhost:8127", }, }, -}) +}); diff --git a/examples/node/vitest.setup.ts b/examples/node/vitest.setup.ts index 920f5559..bfcab3d4 100644 --- a/examples/node/vitest.setup.ts +++ b/examples/node/vitest.setup.ts @@ -1,12 +1,12 @@ -import { dirname, resolve } from 'path' -import { fileURLToPath } from 'url' +import { dirname, resolve } from "path"; +import { fileURLToPath } from "url"; // Examples reference data files relative to the parent `examples/` directory // (e.g. `./node/resources/data.csv`). Change the working directory to the // parent so that cwd()-based path resolution works correctly when examples // run in Vitest forks from this package directory. -const examplesDir = resolve(dirname(fileURLToPath(import.meta.url)), '..') -process.chdir(examplesDir) +const examplesDir = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +process.chdir(examplesDir); // Some examples call `process.exit(0)` as a final success signal. // In a Vitest worker, process.exit is intercepted and treated as an unexpected error, @@ -14,10 +14,10 @@ process.chdir(examplesDir) // - exit(0) → no-op: let the async IIFE return normally so Vitest reports it as passed // - exit(≠0) → throw an Error so Vitest captures the failure with a useful message process.exit = ((code?: number | string | null): never => { - const exitCode = code !== null && code !== undefined ? Number(code) : 0 + const exitCode = code !== null && code !== undefined ? Number(code) : 0; if (exitCode !== 0) { - throw new Error(`process.exit(${exitCode}) called`) + throw new Error(`process.exit(${exitCode}) called`); } // exit(0) — intentional success signal, treat as no-op - return undefined as never -}) as typeof process.exit + return undefined as never; +}) as typeof process.exit; diff --git a/examples/web/coding/array_json_each_row.ts b/examples/web/coding/array_json_each_row.ts index 30429437..aaad58f0 100644 --- a/examples/web/coding/array_json_each_row.ts +++ b/examples/web/coding/array_json_each_row.ts @@ -1,13 +1,13 @@ -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; // Inserting and selecting an array of JS objects using the `JSONEachRow` format. // This is the most common shape for app code: pass `values` as `Array>` // where each object's keys match the table's column names. -const tableName = 'array_json_each_row_web' -const client = createClient() +const tableName = "array_json_each_row_web"; +const client = createClient(); await client.command({ query: `DROP TABLE IF EXISTS ${tableName}`, -}) +}); await client.command({ query: ` CREATE TABLE ${tableName} @@ -15,19 +15,19 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); await client.insert({ table: tableName, // structure should match the desired format, JSONEachRow in this example values: [ - { id: 42, name: 'foo' }, - { id: 42, name: 'bar' }, + { id: 42, name: "foo" }, + { id: 42, name: "bar" }, ], - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); const rows = await client.query({ query: `SELECT * FROM ${tableName}`, - format: 'JSONEachRow', -}) -console.info(await rows.json()) -await client.close() + format: "JSONEachRow", +}); +console.info(await rows.json()); +await client.close(); diff --git a/examples/web/coding/async_insert.ts b/examples/web/coding/async_insert.ts index 8b8836b3..1930e093 100644 --- a/examples/web/coding/async_insert.ts +++ b/examples/web/coding/async_insert.ts @@ -1,4 +1,4 @@ -import { createClient, ClickHouseError } from '@clickhouse/client-web' +import { createClient, ClickHouseError } from "@clickhouse/client-web"; // This example demonstrates how to use asynchronous inserts, avoiding client side batching of the incoming data. // Suitable for ClickHouse Cloud, too. @@ -7,7 +7,7 @@ const client = createClient({ // In a browser application, configure the URL/credentials directly here // (or build them from a runtime configuration object). The defaults below // assume a ClickHouse instance running locally without authentication. - url: 'http://localhost:8123', + url: "http://localhost:8123", max_open_connections: 10, clickhouse_settings: { // https://clickhouse.com/docs/en/operations/settings/settings#async_insert @@ -15,14 +15,14 @@ const client = createClient({ // https://clickhouse.com/docs/en/operations/settings/settings#wait_for_async_insert wait_for_async_insert: 1, // https://clickhouse.com/docs/en/operations/settings/settings#async_insert_max_data_size - async_insert_max_data_size: '1000000', + async_insert_max_data_size: "1000000", // https://clickhouse.com/docs/en/operations/settings/settings#async_insert_busy_timeout_ms async_insert_busy_timeout_ms: 1000, }, -}) +}); // Create the table if necessary -const table = 'async_insert_example_web' +const table = "async_insert_example_web"; await client.command({ query: ` CREATE OR REPLACE TABLE ${table} @@ -34,9 +34,9 @@ await client.command({ clickhouse_settings: { wait_end_of_query: 1, }, -}) +}); -const start = new Date() +const start = new Date(); // Assume that we can receive multiple insert requests at the same time // (e.g. from parallel HTTP requests in your app or similar). const promises = [...new Array(10)].map(async () => { @@ -50,43 +50,43 @@ const promises = [...new Array(10)].map(async () => { const values = [...new Array(1000).keys()].map(() => ({ id: Math.floor(Math.random() * 100_000) + 1, data: Math.random().toString(36).slice(2), - })) + })); await client .insert({ table, values, - format: 'JSONEachRow', // or other, depends on your data + format: "JSONEachRow", // or other, depends on your data }) .catch((err) => { // Depending on the error, it is possible that the request itself was not processed on the server. if (err instanceof ClickHouseError) { // You could decide what to do with a failed insert based on the error code. // An overview of possible error codes is available in the `system.errors` ClickHouse table. - console.error(`ClickHouse error: ${err.code}. Insert failed:`, err) - return + console.error(`ClickHouse error: ${err.code}. Insert failed:`, err); + return; } // You could implement a proper retry mechanism depending on your application needs; // for the sake of this example, we just log an error. - console.error('Insert failed:', err) - }) -}) -await Promise.all(promises) + console.error("Insert failed:", err); + }); +}); +await Promise.all(promises); // In this example, it should take `async_insert_busy_timeout_ms` milliseconds or a bit more, // as the server will wait for more insert operations, // cause due to small amount of data its internal buffer was not exceeded. -console.log('Inserts took', new Date().getTime() - start.getTime(), 'ms') +console.log("Inserts took", new Date().getTime() - start.getTime(), "ms"); const resultSet = await client.query({ query: `SELECT count(*) AS count FROM ${table}`, - format: 'JSONEachRow', -}) -const [{ count }] = await resultSet.json<{ count: string }>() + format: "JSONEachRow", +}); +const [{ count }] = await resultSet.json<{ count: string }>(); // It is expected to have 10k records in the table. -console.info('Select count result:', count) +console.info("Select count result:", count); // Close the client to release any open connections/handles. In a long-lived // browser application you would typically keep the client around for the // lifetime of the page; in a one-shot script like this example, closing it // avoids leaving the process hanging. -await client.close() +await client.close(); diff --git a/examples/web/coding/clickhouse_settings.ts b/examples/web/coding/clickhouse_settings.ts index 6b83ce62..add25638 100644 --- a/examples/web/coding/clickhouse_settings.ts +++ b/examples/web/coding/clickhouse_settings.ts @@ -1,16 +1,16 @@ // Applying ClickHouse settings on the client or the operation level. // See also: {@link ClickHouseSettings} typings. -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; const client = createClient({ // Settings applied in the client settings will be added to every request. clickhouse_settings: { - date_time_input_format: 'best_effort', + date_time_input_format: "best_effort", }, -}) +}); const rows = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 2', - format: 'JSONEachRow', + query: "SELECT number FROM system.numbers LIMIT 2", + format: "JSONEachRow", /** * Apply these settings only for this query; * overrides the defaults set in the client instance settings. @@ -22,6 +22,6 @@ const rows = await client.query({ // default is 0 since 25.8 output_format_json_quote_64bit_integers: 1, }, -}) -console.info(await rows.json()) -await client.close() +}); +console.info(await rows.json()); +await client.close(); diff --git a/examples/web/coding/custom_json_handling.ts b/examples/web/coding/custom_json_handling.ts index edbbb1bd..4da4c47b 100644 --- a/examples/web/coding/custom_json_handling.ts +++ b/examples/web/coding/custom_json_handling.ts @@ -4,43 +4,43 @@ // a custom serializer before passing it to the JSON.stringify function. // // This example also shows how you can serialize Date objects in a custom way. -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; const valueSerializer = (value: unknown): unknown => { if (value instanceof Date) { // if you would have put this in the `replacer` parameter of JSON.stringify, (e.x: JSON.stringify(obj, replacerFn)) // it would have been an ISO string, but since we are serializing before `stringify`ing, // it will convert it before the `.toJSON()` method has been called - return value.getTime() + return value.getTime(); } - if (typeof value === 'bigint') { - return value.toString() + if (typeof value === "bigint") { + return value.toString(); } if (Array.isArray(value)) { - return value.map(valueSerializer) + return value.map(valueSerializer); } - if (typeof value === 'object' && value !== null) { + if (typeof value === "object" && value !== null) { return Object.fromEntries( Object.entries(value).map(([k, v]) => [k, valueSerializer(v)]), - ) + ); } - return value -} + return value; +}; -const tableName = 'inserts_custom_json_handling_web' +const tableName = "inserts_custom_json_handling_web"; const client = createClient({ json: { parse: JSON.parse, stringify: (obj: unknown) => JSON.stringify(valueSerializer(obj)), }, -}) +}); await client.command({ query: `DROP TABLE IF EXISTS ${tableName}`, -}) +}); await client.command({ query: ` CREATE TABLE ${tableName} @@ -48,7 +48,7 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); await client.insert({ table: tableName, values: [ @@ -57,11 +57,11 @@ await client.insert({ dt: new Date(), }, ], - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); const rows = await client.query({ query: `SELECT * FROM ${tableName}`, - format: 'JSONEachRow', -}) -console.info(await rows.json()) -await client.close() + format: "JSONEachRow", +}); +console.info(await rows.json()); +await client.close(); diff --git a/examples/web/coding/default_format_setting.ts b/examples/web/coding/default_format_setting.ts index f4a2a0df..d9fe9734 100644 --- a/examples/web/coding/default_format_setting.ts +++ b/examples/web/coding/default_format_setting.ts @@ -1,10 +1,10 @@ -import { createClient, ResultSet } from '@clickhouse/client-web' +import { createClient, ResultSet } from "@clickhouse/client-web"; // Using the `default_format` ClickHouse setting with `client.exec` so that the query // does not need an explicit `FORMAT` clause and the response can be wrapped in a // `ResultSet` for typed parsing. Useful when issuing arbitrary SQL via `exec`. -const client = createClient() -const format = 'JSONCompactEachRowWithNamesAndTypes' +const client = createClient(); +const format = "JSONCompactEachRowWithNamesAndTypes"; const { stream, query_id } = await client.exec({ // this query fails without `default_format` setting // as it does not have the FORMAT clause @@ -12,7 +12,7 @@ const { stream, query_id } = await client.exec({ clickhouse_settings: { default_format: format, }, -}) -const rs = new ResultSet(stream, format, query_id) -console.log(await rs.json()) -await client.close() +}); +const rs = new ResultSet(stream, format, query_id); +console.log(await rs.json()); +await client.close(); diff --git a/examples/web/coding/dynamic_variant_json.ts b/examples/web/coding/dynamic_variant_json.ts index 00dcc681..03ac3d84 100644 --- a/examples/web/coding/dynamic_variant_json.ts +++ b/examples/web/coding/dynamic_variant_json.ts @@ -1,6 +1,6 @@ -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; -const tableName = `chjs_dynamic_variant_json_web` +const tableName = `chjs_dynamic_variant_json_web`; const client = createClient({ // Since 25.3, all these types are no longer experimental and are enabled by default // However, if you are using an older version of ClickHouse, you might need these settings @@ -16,7 +16,7 @@ const client = createClient({ // https://clickhouse.com/docs/sql-reference/data-types/newjson allow_experimental_json_type: 1, }, -}) +}); await client.command({ query: ` CREATE OR REPLACE TABLE ${tableName} @@ -29,20 +29,20 @@ await client.command({ ENGINE MergeTree ORDER BY id `, -}) +}); // Sample representation in JSONEachRow format const values = [ { id: 1, var: 42, - dynamic: 'foo', + dynamic: "foo", json: { - foo: 'x', + foo: "x", }, }, { id: 2, - var: 'str', + var: "str", // A number will default to Int64; it could be also represented as a string in JSON* family formats // using `output_format_json_quote_64bit_integers` setting (default is 0 since CH 25.8). // See https://clickhouse.com/docs/en/operations/settings/formats#output_format_json_quote_64bit_integers @@ -51,12 +51,12 @@ const values = [ bar: 10, }, }, -] +]; await client.insert({ table: tableName, - format: 'JSONEachRow', + format: "JSONEachRow", values, -}) +}); const rs = await client.query({ query: ` SELECT *, @@ -66,7 +66,7 @@ const rs = await client.query({ dynamicType(json.bar) FROM ${tableName} `, - format: 'JSONEachRow', -}) -console.log(await rs.json()) -await client.close() + format: "JSONEachRow", +}); +console.log(await rs.json()); +await client.close(); diff --git a/examples/web/coding/insert_data_formats_overview.ts b/examples/web/coding/insert_data_formats_overview.ts index dec1f1d4..da7658f6 100644 --- a/examples/web/coding/insert_data_formats_overview.ts +++ b/examples/web/coding/insert_data_formats_overview.ts @@ -25,100 +25,100 @@ import { type DataFormat, type InputJSON, type InputJSONObjectEachRow, -} from '@clickhouse/client-web' +} from "@clickhouse/client-web"; -const tableName = 'insert_data_formats_overview_web' -const client = createClient() -await prepareTestTable() +const tableName = "insert_data_formats_overview_web"; +const client = createClient(); +await prepareTestTable(); // These JSON formats can be streamed as well instead of sending the entire data set at once; // See this example that streams a file: node/insert_file_stream_ndjson.ts -console.log('#### Streamable JSON formats:\n') +console.log("#### Streamable JSON formats:\n"); // All of these formats accept various arrays of objects, depending on the format. -await insertJSON('JSONEachRow', [ - { id: 1, name: 'foo', sku: [1, 2, 3] }, - { id: 2, name: 'bar', sku: [4, 5, 6] }, -]) -await insertJSON('JSONStringsEachRow', [ - { id: '3', name: 'foo', sku: '[1,2,3]' }, - { id: '4', name: 'bar', sku: '[4,5,6]' }, -]) -await insertJSON('JSONCompactEachRow', [ - [5, 'foo', [1, 2, 3]], - [6, 'bar', [4, 5, 6]], -]) -await insertJSON('JSONCompactStringsEachRow', [ - ['7', 'foo', '[1,2,3]'], - ['8', 'bar', '[4,5,6]'], -]) -await insertJSON('JSONCompactEachRowWithNames', [ - ['id', 'name', 'sku'], - [9, 'foo', [1, 2, 3]], - [10, 'bar', [4, 5, 6]], -]) -await insertJSON('JSONCompactEachRowWithNamesAndTypes', [ - ['id', 'name', 'sku'], - ['UInt32', 'String', 'Array(UInt32)'], - [11, 'foo', [1, 2, 3]], - [12, 'bar', [4, 5, 6]], -]) -await insertJSON('JSONCompactStringsEachRowWithNames', [ - ['id', 'name', 'sku'], - ['13', 'foo', '[1,2,3]'], - ['14', 'bar', '[4,5,6]'], -]) -await insertJSON('JSONCompactStringsEachRowWithNamesAndTypes', [ - ['id', 'name', 'sku'], - ['UInt32', 'String', 'Array(UInt32)'], - ['15', 'foo', '[1,2,3]'], - ['16', 'bar', '[4,5,6]'], -]) +await insertJSON("JSONEachRow", [ + { id: 1, name: "foo", sku: [1, 2, 3] }, + { id: 2, name: "bar", sku: [4, 5, 6] }, +]); +await insertJSON("JSONStringsEachRow", [ + { id: "3", name: "foo", sku: "[1,2,3]" }, + { id: "4", name: "bar", sku: "[4,5,6]" }, +]); +await insertJSON("JSONCompactEachRow", [ + [5, "foo", [1, 2, 3]], + [6, "bar", [4, 5, 6]], +]); +await insertJSON("JSONCompactStringsEachRow", [ + ["7", "foo", "[1,2,3]"], + ["8", "bar", "[4,5,6]"], +]); +await insertJSON("JSONCompactEachRowWithNames", [ + ["id", "name", "sku"], + [9, "foo", [1, 2, 3]], + [10, "bar", [4, 5, 6]], +]); +await insertJSON("JSONCompactEachRowWithNamesAndTypes", [ + ["id", "name", "sku"], + ["UInt32", "String", "Array(UInt32)"], + [11, "foo", [1, 2, 3]], + [12, "bar", [4, 5, 6]], +]); +await insertJSON("JSONCompactStringsEachRowWithNames", [ + ["id", "name", "sku"], + ["13", "foo", "[1,2,3]"], + ["14", "bar", "[4,5,6]"], +]); +await insertJSON("JSONCompactStringsEachRowWithNamesAndTypes", [ + ["id", "name", "sku"], + ["UInt32", "String", "Array(UInt32)"], + ["15", "foo", "[1,2,3]"], + ["16", "bar", "[4,5,6]"], +]); // These are single document JSON formats, which are not streamable -console.log('\n#### Single document JSON formats:\n') +console.log("\n#### Single document JSON formats:\n"); // JSON, JSONCompact, JSONColumnsWithMetadata accept the InputJSON shape. // For example: https://clickhouse.com/docs/en/interfaces/formats#json -const meta: InputJSON['meta'] = [ - { name: 'id', type: 'UInt32' }, - { name: 'name', type: 'String' }, - { name: 'sku', type: 'Array(UInt32)' }, -] -await insertJSON('JSON', { +const meta: InputJSON["meta"] = [ + { name: "id", type: "UInt32" }, + { name: "name", type: "String" }, + { name: "sku", type: "Array(UInt32)" }, +]; +await insertJSON("JSON", { meta: [], // not required for JSON format input data: [ - { id: 17, name: 'foo', sku: [1, 2, 3] }, - { id: 18, name: 'bar', sku: [4, 5, 6] }, + { id: 17, name: "foo", sku: [1, 2, 3] }, + { id: 18, name: "bar", sku: [4, 5, 6] }, ], -}) -await insertJSON('JSONCompact', { +}); +await insertJSON("JSONCompact", { meta, data: [ - [19, 'foo', [1, 2, 3]], - [20, 'bar', [4, 5, 6]], + [19, "foo", [1, 2, 3]], + [20, "bar", [4, 5, 6]], ], -}) -await insertJSON('JSONColumnsWithMetadata', { +}); +await insertJSON("JSONColumnsWithMetadata", { meta, data: { id: [21, 22], - name: ['foo', 'bar'], + name: ["foo", "bar"], sku: [ [1, 2, 3], [4, 5, 6], ], }, -}) +}); // JSONObjectEachRow accepts Record (alias: InputJSONObjectEachRow). // See https://clickhouse.com/docs/en/interfaces/formats#jsonobjecteachrow -await insertJSON('JSONObjectEachRow', { - row_1: { id: 23, name: 'foo', sku: [1, 2, 3] }, - row_2: { id: 24, name: 'bar', sku: [4, 5, 6] }, -}) +await insertJSON("JSONObjectEachRow", { + row_1: { id: 23, name: "foo", sku: [1, 2, 3] }, + row_2: { id: 24, name: "bar", sku: [4, 5, 6] }, +}); // Print the inserted data - see that the IDs are matching. -await printInsertedData() -await client.close() +await printInsertedData(); +await client.close(); // Inserting data in different JSON formats async function insertJSON( @@ -130,11 +130,11 @@ async function insertJSON( table: tableName, format: format, values, - }) - console.log(`Successfully inserted data with format ${format}`) + }); + console.log(`Successfully inserted data with format ${format}`); } catch (err) { - console.error(`Failed to insert data with format ${format}, cause:`, err) - throw err + console.error(`Failed to insert data with format ${format}, cause:`, err); + throw err; } } @@ -146,15 +146,15 @@ async function prepareTestTable() { ENGINE MergeTree() ORDER BY (id) `, - }) + }); } async function printInsertedData() { const resultSet = await client.query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, - format: 'JSONEachRow', - }) - const data = await resultSet.json() - console.log('Inserted data:') - console.log(data) + format: "JSONEachRow", + }); + const data = await resultSet.json(); + console.log("Inserted data:"); + console.log(data); } diff --git a/examples/web/coding/insert_decimals.ts b/examples/web/coding/insert_decimals.ts index 45bcfe2d..d5aaa101 100644 --- a/examples/web/coding/insert_decimals.ts +++ b/examples/web/coding/insert_decimals.ts @@ -1,11 +1,11 @@ -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; // Inserting and reading back values for all four `Decimal(P, S)` widths (32/64/128/256-bit). // Decimal values are passed as strings to avoid floating-point precision loss, and read back // using `toString(decN)` for the same reason. Reach for this when storing money or other // fixed-precision quantities. -const client = createClient() -const tableName = 'insert_decimals_example_web' +const client = createClient(); +const tableName = "insert_decimals_example_web"; await client.command({ query: ` CREATE OR REPLACE TABLE ${tableName} @@ -19,27 +19,27 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); const row1 = { id: 1, - dec32: '1234567.89', - dec64: '123456789123456.789', - dec128: '1234567891234567891234567891.1234567891', + dec32: "1234567.89", + dec64: "123456789123456.789", + dec128: "1234567891234567891234567891.1234567891", dec256: - '12345678912345678912345678911234567891234567891234567891.12345678911234567891', -} + "12345678912345678912345678911234567891234567891234567891.12345678911234567891", +}; const row2 = { id: 2, - dec32: '12.01', - dec64: '5000000.405', - dec128: '1.0000000004', - dec256: '42.00000000000000013007', -} + dec32: "12.01", + dec64: "5000000.405", + dec128: "1.0000000004", + dec256: "42.00000000000000013007", +}; await client.insert({ table: tableName, values: [row1, row2], - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); const resultSet = await client.query({ query: ` SELECT toString(dec32) AS decimal32, @@ -48,7 +48,7 @@ const resultSet = await client.query({ toString(dec256) AS decimal256 FROM ${tableName} `, - format: 'JSONEachRow', -}) -console.log('Result:', await resultSet.json()) -await client.close() + format: "JSONEachRow", +}); +console.log("Result:", await resultSet.json()); +await client.close(); diff --git a/examples/web/coding/insert_ephemeral_columns.ts b/examples/web/coding/insert_ephemeral_columns.ts index 03c67d22..1cefd8fc 100644 --- a/examples/web/coding/insert_ephemeral_columns.ts +++ b/examples/web/coding/insert_ephemeral_columns.ts @@ -1,8 +1,8 @@ -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; // Ephemeral columns documentation: https://clickhouse.com/docs/en/sql-reference/statements/create/table#ephemeral -const tableName = 'insert_ephemeral_columns_web' -const client = createClient() +const tableName = "insert_ephemeral_columns_web"; +const client = createClient(); await client.command({ query: ` @@ -15,31 +15,31 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); await client.insert({ table: tableName, values: [ { - id: '42', - message_default: 'foo', + id: "42", + message_default: "foo", }, { - id: '144', - message_default: 'bar', + id: "144", + message_default: "bar", }, ], - format: 'JSONEachRow', + format: "JSONEachRow", // The name of the ephemeral column has to be specified here // to trigger the default values logic for the rest of the columns - columns: ['id', 'message_default'], -}) + columns: ["id", "message_default"], +}); const rows = await client.query({ query: `SELECT * FROM ${tableName}`, - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); -console.info(await rows.json()) -await client.close() +console.info(await rows.json()); +await client.close(); diff --git a/examples/web/coding/insert_exclude_columns.ts b/examples/web/coding/insert_exclude_columns.ts index ad2c58ff..07206d4c 100644 --- a/examples/web/coding/insert_exclude_columns.ts +++ b/examples/web/coding/insert_exclude_columns.ts @@ -1,9 +1,9 @@ // Excluding certain columns from the INSERT statement. // For the inverse (specifying the exact columns to insert into), see `insert_specific_columns.ts`. -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; -const tableName = 'insert_exclude_columns_web' -const client = createClient() +const tableName = "insert_exclude_columns_web"; +const client = createClient(); await client.command({ query: ` @@ -12,32 +12,32 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); await client.insert({ table: tableName, - values: [{ message: 'bar' }], - format: 'JSONEachRow', + values: [{ message: "bar" }], + format: "JSONEachRow", // `id` column value for this row will be zero columns: { - except: ['id'], + except: ["id"], }, -}) +}); await client.insert({ table: tableName, values: [{ id: 144 }], - format: 'JSONEachRow', + format: "JSONEachRow", // `message` column value for this row will be an empty string columns: { - except: ['message'], + except: ["message"], }, -}) +}); const rows = await client.query({ query: `SELECT * FROM ${tableName} ORDER BY id, message DESC`, - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); -console.info(await rows.json()) -await client.close() +console.info(await rows.json()); +await client.close(); diff --git a/examples/web/coding/insert_from_select.ts b/examples/web/coding/insert_from_select.ts index 844222b4..504e2000 100644 --- a/examples/web/coding/insert_from_select.ts +++ b/examples/web/coding/insert_from_select.ts @@ -2,10 +2,10 @@ // Demonstrates that `client.command` can run server-side data movement queries // (no client-side rows are sent), and that aggregate states are read back via // `finalizeAggregation`. Inspired by https://github.com/ClickHouse/clickhouse-js/issues/166 -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; -const tableName = 'insert_from_select_web' -const client = createClient() +const tableName = "insert_from_select_web"; +const client = createClient(); await client.command({ query: ` CREATE OR REPLACE TABLE ${tableName} @@ -13,15 +13,15 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); await client.command({ query: ` INSERT INTO ${tableName} SELECT '42', quantilesBFloat16State(0.5)(arrayJoin([toFloat32(10), toFloat32(20)]))`, -}) +}); const rows = await client.query({ query: `SELECT finalizeAggregation(data) AS result FROM ${tableName}`, - format: 'JSONEachRow', -}) -console.info(await rows.json()) -await client.close() + format: "JSONEachRow", +}); +console.info(await rows.json()); +await client.close(); diff --git a/examples/web/coding/insert_into_different_db.ts b/examples/web/coding/insert_into_different_db.ts index f0acbab0..95a46060 100644 --- a/examples/web/coding/insert_into_different_db.ts +++ b/examples/web/coding/insert_into_different_db.ts @@ -1,20 +1,20 @@ -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; // Writing to a table that lives in a database other than the client's default `database`. // Pass a fully qualified `database.table` name to `client.insert`/`client.query`/`client.command` // when you need to address a different database without recreating the client. -const dbName = 'clickhouse_js_examples_web' -const tableName = `test_table_${Math.random().toString(36).slice(2, 8)}` +const dbName = "clickhouse_js_examples_web"; +const tableName = `test_table_${Math.random().toString(36).slice(2, 8)}`; const client = createClient({ - database: 'system', -}) + database: "system", +}); await client.command({ query: `CREATE DATABASE IF NOT EXISTS ${dbName}`, -}) +}); // Including the database here, as the client is created for "system" -const fullTableName = `${dbName}.${tableName}` +const fullTableName = `${dbName}.${tableName}`; await client.command({ query: ` @@ -23,19 +23,19 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) -console.info(`Created table ${fullTableName}`) +}); +console.info(`Created table ${fullTableName}`); await client.insert({ table: fullTableName, - values: [{ id: 42, message: 'foo' }], - format: 'JSONEachRow', -}) + values: [{ id: 42, message: "foo" }], + format: "JSONEachRow", +}); const rows = await client.query({ query: `SELECT * FROM ${fullTableName} ORDER BY id, message DESC`, - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); -console.info('Result:', await rows.json()) -await client.close() +console.info("Result:", await rows.json()); +await client.close(); diff --git a/examples/web/coding/insert_js_dates.ts b/examples/web/coding/insert_js_dates.ts index 4c45f980..9729c9ef 100644 --- a/examples/web/coding/insert_js_dates.ts +++ b/examples/web/coding/insert_js_dates.ts @@ -1,8 +1,8 @@ -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; // NB: currently, JS Date objects work only with DateTime* fields -const tableName = 'insert_js_date_web' -const client = createClient() +const tableName = "insert_js_date_web"; +const client = createClient(); await client.command({ query: ` CREATE OR REPLACE TABLE ${tableName} @@ -10,24 +10,24 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); await client.insert({ table: tableName, values: [ { - id: '42', + id: "42", dt: new Date(), }, ], clickhouse_settings: { // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z') - date_time_input_format: 'best_effort', + date_time_input_format: "best_effort", }, - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); const rows = await client.query({ query: `SELECT * FROM ${tableName}`, - format: 'JSONEachRow', -}) -console.info(await rows.json()) -await client.close() + format: "JSONEachRow", +}); +console.info(await rows.json()); +await client.close(); diff --git a/examples/web/coding/insert_specific_columns.ts b/examples/web/coding/insert_specific_columns.ts index 8e600e44..206e7f94 100644 --- a/examples/web/coding/insert_specific_columns.ts +++ b/examples/web/coding/insert_specific_columns.ts @@ -1,9 +1,9 @@ // Explicitly specifying a list of columns to insert the data into. // For the inverse (excluding certain columns instead), see `insert_exclude_columns.ts`. -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; -const tableName = 'insert_specific_columns_web' -const client = createClient() +const tableName = "insert_specific_columns_web"; +const client = createClient(); await client.command({ query: ` @@ -12,28 +12,28 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); await client.insert({ table: tableName, - values: [{ message: 'foo' }], - format: 'JSONEachRow', + values: [{ message: "foo" }], + format: "JSONEachRow", // `id` column value for this row will be zero - columns: ['message'], -}) + columns: ["message"], +}); await client.insert({ table: tableName, values: [{ id: 42 }], - format: 'JSONEachRow', + format: "JSONEachRow", // `message` column value for this row will be an empty string - columns: ['id'], -}) + columns: ["id"], +}); const rows = await client.query({ query: `SELECT * FROM ${tableName} ORDER BY id, message DESC`, - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); -console.info(await rows.json()) -await client.close() +console.info(await rows.json()); +await client.close(); diff --git a/examples/web/coding/insert_uuid_into_uint128.ts b/examples/web/coding/insert_uuid_into_uint128.ts index 3d6446c0..3c073dcb 100644 --- a/examples/web/coding/insert_uuid_into_uint128.ts +++ b/examples/web/coding/insert_uuid_into_uint128.ts @@ -1,4 +1,4 @@ -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; function assertEqual( actual: unknown, @@ -6,7 +6,7 @@ function assertEqual( message: string, ): void { if (actual !== expected) { - throw new Error(`${message}: expected ${expected}, got ${actual}`) + throw new Error(`${message}: expected ${expected}, got ${actual}`); } } @@ -26,16 +26,16 @@ function assertEqual( function uuidToUInt128(uuid: string): string { // 8-4-4-4-12 hex digits → 32 hex digits → BigInt → decimal string - return BigInt('0x' + uuid.replace(/-/g, '')).toString() + return BigInt("0x" + uuid.replace(/-/g, "")).toString(); } -const client = createClient() +const client = createClient(); -const uuid = '019982cb-3abf-7e12-9668-c788a9e3639c' -const expectedUInt128 = uuidToUInt128(uuid) +const uuid = "019982cb-3abf-7e12-9668-c788a9e3639c"; +const expectedUInt128 = uuidToUInt128(uuid); // ---- Pattern 1: client-side UUID → UInt128 conversion ---- -const tableName = 'insert_uuid_into_uint128_example_web' +const tableName = "insert_uuid_into_uint128_example_web"; await client.command({ query: ` CREATE OR REPLACE TABLE ${tableName} @@ -46,38 +46,38 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); await client.insert({ table: tableName, values: [ { id: expectedUInt128, - description: 'converted from UUID on the client', + description: "converted from UUID on the client", }, ], - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); const converted = await client.query({ // UInt128 values are returned as decimal strings (too wide for a JS number). query: `SELECT toString(id) AS id_uint128, description FROM ${tableName}`, - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); const convertedRows = await converted.json<{ - id_uint128: string - description: string -}>() -console.info('Pattern 1 (client-side conversion):', convertedRows) + id_uint128: string; + description: string; +}>(); +console.info("Pattern 1 (client-side conversion):", convertedRows); // Round-trip assertion: the value SELECTed back must equal the BigInt-derived // UInt128 we sent, proving the server parsed the JSON input correctly. assertEqual( convertedRows[0].id_uint128, expectedUInt128, - 'Pattern 1 round-trip', -) + "Pattern 1 round-trip", +); // ---- Pattern 2: EPHEMERAL UUID column with UInt128 DEFAULT ---- -const ephemeralTableName = 'insert_uuid_into_uint128_ephemeral_example_web' +const ephemeralTableName = "insert_uuid_into_uint128_ephemeral_example_web"; await client.command({ query: ` CREATE OR REPLACE TABLE ${ephemeralTableName} @@ -89,35 +89,35 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); await client.insert({ table: ephemeralTableName, values: [ { id_uuid: uuid, - description: 'populated via EPHEMERAL UUID column', + description: "populated via EPHEMERAL UUID column", }, ], - format: 'JSONEachRow', + format: "JSONEachRow", // The ephemeral column must be listed explicitly so that the DEFAULT // expression on `id` is evaluated. - columns: ['id_uuid', 'description'], -}) + columns: ["id_uuid", "description"], +}); const ephemeral = await client.query({ query: `SELECT toString(id) AS id_uint128, description FROM ${ephemeralTableName}`, - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); const ephemeralRows = await ephemeral.json<{ - id_uint128: string - description: string -}>() -console.info('Pattern 2 (EPHEMERAL column):', ephemeralRows) + id_uint128: string; + description: string; +}>(); +console.info("Pattern 2 (EPHEMERAL column):", ephemeralRows); // Both patterns must produce the same UInt128 representation of the UUID. assertEqual( ephemeralRows[0].id_uint128, expectedUInt128, - 'Pattern 2 round-trip', -) + "Pattern 2 round-trip", +); -await client.close() +await client.close(); diff --git a/examples/web/coding/insert_values_and_functions.ts b/examples/web/coding/insert_values_and_functions.ts index a8843629..0bfea6e1 100644 --- a/examples/web/coding/insert_values_and_functions.ts +++ b/examples/web/coding/insert_values_and_functions.ts @@ -1,29 +1,29 @@ // An example how to send an INSERT INTO ... VALUES ... query that requires additional functions call. // Inspired by https://github.com/ClickHouse/clickhouse-js/issues/239 -import type { ClickHouseSettings } from '@clickhouse/client-web' -import { createClient } from '@clickhouse/client-web' +import type { ClickHouseSettings } from "@clickhouse/client-web"; +import { createClient } from "@clickhouse/client-web"; interface Data { - id: string - timestamp: number - email: string - name: string | null + id: string; + timestamp: number; + email: string; + name: string | null; } -const tableName = 'insert_values_and_functions_web' -const client = createClient() +const tableName = "insert_values_and_functions_web"; +const client = createClient(); // Recommended for cluster usage to avoid situations where a query processing error occurred after the response code // and HTTP headers were sent to the client, as it might happen before the changes were applied on the server. // See https://clickhouse.com/docs/en/interfaces/http/#response-buffering const commandSettings: ClickHouseSettings = { wait_end_of_query: 1, -} +}; // Prepare an example table await client.command({ query: `DROP TABLE IF EXISTS ${tableName}`, clickhouse_settings: commandSettings, -}) +}); await client.command({ query: ` CREATE TABLE ${tableName} ( @@ -36,57 +36,57 @@ await client.command({ ORDER BY (id) `, clickhouse_settings: commandSettings, -}) +}); // Here we are assuming that we are getting these rows from somewhere... -const rows = getRows(100) +const rows = getRows(100); // Generate the query and insert the values const insertQuery = ` INSERT INTO ${tableName} (id, timestamp, email, name) VALUES - ${rows.map((r) => toInsertValue(r)).join(',')} -` + ${rows.map((r) => toInsertValue(r)).join(",")} +`; await client.command({ query: insertQuery, clickhouse_settings: commandSettings, -}) +}); // Get a few back and print those rows to check what was inserted const sampleResultSet = await client.query({ query: `SELECT * FROM ${tableName} ORDER BY rand() LIMIT 3`, - format: 'JSONEachRow', -}) -console.info(`Sample inserted rows:`) -const sampleRows = await sampleResultSet.json() + format: "JSONEachRow", +}); +console.info(`Sample inserted rows:`); +const sampleRows = await sampleResultSet.json(); sampleRows.forEach((row) => { - console.info(row) -}) + console.info(row); +}); // Close it during your application graceful shutdown -await client.close() +await client.close(); function getRows(n: number): Data[] { - const now = Date.now() // UNIX timestamp in milliseconds + const now = Date.now(); // UNIX timestamp in milliseconds return [...new Array(n)].map((_, i) => ({ id: toHex(i.toString(10)), timestamp: now - i * 1000, // subtract one second for each row email: `email${i}@example.com`, name: i % 2 === 0 ? `Name${i}` : null, // for every second row it is NULL - })) + })); } // Convert an ASCII string to its hexadecimal representation using browser-friendly APIs. // Equivalent to Buffer.from(value).toString('hex') in Node.js, but works in any JS runtime. function toHex(str: string): string { - const bytes = new TextEncoder().encode(str) - let hex = '' + const bytes = new TextEncoder().encode(str); + let hex = ""; for (const b of bytes) { - hex += b.toString(16).padStart(2, '0') + hex += b.toString(16).padStart(2, "0"); } - return hex + return hex; } // Generates something like: @@ -95,9 +95,9 @@ function toHex(str: string): string { // (unhex('144'), '1623677409123', 'email144@example.com', NULL) // if name is null. function toInsertValue(row: Data): string { - const id = `unhex('${row.id}')` - const timestamp = `'${row.timestamp}'` - const email = `'${row.email}'` - const name = row.name === null ? 'NULL' : `'${row.name}'` - return `(${id}, ${timestamp}, ${email}, ${name})` + const id = `unhex('${row.id}')`; + const timestamp = `'${row.timestamp}'`; + const email = `'${row.email}'`; + const name = row.name === null ? "NULL" : `'${row.name}'`; + return `(${id}, ${timestamp}, ${email}, ${name})`; } diff --git a/examples/web/coding/ping_existing_host.ts b/examples/web/coding/ping_existing_host.ts index 724a8874..34d13831 100644 --- a/examples/web/coding/ping_existing_host.ts +++ b/examples/web/coding/ping_existing_host.ts @@ -7,21 +7,21 @@ // // See also: // - `ping_non_existing_host.ts` - ping against a host that does not exist. -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; const client = createClient({ // In a browser application, configure the URL/credentials directly here // (or build them from a runtime configuration object). The defaults below // assume a ClickHouse instance running locally without authentication. - url: 'http://localhost:8123', -}) -const pingResult = await client.ping() + url: "http://localhost:8123", +}); +const pingResult = await client.ping(); if (pingResult.success) { - console.log('[ExistingHostPing] Ping to the existing host is successful') + console.log("[ExistingHostPing] Ping to the existing host is successful"); } else { console.error( - '[ExistingHostPing] Ping expected to succeed, but got:', + "[ExistingHostPing] Ping expected to succeed, but got:", pingResult, - ) + ); } -await client.close() +await client.close(); diff --git a/examples/web/coding/ping_non_existing_host.ts b/examples/web/coding/ping_non_existing_host.ts index b189e5a8..35dfba29 100644 --- a/examples/web/coding/ping_non_existing_host.ts +++ b/examples/web/coding/ping_non_existing_host.ts @@ -12,22 +12,22 @@ // See also: // - `ping_existing_host.ts` - successful ping against an existing host. // - `ping_timeout.ts` - ping that times out. -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; const client = createClient({ - url: 'http://localhost:8100', // non-existing host + url: "http://localhost:8100", // non-existing host request_timeout: 50, // low request_timeout to speed up the example -}) +}); // Ping does not throw an error; instead, { success: false; error: Error } is returned. -const pingResult = await client.ping() +const pingResult = await client.ping(); if (!pingResult.success) { console.info( - '[NonExistingHostPing] Ping failed as expected:', + "[NonExistingHostPing] Ping failed as expected:", pingResult.error, - ) + ); } else { console.error( - '[NonExistingHostPing] Ping was expected to fail, but it succeeded', - ) + "[NonExistingHostPing] Ping was expected to fail, but it succeeded", + ); } -await client.close() +await client.close(); diff --git a/examples/web/coding/qbit.ts b/examples/web/coding/qbit.ts index 04015104..81ab1fee 100644 --- a/examples/web/coding/qbit.ts +++ b/examples/web/coding/qbit.ts @@ -1,5 +1,5 @@ -import { createClient } from '@clickhouse/client-web' -import { expect } from 'vitest' +import { createClient } from "@clickhouse/client-web"; +import { expect } from "vitest"; /** * QBit is a column type that stores float vectors in bit-sliced ("transposed") @@ -24,13 +24,13 @@ import { expect } from 'vitest' * See https://clickhouse.com/docs/sql-reference/data-types/qbit */ -const tableName = `chjs_qbit_web` +const tableName = `chjs_qbit_web`; const client = createClient({ clickhouse_settings: { // Required on ClickHouse 25.10 (experimental); ignored on 26.x where QBit is GA. allow_experimental_qbit_type: 1, }, -}) +}); await client.command({ query: ` @@ -42,7 +42,7 @@ await client.command({ ENGINE MergeTree ORDER BY id `, -}) +}); // Even though QBit is stored internally as a Tuple of FixedString bit planes, // JSON* formats accept (and return) the original Array(Float32) shape. @@ -50,24 +50,24 @@ const values = [ { id: 1, vec: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0] }, { id: 2, vec: [8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0] }, { id: 3, vec: [1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5] }, -] +]; await client.insert({ table: tableName, - format: 'JSONEachRow', + format: "JSONEachRow", values, -}) +}); // Round-trip via JSONEachRow: the vec column comes back as an array of numbers. const rs = await client.query({ query: `SELECT id, vec FROM ${tableName} ORDER BY id`, - format: 'JSONEachRow', -}) -const rows = await rs.json<{ id: number; vec: number[] }>() -console.log('Round-tripped rows:') -console.log(rows) + format: "JSONEachRow", +}); +const rows = await rs.json<{ id: number; vec: number[] }>(); +console.log("Round-tripped rows:"); +console.log(rows); // Even though QBit is stored as a Tuple of FixedString bit planes, JSON* formats // return the original Float32 vectors unchanged. -expect(rows).toEqual(values) +expect(rows).toEqual(values); // Approximate vector search via L2DistanceTransposed. // The third argument is the precision in bits: lower = less I/O, less accurate. @@ -82,14 +82,14 @@ const search = await client.query({ ref: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], bits: 16, }, - format: 'JSONEachRow', -}) -const nearest = await search.json<{ id: number; dist: number }>() -console.log('Nearest neighbours of the reference vector:') -console.log(nearest) + format: "JSONEachRow", +}); +const nearest = await search.json<{ id: number; dist: number }>(); +console.log("Nearest neighbours of the reference vector:"); +console.log(nearest); // The reference vector is exactly row #1, so it must be the closest match (dist 0). -expect(nearest.map((r) => r.id)).toEqual([1, 3, 2]) -expect(nearest[0].dist).toBe(0) +expect(nearest.map((r) => r.id)).toEqual([1, 3, 2]); +expect(nearest[0].dist).toBe(0); // Bit-plane subcolumns (`vec.N`) are exposed as FixedString and therefore are // NOT valid UTF-8. Selecting them directly with a JSON* format would force the @@ -98,15 +98,15 @@ expect(nearest[0].dist).toBe(0) // or read them as hex/base64: const planes = await client.query({ query: `SELECT id, hex(vec.1) AS bit_plane_1_hex FROM ${tableName} ORDER BY id`, - format: 'JSONEachRow', -}) -const planeRows = await planes.json<{ id: number; bit_plane_1_hex: string }>() -console.log('First bit plane per row (hex-encoded to keep JSON UTF-8 safe):') -console.log(planeRows) + format: "JSONEachRow", +}); +const planeRows = await planes.json<{ id: number; bit_plane_1_hex: string }>(); +console.log("First bit plane per row (hex-encoded to keep JSON UTF-8 safe):"); +console.log(planeRows); // hex() yields a UTF-8-safe representation of the raw FixedString bit-plane bytes. -expect(planeRows.map((r) => r.id)).toEqual([1, 2, 3]) +expect(planeRows.map((r) => r.id)).toEqual([1, 2, 3]); for (const row of planeRows) { - expect(row.bit_plane_1_hex).toMatch(/^[0-9A-F]*$/) + expect(row.bit_plane_1_hex).toMatch(/^[0-9A-F]*$/); } -await client.close() +await client.close(); diff --git a/examples/web/coding/query_with_parameter_binding.ts b/examples/web/coding/query_with_parameter_binding.ts index 8a258886..93a542be 100644 --- a/examples/web/coding/query_with_parameter_binding.ts +++ b/examples/web/coding/query_with_parameter_binding.ts @@ -2,9 +2,9 @@ // // For binding parameters that contain special characters (tabs, newlines, quotes, etc.), // see `query_with_parameter_binding_special_chars.ts`. -import { createClient, TupleParam } from '@clickhouse/client-web' +import { createClient, TupleParam } from "@clickhouse/client-web"; -const client = createClient() +const client = createClient(); const resultSet = await client.query({ query: ` SELECT @@ -24,31 +24,31 @@ const resultSet = await client.query({ {var_ipv4: IPv4} AS var_ipv4, {var_null: Nullable(String)} AS var_null `, - format: 'JSONEachRow', + format: "JSONEachRow", query_params: { var_int: 10, - var_float: '10.557', + var_float: "10.557", var_str: 20, var_array: [42, 144], - var_tuple: new TupleParam([42, 'foo']), + var_tuple: new TupleParam([42, "foo"]), var_map: new Map([ - [42, ['a', 'b']], - [144, ['c', 'd']], + [42, ["a", "b"]], + [144, ["c", "d"]], ]), - var_date: '2022-01-01', - var_datetime: '2022-01-01 12:34:56', // or a Date object - var_datetime64_3: '2022-01-01 12:34:56.789', // or a Date object + var_date: "2022-01-01", + var_datetime: "2022-01-01 12:34:56", // or a Date object + var_datetime64_3: "2022-01-01 12:34:56.789", // or a Date object // NB: Date object with DateTime64(9) is still possible, // but there will be precision loss, as JS Date has only milliseconds. - var_datetime64_9: '2022-01-01 12:34:56.123456789', + var_datetime64_9: "2022-01-01 12:34:56.123456789", // It is also possible to provide DateTime64 as a timestamp. - var_datetime64_9_ts: '1651490755.123456789', - var_decimal: '123.45', - var_uuid: '01234567-89ab-cdef-0123-456789abcdef', - var_ipv4: '192.168.0.1', + var_datetime64_9_ts: "1651490755.123456789", + var_decimal: "123.45", + var_uuid: "01234567-89ab-cdef-0123-456789abcdef", + var_ipv4: "192.168.0.1", var_null: null, }, -}) -console.info('Result (different data types):', await resultSet.json()) +}); +console.info("Result (different data types):", await resultSet.json()); -await client.close() +await client.close(); diff --git a/examples/web/coding/query_with_parameter_binding_special_chars.ts b/examples/web/coding/query_with_parameter_binding_special_chars.ts index 19c9f2b9..e3660034 100644 --- a/examples/web/coding/query_with_parameter_binding_special_chars.ts +++ b/examples/web/coding/query_with_parameter_binding_special_chars.ts @@ -2,9 +2,9 @@ // Available since clickhouse-js 0.3.1. // // For an overview of binding regular values of various data types, see `query_with_parameter_binding.ts`. -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; -const client = createClient() +const client = createClient(); const resultSet = await client.query({ query: ` @@ -14,17 +14,17 @@ const resultSet = await client.query({ 'foo_\r_bar' = {carriage_return: String} AS has_carriage_return, 'foo_\\'_bar' = {single_quote: String} AS has_single_quote, 'foo_\\_bar' = {backslash: String} AS has_backslash`, - format: 'JSONEachRow', + format: "JSONEachRow", query_params: { - tab: 'foo_\t_bar', - newline: 'foo_\n_bar', - carriage_return: 'foo_\r_bar', + tab: "foo_\t_bar", + newline: "foo_\n_bar", + carriage_return: "foo_\r_bar", single_quote: "foo_'_bar", - backslash: 'foo_\\_bar', + backslash: "foo_\\_bar", }, -}) +}); // Should return all 1, as query params will match the strings in the SELECT. -console.info('Result (special characters):', await resultSet.json()) +console.info("Result (special characters):", await resultSet.json()); -await client.close() +await client.close(); diff --git a/examples/web/coding/select_data_formats_overview.ts b/examples/web/coding/select_data_formats_overview.ts index b6d728f7..6d947f0f 100644 --- a/examples/web/coding/select_data_formats_overview.ts +++ b/examples/web/coding/select_data_formats_overview.ts @@ -10,61 +10,61 @@ // - INSERT formats overview - insert_data_formats_overview.ts // - JSON data streaming example - select_streaming_json_each_row.ts // - Streaming Parquet into a file - node/select_parquet_as_file.ts -import { createClient, type DataFormat } from '@clickhouse/client-web' +import { createClient, type DataFormat } from "@clickhouse/client-web"; -const tableName = 'select_data_formats_overview_web' -const client = createClient() -await prepareTestData() +const tableName = "select_data_formats_overview_web"; +const client = createClient(); +await prepareTestData(); // These ClickHouse JSON formats can be streamed as well instead of loading the entire result into the app memory; // See this example: node/select_streaming_json_each_row.ts -console.log('#### Streamable JSON formats:\n') -await selectJSON('JSONEachRow') -await selectJSON('JSONStringsEachRow') -await selectJSON('JSONCompactEachRow') -await selectJSON('JSONCompactStringsEachRow') -await selectJSON('JSONCompactEachRowWithNames') -await selectJSON('JSONCompactEachRowWithNamesAndTypes') -await selectJSON('JSONCompactStringsEachRowWithNames') -await selectJSON('JSONCompactStringsEachRowWithNamesAndTypes') +console.log("#### Streamable JSON formats:\n"); +await selectJSON("JSONEachRow"); +await selectJSON("JSONStringsEachRow"); +await selectJSON("JSONCompactEachRow"); +await selectJSON("JSONCompactStringsEachRow"); +await selectJSON("JSONCompactEachRowWithNames"); +await selectJSON("JSONCompactEachRowWithNamesAndTypes"); +await selectJSON("JSONCompactStringsEachRowWithNames"); +await selectJSON("JSONCompactStringsEachRowWithNamesAndTypes"); // These are single document ClickHouse JSON formats, which are not streamable -console.log('\n#### Single document JSON formats:\n') -await selectJSON('JSON') -await selectJSON('JSONStrings') -await selectJSON('JSONCompact') -await selectJSON('JSONCompactStrings') -await selectJSON('JSONColumnsWithMetadata') -await selectJSON('JSONObjectEachRow') +console.log("\n#### Single document JSON formats:\n"); +await selectJSON("JSON"); +await selectJSON("JSONStrings"); +await selectJSON("JSONCompact"); +await selectJSON("JSONCompactStrings"); +await selectJSON("JSONColumnsWithMetadata"); +await selectJSON("JSONObjectEachRow"); // These "raw" ClickHouse formats can be streamed as well instead of loading the entire result into the app memory; // see node/select_streaming_text_line_by_line.ts -console.log('\n#### Raw formats:\n') -await selectText('CSV') -await selectText('CSVWithNames') -await selectText('CSVWithNamesAndTypes') -await selectText('TabSeparated') -await selectText('TabSeparatedRaw') -await selectText('TabSeparatedWithNames') -await selectText('TabSeparatedWithNamesAndTypes') -await selectText('CustomSeparated') -await selectText('CustomSeparatedWithNames') -await selectText('CustomSeparatedWithNamesAndTypes') +console.log("\n#### Raw formats:\n"); +await selectText("CSV"); +await selectText("CSVWithNames"); +await selectText("CSVWithNamesAndTypes"); +await selectText("TabSeparated"); +await selectText("TabSeparatedRaw"); +await selectText("TabSeparatedWithNames"); +await selectText("TabSeparatedWithNamesAndTypes"); +await selectText("CustomSeparated"); +await selectText("CustomSeparatedWithNames"); +await selectText("CustomSeparatedWithNamesAndTypes"); // Parquet can be streamed in and out, too. // See node/select_parquet_as_file.ts, node/insert_file_stream_parquet.ts -await client.close() +await client.close(); // Selecting data in different JSON formats async function selectJSON(format: DataFormat) { const rows = await client.query({ query: `SELECT * FROM ${tableName} LIMIT 10`, // don't use FORMAT clause; specify the format separately format: format, - }) - const data = await rows.json() // get all the data at once - console.log(`Format: ${format}, parsed data:`) - console.log(data) + }); + const data = await rows.json(); // get all the data at once + console.log(`Format: ${format}, parsed data:`); + console.log(data); } // Selecting text data in different formats; `.json()` cannot be used here as it does not make sense. @@ -75,12 +75,12 @@ async function selectText(format: DataFormat) { clickhouse_settings: { // This is for CustomSeparated format demo purposes. // See also: https://clickhouse.com/docs/en/interfaces/formats#format-customseparated - format_custom_field_delimiter: ' | ', + format_custom_field_delimiter: " | ", }, - }) - const data = await rows.text() // get all the data at once - console.log(`Format: ${format}, text data:`) - console.log(data) + }); + const data = await rows.text(); // get all the data at once + console.log(`Format: ${format}, text data:`); + console.log(data); } async function prepareTestData() { @@ -91,14 +91,14 @@ async function prepareTestData() { ENGINE MergeTree() ORDER BY (id) `, - }) + }); // See also: INSERT formats overview - insert_data_formats_overview.ts await client.insert({ table: tableName, values: [ - { id: 42, name: 'foo', sku: [1, 2, 3] }, - { id: 43, name: 'bar', sku: [4, 5, 6] }, + { id: 42, name: "foo", sku: [1, 2, 3] }, + { id: 43, name: "bar", sku: [4, 5, 6] }, ], - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); } diff --git a/examples/web/coding/select_json_each_row.ts b/examples/web/coding/select_json_each_row.ts index 2ce4bbb6..d587c411 100644 --- a/examples/web/coding/select_json_each_row.ts +++ b/examples/web/coding/select_json_each_row.ts @@ -6,17 +6,17 @@ // See also: // - `select_json_with_metadata.ts` for metadata-aware JSON responses. // - `select_data_formats_overview.ts` for a broader format comparison. -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; interface Data { - number: string + number: string; } -const client = createClient() +const client = createClient(); const rows = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 5', - format: 'JSONEachRow', -}) -const result = await rows.json() -result.forEach((row) => console.log(row)) -await client.close() + query: "SELECT number FROM system.numbers LIMIT 5", + format: "JSONEachRow", +}); +const result = await rows.json(); +result.forEach((row) => console.log(row)); +await client.close(); diff --git a/examples/web/coding/select_json_with_metadata.ts b/examples/web/coding/select_json_with_metadata.ts index dc42e47a..ba7393ca 100644 --- a/examples/web/coding/select_json_with_metadata.ts +++ b/examples/web/coding/select_json_with_metadata.ts @@ -5,13 +5,13 @@ // See also: // - `select_json_each_row.ts` for row-by-row JSON output. // - `select_data_formats_overview.ts` for a broader format comparison. -import { createClient, type ResponseJSON } from '@clickhouse/client-web' +import { createClient, type ResponseJSON } from "@clickhouse/client-web"; -const client = createClient() +const client = createClient(); const rows = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 2', - format: 'JSON', -}) -const result = await rows.json>() -console.log(result) -await client.close() + query: "SELECT number FROM system.numbers LIMIT 2", + format: "JSON", +}); +const result = await rows.json>(); +console.log(result); +await client.close(); diff --git a/examples/web/coding/session_id_and_temporary_tables.ts b/examples/web/coding/session_id_and_temporary_tables.ts index caf70810..9989eaa9 100644 --- a/examples/web/coding/session_id_and_temporary_tables.ts +++ b/examples/web/coding/session_id_and_temporary_tables.ts @@ -1,24 +1,24 @@ -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; // Using a `session_id` so that a `TEMPORARY TABLE` created on one request is visible on the next. // Temporary tables only exist for the lifetime of the session and are scoped to the node that // served the CREATE — see also `session_level_commands.ts` for caveats behind load balancers. // Web variant: uses `globalThis.crypto.randomUUID()` instead of Node's `node:crypto`. -const tableName = 'temporary_example_web' +const tableName = "temporary_example_web"; const client = createClient({ session_id: globalThis.crypto.randomUUID(), -}) +}); await client.command({ query: `CREATE TEMPORARY TABLE ${tableName} (i Int32)`, -}) +}); await client.insert({ table: tableName, values: [{ i: 42 }, { i: 144 }], - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); const rs = await client.query({ query: `SELECT * FROM ${tableName}`, - format: 'JSONEachRow', -}) -console.info(await rs.json()) -await client.close() + format: "JSONEachRow", +}); +console.info(await rs.json()); +await client.close(); diff --git a/examples/web/coding/session_level_commands.ts b/examples/web/coding/session_level_commands.ts index 65dff6ee..f9229038 100644 --- a/examples/web/coding/session_level_commands.ts +++ b/examples/web/coding/session_level_commands.ts @@ -1,4 +1,4 @@ -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; // Note that session will work as expected ONLY if you are accessing the Node directly. // If there is a load-balancer in front of ClickHouse nodes, the requests might end up on different nodes, @@ -8,30 +8,30 @@ const client = createClient({ // with session_id defined, SET and other session commands // will affect all the consecutive queries session_id: globalThis.crypto.randomUUID(), -}) +}); await client.command({ query: `SET output_format_json_quote_64bit_integers = 0`, clickhouse_settings: { wait_end_of_query: 1 }, -}) +}); // this query uses output_format_json_quote_64bit_integers = 0 const rows1 = await client.query({ query: `SELECT toInt64(42)`, - format: 'JSONEachRow', -}) -console.log(await rows1.json()) + format: "JSONEachRow", +}); +console.log(await rows1.json()); await client.command({ query: `SET output_format_json_quote_64bit_integers = 1`, clickhouse_settings: { wait_end_of_query: 1 }, -}) +}); // this query uses output_format_json_quote_64bit_integers = 1 const rows2 = await client.query({ query: `SELECT toInt64(144)`, - format: 'JSONEachRow', -}) -console.log(await rows2.json()) + format: "JSONEachRow", +}); +console.log(await rows2.json()); -await client.close() +await client.close(); diff --git a/examples/web/coding/time_time64.ts b/examples/web/coding/time_time64.ts index 74ee0e8c..a5738483 100644 --- a/examples/web/coding/time_time64.ts +++ b/examples/web/coding/time_time64.ts @@ -1,15 +1,15 @@ // See also: // - https://clickhouse.com/docs/sql-reference/data-types/time // - https://clickhouse.com/docs/sql-reference/data-types/time64 -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; -const tableName = `chjs_time_time64_web` +const tableName = `chjs_time_time64_web`; const client = createClient({ clickhouse_settings: { // Since ClickHouse 25.6 enable_time_time64_type: 1, }, -}) +}); await client.command({ query: ` CREATE OR REPLACE TABLE ${tableName} @@ -24,50 +24,50 @@ await client.command({ ENGINE MergeTree ORDER BY id `, -}) +}); // Sample representation in JSONEachRow format const values = [ { id: 1, - t: '12:34:56', - t64_0: '12:34:56', - t64_3: '12:34:56.123', - t64_6: '12:34:56.123456', - t64_9: '12:34:56.123456789', + t: "12:34:56", + t64_0: "12:34:56", + t64_3: "12:34:56.123", + t64_6: "12:34:56.123456", + t64_9: "12:34:56.123456789", }, { id: 2, - t: '23:59:59', - t64_0: '23:59:59', - t64_3: '23:59:59.987', - t64_6: '23:59:59.987654', - t64_9: '23:59:59.987654321', + t: "23:59:59", + t64_0: "23:59:59", + t64_3: "23:59:59.987", + t64_6: "23:59:59.987654", + t64_9: "23:59:59.987654321", }, { id: 3, - t: '999:59:59', - t64_0: '999:59:59', - t64_3: '999:59:59.999', - t64_6: '999:59:59.999999', - t64_9: '999:59:59.999999999', + t: "999:59:59", + t64_0: "999:59:59", + t64_3: "999:59:59.999", + t64_6: "999:59:59.999999", + t64_9: "999:59:59.999999999", }, { id: 4, - t: '-999:59:59', - t64_0: '-999:59:59', - t64_3: '-999:59:59.999', - t64_6: '-999:59:59.999999', - t64_9: '-999:59:59.999999999', + t: "-999:59:59", + t64_0: "-999:59:59", + t64_3: "-999:59:59.999", + t64_6: "-999:59:59.999999", + t64_9: "-999:59:59.999999999", }, -] +]; await client.insert({ table: tableName, - format: 'JSONEachRow', + format: "JSONEachRow", values, -}) +}); const rs = await client.query({ query: `SELECT * FROM ${tableName}`, - format: 'JSONEachRow', -}) -console.log(await rs.json()) -await client.close() + format: "JSONEachRow", +}); +console.log(await rs.json()); +await client.close(); diff --git a/examples/web/coding/url_configuration.ts b/examples/web/coding/url_configuration.ts index 37053263..8534c763 100644 --- a/examples/web/coding/url_configuration.ts +++ b/examples/web/coding/url_configuration.ts @@ -1,4 +1,4 @@ -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; // URL configuration reference with all possible values. // @@ -13,36 +13,36 @@ import { createClient } from '@clickhouse/client-web' // +----------------------------------------------------------------------------------------------------------+ const url = // basic auth (username:password) + database will be extracted from the URL - 'https://bob:secret@my.host:8124/analytics?' + + "https://bob:secret@my.host:8124/analytics?" + [ // arbitrary string values - 'application=my_analytics_app', - 'session_id=random_session_id', - 'pathname=/my_proxy', // or `my_proxy`, without the leading slash + "application=my_analytics_app", + "session_id=random_session_id", + "pathname=/my_proxy", // or `my_proxy`, without the leading slash // a numeric value - 'request_timeout=60000', + "request_timeout=60000", // a numeric value; max_open_connections is expected to be at least 1 when set explicitly. - 'max_open_connections=10', + "max_open_connections=10", // boolean values can be set as 1/0 or true/false; // sets compression.request = true - 'compression_request=1', + "compression_request=1", // sets compression.response = false - 'compression_response=false', + "compression_response=false", // sets log.level = 'TRACE'; // allowed values: TRACE, DEBUG, INFO, WARN, ERROR, OFF. - 'log_level=TRACE', + "log_level=TRACE", // sets keep_alive.enabled = false - 'keep_alive_enabled=false', + "keep_alive_enabled=false", // all values prefixed with clickhouse_setting_ will be added to clickhouse_settings // this will set clickhouse_settings.async_insert = 1 - 'clickhouse_setting_async_insert=1', + "clickhouse_setting_async_insert=1", // ch_ is a shorthand for clickhouse_setting_* parameters; works similarly. // this will set clickhouse_settings.wait_for_async_insert = 0 - 'ch_wait_for_async_insert=0', + "ch_wait_for_async_insert=0", // adds a custom HTTP header 'X-CLICKHOUSE-AUTH' with 'secret_header' value to client requests // arbitrary string value - 'http_header_X-CLICKHOUSE-AUTH=secret_header', - ].join('&') + "http_header_X-CLICKHOUSE-AUTH=secret_header", + ].join("&"); /* The URL above is an equivalent to the following configuration object: { @@ -74,6 +74,6 @@ const url = */ const client = createClient({ url, -}) +}); // your queries will go here... -await client.close() +await client.close(); diff --git a/examples/web/eslint.config.mjs b/examples/web/eslint.config.mjs index 3281acd1..a2047a85 100644 --- a/examples/web/eslint.config.mjs +++ b/examples/web/eslint.config.mjs @@ -1,8 +1,8 @@ -import js from '@eslint/js' -import { defineConfig } from 'eslint/config' -import tseslint from 'typescript-eslint' -import pluginPrettier from 'eslint-plugin-prettier' -import pluginExpectType from 'eslint-plugin-expect-type/configs/recommended' +import js from "@eslint/js"; +import { defineConfig } from "eslint/config"; +import tseslint from "typescript-eslint"; +import pluginPrettier from "eslint-plugin-prettier"; +import pluginExpectType from "eslint-plugin-expect-type/configs/recommended"; export default defineConfig( // Base ESLint recommended rules @@ -11,7 +11,7 @@ export default defineConfig( ...tseslint.configs.strict, ...tseslint.configs.stylistic, { - files: ['**/*.ts'], + files: ["**/*.ts"], languageOptions: { parserOptions: { projectService: true, @@ -20,34 +20,34 @@ export default defineConfig( }, plugins: { prettier: pluginPrettier, - 'expect-type': pluginExpectType, + "expect-type": pluginExpectType, }, rules: { - 'prettier/prettier': 'error', - '@typescript-eslint/no-floating-promises': 'error', - eqeqeq: 'error', + "prettier/prettier": "error", + "@typescript-eslint/no-floating-promises": "error", + eqeqeq: "error", // Keep some rules relaxed until addressed in dedicated PRs - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/consistent-type-imports': 'warn', - '@typescript-eslint/array-type': 'off', + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/consistent-type-imports": "warn", + "@typescript-eslint/array-type": "off", }, }, { - files: ['**/*.ts'], + files: ["**/*.ts"], rules: { - 'no-console': 'off', + "no-console": "off", }, }, // Ignore build artifacts and externals { ignores: [ - 'eslint.config.mjs', - 'vitest.config.ts', - 'vitest.setup.ts', - 'coverage', - 'out', - 'dist', - 'node_modules', + "eslint.config.mjs", + "vitest.config.ts", + "vitest.setup.ts", + "coverage", + "out", + "dist", + "node_modules", ], }, -) +); diff --git a/examples/web/global.d.ts b/examples/web/global.d.ts index 7517b83c..f8b43b4f 100644 --- a/examples/web/global.d.ts +++ b/examples/web/global.d.ts @@ -1,5 +1,5 @@ /* eslint-disable no-var */ // `declare var` is the standard way to declare ambient global variables. -declare var CLICKHOUSE_CLOUD_URL: string -declare var CLICKHOUSE_CLOUD_PASSWORD: string -declare var CLICKHOUSE_CLUSTER_URL: string +declare var CLICKHOUSE_CLOUD_URL: string; +declare var CLICKHOUSE_CLOUD_PASSWORD: string; +declare var CLICKHOUSE_CLUSTER_URL: string; diff --git a/examples/web/performance/select_streaming_json_each_row.ts b/examples/web/performance/select_streaming_json_each_row.ts index 82fad0f4..a4865cd3 100644 --- a/examples/web/performance/select_streaming_json_each_row.ts +++ b/examples/web/performance/select_streaming_json_each_row.ts @@ -29,25 +29,25 @@ // JSON*EachRow formats (see the list above); it's just that the ClickHouse JSON // format and a few others are represented as a single object in the response // and cannot be streamed by the client. -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; -const client = createClient() +const client = createClient(); const rows = await client.query({ - query: 'SELECT number FROM system.numbers_mt LIMIT 5', - format: 'JSONEachRow', // or JSONCompactEachRow, JSONStringsEachRow, etc. -}) -const stream = rows.stream() -const reader = stream.getReader() + query: "SELECT number FROM system.numbers_mt LIMIT 5", + format: "JSONEachRow", // or JSONCompactEachRow, JSONStringsEachRow, etc. +}); +const stream = rows.stream(); +const reader = stream.getReader(); try { while (true) { - const { done, value: chunk } = await reader.read() - if (done) break + const { done, value: chunk } = await reader.read(); + if (done) break; chunk.forEach((row) => { - console.log(row.json()) // or `row.text` to avoid parsing JSON - }) + console.log(row.json()); // or `row.text` to avoid parsing JSON + }); } - console.log('Completed!') + console.log("Completed!"); } finally { - reader.releaseLock() + reader.releaseLock(); } -await client.close() +await client.close(); diff --git a/examples/web/schema-and-deployments/create_table_cloud.ts b/examples/web/schema-and-deployments/create_table_cloud.ts index dd553b0f..b234a5c6 100644 --- a/examples/web/schema-and-deployments/create_table_cloud.ts +++ b/examples/web/schema-and-deployments/create_table_cloud.ts @@ -1,16 +1,16 @@ -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; -if (typeof CLICKHOUSE_CLOUD_URL === 'undefined') { - throw new Error('CLICKHOUSE_CLOUD_URL is required') +if (typeof CLICKHOUSE_CLOUD_URL === "undefined") { + throw new Error("CLICKHOUSE_CLOUD_URL is required"); } -if (typeof CLICKHOUSE_CLOUD_PASSWORD === 'undefined') { - throw new Error('CLICKHOUSE_CLOUD_PASSWORD is required') +if (typeof CLICKHOUSE_CLOUD_PASSWORD === "undefined") { + throw new Error("CLICKHOUSE_CLOUD_PASSWORD is required"); } const client = createClient({ url: CLICKHOUSE_CLOUD_URL, password: CLICKHOUSE_CLOUD_PASSWORD, -}) +}); // Note that ENGINE and ON CLUSTER clauses can be omitted entirely here. // ClickHouse cloud will automatically use ReplicatedMergeTree @@ -28,6 +28,6 @@ await client.command({ clickhouse_settings: { wait_end_of_query: 1, }, -}) +}); -await client.close() +await client.close(); diff --git a/examples/web/schema-and-deployments/create_table_on_premise_cluster.ts b/examples/web/schema-and-deployments/create_table_on_premise_cluster.ts index 82055dcc..e289c676 100644 --- a/examples/web/schema-and-deployments/create_table_on_premise_cluster.ts +++ b/examples/web/schema-and-deployments/create_table_on_premise_cluster.ts @@ -1,14 +1,14 @@ -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; -if (typeof CLICKHOUSE_CLUSTER_URL === 'undefined') { - throw new Error('CLICKHOUSE_CLUSTER_URL is required') +if (typeof CLICKHOUSE_CLUSTER_URL === "undefined") { + throw new Error("CLICKHOUSE_CLUSTER_URL is required"); } // ClickHouse cluster - for example, as defined in our `docker-compose.yml` // (services `clickhouse1`/`clickhouse2` behind the `nginx` round-robin entrypoint on port 8127). const client = createClient({ url: CLICKHOUSE_CLUSTER_URL, -}) +}); await client.command({ // Sample macro definitions are located in `.docker/clickhouse/cluster/serverN_config.xml` @@ -28,6 +28,6 @@ await client.command({ clickhouse_settings: { wait_end_of_query: 1, }, -}) +}); -await client.close() +await client.close(); diff --git a/examples/web/schema-and-deployments/create_table_single_node.ts b/examples/web/schema-and-deployments/create_table_single_node.ts index 09259f53..ffc1fa80 100644 --- a/examples/web/schema-and-deployments/create_table_single_node.ts +++ b/examples/web/schema-and-deployments/create_table_single_node.ts @@ -1,7 +1,7 @@ -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; // A single ClickHouse node - for example, as in our `docker-compose.yml` -const client = createClient() +const client = createClient(); await client.command({ query: ` CREATE TABLE IF NOT EXISTS clickhouse_js_create_table_example @@ -9,5 +9,5 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) -await client.close() +}); +await client.close(); diff --git a/examples/web/schema-and-deployments/insert_ephemeral_columns.ts b/examples/web/schema-and-deployments/insert_ephemeral_columns.ts index 03c67d22..1cefd8fc 100644 --- a/examples/web/schema-and-deployments/insert_ephemeral_columns.ts +++ b/examples/web/schema-and-deployments/insert_ephemeral_columns.ts @@ -1,8 +1,8 @@ -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; // Ephemeral columns documentation: https://clickhouse.com/docs/en/sql-reference/statements/create/table#ephemeral -const tableName = 'insert_ephemeral_columns_web' -const client = createClient() +const tableName = "insert_ephemeral_columns_web"; +const client = createClient(); await client.command({ query: ` @@ -15,31 +15,31 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); await client.insert({ table: tableName, values: [ { - id: '42', - message_default: 'foo', + id: "42", + message_default: "foo", }, { - id: '144', - message_default: 'bar', + id: "144", + message_default: "bar", }, ], - format: 'JSONEachRow', + format: "JSONEachRow", // The name of the ephemeral column has to be specified here // to trigger the default values logic for the rest of the columns - columns: ['id', 'message_default'], -}) + columns: ["id", "message_default"], +}); const rows = await client.query({ query: `SELECT * FROM ${tableName}`, - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); -console.info(await rows.json()) -await client.close() +console.info(await rows.json()); +await client.close(); diff --git a/examples/web/schema-and-deployments/insert_exclude_columns.ts b/examples/web/schema-and-deployments/insert_exclude_columns.ts index 264b9775..14fe3740 100644 --- a/examples/web/schema-and-deployments/insert_exclude_columns.ts +++ b/examples/web/schema-and-deployments/insert_exclude_columns.ts @@ -1,11 +1,11 @@ -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; /** * Excluding certain columns from the INSERT statement. * For the inverse (specifying the exact columns to insert into), see `insert_specific_columns.ts`. */ -const tableName = 'insert_exclude_columns_web' -const client = createClient() +const tableName = "insert_exclude_columns_web"; +const client = createClient(); await client.command({ query: ` @@ -14,32 +14,32 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); await client.insert({ table: tableName, - values: [{ message: 'bar' }], - format: 'JSONEachRow', + values: [{ message: "bar" }], + format: "JSONEachRow", // `id` column value for this row will be zero columns: { - except: ['id'], + except: ["id"], }, -}) +}); await client.insert({ table: tableName, values: [{ id: 144 }], - format: 'JSONEachRow', + format: "JSONEachRow", // `message` column value for this row will be an empty string columns: { - except: ['message'], + except: ["message"], }, -}) +}); const rows = await client.query({ query: `SELECT * FROM ${tableName} ORDER BY id, message DESC`, - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); -console.info(await rows.json()) -await client.close() +console.info(await rows.json()); +await client.close(); diff --git a/examples/web/schema-and-deployments/url_configuration.ts b/examples/web/schema-and-deployments/url_configuration.ts index 37053263..8534c763 100644 --- a/examples/web/schema-and-deployments/url_configuration.ts +++ b/examples/web/schema-and-deployments/url_configuration.ts @@ -1,4 +1,4 @@ -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; // URL configuration reference with all possible values. // @@ -13,36 +13,36 @@ import { createClient } from '@clickhouse/client-web' // +----------------------------------------------------------------------------------------------------------+ const url = // basic auth (username:password) + database will be extracted from the URL - 'https://bob:secret@my.host:8124/analytics?' + + "https://bob:secret@my.host:8124/analytics?" + [ // arbitrary string values - 'application=my_analytics_app', - 'session_id=random_session_id', - 'pathname=/my_proxy', // or `my_proxy`, without the leading slash + "application=my_analytics_app", + "session_id=random_session_id", + "pathname=/my_proxy", // or `my_proxy`, without the leading slash // a numeric value - 'request_timeout=60000', + "request_timeout=60000", // a numeric value; max_open_connections is expected to be at least 1 when set explicitly. - 'max_open_connections=10', + "max_open_connections=10", // boolean values can be set as 1/0 or true/false; // sets compression.request = true - 'compression_request=1', + "compression_request=1", // sets compression.response = false - 'compression_response=false', + "compression_response=false", // sets log.level = 'TRACE'; // allowed values: TRACE, DEBUG, INFO, WARN, ERROR, OFF. - 'log_level=TRACE', + "log_level=TRACE", // sets keep_alive.enabled = false - 'keep_alive_enabled=false', + "keep_alive_enabled=false", // all values prefixed with clickhouse_setting_ will be added to clickhouse_settings // this will set clickhouse_settings.async_insert = 1 - 'clickhouse_setting_async_insert=1', + "clickhouse_setting_async_insert=1", // ch_ is a shorthand for clickhouse_setting_* parameters; works similarly. // this will set clickhouse_settings.wait_for_async_insert = 0 - 'ch_wait_for_async_insert=0', + "ch_wait_for_async_insert=0", // adds a custom HTTP header 'X-CLICKHOUSE-AUTH' with 'secret_header' value to client requests // arbitrary string value - 'http_header_X-CLICKHOUSE-AUTH=secret_header', - ].join('&') + "http_header_X-CLICKHOUSE-AUTH=secret_header", + ].join("&"); /* The URL above is an equivalent to the following configuration object: { @@ -74,6 +74,6 @@ const url = */ const client = createClient({ url, -}) +}); // your queries will go here... -await client.close() +await client.close(); diff --git a/examples/web/security/query_with_parameter_binding.ts b/examples/web/security/query_with_parameter_binding.ts index ea7176e0..25dfcdd2 100644 --- a/examples/web/security/query_with_parameter_binding.ts +++ b/examples/web/security/query_with_parameter_binding.ts @@ -1,4 +1,4 @@ -import { createClient, TupleParam } from '@clickhouse/client-web' +import { createClient, TupleParam } from "@clickhouse/client-web"; /** * Binding query parameters of various data types. @@ -6,7 +6,7 @@ import { createClient, TupleParam } from '@clickhouse/client-web' * For binding parameters that contain special characters (tabs, newlines, quotes, etc.), * see `query_with_parameter_binding_special_chars.ts`. */ -const client = createClient() +const client = createClient(); const resultSet = await client.query({ query: ` SELECT @@ -26,31 +26,31 @@ const resultSet = await client.query({ {var_ipv4: IPv4} AS var_ipv4, {var_null: Nullable(String)} AS var_null `, - format: 'JSONEachRow', + format: "JSONEachRow", query_params: { var_int: 10, - var_float: '10.557', + var_float: "10.557", var_str: 20, var_array: [42, 144], - var_tuple: new TupleParam([42, 'foo']), + var_tuple: new TupleParam([42, "foo"]), var_map: new Map([ - [42, ['a', 'b']], - [144, ['c', 'd']], + [42, ["a", "b"]], + [144, ["c", "d"]], ]), - var_date: '2022-01-01', - var_datetime: '2022-01-01 12:34:56', // or a Date object - var_datetime64_3: '2022-01-01 12:34:56.789', // or a Date object + var_date: "2022-01-01", + var_datetime: "2022-01-01 12:34:56", // or a Date object + var_datetime64_3: "2022-01-01 12:34:56.789", // or a Date object // NB: Date object with DateTime64(9) is still possible, // but there will be precision loss, as JS Date has only milliseconds. - var_datetime64_9: '2022-01-01 12:34:56.123456789', + var_datetime64_9: "2022-01-01 12:34:56.123456789", // It is also possible to provide DateTime64 as a timestamp. - var_datetime64_9_ts: '1651490755.123456789', - var_decimal: '123.45', - var_uuid: '01234567-89ab-cdef-0123-456789abcdef', - var_ipv4: '192.168.0.1', + var_datetime64_9_ts: "1651490755.123456789", + var_decimal: "123.45", + var_uuid: "01234567-89ab-cdef-0123-456789abcdef", + var_ipv4: "192.168.0.1", var_null: null, }, -}) -console.info('Result (different data types):', await resultSet.json()) +}); +console.info("Result (different data types):", await resultSet.json()); -await client.close() +await client.close(); diff --git a/examples/web/security/query_with_parameter_binding_special_chars.ts b/examples/web/security/query_with_parameter_binding_special_chars.ts index b458e59f..343671fa 100644 --- a/examples/web/security/query_with_parameter_binding_special_chars.ts +++ b/examples/web/security/query_with_parameter_binding_special_chars.ts @@ -1,4 +1,4 @@ -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; /** * Binding query parameters that contain special characters (tabs, newlines, quotes, backslashes, etc.). @@ -6,7 +6,7 @@ import { createClient } from '@clickhouse/client-web' * * For an overview of binding regular values of various data types, see `query_with_parameter_binding.ts`. */ -const client = createClient() +const client = createClient(); const resultSet = await client.query({ query: ` @@ -16,17 +16,17 @@ const resultSet = await client.query({ 'foo_\r_bar' = {carriage_return: String} AS has_carriage_return, 'foo_\\'_bar' = {single_quote: String} AS has_single_quote, 'foo_\\_bar' = {backslash: String} AS has_backslash`, - format: 'JSONEachRow', + format: "JSONEachRow", query_params: { - tab: 'foo_\t_bar', - newline: 'foo_\n_bar', - carriage_return: 'foo_\r_bar', + tab: "foo_\t_bar", + newline: "foo_\n_bar", + carriage_return: "foo_\r_bar", single_quote: "foo_'_bar", - backslash: 'foo_\\_bar', + backslash: "foo_\\_bar", }, -}) +}); // Should return all 1, as query params will match the strings in the SELECT. -console.info('Result (special characters):', await resultSet.json()) +console.info("Result (special characters):", await resultSet.json()); -await client.close() +await client.close(); diff --git a/examples/web/security/read_only_user.ts b/examples/web/security/read_only_user.ts index 1b206e02..d8e93c0c 100644 --- a/examples/web/security/read_only_user.ts +++ b/examples/web/security/read_only_user.ts @@ -1,14 +1,14 @@ -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; /** * An illustration of limitations and client-specific settings for users created in `READONLY = 1` mode. */ -const defaultClient = createClient() +const defaultClient = createClient(); // using the default (non-read-only) user to create a read-only one for the purposes of the example -const guid = globalThis.crypto.randomUUID().replace(/-/g, '') -const readOnlyUsername = `clickhouse_js_examples_readonly_user_${guid}` -const readOnlyPassword = `${guid}_pwd` +const guid = globalThis.crypto.randomUUID().replace(/-/g, ""); +const readOnlyUsername = `clickhouse_js_examples_readonly_user_${guid}`; +const readOnlyPassword = `${guid}_pwd`; const commands = [ ` CREATE USER ${readOnlyUsername} @@ -21,22 +21,22 @@ const commands = [ ON default.* TO ${readOnlyUsername} `, -] +]; for (const query of commands) { await defaultClient.command({ query, clickhouse_settings: { wait_end_of_query: 1, }, - }) + }); } console.log( `Created user ${readOnlyUsername} with restricted access to the system database`, -) -printSeparator() +); +printSeparator(); // and a test table with some data in there -const testTableName = 'clickhouse_js_examples_readonly_user_test_data_web' +const testTableName = "clickhouse_js_examples_readonly_user_test_data_web"; await defaultClient.command({ query: ` CREATE OR REPLACE TABLE ${testTableName} @@ -47,114 +47,114 @@ await defaultClient.command({ clickhouse_settings: { wait_end_of_query: 1, }, -}) +}); await defaultClient.insert({ table: testTableName, values: [ - { id: 12, name: 'foo' }, - { id: 42, name: 'bar' }, + { id: 12, name: "foo" }, + { id: 42, name: "bar" }, ], - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); // Read-only user let readOnlyUserClient = createClient({ username: readOnlyUsername, password: readOnlyPassword, -}) +}); // read-only user cannot insert the data into the table await readOnlyUserClient .insert({ table: testTableName, values: [ - { id: 12, name: 'foo' }, - { id: 42, name: 'bar' }, + { id: 12, name: "foo" }, + { id: 42, name: "bar" }, ], - format: 'JSONEachRow', + format: "JSONEachRow", }) .catch((err) => { console.error( - '[Expected error] Readonly user cannot insert the data into the table. Cause:\n', + "[Expected error] Readonly user cannot insert the data into the table. Cause:\n", err, - ) - }) -printSeparator() + ); + }); +printSeparator(); // ... cannot query from system.users because no grant (system.numbers will still work, though) await readOnlyUserClient .query({ - query: 'SELECT * FROM system.users LIMIT 5', - format: 'JSONEachRow', + query: "SELECT * FROM system.users LIMIT 5", + format: "JSONEachRow", }) .catch((err) => { console.error( - '[Expected error] Cannot query system.users cause it was not granted. Cause:\n', + "[Expected error] Cannot query system.users cause it was not granted. Cause:\n", err, - ) - }) -printSeparator() + ); + }); +printSeparator(); // ... can query the test table since it is granted const rs = await readOnlyUserClient.query({ query: `SELECT * FROM ${testTableName}`, - format: 'JSONEachRow', -}) -console.log('Select result:', await rs.json()) -printSeparator() + format: "JSONEachRow", +}); +console.log("Select result:", await rs.json()); +printSeparator(); // ... cannot use ClickHouse settings -await readOnlyUserClient.close() +await readOnlyUserClient.close(); readOnlyUserClient = createClient({ username: readOnlyUsername, password: readOnlyPassword, clickhouse_settings: { send_progress_in_http_headers: 1, }, -}) +}); await readOnlyUserClient .query({ query: `SELECT * FROM ${testTableName}`, - format: 'JSONEachRow', + format: "JSONEachRow", }) .catch((err) => { console.error( `[Expected error] Cannot modify 'send_progress_in_http_headers' setting in readonly mode. Cause:\n`, err, - ) - }) -printSeparator() + ); + }); +printSeparator(); // ... cannot use response compression. Request compression is still allowed. -await readOnlyUserClient.close() +await readOnlyUserClient.close(); readOnlyUserClient = createClient({ username: readOnlyUsername, password: readOnlyPassword, compression: { response: true, }, -}) +}); await readOnlyUserClient .query({ query: `SELECT * FROM ${testTableName}`, - format: 'JSONEachRow', + format: "JSONEachRow", }) .catch((err) => { console.error( `[Expected error] Cannot enable compression setting in readonly mode. Cause:\n`, err, - ) - }) -printSeparator() + ); + }); +printSeparator(); -console.log('All done!') +console.log("All done!"); -await readOnlyUserClient.close() -await defaultClient.close() +await readOnlyUserClient.close(); +await defaultClient.close(); function printSeparator() { console.log( - '------------------------------------------------------------------------', - ) + "------------------------------------------------------------------------", + ); } diff --git a/examples/web/security/role.ts b/examples/web/security/role.ts index 31ba45cc..6ad89c9f 100644 --- a/examples/web/security/role.ts +++ b/examples/web/security/role.ts @@ -1,22 +1,22 @@ -import type { ClickHouseError } from '@clickhouse/client-web' -import { createClient } from '@clickhouse/client-web' +import type { ClickHouseError } from "@clickhouse/client-web"; +import { createClient } from "@clickhouse/client-web"; /** * An example of specifying a role using query parameters * See https://clickhouse.com/docs/en/interfaces/http#setting-role-with-query-parameters */ -const format = 'JSONEachRow' -const username = 'clickhouse_js_role_user_web' -const password = 'role_user_password' -const table1 = 'clickhouse_js_role_table_1_web' -const table2 = 'clickhouse_js_role_table_2_web' +const format = "JSONEachRow"; +const username = "clickhouse_js_role_user_web"; +const password = "role_user_password"; +const table1 = "clickhouse_js_role_table_1_web"; +const table2 = "clickhouse_js_role_table_2_web"; // Create 2 tables, a role for each table allowing SELECT, and a user with access to those roles -const defaultClient = createClient() -await createOrReplaceUser(username, password) -const table1Role = await createTableAndGrantAccess(table1, username) -const table2Role = await createTableAndGrantAccess(table2, username) -await defaultClient.close() +const defaultClient = createClient(); +await createOrReplaceUser(username, password); +const table1Role = await createTableAndGrantAccess(table1, username); +const table2Role = await createTableAndGrantAccess(table2, username); +await defaultClient.close(); // Create a client using a role that only has permission to query table1 const client = createClient({ @@ -25,17 +25,17 @@ const client = createClient({ // This role will be applied to all the queries by default, // unless it is overridden in a specific method call role: table1Role, -}) +}); // Selecting from table1 is allowed using table1Role const resultSet1 = await client.query({ query: `select count(*) from ${table1}`, format, -}) +}); console.log( `Successfully queried from ${table1} using ${table1Role}. Result: `, await resultSet1.json(), -) +); // Selecting from table2 is not allowed using table1Role, // which is set by default in the client instance @@ -45,19 +45,19 @@ await client console.error( `Failed to query from ${table2}, as ${table1Role} does not have sufficient privileges. Expected error:`, e, - ) - }) + ); + }); // Override the client's role to table2Role, allowing a query to table2 const resultSet2 = await client.query({ query: `select count(*) from ${table2}`, role: table2Role, format, -}) +}); console.log( `Successfully queried from ${table2} using ${table2Role}. Result:`, await resultSet2.json(), -) +); // Selecting from table1 is no longer allowed, since table2Role is being used await client @@ -70,40 +70,40 @@ await client console.error( `Failed to query from ${table1}, as ${table2Role} does not have sufficient privileges. Expected error:`, e, - ) - }) + ); + }); // Multiple roles can be specified to allowed querying from either table const resultSet3 = await client.query({ query: `select count(*) from ${table1}`, role: [table1Role, table2Role], format, -}) +}); console.log( `Successfully queried from ${table1} using roles: [${table1Role}, ${table2Role}]. Result:`, await resultSet3.json(), -) +); const resultSet4 = await client.query({ query: `select count(*) from ${table2}`, role: [table1Role, table2Role], format, -}) +}); console.log( `Successfully queried from ${table2} using roles: [${table1Role}, ${table2Role}]. Result: `, await resultSet4.json(), -) +); -await client.close() +await client.close(); async function createOrReplaceUser(username: string, password: string) { await defaultClient.command({ query: `CREATE USER OR REPLACE ${username} IDENTIFIED WITH plaintext_password BY '${password}'`, - }) + }); } async function createTableAndGrantAccess(tableName: string, username: string) { - const role = `${tableName}_role` + const role = `${tableName}_role`; await defaultClient.command({ query: ` @@ -112,13 +112,13 @@ async function createTableAndGrantAccess(tableName: string, username: string) { ENGINE MergeTree() ORDER BY (id) `, - }) + }); - await defaultClient.command({ query: `CREATE ROLE OR REPLACE ${role}` }) + await defaultClient.command({ query: `CREATE ROLE OR REPLACE ${role}` }); await defaultClient.command({ query: `GRANT SELECT ON ${tableName} TO ${role}`, - }) - await defaultClient.command({ query: `GRANT ${role} TO ${username}` }) + }); + await defaultClient.command({ query: `GRANT ${role} TO ${username}` }); - return role + return role; } diff --git a/examples/web/troubleshooting/abort_request.ts b/examples/web/troubleshooting/abort_request.ts index 7f1bbf43..a236353e 100644 --- a/examples/web/troubleshooting/abort_request.ts +++ b/examples/web/troubleshooting/abort_request.ts @@ -1,4 +1,4 @@ -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; /** * Cancelling a request in progress. By default, this does not cancel the query on the server, only the request itself. @@ -18,19 +18,19 @@ const client = createClient({ // https://clickhouse.com/docs/operations/settings/settings#cancel_http_readonly_queries_on_client_close cancel_http_readonly_queries_on_client_close: 1, }, -}) -const controller = new AbortController() +}); +const controller = new AbortController(); const selectPromise = client .query({ - query: 'SELECT sleep(3)', - format: 'CSV', + query: "SELECT sleep(3)", + format: "CSV", abort_signal: controller.signal, }) .catch((e: unknown) => { - console.error(e) - console.info('---------------------------------------------------') - console.info('Select was aborted, see above for the error details') - }) -controller.abort() -await selectPromise -await client.close() + console.error(e); + console.info("---------------------------------------------------"); + console.info("Select was aborted, see above for the error details"); + }); +controller.abort(); +await selectPromise; +await client.close(); diff --git a/examples/web/troubleshooting/cancel_query.ts b/examples/web/troubleshooting/cancel_query.ts index b643f83c..18eec63f 100644 --- a/examples/web/troubleshooting/cancel_query.ts +++ b/examples/web/troubleshooting/cancel_query.ts @@ -1,28 +1,28 @@ -import { createClient, ClickHouseError } from '@clickhouse/client-web' +import { createClient, ClickHouseError } from "@clickhouse/client-web"; /** * An example of cancelling a long-running query on the server side. * See https://clickhouse.com/docs/en/sql-reference/statements/kill */ -const client = createClient() -const query_id = globalThis.crypto.randomUUID() +const client = createClient(); +const query_id = globalThis.crypto.randomUUID(); // Assuming a long-running query on the server. This promise is not awaited. const selectPromise = client .query({ - query: 'SELECT * FROM system.numbers', // it will never end, unless it is cancelled. - format: 'JSONEachRow', + query: "SELECT * FROM system.numbers", // it will never end, unless it is cancelled. + format: "JSONEachRow", query_id, // required in this case; should be unique. }) .catch((err: unknown) => { // An overview of possible error codes is available in the `system.errors` ClickHouse table. // In this example, the expected error code is 394 (QUERY_WAS_CANCELLED). - if (err instanceof ClickHouseError && err.code === '394') { - console.error('Got an expected ClickHouse error:', err) + if (err instanceof ClickHouseError && err.code === "394") { + console.error("Got an expected ClickHouse error:", err); } else { - console.error('Unexpected error', err) + console.error("Unexpected error", err); } - }) + }); // Similarly, a mutation can be cancelled. // See also: https://clickhouse.com/docs/en/sql-reference/statements/kill#kill-mutation @@ -31,8 +31,8 @@ await client.command({ clickhouse_settings: { wait_end_of_query: 1, }, -}) +}); // select promise will be rejected and print the error message -await selectPromise -await client.close() +await selectPromise; +await client.close(); diff --git a/examples/web/troubleshooting/custom_json_handling.ts b/examples/web/troubleshooting/custom_json_handling.ts index c8ba63a6..f2a09f57 100644 --- a/examples/web/troubleshooting/custom_json_handling.ts +++ b/examples/web/troubleshooting/custom_json_handling.ts @@ -1,4 +1,4 @@ -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; /** * Similar to `insert_js_dates.ts` but testing custom JSON handling @@ -13,36 +13,36 @@ const valueSerializer = (value: unknown): unknown => { // if you would have put this in the `replacer` parameter of JSON.stringify, (e.x: JSON.stringify(obj, replacerFn)) // it would have been an ISO string, but since we are serializing before `stringify`ing, // it will convert it before the `.toJSON()` method has been called - return value.getTime() + return value.getTime(); } - if (typeof value === 'bigint') { - return value.toString() + if (typeof value === "bigint") { + return value.toString(); } if (Array.isArray(value)) { - return value.map(valueSerializer) + return value.map(valueSerializer); } - if (typeof value === 'object' && value !== null) { + if (typeof value === "object" && value !== null) { return Object.fromEntries( Object.entries(value).map(([k, v]) => [k, valueSerializer(v)]), - ) + ); } - return value -} + return value; +}; -const tableName = 'inserts_custom_json_handling_web' +const tableName = "inserts_custom_json_handling_web"; const client = createClient({ json: { parse: JSON.parse, stringify: (obj: unknown) => JSON.stringify(valueSerializer(obj)), }, -}) +}); await client.command({ query: `DROP TABLE IF EXISTS ${tableName}`, -}) +}); await client.command({ query: ` CREATE TABLE ${tableName} @@ -50,7 +50,7 @@ await client.command({ ENGINE MergeTree() ORDER BY (id) `, -}) +}); await client.insert({ table: tableName, values: [ @@ -59,11 +59,11 @@ await client.insert({ dt: new Date(), }, ], - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); const rows = await client.query({ query: `SELECT * FROM ${tableName}`, - format: 'JSONEachRow', -}) -console.info(await rows.json()) -await client.close() + format: "JSONEachRow", +}); +console.info(await rows.json()); +await client.close(); diff --git a/examples/web/troubleshooting/long_running_queries_progress_headers.ts b/examples/web/troubleshooting/long_running_queries_progress_headers.ts index 8a0eae33..2bfeb60b 100644 --- a/examples/web/troubleshooting/long_running_queries_progress_headers.ts +++ b/examples/web/troubleshooting/long_running_queries_progress_headers.ts @@ -1,4 +1,4 @@ -import { type ClickHouseClient, createClient } from '@clickhouse/client-web' +import { type ClickHouseClient, createClient } from "@clickhouse/client-web"; /** * If you execute a long-running query without data coming in from the client, @@ -16,7 +16,7 @@ import { type ClickHouseClient, createClient } from '@clickhouse/client-web' * @see https://clickhouse.com/docs/en/operations/settings/settings#send_progress_in_http_headers * @see https://clickhouse.com/docs/en/interfaces/http */ -const tableName = 'long_running_queries_progress_headers_web' +const tableName = "long_running_queries_progress_headers_web"; const client = createClient({ /* Here we assume that: @@ -36,10 +36,10 @@ const client = createClient({ // The interval of sending these progress headers. Here it is less than 120s, // which in this example is assumed to be the LB idle connection timeout. // As it is UInt64 (UInt64 max value > Number.MAX_SAFE_INTEGER), it should be passed as a string. - http_headers_progress_interval_ms: '110000', + http_headers_progress_interval_ms: "110000", }, -}) -await createTestTable(client, tableName) +}); +await createTestTable(client, tableName); // Assuming that this is our long-long running insert, // it should not fail because of LB and the client settings described above. @@ -48,13 +48,13 @@ await client.command({ INSERT INTO ${tableName} SELECT '42', 'foobar' `, -}) +}); const rows = await client.query({ query: `SELECT * FROM ${tableName}`, - format: 'JSONEachRow', -}) -console.info('Inserted data:', await rows.json()) -await client.close() + format: "JSONEachRow", +}); +console.info("Inserted data:", await rows.json()); +await client.close(); async function createTestTable(client: ClickHouseClient, tableName: string) { try { @@ -65,10 +65,10 @@ async function createTestTable(client: ClickHouseClient, tableName: string) { ENGINE MergeTree() ORDER BY (id) `, - }) + }); } catch (err) { - console.error(`Error while creating the table ${tableName}:`, err) - await client.close() - throw err + console.error(`Error while creating the table ${tableName}:`, err); + await client.close(); + throw err; } } diff --git a/examples/web/troubleshooting/ping_non_existing_host.ts b/examples/web/troubleshooting/ping_non_existing_host.ts index f2d20fa2..cdc37822 100644 --- a/examples/web/troubleshooting/ping_non_existing_host.ts +++ b/examples/web/troubleshooting/ping_non_existing_host.ts @@ -1,4 +1,4 @@ -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; /** * This example assumes that your local port 8100 is free. @@ -17,19 +17,19 @@ import { createClient } from '@clickhouse/client-web' * - `ping_timeout.ts` - ping that times out. */ const client = createClient({ - url: 'http://localhost:8100', // non-existing host + url: "http://localhost:8100", // non-existing host request_timeout: 50, // low request_timeout to speed up the example -}) +}); // Ping does not throw an error; instead, { success: false; error: Error } is returned. -const pingResult = await client.ping() +const pingResult = await client.ping(); if (!pingResult.success) { console.info( - '[NonExistingHostPing] Ping failed as expected:', + "[NonExistingHostPing] Ping failed as expected:", pingResult.error, - ) + ); } else { console.error( - '[NonExistingHostPing] Ping was expected to fail, but it succeeded', - ) + "[NonExistingHostPing] Ping was expected to fail, but it succeeded", + ); } -await client.close() +await client.close(); diff --git a/examples/web/troubleshooting/read_only_user.ts b/examples/web/troubleshooting/read_only_user.ts index 1b206e02..d8e93c0c 100644 --- a/examples/web/troubleshooting/read_only_user.ts +++ b/examples/web/troubleshooting/read_only_user.ts @@ -1,14 +1,14 @@ -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; /** * An illustration of limitations and client-specific settings for users created in `READONLY = 1` mode. */ -const defaultClient = createClient() +const defaultClient = createClient(); // using the default (non-read-only) user to create a read-only one for the purposes of the example -const guid = globalThis.crypto.randomUUID().replace(/-/g, '') -const readOnlyUsername = `clickhouse_js_examples_readonly_user_${guid}` -const readOnlyPassword = `${guid}_pwd` +const guid = globalThis.crypto.randomUUID().replace(/-/g, ""); +const readOnlyUsername = `clickhouse_js_examples_readonly_user_${guid}`; +const readOnlyPassword = `${guid}_pwd`; const commands = [ ` CREATE USER ${readOnlyUsername} @@ -21,22 +21,22 @@ const commands = [ ON default.* TO ${readOnlyUsername} `, -] +]; for (const query of commands) { await defaultClient.command({ query, clickhouse_settings: { wait_end_of_query: 1, }, - }) + }); } console.log( `Created user ${readOnlyUsername} with restricted access to the system database`, -) -printSeparator() +); +printSeparator(); // and a test table with some data in there -const testTableName = 'clickhouse_js_examples_readonly_user_test_data_web' +const testTableName = "clickhouse_js_examples_readonly_user_test_data_web"; await defaultClient.command({ query: ` CREATE OR REPLACE TABLE ${testTableName} @@ -47,114 +47,114 @@ await defaultClient.command({ clickhouse_settings: { wait_end_of_query: 1, }, -}) +}); await defaultClient.insert({ table: testTableName, values: [ - { id: 12, name: 'foo' }, - { id: 42, name: 'bar' }, + { id: 12, name: "foo" }, + { id: 42, name: "bar" }, ], - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); // Read-only user let readOnlyUserClient = createClient({ username: readOnlyUsername, password: readOnlyPassword, -}) +}); // read-only user cannot insert the data into the table await readOnlyUserClient .insert({ table: testTableName, values: [ - { id: 12, name: 'foo' }, - { id: 42, name: 'bar' }, + { id: 12, name: "foo" }, + { id: 42, name: "bar" }, ], - format: 'JSONEachRow', + format: "JSONEachRow", }) .catch((err) => { console.error( - '[Expected error] Readonly user cannot insert the data into the table. Cause:\n', + "[Expected error] Readonly user cannot insert the data into the table. Cause:\n", err, - ) - }) -printSeparator() + ); + }); +printSeparator(); // ... cannot query from system.users because no grant (system.numbers will still work, though) await readOnlyUserClient .query({ - query: 'SELECT * FROM system.users LIMIT 5', - format: 'JSONEachRow', + query: "SELECT * FROM system.users LIMIT 5", + format: "JSONEachRow", }) .catch((err) => { console.error( - '[Expected error] Cannot query system.users cause it was not granted. Cause:\n', + "[Expected error] Cannot query system.users cause it was not granted. Cause:\n", err, - ) - }) -printSeparator() + ); + }); +printSeparator(); // ... can query the test table since it is granted const rs = await readOnlyUserClient.query({ query: `SELECT * FROM ${testTableName}`, - format: 'JSONEachRow', -}) -console.log('Select result:', await rs.json()) -printSeparator() + format: "JSONEachRow", +}); +console.log("Select result:", await rs.json()); +printSeparator(); // ... cannot use ClickHouse settings -await readOnlyUserClient.close() +await readOnlyUserClient.close(); readOnlyUserClient = createClient({ username: readOnlyUsername, password: readOnlyPassword, clickhouse_settings: { send_progress_in_http_headers: 1, }, -}) +}); await readOnlyUserClient .query({ query: `SELECT * FROM ${testTableName}`, - format: 'JSONEachRow', + format: "JSONEachRow", }) .catch((err) => { console.error( `[Expected error] Cannot modify 'send_progress_in_http_headers' setting in readonly mode. Cause:\n`, err, - ) - }) -printSeparator() + ); + }); +printSeparator(); // ... cannot use response compression. Request compression is still allowed. -await readOnlyUserClient.close() +await readOnlyUserClient.close(); readOnlyUserClient = createClient({ username: readOnlyUsername, password: readOnlyPassword, compression: { response: true, }, -}) +}); await readOnlyUserClient .query({ query: `SELECT * FROM ${testTableName}`, - format: 'JSONEachRow', + format: "JSONEachRow", }) .catch((err) => { console.error( `[Expected error] Cannot enable compression setting in readonly mode. Cause:\n`, err, - ) - }) -printSeparator() + ); + }); +printSeparator(); -console.log('All done!') +console.log("All done!"); -await readOnlyUserClient.close() -await defaultClient.close() +await readOnlyUserClient.close(); +await defaultClient.close(); function printSeparator() { console.log( - '------------------------------------------------------------------------', - ) + "------------------------------------------------------------------------", + ); } diff --git a/examples/web/vitest.config.ts b/examples/web/vitest.config.ts index 1cbbf8f8..a109243c 100644 --- a/examples/web/vitest.config.ts +++ b/examples/web/vitest.config.ts @@ -1,48 +1,48 @@ -import { defineConfig } from 'vitest/config' -import { playwright } from '@vitest/browser-playwright' +import { defineConfig } from "vitest/config"; +import { playwright } from "@vitest/browser-playwright"; export default defineConfig({ test: { - name: 'examples-web', - include: ['**/*.ts'], + name: "examples-web", + include: ["**/*.ts"], // Examples are intentionally duplicated across category folders so each // category is a self-contained "skill corpus". To keep CI runtime stable, // each example runs once from its primary location; secondary copies are // excluded below. Keep this list in sync with examples/README.md. exclude: [ - 'node_modules/**', - '**/*.d.ts', - 'vitest.config.ts', - 'vitest.setup.ts', + "node_modules/**", + "**/*.d.ts", + "vitest.config.ts", + "vitest.setup.ts", // Duplicates of `coding/` files - 'troubleshooting/ping_non_existing_host.ts', - 'troubleshooting/custom_json_handling.ts', - 'security/query_with_parameter_binding.ts', - 'security/query_with_parameter_binding_special_chars.ts', - 'schema-and-deployments/insert_ephemeral_columns.ts', - 'schema-and-deployments/insert_exclude_columns.ts', - 'schema-and-deployments/url_configuration.ts', + "troubleshooting/ping_non_existing_host.ts", + "troubleshooting/custom_json_handling.ts", + "security/query_with_parameter_binding.ts", + "security/query_with_parameter_binding_special_chars.ts", + "schema-and-deployments/insert_ephemeral_columns.ts", + "schema-and-deployments/insert_exclude_columns.ts", + "schema-and-deployments/url_configuration.ts", // Duplicate of `security/read_only_user.ts` - 'troubleshooting/read_only_user.ts', + "troubleshooting/read_only_user.ts", ], - setupFiles: ['vitest.setup.ts'], - pool: 'forks', + setupFiles: ["vitest.setup.ts"], + pool: "forks", testTimeout: 60_000, hookTimeout: 60_000, passWithNoTests: true, - reporters: ['verbose'], + reporters: ["verbose"], env: { - CLICKHOUSE_URL: process.env['CLICKHOUSE_URL'] ?? 'http://localhost:8123', - CLICKHOUSE_PASSWORD: process.env['CLICKHOUSE_PASSWORD'] ?? '', + CLICKHOUSE_URL: process.env["CLICKHOUSE_URL"] ?? "http://localhost:8123", + CLICKHOUSE_PASSWORD: process.env["CLICKHOUSE_PASSWORD"] ?? "", CLICKHOUSE_CLUSTER_URL: - process.env['CLICKHOUSE_CLUSTER_URL'] ?? 'http://localhost:8127', - CLICKHOUSE_CLOUD_URL: process.env['CLICKHOUSE_CLOUD_URL'], - CLICKHOUSE_CLOUD_PASSWORD: process.env['CLICKHOUSE_CLOUD_PASSWORD'], + process.env["CLICKHOUSE_CLUSTER_URL"] ?? "http://localhost:8127", + CLICKHOUSE_CLOUD_URL: process.env["CLICKHOUSE_CLOUD_URL"], + CLICKHOUSE_CLOUD_PASSWORD: process.env["CLICKHOUSE_CLOUD_PASSWORD"], }, browser: { enabled: true, provider: playwright(), - instances: [{ browser: 'chromium' }], + instances: [{ browser: "chromium" }], }, }, -}) +}); diff --git a/examples/web/vitest.setup.ts b/examples/web/vitest.setup.ts index 481ce885..3d50a2ac 100644 --- a/examples/web/vitest.setup.ts +++ b/examples/web/vitest.setup.ts @@ -2,9 +2,9 @@ // pattern they would use in a real browser app). When running them under Vitest, // expose the corresponding env values on `globalThis` so the bare identifiers // resolve. -const g = globalThis as Record +const g = globalThis as Record; -g['CLICKHOUSE_CLUSTER_URL'] = - import.meta.env['CLICKHOUSE_CLUSTER_URL'] ?? 'http://localhost:8127' -g['CLICKHOUSE_CLOUD_URL'] = import.meta.env['CLICKHOUSE_CLOUD_URL'] -g['CLICKHOUSE_CLOUD_PASSWORD'] = import.meta.env['CLICKHOUSE_CLOUD_PASSWORD'] +g["CLICKHOUSE_CLUSTER_URL"] = + import.meta.env["CLICKHOUSE_CLUSTER_URL"] ?? "http://localhost:8127"; +g["CLICKHOUSE_CLOUD_URL"] = import.meta.env["CLICKHOUSE_CLOUD_URL"]; +g["CLICKHOUSE_CLOUD_PASSWORD"] = import.meta.env["CLICKHOUSE_CLOUD_PASSWORD"]; diff --git a/packages/client-common/__tests__/fixtures/read_only_user.ts b/packages/client-common/__tests__/fixtures/read_only_user.ts index 63cfd47a..c3389993 100644 --- a/packages/client-common/__tests__/fixtures/read_only_user.ts +++ b/packages/client-common/__tests__/fixtures/read_only_user.ts @@ -1,19 +1,19 @@ -import type { ClickHouseClient } from '@clickhouse/client-common' -import { PRINT_DDL } from '@test/utils/test_env' +import type { ClickHouseClient } from "@clickhouse/client-common"; +import { PRINT_DDL } from "@test/utils/test_env"; import { getClickHouseTestEnvironment, getTestDatabaseName, guid, TestEnv, -} from '../utils' +} from "../utils"; export async function createReadOnlyUser(client: ClickHouseClient) { - const username = `clickhousejs__read_only_user_${guid()}` - const password = `CHJS_${guid()};` - const database = getTestDatabaseName() - const env = getClickHouseTestEnvironment() - let createUser: string - let grant: string + const username = `clickhousejs__read_only_user_${guid()}`; + const password = `CHJS_${guid()};`; + const database = getTestDatabaseName(); + const env = getClickHouseTestEnvironment(); + let createUser: string; + let grant: string; switch (env) { // requires select_sequential_consistency = 1 for immediate selects after inserts case TestEnv.Cloud: @@ -22,26 +22,26 @@ export async function createReadOnlyUser(client: ClickHouseClient) { IDENTIFIED WITH sha256_password BY '${password}' DEFAULT DATABASE ${database} SETTINGS readonly = 1, select_sequential_consistency = 1 - ` + `; grant = ` GRANT SHOW TABLES, SELECT ON ${database}.* TO ${username} - ` - break + `; + break; case TestEnv.LocalSingleNode: createUser = ` CREATE USER ${username} IDENTIFIED WITH sha256_password BY '${password}' DEFAULT DATABASE ${database} SETTINGS readonly = 1 - ` + `; grant = ` GRANT SHOW TABLES, SELECT ON ${database}.* TO ${username} - ` - break + `; + break; case TestEnv.LocalCluster: createUser = ` CREATE USER ${username} @@ -49,14 +49,14 @@ export async function createReadOnlyUser(client: ClickHouseClient) { IDENTIFIED WITH sha256_password BY '${password}' DEFAULT DATABASE ${database} SETTINGS readonly = 1 - ` + `; grant = ` GRANT ON CLUSTER '{cluster}' SHOW TABLES, SELECT ON ${database}.* TO ${username} - ` - break + `; + break; } for (const query of [createUser, grant]) { @@ -65,15 +65,15 @@ export async function createReadOnlyUser(client: ClickHouseClient) { clickhouse_settings: { wait_end_of_query: 1, }, - }) + }); if (PRINT_DDL) { - console.info(`\nCreated read-only user with DDL:\n${query}`) + console.info(`\nCreated read-only user with DDL:\n${query}`); } } return { username, password, - } + }; } diff --git a/packages/client-common/__tests__/fixtures/simple_table.ts b/packages/client-common/__tests__/fixtures/simple_table.ts index eefbaeff..6e3885bb 100644 --- a/packages/client-common/__tests__/fixtures/simple_table.ts +++ b/packages/client-common/__tests__/fixtures/simple_table.ts @@ -1,8 +1,8 @@ import type { ClickHouseClient, MergeTreeSettings, -} from '@clickhouse/client-common' -import { createTable, TestEnv } from '../utils' +} from "@clickhouse/client-common"; +import { createTable, TestEnv } from "../utils"; export function createSimpleTable( client: ClickHouseClient, @@ -10,16 +10,16 @@ export function createSimpleTable( settings: MergeTreeSettings = {}, ) { return createTable(client, (env) => { - const filteredSettings = filterSettingsBasedOnEnv(settings, env) + const filteredSettings = filterSettingsBasedOnEnv(settings, env); const _settings = Object.keys(filteredSettings).length - ? 'SETTINGS ' + + ? "SETTINGS " + Object.entries(filteredSettings) .map(([key, value]) => { - const v = typeof value === 'string' ? `'${value}'` : value - return `${key} = ${v}` + const v = typeof value === "string" ? `'${value}'` : value; + return `${key} = ${v}`; }) - .join(', ') - : '' + .join(", ") + : ""; switch (env) { // ENGINE can be omitted in the cloud statements: // it will use ReplicatedMergeTree and will add ON CLUSTER as well @@ -28,14 +28,14 @@ export function createSimpleTable( CREATE TABLE ${tableName} (id UInt64, name String, sku Array(UInt8)) ORDER BY (id) ${_settings} - ` + `; case TestEnv.LocalSingleNode: return ` CREATE TABLE ${tableName} (id UInt64, name String, sku Array(UInt8)) ENGINE MergeTree() ORDER BY (id) ${_settings} - ` + `; case TestEnv.LocalCluster: return ` CREATE TABLE ${tableName} ON CLUSTER '{cluster}' @@ -45,9 +45,9 @@ export function createSimpleTable( '{replica}' ) ORDER BY (id) ${_settings} - ` + `; } - }) + }); } function filterSettingsBasedOnEnv(settings: MergeTreeSettings, env: TestEnv) { @@ -55,14 +55,14 @@ function filterSettingsBasedOnEnv(settings: MergeTreeSettings, env: TestEnv) { case TestEnv.Cloud: // ClickHouse Cloud does not like this particular one // Local cluster, however, does. - if ('non_replicated_deduplication_window' in settings) { - const filtered = Object.assign({}, settings) - delete filtered['non_replicated_deduplication_window'] - return filtered + if ("non_replicated_deduplication_window" in settings) { + const filtered = Object.assign({}, settings); + delete filtered["non_replicated_deduplication_window"]; + return filtered; } - return settings + return settings; case TestEnv.LocalCluster: case TestEnv.LocalSingleNode: - return settings + return settings; } } diff --git a/packages/client-common/__tests__/fixtures/stream_errors.ts b/packages/client-common/__tests__/fixtures/stream_errors.ts index d87fa317..94a5ca8f 100644 --- a/packages/client-common/__tests__/fixtures/stream_errors.ts +++ b/packages/client-common/__tests__/fixtures/stream_errors.ts @@ -1,29 +1,29 @@ -import { expect } from 'vitest' +import { expect } from "vitest"; -import type { QueryParamsWithFormat } from '@clickhouse/client-common' -import { ClickHouseError } from '@clickhouse/client-common' +import type { QueryParamsWithFormat } from "@clickhouse/client-common"; +import { ClickHouseError } from "@clickhouse/client-common"; -export function streamErrorQueryParams(): QueryParamsWithFormat<'JSONEachRow'> { +export function streamErrorQueryParams(): QueryParamsWithFormat<"JSONEachRow"> { return { query: `SELECT toInt32(number) AS n, throwIf(number = 10, 'boom') AS e, sleepEachRow(0.001) FROM system.numbers LIMIT 100`, - format: 'JSONEachRow', + format: "JSONEachRow", clickhouse_settings: { // enforcing at least a few blocks, so that the response code is 200 OK - max_block_size: '1', + max_block_size: "1", // Should be false by default since 25.11; but setting explicitly to make sure // the server configuration doesn't interfere with the test. http_write_exception_in_output_format: false, }, - } + }; } export function assertError(err: Error | null) { - expect(err).toBeInstanceOf(ClickHouseError) + expect(err).toBeInstanceOf(ClickHouseError); - const chErr = err as ClickHouseError - expect(chErr.message).toContain(`boom: while executing 'FUNCTION throwIf`) - expect(chErr.code).toBe('395') + const chErr = err as ClickHouseError; + expect(chErr.message).toContain(`boom: while executing 'FUNCTION throwIf`); + expect(chErr.code).toBe("395"); } diff --git a/packages/client-common/__tests__/fixtures/table_with_fields.ts b/packages/client-common/__tests__/fixtures/table_with_fields.ts index fe844688..dfc7cf36 100644 --- a/packages/client-common/__tests__/fixtures/table_with_fields.ts +++ b/packages/client-common/__tests__/fixtures/table_with_fields.ts @@ -1,8 +1,8 @@ import type { ClickHouseClient, ClickHouseSettings, -} from '@clickhouse/client-common' -import { createTable, guid, TestEnv } from '../utils' +} from "@clickhouse/client-common"; +import { createTable, guid, TestEnv } from "../utils"; export async function createTableWithFields( client: ClickHouseClient, @@ -10,7 +10,7 @@ export async function createTableWithFields( clickhouse_settings?: ClickHouseSettings, table_name?: string, ): Promise { - const tableName = table_name ?? `test_table__${guid()}` + const tableName = table_name ?? `test_table__${guid()}`; await createTable( client, (env) => { @@ -22,14 +22,14 @@ export async function createTableWithFields( CREATE TABLE ${tableName} (id UInt32, ${fields}) ORDER BY (id) - ` + `; case TestEnv.LocalSingleNode: return ` CREATE TABLE ${tableName} (id UInt32, ${fields}) ENGINE MergeTree() ORDER BY (id) - ` + `; case TestEnv.LocalCluster: return ` CREATE TABLE ${tableName} ON CLUSTER '{cluster}' @@ -39,10 +39,10 @@ export async function createTableWithFields( '{replica}' ) ORDER BY (id) - ` + `; } }, clickhouse_settings, - ) - return tableName + ); + return tableName; } diff --git a/packages/client-common/__tests__/fixtures/test_data.ts b/packages/client-common/__tests__/fixtures/test_data.ts index 5582e07c..3a43c975 100644 --- a/packages/client-common/__tests__/fixtures/test_data.ts +++ b/packages/client-common/__tests__/fixtures/test_data.ts @@ -1,14 +1,14 @@ -import { expect } from 'vitest' -import type { ClickHouseClient } from '@clickhouse/client-common' -import { sleep } from '../utils' +import { expect } from "vitest"; +import type { ClickHouseClient } from "@clickhouse/client-common"; +import { sleep } from "../utils"; export const jsonValues = [ - { id: '42', name: 'hello', sku: [0, 1] }, - { id: '43', name: 'world', sku: [2, 3] }, - { id: '44', name: 'foo', sku: [3, 4] }, - { id: '45', name: 'bar', sku: [4, 5] }, - { id: '46', name: 'baz', sku: [6, 7] }, -] + { id: "42", name: "hello", sku: [0, 1] }, + { id: "43", name: "world", sku: [2, 3] }, + { id: "44", name: "foo", sku: [3, 4] }, + { id: "45", name: "bar", sku: [4, 5] }, + { id: "46", name: "baz", sku: [6, 7] }, +]; export async function assertJsonValues( client: ClickHouseClient, @@ -16,19 +16,19 @@ export async function assertJsonValues( tryCount = 1, tryDelayMs = 1000, ) { - let result: unknown[] = [] + let result: unknown[] = []; for (let i = 0; i < tryCount; i++) { result = await client .query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, - format: 'JSONEachRow', + format: "JSONEachRow", }) - .then((r) => r.json()) + .then((r) => r.json()); if (result.length === jsonValues.length) { - break + break; } // wait a bit before retrying - await sleep(tryDelayMs) + await sleep(tryDelayMs); } - expect(result).toEqual(jsonValues) + expect(result).toEqual(jsonValues); } diff --git a/packages/client-common/__tests__/integration/abort_request.test.ts b/packages/client-common/__tests__/integration/abort_request.test.ts index 70c70977..0f084fea 100644 --- a/packages/client-common/__tests__/integration/abort_request.test.ts +++ b/packages/client-common/__tests__/integration/abort_request.test.ts @@ -1,145 +1,145 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import type { ClickHouseClient } from '@clickhouse/client-common' -import { createTestClient, guid, sleep } from '../utils' +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import type { ClickHouseClient } from "@clickhouse/client-common"; +import { createTestClient, guid, sleep } from "../utils"; -describe('abort request', () => { - let client: ClickHouseClient +describe("abort request", () => { + let client: ClickHouseClient; beforeEach(() => { - client = createTestClient() - }) + client = createTestClient(); + }); afterEach(async () => { - await client.close() - }) + await client.close(); + }); - describe('select', () => { - it('cancels a select query after it is sent', async () => { - const controller = new AbortController() + describe("select", () => { + it("cancels a select query after it is sent", async () => { + const controller = new AbortController(); const selectPromise = client.query({ - query: 'SELECT sleep(3)', - format: 'CSV', + query: "SELECT sleep(3)", + format: "CSV", abort_signal: controller.signal, - }) + }); await new Promise((resolve) => { setTimeout(() => { - controller.abort() - resolve(undefined) - }, 50) - }) + controller.abort(); + resolve(undefined); + }, 50); + }); await expect(selectPromise).rejects.toMatchObject({ - message: expect.stringMatching('The user aborted a request'), - }) - }) + message: expect.stringMatching("The user aborted a request"), + }); + }); - it('should not throw an error when aborted the second time', async () => { - const controller = new AbortController() + it("should not throw an error when aborted the second time", async () => { + const controller = new AbortController(); const selectPromise = client.query({ - query: 'SELECT sleep(3)', - format: 'CSV', + query: "SELECT sleep(3)", + format: "CSV", abort_signal: controller.signal, - }) + }); await new Promise((resolve) => { setTimeout(() => { - controller.abort() - resolve(undefined) - }, 50) - }) + controller.abort(); + resolve(undefined); + }, 50); + }); - controller.abort('foo bar') // no-op, does not throw here + controller.abort("foo bar"); // no-op, does not throw here await expect(selectPromise).rejects.toMatchObject({ - message: expect.stringMatching('The user aborted a request'), - }) - }) + message: expect.stringMatching("The user aborted a request"), + }); + }); // FIXME: It does not work with ClickHouse Cloud. // Active queries never contain the long-running query unlike local setup. // To be revisited in https://github.com/ClickHouse/clickhouse-js/issues/177 - it.skip('ClickHouse server must cancel query on abort', async () => { - const controller = new AbortController() + it.skip("ClickHouse server must cancel query on abort", async () => { + const controller = new AbortController(); - const longRunningQuery = `SELECT sleep(3), '${guid()}'` - console.log(`Long running query: ${longRunningQuery}`) + const longRunningQuery = `SELECT sleep(3), '${guid()}'`; + console.log(`Long running query: ${longRunningQuery}`); void client .query({ query: longRunningQuery, abort_signal: controller.signal, - format: 'JSONCompactEachRow', + format: "JSONCompactEachRow", }) .catch(() => { // ignore aborted query exception - }) + }); // Long-running query should be there await assertActiveQueries(client, (queries) => { - console.log(`Active queries: ${JSON.stringify(queries, null, 2)}`) - return queries.some((q) => q.query.includes(longRunningQuery)) - }) + console.log(`Active queries: ${JSON.stringify(queries, null, 2)}`); + return queries.some((q) => q.query.includes(longRunningQuery)); + }); - controller.abort() + controller.abort(); // Long-running query should be cancelled on the server await assertActiveQueries(client, (queries) => queries.every((q) => { - console.log(`${q.query} VS ${longRunningQuery}`) - return !q.query.includes(longRunningQuery) + console.log(`${q.query} VS ${longRunningQuery}`); + return !q.query.includes(longRunningQuery); }), - ) - }) + ); + }); - it('should cancel of the select queries while keeping the others', async () => { - const controller = new AbortController() - const results: number[] = [] + it("should cancel of the select queries while keeping the others", async () => { + const controller = new AbortController(); + const results: number[] = []; const selectPromises = Promise.all( [...Array(5)].map((_, i) => { - const shouldAbort = i === 3 + const shouldAbort = i === 3; const requestPromise = client .query({ query: `SELECT sleep(0.5), ${i} AS foo`, - format: 'JSONEachRow', + format: "JSONEachRow", abort_signal: // we will cancel the request that should've yielded '3' shouldAbort ? controller.signal : undefined, }) .then((r) => r.json<{ foo: number }>()) - .then((r) => results.push(r[0].foo)) + .then((r) => results.push(r[0].foo)); // this way, the cancelled request will not cancel the others if (shouldAbort) { return requestPromise.catch(() => { // ignored - }) + }); } - return requestPromise + return requestPromise; }), - ) + ); - controller.abort() - await selectPromises + controller.abort(); + await selectPromises; - expect(results.sort((a, b) => a - b)).toEqual([0, 1, 2, 4]) - }) - }) -}) + expect(results.sort((a, b) => a - b)).toEqual([0, 1, 2, 4]); + }); + }); +}); async function assertActiveQueries( client: ClickHouseClient, assertQueries: (queries: Array<{ query: string }>) => boolean, ) { - let isRunning = true + let isRunning = true; while (isRunning) { const rs = await client.query({ - query: 'SELECT query FROM system.processes', - format: 'JSON', - }) - const queries = await rs.json<{ query: string }>() + query: "SELECT query FROM system.processes", + format: "JSON", + }); + const queries = await rs.json<{ query: string }>(); if (assertQueries(queries.data)) { - isRunning = false + isRunning = false; } else { - await sleep(100) + await sleep(100); } } } diff --git a/packages/client-common/__tests__/integration/auth.test.ts b/packages/client-common/__tests__/integration/auth.test.ts index 3a950996..dd35bf7f 100644 --- a/packages/client-common/__tests__/integration/auth.test.ts +++ b/packages/client-common/__tests__/integration/auth.test.ts @@ -6,99 +6,99 @@ import { afterAll, beforeEach, afterEach, -} from 'vitest' -import { type ClickHouseClient } from '@clickhouse/client-common' -import { createSimpleTable } from '@test/fixtures/simple_table' -import { getAuthFromEnv } from '@test/utils/env' -import { createTestClient, guid } from '../utils' +} from "vitest"; +import { type ClickHouseClient } from "@clickhouse/client-common"; +import { createSimpleTable } from "@test/fixtures/simple_table"; +import { getAuthFromEnv } from "@test/utils/env"; +import { createTestClient, guid } from "../utils"; -describe('authentication', () => { - let invalidAuthClient: ClickHouseClient +describe("authentication", () => { + let invalidAuthClient: ClickHouseClient; beforeEach(() => { invalidAuthClient = createTestClient({ - username: 'gibberish', - password: 'gibberish', - }) - }) + username: "gibberish", + password: "gibberish", + }); + }); afterEach(async () => { - await invalidAuthClient.close() - }) + await invalidAuthClient.close(); + }); - it('provides authentication error details', async () => { + it("provides authentication error details", async () => { await expect( invalidAuthClient.query({ - query: 'SELECT number FROM system.numbers LIMIT 3', + query: "SELECT number FROM system.numbers LIMIT 3", }), ).rejects.toMatchObject({ - code: '516', - type: 'AUTHENTICATION_FAILED', - message: expect.stringMatching('Authentication failed'), - }) - }) + code: "516", + type: "AUTHENTICATION_FAILED", + message: expect.stringMatching("Authentication failed"), + }); + }); - describe('request auth override', () => { - let defaultClient: ClickHouseClient + describe("request auth override", () => { + let defaultClient: ClickHouseClient; beforeAll(() => { - defaultClient = createTestClient() - }) + defaultClient = createTestClient(); + }); afterAll(async () => { - await defaultClient.close() - }) + await defaultClient.close(); + }); - let tableName: string + let tableName: string; const values = [ { - id: '1', - name: 'foo', + id: "1", + name: "foo", sku: [3, 4], }, - ] - const auth = getAuthFromEnv() + ]; + const auth = getAuthFromEnv(); - it('should with with insert and select', async () => { - tableName = `simple_table_${guid()}` - await createSimpleTable(defaultClient, tableName) + it("should with with insert and select", async () => { + tableName = `simple_table_${guid()}`; + await createSimpleTable(defaultClient, tableName); await invalidAuthClient.insert({ table: tableName, - format: 'JSONEachRow', + format: "JSONEachRow", values, auth, - }) + }); const rs = await invalidAuthClient.query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, - format: 'JSONEachRow', + format: "JSONEachRow", auth, - }) - expect(await rs.json()).toEqual(values) - }) + }); + expect(await rs.json()).toEqual(values); + }); - it('should work with command and select', async () => { - tableName = `simple_table_${guid()}` - await createSimpleTable(defaultClient, tableName) + it("should work with command and select", async () => { + tableName = `simple_table_${guid()}`; + await createSimpleTable(defaultClient, tableName); await invalidAuthClient.command({ query: `INSERT INTO ${tableName} VALUES (1, 'foo', [3, 4])`, auth, - }) + }); const rs = await invalidAuthClient.query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, - format: 'JSONEachRow', + format: "JSONEachRow", auth, - }) - expect(await rs.json()).toEqual(values) - }) + }); + expect(await rs.json()).toEqual(values); + }); - it('should work with exec', async () => { + it("should work with exec", async () => { const { stream } = await invalidAuthClient.exec({ - query: 'SELECT 42, 144 FORMAT CSV', + query: "SELECT 42, 144 FORMAT CSV", auth, - }) - let result = '' - const textDecoder = new TextDecoder() + }); + let result = ""; + const textDecoder = new TextDecoder(); // @ts-expect-error - ReadableStream (Web) or Stream.Readable (Node.js); same API. for await (const chunk of stream) { - result += textDecoder.decode(chunk, { stream: true }) + result += textDecoder.decode(chunk, { stream: true }); } - expect(result).toEqual('42,144\n') - }) - }) -}) + expect(result).toEqual("42,144\n"); + }); + }); +}); diff --git a/packages/client-common/__tests__/integration/clickhouse_settings.test.ts b/packages/client-common/__tests__/integration/clickhouse_settings.test.ts index 1bd1c906..9ac9ffbb 100644 --- a/packages/client-common/__tests__/integration/clickhouse_settings.test.ts +++ b/packages/client-common/__tests__/integration/clickhouse_settings.test.ts @@ -1,82 +1,82 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import type { ClickHouseClient, InsertParams } from '@clickhouse/client-common' -import { SettingsMap } from '@clickhouse/client-common' -import { createSimpleTable } from '../fixtures/simple_table' -import { createTestClient, guid } from '../utils' +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import type { ClickHouseClient, InsertParams } from "@clickhouse/client-common"; +import { SettingsMap } from "@clickhouse/client-common"; +import { createSimpleTable } from "../fixtures/simple_table"; +import { createTestClient, guid } from "../utils"; // TODO: cover at least all enum settings -describe('ClickHouse settings', () => { - let client: ClickHouseClient +describe("ClickHouse settings", () => { + let client: ClickHouseClient; beforeEach(() => { - client = createTestClient() - }) + client = createTestClient(); + }); afterEach(async () => { - await client.close() - }) + await client.close(); + }); - it('should work with additional_table_filters map', async () => { + it("should work with additional_table_filters map", async () => { const result = await client .query({ - query: 'SELECT * FROM system.numbers LIMIT 5', - format: 'CSV', + query: "SELECT * FROM system.numbers LIMIT 5", + format: "CSV", clickhouse_settings: { additional_table_filters: SettingsMap.from({ - 'system.numbers': 'number != 3', + "system.numbers": "number != 3", }), }, }) - .then((r) => r.text()) - expect(result).toEqual('0\n1\n2\n4\n5\n') - }) + .then((r) => r.text()); + expect(result).toEqual("0\n1\n2\n4\n5\n"); + }); // covers both command and insert settings behavior // `insert_deduplication_token` will not work without // `non_replicated_deduplication_window` merge tree table setting // on a single node ClickHouse (but will work on cluster) - it('should work with insert_deduplication_token', async () => { - const tableName = `clickhouse_settings_insert__${guid()}` + it("should work with insert_deduplication_token", async () => { + const tableName = `clickhouse_settings_insert__${guid()}`; await createSimpleTable(client, tableName, { - non_replicated_deduplication_window: '5', - }) + non_replicated_deduplication_window: "5", + }); const params: InsertParams = { table: tableName, - values: [{ id: '1', name: 'foobar', sku: [1, 2] }], - format: 'JSONEachRow', - } + values: [{ id: "1", name: "foobar", sku: [1, 2] }], + format: "JSONEachRow", + }; // See https://clickhouse.com/docs/en/operations/settings/settings/#insert_deduplication_token await client.insert({ // #1 ...params, clickhouse_settings: { - insert_deduplication_token: 'foo', + insert_deduplication_token: "foo", }, - }) + }); await client.insert({ // #2 ...params, clickhouse_settings: { - insert_deduplication_token: 'foo', + insert_deduplication_token: "foo", }, - }) + }); await client.insert({ // #3 ...params, clickhouse_settings: { - insert_deduplication_token: 'bar', + insert_deduplication_token: "bar", }, - }) + }); // we will end up with two records since #2 // is deduplicated due to the same token expect( await client .query({ query: `SELECT * FROM ${tableName}`, - format: 'JSONEachRow', + format: "JSONEachRow", }) .then((r) => r.json()), ).toEqual([ - { id: '1', name: 'foobar', sku: [1, 2] }, - { id: '1', name: 'foobar', sku: [1, 2] }, - ]) - }) -}) + { id: "1", name: "foobar", sku: [1, 2] }, + { id: "1", name: "foobar", sku: [1, 2] }, + ]); + }); +}); diff --git a/packages/client-common/__tests__/integration/config.test.ts b/packages/client-common/__tests__/integration/config.test.ts index 08dcccd7..e67a6a97 100644 --- a/packages/client-common/__tests__/integration/config.test.ts +++ b/packages/client-common/__tests__/integration/config.test.ts @@ -1,36 +1,36 @@ -import { describe, it, expect, afterEach } from 'vitest' -import type { ClickHouseClient } from '@clickhouse/client-common' -import { createTestClient } from '../utils' +import { describe, it, expect, afterEach } from "vitest"; +import type { ClickHouseClient } from "@clickhouse/client-common"; +import { createTestClient } from "../utils"; -describe('config', () => { - let client: ClickHouseClient +describe("config", () => { + let client: ClickHouseClient; afterEach(async () => { - await client.close() - }) + await client.close(); + }); it('should set request timeout with "request_timeout" setting', async () => { client = createTestClient({ request_timeout: 100, - }) + }); await expect( client.query({ - query: 'SELECT sleep(3)', + query: "SELECT sleep(3)", }), ).rejects.toMatchObject({ - message: expect.stringMatching('Timeout error.'), - }) - }) + message: expect.stringMatching("Timeout error."), + }); + }); - it('should specify the default database name on creation', async () => { + it("should specify the default database name on creation", async () => { client = createTestClient({ - database: 'system', - }) + database: "system", + }); const result = await client.query({ - query: 'SELECT * FROM numbers LIMIT 2', - format: 'TabSeparated', - }) - expect(await result.text()).toEqual('0\n1\n') - }) -}) + query: "SELECT * FROM numbers LIMIT 2", + format: "TabSeparated", + }); + expect(await result.text()).toEqual("0\n1\n"); + }); +}); diff --git a/packages/client-common/__tests__/integration/data_types.test.ts b/packages/client-common/__tests__/integration/data_types.test.ts index c8d409b7..ebd86298 100644 --- a/packages/client-common/__tests__/integration/data_types.test.ts +++ b/packages/client-common/__tests__/integration/data_types.test.ts @@ -1,386 +1,388 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' +import { describe, it, expect, beforeEach, afterEach } from "vitest"; import type { ClickHouseClient, ClickHouseSettings, -} from '@clickhouse/client-common' -import { randomUUID } from '@test/utils/guid' -import { createTableWithFields } from '../fixtures/table_with_fields' -import { createTestClient, getRandomInt, TestEnv, isOnEnv } from '../utils' +} from "@clickhouse/client-common"; +import { randomUUID } from "@test/utils/guid"; +import { createTableWithFields } from "../fixtures/table_with_fields"; +import { createTestClient, getRandomInt, TestEnv, isOnEnv } from "../utils"; -describe('data types', () => { - let client: ClickHouseClient +describe("data types", () => { + let client: ClickHouseClient; beforeEach(() => { - client = createTestClient() - }) + client = createTestClient(); + }); afterEach(async () => { - await client.close() - }) + await client.close(); + }); - it('should work with integer types', async () => { + it("should work with integer types", async () => { const values = [ { i1: 127, i2: 32767, i3: 2147483647, - i4: '9223372036854775807', - i5: '170141183460469231731687303715884105727', - i6: '57896044618658097711785492504343953926634992332820282019728792003956564819967', + i4: "9223372036854775807", + i5: "170141183460469231731687303715884105727", + i6: "57896044618658097711785492504343953926634992332820282019728792003956564819967", u1: 255, u2: 65535, u3: 4294967295, - u4: '18446744073709551615', - u5: '340282366920938463463374607431768211455', - u6: '115792089237316195423570985008687907853269984665640564039457584007913129639935', + u4: "18446744073709551615", + u5: "340282366920938463463374607431768211455", + u6: "115792089237316195423570985008687907853269984665640564039457584007913129639935", }, { i1: -128, i2: -32768, i3: -2147483648, - i4: '-9223372036854775808', - i5: '-170141183460469231731687303715884105728', - i6: '-57896044618658097711785492504343953926634992332820282019728792003956564819968', + i4: "-9223372036854775808", + i5: "-170141183460469231731687303715884105728", + i6: "-57896044618658097711785492504343953926634992332820282019728792003956564819968", u1: 120, u2: 1234, u3: 51234, - u4: '421342', - u5: '15324355', - u6: '41345135123432', + u4: "421342", + u5: "15324355", + u6: "41345135123432", }, - ] + ]; const table = await createTableWithFields( client, - 'u1 UInt8, u2 UInt16, u3 UInt32, u4 UInt64, u5 UInt128, u6 UInt256, ' + - 'i1 Int8, i2 Int16, i3 Int32, i4 Int64, i5 Int128, i6 Int256', - ) - await insertAndAssert(table, values) - }) + "u1 UInt8, u2 UInt16, u3 UInt32, u4 UInt64, u5 UInt128, u6 UInt256, " + + "i1 Int8, i2 Int16, i3 Int32, i4 Int64, i5 Int128, i6 Int256", + ); + await insertAndAssert(table, values); + }); - it('should work with floating point types', async () => { + it("should work with floating point types", async () => { const values = [ { f1: 1.234, f2: 3.35245141223232 }, { f1: -0.7968956, f2: -0.113259394344324 }, - ] - const table = await createTableWithFields(client, 'f1 Float32, f2 Float64') - await insertAndAssert(table, values) - }) - - it('should work with boolean', async () => { - const values = [{ b: true }, { b: false }] - const table = await createTableWithFields(client, 'b Boolean') - await insertAndAssert(table, values) - }) - - it('should work with strings', async () => { + ]; + const table = await createTableWithFields(client, "f1 Float32, f2 Float64"); + await insertAndAssert(table, values); + }); + + it("should work with boolean", async () => { + const values = [{ b: true }, { b: false }]; + const table = await createTableWithFields(client, "b Boolean"); + await insertAndAssert(table, values); + }); + + it("should work with strings", async () => { const values = [ - { s: 'foo', fs: 'bar' }, - { s: 'qaz', fs: 'qux' }, - ] + { s: "foo", fs: "bar" }, + { s: "qaz", fs: "qux" }, + ]; const table = await createTableWithFields( client, - 's String, fs FixedString(3)', - ) - await insertAndAssert(table, values) - }) + "s String, fs FixedString(3)", + ); + await insertAndAssert(table, values); + }); - it('should throw if a value is too large for a FixedString field', async () => { - const table = await createTableWithFields(client, 'fs FixedString(3)') + it("should throw if a value is too large for a FixedString field", async () => { + const table = await createTableWithFields(client, "fs FixedString(3)"); await expect( client.insert({ table, - values: [{ fs: 'foobar' }], - format: 'JSONEachRow', + values: [{ fs: "foobar" }], + format: "JSONEachRow", }), ).rejects.toMatchObject( expect.objectContaining({ - message: expect.stringContaining('Too large value for FixedString(3)'), + message: expect.stringContaining("Too large value for FixedString(3)"), }), - ) - }) + ); + }); - it('should work with decimals', async () => { + it("should work with decimals", async () => { const row1 = { id: 1, - d1: '1234567.89', - d2: '123456789123456.789', - d3: '1234567891234567891234567891.1234567891', - d4: '12345678912345678912345678911234567891234567891234567891.12345678911234567891', - } + d1: "1234567.89", + d2: "123456789123456.789", + d3: "1234567891234567891234567891.1234567891", + d4: "12345678912345678912345678911234567891234567891234567891.12345678911234567891", + }; const row2 = { id: 2, - d1: '12.01', - d2: '5000000.405', - d3: '1.0000000004', - d4: '42.00000000000000013007', - } + d1: "12.01", + d2: "5000000.405", + d3: "1.0000000004", + d4: "42.00000000000000013007", + }; const stringRow1 = - '1\t1234567.89\t123456789123456.789\t' + - '1234567891234567891234567891.1234567891\t' + - '12345678912345678912345678911234567891234567891234567891.12345678911234567891\n' + "1\t1234567.89\t123456789123456.789\t" + + "1234567891234567891234567891.1234567891\t" + + "12345678912345678912345678911234567891234567891234567891.12345678911234567891\n"; const stringRow2 = - '2\t12.01\t5000000.405\t1.0000000004\t42.00000000000000013007\n' + "2\t12.01\t5000000.405\t1.0000000004\t42.00000000000000013007\n"; const table = await createTableWithFields( client, - 'd1 Decimal(9, 2), d2 Decimal(18, 3), ' + - 'd3 Decimal(38, 10), d4 Decimal(76, 20)', - ) + "d1 Decimal(9, 2), d2 Decimal(18, 3), " + + "d3 Decimal(38, 10), d4 Decimal(76, 20)", + ); await client.insert({ table, values: [row1, row2], - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); const result = await client .query({ query: `SELECT * FROM ${table} ORDER BY id ASC`, - format: 'TabSeparated', + format: "TabSeparated", }) - .then((r) => r.text()) - expect(result).toEqual(stringRow1 + stringRow2) - }) - - it('should work with UUID', async () => { - const values = [{ u: randomUUID() }, { u: randomUUID() }] - const table = await createTableWithFields(client, 'u UUID') - await insertAndAssert(table, values) - }) - - describe('Dates', () => { - it('should work with strings', async () => { + .then((r) => r.text()); + expect(result).toEqual(stringRow1 + stringRow2); + }); + + it("should work with UUID", async () => { + const values = [{ u: randomUUID() }, { u: randomUUID() }]; + const table = await createTableWithFields(client, "u UUID"); + await insertAndAssert(table, values); + }); + + describe("Dates", () => { + it("should work with strings", async () => { const values = [ { - d1: '2149-06-06', - d2: '2178-04-16', - dt1: '2106-02-07 06:28:15', - dt2: '2106-02-07 06:28:15.123', - dt3: '2106-02-07 06:28:15.123456', - dt4: '2106-02-07 06:28:15.123456789', + d1: "2149-06-06", + d2: "2178-04-16", + dt1: "2106-02-07 06:28:15", + dt2: "2106-02-07 06:28:15.123", + dt3: "2106-02-07 06:28:15.123456", + dt4: "2106-02-07 06:28:15.123456789", }, { - d1: '2022-09-01', - d2: '2007-01-29', - dt1: '2022-09-01 01:40:42', - dt2: '2021-10-02 03:12:42.123', - dt3: '2022-12-15 07:10:42.123456', - dt4: '2008-04-05 03:45:42.123456789', + d1: "2022-09-01", + d2: "2007-01-29", + dt1: "2022-09-01 01:40:42", + dt2: "2021-10-02 03:12:42.123", + dt3: "2022-12-15 07:10:42.123456", + dt4: "2008-04-05 03:45:42.123456789", }, - ] + ]; const table = await createTableWithFields( client, - 'd1 Date, d2 Date32, dt1 DateTime, ' + - 'dt2 DateTime64(3), dt3 DateTime64(6), dt4 DateTime64(9)', - ) - await insertAndAssert(table, values) - }) + "d1 Date, d2 Date32, dt1 DateTime, " + + "dt2 DateTime64(3), dt3 DateTime64(6), dt4 DateTime64(9)", + ); + await insertAndAssert(table, values); + }); // NB: JS Date objects work only with DateTime* fields - it('should work with JS Date objects', async () => { + it("should work with JS Date objects", async () => { const values = [ { - dt1: new Date('2106-02-07T06:28:15Z'), + dt1: new Date("2106-02-07T06:28:15Z"), // JS Date is millis only - dt2: new Date('2106-02-07T06:28:15.123Z'), - dt3: new Date('2106-02-07T06:28:15.123Z'), - dt4: new Date('2106-02-07T06:28:15.123Z'), + dt2: new Date("2106-02-07T06:28:15.123Z"), + dt3: new Date("2106-02-07T06:28:15.123Z"), + dt4: new Date("2106-02-07T06:28:15.123Z"), }, { - dt1: new Date('2022-09-01T01:40:42Z'), - dt2: new Date('2021-10-02T03:12:42.123Z'), - dt3: new Date('2022-12-15T07:10:42.123Z'), - dt4: new Date('2008-04-05T03:45:42.123Z'), + dt1: new Date("2022-09-01T01:40:42Z"), + dt2: new Date("2021-10-02T03:12:42.123Z"), + dt3: new Date("2022-12-15T07:10:42.123Z"), + dt4: new Date("2008-04-05T03:45:42.123Z"), }, - ] + ]; const expected = [ { - dt1: '2106-02-07 06:28:15', + dt1: "2106-02-07 06:28:15", // JS Date is millis only - dt2: '2106-02-07 06:28:15.123', - dt3: '2106-02-07 06:28:15.123000', - dt4: '2106-02-07 06:28:15.123000000', + dt2: "2106-02-07 06:28:15.123", + dt3: "2106-02-07 06:28:15.123000", + dt4: "2106-02-07 06:28:15.123000000", }, { - dt1: '2022-09-01 01:40:42', - dt2: '2021-10-02 03:12:42.123', - dt3: '2022-12-15 07:10:42.123000', - dt4: '2008-04-05 03:45:42.123000000', + dt1: "2022-09-01 01:40:42", + dt2: "2021-10-02 03:12:42.123", + dt3: "2022-12-15 07:10:42.123000", + dt4: "2008-04-05 03:45:42.123000000", }, - ] + ]; const table = await createTableWithFields( client, - 'dt1 DateTime, dt2 DateTime64(3), dt3 DateTime64(6), dt4 DateTime64(9)', - ) + "dt1 DateTime, dt2 DateTime64(3), dt3 DateTime64(6), dt4 DateTime64(9)", + ); await insertData(table, values, { // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z') - date_time_input_format: 'best_effort', - }) - await assertData(table, expected) - }) - }) - - it('should work with custom JSON handling (BigInt and Date)', async () => { - const TEST_BIGINT = BigInt(25000000000000000) - const TEST_DATE = new Date('2023-12-06T10:54:48.123Z') + date_time_input_format: "best_effort", + }); + await assertData(table, expected); + }); + }); + + it("should work with custom JSON handling (BigInt and Date)", async () => { + const TEST_BIGINT = BigInt(25000000000000000); + const TEST_DATE = new Date("2023-12-06T10:54:48.123Z"); const values = [ { big_id: TEST_BIGINT, dt: TEST_DATE, }, - ] + ]; const valueSerializer = (value: unknown): unknown => { if (value instanceof Date) { - return value.getTime() + return value.getTime(); } - if (typeof value === 'bigint') { - return value.toString() + if (typeof value === "bigint") { + return value.toString(); } if (Array.isArray(value)) { - return value.map(valueSerializer) + return value.map(valueSerializer); } - if (value && typeof value === 'object') { + if (value && typeof value === "object") { return Object.fromEntries( Object.entries(value).map(([k, v]) => [k, valueSerializer(v)]), - ) + ); } - return value - } + return value; + }; // modify the client to handle BigInt and Date serialization client = createTestClient({ json: { parse: JSON.parse, stringify: (obj: unknown) => { - const seralized = valueSerializer(obj) - return JSON.stringify(seralized) + const seralized = valueSerializer(obj); + return JSON.stringify(seralized); }, }, - }) + }); const table = await createTableWithFields( client, "big_id UInt64, dt DateTime64(3, 'UTC')", - ) + ); await insertAndAssert(table, values, {}, [ { - dt: TEST_DATE.toISOString().replace('T', ' ').replace('Z', ''), // clickhouse returns DateTime64 in UTC without timezone info + dt: TEST_DATE.toISOString().replace("T", " ").replace("Z", ""), // clickhouse returns DateTime64 in UTC without timezone info big_id: TEST_BIGINT.toString(), // clickhouse by default returns UInt64 as string to be safe }, - ]) - }) + ]); + }); - it('should work with string enums', async () => { + it("should work with string enums", async () => { const values = [ - { e1: 'Foo', e2: 'Qaz' }, - { e1: 'Bar', e2: 'Qux' }, - ] + { e1: "Foo", e2: "Qaz" }, + { e1: "Bar", e2: "Qux" }, + ]; const table = await createTableWithFields( client, `e1 Enum('Foo', 'Bar'), e2 Enum('Qaz', 'Qux')`, - ) - await insertAndAssert(table, values) - }) + ); + await insertAndAssert(table, values); + }); - it('should work with numeric enums', async () => { + it("should work with numeric enums", async () => { const values = [ { e1: 42, e2: 100 }, { e1: 43, e2: 127 }, - ] + ]; const table = await createTableWithFields( client, `e1 Enum('Foo' = 42, 'Bar' = 43), e2 Enum('Qaz' = 100, 'Qux' = 127)`, - ) - await insertData(table, values) + ); + await insertData(table, values); const result = await client .query({ query: `SELECT CAST(e1, 'Int8') AS e1, CAST(e2, 'Int8') AS e2 FROM ${table} ORDER BY id ASC`, - format: 'JSONEachRow', + format: "JSONEachRow", }) - .then((r) => r.json()) - expect(result).toEqual(values) - }) + .then((r) => r.json()); + expect(result).toEqual(values); + }); - it('should work with low cardinality', async () => { + it("should work with low cardinality", async () => { const values = [ { - s: 'foo', - fs: 'bar', + s: "foo", + fs: "bar", }, { - s: 'qaz', - fs: 'qux', + s: "qaz", + fs: "qux", }, - ] + ]; const table = await createTableWithFields( client, - 's LowCardinality(String), fs LowCardinality(FixedString(3))', - ) - await insertAndAssert(table, values) - }) + "s LowCardinality(String), fs LowCardinality(FixedString(3))", + ); + await insertAndAssert(table, values); + }); - it('should work with tuples', async () => { + it("should work with tuples", async () => { const values = [ - { t1: ['foo', 42], t2: ['2022-01-04', [1, 2]] }, - { t1: ['bar', 43], t2: ['2015-04-15', [3, 4]] }, - ] + { t1: ["foo", 42], t2: ["2022-01-04", [1, 2]] }, + { t1: ["bar", 43], t2: ["2015-04-15", [3, 4]] }, + ]; const table = await createTableWithFields( client, - 't1 Tuple(String, Int32), t2 Tuple(Date, Array(Int32))', - ) - await insertAndAssert(table, values) - }) + "t1 Tuple(String, Int32), t2 Tuple(Date, Array(Int32))", + ); + await insertAndAssert(table, values); + }); - it('should work with nullable types', async () => { + it("should work with nullable types", async () => { const values = [ - { s: 'foo', i: null, a: [null, null] }, + { s: "foo", i: null, a: [null, null] }, { s: null, i: 42, a: [51, null] }, - ] + ]; const table = await createTableWithFields( client, - 's Nullable(String), i Nullable(Int32), a Array(Nullable(Int32))', - ) - await insertAndAssert(table, values) - }) + "s Nullable(String), i Nullable(Int32), a Array(Nullable(Int32))", + ); + await insertAndAssert(table, values); + }); - it('should work with IP', async () => { + it("should work with IP", async () => { const values = [ { - ip1: '68.172.195.211', - ip2: 'f984:5f0b:bf33:e2db:16cd:567c:c1b3:20c4', + ip1: "68.172.195.211", + ip2: "f984:5f0b:bf33:e2db:16cd:567c:c1b3:20c4", }, { - ip1: '184.232.227.132', - ip2: '2150:c3d5:f9e0:cdee:a94f:4580:d939:3901', + ip1: "184.232.227.132", + ip2: "2150:c3d5:f9e0:cdee:a94f:4580:d939:3901", }, - ] - const table = await createTableWithFields(client, 'ip1 IPv4, ip2 IPv6') - await insertAndAssert(table, values) - }) + ]; + const table = await createTableWithFields(client, "ip1 IPv4, ip2 IPv6"); + await insertAndAssert(table, values); + }); - it('should work with ((very) nested) arrays', async () => { + it("should work with ((very) nested) arrays", async () => { // it's the largest reasonable nesting value (data is generated within 50 ms); // 25 here can already tank the performance to ~500ms only to generate the data; // 50 simply times out :) // FIXME: investigate fetch max body length // (reduced 20 to 10 cause the body was too large and fetch failed) - const maxNestingLevel = 10 + const maxNestingLevel = 10; function genNestedArray(level: number): unknown { if (level === 1) { return [...Array(getRandomInt(2, 4))].map(() => Math.random().toString(36).slice(2), - ) + ); } - return [...Array(getRandomInt(1, 3))].map(() => genNestedArray(level - 1)) + return [...Array(getRandomInt(1, 3))].map(() => + genNestedArray(level - 1), + ); } function genArrayType(level: number): string { if (level === 0) { - return 'String' + return "String"; } - return `Array(${genArrayType(level - 1)})` + return `Array(${genArrayType(level - 1)})`; } const values = [ @@ -388,12 +390,12 @@ describe('data types', () => { a1: [42, 43], a2: [ [ - ['qaz', 144], - ['qux', 1024], + ["qaz", 144], + ["qux", 1024], ], [ - ['qwerty', 102], - ['dvorak', -500], + ["qwerty", 102], + ["dvorak", -500], ], ], a3: genNestedArray(maxNestingLevel), @@ -402,114 +404,114 @@ describe('data types', () => { a1: [44, 56], a2: [ [ - ['rtx', 60], - ['RDNA', 100], + ["rtx", 60], + ["RDNA", 100], ], [ - ['zen', 42], - ['core', 400], + ["zen", 42], + ["core", 400], ], ], a3: genNestedArray(maxNestingLevel), }, - ] + ]; const fields = - 'a1 Array(Int32), a2 Array(Array(Tuple(String, Int32))), ' + - `a3 ${genArrayType(maxNestingLevel)}` - const table = await createTableWithFields(client, fields) - await insertAndAssert(table, values) - }) + "a1 Array(Int32), a2 Array(Array(Tuple(String, Int32))), " + + `a3 ${genArrayType(maxNestingLevel)}`; + const table = await createTableWithFields(client, fields); + await insertAndAssert(table, values); + }); - it('should work with ((very) nested) maps', async () => { - const maxNestingLevel = 10 + it("should work with ((very) nested) maps", async () => { + const maxNestingLevel = 10; function genNestedMap(level: number): unknown { - const obj: Record = {} + const obj: Record = {}; if (level === 1) { - ;[...Array(getRandomInt(2, 4))].forEach( + [...Array(getRandomInt(2, 4))].forEach( () => (obj[getRandomInt(1, 1000)] = Math.random().toString(36).slice(2)), - ) - return obj + ); + return obj; } - ;[...Array(getRandomInt(1, 3))].forEach( + [...Array(getRandomInt(1, 3))].forEach( () => (obj[getRandomInt(1, 1000)] = genNestedMap(level - 1)), - ) - return obj + ); + return obj; } function genMapType(level: number): string { if (level === 0) { - return 'String' + return "String"; } - return `Map(Int32, ${genMapType(level - 1)})` + return `Map(Int32, ${genMapType(level - 1)})`; } const values = [ { - m1: { foo: 'bar', qwe: 'rty' }, - m2: { 1: '2', 3: '4' }, + m1: { foo: "bar", qwe: "rty" }, + m2: { 1: "2", 3: "4" }, m3: genNestedMap(maxNestingLevel), }, { - m1: { qaz: 'qux', sub: 'q' }, - m2: { 3: '4', 4: '5' }, + m1: { qaz: "qux", sub: "q" }, + m2: { 3: "4", 4: "5" }, m3: {}, }, - ] + ]; const table = await createTableWithFields( client, - 'm1 Map(String, String), m2 Map(Int32, Int64), ' + + "m1 Map(String, String), m2 Map(Int32, Int64), " + `m3 ${genMapType(maxNestingLevel)}`, - ) - await insertAndAssert(table, values) - }) + ); + await insertAndAssert(table, values); + }); - it('should work with (simple) aggregation functions', async () => { + it("should work with (simple) aggregation functions", async () => { const values = [ { route: 53, distance: 20.96 }, { route: 54, distance: 100.52 }, { route: 55, distance: 4.05 }, - ] + ]; const table = await createTableWithFields( client, `route Int32, distance Decimal(10, 2)`, - ) + ); await client.insert({ table, values, - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); expect( await client .query({ query: `SELECT sum(distance) FROM ${table}`, - format: 'TabSeparated', + format: "TabSeparated", }) .then((r) => r.text()), - ).toEqual('125.53\n') + ).toEqual("125.53\n"); expect( await client .query({ query: `SELECT max(distance) FROM ${table}`, - format: 'TabSeparated', + format: "TabSeparated", }) .then((r) => r.text()), - ).toEqual('100.52\n') + ).toEqual("100.52\n"); expect( await client .query({ query: `SELECT uniqExact(distance) FROM ${table}`, - format: 'TabSeparated', + format: "TabSeparated", }) .then((r) => r.text()), - ).toEqual('3\n') - }) + ).toEqual("3\n"); + }); - it('should work with geo', async () => { + it("should work with geo", async () => { const values = [ { p: [42, 144], @@ -556,18 +558,18 @@ describe('data types', () => { ], ], }, - ] + ]; const table = await createTableWithFields( client, - 'p Point, r Ring, pg Polygon, mpg MultiPolygon', - ) - await insertAndAssert(table, values) - }) + "p Point, r Ring, pg Polygon, mpg MultiPolygon", + ); + await insertAndAssert(table, values); + }); // New experimental JSON type // https://clickhouse.com/docs/en/sql-reference/data-types/newjson it.skipIf(!isOnEnv(TestEnv.LocalSingleNode, TestEnv.LocalCluster))( - 'should work with (new) JSON', + "should work with (new) JSON", async () => { const values = [ { @@ -576,77 +578,77 @@ describe('data types', () => { { o: { a: 2, b: { c: 3, d: [4, 5, 6] } }, }, - ] + ]; const table = await createTableWithFields(client, `o JSON`, { allow_experimental_json_type: 1, - }) + }); await insertAndAssert(table, values, { output_format_json_quote_64bit_integers: 0, - }) + }); }, - ) + ); // New experimental Variant type // https://clickhouse.com/docs/en/sql-reference/data-types/variant it.skipIf(!isOnEnv(TestEnv.LocalSingleNode, TestEnv.LocalCluster))( - 'should work with Variant', + "should work with Variant", async () => { - const values = [{ var: 'foo' }, { var: 42 }] + const values = [{ var: "foo" }, { var: 42 }]; const table = await createTableWithFields( client, `var Variant(String, Int32)`, { allow_experimental_variant_type: 1, }, - ) - await insertAndAssert(table, values) + ); + await insertAndAssert(table, values); }, - ) + ); // New experimental Dynamic type // https://clickhouse.com/docs/en/sql-reference/data-types/dynamic it.skipIf(!isOnEnv(TestEnv.LocalSingleNode, TestEnv.LocalCluster))( - 'should work with Dynamic', + "should work with Dynamic", async () => { - const values = [{ dyn: 'foo' }, { dyn: { bar: 'qux' } }] + const values = [{ dyn: "foo" }, { dyn: { bar: "qux" } }]; const table = await createTableWithFields(client, `dyn Dynamic`, { allow_experimental_dynamic_type: 1, - }) - await insertAndAssert(table, values) + }); + await insertAndAssert(table, values); }, - ) + ); - describe('Nested', () => { - it('should work by default', async () => { + describe("Nested", () => { + it("should work by default", async () => { const values = [ { id: 1, - 'n.id': [42], - 'n.name': ['foo'], - 'n.createdAt': ['2001-04-23 00:00:00'], - 'n.roles': [['User']], + "n.id": [42], + "n.name": ["foo"], + "n.createdAt": ["2001-04-23 00:00:00"], + "n.roles": [["User"]], }, { id: 2, - 'n.id': [43], - 'n.name': ['bar'], - 'n.createdAt': ['2000-01-12 00:00:00'], - 'n.roles': [['Admin']], + "n.id": [43], + "n.name": ["bar"], + "n.createdAt": ["2000-01-12 00:00:00"], + "n.roles": [["Admin"]], }, - ] - await insertAndAssertNestedValues(values, {}, {}) - }) + ]; + await insertAndAssertNestedValues(values, {}, {}); + }); - it('should work with nested (flatten_nested = 0)', async () => { + it("should work with nested (flatten_nested = 0)", async () => { const values = [ { id: 1, n: [ { id: 42, - name: 'foo', - createdAt: '2001-04-23 00:00:00', - roles: ['User'], + name: "foo", + createdAt: "2001-04-23 00:00:00", + roles: ["User"], }, ], }, @@ -655,56 +657,56 @@ describe('data types', () => { n: [ { id: 43, - name: 'bar', - createdAt: '2000-01-12 00:00:00', - roles: ['Admin'], + name: "bar", + createdAt: "2000-01-12 00:00:00", + roles: ["Admin"], }, ], }, - ] - await insertAndAssertNestedValues(values, { flatten_nested: 0 }, {}) - }) + ]; + await insertAndAssertNestedValues(values, { flatten_nested: 0 }, {}); + }); - it('should work with nested (input_format_import_nested_json = 1)', async () => { + it("should work with nested (input_format_import_nested_json = 1)", async () => { const values = [ { id: 1, n: { id: [42], - name: ['foo'], - createdAt: ['2001-04-23 00:00:00'], - roles: [['User']], + name: ["foo"], + createdAt: ["2001-04-23 00:00:00"], + roles: [["User"]], }, }, { id: 2, n: { id: [43], - name: ['bar'], - createdAt: ['2000-01-12 00:00:00'], - roles: [['Admin']], + name: ["bar"], + createdAt: ["2000-01-12 00:00:00"], + roles: [["Admin"]], }, }, - ] + ]; await insertAndAssertNestedValues( values, {}, { input_format_import_nested_json: 1, }, - ) - }) + ); + }); - it('should work with nested with (flatten_nested = 0 and input_format_import_nested_json = 1)', async () => { + it("should work with nested with (flatten_nested = 0 and input_format_import_nested_json = 1)", async () => { const values = [ { id: 1, n: [ { id: 42, - name: 'foo', - createdAt: '2001-04-23 00:00:00', - roles: ['User'], + name: "foo", + createdAt: "2001-04-23 00:00:00", + roles: ["User"], }, ], }, @@ -713,21 +715,21 @@ describe('data types', () => { n: [ { id: 43, - name: 'bar', - createdAt: '2000-01-12 00:00:00', - roles: ['Admin'], + name: "bar", + createdAt: "2000-01-12 00:00:00", + roles: ["Admin"], }, ], }, - ] + ]; await insertAndAssertNestedValues( values, { flatten_nested: 0 }, { input_format_import_nested_json: 1, }, - ) - }) + ); + }); async function insertAndAssertNestedValues( values: unknown[], @@ -736,53 +738,53 @@ describe('data types', () => { ) { const table = await createTableWithFields( client, - 'n Nested(id UInt32, name String, createdAt DateTime, ' + + "n Nested(id UInt32, name String, createdAt DateTime, " + `roles Array(Enum('User', 'Admin')))`, createTableSettings, - ) + ); await client.insert({ table, values, clickhouse_settings: insertSettings, - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); const result = await client .query({ query: `SELECT n.id, n.name, n.createdAt, n.roles FROM ${table} ORDER BY id ASC`, - format: 'JSONEachRow', + format: "JSONEachRow", }) - .then((r) => r.json()) + .then((r) => r.json()); expect(result).toEqual([ { - 'n.id': [42], - 'n.name': ['foo'], - 'n.createdAt': ['2001-04-23 00:00:00'], - 'n.roles': [['User']], + "n.id": [42], + "n.name": ["foo"], + "n.createdAt": ["2001-04-23 00:00:00"], + "n.roles": [["User"]], }, { - 'n.id': [43], - 'n.name': ['bar'], - 'n.createdAt': ['2000-01-12 00:00:00'], - 'n.roles': [['Admin']], + "n.id": [43], + "n.name": ["bar"], + "n.createdAt": ["2000-01-12 00:00:00"], + "n.roles": [["Admin"]], }, - ]) + ]); } - }) + }); async function insertData( table: string, data: T[], clickhouse_settings?: ClickHouseSettings, ) { - const values = data.map((v, i) => ({ ...v, id: i + 1 })) + const values = data.map((v, i) => ({ ...v, id: i + 1 })); await client.insert({ - format: 'JSONEachRow', + format: "JSONEachRow", table, values, clickhouse_settings, - }) + }); } async function assertData( @@ -795,11 +797,11 @@ describe('data types', () => { query: `SELECT * EXCEPT (id) FROM ${table} ORDER BY id ASC`, - format: 'JSONEachRow', + format: "JSONEachRow", clickhouse_settings, }) - .then((r) => r.json()) - expect(result).toEqual(data) + .then((r) => r.json()); + expect(result).toEqual(data); } async function insertAndAssert( @@ -808,7 +810,7 @@ describe('data types', () => { clickhouse_settings: ClickHouseSettings = {}, expectedDataBack?: unknown[], ) { - await insertData(table, data, clickhouse_settings) - await assertData(table, expectedDataBack ?? data, clickhouse_settings) + await insertData(table, data, clickhouse_settings); + await assertData(table, expectedDataBack ?? data, clickhouse_settings); } -}) +}); diff --git a/packages/client-common/__tests__/integration/date_time.test.ts b/packages/client-common/__tests__/integration/date_time.test.ts index 7e6ee1ec..6584b269 100644 --- a/packages/client-common/__tests__/integration/date_time.test.ts +++ b/packages/client-common/__tests__/integration/date_time.test.ts @@ -1,84 +1,84 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import type { ClickHouseClient } from '@clickhouse/client-common' -import { createTableWithFields } from '../fixtures/table_with_fields' -import { createTestClient } from '../utils' +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import type { ClickHouseClient } from "@clickhouse/client-common"; +import { createTableWithFields } from "../fixtures/table_with_fields"; +import { createTestClient } from "../utils"; -describe('DateTime', () => { - let client: ClickHouseClient +describe("DateTime", () => { + let client: ClickHouseClient; beforeEach(() => { - client = createTestClient() - }) + client = createTestClient(); + }); afterEach(async () => { - await client.close() - }) + await client.close(); + }); - describe('Date', () => { - it('should insert Date and get it back', async () => { + describe("Date", () => { + it("should insert Date and get it back", async () => { // currently, there is no way to insert a Date as a number via HTTP // the conversion is not performed automatically like in VALUES clause - const table = await createTableWithFields(client, 'd Date') + const table = await createTableWithFields(client, "d Date"); await client.insert({ table, - values: [{ d: '2022-09-05' }], - format: 'JSONEachRow', - }) + values: [{ d: "2022-09-05" }], + format: "JSONEachRow", + }); expect( await client .query({ query: `SELECT * EXCEPT id FROM ${table}`, - format: 'JSONEachRow', + format: "JSONEachRow", }) .then((r) => r.json()), - ).toEqual([{ d: '2022-09-05' }]) - }) - }) + ).toEqual([{ d: "2022-09-05" }]); + }); + }); - describe('Date32', () => { - it('should insert Date32 and get it back', async () => { + describe("Date32", () => { + it("should insert Date32 and get it back", async () => { // currently, there is no way to insert a Date32 as a number via HTTP // the conversion is not performed automatically like in VALUES clause - const table = await createTableWithFields(client, 'd Date32') + const table = await createTableWithFields(client, "d Date32"); await client.insert({ table, - values: [{ d: '2022-09-05' }], - format: 'JSONEachRow', - }) + values: [{ d: "2022-09-05" }], + format: "JSONEachRow", + }); expect( await client .query({ query: `SELECT * EXCEPT id FROM ${table}`, - format: 'JSONEachRow', + format: "JSONEachRow", }) .then((r) => r.json()), - ).toEqual([{ d: '2022-09-05' }]) - }) - }) + ).toEqual([{ d: "2022-09-05" }]); + }); + }); - describe('DateTime', () => { - it('should insert DateTime and get it back', async () => { - const table = await createTableWithFields(client, 'd DateTime') + describe("DateTime", () => { + it("should insert DateTime and get it back", async () => { + const table = await createTableWithFields(client, "d DateTime"); await client.insert({ table, values: [ { d: 1662328969 }, // 2022-09-05 00:02:49 GMT+0200 - { d: '2022-09-05 00:02:49' }, // assumes column timezone (UTC by default) + { d: "2022-09-05 00:02:49" }, // assumes column timezone (UTC by default) ], - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); expect( await client .query({ query: `SELECT d FROM ${table}`, - format: 'JSONEachRow', + format: "JSONEachRow", }) .then((r) => r.json()), ).toEqual([ - { d: '2022-09-04 22:02:49' }, // converted to UTC on the server - { d: '2022-09-05 00:02:49' }, // this one was assumed UTC upon insertion - ]) + { d: "2022-09-04 22:02:49" }, // converted to UTC on the server + { d: "2022-09-05 00:02:49" }, // this one was assumed UTC upon insertion + ]); // toDateTime using Amsterdam timezone // should add 2 hours to each of the inserted dates @@ -86,37 +86,37 @@ describe('DateTime', () => { await client .query({ query: `SELECT toDateTime(d, 'Europe/Amsterdam') AS d FROM ${table}`, - format: 'JSONEachRow', + format: "JSONEachRow", }) .then((r) => r.json()), - ).toEqual([{ d: '2022-09-05 00:02:49' }, { d: '2022-09-05 02:02:49' }]) - }) + ).toEqual([{ d: "2022-09-05 00:02:49" }, { d: "2022-09-05 02:02:49" }]); + }); - it('should insert DateTime and get it back (different timezone)', async () => { + it("should insert DateTime and get it back (different timezone)", async () => { const table = await createTableWithFields( client, `d DateTime('Asia/Istanbul')`, - ) + ); await client.insert({ table, values: [ { d: 1662328969 }, // 2022-09-05 00:02:49 GMT+0200 - { d: '2022-09-05 00:02:49' }, // assumes column timezone (Asia/Istanbul) + { d: "2022-09-05 00:02:49" }, // assumes column timezone (Asia/Istanbul) ], - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); expect( await client .query({ query: `SELECT * EXCEPT id FROM ${table}`, - format: 'JSONEachRow', + format: "JSONEachRow", }) .then((r) => r.json()), ).toEqual([ - { d: '2022-09-05 01:02:49' }, // converted to Asia/Istanbul on the server - { d: '2022-09-05 00:02:49' }, // this one was assumed Asia/Istanbul upon insertion - ]) + { d: "2022-09-05 01:02:49" }, // converted to Asia/Istanbul on the server + { d: "2022-09-05 00:02:49" }, // this one was assumed Asia/Istanbul upon insertion + ]); // toDateTime using Amsterdam timezone // should subtract 1 hour from each of the inserted dates @@ -124,36 +124,36 @@ describe('DateTime', () => { await client .query({ query: `SELECT toDateTime(d, 'Europe/Amsterdam') AS d FROM ${table}`, - format: 'JSONEachRow', + format: "JSONEachRow", }) .then((r) => r.json()), - ).toEqual([{ d: '2022-09-05 00:02:49' }, { d: '2022-09-04 23:02:49' }]) - }) - }) + ).toEqual([{ d: "2022-09-05 00:02:49" }, { d: "2022-09-04 23:02:49" }]); + }); + }); - describe('DateTime64(3)', () => { - it('should insert DateTime64(3) and get it back', async () => { - const table = await createTableWithFields(client, 'd DateTime64(3)') + describe("DateTime64(3)", () => { + it("should insert DateTime64(3) and get it back", async () => { + const table = await createTableWithFields(client, "d DateTime64(3)"); await client.insert({ table, values: [ { d: 1662328969123 }, // 2022-09-05 00:02:49.123 GMT+0200 - { d: '2022-09-05 00:02:49.456' }, // assumes column timezone (UTC by default) + { d: "2022-09-05 00:02:49.456" }, // assumes column timezone (UTC by default) ], - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); expect( await client .query({ query: `SELECT * EXCEPT id FROM ${table}`, - format: 'JSONEachRow', + format: "JSONEachRow", }) .then((r) => r.json()), ).toEqual([ - { d: '2022-09-04 22:02:49.123' }, // converted to UTC on the server - { d: '2022-09-05 00:02:49.456' }, // this one was assumed UTC upon insertion - ]) + { d: "2022-09-04 22:02:49.123" }, // converted to UTC on the server + { d: "2022-09-05 00:02:49.456" }, // this one was assumed UTC upon insertion + ]); // toDateTime using Amsterdam timezone // should add 2 hours to each of the inserted dates @@ -161,40 +161,40 @@ describe('DateTime', () => { await client .query({ query: `SELECT toDateTime64(d, 3, 'Europe/Amsterdam') AS d FROM ${table}`, - format: 'JSONEachRow', + format: "JSONEachRow", }) .then((r) => r.json()), ).toEqual([ - { d: '2022-09-05 00:02:49.123' }, - { d: '2022-09-05 02:02:49.456' }, - ]) - }) + { d: "2022-09-05 00:02:49.123" }, + { d: "2022-09-05 02:02:49.456" }, + ]); + }); - it('should insert DateTime64(3) and get it back (different timezone)', async () => { + it("should insert DateTime64(3) and get it back (different timezone)", async () => { const table = await createTableWithFields( client, `d DateTime64(3, 'Asia/Istanbul')`, - ) + ); await client.insert({ table, values: [ { d: 1662328969123 }, // 2022-09-05 00:02:49.123 GMT+0200 - { d: '2022-09-05 00:02:49.456' }, // assumes column timezone (Asia/Istanbul) + { d: "2022-09-05 00:02:49.456" }, // assumes column timezone (Asia/Istanbul) ], - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); expect( await client .query({ query: `SELECT * EXCEPT id FROM ${table}`, - format: 'JSONEachRow', + format: "JSONEachRow", }) .then((r) => r.json()), ).toEqual([ - { d: '2022-09-05 01:02:49.123' }, // converted to Asia/Istanbul on the server - { d: '2022-09-05 00:02:49.456' }, // this one was assumed Asia/Istanbul upon insertion - ]) + { d: "2022-09-05 01:02:49.123" }, // converted to Asia/Istanbul on the server + { d: "2022-09-05 00:02:49.456" }, // this one was assumed Asia/Istanbul upon insertion + ]); // toDateTime using Amsterdam timezone // should subtract 1 hour from each of the inserted dates @@ -202,13 +202,13 @@ describe('DateTime', () => { await client .query({ query: `SELECT toDateTime64(d, 3, 'Europe/Amsterdam') AS d FROM ${table}`, - format: 'JSONEachRow', + format: "JSONEachRow", }) .then((r) => r.json()), ).toEqual([ - { d: '2022-09-05 00:02:49.123' }, - { d: '2022-09-04 23:02:49.456' }, - ]) - }) - }) -}) + { d: "2022-09-05 00:02:49.123" }, + { d: "2022-09-04 23:02:49.456" }, + ]); + }); + }); +}); diff --git a/packages/client-common/__tests__/integration/error_parsing.test.ts b/packages/client-common/__tests__/integration/error_parsing.test.ts index a50272d2..a8e8965e 100644 --- a/packages/client-common/__tests__/integration/error_parsing.test.ts +++ b/packages/client-common/__tests__/integration/error_parsing.test.ts @@ -1,15 +1,15 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import type { ClickHouseClient } from '@clickhouse/client-common' -import { createTestClient, getTestDatabaseName } from '../utils' +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import type { ClickHouseClient } from "@clickhouse/client-common"; +import { createTestClient, getTestDatabaseName } from "../utils"; -describe('ClickHouse server errors parsing', () => { - let client: ClickHouseClient +describe("ClickHouse server errors parsing", () => { + let client: ClickHouseClient; beforeEach(() => { - client = createTestClient() - }) + client = createTestClient(); + }); afterEach(async () => { - await client.close() - }) + await client.close(); + }); it('returns "unknown identifier" error', async () => { // Possible error messages here: @@ -18,54 +18,54 @@ describe('ClickHouse server errors parsing', () => { // (since 24.9+): Unknown expression identifier `number` in scope SELECT number AS FR const errorMessagePattern = `((?:Missing columns: 'number' while processing query: 'SELECT number AS FR', required columns: 'number')|` + - `(?:Unknown expression identifier ('|\`)number('|\`) in scope SELECT number AS FR))` + `(?:Unknown expression identifier ('|\`)number('|\`) in scope SELECT number AS FR))`; await expect( client.query({ - query: 'SELECT number FR', + query: "SELECT number FR", }), ).rejects.toMatchObject( expect.objectContaining({ message: expect.stringMatching(errorMessagePattern), - code: '47', - type: 'UNKNOWN_IDENTIFIER', + code: "47", + type: "UNKNOWN_IDENTIFIER", }), - ) - }) + ); + }); it('returns "unknown table" error', async () => { // Possible error messages here: // (since 24.3+, Cloud SMT): Unknown table expression identifier 'unknown_table' in scope // (since 23.8+, Cloud RMT): Table foo.unknown_table does not exist. - const dbName = getTestDatabaseName() + const dbName = getTestDatabaseName(); const errorMessagePattern = `((?:^Table ${dbName}.unknown_table does not exist.*)|` + - `(?:Unknown table expression identifier ('|\`)unknown_table('|\`) in scope))` + `(?:Unknown table expression identifier ('|\`)unknown_table('|\`) in scope))`; await expect( client.query({ - query: 'SELECT * FROM unknown_table', + query: "SELECT * FROM unknown_table", }), ).rejects.toMatchObject( expect.objectContaining({ message: expect.stringMatching(errorMessagePattern), - code: '60', - type: 'UNKNOWN_TABLE', + code: "60", + type: "UNKNOWN_TABLE", }), - ) - }) + ); + }); it('returns "syntax error" error', async () => { await expect( client.query({ - query: 'SELECT * FRON unknown_table', + query: "SELECT * FRON unknown_table", }), ).rejects.toMatchObject( expect.objectContaining({ - message: expect.stringContaining('Syntax error: failed at position'), - code: '62', - type: 'SYNTAX_ERROR', + message: expect.stringContaining("Syntax error: failed at position"), + code: "62", + type: "SYNTAX_ERROR", }), - ) - }) + ); + }); it('returns "syntax error" error in a multiline query', async () => { await expect( @@ -80,10 +80,10 @@ describe('ClickHouse server errors parsing', () => { }), ).rejects.toMatchObject( expect.objectContaining({ - message: expect.stringContaining('Syntax error: failed at position'), - code: '62', - type: 'SYNTAX_ERROR', + message: expect.stringContaining("Syntax error: failed at position"), + code: "62", + type: "SYNTAX_ERROR", }), - ) - }) -}) + ); + }); +}); diff --git a/packages/client-common/__tests__/integration/exec_and_command.test.ts b/packages/client-common/__tests__/integration/exec_and_command.test.ts index 33fc0023..29e5138c 100644 --- a/packages/client-common/__tests__/integration/exec_and_command.test.ts +++ b/packages/client-common/__tests__/integration/exec_and_command.test.ts @@ -1,139 +1,139 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import type { ExecParams } from '@clickhouse/client-common' -import { type ClickHouseClient } from '@clickhouse/client-common' +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import type { ExecParams } from "@clickhouse/client-common"; +import { type ClickHouseClient } from "@clickhouse/client-common"; import { createTestClient, getClickHouseTestEnvironment, guid, TestEnv, validateUUID, -} from '../utils' +} from "../utils"; -describe('exec and command', () => { - let client: ClickHouseClient +describe("exec and command", () => { + let client: ClickHouseClient; beforeEach(() => { - client = createTestClient() - }) + client = createTestClient(); + }); afterEach(async () => { - await client.close() - }) + await client.close(); + }); - it('sends a command to execute', async () => { - const { ddl, tableName, engine } = getDDL() + it("sends a command to execute", async () => { + const { ddl, tableName, engine } = getDDL(); const { query_id } = await runExec({ query: ddl, - }) + }); // generated automatically - expect(validateUUID(query_id)).toBeTruthy() + expect(validateUUID(query_id)).toBeTruthy(); await checkCreatedTable({ tableName, engine, - }) - }) + }); + }); - it('should use query_id override', async () => { - const { ddl, tableName, engine } = getDDL() + it("should use query_id override", async () => { + const { ddl, tableName, engine } = getDDL(); - const query_id = guid() + const query_id = guid(); const { query_id: q_id } = await runExec({ query: ddl, query_id, - }) - expect(query_id).toEqual(q_id) + }); + expect(query_id).toEqual(q_id); await checkCreatedTable({ tableName, engine, - }) - }) + }); + }); - it('should not swallow ClickHouse error', async () => { - const { ddl } = getDDL() + it("should not swallow ClickHouse error", async () => { + const { ddl } = getDDL(); const commands = async () => { const command = () => runExec({ query: ddl, - }) - await command() - await command() - } + }); + await command(); + await command(); + }; await expect(commands()).rejects.toMatchObject( expect.objectContaining({ - code: '57', - type: 'TABLE_ALREADY_EXISTS', + code: "57", + type: "TABLE_ALREADY_EXISTS", }), - ) - }) + ); + }); - it('should get the response headers with command', async () => { + it("should get the response headers with command", async () => { // does not actually return anything, but still sends us the headers const result = await client.command({ - query: 'SELECT 42 FORMAT TSV', - }) + query: "SELECT 42 FORMAT TSV", + }); expect( - result.response_headers['Content-Type'] ?? - result.response_headers['content-type'], - ).toEqual('text/tab-separated-values; charset=UTF-8') - }) + result.response_headers["Content-Type"] ?? + result.response_headers["content-type"], + ).toEqual("text/tab-separated-values; charset=UTF-8"); + }); - it('should get the response headers with exec', async () => { + it("should get the response headers with exec", async () => { const result = await client.exec({ - query: 'SELECT 42 FORMAT CSV', - }) + query: "SELECT 42 FORMAT CSV", + }); expect( - result.response_headers['Content-Type'] ?? - result.response_headers['content-type'], - ).toEqual('text/csv; charset=UTF-8; header=absent') - }) + result.response_headers["Content-Type"] ?? + result.response_headers["content-type"], + ).toEqual("text/csv; charset=UTF-8; header=absent"); + }); - it('should get the http status code with exec', async () => { + it("should get the http status code with exec", async () => { const result = await client.exec({ - query: 'SELECT 42 FORMAT CSV', - }) + query: "SELECT 42 FORMAT CSV", + }); - expect(result.http_status_code).toBe(200) - }) + expect(result.http_status_code).toBe(200); + }); - it('can specify a parameterized query', async () => { + it("can specify a parameterized query", async () => { const result = await client.query({ query: `SELECT * from system.tables where name = 'numbers'`, - format: 'JSON', - }) + format: "JSON", + }); - const json = await result.json<{ name: string }>() - expect(json.rows).toBe(1) - expect(json.data[0].name).toBe('numbers') - }) + const json = await result.json<{ name: string }>(); + expect(json.rows).toBe(1); + expect(json.data[0].name).toBe("numbers"); + }); async function checkCreatedTable({ tableName, engine, }: { - tableName: string - engine: string + tableName: string; + engine: string; }) { const selectResult = await client.query({ query: `SELECT * from system.tables where name = '${tableName}'`, - format: 'JSON', - }) + format: "JSON", + }); const { data, rows } = await selectResult.json<{ - name: string - engine: string - create_table_query: string - }>() - - expect(rows).toBe(1) - const table = data[0] - expect(table.name).toBe(tableName) - expect(table.engine).toBe(engine) - expect(typeof table.create_table_query).toBe('string') + name: string; + engine: string; + create_table_query: string; + }>(); + + expect(rows).toBe(1); + const table = data[0]; + expect(table.name).toBe(tableName); + expect(table.engine).toBe(engine); + expect(typeof table.create_table_query).toBe("string"); } async function runExec(params: ExecParams): Promise<{ query_id: string }> { @@ -143,18 +143,18 @@ describe('exec and command', () => { // ClickHouse responds to a command when it's completely finished wait_end_of_query: 1, }, - }) - return { query_id } + }); + return { query_id }; } -}) +}); function getDDL(): { - ddl: string - tableName: string - engine: string + ddl: string; + tableName: string; + engine: string; } { - const env = getClickHouseTestEnvironment() - const tableName = `command_test_${guid()}` + const env = getClickHouseTestEnvironment(); + const tableName = `command_test_${guid()}`; switch (env) { // ENGINE and ON CLUSTER can be omitted in the cloud statements. // It will use Shared (CloudSMT)/Replicated (Cloud) MergeTree by default. @@ -163,8 +163,8 @@ function getDDL(): { CREATE TABLE ${tableName} (id UInt64, name String, sku Array(UInt8), timestamp DateTime) ORDER BY (id) - ` - return { ddl, tableName, engine: 'SharedMergeTree' } + `; + return { ddl, tableName, engine: "SharedMergeTree" }; } case TestEnv.LocalSingleNode: { const ddl = ` @@ -172,8 +172,8 @@ function getDDL(): { (id UInt64, name String, sku Array(UInt8), timestamp DateTime) ENGINE = MergeTree() ORDER BY (id) - ` - return { ddl, tableName, engine: 'MergeTree' } + `; + return { ddl, tableName, engine: "MergeTree" }; } case TestEnv.LocalCluster: { @@ -182,8 +182,8 @@ function getDDL(): { (id UInt64, name String, sku Array(UInt8), timestamp DateTime) ENGINE ReplicatedMergeTree('/clickhouse/{cluster}/tables/{database}/{table}/{shard}', '{replica}') ORDER BY (id) - ` - return { ddl, tableName, engine: 'ReplicatedMergeTree' } + `; + return { ddl, tableName, engine: "ReplicatedMergeTree" }; } } } diff --git a/packages/client-common/__tests__/integration/insert.test.ts b/packages/client-common/__tests__/integration/insert.test.ts index c94cfc90..8d61758c 100644 --- a/packages/client-common/__tests__/integration/insert.test.ts +++ b/packages/client-common/__tests__/integration/insert.test.ts @@ -1,139 +1,139 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import { type ClickHouseClient } from '@clickhouse/client-common' -import { createSimpleTable } from '../fixtures/simple_table' -import { assertJsonValues, jsonValues } from '../fixtures/test_data' -import { createTestClient, guid, validateUUID } from '../utils' +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { type ClickHouseClient } from "@clickhouse/client-common"; +import { createSimpleTable } from "../fixtures/simple_table"; +import { assertJsonValues, jsonValues } from "../fixtures/test_data"; +import { createTestClient, guid, validateUUID } from "../utils"; -describe('insert', () => { - let client: ClickHouseClient - let tableName: string +describe("insert", () => { + let client: ClickHouseClient; + let tableName: string; beforeEach(async () => { - client = createTestClient() - tableName = `insert_test_${guid()}` - await createSimpleTable(client, tableName) - }) + client = createTestClient(); + tableName = `insert_test_${guid()}`; + await createSimpleTable(client, tableName); + }); afterEach(async () => { - await client.close() - }) + await client.close(); + }); - it('inserts values using JSON format and get the response headers', async () => { + it("inserts values using JSON format and get the response headers", async () => { const result = await client.insert({ table: tableName, values: { meta: [ { - name: 'id', - type: 'UInt64', + name: "id", + type: "UInt64", }, { - name: 'name', - type: 'String', + name: "name", + type: "String", }, { - name: 'sku', - type: 'Array(UInt8)', + name: "sku", + type: "Array(UInt8)", }, ], data: jsonValues, }, - format: 'JSON', - }) - await assertJsonValues(client, tableName) - expect(validateUUID(result.query_id)).toBeTruthy() - expect(result.executed).toBeTruthy() + format: "JSON", + }); + await assertJsonValues(client, tableName); + expect(validateUUID(result.query_id)).toBeTruthy(); + expect(result.executed).toBeTruthy(); // Surprisingly, SMT Cloud instances have a different Content-Type here. // Expected 'text/tab-separated-values; charset=UTF-8' to equal 'text/plain; charset=UTF-8' expect( - result.response_headers['Content-Type'] ?? - result.response_headers['content-type'], - ).toEqual(expect.stringMatching(/text\/.+?; charset=UTF-8/)) - }) + result.response_headers["Content-Type"] ?? + result.response_headers["content-type"], + ).toEqual(expect.stringMatching(/text\/.+?; charset=UTF-8/)); + }); - it('should use provide query_id', async () => { - const query_id = guid() + it("should use provide query_id", async () => { + const query_id = guid(); const result = await client.insert({ table: tableName, query_id, values: { meta: [ { - name: 'id', - type: 'UInt64', + name: "id", + type: "UInt64", }, { - name: 'name', - type: 'String', + name: "name", + type: "String", }, { - name: 'sku', - type: 'Array(UInt8)', + name: "sku", + type: "Array(UInt8)", }, ], data: jsonValues, }, - format: 'JSON', - }) - await assertJsonValues(client, tableName) - expect(result.query_id).toEqual(query_id) - expect(result.executed).toBeTruthy() - }) + format: "JSON", + }); + await assertJsonValues(client, tableName); + expect(result.query_id).toEqual(query_id); + expect(result.executed).toBeTruthy(); + }); - it('inserts values using JSONObjectEachRow format', async () => { + it("inserts values using JSONObjectEachRow format", async () => { await client.insert({ table: tableName, values: { - a: { id: '42', name: 'hello', sku: [0, 1] }, - b: { id: '43', name: 'world', sku: [2, 3] }, - c: { id: '44', name: 'foo', sku: [3, 4] }, - d: { id: '45', name: 'bar', sku: [4, 5] }, - e: { id: '46', name: 'baz', sku: [6, 7] }, + a: { id: "42", name: "hello", sku: [0, 1] }, + b: { id: "43", name: "world", sku: [2, 3] }, + c: { id: "44", name: "foo", sku: [3, 4] }, + d: { id: "45", name: "bar", sku: [4, 5] }, + e: { id: "46", name: "baz", sku: [6, 7] }, }, - format: 'JSONObjectEachRow', - }) - await assertJsonValues(client, tableName) - }) + format: "JSONObjectEachRow", + }); + await assertJsonValues(client, tableName); + }); - it('can insert strings with non-ASCII symbols', async () => { + it("can insert strings with non-ASCII symbols", async () => { const values = [ - { id: '42', name: '🅷🅴🅻🅻🅾', sku: [0, 1] }, - { id: '43', name: '🆆🅾🆁🅻🅳 ♥', sku: [3, 4] }, - ] + { id: "42", name: "🅷🅴🅻🅻🅾", sku: [0, 1] }, + { id: "43", name: "🆆🅾🆁🅻🅳 ♥", sku: [3, 4] }, + ]; await client.insert({ table: tableName, values, - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); const rs = await client.query({ query: `SELECT * FROM ${tableName}`, - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); - const result = await rs.json() - expect(result).toEqual(values) - }) + const result = await rs.json(); + expect(result).toEqual(values); + }); - it('can do multiple inserts simultaneously', async () => { + it("can do multiple inserts simultaneously", async () => { await Promise.all( jsonValues.map((row) => client.insert({ values: [row], table: tableName, - format: 'JSONEachRow', + format: "JSONEachRow", }), ), - ) - await assertJsonValues(client, tableName) - }) + ); + await assertJsonValues(client, tableName); + }); - it('should provide error details when sending a request with an unknown clickhouse settings', async () => { + it("should provide error details when sending a request with an unknown clickhouse settings", async () => { await expect( client.insert({ table: tableName, values: jsonValues, - format: 'JSONEachRow', + format: "JSONEachRow", clickhouse_settings: { foobar: 1 } as any, }), ).rejects.toMatchObject( @@ -141,26 +141,26 @@ describe('insert', () => { // Possible error messages: // Unknown setting foobar // Setting foobar is neither a builtin setting nor started with the prefix 'SQL_' registered for user-defined settings. - message: expect.stringContaining('foobar'), - code: '115', - type: 'UNKNOWN_SETTING', + message: expect.stringContaining("foobar"), + code: "115", + type: "UNKNOWN_SETTING", }), - ) - }) + ); + }); - it('should work with async inserts', async () => { + it("should work with async inserts", async () => { await client.insert({ table: tableName, values: jsonValues, - format: 'JSONEachRow', + format: "JSONEachRow", // See https://clickhouse.com/docs/en/optimize/asynchronous-inserts clickhouse_settings: { - insert_quorum: '0', + insert_quorum: "0", async_insert: 1, wait_for_async_insert: 1, }, - }) + }); // Use retry to ensure data is actually inserted - await assertJsonValues(client, tableName, 50, 100) - }) -}) + await assertJsonValues(client, tableName, 50, 100); + }); +}); diff --git a/packages/client-common/__tests__/integration/insert_specific_columns.test.ts b/packages/client-common/__tests__/integration/insert_specific_columns.test.ts index ba2f5ee6..8167cf57 100644 --- a/packages/client-common/__tests__/integration/insert_specific_columns.test.ts +++ b/packages/client-common/__tests__/integration/insert_specific_columns.test.ts @@ -1,255 +1,255 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import { type ClickHouseClient } from '@clickhouse/client-common' -import { createTableWithFields } from '@test/fixtures/table_with_fields' -import { createTestClient, guid } from '../utils' -import { createSimpleTable } from '../fixtures/simple_table' +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { type ClickHouseClient } from "@clickhouse/client-common"; +import { createTableWithFields } from "@test/fixtures/table_with_fields"; +import { createTestClient, guid } from "../utils"; +import { createSimpleTable } from "../fixtures/simple_table"; -describe('Insert with specific columns', () => { - let client: ClickHouseClient - let table: string +describe("Insert with specific columns", () => { + let client: ClickHouseClient; + let table: string; beforeEach(async () => { - client = createTestClient() - }) + client = createTestClient(); + }); afterEach(async () => { - await client.close() - }) + await client.close(); + }); - describe('list of columns', () => { + describe("list of columns", () => { beforeEach(async () => { - table = `insert_specific_columns_${guid()}` - await createSimpleTable(client, table) - }) + table = `insert_specific_columns_${guid()}`; + await createSimpleTable(client, table); + }); const row = { - id: '42', - name: 'foo', + id: "42", + name: "foo", sku: [144], - } + }; - it('should work with a single column', async () => { + it("should work with a single column", async () => { await client.insert({ table, values: [{ id: 42 }], - format: 'JSONEachRow', - columns: ['id'], - }) - const result = await select() + format: "JSONEachRow", + columns: ["id"], + }); + const result = await select(); expect(result).toEqual([ { - id: '42', - name: '', + id: "42", + name: "", sku: [], }, - ]) - }) + ]); + }); - it('should work with multiple columns', async () => { + it("should work with multiple columns", async () => { await client.insert({ table, values: [ { - id: '42', - name: 'foo', + id: "42", + name: "foo", }, ], - format: 'JSONEachRow', - columns: ['id', 'name'], - }) - const result = await select() + format: "JSONEachRow", + columns: ["id", "name"], + }); + const result = await select(); expect(result).toEqual([ { - id: '42', - name: 'foo', + id: "42", + name: "foo", sku: [], }, - ]) - }) + ]); + }); - it('should work when all the columns are specified', async () => { + it("should work when all the columns are specified", async () => { await client.insert({ table, values: [row], - format: 'JSONEachRow', - columns: ['id', 'name', 'sku'], - }) - const result = await select() - expect(result).toEqual([row]) - }) - - it('should fail when an unknown column is specified', async () => { + format: "JSONEachRow", + columns: ["id", "name", "sku"], + }); + const result = await select(); + expect(result).toEqual([row]); + }); + + it("should fail when an unknown column is specified", async () => { await expect( client.insert({ table, values: [row], - format: 'JSONEachRow', - columns: ['foobar', 'id'], + format: "JSONEachRow", + columns: ["foobar", "id"], }), ).rejects.toMatchObject( expect.objectContaining({ - message: expect.stringContaining('No such column foobar'), + message: expect.stringContaining("No such column foobar"), }), - ) - }) - }) + ); + }); + }); - describe('list of columns corner cases', () => { + describe("list of columns corner cases", () => { beforeEach(async () => { table = await createTableWithFields( client, `s String, b Boolean`, // `id UInt32` will be added as well - ) - }) + ); + }); - it('should work when the list is empty', async () => { + it("should work when the list is empty", async () => { const values = [ - { id: 144, s: 'foo', b: true }, - { id: 255, s: 'bar', b: false }, - ] + { id: 144, s: "foo", b: true }, + { id: 255, s: "bar", b: false }, + ]; await client.insert({ table, values, - format: 'JSONEachRow', + format: "JSONEachRow", // Prohibited by the type system, but the client can be used from the JS columns: [] as unknown as [string, ...string[]], - }) + }); const result = await client .query({ query: `SELECT * FROM ${table} ORDER BY id`, - format: 'JSONEachRow', + format: "JSONEachRow", }) - .then((r) => r.json()) + .then((r) => r.json()); - expect(result).toEqual(values) - }) - }) + expect(result).toEqual(values); + }); + }); - describe('list of excluded columns', () => { + describe("list of excluded columns", () => { beforeEach(async () => { table = await createTableWithFields( client, `s String, b Boolean`, // `id UInt32` will be added as well - ) - }) + ); + }); - it('should work with a single excluded column', async () => { + it("should work with a single excluded column", async () => { await client.insert({ table, values: [ { id: 144, b: true }, { id: 255, b: false }, ], - format: 'JSONEachRow', + format: "JSONEachRow", columns: { - except: ['s'], + except: ["s"], }, - }) + }); const result = await client .query({ query: `SELECT * FROM ${table} ORDER BY id`, - format: 'JSONEachRow', + format: "JSONEachRow", }) - .then((r) => r.json()) + .then((r) => r.json()); expect(result).toEqual([ - { id: 144, s: '', b: true }, - { id: 255, s: '', b: false }, - ]) - }) + { id: 144, s: "", b: true }, + { id: 255, s: "", b: false }, + ]); + }); - it('should work with multiple excluded columns', async () => { + it("should work with multiple excluded columns", async () => { await client.insert({ table, - values: [{ s: 'foo' }, { s: 'bar' }], - format: 'JSONEachRow', + values: [{ s: "foo" }, { s: "bar" }], + format: "JSONEachRow", columns: { - except: ['id', 'b'], + except: ["id", "b"], }, - }) + }); const result = await client .query({ query: `SELECT * FROM ${table} ORDER BY s DESC`, - format: 'JSONEachRow', + format: "JSONEachRow", }) - .then((r) => r.json()) + .then((r) => r.json()); expect(result).toEqual([ - { id: 0, s: 'foo', b: false }, - { id: 0, s: 'bar', b: false }, - ]) - }) + { id: 0, s: "foo", b: false }, + { id: 0, s: "bar", b: false }, + ]); + }); - it('should work when the list is empty', async () => { + it("should work when the list is empty", async () => { const values = [ - { id: 144, s: 'foo', b: true }, - { id: 255, s: 'bar', b: false }, - ] + { id: 144, s: "foo", b: true }, + { id: 255, s: "bar", b: false }, + ]; await client.insert({ table, values, - format: 'JSONEachRow', + format: "JSONEachRow", columns: { // Prohibited by the type system, but the client can be used from the JS except: [] as unknown as [string, ...string[]], }, - }) + }); const result = await client .query({ query: `SELECT * FROM ${table}`, - format: 'JSONEachRow', + format: "JSONEachRow", }) - .then((r) => r.json()) + .then((r) => r.json()); - expect(result).toEqual(values) - }) + expect(result).toEqual(values); + }); // Surprisingly, `EXCEPT some_unknown_column` does not fail, even from the CLI - it('should still work when an unknown column is specified', async () => { + it("should still work when an unknown column is specified", async () => { const values = [ - { id: 144, s: 'foo', b: true }, - { id: 255, s: 'bar', b: false }, - ] + { id: 144, s: "foo", b: true }, + { id: 255, s: "bar", b: false }, + ]; await client.insert({ table, values, - format: 'JSONEachRow', + format: "JSONEachRow", columns: { - except: ['foobar'], + except: ["foobar"], }, - }) + }); const result = await client .query({ query: `SELECT * FROM ${table}`, - format: 'JSONEachRow', + format: "JSONEachRow", }) - .then((r) => r.json()) + .then((r) => r.json()); - expect(result).toEqual(values) - }) - }) + expect(result).toEqual(values); + }); + }); async function select() { const rs = await client.query({ query: `SELECT * FROM ${table} ORDER BY id ASC`, - format: 'JSONEachRow', - }) - return rs.json<{ id: string; name: string; sku: number[] }>() + format: "JSONEachRow", + }); + return rs.json<{ id: string; name: string; sku: number[] }>(); } -}) +}); diff --git a/packages/client-common/__tests__/integration/multiple_clients.test.ts b/packages/client-common/__tests__/integration/multiple_clients.test.ts index 4ff93eb0..1bc6c3f0 100644 --- a/packages/client-common/__tests__/integration/multiple_clients.test.ts +++ b/packages/client-common/__tests__/integration/multiple_clients.test.ts @@ -1,93 +1,93 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import type { ClickHouseClient } from '@clickhouse/client-common' -import { createSimpleTable } from '../fixtures/simple_table' -import { createTestClient, guid } from '../utils' +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import type { ClickHouseClient } from "@clickhouse/client-common"; +import { createSimpleTable } from "../fixtures/simple_table"; +import { createTestClient, guid } from "../utils"; -const CLIENTS_COUNT = 5 +const CLIENTS_COUNT = 5; -describe('multiple clients', () => { - const clients: ClickHouseClient[] = Array(CLIENTS_COUNT) +describe("multiple clients", () => { + const clients: ClickHouseClient[] = Array(CLIENTS_COUNT); beforeEach(() => { for (let i = 0; i < CLIENTS_COUNT; i++) { - clients[i] = createTestClient() + clients[i] = createTestClient(); } - }) + }); afterEach(async () => { for (const c of clients) { - await c.close() + await c.close(); } - }) + }); - it('should send multiple parallel selects', async () => { - const results: number[] = [] + it("should send multiple parallel selects", async () => { + const results: number[] = []; await Promise.all( clients.map((client, i) => client .query({ query: `SELECT toInt32(sum(*)) AS sum FROM numbers(0, ${i + 2});`, - format: 'JSONEachRow', + format: "JSONEachRow", }) .then((r) => r.json<{ sum: number }>()) .then((json) => results.push(json[0].sum)), ), - ) - expect(results.sort((a, b) => a - b)).toEqual([1, 3, 6, 10, 15]) - }) + ); + expect(results.sort((a, b) => a - b)).toEqual([1, 3, 6, 10, 15]); + }); - it('should be able to send parallel DDLs', async () => { - const id = guid() - const tableName = (i: number) => `multiple_clients_ddl_test__${id}__${i}` + it("should be able to send parallel DDLs", async () => { + const id = guid(); + const tableName = (i: number) => `multiple_clients_ddl_test__${id}__${i}`; await Promise.all( clients.map((client, i) => createSimpleTable(client, tableName(i))), - ) + ); for (let i = 0; i < CLIENTS_COUNT; i++) { const result = await clients[i].query({ query: `EXISTS ${tableName(i)}`, - format: 'TabSeparated', - }) - expect(await result.text()).toEqual('1\n') + format: "TabSeparated", + }); + expect(await result.text()).toEqual("1\n"); } - }) + }); - describe('insert', () => { - const names = ['foo', 'bar', 'baz', 'qaz', 'qux'] + describe("insert", () => { + const names = ["foo", "bar", "baz", "qaz", "qux"]; function getValue(i: number) { return { id: i, name: names[i], sku: [i, i + 1], - } + }; } const expected = [ - { id: '0', name: 'foo', sku: [0, 1] }, - { id: '1', name: 'bar', sku: [1, 2] }, - { id: '2', name: 'baz', sku: [2, 3] }, - { id: '3', name: 'qaz', sku: [3, 4] }, - { id: '4', name: 'qux', sku: [4, 5] }, - ] + { id: "0", name: "foo", sku: [0, 1] }, + { id: "1", name: "bar", sku: [1, 2] }, + { id: "2", name: "baz", sku: [2, 3] }, + { id: "3", name: "qaz", sku: [3, 4] }, + { id: "4", name: "qux", sku: [4, 5] }, + ]; - it('should be able to send parallel inserts (arrays)', async () => { - const id = guid() - const tableName = `multiple_clients_insert_arrays_test__${id}` - await createSimpleTable(clients[0], tableName) + it("should be able to send parallel inserts (arrays)", async () => { + const id = guid(); + const tableName = `multiple_clients_insert_arrays_test__${id}`; + await createSimpleTable(clients[0], tableName); await Promise.all( clients.map((client, i) => client.insert({ table: tableName, values: [getValue(i)], - format: 'JSONEachRow', + format: "JSONEachRow", }), ), - ) + ); const result = await clients[0].query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, - format: 'JSONEachRow', - }) - expect(await result.json()).toEqual(expected) - }) - }) -}) + format: "JSONEachRow", + }); + expect(await result.json()).toEqual(expected); + }); + }); +}); diff --git a/packages/client-common/__tests__/integration/ping.test.ts b/packages/client-common/__tests__/integration/ping.test.ts index bb7e555e..9acd3b97 100644 --- a/packages/client-common/__tests__/integration/ping.test.ts +++ b/packages/client-common/__tests__/integration/ping.test.ts @@ -1,16 +1,16 @@ -import { describe, it, expect, afterEach } from 'vitest' -import { type ClickHouseClient } from '@clickhouse/client-common' -import { createTestClient } from '../utils' +import { describe, it, expect, afterEach } from "vitest"; +import { type ClickHouseClient } from "@clickhouse/client-common"; +import { createTestClient } from "../utils"; -describe('ping', () => { - let client: ClickHouseClient +describe("ping", () => { + let client: ClickHouseClient; afterEach(async () => { - await client.close() - }) + await client.close(); + }); - it('makes a ping request', async () => { - client = createTestClient() - const response = await client.ping() - expect(response.success).toBe(true) - }) -}) + it("makes a ping request", async () => { + client = createTestClient(); + const response = await client.ping(); + expect(response.success).toBe(true); + }); +}); diff --git a/packages/client-common/__tests__/integration/query_log.test.ts b/packages/client-common/__tests__/integration/query_log.test.ts index 04800edb..94e25c79 100644 --- a/packages/client-common/__tests__/integration/query_log.test.ts +++ b/packages/client-common/__tests__/integration/query_log.test.ts @@ -1,80 +1,80 @@ -import { describe, it, expect, afterEach } from 'vitest' -import type { ClickHouseClient } from '@clickhouse/client-common' -import { createSimpleTable } from '../fixtures/simple_table' -import { createTestClient, guid, TestEnv, isOnEnv } from '../utils' -import { sleep } from '../utils/sleep' +import { describe, it, expect, afterEach } from "vitest"; +import type { ClickHouseClient } from "@clickhouse/client-common"; +import { createSimpleTable } from "../fixtures/simple_table"; +import { createTestClient, guid, TestEnv, isOnEnv } from "../utils"; +import { sleep } from "../utils/sleep"; // these tests are very flaky in the Cloud environment // likely due to the fact that flushing the query_log there happens not too often // it's better to execute only with the local single node or cluster -const testEnvs = [TestEnv.LocalSingleNode] +const testEnvs = [TestEnv.LocalSingleNode]; -describe('query_log', () => { - let client: ClickHouseClient +describe("query_log", () => { + let client: ClickHouseClient; afterEach(async () => { if (client) { - await client.close() + await client.close(); } - }) + }); it.skipIf(!isOnEnv(...testEnvs))( - 'can use query_id to fetch query_log table with select', + "can use query_id to fetch query_log table with select", async () => { - client = createTestClient() - const query = 'SELECT * FROM system.numbers LIMIT 144' + client = createTestClient(); + const query = "SELECT * FROM system.numbers LIMIT 144"; const { query_id } = await client.query({ query, - format: 'JSON', - }) + format: "JSON", + }); const formattedQuery = - 'SELECT * FROM system.numbers LIMIT 144 \nFORMAT JSON' - await assertQueryLog({ formattedQuery, query_id }) + "SELECT * FROM system.numbers LIMIT 144 \nFORMAT JSON"; + await assertQueryLog({ formattedQuery, query_id }); }, - ) + ); it.skipIf(!isOnEnv(...testEnvs))( - 'can use query_id to fetch query_log table with exec', + "can use query_id to fetch query_log table with exec", async () => { - client = createTestClient() - const table = `clickhouse_query_id_exec_test__${guid()}` - const query = `CREATE TABLE ${table} (id String) ENGINE MergeTree() ORDER BY (id)` + client = createTestClient(); + const table = `clickhouse_query_id_exec_test__${guid()}`; + const query = `CREATE TABLE ${table} (id String) ENGINE MergeTree() ORDER BY (id)`; const { query_id } = await client.exec({ query, - }) - await assertQueryLog({ formattedQuery: query, query_id }) + }); + await assertQueryLog({ formattedQuery: query, query_id }); }, - ) + ); it.skipIf(!isOnEnv(...testEnvs))( - 'can use query_id to fetch query_log table with insert', + "can use query_id to fetch query_log table with insert", async () => { - client = createTestClient() - const table = `clickhouse_query_id_insert_test__${guid()}` - await createSimpleTable(client, table) + client = createTestClient(); + const table = `clickhouse_query_id_insert_test__${guid()}`; + await createSimpleTable(client, table); const { query_id } = await client.insert({ table, values: { - a: { id: '42', name: 'hello', sku: [0, 1] }, + a: { id: "42", name: "hello", sku: [0, 1] }, }, - format: 'JSONObjectEachRow', - }) - const formattedQuery = `INSERT INTO ${table} FORMAT JSONObjectEachRow\n` - await assertQueryLog({ formattedQuery, query_id }) + format: "JSONObjectEachRow", + }); + const formattedQuery = `INSERT INTO ${table} FORMAT JSONObjectEachRow\n`; + await assertQueryLog({ formattedQuery, query_id }); }, - ) + ); async function assertQueryLog({ formattedQuery, query_id, }: { - formattedQuery: string - query_id: string + formattedQuery: string; + query_id: string; }) { // query_log is flushed every ~1000 milliseconds // so this might fail a couple of times // FIXME: jasmine did not throw, maybe Vitest does. // RetryOnFailure does not work - await sleep(1200) + await sleep(1200); const logResultSet = await client.query({ query: ` SELECT * FROM system.query_log @@ -84,11 +84,11 @@ describe('query_log', () => { query_params: { query_id, }, - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); expect(await logResultSet.json()).toEqual([ expect.objectContaining({ - type: 'QueryStart', + type: "QueryStart", query: formattedQuery, initial_query_id: query_id, query_duration_ms: expect.any(String), @@ -96,13 +96,13 @@ describe('query_log', () => { read_bytes: expect.any(String), }), expect.objectContaining({ - type: 'QueryFinish', + type: "QueryFinish", query: formattedQuery, initial_query_id: query_id, query_duration_ms: expect.any(String), read_rows: expect.any(String), read_bytes: expect.any(String), }), - ]) + ]); } -}) +}); diff --git a/packages/client-common/__tests__/integration/read_only_user.test.ts b/packages/client-common/__tests__/integration/read_only_user.test.ts index 12db3eda..1305eb99 100644 --- a/packages/client-common/__tests__/integration/read_only_user.test.ts +++ b/packages/client-common/__tests__/integration/read_only_user.test.ts @@ -1,30 +1,30 @@ -import { describe, it, expect, beforeAll, afterAll } from 'vitest' -import type { ClickHouseClient } from '@clickhouse/client-common' -import { isCloudTestEnv } from '@test/utils/test_env' -import { createReadOnlyUser } from '../fixtures/read_only_user' -import { createSimpleTable } from '../fixtures/simple_table' -import { createTestClient, getTestDatabaseName, guid } from '../utils' +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import type { ClickHouseClient } from "@clickhouse/client-common"; +import { isCloudTestEnv } from "@test/utils/test_env"; +import { createReadOnlyUser } from "../fixtures/read_only_user"; +import { createSimpleTable } from "../fixtures/simple_table"; +import { createTestClient, getTestDatabaseName, guid } from "../utils"; -describe('read only user', () => { - let defaultClient: ClickHouseClient - let client: ClickHouseClient - let tableName: string - let userName: string +describe("read only user", () => { + let defaultClient: ClickHouseClient; + let client: ClickHouseClient; + let tableName: string; + let userName: string; beforeAll(async () => { - const database = getTestDatabaseName() - defaultClient = createTestClient() + const database = getTestDatabaseName(); + defaultClient = createTestClient(); - const credentials = await createReadOnlyUser(defaultClient) - userName = credentials.username + const credentials = await createReadOnlyUser(defaultClient); + userName = credentials.username; // Populate some test table to select from - tableName = `read_only_user_data_${guid()}` - await createSimpleTable(defaultClient, tableName) + tableName = `read_only_user_data_${guid()}`; + await createSimpleTable(defaultClient, tableName); await defaultClient.insert({ table: tableName, - values: [[42, 'hello', [0, 1]]], - }) + values: [[42, "hello", [0, 1]]], + }); // Create a client that connects read only user to the test database client = createTestClient({ @@ -38,58 +38,58 @@ describe('read only user', () => { database_replicated_enforce_synchronous_settings: undefined, output_format_json_quote_64bit_integers: undefined, }, - }) - }) + }); + }); afterAll(async () => { if (isCloudTestEnv()) { await defaultClient.command({ query: `DROP USER IF EXISTS ${userName}`, - }) + }); } - await client.close() - await defaultClient.close() - }) + await client.close(); + await defaultClient.close(); + }); - it('should select some data without issues', async () => { + it("should select some data without issues", async () => { const result = await client .query({ query: `SELECT name, sku FROM ${tableName}`, }) - .then((r) => r.json()) - expect(result.data).toEqual([{ name: 'hello', sku: [0, 1] }]) - }) + .then((r) => r.json()); + expect(result.data).toEqual([{ name: "hello", sku: [0, 1] }]); + }); - it('should fail to create a table', async () => { + it("should fail to create a table", async () => { await expect( createSimpleTable(client, `should_not_be_created_${guid()}`), ).rejects.toMatchObject( expect.objectContaining({ - message: expect.stringContaining('Not enough privileges'), + message: expect.stringContaining("Not enough privileges"), }), - ) - }) + ); + }); - it('should fail to insert', async () => { + it("should fail to insert", async () => { await expect( client.insert({ table: tableName, - values: [[43, 'foobar', [5, 25]]], + values: [[43, "foobar", [5, 25]]], }), ).rejects.toMatchObject( expect.objectContaining({ - message: expect.stringContaining('Not enough privileges'), + message: expect.stringContaining("Not enough privileges"), }), - ) - }) + ); + }); // TODO: find a way to restrict all the system tables access - it('should fail to query system tables', async () => { - const query = `SELECT * FROM system.users LIMIT 5` + it("should fail to query system tables", async () => { + const query = `SELECT * FROM system.users LIMIT 5`; await expect(client.query({ query })).rejects.toMatchObject( expect.objectContaining({ - message: expect.stringContaining('Not enough privileges'), + message: expect.stringContaining("Not enough privileges"), }), - ) - }) -}) + ); + }); +}); diff --git a/packages/client-common/__tests__/integration/request_compression.test.ts b/packages/client-common/__tests__/integration/request_compression.test.ts index 0f5adace..eec58d85 100644 --- a/packages/client-common/__tests__/integration/request_compression.test.ts +++ b/packages/client-common/__tests__/integration/request_compression.test.ts @@ -1,44 +1,44 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' +import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { type ClickHouseClient, type ResponseJSON, -} from '@clickhouse/client-common' -import { createSimpleTable } from '../fixtures/simple_table' -import { createTestClient, guid } from '../utils' +} from "@clickhouse/client-common"; +import { createSimpleTable } from "../fixtures/simple_table"; +import { createTestClient, guid } from "../utils"; -describe('insert compression', () => { - let client: ClickHouseClient - let tableName: string +describe("insert compression", () => { + let client: ClickHouseClient; + let tableName: string; beforeEach(async () => { client = await createTestClient({ compression: { request: true, }, - }) - tableName = `insert_compression_test_${guid()}` - await createSimpleTable(client, tableName) - }) + }); + tableName = `insert_compression_test_${guid()}`; + await createSimpleTable(client, tableName); + }); afterEach(async () => { - await client.close() - }) + await client.close(); + }); - it('request compression', async () => { + it("request compression", async () => { const dataToInsert = new Array(1_000).fill(0).map((v, idx) => { - return [idx, `${idx + 5}`, [idx + 1, idx + 2]] - }) + return [idx, `${idx + 5}`, [idx + 1, idx + 2]]; + }); await client.insert({ table: tableName, values: dataToInsert, - }) + }); const rs = await client.query({ query: `SELECT * FROM ${tableName}`, - format: 'JSON', - }) + format: "JSON", + }); - const result = await rs.json() - expect(result.data.length).toBe(1_000) - }) -}) + const result = await rs.json(); + expect(result.data.length).toBe(1_000); + }); +}); diff --git a/packages/client-common/__tests__/integration/response_compression.test.ts b/packages/client-common/__tests__/integration/response_compression.test.ts index a7f68708..a8f3d1f8 100644 --- a/packages/client-common/__tests__/integration/response_compression.test.ts +++ b/packages/client-common/__tests__/integration/response_compression.test.ts @@ -1,19 +1,19 @@ -import { describe, it, expect, afterEach } from 'vitest' -import { type ClickHouseClient } from '@clickhouse/client-common' -import { createTestClient } from '../utils' +import { describe, it, expect, afterEach } from "vitest"; +import { type ClickHouseClient } from "@clickhouse/client-common"; +import { createTestClient } from "../utils"; -describe('response compression', () => { - let client: ClickHouseClient +describe("response compression", () => { + let client: ClickHouseClient; afterEach(async () => { - await client.close() - }) + await client.close(); + }); - it('accepts a compressed response', async () => { + it("accepts a compressed response", async () => { client = createTestClient({ compression: { response: true, }, - }) + }); const rs = await client.query({ query: ` @@ -21,11 +21,11 @@ describe('response compression', () => { FROM system.numbers LIMIT 20000 `, - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); - const response = await rs.json<{ number: string }>() - const last = response[response.length - 1] - expect(last.number).toBe('19999') - }) -}) + const response = await rs.json<{ number: string }>(); + const last = response[response.length - 1]; + expect(last.number).toBe("19999"); + }); +}); diff --git a/packages/client-common/__tests__/integration/role.test.ts b/packages/client-common/__tests__/integration/role.test.ts index 86f5716d..37bddbac 100644 --- a/packages/client-common/__tests__/integration/role.test.ts +++ b/packages/client-common/__tests__/integration/role.test.ts @@ -6,219 +6,219 @@ import { afterEach, beforeAll, afterAll, -} from 'vitest' -import type { ClickHouseClient } from '@clickhouse/client-common' -import { createTestClient, TestEnv, isOnEnv } from '@test/utils' -import { createSimpleTable } from '../fixtures/simple_table' -import { assertJsonValues, jsonValues } from '../fixtures/test_data' -import { getTestDatabaseName, guid } from '../utils' - -describe.skipIf(!isOnEnv(TestEnv.LocalSingleNode))('role settings', () => { - let defaultClient: ClickHouseClient - let client: ClickHouseClient - - let database: string - let username: string - let password: string - let roleName1: string - let roleName2: string +} from "vitest"; +import type { ClickHouseClient } from "@clickhouse/client-common"; +import { createTestClient, TestEnv, isOnEnv } from "@test/utils"; +import { createSimpleTable } from "../fixtures/simple_table"; +import { assertJsonValues, jsonValues } from "../fixtures/test_data"; +import { getTestDatabaseName, guid } from "../utils"; + +describe.skipIf(!isOnEnv(TestEnv.LocalSingleNode))("role settings", () => { + let defaultClient: ClickHouseClient; + let client: ClickHouseClient; + + let database: string; + let username: string; + let password: string; + let roleName1: string; + let roleName2: string; beforeAll(async () => { - defaultClient = createTestClient() - username = `clickhousejs__user_with_roles_${guid()}` - password = `CHJS_${guid()}` - roleName1 = `TEST_ROLE_${guid()}` - roleName2 = `TEST_ROLE_${guid()}` - database = getTestDatabaseName() + defaultClient = createTestClient(); + username = `clickhousejs__user_with_roles_${guid()}`; + password = `CHJS_${guid()}`; + roleName1 = `TEST_ROLE_${guid()}`; + roleName2 = `TEST_ROLE_${guid()}`; + database = getTestDatabaseName(); await defaultClient.command({ query: `CREATE USER ${username} IDENTIFIED WITH sha256_password BY '${password}' DEFAULT DATABASE ${database}`, - }) + }); await defaultClient.command({ query: `CREATE ROLE IF NOT EXISTS ${roleName1}`, - }) + }); await defaultClient.command({ query: `CREATE ROLE IF NOT EXISTS ${roleName2}`, - }) + }); await defaultClient.command({ query: `GRANT ${roleName1}, ${roleName2} TO ${username}`, - }) + }); await defaultClient.command({ query: `GRANT INSERT ON ${database}.* TO ${roleName1}`, - }) + }); await defaultClient.command({ query: `GRANT CREATE TABLE ON * TO ${roleName1}`, - }) - }) + }); + }); afterEach(async () => { - await client.close() - }) + await client.close(); + }); afterAll(async () => { - await defaultClient.close() - }) + await defaultClient.close(); + }); - describe('for queries', () => { + describe("for queries", () => { async function queryCurrentRoles(role?: string | Array) { const rs = await client.query({ - query: 'select currentRoles() as roles', - format: 'JSONEachRow', + query: "select currentRoles() as roles", + format: "JSONEachRow", role, - }) + }); - const jsonResults = (await rs.json()) as { roles: string[] }[] - return jsonResults[0].roles + const jsonResults = (await rs.json()) as { roles: string[] }[]; + return jsonResults[0].roles; } - it('should use a single role from the client configuration', async () => { + it("should use a single role from the client configuration", async () => { client = createTestClient({ username, password, role: roleName1, - }) + }); - const actualRoles = await queryCurrentRoles() - expect(actualRoles).toEqual([roleName1]) - }) + const actualRoles = await queryCurrentRoles(); + expect(actualRoles).toEqual([roleName1]); + }); - it('should use multiple roles from the client configuration', async () => { + it("should use multiple roles from the client configuration", async () => { client = createTestClient({ username, password, role: [roleName1, roleName2], - }) + }); - const actualRoles = await queryCurrentRoles() - expect(actualRoles.length).toBe(2) - expect(actualRoles).toContain(roleName1) - expect(actualRoles).toContain(roleName2) - }) + const actualRoles = await queryCurrentRoles(); + expect(actualRoles.length).toBe(2); + expect(actualRoles).toContain(roleName1); + expect(actualRoles).toContain(roleName2); + }); - it('should use single role from the query options', async () => { + it("should use single role from the query options", async () => { client = createTestClient({ username, password, role: [roleName1, roleName2], - }) + }); - const actualRoles = await queryCurrentRoles(roleName2) - expect(actualRoles).toEqual([roleName2]) - }) + const actualRoles = await queryCurrentRoles(roleName2); + expect(actualRoles).toEqual([roleName2]); + }); - it('should use multiple roles from the query options', async () => { + it("should use multiple roles from the query options", async () => { client = createTestClient({ username, password, - }) + }); - const actualRoles = await queryCurrentRoles([roleName1, roleName2]) - expect(actualRoles.length).toBe(2) - expect(actualRoles).toContain(roleName1) - expect(actualRoles).toContain(roleName2) - }) - }) + const actualRoles = await queryCurrentRoles([roleName1, roleName2]); + expect(actualRoles.length).toBe(2); + expect(actualRoles).toContain(roleName1); + expect(actualRoles).toContain(roleName2); + }); + }); - describe('for inserts', () => { - let tableName: string + describe("for inserts", () => { + let tableName: string; beforeEach(async () => { - tableName = `role_insert_test_${guid()}` - await createSimpleTable(defaultClient, tableName) - }) + tableName = `role_insert_test_${guid()}`; + await createSimpleTable(defaultClient, tableName); + }); async function tryInsert(role?: string | Array) { await client.insert({ table: tableName, values: jsonValues, - format: 'JSONEachRow', + format: "JSONEachRow", role, - }) + }); } - it('should successfully insert when client specifies a role that is allowed to insert', async () => { + it("should successfully insert when client specifies a role that is allowed to insert", async () => { client = createTestClient({ username, password, role: roleName1, - }) + }); - await tryInsert() - await assertJsonValues(defaultClient, tableName) - }) + await tryInsert(); + await assertJsonValues(defaultClient, tableName); + }); - it('should successfully insert when client specifies multiple roles and at least one is allowed to insert', async () => { + it("should successfully insert when client specifies multiple roles and at least one is allowed to insert", async () => { client = createTestClient({ username, password, role: [roleName1, roleName2], - }) + }); - await tryInsert() - await assertJsonValues(defaultClient, tableName) - }) + await tryInsert(); + await assertJsonValues(defaultClient, tableName); + }); - it('should fail to insert when client specifies a role that is not allowed to insert', async () => { + it("should fail to insert when client specifies a role that is not allowed to insert", async () => { client = createTestClient({ username, password, role: roleName2, - }) + }); await expect(tryInsert()).rejects.toMatchObject( expect.objectContaining({ - message: expect.stringContaining('Not enough privileges'), - code: '497', - type: 'ACCESS_DENIED', + message: expect.stringContaining("Not enough privileges"), + code: "497", + type: "ACCESS_DENIED", }), - ) - }) + ); + }); - it('should successfully insert when insert specifies a role that is allowed to insert', async () => { + it("should successfully insert when insert specifies a role that is allowed to insert", async () => { client = createTestClient({ username, password, role: roleName2, - }) + }); - await tryInsert(roleName1) - await assertJsonValues(defaultClient, tableName) - }) + await tryInsert(roleName1); + await assertJsonValues(defaultClient, tableName); + }); - it('should successfully insert when insert specifies multiple roles and at least one is allowed to insert', async () => { + it("should successfully insert when insert specifies multiple roles and at least one is allowed to insert", async () => { client = createTestClient({ username, password, role: roleName2, - }) + }); - await tryInsert([roleName1, roleName2]) - await assertJsonValues(defaultClient, tableName) - }) + await tryInsert([roleName1, roleName2]); + await assertJsonValues(defaultClient, tableName); + }); - it('should fail to insert when insert specifies a role that is not allowed to insert', async () => { + it("should fail to insert when insert specifies a role that is not allowed to insert", async () => { client = createTestClient({ username, password, role: roleName1, - }) + }); await expect(tryInsert(roleName2)).rejects.toMatchObject( expect.objectContaining({ - message: expect.stringContaining('Not enough privileges'), - code: '497', - type: 'ACCESS_DENIED', + message: expect.stringContaining("Not enough privileges"), + code: "497", + type: "ACCESS_DENIED", }), - ) - }) - }) + ); + }); + }); - describe('for commands', () => { - let tableName: string + describe("for commands", () => { + let tableName: string; beforeEach(async () => { - tableName = `role_command_test_${guid()}` - }) + tableName = `role_command_test_${guid()}`; + }); async function tryCreateTable(role?: string | Array) { const query = ` @@ -226,95 +226,95 @@ describe.skipIf(!isOnEnv(TestEnv.LocalSingleNode))('role settings', () => { (id UInt64, name String, sku Array(UInt8), timestamp DateTime) ENGINE = MergeTree() ORDER BY (id) - ` - await client.command({ query, role }) + `; + await client.command({ query, role }); } async function checkCreatedTable(tableName: string) { const selectResult = await defaultClient.query({ query: `SELECT * from system.tables where name = '${tableName}'`, - format: 'JSON', - }) + format: "JSON", + }); - const { data, rows } = await selectResult.json<{ name: string }>() - expect(rows).toBe(1) - expect(data[0].name).toBe(tableName) + const { data, rows } = await selectResult.json<{ name: string }>(); + expect(rows).toBe(1); + expect(data[0].name).toBe(tableName); } - it('should successfully create a table when client specifies a role that is allowed to create tables', async () => { + it("should successfully create a table when client specifies a role that is allowed to create tables", async () => { client = createTestClient({ username, password, role: roleName1, - }) + }); - await tryCreateTable() - await checkCreatedTable(tableName) - }) + await tryCreateTable(); + await checkCreatedTable(tableName); + }); - it('should successfully create table when client specifies multiple roles and at least one is allowed to create tables', async () => { + it("should successfully create table when client specifies multiple roles and at least one is allowed to create tables", async () => { client = createTestClient({ username, password, role: [roleName1, roleName2], - }) + }); - await tryCreateTable() - await checkCreatedTable(tableName) - }) + await tryCreateTable(); + await checkCreatedTable(tableName); + }); - it('should fail to create a table when client specifies a role that is not allowed to create tables', async () => { + it("should fail to create a table when client specifies a role that is not allowed to create tables", async () => { client = createTestClient({ username, password, role: roleName2, - }) + }); await expect(tryCreateTable()).rejects.toMatchObject( expect.objectContaining({ - message: expect.stringContaining('Not enough privileges'), - code: '497', - type: 'ACCESS_DENIED', + message: expect.stringContaining("Not enough privileges"), + code: "497", + type: "ACCESS_DENIED", }), - ) - }) + ); + }); - it('should successfully create table when command specifies a role that is allowed to create tables', async () => { + it("should successfully create table when command specifies a role that is allowed to create tables", async () => { client = createTestClient({ username, password, role: roleName2, - }) + }); - await tryCreateTable(roleName1) - await checkCreatedTable(tableName) - }) + await tryCreateTable(roleName1); + await checkCreatedTable(tableName); + }); - it('should successfully create table when command specifies multiple roles and at least one is allowed to create tables', async () => { + it("should successfully create table when command specifies multiple roles and at least one is allowed to create tables", async () => { client = createTestClient({ username, password, role: roleName2, - }) + }); - await tryCreateTable([roleName1, roleName2]) - await checkCreatedTable(tableName) - }) + await tryCreateTable([roleName1, roleName2]); + await checkCreatedTable(tableName); + }); - it('should fail to create table when command specifies a role that is not allowed to create tables', async () => { + it("should fail to create table when command specifies a role that is not allowed to create tables", async () => { client = createTestClient({ username, password, role: roleName1, - }) + }); await expect(tryCreateTable(roleName2)).rejects.toMatchObject( expect.objectContaining({ - message: expect.stringContaining('Not enough privileges'), - code: '497', - type: 'ACCESS_DENIED', + message: expect.stringContaining("Not enough privileges"), + code: "497", + type: "ACCESS_DENIED", }), - ) - }) - }) -}) + ); + }); + }); +}); diff --git a/packages/client-common/__tests__/integration/select.test.ts b/packages/client-common/__tests__/integration/select.test.ts index 722f5c12..550f923c 100644 --- a/packages/client-common/__tests__/integration/select.test.ts +++ b/packages/client-common/__tests__/integration/select.test.ts @@ -1,70 +1,70 @@ -import { describe, it, expect, afterEach, beforeEach } from 'vitest' -import { type ClickHouseClient } from '@clickhouse/client-common' -import { createTestClient, guid, validateUUID } from '../utils' +import { describe, it, expect, afterEach, beforeEach } from "vitest"; +import { type ClickHouseClient } from "@clickhouse/client-common"; +import { createTestClient, guid, validateUUID } from "../utils"; -describe('select', () => { - let client: ClickHouseClient +describe("select", () => { + let client: ClickHouseClient; afterEach(async () => { - await client.close() - }) + await client.close(); + }); beforeEach(async () => { - client = createTestClient() - }) + client = createTestClient(); + }); - it('gets query_id back', async () => { + it("gets query_id back", async () => { const resultSet = await client.query({ - query: 'SELECT * FROM system.numbers LIMIT 1', - format: 'JSONEachRow', - }) - expect(await resultSet.json()).toEqual([{ number: '0' }]) - expect(validateUUID(resultSet.query_id)).toBeTruthy() - }) - - it('can override query_id', async () => { - const query_id = guid() + query: "SELECT * FROM system.numbers LIMIT 1", + format: "JSONEachRow", + }); + expect(await resultSet.json()).toEqual([{ number: "0" }]); + expect(validateUUID(resultSet.query_id)).toBeTruthy(); + }); + + it("can override query_id", async () => { + const query_id = guid(); const resultSet = await client.query({ - query: 'SELECT * FROM system.numbers LIMIT 1', - format: 'JSONEachRow', + query: "SELECT * FROM system.numbers LIMIT 1", + format: "JSONEachRow", query_id, - }) - expect(await resultSet.json()).toEqual([{ number: '0' }]) - expect(resultSet.query_id).toEqual(query_id) - }) + }); + expect(await resultSet.json()).toEqual([{ number: "0" }]); + expect(resultSet.query_id).toEqual(query_id); + }); - it('can process an empty response', async () => { + it("can process an empty response", async () => { expect( await client .query({ - query: 'SELECT * FROM system.numbers LIMIT 0', - format: 'JSONEachRow', + query: "SELECT * FROM system.numbers LIMIT 0", + format: "JSONEachRow", }) .then((r) => r.json()), - ).toEqual([]) + ).toEqual([]); expect( await client .query({ - query: 'SELECT * FROM system.numbers LIMIT 0', - format: 'TabSeparated', + query: "SELECT * FROM system.numbers LIMIT 0", + format: "TabSeparated", }) .then((r) => r.text()), - ).toEqual('') - }) + ).toEqual(""); + }); - it('can send a multiline query', async () => { + it("can send a multiline query", async () => { const rs = await client.query({ query: ` SELECT number FROM system.numbers LIMIT 2 `, - format: 'CSV', - }) + format: "CSV", + }); - const response = await rs.text() - expect(response).toBe('0\n1\n') - }) + const response = await rs.text(); + expect(response).toBe("0\n1\n"); + }); - it('can send a query with an inline comment', async () => { + it("can send a query with an inline comment", async () => { const rs = await client.query({ query: ` SELECT number @@ -72,14 +72,14 @@ describe('select', () => { FROM system.numbers LIMIT 2 `, - format: 'CSV', - }) + format: "CSV", + }); - const response = await rs.text() - expect(response).toBe('0\n1\n') - }) + const response = await rs.text(); + expect(response).toBe("0\n1\n"); + }); - it('can send a query with a multiline comment', async () => { + it("can send a query with a multiline comment", async () => { const rs = await client.query({ query: ` SELECT number @@ -89,64 +89,64 @@ describe('select', () => { FROM system.numbers LIMIT 2 `, - format: 'CSV', - }) + format: "CSV", + }); - const response = await rs.text() - expect(response).toBe('0\n1\n') - }) + const response = await rs.text(); + expect(response).toBe("0\n1\n"); + }); - it('can send a query with a trailing comment', async () => { + it("can send a query with a trailing comment", async () => { const rs = await client.query({ query: ` SELECT number FROM system.numbers LIMIT 2 -- comment`, - format: 'JSON', - }) + format: "JSON", + }); - const response = await rs.json<{ number: string }>() - expect(response.data).toEqual([{ number: '0' }, { number: '1' }]) - }) + const response = await rs.json<{ number: string }>(); + expect(response.data).toEqual([{ number: "0" }, { number: "1" }]); + }); - it('can specify settings in select', async () => { + it("can specify settings in select", async () => { const rs = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 5', - format: 'CSV', + query: "SELECT number FROM system.numbers LIMIT 5", + format: "CSV", clickhouse_settings: { - limit: '2', + limit: "2", }, - }) + }); - const response = await rs.text() - expect(response).toBe('0\n1\n') - }) + const response = await rs.text(); + expect(response).toBe("0\n1\n"); + }); - it('does not swallow a client error', async () => { + it("does not swallow a client error", async () => { await expect( - client.query({ query: 'SELECT number FR' }), + client.query({ query: "SELECT number FR" }), ).rejects.toMatchObject( expect.objectContaining({ - type: 'UNKNOWN_IDENTIFIER', + type: "UNKNOWN_IDENTIFIER", }), - ) - }) + ); + }); - it('returns an error details provided by ClickHouse', async () => { - await expect(client.query({ query: 'foobar' })).rejects.toMatchObject( + it("returns an error details provided by ClickHouse", async () => { + await expect(client.query({ query: "foobar" })).rejects.toMatchObject( expect.objectContaining({ - message: expect.stringContaining('Syntax error'), - code: '62', - type: 'SYNTAX_ERROR', + message: expect.stringContaining("Syntax error"), + code: "62", + type: "SYNTAX_ERROR", }), - ) - }) + ); + }); - it('should provide error details when sending a request with an unknown clickhouse settings', async () => { + it("should provide error details when sending a request with an unknown clickhouse settings", async () => { await expect( client.query({ - query: 'SELECT * FROM system.numbers', + query: "SELECT * FROM system.numbers", clickhouse_settings: { foobar: 1 } as any, }), ).rejects.toMatchObject( @@ -154,64 +154,64 @@ describe('select', () => { // Possible error messages: // Unknown setting foobar // Setting foobar is neither a builtin setting nor started with the prefix 'SQL_' registered for user-defined settings. - message: expect.stringContaining('foobar'), - code: '115', - type: 'UNKNOWN_SETTING', + message: expect.stringContaining("foobar"), + code: "115", + type: "UNKNOWN_SETTING", }), - ) - }) + ); + }); - it('can send multiple simultaneous requests', async () => { - const results: number[] = [] + it("can send multiple simultaneous requests", async () => { + const results: number[] = []; await Promise.all( [...Array(5)].map((_, i) => client .query({ query: `SELECT toInt32(sum(*)) AS sum FROM numbers(0, ${i + 2});`, - format: 'JSONEachRow', + format: "JSONEachRow", }) .then((r) => r.json<{ sum: number }>()) .then((json) => results.push(json[0].sum)), ), - ) - expect(results.sort((a, b) => a - b)).toEqual([1, 3, 6, 10, 15]) - }) + ); + expect(results.sort((a, b) => a - b)).toEqual([1, 3, 6, 10, 15]); + }); - it('should get the response headers', async () => { + it("should get the response headers", async () => { const rs = await client.query({ - query: 'SELECT * FROM system.numbers LIMIT 1', - format: 'JSONEachRow', - }) + query: "SELECT * FROM system.numbers LIMIT 1", + format: "JSONEachRow", + }); expect( - rs.response_headers['Content-Type'] ?? - rs.response_headers['content-type'], - ).toEqual('application/x-ndjson; charset=UTF-8') - }) + rs.response_headers["Content-Type"] ?? + rs.response_headers["content-type"], + ).toEqual("application/x-ndjson; charset=UTF-8"); + }); - describe('trailing semi', () => { - it('should allow queries with trailing semicolon', async () => { + describe("trailing semi", () => { + it("should allow queries with trailing semicolon", async () => { const numbers = await client.query({ - query: 'SELECT * FROM system.numbers LIMIT 3;', - format: 'CSV', - }) - expect(await numbers.text()).toEqual('0\n1\n2\n') - }) + query: "SELECT * FROM system.numbers LIMIT 3;", + format: "CSV", + }); + expect(await numbers.text()).toEqual("0\n1\n2\n"); + }); - it('should allow queries with multiple trailing semicolons', async () => { + it("should allow queries with multiple trailing semicolons", async () => { const numbers = await client.query({ - query: 'SELECT * FROM system.numbers LIMIT 3;;;;;;;;;;;;;;;;;', - format: 'CSV', - }) - expect(await numbers.text()).toEqual('0\n1\n2\n') - }) + query: "SELECT * FROM system.numbers LIMIT 3;;;;;;;;;;;;;;;;;", + format: "CSV", + }); + expect(await numbers.text()).toEqual("0\n1\n2\n"); + }); - it('should allow semi in select clause', async () => { + it("should allow semi in select clause", async () => { const resultSet = await client.query({ query: `SELECT ';'`, - format: 'CSV', - }) - expect(await resultSet.text()).toEqual('";"\n') - }) - }) -}) + format: "CSV", + }); + expect(await resultSet.text()).toEqual('";"\n'); + }); + }); +}); diff --git a/packages/client-common/__tests__/integration/select_query_binding.test.ts b/packages/client-common/__tests__/integration/select_query_binding.test.ts index 569f415c..703536b7 100644 --- a/packages/client-common/__tests__/integration/select_query_binding.test.ts +++ b/packages/client-common/__tests__/integration/select_query_binding.test.ts @@ -1,70 +1,70 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import type { QueryParams } from '@clickhouse/client-common' -import { TupleParam } from '@clickhouse/client-common' -import { type ClickHouseClient } from '@clickhouse/client-common' -import { createTestClient } from '../utils' - -describe('select with query binding', () => { - let client: ClickHouseClient +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import type { QueryParams } from "@clickhouse/client-common"; +import { TupleParam } from "@clickhouse/client-common"; +import { type ClickHouseClient } from "@clickhouse/client-common"; +import { createTestClient } from "../utils"; + +describe("select with query binding", () => { + let client: ClickHouseClient; beforeEach(() => { - client = createTestClient() - }) + client = createTestClient(); + }); afterEach(async () => { - await client.close() - }) + await client.close(); + }); - it('can specify a parameterized query', async () => { + it("can specify a parameterized query", async () => { const rs = await client.query({ query: - 'SELECT number FROM system.numbers WHERE number > {min_limit: UInt64} LIMIT 3', - format: 'CSV', + "SELECT number FROM system.numbers WHERE number > {min_limit: UInt64} LIMIT 3", + format: "CSV", query_params: { min_limit: 2, }, - }) + }); - const response = await rs.text() - expect(response).toBe('3\n4\n5\n') - }) + const response = await rs.text(); + expect(response).toBe("3\n4\n5\n"); + }); - it('handles boolean in a parameterized query', async () => { + it("handles boolean in a parameterized query", async () => { const rs1 = await client.query({ - query: 'SELECT and({val1: Boolean}, {val2: Boolean})', - format: 'CSV', + query: "SELECT and({val1: Boolean}, {val2: Boolean})", + format: "CSV", query_params: { val1: true, val2: true, }, - }) + }); - expect(await rs1.text()).toBe('true\n') + expect(await rs1.text()).toBe("true\n"); const rs2 = await client.query({ - query: 'SELECT and({val1: Boolean}, {val2: Boolean})', - format: 'CSV', + query: "SELECT and({val1: Boolean}, {val2: Boolean})", + format: "CSV", query_params: { val1: true, val2: false, }, - }) + }); - expect(await rs2.text()).toBe('false\n') - }) + expect(await rs2.text()).toBe("false\n"); + }); - it('handles numbers in a parameterized query', async () => { + it("handles numbers in a parameterized query", async () => { const rs = await client.query({ - query: 'SELECT plus({val1: Int32}, {val2: Int32})', - format: 'CSV', + query: "SELECT plus({val1: Int32}, {val2: Int32})", + format: "CSV", query_params: { val1: 10, val2: 20, }, - }) + }); - expect(await rs.text()).toBe('30\n') - }) + expect(await rs.text()).toBe("30\n"); + }); - it('handles special characters in a parametrized query', async () => { + it("handles special characters in a parametrized query", async () => { const rs = await client.query({ query: ` SELECT @@ -73,15 +73,15 @@ describe('select with query binding', () => { 'foo_\r_bar' = {carriage_return: String} AS has_carriage_return, 'foo_\\'_bar' = {single_quote: String} AS has_single_quote, 'foo_\\_bar' = {backslash: String} AS has_backslash`, - format: 'JSONEachRow', + format: "JSONEachRow", query_params: { - tab: 'foo_\t_bar', - newline: 'foo_\n_bar', - carriage_return: 'foo_\r_bar', + tab: "foo_\t_bar", + newline: "foo_\n_bar", + carriage_return: "foo_\r_bar", single_quote: "foo_'_bar", - backslash: 'foo_\\_bar', + backslash: "foo_\\_bar", }, - }) + }); expect(await rs.json()).toEqual([ { @@ -91,65 +91,65 @@ describe('select with query binding', () => { has_single_quote: 1, has_backslash: 1, }, - ]) - }) + ]); + }); - it('handles tuples in a parametrized query', async () => { + it("handles tuples in a parametrized query", async () => { const rs = await client.query({ query: - 'SELECT {var: Tuple(Int32, String, String, String, Nullable(String))} AS result', - format: 'JSONEachRow', + "SELECT {var: Tuple(Int32, String, String, String, Nullable(String))} AS result", + format: "JSONEachRow", query_params: { - var: new TupleParam([42, 'foo', "foo_'_bar", 'foo_\t_bar', null]), + var: new TupleParam([42, "foo", "foo_'_bar", "foo_\t_bar", null]), }, - }) + }); expect(await rs.json()).toEqual([ { - result: [42, 'foo', "foo_'_bar", 'foo_\t_bar', null], + result: [42, "foo", "foo_'_bar", "foo_\t_bar", null], }, - ]) - }) + ]); + }); - it('handles arrays of tuples in a parametrized query', async () => { + it("handles arrays of tuples in a parametrized query", async () => { const rs = await client.query({ query: - 'SELECT {var: Array(Tuple(Int32, String, String, String, Nullable(String)))} AS result', - format: 'JSONEachRow', + "SELECT {var: Array(Tuple(Int32, String, String, String, Nullable(String)))} AS result", + format: "JSONEachRow", query_params: { - var: [new TupleParam([42, 'foo', "foo_'_bar", 'foo_\t_bar', null])], + var: [new TupleParam([42, "foo", "foo_'_bar", "foo_\t_bar", null])], }, - }) + }); expect(await rs.json()).toEqual([ { - result: [[42, 'foo', "foo_'_bar", 'foo_\t_bar', null]], + result: [[42, "foo", "foo_'_bar", "foo_\t_bar", null]], }, - ]) - }) + ]); + }); - it('handles maps with tuples in a parametrized query', async () => { + it("handles maps with tuples in a parametrized query", async () => { const rs = await client.query({ query: - 'SELECT {var: Map(Int32, Tuple(Int32, String, String, String))} AS result', - format: 'JSONEachRow', + "SELECT {var: Map(Int32, Tuple(Int32, String, String, String))} AS result", + format: "JSONEachRow", query_params: { var: new Map([ - [42, new TupleParam([144, 'foo', "foo_'_bar", 'foo_\t_bar'])], + [42, new TupleParam([144, "foo", "foo_'_bar", "foo_\t_bar"])], ]), }, - }) + }); expect(await rs.json()).toEqual([ { result: { - 42: [144, 'foo', "foo_'_bar", 'foo_\t_bar'], + 42: [144, "foo", "foo_'_bar", "foo_\t_bar"], }, }, - ]) - }) + ]); + }); - it('handles maps with nested arrays in a parametrized query', async () => { + it("handles maps with nested arrays in a parametrized query", async () => { const rs = await client.query({ - query: 'SELECT {var: Map(Int32, Array(Array(Int32)))} AS result', - format: 'JSONEachRow', + query: "SELECT {var: Map(Int32, Array(Array(Int32)))} AS result", + format: "JSONEachRow", query_params: { var: new Map([ [ @@ -161,7 +161,7 @@ describe('select with query binding', () => { ], ]), }, - }) + }); expect(await rs.json()).toEqual([ { result: { @@ -171,28 +171,28 @@ describe('select with query binding', () => { ], }, }, - ]) - }) + ]); + }); - it('handles maps with nullable values in a parametrized query', async () => { + it("handles maps with nullable values in a parametrized query", async () => { const rs = await client.query({ query: ` SELECT {var1: Map(Int32, Nullable(String))} AS var1, {var2: Map(String, Nullable(Int32))} AS var2 `, - format: 'JSONEachRow', + format: "JSONEachRow", query_params: { var1: new Map([ - [42, 'foo'], + [42, "foo"], [144, null], ]), var2: { foo: 42, bar: null }, }, - }) + }); expect(await rs.json()).toEqual([ { var1: { - 42: 'foo', + 42: "foo", 144: null, }, var2: { @@ -200,201 +200,201 @@ describe('select with query binding', () => { bar: null, }, }, - ]) - }) + ]); + }); - describe('Date(Time)', () => { - it('handles Date in a parameterized query', async () => { + describe("Date(Time)", () => { + it("handles Date in a parameterized query", async () => { const rs = await client.query({ - query: 'SELECT toDate({min_time: DateTime})', - format: 'CSV', + query: "SELECT toDate({min_time: DateTime})", + format: "CSV", query_params: { min_time: new Date(Date.UTC(2022, 4, 2)), }, - }) + }); - const response = await rs.text() - expect(response).toBe('"2022-05-02"\n') - }) + const response = await rs.text(); + expect(response).toBe('"2022-05-02"\n'); + }); - it('handles DateTime in a parameterized query', async () => { + it("handles DateTime in a parameterized query", async () => { const rs = await client.query({ - query: 'SELECT toDateTime({min_time: DateTime})', - format: 'CSV', + query: "SELECT toDateTime({min_time: DateTime})", + format: "CSV", query_params: { min_time: new Date(Date.UTC(2022, 4, 2, 13, 25, 55)), }, - }) + }); - const response = await rs.text() - expect(response).toBe('"2022-05-02 13:25:55"\n') - }) + const response = await rs.text(); + expect(response).toBe('"2022-05-02 13:25:55"\n'); + }); - it('handles DateTime64(3) in a parameterized query', async () => { + it("handles DateTime64(3) in a parameterized query", async () => { const rs = await client.query({ - query: 'SELECT toDateTime64({min_time: DateTime64(3)}, 3)', - format: 'CSV', + query: "SELECT toDateTime64({min_time: DateTime64(3)}, 3)", + format: "CSV", query_params: { min_time: new Date(Date.UTC(2022, 4, 2, 13, 25, 55, 789)), }, - }) + }); - const response = await rs.text() - expect(response).toBe('"2022-05-02 13:25:55.789"\n') - }) + const response = await rs.text(); + expect(response).toBe('"2022-05-02 13:25:55.789"\n'); + }); - it('handles DateTime64(6) with timestamp as a string', async () => { + it("handles DateTime64(6) with timestamp as a string", async () => { const rs = await client.query({ query: `SELECT toDateTime64(toDecimal64({ts: String}, 6), 6, 'Europe/Amsterdam')`, - format: 'CSV', + format: "CSV", query_params: { - ts: '1651490755.123456', + ts: "1651490755.123456", }, - }) + }); - const response = await rs.text() - expect(response).toBe('"2022-05-02 13:25:55.123456"\n') - }) + const response = await rs.text(); + expect(response).toBe('"2022-05-02 13:25:55.123456"\n'); + }); - it('handles DateTime64(9) with timestamp as a string', async () => { + it("handles DateTime64(9) with timestamp as a string", async () => { const rs = await client.query({ query: `SELECT toDateTime64(toDecimal128({ts: String}, 9), 9, 'Europe/Amsterdam')`, - format: 'CSV', + format: "CSV", query_params: { - ts: '1651490755.123456789', + ts: "1651490755.123456789", }, - }) + }); - const response = await rs.text() - expect(response).toBe('"2022-05-02 13:25:55.123456789"\n') - }) - }) + const response = await rs.text(); + expect(response).toBe('"2022-05-02 13:25:55.123456789"\n'); + }); + }); - it('handles an array of strings in a parameterized query', async () => { + it("handles an array of strings in a parameterized query", async () => { const rs = await client.query({ query: - 'SELECT arrayConcat({arr1: Array(String)}, {arr2: Array(Nullable(String))})', - format: 'CSV', + "SELECT arrayConcat({arr1: Array(String)}, {arr2: Array(Nullable(String))})", + format: "CSV", query_params: { - arr1: ['1', '2'], - arr2: ['3', null], + arr1: ["1", "2"], + arr2: ["3", null], }, - }) + }); - const response = await rs.text() - expect(response).toBe(`"['1','2','3',NULL]"\n`) - }) + const response = await rs.text(); + expect(response).toBe(`"['1','2','3',NULL]"\n`); + }); - it('handles an array of numbers in a parameterized query', async () => { + it("handles an array of numbers in a parameterized query", async () => { const rs = await client.query({ - query: 'SELECT arrayConcat({arr1: Array(Int32)}, {arr2: Array(Int32)})', - format: 'CSV', + query: "SELECT arrayConcat({arr1: Array(Int32)}, {arr2: Array(Int32)})", + format: "CSV", query_params: { arr1: [1, 2], arr2: [3, 4], }, - }) + }); - const response = await rs.text() - expect(response).toBe(`"[1,2,3,4]"\n`) - }) + const response = await rs.text(); + expect(response).toBe(`"[1,2,3,4]"\n`); + }); - it('escapes strings in a parameterized query', async () => { + it("escapes strings in a parameterized query", async () => { const rs = await client.query({ - query: 'SELECT concat({str1: String},{str2: String})', - format: 'CSV', + query: "SELECT concat({str1: String},{str2: String})", + format: "CSV", query_params: { str1: "co'n", str2: "ca't", }, - }) + }); - const response = await rs.text() - expect(response).toBe('"co\'nca\'t"\n') - }) + const response = await rs.text(); + expect(response).toBe("\"co'nca't\"\n"); + }); - it('handles an object as a map a parameterized query', async () => { + it("handles an object as a map a parameterized query", async () => { const rs = await client.query({ - query: 'SELECT mapKeys({obj: Map(String, UInt32)})', - format: 'CSV', + query: "SELECT mapKeys({obj: Map(String, UInt32)})", + format: "CSV", query_params: { obj: { id: 42 }, }, - }) + }); - const response = await rs.text() - expect(response).toBe(`"['id']"\n`) - }) + const response = await rs.text(); + expect(response).toBe(`"['id']"\n`); + }); - it('should accept non-ASCII symbols in a parameterized query', async () => { + it("should accept non-ASCII symbols in a parameterized query", async () => { const rs = await client.query({ - query: 'SELECT concat({str1: String},{str2: String})', - format: 'CSV', + query: "SELECT concat({str1: String},{str2: String})", + format: "CSV", query_params: { - str1: '𝓯𝓸𝓸', - str2: '𝓫𝓪𝓻', + str1: "𝓯𝓸𝓸", + str2: "𝓫𝓪𝓻", }, - }) + }); - const response = await rs.text() - expect(response).toBe('"𝓯𝓸𝓸𝓫𝓪𝓻"\n') - }) + const response = await rs.text(); + expect(response).toBe('"𝓯𝓸𝓸𝓫𝓪𝓻"\n'); + }); - describe('Enum', () => { - it('should accept numeric enum-like values in a parametrized query', async () => { + describe("Enum", () => { + it("should accept numeric enum-like values in a parametrized query", async () => { const MyEnum = { foo: 0, bar: 1, qaz: 2, - } as const + } as const; const rs = await client.query({ query: - 'SELECT * FROM system.numbers WHERE number = {filter: Int64} LIMIT 1', - format: 'TabSeparated', + "SELECT * FROM system.numbers WHERE number = {filter: Int64} LIMIT 1", + format: "TabSeparated", query_params: { filter: MyEnum.qaz, // translated to 2 }, - }) + }); - const response = await rs.text() - expect(response).toBe('2\n') - }) + const response = await rs.text(); + expect(response).toBe("2\n"); + }); - it('should accept string enum-like values in a parametrized query', async () => { + it("should accept string enum-like values in a parametrized query", async () => { const MyEnum = { - foo: 'foo', - bar: 'bar', - } as const + foo: "foo", + bar: "bar", + } as const; const rs = await client.query({ - query: 'SELECT concat({str1: String},{str2: String})', - format: 'TabSeparated', + query: "SELECT concat({str1: String},{str2: String})", + format: "TabSeparated", query_params: { str1: MyEnum.foo, str2: MyEnum.bar, }, - }) + }); - const response = await rs.text() - expect(response).toBe('foobar\n') - }) + const response = await rs.text(); + expect(response).toBe("foobar\n"); + }); // this one is taken from https://clickhouse.com/docs/en/sql-reference/data-types/enum/#usage-examples - it('should accept the entire enum definition in a parametrized query', async () => { + it("should accept the entire enum definition in a parametrized query", async () => { const rs = await client.query({ query: `SELECT toTypeName(CAST('a', {e: String}))`, - format: 'TabSeparated', + format: "TabSeparated", query_params: { e: `Enum('a' = 1, 'b' = 2)`, }, - }) + }); - const response = await rs.text() - expect(response).toBe(`Enum8(\\'a\\' = 1, \\'b\\' = 2)\n`) - }) + const response = await rs.text(); + expect(response).toBe(`Enum8(\\'a\\' = 1, \\'b\\' = 2)\n`); + }); - it('should provide error details when sending a request with missing parameter', async () => { + it("should provide error details when sending a request with missing parameter", async () => { await expect( client.query({ query: ` @@ -411,124 +411,124 @@ describe('select with query binding', () => { // (pre-23.8) Query parameter `min_limit` was not set /^.+?`min_limit`.+?not set.*$/, ), - code: '456', - type: 'UNKNOWN_QUERY_PARAMETER', - }) - }) - }) + code: "456", + type: "UNKNOWN_QUERY_PARAMETER", + }); + }); + }); - describe('NULL parameter binding', () => { + describe("NULL parameter binding", () => { const baseQuery: QueryParams = { - query: 'SELECT number FROM numbers(3) WHERE {n:Nullable(String)} IS NULL', - format: 'CSV', - } + query: "SELECT number FROM numbers(3) WHERE {n:Nullable(String)} IS NULL", + format: "CSV", + }; - it('should work with nulls', async () => { + it("should work with nulls", async () => { const rs = await client.query({ ...baseQuery, query_params: { n: null, }, - }) + }); - const response = await rs.text() - expect(response).toBe('0\n1\n2\n') - }) + const response = await rs.text(); + expect(response).toBe("0\n1\n2\n"); + }); - it('should with an explicit undefined', async () => { + it("should with an explicit undefined", async () => { const rs = await client.query({ ...baseQuery, query_params: { n: undefined, }, - }) + }); - const response = await rs.text() - expect(response).toBe('0\n1\n2\n') - }) - }) + const response = await rs.text(); + expect(response).toBe("0\n1\n2\n"); + }); + }); - describe('Nested boolean types', () => { - it('handles boolean in an array', async () => { + describe("Nested boolean types", () => { + it("handles boolean in an array", async () => { const params = { foo: [true, false, true], bar: [true, null, false], - } + }; const rs = await client.query({ query: ` SELECT {foo: Array(Boolean)} AS foo, {bar: Array(Nullable(Boolean))} AS bar `, - format: 'JSONEachRow', + format: "JSONEachRow", query_params: params, - }) + }); - const response = await rs.json() - expect(response).toEqual([params]) - }) + const response = await rs.json(); + expect(response).toEqual([params]); + }); - it('handles boolean in a tuple', async () => { - const foo = [1, true, 'foo'] - const bar = [null, 42] + it("handles boolean in a tuple", async () => { + const foo = [1, true, "foo"]; + const bar = [null, 42]; const params = { foo: new TupleParam(foo), bar: new TupleParam(bar), - } + }; const rs = await client.query({ query: ` SELECT {foo: Tuple(Int32, Boolean, String)} AS foo, {bar: Tuple(Nullable(Boolean), Int16)} AS bar `, - format: 'JSONEachRow', + format: "JSONEachRow", query_params: params, - }) + }); - const response = await rs.json() - expect(response).toEqual([{ foo, bar }]) - }) + const response = await rs.json(); + expect(response).toEqual([{ foo, bar }]); + }); - it('handles boolean in a map', async () => { - const foo = { item1: true, item2: false } - const bar = { item1: null, item2: true } + it("handles boolean in a map", async () => { + const foo = { item1: true, item2: false }; + const bar = { item1: null, item2: true }; const params = { foo: new Map(Object.entries(foo)), bar: new Map(Object.entries(bar)), - } + }; const rs = await client.query({ query: ` SELECT {foo: Map(String, Boolean)} AS foo, {bar: Map(String, Nullable(Boolean))} AS bar `, - format: 'JSONEachRow', + format: "JSONEachRow", query_params: params, - }) + }); - const response = await rs.json() - expect(response).toEqual([{ foo, bar }]) - }) + const response = await rs.json(); + expect(response).toEqual([{ foo, bar }]); + }); - it('handles boolean in a mixed nested structure', async () => { + it("handles boolean in a mixed nested structure", async () => { const rs = await client.query({ query: ` SELECT {val: Array(Map(String, Tuple(Int32, Boolean)))} AS result `, - format: 'JSONEachRow', + format: "JSONEachRow", query_params: { val: [ { item1: new TupleParam([1, true]) }, { item2: new TupleParam([2, false]) }, ], }, - }) + }); - const response = await rs.json() + const response = await rs.json(); expect(response).toEqual([ { result: [{ item1: [1, true] }, { item2: [2, false] }], }, - ]) - }) - }) -}) + ]); + }); + }); +}); diff --git a/packages/client-common/__tests__/integration/select_result.test.ts b/packages/client-common/__tests__/integration/select_result.test.ts index dff9b269..f06b5c56 100644 --- a/packages/client-common/__tests__/integration/select_result.test.ts +++ b/packages/client-common/__tests__/integration/select_result.test.ts @@ -1,53 +1,53 @@ -import { describe, it, expect, afterEach, beforeEach } from 'vitest' -import type { ClickHouseClient } from '@clickhouse/client-common' -import { createTestClient } from '../utils' +import { describe, it, expect, afterEach, beforeEach } from "vitest"; +import type { ClickHouseClient } from "@clickhouse/client-common"; +import { createTestClient } from "../utils"; -describe('Select ResultSet', () => { - let client: ClickHouseClient +describe("Select ResultSet", () => { + let client: ClickHouseClient; afterEach(async () => { - await client.close() - }) + await client.close(); + }); beforeEach(async () => { - client = createTestClient() - }) + client = createTestClient(); + }); - describe('text() method', function () { - it('returns values from SELECT query in specified format', async () => { + describe("text() method", function () { + it("returns values from SELECT query in specified format", async () => { const rs = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 3', - format: 'CSV', - }) + query: "SELECT number FROM system.numbers LIMIT 3", + format: "CSV", + }); - expect(await rs.text()).toBe('0\n1\n2\n') - }) + expect(await rs.text()).toBe("0\n1\n2\n"); + }); - it('returns values from SELECT query in specified format', async () => { + it("returns values from SELECT query in specified format", async () => { const rs = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 3', - format: 'JSONEachRow', - }) + query: "SELECT number FROM system.numbers LIMIT 3", + format: "JSONEachRow", + }); expect(await rs.text()).toBe( '{"number":"0"}\n{"number":"1"}\n{"number":"2"}\n', - ) - }) - }) + ); + }); + }); - describe('json() method', () => { + describe("json() method", () => { interface Data { - number: string + number: string; } - it('should have correct fields in the response for JSON format', async () => { + it("should have correct fields in the response for JSON format", async () => { const rs = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 3', - format: 'JSON', - }) - const responseJSON = await rs.json() - expect(Array.isArray(responseJSON.data)).toBe(true) + query: "SELECT number FROM system.numbers LIMIT 3", + format: "JSON", + }); + const responseJSON = await rs.json(); + expect(Array.isArray(responseJSON.data)).toBe(true); expect(responseJSON).toEqual({ - data: [{ number: '0' }, { number: '1' }, { number: '2' }], - meta: [{ name: 'number', type: 'UInt64' }], + data: [{ number: "0" }, { number: "1" }, { number: "2" }], + meta: [{ name: "number", type: "UInt64" }], rows: 3, rows_before_limit_at_least: 3, statistics: { @@ -55,7 +55,7 @@ describe('Select ResultSet', () => { rows_read: expect.any(Number), bytes_read: expect.any(Number), }, - }) - }) - }) -}) + }); + }); + }); +}); diff --git a/packages/client-common/__tests__/integration/session.test.ts b/packages/client-common/__tests__/integration/session.test.ts index a948f789..0b4ebb21 100644 --- a/packages/client-common/__tests__/integration/session.test.ts +++ b/packages/client-common/__tests__/integration/session.test.ts @@ -1,81 +1,81 @@ -import { describe, it, expect, afterEach } from 'vitest' -import type { ClickHouseClient } from '@clickhouse/client-common' -import { createTestClient, guid, TestEnv, isOnEnv } from '@test/utils' +import { describe, it, expect, afterEach } from "vitest"; +import type { ClickHouseClient } from "@clickhouse/client-common"; +import { createTestClient, guid, TestEnv, isOnEnv } from "@test/utils"; -describe('sessions settings', () => { - let client: ClickHouseClient +describe("sessions settings", () => { + let client: ClickHouseClient; afterEach(async () => { - await client.close() - }) + await client.close(); + }); it.skipIf(!isOnEnv(TestEnv.LocalSingleNode))( - 'should use sessions', + "should use sessions", async () => { client = createTestClient({ session_id: `test-session-${guid()}`, - }) + }); - const tableName = `temp_table_${guid()}` + const tableName = `temp_table_${guid()}`; await client.command({ query: getTempTableDDL(tableName), - }) + }); await client.insert({ table: tableName, - values: [{ id: 42, name: 'foo' }], - format: 'JSONEachRow', - }) + values: [{ id: 42, name: "foo" }], + format: "JSONEachRow", + }); await client.exec({ query: `INSERT INTO ${tableName} VALUES (43, 'bar')`, - }) + }); const rs = await client.query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); expect(await rs.json()).toEqual([ - { id: 42, name: 'foo' }, - { id: 43, name: 'bar' }, - ]) + { id: 42, name: "foo" }, + { id: 43, name: "bar" }, + ]); }, - ) + ); it.skipIf(!isOnEnv(TestEnv.LocalSingleNode))( - 'should use session override', + "should use session override", async () => { // no session_id by default - client = createTestClient() + client = createTestClient(); - const sessionId = `test-session-${guid()}` - const tableName = `temp_table_${guid()}` + const sessionId = `test-session-${guid()}`; + const tableName = `temp_table_${guid()}`; await client.command({ query: getTempTableDDL(tableName), session_id: sessionId, - }) + }); await client.insert({ table: tableName, - values: [{ id: 144, name: 'qaz' }], - format: 'JSONEachRow', + values: [{ id: 144, name: "qaz" }], + format: "JSONEachRow", session_id: sessionId, - }) + }); await client.exec({ query: `INSERT INTO ${tableName} VALUES (255, 'qux')`, session_id: sessionId, - }) + }); const rs = await client.query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, - format: 'JSONEachRow', + format: "JSONEachRow", session_id: sessionId, - }) + }); expect(await rs.json()).toEqual([ - { id: 144, name: 'qaz' }, - { id: 255, name: 'qux' }, - ]) + { id: 144, name: "qaz" }, + { id: 255, name: "qux" }, + ]); }, - ) + ); function getTempTableDDL(tableName: string) { return ` CREATE TEMPORARY TABLE ${tableName} (id Int32, name String) - ` + `; } -}) +}); diff --git a/packages/client-common/__tests__/integration/totals.test.ts b/packages/client-common/__tests__/integration/totals.test.ts index 4c87562c..9980c502 100644 --- a/packages/client-common/__tests__/integration/totals.test.ts +++ b/packages/client-common/__tests__/integration/totals.test.ts @@ -1,66 +1,66 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import type { ClickHouseClient } from '@clickhouse/client-common' -import { createSimpleTable } from '@test/fixtures/simple_table' -import { createTestClient, guid } from '@test/utils' +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import type { ClickHouseClient } from "@clickhouse/client-common"; +import { createSimpleTable } from "@test/fixtures/simple_table"; +import { createTestClient, guid } from "@test/utils"; -describe('Queries with totals', () => { - let client: ClickHouseClient - let tableName: string +describe("Queries with totals", () => { + let client: ClickHouseClient; + let tableName: string; beforeEach(async () => { - client = createTestClient() - tableName = `totals_test_${guid()}` - await createSimpleTable(client, tableName) - }) + client = createTestClient(); + tableName = `totals_test_${guid()}`; + await createSimpleTable(client, tableName); + }); afterEach(async () => { - await client.close() - }) + await client.close(); + }); - it('should return the expected totals', async () => { + it("should return the expected totals", async () => { await client.insert({ table: tableName, values: [ - { id: '42', name: 'hello', sku: [0, 1] }, - { id: '43', name: 'hello', sku: [2, 3] }, - { id: '44', name: 'foo', sku: [3, 4] }, - { id: '45', name: 'foo', sku: [4, 5] }, - { id: '46', name: 'foo', sku: [6, 7] }, + { id: "42", name: "hello", sku: [0, 1] }, + { id: "43", name: "hello", sku: [2, 3] }, + { id: "44", name: "foo", sku: [3, 4] }, + { id: "45", name: "foo", sku: [4, 5] }, + { id: "46", name: "foo", sku: [6, 7] }, ], - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); const rs1 = await client.query({ query: `SELECT *, count(*) AS count FROM ${tableName} GROUP BY id, name, sku WITH TOTALS ORDER BY id ASC`, - format: 'JSON', - }) - const result1 = await rs1.json() + format: "JSON", + }); + const result1 = await rs1.json(); expect(result1.data).toEqual([ - { id: '42', name: 'hello', sku: [0, 1], count: '1' }, - { id: '43', name: 'hello', sku: [2, 3], count: '1' }, - { id: '44', name: 'foo', sku: [3, 4], count: '1' }, - { id: '45', name: 'foo', sku: [4, 5], count: '1' }, - { id: '46', name: 'foo', sku: [6, 7], count: '1' }, - ]) + { id: "42", name: "hello", sku: [0, 1], count: "1" }, + { id: "43", name: "hello", sku: [2, 3], count: "1" }, + { id: "44", name: "foo", sku: [3, 4], count: "1" }, + { id: "45", name: "foo", sku: [4, 5], count: "1" }, + { id: "46", name: "foo", sku: [6, 7], count: "1" }, + ]); expect(result1.totals).toEqual({ - id: '0', - name: '', + id: "0", + name: "", sku: [], - count: '5', - }) + count: "5", + }); const rs2 = await client.query({ query: `SELECT 1 :: Int16 AS x, name, count(*) AS count FROM ${tableName} GROUP BY name WITH TOTALS ORDER BY name ASC`, - format: 'JSON', - }) - const result2 = await rs2.json() + format: "JSON", + }); + const result2 = await rs2.json(); expect(result2.data).toEqual([ - { x: 1, name: 'foo', count: '3' }, - { x: 1, name: 'hello', count: '2' }, - ]) + { x: 1, name: "foo", count: "3" }, + { x: 1, name: "hello", count: "2" }, + ]); expect(result2.totals).toEqual({ x: 1, - name: '', - count: '5', - }) - }) -}) + name: "", + count: "5", + }); + }); +}); diff --git a/packages/client-common/__tests__/unit/clickhouse_types.test.ts b/packages/client-common/__tests__/unit/clickhouse_types.test.ts index e0d1da15..af79c005 100644 --- a/packages/client-common/__tests__/unit/clickhouse_types.test.ts +++ b/packages/client-common/__tests__/unit/clickhouse_types.test.ts @@ -1,57 +1,57 @@ -import { describe, it, expect } from 'vitest' -import { isException, isProgressRow, isRow } from '../../src/index' +import { describe, it, expect } from "vitest"; +import { isException, isProgressRow, isRow } from "../../src/index"; -describe('ClickHouse types', () => { - it('should check if a row is progress row', async () => { +describe("ClickHouse types", () => { + it("should check if a row is progress row", async () => { const row = { progress: { - read_rows: '1', - read_bytes: '1', - elapsed_ns: '1', + read_rows: "1", + read_bytes: "1", + elapsed_ns: "1", }, - } - expect(isProgressRow(row)).toBeTruthy() - expect(isProgressRow({})).toBeFalsy() + }; + expect(isProgressRow(row)).toBeTruthy(); + expect(isProgressRow({})).toBeFalsy(); expect( isProgressRow({ ...row, - extra: 'extra', + extra: "extra", }), - ).toBeFalsy() - expect(isProgressRow(null)).toBeFalsy() - expect(isProgressRow(undefined)).toBeFalsy() - expect(isProgressRow(42)).toBeFalsy() - expect(isProgressRow({ foo: 'bar' })).toBeFalsy() - }) + ).toBeFalsy(); + expect(isProgressRow(null)).toBeFalsy(); + expect(isProgressRow(undefined)).toBeFalsy(); + expect(isProgressRow(42)).toBeFalsy(); + expect(isProgressRow({ foo: "bar" })).toBeFalsy(); + }); - it('should check if a row is a data row', async () => { - const row = { row: { foo: 'bar' } } - expect(isRow(row)).toBeTruthy() + it("should check if a row is a data row", async () => { + const row = { row: { foo: "bar" } }; + expect(isRow(row)).toBeTruthy(); expect( isRow({ ...row, - extra: 'extra', + extra: "extra", }), - ).toBeFalsy() - expect(isRow(null)).toBeFalsy() - expect(isRow(undefined)).toBeFalsy() - expect(isRow(42)).toBeFalsy() - expect(isRow({ foo: 'bar' })).toBeFalsy() - }) + ).toBeFalsy(); + expect(isRow(null)).toBeFalsy(); + expect(isRow(undefined)).toBeFalsy(); + expect(isRow(42)).toBeFalsy(); + expect(isRow({ foo: "bar" })).toBeFalsy(); + }); - it('should check if a row has an exception', async () => { - const row = { exception: 'Some error occurred' } - expect(isException(row)).toBeTruthy() + it("should check if a row has an exception", async () => { + const row = { exception: "Some error occurred" }; + expect(isException(row)).toBeTruthy(); expect( isException({ ...row, - extra: 'extra', + extra: "extra", }), - ).toBeFalsy() - expect(isException(null)).toBeFalsy() - expect(isException(undefined)).toBeFalsy() - expect(isException(42)).toBeFalsy() - expect(isException({ foo: 'bar' })).toBeFalsy() - expect(isException({ progress: { read_rows: '1' } })).toBeFalsy() - }) -}) + ).toBeFalsy(); + expect(isException(null)).toBeFalsy(); + expect(isException(undefined)).toBeFalsy(); + expect(isException(42)).toBeFalsy(); + expect(isException({ foo: "bar" })).toBeFalsy(); + expect(isException({ progress: { read_rows: "1" } })).toBeFalsy(); + }); +}); diff --git a/packages/client-common/__tests__/unit/client.test.ts b/packages/client-common/__tests__/unit/client.test.ts index 68ff44ce..727d1058 100644 --- a/packages/client-common/__tests__/unit/client.test.ts +++ b/packages/client-common/__tests__/unit/client.test.ts @@ -1,7 +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' +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 { @@ -9,46 +9,46 @@ function isAwaitUsingStatementSupported(): boolean { (async () => { await using c = null; }) - `) - return true + `); + return true; } catch { - return false + return false; } } function mockImpl(): any { return { make_connection: () => { - return {} as any + return {} as any; }, values_encoder: () => { - return {} as any + return {} as any; }, - } + }; } -describe('client', () => { - it('createSimpleTestClient creates a client without requiring ClickHouse', async () => { +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() - }) + const client = createSimpleTestClient(); + expect(client).toBeDefined(); + await client.close(); + }); it.skipIf(!isAwaitUsingStatementSupported())( - 'closes the client when used with using statement', + "closes the client when used with using statement", async () => { const client = new ClickHouseClient({ - url: 'http://localhost', + url: "http://localhost", impl: mockImpl(), - }) - let isClosed = false - vi.spyOn(client, 'close').mockImplementation(async () => { + }); + let isClosed = false; + vi.spyOn(client, "close").mockImplementation(async () => { // Simulate some delay in closing - await sleep(0) - isClosed = true - }) + await sleep(0); + isClosed = true; + }); // Wrap in eval to allow using statement syntax without // syntax error in older Node.js versions. Might want to @@ -58,9 +58,9 @@ describe('client', () => { await using c = value; // do nothing, just testing the disposal at the end of the block }) - `)(client) + `)(client); - expect(isClosed).toBeTruthy() + expect(isClosed).toBeTruthy(); }, - ) -}) + ); +}); diff --git a/packages/client-common/__tests__/unit/config.test.ts b/packages/client-common/__tests__/unit/config.test.ts index 1680bba9..15330438 100644 --- a/packages/client-common/__tests__/unit/config.test.ts +++ b/packages/client-common/__tests__/unit/config.test.ts @@ -1,16 +1,16 @@ -import { describe, it, expect } from 'vitest' +import { describe, it, expect } from "vitest"; import type { BaseClickHouseClientConfigOptions, HandleImplSpecificURLParams, -} from '../../src/index' +} from "../../src/index"; import { ClickHouseLogLevel, getConnectionParams, LogWriter, numberConfigURLValue, -} from '../../src/index' -import { TestLogger } from '../utils/test_logger' -import type { BaseClickHouseClientConfigOptionsWithURL } from '../../src/config' +} from "../../src/index"; +import { TestLogger } from "../utils/test_logger"; +import type { BaseClickHouseClientConfigOptionsWithURL } from "../../src/config"; import { booleanConfigURLValue, createUrl, @@ -18,37 +18,37 @@ import { loadConfigOptionsFromURL, mergeConfigs, prepareConfigWithURL, -} from '../../src/config' +} from "../../src/config"; -describe('config', () => { - const logger = new TestLogger() +describe("config", () => { + const logger = new TestLogger(); - describe('prepareConfigWithURL', () => { + describe("prepareConfigWithURL", () => { const defaultConfig: BaseClickHouseClientConfigOptionsWithURL = { - url: new URL('http://localhost:8123/'), - } + url: new URL("http://localhost:8123/"), + }; - it('should get all defaults with no extra configuration', async () => { - const res = prepareConfigWithURL({}, logger, null) - expect(res).toEqual(defaultConfig) - }) + it("should get all defaults with no extra configuration", async () => { + const res = prepareConfigWithURL({}, logger, null); + expect(res).toEqual(defaultConfig); + }); - it('should fall back to default HTTP/HTTPS port numbers', async () => { + it("should fall back to default HTTP/HTTPS port numbers", async () => { expect( - prepareConfigWithURL({ url: 'http://localhost:80' }, logger, null), + prepareConfigWithURL({ url: "http://localhost:80" }, logger, null), ).toEqual({ - url: new URL('http://localhost/'), // default HTTP port 80 is omitted - }) + url: new URL("http://localhost/"), // default HTTP port 80 is omitted + }); expect( - prepareConfigWithURL({ url: 'https://localhost:443' }, logger, null), + prepareConfigWithURL({ url: "https://localhost:443" }, logger, null), ).toEqual({ - url: new URL('https://localhost/'), // default HTTPS port 443 is omitted - }) - }) + url: new URL("https://localhost/"), // default HTTPS port 443 is omitted + }); + }); - it('should use non-default HTTP/HTTPS port numbers', async () => { - const sampleValidPorts = ['1', '65535', '8123', '8080', '8443'] - for (const protocol of ['http', 'https']) { + it("should use non-default HTTP/HTTPS port numbers", async () => { + const sampleValidPorts = ["1", "65535", "8123", "8080", "8443"]; + for (const protocol of ["http", "https"]) { for (const port of sampleValidPorts) { expect( prepareConfigWithURL( @@ -59,14 +59,14 @@ describe('config', () => { `${protocol} with valid port ${port} should not throw`, ).toEqual({ url: new URL(`${protocol}://localhost:${port}/`), - }) + }); } } - }) + }); - it('should throw when the HTTP/HTTPS port is not valid', async () => { - const invalidPorts = ['foo', '65536', '-1'] - for (const protocol of ['http', 'https']) { + it("should throw when the HTTP/HTTPS port is not valid", async () => { + const invalidPorts = ["foo", "65536", "-1"]; + for (const protocol of ["http", "https"]) { for (const port of invalidPorts) { expect( () => @@ -78,31 +78,31 @@ describe('config', () => { `${protocol} with invalid port ${port} is expected to throw`, ).toThrow( expect.objectContaining({ - message: expect.stringContaining('ClickHouse URL is malformed'), + message: expect.stringContaining("ClickHouse URL is malformed"), }), - ) + ); } } - }) + }); - it('should set everything, overriding the defaults', async () => { + it("should set everything, overriding the defaults", async () => { const res = prepareConfigWithURL( { - url: 'https://my.host:8443', - pathname: '/my_proxy', + url: "https://my.host:8443", + pathname: "/my_proxy", request_timeout: 42_000, max_open_connections: 144, - username: 'bob', - password: 'secret', - database: 'analytics', + username: "bob", + password: "secret", + database: "analytics", http_headers: { - 'X-CLICKHOUSE-AUTH': 'secret_header', + "X-CLICKHOUSE-AUTH": "secret_header", }, keep_alive: { enabled: false }, - application: 'my_app', + application: "my_app", // override the default HTTP settings + extra CH settings clickhouse_settings: { - http_headers_progress_interval_ms: '55000', + http_headers_progress_interval_ms: "55000", send_progress_in_http_headers: 0, async_insert: 1, }, @@ -114,22 +114,22 @@ describe('config', () => { }, logger, null, - ) + ); expect(res).toEqual({ - url: new URL('https://my.host:8443/my_proxy'), - pathname: '/my_proxy', + url: new URL("https://my.host:8443/my_proxy"), + pathname: "/my_proxy", request_timeout: 42_000, max_open_connections: 144, - username: 'bob', - password: 'secret', - database: 'analytics', + username: "bob", + password: "secret", + database: "analytics", http_headers: { - 'X-CLICKHOUSE-AUTH': 'secret_header', + "X-CLICKHOUSE-AUTH": "secret_header", }, keep_alive: { enabled: false }, - application: 'my_app', + application: "my_app", clickhouse_settings: { - http_headers_progress_interval_ms: '55000', + http_headers_progress_interval_ms: "55000", send_progress_in_http_headers: 0, async_insert: 1, }, @@ -137,51 +137,51 @@ describe('config', () => { request: true, response: true, }, - }) - }) + }); + }); - it('should be able to use the deprecated host parameter', async () => { + it("should be able to use the deprecated host parameter", async () => { const deprecated: BaseClickHouseClientConfigOptions = { - host: 'https://my.host:8443', - } - const res = prepareConfigWithURL(deprecated, logger, null) + host: "https://my.host:8443", + }; + const res = prepareConfigWithURL(deprecated, logger, null); expect(res).toEqual({ ...defaultConfig, - url: new URL('https://my.host:8443/'), - }) + url: new URL("https://my.host:8443/"), + }); expect(deprecated).toEqual({ - host: 'https://my.host:8443', - }) // should not be modified - }) + host: "https://my.host:8443", + }); // should not be modified + }); - it('should be able to use the deprecated additional_headers parameter', async () => { + it("should be able to use the deprecated additional_headers parameter", async () => { const deprecated: BaseClickHouseClientConfigOptions = { additional_headers: { - 'X-CLICKHOUSE-AUTH': 'secret_header', + "X-CLICKHOUSE-AUTH": "secret_header", }, - } - const res = prepareConfigWithURL(deprecated, logger, null) + }; + const res = prepareConfigWithURL(deprecated, logger, null); expect(res).toEqual({ ...defaultConfig, http_headers: { - 'X-CLICKHOUSE-AUTH': 'secret_header', + "X-CLICKHOUSE-AUTH": "secret_header", }, - }) + }); expect(deprecated).toEqual({ additional_headers: { - 'X-CLICKHOUSE-AUTH': 'secret_header', + "X-CLICKHOUSE-AUTH": "secret_header", }, - }) // should not be modified - }) + }); // should not be modified + }); // tested more thoroughly in the loadConfigOptionsFromURL section; // this is just a validation that everything works together - it('should use settings from the URL', async () => { + it("should use settings from the URL", async () => { const res = prepareConfigWithURL( { url: - 'https://bob:secret@my.host:8443/analytics?' + - ['application=my_app', 'impl_specific_setting=42'].join('&'), + "https://bob:secret@my.host:8443/analytics?" + + ["application=my_app", "impl_specific_setting=42"].join("&"), }, logger, (config) => { @@ -190,178 +190,178 @@ describe('config', () => { ...config, impl_specific_setting: 42, }, - handled_params: new Set(['impl_specific_setting']), + handled_params: new Set(["impl_specific_setting"]), unknown_params: new Set(), - } + }; }, - ) + ); expect(res).toEqual({ ...defaultConfig, - url: new URL('https://my.host:8443/'), - username: 'bob', - password: 'secret', - database: 'analytics', - application: 'my_app', + url: new URL("https://my.host:8443/"), + username: "bob", + password: "secret", + database: "analytics", + application: "my_app", impl_specific_setting: 42, - } as unknown as BaseClickHouseClientConfigOptionsWithURL) - }) + } as unknown as BaseClickHouseClientConfigOptionsWithURL); + }); - describe('Pathname', () => { - it('should correctly load pathname + database from the URL', async () => { + describe("Pathname", () => { + it("should correctly load pathname + database from the URL", async () => { const pathNames = [ - 'my_proxy', - 'my_proxy/ch', - 'my_proxy/', - 'my_proxy/ch/', - ] + "my_proxy", + "my_proxy/ch", + "my_proxy/", + "my_proxy/ch/", + ]; pathNames.forEach((pathname) => { const url = new URL( - 'http://my_host:8124/my_db?' + + "http://my_host:8124/my_db?" + [ - 'application=my_app', + "application=my_app", `pathname=${pathname}`, - 'request_timeout=42000', - 'max_open_connections=2', - ].join('&'), - ) - const res = prepareConfigWithURL({ url }, logger, null) + "request_timeout=42000", + "max_open_connections=2", + ].join("&"), + ); + const res = prepareConfigWithURL({ url }, logger, null); expect(res, `With pathname (no trailing slash) ${pathname}`).toEqual({ ...defaultConfig, pathname, url: new URL(`http://my_host:8124/${pathname}`), - application: 'my_app', - database: 'my_db', + application: "my_app", + database: "my_db", request_timeout: 42000, max_open_connections: 2, - } as unknown as BaseClickHouseClientConfigOptionsWithURL) - }) - }) + } as unknown as BaseClickHouseClientConfigOptionsWithURL); + }); + }); - it('should correctly load pathname + database from the URL (leading slash)', async () => { + it("should correctly load pathname + database from the URL (leading slash)", async () => { const leadingSlashPathNames = [ - '/my_proxy', - '/my_proxy/ch', - '/my_proxy/', - '/my_proxy/ch/', - ] + "/my_proxy", + "/my_proxy/ch", + "/my_proxy/", + "/my_proxy/ch/", + ]; leadingSlashPathNames.forEach((pathname) => { const url = new URL( - 'http://my_host:8124/my_db?' + + "http://my_host:8124/my_db?" + [ - 'application=my_app', + "application=my_app", `pathname=${pathname}`, - 'request_timeout=42000', - 'max_open_connections=2', - ].join('&'), - ) - const res = prepareConfigWithURL({ url }, logger, null) + "request_timeout=42000", + "max_open_connections=2", + ].join("&"), + ); + const res = prepareConfigWithURL({ url }, logger, null); expect(res, `With pathname (leading slash only) ${pathname}`).toEqual( { ...defaultConfig, pathname, - url: new URL('http://my_host:8124' + pathname), - application: 'my_app', - database: 'my_db', + url: new URL("http://my_host:8124" + pathname), + application: "my_app", + database: "my_db", request_timeout: 42000, max_open_connections: 2, } as unknown as BaseClickHouseClientConfigOptionsWithURL, - ) - }) - }) + ); + }); + }); - it('should correctly process pathname with the default db', async () => { + it("should correctly process pathname with the default db", async () => { const url = new URL( - 'http://my_host:8124?' + + "http://my_host:8124?" + [ - 'application=my_app', + "application=my_app", `pathname=my_proxy`, - 'request_timeout=42000', - 'max_open_connections=2', - ].join('&'), - ) - const res = prepareConfigWithURL({ url }, logger, null) + "request_timeout=42000", + "max_open_connections=2", + ].join("&"), + ); + const res = prepareConfigWithURL({ url }, logger, null); expect(res).toEqual({ ...defaultConfig, url: new URL(`http://my_host:8124/my_proxy`), - application: 'my_app', - pathname: 'my_proxy', + application: "my_app", + pathname: "my_proxy", // no `database` key request_timeout: 42000, max_open_connections: 2, - } as unknown as BaseClickHouseClientConfigOptionsWithURL) - }) - }) + } as unknown as BaseClickHouseClientConfigOptionsWithURL); + }); + }); - describe('Credentials vs JWT auth parsing', () => { - it('should correctly get JWT access_token from the URL', async () => { + describe("Credentials vs JWT auth parsing", () => { + it("should correctly get JWT access_token from the URL", async () => { const url = new URL( - 'https://my.host:8443/analytics?' + - ['application=my_app', 'access_token=jwt_secret'].join('&'), - ) - const res = prepareConfigWithURL({ url }, logger, null) + "https://my.host:8443/analytics?" + + ["application=my_app", "access_token=jwt_secret"].join("&"), + ); + const res = prepareConfigWithURL({ url }, logger, null); expect(res).toEqual({ ...defaultConfig, - url: new URL('https://my.host:8443'), - database: 'analytics', - application: 'my_app', - access_token: 'jwt_secret', - } as unknown as BaseClickHouseClientConfigOptionsWithURL) - }) + url: new URL("https://my.host:8443"), + database: "analytics", + application: "my_app", + access_token: "jwt_secret", + } as unknown as BaseClickHouseClientConfigOptionsWithURL); + }); // this will throw later during the config validation anyway - it('should correctly override credentials auth with JWT if both are present', async () => { + it("should correctly override credentials auth with JWT if both are present", async () => { const url = new URL( - 'https://bob:secret@my.host:8443/analytics?' + - ['application=my_app', 'access_token=jwt_secret'].join('&'), - ) - const res = prepareConfigWithURL({ url }, logger, null) + "https://bob:secret@my.host:8443/analytics?" + + ["application=my_app", "access_token=jwt_secret"].join("&"), + ); + const res = prepareConfigWithURL({ url }, logger, null); expect(res).toEqual({ ...defaultConfig, - url: new URL('https://my.host:8443'), - database: 'analytics', - application: 'my_app', - username: 'bob', - password: 'secret', - access_token: 'jwt_secret', - } as unknown as BaseClickHouseClientConfigOptionsWithURL) - }) - }) + url: new URL("https://my.host:8443"), + database: "analytics", + application: "my_app", + username: "bob", + password: "secret", + access_token: "jwt_secret", + } as unknown as BaseClickHouseClientConfigOptionsWithURL); + }); + }); // more detailed tests are in the createUrl section - it('should throw when the URL is not valid', async () => { - expect(() => prepareConfigWithURL({ url: 'foo' }, logger, null)).toThrow( + it("should throw when the URL is not valid", async () => { + expect(() => prepareConfigWithURL({ url: "foo" }, logger, null)).toThrow( expect.objectContaining({ - message: expect.stringContaining('ClickHouse URL is malformed.'), + message: expect.stringContaining("ClickHouse URL is malformed."), }), - ) - }) - }) + ); + }); + }); - describe('getConnectionParams', () => { + describe("getConnectionParams", () => { const authErrorMatcher = expect.objectContaining({ message: expect.stringContaining( - 'Please use only one authentication method', + "Please use only one authentication method", ), - }) + }); class SpyLogger extends TestLogger { - warnings: Array<{ module: string; message: string }> = [] + warnings: Array<{ module: string; message: string }> = []; override warn({ module, message }: { module: string; message: string }) { - this.warnings.push({ module, message }) - super.warn({ module, message }) + this.warnings.push({ module, message }); + super.warn({ module, message }); } } - it('should return the default connection params', async () => { + it("should return the default connection params", async () => { const res = getConnectionParams( { - url: new URL('https://my.host:8443/'), + url: new URL("https://my.host:8443/"), }, logger, - ) + ); expect(res).toEqual({ - url: new URL('https://my.host:8443/'), + url: new URL("https://my.host:8443/"), request_timeout: 30_000, max_open_connections: 10, compression: { @@ -369,11 +369,11 @@ describe('config', () => { compress_request: false, }, auth: { - username: 'default', - password: '', - type: 'Credentials', + username: "default", + password: "", + type: "Credentials", }, - database: 'default', + database: "default", clickhouse_settings: {}, log_writer: expect.any(LogWriter), log_level: ClickHouseLogLevel.WARN, @@ -384,35 +384,35 @@ describe('config', () => { parse: JSON.parse, stringify: JSON.stringify, }, - }) - }) + }); + }); - it('should set connection params from the config', async () => { + it("should set connection params from the config", async () => { const res = getConnectionParams( { - url: new URL('https://my.host:8443/'), + url: new URL("https://my.host:8443/"), request_timeout: 42_000, max_open_connections: 144, compression: { request: true, response: false, }, - username: 'bob', - password: 'secret', - database: 'analytics', + username: "bob", + password: "secret", + database: "analytics", clickhouse_settings: { async_insert: 1, }, http_headers: { - 'X-CLICKHOUSE-AUTH': 'secret_header', + "X-CLICKHOUSE-AUTH": "secret_header", }, keep_alive: { enabled: false }, - application: 'my_app', + application: "my_app", }, logger, - ) + ); expect(res).toEqual({ - url: new URL('https://my.host:8443/'), + url: new URL("https://my.host:8443/"), request_timeout: 42_000, max_open_connections: 144, compression: { @@ -420,79 +420,79 @@ describe('config', () => { decompress_response: false, }, auth: { - username: 'bob', - password: 'secret', - type: 'Credentials', + username: "bob", + password: "secret", + type: "Credentials", }, - database: 'analytics', + database: "analytics", clickhouse_settings: { async_insert: 1, }, http_headers: { - 'X-CLICKHOUSE-AUTH': 'secret_header', + "X-CLICKHOUSE-AUTH": "secret_header", }, log_writer: expect.any(LogWriter), log_level: ClickHouseLogLevel.WARN, keep_alive: { enabled: false }, - application_id: 'my_app', + application_id: "my_app", json: { parse: JSON.parse, stringify: JSON.stringify, }, - }) - }) + }); + }); - it('should throw if both JWT and username are set', async () => { + it("should throw if both JWT and username are set", async () => { expect(() => getConnectionParams( { - url: new URL('https://my.host:8443/'), - username: 'bob', - access_token: 'jwt', + url: new URL("https://my.host:8443/"), + username: "bob", + access_token: "jwt", }, logger, ), - ).toThrow(authErrorMatcher) - }) + ).toThrow(authErrorMatcher); + }); - it('should throw if both JWT and password are set', async () => { + it("should throw if both JWT and password are set", async () => { expect(() => getConnectionParams( { - url: new URL('https://my.host:8443/'), - password: 'secret', - access_token: 'jwt', + url: new URL("https://my.host:8443/"), + password: "secret", + access_token: "jwt", }, logger, ), - ).toThrow(authErrorMatcher) - }) + ).toThrow(authErrorMatcher); + }); - it('should throw if JWT, username, and password are all set', async () => { + it("should throw if JWT, username, and password are all set", async () => { expect(() => getConnectionParams( { - url: new URL('https://my.host:8443/'), - username: 'bob', - password: 'secret', - access_token: 'jwt', + url: new URL("https://my.host:8443/"), + username: "bob", + password: "secret", + access_token: "jwt", }, logger, ), - ).toThrow(authErrorMatcher) - }) + ).toThrow(authErrorMatcher); + }); - it('should not throw if only JWT auth is set', async () => { + it("should not throw if only JWT auth is set", async () => { expect( getConnectionParams( { - url: new URL('https://my.host:8443/'), - access_token: 'secret-token', + url: new URL("https://my.host:8443/"), + access_token: "secret-token", }, logger, ), ).toEqual({ - url: new URL('https://my.host:8443/'), + url: new URL("https://my.host:8443/"), request_timeout: 30_000, max_open_connections: 10, compression: { @@ -500,10 +500,10 @@ describe('config', () => { compress_request: false, }, auth: { - access_token: 'secret-token', - type: 'JWT', + access_token: "secret-token", + type: "JWT", }, - database: 'default', + database: "default", clickhouse_settings: {}, log_writer: expect.any(LogWriter), log_level: ClickHouseLogLevel.WARN, @@ -514,219 +514,219 @@ describe('config', () => { parse: JSON.parse, stringify: JSON.stringify, }, - }) - }) + }); + }); - describe('request_timeout warnings for progress headers', () => { - it('should warn when request_timeout is high but send_progress_in_http_headers is not enabled', async () => { - const spyLogger = new SpyLogger() + describe("request_timeout warnings for progress headers", () => { + it("should warn when request_timeout is high but send_progress_in_http_headers is not enabled", async () => { + const spyLogger = new SpyLogger(); getConnectionParams( { - url: new URL('https://my.host:8443/'), + url: new URL("https://my.host:8443/"), request_timeout: 120_000, // 120 seconds }, spyLogger, - ) - expect(spyLogger.warnings).toHaveLength(1) - expect(spyLogger.warnings[0].module).toBe('Config') + ); + expect(spyLogger.warnings).toHaveLength(1); + expect(spyLogger.warnings[0].module).toBe("Config"); expect(spyLogger.warnings[0].message).toContain( - 'request_timeout is set to 120000ms', - ) + "request_timeout is set to 120000ms", + ); expect(spyLogger.warnings[0].message).toContain( - 'send_progress_in_http_headers is not enabled', - ) + "send_progress_in_http_headers is not enabled", + ); expect(spyLogger.warnings[0].message).toContain( - 'Long-running queries may fail with socket hang-up errors', - ) - }) + "Long-running queries may fail with socket hang-up errors", + ); + }); - it('should warn when request_timeout is high and send_progress_in_http_headers is 0', async () => { - const spyLogger = new SpyLogger() + it("should warn when request_timeout is high and send_progress_in_http_headers is 0", async () => { + const spyLogger = new SpyLogger(); getConnectionParams( { - url: new URL('https://my.host:8443/'), + url: new URL("https://my.host:8443/"), request_timeout: 80_000, clickhouse_settings: { send_progress_in_http_headers: 0, }, }, spyLogger, - ) - expect(spyLogger.warnings).toHaveLength(1) + ); + expect(spyLogger.warnings).toHaveLength(1); expect(spyLogger.warnings[0].message).toContain( - 'send_progress_in_http_headers is not enabled', - ) - }) + "send_progress_in_http_headers is not enabled", + ); + }); - it('should warn when send_progress_in_http_headers is enabled but http_headers_progress_interval_ms is not set', async () => { - const spyLogger = new SpyLogger() + it("should warn when send_progress_in_http_headers is enabled but http_headers_progress_interval_ms is not set", async () => { + const spyLogger = new SpyLogger(); getConnectionParams( { - url: new URL('https://my.host:8443/'), + url: new URL("https://my.host:8443/"), request_timeout: 120_000, clickhouse_settings: { send_progress_in_http_headers: 1, }, }, spyLogger, - ) - expect(spyLogger.warnings).toHaveLength(1) - expect(spyLogger.warnings[0].module).toBe('Config') + ); + expect(spyLogger.warnings).toHaveLength(1); + expect(spyLogger.warnings[0].module).toBe("Config"); expect(spyLogger.warnings[0].message).toContain( - 'send_progress_in_http_headers is enabled', - ) + "send_progress_in_http_headers is enabled", + ); expect(spyLogger.warnings[0].message).toContain( - 'http_headers_progress_interval_ms is not set', - ) - }) + "http_headers_progress_interval_ms is not set", + ); + }); - it('should not warn when request_timeout is below threshold', async () => { - const spyLogger = new SpyLogger() + it("should not warn when request_timeout is below threshold", async () => { + const spyLogger = new SpyLogger(); getConnectionParams( { - url: new URL('https://my.host:8443/'), + url: new URL("https://my.host:8443/"), request_timeout: 30_000, // 30 seconds (default) }, spyLogger, - ) - expect(spyLogger.warnings).toHaveLength(0) - }) + ); + expect(spyLogger.warnings).toHaveLength(0); + }); - it('should not warn when request_timeout is exactly at threshold', async () => { - const spyLogger = new SpyLogger() + it("should not warn when request_timeout is exactly at threshold", async () => { + const spyLogger = new SpyLogger(); getConnectionParams( { - url: new URL('https://my.host:8443/'), + url: new URL("https://my.host:8443/"), request_timeout: 60_000, // 60 seconds (exactly at threshold) }, spyLogger, - ) - expect(spyLogger.warnings).toHaveLength(0) - }) + ); + expect(spyLogger.warnings).toHaveLength(0); + }); - it('should not warn when both send_progress_in_http_headers and http_headers_progress_interval_ms are properly configured', async () => { - const spyLogger = new SpyLogger() + it("should not warn when both send_progress_in_http_headers and http_headers_progress_interval_ms are properly configured", async () => { + const spyLogger = new SpyLogger(); getConnectionParams( { - url: new URL('https://my.host:8443/'), + url: new URL("https://my.host:8443/"), request_timeout: 400_000, clickhouse_settings: { send_progress_in_http_headers: 1, - http_headers_progress_interval_ms: '110000', + http_headers_progress_interval_ms: "110000", }, }, spyLogger, - ) - expect(spyLogger.warnings).toHaveLength(0) - }) - }) - }) + ); + expect(spyLogger.warnings).toHaveLength(0); + }); + }); + }); - describe('mergeConfigs', () => { - it('should merge two empty configs', async () => { - expect(mergeConfigs({}, {}, logger)).toEqual({}) - }) + describe("mergeConfigs", () => { + it("should merge two empty configs", async () => { + expect(mergeConfigs({}, {}, logger)).toEqual({}); + }); - it('should leave the base config as-is when there is nothing from the URL', async () => { + it("should leave the base config as-is when there is nothing from the URL", async () => { const base: BaseClickHouseClientConfigOptions = { - url: 'http://localhost:8123', - username: 'bob', - password: 'secret', - } - expect(mergeConfigs(base, {}, logger)).toEqual(base) - }) + url: "http://localhost:8123", + username: "bob", + password: "secret", + }; + expect(mergeConfigs(base, {}, logger)).toEqual(base); + }); - it('should take URL values first, then base config for the rest', async () => { + it("should take URL values first, then base config for the rest", async () => { const base: BaseClickHouseClientConfigOptions = { - url: 'https://my.host:8124', - username: 'bob', - password: 'secret', - } + url: "https://my.host:8124", + username: "bob", + password: "secret", + }; const fromURL: BaseClickHouseClientConfigOptions = { - password: 'secret_from_url!', - } - const res = mergeConfigs(base, fromURL, logger) + password: "secret_from_url!", + }; + const res = mergeConfigs(base, fromURL, logger); expect(res).toEqual({ - url: 'https://my.host:8124', - username: 'bob', - password: 'secret_from_url!', - }) - }) + url: "https://my.host:8124", + username: "bob", + password: "secret_from_url!", + }); + }); - it('should just merge non-conflicting values', async () => { + it("should just merge non-conflicting values", async () => { const base: BaseClickHouseClientConfigOptions = { - url: 'https://my.host:8124', - } + url: "https://my.host:8124", + }; const fromURL: BaseClickHouseClientConfigOptions = { - username: 'bob', - password: 'secret', - } - const res = mergeConfigs(base, fromURL, logger) + username: "bob", + password: "secret", + }; + const res = mergeConfigs(base, fromURL, logger); expect(res).toEqual({ - url: 'https://my.host:8124', - username: 'bob', - password: 'secret', - }) - }) + url: "https://my.host:8124", + username: "bob", + password: "secret", + }); + }); // realistically, we will always have at least URL in the base config - it('should only take the URL values when there is nothing in the base config', async () => { + it("should only take the URL values when there is nothing in the base config", async () => { const fromURL: BaseClickHouseClientConfigOptions = { - url: 'https://my.host:8443', - username: 'bob', - password: 'secret', - } - const res = mergeConfigs({}, fromURL, logger) + url: "https://my.host:8443", + username: "bob", + password: "secret", + }; + const res = mergeConfigs({}, fromURL, logger); expect(res).toEqual({ - url: 'https://my.host:8443', - username: 'bob', - password: 'secret', - }) - }) + url: "https://my.host:8443", + username: "bob", + password: "secret", + }); + }); - it('should correctly work with nested levels when there are no defaults', async () => { + it("should correctly work with nested levels when there are no defaults", async () => { const base: BaseClickHouseClientConfigOptions = { - url: 'https://my.host:8124', - application: 'my_app', + url: "https://my.host:8124", + application: "my_app", // does not have clickhouse_settings - } + }; const fromURL: BaseClickHouseClientConfigOptions = { clickhouse_settings: { wait_for_async_insert: 0, }, - } - const res = mergeConfigs(base, fromURL, logger) + }; + const res = mergeConfigs(base, fromURL, logger); expect(res).toEqual({ - url: 'https://my.host:8124', - application: 'my_app', + url: "https://my.host:8124", + application: "my_app", clickhouse_settings: { wait_for_async_insert: 0, }, - }) - }) + }); + }); - it('should deep merge two configs', async () => { + it("should deep merge two configs", async () => { const base: BaseClickHouseClientConfigOptions = { - url: 'https://my.host:8124', - application: 'my_app', + url: "https://my.host:8124", + application: "my_app", compression: { response: false, }, clickhouse_settings: { async_insert: 1, }, - } + }; const fromURL: BaseClickHouseClientConfigOptions = { - pathname: '/my_proxy', + pathname: "/my_proxy", clickhouse_settings: { wait_for_async_insert: 0, }, - } - const res = mergeConfigs(base, fromURL, logger) + }; + const res = mergeConfigs(base, fromURL, logger); expect(res).toEqual({ - url: 'https://my.host:8124', - application: 'my_app', - pathname: '/my_proxy', + url: "https://my.host:8124", + application: "my_app", + pathname: "/my_proxy", compression: { response: false, }, @@ -734,73 +734,73 @@ describe('config', () => { async_insert: 1, wait_for_async_insert: 0, }, - }) - }) + }); + }); - it('should deep merge more than two levels', async () => { + it("should deep merge more than two levels", async () => { // Currently, we don't have this. Future-proofing. type TestOptions = BaseClickHouseClientConfigOptions & { very: { deeply: { - nested_setting: string + nested_setting: string; nested: { - setting: number - } - this_is_not_overridden?: number[] - } - } - } + setting: number; + }; + this_is_not_overridden?: number[]; + }; + }; + }; const base: TestOptions = { - url: 'https://my.host:8124', + url: "https://my.host:8124", clickhouse_settings: { async_insert: 1, }, very: { deeply: { - nested_setting: 'foo', + nested_setting: "foo", nested: { setting: 42, }, this_is_not_overridden: [1, 2, 3], }, }, - } + }; const fromURL: TestOptions = { clickhouse_settings: { wait_for_async_insert: 0, }, very: { deeply: { - nested_setting: 'bar', + nested_setting: "bar", nested: { setting: 144, }, }, }, - } + }; - const res = mergeConfigs(base, fromURL, logger) + const res = mergeConfigs(base, fromURL, logger); expect(res as TestOptions).toEqual({ - url: 'https://my.host:8124', + url: "https://my.host:8124", clickhouse_settings: { async_insert: 1, wait_for_async_insert: 0, }, very: { deeply: { - nested_setting: 'bar', + nested_setting: "bar", nested: { setting: 144, }, this_is_not_overridden: [1, 2, 3], }, }, - }) - }) + }); + }); - it('should deep merge two configs with nested overrides', async () => { + it("should deep merge two configs with nested overrides", async () => { const base: BaseClickHouseClientConfigOptions = { - url: 'https://my.host:8124', + url: "https://my.host:8124", compression: { request: true, response: false, @@ -808,7 +808,7 @@ describe('config', () => { clickhouse_settings: { async_insert: 1, }, - } + }; const fromURL: BaseClickHouseClientConfigOptions = { compression: { request: false, @@ -818,10 +818,10 @@ describe('config', () => { async_insert: 0, wait_for_async_insert: 0, }, - } - const res = mergeConfigs(base, fromURL, logger) + }; + const res = mergeConfigs(base, fromURL, logger); expect(res).toEqual({ - url: 'https://my.host:8124', + url: "https://my.host:8124", compression: { request: false, response: true, @@ -830,66 +830,66 @@ describe('config', () => { async_insert: 0, wait_for_async_insert: 0, }, - }) - }) - }) + }); + }); + }); - describe('createUrl', () => { - it('should create valid URLs', async () => { - expect(createUrl(undefined)).toEqual(new URL('http://localhost:8123/')) - expect(createUrl('http://localhost:8123')).toEqual( - new URL('http://localhost:8123/'), - ) - expect(createUrl('https://bob:secret@my.host:8124')).toEqual( - new URL('https://bob:secret@my.host:8124/'), - ) - }) + describe("createUrl", () => { + it("should create valid URLs", async () => { + expect(createUrl(undefined)).toEqual(new URL("http://localhost:8123/")); + expect(createUrl("http://localhost:8123")).toEqual( + new URL("http://localhost:8123/"), + ); + expect(createUrl("https://bob:secret@my.host:8124")).toEqual( + new URL("https://bob:secret@my.host:8124/"), + ); + }); - it('should fail when the provided URL is not valid', async () => { - expect(() => createUrl('foo')).toThrow( + it("should fail when the provided URL is not valid", async () => { + expect(() => createUrl("foo")).toThrow( expect.objectContaining({ - message: expect.stringContaining('ClickHouse URL is malformed.'), + message: expect.stringContaining("ClickHouse URL is malformed."), }), - ) - expect(() => createUrl('http://localhost:foo')).toThrow( + ); + expect(() => createUrl("http://localhost:foo")).toThrow( expect.objectContaining({ - message: expect.stringContaining('ClickHouse URL is malformed.'), + message: expect.stringContaining("ClickHouse URL is malformed."), }), - ) - expect(() => createUrl('tcp://localhost:8443')).toThrowError( - 'ClickHouse URL protocol must be either http or https. Got: tcp:', - ) - }) - }) + ); + expect(() => createUrl("tcp://localhost:8443")).toThrowError( + "ClickHouse URL protocol must be either http or https. Got: tcp:", + ); + }); + }); - describe('loadConfigOptionsFromURL', () => { - it('should load all possible config options from the URL params', async () => { + describe("loadConfigOptionsFromURL", () => { + it("should load all possible config options from the URL params", async () => { const url = new URL( - 'https://bob:secret@my.host:8124/analytics?' + + "https://bob:secret@my.host:8124/analytics?" + [ - 'application=my_app', - 'pathname=/my_proxy', - 'session_id=sticky', - 'request_timeout=42', - 'max_open_connections=144', - 'compression_request=1', - 'compression_response=false', - 'log_level=TRACE', - 'keep_alive_enabled=false', - 'clickhouse_setting_async_insert=1', - 'ch_wait_for_async_insert=0', - 'http_header_X-CLICKHOUSE-AUTH=secret_header', - ].join('&'), - ) - const res = loadConfigOptionsFromURL(url, null) - expect(res[0].toString()).toEqual('https://my.host:8124/') // pathname will be attached later. + "application=my_app", + "pathname=/my_proxy", + "session_id=sticky", + "request_timeout=42", + "max_open_connections=144", + "compression_request=1", + "compression_response=false", + "log_level=TRACE", + "keep_alive_enabled=false", + "clickhouse_setting_async_insert=1", + "ch_wait_for_async_insert=0", + "http_header_X-CLICKHOUSE-AUTH=secret_header", + ].join("&"), + ); + const res = loadConfigOptionsFromURL(url, null); + expect(res[0].toString()).toEqual("https://my.host:8124/"); // pathname will be attached later. expect(res[1]).toEqual({ - username: 'bob', - password: 'secret', - database: 'analytics', - application: 'my_app', - pathname: '/my_proxy', - session_id: 'sticky', + username: "bob", + password: "secret", + database: "analytics", + application: "my_app", + pathname: "/my_proxy", + session_id: "sticky", request_timeout: 42, max_open_connections: 144, compression: { @@ -901,168 +901,168 @@ describe('config', () => { clickhouse_settings: { // type (string vs number) does not really matter here, as it will be serialized anyway // it is only important that the value itself is correct. - async_insert: '1', - wait_for_async_insert: '0', + async_insert: "1", + wait_for_async_insert: "0", } as Record, http_headers: { - 'X-CLICKHOUSE-AUTH': 'secret_header', + "X-CLICKHOUSE-AUTH": "secret_header", }, - }) - }) + }); + }); - it('should load only auth from the URL, the rest of the config is unset', async () => { - const url = new URL('http://bob:secret@localhost:8124/analytics') - const res = loadConfigOptionsFromURL(url, null) - expect(res[0].toString()).toEqual('http://localhost:8124/') + it("should load only auth from the URL, the rest of the config is unset", async () => { + const url = new URL("http://bob:secret@localhost:8124/analytics"); + const res = loadConfigOptionsFromURL(url, null); + expect(res[0].toString()).toEqual("http://localhost:8124/"); expect(res[1]).toEqual({ - username: 'bob', - password: 'secret', - database: 'analytics', - }) - }) + username: "bob", + password: "secret", + database: "analytics", + }); + }); - it('should load only the settings from the URL, without auth', async () => { + it("should load only the settings from the URL, without auth", async () => { const url = new URL( - 'http://localhost:8124/?' + + "http://localhost:8124/?" + [ - 'application=my_app', - 'request_timeout=42000', - 'max_open_connections=2', - ].join('&'), - ) - const res = loadConfigOptionsFromURL(url, null) - expect(res[0].toString()).toEqual('http://localhost:8124/') + "application=my_app", + "request_timeout=42000", + "max_open_connections=2", + ].join("&"), + ); + const res = loadConfigOptionsFromURL(url, null); + expect(res[0].toString()).toEqual("http://localhost:8124/"); expect(res[1]).toEqual({ - application: 'my_app', + application: "my_app", request_timeout: 42000, max_open_connections: 2, - }) - }) + }); + }); - it('should not parse anything into the config when the URL params are empty', async () => { - const url = new URL('http://localhost:8124') - const res = loadConfigOptionsFromURL(url, null) - expect(res[0].toString()).toEqual('http://localhost:8124/') - expect(res[1]).toEqual({}) - }) + it("should not parse anything into the config when the URL params are empty", async () => { + const url = new URL("http://localhost:8124"); + const res = loadConfigOptionsFromURL(url, null); + expect(res[0].toString()).toEqual("http://localhost:8124/"); + expect(res[1]).toEqual({}); + }); // this lack of validation is a subject to change; // however, it might be not feasible to define and validate every setting defined in the client - it('should not fail if there is an arbitrary clickhouse_setting provided (not yet typed in the client)', async () => { - const url = new URL('http://localhost:8125/?ch_this_is_a_new_one=1') - const res = loadConfigOptionsFromURL(url, null) - expect(res[0].toString()).toEqual('http://localhost:8125/') + it("should not fail if there is an arbitrary clickhouse_setting provided (not yet typed in the client)", async () => { + const url = new URL("http://localhost:8125/?ch_this_is_a_new_one=1"); + const res = loadConfigOptionsFromURL(url, null); + expect(res[0].toString()).toEqual("http://localhost:8125/"); expect(res[1]).toEqual({ clickhouse_settings: { - this_is_a_new_one: '1', + this_is_a_new_one: "1", }, - }) - }) + }); + }); - it('should fail if there is an unknown setting and the extra URL params handler is not provided', async () => { - const url1 = new URL('http://localhost:8124/?this_was_unexpected=1') + it("should fail if there is an unknown setting and the extra URL params handler is not provided", async () => { + const url1 = new URL("http://localhost:8124/?this_was_unexpected=1"); expect(() => loadConfigOptionsFromURL(url1, null)).toThrowError( - 'Unknown URL parameters: this_was_unexpected', - ) - const url2 = new URL('http://localhost:8124/?url=this_is_not_allowed') + "Unknown URL parameters: this_was_unexpected", + ); + const url2 = new URL("http://localhost:8124/?url=this_is_not_allowed"); expect(() => loadConfigOptionsFromURL(url2, null)).toThrowError( - 'Unknown URL parameters: url', - ) - }) + "Unknown URL parameters: url", + ); + }); - it('should not fail if an unknown default setting is handled by the extra URL params handler', async () => { - const url = new URL('http://localhost:8124/?impl_specific_setting=42') + it("should not fail if an unknown default setting is handled by the extra URL params handler", async () => { + const url = new URL("http://localhost:8124/?impl_specific_setting=42"); const handler: HandleImplSpecificURLParams = (config) => { return { config: { ...config, impl_specific_setting: 42, }, - handled_params: new Set(['impl_specific_setting']), + handled_params: new Set(["impl_specific_setting"]), unknown_params: new Set(), - } - } - const res = loadConfigOptionsFromURL(url, handler) - expect(res[0].toString()).toEqual('http://localhost:8124/') + }; + }; + const res = loadConfigOptionsFromURL(url, handler); + expect(res[0].toString()).toEqual("http://localhost:8124/"); expect(res[1]).toEqual({ impl_specific_setting: 42, - } as unknown as BaseClickHouseClientConfigOptions) - }) + } as unknown as BaseClickHouseClientConfigOptions); + }); - it('should fail if the parameter is still unknown after calling the extra URL params handler', async () => { - const url = new URL('http://localhost:8124/?impl_specific_setting=42') + it("should fail if the parameter is still unknown after calling the extra URL params handler", async () => { + const url = new URL("http://localhost:8124/?impl_specific_setting=42"); const handler: HandleImplSpecificURLParams = (config) => { return { config, handled_params: new Set(), - unknown_params: new Set(['impl_specific_setting']), - } - } + unknown_params: new Set(["impl_specific_setting"]), + }; + }; expect(() => loadConfigOptionsFromURL(url, handler)).toThrowError( - 'Unknown URL parameters: impl_specific_setting', - ) - }) + "Unknown URL parameters: impl_specific_setting", + ); + }); - it('should fail if only some parameters were handled by the extra URL params handler', async () => { + it("should fail if only some parameters were handled by the extra URL params handler", async () => { const url = new URL( - 'http://localhost:8124/?impl_specific_setting=42&whatever=1', - ) + "http://localhost:8124/?impl_specific_setting=42&whatever=1", + ); const handler: HandleImplSpecificURLParams = (config) => { return { config: { ...config, impl_specific_setting: 42, }, - handled_params: new Set(['impl_specific_setting']), - unknown_params: new Set(['whatever']), - } - } + handled_params: new Set(["impl_specific_setting"]), + unknown_params: new Set(["whatever"]), + }; + }; expect(() => loadConfigOptionsFromURL(url, handler)).toThrowError( - 'Unknown URL parameters: whatever', - ) - }) + "Unknown URL parameters: whatever", + ); + }); - it('should not fail when some of the settings were parsed in common and some in the extra URL params handler', async () => { + it("should not fail when some of the settings were parsed in common and some in the extra URL params handler", async () => { const url = new URL( - 'https://bob:secret@my.host:8124/analytics?' + + "https://bob:secret@my.host:8124/analytics?" + [ - 'application=my_app', - 'session_id=sticky', - 'request_timeout=42', - 'max_open_connections=144', - 'compression_request=1', - 'compression_response=true', - 'log_level=TRACE', - 'keep_alive_enabled=false', - 'clickhouse_setting_async_insert=1', - 'ch_wait_for_async_insert=0', - 'http_header_X-CLICKHOUSE-AUTH=secret_header', - 'impl_specific_setting=qaz', - 'another_impl_specific_setting=qux', - ].join('&'), - ) + "application=my_app", + "session_id=sticky", + "request_timeout=42", + "max_open_connections=144", + "compression_request=1", + "compression_response=true", + "log_level=TRACE", + "keep_alive_enabled=false", + "clickhouse_setting_async_insert=1", + "ch_wait_for_async_insert=0", + "http_header_X-CLICKHOUSE-AUTH=secret_header", + "impl_specific_setting=qaz", + "another_impl_specific_setting=qux", + ].join("&"), + ); const handler: HandleImplSpecificURLParams = (config) => { return { config: { ...config, - impl_specific_setting: 'qaz', - another_impl_specific_setting: 'qux', + impl_specific_setting: "qaz", + another_impl_specific_setting: "qux", }, handled_params: new Set([ - 'impl_specific_setting', - 'another_impl_specific_setting', + "impl_specific_setting", + "another_impl_specific_setting", ]), unknown_params: new Set(), - } - } - const res = loadConfigOptionsFromURL(url, handler) - expect(res[0].toString()).toEqual('https://my.host:8124/') + }; + }; + const res = loadConfigOptionsFromURL(url, handler); + expect(res[0].toString()).toEqual("https://my.host:8124/"); expect(res[1]).toEqual({ - username: 'bob', - password: 'secret', - database: 'analytics', - application: 'my_app', - session_id: 'sticky', + username: "bob", + password: "secret", + database: "analytics", + application: "my_app", + session_id: "sticky", request_timeout: 42, max_open_connections: 144, compression: { @@ -1072,144 +1072,144 @@ describe('config', () => { log: { level: ClickHouseLogLevel.TRACE }, keep_alive: { enabled: false }, clickhouse_settings: { - async_insert: '1', - wait_for_async_insert: '0', + async_insert: "1", + wait_for_async_insert: "0", } as Record, http_headers: { - 'X-CLICKHOUSE-AUTH': 'secret_header', + "X-CLICKHOUSE-AUTH": "secret_header", }, - impl_specific_setting: 'qaz', - another_impl_specific_setting: 'qux', - } as unknown as BaseClickHouseClientConfigOptions) - }) + impl_specific_setting: "qaz", + another_impl_specific_setting: "qux", + } as unknown as BaseClickHouseClientConfigOptions); + }); // URL params that were handled by common are removed from the URL passed down to the extra handler by design - it('should not override common config with the extra URL params handler', async () => { + it("should not override common config with the extra URL params handler", async () => { const url = new URL( - 'https://bob:secret@my.host:8124/analytics?' + + "https://bob:secret@my.host:8124/analytics?" + [ - 'application=my_app', - 'session_id=sticky', - 'request_timeout=42', - ].join('&'), - ) + "application=my_app", + "session_id=sticky", + "request_timeout=42", + ].join("&"), + ); const handler: HandleImplSpecificURLParams = (config, url) => { // should fail the assertion if not empty if (url.searchParams.size > 0) { throw new Error( `Unexpected URL params: ${url.searchParams.toString()}`, - ) + ); } return { config, handled_params: new Set(), unknown_params: new Set(), - } - } - const res = loadConfigOptionsFromURL(url, handler) - expect(res[0].toString()).toEqual('https://my.host:8124/') + }; + }; + const res = loadConfigOptionsFromURL(url, handler); + expect(res[0].toString()).toEqual("https://my.host:8124/"); expect(res[1]).toEqual({ - username: 'bob', - password: 'secret', - database: 'analytics', - application: 'my_app', - session_id: 'sticky', + username: "bob", + password: "secret", + database: "analytics", + application: "my_app", + session_id: "sticky", request_timeout: 42, - }) - }) - }) + }); + }); + }); - describe('ConfigURLValues', () => { - const key = 'foo' + describe("ConfigURLValues", () => { + const key = "foo"; - it('should be parsed with booleanConfigURLValue', async () => { + it("should be parsed with booleanConfigURLValue", async () => { const args: [string, boolean][] = [ - ['true', true], - [' true ', true], - ['false', false], - [' false ', false], - ['1', true], - [' 1 ', true], - ['0', false], - [' 0 ', false], - ] + ["true", true], + [" true ", true], + ["false", false], + [" false ", false], + ["1", true], + [" 1 ", true], + ["0", false], + [" 0 ", false], + ]; args.forEach(([value, expected]) => { expect( booleanConfigURLValue({ key, value }), `Expected value "${value}" to be ${expected}`, - ).toEqual(expected) - }) - expect(() => booleanConfigURLValue({ key, value: 'bar' })).toThrowError( + ).toEqual(expected); + }); + expect(() => booleanConfigURLValue({ key, value: "bar" })).toThrowError( `"foo" has invalid boolean value: bar. Expected one of: 0, 1, true, false.`, - ) - }) + ); + }); - it('should be parsed with numberConfigURLValue', async () => { + it("should be parsed with numberConfigURLValue", async () => { const args: [string, number][] = [ - ['0', 0], - [' 0 ', 0], - ['1', 1], - [' 1 ', 1], - ['-1', -1], - [' -1 ', -1], - ['1.5', 1.5], - [' 1.5 ', 1.5], - ] + ["0", 0], + [" 0 ", 0], + ["1", 1], + [" 1 ", 1], + ["-1", -1], + [" -1 ", -1], + ["1.5", 1.5], + [" 1.5 ", 1.5], + ]; args.forEach(([value, expected]) => { expect( numberConfigURLValue({ key, value }), `Expected value "${value}" to be ${expected}`, - ).toEqual(expected) - }) - expect(() => numberConfigURLValue({ key, value: 'bar' })).toThrowError( + ).toEqual(expected); + }); + expect(() => numberConfigURLValue({ key, value: "bar" })).toThrowError( `"foo" has invalid numeric value: bar`, - ) - }) + ); + }); - it('should be parsed with numberConfigURLValue (min constraint)', async () => { - expect(numberConfigURLValue({ key, value: '2', min: 1 })).toEqual(2) - expect(numberConfigURLValue({ key, value: '2', min: 2 })).toEqual(2) + it("should be parsed with numberConfigURLValue (min constraint)", async () => { + expect(numberConfigURLValue({ key, value: "2", min: 1 })).toEqual(2); + expect(numberConfigURLValue({ key, value: "2", min: 2 })).toEqual(2); expect(() => - numberConfigURLValue({ key, value: '2', min: 3 }), - ).toThrowError(`"foo" value 2 is less than min allowed 3`) - }) + numberConfigURLValue({ key, value: "2", min: 3 }), + ).toThrowError(`"foo" value 2 is less than min allowed 3`); + }); - it('should be parsed with numberConfigURLValue (max constraint)', async () => { - expect(numberConfigURLValue({ key, value: '2', max: 2 })).toEqual(2) - expect(numberConfigURLValue({ key, value: '2', max: 3 })).toEqual(2) + it("should be parsed with numberConfigURLValue (max constraint)", async () => { + expect(numberConfigURLValue({ key, value: "2", max: 2 })).toEqual(2); + expect(numberConfigURLValue({ key, value: "2", max: 3 })).toEqual(2); expect(() => - numberConfigURLValue({ key, value: '4', max: 3 }), - ).toThrowError(`"foo" value 4 is greater than max allowed 3`) - }) + numberConfigURLValue({ key, value: "4", max: 3 }), + ).toThrowError(`"foo" value 4 is greater than max allowed 3`); + }); - it('should be parsed with numberConfigURLValue (both min/max constraints)', async () => { - const r1 = numberConfigURLValue({ key, value: '1', min: 1, max: 2 }) - expect(r1).toEqual(1) - const r2 = numberConfigURLValue({ key, value: '2', min: 2, max: 2 }) - expect(r2).toEqual(2) + it("should be parsed with numberConfigURLValue (both min/max constraints)", async () => { + const r1 = numberConfigURLValue({ key, value: "1", min: 1, max: 2 }); + expect(r1).toEqual(1); + const r2 = numberConfigURLValue({ key, value: "2", min: 2, max: 2 }); + expect(r2).toEqual(2); expect(() => - numberConfigURLValue({ key, value: '2', min: 3, max: 4 }), - ).toThrowError(`"foo" value 2 is less than min allowed 3`) + numberConfigURLValue({ key, value: "2", min: 3, max: 4 }), + ).toThrowError(`"foo" value 2 is less than min allowed 3`); expect(() => - numberConfigURLValue({ key, value: '5', min: 3, max: 4 }), - ).toThrowError(`"foo" value 5 is greater than max allowed 4`) - }) + numberConfigURLValue({ key, value: "5", min: 3, max: 4 }), + ).toThrowError(`"foo" value 5 is greater than max allowed 4`); + }); - it('should be parsed with enumConfigURLValue', async () => { + it("should be parsed with enumConfigURLValue", async () => { const args: [string, ClickHouseLogLevel][] = [ - ['TRACE', ClickHouseLogLevel.TRACE], - [' TRACE ', ClickHouseLogLevel.TRACE], - ['DEBUG', ClickHouseLogLevel.DEBUG], - [' DEBUG ', ClickHouseLogLevel.DEBUG], - ['INFO', ClickHouseLogLevel.INFO], - [' INFO ', ClickHouseLogLevel.INFO], - ['WARN', ClickHouseLogLevel.WARN], - [' WARN ', ClickHouseLogLevel.WARN], - ['ERROR', ClickHouseLogLevel.ERROR], - [' ERROR ', ClickHouseLogLevel.ERROR], - ['OFF', ClickHouseLogLevel.OFF], - [' OFF ', ClickHouseLogLevel.OFF], - ] + ["TRACE", ClickHouseLogLevel.TRACE], + [" TRACE ", ClickHouseLogLevel.TRACE], + ["DEBUG", ClickHouseLogLevel.DEBUG], + [" DEBUG ", ClickHouseLogLevel.DEBUG], + ["INFO", ClickHouseLogLevel.INFO], + [" INFO ", ClickHouseLogLevel.INFO], + ["WARN", ClickHouseLogLevel.WARN], + [" WARN ", ClickHouseLogLevel.WARN], + ["ERROR", ClickHouseLogLevel.ERROR], + [" ERROR ", ClickHouseLogLevel.ERROR], + ["OFF", ClickHouseLogLevel.OFF], + [" OFF ", ClickHouseLogLevel.OFF], + ]; args.forEach(([value, expected]) => { expect( enumConfigURLValue({ @@ -1218,17 +1218,17 @@ describe('config', () => { enumObject: ClickHouseLogLevel, }), `Expected log level for value "${value}" is ${expected}`, - ).toEqual(expected) - }) + ).toEqual(expected); + }); expect(() => enumConfigURLValue({ key, - value: 'bar', + value: "bar", enumObject: ClickHouseLogLevel, }), ).toThrowError( `"foo" has invalid value: bar. Expected one of: TRACE, DEBUG, INFO, WARN, ERROR, OFF.`, - ) - }) - }) -}) + ); + }); + }); +}); diff --git a/packages/client-common/__tests__/unit/error.test.ts b/packages/client-common/__tests__/unit/error.test.ts index 1c18cdc8..4d8e14fb 100644 --- a/packages/client-common/__tests__/unit/error.test.ts +++ b/packages/client-common/__tests__/unit/error.test.ts @@ -1,137 +1,137 @@ -import { describe, it, expect } from 'vitest' +import { describe, it, expect } from "vitest"; import { ClickHouseError, enhanceStackTrace, getCurrentStackTrace, parseError, -} from '../../src/index' +} from "../../src/index"; -describe('parseError', () => { - it('parses a single line error', () => { - const message = `Code: 62.DB::Exception: Syntax error: failed at position 15 ('unknown_table') (line 1, col 15): unknown_table FORMAT JSON. Expected alias cannot be here. (SYNTAX_ERROR) (version 22.7.1.2484 (official build))` - const error = parseError(message) as ClickHouseError +describe("parseError", () => { + it("parses a single line error", () => { + const message = `Code: 62.DB::Exception: Syntax error: failed at position 15 ('unknown_table') (line 1, col 15): unknown_table FORMAT JSON. Expected alias cannot be here. (SYNTAX_ERROR) (version 22.7.1.2484 (official build))`; + const error = parseError(message) as ClickHouseError; - expect(error).toBeInstanceOf(ClickHouseError) - expect(error.code).toBe('62') - expect(error.type).toBe('SYNTAX_ERROR') + expect(error).toBeInstanceOf(ClickHouseError); + expect(error.code).toBe("62"); + expect(error.type).toBe("SYNTAX_ERROR"); expect(error.message).toBe( `Syntax error: failed at position 15 ('unknown_table') (line 1, col 15): unknown_table FORMAT JSON. Expected alias cannot be here. `, - ) - }) + ); + }); - it('parses a multiline error', () => { + it("parses a multiline error", () => { const message = `Code: 62.DB::Exception: Syntax error: failed at position 15 ('unknown_table') (line 1, col 15): unknown_table - FORMAT JSON. Expected alias cannot be here. (SYNTAX_ERROR) (version 22.7.1.2484 (official build))` - const error = parseError(message) as ClickHouseError + FORMAT JSON. Expected alias cannot be here. (SYNTAX_ERROR) (version 22.7.1.2484 (official build))`; + const error = parseError(message) as ClickHouseError; - expect(error).toBeInstanceOf(ClickHouseError) - expect(error.code).toBe('62') - expect(error.type).toBe('SYNTAX_ERROR') + expect(error).toBeInstanceOf(ClickHouseError); + expect(error.code).toBe("62"); + expect(error.type).toBe("SYNTAX_ERROR"); expect(error.message).toBe( `Syntax error: failed at position 15 ('unknown_table') (line 1, col 15): unknown_table FORMAT JSON. Expected alias cannot be here. `, - ) - }) + ); + }); - it('should handle replica message corner case', () => { - const message = `Code: 285. DB::Exception: Number of alive replicas (2) is less than requested quorum (3). (TOO_FEW_LIVE_REPLICAS) (version 22.8.1.11291 (official build))` - const error = parseError(message) as ClickHouseError + it("should handle replica message corner case", () => { + const message = `Code: 285. DB::Exception: Number of alive replicas (2) is less than requested quorum (3). (TOO_FEW_LIVE_REPLICAS) (version 22.8.1.11291 (official build))`; + const error = parseError(message) as ClickHouseError; - expect(error).toBeInstanceOf(ClickHouseError) - expect(error.code).toBe('285') - expect(error.type).toBe('TOO_FEW_LIVE_REPLICAS') + expect(error).toBeInstanceOf(ClickHouseError); + expect(error.code).toBe("285"); + expect(error.type).toBe("TOO_FEW_LIVE_REPLICAS"); expect(error.message).toBe( - 'Number of alive replicas (2) is less than requested quorum (3). ', - ) - }) - - describe('error codes with numbers', () => { - it('should work with S3_ERROR', () => { - const message = `Code: 499. DB::Exception: Could not list objects in bucket my-bucket with prefix my-organization, S3 exception: Some S3 error, message: Could not list objects. (S3_ERROR) (version 22.8.1.11291 (official build))` - const error = parseError(message) as ClickHouseError - - expect(error).toBeInstanceOf(ClickHouseError) - expect(error.code).toBe('499') - expect(error.type).toBe('S3_ERROR') + "Number of alive replicas (2) is less than requested quorum (3). ", + ); + }); + + describe("error codes with numbers", () => { + it("should work with S3_ERROR", () => { + const message = `Code: 499. DB::Exception: Could not list objects in bucket my-bucket with prefix my-organization, S3 exception: Some S3 error, message: Could not list objects. (S3_ERROR) (version 22.8.1.11291 (official build))`; + const error = parseError(message) as ClickHouseError; + + expect(error).toBeInstanceOf(ClickHouseError); + expect(error.code).toBe("499"); + expect(error.type).toBe("S3_ERROR"); expect(error.message).toBe( - 'Could not list objects in bucket my-bucket with prefix my-organization, S3 exception: Some S3 error, message: Could not list objects. ', - ) - }) + "Could not list objects in bucket my-bucket with prefix my-organization, S3 exception: Some S3 error, message: Could not list objects. ", + ); + }); - it('should work with BZIP2_STREAM_DECODER_FAILED', () => { - const message = `Code: 594. DB::Exception: bzip2 stream encoder init failed: error code: 42 (BZIP2_STREAM_DECODER_FAILED) (version 22.8.1.11291 (official build))` - const error = parseError(message) as ClickHouseError + it("should work with BZIP2_STREAM_DECODER_FAILED", () => { + const message = `Code: 594. DB::Exception: bzip2 stream encoder init failed: error code: 42 (BZIP2_STREAM_DECODER_FAILED) (version 22.8.1.11291 (official build))`; + const error = parseError(message) as ClickHouseError; - expect(error).toBeInstanceOf(ClickHouseError) - expect(error.code).toBe('594') - expect(error.type).toBe('BZIP2_STREAM_DECODER_FAILED') + expect(error).toBeInstanceOf(ClickHouseError); + expect(error.code).toBe("594"); + expect(error.type).toBe("BZIP2_STREAM_DECODER_FAILED"); expect(error.message).toBe( - 'bzip2 stream encoder init failed: error code: 42 ', - ) - }) + "bzip2 stream encoder init failed: error code: 42 ", + ); + }); - it('should work with LZ4_ENCODER_FAILED', () => { - const message = `Code: 617. DB::Exception: creation of LZ4 compression context failed. LZ4F version: 1.9.3 (LZ4_ENCODER_FAILED) (version 22.8.1.11291 (official build))` - const error = parseError(message) as ClickHouseError + it("should work with LZ4_ENCODER_FAILED", () => { + const message = `Code: 617. DB::Exception: creation of LZ4 compression context failed. LZ4F version: 1.9.3 (LZ4_ENCODER_FAILED) (version 22.8.1.11291 (official build))`; + const error = parseError(message) as ClickHouseError; - expect(error).toBeInstanceOf(ClickHouseError) - expect(error.code).toBe('617') - expect(error.type).toBe('LZ4_ENCODER_FAILED') + expect(error).toBeInstanceOf(ClickHouseError); + expect(error.code).toBe("617"); + expect(error.type).toBe("LZ4_ENCODER_FAILED"); expect(error.message).toBe( - 'creation of LZ4 compression context failed. LZ4F version: 1.9.3 ', - ) - }) - }) - - describe('getCurrentStackTrace', () => { - it('should trim the stack trace as expected', async () => { - const currentStack = getCurrentStackTrace() - expect(currentStack.split('\n').length).toBeGreaterThanOrEqual(3) - expect(currentStack).not.toContain(getCurrentStackTrace.name) - expect(currentStack).not.toContain('Error:') - expect(currentStack).not.toContain('UserContext') - expect(currentStack).not.toContain('error.test.ts') - }) - }) - - describe('enhanceStackTrace', () => { - it('should no-op on an empty error or stack trace', async () => { - const err = new Error('test error') - err.stack = undefined - - const result1 = enhanceStackTrace(err, 'ignored') - expect(result1.stack).toBeUndefined() - expect(result1.message).toBe('test error') - - err.stack = '' - const result2 = enhanceStackTrace(err, 'ignored') - expect(result2.stack).toBe('') - expect(result2.message).toBe('test error') - }) - - it('should enhance the stack trace of an error', () => { - const err = new Error('test error') - err.stack = 'foo\n' - const stackTrace = 'bar\n' - const enhancedError = enhanceStackTrace(err, stackTrace) - - expect(enhancedError.stack).toContain(`foo\nbar\n`) - expect(enhancedError.message).toBe('test error') - }) - }) - - describe.skip('Cluster mode errors', () => { + "creation of LZ4 compression context failed. LZ4F version: 1.9.3 ", + ); + }); + }); + + describe("getCurrentStackTrace", () => { + it("should trim the stack trace as expected", async () => { + const currentStack = getCurrentStackTrace(); + expect(currentStack.split("\n").length).toBeGreaterThanOrEqual(3); + expect(currentStack).not.toContain(getCurrentStackTrace.name); + expect(currentStack).not.toContain("Error:"); + expect(currentStack).not.toContain("UserContext"); + expect(currentStack).not.toContain("error.test.ts"); + }); + }); + + describe("enhanceStackTrace", () => { + it("should no-op on an empty error or stack trace", async () => { + const err = new Error("test error"); + err.stack = undefined; + + const result1 = enhanceStackTrace(err, "ignored"); + expect(result1.stack).toBeUndefined(); + expect(result1.message).toBe("test error"); + + err.stack = ""; + const result2 = enhanceStackTrace(err, "ignored"); + expect(result2.stack).toBe(""); + expect(result2.message).toBe("test error"); + }); + + it("should enhance the stack trace of an error", () => { + const err = new Error("test error"); + err.stack = "foo\n"; + const stackTrace = "bar\n"; + const enhancedError = enhanceStackTrace(err, stackTrace); + + expect(enhancedError.stack).toContain(`foo\nbar\n`); + expect(enhancedError.message).toBe("test error"); + }); + }); + + describe.skip("Cluster mode errors", () => { // FIXME: https://github.com/ClickHouse/clickhouse-js/issues/39 - it('should work with TABLE_ALREADY_EXISTS', async () => { - const message = `Code: 57. DB::Exception: There was an error on [clickhouse2:9000]: Code: 57. DB::Exception: Table default.command_test_2a751694160745f5aebe586c90b27515 already exists. (TABLE_ALREADY_EXISTS) (version 22.6.5.22 (official build)). (TABLE_ALREADY_EXISTS) (version 22.6.5.22 (official build))` - const error = parseError(message) as ClickHouseError + it("should work with TABLE_ALREADY_EXISTS", async () => { + const message = `Code: 57. DB::Exception: There was an error on [clickhouse2:9000]: Code: 57. DB::Exception: Table default.command_test_2a751694160745f5aebe586c90b27515 already exists. (TABLE_ALREADY_EXISTS) (version 22.6.5.22 (official build)). (TABLE_ALREADY_EXISTS) (version 22.6.5.22 (official build))`; + const error = parseError(message) as ClickHouseError; - expect(error).toBeInstanceOf(ClickHouseError) - expect(error.code).toBe('57') - expect(error.type).toBe('TABLE_ALREADY_EXISTS') + expect(error).toBeInstanceOf(ClickHouseError); + expect(error.code).toBe("57"); + expect(error.type).toBe("TABLE_ALREADY_EXISTS"); expect(error.message).toBe( - 'Table default.command_test_2a751694160745f5aebe586c90b27515 already exists. ', - ) - }) - }) -}) + "Table default.command_test_2a751694160745f5aebe586c90b27515 already exists. ", + ); + }); + }); +}); diff --git a/packages/client-common/__tests__/unit/format_query_params.test.ts b/packages/client-common/__tests__/unit/format_query_params.test.ts index 7dc681b4..1da0e6ff 100644 --- a/packages/client-common/__tests__/unit/format_query_params.test.ts +++ b/packages/client-common/__tests__/unit/format_query_params.test.ts @@ -1,190 +1,190 @@ -import { describe, it, expect } from 'vitest' -import { formatQueryParams, TupleParam } from '../../src/index' +import { describe, it, expect } from "vitest"; +import { formatQueryParams, TupleParam } from "../../src/index"; -describe('formatQueryParams', () => { - it('formats null', () => { +describe("formatQueryParams", () => { + it("formats null", () => { expect( formatQueryParams({ value: null, }), - ).toBe('\\N') - }) + ).toBe("\\N"); + }); - it('formats undefined', () => { + it("formats undefined", () => { expect( formatQueryParams({ value: undefined, }), - ).toBe('\\N') - }) + ).toBe("\\N"); + }); - it('formats boolean', () => { + it("formats boolean", () => { expect( formatQueryParams({ value: true, }), - ).toBe('1') + ).toBe("1"); expect( formatQueryParams({ value: false, }), - ).toBe('0') - }) + ).toBe("0"); + }); - it('formats number', () => { + it("formats number", () => { expect( formatQueryParams({ value: 1, }), - ).toBe('1') - }) + ).toBe("1"); + }); - it('formats BigInt', () => { + it("formats BigInt", () => { expect( formatQueryParams({ value: 0n, }), - ).toBe('0') + ).toBe("0"); expect( formatQueryParams({ value: -1n, }), - ).toBe('-1') + ).toBe("-1"); expect( formatQueryParams({ value: 123456789012345678901234567890n, }), - ).toBe('123456789012345678901234567890') - }) + ).toBe("123456789012345678901234567890"); + }); - it('formats NaN', () => { + it("formats NaN", () => { expect( formatQueryParams({ value: NaN, }), - ).toBe('nan') - }) + ).toBe("nan"); + }); - it('formats Infinity', () => { + it("formats Infinity", () => { expect( formatQueryParams({ value: Infinity, }), - ).toBe('+inf') + ).toBe("+inf"); expect( formatQueryParams({ value: +Infinity, }), - ).toBe('+inf') + ).toBe("+inf"); expect( formatQueryParams({ value: -Infinity, }), - ).toBe('-inf') - }) + ).toBe("-inf"); + }); - it('formats an array', () => { - expect(formatQueryParams({ value: [1, 2, 3] })).toBe('[1,2,3]') - }) + it("formats an array", () => { + expect(formatQueryParams({ value: [1, 2, 3] })).toBe("[1,2,3]"); + }); - it('formats an empty Array', () => { - expect(formatQueryParams({ value: [] })).toBe('[]') - }) + it("formats an empty Array", () => { + expect(formatQueryParams({ value: [] })).toBe("[]"); + }); - it('formats a date without timezone', () => { - const date = new Date(Date.UTC(2022, 6, 29, 7, 52, 14)) + it("formats a date without timezone", () => { + const date = new Date(Date.UTC(2022, 6, 29, 7, 52, 14)); expect( formatQueryParams({ value: date, }), - ).toBe('1659081134') - }) + ).toBe("1659081134"); + }); - it('formats a date with only nine digits in its Unix timestamp (seconds)', () => { - const date = new Date(Date.UTC(1973, 10, 29, 21, 33, 9)) + it("formats a date with only nine digits in its Unix timestamp (seconds)", () => { + const date = new Date(Date.UTC(1973, 10, 29, 21, 33, 9)); expect( formatQueryParams({ value: date, }), - ).toBe('0123456789') - }) + ).toBe("0123456789"); + }); - it('formats a date with millis', () => { + it("formats a date with millis", () => { expect( formatQueryParams({ value: new Date(Date.UTC(2022, 6, 29, 7, 52, 14, 123)), }), - ).toBe('1659081134.123') + ).toBe("1659081134.123"); expect( formatQueryParams({ value: new Date(Date.UTC(2022, 6, 29, 7, 52, 14, 42)), }), - ).toBe('1659081134.042') + ).toBe("1659081134.042"); expect( formatQueryParams({ value: new Date(Date.UTC(2022, 6, 29, 7, 52, 14, 5)), }), - ).toBe('1659081134.005') - }) + ).toBe("1659081134.005"); + }); - it('does not wrap a string in quotes', () => { + it("does not wrap a string in quotes", () => { expect( formatQueryParams({ - value: 'hello', + value: "hello", }), - ).toBe('hello') - }) + ).toBe("hello"); + }); - it('escapes special characters in an input string', () => { - expect(formatQueryParams({ value: "hel'lo" })).toBe("hel\\'lo") - expect(formatQueryParams({ value: 'hel\\lo' })).toBe('hel\\\\lo') - expect(formatQueryParams({ value: 'hel\tlo' })).toBe('hel\\tlo') - expect(formatQueryParams({ value: 'hel\nlo' })).toBe('hel\\nlo') - expect(formatQueryParams({ value: 'hel\rlo' })).toBe('hel\\rlo') - }) + it("escapes special characters in an input string", () => { + expect(formatQueryParams({ value: "hel'lo" })).toBe("hel\\'lo"); + expect(formatQueryParams({ value: "hel\\lo" })).toBe("hel\\\\lo"); + expect(formatQueryParams({ value: "hel\tlo" })).toBe("hel\\tlo"); + expect(formatQueryParams({ value: "hel\nlo" })).toBe("hel\\nlo"); + expect(formatQueryParams({ value: "hel\rlo" })).toBe("hel\\rlo"); + }); - it('wraps strings in an array in quotes', () => { - expect(formatQueryParams({ value: ['1', '2'] })).toBe("['1','2']") - }) + it("wraps strings in an array in quotes", () => { + expect(formatQueryParams({ value: ["1", "2"] })).toBe("['1','2']"); + }); - it('formats an object and escapes keys and values', () => { + it("formats an object and escapes keys and values", () => { expect( formatQueryParams({ value: { ["na'me"]: "cust'om", }, }), - ).toBe("{'na\\'me':'cust\\'om'}") + ).toBe("{'na\\'me':'cust\\'om'}"); expect( formatQueryParams({ value: { ["a'b\nc\td\re\\"]: "\\q'w\ne\tr\rt\\y", }, }), - ).toBe("{'a\\'b\\nc\\td\\re\\\\':'\\\\q\\'w\\ne\\tr\\rt\\\\y'}") - }) + ).toBe("{'a\\'b\\nc\\td\\re\\\\':'\\\\q\\'w\\ne\\tr\\rt\\\\y'}"); + }); - it('formats a nested object', () => { + it("formats a nested object", () => { expect( formatQueryParams({ value: { - name: 'custom', + name: "custom", id: 42, params: { refs: [44] }, }, }), - ).toBe("{'name':'custom','id':42,'params':{'refs':[44]}}") - }) + ).toBe("{'name':'custom','id':42,'params':{'refs':[44]}}"); + }); - it('formats booleans in arrays as TRUE/FALSE', () => { - expect(formatQueryParams({ value: [true, false] })).toBe('[TRUE,FALSE]') - expect(formatQueryParams({ value: [true] })).toBe('[TRUE]') - expect(formatQueryParams({ value: [false] })).toBe('[FALSE]') - }) + it("formats booleans in arrays as TRUE/FALSE", () => { + expect(formatQueryParams({ value: [true, false] })).toBe("[TRUE,FALSE]"); + expect(formatQueryParams({ value: [true] })).toBe("[TRUE]"); + expect(formatQueryParams({ value: [false] })).toBe("[FALSE]"); + }); - it('formats booleans in nested arrays as TRUE/FALSE', () => { + it("formats booleans in nested arrays as TRUE/FALSE", () => { expect( formatQueryParams({ value: [ @@ -192,42 +192,42 @@ describe('formatQueryParams', () => { [false, true], ], }), - ).toBe('[[TRUE,FALSE],[FALSE,TRUE]]') + ).toBe("[[TRUE,FALSE],[FALSE,TRUE]]"); expect( formatQueryParams({ value: [[[true]], [[false]]], }), - ).toBe('[[[TRUE]],[[FALSE]]]') - }) + ).toBe("[[[TRUE]],[[FALSE]]]"); + }); - it('formats booleans in arrays with mixed types', () => { - expect(formatQueryParams({ value: [1, true, 'test', false, null] })).toBe( + it("formats booleans in arrays with mixed types", () => { + expect(formatQueryParams({ value: [1, true, "test", false, null] })).toBe( "[1,TRUE,'test',FALSE,NULL]", - ) - }) + ); + }); - it('formats booleans in tuples as TRUE/FALSE', () => { + it("formats booleans in tuples as TRUE/FALSE", () => { expect( formatQueryParams({ value: new TupleParam([true, false]), }), - ).toBe('(TRUE,FALSE)') + ).toBe("(TRUE,FALSE)"); expect( formatQueryParams({ - value: new TupleParam([1, true, 'test', false]), + value: new TupleParam([1, true, "test", false]), }), - ).toBe("(1,TRUE,'test',FALSE)") - }) + ).toBe("(1,TRUE,'test',FALSE)"); + }); - it('formats booleans in nested tuples as TRUE/FALSE', () => { + it("formats booleans in nested tuples as TRUE/FALSE", () => { expect( formatQueryParams({ value: new TupleParam([new TupleParam([true, false]), true]), }), - ).toBe('((TRUE,FALSE),TRUE)') - }) + ).toBe("((TRUE,FALSE),TRUE)"); + }); - it('formats booleans in objects (Maps) as TRUE/FALSE', () => { + it("formats booleans in objects (Maps) as TRUE/FALSE", () => { expect( formatQueryParams({ value: { @@ -235,18 +235,18 @@ describe('formatQueryParams', () => { isDeleted: false, }, }), - ).toBe("{'isActive':TRUE,'isDeleted':FALSE}") - }) + ).toBe("{'isActive':TRUE,'isDeleted':FALSE}"); + }); - it('formats booleans in nested structures', () => { + it("formats booleans in nested structures", () => { expect( formatQueryParams({ value: { - name: 'test', + name: "test", flags: [true, false], tuple: new TupleParam([false, true]), }, }), - ).toBe("{'name':'test','flags':[TRUE,FALSE],'tuple':(FALSE,TRUE)}") - }) -}) + ).toBe("{'name':'test','flags':[TRUE,FALSE],'tuple':(FALSE,TRUE)}"); + }); +}); diff --git a/packages/client-common/__tests__/unit/format_query_settings.test.ts b/packages/client-common/__tests__/unit/format_query_settings.test.ts index cd76c4dd..3624edc0 100644 --- a/packages/client-common/__tests__/unit/format_query_settings.test.ts +++ b/packages/client-common/__tests__/unit/format_query_settings.test.ts @@ -1,32 +1,32 @@ -import { describe, it, expect } from 'vitest' -import { formatQuerySettings, SettingsMap } from '../../src/index' +import { describe, it, expect } from "vitest"; +import { formatQuerySettings, SettingsMap } from "../../src/index"; -describe('formatQuerySettings', () => { - it('formats boolean', () => { - expect(formatQuerySettings(true)).toBe('1') - expect(formatQuerySettings(false)).toBe('0') - }) +describe("formatQuerySettings", () => { + it("formats boolean", () => { + expect(formatQuerySettings(true)).toBe("1"); + expect(formatQuerySettings(false)).toBe("0"); + }); - it('formats a number', () => { - expect(formatQuerySettings(1)).toBe('1') - }) + it("formats a number", () => { + expect(formatQuerySettings(1)).toBe("1"); + }); - it('formats a string', () => { - expect(formatQuerySettings('42')).toBe('42') - }) + it("formats a string", () => { + expect(formatQuerySettings("42")).toBe("42"); + }); - it('formats a Map', () => { + it("formats a Map", () => { expect( - formatQuerySettings(SettingsMap.from({ foo: 'bar', baz: 'qux' })), - ).toBe(`{'foo':'bar','baz':'qux'}`) - }) + formatQuerySettings(SettingsMap.from({ foo: "bar", baz: "qux" })), + ).toBe(`{'foo':'bar','baz':'qux'}`); + }); - it('throws on unsupported values', () => { + it("throws on unsupported values", () => { expect(() => formatQuerySettings(undefined as any)).toThrowError( - 'Unsupported value in query settings: [undefined].', - ) + "Unsupported value in query settings: [undefined].", + ); expect(() => formatQuerySettings([1, 2] as any)).toThrowError( - 'Unsupported value in query settings: [1,2].', - ) - }) -}) + "Unsupported value in query settings: [1,2].", + ); + }); +}); diff --git a/packages/client-common/__tests__/unit/parse_column_types.test.ts b/packages/client-common/__tests__/unit/parse_column_types.test.ts index f7a8b28f..c5b62594 100644 --- a/packages/client-common/__tests__/unit/parse_column_types.test.ts +++ b/packages/client-common/__tests__/unit/parse_column_types.test.ts @@ -1,54 +1,54 @@ -import { describe, it, expect } from 'vitest' -import { parseFixedStringType } from '../../src/parse' +import { describe, it, expect } from "vitest"; +import { parseFixedStringType } from "../../src/parse"; -describe('Columns types parser', () => { - describe('FixedString', () => { - it('should parse FixedString', async () => { +describe("Columns types parser", () => { + describe("FixedString", () => { + it("should parse FixedString", async () => { const args: [string, number][] = [ - ['FixedString(1)', 1], - ['FixedString(42)', 42], - ['FixedString(100)', 100], - ['FixedString(32768)', 32768], - ] + ["FixedString(1)", 1], + ["FixedString(42)", 42], + ["FixedString(100)", 100], + ["FixedString(32768)", 32768], + ]; args.forEach(([columnType, sizeBytes]) => { const result = parseFixedStringType({ columnType, sourceType: columnType, - }) + }); expect( result, `Expected ${columnType} to be parsed as a FixedString with size ${sizeBytes}`, - ).toEqual({ type: 'FixedString', sizeBytes, sourceType: columnType }) - }) - }) + ).toEqual({ type: "FixedString", sizeBytes, sourceType: columnType }); + }); + }); - it('should throw on invalid FixedString type', async () => { + it("should throw on invalid FixedString type", async () => { const args: [string][] = [ - ['FixedString'], - ['FixedString('], - ['FixedString()'], - ['String'], - ] + ["FixedString"], + ["FixedString("], + ["FixedString()"], + ["String"], + ]; args.forEach(([columnType]) => { expect( () => parseFixedStringType({ columnType, sourceType: columnType }), `Expected ${columnType} to throw`, - ).toThrowError('Invalid FixedString type') - }) - }) + ).toThrowError("Invalid FixedString type"); + }); + }); - it('should throw on invalid FixedString size', async () => { + it("should throw on invalid FixedString size", async () => { const args: [string][] = [ - ['FixedString(0)'], - ['FixedString(x)'], + ["FixedString(0)"], + ["FixedString(x)"], [`FixedString(')`], - ] + ]; args.forEach(([columnType]) => { expect( () => parseFixedStringType({ columnType, sourceType: columnType }), `Expected ${columnType} to throw`, - ).toThrowError('Invalid FixedString size in bytes') - }) - }) - }) -}) + ).toThrowError("Invalid FixedString size in bytes"); + }); + }); + }); +}); diff --git a/packages/client-common/__tests__/unit/parse_column_types_array.test.ts b/packages/client-common/__tests__/unit/parse_column_types_array.test.ts index 29dd4f72..1860a4fe 100644 --- a/packages/client-common/__tests__/unit/parse_column_types_array.test.ts +++ b/packages/client-common/__tests__/unit/parse_column_types_array.test.ts @@ -1,90 +1,90 @@ -import { describe, it, expect } from 'vitest' +import { describe, it, expect } from "vitest"; import type { ParsedColumnDateTime, ParsedColumnDateTime64, ParsedColumnEnum, SimpleColumnType, -} from '../../src/parse' -import { parseArrayType } from '../../src/parse' +} from "../../src/parse"; +import { parseArrayType } from "../../src/parse"; -describe('Columns types parser - Array', () => { - it('should parse Array with a simple value type', async () => { +describe("Columns types parser - Array", () => { + it("should parse Array with a simple value type", async () => { interface TestArgs { - columnType: string - valueType: SimpleColumnType - dimensions: number + columnType: string; + valueType: SimpleColumnType; + dimensions: number; } const args: TestArgs[] = [ { - columnType: 'Array(String)', - valueType: 'String', + columnType: "Array(String)", + valueType: "String", dimensions: 1, }, { - columnType: 'Array(UInt8)', - valueType: 'UInt8', + columnType: "Array(UInt8)", + valueType: "UInt8", dimensions: 1, }, { - columnType: 'Array(Array(Int32))', - valueType: 'Int32', + columnType: "Array(Array(Int32))", + valueType: "Int32", dimensions: 2, }, { - columnType: 'Array(Array(Array(Date32)))', - valueType: 'Date32', + columnType: "Array(Array(Array(Date32)))", + valueType: "Date32", dimensions: 3, }, { - columnType: 'Array(Array(Array(Array(Float32))))', - valueType: 'Float32', + columnType: "Array(Array(Array(Array(Float32))))", + valueType: "Float32", dimensions: 4, }, - ] + ]; args.forEach((args: TestArgs) => { - const { columnType, valueType, dimensions } = args - const result = parseArrayType({ columnType, sourceType: columnType }) + const { columnType, valueType, dimensions } = args; + const result = parseArrayType({ columnType, sourceType: columnType }); // Expected ${columnType} to be parsed as an Array with value type ${valueType} and ${dimensions} dimensions expect(result).toEqual({ - type: 'Array', + type: "Array", value: { - type: 'Simple', + type: "Simple", columnType: valueType, sourceType: valueType, // T }, sourceType: columnType, // Array(T) dimensions, - }) - }) - }) + }); + }); + }); - it('should parse Array with Nullable', async () => { + it("should parse Array with Nullable", async () => { interface TestArgs { - columnType: string - valueType: SimpleColumnType - dimensions: number + columnType: string; + valueType: SimpleColumnType; + dimensions: number; } const args: TestArgs[] = [ { - columnType: 'Array(Nullable(String))', - valueType: 'String', + columnType: "Array(Nullable(String))", + valueType: "String", dimensions: 1, }, { - columnType: 'Array(Array(Nullable(Int32)))', - valueType: 'Int32', + columnType: "Array(Array(Nullable(Int32)))", + valueType: "Int32", dimensions: 2, }, - ] + ]; args.forEach(({ columnType, valueType, dimensions }: TestArgs) => { - const result = parseArrayType({ columnType, sourceType: columnType }) + const result = parseArrayType({ columnType, sourceType: columnType }); // Expected ${columnType} to be parsed as an Array with value type ${valueType} and ${dimensions} dimensions expect(result).toEqual({ - type: 'Array', + type: "Array", value: { - type: 'Nullable', + type: "Nullable", value: { - type: 'Simple', + type: "Simple", columnType: valueType, sourceType: valueType, // T }, @@ -92,27 +92,27 @@ describe('Columns types parser - Array', () => { }, sourceType: columnType, // Array(Nullable(T)) dimensions, - }) - }) - }) + }); + }); + }); - it('should parse Array with Enum value type', async () => { + it("should parse Array with Enum value type", async () => { interface TestArgs { - value: ParsedColumnEnum - dimensions: number - columnType: string + value: ParsedColumnEnum; + dimensions: number; + columnType: string; } - const sourceEnum8 = `Enum8('foo' = 42)` - const valuesEnum8 = { 42: 'foo' } - const sourceEnum16 = `Enum16('bar' = 144, 'qaz' = 500)` + const sourceEnum8 = `Enum8('foo' = 42)`; + const valuesEnum8 = { 42: "foo" }; + const sourceEnum16 = `Enum16('bar' = 144, 'qaz' = 500)`; const valuesEnum16 = { - 144: 'bar', - 500: 'qaz', - } + 144: "bar", + 500: "qaz", + }; const args: TestArgs[] = [ { value: { - type: 'Enum', + type: "Enum", intSize: 8, values: valuesEnum8, sourceType: sourceEnum8, @@ -122,7 +122,7 @@ describe('Columns types parser - Array', () => { }, { value: { - type: 'Enum', + type: "Enum", intSize: 16, values: valuesEnum16, sourceType: sourceEnum16, @@ -132,7 +132,7 @@ describe('Columns types parser - Array', () => { }, { value: { - type: 'Enum', + type: "Enum", intSize: 8, values: valuesEnum8, sourceType: sourceEnum8, @@ -142,7 +142,7 @@ describe('Columns types parser - Array', () => { }, { value: { - type: 'Enum', + type: "Enum", intSize: 16, values: valuesEnum16, sourceType: sourceEnum16, @@ -150,39 +150,39 @@ describe('Columns types parser - Array', () => { dimensions: 3, columnType: `Array(Array(Array(${sourceEnum16})))`, }, - ] + ]; args.forEach(({ columnType, dimensions, value }) => { - const result = parseArrayType({ columnType, sourceType: columnType }) + const result = parseArrayType({ columnType, sourceType: columnType }); // Expected ${columnType} to be parsed as an Array with value type ${value.sourceType} and ${dimensions} dimensions expect(result).toEqual({ - type: 'Array', + type: "Array", sourceType: columnType, dimensions, value, - }) - }) - }) + }); + }); + }); - it('should parse Array of DateTime', async () => { + it("should parse Array of DateTime", async () => { interface TestArgs { - value: ParsedColumnDateTime - dimensions: number - columnType: string + value: ParsedColumnDateTime; + dimensions: number; + columnType: string; } const args: TestArgs[] = [ { value: { - type: 'DateTime', + type: "DateTime", timezone: null, - sourceType: 'DateTime', + sourceType: "DateTime", }, dimensions: 1, - columnType: 'Array(DateTime)', + columnType: "Array(DateTime)", }, { value: { - type: 'DateTime', - timezone: 'UTC', + type: "DateTime", + timezone: "UTC", sourceType: `DateTime('UTC')`, }, dimensions: 1, @@ -190,49 +190,49 @@ describe('Columns types parser - Array', () => { }, { value: { - type: 'DateTime', - timezone: 'Etc/GMT-5', + type: "DateTime", + timezone: "Etc/GMT-5", sourceType: `DateTime('Etc/GMT-5')`, }, dimensions: 2, columnType: `Array(Array(DateTime('Etc/GMT-5')))`, }, - ] + ]; args.forEach(({ columnType, dimensions, value }) => { - const result = parseArrayType({ columnType, sourceType: columnType }) + const result = parseArrayType({ columnType, sourceType: columnType }); expect( result, `Expected ${columnType} to be parsed as an Array with value type ${value.sourceType} and ${dimensions} dimensions`, ).toEqual({ - type: 'Array', + type: "Array", sourceType: columnType, dimensions, value, - }) - }) - }) + }); + }); + }); - it('should parse Array of DateTime64', async () => { + it("should parse Array of DateTime64", async () => { interface TestArgs { - value: ParsedColumnDateTime64 - dimensions: number - columnType: string + value: ParsedColumnDateTime64; + dimensions: number; + columnType: string; } const args: TestArgs[] = [ { value: { - type: 'DateTime64', + type: "DateTime64", timezone: null, - sourceType: 'DateTime64(0)', + sourceType: "DateTime64(0)", precision: 0, }, dimensions: 1, - columnType: 'Array(DateTime64(0))', + columnType: "Array(DateTime64(0))", }, { value: { - type: 'DateTime64', - timezone: 'UTC', + type: "DateTime64", + timezone: "UTC", sourceType: `DateTime64(3, 'UTC')`, precision: 3, }, @@ -241,8 +241,8 @@ describe('Columns types parser - Array', () => { }, { value: { - type: 'DateTime64', - timezone: 'Etc/GMT-5', + type: "DateTime64", + timezone: "Etc/GMT-5", sourceType: `DateTime64(6, 'Etc/GMT-5')`, precision: 6, }, @@ -251,49 +251,49 @@ describe('Columns types parser - Array', () => { }, { value: { - type: 'DateTime64', - timezone: 'Europe/Sofia', + type: "DateTime64", + timezone: "Europe/Sofia", sourceType: `DateTime64(9, 'Europe/Sofia')`, precision: 9, }, dimensions: 3, columnType: `Array(Array(Array(DateTime64(9, 'Europe/Sofia'))))`, }, - ] + ]; args.forEach(({ columnType, dimensions, value }) => { - const result = parseArrayType({ columnType, sourceType: columnType }) + const result = parseArrayType({ columnType, sourceType: columnType }); expect( result, `Expected ${columnType} to be parsed as an Array with value type ${value.sourceType} and ${dimensions} dimensions`, ).toEqual({ - type: 'Array', + type: "Array", sourceType: columnType, dimensions, value, - }) - }) - }) + }); + }); + }); // TODO: Map type test. - it('should throw on invalid Array type', async () => { + it("should throw on invalid Array type", async () => { // Array(Int8) is the shortest valid definition const args = [ - ['Array'], - ['Array('], - ['Array()'], - ['Array(a'], - ['Array(ab'], - ['Array(ab)'], - ['Array(abc)'], - ['String'], - ] + ["Array"], + ["Array("], + ["Array()"], + ["Array(a"], + ["Array(ab"], + ["Array(ab)"], + ["Array(abc)"], + ["String"], + ]; args.forEach(([columnType]) => { // Expected ${columnType} to throw expect(() => parseArrayType({ columnType, sourceType: columnType }), - ).toThrowError('Invalid Array type') - }) - }) -}) + ).toThrowError("Invalid Array type"); + }); + }); +}); diff --git a/packages/client-common/__tests__/unit/parse_column_types_datetime.test.ts b/packages/client-common/__tests__/unit/parse_column_types_datetime.test.ts index ff5e1240..88fb81a7 100644 --- a/packages/client-common/__tests__/unit/parse_column_types_datetime.test.ts +++ b/packages/client-common/__tests__/unit/parse_column_types_datetime.test.ts @@ -1,112 +1,115 @@ -import { describe, it, expect } from 'vitest' -import { parseDateTime64Type, parseDateTimeType } from '../../src/parse' +import { describe, it, expect } from "vitest"; +import { parseDateTime64Type, parseDateTimeType } from "../../src/parse"; -describe('Columns types parser - DateTime and DateTime64', () => { - describe('DateTime', () => { - it('should parse DateTime', async () => { +describe("Columns types parser - DateTime and DateTime64", () => { + describe("DateTime", () => { + it("should parse DateTime", async () => { const args: [string, string | null][] = [ - ['DateTime', null], - [`DateTime('GB')`, 'GB'], - [`DateTime('UTC')`, 'UTC'], - [`DateTime('Europe/Amsterdam')`, 'Europe/Amsterdam'], - ] + ["DateTime", null], + [`DateTime('GB')`, "GB"], + [`DateTime('UTC')`, "UTC"], + [`DateTime('Europe/Amsterdam')`, "Europe/Amsterdam"], + ]; args.forEach(([columnType, timezone]) => { - const result = parseDateTimeType({ columnType, sourceType: columnType }) + const result = parseDateTimeType({ + columnType, + sourceType: columnType, + }); expect( result, `Expected ${columnType} to be parsed as a DateTime`, - ).toEqual({ type: 'DateTime', sourceType: columnType, timezone }) - }) - }) + ).toEqual({ type: "DateTime", sourceType: columnType, timezone }); + }); + }); - it('should throw on invalid DateTime', async () => { + it("should throw on invalid DateTime", async () => { // DateTime('GB') has the least amount of chars allowed for a valid DateTime type. const args: [string][] = [ - ['DateTime()'], + ["DateTime()"], [`DateTime(')`], [`DateTime('')`], [`DateTime('A')`], - ['String'], - ] + ["String"], + ]; args.forEach(([columnType]) => { expect( () => parseDateTimeType({ columnType, sourceType: columnType }), `Expected ${columnType} to throw`, - ).toThrowError('Invalid DateTime type') - }) - }) - }) + ).toThrowError("Invalid DateTime type"); + }); + }); + }); - describe('DateTime64', () => { - const precisionRange = [...Array(10).keys()] // 0..9 + describe("DateTime64", () => { + const precisionRange = [...Array(10).keys()]; // 0..9 - it('should parse DateTime64 without timezone', async () => { + it("should parse DateTime64 without timezone", async () => { const args: [string, number][] = precisionRange.map((precision) => [ `DateTime64(${precision})`, precision, - ]) + ]); args.forEach(([columnType, precision]) => { const result = parseDateTime64Type({ columnType, sourceType: columnType, - }) + }); expect( result, `Expected ${columnType} to be parsed as a DateTime64 with precision ${precision}`, ).toEqual({ - type: 'DateTime64', + type: "DateTime64", timezone: null, sourceType: columnType, precision, - }) - }) - }) + }); + }); + }); - it('should parse DateTime64 with timezone', async () => { + it("should parse DateTime64 with timezone", async () => { const allPrecisionArgs: [string, number, string][][] = precisionRange.map( (precision) => [ - [`DateTime64(${precision}, 'GB')`, precision, 'GB'], - [`DateTime64(${precision}, 'UTC')`, precision, 'UTC'], - [`DateTime64(${precision}, 'Etc/GMT-5')`, precision, 'Etc/GMT-5'], + [`DateTime64(${precision}, 'GB')`, precision, "GB"], + [`DateTime64(${precision}, 'UTC')`, precision, "UTC"], + [`DateTime64(${precision}, 'Etc/GMT-5')`, precision, "Etc/GMT-5"], ], - ) + ); allPrecisionArgs.forEach((args) => args.forEach(([columnType, precision, timezone]) => { const result = parseDateTime64Type({ columnType, sourceType: columnType, - }) + }); expect( result, `Expected ${columnType} to be parsed as a DateTime64 with precision ${precision} and timezone ${timezone}`, ).toEqual({ - type: 'DateTime64', + type: "DateTime64", sourceType: columnType, timezone, precision, - }) + }); }), - ) - }) + ); + }); - it('should throw on invalid DateTime64 type', async () => { - const args = [['DateTime64('], ['DateTime64()'], ['String']] + it("should throw on invalid DateTime64 type", async () => { + const args = [["DateTime64("], ["DateTime64()"], ["String"]]; args.forEach(([columnType]) => { expect( () => parseDateTime64Type({ columnType, sourceType: columnType }), `Expected ${columnType} to throw`, - ).toThrowError('Invalid DateTime64 type') - }) - }) + ).toThrowError("Invalid DateTime64 type"); + }); + }); - it('should throw on invalid DateTime64 precision', async () => { - const args = [[`DateTime64(')`], [`DateTime64(foo)`]] + it("should throw on invalid DateTime64 precision", async () => { + const args = [[`DateTime64(')`], [`DateTime64(foo)`]]; args.forEach(([columnType]) => { expect( () => parseDateTime64Type({ columnType, sourceType: columnType }), `Expected ${columnType} to throw`, - ).toThrowError('Invalid DateTime64 precision') - }) - }) - }) -}) + ).toThrowError("Invalid DateTime64 precision"); + }); + }); + }); +}); diff --git a/packages/client-common/__tests__/unit/parse_column_types_decimal.test.ts b/packages/client-common/__tests__/unit/parse_column_types_decimal.test.ts index 1bac26de..52faa629 100644 --- a/packages/client-common/__tests__/unit/parse_column_types_decimal.test.ts +++ b/packages/client-common/__tests__/unit/parse_column_types_decimal.test.ts @@ -1,106 +1,106 @@ -import { describe, it, expect } from 'vitest' -import { parseDecimalType } from '../../src/parse' +import { describe, it, expect } from "vitest"; +import { parseDecimalType } from "../../src/parse"; -describe('Columns types parser - Decimal', () => { +describe("Columns types parser - Decimal", () => { interface TestArgs { - sourceType: string - precision: number - scale: number - intSize: 32 | 64 | 128 | 256 + sourceType: string; + precision: number; + scale: number; + intSize: 32 | 64 | 128 | 256; } - it('should parse Decimal', async () => { + it("should parse Decimal", async () => { const args: TestArgs[] = [ { - sourceType: 'Decimal(7, 2)', + sourceType: "Decimal(7, 2)", precision: 7, scale: 2, intSize: 32, }, { - sourceType: 'Decimal(12, 4)', + sourceType: "Decimal(12, 4)", precision: 12, scale: 4, intSize: 64, }, { - sourceType: 'Decimal(27, 6)', + sourceType: "Decimal(27, 6)", precision: 27, scale: 6, intSize: 128, }, { - sourceType: 'Decimal(42, 8)', + sourceType: "Decimal(42, 8)", precision: 42, scale: 8, intSize: 256, }, - ] + ]; args.forEach(({ sourceType, precision, scale, intSize }) => { - const result = parseDecimalType({ columnType: sourceType, sourceType }) + const result = parseDecimalType({ columnType: sourceType, sourceType }); expect( result, `Expected ${sourceType} to be parsed as a Decimal with precision ${precision}, scale ${scale} and intSize ${intSize}`, ).toEqual({ - type: 'Decimal', + type: "Decimal", params: { precision, scale, intSize }, sourceType, - }) - }) - }) + }); + }); + }); - it('should throw on invalid Decimal type', async () => { + it("should throw on invalid Decimal type", async () => { const args: [string][] = [ - ['Decimal'], - ['Decimal('], - ['Decimal()'], - ['Decimal(1)'], - ['Decimal(1,)'], - ['Decimal(1, )'], - ['String'], - ] + ["Decimal"], + ["Decimal("], + ["Decimal()"], + ["Decimal(1)"], + ["Decimal(1,)"], + ["Decimal(1, )"], + ["String"], + ]; args.forEach(([columnType]) => { expect( () => parseDecimalType({ columnType, sourceType: columnType }), `Expected ${columnType} to throw`, - ).toThrowError('Invalid Decimal type') - }) - }) + ).toThrowError("Invalid Decimal type"); + }); + }); - it('should throw on invalid Decimal precision', async () => { + it("should throw on invalid Decimal precision", async () => { const args: [string][] = [ - ['Decimal(0, 0)'], - ['Decimal(x, 0)'], + ["Decimal(0, 0)"], + ["Decimal(x, 0)"], [`Decimal(', ')`], [`Decimal(77, 1)`], // max is 76 - ] + ]; args.forEach(([columnType]) => { expect( () => parseDecimalType({ columnType, sourceType: columnType }), `Expected ${columnType} to throw`, - ).toThrowError('Invalid Decimal precision') - }) - }) + ).toThrowError("Invalid Decimal precision"); + }); + }); - it('should throw on invalid Decimal scale', async () => { + it("should throw on invalid Decimal scale", async () => { const args: [string][] = [ - ['Decimal(1, 2)'], // scale should be less than precision - ['Decimal(1, x)'], + ["Decimal(1, 2)"], // scale should be less than precision + ["Decimal(1, x)"], [`Decimal(42, ,)`], [`Decimal(42, ')`], - ] + ]; args.forEach(([columnType]) => { expect( () => parseDecimalType({ columnType, sourceType: columnType }), `Expected ${columnType} to throw`, - ).toThrowError('Invalid Decimal scale') - }) - }) + ).toThrowError("Invalid Decimal scale"); + }); + }); - it('should throw when precision or scale cannot be parsed', async () => { - const columnType = 'Decimal(foobar)' + it("should throw when precision or scale cannot be parsed", async () => { + const columnType = "Decimal(foobar)"; expect(() => parseDecimalType({ columnType, sourceType: columnType }), - ).toThrowError('Expected Decimal type to have both precision and scale') - }) -}) + ).toThrowError("Expected Decimal type to have both precision and scale"); + }); +}); diff --git a/packages/client-common/__tests__/unit/parse_column_types_enum.test.ts b/packages/client-common/__tests__/unit/parse_column_types_enum.test.ts index 7ecfb8a6..5956742f 100644 --- a/packages/client-common/__tests__/unit/parse_column_types_enum.test.ts +++ b/packages/client-common/__tests__/unit/parse_column_types_enum.test.ts @@ -1,76 +1,76 @@ -import { describe, it, expect } from 'vitest' -import { enumTypes, parsedEnumTestArgs } from '../utils/native_columns' -import { parseEnumType } from '../../src/parse' +import { describe, it, expect } from "vitest"; +import { enumTypes, parsedEnumTestArgs } from "../utils/native_columns"; +import { parseEnumType } from "../../src/parse"; -describe('Columns types parser - Enum', () => { - it('should parse correct values', async () => { +describe("Columns types parser - Enum", () => { + it("should parse correct values", async () => { parsedEnumTestArgs.forEach((expected) => { const result = parseEnumType({ sourceType: expected.sourceType, columnType: expected.sourceType, - }) + }); expect( result, `Expected ${expected.sourceType} to be parsed as an Enum with intSize ${ expected.intSize } and values ${JSON.stringify(expected.values)}`, - ).toEqual(expected) - }) - }) + ).toEqual(expected); + }); + }); - it('should throw when the type is not a valid enum', async () => { + it("should throw when the type is not a valid enum", async () => { const args: [string][] = [ - ['Enum'], // should be either 8 or 16 - ['Enum32'], - ['Enum64'], - ['String'], - ['Enum(String)'], - ] + ["Enum"], // should be either 8 or 16 + ["Enum32"], + ["Enum64"], + ["String"], + ["Enum(String)"], + ]; args.forEach(([columnType]) => { expect( () => parseEnumType({ columnType, sourceType: columnType }), `Expected ${columnType} to throw`, - ).toThrowError('Expected Enum to be either Enum8 or Enum16') - }) - }) + ).toThrowError("Expected Enum to be either Enum8 or Enum16"); + }); + }); - it('should throw when the values are not valid', async () => { - const args: [string][] = [["Enum8('a' = x)"], ["Enum16('foo' = 'bar')"]] + it("should throw when the values are not valid", async () => { + const args: [string][] = [["Enum8('a' = x)"], ["Enum16('foo' = 'bar')"]]; args.forEach(([columnType]) => { expect( () => parseEnumType({ columnType, sourceType: columnType }), `Expected ${columnType} to throw`, - ).toThrowError('Expected Enum index to be a valid number') - }) - }) + ).toThrowError("Expected Enum index to be a valid number"); + }); + }); - it('should throw on duplicate indices', async () => { + it("should throw on duplicate indices", async () => { const args: [string][] = [ ["Enum8('a' = 0, 'b' = 0)"], ["Enum8('a' = 0, 'b' = 1, 'c' = 1)"], - ] + ]; args.forEach(([columnType]) => { expect( () => parseEnumType({ columnType, sourceType: columnType }), `Expected ${columnType} to throw`, - ).toThrowError('Duplicate Enum index') - }) - }) + ).toThrowError("Duplicate Enum index"); + }); + }); - it('should throw on duplicate names', async () => { + it("should throw on duplicate names", async () => { const args: [string][] = [ ["Enum8('a' = 0, 'a' = 1)"], ["Enum8('a' = 0, 'b' = 1, 'b' = 2)"], - ] + ]; args.forEach(([columnType]) => { expect( () => parseEnumType({ columnType, sourceType: columnType }), `Expected ${columnType} to throw`, - ).toThrowError('Duplicate Enum name') - }) - }) + ).toThrowError("Duplicate Enum name"); + }); + }); - it('should throw when Enum has no values to parse', async () => { + it("should throw when Enum has no values to parse", async () => { // The minimal allowed Enum definition is Enum8('' = 0), i.e. 6 chars inside. const allEnumTypeArgs: string[][] = enumTypes.map(([enumType]) => [ `${enumType}()`, @@ -79,43 +79,43 @@ describe('Columns types parser - Enum', () => { `${enumType}('' )`, `${enumType}('' =)`, `${enumType}('' = )`, - ]) + ]); allEnumTypeArgs.forEach((args) => args.forEach((columnType) => { expect( () => parseEnumType({ columnType, sourceType: columnType }), `Expected ${columnType} to throw`, - ).toThrowError('Invalid Enum type values') + ).toThrowError("Invalid Enum type values"); }), - ) - }) + ); + }); - it('should unescape backslash escape sequences in enum names', async () => { + it("should unescape backslash escape sequences in enum names", async () => { // Test case from issue: parseEnumType returns escaped backslashes instead of unescaping them const result = parseEnumType({ columnType: "Enum8('f\\'' = 1)", sourceType: "Enum8('f\\'' = 1)", - }) + }); expect(result).toEqual({ - type: 'Enum', + type: "Enum", values: { 1: "f'" }, // Should be unescaped, not "f\\'" intSize: 8, sourceType: "Enum8('f\\'' = 1)", - }) + }); // Test various escape sequences const testCases: Array<[string, string]> = [ - ["Enum8('\\n' = 1)", '\n'], // newline - ["Enum8('\\t' = 1)", '\t'], // tab - ["Enum8('\\r' = 1)", '\r'], // carriage return - ["Enum8('\\\\' = 1)", '\\'], // backslash - ["Enum8('a\\nb' = 1)", 'a\nb'], // newline in middle - ["Enum8('\\t\\n\\r' = 1)", '\t\n\r'], // multiple escapes - ] + ["Enum8('\\n' = 1)", "\n"], // newline + ["Enum8('\\t' = 1)", "\t"], // tab + ["Enum8('\\r' = 1)", "\r"], // carriage return + ["Enum8('\\\\' = 1)", "\\"], // backslash + ["Enum8('a\\nb' = 1)", "a\nb"], // newline in middle + ["Enum8('\\t\\n\\r' = 1)", "\t\n\r"], // multiple escapes + ]; testCases.forEach(([sourceType, expectedName]) => { - const parsed = parseEnumType({ columnType: sourceType, sourceType }) - expect(parsed.values[1]).toBe(expectedName) - }) - }) -}) + const parsed = parseEnumType({ columnType: sourceType, sourceType }); + expect(parsed.values[1]).toBe(expectedName); + }); + }); +}); diff --git a/packages/client-common/__tests__/unit/parse_column_types_map.test.ts b/packages/client-common/__tests__/unit/parse_column_types_map.test.ts index 8ea9567f..8186a0f0 100644 --- a/packages/client-common/__tests__/unit/parse_column_types_map.test.ts +++ b/packages/client-common/__tests__/unit/parse_column_types_map.test.ts @@ -1,41 +1,41 @@ -import { describe, it, expect } from 'vitest' -import type { ParsedColumnMap } from '../../src/parse' -import { parseMapType } from '../../src/parse' +import { describe, it, expect } from "vitest"; +import type { ParsedColumnMap } from "../../src/parse"; +import { parseMapType } from "../../src/parse"; -describe('Columns types parser - Map', () => { - it('should parse Map with simple types', async () => { +describe("Columns types parser - Map", () => { + it("should parse Map with simple types", async () => { const args: [ParsedColumnMap, string][] = [ [ { - type: 'Map', - key: { type: 'Simple', columnType: 'String', sourceType: 'String' }, - value: { type: 'Simple', columnType: 'UInt8', sourceType: 'UInt8' }, - sourceType: 'Map(String, UInt8)', + type: "Map", + key: { type: "Simple", columnType: "String", sourceType: "String" }, + value: { type: "Simple", columnType: "UInt8", sourceType: "UInt8" }, + sourceType: "Map(String, UInt8)", }, - 'Map(String, UInt8)', + "Map(String, UInt8)", ], [ { - type: 'Map', - key: { type: 'Simple', columnType: 'Int32', sourceType: 'Int32' }, + type: "Map", + key: { type: "Simple", columnType: "Int32", sourceType: "Int32" }, value: { - type: 'Simple', - columnType: 'Float32', - sourceType: 'Float32', + type: "Simple", + columnType: "Float32", + sourceType: "Float32", }, - sourceType: 'Map(Int32, Float32)', + sourceType: "Map(Int32, Float32)", }, - 'Map(Int32, Float32)', + "Map(Int32, Float32)", ], - ] + ]; args.forEach(([expected, sourceType]) => { - const result = parseMapType({ columnType: sourceType, sourceType }) + const result = parseMapType({ columnType: sourceType, sourceType }); expect( result, `Expected ${sourceType} to be parsed as a Map with key type ${expected.key.sourceType} and value type ${expected.value.sourceType}`, - ).toEqual(expected) - }) - }) + ).toEqual(expected); + }); + }); // TODO: rest of the allowed types. -}) +}); diff --git a/packages/client-common/__tests__/unit/parse_column_types_nullable.test.ts b/packages/client-common/__tests__/unit/parse_column_types_nullable.test.ts index 8e9e23f9..4005f6c1 100644 --- a/packages/client-common/__tests__/unit/parse_column_types_nullable.test.ts +++ b/packages/client-common/__tests__/unit/parse_column_types_nullable.test.ts @@ -1,195 +1,195 @@ -import { describe, it, expect } from 'vitest' +import { describe, it, expect } from "vitest"; import type { ParsedColumnDateTime, ParsedColumnDateTime64, ParsedColumnDecimal, ParsedColumnEnum, ParsedColumnSimple, -} from '../../src/parse' -import { asNullableType } from '../../src/parse' +} from "../../src/parse"; +import { asNullableType } from "../../src/parse"; -describe('Columns types parser - Nullable', () => { - it('should wrap a simple type', async () => { +describe("Columns types parser - Nullable", () => { + it("should wrap a simple type", async () => { const args: [ParsedColumnSimple, string][] = [ [ - { type: 'Simple', columnType: 'String', sourceType: 'String' }, - 'Nullable(String)', + { type: "Simple", columnType: "String", sourceType: "String" }, + "Nullable(String)", ], [ - { type: 'Simple', columnType: 'UInt8', sourceType: 'UInt8' }, - 'Nullable(UInt8)', + { type: "Simple", columnType: "UInt8", sourceType: "UInt8" }, + "Nullable(UInt8)", ], [ - { type: 'Simple', columnType: 'Int32', sourceType: 'Int32' }, - 'Nullable(Int32)', + { type: "Simple", columnType: "Int32", sourceType: "Int32" }, + "Nullable(Int32)", ], [ - { type: 'Simple', columnType: 'Float32', sourceType: 'Float32' }, - 'Nullable(Float32)', + { type: "Simple", columnType: "Float32", sourceType: "Float32" }, + "Nullable(Float32)", ], - ] + ]; args.forEach(([value, sourceType]) => { - const result = asNullableType(value, sourceType) + const result = asNullableType(value, sourceType); expect( result, `Expected ${value.columnType} to be wrapped as ${sourceType}`, ).toEqual({ - type: 'Nullable', + type: "Nullable", sourceType, value, - }) - }) - }) + }); + }); + }); - it('should wrap an Enum', async () => { - const sourceEnum8 = `Enum8('foo' = 42)` - const valuesEnum8 = { 42: 'foo' } - const sourceEnum16 = `Enum16('bar' = 144, 'qaz' = 500)` + it("should wrap an Enum", async () => { + const sourceEnum8 = `Enum8('foo' = 42)`; + const valuesEnum8 = { 42: "foo" }; + const sourceEnum16 = `Enum16('bar' = 144, 'qaz' = 500)`; const valuesEnum16 = { - 144: 'bar', - 500: 'qaz', - } + 144: "bar", + 500: "qaz", + }; const args: [ParsedColumnEnum, string][] = [ [ { - type: 'Enum', + type: "Enum", intSize: 8, values: valuesEnum8, sourceType: sourceEnum8, }, - 'Nullable(Enum8)', + "Nullable(Enum8)", ], [ { - type: 'Enum', + type: "Enum", intSize: 16, values: valuesEnum16, sourceType: sourceEnum16, }, - 'Nullable(Enum16)', + "Nullable(Enum16)", ], - ] + ]; args.forEach(([value, sourceType]) => { - const result = asNullableType(value, sourceType) + const result = asNullableType(value, sourceType); expect( result, `Expected ${value.type} to be wrapped as ${sourceType}`, ).toEqual({ - type: 'Nullable', + type: "Nullable", sourceType, value, - }) - }) - }) + }); + }); + }); - it('should wrap a Decimal', async () => { + it("should wrap a Decimal", async () => { const args: [ParsedColumnDecimal, string][] = [ [ { - type: 'Decimal', + type: "Decimal", params: { intSize: 32, precision: 4, scale: 3 }, - sourceType: 'Decimal(4, 3)', + sourceType: "Decimal(4, 3)", }, - 'Nullable(Decimal(4, 3))', + "Nullable(Decimal(4, 3))", ], [ { - type: 'Decimal', + type: "Decimal", params: { intSize: 64, precision: 12, scale: 6 }, - sourceType: 'Decimal(12, 6)', + sourceType: "Decimal(12, 6)", }, - 'Nullable(Decimal(12, 6))', + "Nullable(Decimal(12, 6))", ], [ { - type: 'Decimal', + type: "Decimal", params: { intSize: 128, precision: 24, scale: 12 }, - sourceType: 'Decimal(24, 12)', + sourceType: "Decimal(24, 12)", }, - 'Nullable(Decimal(24, 12))', + "Nullable(Decimal(24, 12))", ], [ { - type: 'Decimal', + type: "Decimal", params: { intSize: 256, precision: 42, scale: 20 }, - sourceType: 'Decimal(42, 20)', + sourceType: "Decimal(42, 20)", }, - 'Nullable(Decimal(42, 20))', + "Nullable(Decimal(42, 20))", ], - ] + ]; args.forEach(([value, sourceType]) => { - const result = asNullableType(value, sourceType) + const result = asNullableType(value, sourceType); expect( result, `Expected ${value.sourceType} to be wrapped as ${sourceType}`, ).toEqual({ - type: 'Nullable', + type: "Nullable", sourceType, value, - }) - }) - }) + }); + }); + }); - it('should wrap a DateTime', async () => { + it("should wrap a DateTime", async () => { const args: [ParsedColumnDateTime, string][] = [ [ - { type: 'DateTime', timezone: null, sourceType: 'DateTime' }, - 'Nullable(DateTime)', + { type: "DateTime", timezone: null, sourceType: "DateTime" }, + "Nullable(DateTime)", ], [ - { type: 'DateTime', timezone: 'UTC', sourceType: "DateTime('UTC')" }, + { type: "DateTime", timezone: "UTC", sourceType: "DateTime('UTC')" }, `Nullable(DateTime('UTC'))`, ], [ - { type: 'DateTime', timezone: 'GB', sourceType: "DateTime('GB')" }, + { type: "DateTime", timezone: "GB", sourceType: "DateTime('GB')" }, `Nullable(DateTime('GB'))`, ], [ { - type: 'DateTime', - timezone: 'Etc/GMT-5', + type: "DateTime", + timezone: "Etc/GMT-5", sourceType: `DateTime('Etc/GMT-5')`, }, `Nullable(DateTime('Etc/GMT-5'))`, ], - ] + ]; args.forEach(([value, sourceType]) => { - const result = asNullableType(value, sourceType) + const result = asNullableType(value, sourceType); expect( result, `Expected ${value.sourceType} to be wrapped as ${sourceType}`, ).toEqual({ - type: 'Nullable', + type: "Nullable", sourceType, value, - }) - }) - }) + }); + }); + }); - it('should wrap a DateTime64', async () => { + it("should wrap a DateTime64", async () => { const args: [ParsedColumnDateTime64, string][] = [ [ { - type: 'DateTime64', + type: "DateTime64", timezone: null, - sourceType: 'DateTime64(0)', + sourceType: "DateTime64(0)", precision: 3, }, - 'Nullable(DateTime64(0))', + "Nullable(DateTime64(0))", ], [ { - type: 'DateTime64', + type: "DateTime64", timezone: null, - sourceType: 'DateTime64(3)', + sourceType: "DateTime64(3)", precision: 3, }, - 'Nullable(DateTime64(3))', + "Nullable(DateTime64(3))", ], [ { - type: 'DateTime64', - timezone: 'UTC', + type: "DateTime64", + timezone: "UTC", sourceType: `DateTime64(3, 'UTC')`, precision: 3, }, @@ -197,8 +197,8 @@ describe('Columns types parser - Nullable', () => { ], [ { - type: 'DateTime64', - timezone: 'GB', + type: "DateTime64", + timezone: "GB", sourceType: `DateTime64(6, 'GB')`, precision: 6, }, @@ -206,59 +206,59 @@ describe('Columns types parser - Nullable', () => { ], [ { - type: 'DateTime64', - timezone: 'Etc/GMT-5', + type: "DateTime64", + timezone: "Etc/GMT-5", sourceType: `DateTime64(9, 'Etc/GMT-5')`, precision: 9, }, `Nullable(DateTime64(9, 'Etc/GMT-5'))`, ], - ] + ]; args.forEach(([value, sourceType]) => { - const result = asNullableType(value, sourceType) + const result = asNullableType(value, sourceType); expect( result, `Expected ${value.sourceType} to be wrapped as ${sourceType}`, ).toEqual({ - type: 'Nullable', + type: "Nullable", sourceType, value, - }) - }) - }) + }); + }); + }); - it('should throw in case of Array or Map', async () => { + it("should throw in case of Array or Map", async () => { const columnUInt8: ParsedColumnSimple = { - type: 'Simple', - columnType: 'UInt8', - sourceType: 'UInt8', - } + type: "Simple", + columnType: "UInt8", + sourceType: "UInt8", + }; const columnString: ParsedColumnSimple = { - type: 'Simple', - columnType: 'String', - sourceType: 'String', - } + type: "Simple", + columnType: "String", + sourceType: "String", + }; expect(() => asNullableType( { - type: 'Map', + type: "Map", key: columnUInt8, value: columnString, - sourceType: 'Map(UInt8, String)', + sourceType: "Map(UInt8, String)", }, - '...', + "...", ), - ).toThrowError('Map cannot be Nullable') + ).toThrowError("Map cannot be Nullable"); expect(() => asNullableType( { - type: 'Array', + type: "Array", value: columnUInt8, dimensions: 1, - sourceType: 'Array(UInt8)', + sourceType: "Array(UInt8)", }, - '...', + "...", ), - ).toThrowError('Array cannot be Nullable') - }) -}) + ).toThrowError("Array cannot be Nullable"); + }); +}); diff --git a/packages/client-common/__tests__/unit/parse_column_types_tuple.test.ts b/packages/client-common/__tests__/unit/parse_column_types_tuple.test.ts index 42b66998..171064a1 100644 --- a/packages/client-common/__tests__/unit/parse_column_types_tuple.test.ts +++ b/packages/client-common/__tests__/unit/parse_column_types_tuple.test.ts @@ -1,162 +1,162 @@ -import { describe, it, expect } from 'vitest' -import { parsedEnumTestArgs } from '../utils/native_columns' +import { describe, it, expect } from "vitest"; +import { parsedEnumTestArgs } from "../utils/native_columns"; import type { ParsedColumnDateTime, ParsedColumnDateTime64, ParsedColumnFixedString, ParsedColumnSimple, ParsedColumnTuple, -} from '../../src/parse' -import { parseTupleType } from '../../src/parse' +} from "../../src/parse"; +import { parseTupleType } from "../../src/parse"; -describe('Columns types parser - Tuple', () => { - it('should parse Tuple with simple types', async () => { +describe("Columns types parser - Tuple", () => { + it("should parse Tuple with simple types", async () => { const args: TestArgs[] = [ { - sourceType: 'Tuple(String, UInt8)', + sourceType: "Tuple(String, UInt8)", expected: { - type: 'Tuple', + type: "Tuple", elements: [ - { type: 'Simple', columnType: 'String', sourceType: 'String' }, - { type: 'Simple', columnType: 'UInt8', sourceType: 'UInt8' }, + { type: "Simple", columnType: "String", sourceType: "String" }, + { type: "Simple", columnType: "UInt8", sourceType: "UInt8" }, ], - sourceType: 'Tuple(String, UInt8)', + sourceType: "Tuple(String, UInt8)", }, }, { - sourceType: 'Tuple(Int32, Float32)', + sourceType: "Tuple(Int32, Float32)", expected: { - type: 'Tuple', + type: "Tuple", elements: [ - { type: 'Simple', columnType: 'Int32', sourceType: 'Int32' }, - { type: 'Simple', columnType: 'Float32', sourceType: 'Float32' }, + { type: "Simple", columnType: "Int32", sourceType: "Int32" }, + { type: "Simple", columnType: "Float32", sourceType: "Float32" }, ], - sourceType: 'Tuple(Int32, Float32)', + sourceType: "Tuple(Int32, Float32)", }, }, - ] + ]; args.forEach(({ expected, sourceType }) => { - const result = parseTupleType({ columnType: sourceType, sourceType }) + const result = parseTupleType({ columnType: sourceType, sourceType }); expect( result, `Expected ${sourceType} to have ${joinElements(expected)} elements`, - ).toEqual(expected) - }) - }) + ).toEqual(expected); + }); + }); - it('should parse Tuple with Decimals', async () => { + it("should parse Tuple with Decimals", async () => { const args: TestArgs[] = [ { - sourceType: 'Tuple(Decimal(7, 2), Decimal(18, 4))', + sourceType: "Tuple(Decimal(7, 2), Decimal(18, 4))", expected: { - type: 'Tuple', + type: "Tuple", elements: [ { - type: 'Decimal', - sourceType: 'Decimal(7, 2)', + type: "Decimal", + sourceType: "Decimal(7, 2)", params: { precision: 7, scale: 2, intSize: 32 }, }, { - type: 'Decimal', - sourceType: 'Decimal(18, 4)', + type: "Decimal", + sourceType: "Decimal(18, 4)", params: { precision: 18, scale: 4, intSize: 64 }, }, ], - sourceType: 'Tuple(Decimal(7, 2), Decimal(18, 4))', + sourceType: "Tuple(Decimal(7, 2), Decimal(18, 4))", }, }, - ] + ]; args.forEach(({ expected, sourceType }) => { - const result = parseTupleType({ columnType: sourceType, sourceType }) + const result = parseTupleType({ columnType: sourceType, sourceType }); expect( result, `Expected ${sourceType} to have ${joinElements(expected)} elements`, - ).toEqual(expected) - }) - }) + ).toEqual(expected); + }); + }); - it('should parse Tuple with Enums', async () => { + it("should parse Tuple with Enums", async () => { const args: TestArgs[] = parsedEnumTestArgs.map((enumElement) => { // e.g. Tuple(String, Enum8('a' = 1)) - const sourceType = `Tuple(${stringElement.sourceType}, ${enumElement.sourceType})` + const sourceType = `Tuple(${stringElement.sourceType}, ${enumElement.sourceType})`; return { sourceType, expected: { - type: 'Tuple', + type: "Tuple", elements: [stringElement, enumElement], sourceType, }, - } - }) + }; + }); args.forEach(({ expected, sourceType }) => { - const result = parseTupleType({ columnType: sourceType, sourceType }) + const result = parseTupleType({ columnType: sourceType, sourceType }); expect( result, `Expected ${sourceType} to have ${joinElements(expected)} elements`, - ).toEqual(expected) - }) - }) + ).toEqual(expected); + }); + }); - it('should parse Tuple with FixedString/DateTime', async () => { + it("should parse Tuple with FixedString/DateTime", async () => { const fixedStringElement: ParsedColumnFixedString = { - type: 'FixedString', - sourceType: 'FixedString(16)', + type: "FixedString", + sourceType: "FixedString(16)", sizeBytes: 16, - } + }; const dateTimeElement: ParsedColumnDateTime = { - type: 'DateTime', + type: "DateTime", timezone: null, - sourceType: 'DateTime', - } + sourceType: "DateTime", + }; const dateTimeWithTimezoneElement: ParsedColumnDateTime = { - type: 'DateTime', - timezone: 'Europe/Amsterdam', + type: "DateTime", + timezone: "Europe/Amsterdam", sourceType: `DateTime('Europe/Amsterdam')`, - } + }; const dateTime64Element: ParsedColumnDateTime64 = { - type: 'DateTime64', + type: "DateTime64", timezone: null, precision: 3, - sourceType: 'DateTime64(3)', - } + sourceType: "DateTime64(3)", + }; const dateTime64WithTimezoneElement: ParsedColumnDateTime64 = { - type: 'DateTime64', - timezone: 'Europe/Amsterdam', + type: "DateTime64", + timezone: "Europe/Amsterdam", precision: 9, sourceType: `DateTime64(9, 'Europe/Amsterdam')`, - } + }; const elements = [ fixedStringElement, dateTimeElement, dateTimeWithTimezoneElement, dateTime64Element, dateTime64WithTimezoneElement, - ] - const elementsSourceTypes = elements.map((el) => el.sourceType).join(', ') - const sourceType = `Tuple(${elementsSourceTypes})` + ]; + const elementsSourceTypes = elements.map((el) => el.sourceType).join(", "); + const sourceType = `Tuple(${elementsSourceTypes})`; const expected: ParsedColumnTuple = { - type: 'Tuple', + type: "Tuple", elements, sourceType, - } - const result = parseTupleType({ columnType: sourceType, sourceType }) - expect(result).toEqual(expected) - }) + }; + const result = parseTupleType({ columnType: sourceType, sourceType }); + expect(result).toEqual(expected); + }); // TODO: Simple types permutations, Nullable, Arrays, Maps, Nested Tuples const stringElement: ParsedColumnSimple = { - type: 'Simple', - sourceType: 'String', - columnType: 'String', - } -}) + type: "Simple", + sourceType: "String", + columnType: "String", + }; +}); function joinElements(expected: ParsedColumnTuple) { - return expected.elements.map((el) => el.sourceType).join(', ') + return expected.elements.map((el) => el.sourceType).join(", "); } interface TestArgs { - sourceType: string - expected: ParsedColumnTuple + sourceType: string; + expected: ParsedColumnTuple; } diff --git a/packages/client-common/__tests__/unit/stream_utils.test.ts b/packages/client-common/__tests__/unit/stream_utils.test.ts index f8ce6b5c..1e1b9f9d 100644 --- a/packages/client-common/__tests__/unit/stream_utils.test.ts +++ b/packages/client-common/__tests__/unit/stream_utils.test.ts @@ -1,39 +1,39 @@ -import { describe, it, expect } from 'vitest' -import { extractErrorAtTheEndOfChunk } from '../../src/index' +import { describe, it, expect } from "vitest"; +import { extractErrorAtTheEndOfChunk } from "../../src/index"; -describe('utils/stream', () => { - const errMsg = 'boom' - const tag = 'FOOBAR' +describe("utils/stream", () => { + const errMsg = "boom"; + const tag = "FOOBAR"; - it('should handle a valid error chunk', async () => { - const chunk = buildValidErrorChunk(errMsg, tag) + it("should handle a valid error chunk", async () => { + const chunk = buildValidErrorChunk(errMsg, tag); - const err = extractErrorAtTheEndOfChunk(chunk, tag) - expect(err).toBeDefined() - expect(err).toBeInstanceOf(Error) - expect(err!.message).toBe(errMsg) - }) + const err = extractErrorAtTheEndOfChunk(chunk, tag); + expect(err).toBeDefined(); + expect(err).toBeInstanceOf(Error); + expect(err!.message).toBe(errMsg); + }); - it('should handle a broken chunk', async () => { + it("should handle a broken chunk", async () => { const chunk = new TextEncoder().encode( - '\r\nsome random data \nthat does not conform\r\t to the protocol\r\n', - ) + "\r\nsome random data \nthat does not conform\r\t to the protocol\r\n", + ); - const err = extractErrorAtTheEndOfChunk(chunk, tag) - expect(err).toBeDefined() - expect(err).toBeInstanceOf(Error) - expect(err?.message).toContain('error in the stream') - }) + const err = extractErrorAtTheEndOfChunk(chunk, tag); + expect(err).toBeDefined(); + expect(err).toBeInstanceOf(Error); + expect(err?.message).toContain("error in the stream"); + }); - it('should handle a partial of a valid chunk', async () => { - const chunk = buildValidErrorChunk(errMsg, tag).slice(0, 20) + it("should handle a partial of a valid chunk", async () => { + const chunk = buildValidErrorChunk(errMsg, tag).slice(0, 20); - const err = extractErrorAtTheEndOfChunk(chunk, tag) - expect(err).toBeDefined() - expect(err).toBeInstanceOf(Error) - expect(err?.message).toContain('error in the stream') - }) -}) + const err = extractErrorAtTheEndOfChunk(chunk, tag); + expect(err).toBeDefined(); + expect(err).toBeInstanceOf(Error); + expect(err?.message).toContain("error in the stream"); + }); +}); /** * \r\n__exception__\r\nFOOBAR @@ -42,14 +42,14 @@ describe('utils/stream', () => { */ export function buildValidErrorChunk(errMsg: string, tag: string): Uint8Array { const chunkStr = - 'body-body-body-body\r\n__exception__\r\n' + + "body-body-body-body\r\n__exception__\r\n" + tag + - '\n' + + "\n" + errMsg + - '\n' + + "\n" + (errMsg.length + 1) + // +1 to len for the newline character - ' ' + + " " + tag + - '\r\n__exception__\r\n' - return new TextEncoder().encode(chunkStr) + "\r\n__exception__\r\n"; + return new TextEncoder().encode(chunkStr); } diff --git a/packages/client-common/__tests__/unit/to_search_params.test.ts b/packages/client-common/__tests__/unit/to_search_params.test.ts index 9aa9005c..a3778313 100644 --- a/packages/client-common/__tests__/unit/to_search_params.test.ts +++ b/packages/client-common/__tests__/unit/to_search_params.test.ts @@ -1,123 +1,123 @@ -import { describe, it, expect } from 'vitest' -import { toSearchParams } from '../../src/index' -import type { URLSearchParams } from 'url' +import { describe, it, expect } from "vitest"; +import { toSearchParams } from "../../src/index"; +import type { URLSearchParams } from "url"; -describe('toSearchParams', () => { - it('should return only query_id, ignoring the default database', async () => { - const params = toSearchParams({ database: 'default', query_id: 'foo' }) - expect(toSortedArray(params)).toEqual([['query_id', 'foo']]) - }) +describe("toSearchParams", () => { + it("should return only query_id, ignoring the default database", async () => { + const params = toSearchParams({ database: "default", query_id: "foo" }); + expect(toSortedArray(params)).toEqual([["query_id", "foo"]]); + }); - it('should set database', async () => { - const params = toSearchParams({ database: 'my_db', query_id: 'foo' })! + it("should set database", async () => { + const params = toSearchParams({ database: "my_db", query_id: "foo" })!; expect(toSortedArray(params)).toEqual([ - ['database', 'my_db'], - ['query_id', 'foo'], - ]) - }) + ["database", "my_db"], + ["query_id", "foo"], + ]); + }); - it('should set ClickHouse settings', async () => { + it("should set ClickHouse settings", async () => { const params = toSearchParams({ - database: 'default', - query_id: 'foo', + database: "default", + query_id: "foo", clickhouse_settings: { - insert_quorum: '2', - distributed_product_mode: 'global', - limit: '42', + insert_quorum: "2", + distributed_product_mode: "global", + limit: "42", allow_nondeterministic_mutations: undefined, // will be omitted }, - })! + })!; expect(toSortedArray(params)).toEqual([ - ['distributed_product_mode', 'global'], - ['insert_quorum', '2'], - ['limit', '42'], - ['query_id', 'foo'], - ]) - }) + ["distributed_product_mode", "global"], + ["insert_quorum", "2"], + ["limit", "42"], + ["query_id", "foo"], + ]); + }); - it('should set query params', async () => { + it("should set query params", async () => { const params = toSearchParams({ - database: 'default', - query_id: 'foo', + database: "default", + query_id: "foo", query_params: { foo: 42, bar: true, - qaz: 'qux', + qaz: "qux", }, - })! + })!; expect(toSortedArray(params)).toEqual([ - ['param_bar', '1'], - ['param_foo', '42'], - ['param_qaz', 'qux'], - ['query_id', 'foo'], - ]) - }) + ["param_bar", "1"], + ["param_foo", "42"], + ["param_qaz", "qux"], + ["query_id", "foo"], + ]); + }); - it('should set query', async () => { - const query = 'SELECT * FROM system.settings' + it("should set query", async () => { + const query = "SELECT * FROM system.settings"; const params = toSearchParams({ - database: 'default', - query_id: 'foo', + database: "default", + query_id: "foo", query, - })! + })!; expect(toSortedArray(params)).toEqual([ - ['query', query], - ['query_id', 'foo'], - ]) - }) + ["query", query], + ["query_id", "foo"], + ]); + }); - it('should set everything', async () => { - const query = 'SELECT * FROM system.query_log' + it("should set everything", async () => { + const query = "SELECT * FROM system.query_log"; const params = toSearchParams({ - database: 'some_db', + database: "some_db", clickhouse_settings: { extremes: 1, enable_optimize_predicate_expression: 0, wait_end_of_query: 1, }, query_params: { - qaz: 'qux', + qaz: "qux", }, - session_id: 'my-session-id', - role: ['my-role-1', 'my-role-2'], - query_id: 'my-query-id', + session_id: "my-session-id", + role: ["my-role-1", "my-role-2"], + query_id: "my-query-id", query, - })! - const result = toSortedArray(params) + })!; + const result = toSortedArray(params); expect(result).toEqual([ - ['database', 'some_db'], - ['enable_optimize_predicate_expression', '0'], - ['extremes', '1'], - ['param_qaz', 'qux'], - ['query', 'SELECT * FROM system.query_log'], - ['query_id', 'my-query-id'], - ['role', 'my-role-1'], - ['role', 'my-role-2'], - ['session_id', 'my-session-id'], - ['wait_end_of_query', '1'], - ]) - }) + ["database", "some_db"], + ["enable_optimize_predicate_expression", "0"], + ["extremes", "1"], + ["param_qaz", "qux"], + ["query", "SELECT * FROM system.query_log"], + ["query_id", "my-query-id"], + ["role", "my-role-1"], + ["role", "my-role-2"], + ["session_id", "my-session-id"], + ["wait_end_of_query", "1"], + ]); + }); - it('should set a single role', async () => { - const query = 'SELECT * FROM system.query_log' + it("should set a single role", async () => { + const query = "SELECT * FROM system.query_log"; const params = toSearchParams({ query, - database: 'some_db', - query_id: 'my-query-id', - role: 'single-role', - })! - const result = toSortedArray(params) + database: "some_db", + query_id: "my-query-id", + role: "single-role", + })!; + const result = toSortedArray(params); expect(result).toEqual([ - ['database', 'some_db'], - ['query', 'SELECT * FROM system.query_log'], - ['query_id', 'my-query-id'], - ['role', 'single-role'], - ]) - }) -}) + ["database", "some_db"], + ["query", "SELECT * FROM system.query_log"], + ["query_id", "my-query-id"], + ["role", "single-role"], + ]); + }); +}); function toSortedArray(params: URLSearchParams): [string, string][] { return [...params.entries()].sort(([key1], [key2]) => String(key1).localeCompare(String(key2)), - ) + ); } diff --git a/packages/client-common/__tests__/unit/transform_url.test.ts b/packages/client-common/__tests__/unit/transform_url.test.ts index 4d6c6ec2..1c62fcd0 100644 --- a/packages/client-common/__tests__/unit/transform_url.test.ts +++ b/packages/client-common/__tests__/unit/transform_url.test.ts @@ -1,76 +1,76 @@ -import { describe, it, expect } from 'vitest' -import { transformUrl } from '../../src/index' +import { describe, it, expect } from "vitest"; +import { transformUrl } from "../../src/index"; -describe('transformUrl', () => { - it('only adds the trailing slash to a url without pathname', () => { - const url = new URL('http://clickhouse.com') +describe("transformUrl", () => { + it("only adds the trailing slash to a url without pathname", () => { + const url = new URL("http://clickhouse.com"); const newUrl = transformUrl({ url, - }) - expect(newUrl.toString()).toBe('http://clickhouse.com/') - }) + }); + expect(newUrl.toString()).toBe("http://clickhouse.com/"); + }); - it('does nothing with a url with pathname', () => { - const url = new URL('http://clickhouse.com/clickhouse') + it("does nothing with a url with pathname", () => { + const url = new URL("http://clickhouse.com/clickhouse"); const newUrl = transformUrl({ url, - }) - expect(newUrl.toString()).toBe('http://clickhouse.com/clickhouse') - }) + }); + expect(newUrl.toString()).toBe("http://clickhouse.com/clickhouse"); + }); - it('attaches pathname and search params to the url', () => { - const url = new URL('http://clickhouse.com') + it("attaches pathname and search params to the url", () => { + const url = new URL("http://clickhouse.com"); const newUrl = transformUrl({ url, - pathname: '/foo', - searchParams: new URLSearchParams({ bar: 'baz' }), - }) - expect(newUrl.toString()).toBe('http://clickhouse.com/foo?bar=baz') - }) + pathname: "/foo", + searchParams: new URLSearchParams({ bar: "baz" }), + }); + expect(newUrl.toString()).toBe("http://clickhouse.com/foo?bar=baz"); + }); - it('attaches pathname without a leading slash', () => { - const url = new URL('http://clickhouse.com') + it("attaches pathname without a leading slash", () => { + const url = new URL("http://clickhouse.com"); const newUrl = transformUrl({ url, - pathname: 'foo', - }) - expect(newUrl.toString()).toBe('http://clickhouse.com/foo') - }) + pathname: "foo", + }); + expect(newUrl.toString()).toBe("http://clickhouse.com/foo"); + }); - it('attaches pathname to an existing pathname', () => { - const url = new URL('http://clickhouse.com/clickhouse') + it("attaches pathname to an existing pathname", () => { + const url = new URL("http://clickhouse.com/clickhouse"); const newUrl = transformUrl({ url, - pathname: '/foobar', - }) - expect(newUrl.toString()).toBe('http://clickhouse.com/clickhouse/foobar') - }) + pathname: "/foobar", + }); + expect(newUrl.toString()).toBe("http://clickhouse.com/clickhouse/foobar"); + }); - it('allows a trailing slash in the pathname', () => { - const url = new URL('http://clickhouse.com/clickhouse/') + it("allows a trailing slash in the pathname", () => { + const url = new URL("http://clickhouse.com/clickhouse/"); const newUrl = transformUrl({ url, - }) - expect(newUrl.toString()).toBe('http://clickhouse.com/clickhouse/') - }) + }); + expect(newUrl.toString()).toBe("http://clickhouse.com/clickhouse/"); + }); - it('does not mutate an original url', () => { - const url = new URL('http://clickhouse.com') + it("does not mutate an original url", () => { + const url = new URL("http://clickhouse.com"); const newUrl = transformUrl({ url, - pathname: 'foo', - }) - expect(newUrl.toString()).toBe('http://clickhouse.com/foo') - expect(url.toString()).toBe('http://clickhouse.com/') - }) + pathname: "foo", + }); + expect(newUrl.toString()).toBe("http://clickhouse.com/foo"); + expect(url.toString()).toBe("http://clickhouse.com/"); + }); - it('does not mutate an original url search params', () => { - const url = new URL('http://clickhouse.com?slim=shady') + it("does not mutate an original url search params", () => { + const url = new URL("http://clickhouse.com?slim=shady"); const newUrl = transformUrl({ url, - searchParams: new URLSearchParams({ bar: 'baz' }), - }) - expect(newUrl.toString()).toBe('http://clickhouse.com/?bar=baz') - expect(url.toString()).toBe('http://clickhouse.com/?slim=shady') - }) -}) + searchParams: new URLSearchParams({ bar: "baz" }), + }); + expect(newUrl.toString()).toBe("http://clickhouse.com/?bar=baz"); + expect(url.toString()).toBe("http://clickhouse.com/?slim=shady"); + }); +}); diff --git a/packages/client-common/__tests__/utils/client.ts b/packages/client-common/__tests__/utils/client.ts index b0ae31fc..c51bfeee 100644 --- a/packages/client-common/__tests__/utils/client.ts +++ b/packages/client-common/__tests__/utils/client.ts @@ -1,24 +1,24 @@ /* eslint @typescript-eslint/no-var-requires: 0 */ -import { beforeAll } from 'vitest' +import { beforeAll } from "vitest"; import { 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' +} from "@clickhouse/client-common"; +import { EnvKeys, getFromEnv } from "./env"; +import { guid } from "./guid"; +import { createSimpleTestClient, getTestLogConfig } from "./simple_client"; import { getClickHouseTestEnvironment, isCloudTestEnv, PRINT_DDL, SKIP_INIT, TestEnv, -} from './test_env' +} from "./test_env"; -export { createSimpleTestClient } +export { createSimpleTestClient }; -let databaseName: string +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 @@ -27,18 +27,18 @@ if (!SKIP_INIT) { beforeAll(async () => { console.log( `\nTest environment: ${getClickHouseTestEnvironment()}, database: ${ - databaseName ?? 'default' + databaseName ?? "default" }`, - ) + ); const initClient = createTestClient({ request_timeout: 10_000, - }) + }); if (isCloudTestEnv() && databaseName === undefined) { - await wakeUpPing(initClient) - databaseName = await createRandomDatabase(initClient) + await wakeUpPing(initClient); + databaseName = await createRandomDatabase(initClient); } - await initClient.close() - }) + await initClient.close(); + }); } export function createTestClient( @@ -47,22 +47,22 @@ export function createTestClient( // 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) + return createSimpleTestClient(config); } - const env = getClickHouseTestEnvironment() + const env = getClickHouseTestEnvironment(); const clickHouseSettings: ClickHouseSettings = { // (U)Int64 are not quoted by default since 25.8 output_format_json_quote_64bit_integers: 1, - } + }; if (env === TestEnv.LocalCluster) { - clickHouseSettings.insert_quorum = '2' + clickHouseSettings.insert_quorum = "2"; } else if (env === TestEnv.Cloud) { - clickHouseSettings.select_sequential_consistency = '1' + clickHouseSettings.select_sequential_consistency = "1"; } // Allow to override `insert_quorum` if necessary - Object.assign(clickHouseSettings, config?.clickhouse_settings || {}) - const log = getTestLogConfig(config) + Object.assign(clickHouseSettings, config?.clickhouse_settings || {}); + const log = getTestLogConfig(config); if (isCloudTestEnv()) { return (globalThis as any).environmentSpecificCreateClient({ @@ -73,42 +73,42 @@ export function createTestClient( log, ...config, clickhouse_settings: clickHouseSettings, - }) as ClickHouseClient + }) as ClickHouseClient; } else { // The local cluster entrypoint (nginx round-robin LB) is exposed on a different // host port than the single-node setup so both can run side by side. // See docker-compose.yml for the full port mapping. const url = env === TestEnv.LocalCluster - ? 'http://127.0.0.1:8127' - : 'http://127.0.0.1:8123' + ? "http://127.0.0.1:8127" + : "http://127.0.0.1:8123"; return (globalThis as any).environmentSpecificCreateClient({ url, database: databaseName, log, ...config, clickhouse_settings: clickHouseSettings, - }) as ClickHouseClient + }) as ClickHouseClient; } } export async function createRandomDatabase( client: ClickHouseClient, ): Promise { - const databaseName = `clickhousejs__${guid()}__${+new Date()}` - let maybeOnCluster = '' + const databaseName = `clickhousejs__${guid()}__${+new Date()}`; + let maybeOnCluster = ""; if (getClickHouseTestEnvironment() === TestEnv.LocalCluster) { - maybeOnCluster = ` ON CLUSTER '{cluster}'` + maybeOnCluster = ` ON CLUSTER '{cluster}'`; } - const ddl = `CREATE DATABASE IF NOT EXISTS ${databaseName}${maybeOnCluster}` + const ddl = `CREATE DATABASE IF NOT EXISTS ${databaseName}${maybeOnCluster}`; await client.command({ query: ddl, clickhouse_settings: { wait_end_of_query: 1, }, - }) - console.log(`\nCreated database ${databaseName}`) - return databaseName + }); + console.log(`\nCreated database ${databaseName}`); + return databaseName; } export async function createTable( @@ -116,8 +116,8 @@ export async function createTable( definition: (environment: TestEnv) => string, clickhouse_settings?: ClickHouseSettings, ): Promise { - const env = getClickHouseTestEnvironment() - const ddl = definition(env) + const env = getClickHouseTestEnvironment(); + const ddl = definition(env); await client.command({ query: ddl, clickhouse_settings: { @@ -127,41 +127,41 @@ export async function createTable( wait_end_of_query: 1, ...(clickhouse_settings || {}), }, - }) + }); if (PRINT_DDL) { - console.info(`\nCreated a table using DDL:\n${ddl}`) + console.info(`\nCreated a table using DDL:\n${ddl}`); } } export function getTestDatabaseName(): string { - return databaseName || 'default' + return databaseName || "default"; } -const MaxPingRetries = 30 +const MaxPingRetries = 30; export async function wakeUpPing(client: ClickHouseClient): Promise { - let attempts = 1 - let lastError: Error | unknown - let isAwake = false + let attempts = 1; + let lastError: Error | unknown; + let isAwake = false; while (attempts <= MaxPingRetries) { - const result = await client.ping() - isAwake = result.success + const result = await client.ping(); + isAwake = result.success; if (result.success) { - break + break; } console.warn( `Service is still waking up, ping attempts so far: ${attempts}. Cause:`, result.error, - ) - lastError = result.error - attempts++ + ); + lastError = result.error; + attempts++; } if (!isAwake) { console.error( `Failed to wake up the service after ${MaxPingRetries} attempts, exiting. Last error:`, lastError, - ) - await client.close() - throw new Error('Failed to wake up the service') + ); + await client.close(); + throw new Error("Failed to wake up the service"); } } diff --git a/packages/client-common/__tests__/utils/datasets.ts b/packages/client-common/__tests__/utils/datasets.ts index 1966ccb4..7597ceef 100644 --- a/packages/client-common/__tests__/utils/datasets.ts +++ b/packages/client-common/__tests__/utils/datasets.ts @@ -1,6 +1,6 @@ -import type { ClickHouseClient } from '@clickhouse/client-common' -import { fakerRU } from '@faker-js/faker' -import { createTableWithFields } from '@test/fixtures/table_with_fields' +import type { ClickHouseClient } from "@clickhouse/client-common"; +import { fakerRU } from "@faker-js/faker"; +import { createTableWithFields } from "@test/fixtures/table_with_fields"; export async function genLargeStringsDataset( client: ClickHouseClient, @@ -8,30 +8,30 @@ export async function genLargeStringsDataset( rows, words, }: { - rows: number - words: number + rows: number; + words: number; }, ): Promise<{ - table: string - values: { id: number; sentence: string; timestamp: string }[] + table: string; + values: { id: number; sentence: string; timestamp: string }[]; }> { const table = await createTableWithFields( client as ClickHouseClient, `sentence String, timestamp String`, - ) + ); const values = [...new Array(rows)].map((_, id) => ({ id, // it seems that it is easier to trigger an incorrect behavior with non-ASCII symbols sentence: fakerRU.lorem.sentence(words), timestamp: new Date().toISOString(), - })) + })); await client.insert({ table, values, - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); return { table, values, - } + }; } diff --git a/packages/client-common/__tests__/utils/env.test.ts b/packages/client-common/__tests__/utils/env.test.ts index 243f72c4..7f666893 100644 --- a/packages/client-common/__tests__/utils/env.test.ts +++ b/packages/client-common/__tests__/utils/env.test.ts @@ -1,84 +1,84 @@ -import { describe, it, expect, beforeEach, beforeAll, afterAll } from 'vitest' +import { describe, it, expect, beforeEach, beforeAll, afterAll } from "vitest"; import { getTestConnectionType, TestConnectionType, -} from './test_connection_type' -import { getClickHouseTestEnvironment, TestEnv } from './test_env' +} from "./test_connection_type"; +import { getClickHouseTestEnvironment, TestEnv } from "./test_env"; -describe('Test env variables parsing', () => { - describe('CLICKHOUSE_TEST_ENVIRONMENT', () => { - const key = 'CLICKHOUSE_TEST_ENVIRONMENT' - addHooks(key) +describe("Test env variables parsing", () => { + describe("CLICKHOUSE_TEST_ENVIRONMENT", () => { + const key = "CLICKHOUSE_TEST_ENVIRONMENT"; + addHooks(key); - it('should fall back to local_single_node env if unset', async () => { - expect(getClickHouseTestEnvironment()).toBe(TestEnv.LocalSingleNode) - }) + it("should fall back to local_single_node env if unset", async () => { + expect(getClickHouseTestEnvironment()).toBe(TestEnv.LocalSingleNode); + }); - it('should be able to set local_single_node env explicitly', async () => { - process.env[key] = 'local_single_node' - expect(getClickHouseTestEnvironment()).toBe(TestEnv.LocalSingleNode) - }) + it("should be able to set local_single_node env explicitly", async () => { + process.env[key] = "local_single_node"; + expect(getClickHouseTestEnvironment()).toBe(TestEnv.LocalSingleNode); + }); - it('should be able to set local_cluster env', async () => { - process.env[key] = 'local_cluster' - expect(getClickHouseTestEnvironment()).toBe(TestEnv.LocalCluster) - }) + it("should be able to set local_cluster env", async () => { + process.env[key] = "local_cluster"; + expect(getClickHouseTestEnvironment()).toBe(TestEnv.LocalCluster); + }); - it('should be able to set cloud env', async () => { - process.env[key] = 'cloud' - expect(getClickHouseTestEnvironment()).toBe(TestEnv.Cloud) - }) + it("should be able to set cloud env", async () => { + process.env[key] = "cloud"; + expect(getClickHouseTestEnvironment()).toBe(TestEnv.Cloud); + }); - it('should throw in case of an empty string', async () => { - process.env[key] = '' - expect(getClickHouseTestEnvironment).toThrowError() - }) + it("should throw in case of an empty string", async () => { + process.env[key] = ""; + expect(getClickHouseTestEnvironment).toThrowError(); + }); - it('should throw in case of malformed enum value', async () => { - process.env[key] = 'foobar' - expect(getClickHouseTestEnvironment).toThrowError() - }) - }) + it("should throw in case of malformed enum value", async () => { + process.env[key] = "foobar"; + expect(getClickHouseTestEnvironment).toThrowError(); + }); + }); - describe('CLICKHOUSE_TEST_CONNECTION_TYPE', () => { - const key = 'CLICKHOUSE_TEST_CONNECTION_TYPE' - addHooks(key) + describe("CLICKHOUSE_TEST_CONNECTION_TYPE", () => { + const key = "CLICKHOUSE_TEST_CONNECTION_TYPE"; + addHooks(key); - it('should fall back to Node.js if unset', async () => { - expect(getTestConnectionType()).toBe(TestConnectionType.Node) - }) + it("should fall back to Node.js if unset", async () => { + expect(getTestConnectionType()).toBe(TestConnectionType.Node); + }); - it('should be able to set Node.js explicitly', async () => { - process.env[key] = 'node' - expect(getTestConnectionType()).toBe(TestConnectionType.Node) - }) + it("should be able to set Node.js explicitly", async () => { + process.env[key] = "node"; + expect(getTestConnectionType()).toBe(TestConnectionType.Node); + }); - it('should be able to set Browser explicitly', async () => { - process.env[key] = 'browser' - expect(getTestConnectionType()).toBe(TestConnectionType.Browser) - }) + it("should be able to set Browser explicitly", async () => { + process.env[key] = "browser"; + expect(getTestConnectionType()).toBe(TestConnectionType.Browser); + }); - it('should throw in case of an empty string', async () => { - process.env[key] = '' - expect(getTestConnectionType).toThrowError() - }) + it("should throw in case of an empty string", async () => { + process.env[key] = ""; + expect(getTestConnectionType).toThrowError(); + }); - it('should throw in case of malformed enum value', async () => { - process.env[key] = 'foobar' - expect(getTestConnectionType).toThrowError() - }) - }) + it("should throw in case of malformed enum value", async () => { + process.env[key] = "foobar"; + expect(getTestConnectionType).toThrowError(); + }); + }); function addHooks(key: string) { - let previousValue = process.env[key] + let previousValue = process.env[key]; beforeAll(() => { - previousValue = process.env[key] - }) + previousValue = process.env[key]; + }); beforeEach(() => { - Reflect.deleteProperty(process.env, key) - }) + Reflect.deleteProperty(process.env, key); + }); afterAll(() => { - process.env[key] = previousValue - }) + process.env[key] = previousValue; + }); } -}) +}); diff --git a/packages/client-common/__tests__/utils/env.ts b/packages/client-common/__tests__/utils/env.ts index 5c4a22c1..ab424611 100644 --- a/packages/client-common/__tests__/utils/env.ts +++ b/packages/client-common/__tests__/utils/env.ts @@ -1,41 +1,41 @@ export const EnvKeys = { - host: 'CLICKHOUSE_CLOUD_HOST', - username: 'CLICKHOUSE_CLOUD_USERNAME', - password: 'CLICKHOUSE_CLOUD_PASSWORD', - jwt_access_token: 'CLICKHOUSE_CLOUD_JWT_ACCESS_TOKEN', - jwt_secret: 'CLICKHOUSE_CLOUD_JWT_SECRET', -} + host: "CLICKHOUSE_CLOUD_HOST", + username: "CLICKHOUSE_CLOUD_USERNAME", + password: "CLICKHOUSE_CLOUD_PASSWORD", + jwt_access_token: "CLICKHOUSE_CLOUD_JWT_ACCESS_TOKEN", + jwt_secret: "CLICKHOUSE_CLOUD_JWT_SECRET", +}; export function getFromEnv(key: string): string { - let value = process.env[key] + let value = process.env[key]; // Allow overriding org level CI environment variables with "unset" value, // which will be treated as not set - if (value === 'unset') { - value = undefined + if (value === "unset") { + value = undefined; } if (value === undefined) { - throw Error(`Environment variable ${key} is not set`) + throw Error(`Environment variable ${key} is not set`); } - return value + return value; } export function maybeGetFromEnv(key: string): string | undefined { - const value = process.env[key] + const value = process.env[key]; // Allow overriding org level CI environment variables with "unset" value, // which will be treated as not set - if (value === 'unset') { - return undefined + if (value === "unset") { + return undefined; } - return value + return value; } export function getAuthFromEnv() { - if (process.env['CLICKHOUSE_TEST_ENVIRONMENT'] === 'cloud') { - const username = process.env[EnvKeys.username] - const password = process.env[EnvKeys.password] - return { username: username ?? 'default', password: password ?? '' } + if (process.env["CLICKHOUSE_TEST_ENVIRONMENT"] === "cloud") { + const username = process.env[EnvKeys.username]; + const password = process.env[EnvKeys.password]; + return { username: username ?? "default", password: password ?? "" }; } - return { username: 'default', password: '' } + return { username: "default", password: "" }; } diff --git a/packages/client-common/__tests__/utils/guid.ts b/packages/client-common/__tests__/utils/guid.ts index 2da20c64..7dbb2632 100644 --- a/packages/client-common/__tests__/utils/guid.ts +++ b/packages/client-common/__tests__/utils/guid.ts @@ -1,13 +1,13 @@ -import * as uuid from 'uuid' +import * as uuid from "uuid"; export function guid(): string { - return uuid.v4().replace(/-/g, '') + return uuid.v4().replace(/-/g, ""); } export function randomUUID(): string { - return uuid.v4() + return uuid.v4(); } export function validateUUID(s: string): boolean { - return uuid.validate(s) + return uuid.validate(s); } diff --git a/packages/client-common/__tests__/utils/index.ts b/packages/client-common/__tests__/utils/index.ts index fa599e87..8dceba2c 100644 --- a/packages/client-common/__tests__/utils/index.ts +++ b/packages/client-common/__tests__/utils/index.ts @@ -1,15 +1,15 @@ -export { TestLogger } from './test_logger' +export { TestLogger } from "./test_logger"; export { createTestClient, createSimpleTestClient, createRandomDatabase, createTable, getTestDatabaseName, -} from './client' -export { guid, validateUUID } from './guid' -export { getClickHouseTestEnvironment } from './test_env' -export { TestEnv, isOnEnv } from './test_env' -export { sleep } from './sleep' -export { getRandomInt } from './random' -export * from './permutations' -export * from './server_version' +} from "./client"; +export { guid, validateUUID } from "./guid"; +export { getClickHouseTestEnvironment } from "./test_env"; +export { TestEnv, isOnEnv } from "./test_env"; +export { sleep } from "./sleep"; +export { getRandomInt } from "./random"; +export * from "./permutations"; +export * from "./server_version"; diff --git a/packages/client-common/__tests__/utils/native_columns.ts b/packages/client-common/__tests__/utils/native_columns.ts index d1ec920c..0dfdaa70 100644 --- a/packages/client-common/__tests__/utils/native_columns.ts +++ b/packages/client-common/__tests__/utils/native_columns.ts @@ -1,53 +1,53 @@ -import type { ParsedColumnEnum } from '../../src/parse' +import type { ParsedColumnEnum } from "../../src/parse"; -export const enumTypes: ['Enum8' | 'Enum16', 8 | 16][] = [ - ['Enum8', 8], - ['Enum16', 16], -] +export const enumTypes: ["Enum8" | "Enum16", 8 | 16][] = [ + ["Enum8", 8], + ["Enum16", 16], +]; export const parsedEnumTestArgs: ParsedColumnEnum[] = enumTypes.flatMap( ([enumType, intSize]) => [ { - type: 'Enum', + type: "Enum", sourceType: `${enumType}('a' = 1)`, values: { - 1: 'a', + 1: "a", } as Record, intSize, }, { - type: 'Enum', + type: "Enum", sourceType: `${enumType}('a' = 0, 'b' = 2)`, values: { - 0: 'a', - 2: 'b', + 0: "a", + 2: "b", }, intSize, }, { - type: 'Enum', + type: "Enum", sourceType: `${enumType}('a' = 1, 'b' = 2, 'c' = 42)`, values: { - 1: 'a', - 2: 'b', - 42: 'c', + 1: "a", + 2: "b", + 42: "c", }, intSize, }, { - type: 'Enum', + type: "Enum", sourceType: `${enumType}('f\\'' = 1, 'x =' = 2, 'b\\'\\'\\'' = 3, '\\'c=4=' = 42, '4' = 100)`, values: { 1: "f'", - 2: 'x =', + 2: "x =", 3: "b'''", 42: "'c=4=", - 100: '4', + 100: "4", }, intSize, }, { - type: 'Enum', + type: "Enum", sourceType: `${enumType}('f\\'()' = 1)`, values: { 1: "f'()", @@ -55,7 +55,7 @@ export const parsedEnumTestArgs: ParsedColumnEnum[] = enumTypes.flatMap( intSize, }, { - type: 'Enum', + type: "Enum", sourceType: `${enumType}('\\'' = 0)`, values: { 0: `'`, @@ -63,62 +63,62 @@ export const parsedEnumTestArgs: ParsedColumnEnum[] = enumTypes.flatMap( intSize, }, { - type: 'Enum', + type: "Enum", sourceType: `${enumType}('' = 0)`, values: { - 0: '', + 0: "", }, intSize, }, { - type: 'Enum', + type: "Enum", sourceType: `${enumType}('' = 42)`, values: { - 42: '', + 42: "", }, intSize, }, { - type: 'Enum', + type: "Enum", sourceType: `${enumType}('foo' = 1, '' = 42)`, values: { - 1: 'foo', - 42: '', + 1: "foo", + 42: "", }, intSize, }, { - type: 'Enum', + type: "Enum", sourceType: `${enumType}('' = 0, 'foo' = 42)`, values: { - 0: '', - 42: 'foo', + 0: "", + 42: "foo", }, intSize, }, { - type: 'Enum', + type: "Enum", sourceType: `${enumType}('(' = 1)`, values: { - 1: '(', + 1: "(", }, intSize, }, { - type: 'Enum', + type: "Enum", sourceType: `${enumType}(')' = 1)`, values: { - 1: ')', + 1: ")", }, intSize, }, { - type: 'Enum', + type: "Enum", sourceType: `${enumType}('()' = 1)`, values: { - 1: '()', + 1: "()", }, intSize, }, ], -) +); diff --git a/packages/client-common/__tests__/utils/parametrized.ts b/packages/client-common/__tests__/utils/parametrized.ts index 63c2e25b..59a11f65 100644 --- a/packages/client-common/__tests__/utils/parametrized.ts +++ b/packages/client-common/__tests__/utils/parametrized.ts @@ -1,30 +1,30 @@ -import type { ClickHouseClient } from '@clickhouse/client-common' +import type { ClickHouseClient } from "@clickhouse/client-common"; -const baseClientMethod = ['query', 'command', 'exec'] as const +const baseClientMethod = ["query", "command", "exec"] as const; interface TestParam { - methodName: (typeof baseClientMethod)[number] | 'insert' - methodCall: (http_headers: Record) => Promise + methodName: (typeof baseClientMethod)[number] | "insert"; + methodCall: (http_headers: Record) => Promise; } export function getHeadersTestParams( - client: Pick, TestParam['methodName']>, + client: Pick, TestParam["methodName"]>, ): Array { const testParams: Array = baseClientMethod.map((methodName) => ({ methodName, methodCall: (http_headers) => client[methodName]({ - query: 'SELECT 42', + query: "SELECT 42", http_headers, }), - })) + })); testParams.push({ - methodName: 'insert', + methodName: "insert", methodCall: (http_headers) => client.insert({ - table: 'foo', - values: ['foo', 'bar'], + table: "foo", + values: ["foo", "bar"], http_headers, }), - }) - return testParams + }); + return testParams; } diff --git a/packages/client-common/__tests__/utils/permutations.ts b/packages/client-common/__tests__/utils/permutations.ts index db771d66..f8e79465 100644 --- a/packages/client-common/__tests__/utils/permutations.ts +++ b/packages/client-common/__tests__/utils/permutations.ts @@ -1,9 +1,9 @@ // adjusted from https://stackoverflow.com/a/64414875/4575540 export function permutations(args: T[], n: number, prefix: T[] = []): T[][] { if (n === 0) { - return [prefix] + return [prefix]; } return args.flatMap((arg, i) => permutations(args.slice(i + 1), n - 1, [...prefix, arg]), - ) + ); } diff --git a/packages/client-common/__tests__/utils/random.ts b/packages/client-common/__tests__/utils/random.ts index c08815e8..fee09209 100644 --- a/packages/client-common/__tests__/utils/random.ts +++ b/packages/client-common/__tests__/utils/random.ts @@ -1,6 +1,6 @@ /** @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random#getting_a_random_integer_between_two_values */ export function getRandomInt(min: number, max: number): number { - min = Math.ceil(min) - max = Math.floor(max) - return Math.floor(Math.random() * (max - min) + min) // The maximum is exclusive and the minimum is inclusive + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min) + min); // The maximum is exclusive and the minimum is inclusive } diff --git a/packages/client-common/__tests__/utils/server_version.ts b/packages/client-common/__tests__/utils/server_version.ts index 5af0585e..356ecbd5 100644 --- a/packages/client-common/__tests__/utils/server_version.ts +++ b/packages/client-common/__tests__/utils/server_version.ts @@ -1,70 +1,70 @@ -import type { ClickHouseClient } from '@clickhouse/client-common' +import type { ClickHouseClient } from "@clickhouse/client-common"; interface ServerVersion { - major: number - minor: number + major: number; + minor: number; } -const versionCache: WeakMap = new WeakMap() +const versionCache: WeakMap = new WeakMap(); export async function getServerVersion( client: ClickHouseClient, ): Promise { - const cachedVersion = versionCache.get(client) + const cachedVersion = versionCache.get(client); if (cachedVersion) { - return cachedVersion + return cachedVersion; } const rs = await client.query({ query: `SELECT version() as version`, - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); // Example result: [ { version: '25.8.1.3994' } ] - const result = await rs.json<{ version: string }>() - const firstRow = result[0] + const result = await rs.json<{ version: string }>(); + const firstRow = result[0]; if (!firstRow) { throw new Error( `Unable to determine ClickHouse server version, empty result from query`, - ) + ); } - const version = firstRow.version + const version = firstRow.version; if (!version) { throw new Error( `Unable to determine ClickHouse server version, missing 'version' field in query result: ${JSON.stringify( firstRow, )}`, - ) + ); } - console.info('Got server version:', version) + console.info("Got server version:", version); - const versionMatch = version.match(/^(\d+)\.(\d+)/) + const versionMatch = version.match(/^(\d+)\.(\d+)/); if (!versionMatch) { throw new Error( `Unable to parse ClickHouse server version from string: ${version}`, - ) + ); } - const major = parseInt(versionMatch[1], 10) + const major = parseInt(versionMatch[1], 10); if (isNaN(major)) { throw new Error( `Unable to parse ClickHouse server major version component from string: ${versionMatch[1]}`, - ) + ); } - const minor = parseInt(versionMatch[2], 10) + const minor = parseInt(versionMatch[2], 10); if (isNaN(minor)) { throw new Error( `Unable to parse ClickHouse server minor version component from string: ${versionMatch[2]}`, - ) + ); } const serverVersion = { major, minor, - } - versionCache.set(client, serverVersion) - return serverVersion + }; + versionCache.set(client, serverVersion); + return serverVersion; } export async function isClickHouseVersionAtLeast( @@ -72,16 +72,16 @@ export async function isClickHouseVersionAtLeast( major: number, minor: number, ): Promise { - const serverVersion = await getServerVersion(client) + const serverVersion = await getServerVersion(client); if (serverVersion.major > major) { - return true + return true; } if (serverVersion.major === major && serverVersion.minor >= minor) { - return true + return true; } console.info( `ClickHouse server version ${serverVersion.major}.${serverVersion.minor} does not meet required version ${major}.${minor}`, - ) - return false + ); + return false; } diff --git a/packages/client-common/__tests__/utils/simple_client.ts b/packages/client-common/__tests__/utils/simple_client.ts index b73eeac4..3f105374 100644 --- a/packages/client-common/__tests__/utils/simple_client.ts +++ b/packages/client-common/__tests__/utils/simple_client.ts @@ -2,8 +2,8 @@ import { ClickHouseLogLevel, type BaseClickHouseClientConfigOptions, type ClickHouseClient, -} from '@clickhouse/client-common' -import { TestLogger } from './test_logger' +} from "@clickhouse/client-common"; +import { TestLogger } from "./test_logger"; /** * Resolves the test logger configuration based on the provided config and the @@ -12,18 +12,18 @@ import { TestLogger } from './test_logger' */ export function getTestLogConfig( config: BaseClickHouseClientConfigOptions = {}, -): BaseClickHouseClientConfigOptions['log'] { +): BaseClickHouseClientConfigOptions["log"] { const level = config.log?.level ?? - (!process.env.LOG_LEVEL || process.env.LOG_LEVEL === 'undefined' + (!process.env.LOG_LEVEL || process.env.LOG_LEVEL === "undefined" ? undefined : ClickHouseLogLevel[ process.env.LOG_LEVEL as keyof typeof ClickHouseLogLevel - ]) + ]); return { LoggerClass: TestLogger, level, - } + }; } /** @@ -43,5 +43,5 @@ export function createSimpleTestClient( return (globalThis as any).environmentSpecificCreateClient({ log: getTestLogConfig(config), ...config, - }) as ClickHouseClient + }) as ClickHouseClient; } diff --git a/packages/client-common/__tests__/utils/sleep.ts b/packages/client-common/__tests__/utils/sleep.ts index adf71b01..ae67cc70 100644 --- a/packages/client-common/__tests__/utils/sleep.ts +++ b/packages/client-common/__tests__/utils/sleep.ts @@ -1,5 +1,5 @@ export function sleep(ms: number): Promise { return new Promise((resolve) => { - setTimeout(resolve, ms) - }) + setTimeout(resolve, ms); + }); } diff --git a/packages/client-common/__tests__/utils/test_connection_type.ts b/packages/client-common/__tests__/utils/test_connection_type.ts index 9a0fad79..4935d0a6 100644 --- a/packages/client-common/__tests__/utils/test_connection_type.ts +++ b/packages/client-common/__tests__/utils/test_connection_type.ts @@ -1,25 +1,25 @@ export const TestConnectionType = { - Node: 'node', - Browser: 'browser', -} as const + Node: "node", + Browser: "browser", +} as const; export type TestConnectionType = - (typeof TestConnectionType)[keyof typeof TestConnectionType] + (typeof TestConnectionType)[keyof typeof TestConnectionType]; export function getTestConnectionType(): TestConnectionType { - let connectionType - switch (process.env['CLICKHOUSE_TEST_CONNECTION_TYPE']) { - case 'browser': - connectionType = TestConnectionType.Browser - break - case 'node': + let connectionType; + switch (process.env["CLICKHOUSE_TEST_CONNECTION_TYPE"]) { + case "browser": + connectionType = TestConnectionType.Browser; + break; + case "node": case undefined: - connectionType = TestConnectionType.Node - break + connectionType = TestConnectionType.Node; + break; default: throw new Error( - 'Unexpected CLICKHOUSE_TEST_CONNECTION_TYPE value. ' + - 'Possible options: `node`, `browser` ' + - 'or keep it unset to fall back to `node`', - ) + "Unexpected CLICKHOUSE_TEST_CONNECTION_TYPE value. " + + "Possible options: `node`, `browser` " + + "or keep it unset to fall back to `node`", + ); } - return connectionType + return connectionType; } diff --git a/packages/client-common/__tests__/utils/test_env.ts b/packages/client-common/__tests__/utils/test_env.ts index 9deff646..202f560c 100644 --- a/packages/client-common/__tests__/utils/test_env.ts +++ b/packages/client-common/__tests__/utils/test_env.ts @@ -1,48 +1,48 @@ export const TestEnv = { - Cloud: 'cloud', - LocalSingleNode: 'local_single_node', - LocalCluster: 'local_cluster', -} as const -export type TestEnv = (typeof TestEnv)[keyof typeof TestEnv] + Cloud: "cloud", + LocalSingleNode: "local_single_node", + LocalCluster: "local_cluster", +} as const; +export type TestEnv = (typeof TestEnv)[keyof typeof TestEnv]; export function getClickHouseTestEnvironment(): TestEnv { - let env - const value = process.env['CLICKHOUSE_TEST_ENVIRONMENT'] + let env; + const value = process.env["CLICKHOUSE_TEST_ENVIRONMENT"]; switch (value) { - case 'cloud': - env = TestEnv.Cloud - break - case 'local_cluster': - env = TestEnv.LocalCluster - break - case 'local_single_node': - case 'undefined': + case "cloud": + env = TestEnv.Cloud; + break; + case "local_cluster": + env = TestEnv.LocalCluster; + break; + case "local_single_node": + case "undefined": case undefined: - env = TestEnv.LocalSingleNode - break + env = TestEnv.LocalSingleNode; + break; default: throw new Error( `Unexpected CLICKHOUSE_TEST_ENVIRONMENT value: ${value}. ` + - 'Possible options: `local_single_node`, `local_cluster`, `cloud`. ' + - 'You can keep it unset to fall back to `local_single_node`', - ) + "Possible options: `local_single_node`, `local_cluster`, `cloud`. " + + "You can keep it unset to fall back to `local_single_node`", + ); } - return env + return env; } export function isCloudTestEnv(): boolean { - const env = getClickHouseTestEnvironment() - return env === TestEnv.Cloud + const env = getClickHouseTestEnvironment(); + return env === TestEnv.Cloud; } export function isOnEnv(...envs: TestEnv[]): boolean { - const env = getClickHouseTestEnvironment() - return envs.includes(env) + const env = getClickHouseTestEnvironment(); + return envs.includes(env); } function isEnvVarEnabled(key: string): boolean { - return process.env[key] === '1' + return process.env[key] === "1"; } -export const SKIP_INIT = isEnvVarEnabled('CLICKHOUSE_TEST_SKIP_INIT') -export const PRINT_DDL = isEnvVarEnabled('CLICKHOUSE_TEST_PRINT_DDL') +export const SKIP_INIT = isEnvVarEnabled("CLICKHOUSE_TEST_SKIP_INIT"); +export const PRINT_DDL = isEnvVarEnabled("CLICKHOUSE_TEST_PRINT_DDL"); diff --git a/packages/client-common/__tests__/utils/test_logger.ts b/packages/client-common/__tests__/utils/test_logger.ts index de0abe53..0ca6f9e5 100644 --- a/packages/client-common/__tests__/utils/test_logger.ts +++ b/packages/client-common/__tests__/utils/test_logger.ts @@ -2,27 +2,27 @@ import type { ErrorLogParams, Logger, LogParams, -} from '@clickhouse/client-common' +} from "@clickhouse/client-common"; export class TestLogger implements Logger { trace({ module, message, args }: LogParams) { - console.log(formatMessage({ level: 'TRACE', module, message }), args || '') + console.log(formatMessage({ level: "TRACE", module, message }), args || ""); } debug({ module, message, args }: LogParams) { - console.log(formatMessage({ level: 'DEBUG', module, message }), args || '') + console.log(formatMessage({ level: "DEBUG", module, message }), args || ""); } info({ module, message, args }: LogParams) { - console.log(formatMessage({ level: 'INFO', module, message }), args || '') + console.log(formatMessage({ level: "INFO", module, message }), args || ""); } warn({ module, message, args }: LogParams) { - console.log(formatMessage({ level: 'WARN', module, message }), args || '') + console.log(formatMessage({ level: "WARN", module, message }), args || ""); } error({ module, message, args, err }: ErrorLogParams) { console.error( - formatMessage({ level: 'ERROR', module, message }), - args || '', + formatMessage({ level: "ERROR", module, message }), + args || "", err, - ) + ); } } @@ -31,9 +31,9 @@ function formatMessage({ module, message, }: { - level: string - module: string - message: string + level: string; + module: string; + message: string; }): string { - return `[${level}][${module}] ${message}` + return `[${level}][${module}] ${message}`; } diff --git a/packages/client-common/eslint.config.mjs b/packages/client-common/eslint.config.mjs index 82818f89..578dc9cf 100644 --- a/packages/client-common/eslint.config.mjs +++ b/packages/client-common/eslint.config.mjs @@ -1,7 +1,7 @@ -import js from '@eslint/js' -import { defineConfig } from 'eslint/config' -import tseslint from 'typescript-eslint' -import { typescriptEslintConfig } from '../../eslint.config.base.mjs' +import js from "@eslint/js"; +import { defineConfig } from "eslint/config"; +import tseslint from "typescript-eslint"; +import { typescriptEslintConfig } from "../../eslint.config.base.mjs"; export default defineConfig( // Base ESLint recommended rules @@ -13,15 +13,15 @@ export default defineConfig( // Ignore build artifacts and externals { ignores: [ - './__tests__/**/*.ts', - 'eslint.config.mjs', - 'vitest.*.config.ts', - 'vitest.*.setup.ts', - 'coverage', - 'out', - 'dist', - 'node_modules', - 'webpack', + "./__tests__/**/*.ts", + "eslint.config.mjs", + "vitest.*.config.ts", + "vitest.*.setup.ts", + "coverage", + "out", + "dist", + "node_modules", + "webpack", ], }, -) +); diff --git a/packages/client-common/src/clickhouse_types.ts b/packages/client-common/src/clickhouse_types.ts index 70ad67dc..f8cc8d93 100644 --- a/packages/client-common/src/clickhouse_types.ts +++ b/packages/client-common/src/clickhouse_types.ts @@ -1,59 +1,59 @@ export interface ResponseJSON { - data: Array - query_id?: string - totals?: T - extremes?: Record + data: Array; + query_id?: string; + totals?: T; + extremes?: Record; // # Supported only by responses in JSON, XML. // # Otherwise, it can be read from x-clickhouse-summary header - meta?: Array<{ name: string; type: string }> - statistics?: { elapsed: number; rows_read: number; bytes_read: number } - rows?: number - rows_before_limit_at_least?: number + meta?: Array<{ name: string; type: string }>; + statistics?: { elapsed: number; rows_read: number; bytes_read: number }; + rows?: number; + rows_before_limit_at_least?: number; } export interface InputJSON { - meta: { name: string; type: string }[] - data: T[] + meta: { name: string; type: string }[]; + data: T[]; } -export type InputJSONObjectEachRow = Record +export type InputJSONObjectEachRow = Record; export interface ClickHouseSummary { - read_rows: string - read_bytes: string - written_rows: string - written_bytes: string - total_rows_to_read: string - result_rows: string - result_bytes: string - elapsed_ns: string + read_rows: string; + read_bytes: string; + written_rows: string; + written_bytes: string; + total_rows_to_read: string; + result_rows: string; + result_bytes: string; + elapsed_ns: string; /** Available only after ClickHouse 24.9 */ - real_time_microseconds?: string + real_time_microseconds?: string; } -export type ResponseHeaders = Record +export type ResponseHeaders = Record; export interface WithClickHouseSummary { - summary?: ClickHouseSummary + summary?: ClickHouseSummary; } export interface WithResponseHeaders { - response_headers: ResponseHeaders + response_headers: ResponseHeaders; } export interface WithHttpStatusCode { - http_status_code?: number + http_status_code?: number; } export interface ClickHouseProgress { - read_rows: string - read_bytes: string - elapsed_ns: string - total_rows_to_read?: string + read_rows: string; + read_bytes: string; + elapsed_ns: string; + total_rows_to_read?: string; } export interface ProgressRow { - progress: ClickHouseProgress + progress: ClickHouseProgress; } export type SpecialEventRow = @@ -63,37 +63,37 @@ export type SpecialEventRow = | { max: T } | { rows_before_limit_at_least: number | string } | { rows_before_aggregation: number | string } - | { exception: string } + | { exception: string }; export type InsertValues = | ReadonlyArray | Stream | InputJSON - | InputJSONObjectEachRow + | InputJSONObjectEachRow; -export type NonEmptyArray = [T, ...T[]] +export type NonEmptyArray = [T, ...T[]]; export interface ClickHouseCredentialsAuth { - username?: string - password?: string + username?: string; + password?: string; } /** Supported in ClickHouse Cloud only */ export interface ClickHouseJWTAuth { - access_token: string + access_token: string; } -export type ClickHouseAuth = ClickHouseCredentialsAuth | ClickHouseJWTAuth +export type ClickHouseAuth = ClickHouseCredentialsAuth | ClickHouseJWTAuth; /** Type guard to use with `JSONEachRowWithProgress`, checking if the emitted row is a progress row. * @see https://clickhouse.com/docs/interfaces/formats/JSONEachRowWithProgress */ export function isProgressRow(row: unknown): row is ProgressRow { return ( row !== null && - typeof row === 'object' && - 'progress' in row && + typeof row === "object" && + "progress" in row && Object.keys(row).length === 1 - ) + ); } /** Type guard to use with `JSONEachRowWithProgress`, checking if the emitted row is a row with data. @@ -101,10 +101,10 @@ export function isProgressRow(row: unknown): row is ProgressRow { export function isRow(row: unknown): row is { row: T } { return ( row !== null && - typeof row === 'object' && - 'row' in row && + typeof row === "object" && + "row" in row && Object.keys(row).length === 1 - ) + ); } /** Type guard to use with `JSONEachRowWithProgress`, checking if the row contains an exception. @@ -112,8 +112,8 @@ export function isRow(row: unknown): row is { row: T } { export function isException(row: unknown): row is { exception: string } { return ( row !== null && - typeof row === 'object' && - 'exception' in row && + typeof row === "object" && + "exception" in row && Object.keys(row).length === 1 - ) + ); } diff --git a/packages/client-common/src/client.ts b/packages/client-common/src/client.ts index b72b2535..6ccb678e 100644 --- a/packages/client-common/src/client.ts +++ b/packages/client-common/src/client.ts @@ -9,64 +9,64 @@ import type { WithClickHouseSummary, WithResponseHeaders, DataFormat, -} from './index' -import { defaultJSONHandling } from './parse' -import { DefaultLogger, ClickHouseLogLevel } from './logger' +} from "./index"; +import { defaultJSONHandling } from "./parse"; +import { DefaultLogger, ClickHouseLogLevel } from "./logger"; import type { InsertValues, NonEmptyArray, WithHttpStatusCode, -} from './clickhouse_types' -import type { ImplementationDetails, ValuesEncoder } from './config' -import { getConnectionParams, prepareConfigWithURL } from './config' -import type { ConnPingResult } from './connection' -import type { JSONHandling } from './parse/json_handling' -import type { BaseResultSet } from './result' +} from "./clickhouse_types"; +import type { ImplementationDetails, ValuesEncoder } from "./config"; +import { getConnectionParams, prepareConfigWithURL } from "./config"; +import type { ConnPingResult } from "./connection"; +import type { JSONHandling } from "./parse/json_handling"; +import type { BaseResultSet } from "./result"; export interface BaseQueryParams { /** ClickHouse's settings that can be applied on query level. */ - clickhouse_settings?: ClickHouseSettings + clickhouse_settings?: ClickHouseSettings; /** Parameters for query binding. https://clickhouse.com/docs/en/interfaces/http/#cli-queries-with-parameters */ - query_params?: Record + query_params?: Record; /** AbortSignal instance to cancel a request in progress. */ - abort_signal?: AbortSignal + abort_signal?: AbortSignal; /** A specific `query_id` that will be sent with this request. * If it is not set, a random identifier will be generated automatically by the client. */ - query_id?: string + query_id?: string; /** A specific ClickHouse Session id for this query. * If it is not set, {@link BaseClickHouseClientConfigOptions.session_id} will be used. * @default undefined (no override) */ - session_id?: string + session_id?: string; /** A specific list of roles to use for this query. * If it is not set, {@link BaseClickHouseClientConfigOptions.role} will be used. * @default undefined (no override) */ - role?: string | Array + role?: string | Array; /** When defined, overrides {@link BaseClickHouseClientConfigOptions.auth} for this particular request. * @default undefined (no override) */ auth?: | { - username: string - password: string + username: string; + password: string; } - | { access_token: string } + | { access_token: string }; /** Additional HTTP headers to attach to this particular request. * Overrides the headers set in {@link BaseClickHouseClientConfigOptions.http_headers}. * @default empty object */ - http_headers?: Record + http_headers?: Record; } export interface QueryParams extends BaseQueryParams { /** Statement to execute. */ - query: string + query: string; /** Format of the resulting dataset. */ - format?: DataFormat + format?: DataFormat; } /** Same parameters as {@link QueryParams}, but with `format` field as a type */ export type QueryParamsWithFormat = Omit< QueryParams, - 'format' -> & { format?: Format } + "format" +> & { format?: Format }; /** If the Format is not a literal type, fall back to the default behavior of the ResultSet, * allowing to call all methods with all data shapes variants, @@ -74,13 +74,13 @@ export type QueryParamsWithFormat = Omit< export type QueryResult = IsSame extends true ? BaseResultSet - : BaseResultSet + : BaseResultSet; export type ExecParams = BaseQueryParams & { /** Statement to execute (including the FORMAT clause). By default, the query will be sent in the request body; * If {@link ExecParamsWithValues.values} are defined, the query is sent as a request parameter, * and the values are sent in the request body instead. */ - query: string + query: string; /** If set to `false`, the client _will not_ decompress the response stream, even if the response compression * was requested by the client via the {@link BaseClickHouseClientConfigOptions.compression.response } setting. * This could be useful if the response stream is passed to another application as-is, @@ -88,7 +88,7 @@ export type ExecParams = BaseQueryParams & { * @note 1) Node.js only. This setting will have no effect on the Web version. * @note 2) In case of an error, the stream will be decompressed anyway, regardless of this setting. * @default true */ - decompress_response_stream?: boolean + decompress_response_stream?: boolean; /** * If set to `true`, the client will ignore error responses from the server and return them as-is in the response stream. * This could be useful if you want to handle error responses manually. @@ -97,8 +97,8 @@ export type ExecParams = BaseQueryParams & { * is received. This includes decompressing the error response stream if it is compressed. * @default false */ - ignore_error_response?: boolean -} + ignore_error_response?: boolean; +}; export type ExecParamsWithValues = ExecParams & { /** If you have a custom INSERT statement to run with `exec`, the data from this stream will be inserted. * @@ -106,13 +106,13 @@ export type ExecParamsWithValues = ExecParams & { * used in {@link ExecParams.query} in this case. * * @see https://clickhouse.com/docs/en/interfaces/formats */ - values: Stream -} + values: Stream; +}; -export type CommandParams = ExecParams +export type CommandParams = ExecParams; export type CommandResult = { query_id: string } & WithClickHouseSummary & WithResponseHeaders & - WithHttpStatusCode + WithHttpStatusCode; export type InsertResult = { /** @@ -121,21 +121,21 @@ export type InsertResult = { * For example, if {@link InsertParams.values} was an empty array, * the client does not send any requests to the server, and {@link executed} is false. */ - executed: boolean + executed: boolean; /** * Empty string if {@link executed} is false. * Otherwise, either {@link InsertParams.query_id} if it was set, or the id that was generated by the client. */ - query_id: string + query_id: string; } & WithClickHouseSummary & WithResponseHeaders & - WithHttpStatusCode + WithHttpStatusCode; -export type ExecResult = ConnExecResult +export type ExecResult = ConnExecResult; /** {@link except} field contains a non-empty list of columns to exclude when generating `(* EXCEPT (...))` clause */ export interface InsertColumnsExcept { - except: NonEmptyArray + except: NonEmptyArray; } export interface InsertParams< @@ -143,11 +143,11 @@ export interface InsertParams< T = unknown, > extends BaseQueryParams { /** Name of a table to insert into. */ - table: string + table: string; /** A dataset to insert. */ - values: InsertValues + values: InsertValues; /** Format of the dataset to insert. Default: `JSONCompactEachRow` */ - format?: DataFormat + format?: DataFormat; /** * Allows specifying which columns the data will be inserted into. * Accepts either an array of strings (column names) or an object of {@link InsertColumnsExcept} type. @@ -160,64 +160,64 @@ export interface InsertParams< * and the generated statement will be: `INSERT INTO table FORMAT DataFormat`. * * See also: https://clickhouse.com/docs/en/sql-reference/statements/insert-into */ - columns?: NonEmptyArray | InsertColumnsExcept + columns?: NonEmptyArray | InsertColumnsExcept; } /** Parameters for the health-check request - using the built-in `/ping` endpoint. * This is the default behavior for the Node.js version. */ export type PingParamsWithEndpoint = { select: false } & Pick< BaseQueryParams, - 'abort_signal' | 'http_headers' -> + "abort_signal" | "http_headers" +>; /** Parameters for the health-check request - using a SELECT query. * This is the default behavior for the Web version, as the `/ping` endpoint does not support CORS. * Most of the standard `query` method params, e.g., `query_id`, `abort_signal`, `http_headers`, etc. will work, * except for `query_params`, which does not make sense to allow in this method. */ export type PingParamsWithSelectQuery = { select: true } & Omit< BaseQueryParams, - 'query_params' -> -export type PingParams = PingParamsWithEndpoint | PingParamsWithSelectQuery -export type PingResult = ConnPingResult + "query_params" +>; +export type PingParams = PingParamsWithEndpoint | PingParamsWithSelectQuery; +export type PingResult = ConnPingResult; export class ClickHouseClient { - private readonly clientClickHouseSettings: ClickHouseSettings - private readonly connectionParams: ConnectionParams - private readonly connection: Connection - private readonly makeResultSet: MakeResultSet - private readonly valuesEncoder: ValuesEncoder - private readonly sessionId?: string - private readonly role?: string | Array - private readonly jsonHandling: JSONHandling + private readonly clientClickHouseSettings: ClickHouseSettings; + private readonly connectionParams: ConnectionParams; + private readonly connection: Connection; + private readonly makeResultSet: MakeResultSet; + private readonly valuesEncoder: ValuesEncoder; + private readonly sessionId?: string; + private readonly role?: string | Array; + private readonly jsonHandling: JSONHandling; constructor( config: BaseClickHouseClientConfigOptions & ImplementationDetails, ) { const logger = config?.log?.LoggerClass ? new config.log.LoggerClass() - : new DefaultLogger() + : new DefaultLogger(); const configWithURL = prepareConfigWithURL( config, logger, config.impl.handle_specific_url_params ?? null, - ) - this.connectionParams = getConnectionParams(configWithURL, logger) - this.clientClickHouseSettings = this.connectionParams.clickhouse_settings - this.sessionId = config.session_id - this.role = config.role + ); + this.connectionParams = getConnectionParams(configWithURL, logger); + this.clientClickHouseSettings = this.connectionParams.clickhouse_settings; + this.sessionId = config.session_id; + this.role = config.role; this.connection = config.impl.make_connection( configWithURL, this.connectionParams, - ) + ); // Using the connection params log level as it does the parsing. // TODO: it would be better to parse the log level in the client itself. - this.makeResultSet = config.impl.make_result_set + this.makeResultSet = config.impl.make_result_set; this.jsonHandling = { ...defaultJSONHandling, ...config.json, - } + }; - this.valuesEncoder = config.impl.values_encoder(this.jsonHandling) + this.valuesEncoder = config.impl.values_encoder(this.jsonHandling); } /** @@ -237,17 +237,17 @@ export class ClickHouseClient { * * See {@link DataFormat} for the formats supported by the client. */ - async query( + async query( params: QueryParamsWithFormat, ): Promise> { - const format = params.format ?? 'JSON' - const query = formatQuery(params.query, format) - const queryParams = this.withClientQueryParams(params) + const format = params.format ?? "JSON"; + const query = formatQuery(params.query, format); + const queryParams = this.withClientQueryParams(params); const { stream, query_id, response_headers } = await this.connection.query({ query, ...queryParams, - }) - const { log_writer, log_level } = this.connectionParams + }); + const { log_writer, log_level } = this.connectionParams; return this.makeResultSet( stream, format, @@ -256,19 +256,19 @@ export class ClickHouseClient { if (log_level <= ClickHouseLogLevel.ERROR) { log_writer.error({ err, - module: 'Client', - message: 'Error while processing the ResultSet.', + module: "Client", + message: "Error while processing the ResultSet.", args: { session_id: queryParams.session_id, role: queryParams.role, query_id, }, - }) + }); } }, response_headers, this.jsonHandling, - ) + ); } /** @@ -281,14 +281,14 @@ export class ClickHouseClient { * and you are interested in the response data, consider using {@link ClickHouseClient.exec}. */ async command(params: CommandParams): Promise { - const query = removeTrailingSemi(params.query.trim()) - const ignore_error_response = params.ignore_error_response ?? false - const queryParams = this.withClientQueryParams(params) + const query = removeTrailingSemi(params.query.trim()); + const ignore_error_response = params.ignore_error_response ?? false; + const queryParams = this.withClientQueryParams(params); return await this.connection.command({ query, ignore_error_response, ...queryParams, - }) + }); } /** @@ -302,18 +302,19 @@ export class ClickHouseClient { async exec( params: ExecParams | ExecParamsWithValues, ): Promise> { - const query = removeTrailingSemi(params.query.trim()) - const values = 'values' in params ? params.values : undefined - const decompress_response_stream = params.decompress_response_stream ?? true - const ignore_error_response = params.ignore_error_response ?? false - const queryParams = this.withClientQueryParams(params) + const query = removeTrailingSemi(params.query.trim()); + const values = "values" in params ? params.values : undefined; + const decompress_response_stream = + params.decompress_response_stream ?? true; + const ignore_error_response = params.ignore_error_response ?? false; + const queryParams = this.withClientQueryParams(params); return await this.connection.exec({ query, values, decompress_response_stream, ignore_error_response, ...queryParams, - }) + }); } /** @@ -327,20 +328,20 @@ export class ClickHouseClient { */ async insert(params: InsertParams): Promise { if (Array.isArray(params.values) && params.values.length === 0) { - return { executed: false, query_id: '', response_headers: {} } + return { executed: false, query_id: "", response_headers: {} }; } - const format = params.format || 'JSONCompactEachRow' - this.valuesEncoder.validateInsertValues(params.values, format) + const format = params.format || "JSONCompactEachRow"; + this.valuesEncoder.validateInsertValues(params.values, format); - const query = getInsertQuery(params, format) - const queryParams = this.withClientQueryParams(params) + const query = getInsertQuery(params, format); + const queryParams = this.withClientQueryParams(params); const result = await this.connection.insert({ query, values: this.valuesEncoder.encodeValues(params.values, format), ...queryParams, - }) - return { ...result, executed: true } + }); + return { ...result, executed: true }; } /** @@ -353,7 +354,7 @@ export class ClickHouseClient { * **NOTE**: Since the `/ping` endpoint does not support CORS, the Web version always uses a `SELECT` query. */ async ping(params?: PingParams): Promise { - return await this.connection.ping(params ?? { select: false }) + return await this.connection.ping(params ?? { select: false }); } /** @@ -362,7 +363,7 @@ export class ClickHouseClient { * for example, during the graceful shutdown phase. */ async close(): Promise { - return await this.connection.close() + return await this.connection.close(); } /** @@ -373,7 +374,7 @@ export class ClickHouseClient { * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/using */ async [Symbol.asyncDispose]() { - await this.close() + await this.close(); } private withClientQueryParams(params: BaseQueryParams): BaseQueryParams { @@ -389,54 +390,54 @@ export class ClickHouseClient { role: params.role ?? this.role, auth: params.auth, http_headers: params.http_headers, - } + }; } } function formatQuery(query: string, format: DataFormat): string { - query = query.trim() - query = removeTrailingSemi(query) - return query + ' \nFORMAT ' + format + query = query.trim(); + query = removeTrailingSemi(query); + return query + " \nFORMAT " + format; } function removeTrailingSemi(query: string) { - let lastNonSemiIdx = query.length + let lastNonSemiIdx = query.length; for (let i = lastNonSemiIdx; i > 0; i--) { - if (query[i - 1] !== ';') { - lastNonSemiIdx = i - break + if (query[i - 1] !== ";") { + lastNonSemiIdx = i; + break; } } if (lastNonSemiIdx !== query.length) { - return query.slice(0, lastNonSemiIdx) + return query.slice(0, lastNonSemiIdx); } - return query + return query; } function isInsertColumnsExcept(obj: unknown): obj is InsertColumnsExcept { return ( obj !== undefined && obj !== null && - typeof obj === 'object' && + typeof obj === "object" && // Avoiding ESLint no-prototype-builtins error - Object.prototype.hasOwnProperty.call(obj, 'except') - ) + Object.prototype.hasOwnProperty.call(obj, "except") + ); } function getInsertQuery( params: InsertParams, format: DataFormat, ): string { - let columnsPart = '' + let columnsPart = ""; if (params.columns !== undefined) { if (Array.isArray(params.columns) && params.columns.length > 0) { - columnsPart = ` (${params.columns.join(', ')})` + columnsPart = ` (${params.columns.join(", ")})`; } else if ( isInsertColumnsExcept(params.columns) && params.columns.except.length > 0 ) { - columnsPart = ` (* EXCEPT (${params.columns.except.join(', ')}))` + columnsPart = ` (* EXCEPT (${params.columns.except.join(", ")}))`; } } - return `INSERT INTO ${params.table.trim()}${columnsPart} FORMAT ${format}` + return `INSERT INTO ${params.table.trim()}${columnsPart} FORMAT ${format}`; } diff --git a/packages/client-common/src/config.ts b/packages/client-common/src/config.ts index 70229ea4..78b26a40 100644 --- a/packages/client-common/src/config.ts +++ b/packages/client-common/src/config.ts @@ -1,104 +1,104 @@ -import type { InsertValues, ResponseHeaders } from './clickhouse_types' -import type { Connection, ConnectionParams } from './connection' -import type { DataFormat } from './data_formatter' -import type { Logger } from './logger' -import { ClickHouseLogLevel, LogWriter } from './logger' -import { defaultJSONHandling, type JSONHandling } from './parse/json_handling' -import type { BaseResultSet } from './result' -import type { ClickHouseSettings } from './settings' +import type { InsertValues, ResponseHeaders } from "./clickhouse_types"; +import type { Connection, ConnectionParams } from "./connection"; +import type { DataFormat } from "./data_formatter"; +import type { Logger } from "./logger"; +import { ClickHouseLogLevel, LogWriter } from "./logger"; +import { defaultJSONHandling, type JSONHandling } from "./parse/json_handling"; +import type { BaseResultSet } from "./result"; +import type { ClickHouseSettings } from "./settings"; export interface BaseClickHouseClientConfigOptions { /** @deprecated since version 1.0.0. Use {@link url} instead.
* A ClickHouse instance URL. * @default http://localhost:8123 */ - host?: string + host?: string; /** A ClickHouse instance URL. * @default http://localhost:8123 */ - url?: string | URL + url?: string | URL; /** An optional pathname to add to the ClickHouse URL after it is parsed by the client. * For example, if you use a proxy, and your ClickHouse instance can be accessed as http://proxy:8123/clickhouse_server, * specify `clickhouse_server` here (with or without a leading slash); * otherwise, if provided directly in the {@link url}, it will be considered as the `database` option.
* Multiple segments are supported, e.g. `/my_proxy/db`. * @default empty string */ - pathname?: string + pathname?: string; /** The request timeout in milliseconds. * @default 30_000 */ - request_timeout?: number + request_timeout?: number; /** Maximum number of sockets to allow per host. * @default 10 */ - max_open_connections?: number + max_open_connections?: number; /** Request and response compression settings. */ compression?: { /** `response: true` instructs ClickHouse server to respond with compressed response body.
* This will add `Accept-Encoding: gzip` header in the request and `enable_http_compression=1` ClickHouse HTTP setting. *

Warning: Response compression can't be enabled for a user with readonly=1, as ClickHouse will not allow settings modifications for such user.

* @default false */ - response?: boolean + response?: boolean; /** `request: true` enabled compression on the client request body. * @default false */ - request?: boolean - } + request?: boolean; + }; /** The name of the user on whose behalf requests are made. * Should not be set if {@link access_token} is provided. * @default default */ - username?: string + username?: string; /** The user password. * Should not be set if {@link access_token} is provided. * @default empty string */ - password?: string + password?: string; /** A JWT access token to authenticate with ClickHouse. * JWT token authentication is supported in ClickHouse Cloud only. * Should not be set if {@link username} or {@link password} are provided. * @default empty */ - access_token?: string + access_token?: string; /** The name of the application using the JS client. * @default empty string */ - application?: string + application?: string; /** Database name to use. * @default default */ - database?: string + database?: string; /** ClickHouse settings to apply to all requests. * @default empty object */ - clickhouse_settings?: ClickHouseSettings + clickhouse_settings?: ClickHouseSettings; log?: { /** A class to instantiate a custom logger implementation. * @default see {@link DefaultLogger} */ - LoggerClass?: new () => Logger + LoggerClass?: new () => Logger; /** @default set to {@link ClickHouseLogLevel.WARN} */ - level?: ClickHouseLogLevel - } + level?: ClickHouseLogLevel; + }; /** ClickHouse Session id to attach to the outgoing requests. * @default empty string (no session) */ - session_id?: string + session_id?: string; /** ClickHouse role name(s) to attach to the outgoing requests. * @default undefined string (no roles) */ - role?: string | Array + role?: string | Array; /** @deprecated since version 1.0.0. Use {@link http_headers} instead.
* Additional HTTP headers to attach to the outgoing requests. * @default empty object */ - additional_headers?: Record + additional_headers?: Record; /** Additional HTTP headers to attach to the outgoing requests. * @default empty object */ - http_headers?: Record + http_headers?: Record; /** HTTP Keep-Alive related settings. */ keep_alive?: { /** Enable or disable HTTP Keep-Alive mechanism. * @default true */ - enabled?: boolean - } + enabled?: boolean; + }; /** * Custom parsing when handling with JSON objects * * Defaults to using standard `JSON.parse` and `JSON.stringify` */ - json?: Partial + json?: Partial; } export type MakeConnection< Stream, Config = BaseClickHouseClientConfigOptionsWithURL, -> = (config: Config, params: ConnectionParams) => Connection +> = (config: Config, params: ConnectionParams) => Connection; export type MakeResultSet = < Format extends DataFormat, @@ -110,17 +110,17 @@ export type MakeResultSet = < log_error: (err: Error) => void, response_headers: ResponseHeaders, jsonHandling: JSONHandling, -) => ResultSet +) => ResultSet; export type MakeValuesEncoder = ( jsonHandling: JSONHandling, -) => ValuesEncoder +) => ValuesEncoder; export interface ValuesEncoder { validateInsertValues( values: InsertValues, format: DataFormat, - ): void + ): void; /** * A function encodes an array or a stream of JSON objects to a format compatible with ClickHouse. @@ -134,7 +134,7 @@ export interface ValuesEncoder { encodeValues( values: InsertValues, format: DataFormat, - ): string | Stream + ): string | Stream; } /** @@ -148,30 +148,30 @@ export type HandleImplSpecificURLParams = ( config: BaseClickHouseClientConfigOptions, url: URL, ) => { - config: BaseClickHouseClientConfigOptions + config: BaseClickHouseClientConfigOptions; // params that were handled in the implementation; used to calculate final "unknown" URL params // i.e. common package does not know about Node.js-specific ones, // but after handling we will be able to remove them from the final unknown set (and not throw). - handled_params: Set + handled_params: Set; // params that are still unknown even in the implementation - unknown_params: Set -} + unknown_params: Set; +}; /** Things that may vary between Web/Node.js/etc client implementations. */ export interface ImplementationDetails { impl: { - make_connection: MakeConnection - make_result_set: MakeResultSet - values_encoder: MakeValuesEncoder - handle_specific_url_params?: HandleImplSpecificURLParams - } + make_connection: MakeConnection; + make_result_set: MakeResultSet; + values_encoder: MakeValuesEncoder; + handle_specific_url_params?: HandleImplSpecificURLParams; + }; } // Configuration with parameters parsed from the URL, and the URL itself normalized for the connection. export type BaseClickHouseClientConfigOptionsWithURL = Omit< BaseClickHouseClientConfigOptions, - 'url' -> & { url: URL } // not string and not undefined + "url" +> & { url: URL }; // not string and not undefined /** * Validates and normalizes the provided "base" config. @@ -186,83 +186,83 @@ export function prepareConfigWithURL( logger: Logger, handleImplURLParams: HandleImplSpecificURLParams | null, ): BaseClickHouseClientConfigOptionsWithURL { - const baseConfig = { ...baseConfigOptions } + const baseConfig = { ...baseConfigOptions }; if (baseConfig.additional_headers !== undefined) { logger.warn({ - module: 'Config', + module: "Config", message: '"additional_headers" is deprecated. Use "http_headers" instead.', - }) - baseConfig.http_headers = baseConfig.additional_headers - delete baseConfig.additional_headers + }); + baseConfig.http_headers = baseConfig.additional_headers; + delete baseConfig.additional_headers; } - let configURL + let configURL; if (baseConfig.host !== undefined) { logger.warn({ - module: 'Config', + module: "Config", message: '"host" is deprecated. Use "url" instead.', - }) - configURL = createUrl(baseConfig.host) - delete baseConfig.host + }); + configURL = createUrl(baseConfig.host); + delete baseConfig.host; } else { - configURL = createUrl(baseConfig.url) + configURL = createUrl(baseConfig.url); } const [url, configFromURL] = loadConfigOptionsFromURL( configURL, handleImplURLParams, - ) - const config = mergeConfigs(baseConfig, configFromURL, logger) + ); + const config = mergeConfigs(baseConfig, configFromURL, logger); if (config.pathname !== undefined) { - url.pathname = config.pathname + url.pathname = config.pathname; } - config.url = url - return config as BaseClickHouseClientConfigOptionsWithURL + config.url = url; + return config as BaseClickHouseClientConfigOptionsWithURL; } export function getConnectionParams( config: BaseClickHouseClientConfigOptionsWithURL, logger: Logger, ): ConnectionParams { - let auth: ConnectionParams['auth'] + let auth: ConnectionParams["auth"]; if (config.access_token !== undefined) { if (config.username !== undefined || config.password !== undefined) { throw new Error( - 'Both access token and username/password are provided in the configuration. Please use only one authentication method.', - ) + "Both access token and username/password are provided in the configuration. Please use only one authentication method.", + ); } - auth = { access_token: config.access_token, type: 'JWT' } + auth = { access_token: config.access_token, type: "JWT" }; } else { auth = { - username: config.username ?? 'default', - password: config.password ?? '', - type: 'Credentials', - } + username: config.username ?? "default", + password: config.password ?? "", + type: "Credentials", + }; } - const log_level = config.log?.level ?? ClickHouseLogLevel.WARN - const request_timeout = config.request_timeout ?? 30_000 - const clickhouse_settings = config.clickhouse_settings ?? {} + const log_level = config.log?.level ?? ClickHouseLogLevel.WARN; + const request_timeout = config.request_timeout ?? 30_000; + const clickhouse_settings = config.clickhouse_settings ?? {}; if (log_level <= ClickHouseLogLevel.WARN) { // Warn if request_timeout is high but progress headers are not configured // This can lead to socket hang-up errors when long-running queries exceed load balancer idle timeouts - const THRESHOLD_MS = 60_000 // 60 seconds + const THRESHOLD_MS = 60_000; // 60 seconds if (request_timeout > THRESHOLD_MS) { const send_progress = - String(clickhouse_settings.send_progress_in_http_headers) === '1' + String(clickhouse_settings.send_progress_in_http_headers) === "1"; const progress_interval = - clickhouse_settings.http_headers_progress_interval_ms + clickhouse_settings.http_headers_progress_interval_ms; if (!send_progress) { logger.warn({ - module: 'Config', + module: "Config", message: `request_timeout is set to ${request_timeout}ms, but send_progress_in_http_headers is not enabled. Long-running queries may fail with socket hang-up errors if they exceed the load balancer idle timeout. Consider enabling progress headers with clickhouse_settings: { send_progress_in_http_headers: 1, http_headers_progress_interval_ms: '' }. See https://github.com/ClickHouse/clickhouse-js/blob/main/docs/howto/long_running_queries.md for more details.`, - }) + }); } else if (progress_interval === undefined) { logger.warn({ - module: 'Config', + module: "Config", message: `request_timeout is set to ${request_timeout}ms and send_progress_in_http_headers is enabled, but http_headers_progress_interval_ms is not set. It is recommended to set http_headers_progress_interval_ms to a value slightly below your load balancer's idle timeout (e.g., '110000' for a 120s LB timeout). See https://github.com/ClickHouse/clickhouse-js/blob/main/docs/howto/long_running_queries.md for more details.`, - }) + }); } } } @@ -277,8 +277,8 @@ export function getConnectionParams( decompress_response: config.compression?.response ?? false, compress_request: config.compression?.request ?? false, }, - database: config.database ?? 'default', - log_writer: new LogWriter(logger, 'Connection', log_level), + database: config.database ?? "default", + log_writer: new LogWriter(logger, "Connection", log_level), log_level: log_level, keep_alive: { enabled: config.keep_alive?.enabled ?? true }, clickhouse_settings, @@ -287,7 +287,7 @@ export function getConnectionParams( ...defaultJSONHandling, ...config.json, }, - } + }; } /** @@ -306,54 +306,54 @@ export function mergeConfigs( path: string[] = [], ) { for (const key of Object.keys(fromURL)) { - if (typeof fromURL[key] === 'object') { - deepMerge(base, fromURL[key], path.concat(key)) + if (typeof fromURL[key] === "object") { + deepMerge(base, fromURL[key], path.concat(key)); } else { - let baseAtPath: Record = base + let baseAtPath: Record = base; for (const key of path) { if (baseAtPath[key] === undefined) { - baseAtPath[key] = {} + baseAtPath[key] = {}; } - baseAtPath = baseAtPath[key] + baseAtPath = baseAtPath[key]; } - const baseAtKey = baseAtPath[key] + const baseAtKey = baseAtPath[key]; if (baseAtKey !== undefined) { - const fullPath = path.concat(key).join('.') + const fullPath = path.concat(key).join("."); logger.warn({ - module: 'Config', + module: "Config", message: `"${fullPath}" is overridden by a URL parameter.`, - }) + }); } - baseAtPath[key] = fromURL[key] + baseAtPath[key] = fromURL[key]; } } } - const config: Record = { ...baseConfig } - deepMerge(config, configFromURL) - return config as BaseClickHouseClientConfigOptions + const config: Record = { ...baseConfig }; + deepMerge(config, configFromURL); + return config as BaseClickHouseClientConfigOptions; } export function createUrl(configURL: string | URL | undefined): URL { - let url: URL + let url: URL; try { - if (typeof configURL === 'string' || configURL instanceof URL) { - url = new URL(configURL) + if (typeof configURL === "string" || configURL instanceof URL) { + url = new URL(configURL); } else { - return new URL('http://localhost:8123') + return new URL("http://localhost:8123"); } } catch (err) { throw new Error( - 'ClickHouse URL is malformed. Expected format: http[s]://[username:password@]hostname:port[/database][?param1=value1¶m2=value2]', + "ClickHouse URL is malformed. Expected format: http[s]://[username:password@]hostname:port[/database][?param1=value1¶m2=value2]", { cause: err }, - ) + ); } - if (url.protocol !== 'http:' && url.protocol !== 'https:') { + if (url.protocol !== "http:" && url.protocol !== "https:") { throw new Error( `ClickHouse URL protocol must be either http or https. Got: ${url.protocol}`, - ) + ); } - return url + return url; } /** @@ -365,152 +365,152 @@ export function loadConfigOptionsFromURL( url: URL, handleExtraURLParams: HandleImplSpecificURLParams | null, ): [URL, BaseClickHouseClientConfigOptions] { - let config: BaseClickHouseClientConfigOptions = {} + let config: BaseClickHouseClientConfigOptions = {}; // trim is not needed, cause space is not allowed in the URL basic auth and should be encoded as %20 - if (url.username !== '') { - config.username = decodeURIComponent(url.username) + if (url.username !== "") { + config.username = decodeURIComponent(url.username); } - if (url.password !== '') { - config.password = decodeURIComponent(url.password) + if (url.password !== "") { + config.password = decodeURIComponent(url.password); } if (url.pathname.trim().length > 1) { - config.database = url.pathname.slice(1) + config.database = url.pathname.slice(1); } - const urlSearchParamsKeys = [...url.searchParams.keys()] + const urlSearchParamsKeys = [...url.searchParams.keys()]; if (urlSearchParamsKeys.length > 0) { - const unknownParams = new Set() - const settingPrefix = 'clickhouse_setting_' - const settingShortPrefix = 'ch_' - const httpHeaderPrefix = 'http_header_' + const unknownParams = new Set(); + const settingPrefix = "clickhouse_setting_"; + const settingShortPrefix = "ch_"; + const httpHeaderPrefix = "http_header_"; urlSearchParamsKeys.forEach((key) => { - let paramWasProcessed = true - const value = url.searchParams.get(key) as string + let paramWasProcessed = true; + const value = url.searchParams.get(key) as string; if (key.startsWith(settingPrefix)) { // clickhouse_settings_* - const settingKey = key.slice(settingPrefix.length) + const settingKey = key.slice(settingPrefix.length); if (config.clickhouse_settings === undefined) { - config.clickhouse_settings = {} + config.clickhouse_settings = {}; } - config.clickhouse_settings[settingKey] = value + config.clickhouse_settings[settingKey] = value; } else if (key.startsWith(settingShortPrefix)) { // ch_* - const settingKey = key.slice(settingShortPrefix.length) + const settingKey = key.slice(settingShortPrefix.length); if (config.clickhouse_settings === undefined) { - config.clickhouse_settings = {} + config.clickhouse_settings = {}; } - config.clickhouse_settings[settingKey] = value + config.clickhouse_settings[settingKey] = value; } else if (key.startsWith(httpHeaderPrefix)) { // http_headers_* - const headerKey = key.slice(httpHeaderPrefix.length) + const headerKey = key.slice(httpHeaderPrefix.length); if (config.http_headers === undefined) { - config.http_headers = {} + config.http_headers = {}; } - config.http_headers[headerKey] = value + config.http_headers[headerKey] = value; } else { // static known parameters switch (key) { - case 'application': - config.application = value - break - case 'pathname': - config.pathname = value - break - case 'session_id': - config.session_id = value - break - case 'request_timeout': + case "application": + config.application = value; + break; + case "pathname": + config.pathname = value; + break; + case "session_id": + config.session_id = value; + break; + case "request_timeout": config.request_timeout = numberConfigURLValue({ key, value, min: 0, - }) - break - case 'max_open_connections': + }); + break; + case "max_open_connections": config.max_open_connections = numberConfigURLValue({ key, value, min: 1, - }) - break - case 'compression_request': + }); + break; + case "compression_request": if (config.compression === undefined) { - config.compression = {} + config.compression = {}; } - config.compression.request = booleanConfigURLValue({ key, value }) - break - case 'compression_response': + config.compression.request = booleanConfigURLValue({ key, value }); + break; + case "compression_response": if (config.compression === undefined) { - config.compression = {} + config.compression = {}; } config.compression.response = booleanConfigURLValue({ key, value, - }) - break - case 'log_level': + }); + break; + case "log_level": if (config.log === undefined) { - config.log = {} + config.log = {}; } config.log.level = enumConfigURLValue({ key, value, enumObject: ClickHouseLogLevel, - }) as ClickHouseLogLevel - break - case 'keep_alive_enabled': + }) as ClickHouseLogLevel; + break; + case "keep_alive_enabled": if (config.keep_alive === undefined) { - config.keep_alive = {} + config.keep_alive = {}; } - config.keep_alive.enabled = booleanConfigURLValue({ key, value }) - break - case 'access_token': - config.access_token = value - break + config.keep_alive.enabled = booleanConfigURLValue({ key, value }); + break; + case "access_token": + config.access_token = value; + break; default: - paramWasProcessed = false - unknownParams.add(key) - break + paramWasProcessed = false; + unknownParams.add(key); + break; } } if (paramWasProcessed) { // so it won't be passed to the impl URL params handler - url.searchParams.delete(key) + url.searchParams.delete(key); } - }) + }); if (handleExtraURLParams !== null) { - const res = handleExtraURLParams(config, url) - config = res.config + const res = handleExtraURLParams(config, url); + config = res.config; if (unknownParams.size > 0) { - res.handled_params.forEach((k) => unknownParams.delete(k)) + res.handled_params.forEach((k) => unknownParams.delete(k)); } if (res.unknown_params.size > 0) { - res.unknown_params.forEach((k) => unknownParams.add(k)) + res.unknown_params.forEach((k) => unknownParams.add(k)); } } if (unknownParams.size > 0) { throw new Error( - `Unknown URL parameters: ${Array.from(unknownParams).join(', ')}`, - ) + `Unknown URL parameters: ${Array.from(unknownParams).join(", ")}`, + ); } } // clean up the final ClickHouse URL to be used in the connection - const clickHouseURL = new URL(`${url.protocol}//${url.host}`) - return [clickHouseURL, config] + const clickHouseURL = new URL(`${url.protocol}//${url.host}`); + return [clickHouseURL, config]; } export function booleanConfigURLValue({ key, value, }: { - key: string - value: string + key: string; + value: string; }): boolean { - const trimmed = value.trim() - if (trimmed === 'true' || trimmed === '1') return true - if (trimmed === 'false' || trimmed === '0') return false + const trimmed = value.trim(); + if (trimmed === "true" || trimmed === "1") return true; + if (trimmed === "false" || trimmed === "0") return false; throw new Error( `"${key}" has invalid boolean value: ${trimmed}. Expected one of: 0, 1, true, false.`, - ) + ); } export function numberConfigURLValue({ @@ -519,24 +519,26 @@ export function numberConfigURLValue({ min, max, }: { - key: string - value: string - min?: number - max?: number + key: string; + value: string; + min?: number; + max?: number; }): number { - const trimmed = value.trim() - const number = Number(trimmed) + const trimmed = value.trim(); + const number = Number(trimmed); if (isNaN(number)) - throw new Error(`"${key}" has invalid numeric value: ${trimmed}`) + throw new Error(`"${key}" has invalid numeric value: ${trimmed}`); if (min !== undefined && number < min) { - throw new Error(`"${key}" value ${trimmed} is less than min allowed ${min}`) + throw new Error( + `"${key}" value ${trimmed} is less than min allowed ${min}`, + ); } if (max !== undefined && number > max) { throw new Error( `"${key}" value ${trimmed} is greater than max allowed ${max}`, - ) + ); } - return number + return number; } export function enumConfigURLValue({ @@ -544,17 +546,17 @@ export function enumConfigURLValue({ value, enumObject, }: { - key: string - value: string - enumObject: Record + key: string; + value: string; + enumObject: Record; }): Enum { - const values = Object.keys(enumObject).filter((item) => isNaN(Number(item))) - const trimmed = value.trim() + const values = Object.keys(enumObject).filter((item) => isNaN(Number(item))); + const trimmed = value.trim(); if (!values.includes(trimmed)) { - const expected = values.join(', ') + const expected = values.join(", "); throw new Error( `"${key}" has invalid value: ${trimmed}. Expected one of: ${expected}.`, - ) + ); } - return enumObject[trimmed as Key] + return enumObject[trimmed as Key]; } diff --git a/packages/client-common/src/connection.ts b/packages/client-common/src/connection.ts index 0d757db9..adc8444f 100644 --- a/packages/client-common/src/connection.ts +++ b/packages/client-common/src/connection.ts @@ -1,96 +1,96 @@ -import type { JSONHandling } from '.' +import type { JSONHandling } from "."; import type { WithClickHouseSummary, WithHttpStatusCode, WithResponseHeaders, -} from './clickhouse_types' -import type { ClickHouseLogLevel, LogWriter } from './logger' -import type { ClickHouseSettings } from './settings' +} from "./clickhouse_types"; +import type { ClickHouseLogLevel, LogWriter } from "./logger"; +import type { ClickHouseSettings } from "./settings"; export type ConnectionAuth = - | { username: string; password: string; type: 'Credentials' } - | { access_token: string; type: 'JWT' } + | { username: string; password: string; type: "Credentials" } + | { access_token: string; type: "JWT" }; export interface ConnectionParams { - url: URL - request_timeout: number - max_open_connections: number - compression: CompressionSettings - database: string - clickhouse_settings: ClickHouseSettings - log_writer: LogWriter - log_level: ClickHouseLogLevel - keep_alive: { enabled: boolean } - application_id?: string - http_headers?: Record - auth: ConnectionAuth - json?: JSONHandling + url: URL; + request_timeout: number; + max_open_connections: number; + compression: CompressionSettings; + database: string; + clickhouse_settings: ClickHouseSettings; + log_writer: LogWriter; + log_level: ClickHouseLogLevel; + keep_alive: { enabled: boolean }; + application_id?: string; + http_headers?: Record; + auth: ConnectionAuth; + json?: JSONHandling; } export interface CompressionSettings { - decompress_response: boolean - compress_request: boolean + decompress_response: boolean; + compress_request: boolean; } export interface ConnBaseQueryParams { - query: string - clickhouse_settings?: ClickHouseSettings - query_params?: Record - abort_signal?: AbortSignal - session_id?: string - query_id?: string - auth?: { username: string; password: string } | { access_token: string } - role?: string | Array - http_headers?: Record + query: string; + clickhouse_settings?: ClickHouseSettings; + query_params?: Record; + abort_signal?: AbortSignal; + session_id?: string; + query_id?: string; + auth?: { username: string; password: string } | { access_token: string }; + role?: string | Array; + http_headers?: Record; } export type ConnPingParams = { select: boolean } & Omit< ConnBaseQueryParams, - 'query' | 'query_params' -> + "query" | "query_params" +>; export interface ConnCommandParams extends ConnBaseQueryParams { - ignore_error_response?: boolean + ignore_error_response?: boolean; } export interface ConnInsertParams extends ConnBaseQueryParams { - values: string | Stream + values: string | Stream; } export interface ConnExecParams extends ConnBaseQueryParams { - values?: Stream - decompress_response_stream?: boolean - ignore_error_response?: boolean + values?: Stream; + decompress_response_stream?: boolean; + ignore_error_response?: boolean; } export interface ConnBaseResult extends WithResponseHeaders, WithHttpStatusCode { - query_id: string + query_id: string; } export interface ConnQueryResult extends ConnBaseResult { - stream: Stream - query_id: string + stream: Stream; + query_id: string; } -export type ConnInsertResult = ConnBaseResult & WithClickHouseSummary +export type ConnInsertResult = ConnBaseResult & WithClickHouseSummary; export type ConnExecResult = ConnQueryResult & - WithClickHouseSummary -export type ConnCommandResult = ConnBaseResult & WithClickHouseSummary + WithClickHouseSummary; +export type ConnCommandResult = ConnBaseResult & WithClickHouseSummary; export type ConnPingResult = | { - success: true + success: true; } - | { success: false; error: Error } + | { success: false; error: Error }; -export type ConnOperation = 'Ping' | 'Query' | 'Insert' | 'Exec' | 'Command' +export type ConnOperation = "Ping" | "Query" | "Insert" | "Exec" | "Command"; export interface Connection { - ping(params: ConnPingParams): Promise - query(params: ConnBaseQueryParams): Promise> - insert(params: ConnInsertParams): Promise - command(params: ConnCommandParams): Promise - exec(params: ConnExecParams): Promise> - close(): Promise + ping(params: ConnPingParams): Promise; + query(params: ConnBaseQueryParams): Promise>; + insert(params: ConnInsertParams): Promise; + command(params: ConnCommandParams): Promise; + exec(params: ConnExecParams): Promise>; + close(): Promise; } diff --git a/packages/client-common/src/data_formatter/format_query_params.ts b/packages/client-common/src/data_formatter/format_query_params.ts index 13d7faed..136cbf62 100644 --- a/packages/client-common/src/data_formatter/format_query_params.ts +++ b/packages/client-common/src/data_formatter/format_query_params.ts @@ -1,7 +1,7 @@ export class TupleParam { - readonly values: readonly unknown[] + readonly values: readonly unknown[]; constructor(values: readonly unknown[]) { - this.values = values + this.values = values; } } @@ -15,7 +15,7 @@ export function formatQueryParams({ wrapStringInQuotes, printNullAsKeyword, isInArrayOrTuple: false, - }) + }); } function formatQueryParamsInternal({ @@ -25,45 +25,45 @@ function formatQueryParamsInternal({ isInArrayOrTuple, }: FormatQueryParamsOptions & { isInArrayOrTuple: boolean }): string { if (value === null || value === undefined) { - if (printNullAsKeyword) return 'NULL' - return '\\N' + if (printNullAsKeyword) return "NULL"; + return "\\N"; } - if (Number.isNaN(value)) return 'nan' - if (value === Number.POSITIVE_INFINITY) return '+inf' - if (value === Number.NEGATIVE_INFINITY) return '-inf' + if (Number.isNaN(value)) return "nan"; + if (value === Number.POSITIVE_INFINITY) return "+inf"; + if (value === Number.NEGATIVE_INFINITY) return "-inf"; - if (typeof value === 'number' || typeof value === 'bigint') - return String(value) - if (typeof value === 'boolean') { + if (typeof value === "number" || typeof value === "bigint") + return String(value); + if (typeof value === "boolean") { if (isInArrayOrTuple) { - return value ? 'TRUE' : 'FALSE' + return value ? "TRUE" : "FALSE"; } - return value ? '1' : '0' + return value ? "1" : "0"; } - if (typeof value === 'string') { - let result = '' + if (typeof value === "string") { + let result = ""; for (let i = 0; i < value.length; i++) { switch (value.charCodeAt(i)) { case TabASCII: - result += '\\t' - break + result += "\\t"; + break; case NewlineASCII: - result += '\\n' - break + result += "\\n"; + break; case CarriageReturnASCII: - result += '\\r' - break + result += "\\r"; + break; case SingleQuoteASCII: - result += `\\'` - break + result += `\\'`; + break; case BackslashASCII: - result += '\\\\' - break + result += "\\\\"; + break; default: - result += value[i] + result += value[i]; } } - return wrapStringInQuotes ? `'${result}'` : result + return wrapStringInQuotes ? `'${result}'` : result; } if (Array.isArray(value)) { @@ -76,18 +76,18 @@ function formatQueryParamsInternal({ isInArrayOrTuple: true, }), ) - .join(',')}]` + .join(",")}]`; } if (value instanceof Date) { // The ClickHouse server parses numbers as time-zone-agnostic Unix timestamps const unixTimestamp = Math.floor(value.getTime() / 1000) .toString() - .padStart(10, '0') - const milliseconds = value.getUTCMilliseconds() + .padStart(10, "0"); + const milliseconds = value.getUTCMilliseconds(); return milliseconds === 0 ? unixTimestamp - : `${unixTimestamp}.${milliseconds.toString().padStart(3, '0')}` + : `${unixTimestamp}.${milliseconds.toString().padStart(3, "0")}`; } // (42,'foo',NULL) @@ -101,26 +101,26 @@ function formatQueryParamsInternal({ isInArrayOrTuple: true, }), ) - .join(',')})` + .join(",")})`; } if (value instanceof Map) { - return formatObjectLikeParam(value.entries()) + return formatObjectLikeParam(value.entries()); } // This is only useful for simple maps where the keys are strings - if (typeof value === 'object') { - return formatObjectLikeParam(Object.entries(value)) + if (typeof value === "object") { + return formatObjectLikeParam(Object.entries(value)); } - throw new Error(`Unsupported value in query parameters: [${value}].`) + throw new Error(`Unsupported value in query parameters: [${value}].`); } // {'key1':'value1',42:'value2'} function formatObjectLikeParam( entries: [unknown, unknown][] | MapIterator<[unknown, unknown]>, ): string { - const formatted: string[] = [] + const formatted: string[] = []; for (const [key, val] of entries) { formatted.push( `${formatQueryParamsInternal({ @@ -134,20 +134,20 @@ function formatObjectLikeParam( printNullAsKeyword: true, isInArrayOrTuple: true, })}`, - ) + ); } - return `{${formatted.join(',')}}` + return `{${formatted.join(",")}}`; } interface FormatQueryParamsOptions { - value: unknown - wrapStringInQuotes?: boolean + value: unknown; + wrapStringInQuotes?: boolean; // For tuples/arrays, it is required to print NULL instead of \N - printNullAsKeyword?: boolean + printNullAsKeyword?: boolean; } -const TabASCII = 9 -const NewlineASCII = 10 -const CarriageReturnASCII = 13 -const SingleQuoteASCII = 39 -const BackslashASCII = 92 +const TabASCII = 9; +const NewlineASCII = 10; +const CarriageReturnASCII = 13; +const SingleQuoteASCII = 39; +const BackslashASCII = 92; diff --git a/packages/client-common/src/data_formatter/format_query_settings.ts b/packages/client-common/src/data_formatter/format_query_settings.ts index d0dff880..d27de94b 100644 --- a/packages/client-common/src/data_formatter/format_query_settings.ts +++ b/packages/client-common/src/data_formatter/format_query_settings.ts @@ -1,16 +1,16 @@ -import { SettingsMap } from '../settings' +import { SettingsMap } from "../settings"; export function formatQuerySettings( value: number | string | boolean | SettingsMap, ): string { - if (typeof value === 'boolean') return value ? '1' : '0' - if (typeof value === 'number') return String(value) - if (typeof value === 'string') return value + if (typeof value === "boolean") return value ? "1" : "0"; + if (typeof value === "number") return String(value); + if (typeof value === "string") return value; // ClickHouse requires a specific, non-JSON format for passing maps // as a setting value - single quotes instead of double // Example: {'system.numbers':'number != 3'} if (value instanceof SettingsMap) { - return value.toString() + return value.toString(); } - throw new Error(`Unsupported value in query settings: [${value}].`) + throw new Error(`Unsupported value in query settings: [${value}].`); } diff --git a/packages/client-common/src/data_formatter/formatter.ts b/packages/client-common/src/data_formatter/formatter.ts index 288a455c..0dbffa2f 100644 --- a/packages/client-common/src/data_formatter/formatter.ts +++ b/packages/client-common/src/data_formatter/formatter.ts @@ -1,66 +1,66 @@ -import type { JSONHandling } from '../parse' +import type { JSONHandling } from "../parse"; export const StreamableJSONFormats = [ - 'JSONEachRow', - 'JSONStringsEachRow', - 'JSONCompactEachRow', - 'JSONCompactStringsEachRow', - 'JSONCompactEachRowWithNames', - 'JSONCompactEachRowWithNamesAndTypes', - 'JSONCompactStringsEachRowWithNames', - 'JSONCompactStringsEachRowWithNamesAndTypes', - 'JSONEachRowWithProgress', -] as const -export const RecordsJSONFormats = ['JSONObjectEachRow'] as const + "JSONEachRow", + "JSONStringsEachRow", + "JSONCompactEachRow", + "JSONCompactStringsEachRow", + "JSONCompactEachRowWithNames", + "JSONCompactEachRowWithNamesAndTypes", + "JSONCompactStringsEachRowWithNames", + "JSONCompactStringsEachRowWithNamesAndTypes", + "JSONEachRowWithProgress", +] as const; +export const RecordsJSONFormats = ["JSONObjectEachRow"] as const; export const SingleDocumentJSONFormats = [ - 'JSON', - 'JSONStrings', - 'JSONCompact', - 'JSONCompactStrings', - 'JSONColumnsWithMetadata', -] as const + "JSON", + "JSONStrings", + "JSONCompact", + "JSONCompactStrings", + "JSONColumnsWithMetadata", +] as const; export const SupportedJSONFormats = [ ...RecordsJSONFormats, ...SingleDocumentJSONFormats, ...StreamableJSONFormats, -] as const +] as const; export const SupportedRawFormats = [ - 'CSV', - 'CSVWithNames', - 'CSVWithNamesAndTypes', - 'TabSeparated', - 'TabSeparatedRaw', - 'TabSeparatedWithNames', - 'TabSeparatedWithNamesAndTypes', - 'CustomSeparated', - 'CustomSeparatedWithNames', - 'CustomSeparatedWithNamesAndTypes', - 'Parquet', -] as const + "CSV", + "CSVWithNames", + "CSVWithNamesAndTypes", + "TabSeparated", + "TabSeparatedRaw", + "TabSeparatedWithNames", + "TabSeparatedWithNamesAndTypes", + "CustomSeparated", + "CustomSeparatedWithNames", + "CustomSeparatedWithNamesAndTypes", + "Parquet", +] as const; export const StreamableFormats = [ ...StreamableJSONFormats, ...SupportedRawFormats, -] as const +] as const; /** CSV, TSV, etc. - can be streamed, but cannot be decoded as JSON. */ -export type RawDataFormat = (typeof SupportedRawFormats)[number] +export type RawDataFormat = (typeof SupportedRawFormats)[number]; /** Each row is returned as a separate JSON object or an array, and these formats can be streamed. */ -export type StreamableJSONDataFormat = (typeof StreamableJSONFormats)[number] +export type StreamableJSONDataFormat = (typeof StreamableJSONFormats)[number]; /** Returned as a single {@link ResponseJSON} object, cannot be streamed. */ export type SingleDocumentJSONFormat = - (typeof SingleDocumentJSONFormats)[number] + (typeof SingleDocumentJSONFormats)[number]; /** Returned as a single object { row_1: T, row_2: T, ...}
* (i.e. Record), cannot be streamed. */ -export type RecordsJSONFormat = (typeof RecordsJSONFormats)[number] +export type RecordsJSONFormat = (typeof RecordsJSONFormats)[number]; /** All allowed JSON formats, whether streamable or not. */ export type JSONDataFormat = | StreamableJSONDataFormat | SingleDocumentJSONFormat - | RecordsJSONFormat + | RecordsJSONFormat; /** Data formats that are currently supported by the client.
* This is a union of the following types:
@@ -71,10 +71,10 @@ export type JSONDataFormat = * * {@link SingleDocumentJSONFormat} * * {@link RecordsJSONFormat} * @see https://clickhouse.com/docs/en/interfaces/formats */ -export type DataFormat = JSONDataFormat | RawDataFormat +export type DataFormat = JSONDataFormat | RawDataFormat; /** All data formats that can be streamed, whether it can be decoded as JSON or not. */ -export type StreamableDataFormat = (typeof StreamableFormats)[number] +export type StreamableDataFormat = (typeof StreamableFormats)[number]; export function isNotStreamableJSONFamily( format: DataFormat, @@ -82,17 +82,17 @@ export function isNotStreamableJSONFamily( return ( (SingleDocumentJSONFormats as readonly string[]).includes(format) || (RecordsJSONFormats as readonly string[]).includes(format) - ) + ); } export function isStreamableJSONFamily( format: DataFormat, ): format is StreamableJSONDataFormat { - return (StreamableJSONFormats as readonly string[]).includes(format) + return (StreamableJSONFormats as readonly string[]).includes(format); } export function isSupportedRawFormat(dataFormat: DataFormat) { - return (SupportedRawFormats as readonly string[]).includes(dataFormat) + return (SupportedRawFormats as readonly string[]).includes(dataFormat); } export function validateStreamFormat( @@ -101,11 +101,11 @@ export function validateStreamFormat( if (!StreamableFormats.includes(format)) { throw new Error( `${format} format is not streamable. Streamable formats: ${StreamableFormats.join( - ',', + ",", )}`, - ) + ); } - return true + return true; } /** @@ -117,12 +117,12 @@ export function validateStreamFormat( export function encodeJSON( value: any, format: DataFormat, - stringifyFn: JSONHandling['stringify'], + stringifyFn: JSONHandling["stringify"], ): string { if ((SupportedJSONFormats as readonly string[]).includes(format)) { - return stringifyFn(value) + '\n' + return stringifyFn(value) + "\n"; } throw new Error( `The client does not support JSON encoding in [${format}] format.`, - ) + ); } diff --git a/packages/client-common/src/data_formatter/index.ts b/packages/client-common/src/data_formatter/index.ts index c34515d0..01b00387 100644 --- a/packages/client-common/src/data_formatter/index.ts +++ b/packages/client-common/src/data_formatter/index.ts @@ -1,3 +1,3 @@ -export * from './formatter' -export { TupleParam, formatQueryParams } from './format_query_params' -export { formatQuerySettings } from './format_query_settings' +export * from "./formatter"; +export { TupleParam, formatQueryParams } from "./format_query_params"; +export { formatQuerySettings } from "./format_query_settings"; diff --git a/packages/client-common/src/error/error.ts b/packages/client-common/src/error/error.ts index 2f401e11..bb37a9c5 100644 --- a/packages/client-common/src/error/error.ts +++ b/packages/client-common/src/error/error.ts @@ -1,51 +1,51 @@ const errorRe = - /(Code|Error): (?\d+).*Exception: (?.+)\((?(?=.+[A-Z]{3})[A-Z0-9_]+?)\)/s + /(Code|Error): (?\d+).*Exception: (?.+)\((?(?=.+[A-Z]{3})[A-Z0-9_]+?)\)/s; interface ParsedClickHouseError { - message: string - code: string - type?: string + message: string; + code: string; + type?: string; } /** An error that is thrown by the ClickHouse server. */ export class ClickHouseError extends Error { - readonly code: string - readonly type: string | undefined + readonly code: string; + readonly type: string | undefined; constructor({ message, code, type }: ParsedClickHouseError) { - super(message) - this.code = code - this.type = type + super(message); + this.code = code; + this.type = type; // Set the prototype explicitly, see: // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work - Object.setPrototypeOf(this, ClickHouseError.prototype) + Object.setPrototypeOf(this, ClickHouseError.prototype); } } export function parseError(input: string | Error): ClickHouseError | Error { - const inputIsError = input instanceof Error - const message = inputIsError ? input.message : input - const match = message.match(errorRe) - const groups = match?.groups as ParsedClickHouseError | undefined + const inputIsError = input instanceof Error; + const message = inputIsError ? input.message : input; + const match = message.match(errorRe); + const groups = match?.groups as ParsedClickHouseError | undefined; if (groups) { - return new ClickHouseError(groups) + return new ClickHouseError(groups); } else { - return inputIsError ? input : new Error(input) + return inputIsError ? input : new Error(input); } } /** Captures the current stack trace from the sync context before going async. * It is necessary since the majority of the stack trace is lost when an async callback is called. */ export function getCurrentStackTrace(): string { - const stack = new Error().stack - if (!stack) return '' + const stack = new Error().stack; + if (!stack) return ""; // Skip the first three lines of the stack trace, containing useless information // - Text `Error` // - Info about this function call // - Info about the originator of this function call, e.g., `request` // Additionally, the original stack trace is, in fact, reversed. - return stack.split('\n').slice(3).reverse().join('\n') + return stack.split("\n").slice(3).reverse().join("\n"); } /** Having the stack trace produced by the {@link getCurrentStackTrace} function, @@ -56,10 +56,10 @@ export function enhanceStackTrace( stackTrace: string | undefined, ): E { if (err.stack && stackTrace) { - const firstNewlineIndex = err.stack.indexOf('\n') - const firstLine = err.stack.substring(0, firstNewlineIndex) - const errStack = err.stack.substring(firstNewlineIndex + 1) - err.stack = `${firstLine}\n${stackTrace}\n${errStack}` + const firstNewlineIndex = err.stack.indexOf("\n"); + const firstLine = err.stack.substring(0, firstNewlineIndex); + const errStack = err.stack.substring(firstNewlineIndex + 1); + err.stack = `${firstLine}\n${stackTrace}\n${errStack}`; } - return err + return err; } diff --git a/packages/client-common/src/error/index.ts b/packages/client-common/src/error/index.ts index 435ce65c..0ad13d91 100644 --- a/packages/client-common/src/error/index.ts +++ b/packages/client-common/src/error/index.ts @@ -1 +1 @@ -export * from './error' +export * from "./error"; diff --git a/packages/client-common/src/index.ts b/packages/client-common/src/index.ts index da15239e..cb6ad853 100644 --- a/packages/client-common/src/index.ts +++ b/packages/client-common/src/index.ts @@ -15,8 +15,8 @@ export { type PingParams, type PingParamsWithSelectQuery, type PingParamsWithEndpoint, -} from './client' -export { type BaseClickHouseClientConfigOptions } from './config' +} from "./client"; +export { type BaseClickHouseClientConfigOptions } from "./config"; export type { Row, RowOrProgress, @@ -24,7 +24,7 @@ export type { ResultJSONType, RowJSONType, ResultStream, -} from './result' +} from "./result"; export type { DataFormat, RawDataFormat, @@ -32,7 +32,7 @@ export type { StreamableDataFormat, StreamableJSONDataFormat, SingleDocumentJSONFormat, -} from './data_formatter' +} 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, @@ -48,13 +48,13 @@ export { 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' +} from "./data_formatter"; 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' +} 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, @@ -62,7 +62,7 @@ export { type WarnLogParams, type Logger, type LogParams, -} from './logger' +} from "./logger"; export type { ClickHouseSummary, InputJSON, @@ -76,7 +76,7 @@ export type { ClickHouseAuth, ClickHouseJWTAuth, ClickHouseCredentialsAuth, -} from './clickhouse_types' +} 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, @@ -84,13 +84,13 @@ export { 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' +} 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' +} from "./settings"; export type { SimpleColumnType, ParsedColumnSimple, @@ -105,7 +105,7 @@ export type { ParsedColumnMap, ParsedColumnType, JSONHandling, -} from './parse' +} 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, @@ -113,7 +113,7 @@ export { 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' +} from "./parse"; /** For implementation usage only - should not be re-exported */ export { @@ -124,7 +124,7 @@ export { isStreamableJSONFamily, isNotStreamableJSONFamily, validateStreamFormat, -} from './data_formatter' +} from "./data_formatter"; export { type ValuesEncoder, type MakeResultSet, @@ -135,7 +135,7 @@ export { enumConfigURLValue, getConnectionParams, numberConfigURLValue, -} from './config' +} from "./config"; export { EXCEPTION_TAG_HEADER_NAME, isSuccessfulResponse, @@ -148,9 +148,9 @@ export { isJWTAuth, extractErrorAtTheEndOfChunk, CARET_RETURN, -} from './utils' -export { LogWriter, DefaultLogger, type LogWriterParams } from './logger' -export { getCurrentStackTrace, enhanceStackTrace } from './error' +} from "./utils"; +export { LogWriter, DefaultLogger, type LogWriterParams } from "./logger"; +export { getCurrentStackTrace, enhanceStackTrace } from "./error"; export type { CompressionSettings, Connection, @@ -166,6 +166,6 @@ export type { ConnCommandResult, ConnOperation, ConnPingParams, -} from './connection' -export type { QueryParamsWithFormat } from './client' -export type { IsSame } from './ts_utils' +} from "./connection"; +export type { QueryParamsWithFormat } from "./client"; +export type { IsSame } from "./ts_utils"; diff --git a/packages/client-common/src/logger.ts b/packages/client-common/src/logger.ts index 4b608525..322c0a93 100644 --- a/packages/client-common/src/logger.ts +++ b/packages/client-common/src/logger.ts @@ -1,135 +1,135 @@ /* eslint-disable no-console */ export interface LogParams { - module: string - message: string - args?: Record + module: string; + message: string; + args?: Record; } -export type ErrorLogParams = LogParams & { err: Error } -export type WarnLogParams = LogParams & { err?: Error } +export type ErrorLogParams = LogParams & { err: Error }; +export type WarnLogParams = LogParams & { err?: Error }; export interface Logger { - trace(params: LogParams): void - debug(params: LogParams): void - info(params: LogParams): void - warn(params: WarnLogParams): void - error(params: ErrorLogParams): void + trace(params: LogParams): void; + debug(params: LogParams): void; + info(params: LogParams): void; + warn(params: WarnLogParams): void; + error(params: ErrorLogParams): void; } export class DefaultLogger implements Logger { trace({ module, message, args }: LogParams): void { const params: unknown[] = [ - formatMessage({ module, message, level: 'TRACE' }), - ] + formatMessage({ module, message, level: "TRACE" }), + ]; if (args) { - params.push('\nArguments:', args) + params.push("\nArguments:", args); } - console.debug(...params) + console.debug(...params); } debug({ module, message, args }: LogParams): void { const params: unknown[] = [ - formatMessage({ module, message, level: 'DEBUG' }), - ] + formatMessage({ module, message, level: "DEBUG" }), + ]; if (args) { - params.push('\nArguments:', args) + params.push("\nArguments:", args); } - console.debug(...params) + console.debug(...params); } info({ module, message, args }: LogParams): void { const params: unknown[] = [ - formatMessage({ module, message, level: 'INFO' }), - ] + formatMessage({ module, message, level: "INFO" }), + ]; if (args) { - params.push('\nArguments:', args) + params.push("\nArguments:", args); } - console.info(...params) + console.info(...params); } warn({ module, message, args, err }: WarnLogParams): void { const params: unknown[] = [ - formatMessage({ module, message, level: 'WARN' }), - ] + formatMessage({ module, message, level: "WARN" }), + ]; if (args) { - params.push('\nArguments:', args) + params.push("\nArguments:", args); } if (err) { - params.push('\nCaused by:', err) + params.push("\nCaused by:", err); } - console.warn(...params) + console.warn(...params); } error({ module, message, args, err }: ErrorLogParams): void { const params: unknown[] = [ - formatMessage({ module, message, level: 'ERROR' }), - ] + formatMessage({ module, message, level: "ERROR" }), + ]; if (args) { - params.push('\nArguments:', args) + params.push("\nArguments:", args); } - params.push('\nCaused by:', err) - console.error(...params) + params.push("\nCaused by:", err); + console.error(...params); } } export type LogWriterParams = Omit< Parameters[0], - 'module' -> & { module?: string } + "module" +> & { module?: string }; export class LogWriter { - private readonly logger: Logger - private readonly module: string - private readonly logLevel: ClickHouseLogLevel + private readonly logger: Logger; + private readonly module: string; + private readonly logLevel: ClickHouseLogLevel; constructor(logger: Logger, module: string, logLevel: ClickHouseLogLevel) { - this.logger = logger - this.module = module - this.logLevel = logLevel + this.logger = logger; + this.module = module; + this.logLevel = logLevel; this.info({ message: `Log level is set to ${ClickHouseLogLevel[this.logLevel]}`, - }) + }); } - trace(params: LogWriterParams<'trace'>): void { + trace(params: LogWriterParams<"trace">): void { if (this.logLevel <= (ClickHouseLogLevel.TRACE as number)) { this.logger.trace({ ...params, module: params.module ?? this.module, - }) + }); } } - debug(params: LogWriterParams<'debug'>): void { + debug(params: LogWriterParams<"debug">): void { if (this.logLevel <= (ClickHouseLogLevel.DEBUG as number)) { this.logger.debug({ ...params, module: params.module ?? this.module, - }) + }); } } - info(params: LogWriterParams<'info'>): void { + info(params: LogWriterParams<"info">): void { if (this.logLevel <= (ClickHouseLogLevel.INFO as number)) { this.logger.info({ ...params, module: params.module ?? this.module, - }) + }); } } - warn(params: LogWriterParams<'warn'>): void { + warn(params: LogWriterParams<"warn">): void { if (this.logLevel <= (ClickHouseLogLevel.WARN as number)) { this.logger.warn({ ...params, module: params.module ?? this.module, - }) + }); } } - error(params: LogWriterParams<'error'>): void { + error(params: LogWriterParams<"error">): void { if (this.logLevel <= (ClickHouseLogLevel.ERROR as number)) { this.logger.error({ ...params, module: params.module ?? this.module, - }) + }); } } } @@ -167,24 +167,24 @@ export const ClickHouseLogLevel = { * Logging is turned off. */ OFF: 127, - 0: 'TRACE', - 1: 'DEBUG', - 2: 'INFO', - 3: 'WARN', - 4: 'ERROR', - 127: 'OFF', -} as const -export type ClickHouseLogLevel = 0 | 1 | 2 | 3 | 4 | 127 + 0: "TRACE", + 1: "DEBUG", + 2: "INFO", + 3: "WARN", + 4: "ERROR", + 127: "OFF", +} as const; +export type ClickHouseLogLevel = 0 | 1 | 2 | 3 | 4 | 127; function formatMessage({ level, module, message, }: { - level: 'TRACE' | 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' - module: string - message: string + level: "TRACE" | "DEBUG" | "INFO" | "WARN" | "ERROR"; + module: string; + message: string; }): string { - const ts = new Date().toISOString() - return `[${ts}][${level}][@clickhouse/client][${module}] ${message}` + const ts = new Date().toISOString(); + return `[${ts}][${level}][@clickhouse/client][${module}] ${message}`; } diff --git a/packages/client-common/src/parse/column_types.ts b/packages/client-common/src/parse/column_types.ts index a62cba38..264c1855 100644 --- a/packages/client-common/src/parse/column_types.ts +++ b/packages/client-common/src/parse/column_types.ts @@ -1,77 +1,77 @@ export class ColumnTypeParseError extends Error { - readonly args: Record + readonly args: Record; constructor(message: string, args?: Record) { - super(message) - this.args = args ?? {} + super(message); + this.args = args ?? {}; // Set the prototype explicitly, see: // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work - Object.setPrototypeOf(this, ColumnTypeParseError.prototype) + Object.setPrototypeOf(this, ColumnTypeParseError.prototype); } } export const SimpleColumnTypes = [ - 'Bool', - 'UInt8', - 'Int8', - 'UInt16', - 'Int16', - 'UInt32', - 'Int32', - 'UInt64', - 'Int64', - 'UInt128', - 'Int128', - 'UInt256', - 'Int256', - 'Float32', - 'Float64', - 'String', - 'UUID', - 'Date', - 'Date32', - 'IPv4', - 'IPv6', -] as const -export type SimpleColumnType = (typeof SimpleColumnTypes)[number] + "Bool", + "UInt8", + "Int8", + "UInt16", + "Int16", + "UInt32", + "Int32", + "UInt64", + "Int64", + "UInt128", + "Int128", + "UInt256", + "Int256", + "Float32", + "Float64", + "String", + "UUID", + "Date", + "Date32", + "IPv4", + "IPv6", +] as const; +export type SimpleColumnType = (typeof SimpleColumnTypes)[number]; export interface ParsedColumnSimple { - type: 'Simple' + type: "Simple"; /** Without LowCardinality and Nullable. For example: * * UInt8 -> UInt8 * * LowCardinality(Nullable(String)) -> String */ - columnType: SimpleColumnType + columnType: SimpleColumnType; /** The original type before parsing. */ - sourceType: string + sourceType: string; } export interface ParsedColumnFixedString { - type: 'FixedString' - sizeBytes: number - sourceType: string + type: "FixedString"; + sizeBytes: number; + sourceType: string; } export interface ParsedColumnDateTime { - type: 'DateTime' - timezone: string | null - sourceType: string + type: "DateTime"; + timezone: string | null; + sourceType: string; } export interface ParsedColumnDateTime64 { - type: 'DateTime64' - timezone: string | null + type: "DateTime64"; + timezone: string | null; /** Valid range: [0 : 9] */ - precision: number - sourceType: string + precision: number; + sourceType: string; } export interface ParsedColumnEnum { - type: 'Enum' + type: "Enum"; /** Index to name */ - values: Record + values: Record; /** UInt8 or UInt16 */ - intSize: 8 | 16 - sourceType: string + intSize: 8 | 16; + sourceType: string; } /** Int size for Decimal depends on the Precision @@ -81,27 +81,27 @@ export interface ParsedColumnEnum { * * 256 bits for precision >= 39 */ export interface DecimalParams { - precision: number - scale: number - intSize: 32 | 64 | 128 | 256 + precision: number; + scale: number; + intSize: 32 | 64 | 128 | 256; } export interface ParsedColumnDecimal { - type: 'Decimal' - params: DecimalParams - sourceType: string + type: "Decimal"; + params: DecimalParams; + sourceType: string; } /** Tuple, Array or Map itself cannot be Nullable */ export interface ParsedColumnNullable { - type: 'Nullable' + type: "Nullable"; value: | ParsedColumnSimple | ParsedColumnEnum | ParsedColumnDecimal | ParsedColumnFixedString | ParsedColumnDateTime - | ParsedColumnDateTime64 - sourceType: string + | ParsedColumnDateTime64; + sourceType: string; } /** Array cannot be Nullable or LowCardinality, but its value type can be. @@ -109,7 +109,7 @@ export interface ParsedColumnNullable { * Arrays are allowed to have a Map as the value type. */ export interface ParsedColumnArray { - type: 'Array' + type: "Array"; value: | ParsedColumnNullable | ParsedColumnSimple @@ -119,15 +119,15 @@ export interface ParsedColumnArray { | ParsedColumnMap | ParsedColumnDateTime | ParsedColumnDateTime64 - | ParsedColumnTuple + | ParsedColumnTuple; /** Array(T) = 1 dimension, Array(Array(T)) = 2, etc. */ - dimensions: number - sourceType: string + dimensions: number; + sourceType: string; } /** @see https://clickhouse.com/docs/en/sql-reference/data-types/map */ export interface ParsedColumnMap { - type: 'Map' + type: "Map"; /** Possible key types: * - String, Integer, UUID, Date, Date32, etc ({@link ParsedColumnSimple}) * - FixedString @@ -138,17 +138,17 @@ export interface ParsedColumnMap { | ParsedColumnSimple | ParsedColumnFixedString | ParsedColumnEnum - | ParsedColumnDateTime + | ParsedColumnDateTime; /** Value types are arbitrary, including Map, Array, and Tuple. */ - value: ParsedColumnType - sourceType: string + value: ParsedColumnType; + sourceType: string; } export interface ParsedColumnTuple { - type: 'Tuple' + type: "Tuple"; /** Element types are arbitrary, including Map, Array, and Tuple. */ - elements: ParsedColumnType[] - sourceType: string + elements: ParsedColumnType[]; + sourceType: string; } export type ParsedColumnType = @@ -161,7 +161,7 @@ export type ParsedColumnType = | ParsedColumnDateTime64 | ParsedColumnArray | ParsedColumnTuple - | ParsedColumnMap + | ParsedColumnMap; /** * @experimental - incomplete, unstable API; @@ -175,51 +175,51 @@ export type ParsedColumnType = * * Variant */ export function parseColumnType(sourceType: string): ParsedColumnType { - let columnType = sourceType - let isNullable = false + let columnType = sourceType; + let isNullable = false; if (columnType.startsWith(LowCardinalityPrefix)) { - columnType = columnType.slice(LowCardinalityPrefix.length, -1) + columnType = columnType.slice(LowCardinalityPrefix.length, -1); } if (columnType.startsWith(NullablePrefix)) { - columnType = columnType.slice(NullablePrefix.length, -1) - isNullable = true + columnType = columnType.slice(NullablePrefix.length, -1); + isNullable = true; } - let result: ParsedColumnType + let result: ParsedColumnType; if ((SimpleColumnTypes as unknown as string[]).includes(columnType)) { result = { - type: 'Simple', + type: "Simple", columnType: columnType as SimpleColumnType, sourceType, - } + }; } else if (columnType.startsWith(DecimalPrefix)) { result = parseDecimalType({ sourceType, columnType, - }) + }); } else if (columnType.startsWith(DateTime64Prefix)) { - result = parseDateTime64Type({ sourceType, columnType }) + result = parseDateTime64Type({ sourceType, columnType }); } else if (columnType.startsWith(DateTimePrefix)) { - result = parseDateTimeType({ sourceType, columnType }) + result = parseDateTimeType({ sourceType, columnType }); } else if (columnType.startsWith(FixedStringPrefix)) { - result = parseFixedStringType({ sourceType, columnType }) + result = parseFixedStringType({ sourceType, columnType }); } else if ( columnType.startsWith(Enum8Prefix) || columnType.startsWith(Enum16Prefix) ) { - result = parseEnumType({ sourceType, columnType }) + result = parseEnumType({ sourceType, columnType }); } else if (columnType.startsWith(ArrayPrefix)) { - result = parseArrayType({ sourceType, columnType }) + result = parseArrayType({ sourceType, columnType }); } else if (columnType.startsWith(MapPrefix)) { - result = parseMapType({ sourceType, columnType }) + result = parseMapType({ sourceType, columnType }); } else if (columnType.startsWith(TuplePrefix)) { - result = parseTupleType({ sourceType, columnType }) + result = parseTupleType({ sourceType, columnType }); } else { - throw new ColumnTypeParseError('Unsupported column type', { columnType }) + throw new ColumnTypeParseError("Unsupported column type", { columnType }); } if (isNullable) { - return asNullableType(result, sourceType) + return asNullableType(result, sourceType); } else { - return result + return result; } } @@ -231,58 +231,58 @@ export function parseDecimalType({ !columnType.startsWith(DecimalPrefix) || columnType.length < DecimalPrefix.length + 5 // Decimal(1, 0) is the shortest valid definition ) { - throw new ColumnTypeParseError('Invalid Decimal type', { + throw new ColumnTypeParseError("Invalid Decimal type", { sourceType, columnType, - }) + }); } - const split = columnType.slice(DecimalPrefix.length, -1).split(', ') + const split = columnType.slice(DecimalPrefix.length, -1).split(", "); if (split.length !== 2) { throw new ColumnTypeParseError( - 'Expected Decimal type to have both precision and scale', + "Expected Decimal type to have both precision and scale", { sourceType, columnType, split, }, - ) + ); } // SAFETY: `split` length is checked to be exactly 2 above - const [precisionStr, scaleStr] = split as [string, string] - let intSize: DecimalParams['intSize'] = 32 - const precision = parseInt(precisionStr, 10) + const [precisionStr, scaleStr] = split as [string, string]; + let intSize: DecimalParams["intSize"] = 32; + const precision = parseInt(precisionStr, 10); if (Number.isNaN(precision) || precision < 1 || precision > 76) { - throw new ColumnTypeParseError('Invalid Decimal precision', { + throw new ColumnTypeParseError("Invalid Decimal precision", { columnType, sourceType, precision, - }) + }); } - const scale = parseInt(scaleStr, 10) + const scale = parseInt(scaleStr, 10); if (Number.isNaN(scale) || scale < 0 || scale > precision) { - throw new ColumnTypeParseError('Invalid Decimal scale', { + throw new ColumnTypeParseError("Invalid Decimal scale", { columnType, sourceType, precision, scale, - }) + }); } if (precision > 38) { - intSize = 256 + intSize = 256; } else if (precision > 18) { - intSize = 128 + intSize = 128; } else if (precision > 9) { - intSize = 64 + intSize = 64; } return { - type: 'Decimal', + type: "Decimal", params: { precision, scale, intSize, }, sourceType, - } + }; } /** @@ -294,101 +294,101 @@ export function parseDecimalType({ * character is kept verbatim to preserve the previous permissive behavior. */ function unescapeEnumName(escaped: string): string { - let unescaped = '' - let i = 0 + let unescaped = ""; + let i = 0; while (i < escaped.length) { if (escaped.charCodeAt(i) === BackslashASCII && i + 1 < escaped.length) { - i++ + i++; switch (escaped[i]) { - case 'n': - unescaped += '\n' - break - case 't': - unescaped += '\t' - break - case 'r': - unescaped += '\r' - break - case '\\': - unescaped += '\\' - break + case "n": + unescaped += "\n"; + break; + case "t": + unescaped += "\t"; + break; + case "r": + unescaped += "\r"; + break; + case "\\": + unescaped += "\\"; + break; case "'": - unescaped += "'" - break + unescaped += "'"; + break; default: // Preserve previous behavior for unknown escape sequences by // dropping the backslash and keeping the escaped character. - unescaped += escaped[i] - break + unescaped += escaped[i]; + break; } } else { - unescaped += escaped[i] + unescaped += escaped[i]; } - i++ + i++; } - return unescaped + return unescaped; } export function parseEnumType({ columnType, sourceType, }: ParseColumnTypeParams): ParsedColumnEnum { - let intSize: 8 | 16 + let intSize: 8 | 16; if (columnType.startsWith(Enum8Prefix)) { - columnType = columnType.slice(Enum8Prefix.length, -1) - intSize = 8 + columnType = columnType.slice(Enum8Prefix.length, -1); + intSize = 8; } else if (columnType.startsWith(Enum16Prefix)) { - columnType = columnType.slice(Enum16Prefix.length, -1) - intSize = 16 + columnType = columnType.slice(Enum16Prefix.length, -1); + intSize = 16; } else { throw new ColumnTypeParseError( - 'Expected Enum to be either Enum8 or Enum16', + "Expected Enum to be either Enum8 or Enum16", { columnType, sourceType, }, - ) + ); } // The minimal allowed Enum definition is Enum8('' = 0), i.e. 6 chars inside. if (columnType.length < 6) { - throw new ColumnTypeParseError('Invalid Enum type values', { + throw new ColumnTypeParseError("Invalid Enum type values", { columnType, sourceType, - }) + }); } - const names: string[] = [] - const indices: number[] = [] - let parsingName = true // false when parsing the index - let charEscaped = false // we should ignore escaped ticks - let startIndex = 1 // Skip the first ' + const names: string[] = []; + const indices: number[] = []; + let parsingName = true; // false when parsing the index + let charEscaped = false; // we should ignore escaped ticks + let startIndex = 1; // Skip the first ' // Should support the most complicated enums, such as Enum8('f\'' = 1, 'x =' = 2, 'b\'\'\'' = 3, '\'c=4=' = 42, '4' = 100) for (let i = 1; i < columnType.length; i++) { if (parsingName) { if (charEscaped) { - charEscaped = false + charEscaped = false; } else { if (columnType.charCodeAt(i) === BackslashASCII) { - charEscaped = true + charEscaped = true; } else if (columnType.charCodeAt(i) === SingleQuoteASCII) { // non-escaped closing tick - push the name - const rawName = columnType.slice(startIndex, i) + const rawName = columnType.slice(startIndex, i); // Unescape the name by removing backslash escape sequences - const name = unescapeEnumName(rawName) + const name = unescapeEnumName(rawName); if (names.includes(name)) { - throw new ColumnTypeParseError('Duplicate Enum name', { + throw new ColumnTypeParseError("Duplicate Enum name", { columnType, sourceType, name, names, indices, - }) + }); } - names.push(name) - i += 4 // skip ` = ` and the first digit, as it will always have at least one. - startIndex = i - parsingName = false + names.push(name); + i += 4; // skip ` = ` and the first digit, as it will always have at least one. + startIndex = i; + parsingName = false; } } } @@ -397,43 +397,43 @@ export function parseEnumType({ columnType.charCodeAt(i) < ZeroASCII || columnType.charCodeAt(i) > NineASCII ) { - pushEnumIndex(startIndex, i) + pushEnumIndex(startIndex, i); // the char at this index should be comma. - i += 2 // skip ` '`, but not the first char - ClickHouse allows something like Enum8('foo' = 0, '' = 42) - startIndex = i + 1 - parsingName = true - charEscaped = false + i += 2; // skip ` '`, but not the first char - ClickHouse allows something like Enum8('foo' = 0, '' = 42) + startIndex = i + 1; + parsingName = true; + charEscaped = false; } } // Push the last index - pushEnumIndex(startIndex, columnType.length) + pushEnumIndex(startIndex, columnType.length); if (names.length !== indices.length) { throw new ColumnTypeParseError( - 'Expected Enum to have the same number of names and indices', + "Expected Enum to have the same number of names and indices", { columnType, sourceType, names, indices }, - ) + ); } - const values: ParsedColumnEnum['values'] = {} + const values: ParsedColumnEnum["values"] = {}; for (let i = 0; i < names.length; i++) { - const idx = indices[i] - const name = names[i] + const idx = indices[i]; + const name = names[i]; // SAFETY: `names.length !== indices.length` is checked and throws above, so both indexed values are always defined. - values[idx!] = name! + values[idx!] = name!; } return { - type: 'Enum', + type: "Enum", values, intSize, sourceType, - } + }; function pushEnumIndex(start: number, end: number) { - const index = parseInt(columnType.slice(start, end), 10) + const index = parseInt(columnType.slice(start, end), 10); if (Number.isNaN(index) || index < 0) { throw new ColumnTypeParseError( - 'Expected Enum index to be a valid number', + "Expected Enum index to be a valid number", { columnType, sourceType, @@ -443,18 +443,18 @@ export function parseEnumType({ start, end, }, - ) + ); } if (indices.includes(index)) { - throw new ColumnTypeParseError('Duplicate Enum index', { + throw new ColumnTypeParseError("Duplicate Enum index", { columnType, sourceType, index, names, indices, - }) + }); } - indices.push(index) + indices.push(index); } } @@ -466,37 +466,37 @@ export function parseMapType({ !columnType.startsWith(MapPrefix) || columnType.length < MapPrefix.length + 11 // the shortest definition seems to be Map(Int8, Int8) ) { - throw new ColumnTypeParseError('Invalid Map type', { + throw new ColumnTypeParseError("Invalid Map type", { columnType, sourceType, - }) + }); } - columnType = columnType.slice(MapPrefix.length, -1) + columnType = columnType.slice(MapPrefix.length, -1); const [keyType, valueType] = getElementsTypes( { columnType, sourceType }, 2, - ) as [string, string, ...string[]] - const key = parseColumnType(keyType) + ) as [string, string, ...string[]]; + const key = parseColumnType(keyType); if ( - key.type === 'DateTime64' || - key.type === 'Nullable' || - key.type === 'Array' || - key.type === 'Map' || - key.type === 'Decimal' || - key.type === 'Tuple' + key.type === "DateTime64" || + key.type === "Nullable" || + key.type === "Array" || + key.type === "Map" || + key.type === "Decimal" || + key.type === "Tuple" ) { - throw new ColumnTypeParseError('Invalid Map key type', { + throw new ColumnTypeParseError("Invalid Map key type", { key, sourceType, - }) + }); } - const value = parseColumnType(valueType) + const value = parseColumnType(valueType); return { - type: 'Map', + type: "Map", key, value, sourceType, - } + }; } export function parseTupleType({ @@ -507,20 +507,20 @@ export function parseTupleType({ !columnType.startsWith(TuplePrefix) || columnType.length < TuplePrefix.length + 5 // Tuple(Int8) is the shortest valid definition ) { - throw new ColumnTypeParseError('Invalid Tuple type', { + throw new ColumnTypeParseError("Invalid Tuple type", { columnType, sourceType, - }) + }); } - columnType = columnType.slice(TuplePrefix.length, -1) + columnType = columnType.slice(TuplePrefix.length, -1); const elements = getElementsTypes({ columnType, sourceType }, 1).map((type) => parseColumnType(type), - ) + ); return { - type: 'Tuple', + type: "Tuple", elements, sourceType, - } + }; } export function parseArrayType({ @@ -531,41 +531,41 @@ export function parseArrayType({ !columnType.startsWith(ArrayPrefix) || columnType.length < ArrayPrefix.length + 5 // Array(Int8) is the shortest valid definition ) { - throw new ColumnTypeParseError('Invalid Array type', { + throw new ColumnTypeParseError("Invalid Array type", { columnType, sourceType, - }) + }); } - let dimensions = 0 + let dimensions = 0; while (columnType.length > 0) { if (columnType.startsWith(ArrayPrefix)) { - columnType = columnType.slice(ArrayPrefix.length, -1) // Array(T) -> T - dimensions++ + columnType = columnType.slice(ArrayPrefix.length, -1); // Array(T) -> T + dimensions++; } else { - break + break; } } if (dimensions === 0 || dimensions > 10) { // TODO: check how many we can handle; max 10 seems more than enough. throw new ColumnTypeParseError( - 'Expected Array to have between 1 and 10 dimensions', + "Expected Array to have between 1 and 10 dimensions", { columnType }, - ) + ); } - const value = parseColumnType(columnType) - if (value.type === 'Array') { - throw new ColumnTypeParseError('Unexpected Array as value type', { + const value = parseColumnType(columnType); + if (value.type === "Array") { + throw new ColumnTypeParseError("Unexpected Array as value type", { columnType, sourceType, - }) + }); } return { - type: 'Array', + type: "Array", value, dimensions, sourceType, - } + }; } export function parseDateTimeType({ @@ -576,26 +576,29 @@ export function parseDateTimeType({ columnType.startsWith(DateTimeWithTimezonePrefix) && columnType.length > DateTimeWithTimezonePrefix.length + 4 // DateTime('GB') has the least amount of chars ) { - const timezone = columnType.slice(DateTimeWithTimezonePrefix.length + 1, -2) + const timezone = columnType.slice( + DateTimeWithTimezonePrefix.length + 1, + -2, + ); return { - type: 'DateTime', + type: "DateTime", timezone, sourceType, - } + }; } else if ( columnType.startsWith(DateTimePrefix) && columnType.length === DateTimePrefix.length ) { return { - type: 'DateTime', + type: "DateTime", timezone: null, sourceType, - } + }; } else { - throw new ColumnTypeParseError('Invalid DateTime type', { + throw new ColumnTypeParseError("Invalid DateTime type", { columnType, sourceType, - }) + }); } } @@ -607,30 +610,30 @@ export function parseDateTime64Type({ !columnType.startsWith(DateTime64Prefix) || columnType.length < DateTime64Prefix.length + 2 // should at least have a precision ) { - throw new ColumnTypeParseError('Invalid DateTime64 type', { + throw new ColumnTypeParseError("Invalid DateTime64 type", { columnType, sourceType, - }) + }); } - const precision = parseInt(columnType[DateTime64Prefix.length] ?? '', 10) + const precision = parseInt(columnType[DateTime64Prefix.length] ?? "", 10); if (Number.isNaN(precision) || precision < 0 || precision > 9) { - throw new ColumnTypeParseError('Invalid DateTime64 precision', { + throw new ColumnTypeParseError("Invalid DateTime64 precision", { columnType, sourceType, precision, - }) + }); } - let timezone = null + let timezone = null; if (columnType.length > DateTime64Prefix.length + 2) { // e.g. DateTime64(3, 'UTC') -> UTC - timezone = columnType.slice(DateTime64Prefix.length + 4, -2) + timezone = columnType.slice(DateTime64Prefix.length + 4, -2); } return { - type: 'DateTime64', + type: "DateTime64", timezone, precision, sourceType, - } + }; } export function parseFixedStringType({ @@ -641,24 +644,27 @@ export function parseFixedStringType({ !columnType.startsWith(FixedStringPrefix) || columnType.length < FixedStringPrefix.length + 2 // i.e. at least FixedString(1) ) { - throw new ColumnTypeParseError('Invalid FixedString type', { + throw new ColumnTypeParseError("Invalid FixedString type", { columnType, sourceType, - }) + }); } - const sizeBytes = parseInt(columnType.slice(FixedStringPrefix.length, -1), 10) + const sizeBytes = parseInt( + columnType.slice(FixedStringPrefix.length, -1), + 10, + ); if (Number.isNaN(sizeBytes) || sizeBytes < 1) { - throw new ColumnTypeParseError('Invalid FixedString size in bytes', { + throw new ColumnTypeParseError("Invalid FixedString size in bytes", { columnType, sourceType, sizeBytes, - }) + }); } return { - type: 'FixedString', + type: "FixedString", sizeBytes, sourceType, - } + }; } export function asNullableType( @@ -666,23 +672,23 @@ export function asNullableType( sourceType: string, ): ParsedColumnNullable { if ( - value.type === 'Array' || - value.type === 'Map' || - value.type === 'Tuple' || - value.type === 'Nullable' + value.type === "Array" || + value.type === "Map" || + value.type === "Tuple" || + value.type === "Nullable" ) { throw new ColumnTypeParseError(`${value.type} cannot be Nullable`, { sourceType, - }) + }); } if (value.sourceType.startsWith(NullablePrefix)) { - value.sourceType = value.sourceType.slice(NullablePrefix.length, -1) + value.sourceType = value.sourceType.slice(NullablePrefix.length, -1); } return { - type: 'Nullable', + type: "Nullable", sourceType, value, - } + }; } /** Used for Map key/value types and Tuple elements. @@ -693,34 +699,34 @@ export function getElementsTypes( { columnType, sourceType }: ParseColumnTypeParams, minElements: number, ): string[] { - const elements: string[] = [] + const elements: string[] = []; /** Consider the element type parsed once we reach a comma outside of parens AND after an unescaped tick. * The most complicated cases are values names in the self-defined Enum types: * * `Tuple(Enum8('f\'()' = 1))` -> `f\'()` * * `Tuple(Enum8('(' = 1))` -> `(` * See also: {@link parseEnumType }, which works similarly (but has to deal with the indices following the names). */ - let openParens = 0 - let quoteOpen = false - let charEscaped = false - let lastElementIndex = 0 + let openParens = 0; + let quoteOpen = false; + let charEscaped = false; + let lastElementIndex = 0; for (let i = 0; i < columnType.length; i++) { if (charEscaped) { - charEscaped = false + charEscaped = false; } else if (columnType.charCodeAt(i) === BackslashASCII) { - charEscaped = true + charEscaped = true; } else if (columnType.charCodeAt(i) === SingleQuoteASCII) { - quoteOpen = !quoteOpen // unescaped quote + quoteOpen = !quoteOpen; // unescaped quote } else { if (!quoteOpen) { if (columnType.charCodeAt(i) === LeftParenASCII) { - openParens++ + openParens++; } else if (columnType.charCodeAt(i) === RightParenASCII) { - openParens-- + openParens--; } else if (columnType.charCodeAt(i) === CommaASCII) { if (openParens === 0) { - elements.push(columnType.slice(lastElementIndex, i)) - i += 2 // skip ', ' - lastElementIndex = i + elements.push(columnType.slice(lastElementIndex, i)); + i += 2; // skip ', ' + lastElementIndex = i; } } } @@ -729,43 +735,43 @@ export function getElementsTypes( // Push the remaining part of the type if it seems to be valid (at least all parentheses are closed) if (!openParens && lastElementIndex < columnType.length - 1) { - elements.push(columnType.slice(lastElementIndex)) + elements.push(columnType.slice(lastElementIndex)); } if (elements.length < minElements) { - throw new ColumnTypeParseError('Expected more elements in the type', { + throw new ColumnTypeParseError("Expected more elements in the type", { sourceType, columnType, elements, minElements, - }) + }); } - return elements + return elements; } interface ParseColumnTypeParams { /** A particular type to parse, such as DateTime. */ - columnType: string + columnType: string; /** Full type definition, such as Map(String, DateTime). */ - sourceType: string + sourceType: string; } -const NullablePrefix = 'Nullable(' as const -const LowCardinalityPrefix = 'LowCardinality(' as const -const DecimalPrefix = 'Decimal(' as const -const ArrayPrefix = 'Array(' as const -const MapPrefix = 'Map(' as const -const Enum8Prefix = 'Enum8(' as const -const Enum16Prefix = 'Enum16(' as const -const TuplePrefix = 'Tuple(' as const -const DateTimePrefix = 'DateTime' as const -const DateTimeWithTimezonePrefix = 'DateTime(' as const -const DateTime64Prefix = 'DateTime64(' as const -const FixedStringPrefix = 'FixedString(' as const - -const SingleQuoteASCII = 39 as const -const LeftParenASCII = 40 as const -const RightParenASCII = 41 as const -const CommaASCII = 44 as const -const ZeroASCII = 48 as const -const NineASCII = 57 as const -const BackslashASCII = 92 as const +const NullablePrefix = "Nullable(" as const; +const LowCardinalityPrefix = "LowCardinality(" as const; +const DecimalPrefix = "Decimal(" as const; +const ArrayPrefix = "Array(" as const; +const MapPrefix = "Map(" as const; +const Enum8Prefix = "Enum8(" as const; +const Enum16Prefix = "Enum16(" as const; +const TuplePrefix = "Tuple(" as const; +const DateTimePrefix = "DateTime" as const; +const DateTimeWithTimezonePrefix = "DateTime(" as const; +const DateTime64Prefix = "DateTime64(" as const; +const FixedStringPrefix = "FixedString(" as const; + +const SingleQuoteASCII = 39 as const; +const LeftParenASCII = 40 as const; +const RightParenASCII = 41 as const; +const CommaASCII = 44 as const; +const ZeroASCII = 48 as const; +const NineASCII = 57 as const; +const BackslashASCII = 92 as const; diff --git a/packages/client-common/src/parse/index.ts b/packages/client-common/src/parse/index.ts index 3fb05308..1e7b46a1 100644 --- a/packages/client-common/src/parse/index.ts +++ b/packages/client-common/src/parse/index.ts @@ -1,2 +1,2 @@ -export * from './column_types' -export * from './json_handling' +export * from "./column_types"; +export * from "./json_handling"; diff --git a/packages/client-common/src/parse/json_handling.ts b/packages/client-common/src/parse/json_handling.ts index e5653d05..64a7f118 100644 --- a/packages/client-common/src/parse/json_handling.ts +++ b/packages/client-common/src/parse/json_handling.ts @@ -6,7 +6,7 @@ export interface JSONHandling { * @default JSON.parse // See {@link JSON.parse} * @returns parsed object */ - parse: (input: string) => T + parse: (input: string) => T; /** * Custom stringifier for JSON objects * @@ -14,10 +14,10 @@ export interface JSONHandling { * @default JSON.stringify // See {@link JSON.stringify} * @returns stringified JSON */ - stringify: (input: T) => string // T is any because it can LITERALLY be anything + stringify: (input: T) => string; // T is any because it can LITERALLY be anything } export const defaultJSONHandling: JSONHandling = { parse: JSON.parse, stringify: JSON.stringify, -} +}; diff --git a/packages/client-common/src/result.ts b/packages/client-common/src/result.ts index 8f3bf121..19f3b294 100644 --- a/packages/client-common/src/result.ts +++ b/packages/client-common/src/result.ts @@ -3,7 +3,7 @@ import type { ResponseHeaders, ResponseJSON, SpecialEventRow, -} from './clickhouse_types' +} from "./clickhouse_types"; import type { DataFormat, RawDataFormat, @@ -11,9 +11,9 @@ import type { SingleDocumentJSONFormat, StreamableDataFormat, StreamableJSONDataFormat, -} from './data_formatter' +} from "./data_formatter"; -export type RowOrProgress = { row: T } | ProgressRow | SpecialEventRow +export type RowOrProgress = { row: T } | ProgressRow | SpecialEventRow; export type ResultStream = // JSON*EachRow (except JSONObjectEachRow), CSV, TSV etc. @@ -26,11 +26,11 @@ export type ResultStream = Format extends RecordsJSONFormat ? never : // If we fail to infer the literal type, allow getting the stream - Stream + Stream; export type ResultJSONType = // Emits either a { row: T } or an object with progress - F extends 'JSONEachRowWithProgress' + F extends "JSONEachRowWithProgress" ? RowOrProgress[] : // JSON*EachRow formats except JSONObjectEachRow F extends StreamableJSONDataFormat @@ -45,11 +45,11 @@ export type ResultJSONType = F extends RawDataFormat ? never : // happens only when Format could not be inferred from a literal - T[] | Record | ResponseJSON + T[] | Record | ResponseJSON; export type RowJSONType = // Emits either a { row: T } or an object with progress - F extends 'JSONEachRowWithProgress' + F extends "JSONEachRowWithProgress" ? RowOrProgress : // JSON*EachRow formats F extends StreamableJSONDataFormat @@ -57,21 +57,21 @@ export type RowJSONType = : // CSV, TSV, non-streamable JSON formats - cannot be streamed as JSON F extends RawDataFormat | SingleDocumentJSONFormat | RecordsJSONFormat ? never - : T // happens only when Format could not be inferred from a literal + : T; // happens only when Format could not be inferred from a literal export interface Row< JSONType = unknown, Format extends DataFormat | unknown = unknown, > { /** A string representation of a row. */ - text: string + text: string; /** * Returns a JSON representation of a row. * The method will throw if called on a response in JSON incompatible format. * It is safe to call this method multiple times. */ - json(): RowJSONType + json(): RowJSONType; } export interface BaseResultSet { @@ -84,7 +84,7 @@ export interface BaseResultSet { * The method should throw if the underlying stream was already consumed * by calling the other methods. */ - text(): Promise + text(): Promise; /** * The method waits for the all the rows to be fully loaded. @@ -96,7 +96,7 @@ export interface BaseResultSet { * by calling the other methods, or if it is called for non-JSON formats, * such as CSV, TSV, etc. */ - json(): Promise> + json(): Promise>; /** * Returns a readable stream for responses that can be streamed. @@ -139,14 +139,14 @@ export interface BaseResultSet { * and if the underlying stream was already consumed * by calling the other methods. */ - stream(): ResultStream + stream(): ResultStream; /** Close the underlying stream. */ - close(): void + close(): void; /** ClickHouse server QueryID. */ - query_id: string + query_id: string; /** Response headers. */ - response_headers: ResponseHeaders + response_headers: ResponseHeaders; } diff --git a/packages/client-common/src/settings.ts b/packages/client-common/src/settings.ts index 2f91e535..fff34ff9 100644 --- a/packages/client-common/src/settings.ts +++ b/packages/client-common/src/settings.ts @@ -1,4 +1,4 @@ -import type { DataFormat } from './data_formatter' +import type { DataFormat } from "./data_formatter"; /** * @see {@link https://github.com/ClickHouse/ClickHouse/blob/46ed4f6cdf68fbbdc59fbe0f0bfa9a361cc0dec1/src/Core/Settings.h} @@ -10,1594 +10,1594 @@ import type { DataFormat } from './data_formatter' ///// /** $4 */\n$2?: $1,\n interface ClickHouseServerSettings { /** Write add http CORS header. */ - add_http_cors_header?: Bool + add_http_cors_header?: Bool; /** Additional filter expression which would be applied to query result */ - additional_result_filter?: string + additional_result_filter?: string; /** Additional filter expression which would be applied after reading from specified table. Syntax: {'table1': 'expression', 'database.table2': 'expression'} */ - additional_table_filters?: Map + additional_table_filters?: Map; /** Rewrite all aggregate functions in a query, adding -OrNull suffix to them */ - aggregate_functions_null_for_empty?: Bool + aggregate_functions_null_for_empty?: Bool; /** Maximal size of block in bytes accumulated during aggregation in order of primary key. Lower block size allows to parallelize more final merge stage of aggregation. */ - aggregation_in_order_max_block_bytes?: UInt64 + aggregation_in_order_max_block_bytes?: UInt64; /** Number of threads to use for merge intermediate aggregation results in memory efficient mode. When bigger, then more memory is consumed. 0 means - same as 'max_threads'. */ - aggregation_memory_efficient_merge_threads?: UInt64 + aggregation_memory_efficient_merge_threads?: UInt64; /** Enable independent aggregation of partitions on separate threads when partition key suits group by key. Beneficial when number of partitions close to number of cores and partitions have roughly the same size */ - allow_aggregate_partitions_independently?: Bool + allow_aggregate_partitions_independently?: Bool; /** Use background I/O pool to read from MergeTree tables. This setting may increase performance for I/O bound queries */ - allow_asynchronous_read_from_io_pool_for_merge_tree?: Bool + allow_asynchronous_read_from_io_pool_for_merge_tree?: Bool; /** Allow HedgedConnections to change replica until receiving first data packet */ - allow_changing_replica_until_first_data_packet?: Bool + allow_changing_replica_until_first_data_packet?: Bool; /** Allow CREATE INDEX query without TYPE. Query will be ignored. Made for SQL compatibility tests. */ - allow_create_index_without_type?: Bool + allow_create_index_without_type?: Bool; /** Enable custom error code in function throwIf(). If true, thrown exceptions may have unexpected error codes. */ - allow_custom_error_code_in_throwif?: Bool + allow_custom_error_code_in_throwif?: Bool; /** If it is set to true, then a user is allowed to executed DDL queries. */ - allow_ddl?: Bool + allow_ddl?: Bool; /** Allow to create databases with deprecated Ordinary engine */ - allow_deprecated_database_ordinary?: Bool + allow_deprecated_database_ordinary?: Bool; /** Allow to create *MergeTree tables with deprecated engine definition syntax */ - allow_deprecated_syntax_for_merge_tree?: Bool + allow_deprecated_syntax_for_merge_tree?: Bool; /** If it is set to true, then a user is allowed to executed distributed DDL queries. */ - allow_distributed_ddl?: Bool + allow_distributed_ddl?: Bool; /** Allow ALTER TABLE ... DROP DETACHED PART[ITION] ... queries */ - allow_drop_detached?: Bool + allow_drop_detached?: Bool; /** Allow execute multiIf function columnar */ - allow_execute_multiif_columnar?: Bool + allow_execute_multiif_columnar?: Bool; /** Allow atomic alter on Materialized views. Work in progress. */ - allow_experimental_alter_materialized_view_structure?: Bool + allow_experimental_alter_materialized_view_structure?: Bool; /** Allow experimental analyzer */ - allow_experimental_analyzer?: Bool + allow_experimental_analyzer?: Bool; /** Allows to use Annoy index. Disabled by default because this feature is experimental */ - allow_experimental_annoy_index?: Bool + allow_experimental_annoy_index?: Bool; /** If it is set to true, allow to specify experimental compression codecs (but we don't have those yet and this option does nothing). */ - allow_experimental_codecs?: Bool + allow_experimental_codecs?: Bool; /** Allow to create database with Engine=MaterializedMySQL(...). */ - allow_experimental_database_materialized_mysql?: Bool + allow_experimental_database_materialized_mysql?: Bool; /** Allow to create database with Engine=MaterializedPostgreSQL(...). */ - allow_experimental_database_materialized_postgresql?: Bool + allow_experimental_database_materialized_postgresql?: Bool; /** Allow to create databases with Replicated engine */ - allow_experimental_database_replicated?: Bool + allow_experimental_database_replicated?: Bool; /** Enable experimental functions for funnel analysis. */ - allow_experimental_funnel_functions?: Bool + allow_experimental_funnel_functions?: Bool; /** Enable experimental hash functions */ - allow_experimental_hash_functions?: Bool + allow_experimental_hash_functions?: Bool; /** If it is set to true, allow to use experimental inverted index. */ - allow_experimental_inverted_index?: Bool + allow_experimental_inverted_index?: Bool; /** Enable LIVE VIEW. Not mature enough. */ - allow_experimental_live_view?: Bool + allow_experimental_live_view?: Bool; /** Enable experimental functions for natural language processing. */ - allow_experimental_nlp_functions?: Bool + allow_experimental_nlp_functions?: Bool; /** Allow Object and JSON data types */ - allow_experimental_object_type?: Bool + allow_experimental_object_type?: Bool; /** Use all the replicas from a shard for SELECT query execution. Reading is parallelized and coordinated dynamically. 0 - disabled, 1 - enabled, silently disable them in case of failure, 2 - enabled, throw an exception in case of failure */ - allow_experimental_parallel_reading_from_replicas?: UInt64 + allow_experimental_parallel_reading_from_replicas?: UInt64; /** Experimental data deduplication for SELECT queries based on part UUIDs */ - allow_experimental_query_deduplication?: Bool + allow_experimental_query_deduplication?: Bool; /** Allow to use undrop query to restore dropped table in a limited time */ - allow_experimental_undrop_table_query?: Bool + allow_experimental_undrop_table_query?: Bool; /** Enable WINDOW VIEW. Not mature enough. */ - allow_experimental_window_view?: Bool + allow_experimental_window_view?: Bool; /** Support join with inequal conditions which involve columns from both left and right table. e.g. t1.y < t2.y. */ - allow_experimental_join_condition?: Bool + allow_experimental_join_condition?: Bool; /** Since ClickHouse 24.1 */ - allow_experimental_variant_type?: Bool + allow_experimental_variant_type?: Bool; /** Since ClickHouse 24.5 */ - allow_experimental_dynamic_type?: Bool + allow_experimental_dynamic_type?: Bool; /** Since ClickHouse 24.8 */ - allow_experimental_json_type?: Bool + allow_experimental_json_type?: Bool; /** Since ClickHouse 25.3 */ - enable_json_type?: Bool + enable_json_type?: Bool; /** Since ClickHouse 25.6 */ - enable_time_time64_type?: Bool + enable_time_time64_type?: Bool; /** Allow functions that use Hyperscan library. Disable to avoid potentially long compilation times and excessive resource usage. */ - allow_hyperscan?: Bool + allow_hyperscan?: Bool; /** Allow functions for introspection of ELF and DWARF for query profiling. These functions are slow and may impose security considerations. */ - allow_introspection_functions?: Bool + allow_introspection_functions?: Bool; /** Allow to execute alters which affects not only tables metadata, but also data on disk */ - allow_non_metadata_alters?: Bool + allow_non_metadata_alters?: Bool; /** Allow non-const timezone arguments in certain time-related functions like toTimeZone(), fromUnixTimestamp*(), snowflakeToDateTime*() */ - allow_nonconst_timezone_arguments?: Bool + allow_nonconst_timezone_arguments?: Bool; /** Allow non-deterministic functions in ALTER UPDATE/ALTER DELETE statements */ - allow_nondeterministic_mutations?: Bool + allow_nondeterministic_mutations?: Bool; /** Allow non-deterministic functions (includes dictGet) in sharding_key for optimize_skip_unused_shards */ - allow_nondeterministic_optimize_skip_unused_shards?: Bool + allow_nondeterministic_optimize_skip_unused_shards?: Bool; /** Prefer prefethed threadpool if all parts are on remote filesystem */ - allow_prefetched_read_pool_for_local_filesystem?: Bool + allow_prefetched_read_pool_for_local_filesystem?: Bool; /** Prefer prefethed threadpool if all parts are on remote filesystem */ - allow_prefetched_read_pool_for_remote_filesystem?: Bool + allow_prefetched_read_pool_for_remote_filesystem?: Bool; /** Allows push predicate when subquery contains WITH clause */ - allow_push_predicate_when_subquery_contains_with?: Bool + allow_push_predicate_when_subquery_contains_with?: Bool; /** Allow SETTINGS after FORMAT, but note, that this is not always safe (note: this is a compatibility setting). */ - allow_settings_after_format_in_insert?: Bool + allow_settings_after_format_in_insert?: Bool; /** Allow using simdjson library in 'JSON*' functions if AVX2 instructions are available. If disabled rapidjson will be used. */ - allow_simdjson?: Bool + allow_simdjson?: Bool; /** If it is set to true, allow to specify meaningless compression codecs. */ - allow_suspicious_codecs?: Bool + allow_suspicious_codecs?: Bool; /** In CREATE TABLE statement allows creating columns of type FixedString(n) with n > 256. FixedString with length >= 256 is suspicious and most likely indicates misusage */ - allow_suspicious_fixed_string_types?: Bool + allow_suspicious_fixed_string_types?: Bool; /** Reject primary/secondary indexes and sorting keys with identical expressions */ - allow_suspicious_indices?: Bool + allow_suspicious_indices?: Bool; /** In CREATE TABLE statement allows specifying LowCardinality modifier for types of small fixed size (8 or less). Enabling this may increase merge times and memory consumption. */ - allow_suspicious_low_cardinality_types?: Bool + allow_suspicious_low_cardinality_types?: Bool; /** Allow unrestricted (without condition on path) reads from system.zookeeper table, can be handy, but is not safe for zookeeper */ - allow_unrestricted_reads_from_keeper?: Bool + allow_unrestricted_reads_from_keeper?: Bool; /** Output information about affected parts. Currently, works only for FREEZE and ATTACH commands. */ - alter_partition_verbose_result?: Bool + alter_partition_verbose_result?: Bool; /** Wait for actions to manipulate the partitions. 0 - do not wait, 1 - wait for execution only of itself, 2 - wait for everyone. */ - alter_sync?: UInt64 + alter_sync?: UInt64; /** SELECT queries search up to this many nodes in Annoy indexes. */ - annoy_index_search_k_nodes?: Int64 + annoy_index_search_k_nodes?: Int64; /** Enable old ANY JOIN logic with many-to-one left-to-right table keys mapping for all ANY JOINs. It leads to confusing not equal results for 't1 ANY LEFT JOIN t2' and 't2 ANY RIGHT JOIN t1'. ANY RIGHT JOIN needs one-to-many keys mapping to be consistent with LEFT one. */ - any_join_distinct_right_table_keys?: Bool + any_join_distinct_right_table_keys?: Bool; /** Include ALIAS columns for wildcard query */ - asterisk_include_alias_columns?: Bool + asterisk_include_alias_columns?: Bool; /** Include MATERIALIZED columns for wildcard query */ - asterisk_include_materialized_columns?: Bool + asterisk_include_materialized_columns?: Bool; /** If true, data from INSERT query is stored in queue and later flushed to table in background. If wait_for_async_insert is false, INSERT query is processed almost instantly, otherwise client will wait until data will be flushed to table */ - async_insert?: Bool + async_insert?: Bool; /** Maximum time to wait before dumping collected data per query since the first data appeared. * * @see https://clickhouse.com/docs/operations/settings/settings#async_insert_busy_timeout_max_ms */ - async_insert_busy_timeout_max_ms?: Milliseconds + async_insert_busy_timeout_max_ms?: Milliseconds; /** For async INSERT queries in the replicated table, specifies that deduplication of insertings blocks should be performed */ - async_insert_deduplicate?: Bool + async_insert_deduplicate?: Bool; /** Maximum size in bytes of unparsed data collected per query before being inserted */ - async_insert_max_data_size?: UInt64 + async_insert_max_data_size?: UInt64; /** Maximum number of insert queries before being inserted */ - async_insert_max_query_number?: UInt64 + async_insert_max_query_number?: UInt64; /** Asynchronously create connections and send query to shards in remote query */ - async_query_sending_for_remote?: Bool + async_query_sending_for_remote?: Bool; /** Asynchronously read from socket executing remote query */ - async_socket_for_remote?: Bool + async_socket_for_remote?: Bool; /** Enables or disables creating a new file on each insert in azure engine tables */ - azure_create_new_file_on_insert?: Bool + azure_create_new_file_on_insert?: Bool; /** Maximum number of files that could be returned in batch by ListObject request */ - azure_list_object_keys_size?: UInt64 + azure_list_object_keys_size?: UInt64; /** The maximum size of object to upload using singlepart upload to Azure blob storage. */ - azure_max_single_part_upload_size?: UInt64 + azure_max_single_part_upload_size?: UInt64; /** The maximum number of retries during single Azure blob storage read. */ - azure_max_single_read_retries?: UInt64 + azure_max_single_read_retries?: UInt64; /** Enables or disables truncate before insert in azure engine tables. */ - azure_truncate_on_insert?: Bool + azure_truncate_on_insert?: Bool; /** Maximum size of batch for multiread request to [Zoo]Keeper during backup or restore */ - backup_restore_batch_size_for_keeper_multiread?: UInt64 + backup_restore_batch_size_for_keeper_multiread?: UInt64; /** Approximate probability of failure for a keeper request during backup or restore. Valid value is in interval [0.0f, 1.0f] */ - backup_restore_keeper_fault_injection_probability?: Float + backup_restore_keeper_fault_injection_probability?: Float; /** 0 - random seed, otherwise the setting value */ - backup_restore_keeper_fault_injection_seed?: UInt64 + backup_restore_keeper_fault_injection_seed?: UInt64; /** Max retries for keeper operations during backup or restore */ - backup_restore_keeper_max_retries?: UInt64 + backup_restore_keeper_max_retries?: UInt64; /** Initial backoff timeout for [Zoo]Keeper operations during backup or restore */ - backup_restore_keeper_retry_initial_backoff_ms?: UInt64 + backup_restore_keeper_retry_initial_backoff_ms?: UInt64; /** Max backoff timeout for [Zoo]Keeper operations during backup or restore */ - backup_restore_keeper_retry_max_backoff_ms?: UInt64 + backup_restore_keeper_retry_max_backoff_ms?: UInt64; /** Maximum size of data of a [Zoo]Keeper's node during backup */ - backup_restore_keeper_value_max_size?: UInt64 + backup_restore_keeper_value_max_size?: UInt64; /** Text to represent bool value in TSV/CSV formats. */ - bool_false_representation?: string + bool_false_representation?: string; /** Text to represent bool value in TSV/CSV formats. */ - bool_true_representation?: string + bool_true_representation?: string; /** Calculate text stack trace in case of exceptions during query execution. This is the default. It requires symbol lookups that may slow down fuzzing tests when huge amount of wrong queries are executed. In normal cases you should not disable this option. */ - calculate_text_stack_trace?: Bool + calculate_text_stack_trace?: Bool; /** Cancel HTTP readonly queries when a client closes the connection without waiting for response. * @see https://clickhouse.com/docs/operations/settings/settings#cancel_http_readonly_queries_on_client_close */ - cancel_http_readonly_queries_on_client_close?: Bool + cancel_http_readonly_queries_on_client_close?: Bool; /** CAST operator into IPv4, CAST operator into IPV6 type, toIPv4, toIPv6 functions will return default value instead of throwing exception on conversion error. */ - cast_ipv4_ipv6_default_on_conversion_error?: Bool + cast_ipv4_ipv6_default_on_conversion_error?: Bool; /** CAST operator keep Nullable for result data type */ - cast_keep_nullable?: Bool + cast_keep_nullable?: Bool; /** Return check query result as single 1/0 value */ - check_query_single_value_result?: Bool + check_query_single_value_result?: Bool; /** Check that DDL query (such as DROP TABLE or RENAME) will not break referential dependencies */ - check_referential_table_dependencies?: Bool + check_referential_table_dependencies?: Bool; /** Check that DDL query (such as DROP TABLE or RENAME) will not break dependencies */ - check_table_dependencies?: Bool + check_table_dependencies?: Bool; /** Validate checksums on reading. It is enabled by default and should be always enabled in production. Please do not expect any benefits in disabling this setting. It may only be used for experiments and benchmarks. The setting only applicable for tables of MergeTree family. Checksums are always validated for other table engines and when receiving data over network. */ - checksum_on_read?: Bool + checksum_on_read?: Bool; /** Cluster for a shard in which current server is located */ - cluster_for_parallel_replicas?: string + cluster_for_parallel_replicas?: string; /** Enable collecting hash table statistics to optimize memory allocation */ - collect_hash_table_stats_during_aggregation?: Bool + collect_hash_table_stats_during_aggregation?: Bool; /** The list of column names to use in schema inference for formats without column names. The format: 'column1,column2,column3,...' */ - column_names_for_schema_inference?: string + column_names_for_schema_inference?: string; /** Changes other settings according to provided ClickHouse version. If we know that we changed some behaviour in ClickHouse by changing some settings in some version, this compatibility setting will control these settings */ - compatibility?: string + compatibility?: string; /** Ignore AUTO_INCREMENT keyword in column declaration if true, otherwise return error. It simplifies migration from MySQL */ - compatibility_ignore_auto_increment_in_create_table?: Bool + compatibility_ignore_auto_increment_in_create_table?: Bool; /** Compatibility ignore collation in create table */ - compatibility_ignore_collation_in_create_table?: Bool + compatibility_ignore_collation_in_create_table?: Bool; /** Compile aggregate functions to native code. This feature has a bug and should not be used. */ - compile_aggregate_expressions?: Bool + compile_aggregate_expressions?: Bool; /** Compile some scalar functions and operators to native code. */ - compile_expressions?: Bool + compile_expressions?: Bool; /** Compile sort description to native code. */ - compile_sort_description?: Bool + compile_sort_description?: Bool; /** Connection timeout if there are no replicas. */ - connect_timeout?: Seconds + connect_timeout?: Seconds; /** Connection timeout for selecting first healthy replica. */ - connect_timeout_with_failover_ms?: Milliseconds + connect_timeout_with_failover_ms?: Milliseconds; /** Connection timeout for selecting first healthy replica (for secure connections). */ - connect_timeout_with_failover_secure_ms?: Milliseconds + connect_timeout_with_failover_secure_ms?: Milliseconds; /** The wait time when the connection pool is full. */ - connection_pool_max_wait_ms?: Milliseconds + connection_pool_max_wait_ms?: Milliseconds; /** The maximum number of attempts to connect to replicas. */ - connections_with_failover_max_tries?: UInt64 + connections_with_failover_max_tries?: UInt64; /** Convert SELECT query to CNF */ - convert_query_to_cnf?: Bool + convert_query_to_cnf?: Bool; /** What aggregate function to use for implementation of count(DISTINCT ...) */ - count_distinct_implementation?: string + count_distinct_implementation?: string; /** Rewrite count distinct to subquery of group by */ - count_distinct_optimization?: Bool + count_distinct_optimization?: Bool; /** Use inner join instead of comma/cross join if there're joining expressions in the WHERE section. Values: 0 - no rewrite, 1 - apply if possible for comma/cross, 2 - force rewrite all comma joins, cross - if possible */ - cross_to_inner_join_rewrite?: UInt64 + cross_to_inner_join_rewrite?: UInt64; /** Data types without NULL or NOT NULL will make Nullable */ - data_type_default_nullable?: Bool + data_type_default_nullable?: Bool; /** When executing DROP or DETACH TABLE in Atomic database, wait for table data to be finally dropped or detached. */ - database_atomic_wait_for_drop_and_detach_synchronously?: Bool + database_atomic_wait_for_drop_and_detach_synchronously?: Bool; /** Allow to create only Replicated tables in database with engine Replicated */ - database_replicated_allow_only_replicated_engine?: Bool + database_replicated_allow_only_replicated_engine?: Bool; /** Allow to create only Replicated tables in database with engine Replicated with explicit arguments */ - database_replicated_allow_replicated_engine_arguments?: Bool + database_replicated_allow_replicated_engine_arguments?: Bool; /** Execute DETACH TABLE as DETACH TABLE PERMANENTLY if database engine is Replicated */ - database_replicated_always_detach_permanently?: Bool + database_replicated_always_detach_permanently?: Bool; /** Enforces synchronous waiting for some queries (see also database_atomic_wait_for_drop_and_detach_synchronously, mutation_sync, alter_sync). Not recommended to enable these settings. */ - database_replicated_enforce_synchronous_settings?: Bool + database_replicated_enforce_synchronous_settings?: Bool; /** How long initial DDL query should wait for Replicated database to precess previous DDL queue entries */ - database_replicated_initial_query_timeout_sec?: UInt64 + database_replicated_initial_query_timeout_sec?: UInt64; /** Method to read DateTime from text input formats. Possible values: 'basic', 'best_effort' and 'best_effort_us'. */ - date_time_input_format?: DateTimeInputFormat + date_time_input_format?: DateTimeInputFormat; /** Method to write DateTime to text output. Possible values: 'simple', 'iso', 'unix_timestamp'. */ - date_time_output_format?: DateTimeOutputFormat + date_time_output_format?: DateTimeOutputFormat; /** Check overflow of decimal arithmetic/comparison operations */ - decimal_check_overflow?: Bool + decimal_check_overflow?: Bool; /** Should deduplicate blocks for materialized views if the block is not a duplicate for the table. Use true to always deduplicate in dependent tables. */ - deduplicate_blocks_in_dependent_materialized_views?: Bool + deduplicate_blocks_in_dependent_materialized_views?: Bool; /** Maximum size of right-side table if limit is required but max_bytes_in_join is not set. */ - default_max_bytes_in_join?: UInt64 + default_max_bytes_in_join?: UInt64; /** Default table engine used when ENGINE is not set in CREATE statement. */ - default_table_engine?: DefaultTableEngine + default_table_engine?: DefaultTableEngine; /** Default table engine used when ENGINE is not set in CREATE TEMPORARY statement. */ - default_temporary_table_engine?: DefaultTableEngine + default_temporary_table_engine?: DefaultTableEngine; /** Deduce concrete type of columns of type Object in DESCRIBE query */ - describe_extend_object_types?: Bool + describe_extend_object_types?: Bool; /** If true, subcolumns of all table columns will be included into result of DESCRIBE query */ - describe_include_subcolumns?: Bool + describe_include_subcolumns?: Bool; /** Which dialect will be used to parse query */ - dialect?: Dialect + dialect?: Dialect; /** Execute a pipeline for reading from a dictionary with several threads. It's supported only by DIRECT dictionary with CLICKHOUSE source. */ - dictionary_use_async_executor?: Bool + dictionary_use_async_executor?: Bool; /** Allows to disable decoding/encoding path in uri in URL table engine */ - disable_url_encoding?: Bool + disable_url_encoding?: Bool; /** What to do when the limit is exceeded. */ - distinct_overflow_mode?: OverflowMode + distinct_overflow_mode?: OverflowMode; /** Is the memory-saving mode of distributed aggregation enabled. */ - distributed_aggregation_memory_efficient?: Bool + distributed_aggregation_memory_efficient?: Bool; /** Maximum number of connections with one remote server in the pool. */ - distributed_connections_pool_size?: UInt64 + distributed_connections_pool_size?: UInt64; /** Compatibility version of distributed DDL (ON CLUSTER) queries */ - distributed_ddl_entry_format_version?: UInt64 + distributed_ddl_entry_format_version?: UInt64; /** Format of distributed DDL query result */ - distributed_ddl_output_mode?: DistributedDDLOutputMode + distributed_ddl_output_mode?: DistributedDDLOutputMode; /** Timeout for DDL query responses from all hosts in cluster. If a ddl request has not been performed on all hosts, a response will contain a timeout error and a request will be executed in an async mode. Negative value means infinite. Zero means async mode. */ - distributed_ddl_task_timeout?: Int64 + distributed_ddl_task_timeout?: Int64; /** Should StorageDistributed DirectoryMonitors try to batch individual inserts into bigger ones. */ - distributed_directory_monitor_batch_inserts?: Bool + distributed_directory_monitor_batch_inserts?: Bool; /** Maximum sleep time for StorageDistributed DirectoryMonitors, it limits exponential growth too. */ - distributed_directory_monitor_max_sleep_time_ms?: Milliseconds + distributed_directory_monitor_max_sleep_time_ms?: Milliseconds; /** Sleep time for StorageDistributed DirectoryMonitors, in case of any errors delay grows exponentially. */ - distributed_directory_monitor_sleep_time_ms?: Milliseconds + distributed_directory_monitor_sleep_time_ms?: Milliseconds; /** Should StorageDistributed DirectoryMonitors try to split batch into smaller in case of failures. */ - distributed_directory_monitor_split_batch_on_failure?: Bool + distributed_directory_monitor_split_batch_on_failure?: Bool; /** If 1, Do not merge aggregation states from different servers for distributed queries (shards will process query up to the Complete stage, initiator just proxies the data from the shards). If 2 the initiator will apply ORDER BY and LIMIT stages (it is not in case when shard process query up to the Complete stage) */ - distributed_group_by_no_merge?: UInt64 + distributed_group_by_no_merge?: UInt64; /** How are distributed subqueries performed inside IN or JOIN sections? */ - distributed_product_mode?: DistributedProductMode + distributed_product_mode?: DistributedProductMode; /** If 1, LIMIT will be applied on each shard separately. Usually you don't need to use it, since this will be done automatically if it is possible, i.e. for simple query SELECT FROM LIMIT. */ - distributed_push_down_limit?: UInt64 + distributed_push_down_limit?: UInt64; /** Max number of errors per replica, prevents piling up an incredible amount of errors if replica was offline for some time and allows it to be reconsidered in a shorter amount of time. */ - distributed_replica_error_cap?: UInt64 + distributed_replica_error_cap?: UInt64; /** Time period reduces replica error counter by 2 times. */ - distributed_replica_error_half_life?: Seconds + distributed_replica_error_half_life?: Seconds; /** Number of errors that will be ignored while choosing replicas */ - distributed_replica_max_ignored_errors?: UInt64 + distributed_replica_max_ignored_errors?: UInt64; /** Merge parts only in one partition in select final */ - do_not_merge_across_partitions_select_final?: Bool + do_not_merge_across_partitions_select_final?: Bool; /** Return empty result when aggregating by constant keys on empty set. */ - empty_result_for_aggregation_by_constant_keys_on_empty_set?: Bool + empty_result_for_aggregation_by_constant_keys_on_empty_set?: Bool; /** Return empty result when aggregating without keys on empty set. */ - empty_result_for_aggregation_by_empty_set?: Bool + empty_result_for_aggregation_by_empty_set?: Bool; /** Enable/disable the DEFLATE_QPL codec. */ - enable_deflate_qpl_codec?: Bool + enable_deflate_qpl_codec?: Bool; /** Enable query optimization where we analyze function and subqueries results and rewrite query if there're constants there */ - enable_early_constant_folding?: Bool + enable_early_constant_folding?: Bool; /** Enable date functions like toLastDayOfMonth return Date32 results (instead of Date results) for Date32/DateTime64 arguments. */ - enable_extended_results_for_datetime_functions?: Bool + enable_extended_results_for_datetime_functions?: Bool; /** Use cache for remote filesystem. This setting does not turn on/off cache for disks (must be done via disk config), but allows to bypass cache for some queries if intended */ - enable_filesystem_cache?: Bool + enable_filesystem_cache?: Bool; /** Allows to record the filesystem caching log for each query */ - enable_filesystem_cache_log?: Bool + enable_filesystem_cache_log?: Bool; /** Write into cache on write operations. To actually work this setting requires be added to disk config too */ - enable_filesystem_cache_on_write_operations?: Bool + enable_filesystem_cache_on_write_operations?: Bool; /** Log to system.filesystem prefetch_log during query. Should be used only for testing or debugging, not recommended to be turned on by default */ - enable_filesystem_read_prefetches_log?: Bool + enable_filesystem_read_prefetches_log?: Bool; /** Propagate WITH statements to UNION queries and all subqueries */ - enable_global_with_statement?: Bool + enable_global_with_statement?: Bool; /** Compress the result if the client over HTTP said that it understands data compressed by gzip or deflate. */ - enable_http_compression?: Bool + enable_http_compression?: Bool; /** Output stack trace of a job creator when job results in exception */ - enable_job_stack_trace?: Bool + enable_job_stack_trace?: Bool; /** Enable lightweight DELETE mutations for mergetree tables. */ - enable_lightweight_delete?: Bool + enable_lightweight_delete?: Bool; /** Enable memory bound merging strategy for aggregation. */ - enable_memory_bound_merging_of_aggregation_results?: Bool + enable_memory_bound_merging_of_aggregation_results?: Bool; /** Move more conditions from WHERE to PREWHERE and do reads from disk and filtering in multiple steps if there are multiple conditions combined with AND */ - enable_multiple_prewhere_read_steps?: Bool + enable_multiple_prewhere_read_steps?: Bool; /** If it is set to true, optimize predicates to subqueries. */ - enable_optimize_predicate_expression?: Bool + enable_optimize_predicate_expression?: Bool; /** Allow push predicate to final subquery. */ - enable_optimize_predicate_expression_to_final_subquery?: Bool + enable_optimize_predicate_expression_to_final_subquery?: Bool; /** Enable positional arguments in ORDER BY, GROUP BY and LIMIT BY */ - enable_positional_arguments?: Bool + enable_positional_arguments?: Bool; /** Enable reading results of SELECT queries from the query cache */ - enable_reads_from_query_cache?: Bool + enable_reads_from_query_cache?: Bool; /** Enable very explicit logging of S3 requests. Makes sense for debug only. */ - enable_s3_requests_logging?: Bool + enable_s3_requests_logging?: Bool; /** If it is set to true, prevent scalar subqueries from (de)serializing large scalar values and possibly avoid running the same subquery more than once. */ - enable_scalar_subquery_optimization?: Bool + enable_scalar_subquery_optimization?: Bool; /** Allow sharing set objects build for IN subqueries between different tasks of the same mutation. This reduces memory usage and CPU consumption */ - enable_sharing_sets_for_mutations?: Bool + enable_sharing_sets_for_mutations?: Bool; /** Enable use of software prefetch in aggregation */ - enable_software_prefetch_in_aggregation?: Bool + enable_software_prefetch_in_aggregation?: Bool; /** Allow ARRAY JOIN with multiple arrays that have different sizes. When this settings is enabled, arrays will be resized to the longest one. */ - enable_unaligned_array_join?: Bool + enable_unaligned_array_join?: Bool; /** Enable storing results of SELECT queries in the query cache */ - enable_writes_to_query_cache?: Bool + enable_writes_to_query_cache?: Bool; /** Enables or disables creating a new file on each insert in file engine tables if format has suffix. */ - engine_file_allow_create_multiple_files?: Bool + engine_file_allow_create_multiple_files?: Bool; /** Allows to select data from a file engine table without file */ - engine_file_empty_if_not_exists?: Bool + engine_file_empty_if_not_exists?: Bool; /** Allows to skip empty files in file table engine */ - engine_file_skip_empty_files?: Bool + engine_file_skip_empty_files?: Bool; /** Enables or disables truncate before insert in file engine tables */ - engine_file_truncate_on_insert?: Bool + engine_file_truncate_on_insert?: Bool; /** Allows to skip empty files in url table engine */ - engine_url_skip_empty_files?: Bool + engine_url_skip_empty_files?: Bool; /** Method to write Errors to text output. */ - errors_output_format?: string + errors_output_format?: string; /** When enabled, ClickHouse will provide exact value for rows_before_limit_at_least statistic, but with the cost that the data before limit will have to be read completely */ - exact_rows_before_limit?: Bool + exact_rows_before_limit?: Bool; /** Set default mode in EXCEPT query. Possible values: empty string, 'ALL', 'DISTINCT'. If empty, query without mode will throw exception. */ - except_default_mode?: SetOperationMode + except_default_mode?: SetOperationMode; /** Connect timeout in seconds. Now supported only for MySQL */ - external_storage_connect_timeout_sec?: UInt64 + external_storage_connect_timeout_sec?: UInt64; /** Limit maximum number of bytes when table with external engine should flush history data. Now supported only for MySQL table engine, database engine, dictionary and MaterializedMySQL. If equal to 0, this setting is disabled */ - external_storage_max_read_bytes?: UInt64 + external_storage_max_read_bytes?: UInt64; /** Limit maximum number of rows when table with external engine should flush history data. Now supported only for MySQL table engine, database engine, dictionary and MaterializedMySQL. If equal to 0, this setting is disabled */ - external_storage_max_read_rows?: UInt64 + external_storage_max_read_rows?: UInt64; /** Read/write timeout in seconds. Now supported only for MySQL */ - external_storage_rw_timeout_sec?: UInt64 + external_storage_rw_timeout_sec?: UInt64; /** If it is set to true, external table functions will implicitly use Nullable type if needed. Otherwise NULLs will be substituted with default values. Currently supported only by 'mysql', 'postgresql' and 'odbc' table functions. */ - external_table_functions_use_nulls?: Bool + external_table_functions_use_nulls?: Bool; /** If it is set to true, transforming expression to local filter is forbidden for queries to external tables. */ - external_table_strict_query?: Bool + external_table_strict_query?: Bool; /** Max number pairs that can be produced by extractKeyValuePairs function. Used to safeguard against consuming too much memory. */ - extract_kvp_max_pairs_per_row?: UInt64 + extract_kvp_max_pairs_per_row?: UInt64; /** Calculate minimums and maximums of the result columns. They can be output in JSON-formats. */ - extremes?: Bool + extremes?: Bool; /** Suppose max_replica_delay_for_distributed_queries is set and all replicas for the queried table are stale. If this setting is enabled, the query will be performed anyway, otherwise the error will be reported. */ - fallback_to_stale_replicas_for_distributed_queries?: Bool + fallback_to_stale_replicas_for_distributed_queries?: Bool; /** Max remote filesystem cache size that can be downloaded by a single query */ - filesystem_cache_max_download_size?: UInt64 + filesystem_cache_max_download_size?: UInt64; /** Maximum memory usage for prefetches. Zero means unlimited */ - filesystem_prefetch_max_memory_usage?: UInt64 + filesystem_prefetch_max_memory_usage?: UInt64; /** Do not parallelize within one file read less than this amount of bytes. E.g. one reader will not receive a read task of size less than this amount. This setting is recommended to avoid spikes of time for aws getObject requests to aws */ - filesystem_prefetch_min_bytes_for_single_read_task?: UInt64 + filesystem_prefetch_min_bytes_for_single_read_task?: UInt64; /** Prefetch step in bytes. Zero means `auto` - approximately the best prefetch step will be auto deduced, but might not be 100% the best. The actual value might be different because of setting filesystem_prefetch_min_bytes_for_single_read_task */ - filesystem_prefetch_step_bytes?: UInt64 + filesystem_prefetch_step_bytes?: UInt64; /** Prefetch step in marks. Zero means `auto` - approximately the best prefetch step will be auto deduced, but might not be 100% the best. The actual value might be different because of setting filesystem_prefetch_min_bytes_for_single_read_task */ - filesystem_prefetch_step_marks?: UInt64 + filesystem_prefetch_step_marks?: UInt64; /** Maximum number of prefetches. Zero means unlimited. A setting `filesystem_prefetches_max_memory_usage` is more recommended if you want to limit the number of prefetches */ - filesystem_prefetches_limit?: UInt64 + filesystem_prefetches_limit?: UInt64; /** Query with the FINAL modifier by default. If the engine does not support final, it does not have any effect. On queries with multiple tables final is applied only on those that support it. It also works on distributed tables */ - final?: Bool + final?: Bool; /** If true, columns of type Nested will be flattened to separate array columns instead of one array of tuples */ - flatten_nested?: Bool + flatten_nested?: Bool; /** Force the use of optimization when it is applicable, but heuristics decided not to use it */ - force_aggregate_partitions_independently?: Bool + force_aggregate_partitions_independently?: Bool; /** Force use of aggregation in order on remote nodes during distributed aggregation. PLEASE, NEVER CHANGE THIS SETTING VALUE MANUALLY! */ - force_aggregation_in_order?: Bool + force_aggregation_in_order?: Bool; /** Comma separated list of strings or literals with the name of the data skipping indices that should be used during query execution, otherwise an exception will be thrown. */ - force_data_skipping_indices?: string + force_data_skipping_indices?: string; /** Make GROUPING function to return 1 when argument is not used as an aggregation key */ - force_grouping_standard_compatibility?: Bool + force_grouping_standard_compatibility?: Bool; /** Throw an exception if there is a partition key in a table, and it is not used. */ - force_index_by_date?: Bool + force_index_by_date?: Bool; /** If projection optimization is enabled, SELECT queries need to use projection */ - force_optimize_projection?: Bool + force_optimize_projection?: Bool; /** Throw an exception if unused shards cannot be skipped (1 - throw only if the table has the sharding key, 2 - always throw. */ - force_optimize_skip_unused_shards?: UInt64 + force_optimize_skip_unused_shards?: UInt64; /** Same as force_optimize_skip_unused_shards, but accept nesting level until which it will work. */ - force_optimize_skip_unused_shards_nesting?: UInt64 + force_optimize_skip_unused_shards_nesting?: UInt64; /** Throw an exception if there is primary key in a table, and it is not used. */ - force_primary_key?: Bool + force_primary_key?: Bool; /** Recursively remove data on DROP query. Avoids 'Directory not empty' error, but may silently remove detached data */ - force_remove_data_recursively_on_drop?: Bool + force_remove_data_recursively_on_drop?: Bool; /** For AvroConfluent format: Confluent Schema Registry URL. */ - format_avro_schema_registry_url?: URI + format_avro_schema_registry_url?: URI; /** The maximum allowed size for Array in RowBinary format. It prevents allocating large amount of memory in case of corrupted data. 0 means there is no limit */ - format_binary_max_array_size?: UInt64 + format_binary_max_array_size?: UInt64; /** The maximum allowed size for String in RowBinary format. It prevents allocating large amount of memory in case of corrupted data. 0 means there is no limit */ - format_binary_max_string_size?: UInt64 + format_binary_max_string_size?: UInt64; /** How to map ClickHouse Enum and CapnProto Enum */ - format_capn_proto_enum_comparising_mode?: CapnProtoEnumComparingMode + format_capn_proto_enum_comparising_mode?: CapnProtoEnumComparingMode; /** If it is set to true, allow strings in double quotes. */ - format_csv_allow_double_quotes?: Bool + format_csv_allow_double_quotes?: Bool; /** If it is set to true, allow strings in single quotes. */ - format_csv_allow_single_quotes?: Bool + format_csv_allow_single_quotes?: Bool; /** The character to be considered as a delimiter in CSV data. If setting with a string, a string has to have a length of 1. */ - format_csv_delimiter?: Char + format_csv_delimiter?: Char; /** Custom NULL representation in CSV format */ - format_csv_null_representation?: string + format_csv_null_representation?: string; /** Field escaping rule (for CustomSeparated format) */ - format_custom_escaping_rule?: EscapingRule + format_custom_escaping_rule?: EscapingRule; /** Delimiter between fields (for CustomSeparated format) */ - format_custom_field_delimiter?: string + format_custom_field_delimiter?: string; /** Suffix after result set (for CustomSeparated format) */ - format_custom_result_after_delimiter?: string + format_custom_result_after_delimiter?: string; /** Prefix before result set (for CustomSeparated format) */ - format_custom_result_before_delimiter?: string + format_custom_result_before_delimiter?: string; /** Delimiter after field of the last column (for CustomSeparated format) */ - format_custom_row_after_delimiter?: string + format_custom_row_after_delimiter?: string; /** Delimiter before field of the first column (for CustomSeparated format) */ - format_custom_row_before_delimiter?: string + format_custom_row_before_delimiter?: string; /** Delimiter between rows (for CustomSeparated format) */ - format_custom_row_between_delimiter?: string + format_custom_row_between_delimiter?: string; /** Do not hide secrets in SHOW and SELECT queries. */ - format_display_secrets_in_show_and_select?: Bool + format_display_secrets_in_show_and_select?: Bool; /** The name of column that will be used as object names in JSONObjectEachRow format. Column type should be String */ - format_json_object_each_row_column_for_object_name?: string + format_json_object_each_row_column_for_object_name?: string; /** Regular expression (for Regexp format) */ - format_regexp?: string + format_regexp?: string; /** Field escaping rule (for Regexp format) */ - format_regexp_escaping_rule?: EscapingRule + format_regexp_escaping_rule?: EscapingRule; /** Skip lines unmatched by regular expression (for Regexp format) */ - format_regexp_skip_unmatched?: Bool + format_regexp_skip_unmatched?: Bool; /** Schema identifier (used by schema-based formats) */ - format_schema?: string + format_schema?: string; /** Path to file which contains format string for result set (for Template format) */ - format_template_resultset?: string + format_template_resultset?: string; /** Path to file which contains format string for rows (for Template format) */ - format_template_row?: string + format_template_row?: string; /** Delimiter between rows (for Template format) */ - format_template_rows_between_delimiter?: string + format_template_rows_between_delimiter?: string; /** Custom NULL representation in TSV format */ - format_tsv_null_representation?: string + format_tsv_null_representation?: string; /** Formatter '%f' in function 'formatDateTime()' produces a single zero instead of six zeros if the formatted value has no fractional seconds. */ - formatdatetime_f_prints_single_zero?: Bool + formatdatetime_f_prints_single_zero?: Bool; /** Formatter '%M' in functions 'formatDateTime()' and 'parseDateTime()' produces the month name instead of minutes. */ - formatdatetime_parsedatetime_m_is_month_name?: Bool + formatdatetime_parsedatetime_m_is_month_name?: Bool; /** Do fsync after changing metadata for tables and databases (.sql files). Could be disabled in case of poor latency on server with high load of DDL queries and high load of disk subsystem. */ - fsync_metadata?: Bool + fsync_metadata?: Bool; /** Choose function implementation for specific target or variant (experimental). If empty enable all of them. */ - function_implementation?: string + function_implementation?: string; /** Allow function JSON_VALUE to return complex type, such as: struct, array, map. */ - function_json_value_return_type_allow_complex?: Bool + function_json_value_return_type_allow_complex?: Bool; /** Allow function JSON_VALUE to return nullable type. */ - function_json_value_return_type_allow_nullable?: Bool + function_json_value_return_type_allow_nullable?: Bool; /** Maximum number of values generated by function `range` per block of data (sum of array sizes for every row in a block, see also 'max_block_size' and 'min_insert_block_size_rows'). It is a safety threshold. */ - function_range_max_elements_in_block?: UInt64 + function_range_max_elements_in_block?: UInt64; /** Maximum number of microseconds the function `sleep` is allowed to sleep for each block. If a user called it with a larger value, it throws an exception. It is a safety threshold. */ - function_sleep_max_microseconds_per_block?: UInt64 + function_sleep_max_microseconds_per_block?: UInt64; /** Maximum number of allowed addresses (For external storages, table functions, etc). */ - glob_expansion_max_elements?: UInt64 + glob_expansion_max_elements?: UInt64; /** Initial number of grace hash join buckets */ - grace_hash_join_initial_buckets?: UInt64 + grace_hash_join_initial_buckets?: UInt64; /** Limit on the number of grace hash join buckets */ - grace_hash_join_max_buckets?: UInt64 + grace_hash_join_max_buckets?: UInt64; /** What to do when the limit is exceeded. */ - group_by_overflow_mode?: OverflowModeGroupBy + group_by_overflow_mode?: OverflowModeGroupBy; /** From what number of keys, a two-level aggregation starts. 0 - the threshold is not set. */ - group_by_two_level_threshold?: UInt64 + group_by_two_level_threshold?: UInt64; /** From what size of the aggregation state in bytes, a two-level aggregation begins to be used. 0 - the threshold is not set. Two-level aggregation is used when at least one of the thresholds is triggered. */ - group_by_two_level_threshold_bytes?: UInt64 + group_by_two_level_threshold_bytes?: UInt64; /** Treat columns mentioned in ROLLUP, CUBE or GROUPING SETS as Nullable */ - group_by_use_nulls?: Bool + group_by_use_nulls?: Bool; /** Timeout for receiving HELLO packet from replicas. */ - handshake_timeout_ms?: Milliseconds + handshake_timeout_ms?: Milliseconds; /** Enables or disables creating a new file on each insert in hdfs engine tables */ - hdfs_create_new_file_on_insert?: Bool + hdfs_create_new_file_on_insert?: Bool; /** The actual number of replications can be specified when the hdfs file is created. */ - hdfs_replication?: UInt64 + hdfs_replication?: UInt64; /** Allow to skip empty files in hdfs table engine */ - hdfs_skip_empty_files?: Bool + hdfs_skip_empty_files?: Bool; /** Enables or disables truncate before insert in s3 engine tables */ - hdfs_truncate_on_insert?: Bool + hdfs_truncate_on_insert?: Bool; /** Connection timeout for establishing connection with replica for Hedged requests */ - hedged_connection_timeout_ms?: Milliseconds + hedged_connection_timeout_ms?: Milliseconds; /** Expired time for hsts. 0 means disable HSTS. */ - hsts_max_age?: UInt64 + hsts_max_age?: UInt64; /** HTTP connection timeout. */ - http_connection_timeout?: Seconds + http_connection_timeout?: Seconds; /** Do not send HTTP headers X-ClickHouse-Progress more frequently than at each specified interval. */ - http_headers_progress_interval_ms?: UInt64 + http_headers_progress_interval_ms?: UInt64; /** Maximum value of a chunk size in HTTP chunked transfer encoding */ - http_max_chunk_size?: UInt64 + http_max_chunk_size?: UInt64; /** Maximum length of field name in HTTP header */ - http_max_field_name_size?: UInt64 + http_max_field_name_size?: UInt64; /** Maximum length of field value in HTTP header */ - http_max_field_value_size?: UInt64 + http_max_field_value_size?: UInt64; /** Maximum number of fields in HTTP header */ - http_max_fields?: UInt64 + http_max_fields?: UInt64; /** Limit on size of multipart/form-data content. This setting cannot be parsed from URL parameters and should be set in user profile. Note that content is parsed and external tables are created in memory before start of query execution. And this is the only limit that has effect on that stage (limits on max memory usage and max execution time have no effect while reading HTTP form data). */ - http_max_multipart_form_data_size?: UInt64 + http_max_multipart_form_data_size?: UInt64; /** Limit on size of request data used as a query parameter in predefined HTTP requests. */ - http_max_request_param_data_size?: UInt64 + http_max_request_param_data_size?: UInt64; /** Max attempts to read via http. */ - http_max_tries?: UInt64 + http_max_tries?: UInt64; /** Maximum URI length of HTTP request */ - http_max_uri_size?: UInt64 + http_max_uri_size?: UInt64; /** If you uncompress the POST data from the client compressed by the native format, do not check the checksum. */ - http_native_compression_disable_checksumming_on_decompress?: Bool + http_native_compression_disable_checksumming_on_decompress?: Bool; /** HTTP receive timeout */ - http_receive_timeout?: Seconds + http_receive_timeout?: Seconds; /** The number of bytes to buffer in the server memory before sending a HTTP response to the client or flushing to disk (when http_wait_end_of_query is enabled). */ - http_response_buffer_size?: UInt64 + http_response_buffer_size?: UInt64; /** Min milliseconds for backoff, when retrying read via http */ - http_retry_initial_backoff_ms?: UInt64 + http_retry_initial_backoff_ms?: UInt64; /** Max milliseconds for backoff, when retrying read via http */ - http_retry_max_backoff_ms?: UInt64 + http_retry_max_backoff_ms?: UInt64; /** HTTP send timeout */ - http_send_timeout?: Seconds + http_send_timeout?: Seconds; /** Skip url's for globs with HTTP_NOT_FOUND error */ - http_skip_not_found_url_for_globs?: Bool + http_skip_not_found_url_for_globs?: Bool; /** Enable HTTP response buffering on the server-side. */ - http_wait_end_of_query?: Bool + http_wait_end_of_query?: Bool; /** Compression level - used if the client on HTTP said that it understands data compressed by gzip or deflate. */ - http_zlib_compression_level?: Int64 + http_zlib_compression_level?: Int64; /** Close idle TCP connections after specified number of seconds. */ - idle_connection_timeout?: UInt64 + idle_connection_timeout?: UInt64; /** Comma separated list of strings or literals with the name of the data skipping indices that should be excluded during query execution. */ - ignore_data_skipping_indices?: string + ignore_data_skipping_indices?: string; /** If enabled and not already inside a transaction, wraps the query inside a full transaction (begin + commit or rollback) */ - implicit_transaction?: Bool + implicit_transaction?: Bool; /** Maximum absolute amount of errors while reading text formats (like CSV, TSV). In case of error, if at least absolute or relative amount of errors is lower than corresponding value, will skip until next line and continue. */ - input_format_allow_errors_num?: UInt64 + input_format_allow_errors_num?: UInt64; /** Maximum relative amount of errors while reading text formats (like CSV, TSV). In case of error, if at least absolute or relative amount of errors is lower than corresponding value, will skip until next line and continue. */ - input_format_allow_errors_ratio?: Float + input_format_allow_errors_ratio?: Float; /** Allow seeks while reading in ORC/Parquet/Arrow input formats */ - input_format_allow_seeks?: Bool + input_format_allow_seeks?: Bool; /** Allow missing columns while reading Arrow input formats */ - input_format_arrow_allow_missing_columns?: Bool + input_format_arrow_allow_missing_columns?: Bool; /** Ignore case when matching Arrow columns with CH columns. */ - input_format_arrow_case_insensitive_column_matching?: Bool + input_format_arrow_case_insensitive_column_matching?: Bool; /** Allow to insert array of structs into Nested table in Arrow input format. */ - input_format_arrow_import_nested?: Bool + input_format_arrow_import_nested?: Bool; /** Skip columns with unsupported types while schema inference for format Arrow */ - input_format_arrow_skip_columns_with_unsupported_types_in_schema_inference?: Bool + input_format_arrow_skip_columns_with_unsupported_types_in_schema_inference?: Bool; /** For Avro/AvroConfluent format: when field is not found in schema use default value instead of error */ - input_format_avro_allow_missing_fields?: Bool + input_format_avro_allow_missing_fields?: Bool; /** For Avro/AvroConfluent format: insert default in case of null and non Nullable column */ - input_format_avro_null_as_default?: Bool + input_format_avro_null_as_default?: Bool; /** Skip fields with unsupported types while schema inference for format BSON. */ - input_format_bson_skip_fields_with_unsupported_types_in_schema_inference?: Bool + input_format_bson_skip_fields_with_unsupported_types_in_schema_inference?: Bool; /** Skip columns with unsupported types while schema inference for format CapnProto */ - input_format_capn_proto_skip_fields_with_unsupported_types_in_schema_inference?: Bool + input_format_capn_proto_skip_fields_with_unsupported_types_in_schema_inference?: Bool; /** Ignore extra columns in CSV input (if file has more columns than expected) and treat missing fields in CSV input as default values */ - input_format_csv_allow_variable_number_of_columns?: Bool + input_format_csv_allow_variable_number_of_columns?: Bool; /** Allow to use spaces and tabs(\\t) as field delimiter in the CSV strings */ - input_format_csv_allow_whitespace_or_tab_as_delimiter?: Bool + input_format_csv_allow_whitespace_or_tab_as_delimiter?: Bool; /** When reading Array from CSV, expect that its elements were serialized in nested CSV and then put into string. Example: `"[""Hello"", ""world"", ""42"""" TV""]"`. Braces around array can be omitted. */ - input_format_csv_arrays_as_nested_csv?: Bool + input_format_csv_arrays_as_nested_csv?: Bool; /** Automatically detect header with names and types in CSV format */ - input_format_csv_detect_header?: Bool + input_format_csv_detect_header?: Bool; /** Treat empty fields in CSV input as default values. */ - input_format_csv_empty_as_default?: Bool + input_format_csv_empty_as_default?: Bool; /** Treat inserted enum values in CSV formats as enum indices */ - input_format_csv_enum_as_number?: Bool + input_format_csv_enum_as_number?: Bool; /** Skip specified number of lines at the beginning of data in CSV format */ - input_format_csv_skip_first_lines?: UInt64 + input_format_csv_skip_first_lines?: UInt64; /** Skip trailing empty lines in CSV format */ - input_format_csv_skip_trailing_empty_lines?: Bool + input_format_csv_skip_trailing_empty_lines?: Bool; /** Trims spaces and tabs (\\t) characters at the beginning and end in CSV strings */ - input_format_csv_trim_whitespaces?: Bool + input_format_csv_trim_whitespaces?: Bool; /** Use some tweaks and heuristics to infer schema in CSV format */ - input_format_csv_use_best_effort_in_schema_inference?: Bool + input_format_csv_use_best_effort_in_schema_inference?: Bool; /** Allow to set default value to column when CSV field deserialization failed on bad value */ - input_format_csv_use_default_on_bad_values?: Bool + input_format_csv_use_default_on_bad_values?: Bool; /** Automatically detect header with names and types in CustomSeparated format */ - input_format_custom_detect_header?: Bool + input_format_custom_detect_header?: Bool; /** Skip trailing empty lines in CustomSeparated format */ - input_format_custom_skip_trailing_empty_lines?: Bool + input_format_custom_skip_trailing_empty_lines?: Bool; /** For input data calculate default expressions for omitted fields (it works for JSONEachRow, -WithNames, -WithNamesAndTypes formats). */ - input_format_defaults_for_omitted_fields?: Bool + input_format_defaults_for_omitted_fields?: Bool; /** Delimiter between collection(array or map) items in Hive Text File */ - input_format_hive_text_collection_items_delimiter?: Char + input_format_hive_text_collection_items_delimiter?: Char; /** Delimiter between fields in Hive Text File */ - input_format_hive_text_fields_delimiter?: Char + input_format_hive_text_fields_delimiter?: Char; /** Delimiter between a pair of map key/values in Hive Text File */ - input_format_hive_text_map_keys_delimiter?: Char + input_format_hive_text_map_keys_delimiter?: Char; /** Map nested JSON data to nested tables (it works for JSONEachRow format). */ - input_format_import_nested_json?: Bool + input_format_import_nested_json?: Bool; /** Deserialization of IPv4 will use default values instead of throwing exception on conversion error. */ - input_format_ipv4_default_on_conversion_error?: Bool + input_format_ipv4_default_on_conversion_error?: Bool; /** Deserialization of IPV6 will use default values instead of throwing exception on conversion error. */ - input_format_ipv6_default_on_conversion_error?: Bool + input_format_ipv6_default_on_conversion_error?: Bool; /** Insert default value in named tuple element if it's missing in json object */ - input_format_json_defaults_for_missing_elements_in_named_tuple?: Bool + input_format_json_defaults_for_missing_elements_in_named_tuple?: Bool; /** Ignore unknown keys in json object for named tuples */ - input_format_json_ignore_unknown_keys_in_named_tuple?: Bool + input_format_json_ignore_unknown_keys_in_named_tuple?: Bool; /** Deserialize named tuple columns as JSON objects */ - input_format_json_named_tuples_as_objects?: Bool + input_format_json_named_tuples_as_objects?: Bool; /** Allow to parse bools as numbers in JSON input formats */ - input_format_json_read_bools_as_numbers?: Bool + input_format_json_read_bools_as_numbers?: Bool; /** Allow to parse numbers as strings in JSON input formats */ - input_format_json_read_numbers_as_strings?: Bool + input_format_json_read_numbers_as_strings?: Bool; /** Allow to parse JSON objects as strings in JSON input formats */ - input_format_json_read_objects_as_strings?: Bool + input_format_json_read_objects_as_strings?: Bool; /** Throw an exception if JSON string contains bad escape sequence. If disabled, bad escape sequences will remain as is in the data. Default value - true. */ - input_format_json_throw_on_bad_escape_sequence?: Bool + input_format_json_throw_on_bad_escape_sequence?: Bool; /** Try to infer numbers from string fields while schema inference */ - input_format_json_try_infer_numbers_from_strings?: Bool + input_format_json_try_infer_numbers_from_strings?: Bool; /** For JSON/JSONCompact/JSONColumnsWithMetadata input formats this controls whether format parser should check if data types from input metadata match data types of the corresponding columns from the table */ - input_format_json_validate_types_from_metadata?: Bool + input_format_json_validate_types_from_metadata?: Bool; /** The maximum bytes of data to read for automatic schema inference */ - input_format_max_bytes_to_read_for_schema_inference?: UInt64 + input_format_max_bytes_to_read_for_schema_inference?: UInt64; /** The maximum rows of data to read for automatic schema inference */ - input_format_max_rows_to_read_for_schema_inference?: UInt64 + input_format_max_rows_to_read_for_schema_inference?: UInt64; /** The number of columns in inserted MsgPack data. Used for automatic schema inference from data. */ - input_format_msgpack_number_of_columns?: UInt64 + input_format_msgpack_number_of_columns?: UInt64; /** Match columns from table in MySQL dump and columns from ClickHouse table by names */ - input_format_mysql_dump_map_column_names?: Bool + input_format_mysql_dump_map_column_names?: Bool; /** Name of the table in MySQL dump from which to read data */ - input_format_mysql_dump_table_name?: string + input_format_mysql_dump_table_name?: string; /** Allow data types conversion in Native input format */ - input_format_native_allow_types_conversion?: Bool + input_format_native_allow_types_conversion?: Bool; /** Initialize null fields with default values if the data type of this field is not nullable and it is supported by the input format */ - input_format_null_as_default?: Bool + input_format_null_as_default?: Bool; /** Allow missing columns while reading ORC input formats */ - input_format_orc_allow_missing_columns?: Bool + input_format_orc_allow_missing_columns?: Bool; /** Ignore case when matching ORC columns with CH columns. */ - input_format_orc_case_insensitive_column_matching?: Bool + input_format_orc_case_insensitive_column_matching?: Bool; /** Allow to insert array of structs into Nested table in ORC input format. */ - input_format_orc_import_nested?: Bool + input_format_orc_import_nested?: Bool; /** Batch size when reading ORC stripes. */ - input_format_orc_row_batch_size?: Int64 + input_format_orc_row_batch_size?: Int64; /** Skip columns with unsupported types while schema inference for format ORC */ - input_format_orc_skip_columns_with_unsupported_types_in_schema_inference?: Bool + input_format_orc_skip_columns_with_unsupported_types_in_schema_inference?: Bool; /** Enable parallel parsing for some data formats. */ - input_format_parallel_parsing?: Bool + input_format_parallel_parsing?: Bool; /** Allow missing columns while reading Parquet input formats */ - input_format_parquet_allow_missing_columns?: Bool + input_format_parquet_allow_missing_columns?: Bool; /** Ignore case when matching Parquet columns with CH columns. */ - input_format_parquet_case_insensitive_column_matching?: Bool + input_format_parquet_case_insensitive_column_matching?: Bool; /** Allow to insert array of structs into Nested table in Parquet input format. */ - input_format_parquet_import_nested?: Bool + input_format_parquet_import_nested?: Bool; /** Max block size for parquet reader. */ - input_format_parquet_max_block_size?: UInt64 + input_format_parquet_max_block_size?: UInt64; /** Avoid reordering rows when reading from Parquet files. Usually makes it much slower. */ - input_format_parquet_preserve_order?: Bool + input_format_parquet_preserve_order?: Bool; /** Skip columns with unsupported types while schema inference for format Parquet */ - input_format_parquet_skip_columns_with_unsupported_types_in_schema_inference?: Bool + input_format_parquet_skip_columns_with_unsupported_types_in_schema_inference?: Bool; /** Enable Google wrappers for regular non-nested columns, e.g. google.protobuf.StringValue 'str' for String column 'str'. For Nullable columns empty wrappers are recognized as defaults, and missing as nulls */ - input_format_protobuf_flatten_google_wrappers?: Bool + input_format_protobuf_flatten_google_wrappers?: Bool; /** Skip fields with unsupported types while schema inference for format Protobuf */ - input_format_protobuf_skip_fields_with_unsupported_types_in_schema_inference?: Bool + input_format_protobuf_skip_fields_with_unsupported_types_in_schema_inference?: Bool; /** Path of the file used to record errors while reading text formats (CSV, TSV). */ - input_format_record_errors_file_path?: string + input_format_record_errors_file_path?: string; /** Skip columns with unknown names from input data (it works for JSONEachRow, -WithNames, -WithNamesAndTypes and TSKV formats). */ - input_format_skip_unknown_fields?: Bool + input_format_skip_unknown_fields?: Bool; /** Try to infer dates from string fields while schema inference in text formats */ - input_format_try_infer_dates?: Bool + input_format_try_infer_dates?: Bool; /** Try to infer datetimes from string fields while schema inference in text formats */ - input_format_try_infer_datetimes?: Bool + input_format_try_infer_datetimes?: Bool; /** Try to infer integers instead of floats while schema inference in text formats */ - input_format_try_infer_integers?: Bool + input_format_try_infer_integers?: Bool; /** Automatically detect header with names and types in TSV format */ - input_format_tsv_detect_header?: Bool + input_format_tsv_detect_header?: Bool; /** Treat empty fields in TSV input as default values. */ - input_format_tsv_empty_as_default?: Bool + input_format_tsv_empty_as_default?: Bool; /** Treat inserted enum values in TSV formats as enum indices. */ - input_format_tsv_enum_as_number?: Bool + input_format_tsv_enum_as_number?: Bool; /** Skip specified number of lines at the beginning of data in TSV format */ - input_format_tsv_skip_first_lines?: UInt64 + input_format_tsv_skip_first_lines?: UInt64; /** Skip trailing empty lines in TSV format */ - input_format_tsv_skip_trailing_empty_lines?: Bool + input_format_tsv_skip_trailing_empty_lines?: Bool; /** Use some tweaks and heuristics to infer schema in TSV format */ - input_format_tsv_use_best_effort_in_schema_inference?: Bool + input_format_tsv_use_best_effort_in_schema_inference?: Bool; /** For Values format: when parsing and interpreting expressions using template, check actual type of literal to avoid possible overflow and precision issues. */ - input_format_values_accurate_types_of_literals?: Bool + input_format_values_accurate_types_of_literals?: Bool; /** For Values format: if the field could not be parsed by streaming parser, run SQL parser, deduce template of the SQL expression, try to parse all rows using template and then interpret expression for all rows. */ - input_format_values_deduce_templates_of_expressions?: Bool + input_format_values_deduce_templates_of_expressions?: Bool; /** For Values format: if the field could not be parsed by streaming parser, run SQL parser and try to interpret it as SQL expression. */ - input_format_values_interpret_expressions?: Bool + input_format_values_interpret_expressions?: Bool; /** For -WithNames input formats this controls whether format parser is to assume that column data appear in the input exactly as they are specified in the header. */ - input_format_with_names_use_header?: Bool + input_format_with_names_use_header?: Bool; /** For -WithNamesAndTypes input formats this controls whether format parser should check if data types from the input match data types from the header. */ - input_format_with_types_use_header?: Bool + input_format_with_types_use_header?: Bool; /** If setting is enabled, Allow materialized columns in INSERT. */ - insert_allow_materialized_columns?: Bool + insert_allow_materialized_columns?: Bool; /** For INSERT queries in the replicated table, specifies that deduplication of insertings blocks should be performed */ - insert_deduplicate?: Bool + insert_deduplicate?: Bool; /** If not empty, used for duplicate detection instead of data digest */ - insert_deduplication_token?: string + insert_deduplication_token?: string; /** If setting is enabled, inserting into distributed table will choose a random shard to write when there is no sharding key */ - insert_distributed_one_random_shard?: Bool + insert_distributed_one_random_shard?: Bool; /** If setting is enabled, insert query into distributed waits until data will be sent to all nodes in cluster. */ - insert_distributed_sync?: Bool + insert_distributed_sync?: Bool; /** Timeout for insert query into distributed. Setting is used only with insert_distributed_sync enabled. Zero value means no timeout. */ - insert_distributed_timeout?: UInt64 + insert_distributed_timeout?: UInt64; /** Approximate probability of failure for a keeper request during insert. Valid value is in interval [0.0f, 1.0f] */ - insert_keeper_fault_injection_probability?: Float + insert_keeper_fault_injection_probability?: Float; /** 0 - random seed, otherwise the setting value */ - insert_keeper_fault_injection_seed?: UInt64 + insert_keeper_fault_injection_seed?: UInt64; /** Max retries for keeper operations during insert */ - insert_keeper_max_retries?: UInt64 + insert_keeper_max_retries?: UInt64; /** Initial backoff timeout for keeper operations during insert */ - insert_keeper_retry_initial_backoff_ms?: UInt64 + insert_keeper_retry_initial_backoff_ms?: UInt64; /** Max backoff timeout for keeper operations during insert */ - insert_keeper_retry_max_backoff_ms?: UInt64 + insert_keeper_retry_max_backoff_ms?: UInt64; /** Insert DEFAULT values instead of NULL in INSERT SELECT (UNION ALL) */ - insert_null_as_default?: Bool + insert_null_as_default?: Bool; /** For INSERT queries in the replicated table, wait writing for the specified number of replicas and linearize the addition of the data. 0 - disabled, 'auto' - use majority */ - insert_quorum?: UInt64Auto + insert_quorum?: UInt64Auto; /** For quorum INSERT queries - enable to make parallel inserts without linearizability */ - insert_quorum_parallel?: Bool + insert_quorum_parallel?: Bool; /** If the quorum of replicas did not meet in specified time (in milliseconds), exception will be thrown and insertion is aborted. */ - insert_quorum_timeout?: Milliseconds + insert_quorum_timeout?: Milliseconds; /** If non-zero, when insert into a distributed table, the data will be inserted into the shard `insert_shard_id` synchronously. Possible values range from 1 to `shards_number` of corresponding distributed table */ - insert_shard_id?: UInt64 + insert_shard_id?: UInt64; /** The interval in microseconds to check if the request is cancelled, and to send progress info. */ - interactive_delay?: UInt64 + interactive_delay?: UInt64; /** Set default mode in INTERSECT query. Possible values: empty string, 'ALL', 'DISTINCT'. If empty, query without mode will throw exception. */ - intersect_default_mode?: SetOperationMode + intersect_default_mode?: SetOperationMode; /** Textual representation of Interval. Possible values: 'kusto', 'numeric'. */ - interval_output_format?: IntervalOutputFormat + interval_output_format?: IntervalOutputFormat; /** Specify join algorithm. */ - join_algorithm?: JoinAlgorithm + join_algorithm?: JoinAlgorithm; /** When disabled (default) ANY JOIN will take the first found row for a key. When enabled, it will take the last row seen if there are multiple rows for the same key. */ - join_any_take_last_row?: Bool + join_any_take_last_row?: Bool; /** Set default strictness in JOIN query. Possible values: empty string, 'ANY', 'ALL'. If empty, query without strictness will throw exception. */ - join_default_strictness?: JoinStrictness + join_default_strictness?: JoinStrictness; /** For MergeJoin on disk set how much files it's allowed to sort simultaneously. Then this value bigger then more memory used and then less disk I/O needed. Minimum is 2. */ - join_on_disk_max_files_to_merge?: UInt64 + join_on_disk_max_files_to_merge?: UInt64; /** What to do when the limit is exceeded. */ - join_overflow_mode?: OverflowMode + join_overflow_mode?: OverflowMode; /** Use NULLs for non-joined rows of outer JOINs for types that can be inside Nullable. If false, use default value of corresponding columns data type. */ - join_use_nulls?: Bool + join_use_nulls?: Bool; /** Force joined subqueries and table functions to have aliases for correct name qualification. */ - joined_subquery_requires_alias?: Bool + joined_subquery_requires_alias?: Bool; /** Disable limit on kafka_num_consumers that depends on the number of available CPU cores */ - kafka_disable_num_consumers_limit?: Bool + kafka_disable_num_consumers_limit?: Bool; /** The wait time for reading from Kafka before retry. */ - kafka_max_wait_ms?: Milliseconds + kafka_max_wait_ms?: Milliseconds; /** Enforce additional checks during operations on KeeperMap. E.g. throw an exception on an insert for already existing key */ - keeper_map_strict_mode?: Bool + keeper_map_strict_mode?: Bool; /** List all names of element of large tuple literals in their column names instead of hash. This settings exists only for compatibility reasons. It makes sense to set to 'true', while doing rolling update of cluster from version lower than 21.7 to higher. */ - legacy_column_name_of_tuple_literal?: Bool + legacy_column_name_of_tuple_literal?: Bool; /** Limit on read rows from the most 'end' result for select query, default 0 means no limit length */ - limit?: UInt64 + limit?: UInt64; /** Controls the synchronicity of lightweight DELETE operations. It determines whether a DELETE statement will wait for the operation to complete before returning to the client. */ - lightweight_deletes_sync?: UInt64 + lightweight_deletes_sync?: UInt64; /** The heartbeat interval in seconds to indicate live query is alive. */ - live_view_heartbeat_interval?: Seconds + live_view_heartbeat_interval?: Seconds; /** Which replicas (among healthy replicas) to preferably send a query to (on the first attempt) for distributed processing. */ - load_balancing?: LoadBalancing + load_balancing?: LoadBalancing; /** Which replica to preferably send a query when FIRST_OR_RANDOM load balancing strategy is used. */ - load_balancing_first_offset?: UInt64 + load_balancing_first_offset?: UInt64; /** Load MergeTree marks asynchronously */ - load_marks_asynchronously?: Bool + load_marks_asynchronously?: Bool; /** Method of reading data from local filesystem, one of: read, pread, mmap, io_uring, pread_threadpool. The 'io_uring' method is experimental and does not work for Log, TinyLog, StripeLog, File, Set and Join, and other tables with append-able files in presence of concurrent reads and writes. */ - local_filesystem_read_method?: string + local_filesystem_read_method?: string; /** Should use prefetching when reading data from local filesystem. */ - local_filesystem_read_prefetch?: Bool + local_filesystem_read_prefetch?: Bool; /** How long locking request should wait before failing */ - lock_acquire_timeout?: Seconds + lock_acquire_timeout?: Seconds; /** Log comment into system.query_log table and server log. It can be set to arbitrary string no longer than max_query_size. */ - log_comment?: string + log_comment?: string; /** Log formatted queries and write the log to the system table. */ - log_formatted_queries?: Bool + log_formatted_queries?: Bool; /** Log Processors profile events. */ - log_processors_profiles?: Bool + log_processors_profiles?: Bool; /** Log query performance statistics into the query_log, query_thread_log and query_views_log. */ - log_profile_events?: Bool + log_profile_events?: Bool; /** Log requests and write the log to the system table. */ - log_queries?: Bool + log_queries?: Bool; /** If query length is greater than specified threshold (in bytes), then cut query when writing to query log. Also limit length of printed query in ordinary text log. */ - log_queries_cut_to_length?: UInt64 + log_queries_cut_to_length?: UInt64; /** Minimal time for the query to run, to get to the query_log/query_thread_log/query_views_log. */ - log_queries_min_query_duration_ms?: Milliseconds + log_queries_min_query_duration_ms?: Milliseconds; /** Minimal type in query_log to log, possible values (from low to high): QUERY_START, QUERY_FINISH, EXCEPTION_BEFORE_START, EXCEPTION_WHILE_PROCESSING. */ - log_queries_min_type?: LogQueriesType + log_queries_min_type?: LogQueriesType; /** Log queries with the specified probabality. */ - log_queries_probability?: Float + log_queries_probability?: Float; /** Log query settings into the query_log. */ - log_query_settings?: Bool + log_query_settings?: Bool; /** Log query threads into system.query_thread_log table. This setting have effect only when 'log_queries' is true. */ - log_query_threads?: Bool + log_query_threads?: Bool; /** Log query dependent views into system.query_views_log table. This setting have effect only when 'log_queries' is true. */ - log_query_views?: Bool + log_query_views?: Bool; /** Use LowCardinality type in Native format. Otherwise, convert LowCardinality columns to ordinary for select query, and convert ordinary columns to required LowCardinality for insert query. */ - low_cardinality_allow_in_native_format?: Bool + low_cardinality_allow_in_native_format?: Bool; /** Maximum size (in rows) of shared global dictionary for LowCardinality type. */ - low_cardinality_max_dictionary_size?: UInt64 + low_cardinality_max_dictionary_size?: UInt64; /** LowCardinality type serialization setting. If is true, than will use additional keys when global dictionary overflows. Otherwise, will create several shared dictionaries. */ - low_cardinality_use_single_dictionary_for_part?: Bool + low_cardinality_use_single_dictionary_for_part?: Bool; /** Apply TTL for old data, after ALTER MODIFY TTL query */ - materialize_ttl_after_modify?: Bool + materialize_ttl_after_modify?: Bool; /** Allows to ignore errors for MATERIALIZED VIEW, and deliver original block to the table regardless of MVs */ - materialized_views_ignore_errors?: Bool + materialized_views_ignore_errors?: Bool; /** Maximum number of analyses performed by interpreter. */ - max_analyze_depth?: UInt64 + max_analyze_depth?: UInt64; /** Maximum depth of query syntax tree. Checked after parsing. */ - max_ast_depth?: UInt64 + max_ast_depth?: UInt64; /** Maximum size of query syntax tree in number of nodes. Checked after parsing. */ - max_ast_elements?: UInt64 + max_ast_elements?: UInt64; /** The maximum read speed in bytes per second for particular backup on server. Zero means unlimited. */ - max_backup_bandwidth?: UInt64 + max_backup_bandwidth?: UInt64; /** Maximum block size for reading */ - max_block_size?: UInt64 + max_block_size?: UInt64; /** If memory usage during GROUP BY operation is exceeding this threshold in bytes, activate the 'external aggregation' mode (spill data to disk). Recommended value is half of available system memory. */ - max_bytes_before_external_group_by?: UInt64 + max_bytes_before_external_group_by?: UInt64; /** If memory usage during ORDER BY operation is exceeding this threshold in bytes, activate the 'external sorting' mode (spill data to disk). Recommended value is half of available system memory. */ - max_bytes_before_external_sort?: UInt64 + max_bytes_before_external_sort?: UInt64; /** In case of ORDER BY with LIMIT, when memory usage is higher than specified threshold, perform additional steps of merging blocks before final merge to keep just top LIMIT rows. */ - max_bytes_before_remerge_sort?: UInt64 + max_bytes_before_remerge_sort?: UInt64; /** Maximum total size of state (in uncompressed bytes) in memory for the execution of DISTINCT. */ - max_bytes_in_distinct?: UInt64 + max_bytes_in_distinct?: UInt64; /** Maximum size of the hash table for JOIN (in number of bytes in memory). */ - max_bytes_in_join?: UInt64 + max_bytes_in_join?: UInt64; /** Maximum size of the set (in bytes in memory) resulting from the execution of the IN section. */ - max_bytes_in_set?: UInt64 + max_bytes_in_set?: UInt64; /** Limit on read bytes (after decompression) from the most 'deep' sources. That is, only in the deepest subquery. When reading from a remote server, it is only checked on a remote server. */ - max_bytes_to_read?: UInt64 + max_bytes_to_read?: UInt64; /** Limit on read bytes (after decompression) on the leaf nodes for distributed queries. Limit is applied for local reads only excluding the final merge stage on the root node. */ - max_bytes_to_read_leaf?: UInt64 + max_bytes_to_read_leaf?: UInt64; /** If more than specified amount of (uncompressed) bytes have to be processed for ORDER BY operation, the behavior will be determined by the 'sort_overflow_mode' which by default is - throw an exception */ - max_bytes_to_sort?: UInt64 + max_bytes_to_sort?: UInt64; /** Maximum size (in uncompressed bytes) of the transmitted external table obtained when the GLOBAL IN/JOIN section is executed. */ - max_bytes_to_transfer?: UInt64 + max_bytes_to_transfer?: UInt64; /** If a query requires reading more than specified number of columns, exception is thrown. Zero value means unlimited. This setting is useful to prevent too complex queries. */ - max_columns_to_read?: UInt64 + max_columns_to_read?: UInt64; /** The maximum size of blocks of uncompressed data before compressing for writing to a table. */ - max_compress_block_size?: UInt64 + max_compress_block_size?: UInt64; /** The maximum number of concurrent requests for all users. */ - max_concurrent_queries_for_all_users?: UInt64 + max_concurrent_queries_for_all_users?: UInt64; /** The maximum number of concurrent requests per user. */ - max_concurrent_queries_for_user?: UInt64 + max_concurrent_queries_for_user?: UInt64; /** The maximum number of connections for distributed processing of one query (should be greater than max_threads). */ - max_distributed_connections?: UInt64 + max_distributed_connections?: UInt64; /** Maximum distributed query depth */ - max_distributed_depth?: UInt64 + max_distributed_depth?: UInt64; /** The maximal size of buffer for parallel downloading (e.g. for URL engine) per each thread. */ - max_download_buffer_size?: UInt64 + max_download_buffer_size?: UInt64; /** The maximum number of threads to download data (e.g. for URL engine). */ - max_download_threads?: MaxThreads + max_download_threads?: MaxThreads; /** How many entries hash table statistics collected during aggregation is allowed to have */ - max_entries_for_hash_table_stats?: UInt64 + max_entries_for_hash_table_stats?: UInt64; /** Maximum number of execution rows per second. */ - max_execution_speed?: UInt64 + max_execution_speed?: UInt64; /** Maximum number of execution bytes per second. */ - max_execution_speed_bytes?: UInt64 + max_execution_speed_bytes?: UInt64; /** If query run time exceeded the specified number of seconds, the behavior will be determined by the 'timeout_overflow_mode' which by default is - throw an exception. Note that the timeout is checked and query can stop only in designated places during data processing. It currently cannot stop during merging of aggregation states or during query analysis, and the actual run time will be higher than the value of this setting. */ - max_execution_time?: Seconds + max_execution_time?: Seconds; /** Maximum size of query syntax tree in number of nodes after expansion of aliases and the asterisk. */ - max_expanded_ast_elements?: UInt64 + max_expanded_ast_elements?: UInt64; /** Amount of retries while fetching partition from another host. */ - max_fetch_partition_retries_count?: UInt64 + max_fetch_partition_retries_count?: UInt64; /** The maximum number of threads to read from table with FINAL. */ - max_final_threads?: MaxThreads + max_final_threads?: MaxThreads; /** Max number of http GET redirects hops allowed. Make sure additional security measures are in place to prevent a malicious server to redirect your requests to unexpected services. */ - max_http_get_redirects?: UInt64 + max_http_get_redirects?: UInt64; /** Max length of regexp than can be used in hyperscan multi-match functions. Zero means unlimited. */ - max_hyperscan_regexp_length?: UInt64 + max_hyperscan_regexp_length?: UInt64; /** Max total length of all regexps than can be used in hyperscan multi-match functions (per every function). Zero means unlimited. */ - max_hyperscan_regexp_total_length?: UInt64 + max_hyperscan_regexp_total_length?: UInt64; /** The maximum block size for insertion, if we control the creation of blocks for insertion. */ - max_insert_block_size?: UInt64 + max_insert_block_size?: UInt64; /** The maximum number of streams (columns) to delay final part flush. Default - auto (1000 in case of underlying storage supports parallel write, for example S3 and disabled otherwise) */ - max_insert_delayed_streams_for_parallel_write?: UInt64 + max_insert_delayed_streams_for_parallel_write?: UInt64; /** The maximum number of threads to execute the INSERT SELECT query. Values 0 or 1 means that INSERT SELECT is not run in parallel. Higher values will lead to higher memory usage. Parallel INSERT SELECT has effect only if the SELECT part is run on parallel, see 'max_threads' setting. */ - max_insert_threads?: UInt64 + max_insert_threads?: UInt64; /** Maximum block size for JOIN result (if join algorithm supports it). 0 means unlimited. */ - max_joined_block_size_rows?: UInt64 + max_joined_block_size_rows?: UInt64; /** SELECT queries with LIMIT bigger than this setting cannot use ANN indexes. Helps to prevent memory overflows in ANN search indexes. */ - max_limit_for_ann_queries?: UInt64 + max_limit_for_ann_queries?: UInt64; /** Limit maximum number of inserted blocks after which mergeable blocks are dropped and query is re-executed. */ - max_live_view_insert_blocks_before_refresh?: UInt64 + max_live_view_insert_blocks_before_refresh?: UInt64; /** The maximum speed of local reads in bytes per second. */ - max_local_read_bandwidth?: UInt64 + max_local_read_bandwidth?: UInt64; /** The maximum speed of local writes in bytes per second. */ - max_local_write_bandwidth?: UInt64 + max_local_write_bandwidth?: UInt64; /** Maximum memory usage for processing of single query. Zero means unlimited. */ - max_memory_usage?: UInt64 + max_memory_usage?: UInt64; /** Maximum memory usage for processing all concurrently running queries for the user. Zero means unlimited. */ - max_memory_usage_for_user?: UInt64 + max_memory_usage_for_user?: UInt64; /** The maximum speed of data exchange over the network in bytes per second for a query. Zero means unlimited. */ - max_network_bandwidth?: UInt64 + max_network_bandwidth?: UInt64; /** The maximum speed of data exchange over the network in bytes per second for all concurrently running queries. Zero means unlimited. */ - max_network_bandwidth_for_all_users?: UInt64 + max_network_bandwidth_for_all_users?: UInt64; /** The maximum speed of data exchange over the network in bytes per second for all concurrently running user queries. Zero means unlimited. */ - max_network_bandwidth_for_user?: UInt64 + max_network_bandwidth_for_user?: UInt64; /** The maximum number of bytes (compressed) to receive or transmit over the network for execution of the query. */ - max_network_bytes?: UInt64 + max_network_bytes?: UInt64; /** Maximal number of partitions in table to apply optimization */ - max_number_of_partitions_for_independent_aggregation?: UInt64 + max_number_of_partitions_for_independent_aggregation?: UInt64; /** The maximum number of replicas of each shard used when the query is executed. For consistency (to get different parts of the same partition), this option only works for the specified sampling key. The lag of the replicas is not controlled. */ - max_parallel_replicas?: UInt64 + max_parallel_replicas?: UInt64; /** Maximum parser depth (recursion depth of recursive descend parser). */ - max_parser_depth?: UInt64 + max_parser_depth?: UInt64; /** Limit maximum number of partitions in single INSERTed block. Zero means unlimited. Throw exception if the block contains too many partitions. This setting is a safety threshold, because using large number of partitions is a common misconception. */ - max_partitions_per_insert_block?: UInt64 + max_partitions_per_insert_block?: UInt64; /** Limit the max number of partitions that can be accessed in one query. <= 0 means unlimited. */ - max_partitions_to_read?: Int64 + max_partitions_to_read?: Int64; /** The maximum number of bytes of a query string parsed by the SQL parser. Data in the VALUES clause of INSERT queries is processed by a separate stream parser (that consumes O(1) RAM) and not affected by this restriction. */ - max_query_size?: UInt64 + max_query_size?: UInt64; /** The maximum size of the buffer to read from the filesystem. */ - max_read_buffer_size?: UInt64 + max_read_buffer_size?: UInt64; /** The maximum size of the buffer to read from local filesystem. If set to 0 then max_read_buffer_size will be used. */ - max_read_buffer_size_local_fs?: UInt64 + max_read_buffer_size_local_fs?: UInt64; /** The maximum size of the buffer to read from remote filesystem. If set to 0 then max_read_buffer_size will be used. */ - max_read_buffer_size_remote_fs?: UInt64 + max_read_buffer_size_remote_fs?: UInt64; /** The maximum speed of data exchange over the network in bytes per second for read. */ - max_remote_read_network_bandwidth?: UInt64 + max_remote_read_network_bandwidth?: UInt64; /** The maximum speed of data exchange over the network in bytes per second for write. */ - max_remote_write_network_bandwidth?: UInt64 + max_remote_write_network_bandwidth?: UInt64; /** If set, distributed queries of Replicated tables will choose servers with replication delay in seconds less than the specified value (not inclusive). Zero means do not take delay into account. */ - max_replica_delay_for_distributed_queries?: UInt64 + max_replica_delay_for_distributed_queries?: UInt64; /** Limit on result size in bytes (uncompressed). The query will stop after processing a block of data if the threshold is met, but it will not cut the last block of the result, therefore the result size can be larger than the threshold. Caveats: the result size in memory is taken into account for this threshold. Even if the result size is small, it can reference larger data structures in memory, representing dictionaries of LowCardinality columns, and Arenas of AggregateFunction columns, so the threshold can be exceeded despite the small result size. The setting is fairly low level and should be used with caution. */ - max_result_bytes?: UInt64 + max_result_bytes?: UInt64; /** Limit on result size in rows. The query will stop after processing a block of data if the threshold is met, but it will not cut the last block of the result, therefore the result size can be larger than the threshold. */ - max_result_rows?: UInt64 + max_result_rows?: UInt64; /** Maximum number of elements during execution of DISTINCT. */ - max_rows_in_distinct?: UInt64 + max_rows_in_distinct?: UInt64; /** Maximum size of the hash table for JOIN (in number of rows). */ - max_rows_in_join?: UInt64 + max_rows_in_join?: UInt64; /** Maximum size of the set (in number of elements) resulting from the execution of the IN section. */ - max_rows_in_set?: UInt64 + max_rows_in_set?: UInt64; /** Maximal size of the set to filter joined tables by each other row sets before joining. 0 - disable. */ - max_rows_in_set_to_optimize_join?: UInt64 + max_rows_in_set_to_optimize_join?: UInt64; /** If aggregation during GROUP BY is generating more than specified number of rows (unique GROUP BY keys), the behavior will be determined by the 'group_by_overflow_mode' which by default is - throw an exception, but can be also switched to an approximate GROUP BY mode. */ - max_rows_to_group_by?: UInt64 + max_rows_to_group_by?: UInt64; /** Limit on read rows from the most 'deep' sources. That is, only in the deepest subquery. When reading from a remote server, it is only checked on a remote server. */ - max_rows_to_read?: UInt64 + max_rows_to_read?: UInt64; /** Limit on read rows on the leaf nodes for distributed queries. Limit is applied for local reads only excluding the final merge stage on the root node. */ - max_rows_to_read_leaf?: UInt64 + max_rows_to_read_leaf?: UInt64; /** If more than specified amount of records have to be processed for ORDER BY operation, the behavior will be determined by the 'sort_overflow_mode' which by default is - throw an exception */ - max_rows_to_sort?: UInt64 + max_rows_to_sort?: UInt64; /** Maximum size (in rows) of the transmitted external table obtained when the GLOBAL IN/JOIN section is executed. */ - max_rows_to_transfer?: UInt64 + max_rows_to_transfer?: UInt64; /** For how many elements it is allowed to preallocate space in all hash tables in total before aggregation */ - max_size_to_preallocate_for_aggregation?: UInt64 + max_size_to_preallocate_for_aggregation?: UInt64; /** If is not zero, limit the number of reading streams for MergeTree table. */ - max_streams_for_merge_tree_reading?: UInt64 + max_streams_for_merge_tree_reading?: UInt64; /** Ask more streams when reading from Merge table. Streams will be spread across tables that Merge table will use. This allows more even distribution of work across threads and especially helpful when merged tables differ in size. */ - max_streams_multiplier_for_merge_tables?: Float + max_streams_multiplier_for_merge_tables?: Float; /** Allows you to use more sources than the number of threads - to more evenly distribute work across threads. It is assumed that this is a temporary solution, since it will be possible in the future to make the number of sources equal to the number of threads, but for each source to dynamically select available work for itself. */ - max_streams_to_max_threads_ratio?: Float + max_streams_to_max_threads_ratio?: Float; /** If a query has more than specified number of nested subqueries, throw an exception. This allows you to have a sanity check to protect the users of your cluster from going insane with their queries. */ - max_subquery_depth?: UInt64 + max_subquery_depth?: UInt64; /** If a query generates more than the specified number of temporary columns in memory as a result of intermediate calculation, exception is thrown. Zero value means unlimited. This setting is useful to prevent too complex queries. */ - max_temporary_columns?: UInt64 + max_temporary_columns?: UInt64; /** The maximum amount of data consumed by temporary files on disk in bytes for all concurrently running queries. Zero means unlimited. */ - max_temporary_data_on_disk_size_for_query?: UInt64 + max_temporary_data_on_disk_size_for_query?: UInt64; /** The maximum amount of data consumed by temporary files on disk in bytes for all concurrently running user queries. Zero means unlimited. */ - max_temporary_data_on_disk_size_for_user?: UInt64 + max_temporary_data_on_disk_size_for_user?: UInt64; /** Similar to the 'max_temporary_columns' setting but applies only to non-constant columns. This makes sense, because constant columns are cheap and it is reasonable to allow more of them. */ - max_temporary_non_const_columns?: UInt64 + max_temporary_non_const_columns?: UInt64; /** The maximum number of threads to execute the request. By default, it is determined automatically. */ - max_threads?: MaxThreads + max_threads?: MaxThreads; /** Small allocations and deallocations are grouped in thread local variable and tracked or profiled only when amount (in absolute value) becomes larger than specified value. If the value is higher than 'memory_profiler_step' it will be effectively lowered to 'memory_profiler_step'. */ - max_untracked_memory?: UInt64 + max_untracked_memory?: UInt64; /** It represents soft memory limit on the user level. This value is used to compute query overcommit ratio. */ - memory_overcommit_ratio_denominator?: UInt64 + memory_overcommit_ratio_denominator?: UInt64; /** It represents soft memory limit on the global level. This value is used to compute query overcommit ratio. */ - memory_overcommit_ratio_denominator_for_user?: UInt64 + memory_overcommit_ratio_denominator_for_user?: UInt64; /** Collect random allocations and deallocations and write them into system.trace_log with 'MemorySample' trace_type. The probability is for every alloc/free regardless to the size of the allocation. Note that sampling happens only when the amount of untracked memory exceeds 'max_untracked_memory'. You may want to set 'max_untracked_memory' to 0 for extra fine grained sampling. */ - memory_profiler_sample_probability?: Float + memory_profiler_sample_probability?: Float; /** Whenever query memory usage becomes larger than every next step in number of bytes the memory profiler will collect the allocating stack trace. Zero means disabled memory profiler. Values lower than a few megabytes will slow down query processing. */ - memory_profiler_step?: UInt64 + memory_profiler_step?: UInt64; /** For testing of `exception safety` - throw an exception every time you allocate memory with the specified probability. */ - memory_tracker_fault_probability?: Float + memory_tracker_fault_probability?: Float; /** Maximum time thread will wait for memory to be freed in the case of memory overcommit. If timeout is reached and memory is not freed, exception is thrown. */ - memory_usage_overcommit_max_wait_microseconds?: UInt64 + memory_usage_overcommit_max_wait_microseconds?: UInt64; /** If the index segment can contain the required keys, divide it into as many parts and recursively check them. */ - merge_tree_coarse_index_granularity?: UInt64 + merge_tree_coarse_index_granularity?: UInt64; /** The maximum number of bytes per request, to use the cache of uncompressed data. If the request is large, the cache is not used. (For large queries not to flush out the cache.) */ - merge_tree_max_bytes_to_use_cache?: UInt64 + merge_tree_max_bytes_to_use_cache?: UInt64; /** The maximum number of rows per request, to use the cache of uncompressed data. If the request is large, the cache is not used. (For large queries not to flush out the cache.) */ - merge_tree_max_rows_to_use_cache?: UInt64 + merge_tree_max_rows_to_use_cache?: UInt64; /** If at least as many bytes are read from one file, the reading can be parallelized. */ - merge_tree_min_bytes_for_concurrent_read?: UInt64 + merge_tree_min_bytes_for_concurrent_read?: UInt64; /** If at least as many bytes are read from one file, the reading can be parallelized, when reading from remote filesystem. */ - merge_tree_min_bytes_for_concurrent_read_for_remote_filesystem?: UInt64 + merge_tree_min_bytes_for_concurrent_read_for_remote_filesystem?: UInt64; /** You can skip reading more than that number of bytes at the price of one seek per file. */ - merge_tree_min_bytes_for_seek?: UInt64 + merge_tree_min_bytes_for_seek?: UInt64; /** Min bytes to read per task. */ - merge_tree_min_bytes_per_task_for_remote_reading?: UInt64 + merge_tree_min_bytes_per_task_for_remote_reading?: UInt64; /** If at least as many lines are read from one file, the reading can be parallelized. */ - merge_tree_min_rows_for_concurrent_read?: UInt64 + merge_tree_min_rows_for_concurrent_read?: UInt64; /** If at least as many lines are read from one file, the reading can be parallelized, when reading from remote filesystem. */ - merge_tree_min_rows_for_concurrent_read_for_remote_filesystem?: UInt64 + merge_tree_min_rows_for_concurrent_read_for_remote_filesystem?: UInt64; /** You can skip reading more than that number of rows at the price of one seek per file. */ - merge_tree_min_rows_for_seek?: UInt64 + merge_tree_min_rows_for_seek?: UInt64; /** Whether to use constant size tasks for reading from a remote table. */ - merge_tree_use_const_size_tasks_for_remote_reading?: Bool + merge_tree_use_const_size_tasks_for_remote_reading?: Bool; /** If enabled, some of the perf events will be measured throughout queries' execution. */ - metrics_perf_events_enabled?: Bool + metrics_perf_events_enabled?: Bool; /** Comma separated list of perf metrics that will be measured throughout queries' execution. Empty means all events. See PerfEventInfo in sources for the available events. */ - metrics_perf_events_list?: string + metrics_perf_events_list?: string; /** The minimum number of bytes for reading the data with O_DIRECT option during SELECT queries execution. 0 - disabled. */ - min_bytes_to_use_direct_io?: UInt64 + min_bytes_to_use_direct_io?: UInt64; /** The minimum number of bytes for reading the data with mmap option during SELECT queries execution. 0 - disabled. */ - min_bytes_to_use_mmap_io?: UInt64 + min_bytes_to_use_mmap_io?: UInt64; /** The minimum chunk size in bytes, which each thread will parse in parallel. */ - min_chunk_bytes_for_parallel_parsing?: UInt64 + min_chunk_bytes_for_parallel_parsing?: UInt64; /** The actual size of the block to compress, if the uncompressed data less than max_compress_block_size is no less than this value and no less than the volume of data for one mark. */ - min_compress_block_size?: UInt64 + min_compress_block_size?: UInt64; /** The number of identical aggregate expressions before they are JIT-compiled */ - min_count_to_compile_aggregate_expression?: UInt64 + min_count_to_compile_aggregate_expression?: UInt64; /** The number of identical expressions before they are JIT-compiled */ - min_count_to_compile_expression?: UInt64 + min_count_to_compile_expression?: UInt64; /** The number of identical sort descriptions before they are JIT-compiled */ - min_count_to_compile_sort_description?: UInt64 + min_count_to_compile_sort_description?: UInt64; /** Minimum number of execution rows per second. */ - min_execution_speed?: UInt64 + min_execution_speed?: UInt64; /** Minimum number of execution bytes per second. */ - min_execution_speed_bytes?: UInt64 + min_execution_speed_bytes?: UInt64; /** The minimum disk space to keep while writing temporary data used in external sorting and aggregation. */ - min_free_disk_space_for_temporary_data?: UInt64 + min_free_disk_space_for_temporary_data?: UInt64; /** Squash blocks passed to INSERT query to specified size in bytes, if blocks are not big enough. */ - min_insert_block_size_bytes?: UInt64 + min_insert_block_size_bytes?: UInt64; /** Like min_insert_block_size_bytes, but applied only during pushing to MATERIALIZED VIEW (default: min_insert_block_size_bytes) */ - min_insert_block_size_bytes_for_materialized_views?: UInt64 + min_insert_block_size_bytes_for_materialized_views?: UInt64; /** Squash blocks passed to INSERT query to specified size in rows, if blocks are not big enough. */ - min_insert_block_size_rows?: UInt64 + min_insert_block_size_rows?: UInt64; /** Like min_insert_block_size_rows, but applied only during pushing to MATERIALIZED VIEW (default: min_insert_block_size_rows) */ - min_insert_block_size_rows_for_materialized_views?: UInt64 + min_insert_block_size_rows_for_materialized_views?: UInt64; /** Move all viable conditions from WHERE to PREWHERE */ - move_all_conditions_to_prewhere?: Bool + move_all_conditions_to_prewhere?: Bool; /** Move PREWHERE conditions containing primary key columns to the end of AND chain. It is likely that these conditions are taken into account during primary key analysis and thus will not contribute a lot to PREWHERE filtering. */ - move_primary_key_columns_to_end_of_prewhere?: Bool + move_primary_key_columns_to_end_of_prewhere?: Bool; /** Do not add aliases to top level expression list on multiple joins rewrite */ - multiple_joins_try_to_keep_original_names?: Bool + multiple_joins_try_to_keep_original_names?: Bool; /** Wait for synchronous execution of ALTER TABLE UPDATE/DELETE queries (mutations). 0 - execute asynchronously. 1 - wait current server. 2 - wait all replicas if they exist. */ - mutations_sync?: UInt64 + mutations_sync?: UInt64; /** Which MySQL types should be converted to corresponding ClickHouse types (rather than being represented as String). Can be empty or any combination of 'decimal', 'datetime64', 'date2Date32' or 'date2String'. When empty MySQL's DECIMAL and DATETIME/TIMESTAMP with non-zero precision are seen as String on ClickHouse's side. */ - mysql_datatypes_support_level?: MySQLDataTypesSupport + mysql_datatypes_support_level?: MySQLDataTypesSupport; /** The maximum number of rows in MySQL batch insertion of the MySQL storage engine */ - mysql_max_rows_to_insert?: UInt64 + mysql_max_rows_to_insert?: UInt64; /** Allows you to select the method of data compression when writing. */ - network_compression_method?: string + network_compression_method?: string; /** Allows you to select the level of ZSTD compression. */ - network_zstd_compression_level?: Int64 + network_zstd_compression_level?: Int64; /** Normalize function names to their canonical names */ - normalize_function_names?: Bool + normalize_function_names?: Bool; /** If the mutated table contains at least that many unfinished mutations, artificially slow down mutations of table. 0 - disabled */ - number_of_mutations_to_delay?: UInt64 + number_of_mutations_to_delay?: UInt64; /** If the mutated table contains at least that many unfinished mutations, throw 'Too many mutations ...' exception. 0 - disabled */ - number_of_mutations_to_throw?: UInt64 + number_of_mutations_to_throw?: UInt64; /** Connection pool size for each connection settings string in ODBC bridge. */ - odbc_bridge_connection_pool_size?: UInt64 + odbc_bridge_connection_pool_size?: UInt64; /** Use connection pooling in ODBC bridge. If set to false, a new connection is created every time */ - odbc_bridge_use_connection_pooling?: Bool + odbc_bridge_use_connection_pooling?: Bool; /** Offset on read rows from the most 'end' result for select query */ - offset?: UInt64 + offset?: UInt64; /** Probability to start an OpenTelemetry trace for an incoming query. */ - opentelemetry_start_trace_probability?: Float + opentelemetry_start_trace_probability?: Float; /** Collect OpenTelemetry spans for processors. */ - opentelemetry_trace_processors?: Bool + opentelemetry_trace_processors?: Bool; /** Enable GROUP BY optimization for aggregating data in corresponding order in MergeTree tables. */ - optimize_aggregation_in_order?: Bool + optimize_aggregation_in_order?: Bool; /** Eliminates min/max/any/anyLast aggregators of GROUP BY keys in SELECT section */ - optimize_aggregators_of_group_by_keys?: Bool + optimize_aggregators_of_group_by_keys?: Bool; /** Use constraints in order to append index condition (indexHint) */ - optimize_append_index?: Bool + optimize_append_index?: Bool; /** Move arithmetic operations out of aggregation functions */ - optimize_arithmetic_operations_in_aggregate_functions?: Bool + optimize_arithmetic_operations_in_aggregate_functions?: Bool; /** Enable DISTINCT optimization if some columns in DISTINCT form a prefix of sorting. For example, prefix of sorting key in merge tree or ORDER BY statement */ - optimize_distinct_in_order?: Bool + optimize_distinct_in_order?: Bool; /** Optimize GROUP BY sharding_key queries (by avoiding costly aggregation on the initiator server). */ - optimize_distributed_group_by_sharding_key?: Bool + optimize_distributed_group_by_sharding_key?: Bool; /** Transform functions to subcolumns, if possible, to reduce amount of read data. E.g. 'length(arr)' -> 'arr.size0', 'col IS NULL' -> 'col.null' */ - optimize_functions_to_subcolumns?: Bool + optimize_functions_to_subcolumns?: Bool; /** Eliminates functions of other keys in GROUP BY section */ - optimize_group_by_function_keys?: Bool + optimize_group_by_function_keys?: Bool; /** Replace if(cond1, then1, if(cond2, ...)) chains to multiIf. Currently it's not beneficial for numeric types. */ - optimize_if_chain_to_multiif?: Bool + optimize_if_chain_to_multiif?: Bool; /** Replaces string-type arguments in If and Transform to enum. Disabled by default cause it could make inconsistent change in distributed query that would lead to its fail. */ - optimize_if_transform_strings_to_enum?: Bool + optimize_if_transform_strings_to_enum?: Bool; /** Delete injective functions of one argument inside uniq*() functions. */ - optimize_injective_functions_inside_uniq?: Bool + optimize_injective_functions_inside_uniq?: Bool; /** The minimum length of the expression `expr = x1 OR ... expr = xN` for optimization */ - optimize_min_equality_disjunction_chain_length?: UInt64 + optimize_min_equality_disjunction_chain_length?: UInt64; /** Replace monotonous function with its argument in ORDER BY */ - optimize_monotonous_functions_in_order_by?: Bool + optimize_monotonous_functions_in_order_by?: Bool; /** Move functions out of aggregate functions 'any', 'anyLast'. */ - optimize_move_functions_out_of_any?: Bool + optimize_move_functions_out_of_any?: Bool; /** Allows disabling WHERE to PREWHERE optimization in SELECT queries from MergeTree. */ - optimize_move_to_prewhere?: Bool + optimize_move_to_prewhere?: Bool; /** If query has `FINAL`, the optimization `move_to_prewhere` is not always correct and it is enabled only if both settings `optimize_move_to_prewhere` and `optimize_move_to_prewhere_if_final` are turned on */ - optimize_move_to_prewhere_if_final?: Bool + optimize_move_to_prewhere_if_final?: Bool; /** Replace 'multiIf' with only one condition to 'if'. */ - optimize_multiif_to_if?: Bool + optimize_multiif_to_if?: Bool; /** Rewrite aggregate functions that semantically equals to count() as count(). */ - optimize_normalize_count_variants?: Bool + optimize_normalize_count_variants?: Bool; /** Do the same transformation for inserted block of data as if merge was done on this block. */ - optimize_on_insert?: Bool + optimize_on_insert?: Bool; /** Optimize multiple OR LIKE into multiMatchAny. This optimization should not be enabled by default, because it defies index analysis in some cases. */ - optimize_or_like_chain?: Bool + optimize_or_like_chain?: Bool; /** Enable ORDER BY optimization for reading data in corresponding order in MergeTree tables. */ - optimize_read_in_order?: Bool + optimize_read_in_order?: Bool; /** Enable ORDER BY optimization in window clause for reading data in corresponding order in MergeTree tables. */ - optimize_read_in_window_order?: Bool + optimize_read_in_window_order?: Bool; /** Remove functions from ORDER BY if its argument is also in ORDER BY */ - optimize_redundant_functions_in_order_by?: Bool + optimize_redundant_functions_in_order_by?: Bool; /** If it is set to true, it will respect aliases in WHERE/GROUP BY/ORDER BY, that will help with partition pruning/secondary indexes/optimize_aggregation_in_order/optimize_read_in_order/optimize_trivial_count */ - optimize_respect_aliases?: Bool + optimize_respect_aliases?: Bool; /** Rewrite aggregate functions with if expression as argument when logically equivalent. For example, avg(if(cond, col, null)) can be rewritten to avgIf(cond, col) */ - optimize_rewrite_aggregate_function_with_if?: Bool + optimize_rewrite_aggregate_function_with_if?: Bool; /** Rewrite arrayExists() functions to has() when logically equivalent. For example, arrayExists(x -> x = 1, arr) can be rewritten to has(arr, 1) */ - optimize_rewrite_array_exists_to_has?: Bool + optimize_rewrite_array_exists_to_has?: Bool; /** Rewrite sumIf() and sum(if()) function countIf() function when logically equivalent */ - optimize_rewrite_sum_if_to_count_if?: Bool + optimize_rewrite_sum_if_to_count_if?: Bool; /** Skip partitions with one part with level > 0 in optimize final */ - optimize_skip_merged_partitions?: Bool + optimize_skip_merged_partitions?: Bool; /** Assumes that data is distributed by sharding_key. Optimization to skip unused shards if SELECT query filters by sharding_key. */ - optimize_skip_unused_shards?: Bool + optimize_skip_unused_shards?: Bool; /** Limit for number of sharding key values, turns off optimize_skip_unused_shards if the limit is reached */ - optimize_skip_unused_shards_limit?: UInt64 + optimize_skip_unused_shards_limit?: UInt64; /** Same as optimize_skip_unused_shards, but accept nesting level until which it will work. */ - optimize_skip_unused_shards_nesting?: UInt64 + optimize_skip_unused_shards_nesting?: UInt64; /** Rewrite IN in query for remote shards to exclude values that does not belong to the shard (requires optimize_skip_unused_shards) */ - optimize_skip_unused_shards_rewrite_in?: Bool + optimize_skip_unused_shards_rewrite_in?: Bool; /** Optimize sorting by sorting properties of input stream */ - optimize_sorting_by_input_stream_properties?: Bool + optimize_sorting_by_input_stream_properties?: Bool; /** Use constraints for column substitution */ - optimize_substitute_columns?: Bool + optimize_substitute_columns?: Bool; /** Allow applying fuse aggregating function. Available only with `allow_experimental_analyzer` */ - optimize_syntax_fuse_functions?: Bool + optimize_syntax_fuse_functions?: Bool; /** If setting is enabled and OPTIMIZE query didn't actually assign a merge then an explanatory exception is thrown */ - optimize_throw_if_noop?: Bool + optimize_throw_if_noop?: Bool; /** Process trivial 'SELECT count() FROM table' query from metadata. */ - optimize_trivial_count_query?: Bool + optimize_trivial_count_query?: Bool; /** Optimize trivial 'INSERT INTO table SELECT ... FROM TABLES' query */ - optimize_trivial_insert_select?: Bool + optimize_trivial_insert_select?: Bool; /** Automatically choose implicit projections to perform SELECT query */ - optimize_use_implicit_projections?: Bool + optimize_use_implicit_projections?: Bool; /** Automatically choose projections to perform SELECT query */ - optimize_use_projections?: Bool + optimize_use_projections?: Bool; /** Use constraints for query optimization */ - optimize_using_constraints?: Bool + optimize_using_constraints?: Bool; /** If non zero - set corresponding 'nice' value for query processing threads. Can be used to adjust query priority for OS scheduler. */ - os_thread_priority?: Int64 + os_thread_priority?: Int64; /** Compression method for Arrow output format. Supported codecs: lz4_frame, zstd, none (uncompressed) */ - output_format_arrow_compression_method?: ArrowCompression + output_format_arrow_compression_method?: ArrowCompression; /** Use Arrow FIXED_SIZE_BINARY type instead of Binary for FixedString columns. */ - output_format_arrow_fixed_string_as_fixed_byte_array?: Bool + output_format_arrow_fixed_string_as_fixed_byte_array?: Bool; /** Enable output LowCardinality type as Dictionary Arrow type */ - output_format_arrow_low_cardinality_as_dictionary?: Bool + output_format_arrow_low_cardinality_as_dictionary?: Bool; /** Use Arrow String type instead of Binary for String columns */ - output_format_arrow_string_as_string?: Bool + output_format_arrow_string_as_string?: Bool; /** Compression codec used for output. Possible values: 'null', 'deflate', 'snappy'. */ - output_format_avro_codec?: string + output_format_avro_codec?: string; /** Max rows in a file (if permitted by storage) */ - output_format_avro_rows_in_file?: UInt64 + output_format_avro_rows_in_file?: UInt64; /** For Avro format: regexp of String columns to select as AVRO string. */ - output_format_avro_string_column_pattern?: string + output_format_avro_string_column_pattern?: string; /** Sync interval in bytes. */ - output_format_avro_sync_interval?: UInt64 + output_format_avro_sync_interval?: UInt64; /** Use BSON String type instead of Binary for String columns. */ - output_format_bson_string_as_string?: Bool + output_format_bson_string_as_string?: Bool; /** If it is set true, end of line in CSV format will be \\r\\n instead of \\n. */ - output_format_csv_crlf_end_of_line?: Bool + output_format_csv_crlf_end_of_line?: Bool; /** Output trailing zeros when printing Decimal values. E.g. 1.230000 instead of 1.23. */ - output_format_decimal_trailing_zeros?: Bool + output_format_decimal_trailing_zeros?: Bool; /** Enable streaming in output formats that support it. */ - output_format_enable_streaming?: Bool + output_format_enable_streaming?: Bool; /** Output a JSON array of all rows in JSONEachRow(Compact) format. */ - output_format_json_array_of_rows?: Bool + output_format_json_array_of_rows?: Bool; /** Controls escaping forward slashes for string outputs in JSON output format. This is intended for compatibility with JavaScript. Don't confuse with backslashes that are always escaped. */ - output_format_json_escape_forward_slashes?: Bool + output_format_json_escape_forward_slashes?: Bool; /** Serialize named tuple columns as JSON objects. */ - output_format_json_named_tuples_as_objects?: Bool + output_format_json_named_tuples_as_objects?: Bool; /** Controls quoting of 64-bit float numbers in JSON output format. */ - output_format_json_quote_64bit_floats?: Bool + output_format_json_quote_64bit_floats?: Bool; /** Controls quoting of 64-bit integers in JSON output format. */ - output_format_json_quote_64bit_integers?: Bool + output_format_json_quote_64bit_integers?: Bool; /** Controls quoting of decimals in JSON output format. */ - output_format_json_quote_decimals?: Bool + output_format_json_quote_decimals?: Bool; /** Enables '+nan', '-nan', '+inf', '-inf' outputs in JSON output format. */ - output_format_json_quote_denormals?: Bool + output_format_json_quote_denormals?: Bool; /** Validate UTF-8 sequences in JSON output formats, doesn't impact formats JSON/JSONCompact/JSONColumnsWithMetadata, they always validate utf8 */ - output_format_json_validate_utf8?: Bool + output_format_json_validate_utf8?: Bool; /** The way how to output UUID in MsgPack format. */ - output_format_msgpack_uuid_representation?: MsgPackUUIDRepresentation + output_format_msgpack_uuid_representation?: MsgPackUUIDRepresentation; /** Compression method for ORC output format. Supported codecs: lz4, snappy, zlib, zstd, none (uncompressed) */ - output_format_orc_compression_method?: ORCCompression + output_format_orc_compression_method?: ORCCompression; /** Use ORC String type instead of Binary for String columns */ - output_format_orc_string_as_string?: Bool + output_format_orc_string_as_string?: Bool; /** Enable parallel formatting for some data formats. */ - output_format_parallel_formatting?: Bool + output_format_parallel_formatting?: Bool; /** In parquet file schema, use name 'element' instead of 'item' for list elements. This is a historical artifact of Arrow library implementation. Generally increases compatibility, except perhaps with some old versions of Arrow. */ - output_format_parquet_compliant_nested_types?: Bool + output_format_parquet_compliant_nested_types?: Bool; /** Compression method for Parquet output format. Supported codecs: snappy, lz4, brotli, zstd, gzip, none (uncompressed) */ - output_format_parquet_compression_method?: ParquetCompression + output_format_parquet_compression_method?: ParquetCompression; /** Use Parquet FIXED_LENGTH_BYTE_ARRAY type instead of Binary for FixedString columns. */ - output_format_parquet_fixed_string_as_fixed_byte_array?: Bool + output_format_parquet_fixed_string_as_fixed_byte_array?: Bool; /** Target row group size in rows. */ - output_format_parquet_row_group_size?: UInt64 + output_format_parquet_row_group_size?: UInt64; /** Target row group size in bytes, before compression. */ - output_format_parquet_row_group_size_bytes?: UInt64 + output_format_parquet_row_group_size_bytes?: UInt64; /** Use Parquet String type instead of Binary for String columns. */ - output_format_parquet_string_as_string?: Bool + output_format_parquet_string_as_string?: Bool; /** Parquet format version for output format. Supported versions: 1.0, 2.4, 2.6 and 2.latest (default) */ - output_format_parquet_version?: ParquetVersion + output_format_parquet_version?: ParquetVersion; /** Use ANSI escape sequences to paint colors in Pretty formats */ - output_format_pretty_color?: Bool + output_format_pretty_color?: Bool; /** Charset for printing grid borders. Available charsets: ASCII, UTF-8 (default one). */ - output_format_pretty_grid_charset?: string + output_format_pretty_grid_charset?: string; /** Maximum width to pad all values in a column in Pretty formats. */ - output_format_pretty_max_column_pad_width?: UInt64 + output_format_pretty_max_column_pad_width?: UInt64; /** Rows limit for Pretty formats. */ - output_format_pretty_max_rows?: UInt64 + output_format_pretty_max_rows?: UInt64; /** Maximum width of value to display in Pretty formats. If greater - it will be cut. */ - output_format_pretty_max_value_width?: UInt64 + output_format_pretty_max_value_width?: UInt64; /** Add row numbers before each row for pretty output format */ - output_format_pretty_row_numbers?: Bool + output_format_pretty_row_numbers?: Bool; /** When serializing Nullable columns with Google wrappers, serialize default values as empty wrappers. If turned off, default and null values are not serialized */ - output_format_protobuf_nullables_with_google_wrappers?: Bool + output_format_protobuf_nullables_with_google_wrappers?: Bool; /** Include column names in INSERT query */ - output_format_sql_insert_include_column_names?: Bool + output_format_sql_insert_include_column_names?: Bool; /** The maximum number of rows in one INSERT statement. */ - output_format_sql_insert_max_batch_size?: UInt64 + output_format_sql_insert_max_batch_size?: UInt64; /** Quote column names with '`' characters */ - output_format_sql_insert_quote_names?: Bool + output_format_sql_insert_quote_names?: Bool; /** The name of table in the output INSERT query */ - output_format_sql_insert_table_name?: string + output_format_sql_insert_table_name?: string; /** Use REPLACE statement instead of INSERT */ - output_format_sql_insert_use_replace?: Bool + output_format_sql_insert_use_replace?: Bool; /** If it is set true, end of line in TSV format will be \\r\\n instead of \\n. */ - output_format_tsv_crlf_end_of_line?: Bool + output_format_tsv_crlf_end_of_line?: Bool; /** Write statistics about read rows, bytes, time elapsed in suitable output formats. */ - output_format_write_statistics?: Bool + output_format_write_statistics?: Bool; /** Process distributed INSERT SELECT query in the same cluster on local tables on every shard; if set to 1 - SELECT is executed on each shard; if set to 2 - SELECT and INSERT are executed on each shard */ - parallel_distributed_insert_select?: UInt64 + parallel_distributed_insert_select?: UInt64; /** This is internal setting that should not be used directly and represents an implementation detail of the 'parallel replicas' mode. This setting will be automatically set up by the initiator server for distributed queries to the index of the replica participating in query processing among parallel replicas. */ - parallel_replica_offset?: UInt64 + parallel_replica_offset?: UInt64; /** This is internal setting that should not be used directly and represents an implementation detail of the 'parallel replicas' mode. This setting will be automatically set up by the initiator server for distributed queries to the number of parallel replicas participating in query processing. */ - parallel_replicas_count?: UInt64 + parallel_replicas_count?: UInt64; /** Custom key assigning work to replicas when parallel replicas are used. */ - parallel_replicas_custom_key?: string + parallel_replicas_custom_key?: string; /** Type of filter to use with custom key for parallel replicas. default - use modulo operation on the custom key, range - use range filter on custom key using all possible values for the value type of custom key. */ - parallel_replicas_custom_key_filter_type?: ParallelReplicasCustomKeyFilterType + parallel_replicas_custom_key_filter_type?: ParallelReplicasCustomKeyFilterType; /** If true, ClickHouse will use parallel replicas algorithm also for non-replicated MergeTree tables */ - parallel_replicas_for_non_replicated_merge_tree?: Bool + parallel_replicas_for_non_replicated_merge_tree?: Bool; /** If the number of marks to read is less than the value of this setting - parallel replicas will be disabled */ - parallel_replicas_min_number_of_granules_to_enable?: UInt64 + parallel_replicas_min_number_of_granules_to_enable?: UInt64; /** A multiplier which will be added during calculation for minimal number of marks to retrieve from coordinator. This will be applied only for remote replicas. */ - parallel_replicas_single_task_marks_count_multiplier?: Float + parallel_replicas_single_task_marks_count_multiplier?: Float; /** Enables pushing to attached views concurrently instead of sequentially. */ - parallel_view_processing?: Bool + parallel_view_processing?: Bool; /** Parallelize output for reading step from storage. It allows parallelizing query processing right after reading from storage if possible */ - parallelize_output_from_storages?: Bool + parallelize_output_from_storages?: Bool; /** If not 0 group left table blocks in bigger ones for left-side table in partial merge join. It uses up to 2x of specified memory per joining thread. */ - partial_merge_join_left_table_buffer_bytes?: UInt64 + partial_merge_join_left_table_buffer_bytes?: UInt64; /** Split right-hand joining data in blocks of specified size. It's a portion of data indexed by min-max values and possibly unloaded on disk. */ - partial_merge_join_rows_in_right_blocks?: UInt64 + partial_merge_join_rows_in_right_blocks?: UInt64; /** Allows query to return a partial result after cancel. */ - partial_result_on_first_cancel?: Bool + partial_result_on_first_cancel?: Bool; /** If the destination table contains at least that many active parts in a single partition, artificially slow down insert into table. */ - parts_to_delay_insert?: UInt64 + parts_to_delay_insert?: UInt64; /** If more than this number active parts in a single partition of the destination table, throw 'Too many parts ...' exception. */ - parts_to_throw_insert?: UInt64 + parts_to_throw_insert?: UInt64; /** Interval after which periodically refreshed live view is forced to refresh. */ - periodic_live_view_refresh?: Seconds + periodic_live_view_refresh?: Seconds; /** Block at the query wait loop on the server for the specified number of seconds. */ - poll_interval?: UInt64 + poll_interval?: UInt64; /** Close connection before returning connection to the pool. */ - postgresql_connection_pool_auto_close_connection?: Bool + postgresql_connection_pool_auto_close_connection?: Bool; /** Connection pool size for PostgreSQL table engine and database engine. */ - postgresql_connection_pool_size?: UInt64 + postgresql_connection_pool_size?: UInt64; /** Connection pool push/pop timeout on empty pool for PostgreSQL table engine and database engine. By default it will block on empty pool. */ - postgresql_connection_pool_wait_timeout?: UInt64 + postgresql_connection_pool_wait_timeout?: UInt64; /** Prefer using column names instead of aliases if possible. */ - prefer_column_name_to_alias?: Bool + prefer_column_name_to_alias?: Bool; /** If enabled, all IN/JOIN operators will be rewritten as GLOBAL IN/JOIN. It's useful when the to-be-joined tables are only available on the initiator and we need to always scatter their data on-the-fly during distributed processing with the GLOBAL keyword. It's also useful to reduce the need to access the external sources joining external tables. */ - prefer_global_in_and_join?: Bool + prefer_global_in_and_join?: Bool; /** If it's true then queries will be always sent to local replica (if it exists). If it's false then replica to send a query will be chosen between local and remote ones according to load_balancing */ - prefer_localhost_replica?: Bool + prefer_localhost_replica?: Bool; /** This setting adjusts the data block size for query processing and represents additional fine tune to the more rough 'max_block_size' setting. If the columns are large and with 'max_block_size' rows the block size is likely to be larger than the specified amount of bytes, its size will be lowered for better CPU cache locality. */ - preferred_block_size_bytes?: UInt64 + preferred_block_size_bytes?: UInt64; /** Limit on max column size in block while reading. Helps to decrease cache misses count. Should be close to L2 cache size. */ - preferred_max_column_in_block_size_bytes?: UInt64 + preferred_max_column_in_block_size_bytes?: UInt64; /** The maximum size of the prefetch buffer to read from the filesystem. */ - prefetch_buffer_size?: UInt64 + prefetch_buffer_size?: UInt64; /** Priority of the query. 1 - the highest, higher value - lower priority; 0 - do not use priorities. */ - priority?: UInt64 + priority?: UInt64; /** Compress cache entries. */ - query_cache_compress_entries?: Bool + query_cache_compress_entries?: Bool; /** The maximum number of query results the current user may store in the query cache. 0 means unlimited. */ - query_cache_max_entries?: UInt64 + query_cache_max_entries?: UInt64; /** The maximum amount of memory (in bytes) the current user may allocate in the query cache. 0 means unlimited. */ - query_cache_max_size_in_bytes?: UInt64 + query_cache_max_size_in_bytes?: UInt64; /** Minimum time in milliseconds for a query to run for its result to be stored in the query cache. */ - query_cache_min_query_duration?: Milliseconds + query_cache_min_query_duration?: Milliseconds; /** Minimum number a SELECT query must run before its result is stored in the query cache */ - query_cache_min_query_runs?: UInt64 + query_cache_min_query_runs?: UInt64; /** Allow other users to read entry in the query cache */ - query_cache_share_between_users?: Bool + query_cache_share_between_users?: Bool; /** Squash partial result blocks to blocks of size 'max_block_size'. Reduces performance of inserts into the query cache but improves the compressability of cache entries. */ - query_cache_squash_partial_results?: Bool + query_cache_squash_partial_results?: Bool; /** Store results of queries with non-deterministic functions (e.g. rand(), now()) in the query cache */ - query_cache_store_results_of_queries_with_nondeterministic_functions?: Bool + query_cache_store_results_of_queries_with_nondeterministic_functions?: Bool; /** After this time in seconds entries in the query cache become stale */ - query_cache_ttl?: Seconds + query_cache_ttl?: Seconds; /** Use query plan for aggregation-in-order optimisation */ - query_plan_aggregation_in_order?: Bool + query_plan_aggregation_in_order?: Bool; /** Apply optimizations to query plan */ - query_plan_enable_optimizations?: Bool + query_plan_enable_optimizations?: Bool; /** Allow to push down filter by predicate query plan step */ - query_plan_filter_push_down?: Bool + query_plan_filter_push_down?: Bool; /** Limit the total number of optimizations applied to query plan. If zero, ignored. If limit reached, throw exception */ - query_plan_max_optimizations_to_apply?: UInt64 + query_plan_max_optimizations_to_apply?: UInt64; /** Analyze primary key using query plan (instead of AST) */ - query_plan_optimize_primary_key?: Bool + query_plan_optimize_primary_key?: Bool; /** Use query plan for aggregation-in-order optimisation */ - query_plan_optimize_projection?: Bool + query_plan_optimize_projection?: Bool; /** Use query plan for read-in-order optimisation */ - query_plan_read_in_order?: Bool + query_plan_read_in_order?: Bool; /** Remove redundant Distinct step in query plan */ - query_plan_remove_redundant_distinct?: Bool + query_plan_remove_redundant_distinct?: Bool; /** Remove redundant sorting in query plan. For example, sorting steps related to ORDER BY clauses in subqueries */ - query_plan_remove_redundant_sorting?: Bool + query_plan_remove_redundant_sorting?: Bool; /** Period for CPU clock timer of query profiler (in nanoseconds). Set 0 value to turn off the CPU clock query profiler. Recommended value is at least 10000000 (100 times a second) for single queries or 1000000000 (once a second) for cluster-wide profiling. */ - query_profiler_cpu_time_period_ns?: UInt64 + query_profiler_cpu_time_period_ns?: UInt64; /** Period for real clock timer of query profiler (in nanoseconds). Set 0 value to turn off the real clock query profiler. Recommended value is at least 10000000 (100 times a second) for single queries or 1000000000 (once a second) for cluster-wide profiling. */ - query_profiler_real_time_period_ns?: UInt64 + query_profiler_real_time_period_ns?: UInt64; /** The wait time in the request queue, if the number of concurrent requests exceeds the maximum. */ - queue_max_wait_ms?: Milliseconds + queue_max_wait_ms?: Milliseconds; /** The wait time for reading from RabbitMQ before retry. */ - rabbitmq_max_wait_ms?: Milliseconds + rabbitmq_max_wait_ms?: Milliseconds; /** Settings to reduce the number of threads in case of slow reads. Count events when the read bandwidth is less than that many bytes per second. */ - read_backoff_max_throughput?: UInt64 + read_backoff_max_throughput?: UInt64; /** Settings to try keeping the minimal number of threads in case of slow reads. */ - read_backoff_min_concurrency?: UInt64 + read_backoff_min_concurrency?: UInt64; /** Settings to reduce the number of threads in case of slow reads. The number of events after which the number of threads will be reduced. */ - read_backoff_min_events?: UInt64 + read_backoff_min_events?: UInt64; /** Settings to reduce the number of threads in case of slow reads. Do not pay attention to the event, if the previous one has passed less than a certain amount of time. */ - read_backoff_min_interval_between_events_ms?: Milliseconds + read_backoff_min_interval_between_events_ms?: Milliseconds; /** Setting to reduce the number of threads in case of slow reads. Pay attention only to reads that took at least that much time. */ - read_backoff_min_latency_ms?: Milliseconds + read_backoff_min_latency_ms?: Milliseconds; /** Allow to use the filesystem cache in passive mode - benefit from the existing cache entries, but don't put more entries into the cache. If you set this setting for heavy ad-hoc queries and leave it disabled for short real-time queries, this will allows to avoid cache threshing by too heavy queries and to improve the overall system efficiency. */ - read_from_filesystem_cache_if_exists_otherwise_bypass_cache?: Bool + read_from_filesystem_cache_if_exists_otherwise_bypass_cache?: Bool; /** Minimal number of parts to read to run preliminary merge step during multithread reading in order of primary key. */ - read_in_order_two_level_merge_threshold?: UInt64 + read_in_order_two_level_merge_threshold?: UInt64; /** What to do when the limit is exceeded. */ - read_overflow_mode?: OverflowMode + read_overflow_mode?: OverflowMode; /** What to do when the leaf limit is exceeded. */ - read_overflow_mode_leaf?: OverflowMode + read_overflow_mode_leaf?: OverflowMode; /** Priority to read data from local filesystem or remote filesystem. Only supported for 'pread_threadpool' method for local filesystem and for `threadpool` method for remote filesystem. */ - read_priority?: Int64 + read_priority?: Int64; /** 0 - no read-only restrictions. 1 - only read requests, as well as changing explicitly allowed settings. 2 - only read requests, as well as changing settings, except for the 'readonly' setting. */ - readonly?: UInt64 + readonly?: UInt64; /** Connection timeout for receiving first packet of data or packet with positive progress from replica */ - receive_data_timeout_ms?: Milliseconds + receive_data_timeout_ms?: Milliseconds; /** Timeout for receiving data from network, in seconds. If no bytes were received in this interval, exception is thrown. If you set this setting on client, the 'send_timeout' for the socket will be also set on the corresponding connection end on the server. */ - receive_timeout?: Seconds + receive_timeout?: Seconds; /** Allow regexp_tree dictionary using Hyperscan library. */ - regexp_dict_allow_hyperscan?: Bool + regexp_dict_allow_hyperscan?: Bool; /** Max matches of any single regexp per row, used to safeguard 'extractAllGroupsHorizontal' against consuming too much memory with greedy RE. */ - regexp_max_matches_per_row?: UInt64 + regexp_max_matches_per_row?: UInt64; /** Reject patterns which will likely be expensive to evaluate with hyperscan (due to NFA state explosion) */ - reject_expensive_hyperscan_regexps?: Bool + reject_expensive_hyperscan_regexps?: Bool; /** If memory usage after remerge does not reduced by this ratio, remerge will be disabled. */ - remerge_sort_lowered_memory_bytes_ratio?: Float + remerge_sort_lowered_memory_bytes_ratio?: Float; /** Method of reading data from remote filesystem, one of: read, threadpool. */ - remote_filesystem_read_method?: string + remote_filesystem_read_method?: string; /** Should use prefetching when reading data from remote filesystem. */ - remote_filesystem_read_prefetch?: Bool + remote_filesystem_read_prefetch?: Bool; /** Max attempts to read with backoff */ - remote_fs_read_backoff_max_tries?: UInt64 + remote_fs_read_backoff_max_tries?: UInt64; /** Max wait time when trying to read data for remote disk */ - remote_fs_read_max_backoff_ms?: UInt64 + remote_fs_read_max_backoff_ms?: UInt64; /** Min bytes required for remote read (url, s3) to do seek, instead of read with ignore. */ - remote_read_min_bytes_for_seek?: UInt64 + remote_read_min_bytes_for_seek?: UInt64; /** Rename successfully processed files according to the specified pattern; Pattern can include the following placeholders: `%a` (full original file name), `%f` (original filename without extension), `%e` (file extension with dot), `%t` (current timestamp in µs), and `%%` (% sign) */ - rename_files_after_processing?: string + rename_files_after_processing?: string; /** Whether the running request should be canceled with the same id as the new one. */ - replace_running_query?: Bool + replace_running_query?: Bool; /** The wait time for running query with the same query_id to finish when setting 'replace_running_query' is active. */ - replace_running_query_max_wait_ms?: Milliseconds + replace_running_query_max_wait_ms?: Milliseconds; /** Wait for inactive replica to execute ALTER/OPTIMIZE. Time in seconds, 0 - do not wait, negative - wait for unlimited time. */ - replication_wait_for_inactive_replica_timeout?: Int64 + replication_wait_for_inactive_replica_timeout?: Int64; /** What to do when the limit is exceeded. */ - result_overflow_mode?: OverflowMode + result_overflow_mode?: OverflowMode; /** Use multiple threads for s3 multipart upload. It may lead to slightly higher memory usage */ - s3_allow_parallel_part_upload?: Bool + s3_allow_parallel_part_upload?: Bool; /** Check each uploaded object to s3 with head request to be sure that upload was successful */ - s3_check_objects_after_upload?: Bool + s3_check_objects_after_upload?: Bool; /** Enables or disables creating a new file on each insert in s3 engine tables */ - s3_create_new_file_on_insert?: Bool + s3_create_new_file_on_insert?: Bool; /** Maximum number of files that could be returned in batch by ListObject request */ - s3_list_object_keys_size?: UInt64 + s3_list_object_keys_size?: UInt64; /** The maximum number of connections per server. */ - s3_max_connections?: UInt64 + s3_max_connections?: UInt64; /** Max number of requests that can be issued simultaneously before hitting request per second limit. By default (0) equals to `s3_max_get_rps` */ - s3_max_get_burst?: UInt64 + s3_max_get_burst?: UInt64; /** Limit on S3 GET request per second rate before throttling. Zero means unlimited. */ - s3_max_get_rps?: UInt64 + s3_max_get_rps?: UInt64; /** The maximum number of a concurrent loaded parts in multipart upload request. 0 means unlimited. You */ - s3_max_inflight_parts_for_one_file?: UInt64 + s3_max_inflight_parts_for_one_file?: UInt64; /** Max number of requests that can be issued simultaneously before hitting request per second limit. By default (0) equals to `s3_max_put_rps` */ - s3_max_put_burst?: UInt64 + s3_max_put_burst?: UInt64; /** Limit on S3 PUT request per second rate before throttling. Zero means unlimited. */ - s3_max_put_rps?: UInt64 + s3_max_put_rps?: UInt64; /** Max number of S3 redirects hops allowed. */ - s3_max_redirects?: UInt64 + s3_max_redirects?: UInt64; /** The maximum size of object to upload using singlepart upload to S3. */ - s3_max_single_part_upload_size?: UInt64 + s3_max_single_part_upload_size?: UInt64; /** The maximum number of retries during single S3 read. */ - s3_max_single_read_retries?: UInt64 + s3_max_single_read_retries?: UInt64; /** The maximum number of retries in case of unexpected errors during S3 write. */ - s3_max_unexpected_write_error_retries?: UInt64 + s3_max_unexpected_write_error_retries?: UInt64; /** The maximum size of part to upload during multipart upload to S3. */ - s3_max_upload_part_size?: UInt64 + s3_max_upload_part_size?: UInt64; /** The minimum size of part to upload during multipart upload to S3. */ - s3_min_upload_part_size?: UInt64 + s3_min_upload_part_size?: UInt64; /** Idleness timeout for sending and receiving data to/from S3. Fail if a single TCP read or write call blocks for this long. */ - s3_request_timeout_ms?: UInt64 + s3_request_timeout_ms?: UInt64; /** Setting for Aws::Client::RetryStrategy, Aws::Client does retries itself, 0 means no retries */ - s3_retry_attempts?: UInt64 + s3_retry_attempts?: UInt64; /** Allow to skip empty files in s3 table engine */ - s3_skip_empty_files?: Bool + s3_skip_empty_files?: Bool; /** The exact size of part to upload during multipart upload to S3 (some implementations does not supports variable size parts). */ - s3_strict_upload_part_size?: UInt64 + s3_strict_upload_part_size?: UInt64; /** Throw an error, when ListObjects request cannot match any files */ - s3_throw_on_zero_files_match?: Bool + s3_throw_on_zero_files_match?: Bool; /** Enables or disables truncate before insert in s3 engine tables. */ - s3_truncate_on_insert?: Bool + s3_truncate_on_insert?: Bool; /** Multiply s3_min_upload_part_size by this factor each time s3_multiply_parts_count_threshold parts were uploaded from a single write to S3. */ - s3_upload_part_size_multiply_factor?: UInt64 + s3_upload_part_size_multiply_factor?: UInt64; /** Each time this number of parts was uploaded to S3 s3_min_upload_part_size multiplied by s3_upload_part_size_multiply_factor. */ - s3_upload_part_size_multiply_parts_count_threshold?: UInt64 + s3_upload_part_size_multiply_parts_count_threshold?: UInt64; /** Use schema from cache for URL with last modification time validation (for urls with Last-Modified header) */ - schema_inference_cache_require_modification_time_for_url?: Bool + schema_inference_cache_require_modification_time_for_url?: Bool; /** The list of column names and types to use in schema inference for formats without column names. The format: 'column_name1 column_type1, column_name2 column_type2, ...' */ - schema_inference_hints?: string + schema_inference_hints?: string; /** If set to true, all inferred types will be Nullable in schema inference for formats without information about nullability. */ - schema_inference_make_columns_nullable?: Bool + schema_inference_make_columns_nullable?: Bool; /** Use cache in schema inference while using azure table function */ - schema_inference_use_cache_for_azure?: Bool + schema_inference_use_cache_for_azure?: Bool; /** Use cache in schema inference while using file table function */ - schema_inference_use_cache_for_file?: Bool + schema_inference_use_cache_for_file?: Bool; /** Use cache in schema inference while using hdfs table function */ - schema_inference_use_cache_for_hdfs?: Bool + schema_inference_use_cache_for_hdfs?: Bool; /** Use cache in schema inference while using s3 table function */ - schema_inference_use_cache_for_s3?: Bool + schema_inference_use_cache_for_s3?: Bool; /** Use cache in schema inference while using url table function */ - schema_inference_use_cache_for_url?: Bool + schema_inference_use_cache_for_url?: Bool; /** For SELECT queries from the replicated table, throw an exception if the replica does not have a chunk written with the quorum; do not read the parts that have not yet been written with the quorum. */ - select_sequential_consistency?: UInt64 + select_sequential_consistency?: UInt64; /** Send server text logs with specified minimum level to client. Valid values: 'trace', 'debug', 'information', 'warning', 'error', 'fatal', 'none' */ - send_logs_level?: LogsLevel + send_logs_level?: LogsLevel; /** Send server text logs with specified regexp to match log source name. Empty means all sources. */ - send_logs_source_regexp?: string + send_logs_source_regexp?: string; /** Send progress notifications using X-ClickHouse-Progress headers. Some clients do not support high amount of HTTP headers (Python requests in particular), so it is disabled by default. */ - send_progress_in_http_headers?: Bool + send_progress_in_http_headers?: Bool; /** Timeout for sending data to network, in seconds. If client needs to sent some data, but it did not able to send any bytes in this interval, exception is thrown. If you set this setting on client, the 'receive_timeout' for the socket will be also set on the corresponding connection end on the server. */ - send_timeout?: Seconds + send_timeout?: Seconds; /** This setting can be removed in the future due to potential caveats. It is experimental and is not suitable for production usage. The default timezone for current session or query. The server default timezone if empty. */ - session_timezone?: string + session_timezone?: string; /** What to do when the limit is exceeded. */ - set_overflow_mode?: OverflowMode + set_overflow_mode?: OverflowMode; /** Setting for short-circuit function evaluation configuration. Possible values: 'enable' - use short-circuit function evaluation for functions that are suitable for it, 'disable' - disable short-circuit function evaluation, 'force_enable' - use short-circuit function evaluation for all functions. */ - short_circuit_function_evaluation?: ShortCircuitFunctionEvaluation + short_circuit_function_evaluation?: ShortCircuitFunctionEvaluation; /** For tables in databases with Engine=Atomic show UUID of the table in its CREATE query. */ - show_table_uuid_in_table_create_query_if_not_nil?: Bool + show_table_uuid_in_table_create_query_if_not_nil?: Bool; /** For single JOIN in case of identifier ambiguity prefer left table */ - single_join_prefer_left_table?: Bool + single_join_prefer_left_table?: Bool; /** Skip download from remote filesystem if exceeds query cache size */ - skip_download_if_exceeds_query_cache?: Bool + skip_download_if_exceeds_query_cache?: Bool; /** If true, ClickHouse silently skips unavailable shards and nodes unresolvable through DNS. Shard is marked as unavailable when none of the replicas can be reached. */ - skip_unavailable_shards?: Bool + skip_unavailable_shards?: Bool; /** Time to sleep after receiving query in TCPHandler */ - sleep_after_receiving_query_ms?: Milliseconds + sleep_after_receiving_query_ms?: Milliseconds; /** Time to sleep in sending data in TCPHandler */ - sleep_in_send_data_ms?: Milliseconds + sleep_in_send_data_ms?: Milliseconds; /** Time to sleep in sending tables status response in TCPHandler */ - sleep_in_send_tables_status_ms?: Milliseconds + sleep_in_send_tables_status_ms?: Milliseconds; /** What to do when the limit is exceeded. */ - sort_overflow_mode?: OverflowMode + sort_overflow_mode?: OverflowMode; /** Method of reading data from storage file, one of: read, pread, mmap. The mmap method does not apply to clickhouse-server (it's intended for clickhouse-local). */ - storage_file_read_method?: LocalFSReadMethod + storage_file_read_method?: LocalFSReadMethod; /** Maximum time to read from a pipe for receiving information from the threads when querying the `system.stack_trace` table. This setting is used for testing purposes and not meant to be changed by users. */ - storage_system_stack_trace_pipe_read_timeout_ms?: Milliseconds + storage_system_stack_trace_pipe_read_timeout_ms?: Milliseconds; /** Timeout for flushing data from streaming storages. */ - stream_flush_interval_ms?: Milliseconds + stream_flush_interval_ms?: Milliseconds; /** Allow direct SELECT query for Kafka, RabbitMQ, FileLog, Redis Streams and NATS engines. In case there are attached materialized views, SELECT query is not allowed even if this setting is enabled. */ - stream_like_engine_allow_direct_select?: Bool + stream_like_engine_allow_direct_select?: Bool; /** When stream like engine reads from multiple queues, user will need to select one queue to insert into when writing. Used by Redis Streams and NATS. */ - stream_like_engine_insert_queue?: string + stream_like_engine_insert_queue?: string; /** Timeout for polling data from/to streaming storages. */ - stream_poll_timeout_ms?: Milliseconds + stream_poll_timeout_ms?: Milliseconds; /** When querying system.events or system.metrics tables, include all metrics, even with zero values. */ - system_events_show_zero_values?: Bool + system_events_show_zero_values?: Bool; /** The maximum number of different shards and the maximum number of replicas of one shard in the `remote` function. */ - table_function_remote_max_addresses?: UInt64 + table_function_remote_max_addresses?: UInt64; /** The time in seconds the connection needs to remain idle before TCP starts sending keepalive probes */ - tcp_keep_alive_timeout?: Seconds + tcp_keep_alive_timeout?: Seconds; /** Set compression codec for temporary files (sort and join on disk). I.e. LZ4, NONE. */ - temporary_files_codec?: string + temporary_files_codec?: string; /** Enables or disables empty INSERTs, enabled by default */ - throw_if_no_data_to_insert?: Bool + throw_if_no_data_to_insert?: Bool; /** Ignore error from cache when caching on write operations (INSERT, merges) */ - throw_on_error_from_cache_on_write_operations?: Bool + throw_on_error_from_cache_on_write_operations?: Bool; /** Throw exception if unsupported query is used inside transaction */ - throw_on_unsupported_query_inside_transaction?: Bool + throw_on_unsupported_query_inside_transaction?: Bool; /** Check that the speed is not too low after the specified time has elapsed. */ - timeout_before_checking_execution_speed?: Seconds + timeout_before_checking_execution_speed?: Seconds; /** What to do when the limit is exceeded. */ - timeout_overflow_mode?: OverflowMode + timeout_overflow_mode?: OverflowMode; /** The threshold for totals_mode = 'auto'. */ - totals_auto_threshold?: Float + totals_auto_threshold?: Float; /** How to calculate TOTALS when HAVING is present, as well as when max_rows_to_group_by and group_by_overflow_mode = ‘any’ are present. */ - totals_mode?: TotalsMode + totals_mode?: TotalsMode; /** Send to system.trace_log profile event and value of increment on each increment with 'ProfileEvent' trace_type */ - trace_profile_events?: Bool + trace_profile_events?: Bool; /** What to do when the limit is exceeded. */ - transfer_overflow_mode?: OverflowMode + transfer_overflow_mode?: OverflowMode; /** If enabled, NULL values will be matched with 'IN' operator as if they are considered equal. */ - transform_null_in?: Bool + transform_null_in?: Bool; /** Set default mode in UNION query. Possible values: empty string, 'ALL', 'DISTINCT'. If empty, query without mode will throw exception. */ - union_default_mode?: SetOperationMode + union_default_mode?: SetOperationMode; /** Send unknown packet instead of data Nth data packet */ - unknown_packet_in_send_data?: UInt64 + unknown_packet_in_send_data?: UInt64; /** Use client timezone for interpreting DateTime string values, instead of adopting server timezone. */ - use_client_time_zone?: Bool + use_client_time_zone?: Bool; /** Changes format of directories names for distributed table insert parts. */ - use_compact_format_in_distributed_parts_names?: Bool + use_compact_format_in_distributed_parts_names?: Bool; /** Use hedged requests for distributed queries */ - use_hedged_requests?: Bool + use_hedged_requests?: Bool; /** Try using an index if there is a subquery or a table expression on the right side of the IN operator. */ - use_index_for_in_with_subqueries?: Bool + use_index_for_in_with_subqueries?: Bool; /** The maximum size of set on the right hand side of the IN operator to use table index for filtering. It allows to avoid performance degradation and higher memory usage due to preparation of additional data structures for large queries. Zero means no limit. */ - use_index_for_in_with_subqueries_max_values?: UInt64 + use_index_for_in_with_subqueries_max_values?: UInt64; /** Use local cache for remote storage like HDFS or S3, it's used for remote table engine only */ - use_local_cache_for_remote_storage?: Bool + use_local_cache_for_remote_storage?: Bool; /** Use MySQL converted types when connected via MySQL compatibility for show columns query */ - use_mysql_types_in_show_columns?: Bool + use_mysql_types_in_show_columns?: Bool; /** Enable the query cache */ - use_query_cache?: Bool + use_query_cache?: Bool; /** Use data skipping indexes during query execution. */ - use_skip_indexes?: Bool + use_skip_indexes?: Bool; /** If query has FINAL, then skipping data based on indexes may produce incorrect result, hence disabled by default. */ - use_skip_indexes_if_final?: Bool + use_skip_indexes_if_final?: Bool; /** Use structure from insertion table instead of schema inference from data. Possible values: 0 - disabled, 1 - enabled, 2 - auto */ - use_structure_from_insertion_table_in_table_functions?: UInt64 + use_structure_from_insertion_table_in_table_functions?: UInt64; /** Whether to use the cache of uncompressed blocks. */ - use_uncompressed_cache?: Bool + use_uncompressed_cache?: Bool; /** Columns preceding WITH FILL columns in ORDER BY clause form sorting prefix. Rows with different values in sorting prefix are filled independently */ - use_with_fill_by_sorting_prefix?: Bool + use_with_fill_by_sorting_prefix?: Bool; /** Throw exception if polygon is invalid in function pointInPolygon (e.g. self-tangent, self-intersecting). If the setting is false, the function will accept invalid polygons but may silently return wrong result. */ - validate_polygons?: Bool + validate_polygons?: Bool; /** Wait for committed changes to become actually visible in the latest snapshot */ - wait_changes_become_visible_after_commit_mode?: TransactionsWaitCSNMode + wait_changes_become_visible_after_commit_mode?: TransactionsWaitCSNMode; /** If true wait for processing of asynchronous insertion */ - wait_for_async_insert?: Bool + wait_for_async_insert?: Bool; /** Timeout for waiting for processing asynchronous insertion */ - wait_for_async_insert_timeout?: Seconds + wait_for_async_insert_timeout?: Seconds; /** Timeout for waiting for window view fire signal in event time processing */ - wait_for_window_view_fire_signal_timeout?: Seconds + wait_for_window_view_fire_signal_timeout?: Seconds; /** The clean interval of window view in seconds to free outdated data. */ - window_view_clean_interval?: Seconds + window_view_clean_interval?: Seconds; /** The heartbeat interval in seconds to indicate watch query is alive. */ - window_view_heartbeat_interval?: Seconds + window_view_heartbeat_interval?: Seconds; /** Name of workload to be used to access resources */ - workload?: string + workload?: string; /** Allows you to select the max window log of ZSTD (it will not be used for MergeTree family) */ - zstd_window_log_max?: Int64 + zstd_window_log_max?: Int64; } /** @see https://clickhouse.com/docs/en/interfaces/http */ @@ -1607,539 +1607,539 @@ interface ClickHouseHTTPSettings { * This could help prevent errors that might occur during the streaming of SELECT queries. * Additionally, this is useful when executing DDLs on clustered environments, * as the client will receive the response only when the DDL is applied on all nodes of the cluster. */ - wait_end_of_query: Bool + wait_end_of_query: Bool; /** Format to use if a SELECT query is executed without a FORMAT clause. * Only useful for the {@link ClickHouseClient.exec} method, * as {@link ClickHouseClient.query} method always attaches this clause. */ - default_format: DataFormat + default_format: DataFormat; /** By default, the session is terminated after 60 seconds of inactivity * This is regulated by the `default_session_timeout` server setting. */ - session_timeout: UInt64 + session_timeout: UInt64; /** You can use this setting to check the session status before executing the query. * If a session is expired or cannot be found, the server returns `SESSION_NOT_FOUND` with error code 372. * NB: the session mechanism is only reliable when you connect directly to a particular ClickHouse server node. * Due to each particular session not being shared across the cluster, sessions won't work well in a multi-node environment with a load balancer, * as there will be no guarantee that each consequent request will be received on the same node. */ - session_check: Bool + session_check: Bool; } export type ClickHouseSettings = Partial & Partial & - Record + Record; export interface MergeTreeSettings { /** Allow floating point as partition key */ - allow_floating_point_partition_key?: Bool + allow_floating_point_partition_key?: Bool; /** Allow Nullable types as primary keys. */ - allow_nullable_key?: Bool + allow_nullable_key?: Bool; /** Don't use this setting in production, because it is not ready. */ - allow_remote_fs_zero_copy_replication?: Bool + allow_remote_fs_zero_copy_replication?: Bool; /** Reject primary/secondary indexes and sorting keys with identical expressions */ - allow_suspicious_indices?: Bool + allow_suspicious_indices?: Bool; /** Allows vertical merges from compact to wide parts. This settings must have the same value on all replicas */ - allow_vertical_merges_from_compact_to_wide_parts?: Bool + allow_vertical_merges_from_compact_to_wide_parts?: Bool; /** If true, replica never merge parts and always download merged parts from other replicas. */ - always_fetch_merged_part?: Bool + always_fetch_merged_part?: Bool; /** Generate UUIDs for parts. Before enabling check that all replicas support new format. */ - assign_part_uuids?: Bool + assign_part_uuids?: Bool; /** minimum interval between updates of async_block_ids_cache */ - async_block_ids_cache_min_update_interval_ms?: Milliseconds + async_block_ids_cache_min_update_interval_ms?: Milliseconds; /** If true, data from INSERT query is stored in queue and later flushed to table in background. */ - async_insert?: Bool + async_insert?: Bool; /** Obsolete setting, does nothing. */ - check_delay_period?: UInt64 + check_delay_period?: UInt64; /** Check columns or columns by hash for sampling are unsigned integer. */ - check_sample_column_is_correct?: Bool + check_sample_column_is_correct?: Bool; /** Is the Replicated Merge cleanup has to be done automatically at each merge or manually (possible values are 'Always'/'Never' (default)) */ - clean_deleted_rows?: 'Always' | 'Never' + clean_deleted_rows?: "Always" | "Never"; /** Minimum period to clean old queue logs, blocks hashes and parts. */ - cleanup_delay_period?: UInt64 + cleanup_delay_period?: UInt64; /** Add uniformly distributed value from 0 to x seconds to cleanup_delay_period to avoid thundering herd effect and subsequent DoS of ZooKeeper in case of very large number of tables. */ - cleanup_delay_period_random_add?: UInt64 + cleanup_delay_period_random_add?: UInt64; /** Preferred batch size for background cleanup (points are abstract but 1 point is approximately equivalent to 1 inserted block). */ - cleanup_thread_preferred_points_per_iteration?: UInt64 + cleanup_thread_preferred_points_per_iteration?: UInt64; /** Allow to create a table with sampling expression not in primary key. This is needed only to temporarily allow to run the server with wrong tables for backward compatibility. */ - compatibility_allow_sampling_expression_not_in_primary_key?: Bool + compatibility_allow_sampling_expression_not_in_primary_key?: Bool; /** Marks support compression, reduce mark file size and speed up network transmission. */ - compress_marks?: Bool + compress_marks?: Bool; /** Primary key support compression, reduce primary key file size and speed up network transmission. */ - compress_primary_key?: Bool + compress_primary_key?: Bool; /** Activate concurrent part removal (see 'max_part_removal_threads') only if the number of inactive data parts is at least this. */ - concurrent_part_removal_threshold?: UInt64 + concurrent_part_removal_threshold?: UInt64; /** Do not remove non byte-identical parts for ReplicatedMergeTree, instead detach them (maybe useful for further analysis). */ - detach_not_byte_identical_parts?: Bool + detach_not_byte_identical_parts?: Bool; /** Do not remove old local parts when repairing lost replica. */ - detach_old_local_parts_when_cloning_replica?: Bool + detach_old_local_parts_when_cloning_replica?: Bool; /** Name of storage disk. Can be specified instead of storage policy. */ - disk?: string + disk?: string; /** Enable parts with adaptive and non-adaptive granularity */ - enable_mixed_granularity_parts?: Bool + enable_mixed_granularity_parts?: Bool; /** Enable the endpoint id with zookeeper name prefix for the replicated merge tree table */ - enable_the_endpoint_id_with_zookeeper_name_prefix?: Bool + enable_the_endpoint_id_with_zookeeper_name_prefix?: Bool; /** Enable usage of Vertical merge algorithm. */ - enable_vertical_merge_algorithm?: UInt64 + enable_vertical_merge_algorithm?: UInt64; /** When greater than zero only a single replica starts the merge immediately, others wait up to that amount of time to download the result instead of doing merges locally. If the chosen replica doesn't finish the merge during that amount of time, fallback to standard behavior happens. */ - execute_merges_on_single_replica_time_threshold?: Seconds + execute_merges_on_single_replica_time_threshold?: Seconds; /** How many records about mutations that are done to keep. If zero, then keep all of them. */ - finished_mutations_to_keep?: UInt64 + finished_mutations_to_keep?: UInt64; /** Do fsync for every inserted part. Significantly decreases performance of inserts, not recommended to use with wide parts. */ - fsync_after_insert?: Bool + fsync_after_insert?: Bool; /** Do fsync for part directory after all part operations (writes, renames, etc.). */ - fsync_part_directory?: Bool + fsync_part_directory?: Bool; /** Obsolete setting, does nothing. */ - in_memory_parts_enable_wal?: Bool + in_memory_parts_enable_wal?: Bool; /** Obsolete setting, does nothing. */ - in_memory_parts_insert_sync?: Bool + in_memory_parts_insert_sync?: Bool; /** If table contains at least that many inactive parts in single partition, artificially slow down insert into table. */ - inactive_parts_to_delay_insert?: UInt64 + inactive_parts_to_delay_insert?: UInt64; /** If more than this number inactive parts in single partition, throw 'Too many inactive parts ...' exception. */ - inactive_parts_to_throw_insert?: UInt64 + inactive_parts_to_throw_insert?: UInt64; /** How many rows correspond to one primary key value. */ - index_granularity?: UInt64 + index_granularity?: UInt64; /** Approximate amount of bytes in single granule (0 - disabled). */ - index_granularity_bytes?: UInt64 + index_granularity_bytes?: UInt64; /** Retry period for table initialization, in seconds. */ - initialization_retry_period?: Seconds + initialization_retry_period?: Seconds; /** For background operations like merges, mutations etc. How many seconds before failing to acquire table locks. */ - lock_acquire_timeout_for_background_operations?: Seconds + lock_acquire_timeout_for_background_operations?: Seconds; /** Mark compress block size, the actual size of the block to compress. */ - marks_compress_block_size?: UInt64 + marks_compress_block_size?: UInt64; /** Compression encoding used by marks, marks are small enough and cached, so the default compression is ZSTD(3). */ - marks_compression_codec?: string + marks_compression_codec?: string; /** Only recalculate ttl info when MATERIALIZE TTL */ - materialize_ttl_recalculate_only?: Bool + materialize_ttl_recalculate_only?: Bool; /** The 'too many parts' check according to 'parts_to_delay_insert' and 'parts_to_throw_insert' will be active only if the average part size (in the relevant partition) is not larger than the specified threshold. If it is larger than the specified threshold, the INSERTs will be neither delayed or rejected. This allows to have hundreds of terabytes in a single table on a single server if the parts are successfully merged to larger parts. This does not affect the thresholds on inactive parts or total parts. */ - max_avg_part_size_for_too_many_parts?: UInt64 + max_avg_part_size_for_too_many_parts?: UInt64; /** Maximum in total size of parts to merge, when there are maximum free threads in background pool (or entries in replication queue). */ - max_bytes_to_merge_at_max_space_in_pool?: UInt64 + max_bytes_to_merge_at_max_space_in_pool?: UInt64; /** Maximum in total size of parts to merge, when there are minimum free threads in background pool (or entries in replication queue). */ - max_bytes_to_merge_at_min_space_in_pool?: UInt64 + max_bytes_to_merge_at_min_space_in_pool?: UInt64; /** Maximum period to clean old queue logs, blocks hashes and parts. */ - max_cleanup_delay_period?: UInt64 + max_cleanup_delay_period?: UInt64; /** Compress the pending uncompressed data in buffer if its size is larger or equal than the specified threshold. Block of data will be compressed even if the current granule is not finished. If this setting is not set, the corresponding global setting is used. */ - max_compress_block_size?: UInt64 + max_compress_block_size?: UInt64; /** Max number of concurrently executed queries related to the MergeTree table (0 - disabled). Queries will still be limited by other max_concurrent_queries settings. */ - max_concurrent_queries?: UInt64 + max_concurrent_queries?: UInt64; /** Max delay of inserting data into MergeTree table in seconds, if there are a lot of unmerged parts in single partition. */ - max_delay_to_insert?: UInt64 + max_delay_to_insert?: UInt64; /** Max delay of mutating MergeTree table in milliseconds, if there are a lot of unfinished mutations */ - max_delay_to_mutate_ms?: UInt64 + max_delay_to_mutate_ms?: UInt64; /** Max number of bytes to digest per segment to build GIN index. */ - max_digestion_size_per_segment?: UInt64 + max_digestion_size_per_segment?: UInt64; /** Not apply ALTER if number of files for modification(deletion, addition) more than this. */ - max_files_to_modify_in_alter_columns?: UInt64 + max_files_to_modify_in_alter_columns?: UInt64; /** Not apply ALTER, if number of files for deletion more than this. */ - max_files_to_remove_in_alter_columns?: UInt64 + max_files_to_remove_in_alter_columns?: UInt64; /** Maximum sleep time for merge selecting, a lower setting will trigger selecting tasks in background_schedule_pool frequently which result in large amount of requests to zookeeper in large-scale clusters */ - max_merge_selecting_sleep_ms?: UInt64 + max_merge_selecting_sleep_ms?: UInt64; /** When there is more than specified number of merges with TTL entries in pool, do not assign new merge with TTL. This is to leave free threads for regular merges and avoid \"Too many parts\" */ - max_number_of_merges_with_ttl_in_pool?: UInt64 + max_number_of_merges_with_ttl_in_pool?: UInt64; /** Limit the number of part mutations per replica to the specified amount. Zero means no limit on the number of mutations per replica (the execution can still be constrained by other settings). */ - max_number_of_mutations_for_replica?: UInt64 + max_number_of_mutations_for_replica?: UInt64; /** Obsolete setting, does nothing. */ - max_part_loading_threads?: MaxThreads + max_part_loading_threads?: MaxThreads; /** Obsolete setting, does nothing. */ - max_part_removal_threads?: MaxThreads + max_part_removal_threads?: MaxThreads; /** Limit the max number of partitions that can be accessed in one query. <= 0 means unlimited. This setting is the default that can be overridden by the query-level setting with the same name. */ - max_partitions_to_read?: Int64 + max_partitions_to_read?: Int64; /** If more than this number active parts in all partitions in total, throw 'Too many parts ...' exception. */ - max_parts_in_total?: UInt64 + max_parts_in_total?: UInt64; /** Max amount of parts which can be merged at once (0 - disabled). Doesn't affect OPTIMIZE FINAL query. */ - max_parts_to_merge_at_once?: UInt64 + max_parts_to_merge_at_once?: UInt64; /** The maximum speed of data exchange over the network in bytes per second for replicated fetches. Zero means unlimited. */ - max_replicated_fetches_network_bandwidth?: UInt64 + max_replicated_fetches_network_bandwidth?: UInt64; /** How many records may be in log, if there is inactive replica. Inactive replica becomes lost when when this number exceed. */ - max_replicated_logs_to_keep?: UInt64 + max_replicated_logs_to_keep?: UInt64; /** How many tasks of merging and mutating parts are allowed simultaneously in ReplicatedMergeTree queue. */ - max_replicated_merges_in_queue?: UInt64 + max_replicated_merges_in_queue?: UInt64; /** How many tasks of merging parts with TTL are allowed simultaneously in ReplicatedMergeTree queue. */ - max_replicated_merges_with_ttl_in_queue?: UInt64 + max_replicated_merges_with_ttl_in_queue?: UInt64; /** How many tasks of mutating parts are allowed simultaneously in ReplicatedMergeTree queue. */ - max_replicated_mutations_in_queue?: UInt64 + max_replicated_mutations_in_queue?: UInt64; /** The maximum speed of data exchange over the network in bytes per second for replicated sends. Zero means unlimited. */ - max_replicated_sends_network_bandwidth?: UInt64 + max_replicated_sends_network_bandwidth?: UInt64; /** Max broken parts, if more - deny automatic deletion. */ - max_suspicious_broken_parts?: UInt64 + max_suspicious_broken_parts?: UInt64; /** Max size of all broken parts, if more - deny automatic deletion. */ - max_suspicious_broken_parts_bytes?: UInt64 + max_suspicious_broken_parts_bytes?: UInt64; /** How many rows in blocks should be formed for merge operations. By default, has the same value as `index_granularity`. */ - merge_max_block_size?: UInt64 + merge_max_block_size?: UInt64; /** How many bytes in blocks should be formed for merge operations. By default, has the same value as `index_granularity_bytes`. */ - merge_max_block_size_bytes?: UInt64 + merge_max_block_size_bytes?: UInt64; /** Maximum sleep time for merge selecting, a lower setting will trigger selecting tasks in background_schedule_pool frequently which result in large amount of requests to zookeeper in large-scale clusters */ - merge_selecting_sleep_ms?: UInt64 + merge_selecting_sleep_ms?: UInt64; /** The sleep time for merge selecting task is multiplied by this factor when there's nothing to merge and divided when a merge was assigned */ - merge_selecting_sleep_slowdown_factor?: Float + merge_selecting_sleep_slowdown_factor?: Float; /** Remove old broken detached parts in the background if they remained untouched for a specified by this setting period of time. */ - merge_tree_clear_old_broken_detached_parts_ttl_timeout_seconds?: UInt64 + merge_tree_clear_old_broken_detached_parts_ttl_timeout_seconds?: UInt64; /** The period of executing the clear old parts operation in background. */ - merge_tree_clear_old_parts_interval_seconds?: UInt64 + merge_tree_clear_old_parts_interval_seconds?: UInt64; /** The period of executing the clear old temporary directories operation in background. */ - merge_tree_clear_old_temporary_directories_interval_seconds?: UInt64 + merge_tree_clear_old_temporary_directories_interval_seconds?: UInt64; /** Enable clearing old broken detached parts operation in background. */ - merge_tree_enable_clear_old_broken_detached?: UInt64 + merge_tree_enable_clear_old_broken_detached?: UInt64; /** Minimal time in seconds, when merge with recompression TTL can be repeated. */ - merge_with_recompression_ttl_timeout?: Int64 + merge_with_recompression_ttl_timeout?: Int64; /** Minimal time in seconds, when merge with delete TTL can be repeated. */ - merge_with_ttl_timeout?: Int64 + merge_with_ttl_timeout?: Int64; /** Minimal absolute delay to close, stop serving requests and not return Ok during status check. */ - min_absolute_delay_to_close?: UInt64 + min_absolute_delay_to_close?: UInt64; /** Whether min_age_to_force_merge_seconds should be applied only on the entire partition and not on subset. */ - min_age_to_force_merge_on_partition_only?: Bool + min_age_to_force_merge_on_partition_only?: Bool; /** If all parts in a certain range are older than this value, range will be always eligible for merging. Set to 0 to disable. */ - min_age_to_force_merge_seconds?: UInt64 + min_age_to_force_merge_seconds?: UInt64; /** Obsolete setting, does nothing. */ - min_bytes_for_compact_part?: UInt64 + min_bytes_for_compact_part?: UInt64; /** Minimal uncompressed size in bytes to create part in wide format instead of compact */ - min_bytes_for_wide_part?: UInt64 + min_bytes_for_wide_part?: UInt64; /** Minimal amount of bytes to enable part rebalance over JBOD array (0 - disabled). */ - min_bytes_to_rebalance_partition_over_jbod?: UInt64 + min_bytes_to_rebalance_partition_over_jbod?: UInt64; /** When granule is written, compress the data in buffer if the size of pending uncompressed data is larger or equal than the specified threshold. If this setting is not set, the corresponding global setting is used. */ - min_compress_block_size?: UInt64 + min_compress_block_size?: UInt64; /** Minimal number of compressed bytes to do fsync for part after fetch (0 - disabled) */ - min_compressed_bytes_to_fsync_after_fetch?: UInt64 + min_compressed_bytes_to_fsync_after_fetch?: UInt64; /** Minimal number of compressed bytes to do fsync for part after merge (0 - disabled) */ - min_compressed_bytes_to_fsync_after_merge?: UInt64 + min_compressed_bytes_to_fsync_after_merge?: UInt64; /** Min delay of inserting data into MergeTree table in milliseconds, if there are a lot of unmerged parts in single partition. */ - min_delay_to_insert_ms?: UInt64 + min_delay_to_insert_ms?: UInt64; /** Min delay of mutating MergeTree table in milliseconds, if there are a lot of unfinished mutations */ - min_delay_to_mutate_ms?: UInt64 + min_delay_to_mutate_ms?: UInt64; /** Minimum amount of bytes in single granule. */ - min_index_granularity_bytes?: UInt64 + min_index_granularity_bytes?: UInt64; /** Minimal number of marks to honor the MergeTree-level's max_concurrent_queries (0 - disabled). Queries will still be limited by other max_concurrent_queries settings. */ - min_marks_to_honor_max_concurrent_queries?: UInt64 + min_marks_to_honor_max_concurrent_queries?: UInt64; /** Minimal amount of bytes to enable O_DIRECT in merge (0 - disabled). */ - min_merge_bytes_to_use_direct_io?: UInt64 + min_merge_bytes_to_use_direct_io?: UInt64; /** Minimal delay from other replicas to close, stop serving requests and not return Ok during status check. */ - min_relative_delay_to_close?: UInt64 + min_relative_delay_to_close?: UInt64; /** Calculate relative replica delay only if absolute delay is not less that this value. */ - min_relative_delay_to_measure?: UInt64 + min_relative_delay_to_measure?: UInt64; /** Obsolete setting, does nothing. */ - min_relative_delay_to_yield_leadership?: UInt64 + min_relative_delay_to_yield_leadership?: UInt64; /** Keep about this number of last records in ZooKeeper log, even if they are obsolete. It doesn't affect work of tables: used only to diagnose ZooKeeper log before cleaning. */ - min_replicated_logs_to_keep?: UInt64 + min_replicated_logs_to_keep?: UInt64; /** Obsolete setting, does nothing. */ - min_rows_for_compact_part?: UInt64 + min_rows_for_compact_part?: UInt64; /** Minimal number of rows to create part in wide format instead of compact */ - min_rows_for_wide_part?: UInt64 + min_rows_for_wide_part?: UInt64; /** Minimal number of rows to do fsync for part after merge (0 - disabled) */ - min_rows_to_fsync_after_merge?: UInt64 + min_rows_to_fsync_after_merge?: UInt64; /** How many last blocks of hashes should be kept on disk (0 - disabled). */ - non_replicated_deduplication_window?: UInt64 + non_replicated_deduplication_window?: UInt64; /** When there is less than specified number of free entries in pool, do not execute part mutations. This is to leave free threads for regular merges and avoid \"Too many parts\" */ - number_of_free_entries_in_pool_to_execute_mutation?: UInt64 + number_of_free_entries_in_pool_to_execute_mutation?: UInt64; /** When there is less than specified number of free entries in pool (or replicated queue), start to lower maximum size of merge to process (or to put in queue). This is to allow small merges to process - not filling the pool with long running merges. */ - number_of_free_entries_in_pool_to_lower_max_size_of_merge?: UInt64 + number_of_free_entries_in_pool_to_lower_max_size_of_merge?: UInt64; /** If table has at least that many unfinished mutations, artificially slow down mutations of table. Disabled if set to 0 */ - number_of_mutations_to_delay?: UInt64 + number_of_mutations_to_delay?: UInt64; /** If table has at least that many unfinished mutations, throw 'Too many mutations' exception. Disabled if set to 0 */ - number_of_mutations_to_throw?: UInt64 + number_of_mutations_to_throw?: UInt64; /** How many seconds to keep obsolete parts. */ - old_parts_lifetime?: Seconds + old_parts_lifetime?: Seconds; /** Time to wait before/after moving parts between shards. */ - part_moves_between_shards_delay_seconds?: UInt64 + part_moves_between_shards_delay_seconds?: UInt64; /** Experimental/Incomplete feature to move parts between shards. Does not take into account sharding expressions. */ - part_moves_between_shards_enable?: UInt64 + part_moves_between_shards_enable?: UInt64; /** If table contains at least that many active parts in single partition, artificially slow down insert into table. Disabled if set to 0 */ - parts_to_delay_insert?: UInt64 + parts_to_delay_insert?: UInt64; /** If more than this number active parts in single partition, throw 'Too many parts ...' exception. */ - parts_to_throw_insert?: UInt64 + parts_to_throw_insert?: UInt64; /** If sum size of parts exceeds this threshold and time passed after replication log entry creation is greater than \"prefer_fetch_merged_part_time_threshold\", prefer fetching merged part from replica instead of doing merge locally. To speed up very long merges. */ - prefer_fetch_merged_part_size_threshold?: UInt64 + prefer_fetch_merged_part_size_threshold?: UInt64; /** If time passed after replication log entry creation exceeds this threshold and sum size of parts is greater than \"prefer_fetch_merged_part_size_threshold\", prefer fetching merged part from replica instead of doing merge locally. To speed up very long merges. */ - prefer_fetch_merged_part_time_threshold?: Seconds + prefer_fetch_merged_part_time_threshold?: Seconds; /** Primary compress block size, the actual size of the block to compress. */ - primary_key_compress_block_size?: UInt64 + primary_key_compress_block_size?: UInt64; /** Compression encoding used by primary, primary key is small enough and cached, so the default compression is ZSTD(3). */ - primary_key_compression_codec?: string + primary_key_compression_codec?: string; /** Minimal ratio of number of default values to number of all values in column to store it in sparse serializations. If >= 1, columns will be always written in full serialization. */ - ratio_of_defaults_for_sparse_serialization?: Float + ratio_of_defaults_for_sparse_serialization?: Float; /** When greater than zero only a single replica starts the merge immediately if merged part on shared storage and 'allow_remote_fs_zero_copy_replication' is enabled. */ - remote_fs_execute_merges_on_single_replica_time_threshold?: Seconds + remote_fs_execute_merges_on_single_replica_time_threshold?: Seconds; /** Run zero-copy in compatible mode during conversion process. */ - remote_fs_zero_copy_path_compatible_mode?: Bool + remote_fs_zero_copy_path_compatible_mode?: Bool; /** ZooKeeper path for zero-copy table-independent info. */ - remote_fs_zero_copy_zookeeper_path?: string + remote_fs_zero_copy_zookeeper_path?: string; /** Remove empty parts after they were pruned by TTL, mutation, or collapsing merge algorithm. */ - remove_empty_parts?: Bool + remove_empty_parts?: Bool; /** Setting for an incomplete experimental feature. */ - remove_rolled_back_parts_immediately?: Bool + remove_rolled_back_parts_immediately?: Bool; /** If true, Replicated tables replicas on this node will try to acquire leadership. */ - replicated_can_become_leader?: Bool + replicated_can_become_leader?: Bool; /** How many last blocks of hashes should be kept in ZooKeeper (old blocks will be deleted). */ - replicated_deduplication_window?: UInt64 + replicated_deduplication_window?: UInt64; /** How many last hash values of async_insert blocks should be kept in ZooKeeper (old blocks will be deleted). */ - replicated_deduplication_window_for_async_inserts?: UInt64 + replicated_deduplication_window_for_async_inserts?: UInt64; /** Similar to \"replicated_deduplication_window\", but determines old blocks by their lifetime. Hash of an inserted block will be deleted (and the block will not be deduplicated after) if it outside of one \"window\". You can set very big replicated_deduplication_window to avoid duplicating INSERTs during that period of time. */ - replicated_deduplication_window_seconds?: UInt64 + replicated_deduplication_window_seconds?: UInt64; /** Similar to \"replicated_deduplication_window_for_async_inserts\", but determines old blocks by their lifetime. Hash of an inserted block will be deleted (and the block will not be deduplicated after) if it outside of one \"window\". You can set very big replicated_deduplication_window to avoid duplicating INSERTs during that period of time. */ - replicated_deduplication_window_seconds_for_async_inserts?: UInt64 + replicated_deduplication_window_seconds_for_async_inserts?: UInt64; /** HTTP connection timeout for part fetch requests. Inherited from default profile `http_connection_timeout` if not set explicitly. */ - replicated_fetches_http_connection_timeout?: Seconds + replicated_fetches_http_connection_timeout?: Seconds; /** HTTP receive timeout for fetch part requests. Inherited from default profile `http_receive_timeout` if not set explicitly. */ - replicated_fetches_http_receive_timeout?: Seconds + replicated_fetches_http_receive_timeout?: Seconds; /** HTTP send timeout for part fetch requests. Inherited from default profile `http_send_timeout` if not set explicitly. */ - replicated_fetches_http_send_timeout?: Seconds + replicated_fetches_http_send_timeout?: Seconds; /** Max number of mutation commands that can be merged together and executed in one MUTATE_PART entry (0 means unlimited) */ - replicated_max_mutations_in_one_entry?: UInt64 + replicated_max_mutations_in_one_entry?: UInt64; /** Obsolete setting, does nothing. */ - replicated_max_parallel_fetches?: UInt64 + replicated_max_parallel_fetches?: UInt64; /** Limit parallel fetches from endpoint (actually pool size). */ - replicated_max_parallel_fetches_for_host?: UInt64 + replicated_max_parallel_fetches_for_host?: UInt64; /** Obsolete setting, does nothing. */ - replicated_max_parallel_fetches_for_table?: UInt64 + replicated_max_parallel_fetches_for_table?: UInt64; /** Obsolete setting, does nothing. */ - replicated_max_parallel_sends?: UInt64 + replicated_max_parallel_sends?: UInt64; /** Obsolete setting, does nothing. */ - replicated_max_parallel_sends_for_table?: UInt64 + replicated_max_parallel_sends_for_table?: UInt64; /** If ratio of wrong parts to total number of parts is less than this - allow to start. */ - replicated_max_ratio_of_wrong_parts?: Float + replicated_max_ratio_of_wrong_parts?: Float; /** Maximum number of parts to remove during one CleanupThread iteration (0 means unlimited). */ - simultaneous_parts_removal_limit?: UInt64 + simultaneous_parts_removal_limit?: UInt64; /** Name of storage disk policy */ - storage_policy?: string + storage_policy?: string; /** How many seconds to keep tmp_-directories. You should not lower this value because merges and mutations may not be able to work with low value of this setting. */ - temporary_directories_lifetime?: Seconds + temporary_directories_lifetime?: Seconds; /** Recompression works slow in most cases, so we don't start merge with recompression until this timeout and trying to fetch recompressed part from replica which assigned this merge with recompression. */ - try_fetch_recompressed_part_timeout?: Seconds + try_fetch_recompressed_part_timeout?: Seconds; /** Only drop altogether the expired parts and not partially prune them. */ - ttl_only_drop_parts?: Bool + ttl_only_drop_parts?: Bool; /** use in-memory cache to filter duplicated async inserts based on block ids */ - use_async_block_ids_cache?: Bool + use_async_block_ids_cache?: Bool; /** Experimental feature to speed up parts loading process by using MergeTree metadata cache */ - use_metadata_cache?: Bool + use_metadata_cache?: Bool; /** Use small format (dozens bytes) for part checksums in ZooKeeper instead of ordinary ones (dozens KB). Before enabling check that all replicas support new format. */ - use_minimalistic_checksums_in_zookeeper?: Bool + use_minimalistic_checksums_in_zookeeper?: Bool; /** Store part header (checksums and columns) in a compact format and a single part znode instead of separate znodes (/columns and /checksums). This can dramatically reduce snapshot size in ZooKeeper. Before enabling check that all replicas support new format. */ - use_minimalistic_part_header_in_zookeeper?: Bool + use_minimalistic_part_header_in_zookeeper?: Bool; /** Minimal (approximate) uncompressed size in bytes in merging parts to activate Vertical merge algorithm. */ - vertical_merge_algorithm_min_bytes_to_activate?: UInt64 + vertical_merge_algorithm_min_bytes_to_activate?: UInt64; /** Minimal amount of non-PK columns to activate Vertical merge algorithm. */ - vertical_merge_algorithm_min_columns_to_activate?: UInt64 + vertical_merge_algorithm_min_columns_to_activate?: UInt64; /** Minimal (approximate) sum of rows in merging parts to activate Vertical merge algorithm. */ - vertical_merge_algorithm_min_rows_to_activate?: UInt64 + vertical_merge_algorithm_min_rows_to_activate?: UInt64; /** Obsolete setting, does nothing. */ - write_ahead_log_bytes_to_fsync?: UInt64 + write_ahead_log_bytes_to_fsync?: UInt64; /** Obsolete setting, does nothing. */ - write_ahead_log_interval_ms_to_fsync?: UInt64 + write_ahead_log_interval_ms_to_fsync?: UInt64; /** Obsolete setting, does nothing. */ - write_ahead_log_max_bytes?: UInt64 + write_ahead_log_max_bytes?: UInt64; /** Obsolete setting, does nothing. */ - write_final_mark?: Bool + write_final_mark?: Bool; /** Max percentage of top level parts to postpone removal in order to get smaller independent ranges (highly not recommended to change) */ - zero_copy_concurrent_part_removal_max_postpone_ratio?: Float + zero_copy_concurrent_part_removal_max_postpone_ratio?: Float; /** Max recursion depth for splitting independent Outdated parts ranges into smaller subranges (highly not recommended to change) */ - zero_copy_concurrent_part_removal_max_split_times?: UInt64 + zero_copy_concurrent_part_removal_max_split_times?: UInt64; /** If zero copy replication is enabled sleep random amount of time before trying to lock depending on parts size for merge or mutation */ - zero_copy_merge_mutation_min_parts_size_sleep_before_lock?: UInt64 + zero_copy_merge_mutation_min_parts_size_sleep_before_lock?: UInt64; /** ZooKeeper session expiration check period, in seconds. */ - zookeeper_session_expiration_check_period?: Seconds + zookeeper_session_expiration_check_period?: Seconds; } -type Bool = 0 | 1 -type Int64 = string -type UInt64 = string -type UInt64Auto = string -type Float = number -type MaxThreads = number -type Seconds = number -type Milliseconds = number -type Char = string -type URI = string -type Map = SettingsMap +type Bool = 0 | 1; +type Int64 = string; +type UInt64 = string; +type UInt64Auto = string; +type Float = number; +type MaxThreads = number; +type Seconds = number; +type Milliseconds = number; +type Char = string; +type URI = string; +type Map = SettingsMap; export class SettingsMap { - private readonly record: Record + private readonly record: Record; private constructor(record: Record) { - this.record = record + this.record = record; } toString(): string { return `{${Object.entries(this.record) .map(([k, v]) => `'${k}':'${v}'`) - .join(',')}}` + .join(",")}}`; } static from(record: Record) { - return new this(record) + return new this(record); } } export type LoadBalancing = // among replicas with a minimum number of errors selected randomly - | 'random' + | "random" // a replica is selected among the replicas with the minimum number of errors // with the minimum number of distinguished characters // in the replica name and local hostname - | 'nearest_hostname' + | "nearest_hostname" // replicas with the same number of errors are accessed in the same order // as they are specified in the configuration. - | 'in_order' + | "in_order" // if first replica one has higher number of errors, // pick a random one from replicas with minimum number of errors - | 'first_or_random' + | "first_or_random" // round-robin across replicas with the same number of errors - | 'round_robin' + | "round_robin"; // Which rows should be included in TOTALS. export type TotalsMode = // Count HAVING for all read rows // including those not in max_rows_to_group_by // and have not passed HAVING after grouping - | 'before_having' + | "before_having" // Count on all rows except those that have not passed HAVING; // that is, to include in TOTALS all the rows that did not pass max_rows_to_group_by. - | 'after_having_inclusive' + | "after_having_inclusive" // Include only the rows that passed and max_rows_to_group_by, and HAVING. - | 'after_having_exclusive' + | "after_having_exclusive" // Automatically select between INCLUSIVE and EXCLUSIVE - | 'after_having_auto' + | "after_having_auto"; /// The setting for executing distributed sub-queries inside IN or JOIN sections. export type DistributedProductMode = - | 'deny' /// Disable - | 'local' /// Convert to local query - | 'global' /// Convert to global query - | 'allow' /// Enable + | "deny" /// Disable + | "local" /// Convert to local query + | "global" /// Convert to global query + | "allow"; /// Enable export type LogsLevel = - | 'none' /// Disable - | 'fatal' - | 'error' - | 'warning' - | 'information' - | 'debug' - | 'trace' - | 'test' + | "none" /// Disable + | "fatal" + | "error" + | "warning" + | "information" + | "debug" + | "trace" + | "test"; export type LogQueriesType = - | 'QUERY_START' - | 'QUERY_FINISH' - | 'EXCEPTION_BEFORE_START' - | 'EXCEPTION_WHILE_PROCESSING' + | "QUERY_START" + | "QUERY_FINISH" + | "EXCEPTION_BEFORE_START" + | "EXCEPTION_WHILE_PROCESSING"; export type DefaultTableEngine = - | 'Memory' - | 'ReplicatedMergeTree' - | 'ReplacingMergeTree' - | 'MergeTree' - | 'StripeLog' - | 'ReplicatedReplacingMergeTree' - | 'Log' - | 'None' + | "Memory" + | "ReplicatedMergeTree" + | "ReplacingMergeTree" + | "MergeTree" + | "StripeLog" + | "ReplicatedReplacingMergeTree" + | "Log" + | "None"; export type MySQLDataTypesSupport = // default - | '' + | "" // convert MySQL date type to ClickHouse String // (This is usually used when your mysql date is less than 1925) - | 'date2String' + | "date2String" // convert MySQL date type to ClickHouse Date32 - | 'date2Date32' + | "date2Date32" // convert MySQL DATETIME and TIMESTAMP and ClickHouse DateTime64 // if precision is > 0 or range is greater that for DateTime. - | 'datetime64' + | "datetime64" // convert MySQL decimal and number to ClickHouse Decimal when applicable - | 'decimal' + | "decimal"; export type DistributedDDLOutputMode = - | 'never_throw' - | 'null_status_on_timeout' - | 'throw' - | 'none' + | "never_throw" + | "null_status_on_timeout" + | "throw" + | "none"; export type ShortCircuitFunctionEvaluation = // Use short-circuit function evaluation for all functions. - | 'force_enable' + | "force_enable" // Disable short-circuit function evaluation. - | 'disable' + | "disable" // Use short-circuit function evaluation for functions that are suitable for it. - | 'enable' + | "enable"; -export type TransactionsWaitCSNMode = 'wait_unknown' | 'wait' | 'async' +export type TransactionsWaitCSNMode = "wait_unknown" | "wait" | "async"; export type EscapingRule = - | 'CSV' - | 'JSON' - | 'Quoted' - | 'Raw' - | 'XML' - | 'Escaped' - | 'None' + | "CSV" + | "JSON" + | "Quoted" + | "Raw" + | "XML" + | "Escaped" + | "None"; -export type DateTimeOutputFormat = 'simple' | 'iso' | 'unix_timestamp' +export type DateTimeOutputFormat = "simple" | "iso" | "unix_timestamp"; export type DateTimeInputFormat = // Use sophisticated rules to parse American style: mm/dd/yyyy - | 'best_effort_us' + | "best_effort_us" // Use sophisticated rules to parse whatever possible. - | 'best_effort' + | "best_effort" // Default format for fast parsing: YYYY-MM-DD hh:mm:ss // (ISO-8601 without fractional part and timezone) or unix timestamp. - | 'basic' + | "basic"; export type MsgPackUUIDRepresentation = // Output UUID as ExtType = 2 - | 'ext' + | "ext" // Output UUID as a string of 36 characters. - | 'str' + | "str" // Output UUID as 16-bytes binary. - | 'bin' + | "bin"; /// What to do if the limit is exceeded. export type OverflowMode = // Abort query execution, return what is. - | 'break' + | "break" // Throw exception. - | 'throw' + | "throw"; export type OverflowModeGroupBy = | OverflowMode // do not add new rows to the set, // but continue to aggregate for keys that are already in the set. - | 'any' + | "any"; /// Allows more optimal JOIN for typical cases. export type JoinStrictness = // Semi Join with any value from filtering table. // For LEFT JOIN with Any and RightAny are the same. - | 'ANY' + | "ANY" // If there are many suitable rows to join, // use all of them and replicate rows of "left" table (usual semantic of JOIN). - | 'ALL' + | "ALL" // Unspecified - | '' + | ""; export type JoinAlgorithm = - | 'prefer_partial_merge' - | 'hash' - | 'parallel_hash' - | 'partial_merge' - | 'auto' - | 'default' - | 'direct' - | 'full_sorting_merge' - | 'grace_hash' + | "prefer_partial_merge" + | "hash" + | "parallel_hash" + | "partial_merge" + | "auto" + | "default" + | "direct" + | "full_sorting_merge" + | "grace_hash"; -export type Dialect = 'clickhouse' | 'kusto' | 'kusto_auto' | 'prql' +export type Dialect = "clickhouse" | "kusto" | "kusto_auto" | "prql"; export type CapnProtoEnumComparingMode = - | 'by_names' - | 'by_values' - | 'by_names_case_insensitive' + | "by_names" + | "by_values" + | "by_names_case_insensitive"; export type ParquetCompression = - | 'none' - | 'snappy' - | 'zstd' - | 'gzip' - | 'lz4' - | 'brotli' + | "none" + | "snappy" + | "zstd" + | "gzip" + | "lz4" + | "brotli"; -export type ArrowCompression = 'none' | 'lz4_frame' | 'zstd' -export type ORCCompression = 'none' | 'snappy' | 'zstd' | 'gzip' | 'lz4' -export type SetOperationMode = '' | 'ALL' | 'DISTINCT' -export type LocalFSReadMethod = 'read' | 'pread' | 'mmap' -export type ParallelReplicasCustomKeyFilterType = 'default' | 'range' -export type IntervalOutputFormat = 'kusto' | 'numeric' -export type ParquetVersion = '1.0' | '2.4' | '2.6' | '2.latest' +export type ArrowCompression = "none" | "lz4_frame" | "zstd"; +export type ORCCompression = "none" | "snappy" | "zstd" | "gzip" | "lz4"; +export type SetOperationMode = "" | "ALL" | "DISTINCT"; +export type LocalFSReadMethod = "read" | "pread" | "mmap"; +export type ParallelReplicasCustomKeyFilterType = "default" | "range"; +export type IntervalOutputFormat = "kusto" | "numeric"; +export type ParquetVersion = "1.0" | "2.4" | "2.6" | "2.latest"; diff --git a/packages/client-common/src/ts_utils.ts b/packages/client-common/src/ts_utils.ts index f72a18a6..524afc78 100644 --- a/packages/client-common/src/ts_utils.ts +++ b/packages/client-common/src/ts_utils.ts @@ -5,4 +5,4 @@ export type IsSame = [A] extends [B] ? B extends A ? true : false - : false + : false; diff --git a/packages/client-common/src/utils/connection.ts b/packages/client-common/src/utils/connection.ts index 60080827..e737d5b5 100644 --- a/packages/client-common/src/utils/connection.ts +++ b/packages/client-common/src/utils/connection.ts @@ -1,22 +1,22 @@ -import type { ClickHouseSettings } from '../settings' +import type { ClickHouseSettings } from "../settings"; -export type HttpHeader = number | string | string[] -export type HttpHeaders = Record +export type HttpHeader = number | string | string[]; +export type HttpHeaders = Record; export function withCompressionHeaders({ headers, enable_request_compression, enable_response_compression, }: { - headers: HttpHeaders - enable_request_compression: boolean | undefined - enable_response_compression: boolean | undefined + headers: HttpHeaders; + enable_request_compression: boolean | undefined; + enable_response_compression: boolean | undefined; }): Record { return { ...headers, - ...(enable_response_compression ? { 'Accept-Encoding': 'gzip' } : {}), - ...(enable_request_compression ? { 'Content-Encoding': 'gzip' } : {}), - } + ...(enable_response_compression ? { "Accept-Encoding": "gzip" } : {}), + ...(enable_request_compression ? { "Content-Encoding": "gzip" } : {}), + }; } export function withHttpSettings( @@ -30,15 +30,15 @@ export function withHttpSettings( } : {}), ...clickhouse_settings, - } + }; } export function isSuccessfulResponse(statusCode?: number): boolean { - return Boolean(statusCode && 200 <= statusCode && statusCode < 300) + return Boolean(statusCode && 200 <= statusCode && statusCode < 300); } export function isJWTAuth(auth: unknown): auth is { access_token: string } { - return auth !== null && typeof auth === 'object' && 'access_token' in auth + return auth !== null && typeof auth === "object" && "access_token" in auth; } export function isCredentialsAuth( @@ -46,10 +46,10 @@ export function isCredentialsAuth( ): auth is { username: string; password: string } { return ( auth !== null && - typeof auth === 'object' && - 'username' in auth && - 'password' in auth - ) + typeof auth === "object" && + "username" in auth && + "password" in auth + ); } -export const EXCEPTION_TAG_HEADER_NAME = 'x-clickhouse-exception-tag' +export const EXCEPTION_TAG_HEADER_NAME = "x-clickhouse-exception-tag"; diff --git a/packages/client-common/src/utils/index.ts b/packages/client-common/src/utils/index.ts index f46eaaa7..d46feec5 100644 --- a/packages/client-common/src/utils/index.ts +++ b/packages/client-common/src/utils/index.ts @@ -1,4 +1,4 @@ -export * from './connection' -export * from './sleep' -export * from './stream' -export * from './url' +export * from "./connection"; +export * from "./sleep"; +export * from "./stream"; +export * from "./url"; diff --git a/packages/client-common/src/utils/sleep.ts b/packages/client-common/src/utils/sleep.ts index 98f5bcc8..eb41a87a 100644 --- a/packages/client-common/src/utils/sleep.ts +++ b/packages/client-common/src/utils/sleep.ts @@ -4,7 +4,7 @@ export async function sleep(ms: number): Promise { await new Promise((resolve) => setTimeout(() => { - resolve(void 0) + resolve(void 0); }, ms), - ) + ); } diff --git a/packages/client-common/src/utils/stream.ts b/packages/client-common/src/utils/stream.ts index 1c7fbacd..4023d8c3 100644 --- a/packages/client-common/src/utils/stream.ts +++ b/packages/client-common/src/utils/stream.ts @@ -1,9 +1,9 @@ -import { parseError } from '../error' +import { parseError } from "../error"; -const EXCEPTION_MARKER = '__exception__' +const EXCEPTION_MARKER = "__exception__"; -const NEWLINE = 0x0a as const -export const CARET_RETURN = 0x0d as const +const NEWLINE = 0x0a as const; +export const CARET_RETURN = 0x0d as const; /** * After 25.11, a newline error character is preceded by a caret return @@ -28,31 +28,31 @@ export function extractErrorAtTheEndOfChunk( EXCEPTION_MARKER.length + // __exception__ 2 + // \r\n exceptionTag.length + // - 2 // \r\n + 2; // \r\n - let errMsgLenStartIdx = chunk.length - bytesCountAfterErrLenHint + let errMsgLenStartIdx = chunk.length - bytesCountAfterErrLenHint; if (errMsgLenStartIdx < 1) { return new Error( - 'there was an error in the stream, but the last chunk is malformed', - ) + "there was an error in the stream, but the last chunk is malformed", + ); } do { - --errMsgLenStartIdx - } while (chunk[errMsgLenStartIdx] !== NEWLINE) + --errMsgLenStartIdx; + } while (chunk[errMsgLenStartIdx] !== NEWLINE); - const textDecoder = new TextDecoder('utf-8') + const textDecoder = new TextDecoder("utf-8"); const errMsgLen = parseInt( textDecoder.decode( chunk.subarray(errMsgLenStartIdx, -bytesCountAfterErrLenHint), ), - ) + ); if (isNaN(errMsgLen) || errMsgLen <= 0) { return new Error( - 'there was an error in the stream; failed to parse the message length', - ) + "there was an error in the stream; failed to parse the message length", + ); } const errMsg = textDecoder.decode( @@ -60,11 +60,11 @@ export function extractErrorAtTheEndOfChunk( errMsgLenStartIdx - errMsgLen + 1, // skipping the newline character errMsgLenStartIdx, ), - ) + ); - return parseError(errMsg) + return parseError(errMsg); } catch (err) { // theoretically, it can happen if a proxy cuts the last chunk - return err as Error + return err as Error; } } diff --git a/packages/client-common/src/utils/url.ts b/packages/client-common/src/utils/url.ts index c91f7ba5..eb9efab2 100644 --- a/packages/client-common/src/utils/url.ts +++ b/packages/client-common/src/utils/url.ts @@ -1,43 +1,43 @@ -import { formatQueryParams, formatQuerySettings } from '../data_formatter' -import type { ClickHouseSettings } from '../settings' +import { formatQueryParams, formatQuerySettings } from "../data_formatter"; +import type { ClickHouseSettings } from "../settings"; export function transformUrl({ url, pathname, searchParams, }: { - url: URL - pathname?: string - searchParams?: URLSearchParams + url: URL; + pathname?: string; + searchParams?: URLSearchParams; }): URL { - const newUrl = new URL(url) + const newUrl = new URL(url); if (pathname) { // See https://developer.mozilla.org/en-US/docs/Web/API/URL/pathname // > value for such "special scheme" URLs can never be the empty string, // > but will instead always have at least one / character. - if (newUrl.pathname === '/') { - newUrl.pathname = pathname + if (newUrl.pathname === "/") { + newUrl.pathname = pathname; } else { - newUrl.pathname += pathname + newUrl.pathname += pathname; } } if (searchParams) { - newUrl.search = searchParams?.toString() + newUrl.search = searchParams?.toString(); } - return newUrl + return newUrl; } interface ToSearchParamsOptions { - database: string | undefined - clickhouse_settings?: ClickHouseSettings - query_params?: Record - query?: string - session_id?: string - query_id: string - role?: string | Array + database: string | undefined; + clickhouse_settings?: ClickHouseSettings; + query_params?: Record; + query?: string; + session_id?: string; + query_id: string; + role?: string | Array; } // TODO validate max length of the resulting query @@ -51,44 +51,44 @@ export function toSearchParams({ query_id, role, }: ToSearchParamsOptions): URLSearchParams { - const entries: [string, string][] = [['query_id', query_id]] + const entries: [string, string][] = [["query_id", query_id]]; if (query_params !== undefined) { for (const [key, value] of Object.entries(query_params)) { - const formattedParam = formatQueryParams({ value }) - entries.push([`param_${key}`, formattedParam]) + const formattedParam = formatQueryParams({ value }); + entries.push([`param_${key}`, formattedParam]); } } if (clickhouse_settings !== undefined) { for (const [key, value] of Object.entries(clickhouse_settings)) { if (value !== undefined) { - entries.push([key, formatQuerySettings(value)]) + entries.push([key, formatQuerySettings(value)]); } } } - if (database !== undefined && database !== 'default') { - entries.push(['database', database]) + if (database !== undefined && database !== "default") { + entries.push(["database", database]); } if (query) { - entries.push(['query', query]) + entries.push(["query", query]); } if (session_id) { - entries.push(['session_id', session_id]) + entries.push(["session_id", session_id]); } if (role) { - if (typeof role === 'string') { - entries.push(['role', role]) + if (typeof role === "string") { + entries.push(["role", role]); } else if (Array.isArray(role)) { for (const r of role) { - entries.push(['role', r]) + entries.push(["role", r]); } } } - return new URLSearchParams(entries) + return new URLSearchParams(entries); } diff --git a/packages/client-common/src/version.ts b/packages/client-common/src/version.ts index fab0915d..a38a7bdc 100644 --- a/packages/client-common/src/version.ts +++ b/packages/client-common/src/version.ts @@ -1 +1 @@ -export default '1.21.0' +export default "1.21.0"; diff --git a/packages/client-node/__tests__/integration/node_abort_request.test.ts b/packages/client-node/__tests__/integration/node_abort_request.test.ts index 8355b9a4..571a2218 100644 --- a/packages/client-node/__tests__/integration/node_abort_request.test.ts +++ b/packages/client-node/__tests__/integration/node_abort_request.test.ts @@ -1,165 +1,165 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import type { ClickHouseClient, Row } from '@clickhouse/client-common' -import { createSimpleTable } from '@test/fixtures/simple_table' -import { jsonValues } from '@test/fixtures/test_data' -import { createTestClient } from '@test/utils/client' -import { guid } from '@test/utils/guid' -import type Stream from 'stream' -import { makeObjectStream } from '../utils/stream' - -describe('[Node.js] abort request', () => { - let client: ClickHouseClient +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import type { ClickHouseClient, Row } from "@clickhouse/client-common"; +import { createSimpleTable } from "@test/fixtures/simple_table"; +import { jsonValues } from "@test/fixtures/test_data"; +import { createTestClient } from "@test/utils/client"; +import { guid } from "@test/utils/guid"; +import type Stream from "stream"; +import { makeObjectStream } from "../utils/stream"; + +describe("[Node.js] abort request", () => { + let client: ClickHouseClient; beforeEach(() => { - client = createTestClient() - }) + client = createTestClient(); + }); afterEach(async () => { - await client.close() - }) + await client.close(); + }); - it('cancels a select query before it is sent', async () => { - const controller = new AbortController() + it("cancels a select query before it is sent", async () => { + const controller = new AbortController(); const selectPromise = client.query({ - query: 'SELECT sleep(3)', - format: 'CSV', + query: "SELECT sleep(3)", + format: "CSV", abort_signal: controller.signal, - }) - controller.abort() + }); + controller.abort(); await expect(selectPromise).rejects.toMatchObject({ // this happens even before we instantiate the request and its listeners, so that is just a plain AbortError - name: 'AbortError', - }) - }) + name: "AbortError", + }); + }); - it('cancels a select query while reading response', async () => { - const controller = new AbortController() + it("cancels a select query while reading response", async () => { + const controller = new AbortController(); const selectPromise = client .query({ - query: 'SELECT * from system.numbers', - format: 'JSONCompactEachRow', + query: "SELECT * from system.numbers", + format: "JSONCompactEachRow", abort_signal: controller.signal, }) .then(async (rows) => { - const stream = rows.stream() + const stream = rows.stream(); for await (const chunk of stream) { - const [[number]] = chunk.json() + const [[number]] = chunk.json(); // abort when reach number 3 - if (number === '3') { - controller.abort() + if (number === "3") { + controller.abort(); } } - }) + }); // There is no assertion against an error message. // A race condition on events might lead to // Request Aborted or ERR_STREAM_PREMATURE_CLOSE errors. - await expect(selectPromise).rejects.toThrow() - }) + await expect(selectPromise).rejects.toThrow(); + }); - it('cancels a select query while reading response by closing response stream', async () => { + it("cancels a select query while reading response by closing response stream", async () => { const selectPromise = client .query({ - query: 'SELECT * from system.numbers', - format: 'JSONCompactEachRow', + query: "SELECT * from system.numbers", + format: "JSONCompactEachRow", }) .then(async function (rows) { - const stream = rows.stream() + const stream = rows.stream(); for await (const rows of stream) { rows.forEach((row: Row) => { - const [[number]] = row.json<[[string]]>() + const [[number]] = row.json<[[string]]>(); // abort when reach number 3 - if (number === '3') { - stream.destroy() + if (number === "3") { + stream.destroy(); } - }) + }); } - }) - await expect(selectPromise).rejects.toThrow() - }) + }); + await expect(selectPromise).rejects.toThrow(); + }); - describe('insert', () => { - let tableName: string + describe("insert", () => { + let tableName: string; beforeEach(async () => { - tableName = `abort_request_insert_test_${guid()}` - await createSimpleTable(client, tableName) - }) + tableName = `abort_request_insert_test_${guid()}`; + await createSimpleTable(client, tableName); + }); - it('should cancel one insert while keeping the others', async () => { + it("should cancel one insert while keeping the others", async () => { function shouldAbort(i: number) { // we will cancel the request // that should've inserted a value at index 3 - return i === 3 + return i === 3; } - const controller = new AbortController() - const streams: Stream.Readable[] = Array(jsonValues.length) + const controller = new AbortController(); + const streams: Stream.Readable[] = Array(jsonValues.length); const insertStreamPromises = Promise.all( jsonValues.map((value, i) => { - const stream = makeObjectStream() - streams[i] = stream - stream.push(value) + const stream = makeObjectStream(); + streams[i] = stream; + stream.push(value); const insertPromise = client.insert({ values: stream, - format: 'JSONEachRow', + format: "JSONEachRow", table: tableName, abort_signal: shouldAbort(i) ? controller.signal : undefined, - }) + }); if (shouldAbort(i)) { return insertPromise.catch(() => { // ignored - }) + }); } - return insertPromise + return insertPromise; }), - ) + ); setTimeout(() => { streams.forEach((stream, i) => { if (shouldAbort(i)) { - controller.abort() + controller.abort(); } - stream.push(null) - }) - }, 100) + stream.push(null); + }); + }, 100); - await insertStreamPromises + await insertStreamPromises; const result = await client .query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, - format: 'JSONEachRow', + format: "JSONEachRow", }) - .then((r) => r.json()) + .then((r) => r.json()); expect(result).toEqual([ jsonValues[0], jsonValues[1], jsonValues[2], jsonValues[4], - ]) - }) + ]); + }); - it('cancels an insert query before it is sent', async () => { - const controller = new AbortController() - const stream = makeObjectStream() + it("cancels an insert query before it is sent", async () => { + const controller = new AbortController(); + const stream = makeObjectStream(); const insertPromise = client.insert({ table: tableName, values: stream, abort_signal: controller.signal, - }) - controller.abort() + }); + controller.abort(); await expect(insertPromise).rejects.toMatchObject({ // this happens even before we instantiate the request and its listeners, so that is just a plain AbortError - name: 'AbortError', - }) - }) + name: "AbortError", + }); + }); - it('cancels an insert query before it is sent by closing a stream', async () => { - const stream = makeObjectStream() - stream.push(null) + it("cancels an insert query before it is sent by closing a stream", async () => { + const stream = makeObjectStream(); + stream.push(null); expect( await client.insert({ @@ -170,25 +170,25 @@ describe('[Node.js] abort request', () => { expect.objectContaining({ query_id: expect.any(String), }), - ) - }) + ); + }); - it('cancels an insert query after it is sent', async () => { - const controller = new AbortController() - const stream = makeObjectStream() + it("cancels an insert query after it is sent", async () => { + const controller = new AbortController(); + const stream = makeObjectStream(); const insertPromise = client.insert({ table: tableName, values: stream, abort_signal: controller.signal, - }) + }); setTimeout(() => { - controller.abort() - }, 50) + controller.abort(); + }, 50); await expect(insertPromise).rejects.toMatchObject({ - message: expect.stringMatching('The user aborted a request'), - }) - }) - }) -}) + message: expect.stringMatching("The user aborted a request"), + }); + }); + }); +}); diff --git a/packages/client-node/__tests__/integration/node_client.test.ts b/packages/client-node/__tests__/integration/node_client.test.ts index 40acea72..e7f7e105 100644 --- a/packages/client-node/__tests__/integration/node_client.test.ts +++ b/packages/client-node/__tests__/integration/node_client.test.ts @@ -1,182 +1,182 @@ -import { vi, expect, it, describe, beforeEach, afterEach } from 'vitest' -import { getHeadersTestParams } from '@test/utils/parametrized' -import Http from 'http' -import type { ClickHouseClient } from '../../src' -import { createClient } from '../../src' -import { emitResponseBody, stubClientRequest } from '../utils/http_stubs' - -describe('[Node.js] Client', () => { - let httpRequestStub: ReturnType - let clientRequest: Http.ClientRequest +import { vi, expect, it, describe, beforeEach, afterEach } from "vitest"; +import { getHeadersTestParams } from "@test/utils/parametrized"; +import Http from "http"; +import type { ClickHouseClient } from "../../src"; +import { createClient } from "../../src"; +import { emitResponseBody, stubClientRequest } from "../utils/http_stubs"; + +describe("[Node.js] Client", () => { + let httpRequestStub: ReturnType; + let clientRequest: Http.ClientRequest; beforeEach(() => { - vi.clearAllMocks() - clientRequest = stubClientRequest() - httpRequestStub = vi.spyOn(Http, 'request').mockReturnValue(clientRequest) - }) + vi.clearAllMocks(); + clientRequest = stubClientRequest(); + httpRequestStub = vi.spyOn(Http, "request").mockReturnValue(clientRequest); + }); afterEach(() => { - vi.restoreAllMocks() - }) + vi.restoreAllMocks(); + }); - describe('Connection header (KeepAlive)', () => { + describe("Connection header (KeepAlive)", () => { it('should set "keep-alive" by default', async () => { - const client = createClient({}) - await query(client) - expect(httpRequestStub).toHaveBeenCalledTimes(1) - const calledWith = httpRequestStub.mock.lastCall![1] + const client = createClient({}); + await query(client); + expect(httpRequestStub).toHaveBeenCalledTimes(1); + const calledWith = httpRequestStub.mock.lastCall![1]; expect( - (calledWith.headers as Record)['Connection'], - ).toEqual('keep-alive') - }) + (calledWith.headers as Record)["Connection"], + ).toEqual("keep-alive"); + }); it('should set "close" when KeepAlive is disabled', async () => { const client = createClient({ keep_alive: { enabled: false }, - }) - await query(client) - expect(httpRequestStub).toHaveBeenCalledTimes(1) - const calledWith = httpRequestStub.mock.lastCall![1] + }); + await query(client); + expect(httpRequestStub).toHaveBeenCalledTimes(1); + const calledWith = httpRequestStub.mock.lastCall![1]; expect( - (calledWith.headers as Record)['Connection'], - ).toEqual('close') - }) + (calledWith.headers as Record)["Connection"], + ).toEqual("close"); + }); it('should set "keep-alive" when KeepAlive is explicitly enabled', async () => { const client = createClient({ keep_alive: { enabled: true }, - }) - await query(client) - expect(httpRequestStub).toHaveBeenCalledTimes(1) - const calledWith = httpRequestStub.mock.lastCall![1] + }); + await query(client); + expect(httpRequestStub).toHaveBeenCalledTimes(1); + const calledWith = httpRequestStub.mock.lastCall![1]; expect( - (calledWith.headers as Record)['Connection'], - ).toEqual('keep-alive') - }) - }) + (calledWith.headers as Record)["Connection"], + ).toEqual("keep-alive"); + }); + }); - describe('HTTP headers', () => { - it('should be possible to set http_headers', async () => { + describe("HTTP headers", () => { + it("should be possible to set http_headers", async () => { const client = createClient({ http_headers: { - 'Test-Header': 'foobar', - Authorization: 'should-be-overridden-by-client-anyway', + "Test-Header": "foobar", + Authorization: "should-be-overridden-by-client-anyway", }, - }) - await query(client) + }); + await query(client); - expect(httpRequestStub).toHaveBeenCalledTimes(1) - const [callURL, callOptions] = httpRequestStub.mock.lastCall! + expect(httpRequestStub).toHaveBeenCalledTimes(1); + const [callURL, callOptions] = httpRequestStub.mock.lastCall!; expect(callOptions.headers).toEqual({ ...defaultHeaders, - 'Test-Header': 'foobar', - }) - assertSearchParams(callURL) - }) - - it('should work without additional HTTP headers', async () => { - const client = createClient({}) - await query(client) - - expect(httpRequestStub).toHaveBeenCalledTimes(1) - const [callURL, callOptions] = httpRequestStub.mock.lastCall! - expect(callOptions.headers).toEqual(defaultHeaders) - assertSearchParams(callURL) - }) - - it('should work with additional HTTP headers on the method level', async () => { + "Test-Header": "foobar", + }); + assertSearchParams(callURL); + }); + + it("should work without additional HTTP headers", async () => { + const client = createClient({}); + await query(client); + + expect(httpRequestStub).toHaveBeenCalledTimes(1); + const [callURL, callOptions] = httpRequestStub.mock.lastCall!; + expect(callOptions.headers).toEqual(defaultHeaders); + assertSearchParams(callURL); + }); + + it("should work with additional HTTP headers on the method level", async () => { const client = createClient({ http_headers: { - FromInstance: 'foo', - Authorization: 'should-be-overridden-by-client-anyway', + FromInstance: "foo", + Authorization: "should-be-overridden-by-client-anyway", }, - }) + }); async function withEmit(method: () => Promise) { - const promise = method() - await emitResponseBody(clientRequest, 'hi') - await promise + const promise = method(); + await emitResponseBody(clientRequest, "hi"); + await promise; } - let requestCalls = 1 - const testParams = getHeadersTestParams(client) + let requestCalls = 1; + const testParams = getHeadersTestParams(client); for (const param of testParams) { - await withEmit(() => param.methodCall({ FromMethod: 'bar' })) + await withEmit(() => param.methodCall({ FromMethod: "bar" })); // ${param.methodName}: merges custom HTTP headers from both method and instance expect( getRequestHeaders(requestCalls++), `${param.methodName}: merges custom HTTP headers from both method and instance`, ).toEqual({ ...defaultHeaders, - FromInstance: 'foo', - FromMethod: 'bar', - }) + FromInstance: "foo", + FromMethod: "bar", + }); - await withEmit(() => param.methodCall({ FromInstance: 'bar' })) + await withEmit(() => param.methodCall({ FromInstance: "bar" })); // ${param.methodName}: overrides HTTP headers from the instance with the values from the method call expect( getRequestHeaders(requestCalls++), `${param.methodName}: overrides HTTP headers from the instance with the values from the method call`, ).toEqual({ ...defaultHeaders, - FromInstance: 'bar', - }) + FromInstance: "bar", + }); } - }) - }) + }); + }); - describe('Compression headers', () => { - it('should disable response compression by default', async () => { - const client = createClient() - await query(client) + describe("Compression headers", () => { + it("should disable response compression by default", async () => { + const client = createClient(); + await query(client); - expect(httpRequestStub).toHaveBeenCalledTimes(1) - const [callURL, callOptions] = httpRequestStub.mock.lastCall! - expect(callOptions.headers).toEqual(defaultHeaders) - assertSearchParams(callURL) - }) + expect(httpRequestStub).toHaveBeenCalledTimes(1); + const [callURL, callOptions] = httpRequestStub.mock.lastCall!; + expect(callOptions.headers).toEqual(defaultHeaders); + assertSearchParams(callURL); + }); - it('should enable response compression only', async () => { + it("should enable response compression only", async () => { const client = createClient({ compression: { response: true, }, - }) - await query(client) + }); + await query(client); - expect(httpRequestStub).toHaveBeenCalledTimes(1) - const [callURL, callOptions] = httpRequestStub.mock.lastCall! - assertCompressionRequestHeaders(callURL, callOptions) - }) + expect(httpRequestStub).toHaveBeenCalledTimes(1); + const [callURL, callOptions] = httpRequestStub.mock.lastCall!; + assertCompressionRequestHeaders(callURL, callOptions); + }); - it('should enable request compression only', async () => { + it("should enable request compression only", async () => { const client = createClient({ compression: { request: true, response: false, }, - }) - await query(client) + }); + await query(client); - expect(httpRequestStub).toHaveBeenCalledTimes(1) - const [callURL, callOptions] = httpRequestStub.mock.lastCall! + expect(httpRequestStub).toHaveBeenCalledTimes(1); + const [callURL, callOptions] = httpRequestStub.mock.lastCall!; // no additional request headers in this case - expect(callOptions.headers).toEqual(defaultHeaders) - assertSearchParams(callURL) - }) + expect(callOptions.headers).toEqual(defaultHeaders); + assertSearchParams(callURL); + }); - it('should enable both request and response compression', async () => { + it("should enable both request and response compression", async () => { const client = createClient({ compression: { request: true, response: true, }, - }) - await query(client) + }); + await query(client); - expect(httpRequestStub).toHaveBeenCalledTimes(1) - const [callURL, callOptions] = httpRequestStub.mock.lastCall! - assertCompressionRequestHeaders(callURL, callOptions) - }) + expect(httpRequestStub).toHaveBeenCalledTimes(1); + const [callURL, callOptions] = httpRequestStub.mock.lastCall!; + assertCompressionRequestHeaders(callURL, callOptions); + }); function assertCompressionRequestHeaders( callURL: string | URL, @@ -184,39 +184,39 @@ describe('[Node.js] Client', () => { ) { expect(callOptions.headers).toEqual({ ...defaultHeaders, - 'Accept-Encoding': 'gzip', - }) + "Accept-Encoding": "gzip", + }); - const searchParams = new URL(callURL).searchParams - expect(searchParams.get('enable_http_compression')).toEqual('1') - expect(searchParams.get('query_id')).not.toBeNull() - expect(searchParams.size).toEqual(2) + const searchParams = new URL(callURL).searchParams; + expect(searchParams.get("enable_http_compression")).toEqual("1"); + expect(searchParams.get("query_id")).not.toBeNull(); + expect(searchParams.size).toEqual(2); } - }) + }); async function query(client: ClickHouseClient) { const selectPromise = client.query({ - query: 'SELECT * FROM system.numbers LIMIT 5', - }) - await emitResponseBody(clientRequest, 'hi') - await selectPromise + query: "SELECT * FROM system.numbers LIMIT 5", + }); + await emitResponseBody(clientRequest, "hi"); + await selectPromise; } function assertSearchParams(callURL: string | URL) { - const searchParams = new URL(callURL).searchParams - expect(searchParams.get('query_id')).not.toBeNull() - expect(searchParams.size).toEqual(1) // only query_id by default + const searchParams = new URL(callURL).searchParams; + expect(searchParams.get("query_id")).not.toBeNull(); + expect(searchParams.size).toEqual(1); // only query_id by default } function getRequestHeaders(httpRequestStubCalledTimes = 1) { - expect(httpRequestStub).toHaveBeenCalledTimes(httpRequestStubCalledTimes) - const [, callOptions] = httpRequestStub.mock.lastCall! - return callOptions.headers + expect(httpRequestStub).toHaveBeenCalledTimes(httpRequestStubCalledTimes); + const [, callOptions] = httpRequestStub.mock.lastCall!; + return callOptions.headers; } const defaultHeaders: Record = { - Connection: 'keep-alive', - Authorization: 'Basic ZGVmYXVsdDo=', // default user with empty password - 'User-Agent': expect.stringContaining('clickhouse-js'), - } -}) + Connection: "keep-alive", + Authorization: "Basic ZGVmYXVsdDo=", // default user with empty password + "User-Agent": expect.stringContaining("clickhouse-js"), + }; +}); diff --git a/packages/client-node/__tests__/integration/node_command.test.ts b/packages/client-node/__tests__/integration/node_command.test.ts index f8acd7c2..cbd937fb 100644 --- a/packages/client-node/__tests__/integration/node_command.test.ts +++ b/packages/client-node/__tests__/integration/node_command.test.ts @@ -1,6 +1,6 @@ -import type { ClickHouseClient } from '@clickhouse/client-common' -import { describe, it, beforeEach, afterEach, expect } from 'vitest' -import { createTestClient } from '@test/utils/client' +import type { ClickHouseClient } from "@clickhouse/client-common"; +import { describe, it, beforeEach, afterEach, expect } from "vitest"; +import { createTestClient } from "@test/utils/client"; /** * {@link ClickHouseClient.command} re-introduction is the result of @@ -9,62 +9,62 @@ import { createTestClient } from '@test/utils/client' * * This test makes sure that the consequent requests are not blocked by command calls */ -describe('[Node.js] command', () => { - let client: ClickHouseClient +describe("[Node.js] command", () => { + let client: ClickHouseClient; beforeEach(() => { client = createTestClient({ max_open_connections: 1, request_timeout: 10000, - }) - }) + }); + }); afterEach(async () => { - await client.close() - }) + await client.close(); + }); - it('should destroy the response stream immediately', async () => { + it("should destroy the response stream immediately", async () => { const timeout = setTimeout(() => { - throw new Error('Timeout was triggered') - }, 3000).unref() + throw new Error("Timeout was triggered"); + }, 3000).unref(); function command() { return client.command({ query: `SELECT 1 FORMAT CSV`, - }) + }); } - await command() - await command() // if previous call holds the socket, the test will time out - clearTimeout(timeout) - expect(1).toBe(1) // Vitest needs at least 1 assertion - }) + await command(); + await command(); // if previous call holds the socket, the test will time out + clearTimeout(timeout); + expect(1).toBe(1); // Vitest needs at least 1 assertion + }); - describe('ignore error response', () => { - it('should throw an error by default when ignore_error_response is not set', async () => { + describe("ignore error response", () => { + it("should throw an error by default when ignore_error_response is not set", async () => { await expect( client.command({ - query: 'invalid', + query: "invalid", }), ).rejects.toMatchObject({ - message: expect.stringContaining('Syntax error'), - }) - }) + message: expect.stringContaining("Syntax error"), + }); + }); - it('should throw an error when ignore_error_response is false', async () => { + it("should throw an error when ignore_error_response is false", async () => { await expect( client.command({ - query: 'invalid', + query: "invalid", ignore_error_response: false, }), ).rejects.toMatchObject({ - message: expect.stringContaining('Syntax error'), - }) - }) + message: expect.stringContaining("Syntax error"), + }); + }); - it('should not throw an error when ignore_error_response is true', async () => { + it("should not throw an error when ignore_error_response is true", async () => { const result = await client.command({ - query: 'invalid', + query: "invalid", ignore_error_response: true, - }) + }); // command doesn't return a stream, just summary info - expect(result.query_id).toBeDefined() - }) - }) -}) + expect(result.query_id).toBeDefined(); + }); + }); +}); diff --git a/packages/client-node/__tests__/integration/node_compression.test.ts b/packages/client-node/__tests__/integration/node_compression.test.ts index 39e678fa..e1eba243 100644 --- a/packages/client-node/__tests__/integration/node_compression.test.ts +++ b/packages/client-node/__tests__/integration/node_compression.test.ts @@ -1,87 +1,87 @@ -import { describe, it, beforeEach, afterEach, expect } from 'vitest' -import { createTestClient } from '@test/utils/client' -import http from 'http' -import { type AddressInfo } from 'net' +import { describe, it, beforeEach, afterEach, expect } from "vitest"; +import { createTestClient } from "@test/utils/client"; +import http from "http"; +import { type AddressInfo } from "net"; -describe('[Node.js] Compression', () => { - describe('Malformed compression response', () => { +describe("[Node.js] Compression", () => { + describe("Malformed compression response", () => { const logAndQuit = (err: Error | unknown, prefix: string) => { - console.error(prefix, err) + console.error(prefix, err); expect.fail( `An unexpected error was propagated to the global context: ${prefix} ${err}`, - ) - } + ); + }; const uncaughtExceptionListener = (err: Error) => - logAndQuit(err, 'uncaughtException:') + logAndQuit(err, "uncaughtException:"); const unhandledRejectionListener = (err: unknown) => - logAndQuit(err, 'unhandledRejection:') + logAndQuit(err, "unhandledRejection:"); beforeEach(async () => { - process.on('uncaughtException', uncaughtExceptionListener) - process.on('unhandledRejection', unhandledRejectionListener) - }) + process.on("uncaughtException", uncaughtExceptionListener); + process.on("unhandledRejection", unhandledRejectionListener); + }); afterEach(async () => { - process.off('uncaughtException', uncaughtExceptionListener) - process.off('unhandledRejection', unhandledRejectionListener) - }) + process.off("uncaughtException", uncaughtExceptionListener); + process.off("unhandledRejection", unhandledRejectionListener); + }); - it('should not propagate the exception to the global context if a failed response is malformed', async () => { + it("should not propagate the exception to the global context if a failed response is malformed", async () => { const server = http.createServer(async (_req, res) => { - return makeResponse(res, 500) - }) + return makeResponse(res, 500); + }); await new Promise((resolve) => { - server.listen(0, () => resolve()) - }) - const port = (server.address() as AddressInfo).port + server.listen(0, () => resolve()); + }); + const port = (server.address() as AddressInfo).port; const client = createTestClient({ url: `http://localhost:${port}`, compression: { response: true, }, - }) + }); // The request fails completely (and the error message cannot be decompressed) await expect( client.query({ - query: 'SELECT 1', - format: 'JSONEachRow', + query: "SELECT 1", + format: "JSONEachRow", }), ).rejects.toMatchObject({ - code: 'Z_DATA_ERROR', - }) - }) + code: "Z_DATA_ERROR", + }); + }); - it('should not propagate the exception to the global context if a successful response is malformed', async () => { + it("should not propagate the exception to the global context if a successful response is malformed", async () => { const server = http.createServer(async (_req, res) => { - return makeResponse(res, 200) - }) + return makeResponse(res, 200); + }); await new Promise((resolve) => { - server.listen(0, () => resolve()) - }) - const port = (server.address() as AddressInfo).port + server.listen(0, () => resolve()); + }); + const port = (server.address() as AddressInfo).port; const client = createTestClient({ url: `http://localhost:${port}`, compression: { response: true, }, - }) + }); const rs = await client.query({ - query: 'SELECT 1', - format: 'JSONEachRow', - }) + query: "SELECT 1", + format: "JSONEachRow", + }); // Fails during the response streaming - await expect(rs.text()).rejects.toThrow() - }) - }) + await expect(rs.text()).rejects.toThrow(); + }); + }); function makeResponse(res: http.ServerResponse, status: 200 | 500) { - res.appendHeader('Content-Encoding', 'gzip') - res.statusCode = status - res.write('A malformed response without compression') - return res.end() + res.appendHeader("Content-Encoding", "gzip"); + res.statusCode = status; + res.write("A malformed response without compression"); + return res.end(); } -}) +}); diff --git a/packages/client-node/__tests__/integration/node_custom_http_agent.test.ts b/packages/client-node/__tests__/integration/node_custom_http_agent.test.ts index a8d2886a..b482da56 100644 --- a/packages/client-node/__tests__/integration/node_custom_http_agent.test.ts +++ b/packages/client-node/__tests__/integration/node_custom_http_agent.test.ts @@ -1,36 +1,36 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest' -import { TestEnv, isOnEnv } from '@test/utils/test_env' -import http from 'http' -import Http from 'http' -import { createClient } from '../../src' +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { TestEnv, isOnEnv } from "@test/utils/test_env"; +import http from "http"; +import Http from "http"; +import { createClient } from "../../src"; /** HTTPS agent tests are in tls.test.ts as it requires a secure connection. */ // disabled with Cloud as it uses a simple HTTP agent describe.skipIf(!isOnEnv(TestEnv.LocalSingleNode, TestEnv.LocalCluster))( - '[Node.js] custom HTTP agent', + "[Node.js] custom HTTP agent", () => { - let httpRequestStub: ReturnType + let httpRequestStub: ReturnType; beforeEach(() => { - httpRequestStub = vi.spyOn(Http, 'request') - }) + httpRequestStub = vi.spyOn(Http, "request"); + }); - it('should use provided http agent instead of the default one', async () => { + it("should use provided http agent instead of the default one", async () => { const agent = new http.Agent({ maxFreeSockets: 5, - }) + }); const client = createClient({ http_agent: agent, - }) + }); const rs = await client.query({ - query: 'SELECT 42 AS result', - format: 'JSONEachRow', - }) - expect(await rs.json()).toEqual([{ result: 42 }]) - expect(httpRequestStub).toHaveBeenCalledTimes(1) + query: "SELECT 42 AS result", + format: "JSONEachRow", + }); + expect(await rs.json()).toEqual([{ result: 42 }]); + expect(httpRequestStub).toHaveBeenCalledTimes(1); const callArgs = - httpRequestStub.mock.calls[httpRequestStub.mock.calls.length - 1] - expect(callArgs[1].agent).toBe(agent) - }) + httpRequestStub.mock.calls[httpRequestStub.mock.calls.length - 1]; + expect(callArgs[1].agent).toBe(agent); + }); }, -) +); diff --git a/packages/client-node/__tests__/integration/node_eager_socket_destroy.test.ts b/packages/client-node/__tests__/integration/node_eager_socket_destroy.test.ts index e33c32df..50735240 100644 --- a/packages/client-node/__tests__/integration/node_eager_socket_destroy.test.ts +++ b/packages/client-node/__tests__/integration/node_eager_socket_destroy.test.ts @@ -1,34 +1,34 @@ -import { describe, it, expect, vi, afterEach } from 'vitest' +import { describe, it, expect, vi, afterEach } from "vitest"; import { ClickHouseLogLevel, type ErrorLogParams, type Logger, type LogParams, -} from '@clickhouse/client-common' -import { createTestClient } from '@test/utils/client' -import * as http from 'http' -import { AddressInfo } from 'net' -import type { NodeClickHouseClientConfigOptions } from '../../src/config' +} from "@clickhouse/client-common"; +import { createTestClient } from "@test/utils/client"; +import * as http from "http"; +import { AddressInfo } from "net"; +import type { NodeClickHouseClientConfigOptions } from "../../src/config"; -describe('[Node.js] Eager socket destruction', () => { +describe("[Node.js] Eager socket destruction", () => { afterEach(() => { - vi.restoreAllMocks() - }) + vi.restoreAllMocks(); + }); - it('should destroy a stale socket and emit a TRACE log when eagerly_destroy_stale_sockets is true', async () => { + it("should destroy a stale socket and emit a TRACE log when eagerly_destroy_stale_sockets is true", async () => { // A very long TTL so that the idle timer does not fire during the test. // This ensures the socket stays in `freeSockets` until we manually trigger // the eager-destroy logic by mocking Date.now() to a future time. - const socketTTL = 60_000 + const socketTTL = 60_000; const traceMessages: Array<{ - message: string - args?: Record - }> = [] + message: string; + args?: Record; + }> = []; class CapturingLogger implements Logger { trace({ message, args }: LogParams) { - traceMessages.push({ message, args: args as Record }) + traceMessages.push({ message, args: args as Record }); } debug(_params: LogParams) {} info(_params: LogParams) {} @@ -37,9 +37,9 @@ describe('[Node.js] Eager socket destruction', () => { } const [server, port] = await createHTTPServer((_req, res) => { - res.write('Ok.') - res.end() - }) + res.write("Ok."); + res.end(); + }); const client = createTestClient({ url: `http://127.0.0.1:${port}`, @@ -52,71 +52,71 @@ describe('[Node.js] Eager socket destruction', () => { LoggerClass: CapturingLogger, level: ClickHouseLogLevel.TRACE, }, - } as NodeClickHouseClientConfigOptions) + } as NodeClickHouseClientConfigOptions); try { // Capture the current timestamp before the first request so that // futureNow is computed from a stable baseline rather than from // whatever Date.now() returns after the async sleep completes. - const baseNow = Date.now() + const baseNow = Date.now(); // First ping establishes the socket and, once the response is consumed, // returns it to agent.freeSockets with freed_at_timestamp_ms = Date.now(). - await client.ping() + await client.ping(); // Small delay to ensure the 'free' event has fired and the socket is // back in agent.freeSockets before the next request is sent. - await sleep(50) + await sleep(50); // Simulate passage of time beyond the TTL so the eager-destroy loop // considers the free socket to be stale. Using a constant mock so that // the idle timer (which only fires after socketTTL real milliseconds) // has no chance to fire and destroy the socket first. - const futureNow = baseNow + socketTTL + 100 - vi.spyOn(Date, 'now').mockReturnValue(futureNow) + const futureNow = baseNow + socketTTL + 100; + vi.spyOn(Date, "now").mockReturnValue(futureNow); // Second ping triggers the eager-destroy pre-request loop. - await client.ping() + await client.ping(); const destroyLogs = traceMessages.filter((m) => m.message.includes( - 'socket TTL expired based on timestamp, destroying socket', + "socket TTL expired based on timestamp, destroying socket", ), - ) - expect(destroyLogs.length).toBeGreaterThan(0) + ); + expect(destroyLogs.length).toBeGreaterThan(0); expect(destroyLogs[0].args).toMatchObject({ socket_age_ms: expect.any(Number), idle_socket_ttl_ms: socketTTL, - }) + }); } finally { - await client.close() - await closeServer(server) + await client.close(); + await closeServer(server); } - }) + }); - it('should emit a WARN log when reusing a socket whose TTL has expired by timestamp', async () => { + it("should emit a WARN log when reusing a socket whose TTL has expired by timestamp", async () => { // A very long TTL so that the idle timer does not fire during the test. - const socketTTL = 60_000 + const socketTTL = 60_000; const warnMessages: Array<{ - message: string - args?: Record - }> = [] + message: string; + args?: Record; + }> = []; class CapturingLogger implements Logger { trace(_params: LogParams) {} debug(_params: LogParams) {} info(_params: LogParams) {} warn({ message, args }: LogParams) { - warnMessages.push({ message, args: args as Record }) + warnMessages.push({ message, args: args as Record }); } error(_params: ErrorLogParams) {} } const [server, port] = await createHTTPServer((_req, res) => { - res.write('Ok.') - res.end() - }) + res.write("Ok."); + res.end(); + }); const client = createTestClient({ url: `http://127.0.0.1:${port}`, @@ -130,62 +130,62 @@ describe('[Node.js] Eager socket destruction', () => { LoggerClass: CapturingLogger, level: ClickHouseLogLevel.WARN, }, - } as NodeClickHouseClientConfigOptions) + } as NodeClickHouseClientConfigOptions); try { // Capture the current timestamp before the first request so that // futureNow is computed from a stable baseline rather than from // whatever Date.now() returns after the async sleep completes. - const baseNow = Date.now() + const baseNow = Date.now(); // First ping establishes the socket and returns it to freeSockets. - await client.ping() + await client.ping(); // Small delay to ensure the socket is back in agent.freeSockets. - await sleep(50) + await sleep(50); // Simulate passage of time beyond the TTL so the WARN log fires when // the reuse path checks freed_at_timestamp_ms. - const futureNow = baseNow + socketTTL + 2000 - vi.spyOn(Date, 'now').mockReturnValue(futureNow) + const futureNow = baseNow + socketTTL + 2000; + vi.spyOn(Date, "now").mockReturnValue(futureNow); // Second ping reuses the stale socket (eager destroy is off) and should // emit a WARN to alert the user of the situation. - await client.ping() + await client.ping(); const staleReuseWarnings = warnMessages.filter((m) => m.message.includes( - 'reusing socket with TTL expired based on timestamp', + "reusing socket with TTL expired based on timestamp", ), - ) - expect(staleReuseWarnings.length).toBeGreaterThan(0) + ); + expect(staleReuseWarnings.length).toBeGreaterThan(0); expect(staleReuseWarnings[0].args).toMatchObject({ socket_age_ms: expect.any(Number), idle_socket_ttl_ms: socketTTL, - }) + }); } finally { - await client.close() - await closeServer(server) + await client.close(); + await closeServer(server); } - }) -}) + }); +}); async function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)) + return new Promise((resolve) => setTimeout(resolve, ms)); } function closeServer(server: http.Server): Promise { return new Promise((resolve, reject) => { - server.close((err) => (err ? reject(err) : resolve())) - }) + server.close((err) => (err ? reject(err) : resolve())); + }); } async function createHTTPServer( cb: (req: http.IncomingMessage, res: http.ServerResponse) => void, ): Promise<[http.Server, number]> { - const server = http.createServer(cb) + const server = http.createServer(cb); await new Promise((resolve) => { - server.listen(0, () => resolve()) - }) - return [server, (server.address() as AddressInfo).port] + server.listen(0, () => resolve()); + }); + return [server, (server.address() as AddressInfo).port]; } diff --git a/packages/client-node/__tests__/integration/node_errors_parsing.test.ts b/packages/client-node/__tests__/integration/node_errors_parsing.test.ts index 5d72ce35..2ee1f395 100644 --- a/packages/client-node/__tests__/integration/node_errors_parsing.test.ts +++ b/packages/client-node/__tests__/integration/node_errors_parsing.test.ts @@ -1,17 +1,17 @@ -import { describe, it, expect } from 'vitest' -import { createClient } from '../../src' +import { describe, it, expect } from "vitest"; +import { createClient } from "../../src"; -describe('[Node.js] errors parsing', () => { - it('should return an error when URL is unreachable', async () => { +describe("[Node.js] errors parsing", () => { + it("should return an error when URL is unreachable", async () => { const client = createClient({ - url: 'http://localhost:1111', - }) + url: "http://localhost:1111", + }); await expect( client.query({ - query: 'SELECT * FROM system.numbers LIMIT 3', + query: "SELECT * FROM system.numbers LIMIT 3", }), ).rejects.toMatchObject({ - code: 'ECONNREFUSED', - }) - }) -}) + code: "ECONNREFUSED", + }); + }); +}); diff --git a/packages/client-node/__tests__/integration/node_exec.test.ts b/packages/client-node/__tests__/integration/node_exec.test.ts index d0e6004c..43507d4d 100644 --- a/packages/client-node/__tests__/integration/node_exec.test.ts +++ b/packages/client-node/__tests__/integration/node_exec.test.ts @@ -3,279 +3,279 @@ import { LogWriter, type ClickHouseClient, ClickHouseLogLevel, -} from '@clickhouse/client-common' -import { describe, it, beforeEach, afterEach, expect } from 'vitest' -import { createSimpleTable } from '@test/fixtures/simple_table' -import { createTestClient } from '@test/utils/client' -import { guid } from '@test/utils/guid' -import Stream from 'stream' -import Zlib from 'zlib' -import { ResultSet } from '../../src' -import { drainStreamInternal } from '../../src/connection/stream' -import { getAsText } from '../../src/utils' +} from "@clickhouse/client-common"; +import { describe, it, beforeEach, afterEach, expect } from "vitest"; +import { createSimpleTable } from "@test/fixtures/simple_table"; +import { createTestClient } from "@test/utils/client"; +import { guid } from "@test/utils/guid"; +import Stream from "stream"; +import Zlib from "zlib"; +import { ResultSet } from "../../src"; +import { drainStreamInternal } from "../../src/connection/stream"; +import { getAsText } from "../../src/utils"; -describe('[Node.js] exec', () => { - let client: ClickHouseClient - let log_writer: LogWriter +describe("[Node.js] exec", () => { + let client: ClickHouseClient; + let log_writer: LogWriter; beforeEach(() => { - client = createTestClient() + client = createTestClient(); log_writer = new LogWriter( new DefaultLogger(), - 'Connection', + "Connection", ClickHouseLogLevel.OFF, - ) - }) + ); + }); afterEach(async () => { - await client.close() - }) + await client.close(); + }); - it('should send a parametrized query', async () => { + it("should send a parametrized query", async () => { const result = await client.exec({ - query: 'SELECT plus({val1: Int32}, {val2: Int32})', + query: "SELECT plus({val1: Int32}, {val2: Int32})", query_params: { val1: 10, val2: 20, }, - }) - expect(await getAsText(result.stream)).toEqual('30\n') - }) + }); + expect(await getAsText(result.stream)).toEqual("30\n"); + }); - describe('trailing semi', () => { - it('should allow commands with semi in select clause', async () => { + describe("trailing semi", () => { + it("should allow commands with semi in select clause", async () => { const result = await client.exec({ query: `SELECT ';' FORMAT CSV`, - }) - expect(await getAsText(result.stream)).toEqual('";"\n') - }) + }); + expect(await getAsText(result.stream)).toEqual('";"\n'); + }); - it('should allow commands with trailing semi', async () => { + it("should allow commands with trailing semi", async () => { const result = await client.exec({ - query: 'EXISTS system.databases;', - }) - expect(await getAsText(result.stream)).toEqual('1\n') - }) + query: "EXISTS system.databases;", + }); + expect(await getAsText(result.stream)).toEqual("1\n"); + }); - it('should allow commands with multiple trailing semi', async () => { + it("should allow commands with multiple trailing semi", async () => { const result = await client.exec({ - query: 'EXISTS system.foobar;;;;;;', - }) - expect(await getAsText(result.stream)).toEqual('0\n') - }) + query: "EXISTS system.foobar;;;;;;", + }); + expect(await getAsText(result.stream)).toEqual("0\n"); + }); - it('should work with default_format', async () => { - const format = 'JSONEachRow' + it("should work with default_format", async () => { + const format = "JSONEachRow"; const { stream, query_id } = await client.exec({ - query: 'SELECT number FROM system.numbers LIMIT 1', + query: "SELECT number FROM system.numbers LIMIT 1", clickhouse_settings: { default_format: format, }, - }) + }); const rs = ResultSet.instance({ stream, format, query_id, log_error: (err) => { - console.error(err) + console.error(err); }, response_headers: {}, - }) - expect(await rs.json()).toEqual([{ number: '0' }]) - }) - }) + }); + expect(await rs.json()).toEqual([{ number: "0" }]); + }); + }); - describe('custom insert streaming with exec', () => { - let tableName: string + describe("custom insert streaming with exec", () => { + let tableName: string; beforeEach(async () => { - tableName = `test_node_exec_insert_stream_${guid()}` - await createSimpleTable(client, tableName) - }) + tableName = `test_node_exec_insert_stream_${guid()}`; + await createSimpleTable(client, tableName); + }); - it('should send an insert stream', async () => { + it("should send an insert stream", async () => { const stream = Stream.Readable.from(['42,foobar,"[1,2]"'], { objectMode: false, - }) + }); const execResult = await client.exec({ query: `INSERT INTO ${tableName} FORMAT CSV`, values: stream, - }) + }); // the result stream contains nothing useful for an insert and should be immediately drained to release the socket await drainStreamInternal( { - op: 'Insert', + op: "Insert", query_id: execResult.query_id, log_writer, log_level: ClickHouseLogLevel.OFF, }, execResult.stream, - ) + ); await checkInsertedValues([ { - id: '42', - name: 'foobar', + id: "42", + name: "foobar", sku: [1, 2], }, - ]) - }) + ]); + }); - it('should not fail with an empty stream', async () => { + it("should not fail with an empty stream", async () => { const stream = new Stream.Readable({ read() { // required }, objectMode: false, - }) + }); const execPromise = client.exec({ query: `INSERT INTO ${tableName} FORMAT CSV`, values: stream, - }) + }); // close the empty stream after the request is sent - stream.push(null) + stream.push(null); // the result stream contains nothing useful for an insert and should be immediately drained to release the socket - const execResult = await execPromise + const execResult = await execPromise; await drainStreamInternal( { - op: 'Insert', + op: "Insert", query_id: execResult.query_id, log_writer, log_level: ClickHouseLogLevel.OFF, }, execResult.stream, - ) - await checkInsertedValues([]) - }) + ); + await checkInsertedValues([]); + }); - it('should not fail with an already closed stream', async () => { + it("should not fail with an already closed stream", async () => { const stream = new Stream.Readable({ read() { // required }, objectMode: false, - }) - stream.push('42,foobar,"[1,2]"\n') + }); + stream.push('42,foobar,"[1,2]"\n'); // close the stream with some values - stream.push(null) + stream.push(null); const execResult = await client.exec({ query: `INSERT INTO ${tableName} FORMAT CSV`, values: stream, - }) + }); // the result stream contains nothing useful for an insert and should be immediately drained to release the socket await drainStreamInternal( { - op: 'Insert', + op: "Insert", query_id: execResult.query_id, log_writer, log_level: ClickHouseLogLevel.OFF, }, execResult.stream, - ) + ); await checkInsertedValues([ { - id: '42', - name: 'foobar', + id: "42", + name: "foobar", sku: [1, 2], }, - ]) - }) + ]); + }); - it('should not fail with an empty and already closed stream', async () => { + it("should not fail with an empty and already closed stream", async () => { const stream = new Stream.Readable({ read() { // required }, objectMode: false, - }) + }); // close the empty stream immediately - stream.push(null) + stream.push(null); const execResult = await client.exec({ query: `INSERT INTO ${tableName} FORMAT CSV`, values: stream, - }) + }); // the result stream contains nothing useful for an insert and should be immediately drained to release the socket await drainStreamInternal( { - op: 'Insert', + op: "Insert", query_id: execResult.query_id, log_writer, log_level: ClickHouseLogLevel.OFF, }, execResult.stream, - ) - await checkInsertedValues([]) - }) + ); + await checkInsertedValues([]); + }); async function checkInsertedValues(expected: Array) { const rs = await client.query({ query: `SELECT * FROM ${tableName}`, - format: 'JSONEachRow', - }) - expect(await rs.json()).toEqual(expected) + format: "JSONEachRow", + }); + expect(await rs.json()).toEqual(expected); } - }) + }); - describe('disabled stream decompression', () => { + describe("disabled stream decompression", () => { beforeEach(() => { client = createTestClient({ compression: { response: true, }, - }) - }) + }); + }); - it('should get a compressed response stream without decompressing it', async () => { + it("should get a compressed response stream without decompressing it", async () => { const result = await client.exec({ - query: 'SELECT 42 AS result FORMAT JSONEachRow', + query: "SELECT 42 AS result FORMAT JSONEachRow", decompress_response_stream: false, - }) - const text = await getAsText(decompress(result.stream)) - expect(text).toEqual('{"result":42}\n') - }) + }); + const text = await getAsText(decompress(result.stream)); + expect(text).toEqual('{"result":42}\n'); + }); - it('should force decompress in case of an error', async () => { + it("should force decompress in case of an error", async () => { await expect( client.exec({ - query: 'invalid', + query: "invalid", decompress_response_stream: false, }), ).rejects.toMatchObject({ - message: expect.stringContaining('Syntax error'), - }) - }) - }) + message: expect.stringContaining("Syntax error"), + }); + }); + }); - describe('ignore error response', () => { + describe("ignore error response", () => { beforeEach(() => { client = createTestClient({ compression: { response: true, }, - }) - }) + }); + }); - it('should get a decompressed response stream if ignore_error_response is true and default decompression config is passed', async () => { + it("should get a decompressed response stream if ignore_error_response is true and default decompression config is passed", async () => { const result = await client.exec({ - query: 'invalid', + query: "invalid", ignore_error_response: true, - }) - const text = await getAsText(result.stream) - expect(text).toContain('Syntax error') - }) + }); + const text = await getAsText(result.stream); + expect(text).toContain("Syntax error"); + }); - it('should get a compressed response stream if ignore_error_response is true and decompression is disabled', async () => { + it("should get a compressed response stream if ignore_error_response is true and decompression is disabled", async () => { const result = await client.exec({ - query: 'invalid', + query: "invalid", decompress_response_stream: false, ignore_error_response: true, - }) - const text = await getAsText(decompress(result.stream)) - expect(text).toContain('Syntax error') - }) - }) -}) + }); + const text = await getAsText(decompress(result.stream)); + expect(text).toContain("Syntax error"); + }); + }); +}); function decompress(stream: Stream.Readable) { return Stream.pipeline(stream, Zlib.createGunzip(), (err) => { if (err) { - console.error(err) + console.error(err); } - }) + }); } diff --git a/packages/client-node/__tests__/integration/node_insert.test.ts b/packages/client-node/__tests__/integration/node_insert.test.ts index bee3773c..9f69ae3d 100644 --- a/packages/client-node/__tests__/integration/node_insert.test.ts +++ b/packages/client-node/__tests__/integration/node_insert.test.ts @@ -1,64 +1,64 @@ -import type { ClickHouseClient } from '@clickhouse/client-common' -import { describe, it, beforeEach, afterEach, expect } from 'vitest' -import { createSimpleTable } from '@test/fixtures/simple_table' -import { createTestClient } from '@test/utils/client' -import { guid } from '@test/utils/guid' -import Stream from 'stream' +import type { ClickHouseClient } from "@clickhouse/client-common"; +import { describe, it, beforeEach, afterEach, expect } from "vitest"; +import { createSimpleTable } from "@test/fixtures/simple_table"; +import { createTestClient } from "@test/utils/client"; +import { guid } from "@test/utils/guid"; +import Stream from "stream"; -describe('[Node.js] insert', () => { - let client: ClickHouseClient - let tableName: string +describe("[Node.js] insert", () => { + let client: ClickHouseClient; + let tableName: string; afterEach(async () => { - await client.close() - }) + await client.close(); + }); - describe('without request compression', () => { + describe("without request compression", () => { beforeEach(async () => { - client = createTestClient() - tableName = `node_insert_test_${guid()}` - await createSimpleTable(client, tableName) - }) + client = createTestClient(); + tableName = `node_insert_test_${guid()}`; + await createSimpleTable(client, tableName); + }); - it('should provide error details about a dataset with an invalid type', async () => { + it("should provide error details about a dataset with an invalid type", async () => { await expect( client.insert({ table: tableName, values: Stream.Readable.from(['42,foobar,"[1,2]"'], { objectMode: false, }), - format: 'TabSeparated', + format: "TabSeparated", }), ).rejects.toMatchObject({ - message: expect.stringContaining('Cannot parse input'), - code: '27', - type: 'CANNOT_PARSE_INPUT_ASSERTION_FAILED', - }) - }) - }) + message: expect.stringContaining("Cannot parse input"), + code: "27", + type: "CANNOT_PARSE_INPUT_ASSERTION_FAILED", + }); + }); + }); - describe('with request compression', () => { + describe("with request compression", () => { beforeEach(async () => { client = createTestClient({ compression: { request: true, }, - }) - tableName = `node_insert_test_${guid()}` - await createSimpleTable(client, tableName) - }) + }); + tableName = `node_insert_test_${guid()}`; + await createSimpleTable(client, tableName); + }); - it('should not fail if the values array is empty', async () => { + it("should not fail if the values array is empty", async () => { const result = await client.insert({ table: tableName, values: [], - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); expect(result).toEqual({ executed: false, - query_id: '', + query_id: "", response_headers: {}, - }) - }) - }) -}) + }); + }); + }); +}); diff --git a/packages/client-node/__tests__/integration/node_jwt_auth.test.ts b/packages/client-node/__tests__/integration/node_jwt_auth.test.ts index 2afe9808..51d13725 100644 --- a/packages/client-node/__tests__/integration/node_jwt_auth.test.ts +++ b/packages/client-node/__tests__/integration/node_jwt_auth.test.ts @@ -1,57 +1,57 @@ -import { describe, it, expect, beforeAll, afterEach } from 'vitest' -import { TestEnv, isOnEnv } from '@test/utils/test_env' -import { EnvKeys, getFromEnv, maybeGetFromEnv } from '@test/utils/env' -import { createClient } from '../../src' -import type { NodeClickHouseClient } from '../../src/client' +import { describe, it, expect, beforeAll, afterEach } from "vitest"; +import { TestEnv, isOnEnv } from "@test/utils/test_env"; +import { EnvKeys, getFromEnv, maybeGetFromEnv } from "@test/utils/env"; +import { createClient } from "../../src"; +import type { NodeClickHouseClient } from "../../src/client"; -describe.skipIf(!isOnEnv(TestEnv.Cloud))('[Node.js] JWT auth', () => { - let jwtClient: NodeClickHouseClient - let url: string - let jwt: string | undefined +describe.skipIf(!isOnEnv(TestEnv.Cloud))("[Node.js] JWT auth", () => { + let jwtClient: NodeClickHouseClient; + let url: string; + let jwt: string | undefined; beforeAll(() => { - url = `https://${getFromEnv(EnvKeys.host)}:8443` - jwt = maybeGetFromEnv(EnvKeys.jwt_access_token) - }) + url = `https://${getFromEnv(EnvKeys.host)}:8443`; + jwt = maybeGetFromEnv(EnvKeys.jwt_access_token); + }); afterEach(async () => { - await jwtClient?.close() - }) + await jwtClient?.close(); + }); - it('should work with client configuration', async ({ skip }) => { + it("should work with client configuration", async ({ skip }) => { if (!jwt) { - skip(`Environment variable ${EnvKeys.jwt_access_token} is not set`) + skip(`Environment variable ${EnvKeys.jwt_access_token} is not set`); } jwtClient = createClient({ url, access_token: jwt, - }) + }); const rs = await jwtClient.query({ - query: 'SELECT 42 AS result', - format: 'JSONEachRow', - }) - expect(await rs.json()).toEqual([{ result: 42 }]) - }) + query: "SELECT 42 AS result", + format: "JSONEachRow", + }); + expect(await rs.json()).toEqual([{ result: 42 }]); + }); - it('should override the client instance auth', async ({ skip }) => { + it("should override the client instance auth", async ({ skip }) => { if (!jwt) { - skip(`Environment variable ${EnvKeys.jwt_access_token} is not set`) + skip(`Environment variable ${EnvKeys.jwt_access_token} is not set`); // return is needed to satisfy typescript, it does not mark skip() as terminating - return + return; } jwtClient = createClient({ url, - username: 'gibberish', - password: 'gibberish', - }) + username: "gibberish", + password: "gibberish", + }); const rs = await jwtClient.query({ - query: 'SELECT 42 AS result', - format: 'JSONEachRow', + query: "SELECT 42 AS result", + format: "JSONEachRow", auth: { access_token: jwt, }, - }) - expect(await rs.json()).toEqual([{ result: 42 }]) - }) -}) + }); + expect(await rs.json()).toEqual([{ result: 42 }]); + }); +}); diff --git a/packages/client-node/__tests__/integration/node_keep_alive.test.ts b/packages/client-node/__tests__/integration/node_keep_alive.test.ts index d1336912..7476f07c 100644 --- a/packages/client-node/__tests__/integration/node_keep_alive.test.ts +++ b/packages/client-node/__tests__/integration/node_keep_alive.test.ts @@ -1,81 +1,81 @@ -import { describe, it, expect, afterEach } from 'vitest' -import { ClickHouseLogLevel } from '@clickhouse/client-common' -import { createSimpleTable } from '@test/fixtures/simple_table' -import { guid } from '@test/utils/guid' -import { sleep } from '@test/utils/sleep' -import type { ClickHouseClient } from '../../src' -import type { NodeClickHouseClientConfigOptions } from '../../src/config' -import { createNodeTestClient } from '../utils/node_client' +import { describe, it, expect, afterEach } from "vitest"; +import { ClickHouseLogLevel } from "@clickhouse/client-common"; +import { createSimpleTable } from "@test/fixtures/simple_table"; +import { guid } from "@test/utils/guid"; +import { sleep } from "@test/utils/sleep"; +import type { ClickHouseClient } from "../../src"; +import type { NodeClickHouseClientConfigOptions } from "../../src/config"; +import { createNodeTestClient } from "../utils/node_client"; -describe('[Node.js] Keep Alive', () => { - let client: ClickHouseClient - const socketTTL = 2500 // seems to be a sweet spot for testing Keep-Alive socket hangups with 3s in config.xml +describe("[Node.js] Keep Alive", () => { + let client: ClickHouseClient; + const socketTTL = 2500; // seems to be a sweet spot for testing Keep-Alive socket hangups with 3s in config.xml afterEach(async () => { - await client.close() - }) + await client.close(); + }); - describe('query', () => { - it('should recreate the request if socket is potentially expired', async () => { + describe("query", () => { + it("should recreate the request if socket is potentially expired", async () => { client = createNodeTestClient({ max_open_connections: 1, keep_alive: { enabled: true, idle_socket_ttl: socketTTL, }, - } as NodeClickHouseClientConfigOptions) - expect(await query(0)).toEqual(1) - await sleep(socketTTL) + } as NodeClickHouseClientConfigOptions); + expect(await query(0)).toEqual(1); + await sleep(socketTTL); // this one could've failed without idle socket release - expect(await query(1)).toEqual(2) - }) + expect(await query(1)).toEqual(2); + }); - it('should disable keep alive', async () => { + it("should disable keep alive", async () => { client = createNodeTestClient({ max_open_connections: 1, keep_alive: { enabled: true, }, - } as NodeClickHouseClientConfigOptions) - expect(await query(0)).toEqual(1) - await sleep(socketTTL) + } as NodeClickHouseClientConfigOptions); + expect(await query(0)).toEqual(1); + await sleep(socketTTL); // this one won't fail cause a new socket will be assigned - expect(await query(1)).toEqual(2) - }) + expect(await query(1)).toEqual(2); + }); - it('should use multiple connections', async () => { + it("should use multiple connections", async () => { client = createNodeTestClient({ keep_alive: { enabled: true, idle_socket_ttl: socketTTL, }, - } as NodeClickHouseClientConfigOptions) + } as NodeClickHouseClientConfigOptions); const results = await Promise.all( [...Array(4).keys()].map((n) => query(n)), - ) - expect(results.sort()).toEqual([1, 2, 3, 4]) - await sleep(socketTTL) + ); + expect(results.sort()).toEqual([1, 2, 3, 4]); + await sleep(socketTTL); const results2 = await Promise.all( [...Array(4).keys()].map((n) => query(n + 10)), - ) - expect(results2.sort()).toEqual([11, 12, 13, 14]) - }) + ); + expect(results2.sort()).toEqual([11, 12, 13, 14]); + }); async function query(n: number) { const rs = await client.query({ query: `SELECT * FROM system.numbers LIMIT ${1 + n}`, - format: 'JSONEachRow', - }) - return (await rs.json>()).length + format: "JSONEachRow", + }); + return (await rs.json>()).length; } - }) + }); // the stream is not even piped into the request before we check // if the assigned socket is potentially expired, but better safe than sorry. // keep alive sockets for insert operations should be reused as normal - describe('insert', () => { - let tableName: string - it('should not duplicate insert requests (single connection)', async () => { + describe("insert", () => { + let tableName: string; + it("should not duplicate insert requests (single connection)", async () => { client = createNodeTestClient({ max_open_connections: 1, log: { @@ -85,59 +85,59 @@ describe('[Node.js] Keep Alive', () => { enabled: true, idle_socket_ttl: socketTTL, }, - } as NodeClickHouseClientConfigOptions) - tableName = `keep_alive_single_connection_insert_${guid()}` - await createSimpleTable(client, tableName) - await insert(0) - await sleep(socketTTL) + } as NodeClickHouseClientConfigOptions); + tableName = `keep_alive_single_connection_insert_${guid()}`; + await createSimpleTable(client, tableName); + await insert(0); + await sleep(socketTTL); // this one should not fail, as it will have a fresh socket - await insert(1) + await insert(1); const rs = await client.query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); expect(await rs.json()).toEqual([ - { id: `42`, name: 'hello', sku: [0, 1] }, - { id: `43`, name: 'hello', sku: [1, 2] }, - ]) - }) + { id: `42`, name: "hello", sku: [0, 1] }, + { id: `43`, name: "hello", sku: [1, 2] }, + ]); + }); - it('should not duplicate insert requests (multiple connections)', async () => { + it("should not duplicate insert requests (multiple connections)", async () => { client = createNodeTestClient({ max_open_connections: 2, keep_alive: { enabled: true, idle_socket_ttl: socketTTL, }, - } as NodeClickHouseClientConfigOptions) - tableName = `keep_alive_multiple_connection_insert_${guid()}` - await createSimpleTable(client, tableName) - await Promise.all([...Array(3).keys()].map((n) => insert(n))) - await sleep(socketTTL) + } as NodeClickHouseClientConfigOptions); + tableName = `keep_alive_multiple_connection_insert_${guid()}`; + await createSimpleTable(client, tableName); + await Promise.all([...Array(3).keys()].map((n) => insert(n))); + await sleep(socketTTL); // at least two of these should use a fresh socket - await Promise.all([...Array(3).keys()].map((n) => insert(n + 10))) + await Promise.all([...Array(3).keys()].map((n) => insert(n + 10))); const rs = await client.query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); expect(await rs.json()).toEqual([ // first "batch" - { id: `42`, name: 'hello', sku: [0, 1] }, - { id: `43`, name: 'hello', sku: [1, 2] }, - { id: `44`, name: 'hello', sku: [2, 3] }, + { id: `42`, name: "hello", sku: [0, 1] }, + { id: `43`, name: "hello", sku: [1, 2] }, + { id: `44`, name: "hello", sku: [2, 3] }, // second "batch" - { id: `52`, name: 'hello', sku: [10, 11] }, - { id: `53`, name: 'hello', sku: [11, 12] }, - { id: `54`, name: 'hello', sku: [12, 13] }, - ]) - }) + { id: `52`, name: "hello", sku: [10, 11] }, + { id: `53`, name: "hello", sku: [11, 12] }, + { id: `54`, name: "hello", sku: [12, 13] }, + ]); + }); async function insert(n: number) { await client.insert({ table: tableName, - values: [{ id: `${42 + n}`, name: 'hello', sku: [n, n + 1] }], - format: 'JSONEachRow', - }) + values: [{ id: `${42 + n}`, name: "hello", sku: [n, n + 1] }], + format: "JSONEachRow", + }); } - }) -}) + }); +}); diff --git a/packages/client-node/__tests__/integration/node_keep_alive_header.test.ts b/packages/client-node/__tests__/integration/node_keep_alive_header.test.ts index 1112deab..03a361df 100644 --- a/packages/client-node/__tests__/integration/node_keep_alive_header.test.ts +++ b/packages/client-node/__tests__/integration/node_keep_alive_header.test.ts @@ -1,44 +1,44 @@ -import { ClickHouseLogLevel, Logger } from '@clickhouse/client-common' -import { describe, it } from 'vitest' -import { createTestClient } from '@test/utils/client' -import net from 'net' -import type { NodeClickHouseClientConfigOptions } from '../../src/config' -import { AddressInfo } from 'net' - -describe.concurrent('Handling keep-alive header', () => { - it('should log the suggestion', async ({ expect }) => { - let sleepServerPromiseResolve: () => void +import { ClickHouseLogLevel, Logger } from "@clickhouse/client-common"; +import { describe, it } from "vitest"; +import { createTestClient } from "@test/utils/client"; +import net from "net"; +import type { NodeClickHouseClientConfigOptions } from "../../src/config"; +import { AddressInfo } from "net"; + +describe.concurrent("Handling keep-alive header", () => { + it("should log the suggestion", async ({ expect }) => { + let sleepServerPromiseResolve: () => void; let sleepServerPromise = new Promise((resolve) => { - sleepServerPromiseResolve = resolve + sleepServerPromiseResolve = resolve; // Simulate a ClickHouse server that responds with a delay - }) + }); - let attempted = 0 + let attempted = 0; const [server, port] = await createTCPServer(async (socket) => { - attempted++ + attempted++; if (attempted >= 2) { - socket.destroy() - expect.fail('Extra connection attempt - should not happen') + socket.destroy(); + expect.fail("Extra connection attempt - should not happen"); } // Write a valid response socket.write( - 'HTTP/1.1 200 OK\r\n' + - 'Content-Type: text/plain\r\n' + - 'Content-Length: 3\r\n' + - 'Connection: Keep-Alive\r\n' + - 'Keep-Alive: timeout=10, max=9999\r\n' + - '\r\n' + - 'Ok.', - ) + "HTTP/1.1 200 OK\r\n" + + "Content-Type: text/plain\r\n" + + "Content-Length: 3\r\n" + + "Connection: Keep-Alive\r\n" + + "Keep-Alive: timeout=10, max=9999\r\n" + + "\r\n" + + "Ok.", + ); // Then start the next request - await sleepServerPromise + await sleepServerPromise; // …and then close the connection before sending anything, // to trigger the error in the client - socket.end() - }) + socket.end(); + }); - const logs: any[] = [] - const LoggerClass = createLoggerClass(logs) + const logs: any[] = []; + const LoggerClass = createLoggerClass(logs); const client = createTestClient({ url: `http://127.0.0.1:${port}`, @@ -50,9 +50,11 @@ describe.concurrent('Handling keep-alive header', () => { LoggerClass, level: ClickHouseLogLevel.TRACE, }, - } as NodeClickHouseClientConfigOptions) + } as NodeClickHouseClientConfigOptions); - expect(await client.ping({ select: true })).toMatchObject({ success: true }) + expect(await client.ping({ select: true })).toMatchObject({ + success: true, + }); expect( findMatchingLogEvents( @@ -63,22 +65,22 @@ describe.concurrent('Handling keep-alive header', () => { args: { server_keep_alive_timeout_ms: 10000, }, - }) + }); - let ping2 = client.ping({ select: true }) + let ping2 = client.ping({ select: true }); // Client has a sleep(0) inside, the test has to wait for it to complete, // otherwise the socket gets closed before the client gets to use it. // This way we get the "socket hang up" error instead of "ECONNRESET". - await sleep(0) - sleepServerPromiseResolve!() - ping2 = await ping2 + await sleep(0); + sleepServerPromiseResolve!(); + ping2 = await ping2; - expect(ping2).toMatchObject({ success: false }) + expect(ping2).toMatchObject({ success: false }); if (ping2.success) { - throw new Error('Ping should have failed') + throw new Error("Ping should have failed"); } - expect(ping2.error.code).toMatch(/ECONNRESET/i) - expect(ping2.error.message).toMatch(/socket hang up/i) + expect(ping2.error.code).toMatch(/ECONNRESET/i); + expect(ping2.error.message).toMatch(/socket hang up/i); expect( findMatchingLogEvents(logs, /howto\/keep_alive_timeout/)?.[0]?.[0], @@ -86,49 +88,49 @@ describe.concurrent('Handling keep-alive header', () => { args: { server_keep_alive_timeout_ms: 10000, }, - }) + }); // console.log('!!!!!!!!!!!!!!!!!!!!') // console.log(JSON.stringify(logs, null, 2)) // console.log('!!!!!!!!!!!!!!!!!!!!') - server.close() - client.close() - }) + server.close(); + client.close(); + }); - it('should not log the suggestion', async ({ expect }) => { - let sleepServerPromiseResolve: () => void + it("should not log the suggestion", async ({ expect }) => { + let sleepServerPromiseResolve: () => void; let sleepServerPromise = new Promise((resolve) => { - sleepServerPromiseResolve = resolve + sleepServerPromiseResolve = resolve; // Simulate a ClickHouse server that responds with a delay - }) + }); - let attempted = 0 + let attempted = 0; const [server, port] = await createTCPServer(async (socket) => { - attempted++ + attempted++; if (attempted >= 2) { - socket.destroy() - expect.fail('Extra connection attempt - should not happen') + socket.destroy(); + expect.fail("Extra connection attempt - should not happen"); } // Write a valid response socket.write( - 'HTTP/1.1 200 OK\r\n' + - 'Content-Type: text/plain\r\n' + - 'Content-Length: 3\r\n' + - 'Connection: Keep-Alive\r\n' + - 'Keep-Alive: timeout=10, max=9999\r\n' + - '\r\n' + - 'Ok.', - ) + "HTTP/1.1 200 OK\r\n" + + "Content-Type: text/plain\r\n" + + "Content-Length: 3\r\n" + + "Connection: Keep-Alive\r\n" + + "Keep-Alive: timeout=10, max=9999\r\n" + + "\r\n" + + "Ok.", + ); // Then start the next request - await sleepServerPromise + await sleepServerPromise; // …and then close the connection before sending anything, // to trigger the error in the client - socket.end() - }) + socket.end(); + }); - const logs: any[] = [] - const LoggerClass = createLoggerClass(logs) + const logs: any[] = []; + const LoggerClass = createLoggerClass(logs); const client = createTestClient({ url: `http://127.0.0.1:${port}`, @@ -140,9 +142,11 @@ describe.concurrent('Handling keep-alive header', () => { LoggerClass, level: ClickHouseLogLevel.TRACE, }, - } as NodeClickHouseClientConfigOptions) + } as NodeClickHouseClientConfigOptions); - expect(await client.ping({ select: true })).toMatchObject({ success: true }) + expect(await client.ping({ select: true })).toMatchObject({ + success: true, + }); expect( findMatchingLogEvents( @@ -153,66 +157,66 @@ describe.concurrent('Handling keep-alive header', () => { args: { server_keep_alive_timeout_ms: 10000, }, - }) + }); - let ping2 = client.ping({ select: true }) + let ping2 = client.ping({ select: true }); // Client has a sleep(0) inside, the test has to wait for it to complete, // otherwise the socket gets closed before the client gets to use it. // This way we get the "socket hang up" error instead of "ECONNRESET". - await sleep(0) - sleepServerPromiseResolve!() - ping2 = await ping2 + await sleep(0); + sleepServerPromiseResolve!(); + ping2 = await ping2; - expect(ping2).toMatchObject({ success: false }) + expect(ping2).toMatchObject({ success: false }); if (ping2.success) { - throw new Error('Ping should have failed') + throw new Error("Ping should have failed"); } - expect(ping2.error.code).toMatch(/ECONNRESET/i) - expect(ping2.error.message).toMatch(/socket hang up/i) + expect(ping2.error.code).toMatch(/ECONNRESET/i); + expect(ping2.error.message).toMatch(/socket hang up/i); expect( findMatchingLogEvents(logs, /howto\/keep_alive_timeout/)?.[0]?.[0], - ).toBeUndefined() + ).toBeUndefined(); - server.close() - client.close() - }) -}) + server.close(); + client.close(); + }); +}); async function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)) + return new Promise((resolve) => setTimeout(resolve, ms)); } async function createTCPServer( cb: (socket: net.Socket) => void, port: number = 0, ): Promise<[net.Server, number]> { - const server = net.createServer(cb) + const server = net.createServer(cb); await new Promise((resolve) => { - server.listen(port, () => resolve()) - }) - return [server, (server.address() as AddressInfo).port] + server.listen(port, () => resolve()); + }); + return [server, (server.address() as AddressInfo).port]; } const createLoggerClass = (logs: any[]) => class TestLogger implements Logger { trace(...args: any) { - logs.push(args) + logs.push(args); } debug(...args: any) { - logs.push(args) + logs.push(args); } info(...args: any) { - logs.push(args) + logs.push(args); } warn(...args: any) { - logs.push(args) + logs.push(args); } error(...args: any) { - logs.push(args) + logs.push(args); } - } + }; function findMatchingLogEvents(logs: T[], regex: RegExp): T[] { - return logs.filter((args) => regex.test(JSON.stringify(args))) + return logs.filter((args) => regex.test(JSON.stringify(args))); } diff --git a/packages/client-node/__tests__/integration/node_logger_support.test.ts b/packages/client-node/__tests__/integration/node_logger_support.test.ts index 92cc5288..09e5de02 100644 --- a/packages/client-node/__tests__/integration/node_logger_support.test.ts +++ b/packages/client-node/__tests__/integration/node_logger_support.test.ts @@ -3,93 +3,93 @@ import type { ErrorLogParams, Logger, LogParams, -} from '@clickhouse/client-common' -import { describe, it, afterEach, expect, vi } from 'vitest' -import { ClickHouseLogLevel } from '@clickhouse/client-common' -import { createTestClient } from '@test/utils/client' +} from "@clickhouse/client-common"; +import { describe, it, afterEach, expect, vi } from "vitest"; +import { ClickHouseLogLevel } from "@clickhouse/client-common"; +import { createTestClient } from "@test/utils/client"; -describe('[Node.js] logger support', () => { - let client: ClickHouseClient +describe("[Node.js] logger support", () => { + let client: ClickHouseClient; let logs: { - message: string - err?: Error - args?: Record - }[] = [] + message: string; + err?: Error; + args?: Record; + }[] = []; afterEach(async () => { - await client.close() - logs = [] - }) + await client.close(); + logs = []; + }); - describe('Logger support', () => { - it('should use the default logger implementation', async () => { - const infoSpy = vi.spyOn(console, 'info') + describe("Logger support", () => { + it("should use the default logger implementation", async () => { + const infoSpy = vi.spyOn(console, "info"); client = createTestClient({ log: { level: ClickHouseLogLevel.DEBUG, }, - }) - expect(infoSpy).toHaveBeenCalledOnce() + }); + expect(infoSpy).toHaveBeenCalledOnce(); expect(infoSpy).toHaveBeenCalledWith( - expect.stringContaining('Log level is set to DEBUG'), - ) + expect.stringContaining("Log level is set to DEBUG"), + ); - const debugSpy = vi.spyOn(console, 'debug') - await client.ping() - expect(debugSpy).toHaveBeenCalledOnce() + const debugSpy = vi.spyOn(console, "debug"); + await client.ping(); + expect(debugSpy).toHaveBeenCalledOnce(); expect(debugSpy).toHaveBeenCalledWith( expect.stringMatching(/got a response from ClickHouse/), - expect.stringContaining('\nArguments:'), + expect.stringContaining("\nArguments:"), expect.objectContaining({ - request_method: 'GET', - request_path: '/ping', + request_method: "GET", + request_path: "/ping", response_status: 200, }), - ) - }) + ); + }); - it('should provide a custom logger implementation', async () => { + it("should provide a custom logger implementation", async () => { client = createTestClient({ log: { level: ClickHouseLogLevel.DEBUG, LoggerClass: TestLogger, }, - }) - await client.ping() + }); + await client.ping(); // logs[0] are about the current log level expect(logs[1]).toEqual( expect.objectContaining({ message: expect.stringMatching(/got a response from ClickHouse/), args: expect.objectContaining({ - request_path: '/ping', - request_method: 'GET', + request_path: "/ping", + request_method: "GET", }), }), - ) - }) + ); + }); - it('should provide a custom logger implementation (but logs are disabled)', async () => { + it("should provide a custom logger implementation (but logs are disabled)", async () => { client = createTestClient({ log: { // the default level is OFF LoggerClass: TestLogger, }, - }) - await client.ping() - expect(logs.length).toEqual(0) - }) - }) + }); + await client.ping(); + expect(logs.length).toEqual(0); + }); + }); - it('should not log unredacted and params on error', async () => { + it("should not log unredacted and params on error", async () => { client = createTestClient({ - url: 'http://localhost:1', // Invalid URL to trigger errors + url: "http://localhost:1", // Invalid URL to trigger errors log: { level: ClickHouseLogLevel.TRACE, LoggerClass: TestLogger, }, - }) + }); - const secret = 'D75B76DF-C61F-4A8F-8569-829D0BFC4F1D' + const secret = "D75B76DF-C61F-4A8F-8569-829D0BFC4F1D"; // Perform an operation that is expected to include a query in the request URL. await expect( @@ -97,59 +97,59 @@ describe('[Node.js] logger support', () => { query: `SELECT '${secret}'`, // Invalid query to trigger an error query_params: { secret }, }), - ).rejects.toThrow() // We expect this to fail since the query is invalid, but we want to check the logs + ).rejects.toThrow(); // We expect this to fail since the query is invalid, but we want to check the logs for (const entry of logs) { - expect(entry.message).not.toContain(secret) + expect(entry.message).not.toContain(secret); if (entry.args != null) { - const serializedArgs = JSON.stringify(entry.args) - expect(serializedArgs).not.toContain(secret) + const serializedArgs = JSON.stringify(entry.args); + expect(serializedArgs).not.toContain(secret); } } - }) + }); - it('should not log unredacted and params on success', async () => { - const secret = 'D75B76DF-C61F-4A8F-8569-829D0BFC4F1D' + it("should not log unredacted and params on success", async () => { + const secret = "D75B76DF-C61F-4A8F-8569-829D0BFC4F1D"; client = createTestClient({ http_headers: { - 'X-Test-Header': secret, + "X-Test-Header": secret, }, log: { level: ClickHouseLogLevel.TRACE, LoggerClass: TestLogger, }, - }) + }); // Perform an operation that is expected to include a query in the request URL. await client.exec({ query: `SELECT '${secret}'`, query_params: { secret }, - }) + }); for (const entry of logs) { - expect(entry.message).not.toContain(secret) + expect(entry.message).not.toContain(secret); if (entry.args != null) { - const serializedArgs = JSON.stringify(entry.args) - expect(serializedArgs).not.toContain(secret) + const serializedArgs = JSON.stringify(entry.args); + expect(serializedArgs).not.toContain(secret); } } - }) + }); class TestLogger implements Logger { trace(params: LogParams) { - logs.push(params) + logs.push(params); } debug(params: LogParams) { - logs.push(params) + logs.push(params); } info(params: LogParams) { - logs.push(params) + logs.push(params); } warn(params: LogParams) { - logs.push(params) + logs.push(params); } error(params: ErrorLogParams) { - logs.push(params) + logs.push(params); } } -}) +}); diff --git a/packages/client-node/__tests__/integration/node_max_open_connections.test.ts b/packages/client-node/__tests__/integration/node_max_open_connections.test.ts index d8ade0c7..9749f3d8 100644 --- a/packages/client-node/__tests__/integration/node_max_open_connections.test.ts +++ b/packages/client-node/__tests__/integration/node_max_open_connections.test.ts @@ -1,99 +1,99 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import { createSimpleTable } from '@test/fixtures/simple_table' -import { guid } from '@test/utils/guid' -import { sleep } from '@test/utils/sleep' -import type { ClickHouseClient } from '../../src' -import { createNodeTestClient } from '../utils/node_client' +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { createSimpleTable } from "@test/fixtures/simple_table"; +import { guid } from "@test/utils/guid"; +import { sleep } from "@test/utils/sleep"; +import type { ClickHouseClient } from "../../src"; +import { createNodeTestClient } from "../utils/node_client"; -describe('[Node.js] max_open_connections config', () => { - let client: ClickHouseClient - let results: number[] = [] +describe("[Node.js] max_open_connections config", () => { + let client: ClickHouseClient; + let results: number[] = []; beforeEach(async () => { - results = [] - }) + results = []; + }); afterEach(async () => { - await client.close() - }) + await client.close(); + }); async function select(query: string) { return client .query({ query, - format: 'JSONEachRow', + format: "JSONEachRow", }) .then((r) => r.json<{ x: number }>()) - .then(([{ x }]) => results.push(x)) + .then(([{ x }]) => results.push(x)); } - it('should use only one connection', async () => { + it("should use only one connection", async () => { client = createNodeTestClient({ max_open_connections: 1, - }) - void select('SELECT 1 AS x, sleep(0.3)') - void select('SELECT 2 AS x, sleep(0.3)') + }); + void select("SELECT 1 AS x, sleep(0.3)"); + void select("SELECT 2 AS x, sleep(0.3)"); while (results.length !== 1) { - await sleep(100) + await sleep(100); } - expect(results).toEqual([1]) + expect(results).toEqual([1]); while (results.length === 1) { - await sleep(100) + await sleep(100); } - expect(results.sort()).toEqual([1, 2]) - }) + expect(results.sort()).toEqual([1, 2]); + }); - it('should use only one connection for insert', async () => { - const tableName = `node_connections_single_connection_insert_${guid()}` + it("should use only one connection for insert", async () => { + const tableName = `node_connections_single_connection_insert_${guid()}`; client = createNodeTestClient({ max_open_connections: 1, request_timeout: 3000, - }) - await createSimpleTable(client, tableName) + }); + await createSimpleTable(client, tableName); const timeout = setTimeout(() => { - throw new Error('Timeout was triggered') - }, 3000).unref() + throw new Error("Timeout was triggered"); + }, 3000).unref(); - const value1 = { id: '42', name: 'hello', sku: [0, 1] } - const value2 = { id: '43', name: 'hello', sku: [0, 1] } + const value1 = { id: "42", name: "hello", sku: [0, 1] }; + const value2 = { id: "43", name: "hello", sku: [0, 1] }; function insert(value: object) { return client.insert({ table: tableName, values: [value], - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); } - await insert(value1) - await insert(value2) // if previous call holds the socket, the test will time out - clearTimeout(timeout) + await insert(value1); + await insert(value2); // if previous call holds the socket, the test will time out + clearTimeout(timeout); const result = await client.query({ query: `SELECT * FROM ${tableName}`, - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); - const json = await result.json() - expect(json).toContainEqual(value1) - expect(json).toContainEqual(value2) - expect(json.length).toEqual(2) - }) + const json = await result.json(); + expect(json).toContainEqual(value1); + expect(json).toContainEqual(value2); + expect(json.length).toEqual(2); + }); - it('should use several connections', async () => { + it("should use several connections", async () => { client = createNodeTestClient({ max_open_connections: 2, - }) - void select('SELECT 1 AS x, sleep(0.3)') - void select('SELECT 2 AS x, sleep(0.3)') - void select('SELECT 3 AS x, sleep(0.3)') - void select('SELECT 4 AS x, sleep(0.3)') + }); + void select("SELECT 1 AS x, sleep(0.3)"); + void select("SELECT 2 AS x, sleep(0.3)"); + void select("SELECT 3 AS x, sleep(0.3)"); + void select("SELECT 4 AS x, sleep(0.3)"); while (results.length < 2) { - await sleep(100) + await sleep(100); } - expect(results.sort()).toEqual([1, 2]) + expect(results.sort()).toEqual([1, 2]); while (results.length < 4) { - await sleep(100) + await sleep(100); } - expect(results.sort()).toEqual([1, 2, 3, 4]) - }) -}) + expect(results.sort()).toEqual([1, 2, 3, 4]); + }); +}); diff --git a/packages/client-node/__tests__/integration/node_multiple_clients.test.ts b/packages/client-node/__tests__/integration/node_multiple_clients.test.ts index a9c4c6cf..e797fd91 100644 --- a/packages/client-node/__tests__/integration/node_multiple_clients.test.ts +++ b/packages/client-node/__tests__/integration/node_multiple_clients.test.ts @@ -1,62 +1,62 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import type { ClickHouseClient } from '@clickhouse/client-common' -import { createSimpleTable } from '@test/fixtures/simple_table' -import { createTestClient } from '@test/utils/client' -import { guid } from '@test/utils/guid' -import Stream from 'stream' +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import type { ClickHouseClient } from "@clickhouse/client-common"; +import { createSimpleTable } from "@test/fixtures/simple_table"; +import { createTestClient } from "@test/utils/client"; +import { guid } from "@test/utils/guid"; +import Stream from "stream"; -const CLIENTS_COUNT = 5 +const CLIENTS_COUNT = 5; -describe('[Node.js] multiple clients', () => { - const clients: ClickHouseClient[] = Array(CLIENTS_COUNT) +describe("[Node.js] multiple clients", () => { + const clients: ClickHouseClient[] = Array(CLIENTS_COUNT); beforeEach(() => { for (let i = 0; i < CLIENTS_COUNT; i++) { - clients[i] = createTestClient() + clients[i] = createTestClient(); } - }) + }); afterEach(async () => { for (const c of clients) { - await c.close() + await c.close(); } - }) + }); - const names = ['foo', 'bar', 'baz', 'qaz', 'qux'] + const names = ["foo", "bar", "baz", "qaz", "qux"]; function getValue(i: number) { return { id: i, name: names[i], sku: [i, i + 1], - } + }; } const expected = [ - { id: '0', name: 'foo', sku: [0, 1] }, - { id: '1', name: 'bar', sku: [1, 2] }, - { id: '2', name: 'baz', sku: [2, 3] }, - { id: '3', name: 'qaz', sku: [3, 4] }, - { id: '4', name: 'qux', sku: [4, 5] }, - ] - - it('should be able to send parallel inserts (streams)', async () => { - const id = guid() - const tableName = `multiple_clients_insert_streams_test__${id}` - await createSimpleTable(clients[0], tableName) + { id: "0", name: "foo", sku: [0, 1] }, + { id: "1", name: "bar", sku: [1, 2] }, + { id: "2", name: "baz", sku: [2, 3] }, + { id: "3", name: "qaz", sku: [3, 4] }, + { id: "4", name: "qux", sku: [4, 5] }, + ]; + + it("should be able to send parallel inserts (streams)", async () => { + const id = guid(); + const tableName = `multiple_clients_insert_streams_test__${id}`; + await createSimpleTable(clients[0], tableName); await Promise.all( clients.map((client, i) => client.insert({ table: tableName, values: Stream.Readable.from([getValue(i)]), - format: 'JSONEachRow', + format: "JSONEachRow", }), ), - ) + ); const result = await clients[0].query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, - format: 'JSONEachRow', - }) - expect(await result.json()).toEqual(expected) - }) -}) + format: "JSONEachRow", + }); + expect(await result.json()).toEqual(expected); + }); +}); diff --git a/packages/client-node/__tests__/integration/node_ping.test.ts b/packages/client-node/__tests__/integration/node_ping.test.ts index b8488960..080bf49e 100644 --- a/packages/client-node/__tests__/integration/node_ping.test.ts +++ b/packages/client-node/__tests__/integration/node_ping.test.ts @@ -1,54 +1,54 @@ -import { describe, it, expect, afterEach } from 'vitest' +import { describe, it, expect, afterEach } from "vitest"; import type { ClickHouseClient, ClickHouseError, -} from '@clickhouse/client-common' -import { createTestClient } from '@test/utils/client' +} from "@clickhouse/client-common"; +import { createTestClient } from "@test/utils/client"; -describe('[Node.js] ping', () => { - let client: ClickHouseClient +describe("[Node.js] ping", () => { + let client: ClickHouseClient; afterEach(async () => { - await client.close() - }) + await client.close(); + }); - it('does not swallow a client error', async () => { + it("does not swallow a client error", async () => { client = createTestClient({ - url: 'http://localhost:3333', - }) + url: "http://localhost:3333", + }); - const result = await client.ping() - expect(result.success).toBeFalsy() + const result = await client.ping(); + expect(result.success).toBeFalsy(); // @ts-expect-error expect(result.error).toEqual( expect.objectContaining({ - code: 'ECONNREFUSED', + code: "ECONNREFUSED", }), - ) - }) + ); + }); - it('ignores credentials by default', async () => { + it("ignores credentials by default", async () => { client = createTestClient({ - username: 'wrong', - }) - const response = await client.ping() - expect(response.success).toBe(true) - }) + username: "wrong", + }); + const response = await client.ping(); + expect(response.success).toBe(true); + }); - it('checks credentials when select query mode is enabled', async () => { + it("checks credentials when select query mode is enabled", async () => { client = createTestClient({ - username: 'wrong', - }) + username: "wrong", + }); const response = await client.ping({ select: true, - }) - expect(response.success).toBe(false) + }); + expect(response.success).toBe(false); - const err = (response as unknown as { error: ClickHouseError }).error - expect(err.code).toEqual('516') - expect(err.type).toEqual('AUTHENTICATION_FAILED') + const err = (response as unknown as { error: ClickHouseError }).error; + expect(err.code).toEqual("516"); + expect(err.type).toEqual("AUTHENTICATION_FAILED"); expect(err.message).toEqual( - expect.stringContaining('Authentication failed'), - ) - }) -}) + expect.stringContaining("Authentication failed"), + ); + }); +}); diff --git a/packages/client-node/__tests__/integration/node_query_format_types.test.ts b/packages/client-node/__tests__/integration/node_query_format_types.test.ts index 51491739..8832b800 100644 --- a/packages/client-node/__tests__/integration/node_query_format_types.test.ts +++ b/packages/client-node/__tests__/integration/node_query_format_types.test.ts @@ -1,152 +1,152 @@ -import { afterAll, beforeAll, describe, it } from 'vitest' +import { afterAll, beforeAll, describe, it } from "vitest"; import type { ClickHouseClient as BaseClickHouseClient, DataFormat, -} from '@clickhouse/client-common' -import { createTableWithFields } from '@test/fixtures/table_with_fields' -import { guid } from '@test/utils/guid' -import type { ClickHouseClient, ResultSet } from '../../src' -import { createNodeTestClient } from '../utils/node_client' +} from "@clickhouse/client-common"; +import { createTableWithFields } from "@test/fixtures/table_with_fields"; +import { guid } from "@test/utils/guid"; +import type { ClickHouseClient, ResultSet } from "../../src"; +import { createNodeTestClient } from "../utils/node_client"; /* eslint-disable @typescript-eslint/no-unused-expressions */ // Ignored and used only as a source for ESLint checks with $ExpectType // See also: https://www.npmjs.com/package/eslint-plugin-expect-type -describe.skip('[Node.js] Query and ResultSet types', () => { - let client: ClickHouseClient - const tableName = `node_query_format_types_test_${guid()}` - const query = `SELECT * FROM ${tableName} ORDER BY id ASC` +describe.skip("[Node.js] Query and ResultSet types", () => { + let client: ClickHouseClient; + const tableName = `node_query_format_types_test_${guid()}`; + const query = `SELECT * FROM ${tableName} ORDER BY id ASC`; beforeAll(async () => { - client = createNodeTestClient() + client = createNodeTestClient(); await createTableWithFields( client as BaseClickHouseClient, - 'name String, sku Array(UInt32)', + "name String, sku Array(UInt32)", {}, tableName, - ) + ); await client.insert({ table: tableName, values: [ - { id: 42, name: 'foo', sku: [1, 2, 3] }, - { id: 43, name: 'bar', sku: [4, 5, 6] }, + { id: 42, name: "foo", sku: [1, 2, 3] }, + { id: 43, name: "bar", sku: [4, 5, 6] }, ], - format: 'JSONEachRow', - }) - }) + format: "JSONEachRow", + }); + }); afterAll(async () => { - await client.close() - }) + await client.close(); + }); - describe('Streamable JSON formats', () => { - it('should infer types for JSONEachRow', async () => { + describe("Streamable JSON formats", () => { + it("should infer types for JSONEachRow", async () => { // $ExpectType ResultSet<"JSONEachRow"> const rs = await client.query({ query, - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); // $ExpectType unknown[] - await rs.json() + await rs.json(); // $ExpectType Data[] - await rs.json() + await rs.json(); // $ExpectType string - await rs.text() + await rs.text(); // $ExpectType StreamReadable[]> - const stream = rs.stream() + const stream = rs.stream(); // stream + on('data') await new Promise((resolve, reject) => { stream .on( - 'data', + "data", // $ExpectType (rows: Row[]) => void (rows) => { rows.forEach( // $ExpectType (row: Row) => void (row) => { // $ExpectType unknown - row.json() + row.json(); // $ExpectType Data - row.json() + row.json(); // $ExpectType string - row.text + row.text; }, - ) + ); }, ) - .on('end', resolve) - .on('error', reject) - }) + .on("end", resolve) + .on("error", reject); + }); // stream + async iterator for await (const _rows of stream) { // $ExpectType Row[] - const rows = _rows - rows.length // avoid unused variable warning (rows reassigned for type assertion) + const rows = _rows; + rows.length; // avoid unused variable warning (rows reassigned for type assertion) rows.forEach( // $ExpectType (row: Row) => void (row) => { // $ExpectType unknown - row.json() + row.json(); // $ExpectType Data - row.json() + row.json(); // $ExpectType string - row.text + row.text; }, - ) + ); } // stream + T hint + on('data') - const streamTyped = rs.stream() + const streamTyped = rs.stream(); await new Promise((resolve, reject) => { streamTyped .on( - 'data', + "data", // $ExpectType (rows: Row[]) => void (rows) => { rows.forEach( // $ExpectType (row: Row) => void (row) => { // $ExpectType Data - row.json() + row.json(); // $ExpectType Data - row.json() + row.json(); // $ExpectType string - row.text + row.text; }, - ) + ); }, ) - .on('end', resolve) - .on('error', reject) - }) + .on("end", resolve) + .on("error", reject); + }); // stream + T hint + async iterator for await (const _rows of streamTyped) { // $ExpectType Row[] - const rows = _rows - rows.length // avoid unused variable warning (rows reassigned for type assertion) + const rows = _rows; + rows.length; // avoid unused variable warning (rows reassigned for type assertion) rows.forEach( // $ExpectType (row: Row) => void (row) => { // $ExpectType Data - row.json() + row.json(); // $ExpectType Data - row.json() + row.json(); // $ExpectType string - row.text + row.text; }, - ) + ); } - }) + }); - it('should infer ResultSet features when similar JSON formats are used in a function call', async () => { + it("should infer ResultSet features when similar JSON formats are used in a function call", async () => { // $ExpectType (format: "JSONEachRow" | "JSONCompactEachRow") => Promise> - function runQuery(format: 'JSONEachRow' | 'JSONCompactEachRow') { + function runQuery(format: "JSONEachRow" | "JSONCompactEachRow") { return client.query({ query, format, - }) + }); } // ResultSet cannot infer the type from the literal, so it falls back to both possible formats. @@ -155,113 +155,113 @@ describe.skip('[Node.js] Query and ResultSet types', () => { //// JSONCompactEachRow // $ExpectType ResultSet<"JSONEachRow" | "JSONCompactEachRow"> - const rs = await runQuery('JSONCompactEachRow') + const rs = await runQuery("JSONCompactEachRow"); // $ExpectType unknown[] - await rs.json() + await rs.json(); // $ExpectType Data[] - await rs.json() + await rs.json(); // $ExpectType string - await rs.text() + await rs.text(); // $ExpectType StreamReadable[]> - const stream = rs.stream() + const stream = rs.stream(); // stream + on('data') await new Promise((resolve, reject) => { stream .on( - 'data', + "data", // $ExpectType (rows: Row[]) => void (rows) => { rows.forEach( // $ExpectType (row: Row) => void (row) => { // $ExpectType unknown - row.json() + row.json(); // $ExpectType Data - row.json() + row.json(); // $ExpectType string - row.text + row.text; }, - ) + ); }, ) - .on('end', resolve) - .on('error', reject) - }) + .on("end", resolve) + .on("error", reject); + }); // stream + async iterator for await (const _rows of stream) { // $ExpectType Row[] - const rows = _rows - rows.length // avoid unused variable warning (rows reassigned for type assertion) + const rows = _rows; + rows.length; // avoid unused variable warning (rows reassigned for type assertion) rows.forEach( // $ExpectType (row: Row) => void (row) => { // $ExpectType unknown - row.json() + row.json(); // $ExpectType Data - row.json() + row.json(); // $ExpectType string - row.text + row.text; }, - ) + ); } //// JSONEachRow // $ExpectType ResultSet<"JSONEachRow" | "JSONCompactEachRow"> - const rs2 = await runQuery('JSONEachRow') + const rs2 = await runQuery("JSONEachRow"); // $ExpectType unknown[] - await rs2.json() + await rs2.json(); // $ExpectType Data[] - await rs2.json() + await rs2.json(); // $ExpectType string - await rs2.text() + await rs2.text(); // $ExpectType StreamReadable[]> - const stream2 = rs2.stream() + const stream2 = rs2.stream(); // stream + on('data') await new Promise((resolve, reject) => { stream2 .on( - 'data', + "data", // $ExpectType (rows: Row[]) => void (rows) => { rows.forEach( // $ExpectType (row: Row) => void (row) => { // $ExpectType unknown - row.json() + row.json(); // $ExpectType Data - row.json() + row.json(); // $ExpectType string - row.text + row.text; }, - ) + ); }, ) - .on('end', resolve) - .on('error', reject) - }) + .on("end", resolve) + .on("error", reject); + }); // stream + async iterator for await (const _rows of stream2) { // $ExpectType Row[] - const rows = _rows - rows.length // avoid unused variable warning (rows reassigned for type assertion) + const rows = _rows; + rows.length; // avoid unused variable warning (rows reassigned for type assertion) rows.forEach( // $ExpectType (row: Row) => void (row) => { // $ExpectType unknown - row.json() + row.json(); // $ExpectType Data - row.json() + row.json(); // $ExpectType string - row.text + row.text; }, - ) + ); } - }) + }); /** * Not covered, but should behave similarly: @@ -272,55 +272,55 @@ describe.skip('[Node.js] Query and ResultSet types', () => { * 'JSONCompactStringsEachRowWithNames', * 'JSONCompactStringsEachRowWithNamesAndTypes' */ - }) + }); - describe('Single document JSON formats', () => { - it('should infer types when the format is omitted (JSON)', async () => { + describe("Single document JSON formats", () => { + it("should infer types when the format is omitted (JSON)", async () => { // $ExpectType ResultSet<"JSON"> const rs = await client.query({ query, - }) + }); // $ExpectType ResponseJSON - await rs.json() + await rs.json(); // $ExpectType ResponseJSON - await rs.json() + await rs.json(); // $ExpectType string - await rs.text() + await rs.text(); // $ExpectType never - rs.stream() - }) + rs.stream(); + }); - it('should infer types for JSON', async () => { + it("should infer types for JSON", async () => { // $ExpectType ResultSet<"JSON"> const rs = await client.query({ query, - format: 'JSON', - }) + format: "JSON", + }); // $ExpectType ResponseJSON - await rs.json() + await rs.json(); // $ExpectType ResponseJSON - await rs.json() + await rs.json(); // $ExpectType string - await rs.text() + await rs.text(); // $ExpectType never - rs.stream() - }) + rs.stream(); + }); - it('should infer types for JSONObjectEachRow', async () => { + it("should infer types for JSONObjectEachRow", async () => { // $ExpectType ResultSet<"JSONObjectEachRow"> const rs = await client.query({ query, - format: 'JSONObjectEachRow', - }) + format: "JSONObjectEachRow", + }); // $ExpectType Record - await rs.json() + await rs.json(); // $ExpectType Record - await rs.json() + await rs.json(); // $ExpectType string - await rs.text() + await rs.text(); // $ExpectType never - rs.stream() - }) + rs.stream(); + }); /** * Not covered, but should behave similarly: @@ -329,74 +329,74 @@ describe.skip('[Node.js] Query and ResultSet types', () => { * 'JSONCompactStrings', * 'JSONColumnsWithMetadata', */ - }) + }); - describe('Raw formats', () => { - it('should infer types for CSV', async () => { + describe("Raw formats", () => { + it("should infer types for CSV", async () => { // $ExpectType ResultSet<"CSV"> const rs = await client.query({ query, - format: 'CSV', - }) + format: "CSV", + }); // $ExpectType never - await rs.json() + await rs.json(); // $ExpectType never - await rs.json() + await rs.json(); // $ExpectType string - await rs.text() + await rs.text(); // $ExpectType StreamReadable[]> - const stream = rs.stream() + const stream = rs.stream(); // stream + on('data') await new Promise((resolve, reject) => { stream .on( - 'data', + "data", // $ExpectType (rows: Row[]) => void (rows) => { rows.forEach( // $ExpectType (row: Row) => void (row) => { // $ExpectType never - row.json() + row.json(); // $ExpectType never - row.json() + row.json(); // $ExpectType string - row.text + row.text; }, - ) + ); }, ) - .on('end', resolve) - .on('error', reject) - }) + .on("end", resolve) + .on("error", reject); + }); // stream + async iterator for await (const _rows of stream) { // $ExpectType Row[] - const rows = _rows - rows.length // avoid unused variable warning (rows reassigned for type assertion) + const rows = _rows; + rows.length; // avoid unused variable warning (rows reassigned for type assertion) rows.forEach( // $ExpectType (row: Row) => void (row) => { // $ExpectType never - row.json() + row.json(); // $ExpectType never - row.json() + row.json(); // $ExpectType string - row.text + row.text; }, - ) + ); } - }) + }); - it('should infer ResultSet features when similar raw formats are used in a function call', async () => { + it("should infer ResultSet features when similar raw formats are used in a function call", async () => { // $ExpectType (format: "CSV" | "TabSeparated") => Promise> - function runQuery(format: 'CSV' | 'TabSeparated') { + function runQuery(format: "CSV" | "TabSeparated") { return client.query({ query, format, - }) + }); } // ResultSet cannot infer the type from the literal, so it falls back to both possible formats. @@ -405,113 +405,113 @@ describe.skip('[Node.js] Query and ResultSet types', () => { //// CSV // $ExpectType ResultSet<"CSV" | "TabSeparated"> - const rs = await runQuery('CSV') + const rs = await runQuery("CSV"); // $ExpectType never - await rs.json() + await rs.json(); // $ExpectType never - await rs.json() + await rs.json(); // $ExpectType string - await rs.text() + await rs.text(); // $ExpectType StreamReadable[]> - const stream = rs.stream() + const stream = rs.stream(); // stream + on('data') await new Promise((resolve, reject) => { stream .on( - 'data', + "data", // $ExpectType (rows: Row[]) => void (rows) => { rows.forEach( // $ExpectType (row: Row) => void (row) => { // $ExpectType never - row.json() + row.json(); // $ExpectType never - row.json() + row.json(); // $ExpectType string - row.text + row.text; }, - ) + ); }, ) - .on('end', resolve) - .on('error', reject) - }) + .on("end", resolve) + .on("error", reject); + }); // stream + async iterator for await (const _rows of stream) { // $ExpectType Row[] - const rows = _rows - rows.length // avoid unused variable warning (rows reassigned for type assertion) + const rows = _rows; + rows.length; // avoid unused variable warning (rows reassigned for type assertion) rows.forEach( // $ExpectType (row: Row) => void (row) => { // $ExpectType never - row.json() + row.json(); // $ExpectType never - row.json() + row.json(); // $ExpectType string - row.text + row.text; }, - ) + ); } //// TabSeparated // $ExpectType ResultSet<"CSV" | "TabSeparated"> - const rs2 = await runQuery('TabSeparated') + const rs2 = await runQuery("TabSeparated"); // $ExpectType never - await rs2.json() + await rs2.json(); // $ExpectType never - await rs2.json() + await rs2.json(); // $ExpectType string - await rs2.text() + await rs2.text(); // $ExpectType StreamReadable[]> - const stream2 = rs2.stream() + const stream2 = rs2.stream(); // stream + on('data') await new Promise((resolve, reject) => { stream2 .on( - 'data', + "data", // $ExpectType (rows: Row[]) => void (rows) => { rows.forEach( // $ExpectType (row: Row) => void (row) => { // $ExpectType never - row.json() + row.json(); // $ExpectType never - row.json() + row.json(); // $ExpectType string - row.text + row.text; }, - ) + ); }, ) - .on('end', resolve) - .on('error', reject) - }) + .on("end", resolve) + .on("error", reject); + }); // stream + async iterator for await (const _rows of stream2) { // $ExpectType Row[] - const rows = _rows - rows.length // avoid unused variable warning (rows reassigned for type assertion) + const rows = _rows; + rows.length; // avoid unused variable warning (rows reassigned for type assertion) rows.forEach( // $ExpectType (row: Row) => void (row) => { // $ExpectType never - row.json() + row.json(); // $ExpectType never - row.json() + row.json(); // $ExpectType string - row.text + row.text; }, - ) + ); } - }) + }); /** * Not covered, but should behave similarly: @@ -525,114 +525,114 @@ describe.skip('[Node.js] Query and ResultSet types', () => { * 'CustomSeparatedWithNamesAndTypes', * 'Parquet', */ - }) + }); - describe('Type inference with ambiguous format variants', () => { + describe("Type inference with ambiguous format variants", () => { // expect-type itself fails a bit here sometimes. It can get a wrong order of the variants = flaky ESLint run. - type JSONFormat = 'JSON' | 'JSONEachRow' - type ResultSetJSONFormat = ResultSet + type JSONFormat = "JSON" | "JSONEachRow"; + type ResultSetJSONFormat = ResultSet; // TODO: Maybe there is a way to infer the format without an extra type parameter? - it('should infer types for JSON or JSONEachRow (no extra type params)', async () => { + it("should infer types for JSON or JSONEachRow (no extra type params)", async () => { function runQuery(format: JSONFormat): Promise { return client.query({ query, format, - }) + }); } // ResultSet falls back to both possible formats (both JSON and JSONEachRow); 'JSON' string provided to `runQuery` // cannot be used to narrow down the literal type, since the function argument is just DataFormat. // $ExpectType ResultSetJSONFormat - const rs = await runQuery('JSON') + const rs = await runQuery("JSON"); // $ExpectType unknown[] | ResponseJSON - await rs.json() + await rs.json(); // $ExpectType Data[] | ResponseJSON - await rs.json() + await rs.json(); // $ExpectType string - await rs.text() + await rs.text(); // $ExpectType StreamReadable[]> - rs.stream() - }) + rs.stream(); + }); - it('should infer types for JSON or JSONEachRow (with extra type parameter)', async () => { + it("should infer types for JSON or JSONEachRow (with extra type parameter)", async () => { // $ExpectType (format: F) => Promise> function runQuery(format: F) { return client.query({ query, format, - }) + }); } // $ExpectType ResultSet<"JSON"> - const rs = await runQuery('JSON') + const rs = await runQuery("JSON"); // $ExpectType ResponseJSON - await rs.json() + await rs.json(); // $ExpectType ResponseJSON - await rs.json() + await rs.json(); // $ExpectType string - await rs.text() + await rs.text(); // $ExpectType never - rs.stream() + rs.stream(); // $ExpectType ResultSet<"JSONEachRow"> - const rs2 = await runQuery('JSONEachRow') + const rs2 = await runQuery("JSONEachRow"); // $ExpectType unknown[] - await rs2.json() + await rs2.json(); // $ExpectType Data[] - await rs2.json() + await rs2.json(); // $ExpectType string - await rs2.text() + await rs2.text(); // $ExpectType StreamReadable[]> - rs2.stream() - }) + rs2.stream(); + }); - it('should fail to infer the types when the format is any', async () => { + it("should fail to infer the types when the format is any", async () => { // In a separate function, which breaks the format inference from the literal (due to "generic" DataFormat usage) // $ExpectType (format: DataFormat) => Promise> function runQuery(format: DataFormat) { return client.query({ query, format, - }) + }); } // ResultSet falls back to all possible formats; 'JSON' string provided as an argument to `runQuery` // cannot be used to narrow down the literal type, since the function argument is just DataFormat. // $ExpectType ResultSet - const rs = await runQuery('JSON') + const rs = await runQuery("JSON"); // All possible JSON variants are now allowed // FIXME: this line produces a ESLint error due to a different order (which is insignificant). -$ExpectType unknown[] | Record | ResponseJSON - await rs.json() // IDE error here, different type order + await rs.json(); // IDE error here, different type order // $ExpectType Data[] | ResponseJSON | Record - await rs.json() + await rs.json(); // $ExpectType string - await rs.text() + await rs.text(); // Stream is still allowed (can't be inferred, so it is not "never") // $ExpectType StreamReadable[]> - const stream = rs.stream() + const stream = rs.stream(); for await (const _rows of stream) { // $ExpectType Row[] - const rows = _rows - rows.length // avoid unused variable warning (rows reassigned for type assertion) + const rows = _rows; + rows.length; // avoid unused variable warning (rows reassigned for type assertion) rows.forEach( // $ExpectType (row: Row) => void (row) => { // $ExpectType unknown - row.json() + row.json(); // $ExpectType Data - row.json() + row.json(); // $ExpectType string - row.text + row.text; }, - ) + ); } - }) - }) -}) + }); + }); +}); interface Data { - id: number - name: string - sku: number[] + id: number; + name: string; + sku: number[]; } diff --git a/packages/client-node/__tests__/integration/node_response_headers_cap.test.ts b/packages/client-node/__tests__/integration/node_response_headers_cap.test.ts index 2fb9eafb..e521d0eb 100644 --- a/packages/client-node/__tests__/integration/node_response_headers_cap.test.ts +++ b/packages/client-node/__tests__/integration/node_response_headers_cap.test.ts @@ -1,6 +1,6 @@ -import http from 'http' -import net, { type AddressInfo } from 'net' -import { describe, it } from 'vitest' +import http from "http"; +import net, { type AddressInfo } from "net"; +import { describe, it } from "vitest"; // Verifies the behavior of Node.js' built-in http client when parsing responses // with a large block of response headers, depending on the `maxHeaderSize` @@ -10,22 +10,22 @@ import { describe, it } from 'vitest' // // This is a pure Node.js behavior check; the ClickHouse client is intentionally // not involved here. -describe('[Node.js] http client maxHeaderSize behavior', () => { +describe("[Node.js] http client maxHeaderSize behavior", () => { // Build enough X-H-NNNN headers to roughly reach `targetBytes`. function makeHeaders( targetBytes: number, ): Array<{ name: string; value: string }> { - const headers: Array<{ name: string; value: string }> = [] - let total = 0 - let i = 0 + const headers: Array<{ name: string; value: string }> = []; + let total = 0; + let i = 0; while (total < targetBytes) { - const name = `X-H-${String(i).padStart(4, '0')}` - const value = 'a'.repeat(90) - headers.push({ name, value }) - total += name.length + 2 /* ": " */ + value.length + 2 /* CRLF */ - i++ + const name = `X-H-${String(i).padStart(4, "0")}`; + const value = "a".repeat(90); + headers.push({ name, value }); + total += name.length + 2 /* ": " */ + value.length + 2; /* CRLF */ + i++; } - return headers + return headers; } // Raw TCP server that replies with a fixed HTTP/1.1 response containing @@ -34,30 +34,30 @@ describe('[Node.js] http client maxHeaderSize behavior', () => { headers: Array<{ name: string; value: string }>, ): Promise<[net.Server, number]> { const server = net.createServer((socket) => { - socket.once('data', () => { - const body = 'OK' + socket.once("data", () => { + const body = "OK"; const headerBlob = headers .map((h) => `${h.name}: ${h.value}\r\n`) - .join('') + .join(""); const response = - 'HTTP/1.1 200 OK\r\n' + + "HTTP/1.1 200 OK\r\n" + `Content-Length: ${body.length}\r\n` + headerBlob + - '\r\n' + - body - socket.write(response) - socket.end() - }) - }) + "\r\n" + + body; + socket.write(response); + socket.end(); + }); + }); await new Promise((resolve) => { - server.listen(0, '127.0.0.1', () => resolve()) - }) - return [server, (server.address() as AddressInfo).port] + server.listen(0, "127.0.0.1", () => resolve()); + }); + return [server, (server.address() as AddressInfo).port]; } type ClientResult = | { ok: true; headerCount: number; firstValue: string; lastValue: string } - | { ok: false; code?: string; message: string } + | { ok: false; code?: string; message: string }; function tryClient( port: number, @@ -67,135 +67,137 @@ describe('[Node.js] http client maxHeaderSize behavior', () => { ): Promise { return new Promise((resolve) => { const opts: http.RequestOptions = { - hostname: '127.0.0.1', + hostname: "127.0.0.1", port, - path: '/', - } + path: "/", + }; if (maxHeaderSize !== undefined) { - opts.maxHeaderSize = maxHeaderSize + opts.maxHeaderSize = maxHeaderSize; } const req = http.request(opts, (res) => { - const first = res.headers[firstName.toLowerCase()] as string | undefined - const last = res.headers[lastName.toLowerCase()] as string | undefined - res.on('data', () => {}) - res.on('end', () => + const first = res.headers[firstName.toLowerCase()] as + | string + | undefined; + const last = res.headers[lastName.toLowerCase()] as string | undefined; + res.on("data", () => {}); + res.on("end", () => resolve({ ok: true, headerCount: Object.keys(res.headers).length, - firstValue: first ?? '', - lastValue: last ?? '', + firstValue: first ?? "", + lastValue: last ?? "", }), - ) - }) - req.on('error', (e: NodeJS.ErrnoException) => + ); + }); + req.on("error", (e: NodeJS.ErrnoException) => resolve({ ok: false, code: e.code, message: e.message }), - ) - req.end() - }) + ); + req.end(); + }); } async function runScenario(params: { - payloadKB: number - maxHeaderSize?: number + payloadKB: number; + maxHeaderSize?: number; }): Promise<{ - result: ClientResult - headerCount: number - firstName: string - lastName: string + result: ClientResult; + headerCount: number; + firstName: string; + lastName: string; }> { - const headers = makeHeaders(params.payloadKB * 1024) - const firstName = headers[0].name - const lastName = headers[headers.length - 1].name - const [server, port] = await startServer(headers) + const headers = makeHeaders(params.payloadKB * 1024); + const firstName = headers[0].name; + const lastName = headers[headers.length - 1].name; + const [server, port] = await startServer(headers); try { const result = await tryClient( port, firstName, lastName, params.maxHeaderSize, - ) - return { result, headerCount: headers.length, firstName, lastName } + ); + return { result, headerCount: headers.length, firstName, lastName }; } finally { await new Promise((resolve, reject) => { server.close((err) => { - if (err) reject(err) - else resolve() - }) - }) + if (err) reject(err); + else resolve(); + }); + }); } } // ── 16K bucket ──────────────────────────────────────────────── - it('A. ~15K payload at default 16K limit fits', async ({ expect }) => { + it("A. ~15K payload at default 16K limit fits", async ({ expect }) => { const { result, headerCount, firstName, lastName } = await runScenario({ payloadKB: 15, - }) - expect(result.ok).toBe(true) - if (!result.ok) return + }); + expect(result.ok).toBe(true); + if (!result.ok) return; // 1 server-set Content-Length + all generated X-H-NNNN headers - expect(result.headerCount).toBe(headerCount + 1) - expect(result.firstValue.length).toBe(90) - expect(result.lastValue.length).toBe(90) - expect(firstName).toBe('X-H-0000') - expect(lastName).toMatch(/^X-H-\d{4}$/) - }) + expect(result.headerCount).toBe(headerCount + 1); + expect(result.firstValue.length).toBe(90); + expect(result.lastValue.length).toBe(90); + expect(firstName).toBe("X-H-0000"); + expect(lastName).toMatch(/^X-H-\d{4}$/); + }); - it('B. ~20K payload at default 16K limit overflows (HPE_HEADER_OVERFLOW)', async ({ + it("B. ~20K payload at default 16K limit overflows (HPE_HEADER_OVERFLOW)", async ({ expect, }) => { - const { result } = await runScenario({ payloadKB: 20 }) - expect(result.ok).toBe(false) - if (result.ok) return - expect(result.code).toBe('HPE_HEADER_OVERFLOW') - }) + const { result } = await runScenario({ payloadKB: 20 }); + expect(result.ok).toBe(false); + if (result.ok) return; + expect(result.code).toBe("HPE_HEADER_OVERFLOW"); + }); // ── 32K bucket ──────────────────────────────────────────────── - it('C. ~20K payload raised to 32K limit fits', async ({ expect }) => { + it("C. ~20K payload raised to 32K limit fits", async ({ expect }) => { const { result, headerCount } = await runScenario({ payloadKB: 20, maxHeaderSize: 32 * 1024, - }) - expect(result.ok).toBe(true) - if (!result.ok) return - expect(result.headerCount).toBe(headerCount + 1) - expect(result.firstValue.length).toBe(90) - expect(result.lastValue.length).toBe(90) - }) + }); + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.headerCount).toBe(headerCount + 1); + expect(result.firstValue.length).toBe(90); + expect(result.lastValue.length).toBe(90); + }); - it('D. ~40K payload at 32K limit overflows (HPE_HEADER_OVERFLOW)', async ({ + it("D. ~40K payload at 32K limit overflows (HPE_HEADER_OVERFLOW)", async ({ expect, }) => { const { result } = await runScenario({ payloadKB: 40, maxHeaderSize: 32 * 1024, - }) - expect(result.ok).toBe(false) - if (result.ok) return - expect(result.code).toBe('HPE_HEADER_OVERFLOW') - }) + }); + expect(result.ok).toBe(false); + if (result.ok) return; + expect(result.code).toBe("HPE_HEADER_OVERFLOW"); + }); // ── 64K bucket ──────────────────────────────────────────────── - it('E. ~40K payload raised to 64K limit fits', async ({ expect }) => { + it("E. ~40K payload raised to 64K limit fits", async ({ expect }) => { const { result, headerCount } = await runScenario({ payloadKB: 40, maxHeaderSize: 64 * 1024, - }) - expect(result.ok).toBe(true) - if (!result.ok) return - expect(result.headerCount).toBe(headerCount + 1) - expect(result.firstValue.length).toBe(90) - expect(result.lastValue.length).toBe(90) - }) + }); + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.headerCount).toBe(headerCount + 1); + expect(result.firstValue.length).toBe(90); + expect(result.lastValue.length).toBe(90); + }); - it('F. ~60K payload at 64K limit fits', async ({ expect }) => { + it("F. ~60K payload at 64K limit fits", async ({ expect }) => { const { result, headerCount } = await runScenario({ payloadKB: 60, maxHeaderSize: 64 * 1024, - }) - expect(result.ok).toBe(true) - if (!result.ok) return - expect(result.headerCount).toBe(headerCount + 1) - expect(result.firstValue.length).toBe(90) - expect(result.lastValue.length).toBe(90) - }) -}) + }); + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.headerCount).toBe(headerCount + 1); + expect(result.firstValue.length).toBe(90); + expect(result.lastValue.length).toBe(90); + }); +}); diff --git a/packages/client-node/__tests__/integration/node_response_headers_cap_client.test.ts b/packages/client-node/__tests__/integration/node_response_headers_cap_client.test.ts index 8dcfe964..14c3996f 100644 --- a/packages/client-node/__tests__/integration/node_response_headers_cap_client.test.ts +++ b/packages/client-node/__tests__/integration/node_response_headers_cap_client.test.ts @@ -1,7 +1,7 @@ -import net, { type AddressInfo } from 'net' -import { afterEach, describe, it } from 'vitest' -import { createClient } from '../../src' -import type { ClickHouseClient } from '@clickhouse/client-common' +import net, { type AddressInfo } from "net"; +import { afterEach, describe, it } from "vitest"; +import { createClient } from "../../src"; +import type { ClickHouseClient } from "@clickhouse/client-common"; // Verifies that the Node.js client honors the `max_response_headers_size` // configuration option, which is forwarded to `http(s).request` as the @@ -12,31 +12,31 @@ import type { ClickHouseClient } from '@clickhouse/client-common' // `createClient` + `client.ping()`. A raw TCP server is still used to emit a // hand-crafted HTTP/1.1 response with a large block of headers, bypassing the // real ClickHouse server (and its own header-size limits). -describe('[Node.js] client max_response_headers_size behavior', () => { - let client: ClickHouseClient | undefined +describe("[Node.js] client max_response_headers_size behavior", () => { + let client: ClickHouseClient | undefined; afterEach(async () => { if (client !== undefined) { - await client.close() - client = undefined + await client.close(); + client = undefined; } - }) + }); // Build enough X-H-NNNN headers to roughly reach `targetBytes`. function makeHeaders( targetBytes: number, ): Array<{ name: string; value: string }> { - const headers: Array<{ name: string; value: string }> = [] - let total = 0 - let i = 0 + const headers: Array<{ name: string; value: string }> = []; + let total = 0; + let i = 0; while (total < targetBytes) { - const name = `X-H-${String(i).padStart(4, '0')}` - const value = 'a'.repeat(90) - headers.push({ name, value }) - total += name.length + 2 /* ": " */ + value.length + 2 /* CRLF */ - i++ + const name = `X-H-${String(i).padStart(4, "0")}`; + const value = "a".repeat(90); + headers.push({ name, value }); + total += name.length + 2 /* ": " */ + value.length + 2; /* CRLF */ + i++; } - return headers + return headers; } // Raw TCP server that replies with a fixed HTTP/1.1 response containing @@ -45,31 +45,31 @@ describe('[Node.js] client max_response_headers_size behavior', () => { headers: Array<{ name: string; value: string }>, ): Promise<[net.Server, number]> { const server = net.createServer((socket) => { - socket.once('data', () => { - const body = 'Ok.\n' + socket.once("data", () => { + const body = "Ok.\n"; const headerBlob = headers .map((h) => `${h.name}: ${h.value}\r\n`) - .join('') + .join(""); const response = - 'HTTP/1.1 200 OK\r\n' + + "HTTP/1.1 200 OK\r\n" + `Content-Length: ${body.length}\r\n` + - 'Connection: close\r\n' + + "Connection: close\r\n" + headerBlob + - '\r\n' + - body - socket.write(response) - socket.end() - }) - }) + "\r\n" + + body; + socket.write(response); + socket.end(); + }); + }); await new Promise((resolve) => { - server.listen(0, '127.0.0.1', () => resolve()) - }) - return [server, (server.address() as AddressInfo).port] + server.listen(0, "127.0.0.1", () => resolve()); + }); + return [server, (server.address() as AddressInfo).port]; } type ClientResult = | { ok: true } - | { ok: false; code?: string; message: string } + | { ok: false; code?: string; message: string }; async function tryClient( port: number, @@ -81,84 +81,84 @@ describe('[Node.js] client max_response_headers_size behavior', () => { // sockets across the single response from our raw TCP server. keep_alive: { enabled: false }, max_response_headers_size: maxHeaderSize, - }) - const result = await client.ping() + }); + const result = await client.ping(); if (result.success) { - return { ok: true } + return { ok: true }; } - const err = result.error as NodeJS.ErrnoException - return { ok: false, code: err.code, message: err.message } + const err = result.error as NodeJS.ErrnoException; + return { ok: false, code: err.code, message: err.message }; } async function runScenario(params: { - payloadKB: number - maxHeaderSize?: number + payloadKB: number; + maxHeaderSize?: number; }): Promise<{ result: ClientResult }> { - const headers = makeHeaders(params.payloadKB * 1024) - const [server, port] = await startServer(headers) + const headers = makeHeaders(params.payloadKB * 1024); + const [server, port] = await startServer(headers); try { - const result = await tryClient(port, params.maxHeaderSize) - return { result } + const result = await tryClient(port, params.maxHeaderSize); + return { result }; } finally { await new Promise((resolve, reject) => { server.close((err) => { - if (err) reject(err) - else resolve() - }) - }) + if (err) reject(err); + else resolve(); + }); + }); } } // ── 16K bucket ──────────────────────────────────────────────── - it('A. ~15K payload at default 16K limit fits', async ({ expect }) => { - const { result } = await runScenario({ payloadKB: 15 }) - expect(result.ok).toBe(true) - }) + it("A. ~15K payload at default 16K limit fits", async ({ expect }) => { + const { result } = await runScenario({ payloadKB: 15 }); + expect(result.ok).toBe(true); + }); - it('B. ~20K payload at default 16K limit overflows (HPE_HEADER_OVERFLOW)', async ({ + it("B. ~20K payload at default 16K limit overflows (HPE_HEADER_OVERFLOW)", async ({ expect, }) => { - const { result } = await runScenario({ payloadKB: 20 }) - expect(result.ok).toBe(false) - if (result.ok) return - expect(result.code).toBe('HPE_HEADER_OVERFLOW') - }) + const { result } = await runScenario({ payloadKB: 20 }); + expect(result.ok).toBe(false); + if (result.ok) return; + expect(result.code).toBe("HPE_HEADER_OVERFLOW"); + }); // ── 32K bucket ──────────────────────────────────────────────── - it('C. ~20K payload raised to 32K limit fits', async ({ expect }) => { + it("C. ~20K payload raised to 32K limit fits", async ({ expect }) => { const { result } = await runScenario({ payloadKB: 20, maxHeaderSize: 32 * 1024, - }) - expect(result.ok).toBe(true) - }) + }); + expect(result.ok).toBe(true); + }); - it('D. ~40K payload at 32K limit overflows (HPE_HEADER_OVERFLOW)', async ({ + it("D. ~40K payload at 32K limit overflows (HPE_HEADER_OVERFLOW)", async ({ expect, }) => { const { result } = await runScenario({ payloadKB: 40, maxHeaderSize: 32 * 1024, - }) - expect(result.ok).toBe(false) - if (result.ok) return - expect(result.code).toBe('HPE_HEADER_OVERFLOW') - }) + }); + expect(result.ok).toBe(false); + if (result.ok) return; + expect(result.code).toBe("HPE_HEADER_OVERFLOW"); + }); // ── 64K bucket ──────────────────────────────────────────────── - it('E. ~40K payload raised to 64K limit fits', async ({ expect }) => { + it("E. ~40K payload raised to 64K limit fits", async ({ expect }) => { const { result } = await runScenario({ payloadKB: 40, maxHeaderSize: 64 * 1024, - }) - expect(result.ok).toBe(true) - }) + }); + expect(result.ok).toBe(true); + }); - it('F. ~60K payload at 64K limit fits', async ({ expect }) => { + it("F. ~60K payload at 64K limit fits", async ({ expect }) => { const { result } = await runScenario({ payloadKB: 60, maxHeaderSize: 64 * 1024, - }) - expect(result.ok).toBe(true) - }) -}) + }); + expect(result.ok).toBe(true); + }); +}); 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 4c61a6e0..d8c6b287 100644 --- a/packages/client-node/__tests__/integration/node_select_streaming.test.ts +++ b/packages/client-node/__tests__/integration/node_select_streaming.test.ts @@ -1,260 +1,260 @@ -import type { ClickHouseClient, Row } from '@clickhouse/client-common' -import { describe, it, beforeEach, afterEach, expect } from 'vitest' -import { createTestClient } from '@test/utils/client' -import type Stream from 'stream' +import type { ClickHouseClient, Row } from "@clickhouse/client-common"; +import { describe, it, beforeEach, afterEach, expect } from "vitest"; +import { createTestClient } from "@test/utils/client"; +import type Stream from "stream"; -describe('[Node.js] SELECT streaming', () => { - let client: ClickHouseClient +describe("[Node.js] SELECT streaming", () => { + let client: ClickHouseClient; afterEach(async () => { - await client.close() - }) + await client.close(); + }); beforeEach(async () => { - client = createTestClient() - }) + client = createTestClient(); + }); - describe('consume the response only once', () => { + describe("consume the response only once", () => { function assertAlreadyConsumed(fn: () => T) { - expect(fn).toThrow('Stream has been already consumed') + expect(fn).toThrow("Stream has been already consumed"); } - it('should consume a JSON response only once', async () => { + it("should consume a JSON response only once", async () => { const rs = await client.query({ - query: 'SELECT * FROM system.numbers LIMIT 1', - format: 'JSONEachRow', - }) - expect(await rs.json()).toEqual([{ number: '0' }]) + query: "SELECT * FROM system.numbers LIMIT 1", + format: "JSONEachRow", + }); + expect(await rs.json()).toEqual([{ number: "0" }]); // wrap in a func to avoid changing inner "this" 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 () => { + it("should consume a text response only once", async () => { const rs = await client.query({ - query: 'SELECT * FROM system.numbers LIMIT 1', - format: 'JSONEachRow', - }) - expect(await rs.text()).toEqual('{"number":"0"}\n') + query: "SELECT * FROM system.numbers LIMIT 1", + format: "JSONEachRow", + }); + expect(await rs.text()).toEqual('{"number":"0"}\n'); // wrap in a func to avoid changing inner "this" 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 () => { + it("should consume a stream response only once", async () => { const rs = await client.query({ - query: 'SELECT * FROM system.numbers LIMIT 1', - format: 'JSONEachRow', - }) - let result = '' + query: "SELECT * FROM system.numbers LIMIT 1", + format: "JSONEachRow", + }); + let result = ""; for await (const rows of rs.stream()) { rows.forEach((row: Row) => { - result += row.text - }) + result += row.text; + }); } - expect(result).toEqual('{"number":"0"}') + expect(result).toEqual('{"number":"0"}'); // wrap in a func to avoid changing inner "this" 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 as stream()', () => { - it('throws an exception if format is not stream-able', async () => { + 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', - }) + query: "SELECT number FROM system.numbers LIMIT 5", + format: "JSON", + }); try { await expect(async () => result.stream()).rejects.toThrow( /JSON format is not streamable/, - ) + ); } finally { - result.close() + result.close(); } - }) + }); - it('can pause response stream', async () => { + it("can pause response stream", async () => { const result = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 10000', - format: 'CSV', - }) + query: "SELECT number FROM system.numbers LIMIT 10000", + format: "CSV", + }); - const stream = result.stream() + const stream = result.stream(); - let last = '' - let i = 0 + let last = ""; + let i = 0; for await (const rows of stream) { rows.forEach((row: Row) => { - last = row.text - i++ + last = row.text; + i++; if (i % 1000 === 0) { - stream.pause() - setTimeout(() => stream.resume(), 100) + stream.pause(); + setTimeout(() => stream.resume(), 100); } - }) + }); } - expect(last).toBe('9999') - }) + expect(last).toBe("9999"); + }); - describe('text()', () => { - it('returns stream of rows in CSV format', async () => { + describe("text()", () => { + it("returns stream of rows in CSV format", async () => { const result = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 5', - format: 'CSV', - }) + query: "SELECT number FROM system.numbers LIMIT 5", + format: "CSV", + }); - const rs = await rowsText(result.stream()) - expect(rs).toEqual(['0', '1', '2', '3', '4']) - }) + const rs = await rowsText(result.stream()); + expect(rs).toEqual(["0", "1", "2", "3", "4"]); + }); - it('returns stream of rows in TabSeparated format', async () => { + it("returns stream of rows in TabSeparated format", async () => { const result = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 5', - format: 'TabSeparated', - }) + query: "SELECT number FROM system.numbers LIMIT 5", + format: "TabSeparated", + }); - const rs = await rowsText(result.stream()) - expect(rs).toEqual(['0', '1', '2', '3', '4']) - }) - }) + const rs = await rowsText(result.stream()); + expect(rs).toEqual(["0", "1", "2", "3", "4"]); + }); + }); - describe('json()', () => { - it('returns stream of objects in JSONEachRow format', async () => { + describe("json()", () => { + it("returns stream of objects in JSONEachRow format", async () => { const result = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 5', - format: 'JSONEachRow', - }) + query: "SELECT number FROM system.numbers LIMIT 5", + format: "JSONEachRow", + }); - const rs = await rowsValues(result.stream()) + const rs = await rowsValues(result.stream()); expect(rs).toEqual([ - { number: '0' }, - { number: '1' }, - { number: '2' }, - { number: '3' }, - { number: '4' }, - ]) - }) + { number: "0" }, + { number: "1" }, + { number: "2" }, + { number: "3" }, + { number: "4" }, + ]); + }); - it('returns stream of objects in JSONStringsEachRow format', async () => { + it("returns stream of objects in JSONStringsEachRow format", async () => { const result = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 5', - format: 'JSONStringsEachRow', - }) + query: "SELECT number FROM system.numbers LIMIT 5", + format: "JSONStringsEachRow", + }); - const rs = await rowsValues(result.stream()) + const rs = await rowsValues(result.stream()); expect(rs).toEqual([ - { number: '0' }, - { number: '1' }, - { number: '2' }, - { number: '3' }, - { number: '4' }, - ]) - }) + { number: "0" }, + { number: "1" }, + { number: "2" }, + { number: "3" }, + { number: "4" }, + ]); + }); - it('returns stream of objects in JSONCompactEachRow format', async () => { + it("returns stream of objects in JSONCompactEachRow format", async () => { const result = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 5', - format: 'JSONCompactEachRow', - }) + query: "SELECT number FROM system.numbers LIMIT 5", + format: "JSONCompactEachRow", + }); - const rs = await rowsValues(result.stream()) - expect(rs).toEqual([['0'], ['1'], ['2'], ['3'], ['4']]) - }) + const rs = await rowsValues(result.stream()); + expect(rs).toEqual([["0"], ["1"], ["2"], ["3"], ["4"]]); + }); - it('returns stream of objects in JSONCompactEachRowWithNames format', async () => { + it("returns stream of objects in JSONCompactEachRowWithNames format", async () => { const result = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 5', - format: 'JSONCompactEachRowWithNames', - }) + query: "SELECT number FROM system.numbers LIMIT 5", + format: "JSONCompactEachRowWithNames", + }); - const rs = await rowsValues(result.stream()) - expect(rs).toEqual([['number'], ['0'], ['1'], ['2'], ['3'], ['4']]) - }) + const rs = await rowsValues(result.stream()); + expect(rs).toEqual([["number"], ["0"], ["1"], ["2"], ["3"], ["4"]]); + }); - it('returns stream of objects in JSONCompactEachRowWithNamesAndTypes format', async () => { + it("returns stream of objects in JSONCompactEachRowWithNamesAndTypes format", async () => { const result = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 5', - format: 'JSONCompactEachRowWithNamesAndTypes', - }) + query: "SELECT number FROM system.numbers LIMIT 5", + format: "JSONCompactEachRowWithNamesAndTypes", + }); - const rs = await rowsValues(result.stream()) + const rs = await rowsValues(result.stream()); expect(rs).toEqual([ - ['number'], - ['UInt64'], - ['0'], - ['1'], - ['2'], - ['3'], - ['4'], - ]) - }) + ["number"], + ["UInt64"], + ["0"], + ["1"], + ["2"], + ["3"], + ["4"], + ]); + }); - it('returns stream of objects in JSONCompactStringsEachRowWithNames format', async () => { + it("returns stream of objects in JSONCompactStringsEachRowWithNames format", async () => { const result = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 5', - format: 'JSONCompactStringsEachRowWithNames', - }) + query: "SELECT number FROM system.numbers LIMIT 5", + format: "JSONCompactStringsEachRowWithNames", + }); - const rs = await rowsValues(result.stream()) - expect(rs).toEqual([['number'], ['0'], ['1'], ['2'], ['3'], ['4']]) - }) + const rs = await rowsValues(result.stream()); + expect(rs).toEqual([["number"], ["0"], ["1"], ["2"], ["3"], ["4"]]); + }); - it('returns stream of objects in JSONCompactStringsEachRowWithNamesAndTypes format', async () => { + it("returns stream of objects in JSONCompactStringsEachRowWithNamesAndTypes format", async () => { const result = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 5', - format: 'JSONCompactStringsEachRowWithNamesAndTypes', - }) + query: "SELECT number FROM system.numbers LIMIT 5", + format: "JSONCompactStringsEachRowWithNamesAndTypes", + }); - const rs = await rowsValues(result.stream()) + const rs = await rowsValues(result.stream()); expect(rs).toEqual([ - ['number'], - ['UInt64'], - ['0'], - ['1'], - ['2'], - ['3'], - ['4'], - ]) - }) - }) - }) -}) + ["number"], + ["UInt64"], + ["0"], + ["1"], + ["2"], + ["3"], + ["4"], + ]); + }); + }); + }); +}); async function rowsValues(stream: Stream.Readable): Promise { - const result: any[] = [] + const result: any[] = []; for await (const rows of stream) { rows.forEach((row: Row) => { - result.push(row.json()) - }) + result.push(row.json()); + }); } - return result + return result; } async function rowsText(stream: Stream.Readable): Promise { - const result: string[] = [] + const result: string[] = []; for await (const rows of stream) { rows.forEach((row: Row) => { - result.push(row.text) - }) + result.push(row.text); + }); } - return result + return result; } diff --git a/packages/client-node/__tests__/integration/node_socket_handling.test.ts b/packages/client-node/__tests__/integration/node_socket_handling.test.ts index 18c54ae0..7277ff02 100644 --- a/packages/client-node/__tests__/integration/node_socket_handling.test.ts +++ b/packages/client-node/__tests__/integration/node_socket_handling.test.ts @@ -1,39 +1,39 @@ import type { ClickHouseClient, ConnPingResult, -} from '@clickhouse/client-common' -import { describe, it, beforeAll, afterAll, afterEach, expect } from 'vitest' -import { permutations } from '@test/utils/permutations' -import { createTestClient } from '@test/utils/client' -import * as http from 'http' -import net from 'net' -import type Stream from 'stream' -import type { NodeClickHouseClientConfigOptions } from '../../src/config' -import { AddressInfo } from 'net' - -const ClientTimeout = 10 // ms -const Iterations = 5 -const MaxOpenConnections = 2 - -describe.concurrent('Slow server', () => { - let client: ClickHouseClient - let server: http.Server | null = null - let port: number - let sleepServerPromise: Promise - let sleepServerPromiseResolve: () => void +} from "@clickhouse/client-common"; +import { describe, it, beforeAll, afterAll, afterEach, expect } from "vitest"; +import { permutations } from "@test/utils/permutations"; +import { createTestClient } from "@test/utils/client"; +import * as http from "http"; +import net from "net"; +import type Stream from "stream"; +import type { NodeClickHouseClientConfigOptions } from "../../src/config"; +import { AddressInfo } from "net"; + +const ClientTimeout = 10; // ms +const Iterations = 5; +const MaxOpenConnections = 2; + +describe.concurrent("Slow server", () => { + let client: ClickHouseClient; + let server: http.Server | null = null; + let port: number; + let sleepServerPromise: Promise; + let sleepServerPromiseResolve: () => void; beforeAll(async () => { sleepServerPromise = new Promise((resolve) => { - sleepServerPromiseResolve = resolve + sleepServerPromiseResolve = resolve; // Simulate a ClickHouse server that responds with a delay - }) + }); // Simulate a ClickHouse server that does not respond to the request in time - ;[server, port] = await createHTTPServer(async (req, res) => { - await sleepServerPromise - res.write('Ok.') - return res.end() - }) + [server, port] = await createHTTPServer(async (req, res) => { + await sleepServerPromise; + res.write("Ok."); + return res.end(); + }); // Client has request timeout set to lower than the server's "sleep" time client = createTestClient({ url: `http://127.0.0.1:${port}`, @@ -42,118 +42,118 @@ describe.concurrent('Slow server', () => { keep_alive: { enable: true, }, - } as NodeClickHouseClientConfigOptions) - }) + } as NodeClickHouseClientConfigOptions); + }); afterAll(async () => { - await client.close() - sleepServerPromiseResolve() - server && (await closeServer(server)) - }) + await client.close(); + sleepServerPromiseResolve(); + server && (await closeServer(server)); + }); const allOps = [ - { fn: select, opName: 'query' }, - { fn: insert, opName: 'insert' }, - { fn: exec, opName: 'exec' }, - { fn: command, opName: 'command' }, - ] + { fn: select, opName: "query" }, + { fn: insert, opName: "insert" }, + { fn: exec, opName: "exec" }, + { fn: command, opName: "command" }, + ]; // Lightly entering the fuzzing zone. // Ping first, then 2 operations in all possible combinations it.for(permutations(allOps, 2))( - 'should work with all operations permutations', + "should work with all operations permutations", async (ops) => { for (const { fn, opName } of ops) { - const pingResult = await client.ping() - expect(pingResult.success).toBeFalsy() + const pingResult = await client.ping(); + expect(pingResult.success).toBeFalsy(); expect((pingResult as { error: Error }).error.message).toEqual( - expect.stringContaining('Timeout error.'), - ) + expect.stringContaining("Timeout error."), + ); await expect( fn(), `${opName} should have been rejected. Current ops: ${JSON.stringify(ops)}`, - ).rejects.toThrow('Timeout error.') + ).rejects.toThrow("Timeout error."); } }, - ) + ); - it('should not throw unhandled errors with Ping', async () => { + it("should not throw unhandled errors with Ping", async () => { for (let i = 1; i <= Iterations; i++) { - const pingResult = await client.ping() - expect(pingResult.success).toBeFalsy() + const pingResult = await client.ping(); + expect(pingResult.success).toBeFalsy(); expect((pingResult as { error: Error }).error.message).toEqual( - expect.stringContaining('Timeout error.'), - ) + expect.stringContaining("Timeout error."), + ); } - }) + }); - it('should not throw unhandled errors with Select', async () => { + it("should not throw unhandled errors with Select", async () => { for (let i = 1; i <= Iterations; i++) { - await expect(select()).rejects.toThrow('Timeout error.') + await expect(select()).rejects.toThrow("Timeout error."); } - }) + }); - it('should not throw unhandled errors with Insert', async () => { + it("should not throw unhandled errors with Insert", async () => { for (let i = 1; i <= Iterations; i++) { - await expect(insert()).rejects.toThrow('Timeout error.') + await expect(insert()).rejects.toThrow("Timeout error."); } - }) + }); - it('should not throw unhandled errors with Command', async () => { + it("should not throw unhandled errors with Command", async () => { for (let i = 1; i <= Iterations; i++) { - await expect(command()).rejects.toThrow('Timeout error.') + await expect(command()).rejects.toThrow("Timeout error."); } - }) + }); - it('should not throw unhandled errors with Exec', async () => { + it("should not throw unhandled errors with Exec", async () => { for (let i = 1; i <= Iterations; i++) { - await expect(exec()).rejects.toThrow('Timeout error.') + await expect(exec()).rejects.toThrow("Timeout error."); } - }) + }); - it('should not throw unhandled errors with parallel Select operations', async () => { + it("should not throw unhandled errors with parallel Select operations", async () => { for (let i = 1; i <= Iterations; i++) { const promises = [...new Array(MaxOpenConnections)].map(async () => { - await expect(select()).rejects.toThrow('Timeout error.') - }) - await Promise.all(promises) + await expect(select()).rejects.toThrow("Timeout error."); + }); + await Promise.all(promises); } - }) + }); async function select() { - const rs = await client.query({ query: 'SELECT 1' }) - await rs.text() + const rs = await client.query({ query: "SELECT 1" }); + await rs.text(); } async function insert() { await client.insert({ - table: 'test', + table: "test", values: [{ x: 1 }], - }) + }); } async function exec() { - await client.exec({ query: 'SELECT 1' }) + await client.exec({ query: "SELECT 1" }); } async function command() { - await client.command({ query: 'SELECT 1' }) + await client.command({ query: "SELECT 1" }); } -}) +}); -describe('Server that times out', () => { - it('should eventually get a successful ping', async () => { - let requestCount = 0 +describe("Server that times out", () => { + it("should eventually get a successful ping", async () => { + let requestCount = 0; // Simulate an LB where the server is not available const [server, port] = await createHTTPServer(async (req, res) => { - requestCount++ + requestCount++; if (requestCount >= 2) { - res.write('Ok.') - return res.end() + res.write("Ok."); + return res.end(); } else { // don't respond // just keep the connection open until the client times out } - }) + }); // Client has request timeout set to lower than the server's "sleep" time const client = createTestClient({ url: `http://127.0.0.1:${port}`, @@ -161,27 +161,27 @@ describe('Server that times out', () => { keep_alive: { enabled: true, }, - }) + }); // The first request should fail with a timeout error - const pingResult = await client.ping() - expect(pingResult.success).toBeFalsy() + const pingResult = await client.ping(); + expect(pingResult.success).toBeFalsy(); expect( (pingResult as ConnPingResult & { success: false }).error.message, - ).toEqual('Timeout error.') + ).toEqual("Timeout error."); // The second request should be successful - expect(await client.ping()).toEqual({ success: true }) + expect(await client.ping()).toEqual({ success: true }); - await client.close() - await closeServer(server) - }) -}) + await client.close(); + await closeServer(server); + }); +}); -describe('Resource is not available', () => { - let client: ClickHouseClient - let server: http.Server | undefined - const port = 18125 +describe("Resource is not available", () => { + let client: ClickHouseClient; + let server: http.Server | undefined; + const port = 18125; beforeAll(async () => { // Client has request timeout set to lower than the server's "sleep" time client = createTestClient({ @@ -191,148 +191,148 @@ describe('Resource is not available', () => { keep_alive: { enable: true, }, - } as NodeClickHouseClientConfigOptions) - }) + } as NodeClickHouseClientConfigOptions); + }); afterAll(async () => { - await client.close() - await closeServer(server!) - }) + await client.close(); + await closeServer(server!); + }); - it('should fail with a connection error, but then reach out to the server', async () => { + it("should fail with a connection error, but then reach out to the server", async () => { // Try to reach to the unavailable server a few times for (let i = 1; i <= Iterations; i++) { - const pingResult = await client.ping() - expect(pingResult.success).toBeFalsy() + const pingResult = await client.ping(); + expect(pingResult.success).toBeFalsy(); if (pingResult.success) { // suggest to TS what type pingResult is - throw new Error('Ping should have failed') + throw new Error("Ping should have failed"); } - const error = pingResult.error - expect((error as NodeJS.ErrnoException).code).toEqual('ECONNREFUSED') + const error = pingResult.error; + expect((error as NodeJS.ErrnoException).code).toEqual("ECONNREFUSED"); } // now we start the server, and it is available; and we should have already used every socket in the pool - ;[server] = await createHTTPServer(async (req, res) => { - res.write('Ok.') - return res.end() - }, port) + [server] = await createHTTPServer(async (req, res) => { + res.write("Ok."); + return res.end(); + }, port); // no socket timeout or other errors - expect(await client.ping()).toEqual({ success: true }) - }) -}) + expect(await client.ping()).toEqual({ success: true }); + }); +}); -describe.concurrent('Server that drops connections', () => { +describe.concurrent("Server that drops connections", () => { it('should expose "socket hang up" error', async () => { const [server, port] = await createTCPServer(async (socket) => { - drainSocket(socket) - socket.write('HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n') - await sleep(10) + drainSocket(socket); + socket.write("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n"); + await sleep(10); // close the connection without sending the rest of the response headers or body - socket.end() - }) + socket.end(); + }); const client = createTestClient({ url: `http://127.0.0.1:${port}`, keep_alive: { enable: true, }, - } as NodeClickHouseClientConfigOptions) + } as NodeClickHouseClientConfigOptions); - const result = await client.ping() + const result = await client.ping(); - expect(result).toMatchObject({ success: false }) + expect(result).toMatchObject({ success: false }); if (result.success) { - throw new Error('Ping should have failed') + throw new Error("Ping should have failed"); } - expect(String(result.error)).toMatch(/socket hang up/) + expect(String(result.error)).toMatch(/socket hang up/); - await client.close() - await closeServer(server) - }) + await client.close(); + await closeServer(server); + }); it('should expose "invalid header token" error', async () => { const [server, port] = await createTCPServer(async (socket) => { - drainSocket(socket) - socket.write('HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nOk.\r\n') - await sleep(10) - socket.end() - }) + drainSocket(socket); + socket.write("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nOk.\r\n"); + await sleep(10); + socket.end(); + }); const client = createTestClient({ url: `http://127.0.0.1:${port}`, keep_alive: { enable: true, }, - } as NodeClickHouseClientConfigOptions) + } as NodeClickHouseClientConfigOptions); - const result = await client.ping() + const result = await client.ping(); - expect(result).toMatchObject({ success: false }) + expect(result).toMatchObject({ success: false }); if (result.success) { - throw new Error('Ping should have failed') + throw new Error("Ping should have failed"); } - expect(String(result.error)).toMatch(/invalid header/i) + expect(String(result.error)).toMatch(/invalid header/i); - await client.close() - await closeServer(server) - }) + await client.close(); + await closeServer(server); + }); it('should expose "aborted" error', async () => { const [server, port] = await createTCPServer(async (socket) => { - drainSocket(socket) + drainSocket(socket); socket.write( - 'HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length:100\r\n\r\npartial body', - ) - await sleep(10) - socket.end() - }) + "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length:100\r\n\r\npartial body", + ); + await sleep(10); + socket.end(); + }); const client = createTestClient({ url: `http://127.0.0.1:${port}`, keep_alive: { enable: true, }, - } as NodeClickHouseClientConfigOptions) + } as NodeClickHouseClientConfigOptions); - const result = await client.ping() + const result = await client.ping(); - expect(result).toMatchObject({ success: false }) + expect(result).toMatchObject({ success: false }); if (result.success) { - throw new Error('Ping should have failed') + throw new Error("Ping should have failed"); } - expect(String(result.error)).toMatch(/aborted/i) + expect(String(result.error)).toMatch(/aborted/i); - await client.close() - await closeServer(server) - }) + await client.close(); + await closeServer(server); + }); it('should expose "ECONNRESET" error', async () => { - let sleepServerPromiseResolve: () => void + let sleepServerPromiseResolve: () => void; let sleepServerPromise = new Promise((resolve) => { - sleepServerPromiseResolve = resolve + sleepServerPromiseResolve = resolve; // Simulate a ClickHouse server that responds with a delay - }) + }); - let attempted = 0 + let attempted = 0; const [server, port] = await createTCPServer(async (socket) => { - attempted++ + attempted++; if (attempted >= 2) { - socket.destroy() - throw new Error('Extra connection attempt - should not happen') + socket.destroy(); + throw new Error("Extra connection attempt - should not happen"); } // Write a valid response socket.write( - 'HTTP/1.1 200 OK\r\n' + - 'Content-Type: text/plain\r\n' + - 'Content-Length: 3\r\n' + - 'Connection: keep-alive\r\n' + - '\r\n' + - 'Ok.', - ) + "HTTP/1.1 200 OK\r\n" + + "Content-Type: text/plain\r\n" + + "Content-Length: 3\r\n" + + "Connection: keep-alive\r\n" + + "\r\n" + + "Ok.", + ); // Then start the next request - await sleepServerPromise + await sleepServerPromise; // …and then drop the connection before sending the full response - socket.destroy() - }) + socket.destroy(); + }); const client = createTestClient({ url: `http://127.0.0.1:${port}`, @@ -343,63 +343,63 @@ describe.concurrent('Server that drops connections', () => { level: 0, }, max_open_connections: 1, - } as NodeClickHouseClientConfigOptions) + } as NodeClickHouseClientConfigOptions); - expect(await client.ping()).toMatchObject({ success: true }) + expect(await client.ping()).toMatchObject({ success: true }); - let ping2 = client.ping() + let ping2 = client.ping(); // Client has a sleep(0) inside, the test has to wait for it to complete, // otherwise the socket gets closed before the client gets to use it. // This way we get the "socket hang up" error instead of "ECONNRESET". - await sleep(0) - sleepServerPromiseResolve!() - ping2 = await ping2 + await sleep(0); + sleepServerPromiseResolve!(); + ping2 = await ping2; - expect(ping2).toMatchObject({ success: false }) + expect(ping2).toMatchObject({ success: false }); if (ping2.success) { - throw new Error('Ping should have failed') + throw new Error("Ping should have failed"); } - expect(String(ping2.error)).toMatch(/ECONNRESET/i) + expect(String(ping2.error)).toMatch(/ECONNRESET/i); - await client.close() - await closeServer(server) - }) -}) + await client.close(); + await closeServer(server); + }); +}); async function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)) + return new Promise((resolve) => setTimeout(resolve, ms)); } function closeServer(server: http.Server | net.Server): Promise { return new Promise((resolve, reject) => { - server.close((err) => (err ? reject(err) : resolve())) - }) + server.close((err) => (err ? reject(err) : resolve())); + }); } async function createHTTPServer( cb: (req: http.IncomingMessage, res: http.ServerResponse) => void, port: number = 0, ): Promise<[http.Server, number]> { - const server = http.createServer(cb) + const server = http.createServer(cb); await new Promise((resolve) => { - server.listen(port, () => resolve()) - }) - return [server, (server.address() as AddressInfo).port] + server.listen(port, () => resolve()); + }); + return [server, (server.address() as AddressInfo).port]; } async function createTCPServer( cb: (socket: net.Socket) => void, port: number = 0, ): Promise<[net.Server, number]> { - const server = net.createServer(cb) + const server = net.createServer(cb); await new Promise((resolve) => { - server.listen(port, () => resolve()) - }) - return [server, (server.address() as AddressInfo).port] + server.listen(port, () => resolve()); + }); + return [server, (server.address() as AddressInfo).port]; } async function drainSocket(socket: net.Socket): Promise { for await (const chunk of socket) { - console.log('Received from socket:', chunk.toString()) + console.log("Received from socket:", chunk.toString()); } } 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 50b05904..fa6ac381 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 @@ -1,113 +1,113 @@ -import { describe, it, beforeEach, afterEach } from 'vitest' +import { describe, it, beforeEach, afterEach } from "vitest"; import { assertError, streamErrorQueryParams, -} from '@test/fixtures/stream_errors' -import { isClickHouseVersionAtLeast } from '@test/utils/server_version' -import type { ClickHouseClient } from '../../src' -import type { ClickHouseError } from '../../src' -import { createNodeTestClient } from '../utils/node_client' +} from "@test/fixtures/stream_errors"; +import { isClickHouseVersionAtLeast } from "@test/utils/server_version"; +import type { ClickHouseClient } from "../../src"; +import type { ClickHouseError } from "../../src"; +import { createNodeTestClient } from "../utils/node_client"; // See https://github.com/ClickHouse/ClickHouse/pull/88818 -describe('[Node.js] Stream error handling', () => { - let client: ClickHouseClient +describe("[Node.js] Stream error handling", () => { + let client: ClickHouseClient; beforeEach(async () => { - client = createNodeTestClient() - }) + client = createNodeTestClient(); + }); afterEach(async () => { - await client.close() - }) + await client.close(); + }); - it('with promise listeners', async ({ skip }) => { + it("with promise listeners", async ({ skip }) => { if (!(await isClickHouseVersionAtLeast(client, 25, 11))) { - skip() + skip(); } - let caughtError: ClickHouseError | null = null + let caughtError: ClickHouseError | null = null; try { - const queryParams = streamErrorQueryParams() - const rs = await client.query(queryParams) + const queryParams = streamErrorQueryParams(); + const rs = await client.query(queryParams); await new Promise((resolve, reject) => { - const stream = rs.stream<{ n: number }>() - stream.on('data', (rows) => { + const stream = rs.stream<{ n: number }>(); + stream.on("data", (rows) => { for (const row of rows) { - row.json() // ignored + row.json(); // ignored } - }) - stream.on('error', (err) => { - reject(err) - }) - stream.on('end', () => { - resolve() - }) - }) + }); + stream.on("error", (err) => { + reject(err); + }); + stream.on("end", () => { + resolve(); + }); + }); } catch (err) { - caughtError = err as ClickHouseError + caughtError = err as ClickHouseError; } - assertError(caughtError) - }) + assertError(caughtError); + }); - it('with async iterators', async ({ skip }) => { + it("with async iterators", async ({ skip }) => { if (!(await isClickHouseVersionAtLeast(client, 25, 11))) { - skip() + skip(); } - let caughtError: ClickHouseError | null = null + let caughtError: ClickHouseError | null = null; try { - const queryParams = streamErrorQueryParams() - const rs = await client.query(queryParams) + const queryParams = streamErrorQueryParams(); + const rs = await client.query(queryParams); - const stream = rs.stream() + const stream = rs.stream(); for await (const rows of stream) { for (const row of rows) { - row.json() // ignored + row.json(); // ignored } } } catch (err) { - caughtError = err as ClickHouseError + caughtError = err as ClickHouseError; } - assertError(caughtError) - }) + assertError(caughtError); + }); - it.skip('with .json()', async ({ skip }) => { + it.skip("with .json()", async ({ skip }) => { if (!(await isClickHouseVersionAtLeast(client, 25, 11))) { - skip() + skip(); } - let caughtError: ClickHouseError | null = null + let caughtError: ClickHouseError | null = null; try { - const queryParams = streamErrorQueryParams() - const rs = await client.query(queryParams) - await rs.json() + const queryParams = streamErrorQueryParams(); + const rs = await client.query(queryParams); + await rs.json(); } catch (err) { - caughtError = err as ClickHouseError + caughtError = err as ClickHouseError; } - assertError(caughtError) - }) + assertError(caughtError); + }); - it.skip('with .text()', async ({ skip }) => { + it.skip("with .text()", async ({ skip }) => { if (!(await isClickHouseVersionAtLeast(client, 25, 11))) { - skip() + skip(); } - let caughtError: ClickHouseError | null = null + let caughtError: ClickHouseError | null = null; try { - const queryParams = streamErrorQueryParams() - const rs = await client.query(queryParams) - await rs.text() + const queryParams = streamErrorQueryParams(); + const rs = await client.query(queryParams); + await rs.text(); } catch (err) { - caughtError = err as ClickHouseError + caughtError = err as ClickHouseError; } - assertError(caughtError) - }) -}) + assertError(caughtError); + }); +}); diff --git a/packages/client-node/__tests__/integration/node_stream_json_compact_each_row.test.ts b/packages/client-node/__tests__/integration/node_stream_json_compact_each_row.test.ts index a1d1282e..085ab494 100644 --- a/packages/client-node/__tests__/integration/node_stream_json_compact_each_row.test.ts +++ b/packages/client-node/__tests__/integration/node_stream_json_compact_each_row.test.ts @@ -1,197 +1,197 @@ -import { type ClickHouseClient } from '@clickhouse/client-common' -import { describe, it, beforeEach, afterEach, expect } from 'vitest' -import { createSimpleTable } from '@test/fixtures/simple_table' -import { createTestClient } from '@test/utils/client' -import { guid } from '@test/utils/guid' -import { makeObjectStream } from '../utils/stream' +import { type ClickHouseClient } from "@clickhouse/client-common"; +import { describe, it, beforeEach, afterEach, expect } from "vitest"; +import { createSimpleTable } from "@test/fixtures/simple_table"; +import { createTestClient } from "@test/utils/client"; +import { guid } from "@test/utils/guid"; +import { makeObjectStream } from "../utils/stream"; -let client: ClickHouseClient -let tableName: string +let client: ClickHouseClient; +let tableName: string; beforeEach(async () => { - client = createTestClient() - tableName = `insert_stream_json_${guid()}` - await createSimpleTable(client, tableName) -}) + client = createTestClient(); + tableName = `insert_stream_json_${guid()}`; + await createSimpleTable(client, tableName); +}); afterEach(async () => { - await client.close() -}) + await client.close(); +}); -describe('JSONCompactEachRow', () => { - it('should work with JSONCompactEachRow', async () => { - const stream = makeObjectStream() - stream.push(['42', 'foo', [0, 1]]) - stream.push(['43', 'bar', [2, 3]]) - setTimeout(() => stream.push(null), 100) +describe("JSONCompactEachRow", () => { + it("should work with JSONCompactEachRow", async () => { + const stream = makeObjectStream(); + stream.push(["42", "foo", [0, 1]]); + stream.push(["43", "bar", [2, 3]]); + setTimeout(() => stream.push(null), 100); await client.insert({ table: tableName, values: stream, - format: 'JSONCompactEachRow', - }) + format: "JSONCompactEachRow", + }); const result = await client.query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, - format: 'JSONCompactEachRow', - }) + format: "JSONCompactEachRow", + }); expect(await result.json()).toEqual([ - ['42', 'foo', [0, 1]], - ['43', 'bar', [2, 3]], - ]) - }) + ["42", "foo", [0, 1]], + ["43", "bar", [2, 3]], + ]); + }); - it('should work with JSONCompactStringsEachRow', async () => { - const stream = makeObjectStream() - stream.push(['42', 'foo', '[0,1]']) - stream.push(['43', 'bar', '[2,3]']) - setTimeout(() => stream.push(null), 100) + it("should work with JSONCompactStringsEachRow", async () => { + const stream = makeObjectStream(); + stream.push(["42", "foo", "[0,1]"]); + stream.push(["43", "bar", "[2,3]"]); + setTimeout(() => stream.push(null), 100); await client.insert({ table: tableName, values: stream, - format: 'JSONCompactStringsEachRow', - }) + format: "JSONCompactStringsEachRow", + }); const result = await client.query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, - format: 'JSONCompactStringsEachRow', - }) + format: "JSONCompactStringsEachRow", + }); expect(await result.json()).toEqual([ - ['42', 'foo', '[0,1]'], - ['43', 'bar', '[2,3]'], - ]) - }) + ["42", "foo", "[0,1]"], + ["43", "bar", "[2,3]"], + ]); + }); - it('should work with JSONCompactEachRowWithNames', async () => { - const stream = makeObjectStream() - stream.push(['id', 'name', 'sku']) - stream.push(['42', 'foo', [0, 1]]) - stream.push(['43', 'bar', [2, 3]]) - setTimeout(() => stream.push(null), 100) + it("should work with JSONCompactEachRowWithNames", async () => { + const stream = makeObjectStream(); + stream.push(["id", "name", "sku"]); + stream.push(["42", "foo", [0, 1]]); + stream.push(["43", "bar", [2, 3]]); + setTimeout(() => stream.push(null), 100); await client.insert({ table: tableName, values: stream, - format: 'JSONCompactEachRowWithNames', - }) + format: "JSONCompactEachRowWithNames", + }); const result = await client.query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, - format: 'JSONCompactEachRowWithNames', - }) + format: "JSONCompactEachRowWithNames", + }); expect(await result.json()).toEqual([ - ['id', 'name', 'sku'], - ['42', 'foo', [0, 1]], - ['43', 'bar', [2, 3]], - ]) - }) + ["id", "name", "sku"], + ["42", "foo", [0, 1]], + ["43", "bar", [2, 3]], + ]); + }); - it('should work with JSONCompactEachRowWithNamesAndTypes', async () => { - const stream = makeObjectStream() - stream.push(['id', 'name', 'sku']) - stream.push(['UInt64', 'String', 'Array(UInt8)']) - stream.push(['42', 'foo', [0, 1]]) - stream.push(['43', 'bar', [2, 3]]) - setTimeout(() => stream.push(null), 100) + it("should work with JSONCompactEachRowWithNamesAndTypes", async () => { + const stream = makeObjectStream(); + stream.push(["id", "name", "sku"]); + stream.push(["UInt64", "String", "Array(UInt8)"]); + stream.push(["42", "foo", [0, 1]]); + stream.push(["43", "bar", [2, 3]]); + setTimeout(() => stream.push(null), 100); await client.insert({ table: tableName, values: stream, - format: 'JSONCompactEachRowWithNamesAndTypes', - }) + format: "JSONCompactEachRowWithNamesAndTypes", + }); const result = await client.query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, - format: 'JSONCompactEachRowWithNamesAndTypes', - }) + format: "JSONCompactEachRowWithNamesAndTypes", + }); expect(await result.json()).toEqual([ - ['id', 'name', 'sku'], - ['UInt64', 'String', 'Array(UInt8)'], - ['42', 'foo', [0, 1]], - ['43', 'bar', [2, 3]], - ]) - }) + ["id", "name", "sku"], + ["UInt64", "String", "Array(UInt8)"], + ["42", "foo", [0, 1]], + ["43", "bar", [2, 3]], + ]); + }); - it('should insert data with a wrong name in JSONCompactEachRowWithNamesAndTypes', async () => { - const stream = makeObjectStream() - stream.push(['foo', 'name', 'sku']) - stream.push(['UInt64', 'String', 'Array(UInt8)']) - stream.push(['42', 'foo', [0, 1]]) - stream.push(['43', 'bar', [2, 3]]) - setTimeout(() => stream.push(null), 100) + it("should insert data with a wrong name in JSONCompactEachRowWithNamesAndTypes", async () => { + const stream = makeObjectStream(); + stream.push(["foo", "name", "sku"]); + stream.push(["UInt64", "String", "Array(UInt8)"]); + stream.push(["42", "foo", [0, 1]]); + stream.push(["43", "bar", [2, 3]]); + setTimeout(() => stream.push(null), 100); await client.insert({ table: tableName, values: stream, - format: 'JSONCompactEachRowWithNamesAndTypes', - }) + format: "JSONCompactEachRowWithNamesAndTypes", + }); const result = await client.query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, - format: 'JSONCompactEachRowWithNamesAndTypes', - }) + format: "JSONCompactEachRowWithNamesAndTypes", + }); expect(await result.json()).toEqual([ - ['id', 'name', 'sku'], - ['UInt64', 'String', 'Array(UInt8)'], - ['0', 'foo', [0, 1]], - ['0', 'bar', [2, 3]], - ]) - }) + ["id", "name", "sku"], + ["UInt64", "String", "Array(UInt8)"], + ["0", "foo", [0, 1]], + ["0", "bar", [2, 3]], + ]); + }); - it('should throw an exception when insert data with a wrong type in JSONCompactEachRowWithNamesAndTypes', async () => { - const stream = makeObjectStream() - stream.push(['id', 'name', 'sku']) - stream.push(['UInt64', 'UInt64', 'Array(UInt8)']) - stream.push(['42', 'foo', [0, 1]]) - stream.push(['43', 'bar', [2, 3]]) - setTimeout(() => stream.push(null), 100) + it("should throw an exception when insert data with a wrong type in JSONCompactEachRowWithNamesAndTypes", async () => { + const stream = makeObjectStream(); + stream.push(["id", "name", "sku"]); + stream.push(["UInt64", "UInt64", "Array(UInt8)"]); + stream.push(["42", "foo", [0, 1]]); + stream.push(["43", "bar", [2, 3]]); + setTimeout(() => stream.push(null), 100); const insertPromise = client.insert({ table: tableName, values: stream, - format: 'JSONCompactEachRowWithNamesAndTypes', - }) + format: "JSONCompactEachRowWithNamesAndTypes", + }); await expect(insertPromise).rejects.toMatchObject({ message: expect.stringMatching( `Type of 'name' must be String, not UInt64`, ), - }) - }) + }); + }); - it('should work with JSONCompactStringsEachRowWithNames', async () => { - const stream = makeObjectStream() - stream.push(['id', 'name', 'sku']) - stream.push(['42', 'foo', '[0,1]']) - stream.push(['43', 'bar', '[2,3]']) - setTimeout(() => stream.push(null), 100) + it("should work with JSONCompactStringsEachRowWithNames", async () => { + const stream = makeObjectStream(); + stream.push(["id", "name", "sku"]); + stream.push(["42", "foo", "[0,1]"]); + stream.push(["43", "bar", "[2,3]"]); + setTimeout(() => stream.push(null), 100); await client.insert({ table: tableName, values: stream, - format: 'JSONCompactStringsEachRowWithNames', - }) + format: "JSONCompactStringsEachRowWithNames", + }); const result = await client.query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, - format: 'JSONCompactStringsEachRowWithNames', - }) + format: "JSONCompactStringsEachRowWithNames", + }); expect(await result.json()).toEqual([ - ['id', 'name', 'sku'], - ['42', 'foo', '[0,1]'], - ['43', 'bar', '[2,3]'], - ]) - }) + ["id", "name", "sku"], + ["42", "foo", "[0,1]"], + ["43", "bar", "[2,3]"], + ]); + }); - it('should work with JSONCompactStringsEachRowWithNamesAndTypes', async () => { - const stream = makeObjectStream() - stream.push(['id', 'name', 'sku']) - stream.push(['UInt64', 'String', 'Array(UInt8)']) - stream.push(['42', 'foo', '[0,1]']) - stream.push(['43', 'bar', '[2,3]']) - setTimeout(() => stream.push(null), 100) + it("should work with JSONCompactStringsEachRowWithNamesAndTypes", async () => { + const stream = makeObjectStream(); + stream.push(["id", "name", "sku"]); + stream.push(["UInt64", "String", "Array(UInt8)"]); + stream.push(["42", "foo", "[0,1]"]); + stream.push(["43", "bar", "[2,3]"]); + setTimeout(() => stream.push(null), 100); await client.insert({ table: tableName, values: stream, - format: 'JSONCompactStringsEachRowWithNamesAndTypes', - }) + format: "JSONCompactStringsEachRowWithNamesAndTypes", + }); const result = await client.query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, - format: 'JSONCompactStringsEachRowWithNamesAndTypes', - }) + format: "JSONCompactStringsEachRowWithNamesAndTypes", + }); expect(await result.json()).toEqual([ - ['id', 'name', 'sku'], - ['UInt64', 'String', 'Array(UInt8)'], - ['42', 'foo', '[0,1]'], - ['43', 'bar', '[2,3]'], - ]) - }) -}) + ["id", "name", "sku"], + ["UInt64", "String", "Array(UInt8)"], + ["42", "foo", "[0,1]"], + ["43", "bar", "[2,3]"], + ]); + }); +}); diff --git a/packages/client-node/__tests__/integration/node_stream_json_each_row.test.ts b/packages/client-node/__tests__/integration/node_stream_json_each_row.test.ts index bad09f03..490c9e7d 100644 --- a/packages/client-node/__tests__/integration/node_stream_json_each_row.test.ts +++ b/packages/client-node/__tests__/integration/node_stream_json_each_row.test.ts @@ -1,51 +1,51 @@ -import { type ClickHouseClient } from '@clickhouse/client-common' -import { it, beforeEach, afterEach, expect } from 'vitest' -import { createSimpleTable } from '@test/fixtures/simple_table' -import { assertJsonValues, jsonValues } from '@test/fixtures/test_data' -import { createTestClient } from '@test/utils/client' -import { guid } from '@test/utils/guid' -import { makeObjectStream } from '../utils/stream' +import { type ClickHouseClient } from "@clickhouse/client-common"; +import { it, beforeEach, afterEach, expect } from "vitest"; +import { createSimpleTable } from "@test/fixtures/simple_table"; +import { assertJsonValues, jsonValues } from "@test/fixtures/test_data"; +import { createTestClient } from "@test/utils/client"; +import { guid } from "@test/utils/guid"; +import { makeObjectStream } from "../utils/stream"; -let client: ClickHouseClient -let tableName: string +let client: ClickHouseClient; +let tableName: string; beforeEach(async () => { - client = createTestClient() - tableName = `insert_stream_json_${guid()}` - await createSimpleTable(client, tableName) -}) + client = createTestClient(); + tableName = `insert_stream_json_${guid()}`; + await createSimpleTable(client, tableName); +}); afterEach(async () => { - await client.close() -}) + await client.close(); +}); -it('should work with JSONEachRow', async () => { - const stream = makeObjectStream() - jsonValues.forEach((value) => stream.push(value)) - setTimeout(() => stream.push(null), 100) +it("should work with JSONEachRow", async () => { + const stream = makeObjectStream(); + jsonValues.forEach((value) => stream.push(value)); + setTimeout(() => stream.push(null), 100); await client.insert({ table: tableName, values: stream, - format: 'JSONEachRow', - }) - await assertJsonValues(client, tableName) -}) + format: "JSONEachRow", + }); + await assertJsonValues(client, tableName); +}); -it('should work with JSONStringsEachRow', async () => { - const stream = makeObjectStream() - stream.push({ id: '42', name: 'foo', sku: '[0,1]' }) - stream.push({ id: '43', name: 'bar', sku: '[0,1,2]' }) - setTimeout(() => stream.push(null), 100) +it("should work with JSONStringsEachRow", async () => { + const stream = makeObjectStream(); + stream.push({ id: "42", name: "foo", sku: "[0,1]" }); + stream.push({ id: "43", name: "bar", sku: "[0,1,2]" }); + setTimeout(() => stream.push(null), 100); await client.insert({ table: tableName, values: stream, - format: 'JSONStringsEachRow', - }) + format: "JSONStringsEachRow", + }); const result = await client.query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, - format: 'JSONStringsEachRow', - }) + format: "JSONStringsEachRow", + }); expect(await result.json()).toEqual([ - { id: '42', name: 'foo', sku: '[0,1]' }, - { id: '43', name: 'bar', sku: '[0,1,2]' }, - ]) -}) + { id: "42", name: "foo", sku: "[0,1]" }, + { id: "43", name: "bar", sku: "[0,1,2]" }, + ]); +}); diff --git a/packages/client-node/__tests__/integration/node_stream_json_each_row_with_progress.test.ts b/packages/client-node/__tests__/integration/node_stream_json_each_row_with_progress.test.ts index 2fb1c288..ff9bf0f0 100644 --- a/packages/client-node/__tests__/integration/node_stream_json_each_row_with_progress.test.ts +++ b/packages/client-node/__tests__/integration/node_stream_json_each_row_with_progress.test.ts @@ -1,106 +1,106 @@ -import { type ClickHouseClient } from '@clickhouse/client-common' -import { describe, it, beforeEach, afterEach, expect } from 'vitest' -import { createSimpleTable } from '@test/fixtures/simple_table' -import { createTestClient } from '@test/utils/client' -import { isClickHouseVersionAtLeast } from '@test/utils/server_version' -import { guid } from '@test/utils/guid' -import * as simdjson from 'simdjson' -import { makeObjectStream } from '../utils/stream' - -let client: ClickHouseClient -let tableName: string +import { type ClickHouseClient } from "@clickhouse/client-common"; +import { describe, it, beforeEach, afterEach, expect } from "vitest"; +import { createSimpleTable } from "@test/fixtures/simple_table"; +import { createTestClient } from "@test/utils/client"; +import { isClickHouseVersionAtLeast } from "@test/utils/server_version"; +import { guid } from "@test/utils/guid"; +import * as simdjson from "simdjson"; +import { makeObjectStream } from "../utils/stream"; + +let client: ClickHouseClient; +let tableName: string; beforeEach(async () => { - client = createTestClient() - tableName = `insert_stream_json_${guid()}` - await createSimpleTable(client, tableName) -}) + client = createTestClient(); + tableName = `insert_stream_json_${guid()}`; + await createSimpleTable(client, tableName); +}); afterEach(async () => { - await client.close() -}) + await client.close(); +}); -describe('JSONEachRowWithProgress', () => { - it('works with progress rows', async () => { +describe("JSONEachRowWithProgress", () => { + it("works with progress rows", async () => { const rs = await client.query({ - query: 'SELECT sleep(0.1) AS foo FROM numbers(2)', - format: 'JSONEachRowWithProgress', + query: "SELECT sleep(0.1) AS foo FROM numbers(2)", + format: "JSONEachRowWithProgress", clickhouse_settings: { // triggers more progress rows, as it is emitted after each block - max_block_size: '1', + max_block_size: "1", }, - }) - const rows = await rs.json() + }); + const rows = await rs.json(); expect(rows).toEqual([ { progress: { - read_rows: '1', - read_bytes: '8', - total_rows_to_read: '2', + read_rows: "1", + read_bytes: "8", + total_rows_to_read: "2", elapsed_ns: expect.stringMatching(/^\d+$/), }, }, - { meta: [{ name: 'foo', type: 'UInt8' }] }, + { meta: [{ name: "foo", type: "UInt8" }] }, { row: { foo: 0 } }, { progress: { - read_rows: '2', - read_bytes: '16', - total_rows_to_read: '2', + read_rows: "2", + read_bytes: "16", + total_rows_to_read: "2", elapsed_ns: expect.stringMatching(/^\d+$/), }, }, { row: { foo: 0 } }, - ]) - }) + ]); + }); // See https://github.com/ClickHouse/ClickHouse/pull/74181/files#diff-9be59e5a502cccf360c8f2b0419115cfa2513def8f964f7c24459cfa0e877578 - it('works with special events', async () => { + it("works with special events", async () => { const rs = await client.query({ query: `SELECT (123 + number * 456) % 100 AS k, count() AS c, sum(number) AS s FROM numbers(100) GROUP BY ALL WITH TOTALS ORDER BY ALL LIMIT 10`, - format: 'JSONEachRowWithProgress', + format: "JSONEachRowWithProgress", clickhouse_settings: { rows_before_aggregation: 1, extremes: 1, }, - }) - const rows = await rs.json<{ k: number; c: string; s: string }>() + }); + const rows = await rs.json<{ k: number; c: string; s: string }>(); expect(rows).toEqual([ { progress: { - read_rows: '100', - read_bytes: '800', - total_rows_to_read: '100', + read_rows: "100", + read_bytes: "800", + total_rows_to_read: "100", elapsed_ns: expect.stringMatching(/^\d+$/), }, }, { meta: [ - { name: 'k', type: 'UInt8' }, - { name: 'c', type: 'UInt64' }, - { name: 's', type: 'UInt64' }, + { name: "k", type: "UInt8" }, + { name: "c", type: "UInt64" }, + { name: "s", type: "UInt64" }, ], }, - { row: { k: 3, c: '4', s: '170' } }, - { row: { k: 7, c: '4', s: '206' } }, - { row: { k: 11, c: '4', s: '242' } }, - { row: { k: 15, c: '4', s: '178' } }, - { row: { k: 19, c: '4', s: '214' } }, - { row: { k: 23, c: '4', s: '150' } }, - { row: { k: 27, c: '4', s: '186' } }, - { row: { k: 31, c: '4', s: '222' } }, - { row: { k: 35, c: '4', s: '158' } }, - { row: { k: 39, c: '4', s: '194' } }, - { totals: { k: 0, c: '100', s: '4950' } }, - { min: { k: 3, c: '4', s: '150' } }, - { max: { k: 39, c: '4', s: '242' } }, + { row: { k: 3, c: "4", s: "170" } }, + { row: { k: 7, c: "4", s: "206" } }, + { row: { k: 11, c: "4", s: "242" } }, + { row: { k: 15, c: "4", s: "178" } }, + { row: { k: 19, c: "4", s: "214" } }, + { row: { k: 23, c: "4", s: "150" } }, + { row: { k: 27, c: "4", s: "186" } }, + { row: { k: 31, c: "4", s: "222" } }, + { row: { k: 35, c: "4", s: "158" } }, + { row: { k: 39, c: "4", s: "194" } }, + { totals: { k: 0, c: "100", s: "4950" } }, + { min: { k: 3, c: "4", s: "150" } }, + { max: { k: 39, c: "4", s: "242" } }, { rows_before_limit_at_least: 25 }, { rows_before_aggregation: 100 }, - ]) - }) + ]); + }); - it('works with exceptions', async ({ skip }) => { + it("works with exceptions", async ({ skip }) => { if (!(await isClickHouseVersionAtLeast(client, 25, 11))) { - skip() + skip(); } const rs = await client.query({ @@ -108,235 +108,235 @@ describe('JSONEachRowWithProgress', () => { throwIf(number = 10, 'boom') AS e, sleepEachRow(0.001) FROM system.numbers LIMIT 100`, - format: 'JSONEachRowWithProgress', + format: "JSONEachRowWithProgress", clickhouse_settings: { // enforcing at least a few blocks, so that the response code is 200 OK - max_block_size: '1', + max_block_size: "1", // Should be false by default since 25.11; but setting explicitly to make sure // the server configuration doesn't interfere with the test. http_write_exception_in_output_format: false, }, - }) + }); await expect(rs.json()).rejects.toMatchObject({ - code: '395', + code: "395", message: expect.stringContaining( `boom: while executing 'FUNCTION throwIf`, ), - }) - }) + }); + }); - describe('custom JSON handling', () => { - it('should use custom stringify when inserting with JSONEachRow stream', async () => { - let stringifyCalls = 0 + describe("custom JSON handling", () => { + it("should use custom stringify when inserting with JSONEachRow stream", async () => { + let stringifyCalls = 0; const customClient = createTestClient({ json: { parse: JSON.parse, stringify: (value) => { - stringifyCalls++ - return JSON.stringify(value) + stringifyCalls++; + return JSON.stringify(value); }, }, - }) + }); - const stream = makeObjectStream() - stream.push({ id: '42', name: 'foo', sku: [0, 1] }) - stream.push({ id: '43', name: 'bar', sku: [2, 3] }) - setTimeout(() => stream.push(null), 100) + const stream = makeObjectStream(); + stream.push({ id: "42", name: "foo", sku: [0, 1] }); + stream.push({ id: "43", name: "bar", sku: [2, 3] }); + setTimeout(() => stream.push(null), 100); await customClient.insert({ table: tableName, values: stream, - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); - expect(stringifyCalls).toBe(2) + expect(stringifyCalls).toBe(2); const result = await customClient.query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); expect(await result.json()).toEqual([ - { id: '42', name: 'foo', sku: [0, 1] }, - { id: '43', name: 'bar', sku: [2, 3] }, - ]) + { id: "42", name: "foo", sku: [0, 1] }, + { id: "43", name: "bar", sku: [2, 3] }, + ]); - await customClient.close() - }) + await customClient.close(); + }); - it('should use custom stringify when inserting with JSONEachRow array', async () => { - let stringifyCalls = 0 + it("should use custom stringify when inserting with JSONEachRow array", async () => { + let stringifyCalls = 0; const customClient = createTestClient({ json: { parse: JSON.parse, stringify: (value) => { - stringifyCalls++ - return JSON.stringify(value) + stringifyCalls++; + return JSON.stringify(value); }, }, - }) + }); const values = [ - { id: '42', name: 'foo', sku: [0, 1] }, - { id: '43', name: 'bar', sku: [2, 3] }, - ] + { id: "42", name: "foo", sku: [0, 1] }, + { id: "43", name: "bar", sku: [2, 3] }, + ]; await customClient.insert({ table: tableName, values, - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); - expect(stringifyCalls).toBe(2) + expect(stringifyCalls).toBe(2); const result = await customClient.query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, - format: 'JSONEachRow', - }) - expect(await result.json()).toEqual(values) + format: "JSONEachRow", + }); + expect(await result.json()).toEqual(values); - await customClient.close() - }) + await customClient.close(); + }); - it('should use custom parse when querying with JSONEachRow', async () => { - let parseCalls = 0 + it("should use custom parse when querying with JSONEachRow", async () => { + let parseCalls = 0; const customClient = createTestClient({ json: { parse: (text) => { - parseCalls++ - return JSON.parse(text) + parseCalls++; + return JSON.parse(text); }, stringify: JSON.stringify, }, - }) + }); - const stream = makeObjectStream() - stream.push({ id: '42', name: 'foo', sku: [0, 1] }) - stream.push({ id: '43', name: 'bar', sku: [2, 3] }) - setTimeout(() => stream.push(null), 100) + const stream = makeObjectStream(); + stream.push({ id: "42", name: "foo", sku: [0, 1] }); + stream.push({ id: "43", name: "bar", sku: [2, 3] }); + setTimeout(() => stream.push(null), 100); await customClient.insert({ table: tableName, values: stream, - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); - parseCalls = 0 + parseCalls = 0; const result = await customClient.query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, - format: 'JSONEachRow', - }) - await result.json() + format: "JSONEachRow", + }); + await result.json(); - expect(parseCalls).toBeGreaterThan(0) + expect(parseCalls).toBeGreaterThan(0); - await customClient.close() - }) + await customClient.close(); + }); - it('should work with simdjson parser', async () => { + it("should work with simdjson parser", async () => { const customClient = createTestClient({ json: { parse: simdjson.parse, stringify: JSON.stringify, }, - }) + }); - const stream = makeObjectStream() - stream.push({ id: '42', name: 'foo', sku: [0, 1] }) - stream.push({ id: '43', name: 'bar', sku: [2, 3] }) - setTimeout(() => stream.push(null), 100) + const stream = makeObjectStream(); + stream.push({ id: "42", name: "foo", sku: [0, 1] }); + stream.push({ id: "43", name: "bar", sku: [2, 3] }); + setTimeout(() => stream.push(null), 100); await customClient.insert({ table: tableName, values: stream, - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); const result = await customClient.query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); expect(await result.json()).toEqual([ - { id: '42', name: 'foo', sku: [0, 1] }, - { id: '43', name: 'bar', sku: [2, 3] }, - ]) + { id: "42", name: "foo", sku: [0, 1] }, + { id: "43", name: "bar", sku: [2, 3] }, + ]); - await customClient.close() - }) + await customClient.close(); + }); - it('should use custom stringify with JSONCompactEachRow', async () => { - let stringifyCalls = 0 + it("should use custom stringify with JSONCompactEachRow", async () => { + let stringifyCalls = 0; const customClient = createTestClient({ json: { parse: JSON.parse, stringify: (value) => { - stringifyCalls++ - return JSON.stringify(value) + stringifyCalls++; + return JSON.stringify(value); }, }, - }) + }); - const stream = makeObjectStream() - stream.push(['42', 'foo', [0, 1]]) - stream.push(['43', 'bar', [2, 3]]) - setTimeout(() => stream.push(null), 100) + const stream = makeObjectStream(); + stream.push(["42", "foo", [0, 1]]); + stream.push(["43", "bar", [2, 3]]); + setTimeout(() => stream.push(null), 100); await customClient.insert({ table: tableName, values: stream, - format: 'JSONCompactEachRow', - }) + format: "JSONCompactEachRow", + }); - expect(stringifyCalls).toBe(2) + expect(stringifyCalls).toBe(2); const result = await customClient.query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, - format: 'JSONCompactEachRow', - }) + format: "JSONCompactEachRow", + }); expect(await result.json()).toEqual([ - ['42', 'foo', [0, 1]], - ['43', 'bar', [2, 3]], - ]) + ["42", "foo", [0, 1]], + ["43", "bar", [2, 3]], + ]); - await customClient.close() - }) + await customClient.close(); + }); - it('should use custom stringify for data transformation', async () => { + it("should use custom stringify for data transformation", async () => { const customClient = createTestClient({ json: { parse: JSON.parse, stringify: (value) => { - if (typeof value === 'object' && value !== null && 'id' in value) { + if (typeof value === "object" && value !== null && "id" in value) { return JSON.stringify({ ...value, id: String(Number(value.id) * 10), - }) + }); } - return JSON.stringify(value) + return JSON.stringify(value); }, }, - }) + }); const values = [ - { id: '4', name: 'foo', sku: [0, 1] }, - { id: '5', name: 'bar', sku: [2, 3] }, - ] + { id: "4", name: "foo", sku: [0, 1] }, + { id: "5", name: "bar", sku: [2, 3] }, + ]; await customClient.insert({ table: tableName, values, - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); const result = await customClient.query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); expect(await result.json()).toEqual([ - { id: '40', name: 'foo', sku: [0, 1] }, - { id: '50', name: 'bar', sku: [2, 3] }, - ]) - - await customClient.close() - }) - }) -}) + { id: "40", name: "foo", sku: [0, 1] }, + { id: "50", name: "bar", sku: [2, 3] }, + ]); + + await customClient.close(); + }); + }); +}); diff --git a/packages/client-node/__tests__/integration/node_stream_json_insert.test.ts b/packages/client-node/__tests__/integration/node_stream_json_insert.test.ts index 21f88096..afd1a603 100644 --- a/packages/client-node/__tests__/integration/node_stream_json_insert.test.ts +++ b/packages/client-node/__tests__/integration/node_stream_json_insert.test.ts @@ -1,94 +1,94 @@ -import { type ClickHouseClient } from '@clickhouse/client-common' -import { it, beforeEach, afterEach, expect } from 'vitest' -import { createSimpleTable } from '@test/fixtures/simple_table' -import { assertJsonValues, jsonValues } from '@test/fixtures/test_data' -import { createTestClient } from '@test/utils/client' -import { guid } from '@test/utils/guid' -import Stream from 'stream' -import { makeObjectStream } from '../utils/stream' +import { type ClickHouseClient } from "@clickhouse/client-common"; +import { it, beforeEach, afterEach, expect } from "vitest"; +import { createSimpleTable } from "@test/fixtures/simple_table"; +import { assertJsonValues, jsonValues } from "@test/fixtures/test_data"; +import { createTestClient } from "@test/utils/client"; +import { guid } from "@test/utils/guid"; +import Stream from "stream"; +import { makeObjectStream } from "../utils/stream"; -let client: ClickHouseClient -let tableName: string +let client: ClickHouseClient; +let tableName: string; beforeEach(async () => { - client = createTestClient() - tableName = `insert_stream_json_${guid()}` - await createSimpleTable(client, tableName) -}) + client = createTestClient(); + tableName = `insert_stream_json_${guid()}`; + await createSimpleTable(client, tableName); +}); afterEach(async () => { - await client.close() -}) + await client.close(); +}); -it('does not throw if stream closes prematurely', async () => { +it("does not throw if stream closes prematurely", async () => { const stream = new Stream.Readable({ objectMode: true, read() { - this.push(null) // close stream + this.push(null); // close stream }, - }) + }); await expect( client.insert({ table: tableName, values: stream, }), - ).resolves.toBeDefined() -}) + ).resolves.toBeDefined(); +}); -it('waits for stream of values to be closed', async () => { - let closed = false +it("waits for stream of values to be closed", async () => { + let closed = false; const stream = new Stream.Readable({ objectMode: true, read() { setTimeout(() => { - this.push([42, 'hello', [0, 1]]) - this.push([43, 'world', [3, 4]]) - this.push(null) - closed = true - }, 100) + this.push([42, "hello", [0, 1]]); + this.push([43, "world", [3, 4]]); + this.push(null); + closed = true; + }, 100); }, - }) + }); - expect(closed).toBe(false) + expect(closed).toBe(false); await client.insert({ table: tableName, values: stream, - }) - expect(closed).toBe(true) -}) + }); + expect(closed).toBe(true); +}); -it('can insert multiple streams at once', async () => { - const streams: Stream.Readable[] = Array(jsonValues.length) +it("can insert multiple streams at once", async () => { + const streams: Stream.Readable[] = Array(jsonValues.length); const insertStreamPromises = Promise.all( jsonValues.map((value, i) => { - const stream = makeObjectStream() - streams[i] = stream - stream.push(value) + const stream = makeObjectStream(); + streams[i] = stream; + stream.push(value); return client.insert({ values: stream, - format: 'JSONEachRow', + format: "JSONEachRow", table: tableName, - }) + }); }), - ) + ); setTimeout(() => { - streams.forEach((stream) => stream.push(null)) - }, 100) - await insertStreamPromises - await assertJsonValues(client, tableName) -}) + streams.forEach((stream) => stream.push(null)); + }, 100); + await insertStreamPromises; + await assertJsonValues(client, tableName); +}); -it('should throw in case of an invalid format of data', async () => { - const stream = makeObjectStream() - stream.push({ id: 'baz', name: 'foo', sku: '[0,1]' }) - stream.push(null) +it("should throw in case of an invalid format of data", async () => { + const stream = makeObjectStream(); + stream.push({ id: "baz", name: "foo", sku: "[0,1]" }); + stream.push(null); await expect( client.insert({ table: tableName, values: stream, - format: 'JSONEachRow', + format: "JSONEachRow", }), ).rejects.toMatchObject({ - message: expect.stringContaining('Cannot parse input'), - }) -}) + message: expect.stringContaining("Cannot parse input"), + }); +}); diff --git a/packages/client-node/__tests__/integration/node_stream_raw_formats.test.ts b/packages/client-node/__tests__/integration/node_stream_raw_formats.test.ts index d1763a34..c22d8175 100644 --- a/packages/client-node/__tests__/integration/node_stream_raw_formats.test.ts +++ b/packages/client-node/__tests__/integration/node_stream_raw_formats.test.ts @@ -2,349 +2,353 @@ import type { ClickHouseClient, ClickHouseSettings, RawDataFormat, -} from '@clickhouse/client-common' -import { describe, it, beforeEach, afterEach, expect } from 'vitest' -import { createSimpleTable } from '@test/fixtures/simple_table' -import { assertJsonValues, jsonValues } from '@test/fixtures/test_data' -import { createTestClient } from '@test/utils/client' -import { guid } from '@test/utils/guid' -import Stream from 'stream' -import { makeRawStream } from '../utils/stream' +} from "@clickhouse/client-common"; +import { describe, it, beforeEach, afterEach, expect } from "vitest"; +import { createSimpleTable } from "@test/fixtures/simple_table"; +import { assertJsonValues, jsonValues } from "@test/fixtures/test_data"; +import { createTestClient } from "@test/utils/client"; +import { guid } from "@test/utils/guid"; +import Stream from "stream"; +import { makeRawStream } from "../utils/stream"; -describe('[Node.js] stream raw formats', () => { - let client: ClickHouseClient - let tableName: string +describe("[Node.js] stream raw formats", () => { + let client: ClickHouseClient; + let tableName: string; beforeEach(async () => { - tableName = `insert_stream_raw_${guid()}` - client = createTestClient() - await createSimpleTable(client, tableName) - }) + tableName = `insert_stream_raw_${guid()}`; + client = createTestClient(); + await createSimpleTable(client, tableName); + }); afterEach(async () => { - await client.close() - }) + await client.close(); + }); - it('should throw in case of invalid format of data', async () => { + it("should throw in case of invalid format of data", async () => { const stream = Stream.Readable.from( `"baz","foo","[1,2]"\n43,"bar","[3,4]"\n`, { objectMode: false, }, - ) + ); await expect( client.insert({ table: tableName, values: stream, - format: 'CSV', + format: "CSV", }), ).rejects.toMatchObject({ - message: expect.stringContaining('Cannot parse input'), - }) - }) + message: expect.stringContaining("Cannot parse input"), + }); + }); - describe('TSV', () => { - it('should insert a TSV without names or types', async () => { - const values = `42\tfoo\t[1,2]\n43\tbar\t[3,4]\n` + describe("TSV", () => { + it("should insert a TSV without names or types", async () => { + const values = `42\tfoo\t[1,2]\n43\tbar\t[3,4]\n`; const stream = Stream.Readable.from(values, { objectMode: false, - }) + }); await client.insert({ table: tableName, values: stream, - format: 'TabSeparated', - }) - await assertInsertedValues('TabSeparated', values) - }) + format: "TabSeparated", + }); + await assertInsertedValues("TabSeparated", values); + }); - it('should insert a TSV with names', async () => { - const values = `id\tname\tsku\n42\tfoo\t[1,2]\n43\tbar\t[3,4]\n` + it("should insert a TSV with names", async () => { + const values = `id\tname\tsku\n42\tfoo\t[1,2]\n43\tbar\t[3,4]\n`; const stream = Stream.Readable.from(values, { objectMode: false, - }) + }); await client.insert({ table: tableName, values: stream, - format: 'TabSeparatedWithNames', - }) - await assertInsertedValues('TabSeparatedWithNames', values) - }) + format: "TabSeparatedWithNames", + }); + await assertInsertedValues("TabSeparatedWithNames", values); + }); - it('should insert a TSV with names and types', async () => { - const values = `id\tname\tsku\nUInt64\tString\tArray(UInt8)\n42\tfoo\t[1,2]\n43\tbar\t[3,4]\n` + it("should insert a TSV with names and types", async () => { + const values = `id\tname\tsku\nUInt64\tString\tArray(UInt8)\n42\tfoo\t[1,2]\n43\tbar\t[3,4]\n`; const stream = Stream.Readable.from(values, { objectMode: false, - }) + }); await client.insert({ table: tableName, values: stream, - format: 'TabSeparatedWithNamesAndTypes', - }) - await assertInsertedValues('TabSeparatedWithNamesAndTypes', values) - }) + format: "TabSeparatedWithNamesAndTypes", + }); + await assertInsertedValues("TabSeparatedWithNamesAndTypes", values); + }); - it('should insert a TSV (unescaped)', async () => { - const values = `42\t\\bfoo\t[1,2]\n43\tba\\tr\t[3,4]\n` + it("should insert a TSV (unescaped)", async () => { + const values = `42\t\\bfoo\t[1,2]\n43\tba\\tr\t[3,4]\n`; const stream = Stream.Readable.from(values, { objectMode: false, - }) + }); await client.insert({ table: tableName, values: stream, - format: 'TabSeparatedRaw', - }) - await assertInsertedValues('TabSeparatedRaw', values) - }) + format: "TabSeparatedRaw", + }); + await assertInsertedValues("TabSeparatedRaw", values); + }); - it('should throw in case of invalid TSV format', async () => { + it("should throw in case of invalid TSV format", async () => { const stream = Stream.Readable.from(`foobar\t42\n`, { objectMode: false, - }) + }); await expect( client.insert({ table: tableName, values: stream, - format: 'TabSeparated', + format: "TabSeparated", }), ).rejects.toMatchObject({ - message: expect.stringContaining('Cannot parse input'), - }) - }) + message: expect.stringContaining("Cannot parse input"), + }); + }); - it('can insert multiple TSV streams at once', async () => { - const streams: Stream.Readable[] = Array(jsonValues.length) + it("can insert multiple TSV streams at once", async () => { + const streams: Stream.Readable[] = Array(jsonValues.length); const insertStreamPromises = Promise.all( jsonValues.map(({ id, name, sku }, i) => { - const stream = makeRawStream() - streams[i] = stream - stream.push(`${id}\t${name}\t[${sku}]\n`) + const stream = makeRawStream(); + streams[i] = stream; + stream.push(`${id}\t${name}\t[${sku}]\n`); return client.insert({ values: stream, - format: 'TabSeparated', + format: "TabSeparated", table: tableName, - }) + }); }), - ) + ); setTimeout(() => { - streams.forEach((stream) => stream.push(null)) - }, 100) - await insertStreamPromises - await assertJsonValues(client, tableName) - }) - }) + streams.forEach((stream) => stream.push(null)); + }, 100); + await insertStreamPromises; + await assertJsonValues(client, tableName); + }); + }); - describe('CSV', () => { - it('should insert a CSV without names or types', async () => { - const values = `42,"foo","[1,2]"\n43,"bar","[3,4]"\n` + describe("CSV", () => { + it("should insert a CSV without names or types", async () => { + const values = `42,"foo","[1,2]"\n43,"bar","[3,4]"\n`; const stream = Stream.Readable.from(values, { objectMode: false, - }) + }); await client.insert({ table: tableName, values: stream, - format: 'CSV', - }) - await assertInsertedValues('CSV', values) - }) + format: "CSV", + }); + await assertInsertedValues("CSV", values); + }); - it('should insert a CSV with names', async () => { - const values = `"id","name","sku"\n42,"foo","[1,2]"\n43,"bar","[3,4]"\n` + it("should insert a CSV with names", async () => { + const values = `"id","name","sku"\n42,"foo","[1,2]"\n43,"bar","[3,4]"\n`; const stream = Stream.Readable.from(values, { objectMode: false, - }) + }); await client.insert({ table: tableName, values: stream, - format: 'CSVWithNames', - }) - await assertInsertedValues('CSVWithNames', values) - }) + format: "CSVWithNames", + }); + await assertInsertedValues("CSVWithNames", values); + }); - it('should insert a CSV with wrong names', async () => { + it("should insert a CSV with wrong names", async () => { const values = `"foo","name","sku" 42,"foo","[1,2]" 43,"bar","[3,4]" -` +`; const stream = Stream.Readable.from(values, { objectMode: false, - }) + }); await client.insert({ table: tableName, values: stream, - format: 'CSVWithNames', - }) + format: "CSVWithNames", + }); await assertInsertedValues( - 'CSVWithNames', + "CSVWithNames", `"id","name","sku" 0,"foo","[1,2]" 0,"bar","[3,4]" `, - ) - }) + ); + }); - it('should insert a CSV with names and types', async () => { - const values = `"id","name","sku"\n"UInt64","String","Array(UInt8)"\n42,"foo","[1,2]"\n43,"bar","[3,4]"\n` + it("should insert a CSV with names and types", async () => { + const values = `"id","name","sku"\n"UInt64","String","Array(UInt8)"\n42,"foo","[1,2]"\n43,"bar","[3,4]"\n`; const stream = Stream.Readable.from(values, { objectMode: false, - }) + }); await client.insert({ table: tableName, values: stream, - format: 'CSVWithNamesAndTypes', - }) - await assertInsertedValues('CSVWithNamesAndTypes', values) - }) + format: "CSVWithNamesAndTypes", + }); + await assertInsertedValues("CSVWithNamesAndTypes", values); + }); - it('should throw in case of a wrong type in CSV format', async () => { + it("should throw in case of a wrong type in CSV format", async () => { const stream = Stream.Readable.from( `"id","name","sku"\n"UInt64","UInt64","Array(UInt8)"\n42,"foo","[1,2]"\n43,"bar","[3,4]"\n`, { objectMode: false, }, - ) + ); await expect( client.insert({ table: tableName, values: stream, - format: 'CSVWithNamesAndTypes', + format: "CSVWithNamesAndTypes", }), ).rejects.toMatchObject({ message: expect.stringContaining( `Type of 'name' must be String, not UInt64`, ), - }) - }) + }); + }); - it('should throw in case of invalid CSV format', async () => { + it("should throw in case of invalid CSV format", async () => { const stream = Stream.Readable.from(`"foobar","42",,\n`, { objectMode: false, - }) + }); await expect( client.insert({ table: tableName, values: stream, - format: 'CSV', + format: "CSV", }), ).rejects.toMatchObject({ - message: expect.stringContaining('Cannot parse input'), - }) - }) + message: expect.stringContaining("Cannot parse input"), + }); + }); - it('can insert multiple CSV streams at once', async () => { - const streams: Stream.Readable[] = Array(jsonValues.length) + it("can insert multiple CSV streams at once", async () => { + const streams: Stream.Readable[] = Array(jsonValues.length); const insertStreamPromises = Promise.all( jsonValues.map(({ id, name, sku }, i) => { - const stream = makeRawStream() - streams[i] = stream - stream.push(`${id},${name},"${sku}"\n`) + const stream = makeRawStream(); + streams[i] = stream; + stream.push(`${id},${name},"${sku}"\n`); return client.insert({ values: stream, - format: 'CSV', + format: "CSV", table: tableName, - }) + }); }), - ) + ); setTimeout(() => { - streams.forEach((stream) => stream.push(null)) - }, 100) - await insertStreamPromises - await assertJsonValues(client, tableName) - }) - }) + streams.forEach((stream) => stream.push(null)); + }, 100); + await insertStreamPromises; + await assertJsonValues(client, tableName); + }); + }); - describe('Custom separated', () => { + describe("Custom separated", () => { const clickhouse_settings: ClickHouseSettings = { - format_custom_escaping_rule: 'CSV', - format_custom_field_delimiter: '^', - } + format_custom_escaping_rule: "CSV", + format_custom_field_delimiter: "^", + }; - it('should insert a custom separated stream without names or types', async () => { - const values = `42^"foo"^"[1,2]"\n43^"bar"^"[3,4]"\n` + it("should insert a custom separated stream without names or types", async () => { + const values = `42^"foo"^"[1,2]"\n43^"bar"^"[3,4]"\n`; const stream = Stream.Readable.from(values, { objectMode: false, - }) + }); await client.insert({ table: tableName, values: stream, - format: 'CustomSeparated', + format: "CustomSeparated", clickhouse_settings, - }) - await assertInsertedValues('CustomSeparated', values, clickhouse_settings) - }) + }); + await assertInsertedValues( + "CustomSeparated", + values, + clickhouse_settings, + ); + }); - it('should insert a custom separated stream with names', async () => { - const values = `"id"^"name"^"sku"\n42^"foo"^"[1,2]"\n43^"bar"^"[3,4]"\n` + it("should insert a custom separated stream with names", async () => { + const values = `"id"^"name"^"sku"\n42^"foo"^"[1,2]"\n43^"bar"^"[3,4]"\n`; const stream = Stream.Readable.from(values, { objectMode: false, - }) + }); await client.insert({ table: tableName, values: stream, - format: 'CustomSeparatedWithNames', + format: "CustomSeparatedWithNames", clickhouse_settings, - }) + }); await assertInsertedValues( - 'CustomSeparatedWithNames', + "CustomSeparatedWithNames", values, clickhouse_settings, - ) - }) + ); + }); - it('should insert a custom separated stream with names and types', async () => { - const values = `"id"^"name"^"sku"\n"UInt64"^"String"^"Array(UInt8)"\n42^"foo"^"[1,2]"\n43^"bar"^"[3,4]"\n` + it("should insert a custom separated stream with names and types", async () => { + const values = `"id"^"name"^"sku"\n"UInt64"^"String"^"Array(UInt8)"\n42^"foo"^"[1,2]"\n43^"bar"^"[3,4]"\n`; const stream = Stream.Readable.from(values, { objectMode: false, - }) + }); await client.insert({ table: tableName, values: stream, - format: 'CustomSeparatedWithNamesAndTypes', + format: "CustomSeparatedWithNamesAndTypes", clickhouse_settings, - }) + }); await assertInsertedValues( - 'CustomSeparatedWithNamesAndTypes', + "CustomSeparatedWithNamesAndTypes", values, clickhouse_settings, - ) - }) + ); + }); - it('should throw in case of invalid custom separated format', async () => { + it("should throw in case of invalid custom separated format", async () => { const stream = Stream.Readable.from(`"foobar"^"42"^^\n`, { objectMode: false, - }) + }); await expect( client.insert({ table: tableName, values: stream, - format: 'CustomSeparated', + format: "CustomSeparated", clickhouse_settings, }), ).rejects.toMatchObject({ - message: expect.stringContaining('Cannot parse input'), - }) - }) + message: expect.stringContaining("Cannot parse input"), + }); + }); - it('can insert multiple custom-separated streams at once', async () => { - const streams: Stream.Readable[] = Array(jsonValues.length) + it("can insert multiple custom-separated streams at once", async () => { + const streams: Stream.Readable[] = Array(jsonValues.length); const insertStreamPromises = Promise.all( jsonValues.map(({ id, name, sku }, i) => { - const stream = makeRawStream() - streams[i] = stream - stream.push(`${id}^${name}^[${sku}]\n`) + const stream = makeRawStream(); + streams[i] = stream; + stream.push(`${id}^${name}^[${sku}]\n`); return client.insert({ values: stream, - format: 'CustomSeparated', + format: "CustomSeparated", table: tableName, clickhouse_settings, - }) + }); }), - ) + ); setTimeout(() => { - streams.forEach((stream) => stream.push(null)) - }, 100) - await insertStreamPromises - await assertJsonValues(client, tableName) - }) - }) + streams.forEach((stream) => stream.push(null)); + }, 100); + await insertStreamPromises; + await assertJsonValues(client, tableName); + }); + }); async function assertInsertedValues( format: RawDataFormat, @@ -355,7 +359,7 @@ describe('[Node.js] stream raw formats', () => { query: `SELECT * FROM ${tableName} ORDER BY id ASC`, clickhouse_settings, format, - }) - expect(await result.text()).toEqual(expected) + }); + expect(await result.text()).toEqual(expected); } -}) +}); diff --git a/packages/client-node/__tests__/integration/node_stream_row_binary.test.ts b/packages/client-node/__tests__/integration/node_stream_row_binary.test.ts index b59663d0..7a971e1e 100644 --- a/packages/client-node/__tests__/integration/node_stream_row_binary.test.ts +++ b/packages/client-node/__tests__/integration/node_stream_row_binary.test.ts @@ -3,34 +3,34 @@ import { DefaultLogger, LogWriter, type ClickHouseClient, -} from '@clickhouse/client-common' -import { describe, it, beforeEach, afterEach, expect } from 'vitest' -import { createSimpleTable } from '@test/fixtures/simple_table' -import { createTestClient } from '@test/utils/client' -import { guid } from '@test/utils/guid' -import Stream from 'stream' -import { drainStreamInternal } from '../../src/connection/stream' +} from "@clickhouse/client-common"; +import { describe, it, beforeEach, afterEach, expect } from "vitest"; +import { createSimpleTable } from "@test/fixtures/simple_table"; +import { createTestClient } from "@test/utils/client"; +import { guid } from "@test/utils/guid"; +import Stream from "stream"; +import { drainStreamInternal } from "../../src/connection/stream"; -describe('[Node.js] stream RowBinary insert', () => { - let client: ClickHouseClient - let tableName: string - let log_writer: LogWriter +describe("[Node.js] stream RowBinary insert", () => { + let client: ClickHouseClient; + let tableName: string; + let log_writer: LogWriter; beforeEach(async () => { - client = createTestClient() + client = createTestClient(); log_writer = new LogWriter( new DefaultLogger(), - 'Connection', + "Connection", ClickHouseLogLevel.OFF, - ) - tableName = `test_node_row_binary_stream_${guid()}` - await createSimpleTable(client, tableName) - }) + ); + tableName = `test_node_row_binary_stream_${guid()}`; + await createSimpleTable(client, tableName); + }); afterEach(async () => { - await client.close() - }) + await client.close(); + }); - it('should send a RowBinary payload via a stream passed into the request body', async () => { + it("should send a RowBinary payload via a stream passed into the request body", async () => { // Schema: id UInt64, name String, sku Array(UInt8) // RowBinary encoding: // UInt64 -> 8 bytes little-endian @@ -38,69 +38,69 @@ describe('[Node.js] stream RowBinary insert', () => { // Array(T) -> varint length prefix + items const row1 = Buffer.concat([ uint64LE(42n), - varString('foo'), + varString("foo"), varUInt8Array([1, 2]), - ]) + ]); const row2 = Buffer.concat([ uint64LE(43n), - varString('bar'), + varString("bar"), varUInt8Array([3, 4]), - ]) + ]); // Provide the payload via a Readable stream split across multiple chunks // to exercise the streaming code path on the request body. - const stream = Stream.Readable.from([row1, row2], { objectMode: false }) + const stream = Stream.Readable.from([row1, row2], { objectMode: false }); const execResult = await client.exec({ query: `INSERT INTO ${tableName} FORMAT RowBinary`, values: stream, - }) + }); // The result stream contains nothing useful for an insert and should be // immediately drained to release the socket. await drainStreamInternal( { - op: 'Insert', + op: "Insert", query_id: execResult.query_id, log_writer, log_level: ClickHouseLogLevel.OFF, }, execResult.stream, - ) + ); const rs = await client.query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); expect(await rs.json()).toEqual([ - { id: '42', name: 'foo', sku: [1, 2] }, - { id: '43', name: 'bar', sku: [3, 4] }, - ]) - }) -}) + { id: "42", name: "foo", sku: [1, 2] }, + { id: "43", name: "bar", sku: [3, 4] }, + ]); + }); +}); function uint64LE(value: bigint): Buffer { - const buf = Buffer.alloc(8) - buf.writeBigUInt64LE(value) - return buf + const buf = Buffer.alloc(8); + buf.writeBigUInt64LE(value); + return buf; } // LEB128 unsigned varint, used by ClickHouse for length prefixes in RowBinary. function varUInt(value: number): Buffer { - const bytes: number[] = [] - let v = value + const bytes: number[] = []; + let v = value; while (v >= 0x80) { - bytes.push((v & 0x7f) | 0x80) - v >>>= 7 + bytes.push((v & 0x7f) | 0x80); + v >>>= 7; } - bytes.push(v & 0x7f) - return Buffer.from(bytes) + bytes.push(v & 0x7f); + return Buffer.from(bytes); } function varString(value: string): Buffer { - const data = Buffer.from(value, 'utf8') - return Buffer.concat([varUInt(data.length), data]) + const data = Buffer.from(value, "utf8"); + return Buffer.concat([varUInt(data.length), data]); } function varUInt8Array(values: number[]): Buffer { - return Buffer.concat([varUInt(values.length), Buffer.from(values)]) + return Buffer.concat([varUInt(values.length), Buffer.from(values)]); } diff --git a/packages/client-node/__tests__/integration/node_stream_row_binary_select.test.ts b/packages/client-node/__tests__/integration/node_stream_row_binary_select.test.ts index af2bc638..2267f32f 100644 --- a/packages/client-node/__tests__/integration/node_stream_row_binary_select.test.ts +++ b/packages/client-node/__tests__/integration/node_stream_row_binary_select.test.ts @@ -1,105 +1,105 @@ -import type { ClickHouseClient } from '@clickhouse/client-common' -import { describe, it, beforeEach, afterEach, expect } from 'vitest' -import { createSimpleTable } from '@test/fixtures/simple_table' -import { createTestClient } from '@test/utils/client' -import { guid } from '@test/utils/guid' -import type Stream from 'stream' +import type { ClickHouseClient } from "@clickhouse/client-common"; +import { describe, it, beforeEach, afterEach, expect } from "vitest"; +import { createSimpleTable } from "@test/fixtures/simple_table"; +import { createTestClient } from "@test/utils/client"; +import { guid } from "@test/utils/guid"; +import type Stream from "stream"; -describe('[Node.js] stream RowBinary select', () => { - let client: ClickHouseClient - let tableName: string +describe("[Node.js] stream RowBinary select", () => { + let client: ClickHouseClient; + let tableName: string; beforeEach(async () => { - client = createTestClient() - tableName = `test_node_row_binary_select_stream_${guid()}` - await createSimpleTable(client, tableName) + client = createTestClient(); + tableName = `test_node_row_binary_select_stream_${guid()}`; + await createSimpleTable(client, tableName); await client.insert({ table: tableName, values: [ - { id: '42', name: 'foo', sku: [1, 2] }, - { id: '43', name: 'bar', sku: [3, 4] }, + { id: "42", name: "foo", sku: [1, 2] }, + { id: "43", name: "bar", sku: [3, 4] }, ], - format: 'JSONEachRow', - }) - }) + format: "JSONEachRow", + }); + }); afterEach(async () => { - await client.close() - }) + await client.close(); + }); - it('should read a RowBinary payload from the response stream', async () => { + it("should read a RowBinary payload from the response stream", async () => { // Schema: id UInt64, name String, sku Array(UInt8) const { stream } = await client.exec({ query: `SELECT * FROM ${tableName} ORDER BY id ASC FORMAT RowBinary`, - }) + }); - const chunks: Buffer[] = [] + const chunks: Buffer[] = []; for await (const chunk of stream) { - chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); } - const payload = Buffer.concat(chunks) + const payload = Buffer.concat(chunks); // RowBinary decoding: // UInt64 -> 8 bytes little-endian // String -> varint length prefix + UTF-8 bytes // Array(T) -> varint length prefix + items - const reader = new BufferReader(payload) - const rows: Array<{ id: string; name: string; sku: number[] }> = [] + const reader = new BufferReader(payload); + const rows: Array<{ id: string; name: string; sku: number[] }> = []; while (!reader.eof()) { - const id = reader.readUInt64LE().toString() - const name = reader.readString() - const sku = reader.readUInt8Array() - rows.push({ id, name, sku }) + const id = reader.readUInt64LE().toString(); + const name = reader.readString(); + const sku = reader.readUInt8Array(); + rows.push({ id, name, sku }); } - expect(reader.eof()).toBe(true) + expect(reader.eof()).toBe(true); expect(rows).toEqual([ - { id: '42', name: 'foo', sku: [1, 2] }, - { id: '43', name: 'bar', sku: [3, 4] }, - ]) - }) -}) + { id: "42", name: "foo", sku: [1, 2] }, + { id: "43", name: "bar", sku: [3, 4] }, + ]); + }); +}); class BufferReader { - private offset = 0 - private readonly buf: Buffer + private offset = 0; + private readonly buf: Buffer; constructor(buf: Buffer) { - this.buf = buf + this.buf = buf; } eof(): boolean { - return this.offset >= this.buf.length + return this.offset >= this.buf.length; } readUInt64LE(): bigint { - const value = this.buf.readBigUInt64LE(this.offset) - this.offset += 8 - return value + const value = this.buf.readBigUInt64LE(this.offset); + this.offset += 8; + return value; } // LEB128 unsigned varint, used by ClickHouse for length prefixes in RowBinary. readVarUInt(): number { - let value = 0 - let shift = 0 + let value = 0; + let shift = 0; while (true) { - const byte = this.buf[this.offset++] - value |= (byte & 0x7f) << shift + const byte = this.buf[this.offset++]; + value |= (byte & 0x7f) << shift; if ((byte & 0x80) === 0) { - return value >>> 0 + return value >>> 0; } - shift += 7 + shift += 7; } } readString(): string { - const length = this.readVarUInt() - const value = this.buf.toString('utf8', this.offset, this.offset + length) - this.offset += length - return value + const length = this.readVarUInt(); + const value = this.buf.toString("utf8", this.offset, this.offset + length); + this.offset += length; + return value; } readUInt8Array(): number[] { - const length = this.readVarUInt() - const slice = this.buf.subarray(this.offset, this.offset + length) - this.offset += length - return Array.from(slice) + const length = this.readVarUInt(); + const slice = this.buf.subarray(this.offset, this.offset + length); + this.offset += length; + return Array.from(slice); } } diff --git a/packages/client-node/__tests__/integration/node_streaming_e2e.test.ts b/packages/client-node/__tests__/integration/node_streaming_e2e.test.ts index 233681f9..3e6819e7 100644 --- a/packages/client-node/__tests__/integration/node_streaming_e2e.test.ts +++ b/packages/client-node/__tests__/integration/node_streaming_e2e.test.ts @@ -1,100 +1,100 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import type { Row } from '@clickhouse/client-common' +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import type { Row } from "@clickhouse/client-common"; import { type ClickHouseClient, type ClickHouseSettings, -} from '@clickhouse/client-common' -import { createSimpleTable } from '@test/fixtures/simple_table' -import { createTestClient } from '@test/utils/client' -import { guid } from '@test/utils/guid' -import { genLargeStringsDataset } from '@test/utils/datasets' -import { tableFromIPC } from 'apache-arrow' -import { Buffer } from 'buffer' -import Fs from 'fs' +} from "@clickhouse/client-common"; +import { createSimpleTable } from "@test/fixtures/simple_table"; +import { createTestClient } from "@test/utils/client"; +import { guid } from "@test/utils/guid"; +import { genLargeStringsDataset } from "@test/utils/datasets"; +import { tableFromIPC } from "apache-arrow"; +import { Buffer } from "buffer"; +import Fs from "fs"; // Not working out of the box with ESM. See out package.json for the workaround. // Also, see https://github.com/kylebarron/parquet-wasm/issues/798 -import { readParquet } from 'parquet-wasm/node' -import split from 'split2' -import Stream from 'stream' +import { readParquet } from "parquet-wasm/node"; +import split from "split2"; +import Stream from "stream"; -describe('[Node.js] streaming e2e', () => { - let tableName: string - let client: ClickHouseClient +describe("[Node.js] streaming e2e", () => { + let tableName: string; + let client: ClickHouseClient; beforeEach(async () => { - client = createTestClient() + client = createTestClient(); - tableName = `streaming_e2e_test_${guid()}` - await createSimpleTable(client, tableName) - }) + tableName = `streaming_e2e_test_${guid()}`; + await createSimpleTable(client, tableName); + }); afterEach(async () => { - await client.close() - }) + await client.close(); + }); const expected: Array> = [ - ['0', 'a', [1, 2]], - ['1', 'b', [3, 4]], - ['2', 'c', [5, 6]], - ] + ["0", "a", [1, 2]], + ["1", "b", [3, 4]], + ["2", "c", [5, 6]], + ]; - it('should stream an NDJSON file', async () => { + it("should stream an NDJSON file", async () => { // contains id as numbers in JSONCompactEachRow format ["0"]\n["1"]\n... const filename = - 'packages/client-common/__tests__/fixtures/streaming_e2e_data.ndjson' + "packages/client-common/__tests__/fixtures/streaming_e2e_data.ndjson"; await client.insert({ table: tableName, values: Fs.createReadStream(filename).pipe( // should be removed when "insert" accepts a stream of strings/bytes split((row: string) => JSON.parse(row)), ), - format: 'JSONCompactEachRow', - }) + format: "JSONCompactEachRow", + }); const rs = await client.query({ query: `SELECT * from ${tableName}`, - format: 'JSONCompactEachRow', - }) + format: "JSONCompactEachRow", + }); - const actual: unknown[] = [] + const actual: unknown[] = []; for await (const rows of rs.stream()) { rows.forEach((row: Row) => { - actual.push(row.json()) - }) + actual.push(row.json()); + }); } - expect(actual).toEqual(expected) - }) + expect(actual).toEqual(expected); + }); - it('should stream a Parquet file', async () => { + it("should stream a Parquet file", async () => { const streamParquetSettings: ClickHouseSettings = { - output_format_parquet_compression_method: 'none', - output_format_parquet_version: '2.6', + output_format_parquet_compression_method: "none", + output_format_parquet_version: "2.6", // 24.3+ has this enabled by default; prior versions need this setting to be enforced for consistent assertions // Otherwise, the string type for Parquet will be Binary (24.3+) vs Utf8 (24.3-). // https://github.com/ClickHouse/ClickHouse/pull/61817/files#diff-aa3c979016a9f8c6ab5a51560411afa3f4cef55d34c899a2b1e7aff38aca4076R1097 output_format_parquet_string_as_string: 1, - } + }; const filename = - 'packages/client-common/__tests__/fixtures/streaming_e2e_data.parquet' + "packages/client-common/__tests__/fixtures/streaming_e2e_data.parquet"; await client.insert({ table: tableName, values: Fs.createReadStream(filename), - format: 'Parquet', - }) + format: "Parquet", + }); // check that the data was inserted correctly const rs = await client.query({ query: `SELECT * from ${tableName}`, - format: 'JSONCompactEachRow', - }) + format: "JSONCompactEachRow", + }); - const actual: unknown[] = [] + const actual: unknown[] = []; for await (const rows of rs.stream()) { rows.forEach((row: Row) => { - actual.push(row.json()) - }) + actual.push(row.json()); + }); } - expect(actual).toEqual(expected) + expect(actual).toEqual(expected); // check if we can stream it back and get the output matching the input file const stream = await client @@ -102,131 +102,131 @@ describe('[Node.js] streaming e2e', () => { query: `SELECT * from ${tableName} FORMAT Parquet`, clickhouse_settings: streamParquetSettings, }) - .then((r) => r.stream) + .then((r) => r.stream); - const parquetChunks: Buffer[] = [] + const parquetChunks: Buffer[] = []; for await (const chunk of stream) { - parquetChunks.push(chunk) + parquetChunks.push(chunk); } const table = tableFromIPC( readParquet(Buffer.concat(parquetChunks)).intoIPCStream(), - ) + ); expect(table.schema.toString()).toEqual( - 'Schema<{ 0: id: Uint64, 1: name: Utf8, 2: sku: List }>', - ) - const actualParquetData: unknown[] = [] + "Schema<{ 0: id: Uint64, 1: name: Utf8, 2: sku: List }>", + ); + const actualParquetData: unknown[] = []; table.toArray().map((v) => { - const row: Record = {} - row['id'] = v.id - row['name'] = v.name - row['sku'] = Array.from(v.sku.toArray()) // Vector -> UInt8Array -> Array - actualParquetData.push(row) - }) + const row: Record = {}; + row["id"] = v.id; + row["name"] = v.name; + row["sku"] = Array.from(v.sku.toArray()); // Vector -> UInt8Array -> Array + actualParquetData.push(row); + }); expect(actualParquetData).toEqual([ - { id: 0n, name: 'a', sku: [1, 2] }, - { id: 1n, name: 'b', sku: [3, 4] }, - { id: 2n, name: 'c', sku: [5, 6] }, - ]) - }) + { id: 0n, name: "a", sku: [1, 2] }, + { id: 1n, name: "b", sku: [3, 4] }, + { id: 2n, name: "c", sku: [5, 6] }, + ]); + }); - it('should stream a stream created in-place', async () => { + it("should stream a stream created in-place", async () => { await client.insert({ table: tableName, values: Stream.Readable.from(expected), - format: 'JSONCompactEachRow', - }) + format: "JSONCompactEachRow", + }); const rs = await client.query({ query: `SELECT * from ${tableName}`, - format: 'JSONCompactEachRow', - }) + format: "JSONCompactEachRow", + }); - const actual: unknown[] = [] + const actual: unknown[] = []; for await (const rows of rs.stream()) { rows.forEach((row: Row) => { - actual.push(row.json()) - }) + actual.push(row.json()); + }); } - expect(actual).toEqual(expected) - }) + expect(actual).toEqual(expected); + }); // See https://github.com/ClickHouse/clickhouse-js/issues/171 for more details // Here we generate a large enough dataset to break into multiple chunks while streaming, // effectively testing the implementation of incomplete rows handling - describe('should correctly process multiple chunks', () => { - describe('large amount of rows', () => { - it('should work with .json()', async () => { + describe("should correctly process multiple chunks", () => { + describe("large amount of rows", () => { + it("should work with .json()", async () => { const { table, values } = await genLargeStringsDataset(client, { rows: 10000, words: 10, - }) + }); const result = await client .query({ query: `SELECT * FROM ${table} ORDER BY id ASC`, - format: 'JSONEachRow', + format: "JSONEachRow", }) - .then((r) => r.json()) - expect(result).toEqual(values) - }) + .then((r) => r.json()); + expect(result).toEqual(values); + }); - it('should work with .stream()', async () => { + it("should work with .stream()", async () => { const { table, values } = await genLargeStringsDataset(client, { rows: 10000, words: 10, - }) + }); const stream = await client .query({ query: `SELECT * FROM ${table} ORDER BY id ASC`, - format: 'JSONEachRow', + format: "JSONEachRow", }) - .then((r) => r.stream()) + .then((r) => r.stream()); - const result = [] + const result = []; for await (const rows of stream) { for (const row of rows) { - result.push(await row.json()) + result.push(await row.json()); } } - expect(result).toEqual(values) - }) - }) + expect(result).toEqual(values); + }); + }); describe("rows that don't fit into a single chunk", () => { - it('should work with .json()', async () => { + it("should work with .json()", async () => { const { table, values } = await genLargeStringsDataset(client, { rows: 5, words: 10000, - }) + }); const result = await client .query({ query: `SELECT * FROM ${table} ORDER BY id ASC`, - format: 'JSONEachRow', + format: "JSONEachRow", }) - .then((r) => r.json()) - expect(result).toEqual(values) - }) + .then((r) => r.json()); + expect(result).toEqual(values); + }); - it('should work with .stream()', async () => { + it("should work with .stream()", async () => { const { table, values } = await genLargeStringsDataset(client, { rows: 5, words: 10000, - }) + }); const stream = await client .query({ query: `SELECT * FROM ${table} ORDER BY id ASC`, - format: 'JSONEachRow', + format: "JSONEachRow", }) - .then((r) => r.stream()) + .then((r) => r.stream()); - const result = [] + const result = []; for await (const rows of stream) { for (const row of rows) { - result.push(await row.json()) + result.push(await row.json()); } } - expect(result).toEqual(values) - }) - }) - }) -}) + expect(result).toEqual(values); + }); + }); + }); +}); diff --git a/packages/client-node/__tests__/integration/node_summary.test.ts b/packages/client-node/__tests__/integration/node_summary.test.ts index 632303f5..68516cc4 100644 --- a/packages/client-node/__tests__/integration/node_summary.test.ts +++ b/packages/client-node/__tests__/integration/node_summary.test.ts @@ -1,36 +1,36 @@ -import { describe, it, expect, beforeAll, afterAll } from 'vitest' -import type { ClickHouseClient } from '@clickhouse/client-common' -import { createSimpleTable } from '@test/fixtures/simple_table' -import { jsonValues } from '@test/fixtures/test_data' -import { createTestClient } from '@test/utils/client' -import { guid } from '@test/utils/guid' -import { TestEnv, isOnEnv } from '@test/utils/test_env' -import type Stream from 'stream' +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import type { ClickHouseClient } from "@clickhouse/client-common"; +import { createSimpleTable } from "@test/fixtures/simple_table"; +import { jsonValues } from "@test/fixtures/test_data"; +import { createTestClient } from "@test/utils/client"; +import { guid } from "@test/utils/guid"; +import { TestEnv, isOnEnv } from "@test/utils/test_env"; +import type Stream from "stream"; // FIXME: figure out if we can get non-flaky assertion with an SMT Cloud instance. // It could be that it requires full quorum settings for non-flaky assertions. // SharedMergeTree Cloud instance is auto by default (and cannot be modified). describe.skipIf(!isOnEnv(TestEnv.LocalSingleNode, TestEnv.LocalCluster))( - '[Node.js] Summary header parsing', + "[Node.js] Summary header parsing", () => { - let client: ClickHouseClient - let tableName: string + let client: ClickHouseClient; + let tableName: string; beforeAll(async () => { - client = createTestClient() - tableName = `summary_test_${guid()}` - await createSimpleTable(client, tableName) - }) + client = createTestClient(); + tableName = `summary_test_${guid()}`; + await createSimpleTable(client, tableName); + }); afterAll(async () => { - await client.close() - }) + await client.close(); + }); - it('should provide summary for insert/exec', async () => { + it("should provide summary for insert/exec", async () => { const { summary: insertSummary } = await client.insert({ table: tableName, values: jsonValues, - format: 'JSONEachRow', - }) + format: "JSONEachRow", + }); expect(insertSummary).toEqual( expect.objectContaining({ read_rows: expect.any(String), @@ -41,13 +41,13 @@ describe.skipIf(!isOnEnv(TestEnv.LocalSingleNode, TestEnv.LocalCluster))( result_bytes: expect.any(String), elapsed_ns: expect.any(String), }), - ) + ); const { summary: execSummary } = await client.exec({ query: `INSERT INTO ${tableName} SELECT * FROM ${tableName}`, - }) + }); expect(execSummary).toEqual( expect.objectContaining({ read_rows: expect.any(String), @@ -58,10 +58,10 @@ describe.skipIf(!isOnEnv(TestEnv.LocalSingleNode, TestEnv.LocalCluster))( result_bytes: expect.any(String), elapsed_ns: expect.any(String), }), - ) - }) + ); + }); - it('should provide summary for command', async () => { + it("should provide summary for command", async () => { const { summary } = await client.command({ query: `INSERT INTO ${tableName} VALUES (144, 'Hello', [2, 4]), @@ -69,7 +69,7 @@ describe.skipIf(!isOnEnv(TestEnv.LocalSingleNode, TestEnv.LocalCluster))( clickhouse_settings: { wait_end_of_query: 1, }, - }) + }); expect(summary).toEqual( expect.objectContaining({ read_rows: expect.any(String), @@ -80,7 +80,7 @@ describe.skipIf(!isOnEnv(TestEnv.LocalSingleNode, TestEnv.LocalCluster))( result_bytes: expect.any(String), elapsed_ns: expect.any(String), }), - ) - }) + ); + }); }, -) +); diff --git a/packages/client-node/__tests__/tls/tls.test.ts b/packages/client-node/__tests__/tls/tls.test.ts index ade5c78b..254d18f6 100644 --- a/packages/client-node/__tests__/tls/tls.test.ts +++ b/packages/client-node/__tests__/tls/tls.test.ts @@ -1,192 +1,192 @@ -import { it, expect, describe, beforeEach, afterEach } from 'vitest' -import type { ClickHouseClient } from '@clickhouse/client-common' -import { createTestClient } from '@test/utils/client' -import * as fs from 'fs' -import Http from 'http' -import https from 'node:https' -import type Stream from 'stream' -import { createClient } from '../../src' -import Https from 'https' -import http from 'http' -import { vi } from 'vitest' +import { it, expect, describe, beforeEach, afterEach } from "vitest"; +import type { ClickHouseClient } from "@clickhouse/client-common"; +import { createTestClient } from "@test/utils/client"; +import * as fs from "fs"; +import Http from "http"; +import https from "node:https"; +import type Stream from "stream"; +import { createClient } from "../../src"; +import Https from "https"; +import http from "http"; +import { vi } from "vitest"; -describe('[Node.js] TLS connection', () => { - let client: ClickHouseClient +describe("[Node.js] TLS connection", () => { + let client: ClickHouseClient; beforeEach(() => { - client = createTestClient() - }) + client = createTestClient(); + }); afterEach(async () => { - await client.close() - }) + await client.close(); + }); - const certsPath = '.docker/clickhouse/single_node_tls/certificates' - const ca_cert = fs.readFileSync(`${certsPath}/ca.crt`) - const cert = fs.readFileSync(`${certsPath}/client.crt`) - const key = fs.readFileSync(`${certsPath}/client.key`) + const certsPath = ".docker/clickhouse/single_node_tls/certificates"; + const ca_cert = fs.readFileSync(`${certsPath}/ca.crt`); + const cert = fs.readFileSync(`${certsPath}/client.crt`); + const key = fs.readFileSync(`${certsPath}/client.key`); - it('should work with basic TLS', async () => { + it("should work with basic TLS", async () => { client = createClient({ - url: 'https://server.clickhouseconnect.test:8443', + url: "https://server.clickhouseconnect.test:8443", tls: { ca_cert, }, - }) + }); const resultSet = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 3', - format: 'CSV', - }) - expect(await resultSet.text()).toEqual('0\n1\n2\n') - }) + query: "SELECT number FROM system.numbers LIMIT 3", + format: "CSV", + }); + expect(await resultSet.text()).toEqual("0\n1\n2\n"); + }); - it('should work with mutual TLS', async () => { + it("should work with mutual TLS", async () => { client = createClient({ - url: 'https://server.clickhouseconnect.test:8443', - username: 'cert_user', + url: "https://server.clickhouseconnect.test:8443", + username: "cert_user", tls: { ca_cert, cert, key, }, - }) + }); const resultSet = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 3', - format: 'CSV', - }) - expect(await resultSet.text()).toEqual('0\n1\n2\n') - }) + query: "SELECT number FROM system.numbers LIMIT 3", + format: "CSV", + }); + expect(await resultSet.text()).toEqual("0\n1\n2\n"); + }); - it('should fail when hostname does not match', async () => { + it("should fail when hostname does not match", async () => { client = createClient({ - url: 'https://localhost:8443', - username: 'cert_user', + url: "https://localhost:8443", + username: "cert_user", tls: { ca_cert, cert, key, }, - }) + }); await expect( client.query({ - query: 'SELECT number FROM system.numbers LIMIT 3', - format: 'CSV', + query: "SELECT number FROM system.numbers LIMIT 3", + format: "CSV", }), ).rejects.toThrow( expect.objectContaining({ message: expect.stringContaining( - 'Hostname/IP does not match certificate', + "Hostname/IP does not match certificate", ), }), - ) - }) + ); + }); - it('should fail with invalid certificates', async () => { + it("should fail with invalid certificates", async () => { client = createClient({ - url: 'https://server.clickhouseconnect.test:8443', - username: 'cert_user', + url: "https://server.clickhouseconnect.test:8443", + username: "cert_user", tls: { ca_cert, cert: fs.readFileSync(`${certsPath}/server.crt`), key: fs.readFileSync(`${certsPath}/server.key`), }, - }) + }); // FIXME: add proper error message matching (does not work on Node.js 18/20) await expect( client.query({ - query: 'SELECT number FROM system.numbers LIMIT 3', - format: 'CSV', + query: "SELECT number FROM system.numbers LIMIT 3", + format: "CSV", }), - ).rejects.toThrow() - }) + ).rejects.toThrow(); + }); // query only; the rest of the methods are tested in the auth.test.ts in the common package - describe('request auth override', () => { - it('should override the credentials with basic TLS', async () => { + describe("request auth override", () => { + it("should override the credentials with basic TLS", async () => { client = createClient({ - url: 'https://server.clickhouseconnect.test:8443', - username: 'gibberish', - password: 'gibberish', + url: "https://server.clickhouseconnect.test:8443", + username: "gibberish", + password: "gibberish", tls: { ca_cert, }, - }) + }); const resultSet = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 3', - format: 'CSV', + query: "SELECT number FROM system.numbers LIMIT 3", + format: "CSV", auth: { - username: 'default', - password: '', + username: "default", + password: "", }, - }) - expect(await resultSet.text()).toEqual('0\n1\n2\n') - }) + }); + expect(await resultSet.text()).toEqual("0\n1\n2\n"); + }); - it('should override the credentials with mutual TLS', async () => { + it("should override the credentials with mutual TLS", async () => { client = createClient({ - url: 'https://server.clickhouseconnect.test:8443', - username: 'gibberish', - password: 'gibberish', + url: "https://server.clickhouseconnect.test:8443", + username: "gibberish", + password: "gibberish", tls: { ca_cert, cert, key, }, - }) + }); const resultSet = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 3', - format: 'CSV', + query: "SELECT number FROM system.numbers LIMIT 3", + format: "CSV", auth: { - username: 'cert_user', - password: '', + username: "cert_user", + password: "", }, - }) - expect(await resultSet.text()).toEqual('0\n1\n2\n') - }) + }); + expect(await resultSet.text()).toEqual("0\n1\n2\n"); + }); - describe('Custom HTTPS agent', () => { - it('should work with a custom HTTPS agent', async () => { - const httpsRequestStub = vi.spyOn(Https, 'request') + describe("Custom HTTPS agent", () => { + it("should work with a custom HTTPS agent", async () => { + const httpsRequestStub = vi.spyOn(Https, "request"); const agent = new https.Agent({ maxFreeSockets: 5, ca: ca_cert, - }) + }); const client = createClient({ - url: 'https://server.clickhouseconnect.test:8443', + url: "https://server.clickhouseconnect.test:8443", http_agent: agent, http_headers: { - 'X-ClickHouse-User': 'default', - 'X-ClickHouse-Key': '', + "X-ClickHouse-User": "default", + "X-ClickHouse-Key": "", }, set_basic_auth_header: false, - }) + }); const rs = await client.query({ - query: 'SELECT 144 AS result', - format: 'JSONEachRow', - }) - expect(await rs.json()).toEqual([{ result: 144 }]) - expect(httpsRequestStub).toHaveBeenCalledTimes(1) - const callArgs = httpsRequestStub.mock.calls[0] - expect(callArgs[1].agent).toBe(agent) - }) + query: "SELECT 144 AS result", + format: "JSONEachRow", + }); + expect(await rs.json()).toEqual([{ result: 144 }]); + expect(httpsRequestStub).toHaveBeenCalledTimes(1); + const callArgs = httpsRequestStub.mock.calls[0]; + expect(callArgs[1].agent).toBe(agent); + }); // does not really belong to the TLS test; keep it here for consistency - it('should work with a custom HTTP agent', async () => { - const httpRequestStub = vi.spyOn(Http, 'request') + it("should work with a custom HTTP agent", async () => { + const httpRequestStub = vi.spyOn(Http, "request"); const agent = new http.Agent({ maxFreeSockets: 5, - }) + }); const client = createClient({ - url: 'http://localhost:8123', + url: "http://localhost:8123", http_agent: agent, - }) + }); const rs = await client.query({ - query: 'SELECT 144 AS result', - format: 'JSONEachRow', - }) - expect(await rs.json()).toEqual([{ result: 144 }]) - expect(httpRequestStub).toHaveBeenCalledTimes(1) - const callArgs = httpRequestStub.mock.calls[0] - expect(callArgs[1].agent).toBe(agent) - }) - }) - }) -}) + query: "SELECT 144 AS result", + format: "JSONEachRow", + }); + expect(await rs.json()).toEqual([{ result: 144 }]); + expect(httpRequestStub).toHaveBeenCalledTimes(1); + const callArgs = httpRequestStub.mock.calls[0]; + expect(callArgs[1].agent).toBe(agent); + }); + }); + }); +}); diff --git a/packages/client-node/__tests__/unit/node_client.test.ts b/packages/client-node/__tests__/unit/node_client.test.ts index e10bf937..01317078 100644 --- a/packages/client-node/__tests__/unit/node_client.test.ts +++ b/packages/client-node/__tests__/unit/node_client.test.ts @@ -1,95 +1,95 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest' +import { describe, it, expect, beforeEach, vi } from "vitest"; import type { BaseClickHouseClientConfigOptions, ConnectionParams, -} from '@clickhouse/client-common' +} from "@clickhouse/client-common"; import { DefaultLogger, LogWriter, ClickHouseLogLevel, -} from '@clickhouse/client-common' -import { createClient } from '../../src' +} from "@clickhouse/client-common"; +import { createClient } from "../../src"; import { type CreateConnectionParams, NodeConnectionFactory, -} from '../../src/connection' -import { sleep } from '../utils/sleep' -import { isAwaitUsingStatementSupported } from '../utils/feature_detection' -import { createSimpleNodeTestClient } from '../utils/simple_node_client' +} 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 () => { +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() - }) + const client = createSimpleNodeTestClient(); + expect(client).toBeDefined(); + await client.close(); + }); it('throws on incorrect "url" config value', () => { - expect(() => createClient({ url: 'foobar' })).toThrow( + expect(() => createClient({ url: "foobar" })).toThrow( expect.objectContaining({ - message: expect.stringContaining('ClickHouse URL is malformed.'), + message: expect.stringContaining("ClickHouse URL is malformed."), }), - ) - }) + ); + }); - it('should not mutate provided configuration', async () => { + it("should not mutate provided configuration", async () => { const config: BaseClickHouseClientConfigOptions = { - url: 'https://localhost:8443', - } - createClient(config) + url: "https://localhost:8443", + }; + createClient(config); // initial configuration is not overridden by the defaults we assign // when we transform the specified config object to the connection params expect(config).toEqual({ - url: 'https://localhost:8443', - }) - }) + url: "https://localhost:8443", + }); + }); - describe('URL parameters parsing', () => { + describe("URL parameters parsing", () => { const params: ConnectionParams = { - url: new URL('https://my.host:8443'), + url: new URL("https://my.host:8443"), request_timeout: 42_000, max_open_connections: 10, compression: { compress_request: false, decompress_response: false, }, - auth: { username: 'bob', password: 'secret', type: 'Credentials' }, - database: 'analytics', + auth: { username: "bob", password: "secret", type: "Credentials" }, + database: "analytics", clickhouse_settings: {}, log_writer: new LogWriter( new DefaultLogger(), - 'Connection', + "Connection", ClickHouseLogLevel.WARN, ), log_level: ClickHouseLogLevel.WARN, keep_alive: { enabled: true }, http_headers: { - 'X-ClickHouse-Auth': 'secret_token', + "X-ClickHouse-Auth": "secret_token", }, - application_id: 'my_app', - } + application_id: "my_app", + }; - const createConnectionStub = vi.spyOn(NodeConnectionFactory, 'create') + const createConnectionStub = vi.spyOn(NodeConnectionFactory, "create"); beforeEach(() => { - vi.clearAllMocks() - }) + vi.clearAllMocks(); + }); - it('should parse URL parameters and create a valid connection', async () => { + it("should parse URL parameters and create a valid connection", async () => { createClient({ url: - 'https://bob:secret@my.host:8443/analytics?' + + "https://bob:secret@my.host:8443/analytics?" + [ // base config parameters - 'application=my_app', - 'request_timeout=42000', - 'http_header_X-ClickHouse-Auth=secret_token', + "application=my_app", + "request_timeout=42000", + "http_header_X-ClickHouse-Auth=secret_token", // Node.js specific - 'keep_alive_idle_socket_ttl=1500', - ].join('&'), - }) + "keep_alive_idle_socket_ttl=1500", + ].join("&"), + }); expect(createConnectionStub).toHaveBeenCalledWith({ connection_params: { ...params, @@ -107,28 +107,28 @@ describe('[Node.js] createClient', () => { set_basic_auth_header: true, http_agent: undefined, capture_enhanced_stack_trace: false, - } satisfies CreateConnectionParams) - expect(createConnectionStub).toHaveBeenCalledTimes(1) - }) + } satisfies CreateConnectionParams); + expect(createConnectionStub).toHaveBeenCalledTimes(1); + }); - it('should parse pathname and db from the URL and create a valid connection', async () => { + it("should parse pathname and db from the URL and create a valid connection", async () => { createClient({ url: - 'https://bob:secret@my.host:8443/analytics?' + + "https://bob:secret@my.host:8443/analytics?" + [ // base config parameters - 'application=my_app', - 'pathname=my_proxy', - 'request_timeout=42000', - 'http_header_X-ClickHouse-Auth=secret_token', + "application=my_app", + "pathname=my_proxy", + "request_timeout=42000", + "http_header_X-ClickHouse-Auth=secret_token", // Node.js specific - 'keep_alive_idle_socket_ttl=1500', - ].join('&'), - }) + "keep_alive_idle_socket_ttl=1500", + ].join("&"), + }); expect(createConnectionStub).toHaveBeenCalledWith({ connection_params: { ...params, - url: new URL('https://my.host:8443/my_proxy'), + url: new URL("https://my.host:8443/my_proxy"), json: { parse: JSON.parse, stringify: JSON.stringify, @@ -143,32 +143,32 @@ describe('[Node.js] createClient', () => { set_basic_auth_header: true, http_agent: undefined, capture_enhanced_stack_trace: false, - } satisfies CreateConnectionParams) - expect(createConnectionStub).toHaveBeenCalledTimes(1) - }) + } satisfies CreateConnectionParams); + expect(createConnectionStub).toHaveBeenCalledTimes(1); + }); - it('should parse username and password with special characters', async () => { - const username = '! $' - const password = '(#%%@) ' - const auth = `${encodeURIComponent(username)}:${encodeURIComponent(password)}` + it("should parse username and password with special characters", async () => { + const username = "! $"; + const password = "(#%%@) "; + const auth = `${encodeURIComponent(username)}:${encodeURIComponent(password)}`; createClient({ url: `https://${auth}@my.host:8443/analytics?` + [ // base config parameters - 'application=my_app', - 'pathname=my_proxy', - 'request_timeout=42000', - 'http_header_X-ClickHouse-Auth=secret_token', + "application=my_app", + "pathname=my_proxy", + "request_timeout=42000", + "http_header_X-ClickHouse-Auth=secret_token", // Node.js specific - 'keep_alive_idle_socket_ttl=1500', - ].join('&'), - }) + "keep_alive_idle_socket_ttl=1500", + ].join("&"), + }); expect(createConnectionStub).toHaveBeenCalledWith({ connection_params: { ...params, - url: new URL('https://my.host:8443/my_proxy'), - auth: { username, password, type: 'Credentials' }, + url: new URL("https://my.host:8443/my_proxy"), + auth: { username, password, type: "Credentials" }, json: { parse: JSON.parse, stringify: JSON.stringify, @@ -183,23 +183,23 @@ describe('[Node.js] createClient', () => { set_basic_auth_header: true, http_agent: undefined, capture_enhanced_stack_trace: false, - } satisfies CreateConnectionParams) - expect(createConnectionStub).toHaveBeenCalledTimes(1) - }) - }) + } satisfies CreateConnectionParams); + expect(createConnectionStub).toHaveBeenCalledTimes(1); + }); + }); - it('closes the client when used with using statement', async (context) => { + it("closes the client when used with using statement", async (context) => { if (!isAwaitUsingStatementSupported()) { - context.skip('using statement is not supported in this environment') - return + context.skip("using statement is not supported in this environment"); + return; } - const client = createClient() - let isClosed = false - vi.spyOn(client, 'close').mockImplementation(async () => { + const client = createClient(); + let isClosed = false; + vi.spyOn(client, "close").mockImplementation(async () => { // Simulate some delay in closing - await sleep(0) - isClosed = true - }) + await sleep(0); + isClosed = true; + }); // Wrap in eval to allow using statement syntax without // syntax error in older Node.js versions. Might want to @@ -209,8 +209,8 @@ describe('[Node.js] createClient', () => { await using c = value; // do nothing, just testing the disposal at the end of the block }) - `)(client) + `)(client); - expect(isClosed).toBe(true) - }) -}) + expect(isClosed).toBe(true); + }); +}); diff --git a/packages/client-node/__tests__/unit/node_client_query.test.ts b/packages/client-node/__tests__/unit/node_client_query.test.ts index b8da919b..7ca7bf66 100644 --- a/packages/client-node/__tests__/unit/node_client_query.test.ts +++ b/packages/client-node/__tests__/unit/node_client_query.test.ts @@ -1,102 +1,102 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest' -import Http from 'http' -import { NodeClickHouseClient } from '../../src/client' -import { NodeConfigImpl } from '../../src/config' -import { emitResponseBody, stubClientRequest } from '../utils/http_stubs' +import { describe, it, expect, beforeEach, vi } from "vitest"; +import Http from "http"; +import { NodeClickHouseClient } from "../../src/client"; +import { NodeConfigImpl } from "../../src/config"; +import { emitResponseBody, stubClientRequest } from "../utils/http_stubs"; const UUID_REGEX = - /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i + /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i; beforeEach(() => { - vi.clearAllMocks() -}) + vi.clearAllMocks(); +}); -describe('[Node.js] NodeClickHouseClient query method', () => { - it('should call query method and return ResultSet', async () => { +describe("[Node.js] NodeClickHouseClient query method", () => { + it("should call query method and return ResultSet", async () => { // Create a client instance using the internal constructor const client = new NodeClickHouseClient({ impl: NodeConfigImpl, - url: 'http://localhost:8123', - }) + url: "http://localhost:8123", + }); // Mock the underlying HTTP request - const httpRequestStub = vi.spyOn(Http, 'request') - const request = stubClientRequest() - httpRequestStub.mockReturnValue(request) + const httpRequestStub = vi.spyOn(Http, "request"); + const request = stubClientRequest(); + httpRequestStub.mockReturnValue(request); // Start a query const queryPromise = client.query({ - query: 'SELECT number FROM system.numbers LIMIT 1', - format: 'JSONEachRow', - }) + query: "SELECT number FROM system.numbers LIMIT 1", + format: "JSONEachRow", + }); // Emit a response - const responseBody = JSON.stringify({ number: '0' }) + '\n' - await emitResponseBody(request, responseBody) + const responseBody = JSON.stringify({ number: "0" }) + "\n"; + await emitResponseBody(request, responseBody); // Wait for the query to complete - const result = await queryPromise + const result = await queryPromise; // Verify the result is a ResultSet - expect(result).toBeDefined() - expect(result.query_id).toMatch(UUID_REGEX) + expect(result).toBeDefined(); + expect(result.query_id).toMatch(UUID_REGEX); // Verify the stream can be consumed - const text = await result.text() + const text = await result.text(); expect(text).toMatchInlineSnapshot(` "{"number":"0"} " - `) + `); // Close the client - await client.close() - }) + await client.close(); + }); - it('should handle query with different format parameter', async () => { + it("should handle query with different format parameter", async () => { const client = new NodeClickHouseClient({ impl: NodeConfigImpl, - url: 'http://localhost:8123', - }) + url: "http://localhost:8123", + }); - const httpRequestStub = vi.spyOn(Http, 'request') - const request = stubClientRequest() - httpRequestStub.mockReturnValue(request) + const httpRequestStub = vi.spyOn(Http, "request"); + const request = stubClientRequest(); + httpRequestStub.mockReturnValue(request); const queryPromise = client.query({ - query: 'SELECT 1', - format: 'CSV', - }) + query: "SELECT 1", + format: "CSV", + }); - const responseBody = '1\n' - await emitResponseBody(request, responseBody) + const responseBody = "1\n"; + await emitResponseBody(request, responseBody); - const result = await queryPromise - expect(result).toBeDefined() + const result = await queryPromise; + expect(result).toBeDefined(); - const text = await result.text() - expect(text).toMatchInlineSnapshot(`"1\n"`) + const text = await result.text(); + expect(text).toMatchInlineSnapshot(`"1\n"`); - await client.close() - }) + await client.close(); + }); - it('should maintain type safety with query format', async () => { + it("should maintain type safety with query format", async () => { const client = new NodeClickHouseClient({ impl: NodeConfigImpl, - url: 'http://localhost:8123', - }) + url: "http://localhost:8123", + }); - const httpRequestStub = vi.spyOn(Http, 'request') - const request = stubClientRequest() - httpRequestStub.mockReturnValue(request) + const httpRequestStub = vi.spyOn(Http, "request"); + const request = stubClientRequest(); + httpRequestStub.mockReturnValue(request); // Test with JSON format (default) const queryPromise = client.query({ - query: 'SELECT 42 as answer', - format: 'JSON', - }) + query: "SELECT 42 as answer", + format: "JSON", + }); const responseBody = JSON.stringify({ - meta: [{ name: 'answer', type: 'UInt8' }], + meta: [{ name: "answer", type: "UInt8" }], data: [{ answer: 42 }], rows: 1, statistics: { @@ -104,14 +104,14 @@ describe('[Node.js] NodeClickHouseClient query method', () => { rows_read: 1, bytes_read: 1, }, - }) - await emitResponseBody(request, responseBody) + }); + await emitResponseBody(request, responseBody); - const result = await queryPromise - expect(result).toBeDefined() + const result = await queryPromise; + expect(result).toBeDefined(); // Verify we can get JSON response - const json = await result.json() + const json = await result.json(); expect(json).toMatchInlineSnapshot(` { "data": [ @@ -132,8 +132,8 @@ describe('[Node.js] NodeClickHouseClient query method', () => { "rows_read": 1, }, } - `) + `); - await client.close() - }) -}) + await client.close(); + }); +}); diff --git a/packages/client-node/__tests__/unit/node_config.test.ts b/packages/client-node/__tests__/unit/node_config.test.ts index d4c23e56..02e4c3fd 100644 --- a/packages/client-node/__tests__/unit/node_config.test.ts +++ b/packages/client-node/__tests__/unit/node_config.test.ts @@ -1,72 +1,72 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest' +import { describe, it, expect, beforeEach, vi } from "vitest"; import type { BaseClickHouseClientConfigOptions, ConnectionParams, -} from '@clickhouse/client-common' -import { ClickHouseLogLevel, LogWriter } from '@clickhouse/client-common' -import { TestLogger } from '../../../client-common/__tests__/utils/test_logger' -import { Buffer } from 'buffer' -import http from 'http' -import type { NodeClickHouseClientConfigOptions } from '../../src/config' -import { NodeConfigImpl } from '../../src/config' +} from "@clickhouse/client-common"; +import { ClickHouseLogLevel, LogWriter } from "@clickhouse/client-common"; +import { TestLogger } from "../../../client-common/__tests__/utils/test_logger"; +import { Buffer } from "buffer"; +import http from "http"; +import type { NodeClickHouseClientConfigOptions } from "../../src/config"; +import { NodeConfigImpl } from "../../src/config"; import { type CreateConnectionParams, type NodeBaseConnection, NodeConnectionFactory, -} from '../../src/connection' +} from "../../src/connection"; -describe('[Node.js] Config implementation details', () => { - describe('HandleImplSpecificURLParams', () => { - it('should handle known URL params', async () => { +describe("[Node.js] Config implementation details", () => { + describe("HandleImplSpecificURLParams", () => { + it("should handle known URL params", async () => { const url = new URL( - 'http://localhost:8123/?' + - ['keep_alive_idle_socket_ttl=2500'].join('&'), - ) + "http://localhost:8123/?" + + ["keep_alive_idle_socket_ttl=2500"].join("&"), + ); const config: BaseClickHouseClientConfigOptions = { keep_alive: { enabled: false, }, - } - const res = NodeConfigImpl.handle_specific_url_params(config, url) + }; + const res = NodeConfigImpl.handle_specific_url_params(config, url); expect(res.config).toEqual({ keep_alive: { enabled: false, // kept the value from the initial config idle_socket_ttl: 2500, }, - } as unknown as BaseClickHouseClientConfigOptions) - expect([...res.unknown_params]).toEqual([]) - expect([...res.handled_params]).toEqual(['keep_alive_idle_socket_ttl']) - }) + } as unknown as BaseClickHouseClientConfigOptions); + expect([...res.unknown_params]).toEqual([]); + expect([...res.handled_params]).toEqual(["keep_alive_idle_socket_ttl"]); + }); - it('should indicate that one of the URL parameters is unknown without throwing an error', async () => { - const url = new URL('http://localhost:8123?unknown_param=true') + it("should indicate that one of the URL parameters is unknown without throwing an error", async () => { + const url = new URL("http://localhost:8123?unknown_param=true"); const config: BaseClickHouseClientConfigOptions = { - username: 'alice', - } - const res = NodeConfigImpl.handle_specific_url_params(config, url) - expect(res.config).toEqual({ username: 'alice' }) - expect([...res.unknown_params]).toEqual(['unknown_param']) - expect([...res.handled_params]).toEqual([]) - }) + username: "alice", + }; + const res = NodeConfigImpl.handle_specific_url_params(config, url); + expect(res.config).toEqual({ username: "alice" }); + expect([...res.unknown_params]).toEqual(["unknown_param"]); + expect([...res.handled_params]).toEqual([]); + }); - it('should do nothing if there are no parameters to parse', async () => { - const url = new URL('http://localhost:8123') + it("should do nothing if there are no parameters to parse", async () => { + const url = new URL("http://localhost:8123"); const config: BaseClickHouseClientConfigOptions = { - application: 'my_app', - } - const res = NodeConfigImpl.handle_specific_url_params(config, url) + application: "my_app", + }; + const res = NodeConfigImpl.handle_specific_url_params(config, url); expect(res.config).toEqual({ - application: 'my_app', - } as unknown as BaseClickHouseClientConfigOptions) - expect([...res.unknown_params]).toEqual([]) - expect([...res.handled_params]).toEqual([]) - }) - }) + application: "my_app", + } as unknown as BaseClickHouseClientConfigOptions); + expect([...res.unknown_params]).toEqual([]); + expect([...res.handled_params]).toEqual([]); + }); + }); - describe('MakeConnection', () => { + describe("MakeConnection", () => { const params: ConnectionParams = { - url: new URL('http://localhost:8123'), + url: new URL("http://localhost:8123"), request_timeout: 1000, max_open_connections: 10, compression: { @@ -74,34 +74,34 @@ describe('[Node.js] Config implementation details', () => { decompress_response: true, }, auth: { - username: 'alice', - password: 'qwerty', - type: 'Credentials', + username: "alice", + password: "qwerty", + type: "Credentials", }, - database: 'default', + database: "default", clickhouse_settings: {}, log_writer: new LogWriter( new TestLogger(), - 'MakeConnectionTest', + "MakeConnectionTest", ClickHouseLogLevel.OFF, ), log_level: ClickHouseLogLevel.OFF, keep_alive: { enabled: false }, - } + }; - const fakeConnection = { test: true } as unknown as NodeBaseConnection + const fakeConnection = { test: true } as unknown as NodeBaseConnection; const createConnectionStub = vi - .spyOn(NodeConnectionFactory, 'create') - .mockReturnValue(fakeConnection) + .spyOn(NodeConnectionFactory, "create") + .mockReturnValue(fakeConnection); beforeEach(() => { - vi.clearAllMocks() - }) + vi.clearAllMocks(); + }); - it('should create a connection with default KeepAlive settings', async () => { + it("should create a connection with default KeepAlive settings", async () => { const nodeConfig: NodeClickHouseClientConfigOptions = { - url: new URL('http://localhost:8123'), - } - const res = NodeConfigImpl.make_connection(nodeConfig as any, params) + url: new URL("http://localhost:8123"), + }; + const res = NodeConfigImpl.make_connection(nodeConfig as any, params); expect(createConnectionStub).toHaveBeenCalledWith({ connection_params: params, tls: undefined, @@ -113,24 +113,24 @@ describe('[Node.js] Config implementation details', () => { set_basic_auth_header: true, capture_enhanced_stack_trace: false, eagerly_destroy_stale_sockets: false, - } satisfies CreateConnectionParams) - expect(createConnectionStub).toHaveBeenCalledTimes(1) - expect(res).toEqual(fakeConnection) - }) + } satisfies CreateConnectionParams); + expect(createConnectionStub).toHaveBeenCalledTimes(1); + expect(res).toEqual(fakeConnection); + }); - it('should create a connection with basic TLS', async () => { + it("should create a connection with basic TLS", async () => { const nodeConfig: NodeClickHouseClientConfigOptions = { - url: new URL('https://localhost:8123'), + url: new URL("https://localhost:8123"), tls: { - ca_cert: Buffer.from('my_ca_cert'), + ca_cert: Buffer.from("my_ca_cert"), }, - } - const res = NodeConfigImpl.make_connection(nodeConfig as any, params) + }; + const res = NodeConfigImpl.make_connection(nodeConfig as any, params); expect(createConnectionStub).toHaveBeenCalledWith({ connection_params: params, tls: { - type: 'Basic', - ca_cert: Buffer.from('my_ca_cert'), + type: "Basic", + ca_cert: Buffer.from("my_ca_cert"), }, keep_alive: { enabled: true, @@ -140,28 +140,28 @@ describe('[Node.js] Config implementation details', () => { set_basic_auth_header: true, capture_enhanced_stack_trace: false, eagerly_destroy_stale_sockets: false, - } satisfies CreateConnectionParams) - expect(createConnectionStub).toHaveBeenCalledTimes(1) - expect(res).toEqual(fakeConnection) - }) + } satisfies CreateConnectionParams); + expect(createConnectionStub).toHaveBeenCalledTimes(1); + expect(res).toEqual(fakeConnection); + }); - it('should create a connection with mutual TLS', async () => { + it("should create a connection with mutual TLS", async () => { const nodeConfig: NodeClickHouseClientConfigOptions = { - url: new URL('https://localhost:8123'), + url: new URL("https://localhost:8123"), tls: { - ca_cert: Buffer.from('my_ca_cert'), - cert: Buffer.from('my_cert'), - key: Buffer.from('my_key'), + ca_cert: Buffer.from("my_ca_cert"), + cert: Buffer.from("my_cert"), + key: Buffer.from("my_key"), }, - } - const res = NodeConfigImpl.make_connection(nodeConfig as any, params) + }; + const res = NodeConfigImpl.make_connection(nodeConfig as any, params); expect(createConnectionStub).toHaveBeenCalledWith({ connection_params: params, tls: { - type: 'Mutual', - ca_cert: Buffer.from('my_ca_cert'), - cert: Buffer.from('my_cert'), - key: Buffer.from('my_key'), + type: "Mutual", + ca_cert: Buffer.from("my_ca_cert"), + cert: Buffer.from("my_cert"), + key: Buffer.from("my_key"), }, keep_alive: { enabled: true, @@ -171,28 +171,28 @@ describe('[Node.js] Config implementation details', () => { set_basic_auth_header: true, capture_enhanced_stack_trace: false, eagerly_destroy_stale_sockets: false, - } satisfies CreateConnectionParams) - expect(createConnectionStub).toHaveBeenCalledTimes(1) - expect(res).toEqual(fakeConnection) - }) + } satisfies CreateConnectionParams); + expect(createConnectionStub).toHaveBeenCalledTimes(1); + expect(res).toEqual(fakeConnection); + }); - it('should create a connection with custom KeepAlive and TLS', async () => { + it("should create a connection with custom KeepAlive and TLS", async () => { const nodeConfig: NodeClickHouseClientConfigOptions = { - url: new URL('https://localhost:8123'), + url: new URL("https://localhost:8123"), keep_alive: { enabled: false, idle_socket_ttl: 42_000, }, tls: { - ca_cert: Buffer.from('my_ca_cert'), + ca_cert: Buffer.from("my_ca_cert"), }, - } - const res = NodeConfigImpl.make_connection(nodeConfig as any, params) + }; + const res = NodeConfigImpl.make_connection(nodeConfig as any, params); expect(createConnectionStub).toHaveBeenCalledWith({ connection_params: params, tls: { - type: 'Basic', - ca_cert: Buffer.from('my_ca_cert'), + type: "Basic", + ca_cert: Buffer.from("my_ca_cert"), }, keep_alive: { enabled: false, @@ -202,25 +202,25 @@ describe('[Node.js] Config implementation details', () => { set_basic_auth_header: true, capture_enhanced_stack_trace: false, eagerly_destroy_stale_sockets: false, - } satisfies CreateConnectionParams) - expect(createConnectionStub).toHaveBeenCalledTimes(1) - expect(res).toEqual(fakeConnection) - }) + } satisfies CreateConnectionParams); + expect(createConnectionStub).toHaveBeenCalledTimes(1); + expect(res).toEqual(fakeConnection); + }); - it('should create a connection with a custom agent and disabled auth header', async () => { + it("should create a connection with a custom agent and disabled auth header", async () => { const agent = new http.Agent({ keepAlive: true, maxSockets: 2, - }) + }); const nodeConfig: NodeClickHouseClientConfigOptions = { - url: new URL('https://localhost:8123'), + url: new URL("https://localhost:8123"), keep_alive: { enabled: true, }, set_basic_auth_header: false, http_agent: agent, - } - const res = NodeConfigImpl.make_connection(nodeConfig as any, params) + }; + const res = NodeConfigImpl.make_connection(nodeConfig as any, params); expect(createConnectionStub).toHaveBeenCalledWith({ connection_params: params, tls: undefined, @@ -232,17 +232,17 @@ describe('[Node.js] Config implementation details', () => { set_basic_auth_header: false, capture_enhanced_stack_trace: false, eagerly_destroy_stale_sockets: false, - } satisfies CreateConnectionParams) - expect(createConnectionStub).toHaveBeenCalledTimes(1) - expect(res).toEqual(fakeConnection) - }) + } satisfies CreateConnectionParams); + expect(createConnectionStub).toHaveBeenCalledTimes(1); + expect(res).toEqual(fakeConnection); + }); - it('should create a connection with enhanced stack traces option', async () => { + it("should create a connection with enhanced stack traces option", async () => { const nodeConfig: NodeClickHouseClientConfigOptions = { - url: new URL('https://localhost:8123'), + url: new URL("https://localhost:8123"), capture_enhanced_stack_trace: true, - } - const res = NodeConfigImpl.make_connection(nodeConfig as any, params) + }; + const res = NodeConfigImpl.make_connection(nodeConfig as any, params); expect(createConnectionStub).toHaveBeenCalledWith({ connection_params: params, tls: undefined, @@ -254,19 +254,19 @@ describe('[Node.js] Config implementation details', () => { set_basic_auth_header: true, capture_enhanced_stack_trace: true, eagerly_destroy_stale_sockets: false, - } satisfies CreateConnectionParams) - expect(createConnectionStub).toHaveBeenCalledTimes(1) - expect(res).toEqual(fakeConnection) - }) + } satisfies CreateConnectionParams); + expect(createConnectionStub).toHaveBeenCalledTimes(1); + expect(res).toEqual(fakeConnection); + }); - it('should create a connection with eagerly_destroy_stale_sockets enabled', async () => { + it("should create a connection with eagerly_destroy_stale_sockets enabled", async () => { const nodeConfig: NodeClickHouseClientConfigOptions = { - url: new URL('http://localhost:8123'), + url: new URL("http://localhost:8123"), keep_alive: { eagerly_destroy_stale_sockets: true, }, - } - const res = NodeConfigImpl.make_connection(nodeConfig as any, params) + }; + const res = NodeConfigImpl.make_connection(nodeConfig as any, params); expect(createConnectionStub).toHaveBeenCalledWith({ connection_params: params, tls: undefined, @@ -278,17 +278,17 @@ describe('[Node.js] Config implementation details', () => { set_basic_auth_header: true, capture_enhanced_stack_trace: false, eagerly_destroy_stale_sockets: true, - } satisfies CreateConnectionParams) - expect(createConnectionStub).toHaveBeenCalledTimes(1) - expect(res).toEqual(fakeConnection) - }) + } satisfies CreateConnectionParams); + expect(createConnectionStub).toHaveBeenCalledTimes(1); + expect(res).toEqual(fakeConnection); + }); - it('should forward max_response_headers_size to the connection factory', async () => { + it("should forward max_response_headers_size to the connection factory", async () => { const nodeConfig: NodeClickHouseClientConfigOptions = { - url: new URL('http://localhost:8123'), + url: new URL("http://localhost:8123"), max_response_headers_size: 64 * 1024, - } - const res = NodeConfigImpl.make_connection(nodeConfig as any, params) + }; + const res = NodeConfigImpl.make_connection(nodeConfig as any, params); expect(createConnectionStub).toHaveBeenCalledWith({ connection_params: params, tls: undefined, @@ -301,9 +301,9 @@ describe('[Node.js] Config implementation details', () => { capture_enhanced_stack_trace: false, eagerly_destroy_stale_sockets: false, max_response_headers_size: 64 * 1024, - } satisfies CreateConnectionParams) - expect(createConnectionStub).toHaveBeenCalledTimes(1) - expect(res).toEqual(fakeConnection) - }) - }) -}) + } satisfies CreateConnectionParams); + expect(createConnectionStub).toHaveBeenCalledTimes(1); + expect(res).toEqual(fakeConnection); + }); + }); +}); diff --git a/packages/client-node/__tests__/unit/node_connection.test.ts b/packages/client-node/__tests__/unit/node_connection.test.ts index 53902e52..7ca89df9 100644 --- a/packages/client-node/__tests__/unit/node_connection.test.ts +++ b/packages/client-node/__tests__/unit/node_connection.test.ts @@ -1,398 +1,404 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest' +import { describe, it, expect, beforeEach, vi } from "vitest"; -import type { QueryParams } from '@clickhouse/client-common' -import { guid } from '../../../client-common/__tests__/utils/guid' -import Http from 'http' -import { getAsText } from '../../src/utils' -import { assertQueryId, assertConnQueryResult } from '../utils/assert' +import type { QueryParams } from "@clickhouse/client-common"; +import { guid } from "../../../client-common/__tests__/utils/guid"; +import Http from "http"; +import { getAsText } from "../../src/utils"; +import { assertQueryId, assertConnQueryResult } from "../utils/assert"; import { buildHttpConnection, emitResponseBody, MyTestHttpConnection, stubClientRequest, -} from '../utils/http_stubs' +} from "../utils/http_stubs"; beforeEach(() => { - vi.clearAllMocks() -}) - -describe('[Node.js] Connection', () => { - describe('User-Agent', () => { - it('should have proper user agent without app id', async () => { - const myHttpAdapter = new MyTestHttpConnection() - const headers = myHttpAdapter.getDefaultHeaders() - expect(headers['User-Agent']).toMatch( + vi.clearAllMocks(); +}); + +describe("[Node.js] Connection", () => { + describe("User-Agent", () => { + it("should have proper user agent without app id", async () => { + const myHttpAdapter = new MyTestHttpConnection(); + const headers = myHttpAdapter.getDefaultHeaders(); + expect(headers["User-Agent"]).toMatch( /^clickhouse-js\/[0-9\\.]+-?(?:(alpha|beta)\.\d*)? \(lv:nodejs\/v[0-9\\.]+?; os:(?:linux|darwin|win32)\)$/, - ) - }) + ); + }); - it('should have proper user agent with app id', async () => { - const myHttpAdapter = new MyTestHttpConnection('MyFancyApp') - const headers = myHttpAdapter.getDefaultHeaders() - expect(headers['User-Agent']).toMatch( + it("should have proper user agent with app id", async () => { + const myHttpAdapter = new MyTestHttpConnection("MyFancyApp"); + const headers = myHttpAdapter.getDefaultHeaders(); + expect(headers["User-Agent"]).toMatch( /^MyFancyApp clickhouse-js\/[0-9\\.]+-?(?:(alpha|beta)\.\d*)? \(lv:nodejs\/v[0-9\\.]+?; os:(?:linux|darwin|win32)\)$/, - ) - }) - }) - - it('should have proper auth header', async () => { - const myHttpAdapter = new MyTestHttpConnection() - const headers = myHttpAdapter.getDefaultHeaders() - expect(headers['Authorization']).toMatch(/^Basic [A-Za-z0-9/+=]+$/) - }) - - describe('query_id', () => { - it('should generate random query_id for each query', async () => { + ); + }); + }); + + it("should have proper auth header", async () => { + const myHttpAdapter = new MyTestHttpConnection(); + const headers = myHttpAdapter.getDefaultHeaders(); + expect(headers["Authorization"]).toMatch(/^Basic [A-Za-z0-9/+=]+$/); + }); + + describe("query_id", () => { + it("should generate random query_id for each query", async () => { const adapter = buildHttpConnection({ compression: { decompress_response: false, compress_request: false, }, - }) + }); - const httpRequestStub = vi.spyOn(Http, 'request') + const httpRequestStub = vi.spyOn(Http, "request"); - const request1 = stubClientRequest() - httpRequestStub.mockReturnValue(request1) + const request1 = stubClientRequest(); + httpRequestStub.mockReturnValue(request1); const selectPromise1 = adapter.query({ - query: 'SELECT * FROM system.numbers LIMIT 5', - }) - const responseBody1 = 'foobar' - await emitResponseBody(request1, responseBody1) - const queryResult1 = await selectPromise1 + query: "SELECT * FROM system.numbers LIMIT 5", + }); + const responseBody1 = "foobar"; + await emitResponseBody(request1, responseBody1); + const queryResult1 = await selectPromise1; - const request2 = stubClientRequest() - httpRequestStub.mockReturnValue(request2) + const request2 = stubClientRequest(); + httpRequestStub.mockReturnValue(request2); const selectPromise2 = adapter.query({ - query: 'SELECT * FROM system.numbers LIMIT 5', - }) - const responseBody2 = 'qaz' - await emitResponseBody(request2, responseBody2) - const queryResult2 = await selectPromise2 + query: "SELECT * FROM system.numbers LIMIT 5", + }); + const responseBody2 = "qaz"; + await emitResponseBody(request2, responseBody2); + const queryResult2 = await selectPromise2; - await assertConnQueryResult(queryResult1, responseBody1) - await assertConnQueryResult(queryResult2, responseBody2) - expect(queryResult1.query_id).not.toEqual(queryResult2.query_id) + await assertConnQueryResult(queryResult1, responseBody1); + await assertConnQueryResult(queryResult2, responseBody2); + expect(queryResult1.query_id).not.toEqual(queryResult2.query_id); - const url1 = httpRequestStub.mock.calls[0][0] - expect(url1.search).toContain(`?query_id=${queryResult1.query_id}`) + const url1 = httpRequestStub.mock.calls[0][0]; + expect(url1.search).toContain(`?query_id=${queryResult1.query_id}`); - const url2 = httpRequestStub.mock.calls[1][0] - expect(url2.search).toContain(`?query_id=${queryResult2.query_id}`) - }) + const url2 = httpRequestStub.mock.calls[1][0]; + expect(url2.search).toContain(`?query_id=${queryResult2.query_id}`); + }); - it('should use provided query_id for query', async () => { + it("should use provided query_id for query", async () => { const adapter = buildHttpConnection({ compression: { decompress_response: false, compress_request: false, }, - }) + }); - const request = stubClientRequest() - const httpRequestStub = vi.spyOn(Http, 'request').mockReturnValue(request) + const request = stubClientRequest(); + const httpRequestStub = vi + .spyOn(Http, "request") + .mockReturnValue(request); - const query_id = guid() + const query_id = guid(); const selectPromise = adapter.query({ - query: 'SELECT * FROM system.numbers LIMIT 5', + query: "SELECT * FROM system.numbers LIMIT 5", query_id, - }) - const responseBody = 'foobar' - await emitResponseBody(request, responseBody) - const { stream } = await selectPromise - expect(await getAsText(stream)).toBe(responseBody) + }); + const responseBody = "foobar"; + await emitResponseBody(request, responseBody); + const { stream } = await selectPromise; + expect(await getAsText(stream)).toBe(responseBody); - expect(httpRequestStub).toHaveBeenCalledTimes(1) + expect(httpRequestStub).toHaveBeenCalledTimes(1); const [url] = - httpRequestStub.mock.calls[httpRequestStub.mock.calls.length - 1] - expect(url.search).toContain(`?query_id=${query_id}`) - }) + httpRequestStub.mock.calls[httpRequestStub.mock.calls.length - 1]; + expect(url.search).toContain(`?query_id=${query_id}`); + }); - it('should generate random query_id for every exec request', async () => { + it("should generate random query_id for every exec request", async () => { const adapter = buildHttpConnection({ compression: { decompress_response: false, compress_request: false, }, - }) + }); - const httpRequestStub = vi.spyOn(Http, 'request') + const httpRequestStub = vi.spyOn(Http, "request"); - const request1 = stubClientRequest() - httpRequestStub.mockReturnValue(request1) + const request1 = stubClientRequest(); + httpRequestStub.mockReturnValue(request1); const execPromise1 = adapter.exec({ - query: 'SELECT * FROM system.numbers LIMIT 5', - }) - const responseBody1 = 'foobar' - await emitResponseBody(request1, responseBody1) - const queryResult1 = await execPromise1 + query: "SELECT * FROM system.numbers LIMIT 5", + }); + const responseBody1 = "foobar"; + await emitResponseBody(request1, responseBody1); + const queryResult1 = await execPromise1; - const request2 = stubClientRequest() - httpRequestStub.mockReturnValue(request2) + const request2 = stubClientRequest(); + httpRequestStub.mockReturnValue(request2); const execPromise2 = adapter.exec({ - query: 'SELECT * FROM system.numbers LIMIT 5', - }) - const responseBody2 = 'qaz' - await emitResponseBody(request2, responseBody2) - const queryResult2 = await execPromise2 + query: "SELECT * FROM system.numbers LIMIT 5", + }); + const responseBody2 = "qaz"; + await emitResponseBody(request2, responseBody2); + const queryResult2 = await execPromise2; - await assertConnQueryResult(queryResult1, responseBody1) - await assertConnQueryResult(queryResult2, responseBody2) - expect(queryResult1.query_id).not.toEqual(queryResult2.query_id) + await assertConnQueryResult(queryResult1, responseBody1); + await assertConnQueryResult(queryResult2, responseBody2); + expect(queryResult1.query_id).not.toEqual(queryResult2.query_id); - const [url1] = httpRequestStub.mock.calls[0] + const [url1] = httpRequestStub.mock.calls[0]; - expect(url1.search).toContain(`?query_id=${queryResult1.query_id}`) + expect(url1.search).toContain(`?query_id=${queryResult1.query_id}`); - const [url2] = httpRequestStub.mock.calls[1] - expect(url2.search).toContain(`?query_id=${queryResult2.query_id}`) - }) + const [url2] = httpRequestStub.mock.calls[1]; + expect(url2.search).toContain(`?query_id=${queryResult2.query_id}`); + }); - it('should use provided query_id for exec', async () => { + it("should use provided query_id for exec", async () => { const adapter = buildHttpConnection({ compression: { decompress_response: false, compress_request: false, }, - }) + }); - const httpRequestStub = vi.spyOn(Http, 'request') - const request = stubClientRequest() - httpRequestStub.mockReturnValue(request) + const httpRequestStub = vi.spyOn(Http, "request"); + const request = stubClientRequest(); + httpRequestStub.mockReturnValue(request); - const query_id = guid() + const query_id = guid(); const execPromise = adapter.exec({ - query: 'SELECT * FROM system.numbers LIMIT 5', + query: "SELECT * FROM system.numbers LIMIT 5", query_id, - }) - const responseBody = 'foobar' - await emitResponseBody(request, responseBody) - const { stream } = await execPromise - expect(await getAsText(stream)).toBe(responseBody) + }); + const responseBody = "foobar"; + await emitResponseBody(request, responseBody); + const { stream } = await execPromise; + expect(await getAsText(stream)).toBe(responseBody); - expect(httpRequestStub).toHaveBeenCalledTimes(1) + expect(httpRequestStub).toHaveBeenCalledTimes(1); const [url] = - httpRequestStub.mock.calls[httpRequestStub.mock.calls.length - 1] - expect(url.search).toContain(`?query_id=${query_id}`) - }) + httpRequestStub.mock.calls[httpRequestStub.mock.calls.length - 1]; + expect(url.search).toContain(`?query_id=${query_id}`); + }); - it('should generate random query_id for every command request', async () => { + it("should generate random query_id for every command request", async () => { const adapter = buildHttpConnection({ compression: { decompress_response: false, compress_request: false, }, - }) + }); - const httpRequestStub = vi.spyOn(Http, 'request') + const httpRequestStub = vi.spyOn(Http, "request"); - const request1 = stubClientRequest() - httpRequestStub.mockReturnValue(request1) + const request1 = stubClientRequest(); + httpRequestStub.mockReturnValue(request1); const cmdPromise = adapter.command({ - query: 'SELECT * FROM system.numbers LIMIT 5', - }) - await emitResponseBody(request1, 'Ok.') - const { query_id } = await cmdPromise + query: "SELECT * FROM system.numbers LIMIT 5", + }); + await emitResponseBody(request1, "Ok."); + const { query_id } = await cmdPromise; - const request2 = stubClientRequest() - httpRequestStub.mockReturnValue(request2) + const request2 = stubClientRequest(); + httpRequestStub.mockReturnValue(request2); const cmdPromise2 = adapter.command({ - query: 'SELECT * FROM system.numbers LIMIT 5', - }) - await emitResponseBody(request2, 'Ok.') - const { query_id: query_id2 } = await cmdPromise2 - - expect(query_id).not.toEqual(query_id2) - const [url1] = httpRequestStub.mock.calls[0] - expect(url1.search).toContain(`?query_id=${query_id}`) - const [url2] = httpRequestStub.mock.calls[1] - expect(url2.search).toContain(`?query_id=${query_id2}`) - }) - - it('should use provided query_id for command', async () => { + query: "SELECT * FROM system.numbers LIMIT 5", + }); + await emitResponseBody(request2, "Ok."); + const { query_id: query_id2 } = await cmdPromise2; + + expect(query_id).not.toEqual(query_id2); + const [url1] = httpRequestStub.mock.calls[0]; + expect(url1.search).toContain(`?query_id=${query_id}`); + const [url2] = httpRequestStub.mock.calls[1]; + expect(url2.search).toContain(`?query_id=${query_id2}`); + }); + + it("should use provided query_id for command", async () => { const adapter = buildHttpConnection({ compression: { decompress_response: false, compress_request: false, }, - }) + }); - const httpRequestStub = vi.spyOn(Http, 'request') - const request = stubClientRequest() - httpRequestStub.mockReturnValue(request) + const httpRequestStub = vi.spyOn(Http, "request"); + const request = stubClientRequest(); + httpRequestStub.mockReturnValue(request); - const query_id = guid() + const query_id = guid(); const cmdPromise = adapter.command({ - query: 'SELECT * FROM system.numbers LIMIT 5', + query: "SELECT * FROM system.numbers LIMIT 5", query_id, - }) - await emitResponseBody(request, 'Ok.') - const { query_id: result_query_id } = await cmdPromise + }); + await emitResponseBody(request, "Ok."); + const { query_id: result_query_id } = await cmdPromise; - expect(httpRequestStub).toHaveBeenCalledTimes(1) + expect(httpRequestStub).toHaveBeenCalledTimes(1); const [url] = - httpRequestStub.mock.calls[httpRequestStub.mock.calls.length - 1] - expect(url.search).toContain(`?query_id=${query_id}`) - expect(query_id).toEqual(result_query_id) - }) + httpRequestStub.mock.calls[httpRequestStub.mock.calls.length - 1]; + expect(url.search).toContain(`?query_id=${query_id}`); + expect(query_id).toEqual(result_query_id); + }); - it('should generate random query_id for every insert request', async () => { + it("should generate random query_id for every insert request", async () => { const adapter = buildHttpConnection({ compression: { decompress_response: false, compress_request: false, }, - }) + }); - const httpRequestStub = vi.spyOn(Http, 'request') + const httpRequestStub = vi.spyOn(Http, "request"); - const request1 = stubClientRequest() - httpRequestStub.mockReturnValue(request1) + const request1 = stubClientRequest(); + httpRequestStub.mockReturnValue(request1); const insertPromise1 = adapter.insert({ - query: 'INSERT INTO default.foo VALUES (42)', - values: 'foobar', - }) - const responseBody1 = 'foobar' - await emitResponseBody(request1, responseBody1) - const { query_id: queryId1 } = await insertPromise1 + query: "INSERT INTO default.foo VALUES (42)", + values: "foobar", + }); + const responseBody1 = "foobar"; + await emitResponseBody(request1, responseBody1); + const { query_id: queryId1 } = await insertPromise1; - const request2 = stubClientRequest() - httpRequestStub.mockReturnValue(request2) + const request2 = stubClientRequest(); + httpRequestStub.mockReturnValue(request2); const insertPromise2 = adapter.insert({ - query: 'INSERT INTO default.foo VALUES (42)', - values: 'foobar', - }) - const responseBody2 = 'qaz' - await emitResponseBody(request2, responseBody2) - const { query_id: queryId2 } = await insertPromise2 + query: "INSERT INTO default.foo VALUES (42)", + values: "foobar", + }); + const responseBody2 = "qaz"; + await emitResponseBody(request2, responseBody2); + const { query_id: queryId2 } = await insertPromise2; - assertQueryId(queryId1) - assertQueryId(queryId2) - expect(queryId1).not.toEqual(queryId2) + assertQueryId(queryId1); + assertQueryId(queryId2); + expect(queryId1).not.toEqual(queryId2); - const [url1] = httpRequestStub.mock.calls[0] - expect(url1.search).toContain(`?query_id=${queryId1}`) + const [url1] = httpRequestStub.mock.calls[0]; + expect(url1.search).toContain(`?query_id=${queryId1}`); - const [url2] = httpRequestStub.mock.calls[1] - expect(url2.search).toContain(`?query_id=${queryId2}`) - }) + const [url2] = httpRequestStub.mock.calls[1]; + expect(url2.search).toContain(`?query_id=${queryId2}`); + }); - it('should use provided query_id for insert', async () => { + it("should use provided query_id for insert", async () => { const adapter = buildHttpConnection({ compression: { decompress_response: false, compress_request: false, }, - }) + }); - const request = stubClientRequest() - const httpRequestStub = vi.spyOn(Http, 'request').mockReturnValue(request) + const request = stubClientRequest(); + const httpRequestStub = vi + .spyOn(Http, "request") + .mockReturnValue(request); - const query_id = guid() + const query_id = guid(); const insertPromise = adapter.insert({ - query: 'INSERT INTO default.foo VALUES (42)', - values: 'foobar', + query: "INSERT INTO default.foo VALUES (42)", + values: "foobar", query_id, - }) - const responseBody = 'foobar' - await emitResponseBody(request, responseBody) - await insertPromise + }); + const responseBody = "foobar"; + await emitResponseBody(request, responseBody); + await insertPromise; const [url] = - httpRequestStub.mock.calls[httpRequestStub.mock.calls.length - 1] - expect(url.search).toContain(`?query_id=${query_id}`) - }) - }) + httpRequestStub.mock.calls[httpRequestStub.mock.calls.length - 1]; + expect(url.search).toContain(`?query_id=${query_id}`); + }); + }); - it('should set custom HTTP headers for a particular request', async () => { + it("should set custom HTTP headers for a particular request", async () => { const connection = buildHttpConnection({ compression: { decompress_response: false, compress_request: false, }, - }) + }); - const httpRequestStub = vi.spyOn(Http, 'request') - vi.clearAllMocks() + const httpRequestStub = vi.spyOn(Http, "request"); + vi.clearAllMocks(); const traceparent = - '00-12345678901234567890123456789012-1234567890123456-01' - const tracestate = 'rojo=00f067aa0ba902b7' + "00-12345678901234567890123456789012-1234567890123456-01"; + const tracestate = "rojo=00f067aa0ba902b7"; const assertHeaders = (i: number, op: string) => { - const callArgs = httpRequestStub.mock.calls[i][1] - const headers = callArgs.headers as Record - expect(headers['traceparent']).toBe(traceparent) - expect(headers['tracestate']).toBe(tracestate) - expect(headers['op']).toBe(op) + const callArgs = httpRequestStub.mock.calls[i][1]; + const headers = callArgs.headers as Record; + expect(headers["traceparent"]).toBe(traceparent); + expect(headers["tracestate"]).toBe(tracestate); + expect(headers["op"]).toBe(op); // Connection + User-Agent should be enforced on the connection level - expect(headers['User-Agent']).toContain('clickhouse-js/') + expect(headers["User-Agent"]).toContain("clickhouse-js/"); // keep-alive is disabled in this test => close - expect(headers['Connection']).toContain('close') - } + expect(headers["Connection"]).toContain("close"); + }; const getQueryParamsWithCustomHeaders: (op: string) => QueryParams = ( op, ) => { return { - query: 'whatever', + query: "whatever", http_headers: { op, traceparent, tracestate, // Should not be overridden - 'User-Agent': 'foo', - Connection: 'bar', + "User-Agent": "foo", + Connection: "bar", }, - } - } + }; + }; // Query - const queryRequest = stubClientRequest() - httpRequestStub.mockReturnValue(queryRequest) + const queryRequest = stubClientRequest(); + httpRequestStub.mockReturnValue(queryRequest); const queryPromise = connection.query( - getQueryParamsWithCustomHeaders('query'), - ) - await emitResponseBody(queryRequest, 'Ok.') - await queryPromise - assertHeaders(0, 'query') + getQueryParamsWithCustomHeaders("query"), + ); + await emitResponseBody(queryRequest, "Ok."); + await queryPromise; + assertHeaders(0, "query"); // Command - const cmdRequest = stubClientRequest() - httpRequestStub.mockReturnValue(cmdRequest) + const cmdRequest = stubClientRequest(); + httpRequestStub.mockReturnValue(cmdRequest); const cmdPromise = connection.command( - getQueryParamsWithCustomHeaders('command'), - ) - await emitResponseBody(cmdRequest, 'Ok.') - await cmdPromise - assertHeaders(1, 'command') + getQueryParamsWithCustomHeaders("command"), + ); + await emitResponseBody(cmdRequest, "Ok."); + await cmdPromise; + assertHeaders(1, "command"); // Exec - const execRequest = stubClientRequest() - httpRequestStub.mockReturnValue(execRequest) - const execPromise = connection.exec(getQueryParamsWithCustomHeaders('exec')) - await emitResponseBody(execRequest, 'Ok.') - const { stream } = await execPromise - stream.destroy() - assertHeaders(2, 'exec') + const execRequest = stubClientRequest(); + httpRequestStub.mockReturnValue(execRequest); + const execPromise = connection.exec( + getQueryParamsWithCustomHeaders("exec"), + ); + await emitResponseBody(execRequest, "Ok."); + const { stream } = await execPromise; + stream.destroy(); + assertHeaders(2, "exec"); // Insert - const insertRequest = stubClientRequest() - httpRequestStub.mockReturnValue(insertRequest) + const insertRequest = stubClientRequest(); + httpRequestStub.mockReturnValue(insertRequest); const insertPromise = connection.insert({ - ...getQueryParamsWithCustomHeaders('insert'), - values: 'foobar\n', - }) - await emitResponseBody(insertRequest, 'Ok.') - await insertPromise - assertHeaders(3, 'insert') - }) -}) + ...getQueryParamsWithCustomHeaders("insert"), + values: "foobar\n", + }); + await emitResponseBody(insertRequest, "Ok."); + await insertPromise; + assertHeaders(3, "insert"); + }); +}); diff --git a/packages/client-node/__tests__/unit/node_connection_compression.test.ts b/packages/client-node/__tests__/unit/node_connection_compression.test.ts index 208069fd..cd541ad2 100644 --- a/packages/client-node/__tests__/unit/node_connection_compression.test.ts +++ b/packages/client-node/__tests__/unit/node_connection_compression.test.ts @@ -1,9 +1,9 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest' -import { sleep } from '../utils/sleep' -import Http, { type ClientRequest } from 'http' -import Stream from 'stream' -import Zlib from 'zlib' -import { assertConnQueryResult } from '../utils/assert' +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { sleep } from "../utils/sleep"; +import Http, { type ClientRequest } from "http"; +import Stream from "stream"; +import Zlib from "zlib"; +import { assertConnQueryResult } from "../utils/assert"; import { buildHttpConnection, buildIncomingMessage, @@ -11,237 +11,237 @@ import { emitResponseBody, socketStub, stubClientRequest, -} from '../utils/http_stubs' +} from "../utils/http_stubs"; beforeEach(() => { - vi.clearAllMocks() -}) + vi.clearAllMocks(); +}); -const httpRequestStub = vi.spyOn(Http, 'request') +const httpRequestStub = vi.spyOn(Http, "request"); -describe('Node.js Connection compression', () => { - describe('response decompression', () => { - it('hints ClickHouse server to send a gzip compressed response if compress_request: true', async () => { - const request = stubClientRequest() - httpRequestStub.mockReturnValue(request) +describe("Node.js Connection compression", () => { + describe("response decompression", () => { + it("hints ClickHouse server to send a gzip compressed response if compress_request: true", async () => { + const request = stubClientRequest(); + httpRequestStub.mockReturnValue(request); const adapter = buildHttpConnection({ compression: { decompress_response: true, compress_request: false, }, - }) + }); const selectPromise = adapter.query({ - query: 'SELECT * FROM system.numbers LIMIT 5', - }) + query: "SELECT * FROM system.numbers LIMIT 5", + }); - const responseBody = 'foobar' - await emitCompressedBody(request, responseBody) + const responseBody = "foobar"; + await emitCompressedBody(request, responseBody); - await selectPromise + await selectPromise; - expect(httpRequestStub).toHaveBeenCalledTimes(1) + expect(httpRequestStub).toHaveBeenCalledTimes(1); const calledWith = - httpRequestStub.mock.calls[httpRequestStub.mock.calls.length - 1][1] + httpRequestStub.mock.calls[httpRequestStub.mock.calls.length - 1][1]; expect( - (calledWith.headers as Record)['Accept-Encoding'], - ).toBe('gzip') - }) + (calledWith.headers as Record)["Accept-Encoding"], + ).toBe("gzip"); + }); - it('does not send a compression algorithm hint if compress_request: false', async () => { - const request = stubClientRequest() - httpRequestStub.mockReturnValue(request) + it("does not send a compression algorithm hint if compress_request: false", async () => { + const request = stubClientRequest(); + httpRequestStub.mockReturnValue(request); const adapter = buildHttpConnection({ compression: { decompress_response: false, compress_request: false, }, - }) + }); const selectPromise = adapter.query({ - query: 'SELECT * FROM system.numbers LIMIT 5', - }) + query: "SELECT * FROM system.numbers LIMIT 5", + }); - const responseBody = 'foobar' - await emitResponseBody(request, responseBody) + const responseBody = "foobar"; + await emitResponseBody(request, responseBody); - const queryResult = await selectPromise - await assertConnQueryResult(queryResult, responseBody) + const queryResult = await selectPromise; + await assertConnQueryResult(queryResult, responseBody); - expect(httpRequestStub).toHaveBeenCalledTimes(1) + expect(httpRequestStub).toHaveBeenCalledTimes(1); const calledWith = - httpRequestStub.mock.calls[httpRequestStub.mock.calls.length - 1][1] + httpRequestStub.mock.calls[httpRequestStub.mock.calls.length - 1][1]; expect( (calledWith.headers as Record)[ - 'Accept-Encoding' + "Accept-Encoding" ], - ).toBe(undefined) - }) + ).toBe(undefined); + }); - it('uses request-specific settings over config settings', async () => { - const request = stubClientRequest() - httpRequestStub.mockReturnValue(request) + it("uses request-specific settings over config settings", async () => { + const request = stubClientRequest(); + httpRequestStub.mockReturnValue(request); const adapter = buildHttpConnection({ compression: { decompress_response: false, compress_request: false, }, - }) + }); const selectPromise = adapter.query({ - query: 'SELECT * FROM system.numbers LIMIT 5', + query: "SELECT * FROM system.numbers LIMIT 5", clickhouse_settings: { enable_http_compression: 1, }, - }) + }); - const responseBody = 'foobar' - await emitCompressedBody(request, responseBody) + const responseBody = "foobar"; + await emitCompressedBody(request, responseBody); - const queryResult = await selectPromise - await assertConnQueryResult(queryResult, responseBody) + const queryResult = await selectPromise; + await assertConnQueryResult(queryResult, responseBody); - expect(httpRequestStub).toHaveBeenCalledTimes(1) + expect(httpRequestStub).toHaveBeenCalledTimes(1); const calledWith = - httpRequestStub.mock.calls[httpRequestStub.mock.calls.length - 1][1] + httpRequestStub.mock.calls[httpRequestStub.mock.calls.length - 1][1]; expect( - (calledWith.headers as Record)['Accept-Encoding'], - ).toBe('gzip') - }) + (calledWith.headers as Record)["Accept-Encoding"], + ).toBe("gzip"); + }); - it('decompresses a gzip response', async () => { - const request = stubClientRequest() - httpRequestStub.mockReturnValue(request) + it("decompresses a gzip response", async () => { + const request = stubClientRequest(); + httpRequestStub.mockReturnValue(request); const adapter = buildHttpConnection({ compression: { decompress_response: true, compress_request: false, }, - }) + }); const selectPromise = adapter.query({ - query: 'SELECT * FROM system.numbers LIMIT 5', - }) + query: "SELECT * FROM system.numbers LIMIT 5", + }); - const responseBody = 'abc'.repeat(1_000) - await emitCompressedBody(request, responseBody) + const responseBody = "abc".repeat(1_000); + await emitCompressedBody(request, responseBody); - const queryResult = await selectPromise - await assertConnQueryResult(queryResult, responseBody) - }) + const queryResult = await selectPromise; + await assertConnQueryResult(queryResult, responseBody); + }); - it('throws on an unexpected encoding', async () => { - const request = stubClientRequest() - httpRequestStub.mockReturnValue(request) + it("throws on an unexpected encoding", async () => { + const request = stubClientRequest(); + httpRequestStub.mockReturnValue(request); const adapter = buildHttpConnection({ compression: { decompress_response: true, compress_request: false, }, - }) + }); const selectPromise = adapter.query({ - query: 'SELECT * FROM system.numbers LIMIT 5', - }) + query: "SELECT * FROM system.numbers LIMIT 5", + }); - await emitCompressedBody(request, 'abc', 'br') + await emitCompressedBody(request, "abc", "br"); await expect(selectPromise).rejects.toEqual( expect.objectContaining({ - message: 'Unexpected encoding: br', + message: "Unexpected encoding: br", }), - ) - }) + ); + }); - it('provides decompression error to a stream consumer', async () => { - const request = stubClientRequest() - httpRequestStub.mockReturnValue(request) + it("provides decompression error to a stream consumer", async () => { + const request = stubClientRequest(); + httpRequestStub.mockReturnValue(request); const adapter = buildHttpConnection({ compression: { decompress_response: true, compress_request: false, }, - }) + }); const selectPromise = adapter.query({ - query: 'SELECT * FROM system.numbers LIMIT 5', - }) + query: "SELECT * FROM system.numbers LIMIT 5", + }); // No GZIP encoding for the body here - await sleep(0) + await sleep(0); request.emit( - 'response', + "response", buildIncomingMessage({ - body: 'abc', + body: "abc", headers: { - 'content-encoding': 'gzip', + "content-encoding": "gzip", }, }), - ) + ); const readStream = async () => { - const { stream } = await selectPromise + const { stream } = await selectPromise; for await (const chunk of stream) { - void chunk // stub + void chunk; // stub } - } + }; await expect(readStream()).rejects.toEqual( expect.objectContaining({ - message: 'incorrect header check', - code: 'Z_DATA_ERROR', + message: "incorrect header check", + code: "Z_DATA_ERROR", }), - ) - }) - }) + ); + }); + }); - describe('request compression', () => { - it('sends a compressed request if compress_request: true', async () => { + describe("request compression", () => { + it("sends a compressed request if compress_request: true", async () => { const adapter = buildHttpConnection({ compression: { decompress_response: false, compress_request: true, }, - }) + }); - const values = 'abc'.repeat(1_000) + const values = "abc".repeat(1_000); - let chunks = Buffer.alloc(0) - let finalResult: Buffer | undefined = undefined + let chunks = Buffer.alloc(0); + let finalResult: Buffer | undefined = undefined; const request = new Stream.Writable({ write(chunk, encoding, next) { - chunks = Buffer.concat([chunks, chunk]) - next() + chunks = Buffer.concat([chunks, chunk]); + next(); }, final() { Zlib.unzip(chunks, (_err, result) => { - finalResult = result - }) + finalResult = result; + }); }, - }) as ClientRequest - httpRequestStub.mockReturnValue(request) + }) as ClientRequest; + httpRequestStub.mockReturnValue(request); void adapter.insert({ - query: 'INSERT INTO insert_compression_table', + query: "INSERT INTO insert_compression_table", values, - }) + }); // trigger stream pipeline - await sleep(0) - request.emit('socket', socketStub) - await sleep(100) + await sleep(0); + request.emit("socket", socketStub); + await sleep(100); - expect(finalResult!.toString('utf8')).toEqual(values) - expect(httpRequestStub).toHaveBeenCalledTimes(1) + expect(finalResult!.toString("utf8")).toEqual(values); + expect(httpRequestStub).toHaveBeenCalledTimes(1); const calledWith = - httpRequestStub.mock.calls[httpRequestStub.mock.calls.length - 1][1] + httpRequestStub.mock.calls[httpRequestStub.mock.calls.length - 1][1]; expect( - (calledWith.headers as Record)['Content-Encoding'], - ).toBe('gzip') - }) - }) -}) + (calledWith.headers as Record)["Content-Encoding"], + ).toBe("gzip"); + }); + }); +}); diff --git a/packages/client-node/__tests__/unit/node_create_connection.test.ts b/packages/client-node/__tests__/unit/node_create_connection.test.ts index 5a0a5145..53013382 100644 --- a/packages/client-node/__tests__/unit/node_create_connection.test.ts +++ b/packages/client-node/__tests__/unit/node_create_connection.test.ts @@ -1,36 +1,36 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest' -import type { ConnectionParams } from '@clickhouse/client-common' -import http from 'http' -import https from 'node:https' +import { describe, it, expect, beforeEach, vi } from "vitest"; +import type { ConnectionParams } from "@clickhouse/client-common"; +import http from "http"; +import https from "node:https"; import { NodeConnectionFactory, type NodeConnectionParams, NodeHttpConnection, NodeHttpsConnection, -} from '../../src/connection' -import { NodeCustomAgentConnection } from '../../src/connection/node_custom_agent_connection' +} from "../../src/connection"; +import { NodeCustomAgentConnection } from "../../src/connection/node_custom_agent_connection"; beforeEach(() => { - vi.clearAllMocks() -}) + vi.clearAllMocks(); +}); -describe('[Node.js] NodeConnectionFactory', () => { - const keepAliveParams: NodeConnectionParams['keep_alive'] = { +describe("[Node.js] NodeConnectionFactory", () => { + const keepAliveParams: NodeConnectionParams["keep_alive"] = { enabled: true, idle_socket_ttl: 2500, - } - const tlsParams: NodeConnectionParams['tls'] = undefined + }; + const tlsParams: NodeConnectionParams["tls"] = undefined; const defaultConnectionParams = { - url: new URL('http://localhost'), + url: new URL("http://localhost"), auth: { - username: 'default', - password: 'password', - type: 'Credentials', + username: "default", + password: "password", + type: "Credentials", }, - } as ConnectionParams + } as ConnectionParams; - it('should create an instance of HTTP adapter', async () => { + it("should create an instance of HTTP adapter", async () => { const adapter = NodeConnectionFactory.create({ connection_params: defaultConnectionParams, tls: tlsParams, @@ -38,31 +38,31 @@ describe('[Node.js] NodeConnectionFactory', () => { http_agent: undefined, set_basic_auth_header: true, capture_enhanced_stack_trace: false, - }) - expect(adapter).toBeInstanceOf(NodeHttpConnection) - }) + }); + expect(adapter).toBeInstanceOf(NodeHttpConnection); + }); - it('should create an instance of HTTPS adapter', async () => { + it("should create an instance of HTTPS adapter", async () => { const adapter = NodeConnectionFactory.create({ connection_params: { ...defaultConnectionParams, - url: new URL('https://localhost'), + url: new URL("https://localhost"), }, tls: tlsParams, keep_alive: keepAliveParams, http_agent: undefined, set_basic_auth_header: true, capture_enhanced_stack_trace: false, - }) - expect(adapter).toBeInstanceOf(NodeHttpsConnection) - }) + }); + expect(adapter).toBeInstanceOf(NodeHttpsConnection); + }); - it('should throw if the supplied protocol is unknown', async () => { + it("should throw if the supplied protocol is unknown", async () => { expect(() => NodeConnectionFactory.create({ connection_params: { ...defaultConnectionParams, - url: new URL('tcp://localhost'), + url: new URL("tcp://localhost"), }, tls: tlsParams, keep_alive: keepAliveParams, @@ -70,11 +70,11 @@ describe('[Node.js] NodeConnectionFactory', () => { set_basic_auth_header: true, capture_enhanced_stack_trace: false, }), - ).toThrowError('Only HTTP and HTTPS protocols are supported') - }) + ).toThrowError("Only HTTP and HTTPS protocols are supported"); + }); - describe('Custom HTTP agent', () => { - it('should create an instance with a custom HTTP agent', async () => { + describe("Custom HTTP agent", () => { + it("should create an instance with a custom HTTP agent", async () => { const adapter = NodeConnectionFactory.create({ connection_params: defaultConnectionParams, tls: tlsParams, @@ -85,11 +85,11 @@ describe('[Node.js] NodeConnectionFactory', () => { }), set_basic_auth_header: false, capture_enhanced_stack_trace: false, - }) - expect(adapter).toBeInstanceOf(NodeCustomAgentConnection) - }) + }); + expect(adapter).toBeInstanceOf(NodeCustomAgentConnection); + }); - it('should create an instance with a custom HTTPS agent', async () => { + it("should create an instance with a custom HTTPS agent", async () => { const adapter = NodeConnectionFactory.create({ connection_params: defaultConnectionParams, tls: tlsParams, @@ -100,8 +100,8 @@ describe('[Node.js] NodeConnectionFactory', () => { }), set_basic_auth_header: true, capture_enhanced_stack_trace: false, - }) - expect(adapter).toBeInstanceOf(NodeCustomAgentConnection) - }) - }) -}) + }); + expect(adapter).toBeInstanceOf(NodeCustomAgentConnection); + }); + }); +}); diff --git a/packages/client-node/__tests__/unit/node_custom_agent_connection.test.ts b/packages/client-node/__tests__/unit/node_custom_agent_connection.test.ts index 62c71a78..d470b59e 100644 --- a/packages/client-node/__tests__/unit/node_custom_agent_connection.test.ts +++ b/packages/client-node/__tests__/unit/node_custom_agent_connection.test.ts @@ -1,17 +1,17 @@ -import { describe, it, expect, vi } from 'vitest' -import Http from 'http' -import Https from 'https' -import { ClickHouseLogLevel, LogWriter } from '@clickhouse/client-common' -import { TestLogger } from '../../../client-common/__tests__/utils/test_logger' -import type { NodeConnectionParams } from '../../src/connection' -import { NodeCustomAgentConnection } from '../../src/connection/node_custom_agent_connection' +import { describe, it, expect, vi } from "vitest"; +import Http from "http"; +import Https from "https"; +import { ClickHouseLogLevel, LogWriter } from "@clickhouse/client-common"; +import { TestLogger } from "../../../client-common/__tests__/utils/test_logger"; +import type { NodeConnectionParams } from "../../src/connection"; +import { NodeCustomAgentConnection } from "../../src/connection/node_custom_agent_connection"; /** Extends NodeCustomAgentConnection to expose protected methods for testing. */ class TestableCustomAgentConnection extends NodeCustomAgentConnection { public testCreateClientRequest( - ...args: Parameters + ...args: Parameters ): Http.ClientRequest { - return this.createClientRequest(...args) + return this.createClientRequest(...args); } } @@ -19,19 +19,19 @@ function buildCustomAgentConnectionParams( overrides?: Partial, ): NodeConnectionParams { return { - url: new URL('http://localhost:8123'), + url: new URL("http://localhost:8123"), request_timeout: 30_000, compression: { decompress_response: false, compress_request: false, }, max_open_connections: 10, - auth: { username: 'default', password: '', type: 'Credentials' }, - database: 'default', + auth: { username: "default", password: "", type: "Credentials" }, + database: "default", clickhouse_settings: {}, log_writer: new LogWriter( new TestLogger(), - 'CustomAgentConnectionTest', + "CustomAgentConnectionTest", ClickHouseLogLevel.OFF, ), log_level: ClickHouseLogLevel.OFF, @@ -43,12 +43,12 @@ function buildCustomAgentConnectionParams( capture_enhanced_stack_trace: false, http_agent: new Http.Agent(), ...overrides, - } + }; } -describe('[Node.js] NodeCustomAgentConnection', () => { - describe('constructor', () => { - it('should throw when http_agent is not provided', () => { +describe("[Node.js] NodeCustomAgentConnection", () => { + describe("constructor", () => { + it("should throw when http_agent is not provided", () => { expect( () => new TestableCustomAgentConnection( @@ -56,301 +56,301 @@ describe('[Node.js] NodeCustomAgentConnection', () => { http_agent: undefined, }), ), - ).toThrow('http_agent is required to create NodeCustomAgentConnection') - }) - }) - - describe('createClientRequest', () => { - it('should use Https.request for https URLs', () => { - const httpsAgent = new Https.Agent() - const mockRequest = {} as Http.ClientRequest + ).toThrow("http_agent is required to create NodeCustomAgentConnection"); + }); + }); + + describe("createClientRequest", () => { + it("should use Https.request for https URLs", () => { + const httpsAgent = new Https.Agent(); + const mockRequest = {} as Http.ClientRequest; const httpsRequestSpy = vi - .spyOn(Https, 'request') - .mockReturnValue(mockRequest) + .spyOn(Https, "request") + .mockReturnValue(mockRequest); const httpRequestSpy = vi - .spyOn(Http, 'request') + .spyOn(Http, "request") .mockImplementation(() => { - throw new Error('Http.request should not be called for https URLs') - }) + throw new Error("Http.request should not be called for https URLs"); + }); const connection = new TestableCustomAgentConnection( buildCustomAgentConnectionParams({ - url: new URL('https://localhost:8443'), + url: new URL("https://localhost:8443"), http_agent: httpsAgent, }), - ) - expect(connection).toBeInstanceOf(NodeCustomAgentConnection) + ); + expect(connection).toBeInstanceOf(NodeCustomAgentConnection); - const abortController = new AbortController() + const abortController = new AbortController(); connection.testCreateClientRequest({ - method: 'GET', - url: new URL('https://localhost:8443'), + method: "GET", + url: new URL("https://localhost:8443"), headers: {}, abort_signal: abortController.signal, - query: 'SELECT 1', - query_id: 'test', + query: "SELECT 1", + query_id: "test", log_writer: buildCustomAgentConnectionParams().log_writer, log_level: ClickHouseLogLevel.OFF, - }) + }); - expect(httpsRequestSpy).toHaveBeenCalledTimes(1) - expect(httpRequestSpy).not.toHaveBeenCalled() + expect(httpsRequestSpy).toHaveBeenCalledTimes(1); + expect(httpRequestSpy).not.toHaveBeenCalled(); - httpsRequestSpy.mockRestore() - httpRequestSpy.mockRestore() - }) + httpsRequestSpy.mockRestore(); + httpRequestSpy.mockRestore(); + }); - it('should use Http.request for http URLs', () => { - const httpAgent = new Http.Agent() - const mockRequest = {} as Http.ClientRequest + it("should use Http.request for http URLs", () => { + const httpAgent = new Http.Agent(); + const mockRequest = {} as Http.ClientRequest; const httpRequestSpy = vi - .spyOn(Http, 'request') - .mockReturnValue(mockRequest) - const mockHttpsRequest = {} as Http.ClientRequest + .spyOn(Http, "request") + .mockReturnValue(mockRequest); + const mockHttpsRequest = {} as Http.ClientRequest; const httpsRequestSpy = vi - .spyOn(Https, 'request') - .mockReturnValue(mockHttpsRequest) + .spyOn(Https, "request") + .mockReturnValue(mockHttpsRequest); const connection = new TestableCustomAgentConnection( buildCustomAgentConnectionParams({ - url: new URL('http://localhost:8123'), + url: new URL("http://localhost:8123"), http_agent: httpAgent, }), - ) - expect(connection).toBeInstanceOf(NodeCustomAgentConnection) + ); + expect(connection).toBeInstanceOf(NodeCustomAgentConnection); - const abortController = new AbortController() + const abortController = new AbortController(); connection.testCreateClientRequest({ - method: 'GET', - url: new URL('http://localhost:8123'), + method: "GET", + url: new URL("http://localhost:8123"), headers: {}, abort_signal: abortController.signal, - query: 'SELECT 1', - query_id: 'test', + query: "SELECT 1", + query_id: "test", log_writer: buildCustomAgentConnectionParams().log_writer, log_level: ClickHouseLogLevel.OFF, - }) + }); - expect(httpRequestSpy).toHaveBeenCalledTimes(1) - expect(httpsRequestSpy).not.toHaveBeenCalled() + expect(httpRequestSpy).toHaveBeenCalledTimes(1); + expect(httpsRequestSpy).not.toHaveBeenCalled(); - httpRequestSpy.mockRestore() - httpsRequestSpy.mockRestore() - }) - }) + httpRequestSpy.mockRestore(); + httpsRequestSpy.mockRestore(); + }); + }); - describe('createClientRequest', () => { - it('should call Http.request for http URLs', () => { - const httpAgent = new Http.Agent() + describe("createClientRequest", () => { + it("should call Http.request for http URLs", () => { + const httpAgent = new Http.Agent(); - const mockRequest = {} as Http.ClientRequest + const mockRequest = {} as Http.ClientRequest; const httpRequestSpy = vi - .spyOn(Http, 'request') - .mockReturnValue(mockRequest) + .spyOn(Http, "request") + .mockReturnValue(mockRequest); const connection = new TestableCustomAgentConnection( buildCustomAgentConnectionParams({ - url: new URL('http://localhost:8123'), + url: new URL("http://localhost:8123"), http_agent: httpAgent, }), - ) + ); - const url = new URL('http://localhost:8123/?query_id=test-query-id') - const abortController = new AbortController() + const url = new URL("http://localhost:8123/?query_id=test-query-id"); + const abortController = new AbortController(); const result = connection.testCreateClientRequest({ - method: 'POST', + method: "POST", url, - headers: { 'Content-Type': 'text/plain' }, + headers: { "Content-Type": "text/plain" }, abort_signal: abortController.signal, - query: 'SELECT 1', - query_id: 'test-query-id', + query: "SELECT 1", + query_id: "test-query-id", log_writer: new LogWriter( new TestLogger(), - 'CustomAgentConnectionTest', + "CustomAgentConnectionTest", ClickHouseLogLevel.OFF, ), log_level: ClickHouseLogLevel.OFF, - }) + }); - expect(result).toBe(mockRequest) - expect(httpRequestSpy).toHaveBeenCalledTimes(1) + expect(result).toBe(mockRequest); + expect(httpRequestSpy).toHaveBeenCalledTimes(1); expect(httpRequestSpy).toHaveBeenCalledWith(url, { - method: 'POST', + method: "POST", agent: httpAgent, timeout: 30_000, signal: abortController.signal, - headers: { 'Content-Type': 'text/plain' }, - }) + headers: { "Content-Type": "text/plain" }, + }); - httpRequestSpy.mockRestore() - }) + httpRequestSpy.mockRestore(); + }); - it('should call Https.request for https URLs', () => { - const httpsAgent = new Https.Agent() + it("should call Https.request for https URLs", () => { + const httpsAgent = new Https.Agent(); - const mockRequest = {} as Http.ClientRequest + const mockRequest = {} as Http.ClientRequest; const httpsRequestSpy = vi - .spyOn(Https, 'request') - .mockReturnValue(mockRequest) + .spyOn(Https, "request") + .mockReturnValue(mockRequest); const connection = new TestableCustomAgentConnection( buildCustomAgentConnectionParams({ - url: new URL('https://localhost:8443'), + url: new URL("https://localhost:8443"), http_agent: httpsAgent, }), - ) + ); - const url = new URL('https://localhost:8443/?query_id=test') - const abortController = new AbortController() + const url = new URL("https://localhost:8443/?query_id=test"); + const abortController = new AbortController(); const result = connection.testCreateClientRequest({ - method: 'GET', + method: "GET", url, headers: {}, abort_signal: abortController.signal, - query: 'SELECT 1', - query_id: 'test-query-id', + query: "SELECT 1", + query_id: "test-query-id", log_writer: new LogWriter( new TestLogger(), - 'CustomAgentConnectionTest', + "CustomAgentConnectionTest", ClickHouseLogLevel.OFF, ), log_level: ClickHouseLogLevel.OFF, - }) + }); - expect(result).toBe(mockRequest) - expect(httpsRequestSpy).toHaveBeenCalledTimes(1) + expect(result).toBe(mockRequest); + expect(httpsRequestSpy).toHaveBeenCalledTimes(1); expect(httpsRequestSpy).toHaveBeenCalledWith(url, { - method: 'GET', + method: "GET", agent: httpsAgent, timeout: 30_000, signal: abortController.signal, headers: {}, - }) + }); - httpsRequestSpy.mockRestore() - }) + httpsRequestSpy.mockRestore(); + }); - it('should add compression headers when compression is enabled', () => { - const httpAgent = new Http.Agent() + it("should add compression headers when compression is enabled", () => { + const httpAgent = new Http.Agent(); - const mockRequest = {} as Http.ClientRequest + const mockRequest = {} as Http.ClientRequest; const httpRequestSpy = vi - .spyOn(Http, 'request') - .mockReturnValue(mockRequest) + .spyOn(Http, "request") + .mockReturnValue(mockRequest); const connection = new TestableCustomAgentConnection( buildCustomAgentConnectionParams({ - url: new URL('http://localhost:8123'), + url: new URL("http://localhost:8123"), http_agent: httpAgent, }), - ) + ); - const url = new URL('http://localhost:8123/?query_id=test') - const abortController = new AbortController() + const url = new URL("http://localhost:8123/?query_id=test"); + const abortController = new AbortController(); connection.testCreateClientRequest({ - method: 'POST', + method: "POST", url, - headers: { 'Content-Type': 'text/plain' }, + headers: { "Content-Type": "text/plain" }, abort_signal: abortController.signal, enable_request_compression: true, enable_response_compression: true, - query: 'SELECT 1', - query_id: 'test-query-id', + query: "SELECT 1", + query_id: "test-query-id", log_writer: new LogWriter( new TestLogger(), - 'CustomAgentConnectionTest', + "CustomAgentConnectionTest", ClickHouseLogLevel.OFF, ), log_level: ClickHouseLogLevel.OFF, - }) + }); - expect(httpRequestSpy).toHaveBeenCalledTimes(1) + expect(httpRequestSpy).toHaveBeenCalledTimes(1); const calledHeaders = httpRequestSpy.mock.calls[0][1]?.headers as Record< string, string - > - expect(calledHeaders['Content-Encoding']).toBe('gzip') - expect(calledHeaders['Accept-Encoding']).toBe('gzip') + >; + expect(calledHeaders["Content-Encoding"]).toBe("gzip"); + expect(calledHeaders["Accept-Encoding"]).toBe("gzip"); - httpRequestSpy.mockRestore() - }) + httpRequestSpy.mockRestore(); + }); - it('should forward max_response_headers_size as maxHeaderSize when set', () => { - const httpAgent = new Http.Agent() - const mockRequest = {} as Http.ClientRequest + it("should forward max_response_headers_size as maxHeaderSize when set", () => { + const httpAgent = new Http.Agent(); + const mockRequest = {} as Http.ClientRequest; const httpRequestSpy = vi - .spyOn(Http, 'request') - .mockReturnValue(mockRequest) + .spyOn(Http, "request") + .mockReturnValue(mockRequest); const connection = new TestableCustomAgentConnection( buildCustomAgentConnectionParams({ - url: new URL('http://localhost:8123'), + url: new URL("http://localhost:8123"), http_agent: httpAgent, max_response_headers_size: 64 * 1024, }), - ) + ); - const url = new URL('http://localhost:8123/?query_id=test') - const abortController = new AbortController() + const url = new URL("http://localhost:8123/?query_id=test"); + const abortController = new AbortController(); connection.testCreateClientRequest({ - method: 'GET', + method: "GET", url, headers: {}, abort_signal: abortController.signal, - query: 'SELECT 1', - query_id: 'test-query-id', + query: "SELECT 1", + query_id: "test-query-id", log_writer: new LogWriter( new TestLogger(), - 'CustomAgentConnectionTest', + "CustomAgentConnectionTest", ClickHouseLogLevel.OFF, ), log_level: ClickHouseLogLevel.OFF, - }) + }); - expect(httpRequestSpy).toHaveBeenCalledTimes(1) + expect(httpRequestSpy).toHaveBeenCalledTimes(1); const calledOptions = httpRequestSpy.mock - .calls[0][1] as Http.RequestOptions - expect(calledOptions.maxHeaderSize).toBe(64 * 1024) + .calls[0][1] as Http.RequestOptions; + expect(calledOptions.maxHeaderSize).toBe(64 * 1024); - httpRequestSpy.mockRestore() - }) + httpRequestSpy.mockRestore(); + }); - it('should not include maxHeaderSize when max_response_headers_size is undefined', () => { - const httpAgent = new Http.Agent() - const mockRequest = {} as Http.ClientRequest + it("should not include maxHeaderSize when max_response_headers_size is undefined", () => { + const httpAgent = new Http.Agent(); + const mockRequest = {} as Http.ClientRequest; const httpRequestSpy = vi - .spyOn(Http, 'request') - .mockReturnValue(mockRequest) + .spyOn(Http, "request") + .mockReturnValue(mockRequest); const connection = new TestableCustomAgentConnection( buildCustomAgentConnectionParams({ - url: new URL('http://localhost:8123'), + url: new URL("http://localhost:8123"), http_agent: httpAgent, }), - ) + ); - const url = new URL('http://localhost:8123/?query_id=test') - const abortController = new AbortController() + const url = new URL("http://localhost:8123/?query_id=test"); + const abortController = new AbortController(); connection.testCreateClientRequest({ - method: 'GET', + method: "GET", url, headers: {}, abort_signal: abortController.signal, - query: 'SELECT 1', - query_id: 'test-query-id', + query: "SELECT 1", + query_id: "test-query-id", log_writer: new LogWriter( new TestLogger(), - 'CustomAgentConnectionTest', + "CustomAgentConnectionTest", ClickHouseLogLevel.OFF, ), log_level: ClickHouseLogLevel.OFF, - }) + }); - expect(httpRequestSpy).toHaveBeenCalledTimes(1) + expect(httpRequestSpy).toHaveBeenCalledTimes(1); const calledOptions = httpRequestSpy.mock - .calls[0][1] as Http.RequestOptions - expect(calledOptions).not.toHaveProperty('maxHeaderSize') + .calls[0][1] as Http.RequestOptions; + expect(calledOptions).not.toHaveProperty("maxHeaderSize"); - httpRequestSpy.mockRestore() - }) - }) -}) + httpRequestSpy.mockRestore(); + }); + }); +}); diff --git a/packages/client-node/__tests__/unit/node_default_logger.test.ts b/packages/client-node/__tests__/unit/node_default_logger.test.ts index 53e10f5c..9aa41ddc 100644 --- a/packages/client-node/__tests__/unit/node_default_logger.test.ts +++ b/packages/client-node/__tests__/unit/node_default_logger.test.ts @@ -1,159 +1,159 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest' +import { describe, it, expect, beforeEach, vi } from "vitest"; import { ClickHouseLogLevel, DefaultLogger, LogWriter, -} from '@clickhouse/client-common' +} from "@clickhouse/client-common"; beforeEach(() => { - vi.clearAllMocks() -}) + vi.clearAllMocks(); +}); -const debugSpy = vi.spyOn(console, 'debug') -const infoSpy = vi.spyOn(console, 'info') -const warnSpy = vi.spyOn(console, 'warn') -const errSpy = vi.spyOn(console, 'error') +const debugSpy = vi.spyOn(console, "debug"); +const infoSpy = vi.spyOn(console, "info"); +const warnSpy = vi.spyOn(console, "warn"); +const errSpy = vi.spyOn(console, "error"); -describe('[Node.js] Logger/LogWriter', () => { - type LogLevel = 'TRACE' | 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' +describe("[Node.js] Logger/LogWriter", () => { + type LogLevel = "TRACE" | "DEBUG" | "INFO" | "WARN" | "ERROR"; - const module = 'LoggerTest' - const message = 'very informative' - const err = new Error('boo') + const module = "LoggerTest"; + const message = "very informative"; + const err = new Error("boo"); - it('should explicitly use OFF', async () => { + it("should explicitly use OFF", async () => { const logWriter = new LogWriter( new DefaultLogger(), module, ClickHouseLogLevel.OFF, - ) - logEveryLogLevel(logWriter) - expect(debugSpy).toHaveBeenCalledTimes(0) - expect(infoSpy).toHaveBeenCalledTimes(0) - expect(warnSpy).toHaveBeenCalledTimes(0) - expect(errSpy).toHaveBeenCalledTimes(0) - }) - - it('should explicitly use TRACE', async () => { + ); + logEveryLogLevel(logWriter); + expect(debugSpy).toHaveBeenCalledTimes(0); + expect(infoSpy).toHaveBeenCalledTimes(0); + expect(warnSpy).toHaveBeenCalledTimes(0); + expect(errSpy).toHaveBeenCalledTimes(0); + }); + + it("should explicitly use TRACE", async () => { const logWriter = new LogWriter( new DefaultLogger(), module, ClickHouseLogLevel.TRACE, - ) + ); - checkLogLevelSet('TRACE') - logEveryLogLevel(logWriter) + checkLogLevelSet("TRACE"); + logEveryLogLevel(logWriter); // TRACE + DEBUG - expect(debugSpy).toHaveBeenCalledTimes(2) - checkLog(debugSpy, 'TRACE', 0) - checkLog(debugSpy, 'DEBUG', 1) + expect(debugSpy).toHaveBeenCalledTimes(2); + checkLog(debugSpy, "TRACE", 0); + checkLog(debugSpy, "DEBUG", 1); // + set log level call - expect(infoSpy).toHaveBeenCalledTimes(2) - checkLog(infoSpy, 'INFO', 1) + expect(infoSpy).toHaveBeenCalledTimes(2); + checkLog(infoSpy, "INFO", 1); - expect(warnSpy).toHaveBeenCalledTimes(1) - checkLog(warnSpy, 'WARN') + expect(warnSpy).toHaveBeenCalledTimes(1); + checkLog(warnSpy, "WARN"); - expect(errSpy).toHaveBeenCalledTimes(1) - checkErrorLog() - }) + expect(errSpy).toHaveBeenCalledTimes(1); + checkErrorLog(); + }); - it('should explicitly use DEBUG', async () => { + it("should explicitly use DEBUG", async () => { const logWriter = new LogWriter( new DefaultLogger(), module, ClickHouseLogLevel.DEBUG, - ) + ); - checkLogLevelSet('DEBUG') - logEveryLogLevel(logWriter) + checkLogLevelSet("DEBUG"); + logEveryLogLevel(logWriter); // No TRACE, only DEBUG - expect(debugSpy).toHaveBeenCalledTimes(1) - checkLog(debugSpy, 'DEBUG', 0) + expect(debugSpy).toHaveBeenCalledTimes(1); + checkLog(debugSpy, "DEBUG", 0); // + set log level call - expect(infoSpy).toHaveBeenCalledTimes(2) - checkLog(infoSpy, 'INFO', 1) + expect(infoSpy).toHaveBeenCalledTimes(2); + checkLog(infoSpy, "INFO", 1); - expect(warnSpy).toHaveBeenCalledTimes(1) - checkLog(warnSpy, 'WARN') + expect(warnSpy).toHaveBeenCalledTimes(1); + checkLog(warnSpy, "WARN"); - expect(errSpy).toHaveBeenCalledTimes(1) - checkErrorLog() - }) + expect(errSpy).toHaveBeenCalledTimes(1); + checkErrorLog(); + }); - it('should explicitly use INFO', async () => { + it("should explicitly use INFO", async () => { const logWriter = new LogWriter( new DefaultLogger(), module, ClickHouseLogLevel.INFO, - ) + ); - checkLogLevelSet('INFO') - logEveryLogLevel(logWriter) + checkLogLevelSet("INFO"); + logEveryLogLevel(logWriter); // No TRACE or DEBUG logs - expect(debugSpy).toHaveBeenCalledTimes(0) + expect(debugSpy).toHaveBeenCalledTimes(0); // + set log level call - expect(infoSpy).toHaveBeenCalledTimes(2) - checkLog(infoSpy, 'INFO', 1) + expect(infoSpy).toHaveBeenCalledTimes(2); + checkLog(infoSpy, "INFO", 1); - expect(warnSpy).toHaveBeenCalledTimes(1) - checkLog(warnSpy, 'WARN') + expect(warnSpy).toHaveBeenCalledTimes(1); + checkLog(warnSpy, "WARN"); - expect(errSpy).toHaveBeenCalledTimes(1) - checkErrorLog() - }) + expect(errSpy).toHaveBeenCalledTimes(1); + checkErrorLog(); + }); - it('should explicitly use WARN', async () => { + it("should explicitly use WARN", async () => { const logWriter = new LogWriter( new DefaultLogger(), module, ClickHouseLogLevel.WARN, - ) + ); - logEveryLogLevel(logWriter) + logEveryLogLevel(logWriter); // No TRACE, DEBUG, or INFO logs - expect(debugSpy).toHaveBeenCalledTimes(0) - expect(infoSpy).toHaveBeenCalledTimes(0) + expect(debugSpy).toHaveBeenCalledTimes(0); + expect(infoSpy).toHaveBeenCalledTimes(0); - expect(warnSpy).toHaveBeenCalledTimes(1) - checkLog(warnSpy, 'WARN') + expect(warnSpy).toHaveBeenCalledTimes(1); + checkLog(warnSpy, "WARN"); - expect(errSpy).toHaveBeenCalledTimes(1) - checkErrorLog() - }) + expect(errSpy).toHaveBeenCalledTimes(1); + checkErrorLog(); + }); - it('should explicitly use WARN', async () => { + it("should explicitly use WARN", async () => { const logWriter = new LogWriter( new DefaultLogger(), module, ClickHouseLogLevel.ERROR, - ) + ); - logEveryLogLevel(logWriter) + logEveryLogLevel(logWriter); // No TRACE, DEBUG, INFO, or WARN logs - expect(debugSpy).toHaveBeenCalledTimes(0) - expect(infoSpy).toHaveBeenCalledTimes(0) - expect(warnSpy).toHaveBeenCalledTimes(0) + expect(debugSpy).toHaveBeenCalledTimes(0); + expect(infoSpy).toHaveBeenCalledTimes(0); + expect(warnSpy).toHaveBeenCalledTimes(0); - expect(errSpy).toHaveBeenCalledTimes(1) - checkErrorLog() - }) + expect(errSpy).toHaveBeenCalledTimes(1); + checkErrorLog(); + }); function checkLogLevelSet(level: LogLevel) { expect(infoSpy.mock.calls[0]).toEqual([ expect.stringContaining( `[INFO][@clickhouse/client][${module}] Log level is set to ${level}`, ), - ]) - expect(infoSpy).toHaveBeenCalledTimes(1) + ]); + expect(infoSpy).toHaveBeenCalledTimes(1); } function checkLog(spy: any, level: LogLevel, callNumber = 0) { @@ -161,9 +161,9 @@ describe('[Node.js] Logger/LogWriter', () => { expect.stringContaining( `[${level}][@clickhouse/client][${module}] ${message}`, ), - expect.stringContaining('\nArguments:'), + expect.stringContaining("\nArguments:"), { foo: `${level.toLowerCase()}-42` }, - ]) + ]); } function checkErrorLog() { @@ -171,29 +171,29 @@ describe('[Node.js] Logger/LogWriter', () => { expect.stringContaining( `[ERROR][@clickhouse/client][${module}] ${message}`, ), - expect.stringContaining('\nArguments:'), - { foo: 'err-42' }, - expect.stringContaining('\nCaused by:'), + expect.stringContaining("\nArguments:"), + { foo: "err-42" }, + expect.stringContaining("\nCaused by:"), err, - ]) + ]); } function logEveryLogLevel(logWriter: LogWriter) { - for (const level of ['trace', 'debug', 'info', 'warn']) { + for (const level of ["trace", "debug", "info", "warn"]) { // @ts-ignore logWriter[level]({ message, args: { foo: `${level}-42`, }, - }) + }); } logWriter.error({ message, err, args: { - foo: 'err-42', + foo: "err-42", }, - }) + }); } -}) +}); diff --git a/packages/client-node/__tests__/unit/node_getAsText.test.ts b/packages/client-node/__tests__/unit/node_getAsText.test.ts index 7174b53e..d6c4ed97 100644 --- a/packages/client-node/__tests__/unit/node_getAsText.test.ts +++ b/packages/client-node/__tests__/unit/node_getAsText.test.ts @@ -1,37 +1,37 @@ -import { describe, expect, it } from 'vitest' -import Stream from 'stream' -import { constants } from 'buffer' -import { getAsText } from '../../src/utils/stream' +import { describe, expect, it } from "vitest"; +import Stream from "stream"; +import { constants } from "buffer"; +import { getAsText } from "../../src/utils/stream"; function makeStreamFromStrings(chunks: string[]): Stream.Readable { - const encoder = new TextEncoder() + const encoder = new TextEncoder(); async function* gen() { for (const chunk of chunks) { - yield encoder.encode(chunk) + yield encoder.encode(chunk); } } - return Stream.Readable.from(gen(), { objectMode: false }) + return Stream.Readable.from(gen(), { objectMode: false }); } function makeStreamFromBuffers(chunks: Buffer[]): Stream.Readable { async function* gen() { for (const chunk of chunks) { - yield chunk + yield chunk; } } - return Stream.Readable.from(gen(), { objectMode: false }) + return Stream.Readable.from(gen(), { objectMode: false }); } -describe('getAsText', () => { - it('should return a string containing the concatenated chunks', async () => { - expect(await getAsText(makeStreamFromStrings(['123', '456']))).toBe( - '123456', - ) - }) +describe("getAsText", () => { + it("should return a string containing the concatenated chunks", async () => { + expect(await getAsText(makeStreamFromStrings(["123", "456"]))).toBe( + "123456", + ); + }); - it('should throw a custom error if the stream is too long for a string', async () => { + it("should throw a custom error if the stream is too long for a string", async () => { // Passing the fill option is fine as Node always fills the buffer with zeroes otherwise - const bigChunk = Buffer.alloc((constants.MAX_STRING_LENGTH / 8) >> 0, 'a') + const bigChunk = Buffer.alloc((constants.MAX_STRING_LENGTH / 8) >> 0, "a"); await expect( getAsText( makeStreamFromBuffers([ @@ -48,13 +48,13 @@ describe('getAsText', () => { ]), ), ).rejects.toThrowError( - 'The response length exceeds the maximum allowed size of a string:', - ) - }) + "The response length exceeds the maximum allowed size of a string:", + ); + }); - it('should not throw on big but not too big streams', async () => { - const chunkSize = (constants.MAX_STRING_LENGTH / 8) >> 0 - const bigChunk = Buffer.alloc(chunkSize, 'b') + it("should not throw on big but not too big streams", async () => { + const chunkSize = (constants.MAX_STRING_LENGTH / 8) >> 0; + const bigChunk = Buffer.alloc(chunkSize, "b"); expect( await getAsText( makeStreamFromBuffers([ @@ -65,24 +65,24 @@ describe('getAsText', () => { bigChunk, ]), ), - ).toHaveLength(chunkSize * 5) - }) + ).toHaveLength(chunkSize * 5); + }); - it('should use streamed decoding and not break utf-8 characters', async () => { + it("should use streamed decoding and not break utf-8 characters", async () => { const stream = makeStreamFromBuffers([ Buffer.from([0xe2, 0x82]), // first 2 bytes of '€' Buffer.from([0xac, 0x20, 0x61]), // last byte of '€', space and 'a' - ]) - const text = '€ a' - expect(await getAsText(stream)).toBe(text) - }) + ]); + const text = "€ a"; + expect(await getAsText(stream)).toBe(text); + }); - it('should flush the decoder at the end of the stream', async () => { + it("should flush the decoder at the end of the stream", async () => { const stream = makeStreamFromBuffers([ Buffer.from([0x61, 0x20, 0xe2, 0x82]), // first 2 bytes of '€' // no more bytes, but the decoder should be flushed and return the bytes it has buffered - ]) - const text = 'a \ufffd' - expect(await getAsText(stream)).toBe(text) - }) -}) + ]); + const text = "a \ufffd"; + expect(await getAsText(stream)).toBe(text); + }); +}); diff --git a/packages/client-node/__tests__/unit/node_http_connection.test.ts b/packages/client-node/__tests__/unit/node_http_connection.test.ts index f0f1e4e6..2e8ca134 100644 --- a/packages/client-node/__tests__/unit/node_http_connection.test.ts +++ b/packages/client-node/__tests__/unit/node_http_connection.test.ts @@ -1,16 +1,16 @@ -import { describe, it, expect, vi } from 'vitest' -import Http from 'http' -import { ClickHouseLogLevel, LogWriter } from '@clickhouse/client-common' -import { TestLogger } from '../../../client-common/__tests__/utils/test_logger' -import type { NodeConnectionParams } from '../../src/connection' -import { NodeHttpConnection } from '../../src/connection' +import { describe, it, expect, vi } from "vitest"; +import Http from "http"; +import { ClickHouseLogLevel, LogWriter } from "@clickhouse/client-common"; +import { TestLogger } from "../../../client-common/__tests__/utils/test_logger"; +import type { NodeConnectionParams } from "../../src/connection"; +import { NodeHttpConnection } from "../../src/connection"; /** Extends NodeHttpConnection to expose protected methods for testing. */ class TestableHttpConnection extends NodeHttpConnection { public testCreateClientRequest( - ...args: Parameters + ...args: Parameters ): Http.ClientRequest { - return this.createClientRequest(...args) + return this.createClientRequest(...args); } } @@ -18,19 +18,19 @@ function buildHttpConnectionParams( overrides?: Partial, ): NodeConnectionParams { return { - url: new URL('http://localhost:8123'), + url: new URL("http://localhost:8123"), request_timeout: 30_000, compression: { decompress_response: false, compress_request: false, }, max_open_connections: 10, - auth: { username: 'default', password: '', type: 'Credentials' }, - database: 'default', + auth: { username: "default", password: "", type: "Credentials" }, + database: "default", clickhouse_settings: {}, log_writer: new LogWriter( new TestLogger(), - 'HttpConnectionTest', + "HttpConnectionTest", ClickHouseLogLevel.OFF, ), log_level: ClickHouseLogLevel.OFF, @@ -42,79 +42,81 @@ function buildHttpConnectionParams( capture_enhanced_stack_trace: false, eagerly_destroy_stale_sockets: false, ...overrides, - } + }; } -describe('[Node.js] NodeHttpConnection', () => { - describe('createClientRequest', () => { - it('should forward max_response_headers_size as maxHeaderSize when set', () => { - const mockRequest = {} as Http.ClientRequest +describe("[Node.js] NodeHttpConnection", () => { + describe("createClientRequest", () => { + it("should forward max_response_headers_size as maxHeaderSize when set", () => { + const mockRequest = {} as Http.ClientRequest; const httpRequestSpy = vi - .spyOn(Http, 'request') - .mockReturnValue(mockRequest) + .spyOn(Http, "request") + .mockReturnValue(mockRequest); const connection = new TestableHttpConnection( buildHttpConnectionParams({ max_response_headers_size: 64 * 1024, }), - ) + ); - const url = new URL('http://localhost:8123/?query_id=test') - const abortController = new AbortController() + const url = new URL("http://localhost:8123/?query_id=test"); + const abortController = new AbortController(); connection.testCreateClientRequest({ - method: 'GET', + method: "GET", url, headers: {}, abort_signal: abortController.signal, - query: 'SELECT 1', - query_id: 'test-query-id', + query: "SELECT 1", + query_id: "test-query-id", log_writer: new LogWriter( new TestLogger(), - 'HttpConnectionTest', + "HttpConnectionTest", ClickHouseLogLevel.OFF, ), log_level: ClickHouseLogLevel.OFF, - }) + }); - expect(httpRequestSpy).toHaveBeenCalledTimes(1) + expect(httpRequestSpy).toHaveBeenCalledTimes(1); const calledOptions = httpRequestSpy.mock - .calls[0][1] as Http.RequestOptions - expect(calledOptions.maxHeaderSize).toBe(64 * 1024) + .calls[0][1] as Http.RequestOptions; + expect(calledOptions.maxHeaderSize).toBe(64 * 1024); - httpRequestSpy.mockRestore() - }) + httpRequestSpy.mockRestore(); + }); - it('should not include maxHeaderSize when max_response_headers_size is undefined', () => { - const mockRequest = {} as Http.ClientRequest + it("should not include maxHeaderSize when max_response_headers_size is undefined", () => { + const mockRequest = {} as Http.ClientRequest; const httpRequestSpy = vi - .spyOn(Http, 'request') - .mockReturnValue(mockRequest) + .spyOn(Http, "request") + .mockReturnValue(mockRequest); - const connection = new TestableHttpConnection(buildHttpConnectionParams()) + const connection = new TestableHttpConnection( + buildHttpConnectionParams(), + ); - const url = new URL('http://localhost:8123/?query_id=test') - const abortController = new AbortController() + const url = new URL("http://localhost:8123/?query_id=test"); + const abortController = new AbortController(); connection.testCreateClientRequest({ - method: 'GET', + method: "GET", url, headers: {}, abort_signal: abortController.signal, - query: 'SELECT 1', - query_id: 'test-query-id', + query: "SELECT 1", + query_id: "test-query-id", log_writer: new LogWriter( new TestLogger(), - 'HttpConnectionTest', + "HttpConnectionTest", ClickHouseLogLevel.OFF, ), log_level: ClickHouseLogLevel.OFF, - }) + }); - expect(httpRequestSpy).toHaveBeenCalledTimes(1) + expect(httpRequestSpy).toHaveBeenCalledTimes(1); const calledOptions = httpRequestSpy.mock - .calls[0][1] as Http.RequestOptions - expect(calledOptions).not.toHaveProperty('maxHeaderSize') + .calls[0][1] as Http.RequestOptions; + expect(calledOptions).not.toHaveProperty("maxHeaderSize"); - httpRequestSpy.mockRestore() - }) - }) -}) + httpRequestSpy.mockRestore(); + }); + }); +}); diff --git a/packages/client-node/__tests__/unit/node_https_connection.test.ts b/packages/client-node/__tests__/unit/node_https_connection.test.ts index ae71aac6..04d30664 100644 --- a/packages/client-node/__tests__/unit/node_https_connection.test.ts +++ b/packages/client-node/__tests__/unit/node_https_connection.test.ts @@ -1,22 +1,22 @@ -import { describe, it, expect, vi } from 'vitest' -import type Http from 'http' -import Https from 'https' -import { ClickHouseLogLevel, LogWriter } from '@clickhouse/client-common' -import { TestLogger } from '../../../client-common/__tests__/utils/test_logger' -import type { NodeConnectionParams } from '../../src/connection' -import { NodeHttpsConnection } from '../../src/connection' +import { describe, it, expect, vi } from "vitest"; +import type Http from "http"; +import Https from "https"; +import { ClickHouseLogLevel, LogWriter } from "@clickhouse/client-common"; +import { TestLogger } from "../../../client-common/__tests__/utils/test_logger"; +import type { NodeConnectionParams } from "../../src/connection"; +import { NodeHttpsConnection } from "../../src/connection"; /** Extends NodeHttpsConnection to expose protected methods for testing. */ class TestableHttpsConnection extends NodeHttpsConnection { public getHeaders( - params?: Parameters[0], + params?: Parameters[0], ): Http.OutgoingHttpHeaders { - return this.buildRequestHeaders(params) + return this.buildRequestHeaders(params); } public testCreateClientRequest( - ...args: Parameters + ...args: Parameters ): Http.ClientRequest { - return this.createClientRequest(...args) + return this.createClientRequest(...args); } } @@ -24,19 +24,19 @@ function buildHttpsConnectionParams( overrides?: Partial, ): NodeConnectionParams { return { - url: new URL('https://localhost:8443'), + url: new URL("https://localhost:8443"), request_timeout: 30_000, compression: { decompress_response: false, compress_request: false, }, max_open_connections: 10, - auth: { username: 'default', password: '', type: 'Credentials' }, - database: 'default', + auth: { username: "default", password: "", type: "Credentials" }, + database: "default", clickhouse_settings: {}, log_writer: new LogWriter( new TestLogger(), - 'HttpsConnectionTest', + "HttpsConnectionTest", ClickHouseLogLevel.OFF, ), log_level: ClickHouseLogLevel.OFF, @@ -47,203 +47,203 @@ function buildHttpsConnectionParams( set_basic_auth_header: true, capture_enhanced_stack_trace: false, ...overrides, - } + }; } -describe('[Node.js] NodeHttpsConnection', () => { - describe('buildRequestHeaders', () => { - it('should use X-ClickHouse-User/Key headers with Basic TLS', () => { +describe("[Node.js] NodeHttpsConnection", () => { + describe("buildRequestHeaders", () => { + it("should use X-ClickHouse-User/Key headers with Basic TLS", () => { const connection = new TestableHttpsConnection( buildHttpsConnectionParams({ tls: { - type: 'Basic', - ca_cert: Buffer.from('ca_cert'), + type: "Basic", + ca_cert: Buffer.from("ca_cert"), }, }), - ) - const headers = connection.getHeaders() - expect(headers['X-ClickHouse-User']).toBe('default') - expect(headers['X-ClickHouse-Key']).toBe('') - expect(headers).not.toHaveProperty('Authorization') - expect(headers).not.toHaveProperty('X-ClickHouse-SSL-Certificate-Auth') - }) - - it('should add SSL-Certificate-Auth header with Mutual TLS', () => { + ); + const headers = connection.getHeaders(); + expect(headers["X-ClickHouse-User"]).toBe("default"); + expect(headers["X-ClickHouse-Key"]).toBe(""); + expect(headers).not.toHaveProperty("Authorization"); + expect(headers).not.toHaveProperty("X-ClickHouse-SSL-Certificate-Auth"); + }); + + it("should add SSL-Certificate-Auth header with Mutual TLS", () => { const connection = new TestableHttpsConnection( buildHttpsConnectionParams({ tls: { - type: 'Mutual', - ca_cert: Buffer.from('ca_cert'), - cert: Buffer.from('cert'), - key: Buffer.from('key'), + type: "Mutual", + ca_cert: Buffer.from("ca_cert"), + cert: Buffer.from("cert"), + key: Buffer.from("key"), }, }), - ) - const headers = connection.getHeaders() - expect(headers['X-ClickHouse-User']).toBe('default') - expect(headers['X-ClickHouse-Key']).toBe('') - expect(headers['X-ClickHouse-SSL-Certificate-Auth']).toBe('on') - }) - - it('should use per-request credentials when provided with TLS', () => { + ); + const headers = connection.getHeaders(); + expect(headers["X-ClickHouse-User"]).toBe("default"); + expect(headers["X-ClickHouse-Key"]).toBe(""); + expect(headers["X-ClickHouse-SSL-Certificate-Auth"]).toBe("on"); + }); + + it("should use per-request credentials when provided with TLS", () => { const connection = new TestableHttpsConnection( buildHttpsConnectionParams({ tls: { - type: 'Basic', - ca_cert: Buffer.from('ca_cert'), + type: "Basic", + ca_cert: Buffer.from("ca_cert"), }, }), - ) + ); const headers = connection.getHeaders({ - auth: { username: 'alice', password: 's3cret' }, - }) - expect(headers['X-ClickHouse-User']).toBe('alice') - expect(headers['X-ClickHouse-Key']).toBe('s3cret') - }) + auth: { username: "alice", password: "s3cret" }, + }); + expect(headers["X-ClickHouse-User"]).toBe("alice"); + expect(headers["X-ClickHouse-Key"]).toBe("s3cret"); + }); - it('should throw when JWT auth is used with TLS certificates', () => { + it("should throw when JWT auth is used with TLS certificates", () => { const connection = new TestableHttpsConnection( buildHttpsConnectionParams({ - auth: { type: 'JWT', access_token: 'token' }, + auth: { type: "JWT", access_token: "token" }, tls: { - type: 'Basic', - ca_cert: Buffer.from('ca_cert'), + type: "Basic", + ca_cert: Buffer.from("ca_cert"), }, }), - ) + ); expect(() => connection.getHeaders()).toThrow( - 'JWT auth is not supported with HTTPS connection using custom certificates', - ) - }) + "JWT auth is not supported with HTTPS connection using custom certificates", + ); + }); - it('should delegate to super when TLS is undefined', () => { + it("should delegate to super when TLS is undefined", () => { const connection = new TestableHttpsConnection( buildHttpsConnectionParams({ tls: undefined }), - ) - const headers = connection.getHeaders() + ); + const headers = connection.getHeaders(); // Without TLS, it falls through to the base class which uses Authorization header - expect(headers).toHaveProperty('Authorization') - expect(headers).not.toHaveProperty('X-ClickHouse-User') - expect(headers).not.toHaveProperty('X-ClickHouse-Key') - }) - }) - - describe('constructor', () => { - it('should create an Https.Agent with keep-alive and TLS params', () => { - const agentSpy = vi.spyOn(Https, 'Agent') - const caCert = Buffer.from('ca_cert') + expect(headers).toHaveProperty("Authorization"); + expect(headers).not.toHaveProperty("X-ClickHouse-User"); + expect(headers).not.toHaveProperty("X-ClickHouse-Key"); + }); + }); + + describe("constructor", () => { + it("should create an Https.Agent with keep-alive and TLS params", () => { + const agentSpy = vi.spyOn(Https, "Agent"); + const caCert = Buffer.from("ca_cert"); new TestableHttpsConnection( buildHttpsConnectionParams({ keep_alive: { enabled: true, idle_socket_ttl: 5000 }, max_open_connections: 5, tls: { - type: 'Basic', + type: "Basic", ca_cert: caCert, }, }), - ) + ); expect(agentSpy).toHaveBeenCalledWith({ keepAlive: true, maxSockets: 5, ca: caCert, key: undefined, cert: undefined, - }) - agentSpy.mockRestore() - }) - - it('should pass key and cert for Mutual TLS', () => { - const agentSpy = vi.spyOn(Https, 'Agent') - const caCert = Buffer.from('ca_cert') - const cert = Buffer.from('cert') - const key = Buffer.from('key') + }); + agentSpy.mockRestore(); + }); + + it("should pass key and cert for Mutual TLS", () => { + const agentSpy = vi.spyOn(Https, "Agent"); + const caCert = Buffer.from("ca_cert"); + const cert = Buffer.from("cert"); + const key = Buffer.from("key"); new TestableHttpsConnection( buildHttpsConnectionParams({ - tls: { type: 'Mutual', ca_cert: caCert, cert, key }, + tls: { type: "Mutual", ca_cert: caCert, cert, key }, }), - ) + ); expect(agentSpy).toHaveBeenCalledWith({ keepAlive: true, maxSockets: 10, ca: caCert, key, cert, - }) - agentSpy.mockRestore() - }) - }) - - describe('createClientRequest', () => { - it('should forward max_response_headers_size as maxHeaderSize when set', () => { - const mockRequest = {} as Http.ClientRequest + }); + agentSpy.mockRestore(); + }); + }); + + describe("createClientRequest", () => { + it("should forward max_response_headers_size as maxHeaderSize when set", () => { + const mockRequest = {} as Http.ClientRequest; const httpsRequestSpy = vi - .spyOn(Https, 'request') - .mockReturnValue(mockRequest) + .spyOn(Https, "request") + .mockReturnValue(mockRequest); const connection = new TestableHttpsConnection( buildHttpsConnectionParams({ max_response_headers_size: 64 * 1024, }), - ) + ); - const url = new URL('https://localhost:8443/?query_id=test') - const abortController = new AbortController() + const url = new URL("https://localhost:8443/?query_id=test"); + const abortController = new AbortController(); connection.testCreateClientRequest({ - method: 'GET', + method: "GET", url, headers: {}, abort_signal: abortController.signal, - query: 'SELECT 1', - query_id: 'test-query-id', + query: "SELECT 1", + query_id: "test-query-id", log_writer: new LogWriter( new TestLogger(), - 'HttpsConnectionTest', + "HttpsConnectionTest", ClickHouseLogLevel.OFF, ), log_level: ClickHouseLogLevel.OFF, - }) + }); - expect(httpsRequestSpy).toHaveBeenCalledTimes(1) + expect(httpsRequestSpy).toHaveBeenCalledTimes(1); const calledOptions = httpsRequestSpy.mock - .calls[0][1] as Http.RequestOptions - expect(calledOptions.maxHeaderSize).toBe(64 * 1024) + .calls[0][1] as Http.RequestOptions; + expect(calledOptions.maxHeaderSize).toBe(64 * 1024); - httpsRequestSpy.mockRestore() - }) + httpsRequestSpy.mockRestore(); + }); - it('should not include maxHeaderSize when max_response_headers_size is undefined', () => { - const mockRequest = {} as Http.ClientRequest + it("should not include maxHeaderSize when max_response_headers_size is undefined", () => { + const mockRequest = {} as Http.ClientRequest; const httpsRequestSpy = vi - .spyOn(Https, 'request') - .mockReturnValue(mockRequest) + .spyOn(Https, "request") + .mockReturnValue(mockRequest); const connection = new TestableHttpsConnection( buildHttpsConnectionParams(), - ) + ); - const url = new URL('https://localhost:8443/?query_id=test') - const abortController = new AbortController() + const url = new URL("https://localhost:8443/?query_id=test"); + const abortController = new AbortController(); connection.testCreateClientRequest({ - method: 'GET', + method: "GET", url, headers: {}, abort_signal: abortController.signal, - query: 'SELECT 1', - query_id: 'test-query-id', + query: "SELECT 1", + query_id: "test-query-id", log_writer: new LogWriter( new TestLogger(), - 'HttpsConnectionTest', + "HttpsConnectionTest", ClickHouseLogLevel.OFF, ), log_level: ClickHouseLogLevel.OFF, - }) + }); - expect(httpsRequestSpy).toHaveBeenCalledTimes(1) + expect(httpsRequestSpy).toHaveBeenCalledTimes(1); const calledOptions = httpsRequestSpy.mock - .calls[0][1] as Http.RequestOptions - expect(calledOptions).not.toHaveProperty('maxHeaderSize') + .calls[0][1] as Http.RequestOptions; + expect(calledOptions).not.toHaveProperty("maxHeaderSize"); - httpsRequestSpy.mockRestore() - }) - }) -}) + httpsRequestSpy.mockRestore(); + }); + }); +}); 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 9b5c0e1c..818fc990 100644 --- a/packages/client-node/__tests__/unit/node_result_set.test.ts +++ b/packages/client-node/__tests__/unit/node_result_set.test.ts @@ -6,205 +6,205 @@ import { beforeAll, afterAll, vi, -} from 'vitest' -import type { DataFormat, Row } from '@clickhouse/client-common' -import { guid } from '../../../client-common/__tests__/utils/guid' -import Stream, { Readable } from 'stream' -import { ResultSet } from '../../src' -import { isUsingStatementSupported } from '../utils/feature_detection' +} from "vitest"; +import type { DataFormat, Row } from "@clickhouse/client-common"; +import { guid } from "../../../client-common/__tests__/utils/guid"; +import Stream, { Readable } from "stream"; +import { ResultSet } from "../../src"; +import { isUsingStatementSupported } from "../utils/feature_detection"; beforeEach(() => { - vi.clearAllMocks() -}) + vi.clearAllMocks(); +}); -describe('[Node.js] ResultSet', () => { - const expectedText = `{"foo":"bar"}\n{"qaz":"qux"}\n` - const expectedJson = [{ foo: 'bar' }, { qaz: 'qux' }] +describe("[Node.js] ResultSet", () => { + const expectedText = `{"foo":"bar"}\n{"qaz":"qux"}\n`; + const expectedJson = [{ foo: "bar" }, { qaz: "qux" }]; - const errMsg = 'Stream has been already consumed' + const errMsg = "Stream has been already consumed"; const err = expect.objectContaining({ message: expect.stringContaining(errMsg), - }) + }); - it('should consume the response as text only once', async () => { - const rs = makeResultSet(getDataStream()) + it("should consume the response as text only once", async () => { + const rs = makeResultSet(getDataStream()); - expect(await rs.text()).toEqual(expectedText) - await expect(rs.text()).rejects.toEqual(err) - await expect(rs.json()).rejects.toEqual(err) - }) + expect(await rs.text()).toEqual(expectedText); + await expect(rs.text()).rejects.toEqual(err); + await expect(rs.json()).rejects.toEqual(err); + }); - it('should consume the response as JSON only once', async () => { - const rs = makeResultSet(getDataStream()) - expect(await rs.json()).toEqual(expectedJson) - await expect(rs.json()).rejects.toEqual(err) - await expect(rs.text()).rejects.toEqual(err) - }) + it("should consume the response as JSON only once", async () => { + const rs = makeResultSet(getDataStream()); + expect(await rs.json()).toEqual(expectedJson); + await expect(rs.json()).rejects.toEqual(err); + await expect(rs.text()).rejects.toEqual(err); + }); - it('should consume the response as a stream of Row instances', async () => { - const rs = makeResultSet(getDataStream()) - const stream = rs.stream() - expect(stream.readableEnded).toBeFalsy() + it("should consume the response as a stream of Row instances", async () => { + const rs = makeResultSet(getDataStream()); + const stream = rs.stream(); + expect(stream.readableEnded).toBeFalsy(); - const result: unknown[] = [] + const result: unknown[] = []; for await (const rows of stream) { rows.forEach((row: Row) => { - result.push(row.json()) - }) + result.push(row.json()); + }); } - expect(result).toEqual(expectedJson) - expect(stream.readableEnded).toBeTruthy() + expect(result).toEqual(expectedJson); + expect(stream.readableEnded).toBeTruthy(); - expect(() => rs.stream()).toThrow(new Error(errMsg)) - await expect(rs.json()).rejects.toEqual(err) - await expect(rs.text()).rejects.toEqual(err) - }) + expect(() => rs.stream()).toThrow(new Error(errMsg)); + await expect(rs.json()).rejects.toEqual(err); + 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')]) + 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', { + Object.defineProperty(stream, "readableEnded", { get: () => true, configurable: true, - }) + }); - const rs = makeResultSet(stream) + 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 }]) - }) + 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 () => { + 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') + "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') - }) + 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 () => { + 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() + "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]}') - }) + 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 () => { + 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')]), - ) - const allRows: Row[] = [] + ); + const allRows: Row[] = []; for await (const rows of rs.stream()) { - allRows.push(...rows) + allRows.push(...rows); } - expect(allRows.length).toEqual(1) - const [row] = allRows - expect(row.text).toEqual('{"foo":"bar"}') - expect(row.text).toEqual('{"foo":"bar"}') - expect(row.json()).toEqual({ foo: 'bar' }) - expect(row.json()).toEqual({ foo: 'bar' }) - }) - - describe.skip('unhandled exceptions with streamable JSON formats', () => { + expect(allRows.length).toEqual(1); + const [row] = allRows; + expect(row.text).toEqual('{"foo":"bar"}'); + expect(row.text).toEqual('{"foo":"bar"}'); + expect(row.json()).toEqual({ foo: "bar" }); + expect(row.json()).toEqual({ foo: "bar" }); + }); + + describe.skip("unhandled exceptions with streamable JSON formats", () => { const logAndQuit = (err: Error | unknown, prefix: string) => { - console.error(prefix, err) + console.error(prefix, err); expect.fail( `An unexpected error was propagated to the global context: ${prefix} ${err}`, - ) - } + ); + }; const uncaughtExceptionListener = (err: Error) => - logAndQuit(err, 'uncaughtException:') + logAndQuit(err, "uncaughtException:"); const unhandledRejectionListener = (err: unknown) => - logAndQuit(err, 'unhandledRejection:') + logAndQuit(err, "unhandledRejection:"); - const invalidJSON = 'invalid":"foo"}\n' + const invalidJSON = 'invalid":"foo"}\n'; beforeAll(() => { - process.on('uncaughtException', uncaughtExceptionListener) - process.on('unhandledRejection', unhandledRejectionListener) - }) + process.on("uncaughtException", uncaughtExceptionListener); + process.on("unhandledRejection", unhandledRejectionListener); + }); afterAll(() => { - process.off('uncaughtException', uncaughtExceptionListener) - process.off('unhandledRejection', unhandledRejectionListener) - }) + process.off("uncaughtException", uncaughtExceptionListener); + process.off("unhandledRejection", unhandledRejectionListener); + }); - describe('Streamable JSON formats - JSONEachRow', () => { - it('should not be produced (ResultSet.text)', async () => { + describe("Streamable JSON formats - JSONEachRow", () => { + it("should not be produced (ResultSet.text)", async () => { const rs = makeResultSet( Stream.Readable.from([Buffer.from(invalidJSON)]), - ) - const text = await rs.text() - expect(text).toEqual(invalidJSON) - }) + ); + const text = await rs.text(); + expect(text).toEqual(invalidJSON); + }); - it('should not be produced (ResultSet.json)', async () => { + it("should not be produced (ResultSet.json)", async () => { const rs = makeResultSet( Stream.Readable.from([Buffer.from(invalidJSON)]), - ) - const jsonPromise = rs.json() + ); + const jsonPromise = rs.json(); await expect(jsonPromise).rejects.toEqual( expect.objectContaining({ - name: 'SyntaxError', + name: "SyntaxError", }), - ) - }) - }) + ); + }); + }); - describe('Non-streamable JSON formats - JSON', () => { - it('should not be produced (ResultSet.text)', async () => { + describe("Non-streamable JSON formats - JSON", () => { + it("should not be produced (ResultSet.text)", async () => { const rs = makeResultSet( Stream.Readable.from([Buffer.from(invalidJSON)]), - 'JSON', - ) - const text = await rs.text() - expect(text).toEqual(invalidJSON) - }) + "JSON", + ); + const text = await rs.text(); + expect(text).toEqual(invalidJSON); + }); - it('should not be produced (ResultSet.json)', async () => { + it("should not be produced (ResultSet.json)", async () => { const rs = makeResultSet( Stream.Readable.from([Buffer.from(invalidJSON)]), - 'JSON', - ) - const jsonPromise = rs.json() + "JSON", + ); + const jsonPromise = rs.json(); await expect(jsonPromise).rejects.toEqual( expect.objectContaining({ - name: 'SyntaxError', + name: "SyntaxError", }), - ) - }) - }) - }) + ); + }); + }); + }); - it('closes the ResultSet when used with using statement', async (context) => { + it("closes the ResultSet when used with using statement", async (context) => { if (!isUsingStatementSupported()) { - context.skip('using statement is not supported in this environment') - return + context.skip("using statement is not supported in this environment"); + return; } - const rs = makeResultSet(getDataStream()) - let isClosed = false - vi.spyOn(rs, 'close').mockImplementation(() => { + const rs = makeResultSet(getDataStream()); + let isClosed = false; + vi.spyOn(rs, "close").mockImplementation(() => { // Simulate some delay in closing - isClosed = true - }) + isClosed = true; + }); // Wrap in eval to allow using statement syntax without // syntax error in older Node.js versions. Might want to @@ -214,30 +214,30 @@ describe('[Node.js] ResultSet', () => { using c = value; // do nothing, just testing the disposal at the end of the block }) - `)(rs) + `)(rs); - expect(isClosed).toBe(true) - }) + expect(isClosed).toBe(true); + }); function makeResultSet( stream: Stream.Readable, - format: DataFormat = 'JSONEachRow', + format: DataFormat = "JSONEachRow", ) { return ResultSet.instance({ stream, format, query_id: guid(), log_error: (err) => { - console.error(err) + console.error(err); }, response_headers: {}, - }) + }); } function getDataStream() { return Readable.from([ Buffer.from('{"foo":"bar"}\n'), Buffer.from('{"qaz":"qux"}\n'), - ]) + ]); } -}) +}); diff --git a/packages/client-node/__tests__/unit/node_result_set_extra.test.ts b/packages/client-node/__tests__/unit/node_result_set_extra.test.ts index 7255e7ac..96a11815 100644 --- a/packages/client-node/__tests__/unit/node_result_set_extra.test.ts +++ b/packages/client-node/__tests__/unit/node_result_set_extra.test.ts @@ -1,97 +1,97 @@ -import { describe, it, expect, vi, afterEach } from 'vitest' -import Stream from 'stream' -import { ResultSet } from '../../src' -import { guid } from '../../../client-common/__tests__/utils/guid' -import type { DataFormat } from '@clickhouse/client-common' +import { describe, it, expect, vi, afterEach } from "vitest"; +import Stream from "stream"; +import { ResultSet } from "../../src"; +import { guid } from "../../../client-common/__tests__/utils/guid"; +import type { DataFormat } from "@clickhouse/client-common"; -describe('[Node.js] ResultSet (extra coverage)', () => { +describe("[Node.js] ResultSet (extra coverage)", () => { afterEach(() => { - vi.restoreAllMocks() - }) + vi.restoreAllMocks(); + }); - describe('json()', () => { - it('should throw when calling json() with a non-JSON format', async () => { + describe("json()", () => { + it("should throw when calling json() with a non-JSON format", async () => { const rs = makeResultSet( - Stream.Readable.from([Buffer.from('a,b,c\n1,2,3\n')]), - 'CSV', - ) - await expect(rs.json()).rejects.toThrow('Cannot decode CSV as JSON') - }) + Stream.Readable.from([Buffer.from("a,b,c\n1,2,3\n")]), + "CSV", + ); + await expect(rs.json()).rejects.toThrow("Cannot decode CSV as JSON"); + }); - it('should parse non-streamable JSON format (JSON)', async () => { - const data = JSON.stringify({ data: [{ x: 1 }] }) + it("should parse non-streamable JSON format (JSON)", async () => { + const data = JSON.stringify({ data: [{ x: 1 }] }); const rs = makeResultSet( Stream.Readable.from([Buffer.from(data)]), - 'JSON', - ) - const result = await rs.json() - expect(result).toEqual({ data: [{ x: 1 }] }) - }) + "JSON", + ); + const result = await rs.json(); + expect(result).toEqual({ data: [{ x: 1 }] }); + }); - it('should parse non-streamable JSON format (JSONObjectEachRow)', async () => { - const data = JSON.stringify({ row1: { x: 1 }, row2: { x: 2 } }) + it("should parse non-streamable JSON format (JSONObjectEachRow)", async () => { + const data = JSON.stringify({ row1: { x: 1 }, row2: { x: 2 } }); const rs = makeResultSet( Stream.Readable.from([Buffer.from(data)]), - 'JSONObjectEachRow', - ) - const result = await rs.json() - expect(result).toEqual({ row1: { x: 1 }, row2: { x: 2 } }) - }) - }) + "JSONObjectEachRow", + ); + const result = await rs.json(); + expect(result).toEqual({ row1: { x: 1 }, row2: { x: 2 } }); + }); + }); - describe('close()', () => { - it('should destroy the underlying stream', async () => { + describe("close()", () => { + it("should destroy the underlying stream", async () => { const stream = new Stream.Readable({ read() { // never push data; the stream stays open }, - }) + }); // Attach an error listener to avoid unhandled error propagation - stream.on('error', () => { + stream.on("error", () => { // expected: ResultSet.close() destroys the stream with an error - }) - const rs = makeResultSet(stream, 'JSONEachRow') + }); + const rs = makeResultSet(stream, "JSONEachRow"); - expect(stream.destroyed).toBe(false) - rs.close() - expect(stream.destroyed).toBe(true) - }) - }) + expect(stream.destroyed).toBe(false); + rs.close(); + expect(stream.destroyed).toBe(true); + }); + }); - describe('stream()', () => { - it('should throw when streaming a non-streamable format', () => { + describe("stream()", () => { + it("should throw when streaming a non-streamable format", () => { const rs = makeResultSet( - Stream.Readable.from([Buffer.from('{}')]), - 'JSON', - ) - expect(() => rs.stream()).toThrow(/JSON format is not streamable/) - }) - }) + Stream.Readable.from([Buffer.from("{}")]), + "JSON", + ); + expect(() => rs.stream()).toThrow(/JSON format is not streamable/); + }); + }); - describe('constructor defaults', () => { - it('should use console.error as default log_error', async () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { + describe("constructor defaults", () => { + it("should use console.error as default log_error", async () => { + const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => { // noop - }) + }); const errorStream = new Stream.Readable({ read() { queueMicrotask(() => { - this.destroy(new Error('test stream error')) - }) + this.destroy(new Error("test stream error")); + }); }, - }) + }); const rs = new ResultSet( errorStream, - 'JSONEachRow', + "JSONEachRow", guid(), // log_error omitted — should default to console.error - ) - const pipelineStream = rs.stream() + ); + const pipelineStream = rs.stream(); const done = new Promise((resolve) => { - pipelineStream.once('error', () => resolve()) - pipelineStream.once('close', () => resolve()) - pipelineStream.once('end', () => resolve()) - }) + pipelineStream.once("error", () => resolve()); + pipelineStream.once("close", () => resolve()); + pipelineStream.once("end", () => resolve()); + }); // Consume the stream to trigger the pipeline error callback try { for await (const _ of pipelineStream) { @@ -101,31 +101,31 @@ describe('[Node.js] ResultSet (extra coverage)', () => { // stream error expected } // Wait deterministically for the pipeline to complete before asserting - await done - expect(consoleSpy).toHaveBeenCalled() - }) + await done; + expect(consoleSpy).toHaveBeenCalled(); + }); - it('should freeze response_headers', () => { - const headers = { 'content-type': 'application/json' } + it("should freeze response_headers", () => { + const headers = { "content-type": "application/json" }; const rs = ResultSet.instance({ stream: Stream.Readable.from([]), - format: 'JSONEachRow', + format: "JSONEachRow", query_id: guid(), log_error: () => { // noop }, response_headers: headers, - }) - expect(rs.response_headers).toEqual(headers) - expect(Object.isFrozen(rs.response_headers)).toBe(true) - }) + }); + expect(rs.response_headers).toEqual(headers); + expect(Object.isFrozen(rs.response_headers)).toBe(true); + }); - it('should have empty response_headers when not provided', () => { - const rs = new ResultSet(Stream.Readable.from([]), 'JSONEachRow', guid()) - expect(rs.response_headers).toEqual({}) - }) - }) -}) + it("should have empty response_headers when not provided", () => { + const rs = new ResultSet(Stream.Readable.from([]), "JSONEachRow", guid()); + expect(rs.response_headers).toEqual({}); + }); + }); +}); function makeResultSet(stream: Stream.Readable, format: DataFormat) { return ResultSet.instance({ @@ -136,5 +136,5 @@ function makeResultSet(stream: Stream.Readable, format: DataFormat) { // noop }, response_headers: {}, - }) + }); } diff --git a/packages/client-node/__tests__/unit/node_stream.test.ts b/packages/client-node/__tests__/unit/node_stream.test.ts index 21f0c8ad..5394004f 100644 --- a/packages/client-node/__tests__/unit/node_stream.test.ts +++ b/packages/client-node/__tests__/unit/node_stream.test.ts @@ -1,109 +1,109 @@ -import { describe, it, expect } from 'vitest' -import { drainStream } from '../../src/connection/stream' -import stream from 'stream' +import { describe, it, expect } from "vitest"; +import { drainStream } from "../../src/connection/stream"; +import stream from "stream"; -const nextTick = () => new Promise((resolve) => process.nextTick(resolve)) +const nextTick = () => new Promise((resolve) => process.nextTick(resolve)); -describe('drainStream', () => { - it('resolves when the stream ends', async () => { - let ended = false +describe("drainStream", () => { + it("resolves when the stream ends", async () => { + let ended = false; const readable = new stream.Readable({ read() { - this.push('data') - this.push(null) // end the stream - ended = true + this.push("data"); + this.push(null); // end the stream + ended = true; }, - }) + }); - expect(ended).toBe(false) - await expect(drainStream(readable)).resolves.toBeUndefined() - expect(ended).toBe(true) - }) + expect(ended).toBe(false); + await expect(drainStream(readable)).resolves.toBeUndefined(); + expect(ended).toBe(true); + }); - it('resolves when the stream is already ended', async () => { - let ended = false + it("resolves when the stream is already ended", async () => { + let ended = false; const readable = new stream.Readable({ read() { - this.push('data') - this.push(null) // end the stream - ended = true + this.push("data"); + this.push(null); // end the stream + ended = true; }, - }) + }); - expect(ended).toBe(false) + expect(ended).toBe(false); for await (const _ of readable) { // consume the stream } - expect(ended).toBe(true) - await expect(drainStream(readable)).resolves.toBeUndefined() - }) + expect(ended).toBe(true); + await expect(drainStream(readable)).resolves.toBeUndefined(); + }); - it('rejects when the stream emits an error', async () => { - let errored = false + it("rejects when the stream emits an error", async () => { + let errored = false; const readable = new stream.Readable({ read() { - this.emit('error', new Error('Stream error A')) - errored = true + this.emit("error", new Error("Stream error A")); + errored = true; }, - }) + }); - expect(errored).toBe(false) - await expect(drainStream(readable)).rejects.toThrow('Stream error A') - expect(errored).toBe(true) - }) + expect(errored).toBe(false); + await expect(drainStream(readable)).rejects.toThrow("Stream error A"); + expect(errored).toBe(true); + }); - it('rejects when the stream throws', async () => { - let errored = false + it("rejects when the stream throws", async () => { + let errored = false; const readable = new stream.Readable({ read() { - errored = true - throw new Error('Stream error B') + errored = true; + throw new Error("Stream error B"); }, - }) + }); - expect(errored).toBe(false) - await expect(drainStream(readable)).rejects.toThrow('Stream error') - expect(errored).toBe(true) - }) + expect(errored).toBe(false); + await expect(drainStream(readable)).rejects.toThrow("Stream error"); + expect(errored).toBe(true); + }); - it('rejects when the stream is already errored', async () => { - let errored = false + it("rejects when the stream is already errored", async () => { + let errored = false; const readable = new stream.Readable({ read() { - this.emit('error', new Error('Stream error C')) - errored = true + this.emit("error", new Error("Stream error C")); + errored = true; }, - }) + }); - expect(errored).toBe(false) + expect(errored).toBe(false); try { for await (const _ of readable) { // consume the stream } } catch {} - expect(errored).toBe(true) + expect(errored).toBe(true); await expect(drainStream(readable)).rejects.toThrow( - 'The operation was aborted', - ) - }) + "The operation was aborted", + ); + }); - it('resolves when the stream is already closed', async () => { - let closed = false + it("resolves when the stream is already closed", async () => { + let closed = false; const readable = new stream.Readable({ emitClose: true, read() { - this.push('data') + this.push("data"); }, - }) - readable.on('close', () => { - closed = true - }) + }); + readable.on("close", () => { + closed = true; + }); - expect(closed).toBe(false) - readable.destroy() // close the stream - await nextTick() // wait for the close event to be emitted - expect(closed).toBe(true) + expect(closed).toBe(false); + readable.destroy(); // close the stream + await nextTick(); // wait for the close event to be emitted + expect(closed).toBe(true); - await expect(drainStream(readable)).resolves.toBeUndefined() - }) -}) + await expect(drainStream(readable)).resolves.toBeUndefined(); + }); +}); diff --git a/packages/client-node/__tests__/unit/node_stream_internal.test.ts b/packages/client-node/__tests__/unit/node_stream_internal.test.ts index cc8a7159..e459a9ed 100644 --- a/packages/client-node/__tests__/unit/node_stream_internal.test.ts +++ b/packages/client-node/__tests__/unit/node_stream_internal.test.ts @@ -1,140 +1,140 @@ -import { describe, it, expect, vi, beforeAll } from 'vitest' +import { describe, it, expect, vi, beforeAll } from "vitest"; import { DefaultLogger, LogWriter, ClickHouseLogLevel, -} from '@clickhouse/client-common' -import { drainStreamInternal, type Context } from '../../src/connection/stream' -import stream from 'stream' +} from "@clickhouse/client-common"; +import { drainStreamInternal, type Context } from "../../src/connection/stream"; +import stream from "stream"; -const nextTick = () => new Promise((resolve) => process.nextTick(resolve)) +const nextTick = () => new Promise((resolve) => process.nextTick(resolve)); -describe('drainStreamInternal', () => { - let log_writer: LogWriter - let context: Context +describe("drainStreamInternal", () => { + let log_writer: LogWriter; + let context: Context; beforeAll(() => { log_writer = new LogWriter( new DefaultLogger(), - 'Connection', + "Connection", ClickHouseLogLevel.OFF, - ) + ); context = { - op: 'Insert', - query_id: 'test-query-id', + op: "Insert", + query_id: "test-query-id", log_writer, log_level: ClickHouseLogLevel.OFF, - } - }) - it('resolves when the stream ends', async () => { - let ended = false + }; + }); + it("resolves when the stream ends", async () => { + let ended = false; const readable = new stream.Readable({ read() { - this.push('data') - this.push(null) // end the stream - ended = true + this.push("data"); + this.push(null); // end the stream + ended = true; }, - }) + }); - expect(ended).toBe(false) + expect(ended).toBe(false); await expect( drainStreamInternal(context, readable), - ).resolves.toBeUndefined() - expect(ended).toBe(true) - }) + ).resolves.toBeUndefined(); + expect(ended).toBe(true); + }); - it('resolves when the stream is already ended', async () => { - let ended = false + it("resolves when the stream is already ended", async () => { + let ended = false; const readable = new stream.Readable({ read() { - this.push('data') - this.push(null) // end the stream - ended = true + this.push("data"); + this.push(null); // end the stream + ended = true; }, - }) + }); - expect(ended).toBe(false) + expect(ended).toBe(false); for await (const _ of readable) { // consume the stream } - expect(ended).toBe(true) + expect(ended).toBe(true); await expect( drainStreamInternal(context, readable), - ).resolves.toBeUndefined() - }) + ).resolves.toBeUndefined(); + }); - it('rejects when the stream emits an error', async () => { - let errored = false + it("rejects when the stream emits an error", async () => { + let errored = false; const readable = new stream.Readable({ read() { - this.emit('error', new Error('Stream error A')) - errored = true + this.emit("error", new Error("Stream error A")); + errored = true; }, - }) + }); - expect(errored).toBe(false) + expect(errored).toBe(false); await expect(drainStreamInternal(context, readable)).rejects.toThrow( - 'Stream error A', - ) - expect(errored).toBe(true) - }) + "Stream error A", + ); + expect(errored).toBe(true); + }); - it('rejects when the stream throws', async () => { - let errored = false + it("rejects when the stream throws", async () => { + let errored = false; const readable = new stream.Readable({ read() { - errored = true - throw new Error('Stream error B') + errored = true; + throw new Error("Stream error B"); }, - }) + }); - expect(errored).toBe(false) + expect(errored).toBe(false); await expect(drainStreamInternal(context, readable)).rejects.toThrow( - 'Stream error', - ) - expect(errored).toBe(true) - }) + "Stream error", + ); + expect(errored).toBe(true); + }); - it('rejects when the stream is already errored', async () => { - let errored = false + it("rejects when the stream is already errored", async () => { + let errored = false; const readable = new stream.Readable({ read() { - this.emit('error', new Error('Stream error C')) - errored = true + this.emit("error", new Error("Stream error C")); + errored = true; }, - }) + }); - expect(errored).toBe(false) + expect(errored).toBe(false); try { for await (const _ of readable) { // consume the stream } } catch {} - expect(errored).toBe(true) + expect(errored).toBe(true); await expect(drainStreamInternal(context, readable)).rejects.toThrow( - 'The operation was aborted', - ) - }) + "The operation was aborted", + ); + }); - it('resolves when the stream is already closed', async () => { - let closed = false + it("resolves when the stream is already closed", async () => { + let closed = false; const readable = new stream.Readable({ emitClose: true, read() { - this.push('data') + this.push("data"); }, - }) - readable.on('close', () => { - closed = true - }) + }); + readable.on("close", () => { + closed = true; + }); - expect(closed).toBe(false) - readable.destroy() // close the stream - await nextTick() // wait for the close event to be emitted - expect(closed).toBe(true) + expect(closed).toBe(false); + readable.destroy(); // close the stream + await nextTick(); // wait for the close event to be emitted + expect(closed).toBe(true); await expect( drainStreamInternal(context, readable), - ).resolves.toBeUndefined() - }) -}) + ).resolves.toBeUndefined(); + }); +}); diff --git a/packages/client-node/__tests__/unit/node_stream_internal_trace.test.ts b/packages/client-node/__tests__/unit/node_stream_internal_trace.test.ts index 636e5b2b..55ae1d79 100644 --- a/packages/client-node/__tests__/unit/node_stream_internal_trace.test.ts +++ b/packages/client-node/__tests__/unit/node_stream_internal_trace.test.ts @@ -1,82 +1,82 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' +import { describe, it, expect, vi, beforeEach } from "vitest"; import { DefaultLogger, LogWriter, ClickHouseLogLevel, -} from '@clickhouse/client-common' -import { drainStreamInternal, type Context } from '../../src/connection/stream' -import stream from 'stream' +} from "@clickhouse/client-common"; +import { drainStreamInternal, type Context } from "../../src/connection/stream"; +import stream from "stream"; -const nextTick = () => new Promise((resolve) => process.nextTick(resolve)) +const nextTick = () => new Promise((resolve) => process.nextTick(resolve)); -describe('drainStreamInternal (TRACE logging)', () => { - let traceSpy: ReturnType - let log_writer: LogWriter - let context: Context +describe("drainStreamInternal (TRACE logging)", () => { + let traceSpy: ReturnType; + let log_writer: LogWriter; + let context: Context; beforeEach(() => { - const logger = new DefaultLogger() - traceSpy = vi.spyOn(logger, 'trace') - log_writer = new LogWriter(logger, 'Connection', ClickHouseLogLevel.TRACE) + const logger = new DefaultLogger(); + traceSpy = vi.spyOn(logger, "trace"); + log_writer = new LogWriter(logger, "Connection", ClickHouseLogLevel.TRACE); context = { - op: 'Insert', - query_id: 'trace-test-query-id', + op: "Insert", + query_id: "trace-test-query-id", log_writer, log_level: ClickHouseLogLevel.TRACE, - } - }) + }; + }); - it('should log starting stream drain and stream drain completed on end', async () => { + it("should log starting stream drain and stream drain completed on end", async () => { const readable = new stream.Readable({ read() { - this.push('data') - this.push(null) + this.push("data"); + this.push(null); }, - }) + }); await expect( drainStreamInternal(context, readable), - ).resolves.toBeUndefined() + ).resolves.toBeUndefined(); - const calls = traceSpy.mock.calls.map((c) => c[0].message) - expect(calls).toContain('Insert: starting stream drain') - expect(calls).toContain('Insert: received data chunk during drain') - expect(calls).toContain('Insert: stream drain completed (end event)') - }) + const calls = traceSpy.mock.calls.map((c) => c[0].message); + expect(calls).toContain("Insert: starting stream drain"); + expect(calls).toContain("Insert: received data chunk during drain"); + expect(calls).toContain("Insert: stream drain completed (end event)"); + }); - it('should log chunk details when receiving data during drain', async () => { + it("should log chunk details when receiving data during drain", async () => { const readable = new stream.Readable({ read() { - this.push('hello') - this.push('world') - this.push(null) + this.push("hello"); + this.push("world"); + this.push(null); }, - }) + }); await expect( drainStreamInternal(context, readable), - ).resolves.toBeUndefined() + ).resolves.toBeUndefined(); const chunkCalls = traceSpy.mock.calls.filter( - (c) => c[0].message === 'Insert: received data chunk during drain', - ) - expect(chunkCalls.length).toBeGreaterThanOrEqual(1) + (c) => c[0].message === "Insert: received data chunk during drain", + ); + expect(chunkCalls.length).toBeGreaterThanOrEqual(1); expect(chunkCalls[0][0].args).toEqual( expect.objectContaining({ - query_id: 'trace-test-query-id', + query_id: "trace-test-query-id", chunk_number: 1, chunk_size: expect.any(Number), total_bytes_received: expect.any(Number), }), - ) - }) + ); + }); - it('should log stream already errored before drain', async () => { + it("should log stream already errored before drain", async () => { const readable = new stream.Readable({ read() { - this.emit('error', new Error('Pre-existing error')) + this.emit("error", new Error("Pre-existing error")); }, - }) + }); try { for await (const _ of readable) { @@ -86,20 +86,20 @@ describe('drainStreamInternal (TRACE logging)', () => { // expected } - await expect(drainStreamInternal(context, readable)).rejects.toThrow() + await expect(drainStreamInternal(context, readable)).rejects.toThrow(); - const calls = traceSpy.mock.calls.map((c) => c[0].message) - expect(calls).toContain('Insert: starting stream drain') - expect(calls).toContain('Insert: stream already errored before drain') - }) + const calls = traceSpy.mock.calls.map((c) => c[0].message); + expect(calls).toContain("Insert: starting stream drain"); + expect(calls).toContain("Insert: stream already errored before drain"); + }); - it('should log stream already ended before drain', async () => { + it("should log stream already ended before drain", async () => { const readable = new stream.Readable({ read() { - this.push('data') - this.push(null) + this.push("data"); + this.push(null); }, - }) + }); for await (const _ of readable) { // consume the stream @@ -107,125 +107,125 @@ describe('drainStreamInternal (TRACE logging)', () => { await expect( drainStreamInternal(context, readable), - ).resolves.toBeUndefined() + ).resolves.toBeUndefined(); - const calls = traceSpy.mock.calls.map((c) => c[0].message) - expect(calls).toContain('Insert: starting stream drain') - expect(calls).toContain('Insert: stream already ended before drain') - }) + const calls = traceSpy.mock.calls.map((c) => c[0].message); + expect(calls).toContain("Insert: starting stream drain"); + expect(calls).toContain("Insert: stream already ended before drain"); + }); - it('should log stream already closed before drain', async () => { + it("should log stream already closed before drain", async () => { const readable = new stream.Readable({ emitClose: true, read() { - this.push('data') + this.push("data"); }, - }) + }); - readable.destroy() - await nextTick() + readable.destroy(); + await nextTick(); await expect( drainStreamInternal(context, readable), - ).resolves.toBeUndefined() + ).resolves.toBeUndefined(); - const calls = traceSpy.mock.calls.map((c) => c[0].message) - expect(calls).toContain('Insert: starting stream drain') - expect(calls).toContain('Insert: stream already closed before drain') - }) + const calls = traceSpy.mock.calls.map((c) => c[0].message); + expect(calls).toContain("Insert: starting stream drain"); + expect(calls).toContain("Insert: stream already closed before drain"); + }); - it('should log stream drain failed on error event', async () => { + it("should log stream drain failed on error event", async () => { const readable = new stream.Readable({ read() { - this.emit('error', new Error('Stream error during drain')) + this.emit("error", new Error("Stream error during drain")); }, - }) + }); await expect(drainStreamInternal(context, readable)).rejects.toThrow( - 'Stream error during drain', - ) + "Stream error during drain", + ); - const calls = traceSpy.mock.calls.map((c) => c[0].message) - expect(calls).toContain('Insert: starting stream drain') - expect(calls).toContain('Insert: stream drain failed (error event)') + const calls = traceSpy.mock.calls.map((c) => c[0].message); + expect(calls).toContain("Insert: starting stream drain"); + expect(calls).toContain("Insert: stream drain failed (error event)"); const errorCall = traceSpy.mock.calls.find( - (c) => c[0].message === 'Insert: stream drain failed (error event)', - ) + (c) => c[0].message === "Insert: stream drain failed (error event)", + ); expect(errorCall![0].args).toEqual( expect.objectContaining({ - query_id: 'trace-test-query-id', - error: 'Stream error during drain', + query_id: "trace-test-query-id", + error: "Stream error during drain", }), - ) - }) + ); + }); - it('should log stream closed during drain on close event', async () => { + it("should log stream closed during drain on close event", async () => { const readable = new stream.Readable({ emitClose: true, read() { // don't push any data; the stream will be destroyed externally }, - }) + }); - const drainPromise = drainStreamInternal(context, readable) + const drainPromise = drainStreamInternal(context, readable); // Need a tick for the stream listeners to be attached - await nextTick() - readable.destroy() + await nextTick(); + readable.destroy(); - await expect(drainPromise).resolves.toBeUndefined() + await expect(drainPromise).resolves.toBeUndefined(); - const calls = traceSpy.mock.calls.map((c) => c[0].message) - expect(calls).toContain('Insert: starting stream drain') - expect(calls).toContain('Insert: stream closed during drain (close event)') - }) + const calls = traceSpy.mock.calls.map((c) => c[0].message); + expect(calls).toContain("Insert: starting stream drain"); + expect(calls).toContain("Insert: stream closed during drain (close event)"); + }); - it('should include stream state in the starting drain log', async () => { + it("should include stream state in the starting drain log", async () => { const readable = new stream.Readable({ read() { - this.push(null) + this.push(null); }, - }) + }); - await drainStreamInternal(context, readable) + await drainStreamInternal(context, readable); const startCall = traceSpy.mock.calls.find( - (c) => c[0].message === 'Insert: starting stream drain', - ) - expect(startCall).toBeDefined() + (c) => c[0].message === "Insert: starting stream drain", + ); + expect(startCall).toBeDefined(); expect(startCall![0].args).toEqual( expect.objectContaining({ - query_id: 'trace-test-query-id', + query_id: "trace-test-query-id", stream_state: expect.objectContaining({ readable: expect.any(Boolean), readableEnded: expect.any(Boolean), readableLength: expect.any(Number), }), }), - ) - }) + ); + }); - it('should include duration and totals in the end log', async () => { + it("should include duration and totals in the end log", async () => { const readable = new stream.Readable({ read() { - this.push('some data') - this.push(null) + this.push("some data"); + this.push(null); }, - }) + }); - await drainStreamInternal(context, readable) + await drainStreamInternal(context, readable); const endCall = traceSpy.mock.calls.find( - (c) => c[0].message === 'Insert: stream drain completed (end event)', - ) - expect(endCall).toBeDefined() + (c) => c[0].message === "Insert: stream drain completed (end event)", + ); + expect(endCall).toBeDefined(); expect(endCall![0].args).toEqual( expect.objectContaining({ - query_id: 'trace-test-query-id', + query_id: "trace-test-query-id", duration_ms: expect.any(Number), total_bytes_received: expect.any(Number), total_chunks: expect.any(Number), }), - ) - }) -}) + ); + }); +}); diff --git a/packages/client-node/__tests__/unit/node_user_agent.test.ts b/packages/client-node/__tests__/unit/node_user_agent.test.ts index fb49bfc3..8c3242cb 100644 --- a/packages/client-node/__tests__/unit/node_user_agent.test.ts +++ b/packages/client-node/__tests__/unit/node_user_agent.test.ts @@ -1,26 +1,26 @@ -import { describe, it, expect, vi, beforeAll } from 'vitest' -import { getUserAgent } from '../../src/utils' -import { Runtime } from '../../src/utils/runtime' +import { describe, it, expect, vi, beforeAll } from "vitest"; +import { getUserAgent } from "../../src/utils"; +import { Runtime } from "../../src/utils/runtime"; beforeAll(() => { // Mock Runtime to have a fixed package version and node version for testing - vi.spyOn(Runtime, 'package', 'get').mockReturnValue('0.0.42') - vi.spyOn(Runtime, 'node', 'get').mockReturnValue('v16.144') - vi.spyOn(Runtime, 'os', 'get').mockReturnValue('freebsd') -}) + vi.spyOn(Runtime, "package", "get").mockReturnValue("0.0.42"); + vi.spyOn(Runtime, "node", "get").mockReturnValue("v16.144"); + vi.spyOn(Runtime, "os", "get").mockReturnValue("freebsd"); +}); -describe('[Node.js] User-Agent', () => { - it('should generate a user agent without app id', async () => { - const userAgent = getUserAgent() +describe("[Node.js] User-Agent", () => { + it("should generate a user agent without app id", async () => { + const userAgent = getUserAgent(); expect(userAgent).toEqual( - 'clickhouse-js/0.0.42 (lv:nodejs/v16.144; os:freebsd)', - ) - }) + "clickhouse-js/0.0.42 (lv:nodejs/v16.144; os:freebsd)", + ); + }); - it('should generate a user agent with app id', async () => { - const userAgent = getUserAgent() + it("should generate a user agent with app id", async () => { + const userAgent = getUserAgent(); expect(userAgent).toEqual( - 'clickhouse-js/0.0.42 (lv:nodejs/v16.144; os:freebsd)', - ) - }) -}) + "clickhouse-js/0.0.42 (lv:nodejs/v16.144; os:freebsd)", + ); + }); +}); diff --git a/packages/client-node/__tests__/unit/node_values_encoder.test.ts b/packages/client-node/__tests__/unit/node_values_encoder.test.ts index e69e390b..540f9f1f 100644 --- a/packages/client-node/__tests__/unit/node_values_encoder.test.ts +++ b/packages/client-node/__tests__/unit/node_values_encoder.test.ts @@ -1,300 +1,300 @@ -import { describe, it, expect } from 'vitest' +import { describe, it, expect } from "vitest"; import type { DataFormat, InputJSON, InputJSONObjectEachRow, -} from '@clickhouse/client-common' -import Stream from 'stream' -import { NodeValuesEncoder } from '../../src/utils' -import * as simdjson from 'simdjson' +} from "@clickhouse/client-common"; +import Stream from "stream"; +import { NodeValuesEncoder } from "../../src/utils"; +import * as simdjson from "simdjson"; -describe('[Node.js] ValuesEncoder', () => { +describe("[Node.js] ValuesEncoder", () => { const rawFormats = [ - 'CSV', - 'CSVWithNames', - 'CSVWithNamesAndTypes', - 'TabSeparated', - 'TabSeparatedRaw', - 'TabSeparatedWithNames', - 'TabSeparatedWithNamesAndTypes', - 'CustomSeparated', - 'CustomSeparatedWithNames', - 'CustomSeparatedWithNamesAndTypes', - ] + "CSV", + "CSVWithNames", + "CSVWithNamesAndTypes", + "TabSeparated", + "TabSeparatedRaw", + "TabSeparatedWithNames", + "TabSeparatedWithNamesAndTypes", + "CustomSeparated", + "CustomSeparatedWithNames", + "CustomSeparatedWithNamesAndTypes", + ]; const objectFormats = [ - 'JSON', - 'JSONObjectEachRow', - 'JSONEachRow', - 'JSONStringsEachRow', - 'JSONCompactEachRow', - 'JSONCompactEachRowWithNames', - 'JSONCompactEachRowWithNamesAndTypes', - 'JSONCompactStringsEachRowWithNames', - 'JSONCompactStringsEachRowWithNamesAndTypes', - ] + "JSON", + "JSONObjectEachRow", + "JSONEachRow", + "JSONStringsEachRow", + "JSONCompactEachRow", + "JSONCompactEachRowWithNames", + "JSONCompactEachRowWithNamesAndTypes", + "JSONCompactStringsEachRowWithNames", + "JSONCompactStringsEachRowWithNamesAndTypes", + ]; const jsonFormats = [ - 'JSON', - 'JSONStrings', - 'JSONCompact', - 'JSONCompactStrings', - 'JSONColumnsWithMetadata', - 'JSONObjectEachRow', - 'JSONEachRow', - 'JSONStringsEachRow', - 'JSONCompactEachRow', - 'JSONCompactEachRowWithNames', - 'JSONCompactEachRowWithNamesAndTypes', - 'JSONCompactStringsEachRowWithNames', - 'JSONCompactStringsEachRowWithNamesAndTypes', - ] + "JSON", + "JSONStrings", + "JSONCompact", + "JSONCompactStrings", + "JSONColumnsWithMetadata", + "JSONObjectEachRow", + "JSONEachRow", + "JSONStringsEachRow", + "JSONCompactEachRow", + "JSONCompactEachRowWithNames", + "JSONCompactEachRowWithNamesAndTypes", + "JSONCompactStringsEachRowWithNames", + "JSONCompactStringsEachRowWithNamesAndTypes", + ]; const encoder = new NodeValuesEncoder({ parse: JSON.parse, stringify: JSON.stringify, - }) + }); - describe('[Node.js] validateInsertValues', () => { - it('should allow object mode stream for JSON* and raw for Tab* or CSV*', async () => { - const objectModeStream = Stream.Readable.from('foo,bar\n', { + describe("[Node.js] validateInsertValues", () => { + it("should allow object mode stream for JSON* and raw for Tab* or CSV*", async () => { + const objectModeStream = Stream.Readable.from("foo,bar\n", { objectMode: true, - }) - const rawStream = Stream.Readable.from('foo,bar\n', { + }); + const rawStream = Stream.Readable.from("foo,bar\n", { objectMode: false, - }) + }); objectFormats.forEach((format) => { expect(() => encoder.validateInsertValues(objectModeStream, format as DataFormat), - ).not.toThrow() + ).not.toThrow(); expect(() => encoder.validateInsertValues(rawStream, format as DataFormat), ).toThrow( expect.objectContaining({ - message: expect.stringContaining('with enabled object mode'), + message: expect.stringContaining("with enabled object mode"), }), - ) - }) + ); + }); rawFormats.forEach((format) => { expect(() => encoder.validateInsertValues(objectModeStream, format as DataFormat), ).toThrow( expect.objectContaining({ - message: expect.stringContaining('with disabled object mode'), + message: expect.stringContaining("with disabled object mode"), }), - ) + ); expect(() => encoder.validateInsertValues(rawStream, format as DataFormat), - ).not.toThrow() - }) - }) - }) - describe('encodeValues', () => { - it('should not do anything for raw formats streams', async () => { - const values = Stream.Readable.from('foo,bar\n', { + ).not.toThrow(); + }); + }); + }); + describe("encodeValues", () => { + it("should not do anything for raw formats streams", async () => { + const values = Stream.Readable.from("foo,bar\n", { objectMode: false, - }) + }); rawFormats.forEach((format) => { // should be exactly the same object (no duplicate instances) expect(encoder.encodeValues(values, format as DataFormat)).toEqual( values, - ) - }) - }) + ); + }); + }); - it('should encode JSON streams per line', async () => { + it("should encode JSON streams per line", async () => { for (const format of jsonFormats) { - const values = Stream.Readable.from(['foo', 'bar'], { + const values = Stream.Readable.from(["foo", "bar"], { objectMode: true, - }) - const result = encoder.encodeValues(values, format as DataFormat) - let encoded = '' + }); + const result = encoder.encodeValues(values, format as DataFormat); + let encoded = ""; for await (const chunk of result) { - encoded += chunk + encoded += chunk; } - expect(encoded).toEqual('"foo"\n"bar"\n') + expect(encoded).toEqual('"foo"\n"bar"\n'); } - }) + }); - it('should encode JSON arrays', async () => { + it("should encode JSON arrays", async () => { for (const format of jsonFormats) { - const values = ['foo', 'bar'] - const result = encoder.encodeValues(values, format as DataFormat) - let encoded = '' + const values = ["foo", "bar"]; + const result = encoder.encodeValues(values, format as DataFormat); + let encoded = ""; for await (const chunk of result) { - encoded += chunk + encoded += chunk; } - expect(encoded).toEqual('"foo"\n"bar"\n') + expect(encoded).toEqual('"foo"\n"bar"\n'); } - }) + }); - it('should encode JSON input (with and without custom JSON handling)', async () => { + it("should encode JSON input (with and without custom JSON handling)", async () => { const encoders = [ encoder, new NodeValuesEncoder({ parse: simdjson.parse, stringify: JSON.stringify, // simdjson doesn't have a stringify handler }), - ] + ]; for (const encoder of encoders) { const values: InputJSON = { meta: [ { - name: 'name', - type: 'string', + name: "name", + type: "string", }, ], - data: [{ name: 'foo' }, { name: 'bar' }], - } - const result = encoder.encodeValues(values, 'JSON') - let encoded = '' + data: [{ name: "foo" }, { name: "bar" }], + }; + const result = encoder.encodeValues(values, "JSON"); + let encoded = ""; for await (const chunk of result) { - encoded += chunk + encoded += chunk; } - expect(encoded).toEqual(JSON.stringify(values) + '\n') + expect(encoded).toEqual(JSON.stringify(values) + "\n"); } - }) + }); - it('should use custom stringify for JSON streams', async () => { + it("should use custom stringify for JSON streams", async () => { const customEncoder = new NodeValuesEncoder({ parse: JSON.parse, stringify: (value) => `custom:${JSON.stringify(value)}`, - }) + }); - const values = Stream.Readable.from([{ name: 'foo' }, { name: 'bar' }], { + const values = Stream.Readable.from([{ name: "foo" }, { name: "bar" }], { objectMode: true, - }) - const result = customEncoder.encodeValues(values, 'JSON') - let encoded = '' + }); + const result = customEncoder.encodeValues(values, "JSON"); + let encoded = ""; for await (const chunk of result) { - encoded += chunk + encoded += chunk; } - expect(encoded).toEqual('custom:{"name":"foo"}\ncustom:{"name":"bar"}\n') - }) + expect(encoded).toEqual('custom:{"name":"foo"}\ncustom:{"name":"bar"}\n'); + }); - it('should use custom stringify for JSON arrays', async () => { + it("should use custom stringify for JSON arrays", async () => { const customEncoder = new NodeValuesEncoder({ parse: JSON.parse, stringify: (value) => `[${JSON.stringify(value)}]`, - }) + }); - const values = [{ id: 1 }, { id: 2 }] - const result = customEncoder.encodeValues(values, 'JSONEachRow') - let encoded = '' + const values = [{ id: 1 }, { id: 2 }]; + const result = customEncoder.encodeValues(values, "JSONEachRow"); + let encoded = ""; for await (const chunk of result) { - encoded += chunk + encoded += chunk; } - expect(encoded).toEqual('[{"id":1}]\n[{"id":2}]\n') - }) + expect(encoded).toEqual('[{"id":1}]\n[{"id":2}]\n'); + }); - it('should use custom stringify for InputJSON objects', async () => { + it("should use custom stringify for InputJSON objects", async () => { const customEncoder = new NodeValuesEncoder({ parse: JSON.parse, stringify: (value) => JSON.stringify(value).toUpperCase(), - }) + }); const values: InputJSON = { - meta: [{ name: 'id', type: 'UInt32' }], + meta: [{ name: "id", type: "UInt32" }], data: [{ id: 1 }], - } - const result = customEncoder.encodeValues(values, 'JSON') - let encoded = '' + }; + const result = customEncoder.encodeValues(values, "JSON"); + let encoded = ""; for await (const chunk of result) { - encoded += chunk + encoded += chunk; } - expect(encoded).toEqual(JSON.stringify(values).toUpperCase() + '\n') - }) + expect(encoded).toEqual(JSON.stringify(values).toUpperCase() + "\n"); + }); - it('should use custom stringify for JSONObjectEachRow', async () => { + it("should use custom stringify for JSONObjectEachRow", async () => { const customEncoder = new NodeValuesEncoder({ parse: JSON.parse, stringify: (value) => `CUSTOM_${JSON.stringify(value)}`, - }) + }); const values: InputJSONObjectEachRow = { - row1: { name: 'test1' }, - row2: { name: 'test2' }, - } - const result = customEncoder.encodeValues(values, 'JSONObjectEachRow') - let encoded = '' + row1: { name: "test1" }, + row2: { name: "test2" }, + }; + const result = customEncoder.encodeValues(values, "JSONObjectEachRow"); + let encoded = ""; for await (const chunk of result) { - encoded += chunk + encoded += chunk; } - expect(encoded).toEqual(`CUSTOM_${JSON.stringify(values)}\n`) - }) + expect(encoded).toEqual(`CUSTOM_${JSON.stringify(values)}\n`); + }); - it('should handle custom stringify with complex objects', async () => { + it("should handle custom stringify with complex objects", async () => { const customEncoder = new NodeValuesEncoder({ parse: JSON.parse, stringify: (value) => { if ( - typeof value === 'object' && + typeof value === "object" && value !== null && - 'timestamp' in value + "timestamp" in value ) { return JSON.stringify({ ...value, timestamp: new Date(value.timestamp as string).toISOString(), - }) + }); } - return JSON.stringify(value) + return JSON.stringify(value); }, - }) + }); const values = [ - { id: 1, timestamp: '2024-01-01' }, - { id: 2, timestamp: '2024-01-02' }, - ] - const result = customEncoder.encodeValues(values, 'JSONEachRow') - let encoded = '' + { id: 1, timestamp: "2024-01-01" }, + { id: 2, timestamp: "2024-01-02" }, + ]; + const result = customEncoder.encodeValues(values, "JSONEachRow"); + let encoded = ""; for await (const chunk of result) { - encoded += chunk + encoded += chunk; } - expect(encoded).toContain('"timestamp":"2024-01-01T00:00:00.000Z"') - expect(encoded).toContain('"timestamp":"2024-01-02T00:00:00.000Z"') - }) + expect(encoded).toContain('"timestamp":"2024-01-01T00:00:00.000Z"'); + expect(encoded).toContain('"timestamp":"2024-01-02T00:00:00.000Z"'); + }); - it('should use custom stringify across different json formats', async () => { + it("should use custom stringify across different json formats", async () => { const customEncoder = new NodeValuesEncoder({ parse: JSON.parse, stringify: (value) => `>>>${JSON.stringify(value)}<<<`, - }) + }); const testFormats = [ - 'JSONEachRow', - 'JSONStringsEachRow', - 'JSONCompactEachRow', - ] + "JSONEachRow", + "JSONStringsEachRow", + "JSONCompactEachRow", + ]; for (const format of testFormats) { - const values = [{ test: 'data' }] - const result = customEncoder.encodeValues(values, format as DataFormat) - let encoded = '' + const values = [{ test: "data" }]; + const result = customEncoder.encodeValues(values, format as DataFormat); + let encoded = ""; for await (const chunk of result) { - encoded += chunk + encoded += chunk; } - expect(encoded).toEqual('>>>{"test":"data"}<<<\n') + expect(encoded).toEqual('>>>{"test":"data"}<<<\n'); } - }) + }); - it('should encode JSONObjectEachRow input', async () => { + it("should encode JSONObjectEachRow input", async () => { const values: InputJSONObjectEachRow = { - a: { name: 'foo' }, - b: { name: 'bar' }, - } - const result = encoder.encodeValues(values, 'JSON') - let encoded = '' + a: { name: "foo" }, + b: { name: "bar" }, + }; + const result = encoder.encodeValues(values, "JSON"); + let encoded = ""; for await (const chunk of result) { - encoded += chunk + encoded += chunk; } - expect(encoded).toEqual(JSON.stringify(values) + '\n') - }) + expect(encoded).toEqual(JSON.stringify(values) + "\n"); + }); - it('should fail when we try to encode an unknown type of input', async () => { - expect(() => encoder.encodeValues(1 as any, 'JSON')).toThrowError( - 'Cannot encode values of type number with JSON format', - ) - }) - }) -}) + it("should fail when we try to encode an unknown type of input", async () => { + expect(() => encoder.encodeValues(1 as any, "JSON")).toThrowError( + "Cannot encode values of type number with JSON format", + ); + }); + }); +}); diff --git a/packages/client-node/__tests__/utils/assert.ts b/packages/client-node/__tests__/utils/assert.ts index c137e7e3..4b0994c4 100644 --- a/packages/client-node/__tests__/utils/assert.ts +++ b/packages/client-node/__tests__/utils/assert.ts @@ -1,18 +1,18 @@ -import { expect } from 'vitest' -import type { ConnQueryResult } from '@clickhouse/client-common' -import { validateUUID } from '../../../client-common/__tests__/utils/guid' -import type Stream from 'stream' -import { getAsText } from '../../src/utils' +import { expect } from "vitest"; +import type { ConnQueryResult } from "@clickhouse/client-common"; +import { validateUUID } from "../../../client-common/__tests__/utils/guid"; +import type Stream from "stream"; +import { getAsText } from "../../src/utils"; export async function assertConnQueryResult( { stream, query_id }: ConnQueryResult, expectedResponseBody: any, ) { - expect(await getAsText(stream)).toBe(expectedResponseBody) - assertQueryId(query_id) + expect(await getAsText(stream)).toBe(expectedResponseBody); + assertQueryId(query_id); } export function assertQueryId(query_id: string) { - expect(typeof query_id).toBe('string') - expect(validateUUID(query_id)).toBeTruthy() + expect(typeof query_id).toBe("string"); + expect(validateUUID(query_id)).toBeTruthy(); } diff --git a/packages/client-node/__tests__/utils/feature_detection.ts b/packages/client-node/__tests__/utils/feature_detection.ts index 74ccff5e..a52986a8 100644 --- a/packages/client-node/__tests__/utils/feature_detection.ts +++ b/packages/client-node/__tests__/utils/feature_detection.ts @@ -4,10 +4,10 @@ export function isAwaitUsingStatementSupported(): boolean { (async () => { await using c = null; }) - `) - return true + `); + return true; } catch { - return false + return false; } } @@ -17,9 +17,9 @@ export function isUsingStatementSupported(): boolean { (() => { using c = null; }) - `) - return true + `); + return true; } catch { - return false + return false; } } diff --git a/packages/client-node/__tests__/utils/http_stubs.ts b/packages/client-node/__tests__/utils/http_stubs.ts index 76a90940..b194af29 100644 --- a/packages/client-node/__tests__/utils/http_stubs.ts +++ b/packages/client-node/__tests__/utils/http_stubs.ts @@ -1,19 +1,19 @@ -import { ClickHouseLogLevel, LogWriter } from '@clickhouse/client-common' -import { sleep } from '../../../client-common/__tests__/utils/sleep' -import { TestLogger } from '../../../client-common/__tests__/utils/test_logger' -import { randomUUID } from '../../../client-common/__tests__/utils/guid' -import type Http from 'http' -import type { ClientRequest } from 'http' -import Stream from 'stream' -import Util from 'util' -import Zlib from 'zlib' +import { ClickHouseLogLevel, LogWriter } from "@clickhouse/client-common"; +import { sleep } from "../../../client-common/__tests__/utils/sleep"; +import { TestLogger } from "../../../client-common/__tests__/utils/test_logger"; +import { randomUUID } from "../../../client-common/__tests__/utils/guid"; +import type Http from "http"; +import type { ClientRequest } from "http"; +import Stream from "stream"; +import Util from "util"; +import Zlib from "zlib"; import { NodeBaseConnection, type NodeConnectionParams, NodeHttpConnection, -} from '../../src/connection' +} from "../../src/connection"; -const gzip = Util.promisify(Zlib.gzip) +const gzip = Util.promisify(Zlib.gzip); export const socketStub = { on: () => { @@ -25,30 +25,30 @@ export const socketStub = { removeListener: () => { // }, -} +}; export function buildIncomingMessage({ - body = '', + body = "", statusCode = 200, headers = {}, }: { - body?: string | Buffer - statusCode?: number - headers?: Http.IncomingHttpHeaders + body?: string | Buffer; + statusCode?: number; + headers?: Http.IncomingHttpHeaders; }): Http.IncomingMessage { const response = new Stream.Readable({ read() { - this.push(body) - this.push(null) + this.push(body); + this.push(null); }, - }) as Http.IncomingMessage + }) as Http.IncomingMessage; - response.statusCode = statusCode + response.statusCode = statusCode; response.headers = { - 'x-clickhouse-query-id': randomUUID(), + "x-clickhouse-query-id": randomUUID(), ...headers, - } - return response + }; + return response; } export function stubClientRequest(): ClientRequest { @@ -56,48 +56,48 @@ export function stubClientRequest(): ClientRequest { write() { /** stub */ }, - }) as ClientRequest - request.getHeaders = () => ({}) + }) as ClientRequest; + request.getHeaders = () => ({}); Object.assign(request, { socket: socketStub, - }) - return request + }); + return request; } export async function emitResponseBody( request: Http.ClientRequest, body: string | Buffer | undefined, ) { - await sleep(0) + await sleep(0); request.emit( - 'response', + "response", buildIncomingMessage({ body, }), - ) + ); } export async function emitCompressedBody( request: ClientRequest, body: string | Buffer, - encoding = 'gzip', + encoding = "gzip", ) { - await sleep(0) - const compressedBody = await gzip(body) + await sleep(0); + const compressedBody = await gzip(body); request.emit( - 'response', + "response", buildIncomingMessage({ body: compressedBody, headers: { - 'content-encoding': encoding, + "content-encoding": encoding, }, }), - ) + ); } export function buildHttpConnection(config: Partial) { return new NodeHttpConnection({ - url: new URL('http://localhost:8123'), + url: new URL("http://localhost:8123"), request_timeout: 30_000, compression: { @@ -106,13 +106,13 @@ export function buildHttpConnection(config: Partial) { }, max_open_connections: 10, - auth: { username: 'default', password: '', type: 'Credentials' }, - database: 'default', + auth: { username: "default", password: "", type: "Credentials" }, + database: "default", clickhouse_settings: {}, log_writer: new LogWriter( new TestLogger(), - 'NodeConnectionTest', + "NodeConnectionTest", ClickHouseLogLevel.OFF, ), log_level: ClickHouseLogLevel.OFF, @@ -123,7 +123,7 @@ export function buildHttpConnection(config: Partial) { set_basic_auth_header: true, capture_enhanced_stack_trace: false, ...config, - }) + }); } export class MyTestHttpConnection extends NodeBaseConnection { @@ -133,7 +133,7 @@ export class MyTestHttpConnection extends NodeBaseConnection { application_id, log_writer: new LogWriter( new TestLogger(), - 'NodeConnectionTest', + "NodeConnectionTest", ClickHouseLogLevel.OFF, ), log_level: ClickHouseLogLevel.OFF, @@ -142,18 +142,18 @@ export class MyTestHttpConnection extends NodeBaseConnection { }, set_basic_auth_header: true, auth: { - username: 'default', - password: '', - type: 'Credentials', + username: "default", + password: "", + type: "Credentials", }, } as NodeConnectionParams, {} as Http.Agent, - ) + ); } protected createClientRequest(): Http.ClientRequest { - return {} as any + return {} as any; } public getDefaultHeaders() { - return this.buildRequestHeaders() + return this.buildRequestHeaders(); } } diff --git a/packages/client-node/__tests__/utils/jwt.ts b/packages/client-node/__tests__/utils/jwt.ts index 45d36c2c..acc53796 100644 --- a/packages/client-node/__tests__/utils/jwt.ts +++ b/packages/client-node/__tests__/utils/jwt.ts @@ -1,18 +1,18 @@ -import jwt from 'jsonwebtoken' +import jwt from "jsonwebtoken"; export function makeJWT(): string { - const secret = process.env['CLICKHOUSE_CLOUD_JWT_SECRET'] + const secret = process.env["CLICKHOUSE_CLOUD_JWT_SECRET"]; if (secret === undefined) { throw new Error( - 'Environment variable CLICKHOUSE_CLOUD_JWT_SECRET is not set', - ) + "Environment variable CLICKHOUSE_CLOUD_JWT_SECRET is not set", + ); } const payload = { - iss: 'ClickHouse', - sub: 'CI_Test', - aud: '1f7f78b8-da67-480b-8913-726fdd31d2fc', - 'clickhouse:roles': ['default'], - 'clickhouse:grants': [], - } - return jwt.sign(payload, secret, { expiresIn: '15m', algorithm: 'RS256' }) + iss: "ClickHouse", + sub: "CI_Test", + aud: "1f7f78b8-da67-480b-8913-726fdd31d2fc", + "clickhouse:roles": ["default"], + "clickhouse:grants": [], + }; + return jwt.sign(payload, secret, { expiresIn: "15m", algorithm: "RS256" }); } diff --git a/packages/client-node/__tests__/utils/node_client.ts b/packages/client-node/__tests__/utils/node_client.ts index 293b46cf..912913c8 100644 --- a/packages/client-node/__tests__/utils/node_client.ts +++ b/packages/client-node/__tests__/utils/node_client.ts @@ -1,9 +1,12 @@ -import { createTestClient } from '@test/utils' -import type Stream from 'stream' -import type { ClickHouseClient, ClickHouseClientConfigOptions } from '../../src' +import { createTestClient } from "@test/utils"; +import type Stream from "stream"; +import type { + ClickHouseClient, + ClickHouseClientConfigOptions, +} from "../../src"; export function createNodeTestClient( config: ClickHouseClientConfigOptions = {}, ): ClickHouseClient { - return createTestClient(config) as ClickHouseClient + return createTestClient(config) as ClickHouseClient; } diff --git a/packages/client-node/__tests__/utils/simple_node_client.ts b/packages/client-node/__tests__/utils/simple_node_client.ts index a84de9eb..2d515a80 100644 --- a/packages/client-node/__tests__/utils/simple_node_client.ts +++ b/packages/client-node/__tests__/utils/simple_node_client.ts @@ -1,12 +1,15 @@ // 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' +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 + return createSimpleTestClient(config) as ClickHouseClient; } diff --git a/packages/client-node/__tests__/utils/sleep.ts b/packages/client-node/__tests__/utils/sleep.ts index 5ed82bc0..e9d2b2e3 100644 --- a/packages/client-node/__tests__/utils/sleep.ts +++ b/packages/client-node/__tests__/utils/sleep.ts @@ -1,7 +1,7 @@ export async function sleep(ms: number): Promise { await new Promise((resolve) => setTimeout(() => { - resolve(void 0) + resolve(void 0); }, ms), - ) + ); } diff --git a/packages/client-node/__tests__/utils/stream.ts b/packages/client-node/__tests__/utils/stream.ts index e0a85427..2e5f3846 100644 --- a/packages/client-node/__tests__/utils/stream.ts +++ b/packages/client-node/__tests__/utils/stream.ts @@ -1,4 +1,4 @@ -import Stream from 'stream' +import Stream from "stream"; export function makeRawStream() { return new Stream.Readable({ @@ -6,7 +6,7 @@ export function makeRawStream() { read() { /* stub */ }, - }) + }); } export function makeObjectStream() { @@ -15,5 +15,5 @@ export function makeObjectStream() { read() { /* stub */ }, - }) + }); } diff --git a/packages/client-node/eslint.config.mjs b/packages/client-node/eslint.config.mjs index c59edc02..c7164f43 100644 --- a/packages/client-node/eslint.config.mjs +++ b/packages/client-node/eslint.config.mjs @@ -1,7 +1,7 @@ -import js from '@eslint/js' -import { defineConfig } from 'eslint/config' -import tseslint from 'typescript-eslint' -import { typescriptEslintConfig } from '../../eslint.config.base.mjs' +import js from "@eslint/js"; +import { defineConfig } from "eslint/config"; +import tseslint from "typescript-eslint"; +import { typescriptEslintConfig } from "../../eslint.config.base.mjs"; export default defineConfig( // Base ESLint recommended rules @@ -11,23 +11,23 @@ export default defineConfig( ...tseslint.configs.stylistic, { rules: { - '@typescript-eslint/no-unused-vars': 'warn', - '@typescript-eslint/no-unused-expressions': 'warn', + "@typescript-eslint/no-unused-vars": "warn", + "@typescript-eslint/no-unused-expressions": "warn", }, }, typescriptEslintConfig(import.meta.dirname), // Ignore build artifacts and externals { ignores: [ - './__tests__/**/*.ts', - 'eslint.config.mjs', - 'vitest.*.config.ts', - 'vitest.*.setup.ts', - 'coverage', - 'out', - 'dist', - 'node_modules', - 'webpack', + "./__tests__/**/*.ts", + "eslint.config.mjs", + "vitest.*.config.ts", + "vitest.*.setup.ts", + "coverage", + "out", + "dist", + "node_modules", + "webpack", ], }, -) +); diff --git a/packages/client-node/src/client.ts b/packages/client-node/src/client.ts index d4b4576e..93f0ef4e 100644 --- a/packages/client-node/src/client.ts +++ b/packages/client-node/src/client.ts @@ -2,12 +2,12 @@ import type { DataFormat, IsSame, QueryParamsWithFormat, -} from '@clickhouse/client-common' -import { ClickHouseClient } from '@clickhouse/client-common' -import type Stream from 'stream' -import type { NodeClickHouseClientConfigOptions } from './config' -import { NodeConfigImpl } from './config' -import type { ResultSet } from './result_set' +} from "@clickhouse/client-common"; +import { ClickHouseClient } from "@clickhouse/client-common"; +import type Stream from "stream"; +import type { NodeClickHouseClientConfigOptions } from "./config"; +import { NodeConfigImpl } from "./config"; +import type { ResultSet } from "./result_set"; /** If the Format is not a literal type, fall back to the default behavior of the ResultSet, * allowing to call all methods with all data shapes variants, @@ -15,14 +15,14 @@ import type { ResultSet } from './result_set' export type QueryResult = IsSame extends true ? ResultSet - : ResultSet + : ResultSet; export class NodeClickHouseClient extends ClickHouseClient { /** See {@link ClickHouseClient.query}. */ - override query( + override query( params: QueryParamsWithFormat, ): Promise> { - return super.query(params) as Promise> + return super.query(params) as Promise>; } } @@ -32,5 +32,5 @@ export function createClient( return new ClickHouseClient({ impl: NodeConfigImpl, ...(config || {}), - }) as NodeClickHouseClient + }) as NodeClickHouseClient; } diff --git a/packages/client-node/src/config.ts b/packages/client-node/src/config.ts index 19e22350..e74cf9f4 100644 --- a/packages/client-node/src/config.ts +++ b/packages/client-node/src/config.ts @@ -3,52 +3,52 @@ import type { ImplementationDetails, JSONHandling, ResponseHeaders, -} from '@clickhouse/client-common' +} from "@clickhouse/client-common"; import { type BaseClickHouseClientConfigOptions, type ConnectionParams, numberConfigURLValue, -} from '@clickhouse/client-common' -import type http from 'http' -import type https from 'node:https' -import type Stream from 'stream' -import { NodeConnectionFactory, type TLSParams } from './connection' -import { ResultSet } from './result_set' -import { NodeValuesEncoder } from './utils' +} from "@clickhouse/client-common"; +import type http from "http"; +import type https from "node:https"; +import type Stream from "stream"; +import { NodeConnectionFactory, type TLSParams } from "./connection"; +import { ResultSet } from "./result_set"; +import { NodeValuesEncoder } from "./utils"; export type NodeClickHouseClientConfigOptions = BaseClickHouseClientConfigOptions & { - tls?: BasicTLSOptions | MutualTLSOptions + tls?: BasicTLSOptions | MutualTLSOptions; /** HTTP Keep-Alive related settings */ keep_alive?: { /** Enable or disable the HTTP Keep-Alive mechanism. * @default true */ - enabled?: boolean + enabled?: boolean; /** For how long keep a particular idle socket alive on the client side (in milliseconds). * It is supposed to be at least a second less than the ClickHouse server KeepAlive timeout, * which is by default `3000` ms for pre-23.11 versions. * * When set to `0`, the idle socket management feature is disabled. * @default 2500 */ - idle_socket_ttl?: number + idle_socket_ttl?: number; /** Eagerly destroy the sockets that are considered stale (idle for more than `idle_socket_ttl`), * without waiting for the timeout to trigger. This allows freeing up stale sockets * in case of longer event loop delays. * @default false */ - eagerly_destroy_stale_sockets?: boolean - } + eagerly_destroy_stale_sockets?: boolean; + }; /** Custom HTTP agent to use for the outgoing HTTP(s) requests. * If set, {@link BaseClickHouseClientConfigOptions.max_open_connections}, {@link tls} and {@link keep_alive} * options have no effect, as it is part of the default underlying agent configuration. * @experimental - unstable API; it might be a subject to change in the future; * please provide your feedback in the repository. * @default undefined */ - http_agent?: http.Agent | https.Agent + http_agent?: http.Agent | https.Agent; /** Enable or disable the `Authorization` header with basic auth for the outgoing HTTP(s) requests. * @experimental - unstable API; it might be a subject to change in the future; * please provide your feedback in the repository. * @default true (enabled) */ - set_basic_auth_header?: boolean + set_basic_auth_header?: boolean; /** You could try enabling this option if you encounter an error with an unclear or truncated stack trace; * as it might happen due to the way the Node.js handles the stack traces in the async code. * Note that it might have a noticeable performance impact due to @@ -58,7 +58,7 @@ export type NodeClickHouseClientConfigOptions = * @experimental - unstable API; it might be a subject to change in the future; * please provide your feedback in the repository. * @default false (disabled) */ - capture_enhanced_stack_trace?: boolean + capture_enhanced_stack_trace?: boolean; /** Override the maximum length (in bytes) of HTTP response headers accepted from the server. * Forwarded as the `maxHeaderSize` option to {@link http.request} / {@link https.request}. * @@ -75,76 +75,76 @@ export type NodeClickHouseClientConfigOptions = * request implementation; for the bundled HTTP/HTTPS connections it is passed straight * through to the request options. * @default undefined */ - max_response_headers_size?: number - } + max_response_headers_size?: number; + }; interface BasicTLSOptions { - ca_cert: Buffer + ca_cert: Buffer; } interface MutualTLSOptions { - ca_cert: Buffer - cert: Buffer - key: Buffer + ca_cert: Buffer; + cert: Buffer; + key: Buffer; } export const NodeConfigImpl: Required< - ImplementationDetails['impl'] + ImplementationDetails["impl"] > = { handle_specific_url_params: (config, url) => { - const nodeConfig: NodeClickHouseClientConfigOptions = { ...config } - const unknownParams = new Set() - const handledParams = new Set() - const urlSearchParamsKeys = [...url.searchParams.keys()] + const nodeConfig: NodeClickHouseClientConfigOptions = { ...config }; + const unknownParams = new Set(); + const handledParams = new Set(); + const urlSearchParamsKeys = [...url.searchParams.keys()]; if (urlSearchParamsKeys.length > 0) { urlSearchParamsKeys.forEach((key) => { - const value = url.searchParams.get(key) as string + const value = url.searchParams.get(key) as string; switch (key) { - case 'keep_alive_idle_socket_ttl': + case "keep_alive_idle_socket_ttl": if (nodeConfig.keep_alive === undefined) { - nodeConfig.keep_alive = {} + nodeConfig.keep_alive = {}; } nodeConfig.keep_alive.idle_socket_ttl = numberConfigURLValue({ key, value, min: 0, - }) - handledParams.add(key) - break + }); + handledParams.add(key); + break; default: - unknownParams.add(key) + unknownParams.add(key); } - }) + }); } return { config: nodeConfig, unknown_params: unknownParams, handled_params: handledParams, - } + }; }, make_connection: ( nodeConfig: NodeClickHouseClientConfigOptions, params: ConnectionParams, ) => { - let tls: TLSParams | undefined = undefined + let tls: TLSParams | undefined = undefined; if (nodeConfig.tls !== undefined) { - if ('cert' in nodeConfig.tls && 'key' in nodeConfig.tls) { + if ("cert" in nodeConfig.tls && "key" in nodeConfig.tls) { tls = { - type: 'Mutual', + type: "Mutual", ...nodeConfig.tls, - } + }; } else { tls = { - type: 'Basic', + type: "Basic", ...nodeConfig.tls, - } + }; } } // normally, it should be already set after processing the config const keep_alive = { enabled: nodeConfig?.keep_alive?.enabled ?? true, idle_socket_ttl: nodeConfig?.keep_alive?.idle_socket_ttl ?? 2500, - } + }; return NodeConnectionFactory.create({ connection_params: params, set_basic_auth_header: nodeConfig.set_basic_auth_header ?? true, @@ -156,7 +156,7 @@ export const NodeConfigImpl: Required< keep_alive, tls, max_response_headers_size: nodeConfig.max_response_headers_size, - }) + }); }, values_encoder: (jsonHandling: JSONHandling) => new NodeValuesEncoder(jsonHandling), @@ -176,4 +176,4 @@ export const NodeConfigImpl: Required< response_headers, jsonHandling, })) as any, -} +}; diff --git a/packages/client-node/src/connection/compression.ts b/packages/client-node/src/connection/compression.ts index 34d89037..31758376 100644 --- a/packages/client-node/src/connection/compression.ts +++ b/packages/client-node/src/connection/compression.ts @@ -1,19 +1,21 @@ -import type { LogWriter } from '@clickhouse/client-common' -import { ClickHouseLogLevel } from '@clickhouse/client-common' -import type Http from 'http' -import Stream from 'stream' -import Zlib from 'zlib' +import type { LogWriter } from "@clickhouse/client-common"; +import { ClickHouseLogLevel } from "@clickhouse/client-common"; +import type Http from "http"; +import Stream from "stream"; +import Zlib from "zlib"; -type DecompressResponseResult = { response: Stream.Readable } | { error: Error } +type DecompressResponseResult = + | { response: Stream.Readable } + | { error: Error }; export function decompressResponse( response: Http.IncomingMessage, log_writer: LogWriter, log_level: ClickHouseLogLevel, ): DecompressResponseResult { - const encoding = response.headers['content-encoding'] + const encoding = response.headers["content-encoding"]; - if (encoding === 'gzip') { + if (encoding === "gzip") { return { response: Stream.pipeline( response, @@ -22,23 +24,23 @@ export function decompressResponse( if (err) { if (log_level <= ClickHouseLogLevel.ERROR) { log_writer.error({ - message: 'An error occurred while decompressing the response', + message: "An error occurred while decompressing the response", err, - }) + }); } } }, ), - } + }; } else if (encoding !== undefined) { return { error: new Error(`Unexpected encoding: ${encoding}`), - } + }; } - return { response } + return { response }; } export function isDecompressionError(result: any): result is { error: Error } { - return result.error !== undefined + return result.error !== undefined; } diff --git a/packages/client-node/src/connection/create_connection.ts b/packages/client-node/src/connection/create_connection.ts index 2f676074..8a8e31ca 100644 --- a/packages/client-node/src/connection/create_connection.ts +++ b/packages/client-node/src/connection/create_connection.ts @@ -1,23 +1,23 @@ -import type { ConnectionParams } from '@clickhouse/client-common' -import type http from 'http' -import type https from 'node:https' +import type { ConnectionParams } from "@clickhouse/client-common"; +import type http from "http"; +import type https from "node:https"; import type { NodeBaseConnection, NodeConnectionParams, -} from './node_base_connection' -import { NodeCustomAgentConnection } from './node_custom_agent_connection' -import { NodeHttpConnection } from './node_http_connection' -import { NodeHttpsConnection } from './node_https_connection' +} from "./node_base_connection"; +import { NodeCustomAgentConnection } from "./node_custom_agent_connection"; +import { NodeHttpConnection } from "./node_http_connection"; +import { NodeHttpsConnection } from "./node_https_connection"; export interface CreateConnectionParams { - connection_params: ConnectionParams - tls: NodeConnectionParams['tls'] - keep_alive: NodeConnectionParams['keep_alive'] - http_agent: http.Agent | https.Agent | undefined - set_basic_auth_header: boolean - capture_enhanced_stack_trace: boolean - eagerly_destroy_stale_sockets?: boolean - max_response_headers_size?: number + connection_params: ConnectionParams; + tls: NodeConnectionParams["tls"]; + keep_alive: NodeConnectionParams["keep_alive"]; + http_agent: http.Agent | https.Agent | undefined; + set_basic_auth_header: boolean; + capture_enhanced_stack_trace: boolean; + eagerly_destroy_stale_sockets?: boolean; + max_response_headers_size?: number; } /** A factory for easier mocking after Node.js 22.18 */ @@ -42,10 +42,10 @@ export class NodeConnectionFactory { http_agent, eagerly_destroy_stale_sockets, max_response_headers_size, - }) + }); } switch (connection_params.url.protocol) { - case 'http:': + case "http:": return new NodeHttpConnection({ ...connection_params, set_basic_auth_header, @@ -53,8 +53,8 @@ export class NodeConnectionFactory { keep_alive, eagerly_destroy_stale_sockets, max_response_headers_size, - }) - case 'https:': + }); + case "https:": return new NodeHttpsConnection({ ...connection_params, set_basic_auth_header, @@ -63,9 +63,9 @@ export class NodeConnectionFactory { tls, eagerly_destroy_stale_sockets, max_response_headers_size, - }) + }); default: - throw new Error('Only HTTP and HTTPS protocols are supported') + throw new Error("Only HTTP and HTTPS protocols are supported"); } } } diff --git a/packages/client-node/src/connection/index.ts b/packages/client-node/src/connection/index.ts index f9f75a31..cbce90e7 100644 --- a/packages/client-node/src/connection/index.ts +++ b/packages/client-node/src/connection/index.ts @@ -1,4 +1,4 @@ -export * from './node_base_connection' -export * from './node_http_connection' -export * from './node_https_connection' -export * from './create_connection' +export * from "./node_base_connection"; +export * from "./node_http_connection"; +export * from "./node_https_connection"; +export * from "./create_connection"; diff --git a/packages/client-node/src/connection/node_base_connection.ts b/packages/client-node/src/connection/node_base_connection.ts index f3140672..360a7fe3 100644 --- a/packages/client-node/src/connection/node_base_connection.ts +++ b/packages/client-node/src/connection/node_base_connection.ts @@ -12,7 +12,7 @@ import type { ConnPingResult, ConnQueryResult, ResponseHeaders, -} from '@clickhouse/client-common' +} from "@clickhouse/client-common"; import { isCredentialsAuth, isJWTAuth, @@ -20,30 +20,30 @@ import { transformUrl, withHttpSettings, ClickHouseLogLevel, -} from '@clickhouse/client-common' -import { type ConnPingParams } from '@clickhouse/client-common' -import crypto from 'crypto' -import type Http from 'http' -import type Https from 'node:https' -import type Stream from 'stream' -import { getUserAgent } from '../utils' -import { drainStreamInternal } from './stream' -import { type RequestParams, SocketPool } from './socket_pool' +} from "@clickhouse/client-common"; +import { type ConnPingParams } from "@clickhouse/client-common"; +import crypto from "crypto"; +import type Http from "http"; +import type Https from "node:https"; +import type Stream from "stream"; +import { getUserAgent } from "../utils"; +import { drainStreamInternal } from "./stream"; +import { type RequestParams, SocketPool } from "./socket_pool"; export type NodeConnectionParams = ConnectionParams & { - tls?: TLSParams - http_agent?: Http.Agent | Https.Agent - set_basic_auth_header: boolean - capture_enhanced_stack_trace: boolean + tls?: TLSParams; + http_agent?: Http.Agent | Https.Agent; + set_basic_auth_header: boolean; + capture_enhanced_stack_trace: boolean; keep_alive: { - enabled: boolean - idle_socket_ttl: number - } - log_level: ClickHouseLogLevel + enabled: boolean; + idle_socket_ttl: number; + }; + log_level: ClickHouseLogLevel; /** * Eagerly destroy the sockets that are considered stale (idle for more than `idle_socket_ttl`), without waiting for the timeout to trigger. This allows to free up the stale sockets in case of longer event loop delays. */ - eagerly_destroy_stale_sockets: boolean + eagerly_destroy_stale_sockets: boolean; /** * Optional override for {@link Http.RequestOptions.maxHeaderSize} forwarded to * `http(s).request`. Useful for long-running queries that accumulate many @@ -52,71 +52,71 @@ export type NodeConnectionParams = ConnectionParams & { * * When `undefined`, the Node.js default applies. */ - max_response_headers_size?: number -} + max_response_headers_size?: number; +}; export type TLSParams = | { - ca_cert: Buffer - type: 'Basic' + ca_cert: Buffer; + type: "Basic"; } | { - ca_cert: Buffer - cert: Buffer - key: Buffer - type: 'Mutual' - } + ca_cert: Buffer; + cert: Buffer; + key: Buffer; + type: "Mutual"; + }; export abstract class NodeBaseConnection implements Connection { - protected readonly defaultAuthHeader: string - protected readonly defaultHeaders: Http.OutgoingHttpHeaders + protected readonly defaultAuthHeader: string; + protected readonly defaultHeaders: Http.OutgoingHttpHeaders; - private readonly connectionId: string = crypto.randomUUID() - private readonly socketPool: SocketPool - protected readonly params: NodeConnectionParams - protected readonly agent: Http.Agent + private readonly connectionId: string = crypto.randomUUID(); + private readonly socketPool: SocketPool; + protected readonly params: NodeConnectionParams; + protected readonly agent: Http.Agent; protected constructor(params: NodeConnectionParams, agent: Http.Agent) { - this.params = params - this.agent = agent + this.params = params; + this.agent = agent; this.socketPool = new SocketPool( this.connectionId, this.params, this.createClientRequest.bind(this), this.agent, - ) - if (params.auth.type === 'Credentials') { + ); + if (params.auth.type === "Credentials") { this.defaultAuthHeader = `Basic ${Buffer.from( `${params.auth.username}:${params.auth.password}`, - ).toString('base64')}` - } else if (params.auth.type === 'JWT') { - this.defaultAuthHeader = `Bearer ${params.auth.access_token}` + ).toString("base64")}`; + } else if (params.auth.type === "JWT") { + this.defaultAuthHeader = `Bearer ${params.auth.access_token}`; } else { - throw new Error(`Unknown auth type: ${(params.auth as any).type}`) + throw new Error(`Unknown auth type: ${(params.auth as any).type}`); } this.defaultHeaders = { // Node.js HTTP agent, for some reason, does not set this on its own when KeepAlive is enabled - Connection: this.params.keep_alive.enabled ? 'keep-alive' : 'close', - 'User-Agent': getUserAgent(this.params.application_id), - } + Connection: this.params.keep_alive.enabled ? "keep-alive" : "close", + "User-Agent": getUserAgent(this.params.application_id), + }; } async ping(params: ConnPingParams): Promise { - const { log_writer, log_level } = this.params - const query_id = this.getQueryId(params.query_id) - const { controller, controllerCleanup } = this.getAbortController(params) + const { log_writer, log_level } = this.params; + const query_id = this.getQueryId(params.query_id); + const { controller, controllerCleanup } = this.getAbortController(params); try { - let result: RequestResult + let result: RequestResult; if (params.select) { const searchParams = toSearchParams({ database: undefined, query: PingQuery, query_id, - }) + }); result = await this.request( { query: PingQuery, - method: 'GET', + method: "GET", url: transformUrl({ url: this.params.url, searchParams }), abort_signal: controller.signal, headers: this.buildRequestHeaders(), @@ -124,66 +124,66 @@ export abstract class NodeBaseConnection implements Connection log_writer, log_level, }, - 'Ping', - ) + "Ping", + ); } else { result = await this.request( { - query: 'ping', - method: 'GET', - url: transformUrl({ url: this.params.url, pathname: '/ping' }), + query: "ping", + method: "GET", + url: transformUrl({ url: this.params.url, pathname: "/ping" }), abort_signal: controller.signal, headers: this.buildRequestHeaders(), query_id, log_writer, log_level, }, - 'Ping', - ) + "Ping", + ); } await drainStreamInternal( { - op: 'Ping' as const, + op: "Ping" as const, log_writer, query_id, log_level, }, result.stream, - ) - return { success: true } + ); + return { success: true }; } catch (error) { // it is used to ensure that the outgoing request is terminated, // and we don't get unhandled error propagation later - controller.abort('Ping failed') + controller.abort("Ping failed"); // not an error, as this might be semi-expected if (log_level <= ClickHouseLogLevel.WARN) { log_writer.warn({ - message: this.httpRequestErrorMessage('Ping'), + message: this.httpRequestErrorMessage("Ping"), err: error as Error, args: { connection_id: this.connectionId, query_id, }, - }) + }); } return { success: false, error: error as Error, // should NOT be propagated to the user - } + }; } finally { - controllerCleanup() + controllerCleanup(); } } async query( params: ConnBaseQueryParams, ): Promise> { - const { log_writer, log_level } = this.params - const query_id = this.getQueryId(params.query_id) + const { log_writer, log_level } = this.params; + const query_id = this.getQueryId(params.query_id); const clickhouse_settings = withHttpSettings( params.clickhouse_settings, this.params.compression.decompress_response, - ) + ); const searchParams = toSearchParams({ database: this.params.database, query_params: params.query_params, @@ -191,15 +191,15 @@ export abstract class NodeBaseConnection implements Connection clickhouse_settings, query_id, role: params.role, - }) - const { controller, controllerCleanup } = this.getAbortController(params) + }); + const { controller, controllerCleanup } = this.getAbortController(params); // allows enforcing the compression via the settings even if the client instance has it disabled const enableResponseCompression = - clickhouse_settings.enable_http_compression === 1 + clickhouse_settings.enable_http_compression === 1; try { const { response_headers, stream, http_status_code } = await this.request( { - method: 'POST', + method: "POST", url: transformUrl({ url: this.params.url, searchParams }), body: params.query, abort_signal: controller.signal, @@ -210,18 +210,18 @@ export abstract class NodeBaseConnection implements Connection log_writer, log_level, }, - 'Query', - ) + "Query", + ); return { stream, response_headers, query_id, http_status_code, - } + }; } catch (err) { - controller.abort('Query HTTP request failed') + controller.abort("Query HTTP request failed"); this.logRequestError({ - op: 'Query', + op: "Query", query_id: query_id, query_params: params, search_params: searchParams, @@ -230,18 +230,18 @@ export abstract class NodeBaseConnection implements Connection decompress_response: enableResponseCompression, clickhouse_settings, }, - }) - throw err // should be propagated to the user + }); + throw err; // should be propagated to the user } finally { - controllerCleanup() + controllerCleanup(); } } async insert( params: ConnInsertParams, ): Promise { - const { log_writer, log_level } = this.params - const query_id = this.getQueryId(params.query_id) + const { log_writer, log_level } = this.params; + const query_id = this.getQueryId(params.query_id); const searchParams = toSearchParams({ database: this.params.database, clickhouse_settings: params.clickhouse_settings, @@ -250,13 +250,13 @@ export abstract class NodeBaseConnection implements Connection session_id: params.session_id, role: params.role, query_id, - }) - const { controller, controllerCleanup } = this.getAbortController(params) + }); + const { controller, controllerCleanup } = this.getAbortController(params); try { const { stream, summary, response_headers, http_status_code } = await this.request( { - method: 'POST', + method: "POST", url: transformUrl({ url: this.params.url, searchParams }), body: params.values, abort_signal: controller.signal, @@ -269,22 +269,22 @@ export abstract class NodeBaseConnection implements Connection log_writer, log_level, }, - 'Insert', - ) + "Insert", + ); await drainStreamInternal( { - op: 'Insert', + op: "Insert", log_writer, query_id, log_level, }, stream, - ) - return { query_id, summary, response_headers, http_status_code } + ); + return { query_id, summary, response_headers, http_status_code }; } catch (err) { - controller.abort('Insert HTTP request failed') + controller.abort("Insert HTTP request failed"); this.logRequestError({ - op: 'Insert', + op: "Insert", query_id: query_id, query_params: params, search_params: searchParams, @@ -292,51 +292,51 @@ export abstract class NodeBaseConnection implements Connection extra_args: { clickhouse_settings: params.clickhouse_settings ?? {}, }, - }) - throw err // should be propagated to the user + }); + throw err; // should be propagated to the user } finally { - controllerCleanup() + controllerCleanup(); } } async exec( params: ConnExecParams, ): Promise> { - const query_id = this.getQueryId(params.query_id) + const query_id = this.getQueryId(params.query_id); return this.runExec({ ...params, query_id, - op: 'Exec', - }) + op: "Exec", + }); } async command(params: ConnBaseQueryParams): Promise { - const { log_writer, log_level } = this.params - const query_id = this.getQueryId(params.query_id) - const commandStartTime = Date.now() + const { log_writer, log_level } = this.params; + const query_id = this.getQueryId(params.query_id); + const commandStartTime = Date.now(); if (log_level <= ClickHouseLogLevel.TRACE) { log_writer.trace({ - message: 'Command: operation started', + message: "Command: operation started", args: { - operation: 'Command', + operation: "Command", connection_id: this.connectionId, query_id, }, - }) + }); } const { stream, summary, response_headers } = await this.runExec({ ...params, query_id, - op: 'Command', - }) + op: "Command", + }); - const runExecDuration = Date.now() - commandStartTime + const runExecDuration = Date.now() - commandStartTime; if (log_level <= ClickHouseLogLevel.TRACE) { log_writer.trace({ - message: 'Command: runExec completed, starting stream drain', + message: "Command: runExec completed, starting stream drain", args: { - operation: 'Command', + operation: "Command", connection_id: this.connectionId, query_id, runExec_duration_ms: runExecDuration, @@ -346,43 +346,43 @@ export abstract class NodeBaseConnection implements Connection readableLength: stream.readableLength, }, }, - }) + }); } // ignore the response stream and release the socket immediately - const drainStartTime = Date.now() + const drainStartTime = Date.now(); await drainStreamInternal( { - op: 'Command', + op: "Command", log_writer, query_id, log_level, }, stream, - ) + ); if (log_level <= ClickHouseLogLevel.TRACE) { - const drainDuration = Date.now() - drainStartTime - const totalDuration = Date.now() - commandStartTime + const drainDuration = Date.now() - drainStartTime; + const totalDuration = Date.now() - commandStartTime; log_writer.trace({ - message: 'Command: operation completed', + message: "Command: operation completed", args: { - operation: 'Command', + operation: "Command", connection_id: this.connectionId, query_id, drain_duration_ms: drainDuration, total_duration_ms: totalDuration, }, - }) + }); } - return { query_id, summary, response_headers } + return { query_id, summary, response_headers }; } async close(): Promise { if (this.agent !== undefined && this.agent.destroy !== undefined) { - this.agent.destroy() + this.agent.destroy(); } } @@ -398,61 +398,61 @@ export abstract class NodeBaseConnection implements Connection // An appropriate `Authorization` header might be added later // It is not always required - see the TLS headers in `node_https_connection.ts` ...this.defaultHeaders, - } + }; } protected buildRequestHeaders( params?: ConnBaseQueryParams, ): Http.OutgoingHttpHeaders { - const headers = this.defaultHeadersWithOverride(params) + const headers = this.defaultHeadersWithOverride(params); if (isJWTAuth(params?.auth)) { return { ...headers, Authorization: `Bearer ${params.auth.access_token}`, - } + }; } if (this.params.set_basic_auth_header) { if (isCredentialsAuth(params?.auth)) { return { ...headers, - Authorization: `Basic ${Buffer.from(`${params.auth.username}:${params.auth.password}`).toString('base64')}`, - } + Authorization: `Basic ${Buffer.from(`${params.auth.username}:${params.auth.password}`).toString("base64")}`, + }; } else { return { ...headers, Authorization: this.defaultAuthHeader, - } + }; } } return { ...headers, - } + }; } protected abstract createClientRequest( params: RequestParams, - ): Http.ClientRequest + ): Http.ClientRequest; private getQueryId(query_id: string | undefined): string { - return query_id || crypto.randomUUID() + return query_id || crypto.randomUUID(); } // a wrapper over the user's Signal to terminate the failed requests private getAbortController(params: { abort_signal?: AbortSignal }): { - controller: AbortController - controllerCleanup: () => void + controller: AbortController; + controllerCleanup: () => void; } { - const controller = new AbortController() + const controller = new AbortController(); function onAbort() { - controller.abort() + controller.abort(); } - params.abort_signal?.addEventListener('abort', onAbort) + params.abort_signal?.addEventListener("abort", onAbort); return { controller, controllerCleanup: () => { - params.abort_signal?.removeEventListener('abort', onAbort) + params.abort_signal?.removeEventListener("abort", onAbort); }, - } + }; } private logRequestError({ @@ -474,24 +474,24 @@ export abstract class NodeBaseConnection implements Connection session_id: query_params.session_id, ...extra_args, }, - }) + }); } } private httpRequestErrorMessage(op: ConnOperation): string { - return `${op}: HTTP request error.` + return `${op}: HTTP request error.`; } private async runExec( params: RunExecParams, ): Promise> { - const { log_writer, log_level } = this.params - const query_id = params.query_id - const sendQueryInParams = params.values !== undefined + const { log_writer, log_level } = this.params; + const query_id = params.query_id; + const sendQueryInParams = params.values !== undefined; const clickhouse_settings = withHttpSettings( params.clickhouse_settings, this.params.compression.decompress_response, - ) + ); const toSearchParamsOptions = { query: sendQueryInParams ? params.query : undefined, database: this.params.database, @@ -500,23 +500,23 @@ export abstract class NodeBaseConnection implements Connection role: params.role, clickhouse_settings, query_id, - } - const searchParams = toSearchParams(toSearchParamsOptions) - const { controller, controllerCleanup } = this.getAbortController(params) + }; + const searchParams = toSearchParams(toSearchParamsOptions); + const { controller, controllerCleanup } = this.getAbortController(params); const tryDecompressResponseStream = - params.op === 'Exec' + params.op === "Exec" ? // allows disabling stream decompression for the `Exec` operation only (params.decompress_response_stream ?? this.params.compression.decompress_response) : // there is nothing useful in the response stream for the `Command` operation, // and it is immediately destroyed; never decompress it - false - const ignoreErrorResponse = params.ignore_error_response ?? false + false; + const ignoreErrorResponse = params.ignore_error_response ?? false; try { const { stream, summary, response_headers, http_status_code } = await this.request( { - method: 'POST', + method: "POST", url: transformUrl({ url: this.params.url, searchParams }), body: sendQueryInParams ? params.values : params.query, abort_signal: controller.signal, @@ -534,16 +534,16 @@ export abstract class NodeBaseConnection implements Connection log_level, }, params.op, - ) + ); return { stream, query_id, summary, response_headers, http_status_code, - } + }; } catch (err) { - controller.abort(`${params.op} HTTP request failed`) + controller.abort(`${params.op} HTTP request failed`); this.logRequestError({ op: params.op, query_id: query_id, @@ -553,10 +553,10 @@ export abstract class NodeBaseConnection implements Connection extra_args: { clickhouse_settings: params.clickhouse_settings ?? {}, }, - }) - throw err // should be propagated to the user + }); + throw err; // should be propagated to the user } finally { - controllerCleanup() + controllerCleanup(); } } @@ -564,32 +564,32 @@ export abstract class NodeBaseConnection implements Connection params: RequestParams, op: ConnOperation, ): Promise { - return this.socketPool.request(params, op) + return this.socketPool.request(params, op); } } interface RequestResult { - stream: Stream.Readable - response_headers: ResponseHeaders - http_status_code?: number - summary?: ClickHouseSummary + stream: Stream.Readable; + response_headers: ResponseHeaders; + http_status_code?: number; + summary?: ClickHouseSummary; } interface LogRequestErrorParams { - op: ConnOperation - err: Error - query_id: string - query_params: ConnBaseQueryParams - search_params: URLSearchParams | undefined - extra_args: Record + op: ConnOperation; + err: Error; + query_id: string; + query_params: ConnBaseQueryParams; + search_params: URLSearchParams | undefined; + extra_args: Record; } type RunExecParams = ConnBaseQueryParams & { - query_id: string - op: 'Exec' | 'Command' - values?: ConnExecParams['values'] - decompress_response_stream?: boolean - ignore_error_response?: boolean -} + query_id: string; + op: "Exec" | "Command"; + values?: ConnExecParams["values"]; + decompress_response_stream?: boolean; + ignore_error_response?: boolean; +}; -const PingQuery = `SELECT 'ping'` +const PingQuery = `SELECT 'ping'`; diff --git a/packages/client-node/src/connection/node_custom_agent_connection.ts b/packages/client-node/src/connection/node_custom_agent_connection.ts index b6a898e9..0f55b9c0 100644 --- a/packages/client-node/src/connection/node_custom_agent_connection.ts +++ b/packages/client-node/src/connection/node_custom_agent_connection.ts @@ -1,25 +1,25 @@ -import Http from 'http' -import Https from 'https' -import type { NodeConnectionParams } from './node_base_connection' -import type { RequestParams } from './socket_pool' -import { NodeBaseConnection } from './node_base_connection' -import { withCompressionHeaders } from '@clickhouse/client-common' +import Http from "http"; +import Https from "https"; +import type { NodeConnectionParams } from "./node_base_connection"; +import type { RequestParams } from "./socket_pool"; +import { NodeBaseConnection } from "./node_base_connection"; +import { withCompressionHeaders } from "@clickhouse/client-common"; export class NodeCustomAgentConnection extends NodeBaseConnection { - private readonly httpRequestFn: typeof Http.request | typeof Https.request + private readonly httpRequestFn: typeof Http.request | typeof Https.request; constructor(params: NodeConnectionParams) { if (!params.http_agent) { throw new Error( - 'http_agent is required to create NodeCustomAgentConnection', - ) + "http_agent is required to create NodeCustomAgentConnection", + ); } - super(params, params.http_agent) + super(params, params.http_agent); // See https://github.com/ClickHouse/clickhouse-js/issues/352 - if (params.url.protocol.startsWith('https')) { - this.httpRequestFn = Https.request + if (params.url.protocol.startsWith("https")) { + this.httpRequestFn = Https.request; } else { - this.httpRequestFn = Http.request + this.httpRequestFn = Http.request; } } @@ -28,7 +28,7 @@ export class NodeCustomAgentConnection extends NodeBaseConnection { headers: params.headers, enable_request_compression: params.enable_request_compression, enable_response_compression: params.enable_response_compression, - }) + }); return this.httpRequestFn(params.url, { method: params.method, agent: this.agent, @@ -38,6 +38,6 @@ export class NodeCustomAgentConnection extends NodeBaseConnection { ...(this.params.max_response_headers_size !== undefined && { maxHeaderSize: this.params.max_response_headers_size, }), - }) + }); } } diff --git a/packages/client-node/src/connection/node_http_connection.ts b/packages/client-node/src/connection/node_http_connection.ts index eb25493c..5a3f7940 100644 --- a/packages/client-node/src/connection/node_http_connection.ts +++ b/packages/client-node/src/connection/node_http_connection.ts @@ -1,16 +1,16 @@ -import { withCompressionHeaders } from '@clickhouse/client-common' -import Http from 'http' -import type { NodeConnectionParams } from './node_base_connection' -import type { RequestParams } from './socket_pool' -import { NodeBaseConnection } from './node_base_connection' +import { withCompressionHeaders } from "@clickhouse/client-common"; +import Http from "http"; +import type { NodeConnectionParams } from "./node_base_connection"; +import type { RequestParams } from "./socket_pool"; +import { NodeBaseConnection } from "./node_base_connection"; export class NodeHttpConnection extends NodeBaseConnection { constructor(params: NodeConnectionParams) { const agent = new Http.Agent({ keepAlive: params.keep_alive.enabled, maxSockets: params.max_open_connections, - }) - super(params, agent) + }); + super(params, agent); } protected createClientRequest(params: RequestParams): Http.ClientRequest { @@ -18,7 +18,7 @@ export class NodeHttpConnection extends NodeBaseConnection { headers: params.headers, enable_request_compression: params.enable_request_compression, enable_response_compression: params.enable_response_compression, - }) + }); return Http.request(params.url, { method: params.method, agent: this.agent, @@ -28,6 +28,6 @@ export class NodeHttpConnection extends NodeBaseConnection { ...(this.params.max_response_headers_size !== undefined && { maxHeaderSize: this.params.max_response_headers_size, }), - }) + }); } } diff --git a/packages/client-node/src/connection/node_https_connection.ts b/packages/client-node/src/connection/node_https_connection.ts index 2a0d693a..a7d9b98d 100644 --- a/packages/client-node/src/connection/node_https_connection.ts +++ b/packages/client-node/src/connection/node_https_connection.ts @@ -2,12 +2,12 @@ import { type ConnBaseQueryParams, isCredentialsAuth, withCompressionHeaders, -} from '@clickhouse/client-common' -import type Http from 'http' -import Https from 'https' -import type { NodeConnectionParams } from './node_base_connection' -import type { RequestParams } from './socket_pool' -import { NodeBaseConnection } from './node_base_connection' +} from "@clickhouse/client-common"; +import type Http from "http"; +import Https from "https"; +import type { NodeConnectionParams } from "./node_base_connection"; +import type { RequestParams } from "./socket_pool"; +import { NodeBaseConnection } from "./node_base_connection"; export class NodeHttpsConnection extends NodeBaseConnection { constructor(params: NodeConnectionParams) { @@ -15,49 +15,49 @@ export class NodeHttpsConnection extends NodeBaseConnection { keepAlive: params.keep_alive.enabled, maxSockets: params.max_open_connections, ca: params.tls?.ca_cert, - key: params.tls?.type === 'Mutual' ? params.tls.key : undefined, - cert: params.tls?.type === 'Mutual' ? params.tls.cert : undefined, - }) - super(params, agent) + key: params.tls?.type === "Mutual" ? params.tls.key : undefined, + cert: params.tls?.type === "Mutual" ? params.tls.cert : undefined, + }); + super(params, agent); } protected override buildRequestHeaders( params?: ConnBaseQueryParams, ): Http.OutgoingHttpHeaders { if (this.params.tls !== undefined) { - if (this.params.auth.type === 'JWT') { + if (this.params.auth.type === "JWT") { throw new Error( - 'JWT auth is not supported with HTTPS connection using custom certificates', - ) + "JWT auth is not supported with HTTPS connection using custom certificates", + ); } - let headers: Http.OutgoingHttpHeaders + let headers: Http.OutgoingHttpHeaders; if (isCredentialsAuth(params?.auth)) { headers = { ...this.defaultHeadersWithOverride(params), - 'X-ClickHouse-User': params.auth.username, - 'X-ClickHouse-Key': params.auth.password, - } + "X-ClickHouse-User": params.auth.username, + "X-ClickHouse-Key": params.auth.password, + }; } else { headers = { ...this.defaultHeadersWithOverride(params), - 'X-ClickHouse-User': this.params.auth.username, - 'X-ClickHouse-Key': this.params.auth.password, - } + "X-ClickHouse-User": this.params.auth.username, + "X-ClickHouse-Key": this.params.auth.password, + }; } - const tlsType = this.params.tls.type + const tlsType = this.params.tls.type; switch (tlsType) { - case 'Basic': - return headers - case 'Mutual': + case "Basic": + return headers; + case "Mutual": return { ...headers, - 'X-ClickHouse-SSL-Certificate-Auth': 'on', - } + "X-ClickHouse-SSL-Certificate-Auth": "on", + }; default: - throw new Error(`Unknown TLS type: ${tlsType}`) + throw new Error(`Unknown TLS type: ${tlsType}`); } } - return super.buildRequestHeaders(params) + return super.buildRequestHeaders(params); } protected createClientRequest(params: RequestParams): Http.ClientRequest { @@ -65,7 +65,7 @@ export class NodeHttpsConnection extends NodeBaseConnection { headers: params.headers, enable_request_compression: params.enable_request_compression, enable_response_compression: params.enable_response_compression, - }) + }); return Https.request(params.url, { method: params.method, agent: this.agent, @@ -75,6 +75,6 @@ export class NodeHttpsConnection extends NodeBaseConnection { ...(this.params.max_response_headers_size !== undefined && { maxHeaderSize: this.params.max_response_headers_size, }), - }) + }); } } diff --git a/packages/client-node/src/connection/socket_pool.ts b/packages/client-node/src/connection/socket_pool.ts index c6b14043..87ee3a72 100644 --- a/packages/client-node/src/connection/socket_pool.ts +++ b/packages/client-node/src/connection/socket_pool.ts @@ -1,7 +1,7 @@ -import Http from 'http' -import Stream from 'stream' -import type * as net from 'net' -import Zlib from 'zlib' +import Http from "http"; +import Stream from "stream"; +import type * as net from "net"; +import Zlib from "zlib"; import { enhanceStackTrace, getCurrentStackTrace, @@ -14,72 +14,72 @@ import { type ResponseHeaders, type ClickHouseSummary, type JSONHandling, -} from '@clickhouse/client-common' -import { getAsText, isStream } from '../utils' -import { decompressResponse, isDecompressionError } from './compression' -import { type NodeConnectionParams } from './node_base_connection' +} from "@clickhouse/client-common"; +import { getAsText, isStream } from "../utils"; +import { decompressResponse, isDecompressionError } from "./compression"; +import { type NodeConnectionParams } from "./node_base_connection"; export interface RequestParams { - method: 'GET' | 'POST' - url: URL - headers: Http.OutgoingHttpHeaders - body?: string | Stream.Readable + method: "GET" | "POST"; + url: URL; + headers: Http.OutgoingHttpHeaders; + body?: string | Stream.Readable; // provided by the user and wrapped around internally - abort_signal: AbortSignal - enable_response_compression?: boolean - enable_request_compression?: boolean + abort_signal: AbortSignal; + enable_response_compression?: boolean; + enable_request_compression?: boolean; // if there are compression headers, attempt to decompress it - try_decompress_response_stream?: boolean + try_decompress_response_stream?: boolean; // if the response contains an error, ignore it and return the stream as-is - ignore_error_response?: boolean - parse_summary?: boolean - query: string - query_id: string - log_writer: LogWriter - log_level: ClickHouseLogLevel + ignore_error_response?: boolean; + parse_summary?: boolean; + query: string; + query_id: string; + log_writer: LogWriter; + log_level: ClickHouseLogLevel; } export interface RequestResult { - stream: Stream.Readable - response_headers: ResponseHeaders - http_status_code?: number - summary?: ClickHouseSummary + stream: Stream.Readable; + response_headers: ResponseHeaders; + http_status_code?: number; + summary?: ClickHouseSummary; } interface SocketInfo { - id: string - idle_timeout_handle: ReturnType | undefined - usage_count: number - server_keep_alive_timeout_ms?: number - freed_at_timestamp_ms?: number + id: string; + idle_timeout_handle: ReturnType | undefined; + usage_count: number; + server_keep_alive_timeout_ms?: number; + freed_at_timestamp_ms?: number; } -type CreateClientRequest = (params: RequestParams) => Http.ClientRequest +type CreateClientRequest = (params: RequestParams) => Http.ClientRequest; export class SocketPool { - private readonly jsonHandling: JSONHandling - private readonly knownSockets = new WeakMap() + private readonly jsonHandling: JSONHandling; + private readonly knownSockets = new WeakMap(); // For overflow concerns: // node -e 'console.log(Number.MAX_SAFE_INTEGER / (1_000_000 * 60 * 60 * 24 * 366))' // gives 284 years of continuous operation at 1M requests per second // before overflowing the 53-bit integer - private requestCounter = 0 + private requestCounter = 0; private getNewRequestId(): string { - this.requestCounter += 1 - return `${this.connectionId}:${this.requestCounter}` + this.requestCounter += 1; + return `${this.connectionId}:${this.requestCounter}`; } - private socketCounter = 0 + private socketCounter = 0; private getNewSocketId(): string { - this.socketCounter += 1 - return `${this.connectionId}:${this.socketCounter}` + this.socketCounter += 1; + return `${this.connectionId}:${this.socketCounter}`; } - private readonly connectionId: string - private readonly params: NodeConnectionParams - private readonly createClientRequest: CreateClientRequest - private readonly agent: Http.Agent + private readonly connectionId: string; + private readonly params: NodeConnectionParams; + private readonly createClientRequest: CreateClientRequest; + private readonly agent: Http.Agent; constructor( connectionId: string, @@ -87,14 +87,14 @@ export class SocketPool { createClientRequest: CreateClientRequest, agent: Http.Agent, ) { - this.connectionId = connectionId - this.params = params - this.createClientRequest = createClientRequest - this.agent = agent + this.connectionId = connectionId; + this.params = params; + this.createClientRequest = createClientRequest; + this.agent = agent; this.jsonHandling = params.json ?? { parse: JSON.parse, stringify: JSON.stringify, - } + }; } async request( @@ -103,12 +103,12 @@ export class SocketPool { ): Promise { // allows the event loop to process the idle socket timers, if the CPU load is high // otherwise, we can occasionally get an expired socket, see https://github.com/ClickHouse/clickhouse-js/issues/294 - await sleep(0) - const { log_writer, query_id, log_level } = params + await sleep(0); + const { log_writer, query_id, log_level } = params; const currentStackTrace = this.params.capture_enhanced_stack_trace ? getCurrentStackTrace() - : undefined - const requestTimeout = this.params.request_timeout + : undefined; + const requestTimeout = this.params.request_timeout; if ( this.params.eagerly_destroy_stale_sockets && @@ -118,14 +118,14 @@ export class SocketPool { // Only run this cleanup for the built-in Node.js HTTP agent, since it relies on `freeSockets`. if (this.agent instanceof Http.Agent) { for (const host of Object.keys(this.agent.freeSockets)) { - const byHostSockets = this.agent.freeSockets[host] + const byHostSockets = this.agent.freeSockets[host]; if (byHostSockets) { for (const socket of [...byHostSockets]) { - const socketInfo = this.knownSockets.get(socket) + const socketInfo = this.knownSockets.get(socket); if (socketInfo) { - const freedAt = socketInfo.freed_at_timestamp_ms + const freedAt = socketInfo.freed_at_timestamp_ms; if (freedAt) { - const socketAge = Date.now() - freedAt + const socketAge = Date.now() - freedAt; // The check below is still racy on a CPU starved machine. // A throttled machine can check time on one line, then get descheduled, // decide the socket is still good after rescheduling, and then proceed @@ -145,11 +145,11 @@ export class SocketPool { idle_socket_ttl_ms: this.params.keep_alive.idle_socket_ttl, }, - }) + }); } - clearTimeout(socketInfo.idle_timeout_handle) - this.knownSockets.delete(socket) - socket.destroy() + clearTimeout(socketInfo.idle_timeout_handle); + this.knownSockets.delete(socket); + socket.destroy(); } } } @@ -159,15 +159,15 @@ export class SocketPool { } } - const start = Date.now() - const request = this.createClientRequest(params) - const request_id = this.getNewRequestId() + const start = Date.now(); + const request = this.createClientRequest(params); + const request_id = this.getNewRequestId(); return new Promise((resolve, reject) => { const onError = (e: unknown): void => { - removeRequestListeners() + removeRequestListeners(); if (e instanceof Error) { if (log_level <= ClickHouseLogLevel.TRACE) { - if ((e as any).code === 'ECONNRESET') { + if ((e as any).code === "ECONNRESET") { log_writer.trace({ message: `${op}: connection reset by peer`, args: { @@ -176,19 +176,19 @@ export class SocketPool { query_id, request_id, }, - module: 'HTTP Adapter', - }) + module: "HTTP Adapter", + }); } } if (log_level <= ClickHouseLogLevel.WARN) { if (this.params.keep_alive.enabled) { - if ((e as any).code === 'ECONNRESET') { - const socket = request.socket + if ((e as any).code === "ECONNRESET") { + const socket = request.socket; if (socket) { - const socketInfo = this.knownSockets.get(socket) + const socketInfo = this.knownSockets.get(socket); if (socketInfo) { const serverTimeoutMs = - socketInfo.server_keep_alive_timeout_ms + socketInfo.server_keep_alive_timeout_ms; if (serverTimeoutMs !== undefined) { if ( this.params.keep_alive.idle_socket_ttl > serverTimeoutMs @@ -205,8 +205,8 @@ export class SocketPool { idle_socket_ttl: this.params.keep_alive.idle_socket_ttl, }, - module: 'HTTP Adapter', - }) + module: "HTTP Adapter", + }); } } } @@ -215,21 +215,21 @@ export class SocketPool { } } - const err = enhanceStackTrace(e, currentStackTrace) - reject(err) + const err = enhanceStackTrace(e, currentStackTrace); + reject(err); } else { - reject(e) + reject(e); } - } + }; - let responseStream: Stream.Readable + let responseStream: Stream.Readable; const onResponse = async ( _response: Http.IncomingMessage, ): Promise => { if (this.params.log_level <= ClickHouseLogLevel.DEBUG) { - const duration = Date.now() - start + const duration = Date.now() - start; this.params.log_writer.debug({ - module: 'HTTP Adapter', + module: "HTTP Adapter", message: `${op}: got a response from ClickHouse`, args: { operation: op, @@ -241,23 +241,23 @@ export class SocketPool { response_status: _response.statusCode, response_time_ms: duration, }, - }) + }); } if (this.params.keep_alive.enabled) { - const keepAliveHeader = _response.headers['keep-alive'] + const keepAliveHeader = _response.headers["keep-alive"]; if (keepAliveHeader) { const [, timeout] = - /timeout=(\d+)/i.exec(String(keepAliveHeader)) ?? [] + /timeout=(\d+)/i.exec(String(keepAliveHeader)) ?? []; if (timeout) { - const socketInfo = this.knownSockets.get(_response.socket) + const socketInfo = this.knownSockets.get(_response.socket); if (socketInfo) { - const timeoutMs = Number(timeout) * 1000 - socketInfo.server_keep_alive_timeout_ms = timeoutMs + const timeoutMs = Number(timeout) * 1000; + socketInfo.server_keep_alive_timeout_ms = timeoutMs; if (log_level <= ClickHouseLogLevel.TRACE) { this.params.log_writer.trace({ - module: 'HTTP Adapter', + module: "HTTP Adapter", message: `${op}: updated server sent socket keep-alive timeout`, args: { operation: op, @@ -267,7 +267,7 @@ export class SocketPool { socket_id: socketInfo.id, server_keep_alive_timeout_ms: timeoutMs, }, - }) + }); } } } @@ -275,10 +275,10 @@ export class SocketPool { } const tryDecompressResponseStream = - params.try_decompress_response_stream ?? true - const ignoreErrorResponse = params.ignore_error_response ?? false + params.try_decompress_response_stream ?? true; + const ignoreErrorResponse = params.ignore_error_response ?? false; // even if the stream decompression is disabled, we have to decompress it in case of an error - const isFailedResponse = !isSuccessfulResponse(_response.statusCode) + const isFailedResponse = !isSuccessfulResponse(_response.statusCode); if ( tryDecompressResponseStream || (isFailedResponse && !ignoreErrorResponse) @@ -287,17 +287,17 @@ export class SocketPool { _response, log_writer, log_level, - ) + ); if (isDecompressionError(decompressionResult)) { const err = enhanceStackTrace( decompressionResult.error, currentStackTrace, - ) - return reject(err) + ); + return reject(err); } - responseStream = decompressionResult.response + responseStream = decompressionResult.response; } else { - responseStream = _response + responseStream = _response; } if (log_level <= ClickHouseLogLevel.TRACE) { @@ -316,21 +316,21 @@ export class SocketPool { is_failed_response: isFailedResponse, will_decompress: tryDecompressResponseStream, }, - }) + }); } if (isFailedResponse && !ignoreErrorResponse) { try { - const errorMessage = await getAsText(responseStream) + const errorMessage = await getAsText(responseStream); const err = enhanceStackTrace( parseError(errorMessage), currentStackTrace, - ) - reject(err) + ); + reject(err); } catch (e) { // If the ClickHouse response is malformed - const err = enhanceStackTrace(e as Error, currentStackTrace) - reject(err) + const err = enhanceStackTrace(e as Error, currentStackTrace); + reject(err); } } else { return resolve({ @@ -340,56 +340,56 @@ export class SocketPool { : undefined, response_headers: { ..._response.headers }, http_status_code: _response.statusCode ?? undefined, - }) + }); } - } + }; function onAbort(): void { // Prefer 'abort' event since it always triggered unlike 'error' and 'close' // see the full sequence of events https://nodejs.org/api/http.html#httprequesturl-options-callback - removeRequestListeners() - request.once('error', function () { + removeRequestListeners(); + request.once("error", function () { /** * catch "Error: ECONNRESET" error which shouldn't be reported to users. * see the full sequence of events https://nodejs.org/api/http.html#httprequesturl-options-callback * */ - }) + }); const err = enhanceStackTrace( - new Error('The user aborted a request.'), + new Error("The user aborted a request."), currentStackTrace, - ) - reject(err) + ); + reject(err); } function onClose(): void { // Adapter uses 'close' event to clean up listeners after the successful response. // It's necessary in order to handle 'abort' and 'timeout' events while response is streamed. // It's always the last event, according to https://nodejs.org/docs/latest-v14.x/api/http.html#http_http_request_url_options_callback - removeRequestListeners() + removeRequestListeners(); } function pipeStream(): void { // if request.end() was called due to no data to send if (request.writableEnded) { - return + return; } const bodyStream = isStream(params.body) ? params.body - : Stream.Readable.from([params.body]) + : Stream.Readable.from([params.body]); const callback = (e: NodeJS.ErrnoException | null): void => { if (e) { - removeRequestListeners() - const err = enhanceStackTrace(e, currentStackTrace) - reject(err) + removeRequestListeners(); + const err = enhanceStackTrace(e, currentStackTrace); + reject(err); } - } + }; if (params.enable_request_compression) { - Stream.pipeline(bodyStream, Zlib.createGzip(), request, callback) + Stream.pipeline(bodyStream, Zlib.createGzip(), request, callback); } else { - Stream.pipeline(bodyStream, request, callback) + Stream.pipeline(bodyStream, request, callback); } } @@ -399,11 +399,11 @@ export class SocketPool { this.params.keep_alive.enabled && this.params.keep_alive.idle_socket_ttl > 0 ) { - const socketInfo = this.knownSockets.get(socket) + const socketInfo = this.knownSockets.get(socket); // It is the first time we've encountered this socket, // so it doesn't have the idle timeout handler attached to it if (socketInfo === undefined) { - const socket_id = this.getNewSocketId() + const socket_id = this.getNewSocketId(); if (log_level <= ClickHouseLogLevel.TRACE) { log_writer.trace({ message: `${op}: using a fresh socket, setting up a new 'free' listener`, @@ -414,17 +414,17 @@ export class SocketPool { request_id, socket_id, }, - }) + }); } const newSocketInfo: SocketInfo = { id: socket_id, idle_timeout_handle: undefined, usage_count: 1, - } - this.knownSockets.set(socket, newSocketInfo) + }; + this.knownSockets.set(socket, newSocketInfo); // When the request is complete and the socket is released, // make sure that the socket is removed after `idle_socket_ttl`. - socket.on('free', () => { + socket.on("free", () => { if (log_level <= ClickHouseLogLevel.TRACE) { log_writer.trace({ message: `${op}: socket was released`, @@ -435,14 +435,14 @@ export class SocketPool { request_id, socket_id, }, - }) + }); } - const freed_at_timestamp_ms = Date.now() - newSocketInfo.freed_at_timestamp_ms = freed_at_timestamp_ms + const freed_at_timestamp_ms = Date.now(); + newSocketInfo.freed_at_timestamp_ms = freed_at_timestamp_ms; // Avoiding the built-in socket.timeout() method usage here, // as we don't want to clash with the actual request timeout. const idleTimeoutHandle = setTimeout(() => { - const freedAfter = Date.now() - freed_at_timestamp_ms + const freedAfter = Date.now() - freed_at_timestamp_ms; if (log_level <= ClickHouseLogLevel.TRACE) { log_writer.trace({ message: `${op}: removing idle socket`, @@ -456,19 +456,19 @@ export class SocketPool { this.params.keep_alive.idle_socket_ttl, freed_after_ms: freedAfter, }, - }) + }); } - this.knownSockets.delete(socket) - socket.destroy() - }, this.params.keep_alive.idle_socket_ttl).unref() - newSocketInfo.idle_timeout_handle = idleTimeoutHandle - }) + this.knownSockets.delete(socket); + socket.destroy(); + }, this.params.keep_alive.idle_socket_ttl).unref(); + newSocketInfo.idle_timeout_handle = idleTimeoutHandle; + }); const cleanup = (eventName: string) => () => { - const maybeSocketInfo = this.knownSockets.get(socket) + const maybeSocketInfo = this.knownSockets.get(socket); // clean up a possibly dangling idle timeout handle (preventing leaks) if (maybeSocketInfo?.idle_timeout_handle) { - clearTimeout(maybeSocketInfo.idle_timeout_handle) + clearTimeout(maybeSocketInfo.idle_timeout_handle); } if (log_level <= ClickHouseLogLevel.TRACE) { log_writer.trace({ @@ -481,7 +481,7 @@ export class SocketPool { socket_id, event: eventName, }, - }) + }); } if (log_level <= ClickHouseLogLevel.WARN) { @@ -496,22 +496,22 @@ export class SocketPool { socket_id, event: eventName, }, - }) + }); } } - } - socket.once('end', cleanup('end')) - socket.once('close', cleanup('close')) + }; + socket.once("end", cleanup("end")); + socket.once("close", cleanup("close")); } else { - const freedAt = socketInfo.freed_at_timestamp_ms + const freedAt = socketInfo.freed_at_timestamp_ms; if (freedAt) { // On a CPU throttled machine or when event loop is delayed, // the socket can be idle for much longer than `idle_socket_ttl` // as the timers don't fire exactly on time which can lead // to a stale socket being reused. - const socketAge = Date.now() - freedAt + const socketAge = Date.now() - freedAt; const overdueBy = - socketAge - this.params.keep_alive.idle_socket_ttl + socketAge - this.params.keep_alive.idle_socket_ttl; // Give some grace period to account for timer inaccuracy and minor // event loop delays, but log if the socket is significantly overdue if (overdueBy > 1000) { @@ -527,13 +527,13 @@ export class SocketPool { idle_socket_ttl_ms: this.params.keep_alive.idle_socket_ttl, }, - }) + }); } } } - clearTimeout(socketInfo.idle_timeout_handle) - socketInfo.idle_timeout_handle = undefined + clearTimeout(socketInfo.idle_timeout_handle); + socketInfo.idle_timeout_handle = undefined; if (log_level <= ClickHouseLogLevel.TRACE) { log_writer.trace({ message: `${op}: reusing socket`, @@ -545,9 +545,9 @@ export class SocketPool { socket_id: socketInfo.id, usage_count: socketInfo.usage_count, }, - }) + }); } - socketInfo.usage_count++ + socketInfo.usage_count++; } } } catch (e) { @@ -561,17 +561,17 @@ export class SocketPool { query_id, request_id, }, - }) + }); } } // Socket is "prepared" with idle handlers, continue with our request - pipeStream() + pipeStream(); // This is for request timeout only. Surprisingly, it is not always enough to set in the HTTP request. // The socket won't be destroyed, and it will be returned to the pool. if (log_level <= ClickHouseLogLevel.TRACE) { - const socketInfo = this.knownSockets.get(socket) + const socketInfo = this.knownSockets.get(socket); if (socketInfo) { log_writer.trace({ message: `${op}: setting up request timeout`, @@ -583,7 +583,7 @@ export class SocketPool { socket_id: socketInfo.id, timeout_ms: requestTimeout, }, - }) + }); } else { log_writer.trace({ message: `${op}: setting up request timeout on a socket`, @@ -594,20 +594,20 @@ export class SocketPool { request_id, timeout_ms: requestTimeout, }, - }) + }); } } - socket.setTimeout(this.params.request_timeout, onTimeout) - } + socket.setTimeout(this.params.request_timeout, onTimeout); + }; const onTimeout = (): void => { - removeRequestListeners() + removeRequestListeners(); if (log_level <= ClickHouseLogLevel.TRACE) { - const socket = request.socket + const socket = request.socket; const maybeSocketInfo = socket ? this.knownSockets.get(socket) - : undefined + : undefined; const socketState = request.socket ? { @@ -616,14 +616,14 @@ export class SocketPool { destroyed: request.socket.destroyed, readyState: request.socket.readyState, } - : undefined + : undefined; const responseStreamState = responseStream ? { readable: responseStream.readable, readableEnded: responseStream.readableEnded, readableLength: responseStream.readableLength, } - : undefined + : undefined; log_writer.trace({ message: `${op}: timeout occurred`, @@ -638,15 +638,15 @@ export class SocketPool { response_stream_state: responseStreamState, has_response_stream: responseStream !== undefined, }, - }) + }); } const err = enhanceStackTrace( - new Error('Timeout error.'), + new Error("Timeout error."), currentStackTrace, - ) + ); try { - request.destroy() + request.destroy(); } catch (e) { if (log_level <= ClickHouseLogLevel.ERROR) { log_writer.error({ @@ -658,40 +658,40 @@ export class SocketPool { query_id, request_id, }, - }) + }); } } - reject(err) - } + reject(err); + }; function removeRequestListeners(): void { if (request.socket) { - request.socket.setTimeout(0) // reset previously set timeout - request.socket.removeListener('timeout', onTimeout) + request.socket.setTimeout(0); // reset previously set timeout + request.socket.removeListener("timeout", onTimeout); } - request.removeListener('socket', onSocket) - request.removeListener('response', onResponse) - request.removeListener('error', onError) - request.removeListener('close', onClose) + request.removeListener("socket", onSocket); + request.removeListener("response", onResponse); + request.removeListener("error", onError); + request.removeListener("close", onClose); if (params.abort_signal) { - request.removeListener('abort', onAbort) + request.removeListener("abort", onAbort); } } - request.on('socket', onSocket) - request.on('response', onResponse) - request.on('error', onError) - request.on('close', onClose) + request.on("socket", onSocket); + request.on("response", onResponse); + request.on("error", onError); + request.on("close", onClose); if (params.abort_signal) { - params.abort_signal.addEventListener('abort', onAbort, { + params.abort_signal.addEventListener("abort", onAbort, { once: true, - }) + }); } if (!params.body) { try { - return request.end() + return request.end(); } catch (e) { if (log_level <= ClickHouseLogLevel.ERROR) { log_writer.error({ @@ -703,21 +703,21 @@ export class SocketPool { query_id, request_id, }, - }) + }); } } } - }) + }); } private parseSummary( op: ConnOperation, response: Http.IncomingMessage, ): ClickHouseSummary | undefined { - const summaryHeader = response.headers['x-clickhouse-summary'] - if (typeof summaryHeader === 'string') { + const summaryHeader = response.headers["x-clickhouse-summary"]; + if (typeof summaryHeader === "string") { try { - return this.jsonHandling.parse(summaryHeader) + return this.jsonHandling.parse(summaryHeader); } catch (err) { if (this.params.log_level <= ClickHouseLogLevel.ERROR) { this.params.log_writer.error({ @@ -725,10 +725,10 @@ export class SocketPool { args: { operation: op, connection_id: this.connectionId, - 'X-ClickHouse-Summary': summaryHeader, + "X-ClickHouse-Summary": summaryHeader, }, err: err as Error, - }) + }); } } } diff --git a/packages/client-node/src/connection/stream.ts b/packages/client-node/src/connection/stream.ts index 5d79b020..14268907 100644 --- a/packages/client-node/src/connection/stream.ts +++ b/packages/client-node/src/connection/stream.ts @@ -2,14 +2,14 @@ import { type LogWriter, type ConnOperation, ClickHouseLogLevel, -} from '@clickhouse/client-common' -import type Stream from 'stream' +} from "@clickhouse/client-common"; +import type Stream from "stream"; export interface Context { - op: ConnOperation - log_level: ClickHouseLogLevel - log_writer: LogWriter - query_id: string + op: ConnOperation; + log_level: ClickHouseLogLevel; + log_writer: LogWriter; + query_id: string; } /** Drains the response stream, as calling `destroy` on a {@link Stream.Readable} response stream @@ -22,23 +22,23 @@ export async function drainStream(stream: Stream.Readable): Promise { // If the stream has already emitted an error, we can reject the promise immediately. if (stream.errored) { // the stream is already errored, no need to attach listeners - reject(stream.errored) - return + reject(stream.errored); + return; } // Avoid a race condition where the stream has already sent the 'end' event before we attach the listener. // In this case, we can resolve the promise immediately without attaching any listeners. if (stream.readableEnded) { // the stream is already ended, no need to attach listeners - resolve() - return + resolve(); + return; } // If the stream is already closed, we can resolve the promise immediately as well. if (stream.closed) { // the stream is already closed, no need to attach listeners - resolve() - return + resolve(); + return; } function dropData() { @@ -46,34 +46,34 @@ export async function drainStream(stream: Stream.Readable): Promise { } function onEnd() { - removeListeners() - resolve() + removeListeners(); + resolve(); } function onError(err: Error) { - removeListeners() - reject(err) + removeListeners(); + reject(err); } function onClose() { - removeListeners() + removeListeners(); // The `end` event might not be emitted if the server closes the connection. // Making sure to resolve the promise in this case as well. - resolve() + resolve(); } function removeListeners() { - stream.removeListener('data', dropData) - stream.removeListener('end', onEnd) - stream.removeListener('error', onError) - stream.removeListener('close', onClose) + stream.removeListener("data", dropData); + stream.removeListener("end", onEnd); + stream.removeListener("error", onError); + stream.removeListener("close", onClose); } - stream.on('data', dropData) - stream.on('end', onEnd) - stream.on('error', onError) - stream.on('close', onClose) - }) + stream.on("data", dropData); + stream.on("end", onEnd); + stream.on("error", onError); + stream.on("close", onClose); + }); } /** Drains the response stream, as calling `destroy` on a {@link Stream.Readable} response stream @@ -85,9 +85,9 @@ export async function drainStreamInternal( stream: Stream.Readable, ): Promise { return new Promise((resolve, reject) => { - const startTime = Date.now() - let bytesReceived = 0 - let chunkCount = 0 + const startTime = Date.now(); + let bytesReceived = 0; + let chunkCount = 0; if (ctx.log_level <= ClickHouseLogLevel.TRACE) { ctx.log_writer.trace({ @@ -100,7 +100,7 @@ export async function drainStreamInternal( readableLength: stream.readableLength, }, }, - }) + }); } // If the stream has already emitted an error, we can reject the promise immediately. @@ -113,11 +113,11 @@ export async function drainStreamInternal( chunk_number: chunkCount, total_bytes_received: bytesReceived, }, - }) + }); } // the stream is already errored, no need to attach listeners - reject(stream.errored) - return + reject(stream.errored); + return; } // Avoid a race condition where the stream has already sent the 'end' event before we attach the listener. @@ -131,11 +131,11 @@ export async function drainStreamInternal( chunk_number: chunkCount, total_bytes_received: bytesReceived, }, - }) + }); } // the stream is already ended, no need to attach listeners - resolve() - return + resolve(); + return; } // If the stream is already closed, we can resolve the promise immediately as well. @@ -148,19 +148,19 @@ export async function drainStreamInternal( chunk_number: chunkCount, total_bytes_received: bytesReceived, }, - }) + }); } // the stream is already closed, no need to attach listeners - resolve() - return + resolve(); + return; } function dropData(chunk: Buffer | string) { // used only for the methods without expected response; we don't care about the data here if (ctx.log_level <= ClickHouseLogLevel.TRACE) { - chunkCount++ - const chunk_size = Buffer.byteLength(chunk) - bytesReceived += chunk_size + chunkCount++; + const chunk_size = Buffer.byteLength(chunk); + bytesReceived += chunk_size; ctx.log_writer.trace({ message: `${ctx.op}: received data chunk during drain`, args: { @@ -169,14 +169,14 @@ export async function drainStreamInternal( chunk_size, total_bytes_received: bytesReceived, }, - }) + }); } } function onEnd() { - removeListeners() + removeListeners(); if (ctx.log_level <= ClickHouseLogLevel.TRACE) { - const duration = Date.now() - startTime + const duration = Date.now() - startTime; ctx.log_writer.trace({ message: `${ctx.op}: stream drain completed (end event)`, args: { @@ -185,15 +185,15 @@ export async function drainStreamInternal( total_bytes_received: bytesReceived, total_chunks: chunkCount, }, - }) + }); } - resolve() + resolve(); } function onError(err: Error) { - removeListeners() + removeListeners(); if (ctx.log_level <= ClickHouseLogLevel.TRACE) { - const duration = Date.now() - startTime + const duration = Date.now() - startTime; ctx.log_writer.trace({ message: `${ctx.op}: stream drain failed (error event)`, args: { @@ -203,15 +203,15 @@ export async function drainStreamInternal( total_chunks: chunkCount, error: err.message, }, - }) + }); } - reject(err) + reject(err); } function onClose() { - removeListeners() + removeListeners(); if (ctx.log_level <= ClickHouseLogLevel.TRACE) { - const duration = Date.now() - startTime + const duration = Date.now() - startTime; ctx.log_writer.trace({ message: `${ctx.op}: stream closed during drain (close event)`, args: { @@ -220,23 +220,23 @@ export async function drainStreamInternal( total_bytes_received: bytesReceived, total_chunks: chunkCount, }, - }) + }); } // The `end` event might not be emitted if the server closes the connection. // Making sure to resolve the promise in this case as well. - resolve() + resolve(); } function removeListeners() { - stream.removeListener('data', dropData) - stream.removeListener('end', onEnd) - stream.removeListener('error', onError) - stream.removeListener('close', onClose) + stream.removeListener("data", dropData); + stream.removeListener("end", onEnd); + stream.removeListener("error", onError); + stream.removeListener("close", onClose); } - stream.on('data', dropData) - stream.on('end', onEnd) - stream.on('error', onError) - stream.on('close', onClose) - }) + stream.on("data", dropData); + stream.on("end", onEnd); + stream.on("error", onError); + stream.on("close", onClose); + }); } diff --git a/packages/client-node/src/index.ts b/packages/client-node/src/index.ts index 5166afec..63179ffa 100644 --- a/packages/client-node/src/index.ts +++ b/packages/client-node/src/index.ts @@ -1,11 +1,11 @@ export { NodeClickHouseClient as ClickHouseClient, type QueryResult, -} from './client' -export { createClient } from './client' -export { type NodeClickHouseClientConfigOptions as ClickHouseClientConfigOptions } from './config' -export { ResultSet, type StreamReadable } from './result_set' -export { drainStream } from './connection/stream' +} from "./client"; +export { createClient } from "./client"; +export { type NodeClickHouseClientConfigOptions as ClickHouseClientConfigOptions } from "./config"; +export { ResultSet, type StreamReadable } from "./result_set"; +export { drainStream } from "./connection/stream"; /** Re-export @clickhouse/client-common types */ export { @@ -55,7 +55,7 @@ export { type ClickHouseAuth, type ClickHouseJWTAuth, type ClickHouseCredentialsAuth, -} from '@clickhouse/client-common' +} from "@clickhouse/client-common"; /** * Re-export @clickhouse/client-common runtime values. @@ -83,26 +83,26 @@ import { isException as isException_, TupleParam as TupleParam_, defaultJSONHandling as defaultJSONHandling_, -} from '@clickhouse/client-common' +} 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_ +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 f015c5ed..f45f050e 100644 --- a/packages/client-node/src/result_set.ts +++ b/packages/client-node/src/result_set.ts @@ -6,77 +6,77 @@ import type { ResultJSONType, ResultStream, Row, -} from '@clickhouse/client-common' +} from "@clickhouse/client-common"; import { extractErrorAtTheEndOfChunk, defaultJSONHandling, EXCEPTION_TAG_HEADER_NAME, CARET_RETURN, -} from '@clickhouse/client-common' +} from "@clickhouse/client-common"; import { isNotStreamableJSONFamily, isStreamableJSONFamily, validateStreamFormat, -} from '@clickhouse/client-common' -import { Buffer } from 'buffer' -import type { Readable, TransformCallback } from 'stream' -import Stream, { Transform } from 'stream' -import { getAsText } from './utils' +} from "@clickhouse/client-common"; +import { Buffer } from "buffer"; +import type { Readable, TransformCallback } from "stream"; +import Stream, { Transform } from "stream"; +import { getAsText } from "./utils"; -const NEWLINE = 0x0a as const +const NEWLINE = 0x0a as const; /** {@link Stream.Readable} with additional types for the `on(data)` method and the async iterator. * Everything else is an exact copy from stream.d.ts */ -export type StreamReadable = Omit & { - [Symbol.asyncIterator](): NodeJS.AsyncIterator - on(event: 'data', listener: (chunk: T) => void): Stream.Readable +export type StreamReadable = Omit & { + [Symbol.asyncIterator](): NodeJS.AsyncIterator; + on(event: "data", listener: (chunk: T) => void): Stream.Readable; on( event: - | 'close' - | 'drain' - | 'end' - | 'finish' - | 'pause' - | 'readable' - | 'resume' - | 'unpipe', + | "close" + | "drain" + | "end" + | "finish" + | "pause" + | "readable" + | "resume" + | "unpipe", listener: () => void, - ): Stream.Readable - on(event: 'error', listener: (err: Error) => void): Stream.Readable - on(event: 'pipe', listener: (src: Readable) => void): Stream.Readable + ): Stream.Readable; + on(event: "error", listener: (err: Error) => void): Stream.Readable; + on(event: "pipe", listener: (src: Readable) => void): Stream.Readable; on( event: string | symbol, listener: (...args: any[]) => void, - ): Stream.Readable -} + ): Stream.Readable; +}; export interface ResultSetOptions { - stream: Stream.Readable - format: Format - query_id: string - log_error: (error: Error) => void - response_headers: ResponseHeaders - jsonHandling?: JSONHandling + stream: Stream.Readable; + format: Format; + query_id: string; + log_error: (error: Error) => void; + response_headers: ResponseHeaders; + jsonHandling?: JSONHandling; } export class ResultSet< Format extends DataFormat | unknown, > implements BaseResultSet { - public readonly response_headers: ResponseHeaders = {} + public readonly response_headers: ResponseHeaders = {}; - private readonly exceptionTag: string | undefined = undefined - private readonly log_error: (error: Error) => void - private readonly jsonHandling: JSONHandling - private _consumed = false + private readonly exceptionTag: string | undefined = undefined; + private readonly log_error: (error: Error) => void; + private readonly jsonHandling: JSONHandling; + private _consumed = false; /** * 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 + private _stream: Stream.Readable; + private readonly format: Format; + public readonly query_id: string; constructor( _stream: Stream.Readable, @@ -86,91 +86,91 @@ export class ResultSet< _response_headers?: ResponseHeaders, jsonHandling?: JSONHandling, ) { - this._stream = _stream - this.format = format - this.query_id = query_id + this._stream = _stream; + this.format = format; + this.query_id = query_id; this.jsonHandling = { ...defaultJSONHandling, ...jsonHandling, - } + }; // eslint-disable-next-line no-console - this.log_error = log_error ?? ((err: Error) => console.error(err)) + this.log_error = log_error ?? ((err: Error) => console.error(err)); if (_response_headers !== undefined) { - this.response_headers = Object.freeze(_response_headers) + this.response_headers = Object.freeze(_response_headers); this.exceptionTag = _response_headers[EXCEPTION_TAG_HEADER_NAME] as | string - | undefined + | undefined; } } private consume() { if (this._consumed) { - throw new Error(streamAlreadyConsumedMessage) + throw new Error(streamAlreadyConsumedMessage); } - this._consumed = true - return this._stream + this._consumed = true; + return this._stream; } /** See {@link BaseResultSet.text}. */ async text(): Promise { - return await getAsText(this.consume()) + return await getAsText(this.consume()); } /** See {@link BaseResultSet.json}. */ async json(): Promise> { // JSONEachRow, etc. if (isStreamableJSONFamily(this.format as DataFormat)) { - const result: T[] = [] + 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() + const stream = this.stream(); for await (const rows of stream) { for (const row of rows) { - result.push(row.json() as T) + result.push(row.json() as T); } } - return result as ResultJSONType + return result as ResultJSONType; } // JSON, JSONObjectEachRow, etc. if (isNotStreamableJSONFamily(this.format as DataFormat)) { - const text = await getAsText(this.consume()) - return this.jsonHandling.parse(text) + const text = await getAsText(this.consume()); + return this.jsonHandling.parse(text); } // should not be called for CSV, etc. - throw new Error(`Cannot decode ${this.format} as JSON`) + throw new Error(`Cannot decode ${this.format} as JSON`); } /** See {@link BaseResultSet.stream}. */ stream(): ResultStream[]>> { - validateStreamFormat(this.format) + validateStreamFormat(this.format); - const incompleteChunks: Buffer[] = [] - const logError = this.log_error - const exceptionTag = this.exceptionTag - const jsonHandling = this.jsonHandling + const incompleteChunks: Buffer[] = []; + const logError = this.log_error; + const exceptionTag = this.exceptionTag; + const jsonHandling = this.jsonHandling; const toRows = new Transform({ transform( chunk: Buffer, _encoding: BufferEncoding, callback: TransformCallback, ) { - const rows: Row[] = [] + const rows: Row[] = []; - let lastIdx = 0 - let currentChunkPart: Buffer + let lastIdx = 0; + let currentChunkPart: Buffer; while (true) { // an unescaped newline character denotes the end of a row, // or at least the beginning of the exception marker - const idx = chunk.indexOf(NEWLINE, lastIdx) + const idx = chunk.indexOf(NEWLINE, lastIdx); if (idx === -1) { - incompleteChunks.push(chunk.subarray(lastIdx)) + incompleteChunks.push(chunk.subarray(lastIdx)); if (rows.length > 0) { - this.push(rows) + this.push(rows); } - break + break; } else { // Check for exception in the chunk (only after 25.11) if ( @@ -178,34 +178,34 @@ export class ResultSet< idx >= 1 && chunk[idx - 1] === CARET_RETURN ) { - return callback(extractErrorAtTheEndOfChunk(chunk, exceptionTag)) + return callback(extractErrorAtTheEndOfChunk(chunk, exceptionTag)); } if (incompleteChunks.length > 0) { - incompleteChunks.push(chunk.subarray(lastIdx, idx)) - currentChunkPart = Buffer.concat(incompleteChunks) + incompleteChunks.push(chunk.subarray(lastIdx, idx)); + currentChunkPart = Buffer.concat(incompleteChunks); // Removing used buffers and reusing the already allocated memory // by setting length to 0 - incompleteChunks.length = 0 + incompleteChunks.length = 0; } else { - currentChunkPart = chunk.subarray(lastIdx, idx) + currentChunkPart = chunk.subarray(lastIdx, idx); } - const text = currentChunkPart.toString() + const text = currentChunkPart.toString(); rows.push({ text, json(): T { - return jsonHandling.parse(text) + return jsonHandling.parse(text); }, - }) - lastIdx = idx + 1 // skipping newline character + }); + lastIdx = idx + 1; // skipping newline character } } - callback() + callback(); }, autoDestroy: true, objectMode: true, - }) + }); const pipeline = Stream.pipeline( this.consume(), @@ -213,19 +213,19 @@ export class ResultSet< function pipelineCb(err) { if ( err && - err.name !== 'AbortError' && + err.name !== "AbortError" && err.message !== resultSetClosedMessage ) { - logError(err) + logError(err); } }, - ) - return pipeline as any + ); + return pipeline as any; } /** See {@link BaseResultSet.close}. */ close() { - this._stream.destroy(new Error(resultSetClosedMessage)) + this._stream.destroy(new Error(resultSetClosedMessage)); } /** @@ -236,7 +236,7 @@ export class ResultSet< * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/using */ [Symbol.dispose]() { - this.close() + this.close(); } static instance({ @@ -254,9 +254,9 @@ export class ResultSet< log_error, response_headers, jsonHandling, - ) + ); } } -const streamAlreadyConsumedMessage = 'Stream has been already consumed' -const resultSetClosedMessage = 'ResultSet has been closed' +const streamAlreadyConsumedMessage = "Stream has been already consumed"; +const resultSetClosedMessage = "ResultSet has been closed"; diff --git a/packages/client-node/src/utils/encoder.ts b/packages/client-node/src/utils/encoder.ts index 43611ee6..0bda89e5 100644 --- a/packages/client-node/src/utils/encoder.ts +++ b/packages/client-node/src/utils/encoder.ts @@ -3,16 +3,16 @@ import type { InsertValues, JSONHandling, ValuesEncoder, -} from '@clickhouse/client-common' -import { encodeJSON, isSupportedRawFormat } from '@clickhouse/client-common' -import Stream from 'stream' -import { isStream, mapStream } from './stream' +} from "@clickhouse/client-common"; +import { encodeJSON, isSupportedRawFormat } from "@clickhouse/client-common"; +import Stream from "stream"; +import { isStream, mapStream } from "./stream"; export class NodeValuesEncoder implements ValuesEncoder { - private readonly json: JSONHandling + private readonly json: JSONHandling; constructor(customJSONConfig: JSONHandling) { - this.json = customJSONConfig + this.json = customJSONConfig; } encodeValues( @@ -22,28 +22,28 @@ export class NodeValuesEncoder implements ValuesEncoder { if (isStream(values)) { // TSV/CSV/CustomSeparated formats don't require additional serialization if (!values.readableObjectMode) { - return values + return values; } // JSON* formats streams return Stream.pipeline( values, mapStream((value) => encodeJSON(value, format, this.json.stringify)), pipelineCb, - ) + ); } // JSON* arrays if (Array.isArray(values)) { return values .map((value) => encodeJSON(value, format, this.json.stringify)) - .join('') + .join(""); } // JSON & JSONObjectEachRow format input - if (typeof values === 'object') { - return encodeJSON(values, format, this.json.stringify) + if (typeof values === "object") { + return encodeJSON(values, format, this.json.stringify); } throw new Error( `Cannot encode values of type ${typeof values} with ${format} format`, - ) + ); } validateInsertValues( @@ -53,12 +53,12 @@ export class NodeValuesEncoder implements ValuesEncoder { if ( !Array.isArray(values) && !isStream(values) && - typeof values !== 'object' + typeof values !== "object" ) { throw new Error( 'Insert expected "values" to be an array, a stream of values or a JSON object, ' + `got: ${typeof values}`, - ) + ); } if (isStream(values)) { @@ -66,12 +66,12 @@ export class NodeValuesEncoder implements ValuesEncoder { if (values.readableObjectMode) { throw new Error( `Insert for ${format} expected Readable Stream with disabled object mode.`, - ) + ); } } else if (!values.readableObjectMode) { throw new Error( `Insert for ${format} expected Readable Stream with enabled object mode.`, - ) + ); } } } @@ -81,6 +81,6 @@ function pipelineCb(err: NodeJS.ErrnoException | null) { if (err) { // FIXME: use logger instead // eslint-disable-next-line no-console - console.error(err) + console.error(err); } } diff --git a/packages/client-node/src/utils/index.ts b/packages/client-node/src/utils/index.ts index d9fa4870..e231cc4b 100644 --- a/packages/client-node/src/utils/index.ts +++ b/packages/client-node/src/utils/index.ts @@ -1,4 +1,4 @@ -export * from './stream' -export * from './encoder' -export * from './process' -export * from './user_agent' +export * from "./stream"; +export * from "./encoder"; +export * from "./process"; +export * from "./user_agent"; diff --git a/packages/client-node/src/utils/process.ts b/packages/client-node/src/utils/process.ts index e7000494..d2cc406b 100644 --- a/packages/client-node/src/utils/process.ts +++ b/packages/client-node/src/utils/process.ts @@ -1,4 +1,4 @@ // for easy mocking in the tests export function getProcessVersion(): string { - return process.version + return process.version; } diff --git a/packages/client-node/src/utils/runtime.ts b/packages/client-node/src/utils/runtime.ts index ebf3a710..33af1f21 100644 --- a/packages/client-node/src/utils/runtime.ts +++ b/packages/client-node/src/utils/runtime.ts @@ -1,10 +1,10 @@ -import * as os from 'node:os' -import packageVersion from '../version' +import * as os from "node:os"; +import packageVersion from "../version"; /** Indirect export of package version and node version for easier mocking since Node.js 22.18 */ // eslint-disable-next-line @typescript-eslint/no-extraneous-class export class Runtime { - static package = packageVersion - static node = process.version - static os = os.platform() + static package = packageVersion; + static node = process.version; + static os = os.platform(); } diff --git a/packages/client-node/src/utils/stream.ts b/packages/client-node/src/utils/stream.ts index 621f9f64..f3f5db5d 100644 --- a/packages/client-node/src/utils/stream.ts +++ b/packages/client-node/src/utils/stream.ts @@ -1,47 +1,47 @@ -import Stream from 'stream' -import { constants } from 'buffer' +import Stream from "stream"; +import { constants } from "buffer"; -const { MAX_STRING_LENGTH } = constants +const { MAX_STRING_LENGTH } = constants; export function isStream(obj: unknown): obj is Stream.Readable { return ( - typeof obj === 'object' && + typeof obj === "object" && obj !== null && - 'pipe' in obj && - typeof obj.pipe === 'function' && - 'on' in obj && - typeof obj.on === 'function' - ) + "pipe" in obj && + typeof obj.pipe === "function" && + "on" in obj && + typeof obj.on === "function" + ); } export async function getAsText(stream: Stream.Readable): Promise { try { - let text = '' + let text = ""; - const textDecoder = new TextDecoder() + const textDecoder = new TextDecoder(); for await (const chunk of stream) { - text += textDecoder.decode(chunk, { stream: true }) + text += textDecoder.decode(chunk, { stream: true }); } // flush unfinished multi-byte characters - text += textDecoder.decode() + text += textDecoder.decode(); - return text + return text; } catch (err) { // V8 (Node.js) throws a RangeError with "Invalid string length" once a // string grows past MAX_STRING_LENGTH; JavaScriptCore (Bun) throws a // RangeError with "Out of memory" in the same situation. if ( err instanceof RangeError && - (err.message.includes('Invalid string length') || - err.message.includes('Out of memory')) + (err.message.includes("Invalid string length") || + err.message.includes("Out of memory")) ) { throw new Error( `The response length exceeds the maximum allowed size of a string: ${MAX_STRING_LENGTH} characters.`, { cause: err }, - ) + ); } - throw err + throw err; } } @@ -51,7 +51,7 @@ export function mapStream( return new Stream.Transform({ objectMode: true, transform(chunk, encoding, callback) { - callback(null, mapper(chunk)) + callback(null, mapper(chunk)); }, - }) + }); } diff --git a/packages/client-node/src/utils/user_agent.ts b/packages/client-node/src/utils/user_agent.ts index a7f63c16..e4425782 100644 --- a/packages/client-node/src/utils/user_agent.ts +++ b/packages/client-node/src/utils/user_agent.ts @@ -1,4 +1,4 @@ -import { Runtime } from './runtime' +import { Runtime } from "./runtime"; /** * Generate a user agent string like @@ -13,8 +13,8 @@ import { Runtime } from './runtime' export function getUserAgent(application_id?: string): string { const defaultUserAgent = `clickhouse-js/${Runtime.package} (lv:nodejs/${ Runtime.node - }; os:${Runtime.os})` + }; os:${Runtime.os})`; return application_id ? `${application_id} ${defaultUserAgent}` - : defaultUserAgent + : defaultUserAgent; } diff --git a/packages/client-node/src/version.ts b/packages/client-node/src/version.ts index fab0915d..a38a7bdc 100644 --- a/packages/client-node/src/version.ts +++ b/packages/client-node/src/version.ts @@ -1 +1 @@ -export default '1.21.0' +export default "1.21.0"; diff --git a/packages/client-web/__tests__/integration/web_abort_request.test.ts b/packages/client-web/__tests__/integration/web_abort_request.test.ts index 02f836b8..7283808f 100644 --- a/packages/client-web/__tests__/integration/web_abort_request.test.ts +++ b/packages/client-web/__tests__/integration/web_abort_request.test.ts @@ -1,104 +1,104 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import type { Row } from '@clickhouse/client-common' -import { createTestClient } from '@test/utils' -import type { WebClickHouseClient } from '../../src/client' +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import type { Row } from "@clickhouse/client-common"; +import { createTestClient } from "@test/utils"; +import type { WebClickHouseClient } from "../../src/client"; -describe('[Web] abort request', () => { - let client: WebClickHouseClient +describe("[Web] abort request", () => { + let client: WebClickHouseClient; beforeEach(() => { - client = createTestClient() as unknown as WebClickHouseClient - }) + client = createTestClient() as unknown as WebClickHouseClient; + }); afterEach(async () => { - await client.close() - }) + await client.close(); + }); // a slightly different assertion vs the same Node.js test - it('cancels a select query before it is sent', async () => { - const controller = new AbortController() + it("cancels a select query before it is sent", async () => { + const controller = new AbortController(); const selectPromise = client.query({ - query: 'SELECT sleep(3)', - format: 'CSV', + query: "SELECT sleep(3)", + format: "CSV", abort_signal: controller.signal, - }) - controller.abort() + }); + controller.abort(); await expect(selectPromise).rejects.toMatchObject( expect.objectContaining({ - message: expect.stringMatching('The user aborted a request'), + message: expect.stringMatching("The user aborted a request"), }), - ) - }) + ); + }); - it('cancels a select query while reading response', async () => { - const controller = new AbortController() + it("cancels a select query while reading response", async () => { + const controller = new AbortController(); const rs = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 100000', - format: 'JSONCompactEachRow', + query: "SELECT number FROM system.numbers LIMIT 100000", + format: "JSONCompactEachRow", abort_signal: controller.signal, clickhouse_settings: { // low block size to force streaming 1 row at a time - max_block_size: '1', + max_block_size: "1", }, - }) + }); - let caughtError: Error | null = null - let rowCount = 0 + let caughtError: Error | null = null; + let rowCount = 0; - const reader = rs.stream().getReader() + const reader = rs.stream().getReader(); try { while (true) { - const { done, value: rows } = await reader.read() - if (done) break - ;(rows as Row[]).forEach(() => { + const { done, value: rows } = await reader.read(); + if (done) break; + (rows as Row[]).forEach(() => { if (rowCount >= 1) { - controller.abort() + controller.abort(); } - rowCount++ - }) + rowCount++; + }); } } catch (e) { - caughtError = e as Error + caughtError = e as Error; } - expect(caughtError).not.toBeNull() + expect(caughtError).not.toBeNull(); // after fetching ${rowCount} rows expect(caughtError?.message, `after fetching ${rowCount} rows`).toContain( - 'aborted', - ) - }) + "aborted", + ); + }); - it('cancels a select query while reading response by closing response stream', async () => { - let rowCount = 0 + it("cancels a select query while reading response by closing response stream", async () => { + let rowCount = 0; const selectPromise = client .query({ - query: 'SELECT number FROM system.numbers LIMIT 3', - format: 'JSONCompactEachRow', + query: "SELECT number FROM system.numbers LIMIT 3", + format: "JSONCompactEachRow", clickhouse_settings: { // low block size to force streaming 1 row at a time - max_block_size: '1', + max_block_size: "1", }, }) .then(async function (rs) { - const reader = rs.stream().getReader() + const reader = rs.stream().getReader(); while (true) { - const { done, value: rows } = await reader.read() - if (done) break + const { done, value: rows } = await reader.read(); + if (done) break; for (const row of rows as Row[]) { - row.json() + row.json(); if (rowCount >= 1) { - await rs.stream().cancel() + await rs.stream().cancel(); } - rowCount++ + rowCount++; } } - }) + }); await expect(selectPromise).rejects.toMatchObject( expect.objectContaining({ - message: expect.stringContaining('Stream has been already consumed'), + message: expect.stringContaining("Stream has been already consumed"), }), - ) - }) -}) + ); + }); +}); diff --git a/packages/client-web/__tests__/integration/web_client.test.ts b/packages/client-web/__tests__/integration/web_client.test.ts index 8586fdc6..89924616 100644 --- a/packages/client-web/__tests__/integration/web_client.test.ts +++ b/packages/client-web/__tests__/integration/web_client.test.ts @@ -1,118 +1,118 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest' -import { getHeadersTestParams } from '@test/utils/parametrized' -import { createClient } from '../../src' +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { getHeadersTestParams } from "@test/utils/parametrized"; +import { createClient } from "../../src"; -describe('[Web] Client', () => { - let fetchSpy: ReturnType +describe("[Web] Client", () => { + let fetchSpy: ReturnType; beforeEach(() => { fetchSpy = vi - .spyOn(globalThis, 'fetch') - .mockReturnValue(Promise.resolve(new Response())) - vi.clearAllMocks() - }) + .spyOn(globalThis, "fetch") + .mockReturnValue(Promise.resolve(new Response())); + vi.clearAllMocks(); + }); - describe('HTTP headers', () => { - it('should be possible to set', async () => { + describe("HTTP headers", () => { + it("should be possible to set", async () => { const client = createClient({ http_headers: { - 'Test-Header': 'foobar', - Authorization: 'should-be-overridden-by-client-anyway', + "Test-Header": "foobar", + Authorization: "should-be-overridden-by-client-anyway", }, - }) - await client.ping() - const fetchParams = getFetchRequestInit() + }); + await client.ping(); + const fetchParams = getFetchRequestInit(); expect(fetchParams!.headers).toEqual({ ...defaultHeaders, - 'Test-Header': 'foobar', - }) - }) + "Test-Header": "foobar", + }); + }); - it('should work with no additional HTTP headers provided', async () => { - const client = createClient({}) - await client.ping() - const fetchParams = getFetchRequestInit() - expect(fetchParams!.headers).toEqual(defaultHeaders) - }) + it("should work with no additional HTTP headers provided", async () => { + const client = createClient({}); + await client.ping(); + const fetchParams = getFetchRequestInit(); + expect(fetchParams!.headers).toEqual(defaultHeaders); + }); - it('should work with additional HTTP headers on the method level', async () => { + it("should work with additional HTTP headers on the method level", async () => { const client = createClient({ http_headers: { - FromInstance: 'foo', - Authorization: 'should-be-overridden-by-client-anyway', + FromInstance: "foo", + Authorization: "should-be-overridden-by-client-anyway", }, - }) + }); - let fetchCalls = 1 - const testParams = getHeadersTestParams(client) + let fetchCalls = 1; + const testParams = getHeadersTestParams(client); for (const param of testParams) { - await param.methodCall({ FromMethod: 'bar' }) + await param.methodCall({ FromMethod: "bar" }); // ${param.methodName}: merges custom HTTP headers from both method and instance expect( getFetchRequestInit(fetchCalls++).headers, `${param.methodName}: merges custom HTTP headers from both method and instance`, ).toEqual({ ...defaultHeaders, - FromInstance: 'foo', - FromMethod: 'bar', - }) + FromInstance: "foo", + FromMethod: "bar", + }); - await param.methodCall({ FromInstance: 'bar' }) + await param.methodCall({ FromInstance: "bar" }); expect( getFetchRequestInit(fetchCalls++).headers, `${param.methodName}: overrides HTTP headers from the instance with the values from the method call`, ).toEqual({ ...defaultHeaders, - FromInstance: 'bar', - }) + FromInstance: "bar", + }); } - }) - }) + }); + }); - describe('KeepAlive setting', () => { - it('should be enabled by default', async () => { - const client = createClient() - await client.ping() - const fetchParams = getFetchRequestInit() - expect(fetchParams.keepalive).toBeTruthy() - }) + describe("KeepAlive setting", () => { + it("should be enabled by default", async () => { + const client = createClient(); + await client.ping(); + const fetchParams = getFetchRequestInit(); + expect(fetchParams.keepalive).toBeTruthy(); + }); - it('should be possible to disable it', async () => { - const client = createClient({ keep_alive: { enabled: false } }) - await client.ping() - const fetchParams = getFetchRequestInit() - expect(fetchParams!.keepalive).toBeFalsy() - }) + it("should be possible to disable it", async () => { + const client = createClient({ keep_alive: { enabled: false } }); + await client.ping(); + const fetchParams = getFetchRequestInit(); + expect(fetchParams!.keepalive).toBeFalsy(); + }); - it('should be enabled with an explicit setting', async () => { - const client = createClient({ keep_alive: { enabled: true } }) - await client.ping() - const fetchParams = getFetchRequestInit() - expect(fetchParams.keepalive).toBeTruthy() - }) - }) + it("should be enabled with an explicit setting", async () => { + const client = createClient({ keep_alive: { enabled: true } }); + await client.ping(); + const fetchParams = getFetchRequestInit(); + expect(fetchParams.keepalive).toBeTruthy(); + }); + }); - describe('Custom fetch', () => { - it('should use a custom fetch instance', async () => { - let customFetchWasCalled = false + describe("Custom fetch", () => { + it("should use a custom fetch instance", async () => { + let customFetchWasCalled = false; const customFetch: typeof fetch = (input, init) => { - customFetchWasCalled = true - return globalThis.fetch(input, init) - } + customFetchWasCalled = true; + return globalThis.fetch(input, init); + }; const client = createClient({ fetch: customFetch, - }) - await client.ping() - expect(customFetchWasCalled).toBeTruthy() - }) - }) + }); + await client.ping(); + expect(customFetchWasCalled).toBeTruthy(); + }); + }); function getFetchRequestInit(fetchSpyCalledTimes = 1) { - expect(fetchSpy).toHaveBeenCalledTimes(fetchSpyCalledTimes) - const [, requestInit] = fetchSpy.mock.calls[fetchSpyCalledTimes - 1] - return requestInit! + expect(fetchSpy).toHaveBeenCalledTimes(fetchSpyCalledTimes); + const [, requestInit] = fetchSpy.mock.calls[fetchSpyCalledTimes - 1]; + return requestInit!; } const defaultHeaders = { - Authorization: 'Basic ZGVmYXVsdDo=', // default user with empty password - } -}) + Authorization: "Basic ZGVmYXVsdDo=", // default user with empty password + }; +}); diff --git a/packages/client-web/__tests__/integration/web_error_parsing.test.ts b/packages/client-web/__tests__/integration/web_error_parsing.test.ts index 86ee9af7..e2d2c658 100644 --- a/packages/client-web/__tests__/integration/web_error_parsing.test.ts +++ b/packages/client-web/__tests__/integration/web_error_parsing.test.ts @@ -1,20 +1,20 @@ -import { describe, it, expect } from 'vitest' -import { createClient } from '../../src' +import { describe, it, expect } from "vitest"; +import { createClient } from "../../src"; -describe('[Web] errors parsing', () => { - it('should return an error when URL is unreachable', async () => { +describe("[Web] errors parsing", () => { + it("should return an error when URL is unreachable", async () => { const client = createClient({ - url: 'http://localhost:1111', - }) + url: "http://localhost:1111", + }); await expect( client.query({ - query: 'SELECT * FROM system.numbers LIMIT 3', + query: "SELECT * FROM system.numbers LIMIT 3", }), ).rejects.toMatchObject( // Chrome = Failed to fetch; FF = NetworkError when attempting to fetch resource expect.objectContaining({ - message: expect.stringContaining('to fetch'), + message: expect.stringContaining("to fetch"), }), - ) - }) -}) + ); + }); +}); diff --git a/packages/client-web/__tests__/integration/web_exec.test.ts b/packages/client-web/__tests__/integration/web_exec.test.ts index efed2fe4..7b72bd47 100644 --- a/packages/client-web/__tests__/integration/web_exec.test.ts +++ b/packages/client-web/__tests__/integration/web_exec.test.ts @@ -1,61 +1,61 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import type { ClickHouseClient } from '@clickhouse/client-common' -import { createTestClient } from '@test/utils' -import { getAsText } from '../../src/utils' -import { ResultSet } from '../../src' +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import type { ClickHouseClient } from "@clickhouse/client-common"; +import { createTestClient } from "@test/utils"; +import { getAsText } from "../../src/utils"; +import { ResultSet } from "../../src"; -describe('[Web] exec result streaming', () => { - let client: ClickHouseClient +describe("[Web] exec result streaming", () => { + let client: ClickHouseClient; beforeEach(() => { - client = createTestClient() - }) + client = createTestClient(); + }); afterEach(async () => { - await client.close() - }) + await client.close(); + }); - it('should send a parametrized query', async () => { + it("should send a parametrized query", async () => { const result = await client.exec({ - query: 'SELECT plus({val1: Int32}, {val2: Int32})', + query: "SELECT plus({val1: Int32}, {val2: Int32})", query_params: { val1: 10, val2: 20, }, - }) - expect(await getAsText(result.stream)).toEqual('30\n') - }) + }); + expect(await getAsText(result.stream)).toEqual("30\n"); + }); - describe('trailing semi', () => { - it('should allow commands with semi in select clause', async () => { + describe("trailing semi", () => { + it("should allow commands with semi in select clause", async () => { const result = await client.exec({ query: `SELECT ';' FORMAT CSV`, - }) - expect(await getAsText(result.stream)).toEqual('";"\n') - }) + }); + expect(await getAsText(result.stream)).toEqual('";"\n'); + }); - it('should allow commands with trailing semi', async () => { + it("should allow commands with trailing semi", async () => { const result = await client.exec({ - query: 'EXISTS system.databases;', - }) - expect(await getAsText(result.stream)).toEqual('1\n') - }) + query: "EXISTS system.databases;", + }); + expect(await getAsText(result.stream)).toEqual("1\n"); + }); - it('should allow commands with multiple trailing semi', async () => { + it("should allow commands with multiple trailing semi", async () => { const result = await client.exec({ - query: 'EXISTS system.foobar;;;;;;', - }) - expect(await getAsText(result.stream)).toEqual('0\n') - }) + query: "EXISTS system.foobar;;;;;;", + }); + expect(await getAsText(result.stream)).toEqual("0\n"); + }); - it('should work with default_format', async () => { - const format = 'JSONEachRow' + it("should work with default_format", async () => { + const format = "JSONEachRow"; const { stream, query_id } = await client.exec({ - query: 'SELECT number FROM system.numbers LIMIT 1', + query: "SELECT number FROM system.numbers LIMIT 1", clickhouse_settings: { default_format: format, }, - }) - const rs = new ResultSet(stream, format, query_id) - expect(await rs.json()).toEqual([{ number: '0' }]) - }) - }) -}) + }); + const rs = new ResultSet(stream, format, query_id); + expect(await rs.json()).toEqual([{ number: "0" }]); + }); + }); +}); diff --git a/packages/client-web/__tests__/integration/web_ping.test.ts b/packages/client-web/__tests__/integration/web_ping.test.ts index 0bf66058..ff3c7cee 100644 --- a/packages/client-web/__tests__/integration/web_ping.test.ts +++ b/packages/client-web/__tests__/integration/web_ping.test.ts @@ -1,47 +1,47 @@ -import { describe, it, expect, afterEach } from 'vitest' +import { describe, it, expect, afterEach } from "vitest"; import type { ClickHouseClient, ClickHouseError, -} from '@clickhouse/client-common' -import { createTestClient } from '@test/utils' +} from "@clickhouse/client-common"; +import { createTestClient } from "@test/utils"; -describe('[Web] ping', () => { - let client: ClickHouseClient +describe("[Web] ping", () => { + let client: ClickHouseClient; afterEach(async () => { - await client.close() - }) + await client.close(); + }); - it('does not swallow a client error', async () => { + it("does not swallow a client error", async () => { client = createTestClient({ - url: 'http://localhost:3333', - }) + url: "http://localhost:3333", + }); - const result = await client.ping() - expect(result.success).toBeFalsy() + const result = await client.ping(); + expect(result.success).toBeFalsy(); // @ts-expect-error expect(result.error).toEqual( // Chrome = Failed to fetch; FF = NetworkError when attempting to fetch resource expect.objectContaining({ - message: expect.stringContaining('to fetch'), + message: expect.stringContaining("to fetch"), }), - ) - }) + ); + }); - it('checks credentials by default', async () => { + it("checks credentials by default", async () => { client = createTestClient({ - username: 'wrong', - }) + username: "wrong", + }); const response = await client.ping({ select: false, // ignored - }) - expect(response.success).toBe(false) + }); + expect(response.success).toBe(false); - const err = (response as unknown as { error: ClickHouseError }).error - expect(err.code).toEqual('516') - expect(err.type).toEqual('AUTHENTICATION_FAILED') + const err = (response as unknown as { error: ClickHouseError }).error; + expect(err.code).toEqual("516"); + expect(err.type).toEqual("AUTHENTICATION_FAILED"); expect(err.message).toEqual( - expect.stringContaining('Authentication failed'), - ) - }) -}) + expect.stringContaining("Authentication failed"), + ); + }); +}); diff --git a/packages/client-web/__tests__/integration/web_select_streaming.test.ts b/packages/client-web/__tests__/integration/web_select_streaming.test.ts index cf63ba4c..d22f1244 100644 --- a/packages/client-web/__tests__/integration/web_select_streaming.test.ts +++ b/packages/client-web/__tests__/integration/web_select_streaming.test.ts @@ -1,14 +1,14 @@ -import { describe, it, expect, afterEach, beforeEach } from 'vitest' -import type { ClickHouseClient, Row } from '@clickhouse/client-common' -import { isProgressRow } from '@clickhouse/client-common' -import { createTestClient } from '@test/utils' -import { genLargeStringsDataset } from '@test/utils/datasets' - -describe('[Web] SELECT streaming', () => { - let client: ClickHouseClient> +import { describe, it, expect, afterEach, beforeEach } from "vitest"; +import type { ClickHouseClient, Row } from "@clickhouse/client-common"; +import { isProgressRow } from "@clickhouse/client-common"; +import { createTestClient } from "@test/utils"; +import { genLargeStringsDataset } from "@test/utils/datasets"; + +describe("[Web] SELECT streaming", () => { + let client: ClickHouseClient>; afterEach(async () => { - await client.close() - }) + await client.close(); + }); beforeEach(async () => { client = createTestClient({ // It is required to disable keep-alive to allow for larger inserts @@ -17,311 +17,311 @@ describe('[Web] SELECT streaming', () => { // <...> // If the sum of contentLength and inflightKeepaliveBytes is greater than 64 kibibytes, then return a network error. keep_alive: { enabled: false }, - }) - }) + }); + }); - describe('consume the response only once', () => { + describe("consume the response only once", () => { async function assertAlreadyConsumed$(fn: () => Promise) { await expect(fn()).rejects.toMatchObject( expect.objectContaining({ - message: 'Stream has been already consumed', + message: "Stream has been already consumed", }), - ) + ); } function assertAlreadyConsumed(fn: () => T) { expect(fn).toThrow( expect.objectContaining({ - message: 'Stream has been already consumed', + message: "Stream has been already consumed", }), - ) + ); } - it('should consume a JSON response only once', async () => { + it("should consume a JSON response only once", async () => { const rs = await client.query({ - query: 'SELECT * FROM system.numbers LIMIT 1', - format: 'JSONEachRow', - }) - expect(await rs.json()).toEqual([{ number: '0' }]) + query: "SELECT * FROM system.numbers LIMIT 1", + format: "JSONEachRow", + }); + 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 assertAlreadyConsumed(() => rs.stream()) - }) + await assertAlreadyConsumed$(() => rs.json()); + await assertAlreadyConsumed$(() => rs.text()); + await assertAlreadyConsumed(() => rs.stream()); + }); - it('should consume a text response only once', async () => { + it("should consume a text response only once", async () => { const rs = await client.query({ - query: 'SELECT * FROM system.numbers LIMIT 1', - format: 'JSONEachRow', - }) - expect(await rs.text()).toEqual('{"number":"0"}\n') + query: "SELECT * FROM system.numbers LIMIT 1", + format: "JSONEachRow", + }); + 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()) - await assertAlreadyConsumed(() => rs.stream()) - }) + await assertAlreadyConsumed$(() => rs.json()); + await assertAlreadyConsumed$(() => rs.text()); + await assertAlreadyConsumed(() => rs.stream()); + }); - it('should consume a stream response only once', async () => { + it("should consume a stream response only once", async () => { const rs = await client.query({ - query: 'SELECT * FROM system.numbers LIMIT 1', - format: 'JSONEachRow', - }) - const result = await rowsText(rs.stream()) - expect(result).toEqual(['{"number":"0"}']) + query: "SELECT * FROM system.numbers LIMIT 1", + format: "JSONEachRow", + }); + const result = await rowsText(rs.stream()); + 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()) - }) - }) - - describe('select result asStream()', () => { - it('throws an exception if format is not stream-able', async () => { + await assertAlreadyConsumed$(() => rs.json()); + await assertAlreadyConsumed$(() => rs.text()); + assertAlreadyConsumed(() => rs.stream()); + }); + }); + + describe("select result asStream()", () => { + 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', - }) + query: "SELECT number FROM system.numbers LIMIT 5", + format: "JSON", + }); // wrap in a func to avoid changing inner "this" expect(() => result.stream()).toThrow( expect.objectContaining({ - message: expect.stringContaining('JSON format is not streamable'), + message: expect.stringContaining("JSON format is not streamable"), }), - ) - }) - }) + ); + }); + }); - describe('text()', () => { - it('returns stream of rows in CSV format', async () => { + describe("text()", () => { + it("returns stream of rows in CSV format", async () => { const result = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 5', - format: 'CSV', - }) + query: "SELECT number FROM system.numbers LIMIT 5", + format: "CSV", + }); - const rs = await rowsText(result.stream()) - expect(rs).toEqual(['0', '1', '2', '3', '4']) - }) + const rs = await rowsText(result.stream()); + expect(rs).toEqual(["0", "1", "2", "3", "4"]); + }); - it('returns stream of rows in TabSeparated format', async () => { + it("returns stream of rows in TabSeparated format", async () => { const result = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 5', - format: 'TabSeparated', - }) + query: "SELECT number FROM system.numbers LIMIT 5", + format: "TabSeparated", + }); - const rs = await rowsText(result.stream()) - expect(rs).toEqual(['0', '1', '2', '3', '4']) - }) - }) + const rs = await rowsText(result.stream()); + expect(rs).toEqual(["0", "1", "2", "3", "4"]); + }); + }); - describe('json()', () => { - it('returns stream of objects in JSONEachRow format', async () => { + describe("json()", () => { + it("returns stream of objects in JSONEachRow format", async () => { const result = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 5', - format: 'JSONEachRow', - }) + query: "SELECT number FROM system.numbers LIMIT 5", + format: "JSONEachRow", + }); - const rs = await rowsJsonValues<{ number: string }>(result.stream()) + const rs = await rowsJsonValues<{ number: string }>(result.stream()); expect(rs).toEqual([ - { number: '0' }, - { number: '1' }, - { number: '2' }, - { number: '3' }, - { number: '4' }, - ]) - }) - - it.skip('should return objects in JSONEachRowWithProgress format', async () => { - const limit = 2 - const expectedProgressRowsCount = 4 + { number: "0" }, + { number: "1" }, + { number: "2" }, + { number: "3" }, + { number: "4" }, + ]); + }); + + it.skip("should return objects in JSONEachRowWithProgress format", async () => { + const limit = 2; + const expectedProgressRowsCount = 4; const rs = await client.query({ query: `SELECT * FROM system.numbers LIMIT ${limit}`, - format: 'JSONEachRowWithProgress', + format: "JSONEachRowWithProgress", clickhouse_settings: { - max_block_size: '1', // reduce the block size, so the progress is reported more frequently + max_block_size: "1", // reduce the block size, so the progress is reported more frequently }, - }) - const rows = await rs.json<{ number: string }>() - expect(rows.length).toEqual(limit + expectedProgressRowsCount) + }); + const rows = await rs.json<{ number: string }>(); + expect(rows.length).toEqual(limit + expectedProgressRowsCount); expect(rows.filter((r) => !isProgressRow(r)) as unknown[]).toEqual([ - { row: { number: '0' } }, - { row: { number: '1' } }, - ]) - }) + { row: { number: "0" } }, + { row: { number: "1" } }, + ]); + }); - it('returns stream of objects in JSONStringsEachRow format', async () => { + it("returns stream of objects in JSONStringsEachRow format", async () => { const result = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 5', - format: 'JSONStringsEachRow', - }) + query: "SELECT number FROM system.numbers LIMIT 5", + format: "JSONStringsEachRow", + }); - const rs = await rowsJsonValues<{ number: string }>(result.stream()) + const rs = await rowsJsonValues<{ number: string }>(result.stream()); expect(rs).toEqual([ - { number: '0' }, - { number: '1' }, - { number: '2' }, - { number: '3' }, - { number: '4' }, - ]) - }) - - it('returns stream of objects in JSONCompactEachRow format', async () => { + { number: "0" }, + { number: "1" }, + { number: "2" }, + { number: "3" }, + { number: "4" }, + ]); + }); + + it("returns stream of objects in JSONCompactEachRow format", async () => { const result = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 5', - format: 'JSONCompactEachRow', - }) + query: "SELECT number FROM system.numbers LIMIT 5", + format: "JSONCompactEachRow", + }); - const rs = await rowsJsonValues<[string]>(result.stream()) - expect(rs).toEqual([['0'], ['1'], ['2'], ['3'], ['4']]) - }) + const rs = await rowsJsonValues<[string]>(result.stream()); + expect(rs).toEqual([["0"], ["1"], ["2"], ["3"], ["4"]]); + }); - it('returns stream of objects in JSONCompactEachRowWithNames format', async () => { + it("returns stream of objects in JSONCompactEachRowWithNames format", async () => { const result = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 5', - format: 'JSONCompactEachRowWithNames', - }) + query: "SELECT number FROM system.numbers LIMIT 5", + format: "JSONCompactEachRowWithNames", + }); - const rs = await rowsJsonValues<[string]>(result.stream()) - expect(rs).toEqual([['number'], ['0'], ['1'], ['2'], ['3'], ['4']]) - }) + const rs = await rowsJsonValues<[string]>(result.stream()); + expect(rs).toEqual([["number"], ["0"], ["1"], ["2"], ["3"], ["4"]]); + }); - it('returns stream of objects in JSONCompactEachRowWithNamesAndTypes format', async () => { + it("returns stream of objects in JSONCompactEachRowWithNamesAndTypes format", async () => { const result = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 5', - format: 'JSONCompactEachRowWithNamesAndTypes', - }) + query: "SELECT number FROM system.numbers LIMIT 5", + format: "JSONCompactEachRowWithNamesAndTypes", + }); - const rs = await rowsJsonValues<[string]>(result.stream()) + const rs = await rowsJsonValues<[string]>(result.stream()); expect(rs).toEqual([ - ['number'], - ['UInt64'], - ['0'], - ['1'], - ['2'], - ['3'], - ['4'], - ]) - }) - - it('returns stream of objects in JSONCompactStringsEachRowWithNames format', async () => { + ["number"], + ["UInt64"], + ["0"], + ["1"], + ["2"], + ["3"], + ["4"], + ]); + }); + + it("returns stream of objects in JSONCompactStringsEachRowWithNames format", async () => { const result = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 5', - format: 'JSONCompactStringsEachRowWithNames', - }) + query: "SELECT number FROM system.numbers LIMIT 5", + format: "JSONCompactStringsEachRowWithNames", + }); - const rs = await rowsJsonValues<[string]>(result.stream()) - expect(rs).toEqual([['number'], ['0'], ['1'], ['2'], ['3'], ['4']]) - }) + const rs = await rowsJsonValues<[string]>(result.stream()); + expect(rs).toEqual([["number"], ["0"], ["1"], ["2"], ["3"], ["4"]]); + }); - it('returns stream of objects in JSONCompactStringsEachRowWithNamesAndTypes format', async () => { + it("returns stream of objects in JSONCompactStringsEachRowWithNamesAndTypes format", async () => { const result = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 5', - format: 'JSONCompactStringsEachRowWithNamesAndTypes', - }) + query: "SELECT number FROM system.numbers LIMIT 5", + format: "JSONCompactStringsEachRowWithNamesAndTypes", + }); - const rs = await rowsJsonValues<[string]>(result.stream()) + const rs = await rowsJsonValues<[string]>(result.stream()); expect(rs).toEqual([ - ['number'], - ['UInt64'], - ['0'], - ['1'], - ['2'], - ['3'], - ['4'], - ]) - }) - }) + ["number"], + ["UInt64"], + ["0"], + ["1"], + ["2"], + ["3"], + ["4"], + ]); + }); + }); // See https://github.com/ClickHouse/clickhouse-js/issues/171 for more details // Here we generate a large enough dataset to break into multiple chunks while streaming, // effectively testing the implementation of incomplete rows handling - describe('should correctly process multiple chunks', () => { - describe('large amount of rows', () => { - it('should work with .json()', async () => { + describe("should correctly process multiple chunks", () => { + describe("large amount of rows", () => { + it("should work with .json()", async () => { const { table, values } = await genLargeStringsDataset(client, { rows: 10000, words: 10, - }) + }); const result = await client .query({ query: `SELECT * FROM ${table} ORDER BY id ASC`, - format: 'JSONEachRow', + format: "JSONEachRow", }) - .then((r) => r.json()) - expect(result).toEqual(values) - }) + .then((r) => r.json()); + expect(result).toEqual(values); + }); - it('should work with .stream()', async () => { + it("should work with .stream()", async () => { const { table, values } = await genLargeStringsDataset(client, { rows: 10000, words: 10, - }) + }); const stream = await client .query({ query: `SELECT * FROM ${table} ORDER BY id ASC`, - format: 'JSONEachRow', + format: "JSONEachRow", }) - .then((r) => r.stream()) + .then((r) => r.stream()); - const result = await rowsJsonValues(stream) - expect(result).toEqual(values) - }) - }) + const result = await rowsJsonValues(stream); + expect(result).toEqual(values); + }); + }); describe("rows that don't fit into a single chunk", () => { - it('should work with .json()', async () => { + it("should work with .json()", async () => { const { table, values } = await genLargeStringsDataset(client, { rows: 5, words: 10000, - }) + }); const result = await client .query({ query: `SELECT * FROM ${table} ORDER BY id ASC`, - format: 'JSONEachRow', + format: "JSONEachRow", }) - .then((r) => r.json()) - expect(result).toEqual(values) - }) + .then((r) => r.json()); + expect(result).toEqual(values); + }); - it('should work with .stream()', async () => { + it("should work with .stream()", async () => { const { table, values } = await genLargeStringsDataset(client, { rows: 5, words: 10000, - }) + }); const stream = await client .query({ query: `SELECT * FROM ${table} ORDER BY id ASC`, - format: 'JSONEachRow', + format: "JSONEachRow", }) - .then((r) => r.stream()) + .then((r) => r.stream()); - const result = await rowsJsonValues(stream) - expect(result).toEqual(values) - }) - }) - }) -}) + const result = await rowsJsonValues(stream); + expect(result).toEqual(values); + }); + }); + }); +}); async function rowsJsonValues( stream: ReadableStream, ): Promise { - const result: T[] = [] - const reader = stream.getReader() + const result: T[] = []; + const reader = stream.getReader(); while (true) { - const { done, value } = await reader.read() - if (done) break + const { done, value } = await reader.read(); + if (done) break; value.forEach((row) => { - result.push(row.json()) - }) + result.push(row.json()); + }); } - return result + return result; } async function rowsText(stream: ReadableStream): Promise { - const result: string[] = [] - const reader = stream.getReader() + const result: string[] = []; + const reader = stream.getReader(); while (true) { - const { done, value } = await reader.read() - if (done) break + const { done, value } = await reader.read(); + if (done) break; value.forEach((row) => { - result.push(row.text) - }) + result.push(row.text); + }); } - return result + return result; } diff --git a/packages/client-web/__tests__/integration/web_stream_error_handling.test.ts b/packages/client-web/__tests__/integration/web_stream_error_handling.test.ts index 3436b5eb..da648f22 100644 --- a/packages/client-web/__tests__/integration/web_stream_error_handling.test.ts +++ b/packages/client-web/__tests__/integration/web_stream_error_handling.test.ts @@ -1,47 +1,47 @@ -import { describe, it, beforeEach, afterEach } from 'vitest' +import { describe, it, beforeEach, afterEach } from "vitest"; import { assertError, streamErrorQueryParams, -} from '@test/fixtures/stream_errors' -import { isClickHouseVersionAtLeast } from '@test/utils/server_version' -import type { ClickHouseClient } from '../../src' -import type { ClickHouseError } from '../../src' -import { createWebTestClient } from '../utils/web_client' +} from "@test/fixtures/stream_errors"; +import { isClickHouseVersionAtLeast } from "@test/utils/server_version"; +import type { ClickHouseClient } from "../../src"; +import type { ClickHouseError } from "../../src"; +import { createWebTestClient } from "../utils/web_client"; // See https://github.com/ClickHouse/ClickHouse/pull/88818 -describe('[Web] Stream error handling', () => { - let client: ClickHouseClient +describe("[Web] Stream error handling", () => { + let client: ClickHouseClient; beforeEach(async () => { - client = createWebTestClient() - }) + client = createWebTestClient(); + }); afterEach(async () => { - await client.close() - }) + await client.close(); + }); - it('with reader', async ({ skip }) => { + it("with reader", async ({ skip }) => { if (!(await isClickHouseVersionAtLeast(client, 25, 11))) { - skip() + skip(); } - let caughtError: ClickHouseError | null = null + let caughtError: ClickHouseError | null = null; try { - const queryParams = streamErrorQueryParams() - const rs = await client.query(queryParams) + const queryParams = streamErrorQueryParams(); + const rs = await client.query(queryParams); - const reader = rs.stream<{ n: number }>().getReader() + const reader = rs.stream<{ n: number }>().getReader(); while (true) { - const { done, value: rows } = await reader.read() - if (done) break + const { done, value: rows } = await reader.read(); + if (done) break; for (const row of rows) { - row.json() // ignored + row.json(); // ignored } } } catch (err) { - caughtError = err as ClickHouseError + caughtError = err as ClickHouseError; } - assertError(caughtError) - }) -}) + assertError(caughtError); + }); +}); diff --git a/packages/client-web/__tests__/jwt/web_jwt_auth.test.ts b/packages/client-web/__tests__/jwt/web_jwt_auth.test.ts index c01f83bc..29b9b674 100644 --- a/packages/client-web/__tests__/jwt/web_jwt_auth.test.ts +++ b/packages/client-web/__tests__/jwt/web_jwt_auth.test.ts @@ -1,58 +1,58 @@ -import { describe, it, expect, afterEach, beforeAll } from 'vitest' -import { EnvKeys, getFromEnv, maybeGetFromEnv } from '@test/utils/env' -import { createClient } from '../../src' -import type { WebClickHouseClient } from '../../src/client' +import { describe, it, expect, afterEach, beforeAll } from "vitest"; +import { EnvKeys, getFromEnv, maybeGetFromEnv } from "@test/utils/env"; +import { createClient } from "../../src"; +import type { WebClickHouseClient } from "../../src/client"; -const jwt = maybeGetFromEnv(EnvKeys.jwt_access_token) +const jwt = maybeGetFromEnv(EnvKeys.jwt_access_token); /** Cannot use the jsonwebtoken library to generate the token: it is Node.js only. * The access token should be generated externally before running the test, * and set as the CLICKHOUSE_JWT_ACCESS_TOKEN environment variable */ -describe.skipIf(!jwt)('[Web] JWT auth', () => { - let client: WebClickHouseClient | undefined - let url: string +describe.skipIf(!jwt)("[Web] JWT auth", () => { + let client: WebClickHouseClient | undefined; + let url: string; beforeAll(() => { - url = `https://${getFromEnv(EnvKeys.host)}:8443` - }) + url = `https://${getFromEnv(EnvKeys.host)}:8443`; + }); afterEach(async () => { - await client?.close() - }) + await client?.close(); + }); - it('succeeds with without the token', () => { - expect(true).toEqual(true) - }) + it("succeeds with without the token", () => { + expect(true).toEqual(true); + }); - it('should work with client configuration', async () => { + it("should work with client configuration", async () => { client = createClient({ url, access_token: jwt, - }) + }); const rs = await client.query({ - query: 'SELECT 42 AS result', - format: 'JSONEachRow', - }) - expect(await rs.json()).toEqual([{ result: 42 }]) - }) + query: "SELECT 42 AS result", + format: "JSONEachRow", + }); + expect(await rs.json()).toEqual([{ result: 42 }]); + }); - it('should override the client instance auth', async ({ skip }) => { + it("should override the client instance auth", async ({ skip }) => { if (!jwt) { - skip(`Environment variable ${EnvKeys.jwt_access_token} is not set`) - return + skip(`Environment variable ${EnvKeys.jwt_access_token} is not set`); + return; } client = createClient({ url, - username: 'gibberish', - password: 'gibberish', - }) + username: "gibberish", + password: "gibberish", + }); const rs = await client.query({ - query: 'SELECT 42 AS result', - format: 'JSONEachRow', + query: "SELECT 42 AS result", + format: "JSONEachRow", auth: { access_token: jwt, }, - }) - expect(await rs.json()).toEqual([{ result: 42 }]) - }) -}) + }); + expect(await rs.json()).toEqual([{ result: 42 }]); + }); +}); diff --git a/packages/client-web/__tests__/unit/node_getAsText.test.ts b/packages/client-web/__tests__/unit/node_getAsText.test.ts index 5c8b06da..88980d20 100644 --- a/packages/client-web/__tests__/unit/node_getAsText.test.ts +++ b/packages/client-web/__tests__/unit/node_getAsText.test.ts @@ -1,7 +1,7 @@ -import { describe, expect, it } from 'vitest' -import { getAsText } from '../../src/utils/stream' +import { describe, expect, it } from "vitest"; +import { getAsText } from "../../src/utils/stream"; -const MaxStringLength = Math.pow(2, 29) - 24 +const MaxStringLength = Math.pow(2, 29) - 24; // ReadableStream.from() polyfill-ish function generatorToStream( @@ -9,24 +9,24 @@ function generatorToStream( ): ReadableStream { return new ReadableStream({ async pull(controller) { - const { value, done } = await gen.next() + const { value, done } = await gen.next(); if (done) { - controller.close() + controller.close(); } else { - controller.enqueue(value) + controller.enqueue(value); } }, - }) + }); } function makeStreamFromStrings(chunks: string[]): ReadableStream { - const encoder = new TextEncoder() + const encoder = new TextEncoder(); async function* gen() { for (const chunk of chunks) { - yield encoder.encode(chunk) + yield encoder.encode(chunk); } } - return generatorToStream(gen()) + return generatorToStream(gen()); } function makeStreamFromBuffers( @@ -34,22 +34,22 @@ function makeStreamFromBuffers( ): ReadableStream { async function* gen() { for (const chunk of chunks) { - yield chunk + yield chunk; } } - return generatorToStream(gen()) + return generatorToStream(gen()); } -describe('getAsText', () => { - it('should return a string containing the concatenated chunks', async () => { - expect(await getAsText(makeStreamFromStrings(['123', '456']))).toBe( - '123456', - ) - }) +describe("getAsText", () => { + it("should return a string containing the concatenated chunks", async () => { + expect(await getAsText(makeStreamFromStrings(["123", "456"]))).toBe( + "123456", + ); + }); - it('should throw a custom error if the stream is too long for a string', async () => { + it("should throw a custom error if the stream is too long for a string", async () => { // Passing the fill option is fine as Node always fills the buffer with zeroes otherwise - const bigChunk = new Uint8Array((MaxStringLength / 8) >> 0).fill(97) // 'a' + const bigChunk = new Uint8Array((MaxStringLength / 8) >> 0).fill(97); // 'a' await expect( getAsText( makeStreamFromBuffers([ @@ -66,12 +66,12 @@ describe('getAsText', () => { ]), ), ).rejects.toThrowError( - 'The response length exceeds the maximum allowed size of V8 String:', - ) - }) + "The response length exceeds the maximum allowed size of V8 String:", + ); + }); - it('should now throw on big but not too big streams', async () => { - const bigChunk = new Uint8Array((MaxStringLength / 8) >> 0).fill(98) // 'b' + it("should now throw on big but not too big streams", async () => { + const bigChunk = new Uint8Array((MaxStringLength / 8) >> 0).fill(98); // 'b' expect( await getAsText( makeStreamFromBuffers([ @@ -82,24 +82,24 @@ describe('getAsText', () => { bigChunk, ]), ), - ).toHaveLength(335_544_305) - }) + ).toHaveLength(335_544_305); + }); - it('should use streamed decoding and not break utf-8 characters', async () => { + it("should use streamed decoding and not break utf-8 characters", async () => { const stream = makeStreamFromBuffers([ new Uint8Array([0xe2, 0x82]), // first 2 bytes of '€' new Uint8Array([0xac, 0x20, 0x61]), // last byte of '€', space and 'a' - ]) - const text = '€ a' - expect(await getAsText(stream)).toBe(text) - }) + ]); + const text = "€ a"; + expect(await getAsText(stream)).toBe(text); + }); - it('should flush the decoder at the end of the stream', async () => { + it("should flush the decoder at the end of the stream", async () => { const stream = makeStreamFromBuffers([ new Uint8Array([0x61, 0x20, 0xe2, 0x82]), // first 2 bytes of '€' // no more bytes, but the decoder should be flushed and return the bytes it has buffered - ]) - const text = 'a \ufffd' - expect(await getAsText(stream)).toBe(text) - }) -}) + ]); + const text = "a \ufffd"; + expect(await getAsText(stream)).toBe(text); + }); +}); diff --git a/packages/client-web/__tests__/unit/web_client.test.ts b/packages/client-web/__tests__/unit/web_client.test.ts index b6455319..5503d460 100644 --- a/packages/client-web/__tests__/unit/web_client.test.ts +++ b/packages/client-web/__tests__/unit/web_client.test.ts @@ -1,51 +1,51 @@ -import { describe, it, expect, vi } from 'vitest' -import type { BaseClickHouseClientConfigOptions } from '@clickhouse/client-common' -import { createClient } from '../../src' -import { isAwaitUsingStatementSupported } from '../utils/feature_detection' -import { sleep } from '../utils/sleep' -import { createSimpleWebTestClient } from '../utils/simple_web_client' +import { describe, it, expect, vi } from "vitest"; +import type { BaseClickHouseClientConfigOptions } from "@clickhouse/client-common"; +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 () => { +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() - }) + const client = createSimpleWebTestClient(); + expect(client).toBeDefined(); + await client.close(); + }); it('throws on incorrect "url" config value', () => { - expect(() => createClient({ url: 'foo' })).toThrow( + expect(() => createClient({ url: "foo" })).toThrow( expect.objectContaining({ - message: expect.stringContaining('ClickHouse URL is malformed.'), + message: expect.stringContaining("ClickHouse URL is malformed."), }), - ) - }) + ); + }); - it('should not mutate provided configuration', async () => { + it("should not mutate provided configuration", async () => { const config: BaseClickHouseClientConfigOptions = { - url: 'https://localhost:8443', - } - createClient(config) + url: "https://localhost:8443", + }; + createClient(config); // initial configuration is not overridden by the defaults we assign // when we transform the specified config object to the connection params expect(config).toEqual({ - url: 'https://localhost:8443', - }) - }) + url: "https://localhost:8443", + }); + }); - it('closes the client when used with using statement', async ({ skip }) => { + it("closes the client when used with using statement", async ({ skip }) => { if (!isAwaitUsingStatementSupported()) { - skip('using statement is not supported in this environment') - return + skip("using statement is not supported in this environment"); + return; } - const client = createClient() - let isClosed = false - vi.spyOn(client, 'close').mockImplementation(async () => { + const client = createClient(); + let isClosed = false; + vi.spyOn(client, "close").mockImplementation(async () => { // Simulate some delay in closing - await sleep(0) - isClosed = true - }) + await sleep(0); + isClosed = true; + }); // Wrap in eval to allow using statement syntax without // syntax error in older Node.js versions. Might want to @@ -55,8 +55,8 @@ describe('[Web] createClient', () => { await using c = value; // do nothing, just testing the disposal at the end of the block }) - `)(client) + `)(client); - expect(isClosed).toBeTruthy() - }) -}) + expect(isClosed).toBeTruthy(); + }); +}); diff --git a/packages/client-web/__tests__/unit/web_result_set.test.ts b/packages/client-web/__tests__/unit/web_result_set.test.ts index 6fc1a94e..f5d5edcb 100644 --- a/packages/client-web/__tests__/unit/web_result_set.test.ts +++ b/packages/client-web/__tests__/unit/web_result_set.test.ts @@ -1,97 +1,97 @@ -import { describe, it, expect, vi } from 'vitest' -import type { Row } from '@clickhouse/client-common' -import { guid } from '@test/utils' -import { ResultSet } from '../../src' -import { isAwaitUsingStatementSupported } from '../utils/feature_detection' -import { sleep } from '../utils/sleep' - -describe('[Web] ResultSet', () => { - const expectedText = `{"foo":"bar"}\n{"qaz":"qux"}\n` - const expectedJson = [{ foo: 'bar' }, { qaz: 'qux' }] - - const errMsg = 'Stream has been already consumed' +import { describe, it, expect, vi } from "vitest"; +import type { Row } from "@clickhouse/client-common"; +import { guid } from "@test/utils"; +import { ResultSet } from "../../src"; +import { isAwaitUsingStatementSupported } from "../utils/feature_detection"; +import { sleep } from "../utils/sleep"; + +describe("[Web] ResultSet", () => { + const expectedText = `{"foo":"bar"}\n{"qaz":"qux"}\n`; + const expectedJson = [{ foo: "bar" }, { qaz: "qux" }]; + + const errMsg = "Stream has been already consumed"; const err = expect.objectContaining({ message: expect.stringContaining(errMsg), - }) + }); - it('should consume the response as text only once', async () => { - const rs = makeResultSet() + it("should consume the response as text only once", async () => { + const rs = makeResultSet(); - expect(await rs.text()).toEqual(expectedText) - await expect(rs.text()).rejects.toMatchObject(err) - await expect(rs.json()).rejects.toMatchObject(err) - }) + expect(await rs.text()).toEqual(expectedText); + await expect(rs.text()).rejects.toMatchObject(err); + await expect(rs.json()).rejects.toMatchObject(err); + }); - it('should consume the response as JSON only once', async () => { - const rs = makeResultSet() + it("should consume the response as JSON only once", async () => { + const rs = makeResultSet(); - expect(await rs.json()).toEqual(expectedJson) - await expect(rs.json()).rejects.toMatchObject(err) - await expect(rs.text()).rejects.toMatchObject(err) - }) + expect(await rs.json()).toEqual(expectedJson); + await expect(rs.json()).rejects.toMatchObject(err); + await expect(rs.text()).rejects.toMatchObject(err); + }); - it('should consume the response as a stream of Row instances', async () => { - const rs = makeResultSet() - const stream = rs.stream() + it("should consume the response as a stream of Row instances", async () => { + const rs = makeResultSet(); + const stream = rs.stream(); - const result: unknown[] = [] - const reader = stream.getReader() + const result: unknown[] = []; + const reader = stream.getReader(); while (true) { - const { done, value } = await reader.read() - if (done) break + const { done, value } = await reader.read(); + if (done) break; value.forEach((row) => { - result.push(row.json()) - }) + result.push(row.json()); + }); } - expect(result).toEqual(expectedJson) - expect(() => rs.stream()).toThrow(new Error(errMsg)) - await expect(rs.json()).rejects.toMatchObject(err) - await expect(rs.text()).rejects.toMatchObject(err) - }) + expect(result).toEqual(expectedJson); + expect(() => rs.stream()).toThrow(new Error(errMsg)); + await expect(rs.json()).rejects.toMatchObject(err); + await expect(rs.text()).rejects.toMatchObject(err); + }); - it('should be able to call Row.text and Row.json multiple times', async () => { + it("should be able to call Row.text and Row.json multiple times", async () => { const rs = new ResultSet( new ReadableStream({ start(controller) { - controller.enqueue(new TextEncoder().encode('{"foo":"bar"}\n')) - controller.close() + controller.enqueue(new TextEncoder().encode('{"foo":"bar"}\n')); + controller.close(); }, }), - 'JSONEachRow', + "JSONEachRow", guid(), - ) + ); - const allRows: Row[] = [] - const reader = rs.stream().getReader() + const allRows: Row[] = []; + const reader = rs.stream().getReader(); while (true) { - const { done, value } = await reader.read() - if (done) break - allRows.push(...value) + const { done, value } = await reader.read(); + if (done) break; + allRows.push(...value); } - expect(allRows.length).toEqual(1) + expect(allRows.length).toEqual(1); - const [row] = allRows - expect(row.text).toEqual('{"foo":"bar"}') - expect(row.text).toEqual('{"foo":"bar"}') - expect(row.json()).toEqual({ foo: 'bar' }) - expect(row.json()).toEqual({ foo: 'bar' }) - }) + const [row] = allRows; + expect(row.text).toEqual('{"foo":"bar"}'); + expect(row.text).toEqual('{"foo":"bar"}'); + expect(row.json()).toEqual({ foo: "bar" }); + expect(row.json()).toEqual({ foo: "bar" }); + }); - it('closes the ResultSet when used with using statement', async ({ + it("closes the ResultSet when used with using statement", async ({ skip, }) => { if (!isAwaitUsingStatementSupported()) { - skip('using statement is not supported in this environment') - return + skip("using statement is not supported in this environment"); + return; } - const rs = makeResultSet() - let isClosed = false - vi.spyOn(rs, 'close').mockImplementation(async () => { + const rs = makeResultSet(); + let isClosed = false; + vi.spyOn(rs, "close").mockImplementation(async () => { // Simulate some delay in closing - await sleep(0) - isClosed = true - }) + await sleep(0); + isClosed = true; + }); // Wrap in eval to allow using statement syntax without // syntax error in older Node.js versions. Might want to @@ -101,23 +101,23 @@ describe('[Web] ResultSet', () => { await using c = value; // do nothing, just testing the disposal at the end of the block }) - `)(rs) + `)(rs); - expect(isClosed).toBeTruthy() - }) + expect(isClosed).toBeTruthy(); + }); function makeResultSet() { return new ResultSet( new ReadableStream({ start(controller) { - const encoder = new TextEncoder() - controller.enqueue(encoder.encode('{"foo":"bar"}\n')) - controller.enqueue(encoder.encode('{"qaz":"qux"}\n')) - controller.close() + const encoder = new TextEncoder(); + controller.enqueue(encoder.encode('{"foo":"bar"}\n')); + controller.enqueue(encoder.encode('{"qaz":"qux"}\n')); + controller.close(); }, }), - 'JSONEachRow', + "JSONEachRow", guid(), - ) + ); } -}) +}); diff --git a/packages/client-web/__tests__/utils/feature_detection.ts b/packages/client-web/__tests__/utils/feature_detection.ts index 74ccff5e..a52986a8 100644 --- a/packages/client-web/__tests__/utils/feature_detection.ts +++ b/packages/client-web/__tests__/utils/feature_detection.ts @@ -4,10 +4,10 @@ export function isAwaitUsingStatementSupported(): boolean { (async () => { await using c = null; }) - `) - return true + `); + return true; } catch { - return false + return false; } } @@ -17,9 +17,9 @@ export function isUsingStatementSupported(): boolean { (() => { using c = null; }) - `) - return true + `); + return true; } catch { - return false + return false; } } diff --git a/packages/client-web/__tests__/utils/simple_web_client.ts b/packages/client-web/__tests__/utils/simple_web_client.ts index a318da2f..1daeaca7 100644 --- a/packages/client-web/__tests__/utils/simple_web_client.ts +++ b/packages/client-web/__tests__/utils/simple_web_client.ts @@ -1,12 +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' +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 + return createSimpleTestClient(config) as unknown as WebClickHouseClient; } diff --git a/packages/client-web/__tests__/utils/sleep.ts b/packages/client-web/__tests__/utils/sleep.ts index 5ed82bc0..e9d2b2e3 100644 --- a/packages/client-web/__tests__/utils/sleep.ts +++ b/packages/client-web/__tests__/utils/sleep.ts @@ -1,7 +1,7 @@ export async function sleep(ms: number): Promise { await new Promise((resolve) => setTimeout(() => { - resolve(void 0) + resolve(void 0); }, ms), - ) + ); } diff --git a/packages/client-web/__tests__/utils/web_client.ts b/packages/client-web/__tests__/utils/web_client.ts index e415e142..8fb77bdb 100644 --- a/packages/client-web/__tests__/utils/web_client.ts +++ b/packages/client-web/__tests__/utils/web_client.ts @@ -1,9 +1,9 @@ -import { createTestClient } from '@test/utils' -import type { ClickHouseClientConfigOptions } from '../../src' -import type { WebClickHouseClient } from '../../src/client' +import { createTestClient } from "@test/utils"; +import type { ClickHouseClientConfigOptions } from "../../src"; +import type { WebClickHouseClient } from "../../src/client"; export function createWebTestClient( config: ClickHouseClientConfigOptions = {}, ): WebClickHouseClient { - return createTestClient(config) as unknown as WebClickHouseClient + return createTestClient(config) as unknown as WebClickHouseClient; } diff --git a/packages/client-web/eslint.config.mjs b/packages/client-web/eslint.config.mjs index 82818f89..578dc9cf 100644 --- a/packages/client-web/eslint.config.mjs +++ b/packages/client-web/eslint.config.mjs @@ -1,7 +1,7 @@ -import js from '@eslint/js' -import { defineConfig } from 'eslint/config' -import tseslint from 'typescript-eslint' -import { typescriptEslintConfig } from '../../eslint.config.base.mjs' +import js from "@eslint/js"; +import { defineConfig } from "eslint/config"; +import tseslint from "typescript-eslint"; +import { typescriptEslintConfig } from "../../eslint.config.base.mjs"; export default defineConfig( // Base ESLint recommended rules @@ -13,15 +13,15 @@ export default defineConfig( // Ignore build artifacts and externals { ignores: [ - './__tests__/**/*.ts', - 'eslint.config.mjs', - 'vitest.*.config.ts', - 'vitest.*.setup.ts', - 'coverage', - 'out', - 'dist', - 'node_modules', - 'webpack', + "./__tests__/**/*.ts", + "eslint.config.mjs", + "vitest.*.config.ts", + "vitest.*.setup.ts", + "coverage", + "out", + "dist", + "node_modules", + "webpack", ], }, -) +); diff --git a/packages/client-web/src/client.ts b/packages/client-web/src/client.ts index 6dc757f5..653adeea 100644 --- a/packages/client-web/src/client.ts +++ b/packages/client-web/src/client.ts @@ -10,11 +10,11 @@ import type { InsertResult, IsSame, QueryParamsWithFormat, -} from '@clickhouse/client-common' -import { ClickHouseClient } from '@clickhouse/client-common' -import type { WebClickHouseClientConfigOptions } from './config' -import { WebImpl } from './config' -import type { ResultSet } from './result_set' +} from "@clickhouse/client-common"; +import { ClickHouseClient } from "@clickhouse/client-common"; +import type { WebClickHouseClientConfigOptions } from "./config"; +import { WebImpl } from "./config"; +import type { ResultSet } from "./result_set"; /** If the Format is not a literal type, fall back to the default behavior of the ResultSet, * allowing to call all methods with all data shapes variants, @@ -22,42 +22,42 @@ import type { ResultSet } from './result_set' export type QueryResult = IsSame extends true ? ResultSet - : ResultSet + : ResultSet; export type WebClickHouseClient = Omit< WebClickHouseClientImpl, - 'insert' | 'exec' | 'command' + "insert" | "exec" | "command" > & { /** See {@link ClickHouseClient.insert}. * * ReadableStream is removed from possible insert values * until it is supported by all major web platforms. */ insert( - params: Omit, 'values'> & { - values: ReadonlyArray | InputJSON | InputJSONObjectEachRow + params: Omit, "values"> & { + values: ReadonlyArray | InputJSON | InputJSONObjectEachRow; }, - ): Promise + ): Promise; /** See {@link ClickHouseClient.exec}. * * Custom values are currently not supported in the web versions. * The `ignore_error_response` parameter is not supported in the Web version. */ exec( - params: Omit, - ): Promise> + params: Omit, + ): Promise>; /** See {@link ClickHouseClient.command}. * * The `ignore_error_response` parameter is not supported in the Web version. */ command( - params: Omit, - ): Promise -} + params: Omit, + ): Promise; +}; class WebClickHouseClientImpl extends ClickHouseClient { /** See {@link ClickHouseClient.query}. */ override query( params: QueryParamsWithFormat, ): Promise> { - return super.query(params) as Promise> + return super.query(params) as Promise>; } } @@ -67,5 +67,5 @@ export function createClient( return new WebClickHouseClientImpl({ impl: WebImpl, ...(config || {}), - }) + }); } diff --git a/packages/client-web/src/config.ts b/packages/client-web/src/config.ts index 7df293f0..1998848f 100644 --- a/packages/client-web/src/config.ts +++ b/packages/client-web/src/config.ts @@ -5,19 +5,19 @@ import type { ImplementationDetails, JSONHandling, ResponseHeaders, -} from '@clickhouse/client-common' -import { WebConnection } from './connection' -import { ResultSet } from './result_set' -import { WebValuesEncoder } from './utils' +} from "@clickhouse/client-common"; +import { WebConnection } from "./connection"; +import { ResultSet } from "./result_set"; +import { WebValuesEncoder } from "./utils"; export type WebClickHouseClientConfigOptions = BaseClickHouseClientConfigOptions & { /** A custom implementation or wrapper over the global `fetch` method that will be used by the client internally. * This might be helpful if you want to configure mTLS or change other default `fetch` settings. */ - fetch?: typeof fetch - } + fetch?: typeof fetch; + }; -export const WebImpl: ImplementationDetails['impl'] = { +export const WebImpl: ImplementationDetails["impl"] = { make_connection: ( config: WebClickHouseClientConfigOptions, params: ConnectionParams, @@ -35,4 +35,4 @@ export const WebImpl: ImplementationDetails['impl'] = { ) => new ResultSet(stream, format, query_id, response_headers)) as any, values_encoder: (jsonHandling: JSONHandling) => new WebValuesEncoder(jsonHandling), -} +}; diff --git a/packages/client-web/src/connection/index.ts b/packages/client-web/src/connection/index.ts index 09d43427..4eed01b9 100644 --- a/packages/client-web/src/connection/index.ts +++ b/packages/client-web/src/connection/index.ts @@ -1 +1 @@ -export * from './web_connection' +export * from "./web_connection"; diff --git a/packages/client-web/src/connection/web_connection.ts b/packages/client-web/src/connection/web_connection.ts index 9c4d4f7c..3bc0b97c 100644 --- a/packages/client-web/src/connection/web_connection.ts +++ b/packages/client-web/src/connection/web_connection.ts @@ -8,7 +8,7 @@ import type { ConnPingResult, ConnQueryResult, ResponseHeaders, -} from '@clickhouse/client-common' +} from "@clickhouse/client-common"; import { isCredentialsAuth, isJWTAuth, @@ -18,42 +18,42 @@ import { transformUrl, withCompressionHeaders, withHttpSettings, -} from '@clickhouse/client-common' -import { getAsText } from '../utils' +} from "@clickhouse/client-common"; +import { getAsText } from "../utils"; type WebInsertParams = Omit< ConnInsertParams>, - 'values' + "values" > & { - values: string -} + values: string; +}; export type WebConnectionParams = ConnectionParams & { - fetch?: typeof fetch -} + fetch?: typeof fetch; +}; export class WebConnection implements Connection { - private readonly defaultAuthHeader: string - private readonly params: WebConnectionParams + private readonly defaultAuthHeader: string; + private readonly params: WebConnectionParams; constructor(params: WebConnectionParams) { - this.params = params - if (params.auth.type === 'JWT') { - this.defaultAuthHeader = `Bearer ${params.auth.access_token}` - } else if (params.auth.type === 'Credentials') { - this.defaultAuthHeader = `Basic ${btoa(`${params.auth.username}:${params.auth.password}`)}` + this.params = params; + if (params.auth.type === "JWT") { + this.defaultAuthHeader = `Bearer ${params.auth.access_token}`; + } else if (params.auth.type === "Credentials") { + this.defaultAuthHeader = `Basic ${btoa(`${params.auth.username}:${params.auth.password}`)}`; } else { - throw new Error(`Unknown auth type: ${(params.auth as any).type}`) + throw new Error(`Unknown auth type: ${(params.auth as any).type}`); } } async query( params: ConnBaseQueryParams, ): Promise>> { - const query_id = getQueryId(params.query_id) + const query_id = getQueryId(params.query_id); const clickhouse_settings = withHttpSettings( params.clickhouse_settings, this.params.compression.decompress_response, - ) + ); const searchParams = toSearchParams({ database: this.params.database, clickhouse_settings, @@ -61,45 +61,45 @@ export class WebConnection implements Connection { session_id: params.session_id, role: params.role, query_id, - }) + }); const response = await this.request({ body: params.query, params, searchParams, - }) + }); return { query_id, stream: response.body || new ReadableStream(), response_headers: getResponseHeaders(response), http_status_code: response.status, - } + }; } async exec( params: ConnBaseQueryParams, ): Promise>> { - const result = await this.runExec(params) + const result = await this.runExec(params); return { query_id: result.query_id, stream: result.stream || new ReadableStream(), response_headers: result.response_headers, http_status_code: result.http_status_code, - } + }; } async command(params: ConnBaseQueryParams): Promise { const { stream, query_id, response_headers, http_status_code } = - await this.runExec(params) + await this.runExec(params); if (stream !== null) { - await stream.cancel() + await stream.cancel(); } - return { query_id, response_headers, http_status_code } + return { query_id, response_headers, http_status_code }; } async insert( params: WebInsertParams, ): Promise { - const query_id = getQueryId(params.query_id) + const query_id = getQueryId(params.query_id); const searchParams = toSearchParams({ database: this.params.database, clickhouse_settings: params.clickhouse_settings, @@ -108,20 +108,20 @@ export class WebConnection implements Connection { session_id: params.session_id, role: params.role, query_id, - }) + }); const response = await this.request({ body: params.values, params, searchParams, - }) + }); if (response.body !== null) { - await response.text() // drain the response (it's empty anyway) + await response.text(); // drain the response (it's empty anyway) } return { query_id, response_headers: getResponseHeaders(response), http_status_code: response.status, - } + }; } async ping(): Promise { @@ -135,25 +135,25 @@ export class WebConnection implements Connection { query: `SELECT 'ping'`, query_id: getQueryId(undefined), }), - method: 'GET', - }) + method: "GET", + }); if (response.body !== null) { - await response.body.cancel() + await response.body.cancel(); } - return { success: true } + return { success: true }; } catch (error) { if (error instanceof Error) { return { success: false, error, - } + }; } - throw error // should never happen + throw error; // should never happen } } async close(): Promise { - return + return; } private async request({ @@ -163,32 +163,32 @@ export class WebConnection implements Connection { pathname, method, }: { - body: string | null - params?: ConnBaseQueryParams - searchParams?: URLSearchParams - pathname?: string - method?: 'GET' | 'POST' + body: string | null; + params?: ConnBaseQueryParams; + searchParams?: URLSearchParams; + pathname?: string; + method?: "GET" | "POST"; }): Promise { const url = transformUrl({ url: this.params.url, pathname, searchParams, - }).toString() + }).toString(); - const abortController = new AbortController() + const abortController = new AbortController(); - let isTimedOut = false + let isTimedOut = false; const timeout = setTimeout(() => { - isTimedOut = true - abortController.abort() - }, this.params.request_timeout) + isTimedOut = true; + abortController.abort(); + }, this.params.request_timeout); - let isAborted = false + let isAborted = false; if (params?.abort_signal !== undefined) { params.abort_signal.onabort = () => { - isAborted = true - abortController.abort() - } + isAborted = true; + abortController.abort(); + }; } try { @@ -198,46 +198,46 @@ export class WebConnection implements Connection { enable_request_compression: false, enable_response_compression: this.params.compression.decompress_response, - }) + }); // avoiding "fetch called on an object that does not implement interface Window" error - const fetchFn = this.params.fetch ?? fetch + const fetchFn = this.params.fetch ?? fetch; const response = await fetchFn(url, { body, headers, keepalive: this.params.keep_alive.enabled, - method: method ?? 'POST', + method: method ?? "POST", signal: abortController.signal, - }) - clearTimeout(timeout) + }); + clearTimeout(timeout); if (isSuccessfulResponse(response.status)) { - return response + return response; } else { return Promise.reject( parseError( await getAsText(response.body || new ReadableStream()), ), - ) + ); } } catch (err) { - clearTimeout(timeout) + clearTimeout(timeout); if (isAborted) { - return Promise.reject(new Error('The user aborted a request.')) + return Promise.reject(new Error("The user aborted a request.")); } if (isTimedOut) { - return Promise.reject(new Error('Timeout error.')) + return Promise.reject(new Error("Timeout error.")); } if (err instanceof Error) { // maybe it's a ClickHouse error - return Promise.reject(parseError(err)) + return Promise.reject(parseError(err)); } // shouldn't happen - throw err + throw err; } } private async runExec(params: ConnBaseQueryParams): Promise { - const query_id = getQueryId(params.query_id) + const query_id = getQueryId(params.query_id); const searchParams = toSearchParams({ database: this.params.database, clickhouse_settings: params.clickhouse_settings, @@ -245,30 +245,30 @@ export class WebConnection implements Connection { session_id: params.session_id, role: params.role, query_id, - }) + }); const response = await this.request({ body: params.query, params, searchParams, - }) + }); return { stream: response.body, response_headers: getResponseHeaders(response), query_id, http_status_code: response.status, - } + }; } private defaultHeadersWithOverride( params?: ConnBaseQueryParams, ): Record { - let authHeader: string + let authHeader: string; if (isJWTAuth(params?.auth)) { - authHeader = `Bearer ${params?.auth.access_token}` + authHeader = `Bearer ${params?.auth.access_token}`; } else if (isCredentialsAuth(params?.auth)) { - authHeader = `Basic ${btoa(`${params?.auth.username}:${params?.auth.password}`)}` + authHeader = `Basic ${btoa(`${params?.auth.username}:${params?.auth.password}`)}`; } else { - authHeader = this.defaultAuthHeader + authHeader = this.defaultAuthHeader; } return { // Custom HTTP headers from the client configuration @@ -276,25 +276,25 @@ export class WebConnection implements Connection { // Custom HTTP headers for this particular request; it will override the client configuration with the same keys ...(params?.http_headers ?? {}), Authorization: authHeader, - } + }; } } function getQueryId(query_id: string | undefined): string { - return query_id || crypto.randomUUID() + return query_id || crypto.randomUUID(); } function getResponseHeaders(response: Response): ResponseHeaders { - const headers: ResponseHeaders = {} + const headers: ResponseHeaders = {}; response.headers.forEach((value, key) => { - headers[key] = value - }) - return headers + headers[key] = value; + }); + return headers; } interface RunExecResult { - stream: ReadableStream | null - query_id: string - response_headers: ResponseHeaders - http_status_code: number + stream: ReadableStream | null; + query_id: string; + response_headers: ResponseHeaders; + http_status_code: number; } diff --git a/packages/client-web/src/index.ts b/packages/client-web/src/index.ts index e39831c1..33d9dd72 100644 --- a/packages/client-web/src/index.ts +++ b/packages/client-web/src/index.ts @@ -1,10 +1,10 @@ export { type WebClickHouseClient as ClickHouseClient, type QueryResult, -} from './client' -export { createClient } from './client' -export { type WebClickHouseClientConfigOptions as ClickHouseClientConfigOptions } from './config' -export { ResultSet } from './result_set' +} from "./client"; +export { createClient } from "./client"; +export { type WebClickHouseClientConfigOptions as ClickHouseClientConfigOptions } from "./config"; +export { ResultSet } from "./result_set"; /** Re-export @clickhouse/client-common types */ export { @@ -54,7 +54,7 @@ export { type ClickHouseAuth, type ClickHouseJWTAuth, type ClickHouseCredentialsAuth, -} from '@clickhouse/client-common' +} from "@clickhouse/client-common"; /** * Re-export @clickhouse/client-common runtime values. @@ -82,26 +82,26 @@ import { isException as isException_, TupleParam as TupleParam_, defaultJSONHandling as defaultJSONHandling_, -} from '@clickhouse/client-common' +} 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_ +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/result_set.ts b/packages/client-web/src/result_set.ts index 8c2af20e..13e3051b 100644 --- a/packages/client-web/src/result_set.ts +++ b/packages/client-web/src/result_set.ts @@ -6,31 +6,31 @@ import type { ResultJSONType, ResultStream, Row, -} from '@clickhouse/client-common' +} from "@clickhouse/client-common"; import { CARET_RETURN, extractErrorAtTheEndOfChunk, -} from '@clickhouse/client-common' +} from "@clickhouse/client-common"; import { isNotStreamableJSONFamily, isStreamableJSONFamily, validateStreamFormat, -} from '@clickhouse/client-common' -import { getAsText } from './utils' +} from "@clickhouse/client-common"; +import { getAsText } from "./utils"; -const NEWLINE = 0x0a as const +const NEWLINE = 0x0a as const; export class ResultSet< Format extends DataFormat | unknown, > implements BaseResultSet, Format> { - public readonly response_headers: ResponseHeaders + public readonly response_headers: ResponseHeaders; - private readonly exceptionTag: string | undefined = undefined - private isAlreadyConsumed = false - private readonly jsonHandling: JSONHandling - private _stream: ReadableStream - private readonly format: Format - public readonly query_id: string + private readonly exceptionTag: string | undefined = undefined; + private isAlreadyConsumed = false; + private readonly jsonHandling: JSONHandling; + private _stream: ReadableStream; + private readonly format: Format; + public readonly query_id: string; constructor( _stream: ReadableStream, @@ -42,94 +42,94 @@ export class ResultSet< stringify: JSON.stringify, }, ) { - this._stream = _stream - this.format = format - this.query_id = query_id + this._stream = _stream; + this.format = format; + this.query_id = query_id; this.response_headers = - _response_headers !== undefined ? Object.freeze(_response_headers) : {} - this.exceptionTag = this.response_headers['x-clickhouse-exception-tag'] as + _response_headers !== undefined ? Object.freeze(_response_headers) : {}; + this.exceptionTag = this.response_headers["x-clickhouse-exception-tag"] as | string - | undefined + | undefined; - this.jsonHandling = jsonHandling + this.jsonHandling = jsonHandling; } /** See {@link BaseResultSet.text} */ async text(): Promise { - this.markAsConsumed() - return getAsText(this._stream) + this.markAsConsumed(); + return getAsText(this._stream); } /** See {@link BaseResultSet.json} */ async json(): Promise> { // JSONEachRow, etc. if (isStreamableJSONFamily(this.format as DataFormat)) { - const result: T[] = [] - const reader = this.stream().getReader() + const result: T[] = []; + const reader = this.stream().getReader(); while (true) { - const { done, value } = await reader.read() + const { done, value } = await reader.read(); if (done) { - break + break; } for (const row of value) { - result.push(row.json() as T) + result.push(row.json() as T); } } - return result as any + return result as any; } // JSON, JSONObjectEachRow, etc. if (isNotStreamableJSONFamily(this.format as DataFormat)) { - const text = await getAsText(this._stream) - return this.jsonHandling.parse(text) + const text = await getAsText(this._stream); + return this.jsonHandling.parse(text); } // should not be called for CSV, etc. - throw new Error(`Cannot decode ${this.format} as JSON`) + throw new Error(`Cannot decode ${this.format} as JSON`); } /** See {@link BaseResultSet.stream} */ stream(): ResultStream[]>> { - this.markAsConsumed() - validateStreamFormat(this.format) + this.markAsConsumed(); + validateStreamFormat(this.format); - const incompleteChunks: Uint8Array[] = [] - let totalIncompleteLength = 0 + const incompleteChunks: Uint8Array[] = []; + let totalIncompleteLength = 0; - const exceptionTag = this.exceptionTag - const jsonHandling = this.jsonHandling - const decoder = new TextDecoder('utf-8') + const exceptionTag = this.exceptionTag; + const jsonHandling = this.jsonHandling; + const decoder = new TextDecoder("utf-8"); const transform = new TransformStream({ start() { // }, transform: (chunk: Uint8Array, controller) => { if (chunk === null) { - controller.terminate() + controller.terminate(); } - const rows: Row[] = [] + const rows: Row[] = []; - let idx: number - let lastIdx = 0 + let idx: number; + let lastIdx = 0; while (true) { // an unescaped newline character denotes the end of a row, // or at least the beginning of the exception marker - idx = chunk.indexOf(NEWLINE, lastIdx) + idx = chunk.indexOf(NEWLINE, lastIdx); if (idx === -1) { // there is no complete row in the rest of the current chunk // to be processed during the next transform iteration - const incompleteChunk = chunk.slice(lastIdx) - incompleteChunks.push(incompleteChunk) - totalIncompleteLength += incompleteChunk.length + const incompleteChunk = chunk.slice(lastIdx); + incompleteChunks.push(incompleteChunk); + totalIncompleteLength += incompleteChunk.length; // send the extracted rows to the consumer, if any if (rows.length > 0) { - controller.enqueue(rows) + controller.enqueue(rows); } - break + break; } else { - let bytesToDecode: Uint8Array + let bytesToDecode: Uint8Array; // Check for exception in the chunk (only after 25.11) if ( @@ -137,61 +137,63 @@ export class ResultSet< idx >= 1 && chunk[idx - 1] === CARET_RETURN ) { - controller.error(extractErrorAtTheEndOfChunk(chunk, exceptionTag)) + controller.error( + extractErrorAtTheEndOfChunk(chunk, exceptionTag), + ); } // using the incomplete chunks from the previous iterations if (incompleteChunks.length > 0) { const completeRowBytes = new Uint8Array( totalIncompleteLength + idx, - ) + ); - let offset = 0 + let offset = 0; incompleteChunks.forEach((incompleteChunk) => { - completeRowBytes.set(incompleteChunk, offset) - offset += incompleteChunk.length - }) + completeRowBytes.set(incompleteChunk, offset); + offset += incompleteChunk.length; + }); // finalize the row with the current chunk slice that ends with a newline - const finalChunk = chunk.slice(0, idx) - completeRowBytes.set(finalChunk, offset) + const finalChunk = chunk.slice(0, idx); + completeRowBytes.set(finalChunk, offset); // Reset the incomplete chunks. // Removing used buffers and reusing the already allocated memory // by setting length to 0 - incompleteChunks.length = 0 - totalIncompleteLength = 0 + incompleteChunks.length = 0; + totalIncompleteLength = 0; - bytesToDecode = completeRowBytes + bytesToDecode = completeRowBytes; } else { - bytesToDecode = chunk.slice(lastIdx, idx) + bytesToDecode = chunk.slice(lastIdx, idx); } - const text = decoder.decode(bytesToDecode) + const text = decoder.decode(bytesToDecode); rows.push({ text, json(): T { - return jsonHandling.parse(text) + return jsonHandling.parse(text); }, - }) + }); - lastIdx = idx + 1 // skipping newline character + lastIdx = idx + 1; // skipping newline character } } }, - }) + }); const pipeline = this._stream.pipeThrough(transform, { preventClose: false, preventAbort: false, preventCancel: false, - }) - return pipeline as any + }); + return pipeline as any; } async close(): Promise { - this.markAsConsumed() - await this._stream.cancel() + this.markAsConsumed(); + await this._stream.cancel(); } /** @@ -202,15 +204,15 @@ export class ResultSet< * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/using */ async [Symbol.asyncDispose]() { - await this.close() + await this.close(); } private markAsConsumed() { if (this.isAlreadyConsumed) { - throw new Error(streamAlreadyConsumedMessage) + throw new Error(streamAlreadyConsumedMessage); } - this.isAlreadyConsumed = true + this.isAlreadyConsumed = true; } } -const streamAlreadyConsumedMessage = 'Stream has been already consumed' +const streamAlreadyConsumedMessage = "Stream has been already consumed"; diff --git a/packages/client-web/src/utils/encoder.ts b/packages/client-web/src/utils/encoder.ts index b9bbd7a5..b4f7b3a6 100644 --- a/packages/client-web/src/utils/encoder.ts +++ b/packages/client-web/src/utils/encoder.ts @@ -2,12 +2,12 @@ import type { DataFormat, InsertValues, ValuesEncoder, -} from '@clickhouse/client-common' -import { encodeJSON, type JSONHandling } from '@clickhouse/client-common' -import { isStream } from './stream' +} from "@clickhouse/client-common"; +import { encodeJSON, type JSONHandling } from "@clickhouse/client-common"; +import { isStream } from "./stream"; export class WebValuesEncoder implements ValuesEncoder { - private readonly json: JSONHandling + private readonly json: JSONHandling; constructor( jsonHandling: JSONHandling = { @@ -15,36 +15,36 @@ export class WebValuesEncoder implements ValuesEncoder { stringify: JSON.stringify, }, ) { - this.json = jsonHandling + this.json = jsonHandling; } encodeValues( values: InsertValues, format: DataFormat, ): string | ReadableStream { - throwIfStream(values) + throwIfStream(values); // JSON* arrays if (Array.isArray(values)) { return values .map((value) => encodeJSON(value, format, this.json.stringify)) - .join('') + .join(""); } // JSON & JSONObjectEachRow format input - if (typeof values === 'object') { - return encodeJSON(values, format, this.json.stringify) + if (typeof values === "object") { + return encodeJSON(values, format, this.json.stringify); } throw new Error( `Cannot encode values of type ${typeof values} with ${format} format`, - ) + ); } validateInsertValues(values: InsertValues): void { - throwIfStream(values) - if (!Array.isArray(values) && typeof values !== 'object') { + throwIfStream(values); + if (!Array.isArray(values) && typeof values !== "object") { throw new Error( 'Insert expected "values" to be an array or a JSON object, ' + `got: ${typeof values}`, - ) + ); } } } @@ -52,7 +52,7 @@ export class WebValuesEncoder implements ValuesEncoder { function throwIfStream(values: unknown) { if (isStream(values)) { throw new Error( - 'Streaming is not supported for inserts in the web version of the client', - ) + "Streaming is not supported for inserts in the web version of the client", + ); } } diff --git a/packages/client-web/src/utils/index.ts b/packages/client-web/src/utils/index.ts index 99083b36..98c3bba5 100644 --- a/packages/client-web/src/utils/index.ts +++ b/packages/client-web/src/utils/index.ts @@ -1,2 +1,2 @@ -export * from './stream' -export * from './encoder' +export * from "./stream"; +export * from "./encoder"; diff --git a/packages/client-web/src/utils/stream.ts b/packages/client-web/src/utils/stream.ts index b63589c7..89b7712c 100644 --- a/packages/client-web/src/utils/stream.ts +++ b/packages/client-web/src/utils/stream.ts @@ -1,21 +1,21 @@ // See https://github.com/v8/v8/commit/ea56bf5513d0cbd2a35a9035c5c2996272b8b728 -const MaxStringLength = Math.pow(2, 29) - 24 +const MaxStringLength = Math.pow(2, 29) - 24; export function isStream(obj: any): obj is ReadableStream { return ( - obj !== null && obj !== undefined && typeof obj.pipeThrough === 'function' - ) + obj !== null && obj !== undefined && typeof obj.pipeThrough === "function" + ); } export async function getAsText(stream: ReadableStream): Promise { - let text = '' + let text = ""; - const textDecoder = new TextDecoder() - const reader = stream.getReader() + const textDecoder = new TextDecoder(); + const reader = stream.getReader(); while (true) { - const { done, value } = await reader.read() - const decoded = textDecoder.decode(value, { stream: true }) + const { done, value } = await reader.read(); + const decoded = textDecoder.decode(value, { stream: true }); if (decoded.length + text.length > MaxStringLength) { // The error message is crafted to be similar to the one thrown by Node's implementation. // A simple try/catch block around the concatenation of the decoded chunk would not work @@ -24,23 +24,23 @@ export async function getAsText(stream: ReadableStream): Promise { // detect this condition across browsers. // Also, Vitest crashes while running the try/catch implementatioin in Firefox. throw new Error( - 'The response length exceeds the maximum allowed size of V8 String: ' + + "The response length exceeds the maximum allowed size of V8 String: " + `${MaxStringLength}; consider limiting the amount of requested rows.`, - ) + ); } - text += decoded - if (done) break + text += decoded; + if (done) break; } // flush unfinished multi-byte characters - const decoded = textDecoder.decode() + const decoded = textDecoder.decode(); if (decoded.length + text.length > MaxStringLength) { throw new Error( - 'The response length exceeds the maximum allowed size of V8 String: ' + + "The response length exceeds the maximum allowed size of V8 String: " + `${MaxStringLength}; consider limiting the amount of requested rows.`, - ) + ); } - text += decoded + text += decoded; - return text + return text; } diff --git a/packages/client-web/src/version.ts b/packages/client-web/src/version.ts index fab0915d..a38a7bdc 100644 --- a/packages/client-web/src/version.ts +++ b/packages/client-web/src/version.ts @@ -1 +1 @@ -export default '1.21.0' +export default "1.21.0"; diff --git a/skills/clickhouse-js-node-coding/reference/async-insert.md b/skills/clickhouse-js-node-coding/reference/async-insert.md index 2d3741a5..9011e733 100644 --- a/skills/clickhouse-js-node-coding/reference/async-insert.md +++ b/skills/clickhouse-js-node-coding/reference/async-insert.md @@ -17,7 +17,7 @@ Enable on the client level or per-request via `clickhouse_settings`: ```ts -import { createClient, ClickHouseError } from '@clickhouse/client' +import { createClient, ClickHouseError } from "@clickhouse/client"; const client = createClient({ url: process.env.CLICKHOUSE_URL, @@ -26,10 +26,10 @@ const client = createClient({ clickhouse_settings: { async_insert: 1, wait_for_async_insert: 1, // wait for ack from server - async_insert_max_data_size: '1000000', + async_insert_max_data_size: "1000000", async_insert_busy_timeout_ms: 1000, }, -}) +}); ``` ## Concurrent small inserts @@ -42,21 +42,21 @@ const promises = [...new Array(10)].map(async () => { const values = [...new Array(1000).keys()].map(() => ({ id: Math.floor(Math.random() * 100_000) + 1, data: Math.random().toString(36).slice(2), - })) + })); await client - .insert({ table: 'async_insert_example', values, format: 'JSONEachRow' }) + .insert({ table: "async_insert_example", values, format: "JSONEachRow" }) .catch((err) => { if (err instanceof ClickHouseError) { // err.code matches a row in system.errors - console.error(`ClickHouse error ${err.code}:`, err) - return + console.error(`ClickHouse error ${err.code}:`, err); + return; } - console.error('Insert failed:', err) - }) -}) + console.error("Insert failed:", err); + }); +}); -await Promise.all(promises) +await Promise.all(promises); ``` ## `wait_for_async_insert` — fire-and-forget vs ack @@ -82,7 +82,7 @@ await client.command({ ENGINE MergeTree ORDER BY id `, clickhouse_settings: { wait_end_of_query: 1 }, -}) +}); ``` Even better is to create a specialized client for inserts with the appropriate async diff --git a/skills/clickhouse-js-node-coding/reference/client-configuration.md b/skills/clickhouse-js-node-coding/reference/client-configuration.md index f0b05082..94eaefc9 100644 --- a/skills/clickhouse-js-node-coding/reference/client-configuration.md +++ b/skills/clickhouse-js-node-coding/reference/client-configuration.md @@ -30,16 +30,16 @@ When answering configuration questions, include the relevant points: ## Minimal client ```ts -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; const client = createClient({ url: process.env.CLICKHOUSE_URL, // defaults to 'http://localhost:8123' username: process.env.CLICKHOUSE_USER, // defaults to 'default' password: process.env.CLICKHOUSE_PASSWORD, // defaults to '' - database: 'analytics', // defaults to 'default' -}) + database: "analytics", // defaults to 'default' +}); // ... your queries ... -await client.close() +await client.close(); ``` `url` accepts a string or a `URL` object. The accepted string format is: @@ -62,7 +62,7 @@ export CLICKHOUSE_URL='https://bob:secret@my.host:8124/analytics' ```ts // In your Node.js code — no URL construction needed: -const client = createClient({ url: process.env.CLICKHOUSE_URL }) +const client = createClient({ url: process.env.CLICKHOUSE_URL }); ``` ## Per-client vs per-request `clickhouse_settings` ⭐ @@ -79,14 +79,14 @@ const client = createClient({ clickhouse_settings: { output_format_json_quote_64bit_integers: 0, // applied to every request }, -}) +}); const rows = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 2 FORMAT JSONEachRow', + query: "SELECT number FROM system.numbers LIMIT 2 FORMAT JSONEachRow", clickhouse_settings: { output_format_json_quote_64bit_integers: 1, // overrides client default for this call }, -}) +}); ``` ## `default_format` for `exec()` @@ -96,17 +96,17 @@ query has no trailing `FORMAT …` clause, set `default_format` so the server knows what to send back, then wrap the response in a `ResultSet`: ```ts -import { createClient, ResultSet } from '@clickhouse/client' +import { createClient, ResultSet } from "@clickhouse/client"; -const client = createClient() -const format = 'JSONCompactEachRowWithNamesAndTypes' +const client = createClient(); +const format = "JSONCompactEachRowWithNamesAndTypes"; const { stream, query_id } = await client.exec({ - query: 'SELECT database, name, engine FROM system.tables LIMIT 5', + query: "SELECT database, name, engine FROM system.tables LIMIT 5", clickhouse_settings: { default_format: format }, -}) -const rs = new ResultSet(stream, format, query_id) -console.log(await rs.json()) -await client.close() +}); +const rs = new ResultSet(stream, format, query_id); +console.log(await rs.json()); +await client.close(); ``` For ordinary `SELECT`s prefer `client.query({ format })` — `default_format` is diff --git a/skills/clickhouse-js-node-coding/reference/custom-json.md b/skills/clickhouse-js-node-coding/reference/custom-json.md index 715eb9cb..3e5a7784 100644 --- a/skills/clickhouse-js-node-coding/reference/custom-json.md +++ b/skills/clickhouse-js-node-coding/reference/custom-json.md @@ -35,38 +35,38 @@ A custom `{ parse, stringify }` lets you plug in `JSONBig`, ## Recipe: BigInt-safe stringify, custom Date handling ```ts -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; const valueSerializer = (value: unknown): unknown => { // Serialize Date as a UNIX millis number (instead of toJSON's ISO string) if (value instanceof Date) { - return value.getTime() + return value.getTime(); } // Serialize BigInt as a string so JSON.stringify won't throw - if (typeof value === 'bigint') { - return value.toString() + if (typeof value === "bigint") { + return value.toString(); } if (Array.isArray(value)) { - return value.map(valueSerializer) + return value.map(valueSerializer); } - if (typeof value === 'object' && value !== null) { + if (typeof value === "object" && value !== null) { return Object.fromEntries( Object.entries(value).map(([k, v]) => [k, valueSerializer(v)]), - ) + ); } - return value -} + return value; +}; const client = createClient({ json: { parse: JSON.parse, // use default parsing stringify: (obj: unknown) => JSON.stringify(valueSerializer(obj)), }, -}) +}); await client.command({ query: ` @@ -75,20 +75,20 @@ await client.command({ ENGINE MergeTree ORDER BY id `, -}) +}); await client.insert({ - table: 'inserts_custom_json_handling', - format: 'JSONEachRow', + table: "inserts_custom_json_handling", + format: "JSONEachRow", values: [ { - id: BigInt('250000000000000200'), // serialized as a string + id: BigInt("250000000000000200"), // serialized as a string dt: new Date(), // serialized as ms since epoch }, ], -}) +}); -await client.close() +await client.close(); ``` > The custom `valueSerializer` runs **before** `JSON.stringify`, so values @@ -102,10 +102,10 @@ or precision-lossy numbers), plug in a `BigInt`-aware parser such as [`json-bigint`](https://www.npmjs.com/package/json-bigint): ```ts -import { createClient } from '@clickhouse/client' -import JSONBig from 'json-bigint' +import { createClient } from "@clickhouse/client"; +import JSONBig from "json-bigint"; -const bigJson = JSONBig({ useNativeBigInt: true }) +const bigJson = JSONBig({ useNativeBigInt: true }); const client = createClient({ json: { @@ -115,7 +115,7 @@ const client = createClient({ clickhouse_settings: { output_format_json_quote_64bit_integers: 0, }, -}) +}); ``` `output_format_json_quote_64bit_integers: 0` is the default since @@ -134,15 +134,15 @@ hand-rolled reviver. This uses the `context.source` argument that numeric literal is available before it's coerced to a JS `number`: ```ts -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; const parseBigInt = (text: string) => JSON.parse(text, function (key, value, context) { - if (key.endsWith('__bigint')) { - return BigInt(context.source) + if (key.endsWith("__bigint")) { + return BigInt(context.source); } - return value - }) + return value; + }); const client = createClient({ json: { @@ -150,15 +150,15 @@ const client = createClient({ stringify: JSON.stringify, // use default stringify }, clickhouse_settings: { output_format_json_quote_64bit_integers: 0 }, -}) +}); const rs = await client.query({ - query: 'SELECT toUInt64(250000000000000200) AS id__bigint', -}) -const { data } = await rs.json() -console.log(data[0].id__bigint) // 250000000000000200 + query: "SELECT toUInt64(250000000000000200) AS id__bigint", +}); +const { data } = await rs.json(); +console.log(data[0].id__bigint); // 250000000000000200 -await client.close() +await client.close(); ``` Trade-offs versus `json-bigint`: diff --git a/skills/clickhouse-js-node-coding/reference/data-types.md b/skills/clickhouse-js-node-coding/reference/data-types.md index e20771c5..1c1ca5f0 100644 --- a/skills/clickhouse-js-node-coding/reference/data-types.md +++ b/skills/clickhouse-js-node-coding/reference/data-types.md @@ -50,7 +50,7 @@ When answering about storing and reading JSON objects: ## `Dynamic`, `Variant(...)`, `JSON` ```ts -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; const client = createClient({ clickhouse_settings: { @@ -61,7 +61,7 @@ const client = createClient({ allow_experimental_dynamic_type: 1, allow_experimental_json_type: 1, }, -}) +}); await client.command({ query: ` @@ -75,16 +75,16 @@ await client.command({ ENGINE MergeTree ORDER BY id `, -}) +}); await client.insert({ - table: 'chjs_dynamic_variant_json', - format: 'JSONEachRow', + table: "chjs_dynamic_variant_json", + format: "JSONEachRow", values: [ - { id: 1, var: 42, dynamic: 'foo', json: { foo: 'x' } }, - { id: 2, var: 'str', dynamic: 144, json: { bar: 10 } }, + { id: 1, var: 42, dynamic: "foo", json: { foo: "x" } }, + { id: 2, var: "str", dynamic: 144, json: { bar: 10 } }, ], -}) +}); const rs = await client.query({ query: ` @@ -95,36 +95,36 @@ const rs = await client.query({ dynamicType(json.bar) FROM chjs_dynamic_variant_json `, - format: 'JSONEachRow', -}) -console.log(await rs.json()) + format: "JSONEachRow", +}); +console.log(await rs.json()); ``` Outputs: ```js -;[ +[ { - id: '1', - var: '42', - dynamic: 'foo', - json: { foo: 'x' }, - 'variantType(var)': 'Int64', - 'dynamicType(dynamic)': 'String', - 'dynamicType(json.foo)': 'String', - 'dynamicType(json.bar)': 'None', + id: "1", + var: "42", + dynamic: "foo", + json: { foo: "x" }, + "variantType(var)": "Int64", + "dynamicType(dynamic)": "String", + "dynamicType(json.foo)": "String", + "dynamicType(json.bar)": "None", }, { - id: '2', - var: 'str', - dynamic: '144', - json: { bar: '10' }, - 'variantType(var)': 'String', - 'dynamicType(dynamic)': 'Int64', - 'dynamicType(json.foo)': 'None', - 'dynamicType(json.bar)': 'Int64', + id: "2", + var: "str", + dynamic: "144", + json: { bar: "10" }, + "variantType(var)": "String", + "dynamicType(dynamic)": "Int64", + "dynamicType(json.foo)": "None", + "dynamicType(json.bar)": "Int64", }, -] +]; ``` ### Notes @@ -149,7 +149,7 @@ sub-second precision (`p` digits, up to `9` for nanoseconds). Both require ```ts const client = createClient({ clickhouse_settings: { enable_time_time64_type: 1 }, -}) +}); await client.command({ query: ` @@ -165,38 +165,38 @@ await client.command({ ENGINE MergeTree ORDER BY id `, -}) +}); await client.insert({ - table: 'chjs_time_time64', - format: 'JSONEachRow', + table: "chjs_time_time64", + format: "JSONEachRow", values: [ { id: 1, - t: '12:34:56', - t64_0: '12:34:56', - t64_3: '12:34:56.123', - t64_6: '12:34:56.123456', - t64_9: '12:34:56.123456789', + t: "12:34:56", + t64_0: "12:34:56", + t64_3: "12:34:56.123", + t64_6: "12:34:56.123456", + t64_9: "12:34:56.123456789", }, { id: 2, - t: '999:59:59', - t64_0: '999:59:59', - t64_3: '999:59:59.999', - t64_6: '999:59:59.999999', - t64_9: '999:59:59.999999999', + t: "999:59:59", + t64_0: "999:59:59", + t64_3: "999:59:59.999", + t64_6: "999:59:59.999999", + t64_9: "999:59:59.999999999", }, { id: 3, - t: '-999:59:59', - t64_0: '-999:59:59', - t64_3: '-999:59:59.999', - t64_6: '-999:59:59.999999', - t64_9: '-999:59:59.999999999', + t: "-999:59:59", + t64_0: "-999:59:59", + t64_3: "-999:59:59.999", + t64_6: "-999:59:59.999999", + t64_9: "-999:59:59.999999999", }, ], -}) +}); ``` ### Notes @@ -228,16 +228,16 @@ ClickHouse handle the bit-plane layout — don't feed raw `FixedString` bytes through JSON yourself. ```ts -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; -const tableName = `chjs_qbit` +const tableName = `chjs_qbit`; const client = createClient({ clickhouse_settings: { // QBit introduced in ClickHouse 25.10 (experimental), GA since 26.x. // This setting is required only on 25.10; harmless/no-op on >= 26.x. allow_experimental_qbit_type: 1, }, -}) +}); await client.command({ query: ` @@ -249,7 +249,7 @@ await client.command({ ENGINE MergeTree ORDER BY id `, -}) +}); // Even though QBit is stored internally as a Tuple of FixedString bit planes, // JSON* formats accept (and return) the original Array(Float32) shape. @@ -257,21 +257,21 @@ const values = [ { id: 1, vec: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0] }, { id: 2, vec: [8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0] }, { id: 3, vec: [1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5] }, -] +]; await client.insert({ table: tableName, - format: 'JSONEachRow', + format: "JSONEachRow", values, -}) +}); // Round-trip via JSONEachRow: the vec column comes back as an array of numbers. const rs = await client.query({ query: `SELECT id, vec FROM ${tableName} ORDER BY id`, - format: 'JSONEachRow', -}) -const rows = await rs.json<{ id: number; vec: number[] }>() + format: "JSONEachRow", +}); +const rows = await rs.json<{ id: number; vec: number[] }>(); // vec comes back unchanged as the original Float32 array. -console.log(rows) +console.log(rows); // Approximate vector search via L2DistanceTransposed. // The third argument is the precision in bits: lower = less I/O, less accurate. @@ -286,13 +286,13 @@ const search = await client.query({ ref: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], bits: 16, }, - format: 'JSONEachRow', -}) -const nearest = await search.json<{ id: number; dist: number }>() + format: "JSONEachRow", +}); +const nearest = await search.json<{ id: number; dist: number }>(); // The reference vector is exactly row #1, so it's the closest match (dist 0). -console.log(nearest) +console.log(nearest); -await client.close() +await client.close(); ``` ### Notes diff --git a/skills/clickhouse-js-node-coding/reference/insert-columns.md b/skills/clickhouse-js-node-coding/reference/insert-columns.md index fd7cccd2..360a9b97 100644 --- a/skills/clickhouse-js-node-coding/reference/insert-columns.md +++ b/skills/clickhouse-js-node-coding/reference/insert-columns.md @@ -22,11 +22,11 @@ get their declared default. ```ts await client.insert({ - table: 'events', - columns: ['message'], // the rest of the events table columns get their DEFAULTs - format: 'JSONEachRow', - values: [{ message: 'foo' }], -}) + table: "events", + columns: ["message"], // the rest of the events table columns get their DEFAULTs + format: "JSONEachRow", + values: [{ message: "foo" }], +}); ``` ## Insert excluding columns @@ -36,11 +36,11 @@ should default but you want to name only the few to skip. ```ts await client.insert({ - table: 'events', - format: 'JSONEachRow', - values: [{ message: 'bar' }], - columns: { except: ['id'] }, -}) + table: "events", + format: "JSONEachRow", + values: [{ message: "bar" }], + columns: { except: ["id"] }, +}); ``` ## Tables with EPHEMERAL columns @@ -62,18 +62,18 @@ await client.command({ ENGINE MergeTree ORDER BY id `, -}) +}); await client.insert({ - table: 'events', - format: 'JSONEachRow', + table: "events", + format: "JSONEachRow", values: [ - { id: '42', message_default: 'foo' }, - { id: '144', message_default: 'bar' }, + { id: "42", message_default: "foo" }, + { id: "144", message_default: "bar" }, ], // Including the ephemeral column name triggers the DEFAULT expression - columns: ['id', 'message_default'], -}) + columns: ["id", "message_default"], +}); ``` ## Insert into a different database @@ -82,15 +82,15 @@ If the client's default `database` is not the target, qualify the table name with `db.table`: ```ts -const client = createClient({ database: 'system' }) +const client = createClient({ database: "system" }); -await client.command({ query: 'CREATE DATABASE IF NOT EXISTS analytics' }) +await client.command({ query: "CREATE DATABASE IF NOT EXISTS analytics" }); await client.insert({ - table: 'analytics.events', // fully qualified - format: 'JSONEachRow', - values: [{ id: 42, message: 'foo' }], -}) + table: "analytics.events", // fully qualified + format: "JSONEachRow", + values: [{ id: 42, message: "foo" }], +}); ``` There is no per-call `database` override on `insert()` / `query()` — qualify diff --git a/skills/clickhouse-js-node-coding/reference/insert-formats.md b/skills/clickhouse-js-node-coding/reference/insert-formats.md index 6afb596e..c5c053a2 100644 --- a/skills/clickhouse-js-node-coding/reference/insert-formats.md +++ b/skills/clickhouse-js-node-coding/reference/insert-formats.md @@ -27,20 +27,20 @@ When answering "what format/call should I use for an array of JS objects?": This is the right answer for ~90% of inserts. ```ts -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; -const client = createClient() +const client = createClient(); await client.insert({ - table: 'events', - format: 'JSONEachRow', + table: "events", + format: "JSONEachRow", values: [ - { id: 42, name: 'foo' }, - { id: 43, name: 'bar' }, + { id: 42, name: "foo" }, + { id: 43, name: "bar" }, ], -}) +}); -await client.close() +await client.close(); ``` The shape of `values` must match the chosen format. @@ -60,15 +60,15 @@ The shape of `values` must match the chosen format. ```ts await client.insert({ - table: 'events', - format: 'JSONCompactEachRowWithNamesAndTypes', + table: "events", + format: "JSONCompactEachRowWithNamesAndTypes", values: [ - ['id', 'name', 'sku'], - ['UInt32', 'String', 'Array(UInt32)'], - [11, 'foo', [1, 2, 3]], - [12, 'bar', [4, 5, 6]], + ["id", "name", "sku"], + ["UInt32", "String", "Array(UInt32)"], + [11, "foo", [1, 2, 3]], + [12, "bar", [4, 5, 6]], ], -}) +}); ``` These formats can be **streamed** — pass a Node stream of rows instead of an @@ -88,33 +88,33 @@ These cannot be streamed — the entire body is sent in one shot. | `JSONObjectEachRow` | `Record` (the record key labels each row but is not stored) | ```ts -import type { InputJSON, InputJSONObjectEachRow } from '@clickhouse/client' +import type { InputJSON, InputJSONObjectEachRow } from "@clickhouse/client"; -const meta: InputJSON['meta'] = [ - { name: 'id', type: 'UInt32' }, - { name: 'name', type: 'String' }, -] +const meta: InputJSON["meta"] = [ + { name: "id", type: "UInt32" }, + { name: "name", type: "String" }, +]; await client.insert({ - table: 'events', - format: 'JSONCompact', + table: "events", + format: "JSONCompact", values: { meta, data: [ - [19, 'foo'], - [20, 'bar'], + [19, "foo"], + [20, "bar"], ], }, -}) +}); await client.insert({ - table: 'events', - format: 'JSONObjectEachRow', + table: "events", + format: "JSONObjectEachRow", values: { - row_1: { id: 23, name: 'foo' }, - row_2: { id: 24, name: 'bar' }, + row_1: { id: 23, name: "foo" }, + row_2: { id: 24, name: "bar" }, } satisfies InputJSONObjectEachRow<{ id: number; name: string }>, -}) +}); ``` ## Quick chooser diff --git a/skills/clickhouse-js-node-coding/reference/insert-values.md b/skills/clickhouse-js-node-coding/reference/insert-values.md index e71258ed..2b9f038e 100644 --- a/skills/clickhouse-js-node-coding/reference/insert-values.md +++ b/skills/clickhouse-js-node-coding/reference/insert-values.md @@ -14,7 +14,7 @@ await client.command({ INSERT INTO target SELECT * FROM source `, -}) +}); ``` Use `command()` (not `insert()`) — there is no row payload to send. @@ -38,13 +38,13 @@ await client.command({ ) `, query_params: { - id: '00112233445566778899aabbccddeeff', - timestamp: '2026-05-06 12:34:56', - email: 'alice@example.com', - name: 'Alice', + id: "00112233445566778899aabbccddeeff", + timestamp: "2026-05-06 12:34:56", + email: "alice@example.com", + name: "Alice", }, clickhouse_settings: { wait_end_of_query: 1 }, -}) +}); ``` Do not build `VALUES` rows with string interpolation or manual escaping. If @@ -61,13 +61,13 @@ session-wide. ```ts await client.insert({ - table: 'events', - format: 'JSONEachRow', - values: [{ id: '42', dt: new Date() }], + table: "events", + format: "JSONEachRow", + values: [{ id: "42", dt: new Date() }], clickhouse_settings: { - date_time_input_format: 'best_effort', // default on the Cloud + date_time_input_format: "best_effort", // default on the Cloud }, -}) +}); ``` > JS `Date` objects do **not** work for the `Date` type (date-only) — pass @@ -97,22 +97,22 @@ await client.command({ ) ENGINE MergeTree ORDER BY id `, -}) +}); await client.insert({ - table: 'prices', - format: 'JSONEachRow', + table: "prices", + format: "JSONEachRow", values: [ { id: 1, - dec32: '1234567.89', - dec64: '123456789123456.789', - dec128: '1234567891234567891234567891.1234567891', + dec32: "1234567.89", + dec64: "123456789123456.789", + dec128: "1234567891234567891234567891.1234567891", dec256: - '12345678912345678912345678911234567891234567891234567891.12345678911234567891', + "12345678912345678912345678911234567891234567891234567891.12345678911234567891", }, ], -}) +}); ``` When reading them back, cast to string in the SELECT to avoid the same @@ -125,8 +125,8 @@ const rs = await client.query({ toString(dec128) AS decimal128 FROM prices `, - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); ``` ## Inserting a `UUID` into a `UInt128` column @@ -142,11 +142,11 @@ clause**. With the row-oriented JSON formats the client uses (e.g. always pass `UInt128` as a string: ```ts -import * as crypto from 'node:crypto' +import * as crypto from "node:crypto"; function uuidToUInt128(uuid: string): string { // 8-4-4-4-12 hex digits → 32 hex digits → BigInt → decimal string - return BigInt('0x' + uuid.replace(/-/g, '')).toString() + return BigInt("0x" + uuid.replace(/-/g, "")).toString(); } await client.command({ @@ -154,14 +154,14 @@ await client.command({ CREATE OR REPLACE TABLE events (id UInt128, description String) ENGINE MergeTree ORDER BY id `, -}) +}); -const uuid = crypto.randomUUID() +const uuid = crypto.randomUUID(); await client.insert({ - table: 'events', - format: 'JSONEachRow', - values: [{ id: uuidToUInt128(uuid), description: 'converted on the client' }], -}) + table: "events", + format: "JSONEachRow", + values: [{ id: uuidToUInt128(uuid), description: "converted on the client" }], +}); ``` `UInt128` values are also too wide for a JS `number` when reading back — cast @@ -181,15 +181,15 @@ await client.command({ ) ENGINE MergeTree ORDER BY id `, -}) +}); await client.insert({ - table: 'events', - format: 'JSONEachRow', - values: [{ id_uuid: uuid, description: 'populated via EPHEMERAL column' }], + table: "events", + format: "JSONEachRow", + values: [{ id_uuid: uuid, description: "populated via EPHEMERAL column" }], // The ephemeral column must be listed so the DEFAULT on `id` is evaluated. - columns: ['id_uuid', 'description'], -}) + columns: ["id_uuid", "description"], +}); ``` See `reference/insert-columns.md` for more on `EPHEMERAL` columns and why they diff --git a/skills/clickhouse-js-node-coding/reference/ping.md b/skills/clickhouse-js-node-coding/reference/ping.md index da35da98..761880a9 100644 --- a/skills/clickhouse-js-node-coding/reference/ping.md +++ b/skills/clickhouse-js-node-coding/reference/ping.md @@ -28,20 +28,20 @@ When answering "how do I health-check / readiness-probe ClickHouse?": ## Successful ping ```ts -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; const client = createClient({ url: process.env.CLICKHOUSE_URL, password: process.env.CLICKHOUSE_PASSWORD, -}) +}); -const pingResult = await client.ping() +const pingResult = await client.ping(); if (pingResult.success) { - console.info('ClickHouse is reachable') + console.info("ClickHouse is reachable"); } else { - console.error('Ping failed:', pingResult.error) + console.error("Ping failed:", pingResult.error); } -await client.close() +await client.close(); ``` Use `ping()` to: @@ -57,44 +57,44 @@ Use `ping()` to: `{ success: false, error: Error }`, so you can branch without `try/catch`: ```ts -import type { PingResult } from '@clickhouse/client' -import { createClient } from '@clickhouse/client' +import type { PingResult } from "@clickhouse/client"; +import { createClient } from "@clickhouse/client"; const client = createClient({ - url: 'http://localhost:8100', // non-existing host + url: "http://localhost:8100", // non-existing host request_timeout: 50, // keep failure fast -}) +}); -const pingResult = await client.ping() +const pingResult = await client.ping(); if (hasConnectionRefusedError(pingResult)) { - console.info('Connection refused, as expected') + console.info("Connection refused, as expected"); } else { - console.error('Ping expected ECONNREFUSED, got:', pingResult) + console.error("Ping expected ECONNREFUSED, got:", pingResult); } -await client.close() +await client.close(); function hasConnectionRefusedError( pingResult: PingResult, -): pingResult is PingResult & { error: { code: 'ECONNREFUSED' } } { +): pingResult is PingResult & { error: { code: "ECONNREFUSED" } } { return ( !pingResult.success && - 'code' in pingResult.error && - pingResult.error.code === 'ECONNREFUSED' - ) + "code" in pingResult.error && + pingResult.error.code === "ECONNREFUSED" + ); } ``` ## Mapping to an HTTP health endpoint ```ts -app.get('/healthz', async (_req, res) => { - const r = await client.ping() +app.get("/healthz", async (_req, res) => { + const r = await client.ping(); if (r.success) { - res.status(200).json({ ok: true }) + res.status(200).json({ ok: true }); } else { - res.status(503).json({ ok: false, error: String(r.error) }) + res.status(503).json({ ok: false, error: String(r.error) }); } -}) +}); ``` ## `ping()` vs `ping({ select: true })` @@ -107,7 +107,7 @@ pipeline) will still return `{ success: true }` from a plain `ping()`. Pass `{ select: true }` to run a lightweight `SELECT 1` instead: ```ts -const r = await client.ping({ select: true }) +const r = await client.ping({ select: true }); // success only if the server is reachable AND auth is correct AND it can run queries ``` diff --git a/skills/clickhouse-js-node-coding/reference/query-parameters.md b/skills/clickhouse-js-node-coding/reference/query-parameters.md index 8d5fe04b..213d61d6 100644 --- a/skills/clickhouse-js-node-coding/reference/query-parameters.md +++ b/skills/clickhouse-js-node-coding/reference/query-parameters.md @@ -26,10 +26,10 @@ ClickHouse uses `{name: Type}` placeholders — **not** `$1`, `?`, or `:name`. ```ts await client.query({ - query: 'SELECT plus({a: Int32}, {b: Int32})', - format: 'JSONEachRow', + query: "SELECT plus({a: Int32}, {b: Int32})", + format: "JSONEachRow", query_params: { a: 10, b: 20 }, -}) +}); ``` The `Type` must be a valid ClickHouse type (`Int32`, `String`, `Date`, @@ -41,16 +41,16 @@ Interpolating user input into the SQL string bypasses server-side escaping and opens the door to SQL injection: ```ts -const userId = req.params.id +const userId = req.params.id; // ❌ Dangerous — never do this with user-controlled values -await client.query({ query: `SELECT * FROM users WHERE id = ${userId}` }) +await client.query({ query: `SELECT * FROM users WHERE id = ${userId}` }); // ✓ Safe — parameterized await client.query({ - query: 'SELECT * FROM users WHERE id = {id: UInt32}', + query: "SELECT * FROM users WHERE id = {id: UInt32}", query_params: { id: userId }, -}) +}); ``` This is the most common mistake for users coming from PostgreSQL/MySQL. Call @@ -59,7 +59,7 @@ it out explicitly when the user shows template-literal interpolation. ## Common types ```ts -import { TupleParam } from '@clickhouse/client' +import { TupleParam } from "@clickhouse/client"; await client.query({ query: ` @@ -79,27 +79,27 @@ await client.query({ {var_ipv4: IPv4} AS var_ipv4, {var_null: Nullable(String)} AS var_null `, - format: 'JSONEachRow', + format: "JSONEachRow", query_params: { var_int: 10, - var_float: '10.557', - var_str: 'hello', + var_float: "10.557", + var_str: "hello", var_array: [42, 144], - var_tuple: new TupleParam([42, 'foo']), // >= 1.9.0 + var_tuple: new TupleParam([42, "foo"]), // >= 1.9.0 var_map: new Map([ - [42, ['a', 'b']], - [144, ['c', 'd']], + [42, ["a", "b"]], + [144, ["c", "d"]], ]), // >= 1.9.0 - var_date: '2022-01-01', - var_datetime: '2022-01-01 12:34:56', // or a Date - var_datetime64_3: '2022-01-01 12:34:56.789', // or a Date - var_datetime64_9: '2022-01-01 12:34:56.123456789', // string for ns precision - var_decimal: '123.45', // string to avoid precision loss - var_uuid: '01234567-89ab-cdef-0123-456789abcdef', - var_ipv4: '192.168.0.1', + var_date: "2022-01-01", + var_datetime: "2022-01-01 12:34:56", // or a Date + var_datetime64_3: "2022-01-01 12:34:56.789", // or a Date + var_datetime64_9: "2022-01-01 12:34:56.123456789", // string for ns precision + var_decimal: "123.45", // string to avoid precision loss + var_uuid: "01234567-89ab-cdef-0123-456789abcdef", + var_ipv4: "192.168.0.1", var_null: null, // fixed in 0.0.16 }, -}) +}); ``` ### Type-by-type tips @@ -131,14 +131,14 @@ await client.query({ 'foo_\\'_bar' = {single_quote: String} AS has_single_quote, 'foo_\\_bar' = {backslash: String} AS has_backslash `, - format: 'JSONEachRow', + format: "JSONEachRow", query_params: { - tab: 'foo_\t_bar', - newline: 'foo_\n_bar', + tab: "foo_\t_bar", + newline: "foo_\n_bar", single_quote: "foo_'_bar", - backslash: 'foo_\\_bar', + backslash: "foo_\\_bar", }, -}) +}); ``` ## Common pitfalls diff --git a/skills/clickhouse-js-node-coding/reference/select-formats.md b/skills/clickhouse-js-node-coding/reference/select-formats.md index 58d9da8c..e0fa1a5e 100644 --- a/skills/clickhouse-js-node-coding/reference/select-formats.md +++ b/skills/clickhouse-js-node-coding/reference/select-formats.md @@ -9,23 +9,23 @@ Right answer for ~90% of selects when the result fits in memory. ```ts -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; interface Row { - number: string + number: string; } -const client = createClient() +const client = createClient(); const rows = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 5', - format: 'JSONEachRow', -}) -const result = await rows.json() // Row[] -result.forEach((r) => console.log(r)) + query: "SELECT number FROM system.numbers LIMIT 5", + format: "JSONEachRow", +}); +const result = await rows.json(); // Row[] +result.forEach((r) => console.log(r)); // { number: '0' } // { number: '1' } // ... -await client.close() +await client.close(); ``` `UInt64`/`Int64` and other 64-bit integers are returned as **strings** @@ -41,16 +41,16 @@ Use `JSON` (or `JSONCompact`) when you need ClickHouse's response envelope `ResponseJSON`: ```ts -import { createClient, type ResponseJSON } from '@clickhouse/client' +import { createClient, type ResponseJSON } from "@clickhouse/client"; -const client = createClient() +const client = createClient(); const rows = await client.query({ - query: 'SELECT number FROM system.numbers LIMIT 2', - format: 'JSON', -}) -const result = await rows.json>() -console.info(result.meta, result.data, result.rows, result.statistics) -await client.close() + query: "SELECT number FROM system.numbers LIMIT 2", + format: "JSON", +}); +const result = await rows.json>(); +console.info(result.meta, result.data, result.rows, result.statistics); +await client.close(); ``` > `JSON`, `JSONCompact`, `JSONStrings`, `JSONCompactStrings`, @@ -64,10 +64,10 @@ Use `.text()` (not `.json()`) for raw textual formats: ```ts const rs = await client.query({ - query: 'SELECT number, number * 2 AS doubled FROM system.numbers LIMIT 3', - format: 'CSVWithNames', -}) -console.log(await rs.text()) + query: "SELECT number, number * 2 AS doubled FROM system.numbers LIMIT 3", + format: "CSVWithNames", +}); +console.log(await rs.text()); ``` Streaming raw text/Parquet line-by-line belongs in diff --git a/skills/clickhouse-js-node-coding/reference/sessions.md b/skills/clickhouse-js-node-coding/reference/sessions.md index 2dda65fb..b2f9283e 100644 --- a/skills/clickhouse-js-node-coding/reference/sessions.md +++ b/skills/clickhouse-js-node-coding/reference/sessions.md @@ -65,30 +65,30 @@ script, a background job, a single user's session that you've already manually serialized in the code). ```ts -import { createClient } from '@clickhouse/client' -import * as crypto from 'node:crypto' +import { createClient } from "@clickhouse/client"; +import * as crypto from "node:crypto"; const client = createClient({ session_id: crypto.randomUUID(), max_open_connections: 1, // safeguard against concurrent-session errors -}) +}); await client.command({ - query: 'CREATE TEMPORARY TABLE temporary_example (i Int32)', -}) + query: "CREATE TEMPORARY TABLE temporary_example (i Int32)", +}); await client.insert({ - table: 'temporary_example', + table: "temporary_example", values: [{ i: 42 }, { i: 144 }], - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); const rs = await client.query({ - query: 'SELECT * FROM temporary_example', - format: 'JSONEachRow', -}) -console.info(await rs.json()) -await client.close() + query: "SELECT * FROM temporary_example", + format: "JSONEachRow", +}); +console.info(await rs.json()); +await client.close(); ``` ## Session-level `SET` commands @@ -97,37 +97,37 @@ await client.close() client, every subsequent call inherits the change. ```ts -import { createClient } from '@clickhouse/client' -import * as crypto from 'node:crypto' +import { createClient } from "@clickhouse/client"; +import * as crypto from "node:crypto"; const client = createClient({ session_id: crypto.randomUUID(), max_open_connections: 1, // safe-guard against concurrent-session errors -}) +}); await client.command({ - query: 'SET output_format_json_quote_64bit_integers = 0', + query: "SET output_format_json_quote_64bit_integers = 0", clickhouse_settings: { wait_end_of_query: 1 }, // ack before next call -}) +}); const rs1 = await client.query({ - query: 'SELECT toInt64(42)', - format: 'JSONEachRow', -}) + query: "SELECT toInt64(42)", + format: "JSONEachRow", +}); // → 64-bit integers come back as numbers in this query await client.command({ - query: 'SET output_format_json_quote_64bit_integers = 1', + query: "SET output_format_json_quote_64bit_integers = 1", clickhouse_settings: { wait_end_of_query: 1 }, -}) +}); const rs2 = await client.query({ - query: 'SELECT toInt64(144)', - format: 'JSONEachRow', -}) + query: "SELECT toInt64(144)", + format: "JSONEachRow", +}); // → 64-bit integers come back as strings again -await client.close() +await client.close(); ``` > **`wait_end_of_query: 1` matters here.** Without it, a `SET` on one diff --git a/skills/clickhouse-js-node-troubleshooting/reference/compression.md b/skills/clickhouse-js-node-troubleshooting/reference/compression.md index 2fffa331..9a35101a 100644 --- a/skills/clickhouse-js-node-troubleshooting/reference/compression.md +++ b/skills/clickhouse-js-node-troubleshooting/reference/compression.md @@ -5,13 +5,13 @@ Both request and response compression are supported. Only **GZIP** is supported (via zlib). ```js -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; const client = createClient({ compression: { response: true, request: true, }, -}) +}); ``` ## Compression enabled but getting an error? diff --git a/skills/clickhouse-js-node-troubleshooting/reference/data-types.md b/skills/clickhouse-js-node-troubleshooting/reference/data-types.md index f959c31b..40fd1904 100644 --- a/skills/clickhouse-js-node-troubleshooting/reference/data-types.md +++ b/skills/clickhouse-js-node-troubleshooting/reference/data-types.md @@ -10,10 +10,10 @@ To receive them as numbers (use with caution — precision loss possible): ```js const resultSet = await client.query({ - query: 'SELECT toUInt64(9007199254740993)', - format: 'JSONEachRow', + query: "SELECT toUInt64(9007199254740993)", + format: "JSONEachRow", clickhouse_settings: { output_format_json_quote_64bit_integers: 0 }, -}) +}); ``` > **Tip (`>= 1.15.0`):** BigInt values are now supported in query parameters, so you can safely pass large integers as bind params without string workarounds. @@ -30,18 +30,18 @@ const resultSet = await client.query({ SELECT toString(my_decimal) AS my_decimal FROM my_table `, - format: 'JSONEachRow', -}) + format: "JSONEachRow", +}); ``` When inserting, always use the string representation to avoid precision loss: ```js await client.insert({ - table: 'my_table', - values: [{ dec64: '123456789123456.789' }], - format: 'JSONEachRow', -}) + table: "my_table", + values: [{ dec64: "123456789123456.789" }], + format: "JSONEachRow", +}); ``` ## Inserting a UUID into a `UInt128` column fails (`CANNOT_PARSE_INPUT_ASSERTION_FAILED`) @@ -55,19 +55,19 @@ Fix it with one of two patterns: **Pattern 1 — convert the UUID on the client and send it as a decimal string** (recommended). A JS `number` cannot hold 128 bits without precision loss, so always pass `UInt128` as a string: ```js -import * as crypto from 'node:crypto' +import * as crypto from "node:crypto"; function uuidToUInt128(uuid) { // 8-4-4-4-12 hex digits → 32 hex digits → BigInt → decimal string - return BigInt('0x' + uuid.replace(/-/g, '')).toString() + return BigInt("0x" + uuid.replace(/-/g, "")).toString(); } -const uuid = crypto.randomUUID() +const uuid = crypto.randomUUID(); await client.insert({ - table: 'events', - format: 'JSONEachRow', - values: [{ id: uuidToUInt128(uuid), description: 'converted on the client' }], -}) + table: "events", + format: "JSONEachRow", + values: [{ id: uuidToUInt128(uuid), description: "converted on the client" }], +}); ``` Read `UInt128` back with `toString(id)` in the `SELECT` to avoid the same precision loss. @@ -77,11 +77,11 @@ Read `UInt128` back with `toString(id)` in the `SELECT` to avoid the same precis ```js // CREATE TABLE events (id UInt128 DEFAULT id_uuid, id_uuid UUID EPHEMERAL, description String) ... await client.insert({ - table: 'events', - format: 'JSONEachRow', - values: [{ id_uuid: uuid, description: 'populated via EPHEMERAL column' }], - columns: ['id_uuid', 'description'], -}) + table: "events", + format: "JSONEachRow", + values: [{ id_uuid: uuid, description: "populated via EPHEMERAL column" }], + columns: ["id_uuid", "description"], +}); ``` ## Format Selection Quick Reference @@ -106,8 +106,8 @@ await client.insert({ - `DateTime` / `DateTime64` columns accept strings **or** JS `Date` objects. To use `Date` objects, set: ```js -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; const client = createClient({ - clickhouse_settings: { date_time_input_format: 'best_effort' }, -}) + clickhouse_settings: { date_time_input_format: "best_effort" }, +}); ``` diff --git a/skills/clickhouse-js-node-troubleshooting/reference/logging.md b/skills/clickhouse-js-node-troubleshooting/reference/logging.md index 8eead255..06a7280e 100644 --- a/skills/clickhouse-js-node-troubleshooting/reference/logging.md +++ b/skills/clickhouse-js-node-troubleshooting/reference/logging.md @@ -5,20 +5,20 @@ The default log level is **OFF** (for `< 1.18.1`) or **WARN** (for `>= 1.18.1`). Enable it explicitly: ```js -import { ClickHouseLogLevel, createClient } from '@clickhouse/client' +import { ClickHouseLogLevel, createClient } from "@clickhouse/client"; const client = createClient({ log: { level: ClickHouseLogLevel.DEBUG, // TRACE | DEBUG | INFO | WARN | ERROR }, -}) +}); ``` To use a custom logger (e.g., to pipe to your observability stack), implement the `Logger` interface: ```ts -import { ClickHouseLogLevel, createClient } from '@clickhouse/client' -import type { Logger } from '@clickhouse/client' +import { ClickHouseLogLevel, createClient } from "@clickhouse/client"; +import type { Logger } from "@clickhouse/client"; class MyLogger implements Logger { debug({ module, message, args }) { @@ -40,5 +40,5 @@ class MyLogger implements Logger { const client = createClient({ log: { LoggerClass: MyLogger, level: ClickHouseLogLevel.INFO }, -}) +}); ``` diff --git a/skills/clickhouse-js-node-troubleshooting/reference/proxy-pathname.md b/skills/clickhouse-js-node-troubleshooting/reference/proxy-pathname.md index 4f3c44d7..b50e75a8 100644 --- a/skills/clickhouse-js-node-troubleshooting/reference/proxy-pathname.md +++ b/skills/clickhouse-js-node-troubleshooting/reference/proxy-pathname.md @@ -9,12 +9,12 @@ **Fix:** Use the `pathname` option separately: ```js -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; const client = createClient({ - url: 'http://proxy:8123', - pathname: '/clickhouse_server', // leading slash optional; multiple segments supported -}) + url: "http://proxy:8123", + pathname: "/clickhouse_server", // leading slash optional; multiple segments supported +}); ``` For proxies that require custom auth headers: @@ -22,11 +22,11 @@ For proxies that require custom auth headers: > **Requires:** `>= 1.0.0` (`http_headers` config option; replaces the deprecated `additional_headers` from `>= 0.2.9`). Per-request `http_headers` overrides are available since `>= 1.11.0`. ```js -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; const client = createClient({ http_headers: { - 'My-Auth-Header': 'secret', + "My-Auth-Header": "secret", }, -}) +}); ``` diff --git a/skills/clickhouse-js-node-troubleshooting/reference/query-format-clause.md b/skills/clickhouse-js-node-troubleshooting/reference/query-format-clause.md index 5667cb4d..57b89cbd 100644 --- a/skills/clickhouse-js-node-troubleshooting/reference/query-format-clause.md +++ b/skills/clickhouse-js-node-troubleshooting/reference/query-format-clause.md @@ -6,7 +6,7 @@ ```js // What you write: -await client.query({ query: 'SELECT 1', format: 'JSONEachRow' }) +await client.query({ query: "SELECT 1", format: "JSONEachRow" }); // What the client actually sends: // SELECT 1 @@ -19,10 +19,10 @@ If the `query` string already contains a `FORMAT` clause, the client still appen ```js // ❌ Wrong — ends up as `... FORMAT CSV FORMAT JSON` → syntax error -await client.query({ query: 'SELECT 1 FORMAT CSV' }) +await client.query({ query: "SELECT 1 FORMAT CSV" }); // ✓ Correct — let the client append FORMAT via the option -await client.query({ query: 'SELECT 1', format: 'CSV' }) +await client.query({ query: "SELECT 1", format: "CSV" }); ``` If you genuinely need to write the full SQL yourself (including the `FORMAT` clause), or you're running a statement where the appended `FORMAT` suffix is not supported, use `client.exec()` instead of `client.query()`. Use `client.insert()` for data insertion and `client.command()` for DDLs. @@ -33,15 +33,15 @@ Some statements are not parsed by the server with a trailing `FORMAT` clause, so ```js // ❌ Fails — query() appends `FORMAT JSON`, which the short SHOW POLICIES syntax rejects -await client.query({ query: 'SHOW POLICIES', format: 'JSON' }) -await client.query({ query: 'SHOW ROW POLICIES', format: 'JSON' }) +await client.query({ query: "SHOW POLICIES", format: "JSON" }); +await client.query({ query: "SHOW ROW POLICIES", format: "JSON" }); ``` **Fix:** use the full syntax `SHOW POLICIES ON *` (or `SHOW POLICIES ON db.table`), which the parser accepts together with the appended `FORMAT`: ```js // ✓ Works — full syntax accepts the appended FORMAT clause -await client.query({ query: 'SHOW POLICIES ON *', format: 'JSON' }) +await client.query({ query: "SHOW POLICIES ON *", format: "JSON" }); ``` Alternatively, run the short statement through `client.exec()`, where you control the full SQL and no `FORMAT` suffix is appended. diff --git a/skills/clickhouse-js-node-troubleshooting/reference/query-params.md b/skills/clickhouse-js-node-troubleshooting/reference/query-params.md index 022dbb8b..4bc27755 100644 --- a/skills/clickhouse-js-node-troubleshooting/reference/query-params.md +++ b/skills/clickhouse-js-node-troubleshooting/reference/query-params.md @@ -6,10 +6,10 @@ Use the `{name: type}` syntax in the query string and pass values via `query_par ```js await client.query({ - query: 'SELECT plus({val1: Int32}, {val2: Int32})', - format: 'CSV', + query: "SELECT plus({val1: Int32}, {val2: Int32})", + format: "CSV", query_params: { val1: 10, val2: 20 }, -}) +}); ``` ## Never use template literals for user values @@ -18,14 +18,14 @@ When `$1`/`?` don't work, a common instinct is to interpolate values directly wi ```js // ❌ Dangerous — never do this with user-controlled values -const userId = req.params.id -await client.query({ query: `SELECT * FROM users WHERE id = ${userId}` }) +const userId = req.params.id; +await client.query({ query: `SELECT * FROM users WHERE id = ${userId}` }); // ✓ Safe — parameterized await client.query({ - query: 'SELECT * FROM users WHERE id = {id: UInt32}', + query: "SELECT * FROM users WHERE id = {id: UInt32}", query_params: { id: userId }, -}) +}); ``` Always bring this up when answering query-params questions, especially when the user is coming from another database (PostgreSQL, MySQL, etc.) — they're the most likely to reach for template literals as a fallback. @@ -37,27 +37,27 @@ The ClickHouse JS client uses ClickHouse's native `{name: type}` syntax — not ```js // ❌ Wrong — these don't work await client.query({ - query: 'SELECT * FROM t WHERE id = $1', - query: 'SELECT * FROM t WHERE id = ?', - query: 'SELECT * FROM t WHERE id = :id', + query: "SELECT * FROM t WHERE id = $1", + query: "SELECT * FROM t WHERE id = ?", + query: "SELECT * FROM t WHERE id = :id", query_params: { id: 42 }, -}) +}); // ✓ Correct await client.query({ - query: 'SELECT * FROM t WHERE id = {id: UInt32}', + query: "SELECT * FROM t WHERE id = {id: UInt32}", query_params: { id: 42 }, -}) +}); ``` ## Array parameters ```js await client.query({ - query: 'SELECT * FROM t WHERE id IN {ids: Array(UInt32)}', - format: 'JSONEachRow', + query: "SELECT * FROM t WHERE id IN {ids: Array(UInt32)}", + format: "JSONEachRow", query_params: { ids: [1, 2, 3] }, -}) +}); ``` ## Tuple parameters (`>= 1.9.0`) @@ -65,17 +65,17 @@ await client.query({ Use the `TupleParam` wrapper to pass a tuple: ```js -import { TupleParam, createClient } from '@clickhouse/client' +import { TupleParam, createClient } from "@clickhouse/client"; const client = createClient({ - url: 'http://localhost:8123', -}) + url: "http://localhost:8123", +}); await client.query({ - query: 'SELECT {t: Tuple(UInt32, String)}', - format: 'JSONEachRow', - query_params: { t: new TupleParam([42, 'hello']) }, -}) + query: "SELECT {t: Tuple(UInt32, String)}", + format: "JSONEachRow", + query_params: { t: new TupleParam([42, "hello"]) }, +}); ``` ## Map parameters (`>= 1.9.0`) @@ -84,10 +84,10 @@ Pass a JS `Map` directly: ```js await client.query({ - query: 'SELECT {m: Map(String, UInt32)}', - format: 'JSONEachRow', - query_params: { m: new Map([['key', 1]]) }, -}) + query: "SELECT {m: Map(String, UInt32)}", + format: "JSONEachRow", + query_params: { m: new Map([["key", 1]]) }, +}); ``` ## NULL parameters @@ -96,8 +96,8 @@ Pass `null` directly — binding fixed in `0.0.16`: ```js await client.query({ - query: 'SELECT {val: Nullable(String)}', - format: 'JSONEachRow', + query: "SELECT {val: Nullable(String)}", + format: "JSONEachRow", query_params: { val: null }, -}) +}); ``` diff --git a/skills/clickhouse-js-node-troubleshooting/reference/readonly-users.md b/skills/clickhouse-js-node-troubleshooting/reference/readonly-users.md index d660af95..636a4231 100644 --- a/skills/clickhouse-js-node-troubleshooting/reference/readonly-users.md +++ b/skills/clickhouse-js-node-troubleshooting/reference/readonly-users.md @@ -9,17 +9,17 @@ **Fix:** Remove response compression for read-only users: ```js -import { createClient } from '@clickhouse/client' +import { createClient } from "@clickhouse/client"; // Don't do this with a readonly=1 user: // compression: { response: true } const client = createClient({ - username: 'my_readonly_user', - password: '...', + username: "my_readonly_user", + password: "...", // compression omitted, or explicitly set to false compression: { response: false, }, -}) +}); ``` diff --git a/skills/clickhouse-js-node-troubleshooting/reference/socket-hangup.md b/skills/clickhouse-js-node-troubleshooting/reference/socket-hangup.md index 3cbbcd13..94d51bec 100644 --- a/skills/clickhouse-js-node-troubleshooting/reference/socket-hangup.md +++ b/skills/clickhouse-js-node-troubleshooting/reference/socket-hangup.md @@ -16,11 +16,11 @@ > **Requires:** `>= 0.2.0` (logging support with `log.level` config option). In `>= 1.18.1`, the default log level changed from `OFF` to `WARN`, so this step may already be active. In `>= 1.18.2`, the client auto-emits a WARN log with Keep-Alive troubleshooting hints when an `ECONNRESET` is detected. In `>= 1.12.0`, a warning is logged when a socket is closed without fully consuming the stream. ```js -import { createClient, ClickHouseLogLevel } from '@clickhouse/client' +import { createClient, ClickHouseLogLevel } from "@clickhouse/client"; const client = createClient({ log: { level: ClickHouseLogLevel.WARN }, -}) +}); ``` Look for log lines about unconsumed or dangling streams — these are a common hidden cause. A **dangling stream** is a query response stream that was never fully consumed or explicitly closed with `ResultSet.close()`. Because the Node.js client reuses sockets (Keep-Alive), leaving a stream open corrupts the socket and causes the _next_ request to fail with `ECONNRESET`. Errors on **every request** strongly suggest dangling streams rather than a Keep-Alive timeout mismatch. @@ -29,33 +29,33 @@ Look for log lines about unconsumed or dangling streams — these are a common h ```js // ❌ Wrong — result stream never consumed; socket is left open -const resultSet = await client.query({ query: 'SELECT ...' }) +const resultSet = await client.query({ query: "SELECT ..." }); // result is abandoned without calling .json(), .text(), .stream(), or .close() // ❌ Wrong — stream created but not fully piped/iterated const resultSet = await client.query({ - query: 'SELECT ...', - format: 'JSONEachRow', -}) -const stream = resultSet.stream() + query: "SELECT ...", + format: "JSONEachRow", +}); +const stream = resultSet.stream(); // stream is never iterated and resultSet is never closed // ✓ Correct — consume via .json() -const resultSet = await client.query({ query: 'SELECT ...' }) -const data = await resultSet.json() +const resultSet = await client.query({ query: "SELECT ..." }); +const data = await resultSet.json(); // ✓ Correct — consume via async iteration const resultSet = await client.query({ - query: 'SELECT ...', - format: 'JSONEachRow', -}) + query: "SELECT ...", + format: "JSONEachRow", +}); for await (const rows of resultSet.stream()) { // process rows } // ✓ Correct — explicitly close; this destroys the underlying socket immediately -const resultSet = await client.query({ query: 'SELECT ...' }) -resultSet.close() +const resultSet = await client.query({ query: "SELECT ..." }); +resultSet.close(); ``` ## Step 2 — Check your ESLint setup @@ -86,7 +86,7 @@ const client = createClient({ keep_alive: { idle_socket_ttl: 9000, // stay ~500ms below the server's timeout }, -}) +}); ``` > ⚠️ If you still get errors after increasing, **lower** the value, not raise it. @@ -104,9 +104,9 @@ const client = createClient({ request_timeout: 400_000, // e.g. 400s for long queries clickhouse_settings: { send_progress_in_http_headers: 1, - http_headers_progress_interval_ms: '110000', // string — UInt64 type; set ~10s below LB idle timeout + http_headers_progress_interval_ms: "110000", // string — UInt64 type; set ~10s below LB idle timeout }, -}) +}); ``` ### ⚠️ Critical: 16 KB Node.js Header Size Limit @@ -140,9 +140,9 @@ const client = createClient({ max_response_headers_size: 65536, // 64 KB; lifts the per-request header cap clickhouse_settings: { send_progress_in_http_headers: 1, - http_headers_progress_interval_ms: '110000', + http_headers_progress_interval_ms: "110000", }, -}) +}); ``` ```bash @@ -200,5 +200,5 @@ Adds overhead (new TCP connection per request) but eliminates all Keep-Alive iss ```js const client = createClient({ keep_alive: { enabled: false }, -}) +}); ``` diff --git a/skills/clickhouse-js-node-troubleshooting/reference/tls.md b/skills/clickhouse-js-node-troubleshooting/reference/tls.md index 53163b4a..4b43e6ad 100644 --- a/skills/clickhouse-js-node-troubleshooting/reference/tls.md +++ b/skills/clickhouse-js-node-troubleshooting/reference/tls.md @@ -5,34 +5,34 @@ ## Basic TLS (CA certificate only) ```js -import fs from 'fs' -import { createClient } from '@clickhouse/client' +import fs from "fs"; +import { createClient } from "@clickhouse/client"; const client = createClient({ - url: 'https://:', - username: '', - password: '', + url: "https://:", + username: "", + password: "", tls: { - ca_cert: fs.readFileSync('certs/CA.pem'), + ca_cert: fs.readFileSync("certs/CA.pem"), }, -}) +}); ``` ## Mutual TLS (client certificate + key) ```js -import fs from 'fs' -import { createClient } from '@clickhouse/client' +import fs from "fs"; +import { createClient } from "@clickhouse/client"; const client = createClient({ - url: 'https://:', - username: '', + url: "https://:", + username: "", tls: { - ca_cert: fs.readFileSync('certs/CA.pem'), - cert: fs.readFileSync('certs/client.crt'), - key: fs.readFileSync('certs/client.key'), + ca_cert: fs.readFileSync("certs/CA.pem"), + cert: fs.readFileSync("certs/client.crt"), + key: fs.readFileSync("certs/client.key"), }, -}) +}); ``` > **Tip (`>= 1.2.0`):** If you need a custom HTTP(S) agent, use the `http_agent` option. Only set `set_basic_auth_header: false` if you must avoid sending the basic-auth `Authorization` header (for example, due to a header conflict); in that case, provide alternative auth headers such as `X-ClickHouse-User` / `X-ClickHouse-Key` via `http_headers`. @@ -59,29 +59,29 @@ The server uses a self-signed cert (the certificate is its own CA). Options in o ```js tls: { - ca_cert: fs.readFileSync('certs/server.crt') + ca_cert: fs.readFileSync("certs/server.crt"); } ``` 2. For development only — disable verification via a custom agent (`>= 1.2.0`): ```js - import https from 'https' - import { createClient } from '@clickhouse/client' + import https from "https"; + import { createClient } from "@clickhouse/client"; const client = createClient({ - url: 'https://:', - username: '', - password: '', + url: "https://:", + username: "", + password: "", http_agent: new https.Agent({ rejectUnauthorized: false }), // Optional: only disable the basic-auth Authorization header if you need to // provide alternative auth headers instead. set_basic_auth_header: false, http_headers: { - 'X-ClickHouse-User': '', - 'X-ClickHouse-Key': '', + "X-ClickHouse-User": "", + "X-ClickHouse-Key": "", }, - }) + }); ``` > ⚠️ Never use `rejectUnauthorized: false` in production — it disables all certificate verification. diff --git a/tests/clickhouse-test-runner/__tests__/args.test.ts b/tests/clickhouse-test-runner/__tests__/args.test.ts index f5187c50..b6ba2325 100644 --- a/tests/clickhouse-test-runner/__tests__/args.test.ts +++ b/tests/clickhouse-test-runner/__tests__/args.test.ts @@ -1,152 +1,152 @@ -import { describe, expect, it } from 'vitest' -import { parseArgs } from '../src/args.js' -import { SERVER_SETTINGS } from '../src/settings.js' - -describe('parseArgs', () => { - it('returns defaults for an empty argv', () => { - const args = parseArgs([]) - expect(args.host).toBe('localhost') - expect(args.port).toBe(8123) - expect(args.user).toBe('default') - expect(args.password).toBe('') - expect(args.database).toBe('default') - expect(args.multiquery).toBe(false) - expect(args.secure).toBe(false) - expect(args.query).toBeNull() - expect(args.help).toBe(false) - expect(args.logComment).toBeNull() - expect(args.sendLogsLevel).toBeNull() - expect(args.maxInsertThreads).toBeNull() - expect(args.serverSettings).toEqual({}) - }) - - it('parses long-form options with separate values', () => { +import { describe, expect, it } from "vitest"; +import { parseArgs } from "../src/args.js"; +import { SERVER_SETTINGS } from "../src/settings.js"; + +describe("parseArgs", () => { + it("returns defaults for an empty argv", () => { + const args = parseArgs([]); + expect(args.host).toBe("localhost"); + expect(args.port).toBe(8123); + expect(args.user).toBe("default"); + expect(args.password).toBe(""); + expect(args.database).toBe("default"); + expect(args.multiquery).toBe(false); + expect(args.secure).toBe(false); + expect(args.query).toBeNull(); + expect(args.help).toBe(false); + expect(args.logComment).toBeNull(); + expect(args.sendLogsLevel).toBeNull(); + expect(args.maxInsertThreads).toBeNull(); + expect(args.serverSettings).toEqual({}); + }); + + it("parses long-form options with separate values", () => { const args = parseArgs([ - '--host', - 'other', - '--port', - '9000', - '--user', - 'u', - '--password', - 'p', - '--database', - 'd', - ]) - expect(args.host).toBe('other') - expect(args.port).toBe(9000) - expect(args.user).toBe('u') - expect(args.password).toBe('p') - expect(args.database).toBe('d') - }) - - it('parses long-form options with = form', () => { + "--host", + "other", + "--port", + "9000", + "--user", + "u", + "--password", + "p", + "--database", + "d", + ]); + expect(args.host).toBe("other"); + expect(args.port).toBe(9000); + expect(args.user).toBe("u"); + expect(args.password).toBe("p"); + expect(args.database).toBe("d"); + }); + + it("parses long-form options with = form", () => { const args = parseArgs([ - '--host=other', - '--port=9000', - '--user=u', - '--password=p', - '--database=d', - ]) - expect(args.host).toBe('other') - expect(args.port).toBe(9000) - expect(args.user).toBe('u') - expect(args.password).toBe('p') - expect(args.database).toBe('d') - }) - - it('parses short options including -q and -s', () => { + "--host=other", + "--port=9000", + "--user=u", + "--password=p", + "--database=d", + ]); + expect(args.host).toBe("other"); + expect(args.port).toBe(9000); + expect(args.user).toBe("u"); + expect(args.password).toBe("p"); + expect(args.database).toBe("d"); + }); + + it("parses short options including -q and -s", () => { const args = parseArgs([ - '-h', - 'other', - '-u', - 'u', - '-d', - 'd', - '-q', - 'SELECT 1', - '-s', - ]) - expect(args.host).toBe('other') - expect(args.user).toBe('u') - expect(args.database).toBe('d') - expect(args.query).toBe('SELECT 1') - expect(args.secure).toBe(true) - }) - - it('accepts both --multiquery and --multi-query', () => { - expect(parseArgs(['--multiquery']).multiquery).toBe(true) - expect(parseArgs(['--multi-query']).multiquery).toBe(true) - }) - - it('silently accepts --multiline and -n', () => { - expect(() => parseArgs(['--multiline'])).not.toThrow() - expect(() => parseArgs(['-n'])).not.toThrow() - const a = parseArgs(['--multiline', '-n']) - expect(a.serverSettings).toEqual({}) - expect(a.query).toBeNull() - }) - - it('sets help=true for --help', () => { - expect(parseArgs(['--help']).help).toBe(true) - }) - - it('accepts both underscore and dash forms for log_comment', () => { - expect(parseArgs(['--log_comment', 'foo']).logComment).toBe('foo') - expect(parseArgs(['--log-comment', 'foo']).logComment).toBe('foo') - }) - - it('accepts both forms for send_logs_level', () => { - expect(parseArgs(['--send_logs_level', 'trace']).sendLogsLevel).toBe( - 'trace', - ) - expect(parseArgs(['--send-logs-level', 'trace']).sendLogsLevel).toBe( - 'trace', - ) - }) - - it('accepts both forms for max_insert_threads', () => { - expect(parseArgs(['--max_insert_threads', '8']).maxInsertThreads).toBe('8') - expect(parseArgs(['--max-insert-threads', '8']).maxInsertThreads).toBe('8') - }) - - it('routes --max_insert_threads=8 into the explicit field, not serverSettings', () => { - const a = parseArgs(['--max_insert_threads=8']) - expect(a.maxInsertThreads).toBe('8') - expect(a.serverSettings).toEqual({}) - }) - - it('exports a non-empty SERVER_SETTINGS allowlist', () => { - expect(SERVER_SETTINGS).toBeInstanceOf(Set) - expect(SERVER_SETTINGS.size).toBeGreaterThan(0) - }) - - it('silently drops unknown options', () => { - const a = parseArgs(['--brand-new-thing=xyz']) - expect(a.serverSettings).toEqual({}) - expect(a.query).toBeNull() - }) - - it('forwards --max_threads=4 as a server setting', () => { - const a = parseArgs(['--max_threads=4']) - expect(a.serverSettings).toEqual({ max_threads: '4' }) - expect(a.query).toBeNull() - }) - - it('silently drops a CLIENT_ONLY setting like --max_block_size=1024', () => { - const a = parseArgs(['--max_block_size=1024']) - expect(a.serverSettings).toEqual({}) - expect(a.query).toBeNull() - }) - - it('does not place --unknown_thing=42 into serverSettings or throw', () => { - expect(() => parseArgs(['--unknown_thing=42'])).not.toThrow() - const a = parseArgs(['--unknown_thing=42']) - expect(a.serverSettings).toEqual({}) - }) - - it('treats --max-threads=4 the same as --max_threads=4 (forwarded as server setting)', () => { - const a = parseArgs(['--max-threads=4']) - expect(a.serverSettings).toEqual({ max_threads: '4' }) - }) -}) + "-h", + "other", + "-u", + "u", + "-d", + "d", + "-q", + "SELECT 1", + "-s", + ]); + expect(args.host).toBe("other"); + expect(args.user).toBe("u"); + expect(args.database).toBe("d"); + expect(args.query).toBe("SELECT 1"); + expect(args.secure).toBe(true); + }); + + it("accepts both --multiquery and --multi-query", () => { + expect(parseArgs(["--multiquery"]).multiquery).toBe(true); + expect(parseArgs(["--multi-query"]).multiquery).toBe(true); + }); + + it("silently accepts --multiline and -n", () => { + expect(() => parseArgs(["--multiline"])).not.toThrow(); + expect(() => parseArgs(["-n"])).not.toThrow(); + const a = parseArgs(["--multiline", "-n"]); + expect(a.serverSettings).toEqual({}); + expect(a.query).toBeNull(); + }); + + it("sets help=true for --help", () => { + expect(parseArgs(["--help"]).help).toBe(true); + }); + + it("accepts both underscore and dash forms for log_comment", () => { + expect(parseArgs(["--log_comment", "foo"]).logComment).toBe("foo"); + expect(parseArgs(["--log-comment", "foo"]).logComment).toBe("foo"); + }); + + it("accepts both forms for send_logs_level", () => { + expect(parseArgs(["--send_logs_level", "trace"]).sendLogsLevel).toBe( + "trace", + ); + expect(parseArgs(["--send-logs-level", "trace"]).sendLogsLevel).toBe( + "trace", + ); + }); + + it("accepts both forms for max_insert_threads", () => { + expect(parseArgs(["--max_insert_threads", "8"]).maxInsertThreads).toBe("8"); + expect(parseArgs(["--max-insert-threads", "8"]).maxInsertThreads).toBe("8"); + }); + + it("routes --max_insert_threads=8 into the explicit field, not serverSettings", () => { + const a = parseArgs(["--max_insert_threads=8"]); + expect(a.maxInsertThreads).toBe("8"); + expect(a.serverSettings).toEqual({}); + }); + + it("exports a non-empty SERVER_SETTINGS allowlist", () => { + expect(SERVER_SETTINGS).toBeInstanceOf(Set); + expect(SERVER_SETTINGS.size).toBeGreaterThan(0); + }); + + it("silently drops unknown options", () => { + const a = parseArgs(["--brand-new-thing=xyz"]); + expect(a.serverSettings).toEqual({}); + expect(a.query).toBeNull(); + }); + + it("forwards --max_threads=4 as a server setting", () => { + const a = parseArgs(["--max_threads=4"]); + expect(a.serverSettings).toEqual({ max_threads: "4" }); + expect(a.query).toBeNull(); + }); + + it("silently drops a CLIENT_ONLY setting like --max_block_size=1024", () => { + const a = parseArgs(["--max_block_size=1024"]); + expect(a.serverSettings).toEqual({}); + expect(a.query).toBeNull(); + }); + + it("does not place --unknown_thing=42 into serverSettings or throw", () => { + expect(() => parseArgs(["--unknown_thing=42"])).not.toThrow(); + const a = parseArgs(["--unknown_thing=42"]); + expect(a.serverSettings).toEqual({}); + }); + + it("treats --max-threads=4 the same as --max_threads=4 (forwarded as server setting)", () => { + const a = parseArgs(["--max-threads=4"]); + expect(a.serverSettings).toEqual({ max_threads: "4" }); + }); +}); diff --git a/tests/clickhouse-test-runner/__tests__/extract-from-config.test.ts b/tests/clickhouse-test-runner/__tests__/extract-from-config.test.ts index be4dcb3b..c9e110dc 100644 --- a/tests/clickhouse-test-runner/__tests__/extract-from-config.test.ts +++ b/tests/clickhouse-test-runner/__tests__/extract-from-config.test.ts @@ -1,45 +1,45 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' -import { handleExtractFromConfig } from '../src/extract-from-config.js' +import { afterEach, describe, expect, it, vi } from "vitest"; +import { handleExtractFromConfig } from "../src/extract-from-config.js"; -describe('handleExtractFromConfig', () => { +describe("handleExtractFromConfig", () => { afterEach(() => { - vi.restoreAllMocks() - }) + vi.restoreAllMocks(); + }); function captureStdout(): { writes: string[] } { - const writes: string[] = [] - vi.spyOn(process.stdout, 'write').mockImplementation((( + const writes: string[] = []; + vi.spyOn(process.stdout, "write").mockImplementation((( chunk: string | Uint8Array, ): boolean => { writes.push( - typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8'), - ) - return true - }) as typeof process.stdout.write) - return { writes } + typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"), + ); + return true; + }) as typeof process.stdout.write); + return { writes }; } - it('writes 127.0.0.1\\n for --key listen_host', () => { - const { writes } = captureStdout() - handleExtractFromConfig(['--key', 'listen_host']) - expect(writes.join('')).toBe('127.0.0.1\n') - }) + it("writes 127.0.0.1\\n for --key listen_host", () => { + const { writes } = captureStdout(); + handleExtractFromConfig(["--key", "listen_host"]); + expect(writes.join("")).toBe("127.0.0.1\n"); + }); - it('writes 127.0.0.1\\n for --key=listen_host', () => { - const { writes } = captureStdout() - handleExtractFromConfig(['--key=listen_host']) - expect(writes.join('')).toBe('127.0.0.1\n') - }) + it("writes 127.0.0.1\\n for --key=listen_host", () => { + const { writes } = captureStdout(); + handleExtractFromConfig(["--key=listen_host"]); + expect(writes.join("")).toBe("127.0.0.1\n"); + }); - it('writes nothing for --key foo', () => { - const { writes } = captureStdout() - handleExtractFromConfig(['--key', 'foo']) - expect(writes.join('')).toBe('') - }) + it("writes nothing for --key foo", () => { + const { writes } = captureStdout(); + handleExtractFromConfig(["--key", "foo"]); + expect(writes.join("")).toBe(""); + }); - it('writes nothing for empty args', () => { - const { writes } = captureStdout() - handleExtractFromConfig([]) - expect(writes.join('')).toBe('') - }) -}) + it("writes nothing for empty args", () => { + const { writes } = captureStdout(); + handleExtractFromConfig([]); + expect(writes.join("")).toBe(""); + }); +}); diff --git a/tests/clickhouse-test-runner/__tests__/log.test.ts b/tests/clickhouse-test-runner/__tests__/log.test.ts index aad77886..05bc82be 100644 --- a/tests/clickhouse-test-runner/__tests__/log.test.ts +++ b/tests/clickhouse-test-runner/__tests__/log.test.ts @@ -1,54 +1,54 @@ -import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { mkdtempSync, readFileSync, rmSync, existsSync, unlinkSync, -} from 'node:fs' -import { EOL } from 'node:os' -import path from 'node:path' -import { appendLog, safeForLog } from '../src/log.js' +} from "node:fs"; +import { EOL } from "node:os"; +import path from "node:path"; +import { appendLog, safeForLog } from "../src/log.js"; -describe('appendLog', () => { - let tmpDir: string +describe("appendLog", () => { + let tmpDir: string; beforeAll(() => { - tmpDir = mkdtempSync(path.join(process.cwd(), '.test-tmp-cli-log-')) - }) + tmpDir = mkdtempSync(path.join(process.cwd(), ".test-tmp-cli-log-")); + }); afterAll(() => { - rmSync(tmpDir, { recursive: true, force: true }) - }) - - it('appends a line followed by EOL to the given file', () => { - const file = path.join(tmpDir, 'log.txt') - appendLog(file, 'hello') - expect(readFileSync(file, 'utf8')).toBe('hello' + EOL) - }) - - it('does not throw when the path is unwritable', () => { - const fallback = path.resolve(process.cwd(), 'clickhouse-client-cli.log') - const fallbackPreExisting = existsSync(fallback) + rmSync(tmpDir, { recursive: true, force: true }); + }); + + it("appends a line followed by EOL to the given file", () => { + const file = path.join(tmpDir, "log.txt"); + appendLog(file, "hello"); + expect(readFileSync(file, "utf8")).toBe("hello" + EOL); + }); + + it("does not throw when the path is unwritable", () => { + const fallback = path.resolve(process.cwd(), "clickhouse-client-cli.log"); + const fallbackPreExisting = existsSync(fallback); expect(() => - appendLog('/dev/null/non_writable_xyz/log', 'should-not-throw'), - ).not.toThrow() + appendLog("/dev/null/non_writable_xyz/log", "should-not-throw"), + ).not.toThrow(); if (!fallbackPreExisting && existsSync(fallback)) { - unlinkSync(fallback) + unlinkSync(fallback); } - }) -}) - -describe('safeForLog', () => { - it('returns for null', () => { - expect(safeForLog(null)).toBe('') - }) - - it('returns for undefined', () => { - expect(safeForLog(undefined)).toBe('') - }) - - it('returns the original string otherwise', () => { - expect(safeForLog('x')).toBe('x') - }) -}) + }); +}); + +describe("safeForLog", () => { + it("returns for null", () => { + expect(safeForLog(null)).toBe(""); + }); + + it("returns for undefined", () => { + expect(safeForLog(undefined)).toBe(""); + }); + + it("returns the original string otherwise", () => { + expect(safeForLog("x")).toBe("x"); + }); +}); diff --git a/tests/clickhouse-test-runner/__tests__/split-queries.test.ts b/tests/clickhouse-test-runner/__tests__/split-queries.test.ts index 189cfbb4..c0053786 100644 --- a/tests/clickhouse-test-runner/__tests__/split-queries.test.ts +++ b/tests/clickhouse-test-runner/__tests__/split-queries.test.ts @@ -1,77 +1,80 @@ -import { describe, expect, it } from 'vitest' -import { splitQueries } from '../src/split-queries.js' +import { describe, expect, it } from "vitest"; +import { splitQueries } from "../src/split-queries.js"; -describe('splitQueries', () => { - it('splits two simple statements', () => { - expect(splitQueries('SELECT 1; SELECT 2')).toEqual(['SELECT 1', 'SELECT 2']) - }) +describe("splitQueries", () => { + it("splits two simple statements", () => { + expect(splitQueries("SELECT 1; SELECT 2")).toEqual([ + "SELECT 1", + "SELECT 2", + ]); + }); - it('ignores a trailing semicolon', () => { - expect(splitQueries('SELECT 1;')).toEqual(['SELECT 1']) - }) + it("ignores a trailing semicolon", () => { + expect(splitQueries("SELECT 1;")).toEqual(["SELECT 1"]); + }); - it('handles a single statement without trailing semicolon', () => { - expect(splitQueries('SELECT 1')).toEqual(['SELECT 1']) - }) + it("handles a single statement without trailing semicolon", () => { + expect(splitQueries("SELECT 1")).toEqual(["SELECT 1"]); + }); - it('returns [] for an empty string', () => { - expect(splitQueries('')).toEqual([]) - }) + it("returns [] for an empty string", () => { + expect(splitQueries("")).toEqual([]); + }); - it('returns [] for whitespace-only input', () => { - expect(splitQueries(' \n\t ')).toEqual([]) - }) + it("returns [] for whitespace-only input", () => { + expect(splitQueries(" \n\t ")).toEqual([]); + }); - it('does not split on semicolons inside single quotes', () => { + it("does not split on semicolons inside single quotes", () => { expect(splitQueries("SELECT ';'; SELECT 2")).toEqual([ "SELECT ';'", - 'SELECT 2', - ]) - }) + "SELECT 2", + ]); + }); - it('does not split on semicolons inside double quotes', () => { + it("does not split on semicolons inside double quotes", () => { expect(splitQueries('SELECT ";"; SELECT 2')).toEqual([ 'SELECT ";"', - 'SELECT 2', - ]) - }) + "SELECT 2", + ]); + }); - it('does not split on semicolons inside backticks', () => { - expect(splitQueries('SELECT `a;b`; SELECT 2')).toEqual([ - 'SELECT `a;b`', - 'SELECT 2', - ]) - }) + it("does not split on semicolons inside backticks", () => { + expect(splitQueries("SELECT `a;b`; SELECT 2")).toEqual([ + "SELECT `a;b`", + "SELECT 2", + ]); + }); - it('handles escaped quote inside a single-quoted string', () => { + it("handles escaped quote inside a single-quoted string", () => { expect(splitQueries("SELECT '\\''; SELECT 2")).toEqual([ "SELECT '\\''", - 'SELECT 2', - ]) - }) + "SELECT 2", + ]); + }); - it('trims whitespace around statements', () => { - expect(splitQueries(' SELECT 1 ; SELECT 2 ')).toEqual([ - 'SELECT 1', - 'SELECT 2', - ]) - }) + it("trims whitespace around statements", () => { + expect(splitQueries(" SELECT 1 ; SELECT 2 ")).toEqual([ + "SELECT 1", + "SELECT 2", + ]); + }); - it('ignores apostrophes and semicolons inside line comments', () => { + 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" + "-- 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', - ]) - }) + "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" + 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', - ]) - }) -}) + "SELECT 2", + ]); + }); +}); diff --git a/tests/clickhouse-test-runner/eslint.config.mjs b/tests/clickhouse-test-runner/eslint.config.mjs index 7f83c97b..6c435b4b 100644 --- a/tests/clickhouse-test-runner/eslint.config.mjs +++ b/tests/clickhouse-test-runner/eslint.config.mjs @@ -1,10 +1,10 @@ -import js from '@eslint/js' -import { defineConfig } from 'eslint/config' -import tseslint from 'typescript-eslint' +import js from "@eslint/js"; +import { defineConfig } from "eslint/config"; +import tseslint from "typescript-eslint"; import { typescriptEslintConfig, testFilesOverrides, -} from '../../eslint.config.base.mjs' +} from "../../eslint.config.base.mjs"; export default defineConfig( js.configs.recommended, @@ -13,6 +13,6 @@ export default defineConfig( typescriptEslintConfig(import.meta.dirname), testFilesOverrides(), { - ignores: ['dist', 'node_modules', 'bin', 'eslint.config.mjs'], + ignores: ["dist", "node_modules", "bin", "eslint.config.mjs"], }, -) +); diff --git a/tests/clickhouse-test-runner/src/args.ts b/tests/clickhouse-test-runner/src/args.ts index 3d8e7728..386ba7b7 100644 --- a/tests/clickhouse-test-runner/src/args.ts +++ b/tests/clickhouse-test-runner/src/args.ts @@ -1,104 +1,104 @@ -import { classifySetting } from './settings.js' +import { classifySetting } from "./settings.js"; export interface ParsedArgs { - host: string - port: number - user: string - password: string - database: string - secure: boolean - query: string | null - logComment: string | null - sendLogsLevel: string | null - maxInsertThreads: string | null - multiquery: boolean - help: boolean - serverSettings: Record - rawArgv: string[] + host: string; + port: number; + user: string; + password: string; + database: string; + secure: boolean; + query: string | null; + logComment: string | null; + sendLogsLevel: string | null; + maxInsertThreads: string | null; + multiquery: boolean; + help: boolean; + serverSettings: Record; + rawArgv: string[]; } interface OptionSpec { - long: string - short?: string - hasArg: boolean + long: string; + short?: string; + hasArg: boolean; } const BASE_OPTIONS: readonly OptionSpec[] = [ - { long: 'host', short: 'h', hasArg: true }, - { long: 'port', hasArg: true }, - { long: 'user', short: 'u', hasArg: true }, - { long: 'password', hasArg: true }, - { long: 'database', short: 'd', hasArg: true }, - { long: 'query', short: 'q', hasArg: true }, - { long: 'log_comment', hasArg: true }, - { long: 'log-comment', hasArg: true }, - { long: 'send_logs_level', hasArg: true }, - { long: 'send-logs-level', hasArg: true }, - { long: 'max_insert_threads', hasArg: true }, - { long: 'max-insert-threads', hasArg: true }, - { long: 'secure', short: 's', hasArg: false }, - { long: 'multiline', short: 'n', hasArg: false }, - { long: 'multiquery', hasArg: false }, - { long: 'multi-query', hasArg: false }, - { long: 'help', hasArg: false }, -] + { long: "host", short: "h", hasArg: true }, + { long: "port", hasArg: true }, + { long: "user", short: "u", hasArg: true }, + { long: "password", hasArg: true }, + { long: "database", short: "d", hasArg: true }, + { long: "query", short: "q", hasArg: true }, + { long: "log_comment", hasArg: true }, + { long: "log-comment", hasArg: true }, + { long: "send_logs_level", hasArg: true }, + { long: "send-logs-level", hasArg: true }, + { long: "max_insert_threads", hasArg: true }, + { long: "max-insert-threads", hasArg: true }, + { long: "secure", short: "s", hasArg: false }, + { long: "multiline", short: "n", hasArg: false }, + { long: "multiquery", hasArg: false }, + { long: "multi-query", hasArg: false }, + { long: "help", hasArg: false }, +]; export const KNOWN_LONG_OPTIONS: ReadonlySet = new Set( BASE_OPTIONS.map((o) => o.long), -) +); const SHORT_TO_LONG: ReadonlyMap = new Map( BASE_OPTIONS.filter((o) => o.short !== undefined).map((o) => [ o.short as string, o.long, ]), -) +); const LONG_HAS_ARG: ReadonlyMap = new Map( BASE_OPTIONS.map((o) => [o.long, o.hasArg]), -) +); const USAGE_TEXT = [ - 'usage: clickhouse-client [options]', - '', - 'Known server settings are forwarded to ClickHouse.', - 'Client-only and unknown settings are accepted but not sent to server.', - 'If --query is not specified, the query is read from stdin.', - '', - 'Options:', - ' -h, --host HOST Server host (default: localhost)', - ' --port PORT HTTP port (default: 8123)', - ' -u, --user USER Username (default: default)', - ' --password PASSWORD Password (default: empty)', - ' -d, --database DB Database (default: default)', - ' -q, --query SQL SQL query to execute', - ' --log_comment VALUE Comment for query_log records', - ' --send_logs_level VALUE Server log level to send with result', - ' --max_insert_threads VALUE', - ' Max insert threads setting', - ' -s, --secure Use HTTPS', - ' -n, --multiline (ignored, accepted for compatibility)', + "usage: clickhouse-client [options]", + "", + "Known server settings are forwarded to ClickHouse.", + "Client-only and unknown settings are accepted but not sent to server.", + "If --query is not specified, the query is read from stdin.", + "", + "Options:", + " -h, --host HOST Server host (default: localhost)", + " --port PORT HTTP port (default: 8123)", + " -u, --user USER Username (default: default)", + " --password PASSWORD Password (default: empty)", + " -d, --database DB Database (default: default)", + " -q, --query SQL SQL query to execute", + " --log_comment VALUE Comment for query_log records", + " --send_logs_level VALUE Server log level to send with result", + " --max_insert_threads VALUE", + " Max insert threads setting", + " -s, --secure Use HTTPS", + " -n, --multiline (ignored, accepted for compatibility)", ' --multiquery Execute multiple ";"-separated queries', - ' --help Print this help', - '', - 'Environment variables:', - ' CLICKHOUSE_CLIENT_CLI_LOG Path to log file for troubleshooting', - '', -].join('\n') + " --help Print this help", + "", + "Environment variables:", + " CLICKHOUSE_CLIENT_CLI_LOG Path to log file for troubleshooting", + "", +].join("\n"); export function printUsage( stream: NodeJS.WritableStream = process.stdout, ): void { - stream.write(USAGE_TEXT) + stream.write(USAGE_TEXT); } export function parseArgs(argv: string[]): ParsedArgs { const parsed: ParsedArgs = { - host: 'localhost', + host: "localhost", port: 8123, - user: 'default', - password: '', - database: 'default', + user: "default", + password: "", + database: "default", secure: false, query: null, logComment: null, @@ -108,149 +108,149 @@ export function parseArgs(argv: string[]): ParsedArgs { help: false, serverSettings: {}, rawArgv: [...argv], - } + }; // Map of canonical long option -> value (string) or true for flags. - const seen = new Map() + const seen = new Map(); - let i = 0 + let i = 0; while (i < argv.length) { - const arg = argv[i] + const arg = argv[i]; if (arg === undefined) { - i++ - continue + i++; + continue; } - if (arg === '--') { - break + if (arg === "--") { + break; } - if (arg.startsWith('--')) { - const eq = arg.indexOf('=') - const name = eq >= 0 ? arg.substring(2, eq) : arg.substring(2) - const inlineValue = eq >= 0 ? arg.substring(eq + 1) : undefined + if (arg.startsWith("--")) { + const eq = arg.indexOf("="); + const name = eq >= 0 ? arg.substring(2, eq) : arg.substring(2); + const inlineValue = eq >= 0 ? arg.substring(eq + 1) : undefined; if (name.length === 0) { - i++ - continue + i++; + continue; } if (LONG_HAS_ARG.has(name)) { - const hasArg = LONG_HAS_ARG.get(name) === true + const hasArg = LONG_HAS_ARG.get(name) === true; if (!hasArg) { - seen.set(name, true) - i++ - continue + seen.set(name, true); + i++; + continue; } - let value: string + let value: string; if (inlineValue !== undefined) { - value = inlineValue + value = inlineValue; } else { - const next = argv[i + 1] + const next = argv[i + 1]; if (next === undefined) { // Missing required arg: skip silently to mirror lenient behavior. - i++ - continue + i++; + continue; } - value = next - i++ + value = next; + i++; } - seen.set(name, value) - i++ - continue + seen.set(name, value); + i++; + continue; } // Dynamic / unknown long option. Optional arg. - let value: string | undefined = inlineValue + let value: string | undefined = inlineValue; if (value === undefined) { - const next = argv[i + 1] + const next = argv[i + 1]; if (next !== undefined) { const isNegativeNumber = - /^-(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/.test(next) + /^-(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/.test(next); const isKnownLongOption = (() => { - if (!next.startsWith('--')) return false - const nextEq = next.indexOf('=') + if (!next.startsWith("--")) return false; + const nextEq = next.indexOf("="); const nextName = - nextEq >= 0 ? next.substring(2, nextEq) : next.substring(2) - return nextName.length > 0 && LONG_HAS_ARG.has(nextName) - })() + nextEq >= 0 ? next.substring(2, nextEq) : next.substring(2); + return nextName.length > 0 && LONG_HAS_ARG.has(nextName); + })(); - if (!next.startsWith('-') || isNegativeNumber || !isKnownLongOption) { - value = next - i++ + if (!next.startsWith("-") || isNegativeNumber || !isKnownLongOption) { + value = next; + i++; } } } - const settingName = name.split('-').join('_') - if (classifySetting(settingName) === 'server') { - parsed.serverSettings[settingName] = value ?? '1' + const settingName = name.split("-").join("_"); + if (classifySetting(settingName) === "server") { + parsed.serverSettings[settingName] = value ?? "1"; } // CLIENT_ONLY / UNKNOWN: silently dropped. - i++ - continue + i++; + continue; } - if (arg.startsWith('-') && arg.length > 1) { + if (arg.startsWith("-") && arg.length > 1) { // Short option (single-char). We do not bundle. - const shortName = arg.substring(1) - const longName = SHORT_TO_LONG.get(shortName) + const shortName = arg.substring(1); + const longName = SHORT_TO_LONG.get(shortName); if (longName === undefined) { - i++ - continue + i++; + continue; } - const hasArg = LONG_HAS_ARG.get(longName) === true + const hasArg = LONG_HAS_ARG.get(longName) === true; if (!hasArg) { - seen.set(longName, true) - i++ - continue + seen.set(longName, true); + i++; + continue; } - const next = argv[i + 1] + const next = argv[i + 1]; if (next === undefined) { - i++ - continue + i++; + continue; } - seen.set(longName, next) - i += 2 - continue + seen.set(longName, next); + i += 2; + continue; } // Positional argument: ignored. - i++ + i++; } const firstNonNull = (...names: string[]): string | null => { for (const n of names) { - const v = seen.get(n) - if (typeof v === 'string') { - return v + const v = seen.get(n); + if (typeof v === "string") { + return v; } } - return null - } + return null; + }; - const hostVal = firstNonNull('host') - if (hostVal !== null) parsed.host = hostVal - const portVal = firstNonNull('port') + const hostVal = firstNonNull("host"); + if (hostVal !== null) parsed.host = hostVal; + const portVal = firstNonNull("port"); if (portVal !== null) { - const n = Number.parseInt(portVal, 10) - if (!Number.isNaN(n)) parsed.port = n + const n = Number.parseInt(portVal, 10); + if (!Number.isNaN(n)) parsed.port = n; } - const userVal = firstNonNull('user') - if (userVal !== null) parsed.user = userVal - const passwordVal = firstNonNull('password') - if (passwordVal !== null) parsed.password = passwordVal - const databaseVal = firstNonNull('database') - if (databaseVal !== null) parsed.database = databaseVal - parsed.query = firstNonNull('query') - parsed.logComment = firstNonNull('log_comment', 'log-comment') - parsed.sendLogsLevel = firstNonNull('send_logs_level', 'send-logs-level') + const userVal = firstNonNull("user"); + if (userVal !== null) parsed.user = userVal; + const passwordVal = firstNonNull("password"); + if (passwordVal !== null) parsed.password = passwordVal; + const databaseVal = firstNonNull("database"); + if (databaseVal !== null) parsed.database = databaseVal; + parsed.query = firstNonNull("query"); + parsed.logComment = firstNonNull("log_comment", "log-comment"); + parsed.sendLogsLevel = firstNonNull("send_logs_level", "send-logs-level"); parsed.maxInsertThreads = firstNonNull( - 'max_insert_threads', - 'max-insert-threads', - ) - parsed.secure = seen.get('secure') === true + "max_insert_threads", + "max-insert-threads", + ); + parsed.secure = seen.get("secure") === true; parsed.multiquery = - seen.get('multiquery') === true || seen.get('multi-query') === true - parsed.help = seen.get('help') === true + seen.get("multiquery") === true || seen.get("multi-query") === true; + parsed.help = seen.get("help") === true; - return parsed + return parsed; } diff --git a/tests/clickhouse-test-runner/src/backends/client.ts b/tests/clickhouse-test-runner/src/backends/client.ts index 40016f97..d30941db 100644 --- a/tests/clickhouse-test-runner/src/backends/client.ts +++ b/tests/clickhouse-test-runner/src/backends/client.ts @@ -1,38 +1,38 @@ -import { randomUUID } from 'node:crypto' -import { createClient } from '@clickhouse/client' -import type { ParsedArgs } from '../args.js' -import { appendLog } from '../log.js' +import { randomUUID } from "node:crypto"; +import { createClient } from "@clickhouse/client"; +import type { ParsedArgs } from "../args.js"; +import { appendLog } from "../log.js"; export interface BackendOptions { - args: ParsedArgs - queries: string[] - logPath: string + args: ParsedArgs; + queries: string[]; + logPath: string; } function buildClickHouseSettings( args: ParsedArgs, ): Record { - const settings: Record = {} - settings['default_format'] = 'TabSeparated' + const settings: Record = {}; + settings["default_format"] = "TabSeparated"; if (args.logComment !== null && args.logComment.length > 0) { - settings['log_comment'] = args.logComment + settings["log_comment"] = args.logComment; } if (args.sendLogsLevel !== null && args.sendLogsLevel.length > 0) { - settings['send_logs_level'] = args.sendLogsLevel + settings["send_logs_level"] = args.sendLogsLevel; } if (args.maxInsertThreads !== null && args.maxInsertThreads.length > 0) { - settings['max_insert_threads'] = args.maxInsertThreads + settings["max_insert_threads"] = args.maxInsertThreads; } for (const [k, v] of Object.entries(args.serverSettings)) { - settings[k] = v + settings[k] = v; } - return settings + return settings; } export async function executeWithClient(opts: BackendOptions): Promise { - const { args, queries, logPath } = opts - const proto = args.secure ? 'https' : 'http' - const url = `${proto}://${args.host}:${args.port}` + const { args, queries, logPath } = opts; + const proto = args.secure ? "https" : "http"; + const url = `${proto}://${args.host}:${args.port}`; // Use a dedicated per-invocation session_id so that settings applied via // `SET ...` in one statement persist for subsequent statements within the // same .sql script. Without a session, every `client.exec(...)` call is an @@ -40,34 +40,34 @@ export async function executeWithClient(opts: BackendOptions): Promise { // breaks upstream tests that rely on patterns like // SET allow_deprecated_syntax_for_merge_tree = 1; // CREATE TABLE ... ENGINE = MergeTree(d, k, 8192); - const sessionId = `clickhouse-js-test-runner-${randomUUID()}` - appendLog(logPath, 'session_id=' + sessionId) + const sessionId = `clickhouse-js-test-runner-${randomUUID()}`; + appendLog(logPath, "session_id=" + sessionId); const client = createClient({ url, username: args.user, password: args.password, database: args.database, session_id: sessionId, - }) + }); - const clickhouse_settings = buildClickHouseSettings(args) + const clickhouse_settings = buildClickHouseSettings(args); try { for (const q of queries) { - appendLog(logPath, 'executing_query=' + q) + appendLog(logPath, "executing_query=" + q); const result = await client.exec({ query: q, clickhouse_settings, - }) + }); for await (const chunk of result.stream) { - process.stdout.write(chunk) + process.stdout.write(chunk); } } } catch (err) { - const msg = err instanceof Error ? err.message : String(err) - appendLog(logPath, 'error=' + msg) - throw err + const msg = err instanceof Error ? err.message : String(err); + appendLog(logPath, "error=" + msg); + throw err; } finally { - await client.close() + await client.close(); } } diff --git a/tests/clickhouse-test-runner/src/extract-from-config.ts b/tests/clickhouse-test-runner/src/extract-from-config.ts index 4a1d65ce..578571f9 100644 --- a/tests/clickhouse-test-runner/src/extract-from-config.ts +++ b/tests/clickhouse-test-runner/src/extract-from-config.ts @@ -1,27 +1,27 @@ export function handleExtractFromConfig(args: string[]): void { - let key: string | null = null + let key: string | null = null; for (let i = 0; i < args.length; i++) { - const arg = args[i] - if (arg === undefined) continue + const arg = args[i]; + if (arg === undefined) continue; - if (arg.startsWith('--key=')) { - key = arg.substring('--key='.length) - continue + if (arg.startsWith("--key=")) { + key = arg.substring("--key=".length); + continue; } - if (arg === '--key') { - const next = args[i + 1] + if (arg === "--key") { + const next = args[i + 1]; if (next !== undefined) { - key = next - i++ + key = next; + i++; } - continue + continue; } } - if (key === 'listen_host') { - process.stdout.write('127.0.0.1\n') + if (key === "listen_host") { + process.stdout.write("127.0.0.1\n"); } - process.exitCode = 0 + process.exitCode = 0; } diff --git a/tests/clickhouse-test-runner/src/log.ts b/tests/clickhouse-test-runner/src/log.ts index ed347c58..e8a3a0ec 100644 --- a/tests/clickhouse-test-runner/src/log.ts +++ b/tests/clickhouse-test-runner/src/log.ts @@ -1,42 +1,42 @@ -import { appendFileSync, mkdirSync } from 'node:fs' -import { dirname, resolve } from 'node:path' -import { EOL } from 'node:os' +import { appendFileSync, mkdirSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { EOL } from "node:os"; -const DEFAULT_LOG_PATH = '/tmp/clickhouse-client-cli.log' -const FALLBACK_LOG_FILENAME = 'clickhouse-client-cli.log' +const DEFAULT_LOG_PATH = "/tmp/clickhouse-client-cli.log"; +const FALLBACK_LOG_FILENAME = "clickhouse-client-cli.log"; export function resolveLogPath(): string { - const fromEnv = process.env['CLICKHOUSE_CLIENT_CLI_LOG'] + const fromEnv = process.env["CLICKHOUSE_CLIENT_CLI_LOG"]; if (fromEnv !== undefined && fromEnv.trim().length > 0) { - return fromEnv + return fromEnv; } - return DEFAULT_LOG_PATH + return DEFAULT_LOG_PATH; } function tryAppend(path: string, payload: string): boolean { try { - const parent = dirname(path) + const parent = dirname(path); if (parent.length > 0) { - mkdirSync(parent, { recursive: true }) + mkdirSync(parent, { recursive: true }); } - appendFileSync(path, payload, 'utf8') - return true + appendFileSync(path, payload, "utf8"); + return true; } catch { - return false + return false; } } export function appendLog(path: string, line: string): void { - const payload = line + EOL + const payload = line + EOL; if (tryAppend(path, payload)) { - return + return; } - const fallback = resolve(process.cwd(), FALLBACK_LOG_FILENAME) + const fallback = resolve(process.cwd(), FALLBACK_LOG_FILENAME); if (fallback !== path) { - tryAppend(fallback, payload) + tryAppend(fallback, payload); } } export function safeForLog(value: string | null | undefined): string { - return value === null || value === undefined ? '' : value + return value === null || value === undefined ? "" : value; } diff --git a/tests/clickhouse-test-runner/src/main.ts b/tests/clickhouse-test-runner/src/main.ts index 2f6ac381..7c208f49 100644 --- a/tests/clickhouse-test-runner/src/main.ts +++ b/tests/clickhouse-test-runner/src/main.ts @@ -1,86 +1,86 @@ #!/usr/bin/env node -import { readFileSync } from 'node:fs' -import { parseArgs, printUsage } from './args.js' -import { appendLog, resolveLogPath, safeForLog } from './log.js' -import { splitQueries } from './split-queries.js' -import { handleExtractFromConfig } from './extract-from-config.js' -import { executeWithClient } from './backends/client.js' +import { readFileSync } from "node:fs"; +import { parseArgs, printUsage } from "./args.js"; +import { appendLog, resolveLogPath, safeForLog } from "./log.js"; +import { splitQueries } from "./split-queries.js"; +import { handleExtractFromConfig } from "./extract-from-config.js"; +import { executeWithClient } from "./backends/client.js"; async function main(): Promise { - const argv = process.argv.slice(2) + const argv = process.argv.slice(2); - if (argv[0] === 'extract-from-config') { - handleExtractFromConfig(argv.slice(1)) - return + if (argv[0] === "extract-from-config") { + handleExtractFromConfig(argv.slice(1)); + return; } - const logPath = resolveLogPath() - appendLog(logPath, '=== clickhouse-client invocation ===') - appendLog(logPath, 'timestamp=' + new Date().toISOString()) + const logPath = resolveLogPath(); + appendLog(logPath, "=== clickhouse-client invocation ==="); + appendLog(logPath, "timestamp=" + new Date().toISOString()); - let args + let args; try { - args = parseArgs(argv) + args = parseArgs(argv); } catch (err) { - const msg = err instanceof Error ? err.message : String(err) - process.stderr.write('Error: ' + msg + '\n') - printUsage(process.stderr) - process.exitCode = 1 - return + const msg = err instanceof Error ? err.message : String(err); + process.stderr.write("Error: " + msg + "\n"); + printUsage(process.stderr); + process.exitCode = 1; + return; } if (args.help) { - printUsage(process.stdout) - return + printUsage(process.stdout); + return; } - let query: string | null = args.query + let query: string | null = args.query; if (query === null) { try { - query = readFileSync(0, 'utf8') + query = readFileSync(0, "utf8"); } catch { - query = null + query = null; } } if (query === null || query.trim().length === 0) { process.stderr.write( - 'No query provided. Use --query or pipe SQL via stdin.\n', - ) - process.exitCode = 1 - return + "No query provided. Use --query or pipe SQL via stdin.\n", + ); + process.exitCode = 1; + return; } - const queries = args.multiquery ? splitQueries(query) : [query.trim()] + const queries = args.multiquery ? splitQueries(query) : [query.trim()]; if (queries.length === 0) { process.stderr.write( - 'No query provided. Use --query or pipe SQL via stdin.\n', - ) - process.exitCode = 1 - return + "No query provided. Use --query or pipe SQL via stdin.\n", + ); + process.exitCode = 1; + return; } - appendLog(logPath, 'database=' + args.database) - appendLog(logPath, 'user=' + args.user) - appendLog(logPath, 'secure=' + String(args.secure)) - appendLog(logPath, 'multiquery=' + String(args.multiquery)) - appendLog(logPath, 'log_comment=' + safeForLog(args.logComment)) - appendLog(logPath, 'send_logs_level=' + safeForLog(args.sendLogsLevel)) - appendLog(logPath, 'max_insert_threads=' + safeForLog(args.maxInsertThreads)) - appendLog(logPath, 'server_settings=' + JSON.stringify(args.serverSettings)) - appendLog(logPath, 'queries_count=' + String(queries.length)) + appendLog(logPath, "database=" + args.database); + appendLog(logPath, "user=" + args.user); + appendLog(logPath, "secure=" + String(args.secure)); + appendLog(logPath, "multiquery=" + String(args.multiquery)); + appendLog(logPath, "log_comment=" + safeForLog(args.logComment)); + appendLog(logPath, "send_logs_level=" + safeForLog(args.sendLogsLevel)); + appendLog(logPath, "max_insert_threads=" + safeForLog(args.maxInsertThreads)); + appendLog(logPath, "server_settings=" + JSON.stringify(args.serverSettings)); + appendLog(logPath, "queries_count=" + String(queries.length)); for (const q of queries) { - appendLog(logPath, 'query=' + q) + appendLog(logPath, "query=" + q); } try { - await executeWithClient({ args, queries, logPath }) + await executeWithClient({ args, queries, logPath }); } catch (err) { - const msg = err instanceof Error ? err.message : String(err) - appendLog(logPath, 'error=' + msg) - process.stderr.write('Error: ' + msg + '\n') - process.exitCode = 1 + const msg = err instanceof Error ? err.message : String(err); + appendLog(logPath, "error=" + msg); + process.stderr.write("Error: " + msg + "\n"); + process.exitCode = 1; } } -void main() +void main(); diff --git a/tests/clickhouse-test-runner/src/settings.ts b/tests/clickhouse-test-runner/src/settings.ts index b00ade99..ce3cfb45 100644 --- a/tests/clickhouse-test-runner/src/settings.ts +++ b/tests/clickhouse-test-runner/src/settings.ts @@ -1,162 +1,162 @@ -export type SettingScope = 'server' | 'client_only' | 'unknown' +export type SettingScope = "server" | "client_only" | "unknown"; export const SERVER_SETTINGS: ReadonlySet = new Set([ - 'max_insert_threads', - 'max_threads', - 'send_logs_level', -]) + "max_insert_threads", + "max_threads", + "send_logs_level", +]); export const CLIENT_ONLY_SETTINGS: ReadonlySet = new Set([ - 'group_by_two_level_threshold', - 'group_by_two_level_threshold_bytes', - 'distributed_aggregation_memory_efficient', - 'fsync_metadata', - 'output_format_parallel_formatting', - 'input_format_parallel_parsing', - 'min_chunk_bytes_for_parallel_parsing', - 'max_read_buffer_size', - 'prefer_localhost_replica', - 'max_block_size', - 'max_joined_block_size_rows', - 'joined_block_split_single_row', - 'join_output_by_rowlist_perkey_rows_threshold', - 'optimize_append_index', - 'use_hedged_requests', - 'optimize_if_chain_to_multiif', - 'optimize_if_transform_strings_to_enum', - 'optimize_read_in_order', - 'optimize_or_like_chain', - 'optimize_substitute_columns', - 'enable_multiple_prewhere_read_steps', - 'read_in_order_two_level_merge_threshold', - 'optimize_aggregation_in_order', - 'aggregation_in_order_max_block_bytes', - 'use_uncompressed_cache', - 'min_bytes_to_use_direct_io', - 'min_bytes_to_use_mmap_io', - 'local_filesystem_read_method', - 'remote_filesystem_read_method', - 'local_filesystem_read_prefetch', - 'filesystem_cache_segments_batch_size', - 'read_from_filesystem_cache_if_exists_otherwise_bypass_cache', - 'throw_on_error_from_cache_on_write_operations', - 'remote_filesystem_read_prefetch', - 'distributed_cache_discard_connection_if_unread_data', - 'distributed_cache_use_clients_cache_for_write', - 'distributed_cache_use_clients_cache_for_read', - 'allow_prefetched_read_pool_for_remote_filesystem', - 'filesystem_prefetch_max_memory_usage', - 'filesystem_prefetches_limit', - 'filesystem_prefetch_min_bytes_for_single_read_task', - 'filesystem_prefetch_step_marks', - 'filesystem_prefetch_step_bytes', - 'enable_filesystem_cache', - 'enable_filesystem_cache_on_write_operations', - 'compile_expressions', - 'compile_aggregate_expressions', - 'compile_sort_description', - 'merge_tree_coarse_index_granularity', - 'optimize_distinct_in_order', - 'max_bytes_before_remerge_sort', - 'min_compress_block_size', - 'max_compress_block_size', - 'merge_tree_compact_parts_min_granules_to_multibuffer_read', - 'optimize_sorting_by_input_stream_properties', - 'http_response_buffer_size', - 'http_wait_end_of_query', - 'enable_memory_bound_merging_of_aggregation_results', - 'min_count_to_compile_expression', - 'min_count_to_compile_aggregate_expression', - 'min_count_to_compile_sort_description', - 'session_timezone', - 'use_page_cache_for_disks_without_file_cache', - 'use_page_cache_for_local_disks', - 'use_page_cache_for_object_storage', - 'page_cache_inject_eviction', - 'merge_tree_read_split_ranges_into_intersecting_and_non_intersecting_injection_probability', - 'prefer_external_sort_block_bytes', - 'cross_join_min_rows_to_compress', - 'cross_join_min_bytes_to_compress', - 'min_external_table_block_size_bytes', - 'max_parsing_threads', - 'optimize_functions_to_subcolumns', - 'parallel_replicas_local_plan', - 'query_plan_join_swap_table', - 'enable_vertical_final', - 'optimize_extract_common_expressions', - 'optimize_syntax_fuse_functions', - 'use_async_executor_for_materialized_views', - 'use_query_condition_cache', - 'secondary_indices_enable_bulk_filtering', - 'use_skip_indexes_if_final', - 'use_skip_indexes_on_data_read', - 'optimize_rewrite_like_perfect_affix', - 'input_format_parquet_use_native_reader_v3', - 'enable_lazy_columns_replication', - 'allow_special_serialization_kinds_in_output_formats', - 'short_circuit_function_evaluation_for_nulls_threshold', - 'automatic_parallel_replicas_mode', - 'temporary_files_buffer_size', - 'query_plan_optimize_join_order_algorithm', - 'max_bytes_before_external_sort', - 'max_bytes_before_external_group_by', - 'max_bytes_ratio_before_external_sort', - 'max_bytes_ratio_before_external_group_by', - 'allow_repeated_settings', - 'use_skip_indexes_if_final_exact_mode', - 'ratio_of_defaults_for_sparse_serialization', - 'prefer_fetch_merged_part_size_threshold', - 'vertical_merge_algorithm_min_rows_to_activate', - 'vertical_merge_algorithm_min_columns_to_activate', - 'allow_vertical_merges_from_compact_to_wide_parts', - 'min_merge_bytes_to_use_direct_io', - 'index_granularity_bytes', - 'merge_max_block_size', - 'index_granularity', - 'min_bytes_for_wide_part', - 'compress_marks', - 'compress_primary_key', - 'marks_compress_block_size', - 'primary_key_compress_block_size', - 'replace_long_file_name_to_hash', - 'max_file_name_length', - 'min_bytes_for_full_part_storage', - 'compact_parts_max_bytes_to_buffer', - 'compact_parts_max_granules_to_buffer', - 'compact_parts_merge_max_bytes_to_prefetch_part', - 'cache_populated_by_fetch', - 'concurrent_part_removal_threshold', - 'old_parts_lifetime', - 'prewarm_mark_cache', - 'use_const_adaptive_granularity', - 'enable_index_granularity_compression', - 'enable_block_number_column', - 'enable_block_offset_column', - 'use_primary_key_cache', - 'prewarm_primary_key_cache', - 'object_serialization_version', - 'object_shared_data_serialization_version', - 'object_shared_data_serialization_version_for_zero_level_parts', - 'object_shared_data_buckets_for_compact_part', - 'object_shared_data_buckets_for_wide_part', - 'dynamic_serialization_version', - 'auto_statistics_types', - 'serialization_info_version', - 'string_serialization_version', - 'nullable_serialization_version', - 'enable_shared_storage_snapshot_in_query', - 'min_columns_to_activate_adaptive_write_buffer', - 'reduce_blocking_parts_sleep_ms', - 'shared_merge_tree_outdated_parts_group_size', - 'shared_merge_tree_max_outdated_parts_to_process_at_once', -]) + "group_by_two_level_threshold", + "group_by_two_level_threshold_bytes", + "distributed_aggregation_memory_efficient", + "fsync_metadata", + "output_format_parallel_formatting", + "input_format_parallel_parsing", + "min_chunk_bytes_for_parallel_parsing", + "max_read_buffer_size", + "prefer_localhost_replica", + "max_block_size", + "max_joined_block_size_rows", + "joined_block_split_single_row", + "join_output_by_rowlist_perkey_rows_threshold", + "optimize_append_index", + "use_hedged_requests", + "optimize_if_chain_to_multiif", + "optimize_if_transform_strings_to_enum", + "optimize_read_in_order", + "optimize_or_like_chain", + "optimize_substitute_columns", + "enable_multiple_prewhere_read_steps", + "read_in_order_two_level_merge_threshold", + "optimize_aggregation_in_order", + "aggregation_in_order_max_block_bytes", + "use_uncompressed_cache", + "min_bytes_to_use_direct_io", + "min_bytes_to_use_mmap_io", + "local_filesystem_read_method", + "remote_filesystem_read_method", + "local_filesystem_read_prefetch", + "filesystem_cache_segments_batch_size", + "read_from_filesystem_cache_if_exists_otherwise_bypass_cache", + "throw_on_error_from_cache_on_write_operations", + "remote_filesystem_read_prefetch", + "distributed_cache_discard_connection_if_unread_data", + "distributed_cache_use_clients_cache_for_write", + "distributed_cache_use_clients_cache_for_read", + "allow_prefetched_read_pool_for_remote_filesystem", + "filesystem_prefetch_max_memory_usage", + "filesystem_prefetches_limit", + "filesystem_prefetch_min_bytes_for_single_read_task", + "filesystem_prefetch_step_marks", + "filesystem_prefetch_step_bytes", + "enable_filesystem_cache", + "enable_filesystem_cache_on_write_operations", + "compile_expressions", + "compile_aggregate_expressions", + "compile_sort_description", + "merge_tree_coarse_index_granularity", + "optimize_distinct_in_order", + "max_bytes_before_remerge_sort", + "min_compress_block_size", + "max_compress_block_size", + "merge_tree_compact_parts_min_granules_to_multibuffer_read", + "optimize_sorting_by_input_stream_properties", + "http_response_buffer_size", + "http_wait_end_of_query", + "enable_memory_bound_merging_of_aggregation_results", + "min_count_to_compile_expression", + "min_count_to_compile_aggregate_expression", + "min_count_to_compile_sort_description", + "session_timezone", + "use_page_cache_for_disks_without_file_cache", + "use_page_cache_for_local_disks", + "use_page_cache_for_object_storage", + "page_cache_inject_eviction", + "merge_tree_read_split_ranges_into_intersecting_and_non_intersecting_injection_probability", + "prefer_external_sort_block_bytes", + "cross_join_min_rows_to_compress", + "cross_join_min_bytes_to_compress", + "min_external_table_block_size_bytes", + "max_parsing_threads", + "optimize_functions_to_subcolumns", + "parallel_replicas_local_plan", + "query_plan_join_swap_table", + "enable_vertical_final", + "optimize_extract_common_expressions", + "optimize_syntax_fuse_functions", + "use_async_executor_for_materialized_views", + "use_query_condition_cache", + "secondary_indices_enable_bulk_filtering", + "use_skip_indexes_if_final", + "use_skip_indexes_on_data_read", + "optimize_rewrite_like_perfect_affix", + "input_format_parquet_use_native_reader_v3", + "enable_lazy_columns_replication", + "allow_special_serialization_kinds_in_output_formats", + "short_circuit_function_evaluation_for_nulls_threshold", + "automatic_parallel_replicas_mode", + "temporary_files_buffer_size", + "query_plan_optimize_join_order_algorithm", + "max_bytes_before_external_sort", + "max_bytes_before_external_group_by", + "max_bytes_ratio_before_external_sort", + "max_bytes_ratio_before_external_group_by", + "allow_repeated_settings", + "use_skip_indexes_if_final_exact_mode", + "ratio_of_defaults_for_sparse_serialization", + "prefer_fetch_merged_part_size_threshold", + "vertical_merge_algorithm_min_rows_to_activate", + "vertical_merge_algorithm_min_columns_to_activate", + "allow_vertical_merges_from_compact_to_wide_parts", + "min_merge_bytes_to_use_direct_io", + "index_granularity_bytes", + "merge_max_block_size", + "index_granularity", + "min_bytes_for_wide_part", + "compress_marks", + "compress_primary_key", + "marks_compress_block_size", + "primary_key_compress_block_size", + "replace_long_file_name_to_hash", + "max_file_name_length", + "min_bytes_for_full_part_storage", + "compact_parts_max_bytes_to_buffer", + "compact_parts_max_granules_to_buffer", + "compact_parts_merge_max_bytes_to_prefetch_part", + "cache_populated_by_fetch", + "concurrent_part_removal_threshold", + "old_parts_lifetime", + "prewarm_mark_cache", + "use_const_adaptive_granularity", + "enable_index_granularity_compression", + "enable_block_number_column", + "enable_block_offset_column", + "use_primary_key_cache", + "prewarm_primary_key_cache", + "object_serialization_version", + "object_shared_data_serialization_version", + "object_shared_data_serialization_version_for_zero_level_parts", + "object_shared_data_buckets_for_compact_part", + "object_shared_data_buckets_for_wide_part", + "dynamic_serialization_version", + "auto_statistics_types", + "serialization_info_version", + "string_serialization_version", + "nullable_serialization_version", + "enable_shared_storage_snapshot_in_query", + "min_columns_to_activate_adaptive_write_buffer", + "reduce_blocking_parts_sleep_ms", + "shared_merge_tree_outdated_parts_group_size", + "shared_merge_tree_max_outdated_parts_to_process_at_once", +]); export function classifySetting(name: string): SettingScope { if (SERVER_SETTINGS.has(name)) { - return 'server' + return "server"; } if (CLIENT_ONLY_SETTINGS.has(name)) { - return 'client_only' + return "client_only"; } - return 'unknown' + return "unknown"; } diff --git a/tests/clickhouse-test-runner/src/split-queries.ts b/tests/clickhouse-test-runner/src/split-queries.ts index 029d4600..9c9d9e24 100644 --- a/tests/clickhouse-test-runner/src/split-queries.ts +++ b/tests/clickhouse-test-runner/src/split-queries.ts @@ -1,80 +1,80 @@ export function splitQueries(sql: string): string[] { - const queries: string[] = [] - let current = '' + const queries: string[] = []; + let current = ""; - let inSingleQuote = false - let inDoubleQuote = false - let inBacktick = false - let escaping = false + let inSingleQuote = false; + let inDoubleQuote = false; + let inBacktick = false; + let escaping = false; for (let i = 0; i < sql.length; i++) { - const ch = sql.charAt(i) + const ch = sql.charAt(i); if (escaping) { - current += ch - escaping = false - continue + current += ch; + escaping = false; + continue; } - if ((inSingleQuote || inDoubleQuote) && ch === '\\') { - current += ch - escaping = true - continue + if ((inSingleQuote || inDoubleQuote) && ch === "\\") { + current += ch; + escaping = true; + 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 + 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 (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 - continue + inSingleQuote = !inSingleQuote; + current += ch; + continue; } if (!inSingleQuote && !inBacktick && ch === '"') { - inDoubleQuote = !inDoubleQuote - current += ch - continue + inDoubleQuote = !inDoubleQuote; + current += ch; + continue; } - if (!inSingleQuote && !inDoubleQuote && ch === '`') { - inBacktick = !inBacktick - current += ch - continue + if (!inSingleQuote && !inDoubleQuote && ch === "`") { + inBacktick = !inBacktick; + current += ch; + continue; } - if (!inSingleQuote && !inDoubleQuote && !inBacktick && ch === ';') { - const statement = current.trim() + if (!inSingleQuote && !inDoubleQuote && !inBacktick && ch === ";") { + const statement = current.trim(); if (statement.length > 0) { - queries.push(statement) + queries.push(statement); } - current = '' - continue + current = ""; + continue; } - current += ch + current += ch; } - const trailing = current.trim() + const trailing = current.trim(); if (trailing.length > 0) { - queries.push(trailing) + queries.push(trailing); } - return queries + return queries; } diff --git a/tests/clickhouse-test-runner/vitest.config.ts b/tests/clickhouse-test-runner/vitest.config.ts index 422de850..9243ae4c 100644 --- a/tests/clickhouse-test-runner/vitest.config.ts +++ b/tests/clickhouse-test-runner/vitest.config.ts @@ -1,7 +1,7 @@ -import { defineConfig } from 'vitest/config' +import { defineConfig } from "vitest/config"; export default defineConfig({ test: { - include: ['__tests__/**/*.test.ts'], + include: ["__tests__/**/*.test.ts"], }, -}) +}); diff --git a/tests/e2e/install/src/index.ts b/tests/e2e/install/src/index.ts index 4bf7e8c1..c54bb025 100644 --- a/tests/e2e/install/src/index.ts +++ b/tests/e2e/install/src/index.ts @@ -1,29 +1,29 @@ -const assert = require('assert') -const { createClient } = require('@clickhouse/client') -const version = require('@clickhouse/client/dist/version') +const assert = require("assert"); +const { createClient } = require("@clickhouse/client"); +const version = require("@clickhouse/client/dist/version"); async function main() { - const expectedVersion = process.env.EXPECTED_VERSION + const expectedVersion = process.env.EXPECTED_VERSION; assert.ok( expectedVersion, - 'EXPECTED_VERSION environment variable must be set to the published version', - ) + "EXPECTED_VERSION environment variable must be set to the published version", + ); - console.log(`Expected published version: ${expectedVersion}`) - console.log(`Installed @clickhouse/client version: ${version.default}`) + console.log(`Expected published version: ${expectedVersion}`); + console.log(`Installed @clickhouse/client version: ${version.default}`); assert.strictEqual( version.default, expectedVersion, - 'Installed version should match the version published by the workflow', - ) + "Installed version should match the version published by the workflow", + ); assert.strictEqual( typeof createClient, - 'function', - 'createClient should be a function', - ) - assert.ok(createClient(), 'createClient should return a client instance') + "function", + "createClient should be a function", + ); + assert.ok(createClient(), "createClient should return a client instance"); } -main() +main(); diff --git a/tests/e2e/skills/check.js b/tests/e2e/skills/check.js index 7da08b67..3c696d22 100644 --- a/tests/e2e/skills/check.js +++ b/tests/e2e/skills/check.js @@ -1,4 +1,4 @@ -'use strict' +"use strict"; // E2E packaging check for shipped AI-agent skills. // @@ -17,148 +17,152 @@ // missing from the source `skills/` directory, and that `@clickhouse/client-web` // ships no skills. -const assert = require('assert') -const fs = require('fs') -const path = require('path') +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); function check(description, fn) { try { - fn() - console.log(`ok: ${description}`) + fn(); + console.log(`ok: ${description}`); } catch (e) { - console.error(`FAIL: ${description}`) - console.error(e.message) - process.exit(1) + console.error(`FAIL: ${description}`); + console.error(e.message); + process.exit(1); } } -const nm = path.join(__dirname, 'node_modules') -const repoRoot = path.resolve(__dirname, '..', '..', '..') -const skillsSrcDir = path.join(repoRoot, 'skills') +const nm = path.join(__dirname, "node_modules"); +const repoRoot = path.resolve(__dirname, "..", "..", ".."); +const skillsSrcDir = path.join(repoRoot, "skills"); // Discover skills from the source-of-truth `skills/` directory. assert.ok( fs.existsSync(skillsSrcDir), `source-of-truth skills directory not found at ${skillsSrcDir}; this script is meant to run from tests/e2e/skills inside the clickhouse-js repo`, -) +); const expectedSkills = fs .readdirSync(skillsSrcDir, { withFileTypes: true }) - .filter((d) => d.isDirectory() && fs.existsSync(path.join(skillsSrcDir, d.name, 'SKILL.md'))) + .filter( + (d) => + d.isDirectory() && + fs.existsSync(path.join(skillsSrcDir, d.name, "SKILL.md")), + ) .map((d) => d.name) - .sort() + .sort(); -check('repo skills/ directory contains at least one skill', () => { +check("repo skills/ directory contains at least one skill", () => { assert.ok( expectedSkills.length > 0, `expected at least one skill under ${skillsSrcDir}`, - ) -}) + ); +}); // @clickhouse/client (Node.js) — ships every skill from the repo `skills/` tree. -const nodeRoot = path.join(nm, '@clickhouse', 'client') +const nodeRoot = path.join(nm, "@clickhouse", "client"); const nodePkg = JSON.parse( - fs.readFileSync(path.join(nodeRoot, 'package.json'), 'utf8'), -) + fs.readFileSync(path.join(nodeRoot, "package.json"), "utf8"), +); const declaredSkills = Array.isArray(nodePkg.agents?.skills) ? nodePkg.agents.skills - : [] + : []; -check('@clickhouse/client skills dir exists', () => - assert.ok(fs.existsSync(path.join(nodeRoot, 'skills'))), -) +check("@clickhouse/client skills dir exists", () => + assert.ok(fs.existsSync(path.join(nodeRoot, "skills"))), +); check( - '@clickhouse/client agents.skills declares every skill from skills/', + "@clickhouse/client agents.skills declares every skill from skills/", () => { - const declaredNames = declaredSkills.map((s) => s.name).sort() + const declaredNames = declaredSkills.map((s) => s.name).sort(); assert.deepStrictEqual( declaredNames, expectedSkills, `agents.skills (${JSON.stringify(declaredNames)}) must match the skills found under skills/ (${JSON.stringify(expectedSkills)})`, - ) + ); }, -) +); for (const skill of declaredSkills) { check( `@clickhouse/client agents.skills entry "${skill.name}" has a valid path`, () => { assert.ok( - typeof skill.path === 'string' && skill.path.length > 0, + typeof skill.path === "string" && skill.path.length > 0, `agents.skills entry for ${skill.name} must declare a "path"`, - ) - const expectedPath = `./skills/${skill.name}` + ); + const expectedPath = `./skills/${skill.name}`; assert.strictEqual( skill.path, expectedPath, `agents.skills entry for ${skill.name} should declare path "${expectedPath}"`, - ) - const resolved = path.join(nodeRoot, skill.path) + ); + const resolved = path.join(nodeRoot, skill.path); assert.ok( fs.existsSync(resolved), `declared skill path does not exist in installed package: ${skill.path}`, - ) + ); assert.ok( - fs.existsSync(path.join(resolved, 'SKILL.md')), + fs.existsSync(path.join(resolved, "SKILL.md")), `declared skill at ${skill.path} should contain SKILL.md`, - ) + ); }, - ) + ); } // @clickhouse/client-web — no skills yet; verify the package installed cleanly and does not ship skills -check('@clickhouse/client-web installs without skills dir', () => { - const webRoot = path.join(nm, '@clickhouse', 'client-web') +check("@clickhouse/client-web installs without skills dir", () => { + const webRoot = path.join(nm, "@clickhouse", "client-web"); assert.ok( fs.existsSync(webRoot), - '@clickhouse/client-web should be installed', - ) + "@clickhouse/client-web should be installed", + ); assert.ok( - !fs.existsSync(path.join(webRoot, 'skills')), - '@clickhouse/client-web should not include a skills directory', - ) -}) + !fs.existsSync(path.join(webRoot, "skills")), + "@clickhouse/client-web should not include a skills directory", + ); +}); // skills-npm — symlinks each declared skill under `.claude/skills/`. -const skillsLinkDir = path.join(__dirname, '.claude', 'skills') +const skillsLinkDir = path.join(__dirname, ".claude", "skills"); -check('skills-npm creates a skills/ directory', () => { +check("skills-npm creates a skills/ directory", () => { assert.ok( fs.existsSync(skillsLinkDir), - 'skills/ directory should be created by skills-npm', - ) -}) + "skills/ directory should be created by skills-npm", + ); +}); const npmLinks = () => fs.existsSync(skillsLinkDir) - ? fs.readdirSync(skillsLinkDir).filter((e) => e.startsWith('npm-')) - : [] + ? fs.readdirSync(skillsLinkDir).filter((e) => e.startsWith("npm-")) + : []; -check('skills-npm creates at least one npm-* symlink', () => { - const links = npmLinks() +check("skills-npm creates at least one npm-* symlink", () => { + const links = npmLinks(); assert.ok( links.length > 0, - 'skills/ should contain at least one npm-* symlink', - ) -}) + "skills/ should contain at least one npm-* symlink", + ); +}); for (const skill of declaredSkills) { check(`skills-npm symlinks ${skill.name}`, () => { - const links = npmLinks() - const link = links.find((e) => e.includes(skill.name)) + const links = npmLinks(); + const link = links.find((e) => e.includes(skill.name)); assert.ok( link, - `skills/ should contain a symlink for ${skill.name}, found: [${links.join(', ')}]`, - ) - const linkPath = path.join(skillsLinkDir, link) + `skills/ should contain a symlink for ${skill.name}, found: [${links.join(", ")}]`, + ); + const linkPath = path.join(skillsLinkDir, link); assert.ok( fs.lstatSync(linkPath).isSymbolicLink(), `${link} should be a symlink`, - ) + ); assert.ok( - fs.existsSync(path.join(linkPath, 'SKILL.md')), + fs.existsSync(path.join(linkPath, "SKILL.md")), `symlinked skill ${skill.name} should contain SKILL.md`, - ) - }) + ); + }); } -console.log('\nAll checks passed.') +console.log("\nAll checks passed."); diff --git a/vitest.node.config.ts b/vitest.node.config.ts index 33a09929..65cecf04 100644 --- a/vitest.node.config.ts +++ b/vitest.node.config.ts @@ -1,78 +1,78 @@ -import { defineConfig } from 'vitest/config' +import { defineConfig } from "vitest/config"; -const testMode = process.env.TEST_MODE +const testMode = process.env.TEST_MODE; if ( - testMode !== 'unit' && - testMode !== 'integration' && - testMode !== 'tls' && - testMode !== 'common' && - testMode !== 'common-integration' && - testMode !== 'all' + testMode !== "unit" && + testMode !== "integration" && + testMode !== "tls" && + testMode !== "common" && + testMode !== "common-integration" && + testMode !== "all" ) { throw new Error( `Unsupported TEST_MODE: [${testMode}]. Supported modes are: unit, integration, tls, common, common-integration, all.`, - ) + ); } const collections = { unit: [ - 'packages/client-node/__tests__/unit/*.test.ts', - 'packages/client-node/__tests__/utils/*.test.ts', + "packages/client-node/__tests__/unit/*.test.ts", + "packages/client-node/__tests__/utils/*.test.ts", ], integration: [ - 'packages/client-node/__tests__/integration/*.test.ts', - 'packages/client-common/__tests__/integration/*.test.ts', + "packages/client-node/__tests__/integration/*.test.ts", + "packages/client-common/__tests__/integration/*.test.ts", ], // TLS tests require a specific environment setup // This list is integration + TLS tests tls: [ - 'packages/client-node/__tests__/integration/*.test.ts', - 'packages/client-common/__tests__/integration/*.test.ts', - 'packages/client-node/__tests__/tls/*.test.ts', + "packages/client-node/__tests__/integration/*.test.ts", + "packages/client-common/__tests__/integration/*.test.ts", + "packages/client-node/__tests__/tls/*.test.ts", ], common: [ - 'packages/client-common/__tests__/unit/*.test.ts', - 'packages/client-common/__tests__/utils/*.test.ts', + "packages/client-common/__tests__/unit/*.test.ts", + "packages/client-common/__tests__/utils/*.test.ts", ], - 'common-integration': [ - 'packages/client-common/__tests__/integration/*.test.ts', + "common-integration": [ + "packages/client-common/__tests__/integration/*.test.ts", ], all: [ - 'packages/client-common/__tests__/unit/*.test.ts', - 'packages/client-common/__tests__/utils/*.test.ts', - 'packages/client-common/__tests__/integration/*.test.ts', - 'packages/client-node/__tests__/tls/*.test.ts', - 'packages/client-node/__tests__/unit/*.test.ts', - 'packages/client-node/__tests__/utils/*.test.ts', - 'packages/client-node/__tests__/integration/*.test.ts', + "packages/client-common/__tests__/unit/*.test.ts", + "packages/client-common/__tests__/utils/*.test.ts", + "packages/client-common/__tests__/integration/*.test.ts", + "packages/client-node/__tests__/tls/*.test.ts", + "packages/client-node/__tests__/unit/*.test.ts", + "packages/client-node/__tests__/utils/*.test.ts", + "packages/client-node/__tests__/integration/*.test.ts", ], -} +}; export default defineConfig({ test: { // Increase maxWorkers to speed up integration tests // as we're not bound by the CPU here. - maxWorkers: '400%', + maxWorkers: "400%", // Cover the Cloud instance wake-up time hookTimeout: 300_000, testTimeout: 300_000, - slowTestThreshold: testMode === 'unit' ? 10_000 : undefined, - setupFiles: ['vitest.node.setup.ts'], + slowTestThreshold: testMode === "unit" ? 10_000 : undefined, + setupFiles: ["vitest.node.setup.ts"], include: collections[testMode], coverage: { - enabled: process.env.VITEST_COVERAGE === 'true', - provider: 'istanbul', - reporter: ['lcov', 'text'], + enabled: process.env.VITEST_COVERAGE === "true", + provider: "istanbul", + reporter: ["lcov", "text"], include: [ - 'packages/client-common/src/**/*.ts', - 'packages/client-node/src/**/*.ts', + "packages/client-common/src/**/*.ts", + "packages/client-node/src/**/*.ts", ], exclude: [ - 'packages/**/version.ts', - 'packages/client-common/src/clickhouse_types.ts', - 'packages/client-common/src/connection.ts', - 'packages/client-common/src/result.ts', - 'packages/client-common/src/ts_utils.ts', + "packages/**/version.ts", + "packages/client-common/src/clickhouse_types.ts", + "packages/client-common/src/connection.ts", + "packages/client-common/src/result.ts", + "packages/client-common/src/ts_utils.ts", ], }, env: { @@ -86,19 +86,19 @@ export default defineConfig({ experimental: { openTelemetry: { enabled: - process.env.VITEST_OTEL_ENABLED === 'true' && + process.env.VITEST_OTEL_ENABLED === "true" && // not set in dependabot PRs !!process.env.OTEL_EXPORTER_OTLP_ENDPOINT, - sdkPath: './vitest.node.otel.js', + sdkPath: "./vitest.node.otel.js", }, }, retry: process.env.CI ? 2 : 0, }, resolve: { alias: { - '@clickhouse/client-common': 'packages/client-common/src', - '@clickhouse/client-node': 'packages/client-node/src', - '@test': 'packages/client-common/__tests__', + "@clickhouse/client-common": "packages/client-common/src", + "@clickhouse/client-node": "packages/client-node/src", + "@test": "packages/client-common/__tests__", }, }, -}) +}); diff --git a/vitest.node.otel.js b/vitest.node.otel.js index 49a57216..0dc8e816 100644 --- a/vitest.node.otel.js +++ b/vitest.node.otel.js @@ -1,20 +1,20 @@ // https://vitest.dev/guide/open-telemetry -import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node' -import { resourceFromAttributes } from '@opentelemetry/resources' -import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto' -import { NodeSDK, resources, logs } from '@opentelemetry/sdk-node' -import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http' -import { ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions' +import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node"; +import { resourceFromAttributes } from "@opentelemetry/resources"; +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"; +import { NodeSDK, resources, logs } from "@opentelemetry/sdk-node"; +import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http"; +import { ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions"; -console.log('Initializing OTEL for Vitest...') +console.log("Initializing OTEL for Vitest..."); const resource = resourceFromAttributes({ [ATTR_SERVICE_VERSION]: process.env.GITHUB_SHA?.substring(0, 7), - 'ci.run.id': process.env.GITHUB_RUN_ID, - 'ci.job.name': process.env.GITHUB_JOB_NAME, - 'ci.workflow': process.env.GITHUB_WORKFLOW, - 'test.platform': 'node', -}) + "ci.run.id": process.env.GITHUB_RUN_ID, + "ci.job.name": process.env.GITHUB_JOB_NAME, + "ci.workflow": process.env.GITHUB_WORKFLOW, + "test.platform": "node", +}); const sdk = new NodeSDK({ resource, @@ -25,9 +25,9 @@ const sdk = new NodeSDK({ new logs.SimpleLogRecordProcessor(new logs.ConsoleLogRecordExporter()), new logs.BatchLogRecordProcessor(new OTLPLogExporter()), ], -}) +}); -sdk.start() -export default sdk +sdk.start(); +export default sdk; -console.log('OTEL for Vitest initialized') +console.log("OTEL for Vitest initialized"); diff --git a/vitest.node.setup.ts b/vitest.node.setup.ts index dd1caa84..962c13b7 100644 --- a/vitest.node.setup.ts +++ b/vitest.node.setup.ts @@ -1,7 +1,7 @@ // @ts-nocheck -import { createClient } from '@clickhouse/client-node' +import { createClient } from "@clickhouse/client-node"; /** * This file is used to set up the test environment for Vitest when running tests in Node.js. */ -globalThis.environmentSpecificCreateClient = createClient +globalThis.environmentSpecificCreateClient = createClient; diff --git a/vitest.web.config.ts b/vitest.web.config.ts index 875eb0e9..1c590f3e 100644 --- a/vitest.web.config.ts +++ b/vitest.web.config.ts @@ -1,88 +1,88 @@ -import { defineConfig } from 'vitest/config' -import { playwright } from '@vitest/browser-playwright' -import { fileURLToPath } from 'node:url' +import { defineConfig } from "vitest/config"; +import { playwright } from "@vitest/browser-playwright"; +import { fileURLToPath } from "node:url"; -const browser = process.env.BROWSER ?? 'chromium' -if (browser !== 'chromium' && browser !== 'firefox' && browser !== 'webkit') { +const browser = process.env.BROWSER ?? "chromium"; +if (browser !== "chromium" && browser !== "firefox" && browser !== "webkit") { throw new Error( `Unsupported BROWSER: [${browser}]. Supported browsers are: chromium, firefox, webkit.`, - ) + ); } -const testMode = process.env.TEST_MODE +const testMode = process.env.TEST_MODE; if ( - testMode !== 'unit' && - testMode !== 'integration' && - testMode !== 'jwt' && - testMode !== 'common' && - testMode !== 'common-integration' && - testMode !== 'all' + testMode !== "unit" && + testMode !== "integration" && + testMode !== "jwt" && + testMode !== "common" && + testMode !== "common-integration" && + testMode !== "all" ) { throw new Error( `Unsupported TEST_MODE: [${testMode}]. Supported modes are: unit, integration, jwt, all.`, - ) + ); } const collections = { unit: [ - 'packages/client-common/__tests__/unit/*.test.ts', - 'packages/client-common/__tests__/utils/*.test.ts', - 'packages/client-web/__tests__/unit/*.test.ts', + "packages/client-common/__tests__/unit/*.test.ts", + "packages/client-common/__tests__/utils/*.test.ts", + "packages/client-web/__tests__/unit/*.test.ts", ], integration: [ - 'packages/client-common/__tests__/integration/*.test.ts', - 'packages/client-web/__tests__/integration/*.test.ts', + "packages/client-common/__tests__/integration/*.test.ts", + "packages/client-web/__tests__/integration/*.test.ts", ], // JWT tests require a specific environment setup (a valid access token) // This list is integration + JWT tests jwt: [ - 'packages/client-common/__tests__/integration/*.test.ts', - 'packages/client-web/__tests__/integration/*.test.ts', - 'packages/client-web/__tests__/jwt/*.test.ts', + "packages/client-common/__tests__/integration/*.test.ts", + "packages/client-web/__tests__/integration/*.test.ts", + "packages/client-web/__tests__/jwt/*.test.ts", ], common: [ - 'packages/client-common/__tests__/unit/*.test.ts', - 'packages/client-common/__tests__/utils/*.test.ts', + "packages/client-common/__tests__/unit/*.test.ts", + "packages/client-common/__tests__/utils/*.test.ts", ], - 'common-integration': [ - 'packages/client-common/__tests__/integration/*.test.ts', + "common-integration": [ + "packages/client-common/__tests__/integration/*.test.ts", ], all: [ - 'packages/client-common/__tests__/unit/*.test.ts', - 'packages/client-common/__tests__/utils/*.test.ts', - 'packages/client-common/__tests__/integration/*.test.ts', - 'packages/client-web/__tests__/unit/*.test.ts', - 'packages/client-web/__tests__/integration/*.test.ts', - 'packages/client-web/__tests__/jwt/*.test.ts', + "packages/client-common/__tests__/unit/*.test.ts", + "packages/client-common/__tests__/utils/*.test.ts", + "packages/client-common/__tests__/integration/*.test.ts", + "packages/client-web/__tests__/unit/*.test.ts", + "packages/client-web/__tests__/integration/*.test.ts", + "packages/client-web/__tests__/jwt/*.test.ts", ], -} +}; export default defineConfig({ test: { // Increase maxWorkers to speed up integration tests // as we're not bound by the CPU here. - maxWorkers: '400%', + maxWorkers: "400%", // Cover the Cloud instance wake-up time hookTimeout: 300_000, testTimeout: 300_000, - slowTestThreshold: testMode === 'unit' ? 10_000 : undefined, - setupFiles: ['vitest.web.setup.ts'], + slowTestThreshold: testMode === "unit" ? 10_000 : undefined, + setupFiles: ["vitest.web.setup.ts"], include: collections[testMode], coverage: { - enabled: process.env.VITEST_COVERAGE === 'true', - provider: 'istanbul', - reporter: ['lcov', 'text'], + enabled: process.env.VITEST_COVERAGE === "true", + provider: "istanbul", + reporter: ["lcov", "text"], include: [ - 'packages/client-common/src/**/*.ts', - 'packages/client-web/src/**/*.ts', + "packages/client-common/src/**/*.ts", + "packages/client-web/src/**/*.ts", ], exclude: [ - 'packages/**/version.ts', - 'packages/client-common/src/clickhouse_types.ts', - 'packages/client-common/src/connection.ts', - 'packages/client-common/src/result.ts', - 'packages/client-common/src/ts_utils.ts', - 'packages/client-common/__tests__/utils/*.ts', + "packages/**/version.ts", + "packages/client-common/src/clickhouse_types.ts", + "packages/client-common/src/connection.ts", + "packages/client-common/src/result.ts", + "packages/client-common/src/ts_utils.ts", + "packages/client-common/__tests__/utils/*.ts", ], }, env: { @@ -104,10 +104,10 @@ export default defineConfig({ experimental: { openTelemetry: { enabled: - process.env.VITEST_OTEL_ENABLED === 'true' && + process.env.VITEST_OTEL_ENABLED === "true" && // not set in dependabot PRs !!process.env.OTEL_EXPORTER_OTLP_ENDPOINT, - sdkPath: './vitest.node.otel.js', + sdkPath: "./vitest.node.otel.js", // According to testing, runners hang indefinitely when OTEL is enabled in browser tests, // and when they don't the exporter visibly slows the tests down (2x-5x). // Tests also crash (their iframe?) when the devtools are open in Chrome. @@ -122,17 +122,17 @@ export default defineConfig({ }, resolve: { // Use the unittest entry point to get the source files instead of built files - conditions: ['unittest'], + conditions: ["unittest"], alias: { - '@clickhouse/client-common': fileURLToPath( - new URL('./packages/client-common/src', import.meta.url), + "@clickhouse/client-common": fileURLToPath( + new URL("./packages/client-common/src", import.meta.url), ), - '@clickhouse/client-web': fileURLToPath( - new URL('./packages/client-web', import.meta.url), + "@clickhouse/client-web": fileURLToPath( + new URL("./packages/client-web", import.meta.url), ), - '@test': fileURLToPath( - new URL('./packages/client-common/__tests__', import.meta.url), + "@test": fileURLToPath( + new URL("./packages/client-common/__tests__", import.meta.url), ), }, }, -}) +}); diff --git a/vitest.web.otel.js b/vitest.web.otel.js index 4452b7b7..c1be4ade 100644 --- a/vitest.web.otel.js +++ b/vitest.web.otel.js @@ -1,28 +1,28 @@ import { BatchSpanProcessor, WebTracerProvider, -} from '@opentelemetry/sdk-trace-web' -import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto' -import { resourceFromAttributes } from '@opentelemetry/resources' +} from "@opentelemetry/sdk-trace-web"; +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"; +import { resourceFromAttributes } from "@opentelemetry/resources"; // import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load' -import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch' -import { ZoneContextManager } from '@opentelemetry/context-zone' -import { registerInstrumentations } from '@opentelemetry/instrumentation' +import { FetchInstrumentation } from "@opentelemetry/instrumentation-fetch"; +import { ZoneContextManager } from "@opentelemetry/context-zone"; +import { registerInstrumentations } from "@opentelemetry/instrumentation"; import { ATTR_SERVICE_VERSION, ATTR_SERVICE_NAME, -} from '@opentelemetry/semantic-conventions' +} from "@opentelemetry/semantic-conventions"; -console.log('Initializing OTEL for Vitest in the browser...') +console.log("Initializing OTEL for Vitest in the browser..."); const resource = resourceFromAttributes({ [ATTR_SERVICE_NAME]: import.meta.env.OTEL_SERVICE_NAME, [ATTR_SERVICE_VERSION]: import.meta.env.GITHUB_SHA?.substring(0, 7), - 'ci.run.id': import.meta.env.GITHUB_RUN_ID, - 'ci.job.name': import.meta.env.GITHUB_JOB_NAME, - 'ci.workflow': import.meta.env.GITHUB_WORKFLOW, - 'test.platform': 'web', -}) + "ci.run.id": import.meta.env.GITHUB_RUN_ID, + "ci.job.name": import.meta.env.GITHUB_JOB_NAME, + "ci.workflow": import.meta.env.GITHUB_WORKFLOW, + "test.platform": "web", +}); const provider = new WebTracerProvider({ resource, @@ -32,29 +32,29 @@ const provider = new WebTracerProvider({ new OTLPTraceExporter({ url: `${import.meta.env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces`, // optional - collection of custom headers to be sent with each request, empty by default - headers: import.meta.env.OTEL_EXPORTER_OTLP_HEADERS.split(',').reduce( + headers: import.meta.env.OTEL_EXPORTER_OTLP_HEADERS.split(",").reduce( (acc, header) => { - const [key, ...value] = header.split('=') - acc[key.trim()] = value.join('=').trim() - return acc + const [key, ...value] = header.split("="); + acc[key.trim()] = value.join("=").trim(); + return acc; }, {}, ), }), ), ], -}) +}); provider.register({ // Changing default contextManager to use ZoneContextManager - supports asynchronous operations - optional contextManager: new ZoneContextManager(), -}) +}); registerInstrumentations({ instrumentations: [ new FetchInstrumentation(), // new DocumentLoadInstrumentation() ], -}) +}); -export default provider +export default provider; diff --git a/vitest.web.setup.ts b/vitest.web.setup.ts index 20f7e795..82b3ccef 100644 --- a/vitest.web.setup.ts +++ b/vitest.web.setup.ts @@ -1,10 +1,10 @@ // @ts-nocheck -import { createClient } from '@clickhouse/client-web' +import { createClient } from "@clickhouse/client-web"; /** * This file is used to set up the test environment for Vitest when running tests in Node.js. */ -globalThis.environmentSpecificCreateClient = createClient +globalThis.environmentSpecificCreateClient = createClient; // Port to import.meta.env once all modules support ESM globalThis.process = { @@ -16,4 +16,4 @@ globalThis.process = { CLICKHOUSE_TEST_SKIP_INIT: import.meta.env.CLICKHOUSE_TEST_SKIP_INIT, CLICKHOUSE_TEST_ENVIRONMENT: import.meta.env.CLICKHOUSE_TEST_ENVIRONMENT, }, -} +}; From 6aacc08bbfd228033bdb5cc3c0468c9c9fba60d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 19:38:58 +0200 Subject: [PATCH 08/20] Bump the dev-dependencies group with 2 updates (#818) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the dev-dependencies group with 2 updates: [prettier](https://github.com/prettier/prettier) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint). Updates `prettier` from 3.8.3 to 3.8.4
Release notes

Sourced from prettier's releases.

3.8.4

🔗 Changelog

Changelog

Sourced from prettier's changelog.

3.8.4

diff

Markdown: Fix blank lines between list items and nested sub-lists being removed in Markdown/MDX (#17746 by @​byplayer)

Prettier was removing blank lines between list items and their nested sub-lists, converting loose lists into tight lists and changing their semantic meaning.

<!-- Input -->
- a
  • b

  • c

    • d

<!-- Prettier 3.8.3 -->

  • a
    • b
  • c
    • d

<!-- Prettier 3.8.4 -->

  • a

    • b
  • c

    • d
Commits
  • 1c6ba55 Release 3.8.4
  • 4a673dc Fix blank lines between list items and nested sub-lists being removed in Mark...
  • 074aaed Replace main branch in changelog link with tags (#19054)
  • c22a003 Bump Prettier dependency to 3.8.3
  • 07bad1f Clean changelog_unreleased
  • See full diff in compare view

Updates `typescript-eslint` from 8.60.1 to 8.61.0
Release notes

Sourced from typescript-eslint's releases.

v8.61.0

8.61.0 (2026-06-08)

🚀 Features

  • ast-spec: change type of UnaryExpression.prefix to always true (#12372)
  • ast-spec: tighten types of ArrowFunction, YieldExpression, TSTypePredicate (#12373)

🩹 Fixes

  • rule-schema-to-typescript-types: respect ECMAScript line terminators (#12374)

❤️ Thank You

See GitHub Releases for more information.

You can read about our versioning strategy and releases on our website.

Changelog

Sourced from typescript-eslint's changelog.

8.61.0 (2026-06-08)

This was a version bump only for typescript-eslint to align it with other projects, there were no code changes.

See GitHub Releases for more information.

You can read about our versioning strategy and releases on our website.

Commits

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 136 +++++++++++----------- package.json | 4 +- tests/clickhouse-test-runner/package.json | 4 +- 3 files changed, 72 insertions(+), 72 deletions(-) diff --git a/package-lock.json b/package-lock.json index 146ff430..739dd970 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,10 +39,10 @@ "lint-staged": "^17.0.7", "parquet-wasm": "0.7.1", "playwright": "^1.60.0", - "prettier": "3.8.3", + "prettier": "3.8.4", "split2": "^4.2.0", "typescript": "^6.0.3", - "typescript-eslint": "^8.60.1", + "typescript-eslint": "^8.61.0", "uuid": "^14.0.0", "vitest": "^4.0.16" }, @@ -2796,17 +2796,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz", - "integrity": "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz", + "integrity": "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.60.1", - "@typescript-eslint/type-utils": "8.60.1", - "@typescript-eslint/utils": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/type-utils": "8.61.0", + "@typescript-eslint/utils": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -2819,7 +2819,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.60.1", + "@typescript-eslint/parser": "^8.61.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -2835,16 +2835,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.1.tgz", - "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.0.tgz", + "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", "debug": "^4.4.3" }, "engines": { @@ -2860,14 +2860,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", - "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.0.tgz", + "integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.60.1", - "@typescript-eslint/types": "^8.60.1", + "@typescript-eslint/tsconfig-utils": "^8.61.0", + "@typescript-eslint/types": "^8.61.0", "debug": "^4.4.3" }, "engines": { @@ -2882,14 +2882,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz", - "integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz", + "integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1" + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2900,9 +2900,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", - "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz", + "integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==", "dev": true, "license": "MIT", "engines": { @@ -2917,15 +2917,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz", - "integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz", + "integrity": "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1", - "@typescript-eslint/utils": "8.60.1", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -2942,9 +2942,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz", - "integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.0.tgz", + "integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==", "dev": true, "license": "MIT", "engines": { @@ -2956,16 +2956,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", - "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz", + "integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.60.1", - "@typescript-eslint/tsconfig-utils": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", + "@typescript-eslint/project-service": "8.61.0", + "@typescript-eslint/tsconfig-utils": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -2984,16 +2984,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz", - "integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.0.tgz", + "integrity": "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1" + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3008,13 +3008,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz", - "integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz", + "integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/types": "8.61.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -5726,9 +5726,9 @@ } }, "node_modules/prettier": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", - "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", + "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", "dev": true, "license": "MIT", "bin": { @@ -6285,16 +6285,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.60.1.tgz", - "integrity": "sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.0.tgz", + "integrity": "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.60.1", - "@typescript-eslint/parser": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1", - "@typescript-eslint/utils": "8.60.1" + "@typescript-eslint/eslint-plugin": "8.61.0", + "@typescript-eslint/parser": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6818,9 +6818,9 @@ "@types/node": "25.9.2", "eslint": "^10.4.1", "eslint-plugin-prettier": "^5.5.6", - "prettier": "3.8.3", + "prettier": "3.8.4", "typescript": "^6.0.3", - "typescript-eslint": "^8.60.1", + "typescript-eslint": "^8.61.0", "vitest": "^4.0.16" }, "engines": { diff --git a/package.json b/package.json index cff71baa..c3e26734 100644 --- a/package.json +++ b/package.json @@ -74,10 +74,10 @@ "lint-staged": "^17.0.7", "parquet-wasm": "0.7.1", "playwright": "^1.60.0", - "prettier": "3.8.3", + "prettier": "3.8.4", "split2": "^4.2.0", "typescript": "^6.0.3", - "typescript-eslint": "^8.60.1", + "typescript-eslint": "^8.61.0", "uuid": "^14.0.0", "vitest": "^4.0.16" }, diff --git a/tests/clickhouse-test-runner/package.json b/tests/clickhouse-test-runner/package.json index f6723b0d..57b4d11f 100644 --- a/tests/clickhouse-test-runner/package.json +++ b/tests/clickhouse-test-runner/package.json @@ -26,9 +26,9 @@ "@types/node": "25.9.2", "eslint": "^10.4.1", "eslint-plugin-prettier": "^5.5.6", - "prettier": "3.8.3", + "prettier": "3.8.4", "typescript": "^6.0.3", - "typescript-eslint": "^8.60.1", + "typescript-eslint": "^8.61.0", "vitest": "^4.0.16" } } From 1564b1634c31df5ed3ca5f684249e5700d66900a Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 20:44:01 +0200 Subject: [PATCH 09/20] Add typings for the remaining HTTP-specific ClickHouse settings (#821) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Only `wait_end_of_query`, `default_format`, `session_timeout`, and `session_check` were typed in `ClickHouseHTTPSettings`. This adds the remaining HTTP-handler params from ClickHouse's `reserved_param_names` so they surface in autocomplete when passed via `clickhouse_settings`. - **`packages/client-common/src/settings.ts`** — Added `buffer_size`, `compress`, `decompress`, `quota_key`, and `stacktrace` to `ClickHouseHTTPSettings`, each with JSDoc. Deliberately omitted `user`, `password`, `query_id`, `session_id`, and `database`, which are already first-class client config / query params (the first two being credentials). - **`to_search_params.test.ts`** — Added a unit test asserting the new settings serialize into the request URL. - **`CHANGELOG.md`** — Added an Improvements entry. ```ts await client.query({ query: "SELECT 1", clickhouse_settings: { // buffer the entire response server-side before sending to the client wait_end_of_query: 1, buffer_size: "1048576", }, }); ``` These are additive type declarations only; the settings already worked at runtime via the `Record` index signature on `ClickHouseSettings`. Fixes #53. ## 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> --- CHANGELOG.md | 15 ++++++++++++ .../__tests__/unit/to_search_params.test.ts | 24 +++++++++++++++++++ packages/client-common/src/settings.ts | 17 +++++++++++++ 3 files changed, 56 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38736d0d..77642b97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ - TypeScript: `ClickHouseLogLevel` is now exported as a literal numeric union type (`0 | 1 | 2 | 3 | 4 | 127`) instead of a TypeScript `enum` type. If you were assigning arbitrary `number` values to `ClickHouseLogLevel`, you may need to narrow/cast those values during migration. +## Improvements + +- Added TypeScript typings for the remaining HTTP-specific ClickHouse settings, so they are now suggested by autocomplete when used in `clickhouse_settings`: `buffer_size`, `compress`, `decompress`, `quota_key`, and `stacktrace` (in addition to the existing `wait_end_of_query`, `default_format`, `session_timeout`, and `session_check`). + +```ts +await client.query({ + query: "SELECT 1", + clickhouse_settings: { + // Buffer the entire response on the server before sending it to the client + wait_end_of_query: 1, + buffer_size: "1048576", + }, +}); +``` + ## 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]) diff --git a/packages/client-common/__tests__/unit/to_search_params.test.ts b/packages/client-common/__tests__/unit/to_search_params.test.ts index a3778313..15950ef6 100644 --- a/packages/client-common/__tests__/unit/to_search_params.test.ts +++ b/packages/client-common/__tests__/unit/to_search_params.test.ts @@ -35,6 +35,30 @@ describe("toSearchParams", () => { ]); }); + it("should set HTTP-specific settings", async () => { + const params = toSearchParams({ + database: "default", + query_id: "foo", + clickhouse_settings: { + wait_end_of_query: 1, + buffer_size: "3000000", + compress: 1, + decompress: 0, + quota_key: "my-quota", + stacktrace: 1, + }, + })!; + expect(toSortedArray(params)).toEqual([ + ["buffer_size", "3000000"], + ["compress", "1"], + ["decompress", "0"], + ["query_id", "foo"], + ["quota_key", "my-quota"], + ["stacktrace", "1"], + ["wait_end_of_query", "1"], + ]); + }); + it("should set query params", async () => { const params = toSearchParams({ database: "default", diff --git a/packages/client-common/src/settings.ts b/packages/client-common/src/settings.ts index fff34ff9..e7201a72 100644 --- a/packages/client-common/src/settings.ts +++ b/packages/client-common/src/settings.ts @@ -1608,6 +1608,23 @@ interface ClickHouseHTTPSettings { * Additionally, this is useful when executing DDLs on clustered environments, * as the client will receive the response only when the DDL is applied on all nodes of the cluster. */ wait_end_of_query: Bool; + /** Number of bytes in the result to buffer in the server memory. + * If the result body is larger than this threshold, the buffer is written to the HTTP channel, + * and the remaining data is sent directly to the HTTP channel. */ + buffer_size: UInt64; + /** If enabled, the server compresses the data it sends to the client using the internal ClickHouse + * compression format (not a standard HTTP one). The compressed data has a non-standard format, + * and the `clickhouse-compressor` program is required to work with it. + * For standard HTTP (gzip) compression, use the `compression` client configuration option instead. */ + compress: Bool; + /** If enabled, the server decompresses the data passed in the POST request body, expecting it to be + * compressed using the internal ClickHouse compression format (not a standard HTTP one). */ + decompress: Bool; + /** Can be passed as the quota key (any string). + * @see https://clickhouse.com/docs/operations/quotas */ + quota_key: string; + /** If enabled, include the stack trace in the response body when an exception occurs. */ + stacktrace: Bool; /** Format to use if a SELECT query is executed without a FORMAT clause. * Only useful for the {@link ClickHouseClient.exec} method, * as {@link ClickHouseClient.query} method always attaches this clause. */ From e19237b371c2ab6ebdd55ff02ade7f6a7328482e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 21:08:52 +0200 Subject: [PATCH 10/20] ci: bump codecov/codecov-action to v6.0.2 (#824) ## Summary Bumps `codecov/codecov-action` from v5.5.2 to v6.0.2 (latest 6.x), pinned to commit SHA `fb8b3582c8e4def4969c97caa2f19720cb33a72f`. - Updated `.github/workflows/tests-node.yml` - Updated `.github/workflows/tests-web.yml` Note: v6.0.0+ runs on the `node24` runtime. ## Checklist - [ ] 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> --- .github/workflows/tests-node.yml | 2 +- .github/workflows/tests-web.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests-node.yml b/.github/workflows/tests-node.yml index a4091268..cb90d37a 100644 --- a/.github/workflows/tests-node.yml +++ b/.github/workflows/tests-node.yml @@ -299,7 +299,7 @@ jobs: npm run test:node:coverage - name: Upload coverage to Codecov - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v6.0.2 with: name: node token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/tests-web.yml b/.github/workflows/tests-web.yml index 7c7381c5..f15e1768 100644 --- a/.github/workflows/tests-web.yml +++ b/.github/workflows/tests-web.yml @@ -225,7 +225,7 @@ jobs: npm run test:web:coverage - name: Upload coverage to Codecov - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v6.0.2 with: name: web token: ${{ secrets.CODECOV_TOKEN }} From 0ba4dee3147ce3bd712f0b03eaf20798d7b0564a Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 19:12:41 +0000 Subject: [PATCH 11/20] Adopt TypeScript 6.0 options to prepare for the TS 7.0 (native) migration (#823) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary TypeScript 6.0 is a transition release: options it deprecates are **removed entirely in TypeScript 7.0** (the native port). This aligns the repo's tsconfigs with the TS 6.0 "Breaking Changes and Deprecations" / "Preparing for TypeScript 7.0" guidance so the upgrade is a no-op. - **Removed deprecated `baseUrl`** in `tsconfig.dev.json` and `benchmarks/tsconfig.json` (the only 7.0-breaking option present), folding the prefix into each `paths` entry. - **Explicit `types: []`** in `client-common`/`client-web` tsconfigs — matches the new 6.0 default and the recommendation to pin `types` for predictability/perf (neither package's `src` uses Node globals). - **Pinned new 6.0 defaults** in `tsconfig.base.json`: `noUncheckedSideEffectImports: true` and `libReplacement: false`, so behavior doesn't ride on floating defaults. Already compliant, left as-is: `strict`, `esModuleInterop`, `NodeNext` module/resolution, explicit `target`/`rootDir`, `erasableSyntaxOnly`; no `outFile`/`downlevelIteration`, legacy `module` namespaces, or `asserts` import attributes. ```diff { "compilerOptions": { - "baseUrl": "./", "paths": { - "@clickhouse/client-common": ["packages/client-common/src/index.ts"] + "@clickhouse/client-common": ["./packages/client-common/src/index.ts"] } } } ``` ## Checklist - [x] A human-readable description of the changes was provided to include in CHANGELOG - Aligned tsconfigs with TypeScript 6.0 defaults and removed the deprecated `baseUrl` option to prepare for the TypeScript 7.0 (native) migration. Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- benchmarks/tsconfig.json | 1 - packages/client-common/tsconfig.json | 1 + packages/client-web/tsconfig.json | 1 + tsconfig.base.json | 4 +++- tsconfig.dev.json | 5 ++--- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/benchmarks/tsconfig.json b/benchmarks/tsconfig.json index 069f5d32..6d8c7f8c 100644 --- a/benchmarks/tsconfig.json +++ b/benchmarks/tsconfig.json @@ -5,7 +5,6 @@ "noUnusedLocals": false, "noUnusedParameters": false, "outDir": "dist", - "baseUrl": "./", "paths": { "@clickhouse/client-common": ["../packages/client-common/src/index.ts"], "@clickhouse/client": ["../packages/client-node/src/index.ts"], diff --git a/packages/client-common/tsconfig.json b/packages/client-common/tsconfig.json index 378ea626..3c2adbf8 100644 --- a/packages/client-common/tsconfig.json +++ b/packages/client-common/tsconfig.json @@ -4,6 +4,7 @@ "compilerOptions": { "rootDir": "./src", "lib": ["ES2022", "ESNext.Disposable", "DOM"], + "types": [], "outDir": "./dist" } } diff --git a/packages/client-web/tsconfig.json b/packages/client-web/tsconfig.json index 378ea626..3c2adbf8 100644 --- a/packages/client-web/tsconfig.json +++ b/packages/client-web/tsconfig.json @@ -4,6 +4,7 @@ "compilerOptions": { "rootDir": "./src", "lib": ["ES2022", "ESNext.Disposable", "DOM"], + "types": [], "outDir": "./dist" } } diff --git a/tsconfig.base.json b/tsconfig.base.json index 2c524f25..dcc54c47 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -21,7 +21,9 @@ "skipLibCheck": false, "esModuleInterop": true, "importHelpers": false, - "erasableSyntaxOnly": true + "erasableSyntaxOnly": true, + "noUncheckedSideEffectImports": true, + "libReplacement": false }, "exclude": ["node_modules"] } diff --git a/tsconfig.dev.json b/tsconfig.dev.json index cd682114..9eccd75e 100644 --- a/tsconfig.dev.json +++ b/tsconfig.dev.json @@ -7,10 +7,9 @@ "noUnusedLocals": false, "noUnusedParameters": false, "outDir": "out", - "baseUrl": "./", "paths": { - "@test/*": ["packages/client-common/__tests__/*"], - "@clickhouse/client-common": ["packages/client-common/src/index.ts"] + "@test/*": ["./packages/client-common/__tests__/*"], + "@clickhouse/client-common": ["./packages/client-common/src/index.ts"] } } } From 4b93a35731fcba355feb9926430e7037b80b9f4c Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 21:19:58 +0200 Subject: [PATCH 12/20] Add integration test covering every ClickHouse setting (#820) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Verifies the client can transmit every setting the server supports, replacing the `// TODO: cover at least all enum settings` placeholder in `clickhouse_settings.test.ts`. The new test reads all settings from `system.settings` and sends each one back at its current value, asserting none is rejected. Iterating over what the server reports means enum settings, `Map` settings, and any settings added in future server versions are covered automatically across the CI version matrix. Follow-up fixes from review/CI feedback are included: - bounded worker count with `Math.min(10, settings.length)` - removed the inline type assertion in favor of a typed variable - fixed worker loop runtime error by restoring the shared `index` counter (`let index = 0`) - **Test scope**: limited to `LocalSingleNode` / `LocalCluster` (every setting is writable there), so the assertion can be strict; Cloud is skipped because its read-only setting constraints would reject same-value sends. - **Execution**: 10-way concurrency keeps the ~1.6k single-statement requests well under a second. ```ts const settings = await client .query({ query: "SELECT name, value FROM system.settings", format: "JSONEachRow" }) .then((r) => r.json<{ name: string; value: string }>()); let index = 0; const clickhouse_settings: ClickHouseSettings = { [name]: value }; await client.command({ query: "SELECT 1", clickhouse_settings, }); ``` > Note: while building this, two stale enum unions surfaced against ClickHouse 26.6 — `Dialect` (invalid `kusto_auto`, missing `polyglot`/`promql`) and `ORCCompression` (`gzip` should be `zlib`). These are left unchanged here since they are version-sensitive and out of scope for this test-only change. ## Checklist - [x] Unit and integration tests covering the common scenarios were added - [ ] 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> Co-authored-by: Peter Leonov Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../integration/clickhouse_settings.test.ts | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/packages/client-common/__tests__/integration/clickhouse_settings.test.ts b/packages/client-common/__tests__/integration/clickhouse_settings.test.ts index 9ac9ffbb..97eddd6f 100644 --- a/packages/client-common/__tests__/integration/clickhouse_settings.test.ts +++ b/packages/client-common/__tests__/integration/clickhouse_settings.test.ts @@ -1,10 +1,13 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import type { ClickHouseClient, InsertParams } from "@clickhouse/client-common"; +import type { + ClickHouseClient, + ClickHouseSettings, + InsertParams, +} from "@clickhouse/client-common"; import { SettingsMap } from "@clickhouse/client-common"; import { createSimpleTable } from "../fixtures/simple_table"; -import { createTestClient, guid } from "../utils"; +import { createTestClient, guid, isOnEnv, TestEnv } from "../utils"; -// TODO: cover at least all enum settings describe("ClickHouse settings", () => { let client: ClickHouseClient; beforeEach(() => { @@ -14,6 +17,43 @@ describe("ClickHouse settings", () => { await client.close(); }); + // Guards against transport/serialization regressions: the client must be able + // to send any setting the server advertises (including enum and Map settings). + // Scoped to local environments, where every setting is writable, so we can + // assert that none of them is rejected when sent back at its current value. + it.skipIf(!isOnEnv(TestEnv.LocalSingleNode, TestEnv.LocalCluster))( + "should be able to send every setting reported by system.settings", + async () => { + const settings = await client + .query({ + query: "SELECT name, value FROM system.settings", + format: "JSONEachRow", + }) + .then((r) => r.json<{ name: string; value: string }>()); + expect(settings.length).toBeGreaterThan(0); + + const failures: { name: string; error: string }[] = []; + const concurrency = Math.min(10, settings.length); + let index = 0; + async function worker() { + while (index < settings.length) { + const { name, value } = settings[index++]; + const clickhouse_settings: ClickHouseSettings = { [name]: value }; + try { + await client.command({ + query: "SELECT 1", + clickhouse_settings, + }); + } catch (err) { + failures.push({ name, error: (err as Error).message }); + } + } + } + await Promise.all(Array.from({ length: concurrency }, () => worker())); + expect(failures).toEqual([]); + }, + ); + it("should work with additional_table_filters map", async () => { const result = await client .query({ From 82676f9fd826360bb0dc15504e6149066397a5d2 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 21:33:34 +0200 Subject: [PATCH 13/20] Fix error parsing for cluster-mode nested exceptions (#819) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary In cluster mode the server wraps the original exception, repeating the `Code`/`Exception`/`(TYPE)` markers: ``` Code: 57. DB::Exception: There was an error on [clickhouse2:9000]: Code: 57. DB::Exception: Table default.t already exists. (TABLE_ALREADY_EXISTS) (version ...). (TABLE_ALREADY_EXISTS) (version ...) ``` The old greedy regex captured the repeated `(TYPE)` / `(version ...)` suffixes into `message`, yielding garbage instead of `Table default.t already exists.`. Changes in `packages/client-common/src/error/error.ts`: - **`errorRe`** — greedy `.*Exception: ` anchors on the innermost exception; lazy `(?.+?)` stops at the first `(TYPE)` marker instead of swallowing the wrapper suffixes. - **Type lookahead** — requires ≥3 consecutive uppercase letters, so parenthesised groups like `(2)` or `(official build)` are not mistaken for the error type (preserves the `TOO_FEW_LIVE_REPLICAS` corner case). - **Note on ReDoS** — documented that error strings are never freeform; the format is rigid and server-controlled, never attacker-supplied, so greedy matching is safe here and the theoretical backtracking cost is not a real concern. - Enabled the previously `describe.skip`'d "Cluster mode errors" unit test. ## 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 CHANGELOG: `parseError` now correctly extracts message, code, and type from nested exceptions returned by the server in cluster mode. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Peter Leonov Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../client-common/__tests__/unit/error.test.ts | 3 +-- packages/client-common/src/error/error.ts | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/client-common/__tests__/unit/error.test.ts b/packages/client-common/__tests__/unit/error.test.ts index 4d8e14fb..f8d2cb56 100644 --- a/packages/client-common/__tests__/unit/error.test.ts +++ b/packages/client-common/__tests__/unit/error.test.ts @@ -120,8 +120,7 @@ describe("parseError", () => { }); }); - describe.skip("Cluster mode errors", () => { - // FIXME: https://github.com/ClickHouse/clickhouse-js/issues/39 + describe("Cluster mode errors", () => { it("should work with TABLE_ALREADY_EXISTS", async () => { const message = `Code: 57. DB::Exception: There was an error on [clickhouse2:9000]: Code: 57. DB::Exception: Table default.command_test_2a751694160745f5aebe586c90b27515 already exists. (TABLE_ALREADY_EXISTS) (version 22.6.5.22 (official build)). (TABLE_ALREADY_EXISTS) (version 22.6.5.22 (official build))`; const error = parseError(message) as ClickHouseError; diff --git a/packages/client-common/src/error/error.ts b/packages/client-common/src/error/error.ts index bb37a9c5..a2c6d4f5 100644 --- a/packages/client-common/src/error/error.ts +++ b/packages/client-common/src/error/error.ts @@ -1,5 +1,19 @@ +// In cluster mode, the server wraps the original exception, repeating the +// `Code`/`Exception`/`(TYPE)` markers, e.g.: +// Code: 57. DB::Exception: There was an error on [host:9000]: Code: 57. DB::Exception: . (TABLE_ALREADY_EXISTS) (version ...). (TABLE_ALREADY_EXISTS) (version ...) +// The leading `.*Exception: ` is greedy so we anchor on the innermost (last) +// exception, while `(?.+?)` is lazy so the message stops at the first +// `(TYPE)` marker rather than swallowing the repeated suffixes. The type +// lookahead requires at least three consecutive uppercase letters so that +// parenthesised groups like `(2)` or `(official build)` are not mistaken for it. +// +// NOTE: In normal usage, the string fed to `parseError` comes from ClickHouse, +// but it may still include user-controlled fragments (e.g. parts of a query) or +// even non-ClickHouse payloads (proxy/HTML errors) when parsing failed requests. +// Keep this regex simple to avoid excessive backtracking on large/unexpected input. +// (If this ever becomes a concern, consider a non-regex parser or input length limits.) const errorRe = - /(Code|Error): (?\d+).*Exception: (?.+)\((?(?=.+[A-Z]{3})[A-Z0-9_]+?)\)/s; + /(Code|Error): (?\d+).*Exception: (?.+?)\((?(?=[A-Z0-9_]*[A-Z]{3})[A-Z0-9_]+)\)/s; interface ParsedClickHouseError { message: string; From 8f1b779fac597e44431de017b60209f810f4dda5 Mon Sep 17 00:00:00 2001 From: Peter Leonov Date: Thu, 11 Jun 2026 01:26:47 +0200 Subject: [PATCH 14/20] Require CHANGELOG updates in-PR (contributor + review-agent guidance) (#826) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Makes updating `CHANGELOG.md` a same-PR requirement rather than something deferred to release time, and teaches the Copilot review agent to enforce it. - **`AGENTS.md`** (*API quality and stability*): the CHANGELOG step is now mandatory in the same PR, with explicit formatting rules — top-most version heading (or a new `# x.y.z` heading matching the unreleased `package.json` version when the latest is already tagged), lowercase section headings (`## New features` / `## Improvements` / `## Bug fixes`), a `([#n])` PR reference link, platform scoping (Node.js-/Web-only), and a prettier pass. - **`.github/instructions/review.instructions.md`**: instructs the review agent to flag any feature/bugfix/behavior PR that doesn't touch `CHANGELOG.md` (a PR-description note is not sufficient) and to verify the entry follows the conventions. Pure refactors/tests/docs/CI are exempt. Split out of #825, where this guidance was first drafted. ## 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 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) --- .github/instructions/review.instructions.md | 4 ++++ AGENTS.md | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/instructions/review.instructions.md b/.github/instructions/review.instructions.md index 7550ddf8..84aa789b 100644 --- a/.github/instructions/review.instructions.md +++ b/.github/instructions/review.instructions.md @@ -17,6 +17,10 @@ In other cases when you've found a meaningful change to make, make it and push c When reviewing PRs to the `release` branch focus on flagging breaking changes and security issues in the broader context, as PRs to the `release` branch are normally bigger and include a cumulative change from a number of PRs. +## CHANGELOG + +Every PR that adds a feature, fixes a bug, or changes observable behavior or the public API **must update `CHANGELOG.md` in the same PR**. If such a change does not touch `CHANGELOG.md`, flag it and ask the author to add an entry (a note in the PR description alone is not sufficient). Verify the entry follows the conventions in [`AGENTS.md`](../../AGENTS.md) ("API quality and stability"): placed under the top-most version heading (a new `# x.y.z` heading matching the unreleased `package.json` version when the latest heading is already released), grouped under a lowercase section heading (`## New features` / `## Improvements` / `## Bug fixes`), and ending with a `([#])` PR reference link. Pure refactors, test-only changes, docs, and CI/tooling changes do not require a CHANGELOG entry. + ## Breaking changes Always flag major breaking changes to the public API (removed/renamed exports, changed function signatures, changed default behavior, removed configuration options, etc.) when they are not accompanied by a clear explanation in the PR description or PR discussion/code comments justifying why the break is necessary. This is important because LLMs generating code against newer client versions rely heavily on training data from older versions; undocumented breaking changes degrade their effectiveness and produce broken code for users. When such a change lacks justification, request that the author either add a clear rationale (and, where possible, a migration note) or restore backwards compatibility (e.g., via a deprecation path). diff --git a/AGENTS.md b/AGENTS.md index ee3ab526..9c0ac2b0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -113,6 +113,11 @@ For every pull request review, make sure to provide an evaluation of the followi 1. When reviewing code changes, it is important to consider the impact on the API quality and stability. For example, if the code changes involve modifying the library's public API surface (such as exported functions, classes, or types) or adding new public APIs, it is important to ensure that the changes are well-documented and do not break existing functionality for users of the library. -2. When introducing new features or making changes to the API, make sure the PR description includes a concise, human-readable CHANGELOG entry (followed by an example usage if applicable) so it can be folded into `CHANGELOG.md` at release time. This matches the PR template checklist item ("A human-readable description of the changes was provided to include in CHANGELOG"). +2. When introducing new features, fixing bugs, or making any change to observable behavior or the public API, you **must update [`CHANGELOG.md`](CHANGELOG.md) in the same PR** — do not defer it to "release time" or leave it only in the PR description. This satisfies the PR template checklist item ("A human-readable description of the changes was provided to include in CHANGELOG"). Follow the existing format exactly: + - Entries go under the **top-most version heading**. If the most recent `# x.y.z` heading corresponds to an **already-released** version (check `git tag`), open a **new** top-level `# x.y.z` heading that matches the unreleased version in `package.json` (e.g. the `version` field of [`packages/client-common/package.json`](packages/client-common/package.json)); otherwise append to the existing top heading. + - Group entries under lowercase section headings, reusing the ones already in the file: `## New features`, `## Improvements`, `## Bug fixes` (and `## Migration Notes` / `## Breaking changes` when relevant). + - Write a concise, human-readable entry, add an example usage when it helps, and end it with a PR reference link, e.g. `([#825])` plus a matching `[#825]: https://github.com/ClickHouse/clickhouse-js/pull/` reference at the bottom of the section. + - When a change is Node.js- or Web-only, say so explicitly in the entry (e.g. "(Node.js only)"). + - Run `npx prettier --write CHANGELOG.md` before committing. 3. Additionally, make sure that the official documentation is in sync with the changes. From 2353dcb2852303bd9da8204357f70cfd953fdf49 Mon Sep 17 00:00:00 2001 From: Peter Leonov Date: Thu, 11 Jun 2026 01:31:10 +0200 Subject: [PATCH 15/20] Add use_multipart_params option for large query parameters (rebase of #584) (#825) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This is a rebase of #584 onto current `main` to resolve the merge conflicts that arose after the `default_prettier` reformatting and the `node_base_connection` socket-pool refactor landed. All credit for the implementation goes to @jzila — the original commits are preserved as-authored. This PR only rebases them onto `main` and normalizes formatting to the repo's current Prettier config. See the original PR #584 for the full discussion and review history. ### Original description (from #584) - Adds a `use_multipart_params` client config option (default: `false`) that sends query parameters as `multipart/form-data` parts instead of URL query string entries - When enabled, `query()` moves the SQL into a `query` multipart part and each `param_*` entry into its own named part, avoiding URL length limits that cause HTTP 400 on ClickHouse Cloud with large arrays (25K+ values) - Uses the multipart format supported by ClickHouse since https://github.com/ClickHouse/ClickHouse/pull/40055 - A per-request `use_multipart_params` override is also supported on `BaseQueryParams` - All non-param URL search params (database, query_id, settings, session_id, role) remain in the URL as-is ## Test plan - [x] `buildMultipartBody()` unit tests (client-common) - [x] Connection-level multipart unit tests (client-node) - [x] All common unit tests pass (63 in the multipart/config suites verified locally) - [x] All Node.js unit tests pass (134 passed / 4 skipped locally) - [ ] Web unit tests (require a browser runner; not run locally) Closes #584 if merged here, or alternatively the rebased commits can be pulled back into the original fork branch. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: John Zila Co-authored-by: Claude Opus 4.8 (1M context) --- AGENTS.md | 18 ++ CHANGELOG.md | 19 ++ .../__tests__/unit/multipart.test.ts | 95 ++++++++ packages/client-common/src/client.ts | 5 + packages/client-common/src/config.ts | 8 + packages/client-common/src/connection.ts | 2 + packages/client-common/src/index.ts | 1 + packages/client-common/src/utils/index.ts | 1 + packages/client-common/src/utils/multipart.ts | 48 ++++ .../unit/node_multipart_query.test.ts | 205 ++++++++++++++++++ .../src/connection/node_base_connection.ts | 28 ++- .../unit/web_multipart_query.test.ts | 184 ++++++++++++++++ .../src/connection/web_connection.ts | 30 ++- 13 files changed, 638 insertions(+), 6 deletions(-) create mode 100644 packages/client-common/__tests__/unit/multipart.test.ts create mode 100644 packages/client-common/src/utils/multipart.ts create mode 100644 packages/client-node/__tests__/unit/node_multipart_query.test.ts create mode 100644 packages/client-web/__tests__/unit/web_multipart_query.test.ts diff --git a/AGENTS.md b/AGENTS.md index 9c0ac2b0..d3be4215 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,6 +23,24 @@ } ``` +## Package structure and code duplication + +The source packages under [`packages/`](packages) are: + +- `client-common` — platform-agnostic shared code (config, query-param formatting, multipart + assembly, URL handling, result sets, etc.). It must not depend on Node.js-only or Web-only APIs. +- `client-node` (`@clickhouse/client`) — the Node.js client. +- `client-web` (`@clickhouse/client-web`) — the Web/edge client. + +`client-node` and `client-web` are slated to be **separated into fully independent packages**. Because +of that, some logic is **intentionally duplicated** between the two connection implementations +(`packages/client-node/src/connection/node_base_connection.ts` and +`packages/client-web/src/connection/web_connection.ts`) rather than hoisted into `client-common` — for +example the per-request `use_multipart_params` resolution and the `param_*` multipart-part assembly +loop. **Do not flag this node/web duplication as something to consolidate**, and prefer keeping each +client self-contained over adding shared helpers that only exist to remove the duplication. Genuinely +platform-agnostic primitives (like `buildMultipartBody`) still belong in `client-common`. + ## Examples The repository contains an [`examples`](examples) directory that is being refactored to be AI-agent-friendly. diff --git a/CHANGELOG.md b/CHANGELOG.md index 77642b97..ca3b3405 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +# 1.21.0 + +## New features + +- Added a `use_multipart_params` client option (default: `false`). When enabled, `query()` sends `query_params` as `multipart/form-data` body parts (with the SQL moved into a `query` part) instead of URL query-string entries, avoiding HTTP 400 errors caused by over-long URLs when parameters contain large arrays (25K+ values). All other URL search params (database, query_id, settings, session_id, role) remain in the URL. Supported on both `@clickhouse/client` and `@clickhouse/client-web`, and overridable per request via `use_multipart_params` on `query()`. ([#825]) + +```ts +const client = createClient({ use_multipart_params: true }); + +await client.query({ + query: "SELECT * FROM events WHERE id IN {ids:Array(UInt64)}", + query_params: { ids: veryLargeArrayOfIds }, + // Per-request override is also supported: + // use_multipart_params: false, +}); +``` + +[#825]: https://github.com/ClickHouse/clickhouse-js/pull/825 + # 1.20.0 ## Migration Notes diff --git a/packages/client-common/__tests__/unit/multipart.test.ts b/packages/client-common/__tests__/unit/multipart.test.ts new file mode 100644 index 00000000..45b25b87 --- /dev/null +++ b/packages/client-common/__tests__/unit/multipart.test.ts @@ -0,0 +1,95 @@ +import { describe, it, expect } from "vitest"; +import { buildMultipartBody } from "../../src/utils/multipart"; + +describe("buildMultipartBody", () => { + const boundary = "----test-boundary"; + + it("should build a multipart body with one part", () => { + const result = buildMultipartBody({ query: "SELECT 1" }, boundary); + + expect(result).toMatchInlineSnapshot(` + "------test-boundary + Content-Disposition: form-data; name="query" + + SELECT 1 + ------test-boundary-- + " + `); + }); + + it("should build a multipart body with multiple parts", () => { + const result = buildMultipartBody( + { + query: "SELECT * FROM t WHERE a = {x:UInt32} AND b = {y:String}", + param_x: "42", + param_y: "test", + }, + boundary, + ); + + expect(result).toMatchInlineSnapshot(` + "------test-boundary + Content-Disposition: form-data; name="query" + + SELECT * FROM t WHERE a = {x:UInt32} AND b = {y:String} + ------test-boundary + Content-Disposition: form-data; name="param_x" + + 42 + ------test-boundary + Content-Disposition: form-data; name="param_y" + + test + ------test-boundary-- + " + `); + }); + + it("should handle empty parts record", () => { + const result = buildMultipartBody({}, boundary); + + expect(result).toMatchInlineSnapshot(` + "------test-boundary-- + " + `); + }); + + it("should return a string", () => { + const result = buildMultipartBody({ query: "SELECT 1" }, boundary); + expect(typeof result).toBe("string"); + }); + + it("should reject part names with special characters", () => { + expect(() => buildMultipartBody({ "bad name": "value" }, boundary)).toThrow( + 'Invalid multipart part name: "bad name"', + ); + }); + + it("should reject part names with quotes", () => { + expect(() => + buildMultipartBody({ 'key"injection': "value" }, boundary), + ).toThrow('Invalid multipart part name: "key"injection"'); + }); + + it("should reject part names with newlines", () => { + expect(() => + buildMultipartBody({ "key\r\ninjection": "value" }, boundary), + ).toThrow(/Invalid multipart part name/); + }); + + it("should accept part names with underscores and digits", () => { + const result = buildMultipartBody({ param_my_value_123: "ok" }, boundary); + + expect(result).toContain( + `Content-Disposition: form-data; name="param_my_value_123"\r\n`, + ); + }); + + it("should accept part names with hyphens and dots", () => { + const result = buildMultipartBody({ "param_my-key.name": "ok" }, boundary); + + expect(result).toContain( + `Content-Disposition: form-data; name="param_my-key.name"\r\n`, + ); + }); +}); diff --git a/packages/client-common/src/client.ts b/packages/client-common/src/client.ts index 6ccb678e..415e7728 100644 --- a/packages/client-common/src/client.ts +++ b/packages/client-common/src/client.ts @@ -53,6 +53,10 @@ export interface BaseQueryParams { * Overrides the headers set in {@link BaseClickHouseClientConfigOptions.http_headers}. * @default empty object */ http_headers?: Record; + /** When defined, overrides {@link BaseClickHouseClientConfigOptions.use_multipart_params} + * for this particular request. + * @default undefined (no override) */ + use_multipart_params?: boolean; } export interface QueryParams extends BaseQueryParams { @@ -390,6 +394,7 @@ export class ClickHouseClient { role: params.role ?? this.role, auth: params.auth, http_headers: params.http_headers, + use_multipart_params: params.use_multipart_params, }; } } diff --git a/packages/client-common/src/config.ts b/packages/client-common/src/config.ts index 78b26a40..02021e8c 100644 --- a/packages/client-common/src/config.ts +++ b/packages/client-common/src/config.ts @@ -93,6 +93,13 @@ export interface BaseClickHouseClientConfigOptions { * Defaults to using standard `JSON.parse` and `JSON.stringify` */ json?: Partial; + /** When true, query() sends query_params as multipart/form-data parts + * instead of URL query string entries. This avoids HTTP URL length limits + * when query parameters contain large arrays (25K+ values). + * The SQL query is also moved into a multipart part named "query". + * All other URL search params (database, query_id, settings, etc.) remain in the URL. + * @default false */ + use_multipart_params?: boolean; } export type MakeConnection< @@ -287,6 +294,7 @@ export function getConnectionParams( ...defaultJSONHandling, ...config.json, }, + ...(config.use_multipart_params ? { use_multipart_params: true } : {}), }; } diff --git a/packages/client-common/src/connection.ts b/packages/client-common/src/connection.ts index adc8444f..e99afdf0 100644 --- a/packages/client-common/src/connection.ts +++ b/packages/client-common/src/connection.ts @@ -25,6 +25,7 @@ export interface ConnectionParams { http_headers?: Record; auth: ConnectionAuth; json?: JSONHandling; + use_multipart_params?: boolean; } export interface CompressionSettings { @@ -42,6 +43,7 @@ export interface ConnBaseQueryParams { auth?: { username: string; password: string } | { access_token: string }; role?: string | Array; http_headers?: Record; + use_multipart_params?: boolean; } export type ConnPingParams = { select: boolean } & Omit< diff --git a/packages/client-common/src/index.ts b/packages/client-common/src/index.ts index cb6ad853..b2ef1841 100644 --- a/packages/client-common/src/index.ts +++ b/packages/client-common/src/index.ts @@ -140,6 +140,7 @@ export { EXCEPTION_TAG_HEADER_NAME, isSuccessfulResponse, sleep, + buildMultipartBody, toSearchParams, transformUrl, withCompressionHeaders, diff --git a/packages/client-common/src/utils/index.ts b/packages/client-common/src/utils/index.ts index d46feec5..ec1357b2 100644 --- a/packages/client-common/src/utils/index.ts +++ b/packages/client-common/src/utils/index.ts @@ -1,4 +1,5 @@ export * from "./connection"; +export * from "./multipart"; export * from "./sleep"; export * from "./stream"; export * from "./url"; diff --git a/packages/client-common/src/utils/multipart.ts b/packages/client-common/src/utils/multipart.ts new file mode 100644 index 00000000..c9dbf181 --- /dev/null +++ b/packages/client-common/src/utils/multipart.ts @@ -0,0 +1,48 @@ +const SAFE_PART_NAME = /^[A-Za-z0-9_.-]+$/; + +/** + * Builds a multipart/form-data body from a record of named string parts. + * + * Example output: + * + * --BOUNDARY\r\n + * Content-Disposition: form-data; name="query"\r\n + * \r\n + * SELECT * FROM t WHERE x IN {values:Array(String)}\r\n + * --BOUNDARY\r\n + * Content-Disposition: form-data; name="param_values"\r\n + * \r\n + * ['a@b.com','c@d.com']\r\n + * --BOUNDARY--\r\n + */ +export function buildMultipartBody( + parts: Record, + boundary: string, +): string { + const chunks: string[] = []; + + // Part names are validated against SAFE_PART_NAME to prevent header injection + // (a name could otherwise smuggle CRLF or quotes into the Content-Disposition + // line). Part values are intentionally NOT validated/escaped: the only way a + // value could forge a part delimiter is by containing the boundary, and the + // boundary is a random UUID generated per request by the caller, so it cannot + // be predicted or collided with by user-supplied input. + for (const [name, value] of Object.entries(parts)) { + if (!SAFE_PART_NAME.test(name)) { + throw new Error( + `Invalid multipart part name: "${name}". ` + + `Part names must match ${SAFE_PART_NAME}.`, + ); + } + chunks.push( + `--${boundary}\r\n` + + `Content-Disposition: form-data; name="${name}"\r\n` + + `\r\n` + + `${value}\r\n`, + ); + } + + chunks.push(`--${boundary}--\r\n`); + + return chunks.join(""); +} diff --git a/packages/client-node/__tests__/unit/node_multipart_query.test.ts b/packages/client-node/__tests__/unit/node_multipart_query.test.ts new file mode 100644 index 00000000..96e4eb2e --- /dev/null +++ b/packages/client-node/__tests__/unit/node_multipart_query.test.ts @@ -0,0 +1,205 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import Http from "http"; +import { + buildHttpConnection, + emitResponseBody, + stubClientRequest, +} from "../utils/http_stubs"; + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe("[Node.js] Multipart query params", () => { + describe("when use_multipart_params is true", () => { + it("should move query_params from URL into multipart body parts", async () => { + const adapter = buildHttpConnection({ + use_multipart_params: true, + }); + + const request = stubClientRequest(); + const httpRequestStub = vi + .spyOn(Http, "request") + .mockReturnValue(request); + + const query = "SELECT * FROM t WHERE x IN {values:Array(String)}"; + const selectPromise = adapter.query({ + query, + query_params: { + values: ["a@b.com", "c@d.com"], + }, + }); + await emitResponseBody(request, "ok"); + await selectPromise; + + // URL should NOT contain param_values + const [url, options] = httpRequestStub.mock.calls[0]; + expect(url.search).not.toContain("param_values"); + expect(url.search).toContain("query_id="); + + // Content-Type should be multipart/form-data with a boundary + expect(options.headers["Content-Type"]).toMatch( + /^multipart\/form-data; boundary=/, + ); + }); + + it("should set Content-Type with a boundary when using multipart", async () => { + const adapter = buildHttpConnection({ + use_multipart_params: true, + }); + + const request = stubClientRequest(); + const httpRequestStub = vi + .spyOn(Http, "request") + .mockReturnValue(request); + + const selectPromise = adapter.query({ + query: "SELECT * FROM t WHERE x = {v:String}", + query_params: { v: "hello" }, + }); + await emitResponseBody(request, "ok"); + await selectPromise; + + const [_url, options] = httpRequestStub.mock.calls[0]; + const contentType = options.headers["Content-Type"] as string; + expect(contentType).toBeDefined(); + // Boundary should contain the clickhouse-js prefix + expect(contentType).toMatch( + /^multipart\/form-data; boundary=----clickhouse-js-/, + ); + }); + + it("should keep non-param search params (database, query_id, session_id, settings) in URL", async () => { + const adapter = buildHttpConnection({ + database: "my_db", + compression: { + decompress_response: false, + compress_request: false, + }, + use_multipart_params: true, + }); + + const request = stubClientRequest(); + const httpRequestStub = vi + .spyOn(Http, "request") + .mockReturnValue(request); + + const selectPromise = adapter.query({ + query: "SELECT {v:Int32}", + query_params: { v: 42 }, + session_id: "my-session", + clickhouse_settings: { + extremes: 1, + }, + }); + await emitResponseBody(request, "ok"); + await selectPromise; + + const [url] = httpRequestStub.mock.calls[0]; + const search = url.search as string; + expect(search).toContain("query_id="); + expect(search).toContain("database=my_db"); + expect(search).toContain("extremes=1"); + expect(search).toContain("session_id=my-session"); + // param_v should NOT be in URL + expect(search).not.toContain("param_v"); + }); + + it("should not include param entries in URL when multiple params are provided", async () => { + const adapter = buildHttpConnection({ + use_multipart_params: true, + }); + + const request = stubClientRequest(); + const httpRequestStub = vi + .spyOn(Http, "request") + .mockReturnValue(request); + + const selectPromise = adapter.query({ + query: "SELECT {a:Int32}, {b:String}, {c:Array(String)}", + query_params: { + a: 1, + b: "two", + c: ["x", "y", "z"], + }, + }); + await emitResponseBody(request, "ok"); + await selectPromise; + + const [url] = httpRequestStub.mock.calls[0]; + const search = url.search as string; + expect(search).not.toContain("param_a"); + expect(search).not.toContain("param_b"); + expect(search).not.toContain("param_c"); + }); + + it("should fall back to normal behavior when query_params is undefined", async () => { + const adapter = buildHttpConnection({ + use_multipart_params: true, + }); + + const request = stubClientRequest(); + const httpRequestStub = vi + .spyOn(Http, "request") + .mockReturnValue(request); + + const selectPromise = adapter.query({ + query: "SELECT 1", + }); + await emitResponseBody(request, "ok"); + await selectPromise; + + const [_url, options] = httpRequestStub.mock.calls[0]; + // Should NOT use multipart when there are no query_params + expect(options.headers["Content-Type"]).toBeUndefined(); + }); + + it("should fall back to normal behavior when query_params is empty object", async () => { + const adapter = buildHttpConnection({ + use_multipart_params: true, + }); + + const request = stubClientRequest(); + const httpRequestStub = vi + .spyOn(Http, "request") + .mockReturnValue(request); + + const selectPromise = adapter.query({ + query: "SELECT 1", + query_params: {}, + }); + await emitResponseBody(request, "ok"); + await selectPromise; + + const [_url, options] = httpRequestStub.mock.calls[0]; + // Should NOT use multipart when query_params is empty + expect(options.headers["Content-Type"]).toBeUndefined(); + }); + }); + + describe("when use_multipart_params is false (default)", () => { + it("should send query_params as URL search params", async () => { + const adapter = buildHttpConnection({ + use_multipart_params: false, + }); + + const request = stubClientRequest(); + const httpRequestStub = vi + .spyOn(Http, "request") + .mockReturnValue(request); + + const selectPromise = adapter.query({ + query: "SELECT {v:Int32}", + query_params: { v: 42 }, + }); + await emitResponseBody(request, "ok"); + await selectPromise; + + const [url, options] = httpRequestStub.mock.calls[0]; + // param_v should be in URL + expect(url.search).toContain("param_v=42"); + // Should NOT set multipart content type + expect(options.headers["Content-Type"]).toBeUndefined(); + }); + }); +}); diff --git a/packages/client-node/src/connection/node_base_connection.ts b/packages/client-node/src/connection/node_base_connection.ts index 360a7fe3..b198b27a 100644 --- a/packages/client-node/src/connection/node_base_connection.ts +++ b/packages/client-node/src/connection/node_base_connection.ts @@ -14,6 +14,8 @@ import type { ResponseHeaders, } from "@clickhouse/client-common"; import { + buildMultipartBody, + formatQueryParams, isCredentialsAuth, isJWTAuth, toSearchParams, @@ -184,9 +186,16 @@ export abstract class NodeBaseConnection implements Connection params.clickhouse_settings, this.params.compression.decompress_response, ); + + const useMultipart = + (params.use_multipart_params ?? this.params.use_multipart_params) && + params.query_params !== undefined && + Object.keys(params.query_params).length > 0; + const searchParams = toSearchParams({ database: this.params.database, - query_params: params.query_params, + // When using multipart, query_params are sent in the multipart body + query_params: useMultipart ? undefined : params.query_params, session_id: params.session_id, clickhouse_settings, query_id, @@ -196,15 +205,28 @@ export abstract class NodeBaseConnection implements Connection // allows enforcing the compression via the settings even if the client instance has it disabled const enableResponseCompression = clickhouse_settings.enable_http_compression === 1; + + let body: string = params.query; + const headers = this.buildRequestHeaders(params); + if (useMultipart && params.query_params !== undefined) { + const boundary = `----clickhouse-js-${crypto.randomUUID()}`; + const parts: Record = { query: params.query }; + for (const [key, value] of Object.entries(params.query_params)) { + parts[`param_${key}`] = formatQueryParams({ value }); + } + body = buildMultipartBody(parts, boundary); + headers["Content-Type"] = `multipart/form-data; boundary=${boundary}`; + } + try { const { response_headers, stream, http_status_code } = await this.request( { method: "POST", url: transformUrl({ url: this.params.url, searchParams }), - body: params.query, + body, abort_signal: controller.signal, enable_response_compression: enableResponseCompression, - headers: this.buildRequestHeaders(params), + headers, query: params.query, query_id, log_writer, diff --git a/packages/client-web/__tests__/unit/web_multipart_query.test.ts b/packages/client-web/__tests__/unit/web_multipart_query.test.ts new file mode 100644 index 00000000..e056f720 --- /dev/null +++ b/packages/client-web/__tests__/unit/web_multipart_query.test.ts @@ -0,0 +1,184 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { ClickHouseLogLevel, LogWriter } from "@clickhouse/client-common"; +import { TestLogger } from "../../../client-common/__tests__/utils/test_logger"; +import { WebConnection, type WebConnectionParams } from "../../src/connection"; + +beforeEach(() => { + vi.clearAllMocks(); +}); + +// A minimal fetch stub that always resolves with a successful, empty response +// and records the (url, init) it was called with. +function stubFetch() { + return vi.fn(async () => ({ + status: 200, + body: null, + headers: new Headers({ "x-clickhouse-query-id": "test-query-id" }), + })) as unknown as ReturnType & typeof fetch; +} + +function buildWebConnection( + config: Partial & { fetch: typeof fetch }, +) { + return new WebConnection({ + url: new URL("https://localhost:8443"), + request_timeout: 30_000, + compression: { + decompress_response: false, + compress_request: false, + }, + max_open_connections: 10, + auth: { username: "default", password: "", type: "Credentials" }, + database: "default", + clickhouse_settings: {}, + log_writer: new LogWriter( + new TestLogger(), + "WebConnectionTest", + ClickHouseLogLevel.OFF, + ), + log_level: ClickHouseLogLevel.OFF, + keep_alive: { enabled: false }, + ...config, + }); +} + +// Helper: the recorded fetch call as [url, init]. +function lastFetchCall(fetchStub: ReturnType) { + const [url, init] = fetchStub.mock.calls[0] as [string, RequestInit]; + const headers = init.headers as Record; + return { url, init, headers }; +} + +describe("[Web] Multipart query params", () => { + describe("when use_multipart_params is true", () => { + it("should move query_params from URL into the multipart body", async () => { + const fetchStub = stubFetch(); + const adapter = buildWebConnection({ + use_multipart_params: true, + fetch: fetchStub, + }); + + await adapter.query({ + query: "SELECT * FROM t WHERE x IN {values:Array(String)}", + query_params: { values: ["a@b.com", "c@d.com"] }, + }); + + const { url, headers } = lastFetchCall(fetchStub); + // params live in the body, not the URL + expect(url).not.toContain("param_values"); + expect(url).toContain("query_id="); + // Content-Type announces multipart with a clickhouse-js boundary + expect(headers["Content-Type"]).toMatch( + /^multipart\/form-data; boundary=----clickhouse-js-/, + ); + }); + + it("should keep non-param search params (database, query_id, session_id, settings) in URL", async () => { + const fetchStub = stubFetch(); + const adapter = buildWebConnection({ + database: "my_db", + use_multipart_params: true, + fetch: fetchStub, + }); + + await adapter.query({ + query: "SELECT {v:Int32}", + query_params: { v: 42 }, + session_id: "my-session", + clickhouse_settings: { extremes: 1 }, + }); + + const { url } = lastFetchCall(fetchStub); + expect(url).toContain("query_id="); + expect(url).toContain("database=my_db"); + expect(url).toContain("extremes=1"); + expect(url).toContain("session_id=my-session"); + expect(url).not.toContain("param_v"); + }); + + it("should fall back to normal behavior when query_params is undefined", async () => { + const fetchStub = stubFetch(); + const adapter = buildWebConnection({ + use_multipart_params: true, + fetch: fetchStub, + }); + + await adapter.query({ query: "SELECT 1" }); + + const { headers } = lastFetchCall(fetchStub); + expect(headers["Content-Type"]).toBeUndefined(); + }); + + it("should fall back to normal behavior when query_params is empty object", async () => { + const fetchStub = stubFetch(); + const adapter = buildWebConnection({ + use_multipart_params: true, + fetch: fetchStub, + }); + + await adapter.query({ query: "SELECT 1", query_params: {} }); + + const { headers } = lastFetchCall(fetchStub); + expect(headers["Content-Type"]).toBeUndefined(); + }); + }); + + describe("when use_multipart_params is false (default)", () => { + it("should send query_params as URL search params", async () => { + const fetchStub = stubFetch(); + const adapter = buildWebConnection({ + use_multipart_params: false, + fetch: fetchStub, + }); + + await adapter.query({ + query: "SELECT {v:Int32}", + query_params: { v: 42 }, + }); + + const { url, headers } = lastFetchCall(fetchStub); + expect(url).toContain("param_v=42"); + expect(headers["Content-Type"]).toBeUndefined(); + }); + }); + + describe("per-request use_multipart_params override", () => { + it("should enable multipart for a request when the client default is false", async () => { + const fetchStub = stubFetch(); + const adapter = buildWebConnection({ + use_multipart_params: false, + fetch: fetchStub, + }); + + await adapter.query({ + query: "SELECT {v:Int32}", + query_params: { v: 42 }, + use_multipart_params: true, + }); + + const { url, headers } = lastFetchCall(fetchStub); + expect(url).not.toContain("param_v"); + expect(headers["Content-Type"]).toMatch( + /^multipart\/form-data; boundary=/, + ); + }); + + it("should disable multipart for a request when the client default is true", async () => { + const fetchStub = stubFetch(); + const adapter = buildWebConnection({ + use_multipart_params: true, + fetch: fetchStub, + }); + + await adapter.query({ + query: "SELECT {v:Int32}", + query_params: { v: 42 }, + use_multipart_params: false, + }); + + const { url, headers } = lastFetchCall(fetchStub); + expect(url).toContain("param_v=42"); + expect(headers["Content-Type"]).toBeUndefined(); + }); + }); +}); diff --git a/packages/client-web/src/connection/web_connection.ts b/packages/client-web/src/connection/web_connection.ts index 3bc0b97c..9f02a372 100644 --- a/packages/client-web/src/connection/web_connection.ts +++ b/packages/client-web/src/connection/web_connection.ts @@ -10,6 +10,8 @@ import type { ResponseHeaders, } from "@clickhouse/client-common"; import { + buildMultipartBody, + formatQueryParams, isCredentialsAuth, isJWTAuth, isSuccessfulResponse, @@ -54,18 +56,38 @@ export class WebConnection implements Connection { params.clickhouse_settings, this.params.compression.decompress_response, ); + + const useMultipart = + (params.use_multipart_params ?? this.params.use_multipart_params) && + params.query_params !== undefined && + Object.keys(params.query_params).length > 0; + const searchParams = toSearchParams({ database: this.params.database, clickhouse_settings, - query_params: params.query_params, + query_params: useMultipart ? undefined : params.query_params, session_id: params.session_id, role: params.role, query_id, }); + + let body: string = params.query; + const headers = this.defaultHeadersWithOverride(params); + if (useMultipart && params.query_params !== undefined) { + const boundary = `----clickhouse-js-${crypto.randomUUID()}`; + const multipartParts: Record = { query: params.query }; + for (const [key, value] of Object.entries(params.query_params)) { + multipartParts[`param_${key}`] = formatQueryParams({ value }); + } + body = buildMultipartBody(multipartParts, boundary); + headers["Content-Type"] = `multipart/form-data; boundary=${boundary}`; + } + const response = await this.request({ - body: params.query, + body, params, searchParams, + headers, }); return { query_id, @@ -162,12 +184,14 @@ export class WebConnection implements Connection { searchParams, pathname, method, + headers: prebuiltHeaders, }: { body: string | null; params?: ConnBaseQueryParams; searchParams?: URLSearchParams; pathname?: string; method?: "GET" | "POST"; + headers?: Record; }): Promise { const url = transformUrl({ url: this.params.url, @@ -193,7 +217,7 @@ export class WebConnection implements Connection { try { const headers = withCompressionHeaders({ - headers: this.defaultHeadersWithOverride(params), + headers: prebuiltHeaders ?? this.defaultHeadersWithOverride(params), // It is not currently working as expected in all major browsers enable_request_compression: false, enable_response_compression: From 62fca64207bc95f50d5e12a75098c1eebd652ac2 Mon Sep 17 00:00:00 2001 From: Peter Leonov Date: Thu, 11 Jun 2026 01:40:58 +0200 Subject: [PATCH 16/20] fix: check the X-ClickHouse-Exception-Code for error code (#554) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Ingesting: - https://github.com/ClickHouse/clickhouse-js/pull/350 Use the `X-ClickHouse-Exception-Code` header (https://github.com/ClickHouse/ClickHouse/pull/8786) to detect ClickHouse errors even when the HTTP status is successful. Previously, such 200 responses were treated as successful and the exception text could surface as malformed result data; now the request is rejected with a parsed `ClickHouseError`, consistent with non-2xx error responses. Related issue: #332 (similar issue can happen with inserts). Changes: - **Node.js client** (`socket_pool.ts`): a response is treated as failed when the `x-clickhouse-exception-code` header is present, even with a 2xx status. - **Web client** (`web_connection.ts`): a 2xx `fetch` response with the header set is rejected via the existing `parseError` path. - **Unit tests**: in both flavors simulating a 200 response with `X-ClickHouse-Exception-Code: 395` and an exception body — Node via a local `http.Server`, Web via a mocked `fetch` injected through the client config; assert `query`/`insert`/`command`/`exec` all reject with a `ClickHouseError` (proper `code`, `type`, `message`). - **Integration test** (`packages/client-common/__tests__/integration/exception_header.test.ts`): runs against a real ClickHouse server using `send_progress_in_http_headers: 1` and `http_headers_progress_interval_ms: '100'` to reproduce the exact scenario where ClickHouse commits to HTTP 200 before an exception fires (via `throwIf` at row 10 with `sleepEachRow` and `max_block_size = 1`), then asserts the client rejects with a `ClickHouseError` carrying `code: '395'`. The test lives in `client-common` so it runs under both the Node.js and Web vitest configurations. - **CHANGELOG**: added a `1.21.0` Bug Fixes entry. ## 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: Cheskel Twersky Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- CHANGELOG.md | 8 ++ .../integration/exception_header.test.ts | 44 ++++++++++ .../unit/node_exception_header.test.ts | 80 +++++++++++++++++++ .../client-node/src/connection/socket_pool.ts | 4 +- .../unit/web_exception_header.test.ts | 70 ++++++++++++++++ .../src/connection/web_connection.ts | 5 +- 6 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 packages/client-common/__tests__/integration/exception_header.test.ts create mode 100644 packages/client-node/__tests__/unit/node_exception_header.test.ts create mode 100644 packages/client-web/__tests__/unit/web_exception_header.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ca3b3405..98ef09cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,14 @@ await client.query({ [#825]: https://github.com/ClickHouse/clickhouse-js/pull/825 +## Bug Fixes + +- The client now checks the `X-ClickHouse-Exception-Code` response header to detect server errors even when the HTTP status code indicates success. In some scenarios (for example, when an exception occurs while streaming the response progress in headers, or with certain proxy setups), ClickHouse responds with HTTP 200 but sets the `X-ClickHouse-Exception-Code` header. Previously, such responses were treated as successful, and the exception text could surface as malformed response data; now the request is rejected with a parsed `ClickHouseError` (with the proper `code` and `type`), consistent with non-2xx error responses. This applies to both the Node.js and Web clients. ([#554], supersedes [#350], related issue: [#332]) + +[#554]: https://github.com/ClickHouse/clickhouse-js/pull/554 +[#350]: https://github.com/ClickHouse/clickhouse-js/pull/350 +[#332]: https://github.com/ClickHouse/clickhouse-js/issues/332 + # 1.20.0 ## Migration Notes diff --git a/packages/client-common/__tests__/integration/exception_header.test.ts b/packages/client-common/__tests__/integration/exception_header.test.ts new file mode 100644 index 00000000..b43a2bfb --- /dev/null +++ b/packages/client-common/__tests__/integration/exception_header.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import type { ClickHouseClient } from "@clickhouse/client-common"; +import { createTestClient } from "../utils"; + +// Regression test for the X-ClickHouse-Exception-Code header check. +// +// When `send_progress_in_http_headers` is enabled, ClickHouse commits to HTTP +// 200 before the query completes. If an exception then occurs (e.g. via +// throwIf in a WHERE clause) the server cannot change the status code; instead +// it signals the failure via the `X-ClickHouse-Exception-Code` response header. +// The client must detect that header and reject with a ClickHouseError even +// though the HTTP status is 200. +// +// Repro (adapted from the original): +// curl -sS -i 'http://localhost:8123/?send_progress_in_http_headers=1&http_headers_progress_interval_ms=100' \ +// --data-binary "SELECT count() FROM numbers(20) WHERE sleepEachRow(0.2) = 0 AND NOT throwIf(number = 10, 'simulated mid-query failure') SETTINGS max_block_size = 1" +describe("X-ClickHouse-Exception-Code header (200 with exception)", () => { + let client: ClickHouseClient; + beforeEach(() => { + client = createTestClient(); + }); + afterEach(async () => { + await client.close(); + }); + + it("rejects with a ClickHouseError when X-ClickHouse-Exception-Code is set on a 200 response", async () => { + // sleepEachRow(0.05) ensures enough time passes for ClickHouse to flush the + // first progress headers (committing the HTTP 200 status) before throwIf + // fires at row 10, reproducing the "200 + exception header" scenario. + await expect( + client.query({ + query: + "SELECT count() FROM numbers(20) WHERE sleepEachRow(0.05) = 0 AND NOT throwIf(number = 10, 'simulated mid-query failure') SETTINGS max_block_size = 1", + clickhouse_settings: { + send_progress_in_http_headers: 1, + http_headers_progress_interval_ms: "100", + }, + }), + ).rejects.toMatchObject({ + code: "395", + message: expect.stringContaining("simulated mid-query failure"), + }); + }); +}); diff --git a/packages/client-node/__tests__/unit/node_exception_header.test.ts b/packages/client-node/__tests__/unit/node_exception_header.test.ts new file mode 100644 index 00000000..6e188ed1 --- /dev/null +++ b/packages/client-node/__tests__/unit/node_exception_header.test.ts @@ -0,0 +1,80 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import Http from "node:http"; +import { AddressInfo } from "node:net"; +import type Stream from "stream"; +import type { ClickHouseClient } from "@clickhouse/client-common"; +import { ClickHouseError } from "@clickhouse/client-common"; +import { createSimpleNodeTestClient } from "../utils/simple_node_client"; + +// ClickHouse can respond with HTTP 200 but still report an exception via the +// `X-ClickHouse-Exception-Code` header (e.g., when an error occurs while the +// response is being streamed). See https://github.com/ClickHouse/ClickHouse/pull/8786 +describe("[Node.js] 200 response with X-ClickHouse-Exception-Code header", () => { + const errorMessage = + "Code: 395. DB::Exception: Value passed to 'throwIf' function is non-zero: " + + "while executing 'FUNCTION throwIf(equals(number, 3) :: 1) -> throwIf(equals(number, 3))'. " + + "(FUNCTION_THROW_IF_VALUE_IS_NON_ZERO) (version 24.3.1)"; + + let server: Http.Server; + let client: ClickHouseClient; + + beforeAll(async () => { + server = Http.createServer((_req, res) => { + res.writeHead(200, { + "Content-Type": "text/plain; charset=UTF-8", + "X-ClickHouse-Exception-Code": "395", + }); + res.end(errorMessage); + }); + await new Promise((resolve) => + server.listen(0, "127.0.0.1", resolve), + ); + const { port } = server.address() as AddressInfo; + client = createSimpleNodeTestClient({ + url: `http://127.0.0.1:${port}`, + }) as unknown as ClickHouseClient; + }); + + afterAll(async () => { + await client.close(); + await new Promise((resolve, reject) => + server.close((err) => (err ? reject(err) : resolve())), + ); + }); + + it("should reject a query with a parsed ClickHouseError", async () => { + await expect( + client.query({ query: "SELECT throwIf(number = 3) FROM numbers(10)" }), + ).rejects.toEqual( + expect.objectContaining({ + code: "395", + type: "FUNCTION_THROW_IF_VALUE_IS_NON_ZERO", + message: expect.stringContaining( + "Value passed to 'throwIf' function is non-zero", + ), + }), + ); + }); + + it("should reject with a ClickHouseError instance", async () => { + await expect(client.query({ query: "SELECT 1" })).rejects.toBeInstanceOf( + ClickHouseError, + ); + }); + + it("should reject insert/command/exec as well", async () => { + await expect( + client.insert({ + table: "test", + values: [{ x: 1 }], + format: "JSONEachRow", + }), + ).rejects.toBeInstanceOf(ClickHouseError); + await expect( + client.command({ query: "OPTIMIZE TABLE test" }), + ).rejects.toBeInstanceOf(ClickHouseError); + await expect(client.exec({ query: "SELECT 1" })).rejects.toBeInstanceOf( + ClickHouseError, + ); + }); +}); diff --git a/packages/client-node/src/connection/socket_pool.ts b/packages/client-node/src/connection/socket_pool.ts index 87ee3a72..fba5c253 100644 --- a/packages/client-node/src/connection/socket_pool.ts +++ b/packages/client-node/src/connection/socket_pool.ts @@ -278,7 +278,9 @@ export class SocketPool { params.try_decompress_response_stream ?? true; const ignoreErrorResponse = params.ignore_error_response ?? false; // even if the stream decompression is disabled, we have to decompress it in case of an error - const isFailedResponse = !isSuccessfulResponse(_response.statusCode); + const isFailedResponse = + !isSuccessfulResponse(_response.statusCode) || + _response.headers["x-clickhouse-exception-code"] !== undefined; if ( tryDecompressResponseStream || (isFailedResponse && !ignoreErrorResponse) diff --git a/packages/client-web/__tests__/unit/web_exception_header.test.ts b/packages/client-web/__tests__/unit/web_exception_header.test.ts new file mode 100644 index 00000000..9c9cf26d --- /dev/null +++ b/packages/client-web/__tests__/unit/web_exception_header.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect } from "vitest"; +import { ClickHouseError } from "@clickhouse/client-common"; +import { createSimpleWebTestClient } from "../utils/simple_web_client"; + +// ClickHouse can respond with HTTP 200 but still report an exception via the +// `X-ClickHouse-Exception-Code` header (e.g., when an error occurs while the +// response is being streamed). See https://github.com/ClickHouse/ClickHouse/pull/8786 +describe("[Web] 200 response with X-ClickHouse-Exception-Code header", () => { + const errorMessage = + "Code: 395. DB::Exception: Value passed to 'throwIf' function is non-zero: " + + "while executing 'FUNCTION throwIf(equals(number, 3) :: 1) -> throwIf(equals(number, 3))'. " + + "(FUNCTION_THROW_IF_VALUE_IS_NON_ZERO) (version 24.3.1)"; + + function createClientWithMockedFetch() { + const mockedFetch: typeof fetch = async () => + new Response(errorMessage, { + status: 200, + headers: { + "Content-Type": "text/plain; charset=UTF-8", + "X-ClickHouse-Exception-Code": "395", + }, + }); + return createSimpleWebTestClient({ + url: "http://localhost:8123", + fetch: mockedFetch, + }); + } + + it("should reject a query with a parsed ClickHouseError", async () => { + const client = createClientWithMockedFetch(); + await expect( + client.query({ query: "SELECT throwIf(number = 3) FROM numbers(10)" }), + ).rejects.toEqual( + expect.objectContaining({ + code: "395", + type: "FUNCTION_THROW_IF_VALUE_IS_NON_ZERO", + message: expect.stringContaining( + "Value passed to 'throwIf' function is non-zero", + ), + }), + ); + await client.close(); + }); + + it("should reject with a ClickHouseError instance", async () => { + const client = createClientWithMockedFetch(); + await expect(client.query({ query: "SELECT 1" })).rejects.toBeInstanceOf( + ClickHouseError, + ); + await client.close(); + }); + + it("should reject insert/command/exec as well", async () => { + const client = createClientWithMockedFetch(); + await expect( + client.insert({ + table: "test", + values: [{ x: 1 }], + format: "JSONEachRow", + }), + ).rejects.toBeInstanceOf(ClickHouseError); + await expect( + client.command({ query: "OPTIMIZE TABLE test" }), + ).rejects.toBeInstanceOf(ClickHouseError); + await expect(client.exec({ query: "SELECT 1" })).rejects.toBeInstanceOf( + ClickHouseError, + ); + await client.close(); + }); +}); diff --git a/packages/client-web/src/connection/web_connection.ts b/packages/client-web/src/connection/web_connection.ts index 9f02a372..ccf9344b 100644 --- a/packages/client-web/src/connection/web_connection.ts +++ b/packages/client-web/src/connection/web_connection.ts @@ -234,7 +234,10 @@ export class WebConnection implements Connection { signal: abortController.signal, }); clearTimeout(timeout); - if (isSuccessfulResponse(response.status)) { + if ( + isSuccessfulResponse(response.status) && + !response.headers.has("x-clickhouse-exception-code") + ) { return response; } else { return Promise.reject( From 30c8af1b6b82f5360109076ba6885b2ada02444e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Jun 2026 01:44:32 +0200 Subject: [PATCH 17/20] Add zero-dependency tracer API with OTEL-compatible startActiveSpan interface (#782) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Adds an optional `tracer` config option that traces the client's lifecycle operations (`query` / `command` / `exec` / `insert` / `ping`) with a **zero-dependency** interface that is a structural subset of the OpenTelemetry `Tracer`/`Span` APIs — a raw `trace.getTracer(...)` is assignable as-is, no adapter, no casts. This iteration redesigns the surface per review feedback: `startActiveSpan()` instead of the `startSpan` + `withActiveSpan` hack, and a no-op *tracer* (not just a no-op span) assigned once at client creation. ### Interface - `ClickHouseTracer` now has a single method: `startActiveSpan(name, options, fn)` — same shape as OTEL's `Tracer.startActiveSpan(name, options, fn)` overload (assignability re-verified via `tsc` against `@opentelemetry/api`). - The optional `withActiveSpan` scope hack is removed. Active-span propagation across the operation's `await` points requires OTEL's `AsyncLocalStorageContextManager` — the default in the Node OTEL SDK. Documented in the `ClickHouseTracer` / config JSDoc and `docs/howto/tracing.md`. - Also adds a new `use_multipart_params` config option (merged from `main`) that sends `query_params` as `multipart/form-data` parts instead of URL query string entries, avoiding HTTP URL length limits for large query parameters. ### Hot path - `NoopClickHouseSpan` is superseded by `NoopClickHouseTracer` (which hands out `NoopClickHouseSpan`); the client assigns `config.tracer ?? NoopClickHouseTracer` once in the constructor, keeping call sites branch-free and monomorphic for the JIT. - `recordSpanError()` kept, moved next to the types in `client-common/src/tracing.ts`. - Tracing stays imperative and repetitive per method (golang style) — no decorators, no shared wrappers. Tracer/span exceptions are intentionally not caught. ### Spans Each operation emits one `CLIENT`-kind span (`clickhouse.query`, `clickhouse.insert`, …) with OTEL semantic-convention attributes (`db.system`, `db.namespace`, `server.address`) plus operation specifics (`clickhouse.query_id` — including server-assigned ids, `clickhouse.table`, `clickhouse.format`, `clickhouse.session_id`), OK/ERROR status, `recordException` on failure, `end()` in `finally`. ```ts import { createClient } from "@clickhouse/client"; import { trace } from "@opentelemetry/api"; // raw OTEL tracer, no adapter; spans become active across awaits when // AsyncLocalStorageContextManager is registered (Node OTEL SDK default) const client = createClient({ url: "http://localhost:8123", tracer: trace.getTracer("@clickhouse/client"), }); ``` ### Docs & example - `docs/howto/tracing.md`: full surface description, context-manager requirement, recording-tracer recipe. - `examples/node/coding/otel_tracing.ts`: runnable example passing the raw OTEL tracer directly, registering `AsyncLocalStorageContextManager` for a bare `BasicTracerProvider` (excluded from examples CI until the release ships). ## 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 Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- CHANGELOG.md | 16 + docs/howto/tracing.md | 178 ++++++++++ examples/README.md | 1 + examples/node/README.md | 3 +- examples/node/coding/otel_tracing.ts | 97 ++++++ examples/node/eslint.config.mjs | 11 +- examples/node/out.parquet | Bin 516 -> 516 bytes examples/node/package-lock.json | 87 +++++ examples/node/package.json | 3 + examples/node/tsconfig.json | 4 +- examples/node/vitest.config.ts | 3 + .../__tests__/unit/tracing.test.ts | 308 ++++++++++++++++++ packages/client-common/src/client.ts | 223 ++++++++++--- packages/client-common/src/config.ts | 23 ++ packages/client-common/src/index.ts | 11 + packages/client-common/src/tracing.ts | 164 ++++++++++ packages/client-node/src/index.ts | 12 + packages/client-web/src/index.ts | 12 + 18 files changed, 1113 insertions(+), 43 deletions(-) create mode 100644 docs/howto/tracing.md create mode 100644 examples/node/coding/otel_tracing.ts create mode 100644 packages/client-common/__tests__/unit/tracing.test.ts create mode 100644 packages/client-common/src/tracing.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 98ef09cd..c56f002d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,21 @@ await client.query({ # 1.20.0 +## New Features + +- Added an optional **tracer API** that the user can pass through the client config (`tracer`) and that gets called around key lifecycle operations (`query`, `command`, `exec`, `insert`, `ping`). The `ClickHouseTracer` interface is a structural subset of the OpenTelemetry `Tracer`/`Span` APIs, so a raw OTEL tracer (`trace.getTracer(...)`) can be passed to the client as-is - but the client itself ships no tracing dependency. Each operation runs inside `tracer.startActiveSpan(...)`, so auto-instrumented child spans nest under the ClickHouse operation spans; for OpenTelemetry, this requires the `AsyncLocalStorageContextManager` to be registered (the default in the OpenTelemetry Node.js SDK). Tracer exceptions are NOT caught, so a broken tracer will break client operations. See [`docs/howto/tracing.md`](./docs/howto/tracing.md) for the full surface description, and [`examples/node/coding/otel_tracing.ts`](./examples/node/coding/otel_tracing.ts) for a runnable Node.js example. ([#776]) + +```ts +import { createClient } from "@clickhouse/client"; +import { trace } from "@opentelemetry/api"; + +// a raw OpenTelemetry tracer is structurally compatible - no adapter needed +const client = createClient({ + url: "http://localhost:8123", + tracer: trace.getTracer("@clickhouse/client"), +}); +``` + ## Migration Notes - TypeScript: `ClickHouseLogLevel` is now exported as a literal numeric union type (`0 | 1 | 2 | 3 | 4 | 127`) instead of a TypeScript `enum` type. If you were assigning arbitrary `number` values to `ClickHouseLogLevel`, you may need to narrow/cast those values during migration. @@ -59,6 +74,7 @@ await client.query({ - 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 +[#776]: https://github.com/ClickHouse/clickhouse-js/pull/776 ## Bug Fixes diff --git a/docs/howto/tracing.md b/docs/howto/tracing.md new file mode 100644 index 00000000..65def02b --- /dev/null +++ b/docs/howto/tracing.md @@ -0,0 +1,178 @@ +# Tracing the ClickHouse client with the `tracer` API + +`@clickhouse/client` (and `@clickhouse/client-web`) ships a small, +**zero-dependency** `tracer` configuration option you can use to plug the +client's per-operation lifecycle into any tracing or metrics backend - most +notably [OpenTelemetry](https://opentelemetry.io/), but also Prometheus +counters, a plain `EventEmitter`, or your own logger. + +The tracer surface lives entirely inside the client (no extra packages on +`npm install`, nothing to add to your bundle). It is declared as a +**structural subset of the OpenTelemetry `Tracer`/`Span` APIs**, so a raw +OTEL tracer can be passed to the client **as-is** - no adapter, no casts: + +```ts +import { createClient } from "@clickhouse/client"; +import { trace } from "@opentelemetry/api"; + +const client = createClient({ + url: "http://localhost:8123", + tracer: trace.getTracer("@clickhouse/client"), +}); +``` + +## Why a structural subset instead of a built-in dependency + +OpenTelemetry's full Node.js distribution (`@opentelemetry/sdk-node` + +`@opentelemetry/sdk-metrics`) adds several megabytes of dependencies on top of +the ~500 KB OpenTelemetry API package (`@opentelemetry/api`) and is +undesirable for many users. The client therefore ships **only the type +shapes**, declared so that OTEL's real `Tracer` and `Span` satisfy them +structurally - and users who don't want tracing pay nothing for it, on disk +or at runtime. + +## Tracer surface + +```ts +import type { + ClickHouseTracer, + ClickHouseSpan, + ClickHouseSpanOptions, + ClickHouseSpanAttributes, + ClickHouseSpanStatus, +} from "@clickhouse/client"; // or '@clickhouse/client-web' + +interface ClickHouseTracer { + startActiveSpan( + name: string, + options: ClickHouseSpanOptions, + fn: (span: TSpan) => T, + ): T; +} + +interface ClickHouseSpan { + setAttributes(attributes: ClickHouseSpanAttributes): void; + setStatus(status: ClickHouseSpanStatus): void; // { code: number; message?: string } + recordException(error: Error): void; + end(): void; +} +``` + +- `startActiveSpan` has the same shape as OTEL's + `Tracer.startActiveSpan(name, options, fn)` overload; the options carry + `kind` (always `ClickHouseSpanKind.CLIENT`, value-identical to OTEL's + `SpanKind.CLIENT`, per the OTEL database semantic conventions) and the + initial `attributes`. Implementations must invoke `fn` with the new span + and return `fn`'s result untouched - the client runs the entire operation + (an `async` function) inside `fn`. +- The span only needs the four methods above; OTEL's `Span` satisfies them + as-is (its chainable `this`-returning methods are compatible with the + `void` declarations). +- Status codes are numbers, value-identical to OTEL's `SpanStatusCode`. + Non-OTEL implementations can match on the exported + `ClickHouseSpanStatusCode` constant (`UNSET: 0`, `OK: 1`, `ERROR: 2`). + +### Active-span context propagation + +Because the client's operation callback is asynchronous (the span is used +across `await` points inside `fn`), OpenTelemetry needs the +`AsyncLocalStorageContextManager` (from `@opentelemetry/context-async-hooks`) +to keep the ClickHouse operation span _active_ for the duration of the +request - that is what causes auto-instrumented child spans (e.g. from +`@opentelemetry/instrumentation-http`) to be parented under it. + +**This context manager is the default in the OpenTelemetry Node.js SDK** +(`@opentelemetry/sdk-node` / `NodeTracerProvider`), so if you use the +standard SDK setup, no extra work is needed. With a bare +`BasicTracerProvider` (e.g. in tests), register it manually: + +```ts +import { context } from "@opentelemetry/api"; +import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks"; + +context.setGlobalContextManager(new AsyncLocalStorageContextManager().enable()); +``` + +A complete, runnable version of this setup (wired to an in-memory span +exporter so you can see the emitted spans) lives in +[`examples/node/coding/otel_tracing.ts`](../../examples/node/coding/otel_tracing.ts). + +## When the tracer is called + +For every call to `query` / `command` / `exec` / `insert` / `ping` (with the +single exception of `insert` with an empty `values` array, which short-circuits +before talking to the server), the client invokes +`startActiveSpan(name, { kind, attributes }, fn)`: + +1. The name is one of `clickhouse.query`, `clickhouse.command`, + `clickhouse.exec`, `clickhouse.insert`, `clickhouse.ping` (also exported + as `ClickHouseSpanNames`); `kind` is `ClickHouseSpanKind.CLIENT`. The + initial attribute bag always includes `db.system`, `db.namespace`, + `server.address`, and - when set - `clickhouse.application`, plus + operation-specific entries such as `clickhouse.format`, + `clickhouse.table`, `clickhouse.query_id`, and `clickhouse.session_id`. +2. Inside `fn`, the network operation runs with the span as the active span + (when the context manager supports it; see above). +3. `span.setAttributes({ 'clickhouse.query_id': })` - so + you always have the final `query_id`, even when the caller did not pass one + and the connection layer generated it. +4. `span.setStatus({ code: ClickHouseSpanStatusCode.OK })` on success, or + `span.recordException(error)` immediately followed by + `span.setStatus({ code: ClickHouseSpanStatusCode.ERROR, message })` on + failure. Non-`Error` throwables are normalized to `Error` before + `recordException`. +5. `span.end()` - always, in a `finally` block. + +Tracer calls are inlined directly on the client's hot path and are **not** +wrapped in defensive try/catch - if your tracer or span throws, the exception +propagates to the caller of `query` / `command` / `exec` / `insert` / +`ping`. Make sure your tracer implementation doesn't throw. + +> **Stream lifecycle:** for `query`/`exec`, the span ends when the request +> promise settles (i.e. headers received, stream handed to the caller). The +> client does not currently emit a separate "download finished" span when the +> returned `ResultSet` stream is fully consumed; if you need that, wrap the +> stream on the caller side. + +## Recording-only tracer for tests / debugging + +```ts +import { + ClickHouseSpanStatusCode, + type ClickHouseSpan, + type ClickHouseSpanStatus, + type ClickHouseTracer, +} from "@clickhouse/client"; + +interface RecordedSpan extends ClickHouseSpan { + name: string; + attributes: Record; + status?: ClickHouseSpanStatus; + error?: Error; +} + +const recorded: RecordedSpan[] = []; +const tracer: ClickHouseTracer = { + startActiveSpan: (name, options, fn) => { + const span: RecordedSpan = { + name, + attributes: { ...options.attributes }, + setAttributes: (attrs) => Object.assign(span.attributes, attrs), + setStatus: (status) => { + span.status = + status.code === ClickHouseSpanStatusCode.UNSET ? undefined : status; + }, + recordException: (err) => { + span.error = err; + }, + end: () => {}, + }; + recorded.push(span); + return fn(span); + }, +}; +``` + +## Disabling tracing + +Omit the `tracer` option (or set it to `undefined`) and the client will not emit any spans. Internally, it uses a shared no-op tracer/span so the call sites remain monomorphic (branch-free), keeping the overhead minimal (but not strictly zero). diff --git a/examples/README.md b/examples/README.md index 36c98627..0561012c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -78,6 +78,7 @@ parameter binding, sessions, data types, and custom JSON handling. | `QBit` vector type (ClickHouse 25.10+) | [node/coding/qbit.ts](node/coding/qbit.ts) | [web/coding/qbit.ts](web/coding/qbit.ts) | | `Time` / `Time64` (ClickHouse 25.6+) | [node/coding/time_time64.ts](node/coding/time_time64.ts) | [web/coding/time_time64.ts](web/coding/time_time64.ts) | | Custom JSON `parse`/`stringify` | [node/coding/custom_json_handling.ts](node/coding/custom_json_handling.ts) | [web/coding/custom_json_handling.ts](web/coding/custom_json_handling.ts) | +| OpenTelemetry tracing via the `tracer` hooks | [node/coding/otel_tracing.ts](node/coding/otel_tracing.ts) | — | ### `performance/` — Streaming, batching, and high-throughput patterns diff --git a/examples/node/README.md b/examples/node/README.md index 48d9ee6a..345accca 100644 --- a/examples/node/README.md +++ b/examples/node/README.md @@ -7,7 +7,8 @@ Each subfolder is a self-contained corpus for one use case, suitable for backing a focused AI agent skill: - [`coding/`](coding/) — day-to-day API usage: connect, configure, ping, basic - insert/select, parameter binding, sessions, data types, custom JSON handling. + insert/select, parameter binding, sessions, data types, custom JSON handling, + and OpenTelemetry tracing via the `tracer` hooks. - [`performance/`](performance/) — async inserts, streaming with backpressure, file/Parquet streams, progress streaming, and `INSERT FROM SELECT`. Node-only. - [`troubleshooting/`](troubleshooting/) — abort/cancel, timeouts, long-running diff --git a/examples/node/coding/otel_tracing.ts b/examples/node/coding/otel_tracing.ts new file mode 100644 index 00000000..58ad240c --- /dev/null +++ b/examples/node/coding/otel_tracing.ts @@ -0,0 +1,97 @@ +// This example assumes that you have a ClickHouse server running locally +// (for example, from our root docker-compose.yml file). +// +// Demonstrates how to forward the client's per-operation lifecycle into +// OpenTelemetry via the zero-dependency `tracer` config option. +// +// The client ships only the `ClickHouseTracer` shape (no OpenTelemetry +// dependency of its own); that shape is a structural subset of the +// OpenTelemetry `Tracer`/`Span` APIs, so a raw OTEL tracer can be passed to +// the client **as-is**, with no adapter and no casts. The client runs each +// operation inside `tracer.startActiveSpan(...)`, so auto-instrumented child +// spans (e.g. from `@opentelemetry/instrumentation-http`) nest under the +// ClickHouse operation spans - provided the `AsyncLocalStorageContextManager` +// is registered (see step 1 below; the OpenTelemetry Node.js SDK registers it +// by default). +// +// To keep this example self-contained and runnable without an external +// collector, it wires up an in-memory span exporter from +// `@opentelemetry/sdk-trace-base` and prints the spans the client produced. +// +// See also: +// - `../../../docs/howto/tracing.md` - full description of the tracer surface. +import { context, SpanStatusCode } from "@opentelemetry/api"; +import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks"; +import { + BasicTracerProvider, + InMemorySpanExporter, + SimpleSpanProcessor, +} from "@opentelemetry/sdk-trace-base"; +import { createClient, type ClickHouseTracer } from "@clickhouse/client"; + +// 1. Register the AsyncLocalStorageContextManager so that the span started by +// `startActiveSpan` stays *active* across the `await` points inside the +// client operation. This is required for active-span context propagation; +// when using the full OpenTelemetry Node.js SDK (`@opentelemetry/sdk-node` +// / `NodeTracerProvider`), this context manager is the default and this +// step is unnecessary. +context.setGlobalContextManager(new AsyncLocalStorageContextManager().enable()); + +// 2. Set up a minimal, in-memory OpenTelemetry tracer provider so that the +// spans the client emits are actually recorded (the global no-op tracer +// would silently drop them). A real application would instead register an +// exporter that ships spans to its OTEL collector. +const exporter = new InMemorySpanExporter(); +const provider = new BasicTracerProvider({ + spanProcessors: [new SimpleSpanProcessor(exporter)], +}); +const otelTracer = provider.getTracer("@clickhouse/client"); + +// 3. The zero-adapter path: a raw OpenTelemetry tracer is structurally +// assignable to `ClickHouseTracer` - this compiles with no casts. +const tracer: ClickHouseTracer = otelTracer; + +// 4. Pass the tracer through the client config; from here on, every +// `query`/`command`/`exec`/`insert`/`ping` call is traced automatically. +const client = createClient({ + url: process.env["CLICKHOUSE_URL"], // defaults to 'http://localhost:8123' + password: process.env["CLICKHOUSE_PASSWORD"], // defaults to an empty string + tracer, +}); + +await client.ping(); +const rs = await client.query({ + query: "SELECT number FROM system.numbers LIMIT 3", + format: "JSONEachRow", +}); +console.info("[OtelTracing] Query result:", await rs.json()); + +await client.close(); + +// 5. Flush and inspect the spans the client produced. Each operation yields a +// single CLIENT-kind span named `clickhouse.` (also exported as +// `ClickHouseSpanNames`) carrying OTEL-style attributes such as +// `db.system`, `server.address`, and the server-assigned `clickhouse.query_id`. +await provider.forceFlush(); +const spans = exporter.getFinishedSpans(); +for (const span of spans) { + console.info("[OtelTracing] Span:", { + name: span.name, + status: SpanStatusCode[span.status.code], + "db.system": span.attributes["db.system"], + "server.address": span.attributes["server.address"], + "clickhouse.query_id": span.attributes["clickhouse.query_id"], + }); +} +await provider.shutdown(); + +const spanNames = spans.map((span) => span.name); +if ( + !spanNames.includes("clickhouse.ping") || + !spanNames.includes("clickhouse.query") +) { + throw new Error( + `[OtelTracing] Expected ping and query spans, but got: ${spanNames.join(", ")}`, + ); +} +console.info("[OtelTracing] Recorded spans:", spanNames); diff --git a/examples/node/eslint.config.mjs b/examples/node/eslint.config.mjs index b965e9e6..5c1096ae 100644 --- a/examples/node/eslint.config.mjs +++ b/examples/node/eslint.config.mjs @@ -41,6 +41,15 @@ export default defineConfig( }, // Ignore build artifacts and externals { - ignores: ["eslint.config.mjs", "coverage", "out", "dist", "node_modules"], + ignores: [ + "eslint.config.mjs", + "coverage", + "out", + "dist", + "node_modules", + // TODO: stop ignoring once @clickhouse/client@1.20.0 (which adds the + // `tracer` config option) is published to npm. + "coding/otel_tracing.ts", + ], }, ); diff --git a/examples/node/out.parquet b/examples/node/out.parquet index 6025180ae345f422360f4654612a15026ebace84..1241d75cfa13fca0e259970be971090477e2ae8e 100644 GIT binary patch delta 74 zcmZo+X<^xLh|$SR&rm@lsWdYuMZqjN)if=|(7@2hz}zAwF)<~@*fPZ+(Zt*&#oX91 c#nQyw$lTB$tnBGKGD(aa>p c$izI^Bq=S;BvDgRMuvfb;VKXZI0hL401+q?UH||9 diff --git a/examples/node/package-lock.json b/examples/node/package-lock.json index 41bacf5f..d454e293 100644 --- a/examples/node/package-lock.json +++ b/examples/node/package-lock.json @@ -12,6 +12,9 @@ "@clickhouse/client": "latest" }, "devDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^2.7.1", + "@opentelemetry/sdk-trace-base": "^2.0.0", "@types/node": "^25.2.3", "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", @@ -818,6 +821,90 @@ "@emnapi/runtime": "^1.7.1" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.7.1.tgz", + "integrity": "sha512-OPFBYuXEn1E4ja3Y6eeA7O+ZnLBNcXTV5Cgsn1VaqBZ6hC5FnpZPLBNme1LJY8ZtF4aOujPKFoeWN4ik487KuQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.1.tgz", + "integrity": "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", + "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", + "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/resources": "2.7.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@oxc-project/types": { "version": "0.127.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", diff --git a/examples/node/package.json b/examples/node/package.json index 5d3c8f03..f0f5a703 100644 --- a/examples/node/package.json +++ b/examples/node/package.json @@ -20,6 +20,9 @@ "@clickhouse/client": "latest" }, "devDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^2.7.1", + "@opentelemetry/sdk-trace-base": "^2.0.0", "@types/node": "^25.2.3", "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", diff --git a/examples/node/tsconfig.json b/examples/node/tsconfig.json index b3611c56..bec55de3 100644 --- a/examples/node/tsconfig.json +++ b/examples/node/tsconfig.json @@ -20,5 +20,7 @@ "types": ["node"] }, "include": ["./**/*.ts"], - "exclude": ["node_modules"] + // TODO: stop excluding coding/otel_tracing.ts once @clickhouse/client@1.20.0 + // (which adds the `tracer` config option) is published to npm. + "exclude": ["node_modules", "coding/otel_tracing.ts"] } diff --git a/examples/node/vitest.config.ts b/examples/node/vitest.config.ts index 8fefe7bb..ec4d6d12 100644 --- a/examples/node/vitest.config.ts +++ b/examples/node/vitest.config.ts @@ -24,6 +24,9 @@ export default defineConfig({ "schema-and-deployments/url_configuration.ts", // Duplicate of `security/read_only_user.ts` "troubleshooting/read_only_user.ts", + // TODO: stop excluding once @clickhouse/client@1.20.0 (which adds the + // `tracer` config option) is published to npm. + "coding/otel_tracing.ts", ], setupFiles: ["vitest.setup.ts"], pool: "forks", diff --git a/packages/client-common/__tests__/unit/tracing.test.ts b/packages/client-common/__tests__/unit/tracing.test.ts new file mode 100644 index 00000000..15a99a8d --- /dev/null +++ b/packages/client-common/__tests__/unit/tracing.test.ts @@ -0,0 +1,308 @@ +import { describe, it, expect } from "vitest"; +import { + ClickHouseSpanKind, + ClickHouseSpanNames, + ClickHouseSpanStatusCode, + type ClickHouseSpan, + type ClickHouseSpanAttributes, + type ClickHouseSpanOptions, + type ClickHouseSpanStatus, + type ClickHouseTracer, +} from "@clickhouse/client-common"; +import { ClickHouseClient } from "../../src/client"; +import { NoopClickHouseSpan } from "../../src/tracing"; + +class RecordedSpan implements ClickHouseSpan { + readonly initialAttributes: ClickHouseSpanAttributes; + attributes: ClickHouseSpanAttributes; + status?: ClickHouseSpanStatus; + exception?: Error; + ended = false; + + constructor( + readonly name: string, + readonly options?: ClickHouseSpanOptions, + ) { + this.initialAttributes = { ...options?.attributes }; + this.attributes = { ...options?.attributes }; + } + setAttributes(attributes: ClickHouseSpanAttributes) { + this.attributes = { ...this.attributes, ...attributes }; + } + setStatus(status: ClickHouseSpanStatus) { + this.status = status; + } + recordException(error: Error) { + this.exception = error; + } + end() { + this.ended = true; + } +} + +function createRecordingTracer(): { + tracer: ClickHouseTracer; + spans: RecordedSpan[]; +} { + const spans: RecordedSpan[] = []; + const tracer: ClickHouseTracer = { + startActiveSpan(name, options, fn) { + const span = new RecordedSpan(name, options); + spans.push(span); + return fn(span); + }, + }; + return { tracer, spans }; +} + +interface MockConnection { + query: ReturnType; + command: ReturnType; + exec: ReturnType; + insert: ReturnType; + ping: ReturnType; + close: () => Promise; +} + +function makeQuery(impl?: () => Promise) { + return ( + impl ?? + (async () => ({ + stream: {} as any, + query_id: "q-1", + response_headers: {}, + })) + ); +} +function makeCommand(impl?: () => Promise) { + return impl ?? (async () => ({ query_id: "c-1", response_headers: {} })); +} +function makeExec(impl?: () => Promise) { + return ( + impl ?? + (async () => ({ + stream: {} as any, + query_id: "e-1", + response_headers: {}, + })) + ); +} +function makeInsert(impl?: () => Promise) { + return impl ?? (async () => ({ query_id: "i-1", response_headers: {} })); +} +function makePing(impl?: () => Promise) { + return impl ?? (async () => ({ success: true })); +} + +function buildClient( + tracer: ClickHouseTracer | undefined, + overrides: Partial = {}, +): ClickHouseClient { + const connection: MockConnection = { + query: makeQuery(overrides.query), + command: makeCommand(overrides.command), + exec: makeExec(overrides.exec), + insert: makeInsert(overrides.insert), + ping: makePing(overrides.ping), + close: async () => {}, + }; + return new ClickHouseClient({ + url: "http://localhost:8123", + database: "my_db", + application: "my_app", + tracer, + impl: { + make_connection: () => connection as any, + make_result_set: ((_s, _f, q) => ({ query_id: q }) as any) as any, + values_encoder: () => + ({ + validateInsertValues: () => {}, + encodeValues: (v: any) => v, + }) as any, + }, + }); +} + +describe("tracer", () => { + it("emits a CLIENT span for query() with OK status and query_id attribute", async () => { + const { tracer, spans } = createRecordingTracer(); + const client = buildClient(tracer); + await client.query({ query: "SELECT 1", query_id: "caller-q" }); + expect(spans).toHaveLength(1); + const [span] = spans; + expect(span.name).toBe(ClickHouseSpanNames.query); + expect(span.options?.kind).toBe(ClickHouseSpanKind.CLIENT); + expect(span.initialAttributes["db.system"]).toBe("clickhouse"); + expect(span.initialAttributes["db.namespace"]).toBe("my_db"); + expect(span.initialAttributes["server.address"]).toBe("localhost:8123"); + expect(span.initialAttributes["clickhouse.application"]).toBe("my_app"); + expect(span.initialAttributes["clickhouse.format"]).toBe("JSON"); + expect(span.initialAttributes["clickhouse.query_id"]).toBe("caller-q"); + expect(span.attributes["clickhouse.query_id"]).toBe("q-1"); + expect(span.status).toEqual({ code: ClickHouseSpanStatusCode.OK }); + expect(span.exception).toBeUndefined(); + expect(span.ended).toBe(true); + }); + + it("emits the operation span via startActiveSpan", async () => { + const calls: string[] = []; + const tracer: ClickHouseTracer = { + startActiveSpan(name, _options, fn) { + calls.push(`start:${name}`); + const result = fn(NoopClickHouseSpan); + calls.push(`returned:${name}`); + return result; + }, + }; + const client = buildClient(tracer); + await client.query({ query: "SELECT 1" }); + expect(calls).toEqual([ + "start:clickhouse.query", + "returned:clickhouse.query", + ]); + }); + + it("emits a span for command() with OK status", async () => { + const { tracer, spans } = createRecordingTracer(); + const client = buildClient(tracer); + await client.command({ query: "CREATE TABLE t (a UInt8) ENGINE = Memory" }); + expect(spans).toHaveLength(1); + expect(spans[0].name).toBe(ClickHouseSpanNames.command); + expect(spans[0].attributes["clickhouse.query_id"]).toBe("c-1"); + expect(spans[0].status).toEqual({ code: ClickHouseSpanStatusCode.OK }); + expect(spans[0].ended).toBe(true); + }); + + it("emits a span for exec()", async () => { + const { tracer, spans } = createRecordingTracer(); + const client = buildClient(tracer); + await client.exec({ query: "SELECT 1" }); + expect(spans).toHaveLength(1); + expect(spans[0].name).toBe(ClickHouseSpanNames.exec); + expect(spans[0].attributes["clickhouse.query_id"]).toBe("e-1"); + expect(spans[0].status).toEqual({ code: ClickHouseSpanStatusCode.OK }); + }); + + it("emits a span for insert()", async () => { + const { tracer, spans } = createRecordingTracer(); + const client = buildClient(tracer); + await client.insert({ table: "my_table", values: [{ a: 1 }] }); + expect(spans).toHaveLength(1); + expect(spans[0].name).toBe(ClickHouseSpanNames.insert); + expect(spans[0].initialAttributes["clickhouse.table"]).toBe("my_table"); + expect(spans[0].initialAttributes["clickhouse.format"]).toBe( + "JSONCompactEachRow", + ); + expect(spans[0].attributes["clickhouse.query_id"]).toBe("i-1"); + expect(spans[0].status).toEqual({ code: ClickHouseSpanStatusCode.OK }); + }); + + it("does NOT emit an insert span when there are no rows to insert", async () => { + const { tracer, spans } = createRecordingTracer(); + const client = buildClient(tracer); + const result = await client.insert({ table: "my_table", values: [] }); + expect(result.executed).toBe(false); + expect(spans).toHaveLength(0); + }); + + it("emits a span for ping()", async () => { + const { tracer, spans } = createRecordingTracer(); + const client = buildClient(tracer); + await client.ping(); + expect(spans).toHaveLength(1); + expect(spans[0].name).toBe(ClickHouseSpanNames.ping); + expect(spans[0].initialAttributes["clickhouse.ping.select"]).toBe(false); + expect(spans[0].status).toEqual({ code: ClickHouseSpanStatusCode.OK }); + }); + + it("records the exception and sets ERROR status when an operation throws", async () => { + const { tracer, spans } = createRecordingTracer(); + const failure = new Error("boom"); + const client = buildClient(tracer, { + query: async () => { + throw failure; + }, + }); + await expect(client.query({ query: "SELECT 1" })).rejects.toThrow("boom"); + expect(spans).toHaveLength(1); + expect(spans[0].exception).toBe(failure); + expect(spans[0].status).toEqual({ + code: ClickHouseSpanStatusCode.ERROR, + message: "boom", + }); + expect(spans[0].ended).toBe(true); + }); + + it("normalizes non-Error throwables before recordException", async () => { + const { tracer, spans } = createRecordingTracer(); + const client = buildClient(tracer, { + query: async () => { + throw "string failure"; + }, + }); + await expect(client.query({ query: "SELECT 1" })).rejects.toBe( + "string failure", + ); + expect(spans[0].exception).toBeInstanceOf(Error); + expect(spans[0].exception?.message).toBe("string failure"); + expect(spans[0].status).toEqual({ + code: ClickHouseSpanStatusCode.ERROR, + message: "string failure", + }); + }); + + it("uses the no-op tracer when none is configured", async () => { + const client = buildClient(undefined); + // Must not throw. + const result = await client.query({ query: "SELECT 1" }); + expect(result).toBeDefined(); + }); + + it("propagates tracer exceptions to the caller (no defensive wrapper)", async () => { + const brokenTracer: ClickHouseTracer = { + startActiveSpan: () => { + throw new Error("start failed"); + }, + }; + const client = buildClient(brokenTracer); + await expect(client.query({ query: "SELECT 1" })).rejects.toThrow( + "start failed", + ); + }); + + it("propagates span method exceptions to the caller", async () => { + const tracer: ClickHouseTracer = { + startActiveSpan: (_name, _options, fn) => + fn({ + setAttributes: () => { + throw new Error("setAttributes failed"); + }, + setStatus: () => {}, + recordException: () => {}, + end: () => {}, + }), + }; + const client = buildClient(tracer); + await expect(client.query({ query: "SELECT 1" })).rejects.toThrow( + "setAttributes failed", + ); + }); + + it("still ends the span on success even when setStatus is a no-op", async () => { + let ended = false; + const tracer: ClickHouseTracer = { + startActiveSpan: (_name, _options, fn) => + fn({ + setAttributes: () => {}, + setStatus: () => {}, + recordException: () => {}, + end: () => { + ended = true; + }, + }), + }; + const client = buildClient(tracer); + await client.query({ query: "SELECT 1" }); + expect(ended).toBe(true); + }); +}); diff --git a/packages/client-common/src/client.ts b/packages/client-common/src/client.ts index 415e7728..ea834e4f 100644 --- a/packages/client-common/src/client.ts +++ b/packages/client-common/src/client.ts @@ -22,6 +22,14 @@ import { getConnectionParams, prepareConfigWithURL } from "./config"; import type { ConnPingResult } from "./connection"; import type { JSONHandling } from "./parse/json_handling"; import type { BaseResultSet } from "./result"; +import type { ClickHouseSpanAttributes, ClickHouseTracer } from "./tracing"; +import { + ClickHouseSpanKind, + ClickHouseSpanNames, + ClickHouseSpanStatusCode, + NoopClickHouseTracer, + recordSpanError, +} from "./tracing"; export interface BaseQueryParams { /** ClickHouse's settings that can be applied on query level. */ @@ -193,6 +201,7 @@ export class ClickHouseClient { private readonly sessionId?: string; private readonly role?: string | Array; private readonly jsonHandling: JSONHandling; + private readonly tracer: ClickHouseTracer; constructor( config: BaseClickHouseClientConfigOptions & ImplementationDetails, @@ -222,6 +231,9 @@ export class ClickHouseClient { }; this.valuesEncoder = config.impl.values_encoder(this.jsonHandling); + // Assigned once at client creation: when no tracer is configured, the + // shared no-op tracer keeps the hot path branch-free. + this.tracer = config.tracer ?? NoopClickHouseTracer; } /** @@ -247,31 +259,55 @@ export class ClickHouseClient { const format = params.format ?? "JSON"; const query = formatQuery(params.query, format); const queryParams = this.withClientQueryParams(params); - const { stream, query_id, response_headers } = await this.connection.query({ - query, - ...queryParams, - }); const { log_writer, log_level } = this.connectionParams; - return this.makeResultSet( - stream, - format, - query_id, - (err) => { - if (log_level <= ClickHouseLogLevel.ERROR) { - log_writer.error({ - err, - module: "Client", - message: "Error while processing the ResultSet.", - args: { - session_id: queryParams.session_id, - role: queryParams.role, - query_id, + return this.tracer.startActiveSpan( + ClickHouseSpanNames.query, + { + kind: ClickHouseSpanKind.CLIENT, + attributes: this.withBaseSpanAttributes({ + "clickhouse.format": format, + "clickhouse.query_id": queryParams.query_id, + "clickhouse.session_id": queryParams.session_id, + }), + }, + async (span) => { + try { + const { stream, query_id, response_headers } = + await this.connection.query({ + query, + ...queryParams, + }); + // The query_id may have been generated by the connection layer. + span.setAttributes({ "clickhouse.query_id": query_id }); + span.setStatus({ code: ClickHouseSpanStatusCode.OK }); + return this.makeResultSet( + stream, + format, + query_id, + (err) => { + if (log_level <= ClickHouseLogLevel.ERROR) { + log_writer.error({ + err, + module: "Client", + message: "Error while processing the ResultSet.", + args: { + session_id: queryParams.session_id, + role: queryParams.role, + query_id, + }, + }); + } }, - }); + response_headers, + this.jsonHandling, + ); + } catch (err) { + recordSpanError(span, err); + throw err; + } finally { + span.end(); } }, - response_headers, - this.jsonHandling, ); } @@ -288,11 +324,33 @@ export class ClickHouseClient { const query = removeTrailingSemi(params.query.trim()); const ignore_error_response = params.ignore_error_response ?? false; const queryParams = this.withClientQueryParams(params); - return await this.connection.command({ - query, - ignore_error_response, - ...queryParams, - }); + return this.tracer.startActiveSpan( + ClickHouseSpanNames.command, + { + kind: ClickHouseSpanKind.CLIENT, + attributes: this.withBaseSpanAttributes({ + "clickhouse.query_id": queryParams.query_id, + "clickhouse.session_id": queryParams.session_id, + }), + }, + async (span) => { + try { + const result = await this.connection.command({ + query, + ignore_error_response, + ...queryParams, + }); + span.setAttributes({ "clickhouse.query_id": result.query_id }); + span.setStatus({ code: ClickHouseSpanStatusCode.OK }); + return result; + } catch (err) { + recordSpanError(span, err); + throw err; + } finally { + span.end(); + } + }, + ); } /** @@ -312,13 +370,35 @@ export class ClickHouseClient { params.decompress_response_stream ?? true; const ignore_error_response = params.ignore_error_response ?? false; const queryParams = this.withClientQueryParams(params); - return await this.connection.exec({ - query, - values, - decompress_response_stream, - ignore_error_response, - ...queryParams, - }); + return this.tracer.startActiveSpan( + ClickHouseSpanNames.exec, + { + kind: ClickHouseSpanKind.CLIENT, + attributes: this.withBaseSpanAttributes({ + "clickhouse.query_id": queryParams.query_id, + "clickhouse.session_id": queryParams.session_id, + }), + }, + async (span) => { + try { + const result = await this.connection.exec({ + query, + values, + decompress_response_stream, + ignore_error_response, + ...queryParams, + }); + span.setAttributes({ "clickhouse.query_id": result.query_id }); + span.setStatus({ code: ClickHouseSpanStatusCode.OK }); + return result; + } catch (err) { + recordSpanError(span, err); + throw err; + } finally { + span.end(); + } + }, + ); } /** @@ -340,12 +420,35 @@ export class ClickHouseClient { const query = getInsertQuery(params, format); const queryParams = this.withClientQueryParams(params); - const result = await this.connection.insert({ - query, - values: this.valuesEncoder.encodeValues(params.values, format), - ...queryParams, - }); - return { ...result, executed: true }; + return this.tracer.startActiveSpan( + ClickHouseSpanNames.insert, + { + kind: ClickHouseSpanKind.CLIENT, + attributes: this.withBaseSpanAttributes({ + "clickhouse.table": params.table, + "clickhouse.format": format, + "clickhouse.query_id": queryParams.query_id, + "clickhouse.session_id": queryParams.session_id, + }), + }, + async (span) => { + try { + const result = await this.connection.insert({ + query, + values: this.valuesEncoder.encodeValues(params.values, format), + ...queryParams, + }); + span.setAttributes({ "clickhouse.query_id": result.query_id }); + span.setStatus({ code: ClickHouseSpanStatusCode.OK }); + return { ...result, executed: true }; + } catch (err) { + recordSpanError(span, err); + throw err; + } finally { + span.end(); + } + }, + ); } /** @@ -358,7 +461,30 @@ export class ClickHouseClient { * **NOTE**: Since the `/ping` endpoint does not support CORS, the Web version always uses a `SELECT` query. */ async ping(params?: PingParams): Promise { - return await this.connection.ping(params ?? { select: false }); + const select = params?.select ?? false; + return this.tracer.startActiveSpan( + ClickHouseSpanNames.ping, + { + kind: ClickHouseSpanKind.CLIENT, + attributes: this.withBaseSpanAttributes({ + "clickhouse.ping.select": select, + }), + }, + async (span) => { + try { + const result = await this.connection.ping( + params ?? { select: false }, + ); + span.setStatus({ code: ClickHouseSpanStatusCode.OK }); + return result; + } catch (err) { + recordSpanError(span, err); + throw err; + } finally { + span.end(); + } + }, + ); } /** @@ -381,6 +507,23 @@ export class ClickHouseClient { await this.close(); } + private withBaseSpanAttributes( + extra: ClickHouseSpanAttributes, + ): ClickHouseSpanAttributes { + const attrs: ClickHouseSpanAttributes = { + "db.system": "clickhouse", + "server.address": this.connectionParams.url.host, + "db.namespace": this.connectionParams.database, + }; + if (this.connectionParams.application_id !== undefined) { + attrs["clickhouse.application"] = this.connectionParams.application_id; + } + for (const [k, v] of Object.entries(extra)) { + if (v !== undefined) attrs[k] = v; + } + return attrs; + } + private withClientQueryParams(params: BaseQueryParams): BaseQueryParams { return { clickhouse_settings: { diff --git a/packages/client-common/src/config.ts b/packages/client-common/src/config.ts index 02021e8c..cc617eb9 100644 --- a/packages/client-common/src/config.ts +++ b/packages/client-common/src/config.ts @@ -6,6 +6,7 @@ import { ClickHouseLogLevel, LogWriter } from "./logger"; import { defaultJSONHandling, type JSONHandling } from "./parse/json_handling"; import type { BaseResultSet } from "./result"; import type { ClickHouseSettings } from "./settings"; +import type { ClickHouseTracer } from "./tracing"; export interface BaseClickHouseClientConfigOptions { /** @deprecated since version 1.0.0. Use {@link url} instead.
@@ -93,6 +94,28 @@ export interface BaseClickHouseClientConfigOptions { * Defaults to using standard `JSON.parse` and `JSON.stringify` */ json?: Partial; + /** + * Optional tracer called by the client around key lifecycle operations + * (`query` / `command` / `exec` / `insert` / `ping`). The interface is a + * structural subset of the OpenTelemetry `Tracer`/`Span` APIs, so a raw + * OTEL tracer (`trace.getTracer(...)`) can be passed here as-is - but the + * client itself depends on no tracing library. + * + * Each operation runs inside `tracer.startActiveSpan(name, options, fn)`. + * For OpenTelemetry, active-span context propagation across the operation's + * `await` points requires the `AsyncLocalStorageContextManager` (from + * `@opentelemetry/context-async-hooks`) to be registered; this is the + * default context manager in the OpenTelemetry Node.js SDK. + * + * Tracer calls are inlined into the client's hot path with no defensive + * wrapper: exceptions thrown by the tracer or its spans propagate to the + * caller. Make sure your tracer doesn't throw. + * + * @see {@link ClickHouseTracer} + * @default undefined (no spans are emitted; the client uses a shared no-op tracer/span to keep call sites monomorphic, + * at the cost of a small fixed per-operation overhead) + */ + tracer?: ClickHouseTracer; /** When true, query() sends query_params as multipart/form-data parts * instead of URL query string entries. This avoids HTTP URL length limits * when query parameters contain large arrays (25K+ values). diff --git a/packages/client-common/src/index.ts b/packages/client-common/src/index.ts index b2ef1841..d0f8505e 100644 --- a/packages/client-common/src/index.ts +++ b/packages/client-common/src/index.ts @@ -114,6 +114,17 @@ export { /** @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"; +export { + type ClickHouseTracer, + type ClickHouseSpan, + type ClickHouseSpanOptions, + type ClickHouseSpanAttributes, + type ClickHouseSpanStatus, + type ClickHouseSpanName, + ClickHouseSpanStatusCode, + ClickHouseSpanKind, + ClickHouseSpanNames, +} from "./tracing"; /** For implementation usage only - should not be re-exported */ export { diff --git a/packages/client-common/src/tracing.ts b/packages/client-common/src/tracing.ts new file mode 100644 index 00000000..c64f5a45 --- /dev/null +++ b/packages/client-common/src/tracing.ts @@ -0,0 +1,164 @@ +/** + * A minimal, dependency-free tracer interface that is a structural subset of + * the {@link https://opentelemetry.io/docs/specs/otel/trace/api/#tracer OpenTelemetry `Tracer` API}. + * + * The shapes below are deliberately declared so that a raw OpenTelemetry + * tracer (the object returned by `trace.getTracer(...)` from + * `@opentelemetry/api`) is assignable to {@link ClickHouseTracer} **as-is**, + * with no adapter and no casts: + * + * ```ts + * import { trace } from '@opentelemetry/api' + * const client = createClient({ tracer: trace.getTracer('clickhouse-js') }) + * ``` + * + * At the same time, the client itself imports nothing from OpenTelemetry - + * non-OTEL backends (Prometheus counters, an `EventEmitter`, a plain logger) + * can implement the same small surface directly. + * + * When a {@link ClickHouseTracer} is provided via + * {@link BaseClickHouseClientConfigOptions.tracer}, the client runs each + * tracked operation (`query`, `command`, `exec`, `insert`, `ping`) inside + * {@link ClickHouseTracer.startActiveSpan}, mutates the provided + * {@link ClickHouseSpan} during the operation + * ({@link ClickHouseSpan.setAttributes}, {@link ClickHouseSpan.setStatus}, + * {@link ClickHouseSpan.recordException}), and finally calls + * {@link ClickHouseSpan.end} when it completes (regardless of outcome). + * + * Calls are inlined directly into the client's hot path - there is no + * defensive wrapper around them. Any exception thrown by a tracer or span + * method will propagate up to the caller of the corresponding client method + * (`query`/`command`/`exec`/`insert`/`ping`). Implementations are therefore + * expected to be non-throwing; a trivial e2e test against your tracer is + * usually enough to catch regressions. + */ +export interface ClickHouseTracer< + TSpan extends ClickHouseSpan = ClickHouseSpan, +> { + /** + * Called when a tracked operation begins. Same shape as OpenTelemetry's + * `Tracer.startActiveSpan(name, options, fn)` overload: implementations + * must invoke `fn` with the new span and return `fn`'s result untouched. + * The client runs the entire operation (an `async` function) inside `fn`, + * mutates the span during the operation, and ends it exactly once. + * + * @note The callback is asynchronous under the hood: the client keeps using + * the span across `await` points inside `fn`. For OpenTelemetry, active-span + * context propagation across those `await`s requires the + * `AsyncLocalStorageContextManager` (from + * `@opentelemetry/context-async-hooks`) to be registered - which is the + * default context manager in the OpenTelemetry Node.js SDK + * (`@opentelemetry/sdk-node` / `NodeTracerProvider`). With it in place, + * auto-instrumented child spans (e.g. from + * `@opentelemetry/instrumentation-http`) are parented under the ClickHouse + * operation span. + */ + startActiveSpan( + name: string, + options: ClickHouseSpanOptions, + fn: (span: TSpan) => T, + ): T; +} + +/** Structural subset of the OpenTelemetry `Span` interface - a real OTEL + * `Span` is assignable to this type as-is. Methods are declared as + * `void`-returning, so OTEL's chainable `this`-returning methods remain + * compatible. */ +export interface ClickHouseSpan { + /** Attach additional attributes to an in-flight span. Called at least once + * for every span - typically right before {@link ClickHouseSpan.end} - + * with operation-specific attributes such as `clickhouse.query_id`. */ + setAttributes(attributes: ClickHouseSpanAttributes): void; + /** Set the logical status of the span. The codes are value-identical to + * OTEL's `SpanStatusCode`; see {@link ClickHouseSpanStatusCode}. */ + setStatus(status: ClickHouseSpanStatus): void; + /** Attach an exception that occurred during the span. Called before + * {@link ClickHouseSpan.setStatus} with the `ERROR` code, before + * {@link ClickHouseSpan.end}. Non-`Error` throwables are normalized + * to `Error` by the client before this call. */ + recordException(error: Error): void; + /** Called exactly once per span, regardless of success or failure. */ + end(): void; +} + +/** Structural subset of OTEL's `SpanOptions`. */ +export interface ClickHouseSpanOptions { + /** Value-identical to OTEL's `SpanKind`; see {@link ClickHouseSpanKind}. + * The client always passes {@link ClickHouseSpanKind.CLIENT}, per the + * OTEL database semantic conventions. */ + kind?: number; + /** Initial attributes for the span. */ + attributes?: ClickHouseSpanAttributes; +} + +/** Span status; `code` values are listed in {@link ClickHouseSpanStatusCode} + * and are value-identical to OTEL's `SpanStatusCode`. */ +export interface ClickHouseSpanStatus { + code: number; + message?: string; +} + +/** Value-identical to OTEL's `SpanStatusCode`, so non-OTEL implementations + * do not have to deal with magic numbers. */ +export const ClickHouseSpanStatusCode = { + UNSET: 0, + OK: 1, + ERROR: 2, +} as const; + +/** Value-identical to OTEL's `SpanKind`. The client only ever uses + * {@link ClickHouseSpanKind.CLIENT}. */ +export const ClickHouseSpanKind = { + INTERNAL: 0, + SERVER: 1, + CLIENT: 2, + PRODUCER: 3, + CONSUMER: 4, +} as const; + +/** Free-form attribute bag; a subset of OTEL's `Attributes`. Implementations + * should be tolerant of `undefined` values (skip them) and stringify + * non-primitive values as needed. */ +export type ClickHouseSpanAttributes = Record< + string, + string | number | boolean | undefined +>; + +/** Span name constants used by the client when starting spans. + * Exposed so that adapters and tests can match on them. */ +export const ClickHouseSpanNames = { + query: "clickhouse.query", + command: "clickhouse.command", + exec: "clickhouse.exec", + insert: "clickhouse.insert", + ping: "clickhouse.ping", +} as const; +export type ClickHouseSpanName = + (typeof ClickHouseSpanNames)[keyof typeof ClickHouseSpanNames]; + +const noop = (): void => undefined; +/** Shared no-op span handed out by {@link NoopClickHouseTracer}. @internal */ +export const NoopClickHouseSpan: ClickHouseSpan = { + setAttributes: noop, + setStatus: noop, + recordException: noop, + end: noop, +}; + +/** No-op tracer assigned once at client creation when no tracer is + * configured, so the hot path stays branch-free (monomorphic call sites + * that the JIT can inline). @internal */ +export const NoopClickHouseTracer: ClickHouseTracer = { + startActiveSpan: (_name, _options, fn) => fn(NoopClickHouseSpan), +}; + +/** Records the exception on the span and marks it with the ERROR status, + * normalizing non-`Error` throwables to `Error`. */ +export function recordSpanError(span: ClickHouseSpan, err: unknown): void { + const error = err instanceof Error ? err : new Error(String(err)); + span.recordException(error); + span.setStatus({ + code: ClickHouseSpanStatusCode.ERROR, + message: error.message, + }); +} diff --git a/packages/client-node/src/index.ts b/packages/client-node/src/index.ts index 63179ffa..a73f1a03 100644 --- a/packages/client-node/src/index.ts +++ b/packages/client-node/src/index.ts @@ -55,6 +55,12 @@ export { type ClickHouseAuth, type ClickHouseJWTAuth, type ClickHouseCredentialsAuth, + type ClickHouseTracer, + type ClickHouseSpan, + type ClickHouseSpanOptions, + type ClickHouseSpanAttributes, + type ClickHouseSpanStatus, + type ClickHouseSpanName, } from "@clickhouse/client-common"; /** @@ -82,6 +88,9 @@ import { isRow as isRow_, isException as isException_, TupleParam as TupleParam_, + ClickHouseSpanNames as ClickHouseSpanNames_, + ClickHouseSpanStatusCode as ClickHouseSpanStatusCode_, + ClickHouseSpanKind as ClickHouseSpanKind_, defaultJSONHandling as defaultJSONHandling_, } from "@clickhouse/client-common"; @@ -105,4 +114,7 @@ export const isRow = isRow_; export const isException = isException_; export const TupleParam = TupleParam_; export type TupleParam = TupleParam_; +export const ClickHouseSpanNames = ClickHouseSpanNames_; +export const ClickHouseSpanStatusCode = ClickHouseSpanStatusCode_; +export const ClickHouseSpanKind = ClickHouseSpanKind_; export const defaultJSONHandling = defaultJSONHandling_; diff --git a/packages/client-web/src/index.ts b/packages/client-web/src/index.ts index 33d9dd72..b283af44 100644 --- a/packages/client-web/src/index.ts +++ b/packages/client-web/src/index.ts @@ -54,6 +54,12 @@ export { type ClickHouseAuth, type ClickHouseJWTAuth, type ClickHouseCredentialsAuth, + type ClickHouseTracer, + type ClickHouseSpan, + type ClickHouseSpanOptions, + type ClickHouseSpanAttributes, + type ClickHouseSpanStatus, + type ClickHouseSpanName, } from "@clickhouse/client-common"; /** @@ -81,6 +87,9 @@ import { isRow as isRow_, isException as isException_, TupleParam as TupleParam_, + ClickHouseSpanNames as ClickHouseSpanNames_, + ClickHouseSpanStatusCode as ClickHouseSpanStatusCode_, + ClickHouseSpanKind as ClickHouseSpanKind_, defaultJSONHandling as defaultJSONHandling_, } from "@clickhouse/client-common"; @@ -104,4 +113,7 @@ export const isRow = isRow_; export const isException = isException_; export const TupleParam = TupleParam_; export type TupleParam = TupleParam_; +export const ClickHouseSpanNames = ClickHouseSpanNames_; +export const ClickHouseSpanStatusCode = ClickHouseSpanStatusCode_; +export const ClickHouseSpanKind = ClickHouseSpanKind_; export const defaultJSONHandling = defaultJSONHandling_; From 6d2edceff78dc7d3e96a10566ded96db0841b25e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Jun 2026 20:19:52 +0200 Subject: [PATCH 18/20] Add `use_multipart_params_auto` to auto-promote oversized query params to multipart (#827) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Large `query_params` are URL-encoded into the request line, where HTTP intermediaries (nginx, AWS ALB, CloudFront) reject them with 414/400. This adds an opt-in `use_multipart_params_auto` setting (client config + per-request override, default `false`) that automatically sends `query_params` as a `multipart/form-data` body — same mechanism as `use_multipart_params` — but only when their URL-encoded length exceeds `MAX_URL_BIND_PARAM_LENGTH` (4096). `use_multipart_params` semantics are unchanged, preserving full backwards compatibility. - **`serializeQueryParamsForUrl` (client-common)** — early-return copy of the `param_*` serializer: returns the serialized URL entries, or `null` as soon as the budget is exceeded (raw-length lower bound short-circuits before the exact encoded-length check). This makes the promotion decision without serializing params twice. - **`toSearchParams`** — accepts optional pre-serialized `param_entries`; in that path it now reuses the same entries array and appends `query_id` directly (instead of spread-copying), while the default `query_params` serialization path remains unchanged. - **Node & web connections** — in auto mode, params are serialized once: `null` → multipart body, otherwise the entries are handed straight to `toSearchParams`. Forced `use_multipart_params` behaves as before. (Per-connection duplication is intentional per repo conventions.) - **Tests** — unit coverage for threshold behavior (percent-encoding expansion, non-string params, exact boundary) plus integration tests for forced and auto multipart round-trips. ```ts const client = createClient({ use_multipart_params_auto: true }) // small params → URL as usual; oversized params → multipart body, no 414 await client.query({ query: 'SELECT * FROM t WHERE id IN {ids:Array(String)}', query_params: { ids: hugeArray }, format: 'JSONEachRow', }) ``` ## 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 - [ ] 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> Co-authored-by: Peter Leonov --- CHANGELOG.md | 13 +++ .../integration/multipart_params.test.ts | 55 +++++++++++ .../__tests__/unit/multipart.test.ts | 67 ++++++++++++- packages/client-common/src/client.ts | 5 + packages/client-common/src/config.ts | 11 +++ packages/client-common/src/connection.ts | 2 + packages/client-common/src/index.ts | 2 + packages/client-common/src/utils/multipart.ts | 45 +++++++++ packages/client-common/src/utils/url.ts | 23 +++-- .../unit/node_multipart_query.test.ts | 96 +++++++++++++++++++ .../src/connection/node_base_connection.ts | 29 +++++- .../unit/web_multipart_query.test.ts | 74 ++++++++++++++ .../src/connection/web_connection.ts | 29 +++++- 13 files changed, 434 insertions(+), 17 deletions(-) create mode 100644 packages/client-common/__tests__/integration/multipart_params.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c56f002d..c40a9e86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ ## New features +- Added a `use_multipart_params_auto` client option (default: `false`). When enabled, `query()` automatically sends `query_params` as `multipart/form-data` body parts (the same mechanism as `use_multipart_params`) once their URL-encoded length exceeds 4096 characters, avoiding HTTP 414/400 errors from HTTP intermediaries (nginx, AWS ALB, CloudFront) caused by over-long URLs - for example, a large `IN` list or a high-dimensional vector embedding. Smaller parameter payloads remain in the URL query string, so existing behavior is unchanged unless the threshold is crossed. `use_multipart_params: true` still forces multipart for all queries regardless of size. This does not change the server's per-value size limit, which is governed by `http_max_field_value_size`. Supported on both `@clickhouse/client` and `@clickhouse/client-web`, and overridable per request via `use_multipart_params_auto` on `query()`. Ported from [clickhouse-connect#789](https://github.com/ClickHouse/clickhouse-connect/pull/789). ([#827]) + +```ts +const client = createClient({ use_multipart_params_auto: true }); + +await client.query({ + query: "SELECT * FROM events WHERE id IN {ids:Array(UInt64)}", + // Sent in the URL when small, auto-promoted to the multipart body when large + query_params: { ids: veryLargeArrayOfIds }, +}); +``` + - Added a `use_multipart_params` client option (default: `false`). When enabled, `query()` sends `query_params` as `multipart/form-data` body parts (with the SQL moved into a `query` part) instead of URL query-string entries, avoiding HTTP 400 errors caused by over-long URLs when parameters contain large arrays (25K+ values). All other URL search params (database, query_id, settings, session_id, role) remain in the URL. Supported on both `@clickhouse/client` and `@clickhouse/client-web`, and overridable per request via `use_multipart_params` on `query()`. ([#825]) ```ts @@ -16,6 +28,7 @@ await client.query({ ``` [#825]: https://github.com/ClickHouse/clickhouse-js/pull/825 +[#827]: https://github.com/ClickHouse/clickhouse-js/pull/827 ## Bug Fixes diff --git a/packages/client-common/__tests__/integration/multipart_params.test.ts b/packages/client-common/__tests__/integration/multipart_params.test.ts new file mode 100644 index 00000000..59699861 --- /dev/null +++ b/packages/client-common/__tests__/integration/multipart_params.test.ts @@ -0,0 +1,55 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { type ClickHouseClient } from "@clickhouse/client-common"; +import { createTestClient } from "../utils"; + +describe("multipart query params", () => { + let client: ClickHouseClient; + afterEach(async () => { + await client.close(); + }); + + const ids = [...Array(3000).keys()]; + const query = + "SELECT count() FROM numbers(5000) WHERE number IN {ids:Array(UInt64)}"; + + it("executes a query with large params when use_multipart_params is enabled", async () => { + client = createTestClient({ use_multipart_params: true }); + const rs = await client.query({ + query, + format: "CSV", + query_params: { ids }, + }); + expect(await rs.text()).toBe("3000\n"); + }); + + it("executes a query with large params when use_multipart_params_auto is enabled", async () => { + client = createTestClient({ use_multipart_params_auto: true }); + const rs = await client.query({ + query, + format: "CSV", + query_params: { ids }, + }); + expect(await rs.text()).toBe("3000\n"); + }); + + it("keeps small params working when use_multipart_params_auto is enabled", async () => { + client = createTestClient({ use_multipart_params_auto: true }); + const rs = await client.query({ + query, + format: "CSV", + query_params: { ids: [1, 2, 3] }, + }); + expect(await rs.text()).toBe("3\n"); + }); + + it("supports a per-request use_multipart_params_auto override", async () => { + client = createTestClient(); + const rs = await client.query({ + query, + format: "CSV", + query_params: { ids }, + use_multipart_params_auto: true, + }); + expect(await rs.text()).toBe("3000\n"); + }); +}); diff --git a/packages/client-common/__tests__/unit/multipart.test.ts b/packages/client-common/__tests__/unit/multipart.test.ts index 45b25b87..e52d06fd 100644 --- a/packages/client-common/__tests__/unit/multipart.test.ts +++ b/packages/client-common/__tests__/unit/multipart.test.ts @@ -1,5 +1,9 @@ import { describe, it, expect } from "vitest"; -import { buildMultipartBody } from "../../src/utils/multipart"; +import { + buildMultipartBody, + MAX_URL_BIND_PARAM_LENGTH, + serializeQueryParamsForUrl, +} from "../../src/utils/multipart"; describe("buildMultipartBody", () => { const boundary = "----test-boundary"; @@ -93,3 +97,64 @@ describe("buildMultipartBody", () => { ); }); }); + +describe("serializeQueryParamsForUrl", () => { + it("should return empty entries for empty params", () => { + expect(serializeQueryParamsForUrl({})).toEqual([]); + }); + + it("should serialize small params under the threshold", () => { + expect(serializeQueryParamsForUrl({ id: "123", name: "abc" })).toEqual([ + ["param_id", "123"], + ["param_name", "abc"], + ]); + }); + + it("should return null for a single oversized param", () => { + expect( + serializeQueryParamsForUrl({ + big: "x".repeat(MAX_URL_BIND_PARAM_LENGTH + 1), + }), + ).toBeNull(); + }); + + it("should return null for many individually small params whose total exceeds the threshold", () => { + const params: Record = {}; + for (let i = 0; i < 40; i++) { + params[`p${i}`] = "v".repeat(200); + } + expect(serializeQueryParamsForUrl(params)).toBeNull(); + }); + + it("should account for percent-encoding expansion", () => { + // Raw length is under the budget, but each ampersand encodes to three characters (%26) + expect( + serializeQueryParamsForUrl({ + s: "&".repeat(Math.ceil(MAX_URL_BIND_PARAM_LENGTH / 2)), + }), + ).toBeNull(); + }); + + it("should measure the formatted value of non-string params", () => { + // A large array formats to a long bracketed list + const ids = [...Array(2000).keys()]; + expect(serializeQueryParamsForUrl({ ids })).toBeNull(); + expect(serializeQueryParamsForUrl({ ids: [1, 2, 3] })).toEqual([ + ["param_ids", "[1,2,3]"], + ]); + }); + + it("should stay just under and flip just over the threshold", () => { + // "param_v=" prefix is 8 characters + expect( + serializeQueryParamsForUrl({ + v: "x".repeat(MAX_URL_BIND_PARAM_LENGTH - 8), + }), + ).toEqual([["param_v", "x".repeat(MAX_URL_BIND_PARAM_LENGTH - 8)]]); + expect( + serializeQueryParamsForUrl({ + v: "x".repeat(MAX_URL_BIND_PARAM_LENGTH - 7), + }), + ).toBeNull(); + }); +}); diff --git a/packages/client-common/src/client.ts b/packages/client-common/src/client.ts index ea834e4f..db6306b0 100644 --- a/packages/client-common/src/client.ts +++ b/packages/client-common/src/client.ts @@ -65,6 +65,10 @@ export interface BaseQueryParams { * for this particular request. * @default undefined (no override) */ use_multipart_params?: boolean; + /** When defined, overrides {@link BaseClickHouseClientConfigOptions.use_multipart_params_auto} + * for this particular request. + * @default undefined (no override) */ + use_multipart_params_auto?: boolean; } export interface QueryParams extends BaseQueryParams { @@ -538,6 +542,7 @@ export class ClickHouseClient { auth: params.auth, http_headers: params.http_headers, use_multipart_params: params.use_multipart_params, + use_multipart_params_auto: params.use_multipart_params_auto, }; } } diff --git a/packages/client-common/src/config.ts b/packages/client-common/src/config.ts index cc617eb9..ba6a0818 100644 --- a/packages/client-common/src/config.ts +++ b/packages/client-common/src/config.ts @@ -123,6 +123,14 @@ export interface BaseClickHouseClientConfigOptions { * All other URL search params (database, query_id, settings, etc.) remain in the URL. * @default false */ use_multipart_params?: boolean; + /** When true, query() automatically sends query_params as multipart/form-data + * parts (see {@link use_multipart_params}) once their URL-encoded length + * exceeds a threshold (4096 characters), avoiding HTTP 414/400 errors from + * over-long URLs. Smaller parameter payloads remain in the URL query string. + * Has no effect when {@link use_multipart_params} is enabled, as that always + * sends the parameters as multipart/form-data parts. + * @default false */ + use_multipart_params_auto?: boolean; } export type MakeConnection< @@ -318,6 +326,9 @@ export function getConnectionParams( ...config.json, }, ...(config.use_multipart_params ? { use_multipart_params: true } : {}), + ...(config.use_multipart_params_auto + ? { use_multipart_params_auto: true } + : {}), }; } diff --git a/packages/client-common/src/connection.ts b/packages/client-common/src/connection.ts index e99afdf0..2a23ea65 100644 --- a/packages/client-common/src/connection.ts +++ b/packages/client-common/src/connection.ts @@ -26,6 +26,7 @@ export interface ConnectionParams { auth: ConnectionAuth; json?: JSONHandling; use_multipart_params?: boolean; + use_multipart_params_auto?: boolean; } export interface CompressionSettings { @@ -44,6 +45,7 @@ export interface ConnBaseQueryParams { role?: string | Array; http_headers?: Record; use_multipart_params?: boolean; + use_multipart_params_auto?: boolean; } export type ConnPingParams = { select: boolean } & Omit< diff --git a/packages/client-common/src/index.ts b/packages/client-common/src/index.ts index d0f8505e..94a9cef6 100644 --- a/packages/client-common/src/index.ts +++ b/packages/client-common/src/index.ts @@ -152,6 +152,8 @@ export { isSuccessfulResponse, sleep, buildMultipartBody, + MAX_URL_BIND_PARAM_LENGTH, + serializeQueryParamsForUrl, toSearchParams, transformUrl, withCompressionHeaders, diff --git a/packages/client-common/src/utils/multipart.ts b/packages/client-common/src/utils/multipart.ts index c9dbf181..ec62f5e0 100644 --- a/packages/client-common/src/utils/multipart.ts +++ b/packages/client-common/src/utils/multipart.ts @@ -1,5 +1,50 @@ +import { formatQueryParams } from "../data_formatter"; + const SAFE_PART_NAME = /^[A-Za-z0-9_.-]+$/; +/** + * Query parameters are URL-encoded into the request URL as `param_*` entries. + * Once their encoded length passes this budget, {@link serializeQueryParamsForUrl} + * returns null so that they are routed through the multipart body instead, keeping + * oversized payloads out of the URL where HTTP intermediaries (nginx, AWS ALB, + * CloudFront) reject them with HTTP 414 or 400. The threshold leaves ample + * headroom under common request line limits. + */ +export const MAX_URL_BIND_PARAM_LENGTH = 4096; + +/** + * Early-return variant of the `param_*` serialization performed by + * {@link toSearchParams}: serializes {@link query_params} into `param_*` + * URL entries, returning them so the caller can reuse the result without + * serializing the params a second time, or returns null as soon as the + * URL-encoded length exceeds {@link MAX_URL_BIND_PARAM_LENGTH} — in which + * case the params should be sent as a multipart/form-data body instead. + */ +export function serializeQueryParamsForUrl( + query_params: Record, +): [string, string][] | null { + const entries: [string, string][] = []; + // Raw length is a lower bound on the encoded length, so large payloads + // short-circuit without materializing the encoded string. + let rawLength = 0; + for (const [key, value] of Object.entries(query_params)) { + const name = `param_${key}`; + const formatted = formatQueryParams({ value }); + rawLength += name.length + formatted.length; + if (rawLength > MAX_URL_BIND_PARAM_LENGTH) { + return null; + } + entries.push([name, formatted]); + } + // Measure the exact encoded length, accounting for percent-encoding expansion. + if ( + new URLSearchParams(entries).toString().length > MAX_URL_BIND_PARAM_LENGTH + ) { + return null; + } + return entries; +} + /** * Builds a multipart/form-data body from a record of named string parts. * diff --git a/packages/client-common/src/utils/url.ts b/packages/client-common/src/utils/url.ts index eb9efab2..6798adcc 100644 --- a/packages/client-common/src/utils/url.ts +++ b/packages/client-common/src/utils/url.ts @@ -34,29 +34,36 @@ interface ToSearchParamsOptions { database: string | undefined; clickhouse_settings?: ClickHouseSettings; query_params?: Record; + /** Pre-serialized `param_*` entries (e.g. from {@link serializeQueryParamsForUrl}); + * used as-is instead of serializing {@link query_params} again. */ + param_entries?: [string, string][]; query?: string; session_id?: string; query_id: string; role?: string | Array; } -// TODO validate max length of the resulting query -// https://stackoverflow.com/questions/812925/what-is-the-maximum-possible-length-of-a-query-string export function toSearchParams({ database, query, query_params, + param_entries, clickhouse_settings, session_id, query_id, role, }: ToSearchParamsOptions): URLSearchParams { - const entries: [string, string][] = [["query_id", query_id]]; - - if (query_params !== undefined) { - for (const [key, value] of Object.entries(query_params)) { - const formattedParam = formatQueryParams({ value }); - entries.push([`param_${key}`, formattedParam]); + let entries: [string, string][]; + if (param_entries) { + entries = param_entries; + entries.push(["query_id", query_id]); + } else { + entries = [["query_id", query_id]]; + if (query_params !== undefined) { + for (const [key, value] of Object.entries(query_params)) { + const formattedParam = formatQueryParams({ value }); + entries.push([`param_${key}`, formattedParam]); + } } } diff --git a/packages/client-node/__tests__/unit/node_multipart_query.test.ts b/packages/client-node/__tests__/unit/node_multipart_query.test.ts index 96e4eb2e..ebd12b5f 100644 --- a/packages/client-node/__tests__/unit/node_multipart_query.test.ts +++ b/packages/client-node/__tests__/unit/node_multipart_query.test.ts @@ -202,4 +202,100 @@ describe("[Node.js] Multipart query params", () => { expect(options.headers["Content-Type"]).toBeUndefined(); }); }); + + describe("when use_multipart_params_auto is true", () => { + it("should keep small query_params in the URL", async () => { + const adapter = buildHttpConnection({ + use_multipart_params_auto: true, + }); + + const request = stubClientRequest(); + const httpRequestStub = vi + .spyOn(Http, "request") + .mockReturnValue(request); + + const selectPromise = adapter.query({ + query: "SELECT {v:Int32}", + query_params: { v: 42 }, + }); + await emitResponseBody(request, "ok"); + await selectPromise; + + const [url, options] = httpRequestStub.mock.calls[0]; + expect(url.search).toContain("param_v=42"); + expect(options.headers["Content-Type"]).toBeUndefined(); + }); + + it("should promote large query_params to a multipart body", async () => { + const adapter = buildHttpConnection({ + use_multipart_params_auto: true, + }); + + const request = stubClientRequest(); + const httpRequestStub = vi + .spyOn(Http, "request") + .mockReturnValue(request); + + const ids = [...Array(3000).keys()]; + const selectPromise = adapter.query({ + query: + "SELECT count() FROM numbers(5000) WHERE number IN {ids:Array(UInt64)}", + query_params: { ids }, + }); + await emitResponseBody(request, "ok"); + await selectPromise; + + const [url, options] = httpRequestStub.mock.calls[0]; + expect(url.search).not.toContain("param_ids"); + expect(url.search).toContain("query_id="); + expect(options.headers["Content-Type"]).toMatch( + /^multipart\/form-data; boundary=/, + ); + }); + + it("should respect a per-request use_multipart_params_auto override", async () => { + const adapter = buildHttpConnection({}); + + const request = stubClientRequest(); + const httpRequestStub = vi + .spyOn(Http, "request") + .mockReturnValue(request); + + const selectPromise = adapter.query({ + query: "SELECT {big:String}", + query_params: { big: "x".repeat(5000) }, + use_multipart_params_auto: true, + }); + await emitResponseBody(request, "ok"); + await selectPromise; + + const [url, options] = httpRequestStub.mock.calls[0]; + expect(url.search).not.toContain("param_big"); + expect(options.headers["Content-Type"]).toMatch( + /^multipart\/form-data; boundary=/, + ); + }); + }); + + describe("when use_multipart_params_auto is false (default)", () => { + it("should keep large query_params in the URL", async () => { + const adapter = buildHttpConnection({}); + + const request = stubClientRequest(); + const httpRequestStub = vi + .spyOn(Http, "request") + .mockReturnValue(request); + + const selectPromise = adapter.query({ + query: "SELECT {big:String}", + query_params: { big: "x".repeat(5000) }, + }); + await emitResponseBody(request, "ok"); + await selectPromise; + + const [url, options] = httpRequestStub.mock.calls[0]; + expect(url.search).toContain("param_big"); + expect(options.headers["Content-Type"]).toBeUndefined(); + }); + }); }); diff --git a/packages/client-node/src/connection/node_base_connection.ts b/packages/client-node/src/connection/node_base_connection.ts index b198b27a..e1e6577c 100644 --- a/packages/client-node/src/connection/node_base_connection.ts +++ b/packages/client-node/src/connection/node_base_connection.ts @@ -15,6 +15,7 @@ import type { } from "@clickhouse/client-common"; import { buildMultipartBody, + serializeQueryParamsForUrl, formatQueryParams, isCredentialsAuth, isJWTAuth, @@ -187,15 +188,35 @@ export abstract class NodeBaseConnection implements Connection this.params.compression.decompress_response, ); - const useMultipart = - (params.use_multipart_params ?? this.params.use_multipart_params) && - params.query_params !== undefined && - Object.keys(params.query_params).length > 0; + const queryParams = params.query_params; + const hasQueryParams = + queryParams !== undefined && Object.keys(queryParams).length > 0; + let useMultipart = + hasQueryParams && + (params.use_multipart_params ?? this.params.use_multipart_params); + // In auto mode, serialize the params for the URL once with an early + // return: a null result means they exceed the URL budget and should be + // promoted to a multipart body; otherwise the entries are reused below. + let urlParamEntries: [string, string][] | undefined; + if ( + hasQueryParams && + !useMultipart && + (params.use_multipart_params_auto ?? + this.params.use_multipart_params_auto) + ) { + const entries = serializeQueryParamsForUrl(queryParams); + if (entries === null) { + useMultipart = true; + } else { + urlParamEntries = entries; + } + } const searchParams = toSearchParams({ database: this.params.database, // When using multipart, query_params are sent in the multipart body query_params: useMultipart ? undefined : params.query_params, + param_entries: urlParamEntries, session_id: params.session_id, clickhouse_settings, query_id, diff --git a/packages/client-web/__tests__/unit/web_multipart_query.test.ts b/packages/client-web/__tests__/unit/web_multipart_query.test.ts index e056f720..9aed2956 100644 --- a/packages/client-web/__tests__/unit/web_multipart_query.test.ts +++ b/packages/client-web/__tests__/unit/web_multipart_query.test.ts @@ -181,4 +181,78 @@ describe("[Web] Multipart query params", () => { expect(headers["Content-Type"]).toBeUndefined(); }); }); + + describe("when use_multipart_params_auto is true", () => { + it("should keep small query_params in the URL", async () => { + const fetchStub = stubFetch(); + const adapter = buildWebConnection({ + use_multipart_params_auto: true, + fetch: fetchStub, + }); + + await adapter.query({ + query: "SELECT {v:Int32}", + query_params: { v: 42 }, + }); + + const { url, headers } = lastFetchCall(fetchStub); + expect(url).toContain("param_v=42"); + expect(headers["Content-Type"]).toBeUndefined(); + }); + + it("should promote large query_params to a multipart body", async () => { + const fetchStub = stubFetch(); + const adapter = buildWebConnection({ + use_multipart_params_auto: true, + fetch: fetchStub, + }); + + const ids = [...Array(3000).keys()]; + await adapter.query({ + query: + "SELECT count() FROM numbers(5000) WHERE number IN {ids:Array(UInt64)}", + query_params: { ids }, + }); + + const { url, headers } = lastFetchCall(fetchStub); + expect(url).not.toContain("param_ids"); + expect(url).toContain("query_id="); + expect(headers["Content-Type"]).toMatch( + /^multipart\/form-data; boundary=/, + ); + }); + + it("should respect a per-request use_multipart_params_auto override", async () => { + const fetchStub = stubFetch(); + const adapter = buildWebConnection({ fetch: fetchStub }); + + await adapter.query({ + query: "SELECT {big:String}", + query_params: { big: "x".repeat(5000) }, + use_multipart_params_auto: true, + }); + + const { url, headers } = lastFetchCall(fetchStub); + expect(url).not.toContain("param_big"); + expect(headers["Content-Type"]).toMatch( + /^multipart\/form-data; boundary=/, + ); + }); + }); + + describe("when use_multipart_params_auto is false (default)", () => { + it("should keep large query_params in the URL", async () => { + const fetchStub = stubFetch(); + const adapter = buildWebConnection({ fetch: fetchStub }); + + await adapter.query({ + query: "SELECT {big:String}", + query_params: { big: "x".repeat(5000) }, + }); + + const { url, headers } = lastFetchCall(fetchStub); + expect(url).toContain("param_big"); + expect(headers["Content-Type"]).toBeUndefined(); + }); + }); }); diff --git a/packages/client-web/src/connection/web_connection.ts b/packages/client-web/src/connection/web_connection.ts index ccf9344b..26f93d1d 100644 --- a/packages/client-web/src/connection/web_connection.ts +++ b/packages/client-web/src/connection/web_connection.ts @@ -11,6 +11,7 @@ import type { } from "@clickhouse/client-common"; import { buildMultipartBody, + serializeQueryParamsForUrl, formatQueryParams, isCredentialsAuth, isJWTAuth, @@ -57,15 +58,35 @@ export class WebConnection implements Connection { this.params.compression.decompress_response, ); - const useMultipart = - (params.use_multipart_params ?? this.params.use_multipart_params) && - params.query_params !== undefined && - Object.keys(params.query_params).length > 0; + const queryParams = params.query_params; + const hasQueryParams = + queryParams !== undefined && Object.keys(queryParams).length > 0; + let useMultipart = + hasQueryParams && + (params.use_multipart_params ?? this.params.use_multipart_params); + // In auto mode, serialize the params for the URL once with an early + // return: a null result means they exceed the URL budget and should be + // promoted to a multipart body; otherwise the entries are reused below. + let urlParamEntries: [string, string][] | undefined; + if ( + hasQueryParams && + !useMultipart && + (params.use_multipart_params_auto ?? + this.params.use_multipart_params_auto) + ) { + const entries = serializeQueryParamsForUrl(queryParams); + if (entries === null) { + useMultipart = true; + } else { + urlParamEntries = entries; + } + } const searchParams = toSearchParams({ database: this.params.database, clickhouse_settings, query_params: useMultipart ? undefined : params.query_params, + param_entries: urlParamEntries, session_id: params.session_id, role: params.role, query_id, From 22f379f62116d3dbf0302f0ee197bcbe0eaad6d7 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Jun 2026 21:40:31 +0200 Subject: [PATCH 19/20] Add typescript-language-server for agent code intelligence (#829) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Makes a TypeScript LSP discoverable and usable by AI agent runs in this repo, per the request to install `typescript-language-server`, test it, and commit the enabling changes. The server was verified end-to-end over stdio (JSON-RPC) from the repo root against `packages/client-common/src/client.ts`: - `textDocument/hover` — full inferred signature + JSDoc for `ClickHouseClient.query` - `textDocument/definition`, `textDocument/references` - `workspace/symbol` — finds `ClickHouseClient` class + re-exports across packages - `textDocument/completion` - Diagnostics arrive via push (`textDocument/publishDiagnostics`); pull `textDocument/diagnostic` is unsupported (`-32601`) — documented as a caveat It uses the workspace-local `typescript@6.x` and picks up each workspace's `tsconfig.json` automatically; no extra configuration needed. Changes: - **Dependency** — `typescript-language-server@^5.3.0` added as a root devDependency, so the existing `setup` skill (`npm install`) makes it available via `npx typescript-language-server --stdio` - **Skill** — new `.claude/skills/typescript-lsp/SKILL.md` with the startup command, protocol essentials (0-based positions, `didOpen` requirement), verified capability matrix, and the push-only diagnostics caveat - **AGENTS.md** — new "Code intelligence (TypeScript LSP)" section pointing agents at the skill and recommending the LSP over text search for symbol resolution The skill is repo-internal, so it is intentionally **not** added to the shipped `agents.skills` array in `packages/client-node/package.json` (checked by the Skills E2E workflow). No production code changed. ## Checklist - [x] A human-readable description of the changes was provided to include in CHANGELOG (N/A — tooling/docs only, no observable client behavior change) --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- .claude/skills/typescript-lsp/SKILL.md | 64 ++++++++++++++++++++++++++ AGENTS.md | 4 ++ package-lock.json | 14 ++++++ package.json | 1 + 4 files changed, 83 insertions(+) create mode 100644 .claude/skills/typescript-lsp/SKILL.md diff --git a/.claude/skills/typescript-lsp/SKILL.md b/.claude/skills/typescript-lsp/SKILL.md new file mode 100644 index 00000000..803294d5 --- /dev/null +++ b/.claude/skills/typescript-lsp/SKILL.md @@ -0,0 +1,64 @@ +--- +name: typescript-lsp +description: > + Use the TypeScript language server (`typescript-language-server`) for precise + code intelligence in the `clickhouse-js` repository: go-to-definition, find + references, hover (type signatures and JSDoc, including `@deprecated` info), + workspace-wide symbol search, completions, and per-file type diagnostics. + Prefer this over grep when you need to resolve a symbol's actual definition, + find all usages of an exported API, or inspect inferred types across the + `packages/*` workspaces. The server is preinstalled as a root devDependency — + run the repository `setup` skill (`npm install`) first so `node_modules` is + populated. Do NOT use this skill for downstream projects that merely depend + on `@clickhouse/client`; it is specific to working inside this repo. +--- + +# TypeScript Language Server in clickhouse-js + +`typescript-language-server` is declared as a root devDependency, so after +`npm install` (see the `setup` skill) it is available via `npx` and uses the +workspace-local `typescript` (6.x) compiler, matching CI exactly. + +## Starting the server + +From the repo root: + +```bash +npx typescript-language-server --stdio +``` + +It speaks LSP (JSON-RPC, `Content-Length`-framed) over stdio. There is no +extra configuration: each `packages/*` workspace, `tests/clickhouse-test-runner`, +`examples/node`, and `examples/web` has its own `tsconfig.json`, and the +server picks the nearest one per opened file automatically. + +## Protocol essentials + +1. Send `initialize` with `rootUri`/`workspaceFolders` pointing at the repo + root, then the `initialized` notification. +2. Send `textDocument/didOpen` with the full file text for every file you + want to query (the server reads dependencies from disk on its own; only + files you query positions in need to be opened). +3. Positions are **0-based** (line and character). + +## Verified capabilities (typescript-language-server 5.x, TS 6.x) + +| Request | Notes | +| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `textDocument/hover` | Returns the full inferred signature + JSDoc markdown | +| `textDocument/definition` | Resolves across workspace packages via source, not `dist/` | +| `textDocument/references` | Within the opened file's project | +| `workspace/symbol` | e.g. query `ClickHouseClient` finds class + re-exports | +| `textDocument/completion` | Context-aware member completions | +| Diagnostics | **Push only** (`textDocument/publishDiagnostics` notifications, sent ~1–3s after `didOpen`). The pull `textDocument/diagnostic` request is _not_ supported (`-32601`) — wait for the push notification instead. | + +## Tips + +- If your environment provides built-in LSP/code-intelligence tooling, point + it at `npx typescript-language-server --stdio` with the repo root as the + workspace folder. +- For one-off scripted queries, spawn the server from Node, frame messages + with `Content-Length: \r\n\r\n`, and match responses by `id`. +- A full workspace typecheck is still `npm run build && npm run typecheck` + (build first — some workspaces import compiled `dist/` outputs); the LSP is + for targeted, interactive queries, not for CI-equivalent validation. diff --git a/AGENTS.md b/AGENTS.md index d3be4215..01ff6890 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,6 +41,10 @@ loop. **Do not flag this node/web duplication as something to consolidate**, and client self-contained over adding shared helpers that only exist to remove the duplication. Genuinely platform-agnostic primitives (like `buildMultipartBody`) still belong in `client-common`. +## Code intelligence (TypeScript LSP) + +The repository ships `typescript-language-server` as a root devDependency, so after `npm install` you can start a TypeScript language server with `npx typescript-language-server --stdio` from the repo root for precise go-to-definition, find-references, hover (signatures and JSDoc, including `@deprecated`), workspace symbol search, completions, and type diagnostics. Prefer it over text search when resolving symbols or usages across the `packages/*` workspaces. See [`.claude/skills/typescript-lsp/SKILL.md`](.claude/skills/typescript-lsp/SKILL.md) for verified capabilities and protocol notes. + ## Examples The repository contains an [`examples`](examples) directory that is being refactored to be AI-agent-friendly. diff --git a/package-lock.json b/package-lock.json index 739dd970..80205b9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "split2": "^4.2.0", "typescript": "^6.0.3", "typescript-eslint": "^8.61.0", + "typescript-language-server": "^5.3.0", "uuid": "^14.0.0", "vitest": "^4.0.16" }, @@ -6308,6 +6309,19 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/typescript-language-server": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/typescript-language-server/-/typescript-language-server-5.3.0.tgz", + "integrity": "sha512-5puofxZHgFdAYtfNpmwCAvgtaYgg8wrUnH30m7Ze3QuguId5RNRadKASpOpyDxTyUdAF51FjhTdjntLw/EuWcQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "typescript-language-server": "lib/cli.mjs" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/typical": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz", diff --git a/package.json b/package.json index c3e26734..06bdbcf7 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "split2": "^4.2.0", "typescript": "^6.0.3", "typescript-eslint": "^8.61.0", + "typescript-language-server": "^5.3.0", "uuid": "^14.0.0", "vitest": "^4.0.16" }, From 448e7885c72e907d6163e162f0c2e9eeebb21a91 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Jun 2026 23:51:16 +0200 Subject: [PATCH 20/20] Align OTEL tracer API with DB semantic conventions; move query span lifecycle into ResultSet (#828) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Aligns the tracer API (unreleased, introduced in #776) with the OpenTelemetry database semantic conventions and matches the attribute vocabulary of the Rust client (clickhouse-rs). The `QuerySpanTracker` indirection is removed — `ResultSet` implementations now receive the raw `ClickHouseSpan` and finalize it directly. - **`client-common`** - Removed the `QuerySpanTracker` class (unreleased API) and its export; exported `recordSpanError` instead for the result sets. - `MakeResultSet` now takes `span?: ClickHouseSpan`; `client.query()` hands the span to `makeResultSet` and records/ends it itself if `makeResultSet` throws. - Simplified `client.ping()` to forward the existing immutable params object directly instead of cloning it first. - `recordSpanError` now coerces `ClickHouseError.code` to a number when it is numeric, so `clickhouse.error.code` is consistently typed as a number in OTEL backends. - **Span attribute alignment** - Spans now carry `db.system.name` (instead of `db.system`), `server.address` + `server.port` (instead of a combined `host:port`), `clickhouse.request.query_id` / `clickhouse.request.session_id` (instead of `clickhouse.query_id` / `clickhouse.session_id`), `clickhouse.response.format` on `query` and `clickhouse.request.format` on `insert` (instead of `clickhouse.format`), and `db.operation.name` + `db.collection.name` on `insert` (instead of `clickhouse.table`). - The span status is left unset on success (per the OTEL spec recommendation for client spans, previously set to `OK`); on failure, the span gets the `error.type` attribute (the error class name) and, for server-side errors, `clickhouse.error.code` (the numeric ClickHouse error code). - Spans record response-side attributes: `db.response.status_code` (HTTP status) and, when the `X-ClickHouse-Summary` header is available, `clickhouse.summary.*` counters (`read_rows`, `written_rows`, etc.). - The `insert` span records `clickhouse.request.sent_rows` for array-based inserts. - **Two-span model for `query()`** (deviates from clickhouse-rs; makes request duration and stream duration independently observable) - `clickhouse.query` ends as soon as the HTTP response headers are received. - A child `clickhouse.query.stream` span is started immediately after and handed to the `ResultSet`; it ends with `clickhouse.response.decoded_bytes` (and `db.response.returned_rows` for row-streaming paths) when the result set is fully consumed, closed, or fails. This separation makes it easy to distinguish the original request round-trip from a stream that may never end (e.g. tailing a live materialized view). - **Node & Web `ResultSet`** (duplicated per the node/web convention) - Each tracks `span_bytes` / `span_rows` internally and finalizes via a private `finishSpan(err?)`: records `clickhouse.response.decoded_bytes` (+ `db.response.returned_rows` when rows were counted), records the error if any, and ends the span exactly once on full consumption, `close()`, or failure. - Node `ResultSet.close()` destroys the underlying stream (it is not marked as consumed); a clarifying comment explains why setting `_consumed` there would produce a misleading "already consumed" error when the stream was actually destroyed. - Web `ResultSet.text()` and the non-streamable `json()` path record `clickhouse.response.text_length` (UTF-16 code-unit count of the response body) instead of `clickhouse.response.decoded_bytes`, avoiding the `TextEncoder` allocation that accurate byte counting would require; a comment in each explains the distinction. - **Web `ResultSet` span leak fix** - In `ResultSet.stream()`, the `TransformStream` transformer now includes a `cancel(reason?)` callback. Per the WHATWG Streams spec, `cancel` is called both when the consumer cancels the readable side and when the writable side is aborted (e.g. source stream network error), ensuring `clickhouse.query.stream` is always ended even on network failures. - Added `return` after `controller.error(err)` in the exception-tag branch to stop further processing once the stream is errored. - **Tests & docs** - `node_result_set_span.test.ts` passes the span directly; all span-lifecycle scenarios unchanged. - `tracing.test.ts`: updated for the two-span query lifecycle; tracker-specific tests replaced with span-handoff and `makeResultSet`-throw coverage. - Removed the no-longer-needed `OTEL.md` planning document and its remaining repository reference. - Removed `trace_context_propagator` / `injectTraceContext` (traceparent propagation is left to HTTP auto-instrumentation). ## 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 - [ ] 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> Co-authored-by: Peter Leonov Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .docker/clickhouse/cluster/server1_config.xml | 11 + .docker/clickhouse/cluster/server2_config.xml | 11 + .docker/clickhouse/single_node/config.xml | 11 + CHANGELOG.md | 10 + docs/howto/tracing.md | 83 ++++-- examples/node/coding/otel_tracing.ts | 12 +- .../__tests__/unit/tracing.test.ts | 236 +++++++++++++++--- packages/client-common/src/client.ts | 192 +++++++++----- packages/client-common/src/config.ts | 8 +- packages/client-common/src/index.ts | 1 + packages/client-common/src/tracing.ts | 37 ++- .../unit/node_result_set_span.test.ts | 112 +++++++++ packages/client-node/src/config.ts | 3 + packages/client-node/src/result_set.ts | 83 +++++- packages/client-web/src/config.ts | 13 +- packages/client-web/src/result_set.ts | 134 ++++++++-- 16 files changed, 826 insertions(+), 131 deletions(-) create mode 100644 packages/client-node/__tests__/unit/node_result_set_span.test.ts diff --git a/.docker/clickhouse/cluster/server1_config.xml b/.docker/clickhouse/cluster/server1_config.xml index 0ca1b8bd..621626c7 100644 --- a/.docker/clickhouse/cluster/server1_config.xml +++ b/.docker/clickhouse/cluster/server1_config.xml @@ -90,6 +90,17 @@ 1000 + + system + opentelemetry_span_log
+ + ENGINE = MergeTree + PARTITION BY finish_date + ORDER BY (finish_date, finish_time_us) + + 1000 +
+
Access-Control-Allow-Origin diff --git a/.docker/clickhouse/cluster/server2_config.xml b/.docker/clickhouse/cluster/server2_config.xml index 0ddd8049..220d38c2 100644 --- a/.docker/clickhouse/cluster/server2_config.xml +++ b/.docker/clickhouse/cluster/server2_config.xml @@ -90,6 +90,17 @@ 1000 + + system + opentelemetry_span_log
+ + ENGINE = MergeTree + PARTITION BY finish_date + ORDER BY (finish_date, finish_time_us) + + 1000 +
+
Access-Control-Allow-Origin diff --git a/.docker/clickhouse/single_node/config.xml b/.docker/clickhouse/single_node/config.xml index fd06c1c8..2c0ad750 100644 --- a/.docker/clickhouse/single_node/config.xml +++ b/.docker/clickhouse/single_node/config.xml @@ -32,6 +32,17 @@ 1000 + + system + opentelemetry_span_log
+ + ENGINE = MergeTree + PARTITION BY finish_date + ORDER BY (finish_date, finish_time_us) + + 1000 +
+
Access-Control-Allow-Origin diff --git a/CHANGELOG.md b/CHANGELOG.md index c40a9e86..fcc92e11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## New features +- The tracer API (unreleased, introduced in [#776]) now follows the [OpenTelemetry database semantic conventions](https://opentelemetry.io/docs/specs/semconv/db/sql/) and matches the attribute vocabulary of the Rust client ([clickhouse-rs](https://github.com/ClickHouse/clickhouse-rs)); see [`docs/howto/tracing.md`](./docs/howto/tracing.md) for the documentation. In particular ([#828]): + - Spans now carry `db.system.name` (instead of `db.system`), `server.address` + `server.port` (instead of a combined `host:port`), `clickhouse.request.query_id` / `clickhouse.request.session_id` (instead of `clickhouse.query_id` / `clickhouse.session_id`), `clickhouse.response.format` on `query` and `clickhouse.request.format` on `insert` (instead of `clickhouse.format`), and `db.operation.name` + `db.collection.name` on `insert` (instead of `clickhouse.table`). + - The span status is left unset on success (per the OTEL spec recommendation for client spans, previously set to `OK`); on failure, the span gets the `error.type` attribute (the error class name) and, for server-side errors, `clickhouse.error.code` (the numeric ClickHouse error code). + - Spans record response-side attributes: `db.response.status_code` (HTTP status) and, when the `X-ClickHouse-Summary` header is available, `clickhouse.summary.*` counters (`read_rows`, `written_rows`, etc.). + - `query()` now emits two spans: `clickhouse.query` covers the HTTP request lifetime and ends as soon as the response headers are received; a child `clickhouse.query.stream` span is handed to the `ResultSet` and tracks the stream consumption, ending when the response is fully read, closed, or fails - with the final `clickhouse.response.decoded_bytes` and (for row-streaming) `db.response.returned_rows` metrics. This separation makes it easy to distinguish the original request duration from a stream that may never end (e.g. tailing a live table). + - Fixed a span leak in the Web `ResultSet.stream()` path: if the underlying fetch response stream was aborted (e.g. due to a network error), the `clickhouse.query.stream` span was never ended. The TransformStream now handles both source-stream aborts and consumer-side cancellations via a `cancel` callback. + - The `insert` span records `clickhouse.request.sent_rows` for array-based inserts. + - Added a `use_multipart_params_auto` client option (default: `false`). When enabled, `query()` automatically sends `query_params` as `multipart/form-data` body parts (the same mechanism as `use_multipart_params`) once their URL-encoded length exceeds 4096 characters, avoiding HTTP 414/400 errors from HTTP intermediaries (nginx, AWS ALB, CloudFront) caused by over-long URLs - for example, a large `IN` list or a high-dimensional vector embedding. Smaller parameter payloads remain in the URL query string, so existing behavior is unchanged unless the threshold is crossed. `use_multipart_params: true` still forces multipart for all queries regardless of size. This does not change the server's per-value size limit, which is governed by `http_max_field_value_size`. Supported on both `@clickhouse/client` and `@clickhouse/client-web`, and overridable per request via `use_multipart_params_auto` on `query()`. Ported from [clickhouse-connect#789](https://github.com/ClickHouse/clickhouse-connect/pull/789). ([#827]) ```ts @@ -27,8 +35,10 @@ await client.query({ }); ``` +[#776]: https://github.com/ClickHouse/clickhouse-js/pull/776 [#825]: https://github.com/ClickHouse/clickhouse-js/pull/825 [#827]: https://github.com/ClickHouse/clickhouse-js/pull/827 +[#828]: https://github.com/ClickHouse/clickhouse-js/pull/828 ## Bug Fixes diff --git a/docs/howto/tracing.md b/docs/howto/tracing.md index 65def02b..44f75578 100644 --- a/docs/howto/tracing.md +++ b/docs/howto/tracing.md @@ -107,32 +107,57 @@ before talking to the server), the client invokes 1. The name is one of `clickhouse.query`, `clickhouse.command`, `clickhouse.exec`, `clickhouse.insert`, `clickhouse.ping` (also exported as `ClickHouseSpanNames`); `kind` is `ClickHouseSpanKind.CLIENT`. The - initial attribute bag always includes `db.system`, `db.namespace`, - `server.address`, and - when set - `clickhouse.application`, plus - operation-specific entries such as `clickhouse.format`, - `clickhouse.table`, `clickhouse.query_id`, and `clickhouse.session_id`. + initial attribute bag always includes `db.system.name`, `db.namespace`, + `server.address`, and `server.port`, and - when set - + `clickhouse.application`, plus operation-specific entries such as + `clickhouse.response.format` (query), `clickhouse.request.format`, + `db.operation.name`, `db.collection.name` and `clickhouse.request.sent_rows` + (insert; the row count is recorded for array-based inserts only), + `clickhouse.request.query_id`, and + `clickhouse.request.session_id`. 2. Inside `fn`, the network operation runs with the span as the active span (when the context manager supports it; see above). -3. `span.setAttributes({ 'clickhouse.query_id': })` - so - you always have the final `query_id`, even when the caller did not pass one - and the connection layer generated it. -4. `span.setStatus({ code: ClickHouseSpanStatusCode.OK })` on success, or - `span.recordException(error)` immediately followed by - `span.setStatus({ code: ClickHouseSpanStatusCode.ERROR, message })` on - failure. Non-`Error` throwables are normalized to `Error` before - `recordException`. -5. `span.end()` - always, in a `finally` block. +3. `span.setAttributes({ 'clickhouse.request.query_id': })` - + so you always have the final `query_id`, even when the caller did not pass + one and the connection layer generated it. Once the response arrives, the + span also gets `db.response.status_code` (HTTP status) and, when the + `X-ClickHouse-Summary` header is present (e.g. with `wait_end_of_query`), + `clickhouse.summary.*` counters (`read_rows`, `written_rows`, …). +4. On success, the span status is left **unset**, per the OTEL span status + spec for client spans. On failure, + `span.setAttributes({ 'error.type': })` (plus + `clickhouse.error.code` with the numeric server error code when the error + is a server-side `ClickHouseError`), then `span.recordException(error)` + immediately followed by + `span.setStatus({ code: ClickHouseSpanStatusCode.ERROR, message })`. + Non-`Error` throwables are normalized to `Error` before `recordException`. +5. `span.end()` - exactly once. For `command`/`exec`/`insert`/`ping`, in a + `finally` block when the method settles; for `query`, see the stream + lifecycle note below. Tracer calls are inlined directly on the client's hot path and are **not** wrapped in defensive try/catch - if your tracer or span throws, the exception propagates to the caller of `query` / `command` / `exec` / `insert` / `ping`. Make sure your tracer implementation doesn't throw. -> **Stream lifecycle:** for `query`/`exec`, the span ends when the request -> promise settles (i.e. headers received, stream handed to the caller). The -> client does not currently emit a separate "download finished" span when the -> returned `ResultSet` stream is fully consumed; if you need that, wrap the -> stream on the caller side. +> **Stream lifecycle:** `query()` emits **two spans**. +> +> - `clickhouse.query` — covers the HTTP request: starts when `query()` is +> called and ends as soon as the response headers arrive (regardless of how +> much data is in the body). +> - `clickhouse.query.stream` — a child span that covers the `ResultSet` +> lifetime: starts immediately after the response headers are received and +> ends when the result set is fully consumed (`text()`/`json()` resolve, or +> the `stream()` is read to completion), closed via `close()`, or fails +> (the error is recorded on this span). When it ends it carries the final +> `clickhouse.response.decoded_bytes` and, for row-streaming consumption, +> `db.response.returned_rows` metrics. +> +> This split makes it easy to distinguish the original request round-trip from +> a stream that may never end (e.g. tailing a live materialized view). If the +> `ResultSet` is never consumed nor closed, the `clickhouse.query.stream` span +> is never ended. For `command`/`exec`/`insert`/`ping`, a single span ends +> when the method returns. ## Recording-only tracer for tests / debugging @@ -173,6 +198,28 @@ const tracer: ClickHouseTracer = { }; ``` +## Trace context propagation (`traceparent`) + +To let the ClickHouse server link its own spans (recorded in +`system.opentelemetry_span_log`) to your client trace, the outgoing requests +must carry the W3C `traceparent` / `tracestate` headers. With OpenTelemetry, +this happens automatically: Node.js users get header propagation for free +from `@opentelemetry/instrumentation-http` (Web: `instrumentation-fetch`), +since the client uses the platform HTTP stack. With the +`AsyncLocalStorageContextManager` registered (see above), those +auto-instrumented HTTP spans parent under the `clickhouse.` span, +so the injected `traceparent` points at the client trace. + +To see the server-side spans, the server must have the +`opentelemetry_span_log` table configured (see this repository's +`.docker/clickhouse/single_node/config.xml` for an example); you can then +correlate by trace id: + +```sql +SELECT * FROM system.opentelemetry_span_log +WHERE lower(hex(trace_id)) = '' +``` + ## Disabling tracing Omit the `tracer` option (or set it to `undefined`) and the client will not emit any spans. Internally, it uses a shared no-op tracer/span so the call sites remain monomorphic (branch-free), keeping the overhead minimal (but not strictly zero). diff --git a/examples/node/coding/otel_tracing.ts b/examples/node/coding/otel_tracing.ts index 58ad240c..758c21cc 100644 --- a/examples/node/coding/otel_tracing.ts +++ b/examples/node/coding/otel_tracing.ts @@ -53,6 +53,9 @@ const tracer: ClickHouseTracer = otelTracer; // 4. Pass the tracer through the client config; from here on, every // `query`/`command`/`exec`/`insert`/`ping` call is traced automatically. +// Trace context propagation (the W3C `traceparent` header) is handled +// automatically by `@opentelemetry/instrumentation-http`, since the +// client uses the platform HTTP stack. const client = createClient({ url: process.env["CLICKHOUSE_URL"], // defaults to 'http://localhost:8123' password: process.env["CLICKHOUSE_PASSWORD"], // defaults to an empty string @@ -71,16 +74,19 @@ await client.close(); // 5. Flush and inspect the spans the client produced. Each operation yields a // single CLIENT-kind span named `clickhouse.` (also exported as // `ClickHouseSpanNames`) carrying OTEL-style attributes such as -// `db.system`, `server.address`, and the server-assigned `clickhouse.query_id`. +// `db.system.name`, `server.address`, and the server-assigned +// `clickhouse.request.query_id`. await provider.forceFlush(); const spans = exporter.getFinishedSpans(); for (const span of spans) { console.info("[OtelTracing] Span:", { name: span.name, status: SpanStatusCode[span.status.code], - "db.system": span.attributes["db.system"], + "db.system.name": span.attributes["db.system.name"], "server.address": span.attributes["server.address"], - "clickhouse.query_id": span.attributes["clickhouse.query_id"], + "server.port": span.attributes["server.port"], + "clickhouse.request.query_id": + span.attributes["clickhouse.request.query_id"], }); } await provider.shutdown(); diff --git a/packages/client-common/__tests__/unit/tracing.test.ts b/packages/client-common/__tests__/unit/tracing.test.ts index 15a99a8d..1980b77f 100644 --- a/packages/client-common/__tests__/unit/tracing.test.ts +++ b/packages/client-common/__tests__/unit/tracing.test.ts @@ -9,6 +9,7 @@ import { type ClickHouseSpanStatus, type ClickHouseTracer, } from "@clickhouse/client-common"; +import { parseError } from "@clickhouse/client-common"; import { ClickHouseClient } from "../../src/client"; import { NoopClickHouseSpan } from "../../src/tracing"; @@ -113,35 +114,111 @@ function buildClient( tracer, impl: { make_connection: () => connection as any, - make_result_set: ((_s, _f, q) => ({ query_id: q }) as any) as any, + make_result_set: ((_s, _f, q, _log, _h, _j, span) => ({ + query_id: q, + // Test result set: pretend immediate full consumption. + consume: () => span?.end(), + span, + })) as any, values_encoder: () => ({ validateInsertValues: () => {}, - encodeValues: (v: any) => v, + encodeValues: (v: any) => + typeof v === "string" ? v : JSON.stringify(v), }) as any, }, }); } describe("tracer", () => { - it("emits a CLIENT span for query() with OK status and query_id attribute", async () => { + it("emits a CLIENT span for query() with unset status and query_id attribute", async () => { const { tracer, spans } = createRecordingTracer(); const client = buildClient(tracer); - await client.query({ query: "SELECT 1", query_id: "caller-q" }); - expect(spans).toHaveLength(1); - const [span] = spans; - expect(span.name).toBe(ClickHouseSpanNames.query); - expect(span.options?.kind).toBe(ClickHouseSpanKind.CLIENT); - expect(span.initialAttributes["db.system"]).toBe("clickhouse"); - expect(span.initialAttributes["db.namespace"]).toBe("my_db"); - expect(span.initialAttributes["server.address"]).toBe("localhost:8123"); - expect(span.initialAttributes["clickhouse.application"]).toBe("my_app"); - expect(span.initialAttributes["clickhouse.format"]).toBe("JSON"); - expect(span.initialAttributes["clickhouse.query_id"]).toBe("caller-q"); - expect(span.attributes["clickhouse.query_id"]).toBe("q-1"); - expect(span.status).toEqual({ code: ClickHouseSpanStatusCode.OK }); - expect(span.exception).toBeUndefined(); - expect(span.ended).toBe(true); + const rs = await client.query({ query: "SELECT 1", query_id: "caller-q" }); + // query() emits two spans: clickhouse.query (HTTP request) and + // clickhouse.query.stream (ResultSet consumption). + expect(spans).toHaveLength(2); + const [querySpan, streamSpan] = spans; + expect(querySpan.name).toBe(ClickHouseSpanNames.query); + expect(querySpan.options?.kind).toBe(ClickHouseSpanKind.CLIENT); + expect(querySpan.initialAttributes["db.system.name"]).toBe("clickhouse"); + expect(querySpan.initialAttributes["db.namespace"]).toBe("my_db"); + expect(querySpan.initialAttributes["server.address"]).toBe("localhost"); + expect(querySpan.initialAttributes["server.port"]).toBe(8123); + expect(querySpan.initialAttributes["clickhouse.application"]).toBe( + "my_app", + ); + expect(querySpan.initialAttributes["clickhouse.response.format"]).toBe( + "JSON", + ); + expect(querySpan.initialAttributes["clickhouse.request.query_id"]).toBe( + "caller-q", + ); + expect(querySpan.attributes["clickhouse.request.query_id"]).toBe("q-1"); + // Per the OTEL spec, the status is left unset on success. + expect(querySpan.status).toBeUndefined(); + expect(querySpan.exception).toBeUndefined(); + // The query span ends as soon as the HTTP response headers are received. + expect(querySpan.ended).toBe(true); + // The stream span stays open until the ResultSet is consumed or closed. + expect(streamSpan.name).toBe(ClickHouseSpanNames.query_stream); + expect(streamSpan.ended).toBe(false); + (rs as any).consume(); + expect(streamSpan.ended).toBe(true); + }); + + it("hands the stream span to the ResultSet without ending it", async () => { + const { tracer, spans } = createRecordingTracer(); + const client = buildClient(tracer); + const rs = await client.query({ query: "SELECT 1" }); + const [querySpan, streamSpan] = spans; + // The query span ends immediately after the HTTP response is received. + expect(querySpan.ended).toBe(true); + // The stream span is passed to the ResultSet and owns its lifetime. + expect((rs as any).span).toBe(streamSpan); + expect(streamSpan.ended).toBe(false); + (rs as any).consume(); + expect(streamSpan.ended).toBe(true); + }); + + it("records the error on the stream span and ends it when makeResultSet throws", async () => { + const { tracer, spans } = createRecordingTracer(); + const connection: MockConnection = { + query: makeQuery(), + command: makeCommand(), + exec: makeExec(), + insert: makeInsert(), + ping: makePing(), + close: async () => {}, + }; + const client = new ClickHouseClient({ + url: "http://localhost:8123", + tracer, + impl: { + make_connection: () => connection as any, + make_result_set: (() => { + throw new Error("make_result_set failed"); + }) as any, + values_encoder: () => + ({ + validateInsertValues: () => {}, + encodeValues: (v: any) => + typeof v === "string" ? v : JSON.stringify(v), + }) as any, + }, + }); + await expect(client.query({ query: "SELECT 1" })).rejects.toThrow( + "make_result_set failed", + ); + // The query span ends normally (HTTP response was received). + expect(spans[0].name).toBe(ClickHouseSpanNames.query); + expect(spans[0].ended).toBe(true); + expect(spans[0].exception).toBeUndefined(); + // The stream span captures the makeResultSet error. + expect(spans[1].name).toBe(ClickHouseSpanNames.query_stream); + expect(spans[1].exception?.message).toBe("make_result_set failed"); + expect(spans[1].status?.code).toBe(ClickHouseSpanStatusCode.ERROR); + expect(spans[1].ended).toBe(true); }); it("emits the operation span via startActiveSpan", async () => { @@ -159,17 +236,19 @@ describe("tracer", () => { expect(calls).toEqual([ "start:clickhouse.query", "returned:clickhouse.query", + "start:clickhouse.query.stream", + "returned:clickhouse.query.stream", ]); }); - it("emits a span for command() with OK status", async () => { + it("emits a span for command() with unset status", async () => { const { tracer, spans } = createRecordingTracer(); const client = buildClient(tracer); await client.command({ query: "CREATE TABLE t (a UInt8) ENGINE = Memory" }); expect(spans).toHaveLength(1); expect(spans[0].name).toBe(ClickHouseSpanNames.command); - expect(spans[0].attributes["clickhouse.query_id"]).toBe("c-1"); - expect(spans[0].status).toEqual({ code: ClickHouseSpanStatusCode.OK }); + expect(spans[0].attributes["clickhouse.request.query_id"]).toBe("c-1"); + expect(spans[0].status).toBeUndefined(); expect(spans[0].ended).toBe(true); }); @@ -179,8 +258,8 @@ describe("tracer", () => { await client.exec({ query: "SELECT 1" }); expect(spans).toHaveLength(1); expect(spans[0].name).toBe(ClickHouseSpanNames.exec); - expect(spans[0].attributes["clickhouse.query_id"]).toBe("e-1"); - expect(spans[0].status).toEqual({ code: ClickHouseSpanStatusCode.OK }); + expect(spans[0].attributes["clickhouse.request.query_id"]).toBe("e-1"); + expect(spans[0].status).toBeUndefined(); }); it("emits a span for insert()", async () => { @@ -189,12 +268,35 @@ describe("tracer", () => { await client.insert({ table: "my_table", values: [{ a: 1 }] }); expect(spans).toHaveLength(1); expect(spans[0].name).toBe(ClickHouseSpanNames.insert); - expect(spans[0].initialAttributes["clickhouse.table"]).toBe("my_table"); - expect(spans[0].initialAttributes["clickhouse.format"]).toBe( + expect(spans[0].initialAttributes["db.operation.name"]).toBe("INSERT"); + expect(spans[0].initialAttributes["db.collection.name"]).toBe("my_table"); + expect(spans[0].initialAttributes["clickhouse.request.format"]).toBe( "JSONCompactEachRow", ); - expect(spans[0].attributes["clickhouse.query_id"]).toBe("i-1"); - expect(spans[0].status).toEqual({ code: ClickHouseSpanStatusCode.OK }); + expect(spans[0].attributes["clickhouse.request.query_id"]).toBe("i-1"); + expect(spans[0].status).toBeUndefined(); + }); + + it("records sent_rows for array-based inserts", async () => { + const { tracer, spans } = createRecordingTracer(); + const client = buildClient(tracer); + await client.insert({ table: "my_table", values: [{ a: 1 }, { a: 2 }] }); + const [span] = spans; + expect(span.initialAttributes["clickhouse.request.sent_rows"]).toBe(2); + }); + + it("does not record sent_rows for streamed inserts", async () => { + const { tracer, spans } = createRecordingTracer(); + const client = buildClient(tracer); + // Anything that is not an array nor encoded to a string stands for a stream. + const stream = { pipe: () => {} }; + const clientAny = client as any; + clientAny.valuesEncoder.encodeValues = () => stream; + await client.insert({ table: "my_table", values: stream as any }); + const [span] = spans; + expect( + span.initialAttributes["clickhouse.request.sent_rows"], + ).toBeUndefined(); }); it("does NOT emit an insert span when there are no rows to insert", async () => { @@ -205,6 +307,48 @@ describe("tracer", () => { expect(spans).toHaveLength(0); }); + it("records db.response.status_code when the connection reports it", async () => { + const { tracer, spans } = createRecordingTracer(); + const client = buildClient(tracer, { + query: async () => ({ + stream: {} as any, + query_id: "q-1", + response_headers: {}, + http_status_code: 200, + }), + }); + await client.query({ query: "SELECT 1" }); + expect(spans[0].attributes["db.response.status_code"]).toBe(200); + }); + + it("records clickhouse.summary.* attributes when the summary is present", async () => { + const { tracer, spans } = createRecordingTracer(); + const client = buildClient(tracer, { + command: async () => ({ + query_id: "c-1", + response_headers: {}, + http_status_code: 200, + summary: { + read_rows: "10", + read_bytes: "100", + written_rows: "5", + written_bytes: "50", + total_rows_to_read: "10", + result_rows: "5", + result_bytes: "50", + elapsed_ns: "1000", + }, + }), + }); + await client.command({ query: "INSERT INTO t SELECT * FROM s" }); + const attrs = spans[0].attributes; + expect(attrs["db.response.status_code"]).toBe(200); + expect(attrs["clickhouse.summary.read_rows"]).toBe("10"); + expect(attrs["clickhouse.summary.written_rows"]).toBe("5"); + expect(attrs["clickhouse.summary.result_bytes"]).toBe("50"); + expect(attrs["clickhouse.summary.elapsed_ns"]).toBe("1000"); + }); + it("emits a span for ping()", async () => { const { tracer, spans } = createRecordingTracer(); const client = buildClient(tracer); @@ -212,7 +356,7 @@ describe("tracer", () => { expect(spans).toHaveLength(1); expect(spans[0].name).toBe(ClickHouseSpanNames.ping); expect(spans[0].initialAttributes["clickhouse.ping.select"]).toBe(false); - expect(spans[0].status).toEqual({ code: ClickHouseSpanStatusCode.OK }); + expect(spans[0].status).toBeUndefined(); }); it("records the exception and sets ERROR status when an operation throws", async () => { @@ -226,6 +370,7 @@ describe("tracer", () => { await expect(client.query({ query: "SELECT 1" })).rejects.toThrow("boom"); expect(spans).toHaveLength(1); expect(spans[0].exception).toBe(failure); + expect(spans[0].attributes["error.type"]).toBe("Error"); expect(spans[0].status).toEqual({ code: ClickHouseSpanStatusCode.ERROR, message: "boom", @@ -233,6 +378,22 @@ describe("tracer", () => { expect(spans[0].ended).toBe(true); }); + it("sets error.type and clickhouse.error.code for server-side errors", async () => { + const { tracer, spans } = createRecordingTracer(); + const failure = parseError( + "Code: 62. DB::Exception: Syntax error: failed at position 1. (SYNTAX_ERROR) (version 24.3.1)", + ); + const client = buildClient(tracer, { + query: async () => { + throw failure; + }, + }); + await expect(client.query({ query: "SELECT 1" })).rejects.toThrow(failure); + expect(spans[0].attributes["error.type"]).toBe("ClickHouseError"); + expect(spans[0].attributes["clickhouse.error.code"]).toBe(62); + expect(spans[0].status?.code).toBe(ClickHouseSpanStatusCode.ERROR); + }); + it("normalizes non-Error throwables before recordException", async () => { const { tracer, spans } = createRecordingTracer(); const client = buildClient(tracer, { @@ -245,6 +406,7 @@ describe("tracer", () => { ); expect(spans[0].exception).toBeInstanceOf(Error); expect(spans[0].exception?.message).toBe("string failure"); + expect(spans[0].attributes["error.type"]).toBe("Error"); expect(spans[0].status).toEqual({ code: ClickHouseSpanStatusCode.ERROR, message: "string failure", @@ -302,7 +464,23 @@ describe("tracer", () => { }), }; const client = buildClient(tracer); - await client.query({ query: "SELECT 1" }); + const rs = await client.query({ query: "SELECT 1" }); + (rs as any).consume(); expect(ended).toBe(true); }); + + it("passes the request HTTP headers through unaltered", async () => { + const captured: any[] = []; + const client = buildClient(undefined, { + command: async (params: any) => { + captured.push(params); + return { query_id: "c-1", response_headers: {} }; + }, + }); + await client.command({ + query: "SELECT 1", + http_headers: { "x-custom": "value" }, + }); + expect(captured[0].http_headers).toEqual({ "x-custom": "value" }); + }); }); diff --git a/packages/client-common/src/client.ts b/packages/client-common/src/client.ts index db6306b0..e10072f1 100644 --- a/packages/client-common/src/client.ts +++ b/packages/client-common/src/client.ts @@ -22,11 +22,14 @@ import { getConnectionParams, prepareConfigWithURL } from "./config"; import type { ConnPingResult } from "./connection"; import type { JSONHandling } from "./parse/json_handling"; import type { BaseResultSet } from "./result"; -import type { ClickHouseSpanAttributes, ClickHouseTracer } from "./tracing"; +import type { + ClickHouseSpan, + ClickHouseSpanAttributes, + ClickHouseTracer, +} from "./tracing"; import { ClickHouseSpanKind, ClickHouseSpanNames, - ClickHouseSpanStatusCode, NoopClickHouseTracer, recordSpanError, } from "./tracing"; @@ -269,48 +272,71 @@ export class ClickHouseClient { { kind: ClickHouseSpanKind.CLIENT, attributes: this.withBaseSpanAttributes({ - "clickhouse.format": format, - "clickhouse.query_id": queryParams.query_id, - "clickhouse.session_id": queryParams.session_id, + "clickhouse.response.format": format, + "clickhouse.request.query_id": queryParams.query_id, + "clickhouse.request.session_id": queryParams.session_id, }), }, async (span) => { + let queryResult; try { - const { stream, query_id, response_headers } = - await this.connection.query({ - query, - ...queryParams, - }); - // The query_id may have been generated by the connection layer. - span.setAttributes({ "clickhouse.query_id": query_id }); - span.setStatus({ code: ClickHouseSpanStatusCode.OK }); - return this.makeResultSet( - stream, - format, - query_id, - (err) => { - if (log_level <= ClickHouseLogLevel.ERROR) { - log_writer.error({ - err, - module: "Client", - message: "Error while processing the ResultSet.", - args: { - session_id: queryParams.session_id, - role: queryParams.role, - query_id, - }, - }); - } - }, - response_headers, - this.jsonHandling, - ); + queryResult = await this.connection.query({ + query, + ...queryParams, + }); } catch (err) { recordSpanError(span, err); - throw err; - } finally { span.end(); + throw err; } + const { stream, query_id, response_headers, http_status_code } = + queryResult; + // The query_id may have been generated by the connection layer. + span.setAttributes({ "clickhouse.request.query_id": query_id }); + setResponseSpanAttributes(span, { http_status_code }); + // The clickhouse.query span covers the HTTP request lifetime only: + // it ends here, once response headers are received. A separate + // clickhouse.query.stream child span is started and handed to the + // ResultSet to track the response stream consumption (decoded bytes, + // returned rows, streaming errors). Separating the two spans makes + // it easy to distinguish the original request duration from a stream + // that may never end (e.g. tailing a live table). + span.end(); + return this.tracer.startActiveSpan( + ClickHouseSpanNames.query_stream, + { kind: ClickHouseSpanKind.CLIENT }, + (streamSpan) => { + try { + return this.makeResultSet( + stream, + format, + query_id, + (err) => { + if (log_level <= ClickHouseLogLevel.ERROR) { + log_writer.error({ + err, + module: "Client", + message: "Error while processing the ResultSet.", + args: { + session_id: queryParams.session_id, + role: queryParams.role, + query_id, + }, + }); + } + }, + response_headers, + this.jsonHandling, + streamSpan, + ); + } catch (err) { + recordSpanError(streamSpan, err); + streamSpan.end(); + throw err; + } + // streamSpan is NOT ended here - the ResultSet owns its lifetime. + }, + ); }, ); } @@ -333,8 +359,8 @@ export class ClickHouseClient { { kind: ClickHouseSpanKind.CLIENT, attributes: this.withBaseSpanAttributes({ - "clickhouse.query_id": queryParams.query_id, - "clickhouse.session_id": queryParams.session_id, + "clickhouse.request.query_id": queryParams.query_id, + "clickhouse.request.session_id": queryParams.session_id, }), }, async (span) => { @@ -344,8 +370,10 @@ export class ClickHouseClient { ignore_error_response, ...queryParams, }); - span.setAttributes({ "clickhouse.query_id": result.query_id }); - span.setStatus({ code: ClickHouseSpanStatusCode.OK }); + span.setAttributes({ + "clickhouse.request.query_id": result.query_id, + }); + setResponseSpanAttributes(span, result); return result; } catch (err) { recordSpanError(span, err); @@ -379,8 +407,8 @@ export class ClickHouseClient { { kind: ClickHouseSpanKind.CLIENT, attributes: this.withBaseSpanAttributes({ - "clickhouse.query_id": queryParams.query_id, - "clickhouse.session_id": queryParams.session_id, + "clickhouse.request.query_id": queryParams.query_id, + "clickhouse.request.session_id": queryParams.session_id, }), }, async (span) => { @@ -392,8 +420,10 @@ export class ClickHouseClient { ignore_error_response, ...queryParams, }); - span.setAttributes({ "clickhouse.query_id": result.query_id }); - span.setStatus({ code: ClickHouseSpanStatusCode.OK }); + span.setAttributes({ + "clickhouse.request.query_id": result.query_id, + }); + setResponseSpanAttributes(span, result); return result; } catch (err) { recordSpanError(span, err); @@ -429,21 +459,37 @@ export class ClickHouseClient { { kind: ClickHouseSpanKind.CLIENT, attributes: this.withBaseSpanAttributes({ - "clickhouse.table": params.table, - "clickhouse.format": format, - "clickhouse.query_id": queryParams.query_id, - "clickhouse.session_id": queryParams.session_id, + "db.operation.name": "INSERT", + "db.collection.name": params.table, + "clickhouse.request.format": format, + "clickhouse.request.query_id": queryParams.query_id, + "clickhouse.request.session_id": queryParams.session_id, + // Only known up front for array-based inserts; for streamed + // inserts, the row count is not observable by the client. + "clickhouse.request.sent_rows": Array.isArray(params.values) + ? params.values.length + : undefined, }), }, async (span) => { try { + const values = this.valuesEncoder.encodeValues(params.values, format); + // TODO: record `clickhouse.request.encoded_bytes` (the + // pre-compression request body size) here. This is a + // post-common-deprecation feature: once `client-common` is + // deprecated and the Node.js / Web clients are fully independent, + // each client can measure the encoded payload using its own + // platform-native byte-length primitive (e.g. `Buffer.byteLength`) + // instead of a hand-rolled common implementation. const result = await this.connection.insert({ query, - values: this.valuesEncoder.encodeValues(params.values, format), + values, ...queryParams, }); - span.setAttributes({ "clickhouse.query_id": result.query_id }); - span.setStatus({ code: ClickHouseSpanStatusCode.OK }); + span.setAttributes({ + "clickhouse.request.query_id": result.query_id, + }); + setResponseSpanAttributes(span, result); return { ...result, executed: true }; } catch (err) { recordSpanError(span, err); @@ -476,11 +522,9 @@ export class ClickHouseClient { }, async (span) => { try { - const result = await this.connection.ping( - params ?? { select: false }, + return await this.connection.ping( + params ?? { select: false as const }, ); - span.setStatus({ code: ClickHouseSpanStatusCode.OK }); - return result; } catch (err) { recordSpanError(span, err); throw err; @@ -514,9 +558,11 @@ export class ClickHouseClient { private withBaseSpanAttributes( extra: ClickHouseSpanAttributes, ): ClickHouseSpanAttributes { + const url = this.connectionParams.url; const attrs: ClickHouseSpanAttributes = { - "db.system": "clickhouse", - "server.address": this.connectionParams.url.host, + "db.system.name": "clickhouse", + "server.address": url.hostname, + "server.port": getServerPort(url), "db.namespace": this.connectionParams.database, }; if (this.connectionParams.application_id !== undefined) { @@ -547,6 +593,38 @@ export class ClickHouseClient { } } +function getServerPort(url: URL): number { + if (url.port !== "") { + return Number(url.port); + } + return url.protocol === "https:" ? 443 : 80; +} + +/** Records HTTP status and `X-ClickHouse-Summary` counters on the span once + * the response (headers) arrived. The summary values are complete only when + * the query was executed with `wait_end_of_query=1`; see + * {@link ClickHouseSummary}. */ +function setResponseSpanAttributes( + span: ClickHouseSpan, + result: WithHttpStatusCode & WithClickHouseSummary, +): void { + const attributes: ClickHouseSpanAttributes = {}; + if (result.http_status_code !== undefined) { + attributes["db.response.status_code"] = result.http_status_code; + } + const summary = result.summary; + if (summary !== undefined) { + attributes["clickhouse.summary.read_rows"] = summary.read_rows; + attributes["clickhouse.summary.read_bytes"] = summary.read_bytes; + attributes["clickhouse.summary.written_rows"] = summary.written_rows; + attributes["clickhouse.summary.written_bytes"] = summary.written_bytes; + attributes["clickhouse.summary.result_rows"] = summary.result_rows; + attributes["clickhouse.summary.result_bytes"] = summary.result_bytes; + attributes["clickhouse.summary.elapsed_ns"] = summary.elapsed_ns; + } + span.setAttributes(attributes); +} + function formatQuery(query: string, format: DataFormat): string { query = query.trim(); query = removeTrailingSemi(query); diff --git a/packages/client-common/src/config.ts b/packages/client-common/src/config.ts index ba6a0818..5c256b89 100644 --- a/packages/client-common/src/config.ts +++ b/packages/client-common/src/config.ts @@ -6,7 +6,7 @@ import { ClickHouseLogLevel, LogWriter } from "./logger"; import { defaultJSONHandling, type JSONHandling } from "./parse/json_handling"; import type { BaseResultSet } from "./result"; import type { ClickHouseSettings } from "./settings"; -import type { ClickHouseTracer } from "./tracing"; +import type { ClickHouseSpan, ClickHouseTracer } from "./tracing"; export interface BaseClickHouseClientConfigOptions { /** @deprecated since version 1.0.0. Use {@link url} instead.
@@ -148,6 +148,12 @@ export type MakeResultSet = < log_error: (err: Error) => void, response_headers: ResponseHeaders, jsonHandling: JSONHandling, + /** When the client was configured with a {@link ClickHouseTracer}, a + * `clickhouse.query.stream` child span is created and passed here. The + * result set tracks its own streaming progress (decoded bytes/rows) and + * must record the final response metrics on the span and end it when the + * stream is fully consumed, closed, or fails. */ + span?: ClickHouseSpan, ) => ResultSet; export type MakeValuesEncoder = ( diff --git a/packages/client-common/src/index.ts b/packages/client-common/src/index.ts index 94a9cef6..1788a479 100644 --- a/packages/client-common/src/index.ts +++ b/packages/client-common/src/index.ts @@ -124,6 +124,7 @@ export { ClickHouseSpanStatusCode, ClickHouseSpanKind, ClickHouseSpanNames, + recordSpanError, } from "./tracing"; /** For implementation usage only - should not be re-exported */ diff --git a/packages/client-common/src/tracing.ts b/packages/client-common/src/tracing.ts index c64f5a45..62c1abdd 100644 --- a/packages/client-common/src/tracing.ts +++ b/packages/client-common/src/tracing.ts @@ -1,3 +1,5 @@ +import { ClickHouseError } from "./error"; + /** * A minimal, dependency-free tracer interface that is a structural subset of * the {@link https://opentelemetry.io/docs/specs/otel/trace/api/#tracer OpenTelemetry `Tracer` API}. @@ -22,8 +24,14 @@ * {@link ClickHouseTracer.startActiveSpan}, mutates the provided * {@link ClickHouseSpan} during the operation * ({@link ClickHouseSpan.setAttributes}, {@link ClickHouseSpan.setStatus}, - * {@link ClickHouseSpan.recordException}), and finally calls - * {@link ClickHouseSpan.end} when it completes (regardless of outcome). + * {@link ClickHouseSpan.recordException}), and calls + * {@link ClickHouseSpan.end} exactly once. For `command`, `exec`, `insert`, + * and `ping`, the span ends when the operation settles (regardless of + * outcome). For `query`, two spans are emitted: the `clickhouse.query` span + * ends as soon as the HTTP response headers are received; a child + * `clickhouse.query.stream` span is then handed to the `ResultSet`, which + * tracks the streaming progress and ends it when the response stream is fully + * consumed, closed, or fails. * * Calls are inlined directly into the client's hot path - there is no * defensive wrapper around them. Any exception thrown by a tracer or span @@ -67,7 +75,7 @@ export interface ClickHouseTracer< export interface ClickHouseSpan { /** Attach additional attributes to an in-flight span. Called at least once * for every span - typically right before {@link ClickHouseSpan.end} - - * with operation-specific attributes such as `clickhouse.query_id`. */ + * with operation-specific attributes such as `clickhouse.request.query_id`. */ setAttributes(attributes: ClickHouseSpanAttributes): void; /** Set the logical status of the span. The codes are value-identical to * OTEL's `SpanStatusCode`; see {@link ClickHouseSpanStatusCode}. */ @@ -128,6 +136,12 @@ export type ClickHouseSpanAttributes = Record< * Exposed so that adapters and tests can match on them. */ export const ClickHouseSpanNames = { query: "clickhouse.query", + /** A child of {@link ClickHouseSpanNames.query} that covers the lifetime + * of the `ResultSet` stream - from the first byte read to full + * consumption, cancellation, or failure. Ends with + * `clickhouse.response.decoded_bytes` and (for row-streaming paths) + * `db.response.returned_rows`. */ + query_stream: "clickhouse.query.stream", command: "clickhouse.command", exec: "clickhouse.exec", insert: "clickhouse.insert", @@ -153,9 +167,24 @@ export const NoopClickHouseTracer: ClickHouseTracer = { }; /** Records the exception on the span and marks it with the ERROR status, - * normalizing non-`Error` throwables to `Error`. */ + * normalizing non-`Error` throwables to `Error`. + * + * Sets the {@link https://opentelemetry.io/docs/specs/semconv/registry/attributes/error/#error-type `error.type`} + * attribute to the error class name (e.g. `ClickHouseError`, `TypeError`), + * and, for server-side errors ({@link ClickHouseError}), the numeric server + * error code as `clickhouse.error.code`. */ export function recordSpanError(span: ClickHouseSpan, err: unknown): void { const error = err instanceof Error ? err : new Error(String(err)); + const attributes: ClickHouseSpanAttributes = { + "error.type": error.constructor.name, + }; + if (error instanceof ClickHouseError) { + const code = Number(error.code); + attributes["clickhouse.error.code"] = Number.isNaN(code) + ? error.code + : code; + } + span.setAttributes(attributes); span.recordException(error); span.setStatus({ code: ClickHouseSpanStatusCode.ERROR, diff --git a/packages/client-node/__tests__/unit/node_result_set_span.test.ts b/packages/client-node/__tests__/unit/node_result_set_span.test.ts new file mode 100644 index 00000000..a0b1d65b --- /dev/null +++ b/packages/client-node/__tests__/unit/node_result_set_span.test.ts @@ -0,0 +1,112 @@ +import { describe, expect, it } from "vitest"; +import type { + ClickHouseSpan, + ClickHouseSpanAttributes, + ClickHouseSpanStatus, +} from "@clickhouse/client-common"; +import Stream, { Readable } from "stream"; +import { ResultSet } from "../../src"; + +class RecordedSpan implements ClickHouseSpan { + attributes: ClickHouseSpanAttributes = {}; + status?: ClickHouseSpanStatus; + exception?: Error; + endedTimes = 0; + + setAttributes(attributes: ClickHouseSpanAttributes) { + this.attributes = { ...this.attributes, ...attributes }; + } + setStatus(status: ClickHouseSpanStatus) { + this.status = status; + } + recordException(error: Error) { + this.exception = error; + } + end() { + this.endedTimes++; + } +} + +describe("[Node.js] ResultSet span tracking", () => { + const expectedText = `{"foo":"bar"}\n{"qaz":"qux"}\n`; + + function makeResultSet(span: RecordedSpan, stream?: Stream.Readable) { + return new ResultSet( + stream ?? Readable.from([Buffer.from(expectedText)]), + "JSONEachRow", + "query-id", + undefined, + undefined, + undefined, + span, + ); + } + + it("ends the span and records decoded bytes after text()", async () => { + const span = new RecordedSpan(); + const rs = makeResultSet(span); + await rs.text(); + expect(span.endedTimes).toBe(1); + expect(span.attributes["clickhouse.response.decoded_bytes"]).toBe( + expectedText.length, + ); + // No rows were counted on the text() path. + expect(span.attributes["db.response.returned_rows"]).toBeUndefined(); + expect(span.status).toBeUndefined(); + }); + + it("ends the span and records rows + bytes after json()", async () => { + const span = new RecordedSpan(); + const rs = makeResultSet(span); + expect(await rs.json()).toEqual([{ foo: "bar" }, { qaz: "qux" }]); + expect(span.endedTimes).toBe(1); + expect(span.attributes["db.response.returned_rows"]).toBe(2); + expect(span.attributes["clickhouse.response.decoded_bytes"]).toBe( + expectedText.length, + ); + expect(span.status).toBeUndefined(); + }); + + it("ends the span and records rows + bytes after the stream is fully consumed", async () => { + const span = new RecordedSpan(); + const rs = makeResultSet(span); + let rows = 0; + for await (const batch of rs.stream()) { + rows += batch.length; + } + expect(rows).toBe(2); + expect(span.endedTimes).toBe(1); + expect(span.attributes["db.response.returned_rows"]).toBe(2); + expect(span.attributes["clickhouse.response.decoded_bytes"]).toBe( + expectedText.length, + ); + }); + + it("ends the span without an error when the ResultSet is closed", async () => { + const span = new RecordedSpan(); + const stream = Readable.from([Buffer.from(expectedText)]); + // Swallow the synthetic "ResultSet has been closed" destroy error. + stream.on("error", () => {}); + const rs = makeResultSet(span, stream); + rs.close(); + expect(span.endedTimes).toBe(1); + expect(span.status).toBeUndefined(); + expect(span.exception).toBeUndefined(); + }); + + it("records a streaming error on the span and ends it exactly once", async () => { + const span = new RecordedSpan(); + const failure = new Error("stream failed"); + const stream = new Readable({ + read() { + this.destroy(failure); + }, + }); + const rs = makeResultSet(span, stream); + await expect(rs.text()).rejects.toThrow("stream failed"); + expect(span.endedTimes).toBe(1); + expect(span.exception).toBe(failure); + expect(span.attributes["error.type"]).toBe("Error"); + expect(span.status?.code).toBe(2); // ERROR + }); +}); diff --git a/packages/client-node/src/config.ts b/packages/client-node/src/config.ts index e74cf9f4..6c1bcbee 100644 --- a/packages/client-node/src/config.ts +++ b/packages/client-node/src/config.ts @@ -1,4 +1,5 @@ import type { + ClickHouseSpan, DataFormat, ImplementationDetails, JSONHandling, @@ -167,6 +168,7 @@ export const NodeConfigImpl: Required< log_error: (err: Error) => void, response_headers: ResponseHeaders, jsonHandling: JSONHandling, + span?: ClickHouseSpan, ) => ResultSet.instance({ stream, @@ -175,5 +177,6 @@ export const NodeConfigImpl: Required< log_error, response_headers, jsonHandling, + span, })) as any, }; diff --git a/packages/client-node/src/result_set.ts b/packages/client-node/src/result_set.ts index f45f050e..3d97c16e 100644 --- a/packages/client-node/src/result_set.ts +++ b/packages/client-node/src/result_set.ts @@ -1,5 +1,7 @@ import type { BaseResultSet, + ClickHouseSpan, + ClickHouseSpanAttributes, DataFormat, JSONHandling, ResponseHeaders, @@ -12,6 +14,7 @@ import { defaultJSONHandling, EXCEPTION_TAG_HEADER_NAME, CARET_RETURN, + recordSpanError, } from "@clickhouse/client-common"; import { isNotStreamableJSONFamily, @@ -57,6 +60,7 @@ export interface ResultSetOptions { log_error: (error: Error) => void; response_headers: ResponseHeaders; jsonHandling?: JSONHandling; + span?: ClickHouseSpan; } export class ResultSet< @@ -76,6 +80,16 @@ export class ResultSet< */ private _stream: Stream.Readable; private readonly format: Format; + /** The `clickhouse.query.stream` span owned by this result set (if the + * client was configured with a tracer); it ends via {@link finishSpan} + * when the response stream is fully consumed, closed, or fails. */ + private readonly span: ClickHouseSpan | undefined; + /** Decoded (decompressed) bytes received from the server so far. */ + private span_bytes = 0; + /** Rows decoded from the response stream so far. */ + private span_rows = 0; + private span_rows_counted = false; + private span_finished = false; public readonly query_id: string; constructor( @@ -85,10 +99,12 @@ export class ResultSet< log_error?: (error: Error) => void, _response_headers?: ResponseHeaders, jsonHandling?: JSONHandling, + span?: ClickHouseSpan, ) { this._stream = _stream; this.format = format; this.query_id = query_id; + this.span = span; this.jsonHandling = { ...defaultJSONHandling, ...jsonHandling, @@ -112,9 +128,46 @@ export class ResultSet< return this._stream; } + /** Add the number of rows decoded from the response stream. */ + private addSpanRows(count: number): void { + this.span_rows_counted = true; + this.span_rows += count; + } + + /** Record the final response metrics (`clickhouse.response.decoded_bytes` + * and, when rows were counted, `db.response.returned_rows`) and the error + * (if any) on the span, and end it. Safe to call multiple times - only + * the first call wins. */ + private finishSpan(err?: unknown): void { + if (!this.span || this.span_finished) { + return; + } + this.span_finished = true; + const attributes: ClickHouseSpanAttributes = { + "clickhouse.response.decoded_bytes": this.span_bytes, + }; + if (this.span_rows_counted) { + attributes["db.response.returned_rows"] = this.span_rows; + } + this.span.setAttributes(attributes); + if (err) { + recordSpanError(this.span, err); + } + this.span.end(); + } + /** See {@link BaseResultSet.text}. */ async text(): Promise { - return await getAsText(this.consume()); + const stream = this.consume(); + try { + const text = await getAsText(stream); + this.span_bytes += Buffer.byteLength(text); + this.finishSpan(); + return text; + } catch (err) { + this.finishSpan(err); + throw err; + } } /** See {@link BaseResultSet.json}. */ @@ -124,6 +177,7 @@ export class ResultSet< const result: T[] = []; // Using the stream() instead of _stream directly to leverage the existing logic // for handling incomplete chunks and exception tags. + // The span progress is updated and the span is finished by the stream() pipeline. // TODO: consider using stream() for all formats to unify the logic and error handling. const stream = this.stream(); for await (const rows of stream) { @@ -135,8 +189,16 @@ export class ResultSet< } // JSON, JSONObjectEachRow, etc. if (isNotStreamableJSONFamily(this.format as DataFormat)) { - const text = await getAsText(this.consume()); - return this.jsonHandling.parse(text); + const stream = this.consume(); + try { + const text = await getAsText(stream); + this.span_bytes += Buffer.byteLength(text); + this.finishSpan(); + return this.jsonHandling.parse(text); + } catch (err) { + this.finishSpan(err); + throw err; + } } // should not be called for CSV, etc. throw new Error(`Cannot decode ${this.format} as JSON`); @@ -150,12 +212,15 @@ export class ResultSet< const logError = this.log_error; const exceptionTag = this.exceptionTag; const jsonHandling = this.jsonHandling; + // eslint-disable-next-line @typescript-eslint/no-this-alias + const resultSet = this; const toRows = new Transform({ transform( chunk: Buffer, _encoding: BufferEncoding, callback: TransformCallback, ) { + resultSet.span_bytes += chunk.length; const rows: Row[] = []; let lastIdx = 0; @@ -168,6 +233,7 @@ export class ResultSet< if (idx === -1) { incompleteChunks.push(chunk.subarray(lastIdx)); if (rows.length > 0) { + resultSet.addSpanRows(rows.length); this.push(rows); } break; @@ -217,6 +283,9 @@ export class ResultSet< err.message !== resultSetClosedMessage ) { logError(err); + resultSet.finishSpan(err); + } else { + resultSet.finishSpan(); } }, ); @@ -225,7 +294,13 @@ export class ResultSet< /** See {@link BaseResultSet.close}. */ close() { + // NB: _consumed is intentionally NOT set here. The stream is destroyed + // (not read to completion), so marking it as "consumed" would produce the + // misleading "Stream has been already consumed" error on any subsequent + // call. Downstream code will instead receive a "ResultSet has been closed" + // error from the destroyed stream, which accurately describes what happened. this._stream.destroy(new Error(resultSetClosedMessage)); + this.finishSpan(); } /** @@ -246,6 +321,7 @@ export class ResultSet< log_error, response_headers, jsonHandling, + span, }: ResultSetOptions): ResultSet { return new ResultSet( stream, @@ -254,6 +330,7 @@ export class ResultSet< log_error, response_headers, jsonHandling, + span, ); } } diff --git a/packages/client-web/src/config.ts b/packages/client-web/src/config.ts index 1998848f..cdfb0f1f 100644 --- a/packages/client-web/src/config.ts +++ b/packages/client-web/src/config.ts @@ -1,5 +1,6 @@ import type { BaseClickHouseClientConfigOptions, + ClickHouseSpan, ConnectionParams, DataFormat, ImplementationDetails, @@ -32,7 +33,17 @@ export const WebImpl: ImplementationDetails["impl"] = { query_id: string, _log_error: (err: Error) => void, response_headers: ResponseHeaders, - ) => new ResultSet(stream, format, query_id, response_headers)) as any, + jsonHandling: JSONHandling, + span?: ClickHouseSpan, + ) => + new ResultSet( + stream, + format, + query_id, + response_headers, + jsonHandling, + span, + )) as any, values_encoder: (jsonHandling: JSONHandling) => new WebValuesEncoder(jsonHandling), }; diff --git a/packages/client-web/src/result_set.ts b/packages/client-web/src/result_set.ts index 13e3051b..3c57cbd7 100644 --- a/packages/client-web/src/result_set.ts +++ b/packages/client-web/src/result_set.ts @@ -1,5 +1,7 @@ import type { BaseResultSet, + ClickHouseSpan, + ClickHouseSpanAttributes, DataFormat, JSONHandling, ResponseHeaders, @@ -10,6 +12,7 @@ import type { import { CARET_RETURN, extractErrorAtTheEndOfChunk, + recordSpanError, } from "@clickhouse/client-common"; import { isNotStreamableJSONFamily, @@ -20,6 +23,13 @@ import { getAsText } from "./utils"; const NEWLINE = 0x0a as const; +/** The WHATWG Streams spec includes a `cancel` callback on `Transformer`, but + * TypeScript's DOM lib does not yet declare it. This local extension adds it + * so we can detect source-stream aborts and consumer-side cancellations. */ +type TransformerWithCancel = Transformer & { + cancel?: (reason?: unknown) => void | PromiseLike; +}; + export class ResultSet< Format extends DataFormat | unknown, > implements BaseResultSet, Format> { @@ -30,6 +40,20 @@ export class ResultSet< private readonly jsonHandling: JSONHandling; private _stream: ReadableStream; private readonly format: Format; + /** The `clickhouse.query.stream` span owned by this result set (if the + * client was configured with a tracer); it ends via {@link finishSpan} + * when the response stream is fully consumed, closed, or fails. */ + private readonly span: ClickHouseSpan | undefined; + /** Decoded (decompressed) bytes received from the server so far. */ + private span_bytes = 0; + /** Rows decoded from the response stream so far. */ + private span_rows = 0; + private span_rows_counted = false; + /** UTF-16 code-unit length of the response body, recorded by {@link text} + * instead of {@link span_bytes} to avoid the `TextEncoder` allocation that + * accurate byte counting would require. */ + private span_text_length: number | undefined; + private span_finished = false; public readonly query_id: string; constructor( @@ -41,10 +65,12 @@ export class ResultSet< parse: JSON.parse, stringify: JSON.stringify, }, + span?: ClickHouseSpan, ) { this._stream = _stream; this.format = format; this.query_id = query_id; + this.span = span; this.response_headers = _response_headers !== undefined ? Object.freeze(_response_headers) : {}; this.exceptionTag = this.response_headers["x-clickhouse-exception-tag"] as @@ -57,7 +83,19 @@ export class ResultSet< /** See {@link BaseResultSet.text} */ async text(): Promise { this.markAsConsumed(); - return getAsText(this._stream); + try { + const text = await getAsText(this._stream); + // text.length is the UTF-16 code-unit count of the response body, not a + // byte count. We record it as `clickhouse.response.text_length` rather + // than `span_bytes` (`clickhouse.response.decoded_bytes`) to avoid the + // `TextEncoder` allocation that accurate byte counting would require here. + this.span_text_length = text.length; + this.finishSpan(); + return text; + } catch (err) { + this.finishSpan(err); + throw err; + } } /** See {@link BaseResultSet.json} */ @@ -65,23 +103,38 @@ export class ResultSet< // JSONEachRow, etc. if (isStreamableJSONFamily(this.format as DataFormat)) { const result: T[] = []; + // The span progress is updated and the span is finished by the stream() pipeline. const reader = this.stream().getReader(); - while (true) { - const { done, value } = await reader.read(); - if (done) { - break; - } - for (const row of value) { - result.push(row.json() as T); + try { + while (true) { + const { done, value } = await reader.read(); + if (done) { + break; + } + for (const row of value) { + result.push(row.json() as T); + } } + } catch (err) { + this.finishSpan(err); + throw err; } return result as any; } // JSON, JSONObjectEachRow, etc. if (isNotStreamableJSONFamily(this.format as DataFormat)) { - const text = await getAsText(this._stream); - return this.jsonHandling.parse(text); + try { + const text = await getAsText(this._stream); + // Same as text(): record text.length (UTF-16 code-unit count) rather + // than span_bytes to avoid the TextEncoder allocation. + this.span_text_length = text.length; + this.finishSpan(); + return this.jsonHandling.parse(text); + } catch (err) { + this.finishSpan(err); + throw err; + } } // should not be called for CSV, etc. throw new Error(`Cannot decode ${this.format} as JSON`); @@ -98,7 +151,7 @@ export class ResultSet< const exceptionTag = this.exceptionTag; const jsonHandling = this.jsonHandling; const decoder = new TextDecoder("utf-8"); - const transform = new TransformStream({ + const transformerOptions: TransformerWithCancel = { start() { // }, @@ -107,6 +160,7 @@ export class ResultSet< controller.terminate(); } + this.span_bytes += chunk.length; const rows: Row[] = []; let idx: number; @@ -125,6 +179,7 @@ export class ResultSet< // send the extracted rows to the consumer, if any if (rows.length > 0) { + this.addSpanRows(rows.length); controller.enqueue(rows); } break; @@ -137,9 +192,10 @@ export class ResultSet< idx >= 1 && chunk[idx - 1] === CARET_RETURN ) { - controller.error( - extractErrorAtTheEndOfChunk(chunk, exceptionTag), - ); + const err = extractErrorAtTheEndOfChunk(chunk, exceptionTag); + this.finishSpan(err); + controller.error(err); + return; // stop further processing once the stream is errored } // using the incomplete chunks from the previous iterations @@ -181,7 +237,19 @@ export class ResultSet< } } }, - }); + flush: () => { + // The readable side of the transform completes when the source + // stream is fully consumed - finalize the query span. + this.finishSpan(); + }, + cancel: (reason?: unknown) => { + // Called when the readable side is cancelled by the consumer, or + // when the writable side is aborted (e.g. source stream network + // error). Either way, the span must be properly ended. + this.finishSpan(reason); + }, + }; + const transform = new TransformStream(transformerOptions); const pipeline = this._stream.pipeThrough(transform, { preventClose: false, @@ -194,6 +262,7 @@ export class ResultSet< async close(): Promise { this.markAsConsumed(); await this._stream.cancel(); + this.finishSpan(); } /** @@ -213,6 +282,41 @@ export class ResultSet< } this.isAlreadyConsumed = true; } + + /** Add the number of rows decoded from the response stream. */ + private addSpanRows(count: number): void { + this.span_rows_counted = true; + this.span_rows += count; + } + + /** Record the final response metrics (`clickhouse.response.decoded_bytes`, + * `clickhouse.response.text_length` when called from {@link text}, and + * `db.response.returned_rows` when rows were counted) and the error + * (if any) on the span, and end it. Safe to call multiple times - only + * the first call wins. */ + private finishSpan(err?: unknown): void { + if (this.span === undefined || this.span_finished) { + return; + } + this.span_finished = true; + const attributes: ClickHouseSpanAttributes = {}; + if (this.span_text_length !== undefined) { + // Recorded by text(): UTF-16 code-unit count, not a byte count. + attributes["clickhouse.response.text_length"] = this.span_text_length; + } else { + // Recorded by stream() / json() streamable path: actual decoded bytes + // accumulated from raw Uint8Array chunks. + attributes["clickhouse.response.decoded_bytes"] = this.span_bytes; + } + if (this.span_rows_counted) { + attributes["db.response.returned_rows"] = this.span_rows; + } + this.span.setAttributes(attributes); + if (err !== undefined && err !== null) { + recordSpanError(this.span, err); + } + this.span.end(); + } } const streamAlreadyConsumedMessage = "Stream has been already consumed";