From 243a218f9c8692ceba85d495661e073bbb6467c0 Mon Sep 17 00:00:00 2001 From: slvrtrn Date: Fri, 8 Mar 2024 05:19:30 +0100 Subject: [PATCH 01/16] Infer ResultSet types based on DataFormat --- examples/select_json_each_row.ts | 4 +- .../integration/abort_request.test.ts | 8 +-- .../__tests__/integration/exec.test.ts | 15 ++-- .../integration/multiple_clients.test.ts | 5 +- .../integration/read_only_user.test.ts | 2 +- .../integration/response_compression.test.ts | 2 +- .../__tests__/integration/select.test.ts | 12 ++-- .../integration/select_result.test.ts | 4 +- packages/client-common/src/client.ts | 12 +++- packages/client-common/src/config.ts | 20 ++++-- .../src/data_formatter/formatter.ts | 24 ++++--- packages/client-common/src/result.ts | 69 +++++++++++++++++-- .../node_max_open_connections.test.ts | 6 +- packages/client-node/src/client.ts | 4 +- packages/client-node/src/config.ts | 6 +- packages/client-node/src/index.ts | 3 +- packages/client-node/src/result_set.ts | 6 +- packages/client-web/src/config.ts | 4 +- packages/client-web/src/index.ts | 3 +- packages/client-web/src/result_set.ts | 6 +- 20 files changed, 148 insertions(+), 67 deletions(-) diff --git a/examples/select_json_each_row.ts b/examples/select_json_each_row.ts index 99835946..dbe8b802 100644 --- a/examples/select_json_each_row.ts +++ b/examples/select_json_each_row.ts @@ -8,7 +8,7 @@ void (async () => { query: 'SELECT number FROM system.numbers LIMIT 5', format: 'JSONEachRow', }) - const result = await rows.json>() - result.map((row: Data) => console.log(row)) + const result = await rows.json() + result.map((row) => console.log(row)) await client.close() })() diff --git a/packages/client-common/__tests__/integration/abort_request.test.ts b/packages/client-common/__tests__/integration/abort_request.test.ts index 268dabcb..2676be2a 100644 --- a/packages/client-common/__tests__/integration/abort_request.test.ts +++ b/packages/client-common/__tests__/integration/abort_request.test.ts @@ -1,4 +1,4 @@ -import type { ClickHouseClient, ResponseJSON } from '@clickhouse/client-common' +import type { ClickHouseClient } from '@clickhouse/client-common' import { createTestClient, guid, sleep } from '../utils' describe('abort request', () => { @@ -111,8 +111,6 @@ describe('abort request', () => { }) it('should cancel of the select queries while keeping the others', async () => { - type Res = Array<{ foo: number }> - const controller = new AbortController() const results: number[] = [] @@ -127,7 +125,7 @@ describe('abort request', () => { // we will cancel the request that should've yielded '3' shouldAbort ? controller.signal : undefined, }) - .then((r) => r.json()) + .then((r) => r.json<{ foo: number }>()) .then((r) => results.push(r[0].foo)) // this way, the cancelled request will not cancel the others if (shouldAbort) { @@ -157,7 +155,7 @@ async function assertActiveQueries( query: 'SELECT query FROM system.processes', format: 'JSON', }) - const queries = await rs.json>() + const queries = await rs.json<{ query: string }>() if (assertQueries(queries.data)) { isRunning = false } else { diff --git a/packages/client-common/__tests__/integration/exec.test.ts b/packages/client-common/__tests__/integration/exec.test.ts index ce6eae97..39d3cf27 100644 --- a/packages/client-common/__tests__/integration/exec.test.ts +++ b/packages/client-common/__tests__/integration/exec.test.ts @@ -1,4 +1,4 @@ -import type { ExecParams, ResponseJSON } from '@clickhouse/client-common' +import type { ExecParams } from '@clickhouse/client-common' import { type ClickHouseClient } from '@clickhouse/client-common' import { createTestClient, @@ -100,10 +100,7 @@ describe('exec', () => { format: 'JSON', }) - const json = await result.json<{ - rows: number - data: Array<{ name: string }> - }>() + const json = await result.json<{ name: string }>() expect(json.rows).toBe(1) expect(json.data[0].name).toBe('numbers') }) @@ -120,9 +117,11 @@ describe('exec', () => { format: 'JSON', }) - const { data, rows } = await selectResult.json< - ResponseJSON<{ name: string; engine: string; create_table_query: string }> - >() + const { data, rows } = await selectResult.json<{ + name: string + engine: string + create_table_query: string + }>() expect(rows).toBe(1) const table = data[0] diff --git a/packages/client-common/__tests__/integration/multiple_clients.test.ts b/packages/client-common/__tests__/integration/multiple_clients.test.ts index 6fa89a7f..716e7fd0 100644 --- a/packages/client-common/__tests__/integration/multiple_clients.test.ts +++ b/packages/client-common/__tests__/integration/multiple_clients.test.ts @@ -20,7 +20,6 @@ describe('multiple clients', () => { }) it('should send multiple parallel selects', async () => { - type Res = Array<{ sum: number }> const results: number[] = [] await Promise.all( clients.map((client, i) => @@ -29,8 +28,8 @@ describe('multiple clients', () => { query: `SELECT toInt32(sum(*)) AS sum FROM numbers(0, ${i + 2});`, format: 'JSONEachRow', }) - .then((r) => r.json()) - .then((json: Res) => results.push(json[0].sum)) + .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]) 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 b0051aa3..070c84df 100644 --- a/packages/client-common/__tests__/integration/read_only_user.test.ts +++ b/packages/client-common/__tests__/integration/read_only_user.test.ts @@ -45,7 +45,7 @@ describe('read only user', () => { .query({ query: `SELECT * FROM ${tableName}`, }) - .then((r) => r.json<{ data: unknown[] }>()) + .then((r) => r.json()) expect(result.data).toEqual([{ id: '42', name: 'hello', sku: [0, 1] }]) }) diff --git a/packages/client-common/__tests__/integration/response_compression.test.ts b/packages/client-common/__tests__/integration/response_compression.test.ts index ed06a28b..c4476536 100644 --- a/packages/client-common/__tests__/integration/response_compression.test.ts +++ b/packages/client-common/__tests__/integration/response_compression.test.ts @@ -23,7 +23,7 @@ describe('response compression', () => { format: 'JSONEachRow', }) - const response = await rs.json<{ number: string }[]>() + 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/select.test.ts b/packages/client-common/__tests__/integration/select.test.ts index b09d44e8..64ad785f 100644 --- a/packages/client-common/__tests__/integration/select.test.ts +++ b/packages/client-common/__tests__/integration/select.test.ts @@ -1,7 +1,4 @@ -import { - type ClickHouseClient, - type ResponseJSON, -} from '@clickhouse/client-common' +import { type ClickHouseClient } from '@clickhouse/client-common' import { createTestClient, guid, validateUUID } from '../utils' describe('select', () => { @@ -108,7 +105,7 @@ describe('select', () => { format: 'JSON', }) - const response = await rs.json>() + const response = await rs.json<{ number: string }>() expect(response.data).toEqual([{ number: '0' }, { number: '1' }]) }) @@ -164,7 +161,6 @@ describe('select', () => { }) it('can send multiple simultaneous requests', async () => { - type Res = Array<{ sum: number }> const results: number[] = [] await Promise.all( [...Array(5)].map((_, i) => @@ -173,8 +169,8 @@ describe('select', () => { query: `SELECT toInt32(sum(*)) AS sum FROM numbers(0, ${i + 2});`, format: 'JSONEachRow', }) - .then((r) => r.json()) - .then((json: Res) => results.push(json[0].sum)) + .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]) diff --git a/packages/client-common/__tests__/integration/select_result.test.ts b/packages/client-common/__tests__/integration/select_result.test.ts index 2699154a..116240f1 100644 --- a/packages/client-common/__tests__/integration/select_result.test.ts +++ b/packages/client-common/__tests__/integration/select_result.test.ts @@ -38,7 +38,7 @@ describe('Select ResultSet', () => { format: 'JSON', }) - const { data: nums } = await rs.json>() + const { data: nums } = await rs.json<{ number: string }>() expect(Array.isArray(nums)).toBe(true) expect(nums.length).toEqual(5) const values = nums.map((i) => i.number) @@ -51,7 +51,7 @@ describe('Select ResultSet', () => { format: 'JSON', }) - const { meta } = await rs.json>() + const { meta } = await rs.json<{ number: string }>() expect(meta?.length).toBe(1) const column = meta ? meta[0] : undefined diff --git a/packages/client-common/src/client.ts b/packages/client-common/src/client.ts index ea2d90a0..7ebb0a3b 100644 --- a/packages/client-common/src/client.ts +++ b/packages/client-common/src/client.ts @@ -10,7 +10,6 @@ import type { InputJSON, InputJSONObjectEachRow } from './clickhouse_types' import type { CloseStream, ImplementationDetails, - MakeResultSet, ValuesEncoder, } from './config' import { getConnectionParams, prepareConfigWithURL } from './config' @@ -102,7 +101,7 @@ export interface InsertParams export class ClickHouseClient { private readonly clientClickHouseSettings: ClickHouseSettings private readonly connection: Connection - private readonly makeResultSet: MakeResultSet + private readonly makeResultSet: ImplementationDetails['impl']['make_result_set'] private readonly valuesEncoder: ValuesEncoder private readonly closeStream: CloseStream private readonly sessionId?: string @@ -136,7 +135,14 @@ export class ClickHouseClient { * Consider using {@link ClickHouseClient.insert} for data insertion, * or {@link ClickHouseClient.command} for DDLs. */ - async query(params: QueryParams): Promise> { + async query( + params: Omit & { format?: Format } + ): Promise< + BaseResultSet< + Stream, + Format extends undefined ? 'JSON' : NonNullable + > + > { const format = params.format ?? 'JSON' const query = formatQuery(params.query, format) const { stream, query_id } = await this.connection.query({ diff --git a/packages/client-common/src/config.ts b/packages/client-common/src/config.ts index cdff26f9..298005e7 100644 --- a/packages/client-common/src/config.ts +++ b/packages/client-common/src/config.ts @@ -88,11 +88,11 @@ export type MakeConnection< Config = BaseClickHouseClientConfigOptionsWithURL > = (config: Config, params: ConnectionParams) => Connection -export type MakeResultSet = ( - stream: Stream, - format: DataFormat, - session_id: string -) => BaseResultSet +export type MakeResultSet< + Stream, + Format extends DataFormat, + ResultSet extends BaseResultSet +> = (stream: Stream, format: Format, query_id: string) => ResultSet export interface ValuesEncoder { validateInsertValues( @@ -141,7 +141,15 @@ export type HandleImplSpecificURLParams = ( export interface ImplementationDetails { impl: { make_connection: MakeConnection - make_result_set: MakeResultSet + make_result_set: < + Stream, + Format extends DataFormat, + ResultSet extends BaseResultSet + >( + stream: Stream, + format: Format, + query_id: string + ) => ResultSet values_encoder: ValuesEncoder close_stream: CloseStream handle_specific_url_params?: HandleImplSpecificURLParams diff --git a/packages/client-common/src/data_formatter/formatter.ts b/packages/client-common/src/data_formatter/formatter.ts index 259e9b00..306c21e8 100644 --- a/packages/client-common/src/data_formatter/formatter.ts +++ b/packages/client-common/src/data_formatter/formatter.ts @@ -8,15 +8,18 @@ const streamableJSONFormats = [ 'JSONCompactStringsEachRowWithNames', 'JSONCompactStringsEachRowWithNamesAndTypes', ] as const +// Returned as { row_1: T, row_2: T, ...} +const recordsJSONFormats = ['JSONObjectEachRow'] as const +// See ResponseJSON type const singleDocumentJSONFormats = [ 'JSON', 'JSONStrings', 'JSONCompact', 'JSONCompactStrings', 'JSONColumnsWithMetadata', - 'JSONObjectEachRow', ] as const const supportedJSONFormats = [ + ...recordsJSONFormats, ...singleDocumentJSONFormats, ...streamableJSONFormats, ] as const @@ -34,31 +37,36 @@ const supportedRawFormats = [ 'Parquet', ] as const -export type JSONDataFormat = (typeof supportedJSONFormats)[number] export type RawDataFormat = (typeof supportedRawFormats)[number] -export type DataFormat = JSONDataFormat | RawDataFormat -type SingleDocumentStreamableJsonDataFormat = +export type StreamableJSONDataFormat = (typeof streamableJSONFormats)[number] +export type SingleDocumentJSONFormat = (typeof singleDocumentJSONFormats)[number] -type StreamableJsonDataFormat = (typeof streamableJSONFormats)[number] +export type RecordsJSONFormat = (typeof recordsJSONFormats)[number] +export type JSONDataFormat = + | StreamableJSONDataFormat + | SingleDocumentJSONFormat + | RecordsJSONFormat + +export type DataFormat = JSONDataFormat | RawDataFormat // TODO add others formats const streamableFormat = [ ...streamableJSONFormats, ...supportedRawFormats, ] as const -type StreamableDataFormat = (typeof streamableFormat)[number] +export type StreamableDataFormat = (typeof streamableFormat)[number] function isNotStreamableJSONFamily( format: DataFormat -): format is SingleDocumentStreamableJsonDataFormat { +): format is SingleDocumentJSONFormat { // @ts-expect-error JSON is not assignable to notStreamableJSONFormats return singleDocumentJSONFormats.includes(format) } function isStreamableJSONFamily( format: DataFormat -): format is StreamableJsonDataFormat { +): format is StreamableJSONDataFormat { // @ts-expect-error JSON is not assignable to streamableJSONFormats return streamableJSONFormats.includes(format) } diff --git a/packages/client-common/src/result.ts b/packages/client-common/src/result.ts index a86b1f9b..2e161499 100644 --- a/packages/client-common/src/result.ts +++ b/packages/client-common/src/result.ts @@ -1,3 +1,27 @@ +import type { ResponseJSON } from './clickhouse_types' +import type { + DataFormat, + RawDataFormat, + RecordsJSONFormat, + SingleDocumentJSONFormat, + StreamableJSONDataFormat, +} from './data_formatter' + +export type JSONReturnType = + // JSON*EachRow formats + F extends StreamableJSONDataFormat + ? T[] + : // JSON formats with known layout { data, meta, statistics, ... } + F extends SingleDocumentJSONFormat + ? ResponseJSON + : // JSON formats returned as a Record + F extends RecordsJSONFormat + ? Record + : // CSV, TSV etc. - cannot be represented as JSON + F extends RawDataFormat + ? never + : T // technically, should never happen + export interface Row { /** A string representation of a row. */ text: string @@ -10,11 +34,16 @@ export interface Row { json(): T } -export interface BaseResultSet { +export interface BaseResultSet< + Stream, + Format extends DataFormat | unknown = unknown +> { /** * The method waits for all the rows to be fully loaded * and returns the result as a string. * + * It is possible to call this method for all supported formats. + * * The method should throw if the underlying stream was already consumed * by calling the other methods. */ @@ -24,14 +53,44 @@ export interface BaseResultSet { * The method waits for the all the rows to be fully loaded. * When the response is received in full, it will be decoded to return JSON. * + * Should be called only for JSON* formats family. + * * The method should throw if the underlying stream was already consumed - * by calling the other methods. + * 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 - * (i.e. all except JSON). + * Returns a readable stream for responses that can be streamed. + * + * Formats that CAN be streamed ({@link StreamableDataFormat}): + * * JSONEachRow + * * JSONStringsEachRow + * * JSONCompactEachRow + * * JSONCompactStringsEachRow + * * JSONCompactEachRowWithNames + * * JSONCompactEachRowWithNamesAndTypes + * * JSONCompactStringsEachRowWithNames + * * JSONCompactStringsEachRowWithNamesAndTypes + * * CSV + * * CSVWithNames + * * CSVWithNamesAndTypes + * * TabSeparated + * * TabSeparatedRaw + * * TabSeparatedWithNames + * * TabSeparatedWithNamesAndTypes + * * CustomSeparated + * * CustomSeparatedWithNames + * * CustomSeparatedWithNamesAndTypes + * + * Formats that CANNOT be streamed: + * * JSON + * * JSONStrings + * * JSONCompact + * * JSONCompactStrings + * * JSONColumnsWithMetadata + * * JSONObjectEachRow * * Every iteration provides an array of {@link Row} instances * for {@link StreamableDataFormat} format. 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 ada9788a..0026fc3a 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 @@ -11,13 +11,13 @@ describe('[Node.js] max_open_connections config', () => { results = [] }) - function select(query: string) { + async function select(query: string) { return client .query({ query, format: 'JSONEachRow', }) - .then((r) => r.json<[{ x: number }]>()) + .then((r) => r.json<{ x: number }>()) .then(([{ x }]) => results.push(x)) } @@ -67,7 +67,7 @@ describe('[Node.js] max_open_connections config', () => { format: 'JSONEachRow', }) - const json = await result.json() + const json = await result.json() expect(json).toContain(value1) expect(json).toContain(value2) expect(json.length).toEqual(2) diff --git a/packages/client-node/src/client.ts b/packages/client-node/src/client.ts index 1191e49f..7b420fb2 100644 --- a/packages/client-node/src/client.ts +++ b/packages/client-node/src/client.ts @@ -3,9 +3,11 @@ import type Stream from 'stream' import type { NodeClickHouseClientConfigOptions } from './config' import { NodeConfigImpl } from './config' +export type NodeClickHouseClient = ClickHouseClient + export function createClient( config?: NodeClickHouseClientConfigOptions -): ClickHouseClient { +): NodeClickHouseClient { return new ClickHouseClient({ impl: NodeConfigImpl, ...(config || {}), diff --git a/packages/client-node/src/config.ts b/packages/client-node/src/config.ts index b8f960fb..327be96f 100644 --- a/packages/client-node/src/config.ts +++ b/packages/client-node/src/config.ts @@ -115,11 +115,11 @@ export const NodeConfigImpl: Required< return createConnection(params, tls, keep_alive) }, values_encoder: new NodeValuesEncoder(), - make_result_set: ( + make_result_set: (( stream: Stream.Readable, format: DataFormat, - session_id: string - ) => new ResultSet(stream, format, session_id), + query_id: string + ) => new ResultSet(stream, format, query_id)) as any, // FIXME: resolve weird type issue - the types actually match close_stream: async (stream) => { stream.destroy() }, diff --git a/packages/client-node/src/index.ts b/packages/client-node/src/index.ts index 6ae917df..6fe48421 100644 --- a/packages/client-node/src/index.ts +++ b/packages/client-node/src/index.ts @@ -1,3 +1,4 @@ +export type { NodeClickHouseClient } from './client' export { createClient } from './client' export { NodeClickHouseClientConfigOptions as ClickHouseClientConfigOptions } from './config' export { ResultSet } from './result_set' @@ -26,8 +27,8 @@ export { type InputJSONObjectEachRow, type BaseResultSet, type PingResult, + ClickHouseClient, ClickHouseError, ClickHouseLogLevel, - ClickHouseClient, SettingsMap, } from '@clickhouse/client-common' diff --git a/packages/client-node/src/result_set.ts b/packages/client-node/src/result_set.ts index a0c1eff1..233d3642 100644 --- a/packages/client-node/src/result_set.ts +++ b/packages/client-node/src/result_set.ts @@ -7,10 +7,12 @@ import { getAsText } from './utils' const NEWLINE = 0x0a as const -export class ResultSet implements BaseResultSet { +export class ResultSet + implements BaseResultSet +{ constructor( private _stream: Stream.Readable, - private readonly format: DataFormat, + private readonly format: Format, public readonly query_id: string ) {} diff --git a/packages/client-web/src/config.ts b/packages/client-web/src/config.ts index ed332f87..0b934bf8 100644 --- a/packages/client-web/src/config.ts +++ b/packages/client-web/src/config.ts @@ -12,11 +12,11 @@ export type WebClickHouseClientConfigOptions = BaseClickHouseClientConfigOptions export const WebImpl: ImplementationDetails['impl'] = { make_connection: (_, params: ConnectionParams) => new WebConnection(params), - make_result_set: ( + make_result_set: (( stream: ReadableStream, format: DataFormat, query_id: string - ) => new ResultSet(stream, format, query_id), + ) => new ResultSet(stream, format, query_id)) as any, // FIXME: resolve weird type issue - the types actually match values_encoder: new WebValuesEncoder(), close_stream: (stream) => stream.cancel(), } diff --git a/packages/client-web/src/index.ts b/packages/client-web/src/index.ts index 997b7b1f..7f356712 100644 --- a/packages/client-web/src/index.ts +++ b/packages/client-web/src/index.ts @@ -1,3 +1,4 @@ +export type { WebClickHouseClient } from './client' export { createClient } from './client' export { WebClickHouseClientConfigOptions as ClickHouseClientConfigOptions } from './config' export { ResultSet } from './result_set' @@ -26,8 +27,8 @@ export { type InputJSONObjectEachRow, type BaseResultSet, type PingResult, + ClickHouseClient, ClickHouseError, ClickHouseLogLevel, - ClickHouseClient, SettingsMap, } from '@clickhouse/client-common' diff --git a/packages/client-web/src/result_set.ts b/packages/client-web/src/result_set.ts index 9052afe0..cbd3d672 100644 --- a/packages/client-web/src/result_set.ts +++ b/packages/client-web/src/result_set.ts @@ -2,11 +2,13 @@ import type { BaseResultSet, DataFormat, Row } from '@clickhouse/client-common' import { decode, validateStreamFormat } from '@clickhouse/client-common' import { getAsText } from './utils' -export class ResultSet implements BaseResultSet> { +export class ResultSet + implements BaseResultSet, Format> +{ private isAlreadyConsumed = false constructor( private _stream: ReadableStream, - private readonly format: DataFormat, + private readonly format: Format, public readonly query_id: string ) {} From 4b926e8790268d9070e034dccdd3f6f9f3c825fb Mon Sep 17 00:00:00 2001 From: slvrtrn Date: Fri, 8 Mar 2024 20:01:42 +0100 Subject: [PATCH 02/16] Stream experiments --- packages/client-common/src/config.ts | 8 ++-- packages/client-common/src/index.ts | 8 +++- packages/client-common/src/result.ts | 24 +++++++++--- .../__tests__/integration/node_client.test.ts | 17 +++++++-- packages/client-node/src/client.ts | 18 +++++++-- packages/client-node/src/config.ts | 2 +- packages/client-node/src/index.ts | 2 +- packages/client-node/src/result_set.ts | 38 ++++++++++++++++--- packages/client-web/src/config.ts | 2 +- packages/client-web/src/result_set.ts | 9 ++++- 10 files changed, 103 insertions(+), 25 deletions(-) diff --git a/packages/client-common/src/config.ts b/packages/client-common/src/config.ts index 298005e7..ebd6fc2b 100644 --- a/packages/client-common/src/config.ts +++ b/packages/client-common/src/config.ts @@ -142,9 +142,11 @@ export interface ImplementationDetails { impl: { make_connection: MakeConnection make_result_set: < - Stream, - Format extends DataFormat, - ResultSet extends BaseResultSet + Format extends DataFormat | undefined, + ResultSet extends BaseResultSet< + Stream, + Format extends undefined ? 'JSON' : NonNullable + > >( stream: Stream, format: Format, diff --git a/packages/client-common/src/index.ts b/packages/client-common/src/index.ts index f738f537..29001b31 100644 --- a/packages/client-common/src/index.ts +++ b/packages/client-common/src/index.ts @@ -13,7 +13,13 @@ export { type PingResult, } from './client' export { type BaseClickHouseClientConfigOptions } from './config' -export type { Row, BaseResultSet } from './result' +export type { + Row, + BaseResultSet, + ResultJSONType, + RowJSONType, + StreamWithFormat, +} from './result' export { type DataFormat } from './data_formatter' export { ClickHouseError } from './error' export { diff --git a/packages/client-common/src/result.ts b/packages/client-common/src/result.ts index 2e161499..a4dbe9a8 100644 --- a/packages/client-common/src/result.ts +++ b/packages/client-common/src/result.ts @@ -1,13 +1,14 @@ import type { ResponseJSON } from './clickhouse_types' -import type { +import { DataFormat, RawDataFormat, RecordsJSONFormat, SingleDocumentJSONFormat, + StreamableDataFormat, StreamableJSONDataFormat, } from './data_formatter' -export type JSONReturnType = +export type ResultJSONType = // JSON*EachRow formats F extends StreamableJSONDataFormat ? T[] @@ -22,7 +23,19 @@ export type JSONReturnType = ? never : T // technically, should never happen -export interface Row { +export type RowJSONType = + // JSON*EachRow formats + F extends StreamableJSONDataFormat + ? T + : // CSV, TSV, non-streamable JSON formats - cannot be streamed as JSON + F extends RawDataFormat | SingleDocumentJSONFormat | RecordsJSONFormat + ? never + : T // technically, should never happen + +export interface Row< + Format extends DataFormat | unknown = unknown, + JSONType = unknown +> { /** A string representation of a row. */ text: string @@ -31,7 +44,7 @@ export interface Row { * The method will throw if called on a response in JSON incompatible format. * It is safe to call this method multiple times. */ - json(): T + json(): RowJSONType } export interface BaseResultSet< @@ -59,7 +72,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. @@ -83,6 +96,7 @@ export interface BaseResultSet< * * CustomSeparated * * CustomSeparatedWithNames * * CustomSeparatedWithNamesAndTypes + * * Parquet * * Formats that CANNOT be streamed: * * JSON diff --git a/packages/client-node/__tests__/integration/node_client.test.ts b/packages/client-node/__tests__/integration/node_client.test.ts index 115638bc..0b3904d1 100644 --- a/packages/client-node/__tests__/integration/node_client.test.ts +++ b/packages/client-node/__tests__/integration/node_client.test.ts @@ -1,6 +1,6 @@ +import { ResultSet } from '../../src' import Http from 'http' -import type Stream from 'stream' -import type { ClickHouseClient } from '../../src' +import type { NodeClickHouseClient } from '../../src' import { createClient } from '../../src' import { emitResponseBody, stubClientRequest } from '../utils/http_stubs' @@ -77,9 +77,20 @@ describe('[Node.js] Client', () => { }) }) - async function query(client: ClickHouseClient) { + async function query(client: NodeClickHouseClient) { const selectPromise = client.query({ query: 'SELECT * FROM system.numbers LIMIT 5', + format: 'JSONEachRow', + }) + type Data = { foo: string } + const rs = await selectPromise + const x = rs.json() + const s = rs.stream() + s.on('data', (r) => { + r.forEach((r) => { + const t = r.text + const j = r.json() + }) }) emitResponseBody(clientRequest, 'hi') await selectPromise diff --git a/packages/client-node/src/client.ts b/packages/client-node/src/client.ts index 7b420fb2..0cb91b90 100644 --- a/packages/client-node/src/client.ts +++ b/packages/client-node/src/client.ts @@ -1,9 +1,21 @@ -import { ClickHouseClient } from '@clickhouse/client-common' +import { + ClickHouseClient, + type DataFormat, + QueryParams, +} from '@clickhouse/client-common' import type Stream from 'stream' import type { NodeClickHouseClientConfigOptions } from './config' import { NodeConfigImpl } from './config' +import { ResultSet } from './result_set' -export type NodeClickHouseClient = ClickHouseClient +export type NodeClickHouseClient = Omit< + ClickHouseClient, + 'query' +> & { + query( + params: Omit & { format?: Format } + ): Promise>> +} export function createClient( config?: NodeClickHouseClientConfigOptions @@ -11,5 +23,5 @@ export function createClient( return new ClickHouseClient({ impl: NodeConfigImpl, ...(config || {}), - }) + }) as any } diff --git a/packages/client-node/src/config.ts b/packages/client-node/src/config.ts index 327be96f..b2d2ac36 100644 --- a/packages/client-node/src/config.ts +++ b/packages/client-node/src/config.ts @@ -119,7 +119,7 @@ export const NodeConfigImpl: Required< stream: Stream.Readable, format: DataFormat, query_id: string - ) => new ResultSet(stream, format, query_id)) as any, // FIXME: resolve weird type issue - the types actually match + ) => new ResultSet(stream, format, query_id)) as any, close_stream: async (stream) => { stream.destroy() }, diff --git a/packages/client-node/src/index.ts b/packages/client-node/src/index.ts index 6fe48421..c8d0f0ef 100644 --- a/packages/client-node/src/index.ts +++ b/packages/client-node/src/index.ts @@ -1,7 +1,7 @@ export type { NodeClickHouseClient } from './client' export { createClient } from './client' export { NodeClickHouseClientConfigOptions as ClickHouseClientConfigOptions } from './config' -export { ResultSet } from './result_set' +export { ResultSet, ResultStream } from './result_set' /** Re-export @clickhouse/client-common types */ export { diff --git a/packages/client-node/src/result_set.ts b/packages/client-node/src/result_set.ts index 233d3642..cca72265 100644 --- a/packages/client-node/src/result_set.ts +++ b/packages/client-node/src/result_set.ts @@ -1,12 +1,38 @@ -import type { BaseResultSet, DataFormat, Row } from '@clickhouse/client-common' +import type { + BaseResultSet, + DataFormat, + ResultJSONType, + Row, + RowJSONType, + StreamWithFormat, +} from '@clickhouse/client-common' import { decode, validateStreamFormat } from '@clickhouse/client-common' +import { StreamableDataFormat } from '@clickhouse/client-common/src/data_formatter' import { Buffer } from 'buffer' -import type { TransformCallback } from 'stream' -import Stream, { Transform } from 'stream' +import Stream, { Readable, Transform, TransformCallback } from 'stream' import { getAsText } from './utils' const NEWLINE = 0x0a as const +// an exact copy from stream.d.ts, but with typed `on('data')` +export type ResultStream = Omit & { + on(event: 'data', listener: (chunk: T) => void): Stream.Readable + on(event: 'close', listener: () => void): Stream.Readable + on(event: 'drain', listener: () => void): Stream.Readable + on(event: 'end', listener: () => void): Stream.Readable + on(event: 'error', listener: (err: Error) => void): Stream.Readable + on(event: 'finish', listener: () => void): Stream.Readable + on(event: 'pause', listener: () => void): Stream.Readable + on(event: 'pipe', listener: (src: Readable) => void): Stream.Readable + on(event: 'readable', listener: () => void): Stream.Readable + on(event: 'resume', listener: () => void): Stream.Readable + on(event: 'unpipe', listener: (src: Readable) => void): Stream.Readable + on( + event: string | symbol, + listener: (...args: any[]) => void + ): Stream.Readable +} + export class ResultSet implements BaseResultSet { @@ -23,14 +49,16 @@ export class ResultSet return (await getAsText(this._stream)).toString() } - async json(): Promise { + async json(): Promise> { if (this._stream.readableEnded) { throw Error(streamAlreadyConsumedMessage) } return decode(await this.text(), this.format) } - stream(): Stream.Readable { + stream(): Format extends StreamableDataFormat + ? ResultStream[]> + : never { // If the underlying stream has already ended by calling `text` or `json`, // Stream.pipeline will create a new empty stream // but without "readableEnded" flag set to true diff --git a/packages/client-web/src/config.ts b/packages/client-web/src/config.ts index 0b934bf8..f149e6d4 100644 --- a/packages/client-web/src/config.ts +++ b/packages/client-web/src/config.ts @@ -16,7 +16,7 @@ export const WebImpl: ImplementationDetails['impl'] = { stream: ReadableStream, format: DataFormat, query_id: string - ) => new ResultSet(stream, format, query_id)) as any, // FIXME: resolve weird type issue - the types actually match + ) => new ResultSet(stream, format, query_id)) as any, values_encoder: new WebValuesEncoder(), close_stream: (stream) => stream.cancel(), } diff --git a/packages/client-web/src/result_set.ts b/packages/client-web/src/result_set.ts index cbd3d672..18a8dedf 100644 --- a/packages/client-web/src/result_set.ts +++ b/packages/client-web/src/result_set.ts @@ -1,4 +1,9 @@ -import type { BaseResultSet, DataFormat, Row } from '@clickhouse/client-common' +import type { + BaseResultSet, + DataFormat, + ResultJSONType, + Row, +} from '@clickhouse/client-common' import { decode, validateStreamFormat } from '@clickhouse/client-common' import { getAsText } from './utils' @@ -17,7 +22,7 @@ export class ResultSet return getAsText(this._stream) } - async json(): Promise { + async json(): Promise> { const text = await this.text() return decode(text, this.format) } From 7281ba3d82800716b6d9347d7cf12be95402f98a Mon Sep 17 00:00:00 2001 From: slvrtrn Date: Sat, 9 Mar 2024 05:58:13 +0100 Subject: [PATCH 03/16] More experiments --- .../integration/select_query_binding.test.ts | 2 +- packages/client-common/src/client.ts | 36 ++++++++++++---- packages/client-common/src/config.ts | 21 ++++------ packages/client-common/src/index.ts | 10 ++--- packages/client-common/src/result.ts | 12 ++---- .../integration/node_keep_alive.test.ts | 18 ++++---- .../node_max_open_connections.test.ts | 13 +++--- .../__tests__/utils/node_client.ts | 10 +++++ packages/client-node/src/client.ts | 37 +++++++++++------ packages/client-node/src/index.ts | 2 +- packages/client-node/src/result_set.ts | 31 ++++++++------ packages/client-web/src/client.ts | 41 ++++++++++++++----- packages/client-web/src/result_set.ts | 8 +++- 13 files changed, 147 insertions(+), 94 deletions(-) create mode 100644 packages/client-node/__tests__/utils/node_client.ts 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 f11ed5f7..6870df26 100644 --- a/packages/client-common/__tests__/integration/select_query_binding.test.ts +++ b/packages/client-common/__tests__/integration/select_query_binding.test.ts @@ -274,7 +274,7 @@ describe('select with query binding', () => { }) describe('NULL parameter binding', () => { - const baseQuery: Pick = { + const baseQuery: QueryParams = { query: 'SELECT number FROM numbers(3) WHERE {n:Nullable(String)} IS NULL', format: 'CSV', } diff --git a/packages/client-common/src/client.ts b/packages/client-common/src/client.ts index 7ebb0a3b..dac74a01 100644 --- a/packages/client-common/src/client.ts +++ b/packages/client-common/src/client.ts @@ -3,6 +3,7 @@ import type { ClickHouseSettings, Connection, ConnExecResult, + MakeResultSet, WithClickHouseSummary, } from '@clickhouse/client-common' import { type DataFormat, DefaultLogger } from '@clickhouse/client-common' @@ -36,6 +37,15 @@ export interface QueryParams extends BaseQueryParams { format?: DataFormat } +/** Same parameters as {@link QueryParams}, but with `format` field as a type */ +export type QueryParamsWithFormat = Omit< + QueryParams, + 'format' +> & { format: Format } + +/** Same parameters as {@link QueryParams}, but with `format` field omitted */ +export type QueryParamsWithoutFormat = Omit + export interface ExecParams extends BaseQueryParams { /** Statement to execute. */ query: string @@ -101,7 +111,7 @@ export interface InsertParams export class ClickHouseClient { private readonly clientClickHouseSettings: ClickHouseSettings private readonly connection: Connection - private readonly makeResultSet: ImplementationDetails['impl']['make_result_set'] + private readonly makeResultSet: MakeResultSet private readonly valuesEncoder: ValuesEncoder private readonly closeStream: CloseStream private readonly sessionId?: string @@ -129,20 +139,28 @@ export class ClickHouseClient { this.closeStream = config.impl.close_stream } + /** Overloads for proper {@link DataFormat} variants handling. + * See the implementation: {@link ClickHouseClient.query} */ + async query( + params: QueryParamsWithoutFormat & { format: undefined } + ): Promise> + async query( + params: QueryParamsWithoutFormat + ): Promise> + async query( + params: QueryParamsWithFormat + ): Promise> + /** * Used for most statements that can have a response, such as SELECT. * FORMAT clause should be specified separately via {@link QueryParams.format} (default is JSON) * Consider using {@link ClickHouseClient.insert} for data insertion, * or {@link ClickHouseClient.command} for DDLs. + * Returns an implementation of {@link BaseResultSet}. */ - async query( - params: Omit & { format?: Format } - ): Promise< - BaseResultSet< - Stream, - Format extends undefined ? 'JSON' : NonNullable - > - > { + async query( + params: QueryParams | QueryParamsWithFormat + ) { const format = params.format ?? 'JSON' const query = formatQuery(params.query, format) const { stream, query_id } = await this.connection.query({ diff --git a/packages/client-common/src/config.ts b/packages/client-common/src/config.ts index ebd6fc2b..737ae245 100644 --- a/packages/client-common/src/config.ts +++ b/packages/client-common/src/config.ts @@ -88,11 +88,14 @@ export type MakeConnection< Config = BaseClickHouseClientConfigOptionsWithURL > = (config: Config, params: ConnectionParams) => Connection -export type MakeResultSet< - Stream, +export type MakeResultSet = < Format extends DataFormat, ResultSet extends BaseResultSet -> = (stream: Stream, format: Format, query_id: string) => ResultSet +>( + stream: Stream, + format: Format, + query_id: string +) => ResultSet export interface ValuesEncoder { validateInsertValues( @@ -141,17 +144,7 @@ export type HandleImplSpecificURLParams = ( export interface ImplementationDetails { impl: { make_connection: MakeConnection - make_result_set: < - Format extends DataFormat | undefined, - ResultSet extends BaseResultSet< - Stream, - Format extends undefined ? 'JSON' : NonNullable - > - >( - stream: Stream, - format: Format, - query_id: string - ) => ResultSet + make_result_set: MakeResultSet values_encoder: ValuesEncoder close_stream: CloseStream handle_specific_url_params?: HandleImplSpecificURLParams diff --git a/packages/client-common/src/index.ts b/packages/client-common/src/index.ts index 29001b31..f589c38b 100644 --- a/packages/client-common/src/index.ts +++ b/packages/client-common/src/index.ts @@ -13,13 +13,7 @@ export { type PingResult, } from './client' export { type BaseClickHouseClientConfigOptions } from './config' -export type { - Row, - BaseResultSet, - ResultJSONType, - RowJSONType, - StreamWithFormat, -} from './result' +export type { Row, BaseResultSet, ResultJSONType, RowJSONType } from './result' export { type DataFormat } from './data_formatter' export { ClickHouseError } from './error' export { @@ -47,6 +41,7 @@ export { isSupportedRawFormat, decode, validateStreamFormat, + StreamableDataFormat, } from './data_formatter' export { type ValuesEncoder, @@ -86,3 +81,4 @@ export { formatQuerySettings, formatQueryParams, } from './data_formatter' +export type { QueryParamsWithFormat, QueryParamsWithoutFormat } from './client' diff --git a/packages/client-common/src/result.ts b/packages/client-common/src/result.ts index a4dbe9a8..42d7c88f 100644 --- a/packages/client-common/src/result.ts +++ b/packages/client-common/src/result.ts @@ -1,10 +1,9 @@ import type { ResponseJSON } from './clickhouse_types' -import { +import type { DataFormat, RawDataFormat, RecordsJSONFormat, SingleDocumentJSONFormat, - StreamableDataFormat, StreamableJSONDataFormat, } from './data_formatter' @@ -33,8 +32,8 @@ export type RowJSONType = : T // technically, should never happen export interface Row< - Format extends DataFormat | unknown = unknown, - JSONType = unknown + JSONType = unknown, + Format extends DataFormat | unknown = unknown > { /** A string representation of a row. */ text: string @@ -47,10 +46,7 @@ export interface Row< json(): RowJSONType } -export interface BaseResultSet< - Stream, - Format extends DataFormat | unknown = unknown -> { +export interface BaseResultSet { /** * The method waits for all the rows to be fully loaded * and returns the result as a string. 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 0263e801..004091d7 100644 --- a/packages/client-node/__tests__/integration/node_keep_alive.test.ts +++ b/packages/client-node/__tests__/integration/node_keep_alive.test.ts @@ -1,8 +1,8 @@ -import type { ClickHouseClient } from '@clickhouse/client-common' import { createSimpleTable } from '@test/fixtures/simple_table' -import { createTestClient, guid, sleep } from '@test/utils' -import type Stream from 'stream' +import { guid, sleep } from '@test/utils' +import type { NodeClickHouseClient } from '../../src' import type { NodeClickHouseClientConfigOptions } from '../../src/config' +import { createNodeTestClient } from '../utils/node_client' /** * FIXME: Works fine during the local runs, but it is flaky on GHA, @@ -10,7 +10,7 @@ import type { NodeClickHouseClientConfigOptions } from '../../src/config' * To be revisited in https://github.com/ClickHouse/clickhouse-js/issues/177 */ xdescribe('[Node.js] Keep Alive', () => { - let client: ClickHouseClient + let client: NodeClickHouseClient 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() @@ -18,7 +18,7 @@ xdescribe('[Node.js] Keep Alive', () => { describe('query', () => { it('should recreate the request if socket is potentially expired', async () => { - client = createTestClient({ + client = createNodeTestClient({ max_open_connections: 1, keep_alive: { enabled: true, @@ -33,7 +33,7 @@ xdescribe('[Node.js] Keep Alive', () => { }) it('should disable keep alive', async () => { - client = createTestClient({ + client = createNodeTestClient({ max_open_connections: 1, keep_alive: { enabled: false, @@ -46,7 +46,7 @@ xdescribe('[Node.js] Keep Alive', () => { }) it('should use multiple connections', async () => { - client = createTestClient({ + client = createNodeTestClient({ keep_alive: { enabled: true, socket_ttl: socketTTL, @@ -80,7 +80,7 @@ xdescribe('[Node.js] Keep Alive', () => { describe('insert', () => { let tableName: string it('should not duplicate insert requests (single connection)', async () => { - client = createTestClient({ + client = createNodeTestClient({ max_open_connections: 1, keep_alive: { enabled: true, @@ -105,7 +105,7 @@ xdescribe('[Node.js] Keep Alive', () => { }) it('should not duplicate insert requests (multiple connections)', async () => { - client = createTestClient({ + client = createNodeTestClient({ max_open_connections: 2, keep_alive: { enabled: true, 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 0026fc3a..cf336099 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,9 +1,10 @@ -import type { ClickHouseClient } from '@clickhouse/client-common' import { createSimpleTable } from '@test/fixtures/simple_table' -import { createTestClient, guid, sleep } from '@test/utils' +import { guid, sleep } from '@test/utils' +import type { NodeClickHouseClient } from '../../src' +import { createNodeTestClient } from '../utils/node_client' describe('[Node.js] max_open_connections config', () => { - let client: ClickHouseClient + let client: NodeClickHouseClient let results: number[] = [] afterEach(async () => { @@ -22,7 +23,7 @@ describe('[Node.js] max_open_connections config', () => { } it('should use only one connection', async () => { - client = createTestClient({ + client = createNodeTestClient({ max_open_connections: 1, }) void select('SELECT 1 AS x, sleep(0.3)') @@ -39,7 +40,7 @@ describe('[Node.js] max_open_connections config', () => { it('should use only one connection for insert', async () => { const tableName = `node_connections_single_connection_insert_${guid()}` - client = createTestClient({ + client = createNodeTestClient({ max_open_connections: 1, request_timeout: 3000, }) @@ -74,7 +75,7 @@ describe('[Node.js] max_open_connections config', () => { }) it('should use several connections', async () => { - client = createTestClient({ + client = createNodeTestClient({ max_open_connections: 2, }) void select('SELECT 1 AS x, sleep(0.3)') diff --git a/packages/client-node/__tests__/utils/node_client.ts b/packages/client-node/__tests__/utils/node_client.ts new file mode 100644 index 00000000..cd09736a --- /dev/null +++ b/packages/client-node/__tests__/utils/node_client.ts @@ -0,0 +1,10 @@ +import { createTestClient } from '@test/utils' +import type Stream from 'stream' +import type { NodeClickHouseClient } from '../../src' +import { type BaseClickHouseClientConfigOptions } from '../../src' + +export function createNodeTestClient( + config: BaseClickHouseClientConfigOptions = {} +): NodeClickHouseClient { + return createTestClient(config) as NodeClickHouseClient +} diff --git a/packages/client-node/src/client.ts b/packages/client-node/src/client.ts index 0cb91b90..8567752a 100644 --- a/packages/client-node/src/client.ts +++ b/packages/client-node/src/client.ts @@ -1,26 +1,37 @@ -import { - ClickHouseClient, - type DataFormat, - QueryParams, +import type { + DataFormat, + QueryParamsWithFormat, + QueryParamsWithoutFormat, } 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 { ResultSet } from './result_set' +import type { ResultSet } from './result_set' -export type NodeClickHouseClient = Omit< - ClickHouseClient, - 'query' -> & { - query( - params: Omit & { format?: Format } - ): Promise>> +export class NodeClickHouseClient extends ClickHouseClient { + /** Overloads for proper {@link DataFormat} variants handling. + * See the implementation: {@link ClickHouseClient.query} */ + async query( + params: QueryParamsWithoutFormat & { format: undefined } + ): Promise> + async query(params: QueryParamsWithoutFormat): Promise> + async query( + params: QueryParamsWithFormat + ): Promise> + + /** See the base implementation: {@link ClickHouseClient.query} */ + query( + params: QueryParamsWithFormat + ): Promise> { + return super.query(params) as any + } } export function createClient( config?: NodeClickHouseClientConfigOptions ): NodeClickHouseClient { - return new ClickHouseClient({ + return new ClickHouseClient({ impl: NodeConfigImpl, ...(config || {}), }) as any diff --git a/packages/client-node/src/index.ts b/packages/client-node/src/index.ts index c8d0f0ef..7fdac071 100644 --- a/packages/client-node/src/index.ts +++ b/packages/client-node/src/index.ts @@ -1,7 +1,7 @@ export type { NodeClickHouseClient } from './client' export { createClient } from './client' export { NodeClickHouseClientConfigOptions as ClickHouseClientConfigOptions } from './config' -export { ResultSet, ResultStream } from './result_set' +export { ResultSet, StreamReadable } from './result_set' /** Re-export @clickhouse/client-common types */ export { diff --git a/packages/client-node/src/result_set.ts b/packages/client-node/src/result_set.ts index cca72265..98603dbe 100644 --- a/packages/client-node/src/result_set.ts +++ b/packages/client-node/src/result_set.ts @@ -3,19 +3,19 @@ import type { DataFormat, ResultJSONType, Row, - RowJSONType, - StreamWithFormat, + StreamableDataFormat, } from '@clickhouse/client-common' import { decode, validateStreamFormat } from '@clickhouse/client-common' -import { StreamableDataFormat } from '@clickhouse/client-common/src/data_formatter' import { Buffer } from 'buffer' -import Stream, { Readable, Transform, TransformCallback } from 'stream' +import type { Readable, TransformCallback } from 'stream' +import Stream, { Transform } from 'stream' import { getAsText } from './utils' const NEWLINE = 0x0a as const -// an exact copy from stream.d.ts, but with typed `on('data')` -export type ResultStream = Omit & { +/** {@link Stream.Readable} with additional types for the `on(data)` method. + * Everything else is an exact copy from stream.d.ts */ +export type StreamReadable = Omit & { on(event: 'data', listener: (chunk: T) => void): Stream.Readable on(event: 'close', listener: () => void): Stream.Readable on(event: 'drain', listener: () => void): Stream.Readable @@ -57,7 +57,7 @@ export class ResultSet } stream(): Format extends StreamableDataFormat - ? ResultStream[]> + ? StreamReadable[]> : never { // If the underlying stream has already ended by calling `text` or `json`, // Stream.pipeline will create a new empty stream @@ -126,13 +126,18 @@ export class ResultSet objectMode: true, }) - return Stream.pipeline(this._stream, toRows, function pipelineCb(err) { - if (err) { - // FIXME: use logger instead - // eslint-disable-next-line no-console - console.error(err) + const pipeline = Stream.pipeline( + this._stream, + toRows, + function pipelineCb(err) { + if (err) { + // FIXME: use logger instead + // eslint-disable-next-line no-console + console.error(err) + } } - }) + ) + return pipeline as any } close() { diff --git a/packages/client-web/src/client.ts b/packages/client-web/src/client.ts index 1a2e2441..7b06d732 100644 --- a/packages/client-web/src/client.ts +++ b/packages/client-web/src/client.ts @@ -1,34 +1,53 @@ import type { - BaseResultSet, + DataFormat, InputJSON, InputJSONObjectEachRow, InsertParams, InsertResult, - QueryParams, - Row, + QueryParamsWithFormat, + QueryParamsWithoutFormat, } 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' -export type WebClickHouseClient = Omit< - ClickHouseClient, - 'insert' | 'query' -> & { - // restrict ReadableStream as a possible insert value +export type WebClickHouseClient = Omit & { + /** + * See the base implementation: {@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 } ): Promise - // narrow down the return type here for better type-hinting - query(params: QueryParams): Promise>> +} + +class WebClickHouseClientImpl extends ClickHouseClient { + /** Overloads for proper {@link DataFormat} variants handling. + * See the implementation: {@link ClickHouseClient.query} */ + async query( + params: QueryParamsWithoutFormat & { format: undefined } + ): Promise> + async query(params: QueryParamsWithoutFormat): Promise> + async query( + params: QueryParamsWithFormat + ): Promise> + + /** See the base implementation: {@link ClickHouseClient.query} */ + query( + params: QueryParamsWithFormat + ): Promise> { + return super.query(params) as any + } } export function createClient( config?: WebClickHouseClientConfigOptions ): WebClickHouseClient { - return new ClickHouseClient({ + return new WebClickHouseClientImpl({ impl: WebImpl, ...(config || {}), }) diff --git a/packages/client-web/src/result_set.ts b/packages/client-web/src/result_set.ts index 18a8dedf..1ba754a5 100644 --- a/packages/client-web/src/result_set.ts +++ b/packages/client-web/src/result_set.ts @@ -5,6 +5,7 @@ import type { Row, } from '@clickhouse/client-common' import { decode, validateStreamFormat } from '@clickhouse/client-common' +import type { StreamableDataFormat } from '@clickhouse/client-common/src/data_formatter' import { getAsText } from './utils' export class ResultSet @@ -27,7 +28,9 @@ export class ResultSet return decode(text, this.format) } - stream(): ReadableStream { + stream(): Format extends StreamableDataFormat + ? ReadableStream[]> + : never { this.markAsConsumed() validateStreamFormat(this.format) @@ -68,11 +71,12 @@ export class ResultSet }, }) - return this._stream.pipeThrough(transform, { + const pipeline = this._stream.pipeThrough(transform, { preventClose: false, preventAbort: false, preventCancel: false, }) + return pipeline as any } async close(): Promise { From b7b78455f039a258f16140682f1aae07fa392cf4 Mon Sep 17 00:00:00 2001 From: slvrtrn Date: Sat, 9 Mar 2024 08:48:34 +0100 Subject: [PATCH 04/16] Remove overloads --- packages/client-common/src/client.ts | 18 +++--------------- packages/client-common/src/result.ts | 4 ++-- packages/client-node/src/client.ts | 24 +++++------------------- packages/client-web/src/client.ts | 17 +++-------------- 4 files changed, 13 insertions(+), 50 deletions(-) diff --git a/packages/client-common/src/client.ts b/packages/client-common/src/client.ts index dac74a01..a473fe8a 100644 --- a/packages/client-common/src/client.ts +++ b/packages/client-common/src/client.ts @@ -139,18 +139,6 @@ export class ClickHouseClient { this.closeStream = config.impl.close_stream } - /** Overloads for proper {@link DataFormat} variants handling. - * See the implementation: {@link ClickHouseClient.query} */ - async query( - params: QueryParamsWithoutFormat & { format: undefined } - ): Promise> - async query( - params: QueryParamsWithoutFormat - ): Promise> - async query( - params: QueryParamsWithFormat - ): Promise> - /** * Used for most statements that can have a response, such as SELECT. * FORMAT clause should be specified separately via {@link QueryParams.format} (default is JSON) @@ -158,9 +146,9 @@ export class ClickHouseClient { * or {@link ClickHouseClient.command} for DDLs. * Returns an implementation of {@link BaseResultSet}. */ - async query( - params: QueryParams | QueryParamsWithFormat - ) { + async query( + params: Omit & { format?: Format } + ): Promise> { const format = params.format ?? 'JSON' const query = formatQuery(params.query, format) const { stream, query_id } = await this.connection.query({ diff --git a/packages/client-common/src/result.ts b/packages/client-common/src/result.ts index 42d7c88f..83847aba 100644 --- a/packages/client-common/src/result.ts +++ b/packages/client-common/src/result.ts @@ -20,7 +20,7 @@ export type ResultJSONType = : // CSV, TSV etc. - cannot be represented as JSON F extends RawDataFormat ? never - : T // technically, should never happen + : T // happens only when Format could not be inferred from a literal export type RowJSONType = // JSON*EachRow formats @@ -29,7 +29,7 @@ export type RowJSONType = : // CSV, TSV, non-streamable JSON formats - cannot be streamed as JSON F extends RawDataFormat | SingleDocumentJSONFormat | RecordsJSONFormat ? never - : T // technically, should never happen + : T // happens only when Format could not be inferred from a literal export interface Row< JSONType = unknown, diff --git a/packages/client-node/src/client.ts b/packages/client-node/src/client.ts index 8567752a..adf85668 100644 --- a/packages/client-node/src/client.ts +++ b/packages/client-node/src/client.ts @@ -1,8 +1,4 @@ -import type { - DataFormat, - QueryParamsWithFormat, - QueryParamsWithoutFormat, -} from '@clickhouse/client-common' +import type { DataFormat, QueryParams } from '@clickhouse/client-common' import { ClickHouseClient } from '@clickhouse/client-common' import type Stream from 'stream' import type { NodeClickHouseClientConfigOptions } from './config' @@ -10,21 +6,11 @@ import { NodeConfigImpl } from './config' import type { ResultSet } from './result_set' export class NodeClickHouseClient extends ClickHouseClient { - /** Overloads for proper {@link DataFormat} variants handling. - * See the implementation: {@link ClickHouseClient.query} */ - async query( - params: QueryParamsWithoutFormat & { format: undefined } - ): Promise> - async query(params: QueryParamsWithoutFormat): Promise> - async query( - params: QueryParamsWithFormat - ): Promise> - /** See the base implementation: {@link ClickHouseClient.query} */ - query( - params: QueryParamsWithFormat + query( + params: Omit & { format?: Format } ): Promise> { - return super.query(params) as any + return super.query(params) as Promise> } } @@ -34,5 +20,5 @@ export function createClient( return new ClickHouseClient({ impl: NodeConfigImpl, ...(config || {}), - }) as any + }) as NodeClickHouseClient } diff --git a/packages/client-web/src/client.ts b/packages/client-web/src/client.ts index 7b06d732..eded876c 100644 --- a/packages/client-web/src/client.ts +++ b/packages/client-web/src/client.ts @@ -4,8 +4,7 @@ import type { InputJSONObjectEachRow, InsertParams, InsertResult, - QueryParamsWithFormat, - QueryParamsWithoutFormat, + QueryParams, } from '@clickhouse/client-common' import { ClickHouseClient } from '@clickhouse/client-common' import type { WebClickHouseClientConfigOptions } from './config' @@ -26,21 +25,11 @@ export type WebClickHouseClient = Omit & { } class WebClickHouseClientImpl extends ClickHouseClient { - /** Overloads for proper {@link DataFormat} variants handling. - * See the implementation: {@link ClickHouseClient.query} */ - async query( - params: QueryParamsWithoutFormat & { format: undefined } - ): Promise> - async query(params: QueryParamsWithoutFormat): Promise> - async query( - params: QueryParamsWithFormat - ): Promise> - /** See the base implementation: {@link ClickHouseClient.query} */ query( - params: QueryParamsWithFormat + params: Omit & { format?: Format } ): Promise> { - return super.query(params) as any + return super.query(params) as Promise> } } From cee3c53e9a799d5592acec56b2bc040ef5c7c4f6 Mon Sep 17 00:00:00 2001 From: slvrtrn Date: Mon, 11 Mar 2024 17:31:31 +0100 Subject: [PATCH 05/16] Add changelog --- CHANGELOG.md | 145 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 140 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f7bc8db..41013dee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,10 @@ -## 1.0.0 (Common, Node.js, Web) +# 1.0.0 (Common, Node.js, Web) -Formal stable release milestone. The client will follow the [official semantic versioning](https://docs.npmjs.com/about-semantic-versioning) guidelines. +Formal stable release milestone with a lot of improvements and a fair bit of breaking changes. -### Deprecated API +The client will follow the [official semantic versioning](https://docs.npmjs.com/about-semantic-versioning) guidelines. + +## Deprecated API The following configuration parameters are marked as deprecated: @@ -15,7 +17,7 @@ These parameters will be removed in the next major release (2.0.0). See "New features" section for more details. -### Breaking changes +## Breaking changes - `request_timeout` default value was incorrectly set to 300s instead of 30s. It is now correctly set to 30s by default. If your code relies on the previous incorrect default value, consider setting it explicitly. - Client will enable [send_progress_in_http_headers](https://clickhouse.com/docs/en/operations/settings/settings#send_progress_in_http_headers) and set `http_headers_progress_interval_ms` to `20000` (20 seconds) by default. These settings in combination allow to avoid potential load balancer timeout issues in case of long-running queries without data coming in or out, such as `INSERT FROM SELECT` and similar ones, as the connection could be marked as idle by the LB and closed abruptly. In that case, a `socket hang up` error could be thrown on the client side. Currently, 20s is chosen as a safe value, since most LBs will have at least 30s of idle timeout; this is also in line with the default [AWS LB KeepAlive interval](https://docs.aws.amazon.com/elasticloadbalancing/latest/network/network-load-balancers.html#connection-idle-timeout), which is 20s by default. It can be overridden when creating a client instance if your LB timeout value is even lower than that by manually changing the `clickhouse_settings.http_headers_progress_interval_ms` value. @@ -37,6 +39,7 @@ const client = createClient({ With `send_progress_in_http_headers` and `http_headers_progress_interval_ms` settings now enabled by default, this is no longer sufficient. If you need to create a client instance for a read-only user, consider this instead: ```ts +// 1.0.0 const client = createClient({ readonly: true, }) @@ -48,7 +51,139 @@ NB: this is not necessary if a user has `READONLY = 2` mode as it allows to modi See also: [readonly documentation](https://clickhouse.com/docs/en/operations/settings/permissions-for-queries#readonly). -### New features +- (TypeScript only) `ResultSet` and `Row` are now more strictly typed, according to the format used during the `query` call. See "New features" section for more details. +- (TypeScript only) Instead of using `ClickHouseClient`, a corresponding type name (`NodeClickHouseClient` or `WebClickHouseClient`) should be used when providing a _type hint_ for your client instance. NB: you should still use `createClient` factory function provided in the package. For example: + +```ts +// pre-1.0.0 (Node.js) +class MyClickHouseService { + private client: ClickHouseClient + constructor() { + this.client = createClient({ ... }) + } +} + +// 1.0.0 (Node.js) +class MyClickHouseService { + private client: NodeClickHouseClient + constructor() { + this.client = createClient({ ... }) + } +} +``` + +## New features + +- Advanced TypeScript support for `query` + `ResultSet`. Client will now try its best to figure out the shape of the data based on the DataFormat literal specified to the `query` call, as well as which methods are allowed to be called on the `ResultSet`. + +Live demo (see the full description below): + +[Screencast from 2024-03-09 08-10-26.webm](https://github.com/ClickHouse/clickhouse-js/assets/3175289/b66afcb2-3a10-4411-af59-51d2754c417e) + +Complete reference: + +| Format | `ResultSet.json()` | `ResultSet.stream()` | Stream data | `Row.json()` | +| ------------------------------- | --------------------- | --------------------------- | ----------------- | --------------- | +| JSON | ResponseJSON\ | never | never | never | +| JSONObjectsEachRow | Record\ | never | never | never | +| JSON\*EachRow | Array\ | Stream\\>\> | Array\\> | T | +| CSV/TSV/CustomSeparated/Parquet | never | Stream\\>\> | Array\\> | never | + +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 } + +const resultSet = await client.query('SELECT * FROM table', { + format: 'JSONObjectsEachRow', +}) + +// pre-1.0.0, `resultOld` has type Record +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() +``` + +This is even more handy in case of streaming on the Node.js platform: + +```ts +const resultSet = await client.query('SELECT * FROM table', { + format: 'JSONEachRow', +}) + +// pre-1.0.0 +// `streamOld` was just a regular Node.js Stream.Readable +const streamOld = resultSet.stream() +// `rows` were `any`, needed an explicit type hint +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` + }) +}) + +// 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() +// `rows` inferred as an Array> instead of `any` +streamNew.on('data', (rows) => { + 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 + }) +}) +``` + +Calling `ResultSet.stream` is not allowed for certain data formats, such as `JSON` and `JSONObjectsEachRow`. 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` +``` + +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', +}) +// `json` is `never`; same if you stream CSV, and call `Row.json` - it will be `never`, too. +const json = resultSet.json() +``` + +There is one currently 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: + +```ts +// assuming that `queryParams` has `JSONObjectsEachRow` format inside +async function runQuery( + queryParams: QueryParams +): Promise> { + const resultSet = await client.query(queryParams) + // type hint here will provide a union of all known shapes instead of a specific one + return resultSet.json() +} +``` + +In this case, as it is _likely_ that you already know the desired format in advance (otherwise, returning a specific shape like `Record` would've been incorrect), consider helping the client a bit: + +```ts +async function runQuery( + queryParams: QueryParams +): Promise> { + const resultSet = await client.query({ + ...queryParams, + format: 'JSONObjectsEachRow', + }) + return resultSet.json() // TS understands that it is a Record now +} +``` - Added `url` configuration parameter. It is intended to replace the deprecated `host`, which was already supposed to be passed as a valid URL. - Added `http_headers` configuration parameter as a direct replacement for `additional_headers`. Functionally, it is the same, and the change is purely cosmetic, as we'd like to leave an option to implement TCP connection in the future open. From 5b1b2dffd4b5d4766fc40209858141b9bfab9ec0 Mon Sep 17 00:00:00 2001 From: slvrtrn Date: Mon, 11 Mar 2024 17:34:38 +0100 Subject: [PATCH 06/16] Revert node client test --- .../__tests__/integration/node_client.test.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/client-node/__tests__/integration/node_client.test.ts b/packages/client-node/__tests__/integration/node_client.test.ts index 0b3904d1..3fe3e901 100644 --- a/packages/client-node/__tests__/integration/node_client.test.ts +++ b/packages/client-node/__tests__/integration/node_client.test.ts @@ -1,4 +1,3 @@ -import { ResultSet } from '../../src' import Http from 'http' import type { NodeClickHouseClient } from '../../src' import { createClient } from '../../src' @@ -80,17 +79,6 @@ describe('[Node.js] Client', () => { async function query(client: NodeClickHouseClient) { const selectPromise = client.query({ query: 'SELECT * FROM system.numbers LIMIT 5', - format: 'JSONEachRow', - }) - type Data = { foo: string } - const rs = await selectPromise - const x = rs.json() - const s = rs.stream() - s.on('data', (r) => { - r.forEach((r) => { - const t = r.text - const j = r.json() - }) }) emitResponseBody(clientRequest, 'hi') await selectPromise From 4045653fddd05890221da55bb69963faafd9c98e Mon Sep 17 00:00:00 2001 From: slvrtrn Date: Mon, 11 Mar 2024 17:41:49 +0100 Subject: [PATCH 07/16] Add docs links to the result set implementations --- packages/client-node/src/result_set.ts | 3 +++ packages/client-web/src/result_set.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/packages/client-node/src/result_set.ts b/packages/client-node/src/result_set.ts index 98603dbe..6613feee 100644 --- a/packages/client-node/src/result_set.ts +++ b/packages/client-node/src/result_set.ts @@ -42,6 +42,7 @@ export class ResultSet public readonly query_id: string ) {} + /** See {@link BaseResultSet.text} */ async text(): Promise { if (this._stream.readableEnded) { throw Error(streamAlreadyConsumedMessage) @@ -49,6 +50,7 @@ export class ResultSet return (await getAsText(this._stream)).toString() } + /** See {@link BaseResultSet.json} */ async json(): Promise> { if (this._stream.readableEnded) { throw Error(streamAlreadyConsumedMessage) @@ -56,6 +58,7 @@ export class ResultSet return decode(await this.text(), this.format) } + /** See {@link BaseResultSet.stream} */ stream(): Format extends StreamableDataFormat ? StreamReadable[]> : never { diff --git a/packages/client-web/src/result_set.ts b/packages/client-web/src/result_set.ts index 1ba754a5..e7ee3ec3 100644 --- a/packages/client-web/src/result_set.ts +++ b/packages/client-web/src/result_set.ts @@ -18,16 +18,19 @@ export class ResultSet public readonly query_id: string ) {} + /** See {@link BaseResultSet.text} */ async text(): Promise { this.markAsConsumed() return getAsText(this._stream) } + /** See {@link BaseResultSet.json} */ async json(): Promise> { const text = await this.text() return decode(text, this.format) } + /** See {@link BaseResultSet.stream} */ stream(): Format extends StreamableDataFormat ? ReadableStream[]> : never { From 3c948d08408f4ecb9e969436e0cf5b02582554d3 Mon Sep 17 00:00:00 2001 From: slvrtrn Date: Mon, 11 Mar 2024 17:49:27 +0100 Subject: [PATCH 08/16] Cleanup unused interfaces --- packages/client-common/src/client.ts | 7 ++----- packages/client-common/src/index.ts | 2 +- packages/client-node/src/client.ts | 7 +++++-- packages/client-web/src/client.ts | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/client-common/src/client.ts b/packages/client-common/src/client.ts index a473fe8a..90102156 100644 --- a/packages/client-common/src/client.ts +++ b/packages/client-common/src/client.ts @@ -41,10 +41,7 @@ export interface QueryParams extends BaseQueryParams { export type QueryParamsWithFormat = Omit< QueryParams, 'format' -> & { format: Format } - -/** Same parameters as {@link QueryParams}, but with `format` field omitted */ -export type QueryParamsWithoutFormat = Omit +> & { format?: Format } export interface ExecParams extends BaseQueryParams { /** Statement to execute. */ @@ -147,7 +144,7 @@ export class ClickHouseClient { * Returns an implementation of {@link BaseResultSet}. */ async query( - params: Omit & { format?: Format } + params: QueryParamsWithFormat ): Promise> { const format = params.format ?? 'JSON' const query = formatQuery(params.query, format) diff --git a/packages/client-common/src/index.ts b/packages/client-common/src/index.ts index f589c38b..0456c60e 100644 --- a/packages/client-common/src/index.ts +++ b/packages/client-common/src/index.ts @@ -81,4 +81,4 @@ export { formatQuerySettings, formatQueryParams, } from './data_formatter' -export type { QueryParamsWithFormat, QueryParamsWithoutFormat } from './client' +export type { QueryParamsWithFormat } from './client' diff --git a/packages/client-node/src/client.ts b/packages/client-node/src/client.ts index adf85668..2f6bac32 100644 --- a/packages/client-node/src/client.ts +++ b/packages/client-node/src/client.ts @@ -1,4 +1,7 @@ -import type { DataFormat, QueryParams } from '@clickhouse/client-common' +import type { + DataFormat, + QueryParamsWithFormat, +} from '@clickhouse/client-common' import { ClickHouseClient } from '@clickhouse/client-common' import type Stream from 'stream' import type { NodeClickHouseClientConfigOptions } from './config' @@ -8,7 +11,7 @@ import type { ResultSet } from './result_set' export class NodeClickHouseClient extends ClickHouseClient { /** See the base implementation: {@link ClickHouseClient.query} */ query( - params: Omit & { format?: Format } + 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 eded876c..d03aac77 100644 --- a/packages/client-web/src/client.ts +++ b/packages/client-web/src/client.ts @@ -4,7 +4,7 @@ import type { InputJSONObjectEachRow, InsertParams, InsertResult, - QueryParams, + QueryParamsWithFormat, } from '@clickhouse/client-common' import { ClickHouseClient } from '@clickhouse/client-common' import type { WebClickHouseClientConfigOptions } from './config' @@ -27,7 +27,7 @@ export type WebClickHouseClient = Omit & { class WebClickHouseClientImpl extends ClickHouseClient { /** See the base implementation: {@link ClickHouseClient.query} */ query( - params: Omit & { format?: Format } + params: QueryParamsWithFormat ): Promise> { return super.query(params) as Promise> } From 553137771f147ef70dc7af4755e5eeb7aabd7c52 Mon Sep 17 00:00:00 2001 From: slvrtrn Date: Mon, 25 Mar 2024 18:45:01 +0100 Subject: [PATCH 09/16] Prettier --- benchmarks/leaks/memory_leak_arrays.ts | 2 +- benchmarks/leaks/memory_leak_brown.ts | 2 +- .../leaks/memory_leak_random_integers.ts | 2 +- benchmarks/leaks/shared.ts | 2 +- examples/node/basic_tls.ts | 2 +- examples/node/insert_file_stream_ndjson.ts | 2 +- examples/read_only_user.ts | 10 +-- .../__tests__/fixtures/read_only_user.ts | 2 +- .../__tests__/fixtures/simple_table.ts | 2 +- .../__tests__/fixtures/table_with_fields.ts | 4 +- .../__tests__/fixtures/test_data.ts | 2 +- .../integration/abort_request.test.ts | 12 ++-- .../__tests__/integration/auth.test.ts | 4 +- .../integration/clickhouse_settings.test.ts | 2 +- .../__tests__/integration/config.test.ts | 4 +- .../__tests__/integration/data_types.test.ts | 52 +++++++-------- .../__tests__/integration/date_time.test.ts | 24 +++---- .../integration/error_parsing.test.ts | 16 ++--- .../__tests__/integration/exec.test.ts | 8 +-- .../__tests__/integration/insert.test.ts | 8 +-- .../insert_specific_columns.test.ts | 10 +-- .../integration/multiple_clients.test.ts | 10 +-- .../__tests__/integration/query_log.test.ts | 6 +- .../integration/read_only_user.test.ts | 10 +-- .../__tests__/integration/select.test.ts | 18 +++--- .../integration/select_query_binding.test.ts | 6 +- .../integration/select_result.test.ts | 4 +- .../__tests__/unit/config.test.ts | 64 +++++++++---------- .../unit/format_query_params.test.ts | 10 +-- .../unit/format_query_settings.test.ts | 6 +- .../__tests__/unit/parse_error.test.ts | 14 ++-- .../__tests__/unit/to_search_params.test.ts | 2 +- .../client-common/__tests__/utils/client.ts | 12 ++-- .../__tests__/utils/test_connection_type.ts | 2 +- .../client-common/__tests__/utils/test_env.ts | 2 +- .../__tests__/utils/test_logger.ts | 2 +- packages/client-common/src/client.ts | 10 +-- packages/client-common/src/config.ts | 36 +++++------ .../src/data_formatter/format_query_params.ts | 4 +- .../data_formatter/format_query_settings.ts | 2 +- .../src/data_formatter/formatter.ts | 12 ++-- packages/client-common/src/logger.ts | 5 +- packages/client-common/src/result.ts | 26 ++++---- .../client-common/src/utils/connection.ts | 2 +- packages/client-common/src/utils/string.ts | 2 +- .../integration/node_abort_request.test.ts | 10 +-- .../integration/node_errors_parsing.test.ts | 4 +- .../__tests__/integration/node_insert.test.ts | 4 +- .../integration/node_keep_alive.test.ts | 4 +- .../__tests__/integration/node_logger.ts | 4 +- .../integration/node_multiple_clients.test.ts | 4 +- .../__tests__/integration/node_ping.test.ts | 2 +- .../integration/node_select_streaming.test.ts | 6 +- .../node_stream_json_formats.test.ts | 12 ++-- .../node_stream_raw_formats.test.ts | 40 ++++++------ .../integration/node_streaming_e2e.test.ts | 8 +-- .../client-node/__tests__/tls/tls.test.ts | 8 +-- .../__tests__/unit/node_client.test.ts | 4 +- .../__tests__/unit/node_config.test.ts | 12 ++-- .../__tests__/unit/node_connection.test.ts | 16 ++--- .../__tests__/unit/node_result_set.test.ts | 4 +- .../__tests__/unit/node_user_agent.test.ts | 4 +- .../unit/node_values_encoder.test.ts | 16 ++--- .../client-node/__tests__/utils/http_stubs.ts | 8 +-- .../__tests__/utils/node_client.ts | 2 +- packages/client-node/src/client.ts | 4 +- packages/client-node/src/config.ts | 4 +- .../src/connection/create_connection.ts | 2 +- .../src/connection/node_base_connection.ts | 28 ++++---- .../src/connection/node_https_connection.ts | 2 +- packages/client-node/src/result_set.ts | 10 +-- packages/client-node/src/utils/encoder.ts | 14 ++-- packages/client-node/src/utils/stream.ts | 2 +- .../integration/web_abort_request.test.ts | 4 +- .../__tests__/integration/web_client.test.ts | 2 +- .../integration/web_error_parsing.test.ts | 4 +- .../__tests__/integration/web_ping.test.ts | 2 +- .../integration/web_select_streaming.test.ts | 8 +-- .../__tests__/unit/web_client.test.ts | 2 +- .../__tests__/unit/web_result_set.test.ts | 4 +- packages/client-web/src/client.ts | 6 +- packages/client-web/src/config.ts | 2 +- .../src/connection/web_connection.ts | 12 ++-- packages/client-web/src/result_set.ts | 2 +- packages/client-web/src/utils/encoder.ts | 8 +-- 85 files changed, 368 insertions(+), 365 deletions(-) diff --git a/benchmarks/leaks/memory_leak_arrays.ts b/benchmarks/leaks/memory_leak_arrays.ts index d845080b..0b64cfd7 100644 --- a/benchmarks/leaks/memory_leak_arrays.ts +++ b/benchmarks/leaks/memory_leak_arrays.ts @@ -36,7 +36,7 @@ const program = async () => { console.log() console.log( `Batch size: ${BATCH_SIZE}, iterations count: ${ITERATIONS}, ` + - `logging memory usage every ${LOG_INTERVAL} iterations` + `logging memory usage every ${LOG_INTERVAL} iterations`, ) console.log() diff --git a/benchmarks/leaks/memory_leak_brown.ts b/benchmarks/leaks/memory_leak_brown.ts index b346c520..7453f633 100644 --- a/benchmarks/leaks/memory_leak_brown.ts +++ b/benchmarks/leaks/memory_leak_brown.ts @@ -71,7 +71,7 @@ const program = async () => { console.time('insert') const filename = Path.resolve( process.cwd(), - 'benchmarks/leaks/input/mgbench1.csv' + 'benchmarks/leaks/input/mgbench1.csv', ) await client.insert({ table: tableName, diff --git a/benchmarks/leaks/memory_leak_random_integers.ts b/benchmarks/leaks/memory_leak_random_integers.ts index cb875f01..941bb9cf 100644 --- a/benchmarks/leaks/memory_leak_random_integers.ts +++ b/benchmarks/leaks/memory_leak_random_integers.ts @@ -31,7 +31,7 @@ const program = async () => { console.log() console.log( `Batch size: ${BATCH_SIZE}, iterations count: ${ITERATIONS}, ` + - `logging memory usage every ${LOG_INTERVAL} iterations` + `logging memory usage every ${LOG_INTERVAL} iterations`, ) console.log() diff --git a/benchmarks/leaks/shared.ts b/benchmarks/leaks/shared.ts index 1ba7da05..b7cb6629 100644 --- a/benchmarks/leaks/shared.ts +++ b/benchmarks/leaks/shared.ts @@ -47,7 +47,7 @@ export function logMemoryUsageDiff({ const k = key as keyof MemoryUsage const diff = current[k] - previous[k] console.log( - `${k}: ${diff > 0 ? `+${diff.toFixed(2)}` : diff.toFixed(2)} MB` + `${k}: ${diff > 0 ? `+${diff.toFixed(2)}` : diff.toFixed(2)} MB`, ) } } diff --git a/examples/node/basic_tls.ts b/examples/node/basic_tls.ts index b9a6b171..4a8b9468 100644 --- a/examples/node/basic_tls.ts +++ b/examples/node/basic_tls.ts @@ -6,7 +6,7 @@ void (async () => { 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', ), }, }) diff --git a/examples/node/insert_file_stream_ndjson.ts b/examples/node/insert_file_stream_ndjson.ts index 4556a4ae..d0777cdf 100644 --- a/examples/node/insert_file_stream_ndjson.ts +++ b/examples/node/insert_file_stream_ndjson.ts @@ -26,7 +26,7 @@ void (async () => { await client.insert({ table: tableName, values: Fs.createReadStream(filename).pipe( - split((row: string) => JSON.parse(row)) + split((row: string) => JSON.parse(row)), ), format: 'JSONCompactEachRow', }) diff --git a/examples/read_only_user.ts b/examples/read_only_user.ts index f3399b46..190be6fe 100644 --- a/examples/read_only_user.ts +++ b/examples/read_only_user.ts @@ -33,7 +33,7 @@ void (async () => { }) } console.log( - `Created user ${readOnlyUsername} with restricted access to the system database` + `Created user ${readOnlyUsername} with restricted access to the system database`, ) printSeparator() @@ -79,7 +79,7 @@ void (async () => { .catch((err) => { console.error( '[Expected error] Readonly user cannot insert the data into the table. Cause:\n', - err + err, ) }) printSeparator() @@ -93,7 +93,7 @@ void (async () => { .catch((err) => { console.error( '[Expected error] Cannot query system.users cause it was not granted. Cause:\n', - err + err, ) }) printSeparator() @@ -125,7 +125,7 @@ void (async () => { .catch((err) => { console.error( `[Expected error] Cannot modify 'send_progress_in_http_headers' setting in readonly mode. Cause:\n`, - err + err, ) }) printSeparator() @@ -137,6 +137,6 @@ void (async () => { function printSeparator() { console.log( - '------------------------------------------------------------------------' + '------------------------------------------------------------------------', ) } diff --git a/packages/client-common/__tests__/fixtures/read_only_user.ts b/packages/client-common/__tests__/fixtures/read_only_user.ts index d727bceb..0cadc988 100644 --- a/packages/client-common/__tests__/fixtures/read_only_user.ts +++ b/packages/client-common/__tests__/fixtures/read_only_user.ts @@ -54,7 +54,7 @@ export async function createReadOnlyUser(client: ClickHouseClient) { } console.log( `Created user ${username} with default database ${database} ` + - 'and restricted access to the system database' + 'and restricted access to the system database', ) return { diff --git a/packages/client-common/__tests__/fixtures/simple_table.ts b/packages/client-common/__tests__/fixtures/simple_table.ts index b8627b31..eefbaeff 100644 --- a/packages/client-common/__tests__/fixtures/simple_table.ts +++ b/packages/client-common/__tests__/fixtures/simple_table.ts @@ -7,7 +7,7 @@ import { createTable, TestEnv } from '../utils' export function createSimpleTable( client: ClickHouseClient, tableName: string, - settings: MergeTreeSettings = {} + settings: MergeTreeSettings = {}, ) { return createTable(client, (env) => { const filteredSettings = filterSettingsBasedOnEnv(settings, env) diff --git a/packages/client-common/__tests__/fixtures/table_with_fields.ts b/packages/client-common/__tests__/fixtures/table_with_fields.ts index 13bda0fe..c314078e 100644 --- a/packages/client-common/__tests__/fixtures/table_with_fields.ts +++ b/packages/client-common/__tests__/fixtures/table_with_fields.ts @@ -7,7 +7,7 @@ import { createTable, guid, TestEnv } from '../utils' export async function createTableWithFields( client: ClickHouseClient, fields: string, - clickhouse_settings?: ClickHouseSettings + clickhouse_settings?: ClickHouseSettings, ): Promise { const tableName = `test_table__${guid()}` await createTable( @@ -41,7 +41,7 @@ export async function createTableWithFields( ` } }, - clickhouse_settings + clickhouse_settings, ) return tableName } diff --git a/packages/client-common/__tests__/fixtures/test_data.ts b/packages/client-common/__tests__/fixtures/test_data.ts index 448201b1..4d0683a2 100644 --- a/packages/client-common/__tests__/fixtures/test_data.ts +++ b/packages/client-common/__tests__/fixtures/test_data.ts @@ -10,7 +10,7 @@ export const jsonValues = [ export async function assertJsonValues( client: ClickHouseClient, - tableName: string + tableName: string, ) { const result = await client .query({ diff --git a/packages/client-common/__tests__/integration/abort_request.test.ts b/packages/client-common/__tests__/integration/abort_request.test.ts index 2676be2a..4ee91c7d 100644 --- a/packages/client-common/__tests__/integration/abort_request.test.ts +++ b/packages/client-common/__tests__/integration/abort_request.test.ts @@ -25,7 +25,7 @@ describe('abort request', () => { await expectAsync(selectPromise).toBeRejectedWith( jasmine.objectContaining({ message: jasmine.stringMatching('The user aborted a request'), - }) + }), ) }) @@ -47,7 +47,7 @@ describe('abort request', () => { await expectAsync(selectPromise).toBeRejectedWith( jasmine.objectContaining({ message: jasmine.stringMatching('The user aborted a request'), - }) + }), ) }) @@ -71,7 +71,7 @@ describe('abort request', () => { await expectAsync(selectPromise).toBeRejectedWith( jasmine.objectContaining({ message: jasmine.stringMatching('The user aborted a request'), - }) + }), ) }) @@ -106,7 +106,7 @@ describe('abort request', () => { queries.every((q) => { console.log(`${q.query} VS ${longRunningQuery}`) return !q.query.includes(longRunningQuery) - }) + }), ) }) @@ -134,7 +134,7 @@ describe('abort request', () => { }) } return requestPromise - }) + }), ) controller.abort() @@ -147,7 +147,7 @@ describe('abort request', () => { async function assertActiveQueries( client: ClickHouseClient, - assertQueries: (queries: Array<{ query: string }>) => boolean + assertQueries: (queries: Array<{ query: string }>) => boolean, ) { let isRunning = true while (isRunning) { diff --git a/packages/client-common/__tests__/integration/auth.test.ts b/packages/client-common/__tests__/integration/auth.test.ts index 0c350cf0..3a734cc3 100644 --- a/packages/client-common/__tests__/integration/auth.test.ts +++ b/packages/client-common/__tests__/integration/auth.test.ts @@ -16,13 +16,13 @@ describe('authentication', () => { await expectAsync( client.query({ query: 'SELECT number FROM system.numbers LIMIT 3', - }) + }), ).toBeRejectedWith( jasmine.objectContaining({ code: '516', type: 'AUTHENTICATION_FAILED', message: jasmine.stringMatching('Authentication failed'), - }) + }), ) }) }) diff --git a/packages/client-common/__tests__/integration/clickhouse_settings.test.ts b/packages/client-common/__tests__/integration/clickhouse_settings.test.ts index 2fee6caf..1f89f5b6 100644 --- a/packages/client-common/__tests__/integration/clickhouse_settings.test.ts +++ b/packages/client-common/__tests__/integration/clickhouse_settings.test.ts @@ -72,7 +72,7 @@ describe('ClickHouse settings', () => { query: `SELECT * FROM ${tableName}`, format: 'JSONEachRow', }) - .then((r) => r.json()) + .then((r) => r.json()), ).toEqual([ { 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 3bad6c3d..606c50e6 100644 --- a/packages/client-common/__tests__/integration/config.test.ts +++ b/packages/client-common/__tests__/integration/config.test.ts @@ -16,11 +16,11 @@ describe('config', () => { await expectAsync( client.query({ query: 'SELECT sleep(3)', - }) + }), ).toBeRejectedWith( jasmine.objectContaining({ message: jasmine.stringMatching('Timeout error.'), - }) + }), ) }) diff --git a/packages/client-common/__tests__/integration/data_types.test.ts b/packages/client-common/__tests__/integration/data_types.test.ts index d5abfab8..d775fca4 100644 --- a/packages/client-common/__tests__/integration/data_types.test.ts +++ b/packages/client-common/__tests__/integration/data_types.test.ts @@ -49,7 +49,7 @@ describe('data types', () => { 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' + 'i1 Int8, i2 Int16, i3 Int32, i4 Int64, i5 Int128, i6 Int256', ) await insertAndAssert(table, values) }) @@ -76,7 +76,7 @@ describe('data types', () => { ] const table = await createTableWithFields( client, - 's String, fs FixedString(3)' + 's String, fs FixedString(3)', ) await insertAndAssert(table, values) }) @@ -88,11 +88,11 @@ describe('data types', () => { table, values: [{ fs: 'foobar' }], format: 'JSONEachRow', - }) + }), ).toBeRejectedWith( jasmine.objectContaining({ message: jasmine.stringContaining('Too large value for FixedString(3)'), - }) + }), ) }) @@ -120,7 +120,7 @@ describe('data types', () => { const table = await createTableWithFields( client, 'd1 Decimal(9, 2), d2 Decimal(18, 3), ' + - 'd3 Decimal(38, 10), d4 Decimal(76, 20)' + 'd3 Decimal(38, 10), d4 Decimal(76, 20)', ) await client.insert({ table, @@ -165,7 +165,7 @@ describe('data types', () => { const table = await createTableWithFields( client, 'd1 Date, d2 Date32, dt1 DateTime, ' + - 'dt2 DateTime64(3), dt3 DateTime64(6), dt4 DateTime64(9)' + 'dt2 DateTime64(3), dt3 DateTime64(6), dt4 DateTime64(9)', ) await insertAndAssert(table, values) }) @@ -204,7 +204,7 @@ describe('data types', () => { ] 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') @@ -221,7 +221,7 @@ describe('data types', () => { ] const table = await createTableWithFields( client, - `e1 Enum('Foo', 'Bar'), e2 Enum('Qaz', 'Qux')` + `e1 Enum('Foo', 'Bar'), e2 Enum('Qaz', 'Qux')`, ) await insertAndAssert(table, values) }) @@ -233,7 +233,7 @@ describe('data types', () => { ] const table = await createTableWithFields( client, - `e1 Enum('Foo' = 42, 'Bar' = 43), e2 Enum('Qaz' = 100, 'Qux' = 127)` + `e1 Enum('Foo' = 42, 'Bar' = 43), e2 Enum('Qaz' = 100, 'Qux' = 127)`, ) await insertData(table, values) const result = await client @@ -258,7 +258,7 @@ describe('data types', () => { ] const table = await createTableWithFields( client, - 's LowCardinality(String), fs LowCardinality(FixedString(3))' + 's LowCardinality(String), fs LowCardinality(FixedString(3))', ) await insertAndAssert(table, values) }) @@ -270,7 +270,7 @@ describe('data types', () => { ] const table = await createTableWithFields( client, - 't1 Tuple(String, Int32), t2 Tuple(Date, Array(Int32))' + 't1 Tuple(String, Int32), t2 Tuple(Date, Array(Int32))', ) await insertAndAssert(table, values) }) @@ -282,7 +282,7 @@ describe('data types', () => { ] const table = await createTableWithFields( client, - 's Nullable(String), i Nullable(Int32), a Array(Nullable(Int32))' + 's Nullable(String), i Nullable(Int32), a Array(Nullable(Int32))', ) await insertAndAssert(table, values) }) @@ -313,7 +313,7 @@ describe('data types', () => { function genNestedArray(level: number): unknown { if (level === 1) { return [...Array(getRandomInt(2, 4))].map(() => - Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2), ) } return [...Array(getRandomInt(1, 3))].map(() => genNestedArray(level - 1)) @@ -371,12 +371,12 @@ describe('data types', () => { if (level === 1) { ;[...Array(getRandomInt(2, 4))].forEach( () => - (obj[getRandomInt(1, 1000)] = Math.random().toString(36).slice(2)) + (obj[getRandomInt(1, 1000)] = Math.random().toString(36).slice(2)), ) return obj } ;[...Array(getRandomInt(1, 3))].forEach( - () => (obj[getRandomInt(1, 1000)] = genNestedMap(level - 1)) + () => (obj[getRandomInt(1, 1000)] = genNestedMap(level - 1)), ) return obj } @@ -403,7 +403,7 @@ describe('data types', () => { const table = await createTableWithFields( client, 'm1 Map(String, String), m2 Map(Int32, Int64), ' + - `m3 ${genMapType(maxNestingLevel)}` + `m3 ${genMapType(maxNestingLevel)}`, ) await insertAndAssert(table, values) }) @@ -416,7 +416,7 @@ describe('data types', () => { ] const table = await createTableWithFields( client, - `route Int32, distance Decimal(10, 2)` + `route Int32, distance Decimal(10, 2)`, ) await client.insert({ table, @@ -429,7 +429,7 @@ describe('data types', () => { query: `SELECT sum(distance) FROM ${table}`, format: 'TabSeparated', }) - .then((r) => r.text()) + .then((r) => r.text()), ).toEqual('125.53\n') expect( await client @@ -437,7 +437,7 @@ describe('data types', () => { query: `SELECT max(distance) FROM ${table}`, format: 'TabSeparated', }) - .then((r) => r.text()) + .then((r) => r.text()), ).toEqual('100.52\n') expect( await client @@ -445,7 +445,7 @@ describe('data types', () => { query: `SELECT uniqExact(distance) FROM ${table}`, format: 'TabSeparated', }) - .then((r) => r.text()) + .then((r) => r.text()), ).toEqual('3\n') }) @@ -499,7 +499,7 @@ describe('data types', () => { ] const table = await createTableWithFields( client, - 'p Point, r Ring, pg Polygon, mpg MultiPolygon' + 'p Point, r Ring, pg Polygon, mpg MultiPolygon', ) await insertAndAssert(table, values) }) @@ -539,7 +539,7 @@ describe('data types', () => { const table = await createTableWithFields( client, 'n Nested(id UInt32, name String, createdAt DateTime, ' + - `roles Array(Enum('User', 'Admin')))` + `roles Array(Enum('User', 'Admin')))`, ) await client.insert({ table, @@ -599,7 +599,7 @@ describe('data types', () => { `roles Array(Enum('User', 'Admin')))`, { flatten_nested: 0, - } + }, ) await client.insert({ table, @@ -652,7 +652,7 @@ describe('data types', () => { const table = await createTableWithFields( client, 'n Nested(id UInt32, name String, createdAt DateTime, ' + - `roles Array(Enum('User', 'Admin')))` + `roles Array(Enum('User', 'Admin')))`, ) await client.insert({ table, @@ -715,7 +715,7 @@ describe('data types', () => { `roles Array(Enum('User', 'Admin')))`, { flatten_nested: 0, - } + }, ) await client.insert({ table, @@ -750,7 +750,7 @@ describe('data types', () => { async function insertData( table: string, data: T[], - clickhouse_settings?: ClickHouseSettings + clickhouse_settings?: ClickHouseSettings, ) { const values = data.map((v, i) => ({ ...v, id: i + 1 })) await client.insert({ diff --git a/packages/client-common/__tests__/integration/date_time.test.ts b/packages/client-common/__tests__/integration/date_time.test.ts index 1ab5a25c..84c8a13d 100644 --- a/packages/client-common/__tests__/integration/date_time.test.ts +++ b/packages/client-common/__tests__/integration/date_time.test.ts @@ -28,7 +28,7 @@ describe('DateTime', () => { query: `SELECT * EXCEPT id FROM ${table}`, format: 'JSONEachRow', }) - .then((r) => r.json()) + .then((r) => r.json()), ).toEqual([{ d: '2022-09-05' }]) }) }) @@ -50,7 +50,7 @@ describe('DateTime', () => { query: `SELECT * EXCEPT id FROM ${table}`, format: 'JSONEachRow', }) - .then((r) => r.json()) + .then((r) => r.json()), ).toEqual([{ d: '2022-09-05' }]) }) }) @@ -73,7 +73,7 @@ describe('DateTime', () => { query: `SELECT d FROM ${table}`, format: 'JSONEachRow', }) - .then((r) => r.json()) + .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 @@ -87,14 +87,14 @@ describe('DateTime', () => { query: `SELECT toDateTime(d, 'Europe/Amsterdam') AS d FROM ${table}`, format: 'JSONEachRow', }) - .then((r) => r.json()) + .then((r) => r.json()), ).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 () => { const table = await createTableWithFields( client, - `d DateTime('Asia/Istanbul')` + `d DateTime('Asia/Istanbul')`, ) await client.insert({ table, @@ -111,7 +111,7 @@ describe('DateTime', () => { query: `SELECT * EXCEPT id FROM ${table}`, format: 'JSONEachRow', }) - .then((r) => r.json()) + .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 @@ -125,7 +125,7 @@ describe('DateTime', () => { query: `SELECT toDateTime(d, 'Europe/Amsterdam') AS d FROM ${table}`, format: 'JSONEachRow', }) - .then((r) => r.json()) + .then((r) => r.json()), ).toEqual([{ d: '2022-09-05 00:02:49' }, { d: '2022-09-04 23:02:49' }]) }) }) @@ -148,7 +148,7 @@ describe('DateTime', () => { query: `SELECT * EXCEPT id FROM ${table}`, format: 'JSONEachRow', }) - .then((r) => r.json()) + .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 @@ -162,7 +162,7 @@ describe('DateTime', () => { query: `SELECT toDateTime64(d, 3, 'Europe/Amsterdam') AS d FROM ${table}`, format: 'JSONEachRow', }) - .then((r) => r.json()) + .then((r) => r.json()), ).toEqual([ { d: '2022-09-05 00:02:49.123' }, { d: '2022-09-05 02:02:49.456' }, @@ -172,7 +172,7 @@ describe('DateTime', () => { it('should insert DateTime64(3) and get it back (different timezone)', async () => { const table = await createTableWithFields( client, - `d DateTime64(3, 'Asia/Istanbul')` + `d DateTime64(3, 'Asia/Istanbul')`, ) await client.insert({ table, @@ -189,7 +189,7 @@ describe('DateTime', () => { query: `SELECT * EXCEPT id FROM ${table}`, format: 'JSONEachRow', }) - .then((r) => r.json()) + .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 @@ -203,7 +203,7 @@ describe('DateTime', () => { query: `SELECT toDateTime64(d, 3, 'Europe/Amsterdam') AS d FROM ${table}`, format: 'JSONEachRow', }) - .then((r) => r.json()) + .then((r) => r.json()), ).toEqual([ { 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 612bfa5a..3643d7ca 100644 --- a/packages/client-common/__tests__/integration/error_parsing.test.ts +++ b/packages/client-common/__tests__/integration/error_parsing.test.ts @@ -14,13 +14,13 @@ describe('ClickHouse server errors parsing', () => { await expectAsync( client.query({ query: 'SELECT number FR', - }) + }), ).toBeRejectedWith( jasmine.objectContaining({ message: `Missing columns: 'number' while processing query: 'SELECT number AS FR', required columns: 'number'. `, code: '47', type: 'UNKNOWN_IDENTIFIER', - }) + }), ) }) @@ -32,13 +32,13 @@ describe('ClickHouse server errors parsing', () => { await expectAsync( client.query({ query: 'SELECT * FROM unknown_table', - }) + }), ).toBeRejectedWith( jasmine.objectContaining({ message: jasmine.stringMatching(errorMessagePattern), code: '60', type: 'UNKNOWN_TABLE', - }) + }), ) }) @@ -46,13 +46,13 @@ describe('ClickHouse server errors parsing', () => { await expectAsync( client.query({ query: 'SELECT * FRON unknown_table', - }) + }), ).toBeRejectedWith( jasmine.objectContaining({ message: jasmine.stringContaining('Syntax error: failed at position'), code: '62', type: 'SYNTAX_ERROR', - }) + }), ) }) @@ -66,13 +66,13 @@ describe('ClickHouse server errors parsing', () => { */ FRON unknown_table `, - }) + }), ).toBeRejectedWith( jasmine.objectContaining({ message: jasmine.stringContaining('Syntax error: failed at position'), code: '62', type: 'SYNTAX_ERROR', - }) + }), ) }) }) diff --git a/packages/client-common/__tests__/integration/exec.test.ts b/packages/client-common/__tests__/integration/exec.test.ts index 39d3cf27..076f20c9 100644 --- a/packages/client-common/__tests__/integration/exec.test.ts +++ b/packages/client-common/__tests__/integration/exec.test.ts @@ -66,9 +66,9 @@ describe('exec', () => { code: '57', type: 'TABLE_ALREADY_EXISTS', message: jasmine.stringContaining( - `Table ${getTestDatabaseName()}.${tableName} already exists. ` + `Table ${getTestDatabaseName()}.${tableName} already exists. `, ), - }) + }), ) }) @@ -89,7 +89,7 @@ describe('exec', () => { await expectAsync( sessionClient.exec({ query: `CREATE TEMPORARY TABLE ${tableName} (val Int32)`, - }) + }), ).toBeResolved() }) }) @@ -132,7 +132,7 @@ describe('exec', () => { async function runExec(params: ExecParams): Promise<{ query_id: string }> { console.log( - `Running command with query_id ${params.query_id}:\n${params.query}` + `Running command with query_id ${params.query_id}:\n${params.query}`, ) const { query_id } = await client.exec({ ...params, diff --git a/packages/client-common/__tests__/integration/insert.test.ts b/packages/client-common/__tests__/integration/insert.test.ts index e1a6e50e..6fed8c9b 100644 --- a/packages/client-common/__tests__/integration/insert.test.ts +++ b/packages/client-common/__tests__/integration/insert.test.ts @@ -114,8 +114,8 @@ describe('insert', () => { values: [row], table: tableName, format: 'JSONEachRow', - }) - ) + }), + ), ) await assertJsonValues(client, tableName) }) @@ -127,7 +127,7 @@ describe('insert', () => { values: jsonValues, format: 'JSONEachRow', clickhouse_settings: { foobar: 1 } as any, - }) + }), ).toBeRejectedWith( jasmine.objectContaining({ // Possible error messages: @@ -136,7 +136,7 @@ describe('insert', () => { message: jasmine.stringContaining('foobar'), code: '115', type: 'UNKNOWN_SETTING', - }) + }), ) }) 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 0dd1f657..1250bd1e 100644 --- a/packages/client-common/__tests__/integration/insert_specific_columns.test.ts +++ b/packages/client-common/__tests__/integration/insert_specific_columns.test.ts @@ -29,7 +29,7 @@ describe('Insert with specific columns', () => { repo_name LowCardinality(String) DEFAULT JSONExtractString(message_raw, 'repo', 'name'), message JSON DEFAULT message_raw, message_raw String EPHEMERAL - ` // `id UInt32` will be added as well + `, // `id UInt32` will be added as well ) }) @@ -209,11 +209,11 @@ describe('Insert with specific columns', () => { ], format: 'JSONEachRow', columns: ['foobar', 'message_raw'], - }) + }), ).toBeRejectedWith( jasmine.objectContaining({ message: jasmine.stringContaining('No such column foobar'), - }) + }), ) }) }) @@ -224,7 +224,7 @@ describe('Insert with specific columns', () => { beforeEach(async () => { table = await createTableWithFields( client, - `s String, b Boolean` // `id UInt32` will be added as well + `s String, b Boolean`, // `id UInt32` will be added as well ) }) @@ -259,7 +259,7 @@ describe('Insert with specific columns', () => { beforeEach(async () => { table = await createTableWithFields( client, - `s String, b Boolean` // `id UInt32` will be added as well + `s String, b Boolean`, // `id UInt32` will be added as well ) }) diff --git a/packages/client-common/__tests__/integration/multiple_clients.test.ts b/packages/client-common/__tests__/integration/multiple_clients.test.ts index 716e7fd0..25ce0ef1 100644 --- a/packages/client-common/__tests__/integration/multiple_clients.test.ts +++ b/packages/client-common/__tests__/integration/multiple_clients.test.ts @@ -29,8 +29,8 @@ describe('multiple clients', () => { format: 'JSONEachRow', }) .then((r) => r.json<{ sum: number }>()) - .then((json) => results.push(json[0].sum)) - ) + .then((json) => results.push(json[0].sum)), + ), ) expect(results.sort((a, b) => a - b)).toEqual([1, 3, 6, 10, 15]) }) @@ -39,7 +39,7 @@ describe('multiple clients', () => { const id = guid() const tableName = (i: number) => `multiple_clients_ddl_test__${id}__${i}` await Promise.all( - clients.map((client, i) => createSimpleTable(client, tableName(i))) + clients.map((client, i) => createSimpleTable(client, tableName(i))), ) for (let i = 0; i < CLIENTS_COUNT; i++) { const result = await clients[i].query({ @@ -79,8 +79,8 @@ describe('multiple clients', () => { table: tableName, values: [getValue(i)], format: 'JSONEachRow', - }) - ) + }), + ), ) const result = await clients[0].query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, diff --git a/packages/client-common/__tests__/integration/query_log.test.ts b/packages/client-common/__tests__/integration/query_log.test.ts index 66b5c2c3..2331a20c 100644 --- a/packages/client-common/__tests__/integration/query_log.test.ts +++ b/packages/client-common/__tests__/integration/query_log.test.ts @@ -28,7 +28,7 @@ describe('query_log', () => { const formattedQuery = 'SELECT * FROM system.numbers LIMIT 144 \nFORMAT JSON' await assertQueryLog({ formattedQuery, query_id }) - } + }, ) whenOnEnv(...testEnvs).it( @@ -41,7 +41,7 @@ describe('query_log', () => { query, }) await assertQueryLog({ formattedQuery: query, query_id }) - } + }, ) whenOnEnv(...testEnvs).it( @@ -59,7 +59,7 @@ describe('query_log', () => { }) const formattedQuery = `INSERT INTO ${table} FORMAT JSONObjectEachRow\n` await assertQueryLog({ formattedQuery, query_id }) - } + }, ) async function assertQueryLog({ 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 070c84df..b601b014 100644 --- a/packages/client-common/__tests__/integration/read_only_user.test.ts +++ b/packages/client-common/__tests__/integration/read_only_user.test.ts @@ -51,11 +51,11 @@ describe('read only user', () => { it('should fail to create a table', async () => { await expectAsync( - createSimpleTable(client, `should_not_be_created_${guid()}`) + createSimpleTable(client, `should_not_be_created_${guid()}`), ).toBeRejectedWith( jasmine.objectContaining({ message: jasmine.stringContaining('Not enough privileges'), - }) + }), ) }) @@ -64,11 +64,11 @@ describe('read only user', () => { client.insert({ table: tableName, values: [[43, 'foobar', [5, 25]]], - }) + }), ).toBeRejectedWith( jasmine.objectContaining({ message: jasmine.stringContaining('Not enough privileges'), - }) + }), ) }) @@ -78,7 +78,7 @@ describe('read only user', () => { await expectAsync(client.query({ query })).toBeRejectedWith( jasmine.objectContaining({ message: jasmine.stringContaining('Not enough privileges'), - }) + }), ) }) }) diff --git a/packages/client-common/__tests__/integration/select.test.ts b/packages/client-common/__tests__/integration/select.test.ts index 64ad785f..d8cabfbc 100644 --- a/packages/client-common/__tests__/integration/select.test.ts +++ b/packages/client-common/__tests__/integration/select.test.ts @@ -37,7 +37,7 @@ describe('select', () => { query: 'SELECT * FROM system.numbers LIMIT 0', format: 'JSONEachRow', }) - .then((r) => r.json()) + .then((r) => r.json()), ).toEqual([]) expect( await client @@ -45,7 +45,7 @@ describe('select', () => { query: 'SELECT * FROM system.numbers LIMIT 0', format: 'TabSeparated', }) - .then((r) => r.text()) + .then((r) => r.text()), ).toEqual('') }) @@ -124,11 +124,11 @@ describe('select', () => { it('does not swallow a client error', async () => { await expectAsync( - client.query({ query: 'SELECT number FR' }) + client.query({ query: 'SELECT number FR' }), ).toBeRejectedWith( jasmine.objectContaining({ type: 'UNKNOWN_IDENTIFIER', - }) + }), ) }) @@ -138,7 +138,7 @@ describe('select', () => { message: jasmine.stringContaining('Syntax error'), code: '62', type: 'SYNTAX_ERROR', - }) + }), ) }) @@ -147,7 +147,7 @@ describe('select', () => { client.query({ query: 'SELECT * FROM system.numbers', clickhouse_settings: { foobar: 1 } as any, - }) + }), ).toBeRejectedWith( jasmine.objectContaining({ // Possible error messages: @@ -156,7 +156,7 @@ describe('select', () => { message: jasmine.stringContaining('foobar'), code: '115', type: 'UNKNOWN_SETTING', - }) + }), ) }) @@ -170,8 +170,8 @@ describe('select', () => { format: 'JSONEachRow', }) .then((r) => r.json<{ sum: number }>()) - .then((json) => results.push(json[0].sum)) - ) + .then((json) => results.push(json[0].sum)), + ), ) expect(results.sort((a, b) => a - b)).toEqual([1, 3, 6, 10, 15]) }) 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 6870df26..004f5e8f 100644 --- a/packages/client-common/__tests__/integration/select_query_binding.test.ts +++ b/packages/client-common/__tests__/integration/select_query_binding.test.ts @@ -257,18 +257,18 @@ describe('select with query binding', () => { SELECT * FROM system.numbers WHERE number > {min_limit: UInt64} LIMIT 3 `, - }) + }), ).toBeRejectedWith( jasmine.objectContaining({ message: jasmine.stringMatching( // possible error messages here: // (since 23.8+) Substitution `min_limit` is not set. // (pre-23.8) Query parameter `min_limit` was not set - /^.+?`min_limit`.+?not set.*$/ + /^.+?`min_limit`.+?not set.*$/, ), code: '456', type: 'UNKNOWN_QUERY_PARAMETER', - }) + }), ) }) }) diff --git a/packages/client-common/__tests__/integration/select_result.test.ts b/packages/client-common/__tests__/integration/select_result.test.ts index 116240f1..2bc28a13 100644 --- a/packages/client-common/__tests__/integration/select_result.test.ts +++ b/packages/client-common/__tests__/integration/select_result.test.ts @@ -26,7 +26,7 @@ describe('Select ResultSet', () => { }) expect(await rs.text()).toBe( - '{"number":"0"}\n{"number":"1"}\n{"number":"2"}\n' + '{"number":"0"}\n{"number":"1"}\n{"number":"2"}\n', ) }) }) @@ -86,7 +86,7 @@ describe('Select ResultSet', () => { rows_read: jasmine.any(Number), bytes_read: jasmine.any(Number), }, - }) + }), ) }) }) diff --git a/packages/client-common/__tests__/unit/config.test.ts b/packages/client-common/__tests__/unit/config.test.ts index c5a11ebb..707d426f 100644 --- a/packages/client-common/__tests__/unit/config.test.ts +++ b/packages/client-common/__tests__/unit/config.test.ts @@ -67,7 +67,7 @@ describe('config', () => { }, }, logger, - null + null, ) expect(res).toEqual({ url: new URL('https://my.host:8443/'), @@ -137,7 +137,7 @@ describe('config', () => { }, }, logger, - null + null, ) expect(res).toEqual({ url: new URL('https://my.host:8443/'), @@ -164,7 +164,7 @@ describe('config', () => { }, }, logger, - null + null, ) expect(res).toEqual({ url: new URL('https://my.host:8443/'), @@ -190,7 +190,7 @@ describe('config', () => { }, }, logger, - null + null, ) expect(res).toEqual({ url: new URL('https://my.host:8443/'), @@ -224,7 +224,7 @@ describe('config', () => { handled_params: new Set(['impl_specific_setting']), unknown_params: new Set(), } - } + }, ) expect(res).toEqual({ ...defaultConfig, @@ -242,7 +242,7 @@ describe('config', () => { expect(() => prepareConfigWithURL({ url: 'foo' }, logger, null)).toThrow( jasmine.objectContaining({ message: jasmine.stringContaining('ClickHouse URL is malformed.'), - }) + }), ) }) }) @@ -253,7 +253,7 @@ describe('config', () => { { url: new URL('https://my.host:8443/'), }, - logger + logger, ) expect(res).toEqual({ url: new URL('https://my.host:8443/'), @@ -296,7 +296,7 @@ describe('config', () => { keep_alive: { enabled: false }, application: 'my_app', }, - logger + logger, ) expect(res).toEqual({ url: new URL('https://my.host:8443/'), @@ -389,10 +389,10 @@ describe('config', () => { 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/') + new URL('http://localhost:8123/'), ) expect(createUrl('https://bob:secret@my.host:8124')).toEqual( - new URL('https://bob:secret@my.host:8124/') + new URL('https://bob:secret@my.host:8124/'), ) }) @@ -400,13 +400,13 @@ describe('config', () => { expect(() => createUrl('foo')).toThrow( jasmine.objectContaining({ message: jasmine.stringContaining('ClickHouse URL is malformed.'), - }) + }), ) expect(() => createUrl('http://localhost/foo')).toThrowError( - 'ClickHouse URL must contain a valid port number.' + 'ClickHouse URL must contain a valid port number.', ) expect(() => createUrl('tcp://localhost:8443')).toThrowError( - 'ClickHouse URL protocol must be either http or https. Got: tcp:' + 'ClickHouse URL protocol must be either http or https. Got: tcp:', ) }) }) @@ -428,7 +428,7 @@ describe('config', () => { 'clickhouse_setting_async_insert=1', 'ch_wait_for_async_insert=0', 'http_header_X-CLICKHOUSE-AUTH=secret_header', - ].join('&') + ].join('&'), ) const res = loadConfigOptionsFromURL(url, null) expect(res[0].toString()).toEqual('https://my.host:8124/') @@ -477,7 +477,7 @@ describe('config', () => { 'application=my_app', 'request_timeout=42000', 'max_open_connections=2', - ].join('&') + ].join('&'), ) const res = loadConfigOptionsFromURL(url, null) expect(res[0].toString()).toEqual('http://localhost:8124/') @@ -511,11 +511,11 @@ describe('config', () => { 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' + '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', ) }) @@ -548,13 +548,13 @@ describe('config', () => { } } 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 () => { 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 { @@ -567,7 +567,7 @@ describe('config', () => { } } expect(() => loadConfigOptionsFromURL(url, handler)).toThrowError( - 'Unknown URL parameters: whatever' + 'Unknown URL parameters: whatever', ) }) @@ -589,7 +589,7 @@ describe('config', () => { 'http_header_X-CLICKHOUSE-AUTH=secret_header', 'impl_specific_setting=qaz', 'another_impl_specific_setting=qux', - ].join('&') + ].join('&'), ) const handler: HandleImplSpecificURLParams = (config) => { return { @@ -642,13 +642,13 @@ describe('config', () => { 'application=my_app', 'session_id=sticky', 'request_timeout=42', - ].join('&') + ].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()}` + `Unexpected URL params: ${url.searchParams.toString()}`, ) } return { @@ -690,7 +690,7 @@ describe('config', () => { .toEqual(expected) }) expect(() => booleanConfigURLValue({ key, value: 'bar' })).toThrowError( - `"foo" has invalid boolean value: bar. Expected one of: 0, 1, true, false.` + `"foo" has invalid boolean value: bar. Expected one of: 0, 1, true, false.`, ) }) @@ -711,7 +711,7 @@ describe('config', () => { .toEqual(expected) }) expect(() => numberConfigURLValue({ key, value: 'bar' })).toThrowError( - `"foo" has invalid numeric value: bar` + `"foo" has invalid numeric value: bar`, ) }) @@ -719,7 +719,7 @@ describe('config', () => { 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 }) + numberConfigURLValue({ key, value: '2', min: 3 }), ).toThrowError(`"foo" value 2 is less than min allowed 3`) }) @@ -727,7 +727,7 @@ describe('config', () => { 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 }) + numberConfigURLValue({ key, value: '4', max: 3 }), ).toThrowError(`"foo" value 4 is greater than max allowed 3`) }) @@ -737,10 +737,10 @@ describe('config', () => { const r2 = numberConfigURLValue({ key, value: '2', min: 2, max: 2 }) expect(r2).toEqual(2) expect(() => - numberConfigURLValue({ key, value: '2', min: 3, max: 4 }) + 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 }) + numberConfigURLValue({ key, value: '5', min: 3, max: 4 }), ).toThrowError(`"foo" value 5 is greater than max allowed 4`) }) @@ -765,7 +765,7 @@ describe('config', () => { key, value, enumObject: ClickHouseLogLevel, - }) + }), ) .withContext(`Expected log level for value "${value}" is ${expected}`) .toEqual(expected) @@ -775,9 +775,9 @@ describe('config', () => { key, value: 'bar', enumObject: ClickHouseLogLevel, - }) + }), ).toThrowError( - `"foo" has invalid value: bar. Expected one of: TRACE, DEBUG, INFO, WARN, ERROR, OFF.` + `"foo" has invalid value: bar. Expected one of: TRACE, DEBUG, INFO, WARN, ERROR, OFF.`, ) }) }) 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 3107ff7b..23825346 100644 --- a/packages/client-common/__tests__/unit/format_query_params.test.ts +++ b/packages/client-common/__tests__/unit/format_query_params.test.ts @@ -50,13 +50,13 @@ describe('formatQueryParams', () => { it('formats a date with millis', () => { expect( - formatQueryParams(new Date(Date.UTC(2022, 6, 29, 7, 52, 14, 123))) + formatQueryParams(new Date(Date.UTC(2022, 6, 29, 7, 52, 14, 123))), ).toBe('1659081134.123') expect( - formatQueryParams(new Date(Date.UTC(2022, 6, 29, 7, 52, 14, 42))) + formatQueryParams(new Date(Date.UTC(2022, 6, 29, 7, 52, 14, 42))), ).toBe('1659081134.042') expect( - formatQueryParams(new Date(Date.UTC(2022, 6, 29, 7, 52, 14, 5))) + formatQueryParams(new Date(Date.UTC(2022, 6, 29, 7, 52, 14, 5))), ).toBe('1659081134.005') }) @@ -77,7 +77,7 @@ describe('formatQueryParams', () => { expect( formatQueryParams({ ["na'me"]: "cust'om", - }) + }), ).toBe("{'na\\'me':'cust\\'om'}") }) @@ -87,7 +87,7 @@ describe('formatQueryParams', () => { name: 'custom', id: 42, params: { refs: [44] }, - }) + }), ).toBe("{'name':'custom','id':42,'params':{'refs':[44]}}") }) }) 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 133206f6..8fc50d95 100644 --- a/packages/client-common/__tests__/unit/format_query_settings.test.ts +++ b/packages/client-common/__tests__/unit/format_query_settings.test.ts @@ -16,16 +16,16 @@ describe('formatQuerySettings', () => { it('formats a Map', () => { expect( - formatQuerySettings(SettingsMap.from({ foo: 'bar', baz: 'qux' })) + formatQuerySettings(SettingsMap.from({ foo: 'bar', baz: 'qux' })), ).toBe(`{'foo':'bar','baz':'qux'}`) }) 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_error.test.ts b/packages/client-common/__tests__/unit/parse_error.test.ts index e3b95f4c..7e413a5b 100644 --- a/packages/client-common/__tests__/unit/parse_error.test.ts +++ b/packages/client-common/__tests__/unit/parse_error.test.ts @@ -9,7 +9,7 @@ describe('parseError', () => { 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. ` + `Syntax error: failed at position 15 ('unknown_table') (line 1, col 15): unknown_table FORMAT JSON. Expected alias cannot be here. `, ) }) @@ -23,7 +23,7 @@ describe('parseError', () => { 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. ` + FORMAT JSON. Expected alias cannot be here. `, ) }) @@ -35,7 +35,7 @@ describe('parseError', () => { 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). ' + 'Number of alive replicas (2) is less than requested quorum (3). ', ) }) @@ -48,7 +48,7 @@ describe('parseError', () => { 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. ', ) }) @@ -60,7 +60,7 @@ describe('parseError', () => { 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 ', ) }) @@ -72,7 +72,7 @@ describe('parseError', () => { 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 ' + 'creation of LZ4 compression context failed. LZ4F version: 1.9.3 ', ) }) }) @@ -87,7 +87,7 @@ describe('parseError', () => { 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/to_search_params.test.ts b/packages/client-common/__tests__/unit/to_search_params.test.ts index a327cb57..b9b338fa 100644 --- a/packages/client-common/__tests__/unit/to_search_params.test.ts +++ b/packages/client-common/__tests__/unit/to_search_params.test.ts @@ -85,6 +85,6 @@ describe('toSearchParams', () => { function toSortedArray(params: URLSearchParams): [string, string][] { return [...params.entries()].sort(([key1], [key2]) => - String(key1).localeCompare(String(key2)) + String(key1).localeCompare(String(key2)), ) } diff --git a/packages/client-common/__tests__/utils/client.ts b/packages/client-common/__tests__/utils/client.ts index 91833ee6..3765488e 100644 --- a/packages/client-common/__tests__/utils/client.ts +++ b/packages/client-common/__tests__/utils/client.ts @@ -24,13 +24,13 @@ beforeAll(async () => { }) export function createTestClient( - config: BaseClickHouseClientConfigOptions = {} + config: BaseClickHouseClientConfigOptions = {}, ): ClickHouseClient { const env = getClickHouseTestEnvironment() console.log( `Using ${env} test environment to create a Client instance for database ${ databaseName || 'default' - }` + }`, ) const clickHouseSettings: ClickHouseSettings = {} if (env === TestEnv.LocalCluster) { @@ -62,7 +62,7 @@ export function createTestClient( // props to https://stackoverflow.com/a/41063795/4575540 // @ts-expect-error return eval('require')('../../../client-node/src/client').createClient( - cloudConfig + cloudConfig, ) as ClickHouseClient } } else { @@ -77,14 +77,14 @@ export function createTestClient( } else { // @ts-expect-error return eval('require')('../../../client-node/src/client').createClient( - localConfig + localConfig, ) as ClickHouseClient } } } export async function createRandomDatabase( - client: ClickHouseClient + client: ClickHouseClient, ): Promise { const databaseName = `clickhousejs__${guid()}__${+new Date()}` let maybeOnCluster = '' @@ -104,7 +104,7 @@ export async function createRandomDatabase( export async function createTable( client: ClickHouseClient, definition: (environment: TestEnv) => string, - clickhouse_settings?: ClickHouseSettings + clickhouse_settings?: ClickHouseSettings, ) { const env = getClickHouseTestEnvironment() const ddl = definition(env) diff --git a/packages/client-common/__tests__/utils/test_connection_type.ts b/packages/client-common/__tests__/utils/test_connection_type.ts index 8e433c00..409ba627 100644 --- a/packages/client-common/__tests__/utils/test_connection_type.ts +++ b/packages/client-common/__tests__/utils/test_connection_type.ts @@ -16,7 +16,7 @@ export function getTestConnectionType(): TestConnectionType { throw new Error( 'Unexpected CLICKHOUSE_TEST_CONNECTION_TYPE value. ' + 'Possible options: `node`, `browser` ' + - 'or keep it unset to fall back to `node`' + 'or keep it unset to fall back to `node`', ) } return connectionType diff --git a/packages/client-common/__tests__/utils/test_env.ts b/packages/client-common/__tests__/utils/test_env.ts index 1c7b340d..7d10b8ae 100644 --- a/packages/client-common/__tests__/utils/test_env.ts +++ b/packages/client-common/__tests__/utils/test_env.ts @@ -23,7 +23,7 @@ export function getClickHouseTestEnvironment(): TestEnv { throw new Error( `Unexpected CLICKHOUSE_TEST_ENVIRONMENT value: ${value}. ` + 'Possible options: `local_single_node`, `local_cluster`, `cloud` ' + - 'or keep it unset to fall back to `local_single_node`' + 'or keep it unset to fall back to `local_single_node`', ) } return env diff --git a/packages/client-common/__tests__/utils/test_logger.ts b/packages/client-common/__tests__/utils/test_logger.ts index c9e35835..de0abe53 100644 --- a/packages/client-common/__tests__/utils/test_logger.ts +++ b/packages/client-common/__tests__/utils/test_logger.ts @@ -21,7 +21,7 @@ export class TestLogger implements Logger { console.error( formatMessage({ level: 'ERROR', module, message }), args || '', - err + err, ) } } diff --git a/packages/client-common/src/client.ts b/packages/client-common/src/client.ts index 90102156..849bf176 100644 --- a/packages/client-common/src/client.ts +++ b/packages/client-common/src/client.ts @@ -114,7 +114,7 @@ export class ClickHouseClient { private readonly sessionId?: string constructor( - config: BaseClickHouseClientConfigOptions & ImplementationDetails + config: BaseClickHouseClientConfigOptions & ImplementationDetails, ) { const logger = config?.log?.LoggerClass ? new config.log.LoggerClass() @@ -122,14 +122,14 @@ export class ClickHouseClient { const configWithURL = prepareConfigWithURL( config, logger, - config.impl.handle_specific_url_params ?? null + config.impl.handle_specific_url_params ?? null, ) const connectionParams = getConnectionParams(configWithURL, logger) this.clientClickHouseSettings = connectionParams.clickhouse_settings this.sessionId = config.session_id this.connection = config.impl.make_connection( configWithURL, - connectionParams + connectionParams, ) this.makeResultSet = config.impl.make_result_set this.valuesEncoder = config.impl.values_encoder @@ -144,7 +144,7 @@ export class ClickHouseClient { * Returns an implementation of {@link BaseResultSet}. */ async query( - params: QueryParamsWithFormat + params: QueryParamsWithFormat, ): Promise> { const format = params.format ?? 'JSON' const query = formatQuery(params.query, format) @@ -268,7 +268,7 @@ function isInsertColumnsExcept(obj: unknown): obj is InsertColumnsExcept { function getInsertQuery( params: InsertParams, - format: DataFormat + format: DataFormat, ): string { let columnsPart = '' if (params.columns !== undefined) { diff --git a/packages/client-common/src/config.ts b/packages/client-common/src/config.ts index 737ae245..ba060723 100644 --- a/packages/client-common/src/config.ts +++ b/packages/client-common/src/config.ts @@ -85,22 +85,22 @@ export interface BaseClickHouseClientConfigOptions { export type MakeConnection< Stream, - Config = BaseClickHouseClientConfigOptionsWithURL + Config = BaseClickHouseClientConfigOptionsWithURL, > = (config: Config, params: ConnectionParams) => Connection export type MakeResultSet = < Format extends DataFormat, - ResultSet extends BaseResultSet + ResultSet extends BaseResultSet, >( stream: Stream, format: Format, - query_id: string + query_id: string, ) => ResultSet export interface ValuesEncoder { validateInsertValues( values: InsertValues, - format: DataFormat + format: DataFormat, ): void /** @@ -114,7 +114,7 @@ export interface ValuesEncoder { */ encodeValues( values: InsertValues, - format: DataFormat + format: DataFormat, ): string | Stream } @@ -129,7 +129,7 @@ export type CloseStream = (stream: Stream) => Promise */ export type HandleImplSpecificURLParams = ( config: BaseClickHouseClientConfigOptions, - url: URL + url: URL, ) => { config: BaseClickHouseClientConfigOptions // params that were handled in the implementation; used to calculate final "unknown" URL params @@ -168,7 +168,7 @@ export type BaseClickHouseClientConfigOptionsWithURL = Omit< export function prepareConfigWithURL( baseConfigOptions: BaseClickHouseClientConfigOptions, logger: Logger, - handleImplURLParams: HandleImplSpecificURLParams | null + handleImplURLParams: HandleImplSpecificURLParams | null, ): BaseClickHouseClientConfigOptionsWithURL { const baseConfig = { ...baseConfigOptions } if (baseConfig.additional_headers !== undefined) { @@ -193,7 +193,7 @@ export function prepareConfigWithURL( } const [url, configFromURL] = loadConfigOptionsFromURL( configURL, - handleImplURLParams + handleImplURLParams, ) const config = mergeConfigs(baseConfig, configFromURL, logger) let clickHouseSettings: ClickHouseSettings @@ -242,7 +242,7 @@ export function prepareConfigWithURL( export function getConnectionParams( config: BaseClickHouseClientConfigOptionsWithURL, - logger: Logger + logger: Logger, ): ConnectionParams { return { url: config.url, @@ -272,11 +272,11 @@ export function getConnectionParams( export function mergeConfigs( baseConfig: BaseClickHouseClientConfigOptions, configFromURL: BaseClickHouseClientConfigOptions, - logger: Logger + logger: Logger, ): BaseClickHouseClientConfigOptions { const config = { ...baseConfig } const keys = Object.keys( - configFromURL + configFromURL, ) as (keyof BaseClickHouseClientConfigOptions)[] for (const key of keys) { if (config[key] !== undefined) { @@ -300,12 +300,12 @@ export function createUrl(configURL: string | URL | undefined): URL { } } 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]', ) } if (url.protocol !== 'http:' && url.protocol !== 'https:') { throw new Error( - `ClickHouse URL protocol must be either http or https. Got: ${url.protocol}` + `ClickHouse URL protocol must be either http or https. Got: ${url.protocol}`, ) } if (url.port === '' || isNaN(Number(url.port))) { @@ -321,7 +321,7 @@ export function createUrl(configURL: string | URL | undefined): URL { */ export function loadConfigOptionsFromURL( url: URL, - handleExtraURLParams: HandleImplSpecificURLParams | null + handleExtraURLParams: HandleImplSpecificURLParams | null, ): [URL, BaseClickHouseClientConfigOptions] { let config: BaseClickHouseClientConfigOptions = {} if (url.username.trim() !== '') { @@ -444,7 +444,7 @@ export function loadConfigOptionsFromURL( } if (unknownParams.size > 0) { throw new Error( - `Unknown URL parameters: ${Array.from(unknownParams).join(', ')}` + `Unknown URL parameters: ${Array.from(unknownParams).join(', ')}`, ) } } @@ -464,7 +464,7 @@ export function booleanConfigURLValue({ 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.` + `"${key}" has invalid boolean value: ${trimmed}. Expected one of: 0, 1, true, false.`, ) } @@ -488,7 +488,7 @@ export function numberConfigURLValue({ } if (max !== undefined && number > max) { throw new Error( - `"${key}" value ${trimmed} is greater than max allowed ${max}` + `"${key}" value ${trimmed} is greater than max allowed ${max}`, ) } return number @@ -510,7 +510,7 @@ export function enumConfigURLValue({ if (!values.includes(trimmed)) { const expected = values.join(', ') throw new Error( - `"${key}" has invalid value: ${trimmed}. Expected one of: ${expected}.` + `"${key}" has invalid value: ${trimmed}. Expected one of: ${expected}.`, ) } return enumObject[trimmed as Key] 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 a16c0262..2095f31b 100644 --- a/packages/client-common/src/data_formatter/format_query_params.ts +++ b/packages/client-common/src/data_formatter/format_query_params.ts @@ -2,7 +2,7 @@ import { replaceAll } from '../utils' export function formatQueryParams( value: any, - wrapStringInQuotes = false + wrapStringInQuotes = false, ): string { if (value === null || value === undefined) return '\\N' if (Number.isNaN(value)) return 'nan' @@ -36,7 +36,7 @@ export function formatQueryParams( const formatted: string[] = [] for (const [key, val] of Object.entries(value)) { formatted.push( - `${formatQueryParams(key, true)}:${formatQueryParams(val, true)}` + `${formatQueryParams(key, true)}:${formatQueryParams(val, true)}`, ) } return `{${formatted.join(',')}}` 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 c100b74a..d0dff880 100644 --- a/packages/client-common/src/data_formatter/format_query_settings.ts +++ b/packages/client-common/src/data_formatter/format_query_settings.ts @@ -1,7 +1,7 @@ import { SettingsMap } from '../settings' export function formatQuerySettings( - value: number | string | boolean | SettingsMap + value: number | string | boolean | SettingsMap, ): string { if (typeof value === 'boolean') return value ? '1' : '0' if (typeof value === 'number') return String(value) diff --git a/packages/client-common/src/data_formatter/formatter.ts b/packages/client-common/src/data_formatter/formatter.ts index 306c21e8..83b4a6e0 100644 --- a/packages/client-common/src/data_formatter/formatter.ts +++ b/packages/client-common/src/data_formatter/formatter.ts @@ -58,14 +58,14 @@ const streamableFormat = [ export type StreamableDataFormat = (typeof streamableFormat)[number] function isNotStreamableJSONFamily( - format: DataFormat + format: DataFormat, ): format is SingleDocumentJSONFormat { // @ts-expect-error JSON is not assignable to notStreamableJSONFormats return singleDocumentJSONFormats.includes(format) } function isStreamableJSONFamily( - format: DataFormat + format: DataFormat, ): format is StreamableJSONDataFormat { // @ts-expect-error JSON is not assignable to streamableJSONFormats return streamableJSONFormats.includes(format) @@ -76,13 +76,13 @@ export function isSupportedRawFormat(dataFormat: DataFormat) { } export function validateStreamFormat( - format: any + format: any, ): format is StreamableDataFormat { if (!streamableFormat.includes(format)) { throw new Error( `${format} format is not streamable. Streamable formats: ${streamableFormat.join( - ',' - )}` + ',', + )}`, ) } return true @@ -120,6 +120,6 @@ export function encodeJSON(value: any, format: DataFormat): string { return JSON.stringify(value) + '\n' } throw new Error( - `The client does not support JSON encoding in [${format}] format.` + `The client does not support JSON encoding in [${format}] format.`, ) } diff --git a/packages/client-common/src/logger.ts b/packages/client-common/src/logger.ts index 57a1eb43..61af0835 100644 --- a/packages/client-common/src/logger.ts +++ b/packages/client-common/src/logger.ts @@ -36,7 +36,10 @@ export class DefaultLogger implements Logger { } export class LogWriter { private readonly logLevel: ClickHouseLogLevel - constructor(private readonly logger: Logger, logLevel?: ClickHouseLogLevel) { + constructor( + private readonly logger: Logger, + logLevel?: ClickHouseLogLevel, + ) { this.logLevel = logLevel ?? ClickHouseLogLevel.OFF this.info({ module: 'Logger', diff --git a/packages/client-common/src/result.ts b/packages/client-common/src/result.ts index 83847aba..2a6257aa 100644 --- a/packages/client-common/src/result.ts +++ b/packages/client-common/src/result.ts @@ -12,28 +12,28 @@ export type ResultJSONType = F extends StreamableJSONDataFormat ? T[] : // JSON formats with known layout { data, meta, statistics, ... } - F extends SingleDocumentJSONFormat - ? ResponseJSON - : // JSON formats returned as a Record - F extends RecordsJSONFormat - ? Record - : // CSV, TSV etc. - cannot be represented as JSON - F extends RawDataFormat - ? never - : T // happens only when Format could not be inferred from a literal + F extends SingleDocumentJSONFormat + ? ResponseJSON + : // JSON formats returned as a Record + F extends RecordsJSONFormat + ? Record + : // CSV, TSV etc. - cannot be represented as JSON + F extends RawDataFormat + ? never + : T // happens only when Format could not be inferred from a literal export type RowJSONType = // JSON*EachRow formats F extends StreamableJSONDataFormat ? T : // 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 + F extends RawDataFormat | SingleDocumentJSONFormat | RecordsJSONFormat + ? never + : T // happens only when Format could not be inferred from a literal export interface Row< JSONType = unknown, - Format extends DataFormat | unknown = unknown + Format extends DataFormat | unknown = unknown, > { /** A string representation of a row. */ text: string diff --git a/packages/client-common/src/utils/connection.ts b/packages/client-common/src/utils/connection.ts index 8fe7f2a7..efef8503 100644 --- a/packages/client-common/src/utils/connection.ts +++ b/packages/client-common/src/utils/connection.ts @@ -21,7 +21,7 @@ export function withCompressionHeaders({ export function withHttpSettings( clickhouse_settings?: ClickHouseSettings, - compression?: boolean + compression?: boolean, ): ClickHouseSettings { return { ...(compression diff --git a/packages/client-common/src/utils/string.ts b/packages/client-common/src/utils/string.ts index fd61e4d0..e3bb4688 100644 --- a/packages/client-common/src/utils/string.ts +++ b/packages/client-common/src/utils/string.ts @@ -1,7 +1,7 @@ export function replaceAll( input: string, replace_char: string, - new_char: string + new_char: string, ): string { return input.split(replace_char).join(new_char) } 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 005d16c4..25f457b4 100644 --- a/packages/client-node/__tests__/integration/node_abort_request.test.ts +++ b/packages/client-node/__tests__/integration/node_abort_request.test.ts @@ -104,7 +104,7 @@ describe('[Node.js] abort request streaming', () => { }) } return insertPromise - }) + }), ) setTimeout(() => { @@ -146,7 +146,7 @@ describe('[Node.js] abort request streaming', () => { await expectAsync(insertPromise).toBeRejectedWith( jasmine.objectContaining({ message: jasmine.stringMatching('The user aborted a request'), - }) + }), ) }) @@ -158,11 +158,11 @@ describe('[Node.js] abort request streaming', () => { await client.insert({ table: tableName, values: stream, - }) + }), ).toEqual( jasmine.objectContaining({ query_id: jasmine.any(String), - }) + }), ) }) @@ -182,7 +182,7 @@ describe('[Node.js] abort request streaming', () => { await expectAsync(insertPromise).toBeRejectedWith( jasmine.objectContaining({ message: jasmine.stringMatching('The user aborted a request'), - }) + }), ) }) }) 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 b4e83bb0..7254c45e 100644 --- a/packages/client-node/__tests__/integration/node_errors_parsing.test.ts +++ b/packages/client-node/__tests__/integration/node_errors_parsing.test.ts @@ -8,11 +8,11 @@ describe('[Node.js] errors parsing', () => { await expectAsync( client.query({ query: 'SELECT * FROM system.numbers LIMIT 3', - }) + }), ).toBeRejectedWith( jasmine.objectContaining({ code: 'ECONNREFUSED', - }) + }), ) }) }) diff --git a/packages/client-node/__tests__/integration/node_insert.test.ts b/packages/client-node/__tests__/integration/node_insert.test.ts index 3f72e077..9e6b5f51 100644 --- a/packages/client-node/__tests__/integration/node_insert.test.ts +++ b/packages/client-node/__tests__/integration/node_insert.test.ts @@ -26,13 +26,13 @@ describe('[Node.js] insert', () => { objectMode: false, }), format: 'TabSeparated', - }) + }), ).toBeRejectedWith( jasmine.objectContaining({ message: jasmine.stringContaining('Cannot parse input'), code: '27', type: 'CANNOT_PARSE_INPUT_ASSERTION_FAILED', - }) + }), ) }) }) 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 004091d7..c5015a27 100644 --- a/packages/client-node/__tests__/integration/node_keep_alive.test.ts +++ b/packages/client-node/__tests__/integration/node_keep_alive.test.ts @@ -55,12 +55,12 @@ xdescribe('[Node.js] Keep Alive', () => { } as NodeClickHouseClientConfigOptions) const results = await Promise.all( - [...Array(4).keys()].map((n) => query(n)) + [...Array(4).keys()].map((n) => query(n)), ) expect(results.sort()).toEqual([1, 2, 3, 4]) await sleep(socketTTL) const results2 = await Promise.all( - [...Array(4).keys()].map((n) => query(n + 10)) + [...Array(4).keys()].map((n) => query(n + 10)), ) expect(results2.sort()).toEqual([11, 12, 13, 14]) }) diff --git a/packages/client-node/__tests__/integration/node_logger.ts b/packages/client-node/__tests__/integration/node_logger.ts index 8db13e66..0d037bea 100644 --- a/packages/client-node/__tests__/integration/node_logger.ts +++ b/packages/client-node/__tests__/integration/node_logger.ts @@ -54,7 +54,7 @@ describe('[Node.js] logger support', () => { 'transfer-encoding': 'chunked', }), response_status: 200, - }) + }), ) }) @@ -74,7 +74,7 @@ describe('[Node.js] logger support', () => { request_path: '/ping', request_method: 'GET', }), - }) + }), ) }) 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 179a6653..d229e25a 100644 --- a/packages/client-node/__tests__/integration/node_multiple_clients.test.ts +++ b/packages/client-node/__tests__/integration/node_multiple_clients.test.ts @@ -48,8 +48,8 @@ describe('[Node.js] multiple clients', () => { table: tableName, values: Stream.Readable.from([getValue(i)]), format: 'JSONEachRow', - }) - ) + }), + ), ) const result = await clients[0].query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, diff --git a/packages/client-node/__tests__/integration/node_ping.test.ts b/packages/client-node/__tests__/integration/node_ping.test.ts index 3a284b14..9207495d 100644 --- a/packages/client-node/__tests__/integration/node_ping.test.ts +++ b/packages/client-node/__tests__/integration/node_ping.test.ts @@ -17,7 +17,7 @@ describe('[Node.js] ping', () => { expect(result.error).toEqual( jasmine.objectContaining({ code: 'ECONNREFUSED', - }) + }), ) }) }) 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 f49ab7d2..31413e77 100644 --- a/packages/client-node/__tests__/integration/node_select_streaming.test.ts +++ b/packages/client-node/__tests__/integration/node_select_streaming.test.ts @@ -16,14 +16,14 @@ describe('[Node.js] SELECT streaming', () => { await expectAsync(fn()).toBeRejectedWith( jasmine.objectContaining({ message: 'Stream has been already consumed', - }) + }), ) } function assertAlreadyConsumed(fn: () => T) { expect(fn).toThrow( jasmine.objectContaining({ message: 'Stream has been already consumed', - }) + }), ) } it('should consume a JSON response only once', async () => { @@ -79,7 +79,7 @@ describe('[Node.js] SELECT streaming', () => { await expectAsync((async () => result.stream())()).toBeRejectedWith( jasmine.objectContaining({ message: jasmine.stringContaining('JSON format is not streamable'), - }) + }), ) } finally { result.close() diff --git a/packages/client-node/__tests__/integration/node_stream_json_formats.test.ts b/packages/client-node/__tests__/integration/node_stream_json_formats.test.ts index fb0ff6d2..c196e2e2 100644 --- a/packages/client-node/__tests__/integration/node_stream_json_formats.test.ts +++ b/packages/client-node/__tests__/integration/node_stream_json_formats.test.ts @@ -178,9 +178,9 @@ describe('[Node.js] stream JSON formats', () => { await expectAsync(insertPromise).toBeRejectedWith( jasmine.objectContaining({ message: jasmine.stringMatching( - `Type of 'name' must be String, not UInt64` + `Type of 'name' must be String, not UInt64`, ), - }) + }), ) }) @@ -243,7 +243,7 @@ describe('[Node.js] stream JSON formats', () => { client.insert({ table: tableName, values: stream, - }) + }), ).toBeResolved() }) @@ -281,7 +281,7 @@ describe('[Node.js] stream JSON formats', () => { format: 'JSONEachRow', table: tableName, }) - }) + }), ) setTimeout(() => { streams.forEach((stream) => stream.push(null)) @@ -299,11 +299,11 @@ describe('[Node.js] stream JSON formats', () => { table: tableName, values: stream, format: 'JSONEachRow', - }) + }), ).toBeRejectedWith( jasmine.objectContaining({ message: jasmine.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 08b0549a..8bab203f 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 @@ -27,18 +27,18 @@ describe('[Node.js] stream raw formats', () => { `"baz","foo","[1,2]"\n43,"bar","[3,4]"\n`, { objectMode: false, - } + }, ) await expectAsync( client.insert({ table: tableName, values: stream, format: 'CSV', - }) + }), ).toBeRejectedWith( jasmine.objectContaining({ message: jasmine.stringContaining('Cannot parse input'), - }) + }), ) }) @@ -104,11 +104,11 @@ describe('[Node.js] stream raw formats', () => { table: tableName, values: stream, format: 'TabSeparated', - }) + }), ).toBeRejectedWith( jasmine.objectContaining({ message: jasmine.stringContaining('Cannot parse input'), - }) + }), ) }) @@ -124,7 +124,7 @@ describe('[Node.js] stream raw formats', () => { format: 'TabSeparated', table: tableName, }) - }) + }), ) setTimeout(() => { streams.forEach((stream) => stream.push(null)) @@ -179,7 +179,7 @@ describe('[Node.js] stream raw formats', () => { `"id","name","sku" 0,"foo","[1,2]" 0,"bar","[3,4]" -` +`, ) }) @@ -201,20 +201,20 @@ describe('[Node.js] stream raw formats', () => { `"id","name","sku"\n"UInt64","UInt64","Array(UInt8)"\n42,"foo","[1,2]"\n43,"bar","[3,4]"\n`, { objectMode: false, - } + }, ) await expectAsync( client.insert({ table: tableName, values: stream, format: 'CSVWithNamesAndTypes', - }) + }), ).toBeRejectedWith( jasmine.objectContaining({ message: jasmine.stringContaining( - `Type of 'name' must be String, not UInt64` + `Type of 'name' must be String, not UInt64`, ), - }) + }), ) }) @@ -227,11 +227,11 @@ describe('[Node.js] stream raw formats', () => { table: tableName, values: stream, format: 'CSV', - }) + }), ).toBeRejectedWith( jasmine.objectContaining({ message: jasmine.stringContaining('Cannot parse input'), - }) + }), ) }) @@ -247,7 +247,7 @@ describe('[Node.js] stream raw formats', () => { format: 'CSV', table: tableName, }) - }) + }), ) setTimeout(() => { streams.forEach((stream) => stream.push(null)) @@ -291,7 +291,7 @@ describe('[Node.js] stream raw formats', () => { await assertInsertedValues( 'CustomSeparatedWithNames', values, - clickhouse_settings + clickhouse_settings, ) }) @@ -309,7 +309,7 @@ describe('[Node.js] stream raw formats', () => { await assertInsertedValues( 'CustomSeparatedWithNamesAndTypes', values, - clickhouse_settings + clickhouse_settings, ) }) @@ -323,11 +323,11 @@ describe('[Node.js] stream raw formats', () => { values: stream, format: 'CustomSeparated', clickhouse_settings, - }) + }), ).toBeRejectedWith( jasmine.objectContaining({ message: jasmine.stringContaining('Cannot parse input'), - }) + }), ) }) @@ -344,7 +344,7 @@ describe('[Node.js] stream raw formats', () => { table: tableName, clickhouse_settings, }) - }) + }), ) setTimeout(() => { streams.forEach((stream) => stream.push(null)) @@ -357,7 +357,7 @@ describe('[Node.js] stream raw formats', () => { async function assertInsertedValues( format: RawDataFormat, expected: string, - clickhouse_settings?: ClickHouseSettings + clickhouse_settings?: ClickHouseSettings, ) { const result = await client.query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, 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 f3f247a9..0f137a37 100644 --- a/packages/client-node/__tests__/integration/node_streaming_e2e.test.ts +++ b/packages/client-node/__tests__/integration/node_streaming_e2e.test.ts @@ -39,7 +39,7 @@ describe('[Node.js] streaming e2e', () => { table: tableName, values: Fs.createReadStream(filename).pipe( // should be removed when "insert" accepts a stream of strings/bytes - split((row: string) => JSON.parse(row)) + split((row: string) => JSON.parse(row)), ), format: 'JSONCompactEachRow', }) @@ -98,10 +98,10 @@ describe('[Node.js] streaming e2e', () => { } const table = tableFromIPC( - readParquet(Buffer.concat(parquetChunks)).intoIPCStream() + readParquet(Buffer.concat(parquetChunks)).intoIPCStream(), ) expect(table.schema.toString()).toEqual( - 'Schema<{ 0: id: Uint64, 1: name: Binary, 2: sku: List }>' + 'Schema<{ 0: id: Uint64, 1: name: Binary, 2: sku: List }>', ) const actualParquetData: unknown[] = [] const textDecoder = new TextDecoder() @@ -156,7 +156,7 @@ describe('[Node.js] streaming e2e', () => { }> { const table = await createTableWithFields( client as ClickHouseClient, - `sentence String, timestamp String` + `sentence String, timestamp String`, ) const values = [...new Array(rows)].map((_, id) => ({ id, diff --git a/packages/client-node/__tests__/tls/tls.test.ts b/packages/client-node/__tests__/tls/tls.test.ts index c11de522..c7b590c4 100644 --- a/packages/client-node/__tests__/tls/tls.test.ts +++ b/packages/client-node/__tests__/tls/tls.test.ts @@ -63,13 +63,13 @@ describe('[Node.js] TLS connection', () => { client.query({ query: 'SELECT number FROM system.numbers LIMIT 3', format: 'CSV', - }) + }), ).toBeRejectedWith( jasmine.objectContaining({ message: jasmine.stringContaining( - 'Hostname/IP does not match certificate' + 'Hostname/IP does not match certificate', ), - }) + }), ) }) @@ -88,7 +88,7 @@ describe('[Node.js] TLS connection', () => { client.query({ query: 'SELECT number FROM system.numbers LIMIT 3', format: 'CSV', - }) + }), ).toBeRejectedWithError() }) }) diff --git a/packages/client-node/__tests__/unit/node_client.test.ts b/packages/client-node/__tests__/unit/node_client.test.ts index fa93c5d4..f3ac85e6 100644 --- a/packages/client-node/__tests__/unit/node_client.test.ts +++ b/packages/client-node/__tests__/unit/node_client.test.ts @@ -11,7 +11,7 @@ describe('[Node.js] createClient', () => { expect(() => createClient({ url: 'foo' })).toThrow( jasmine.objectContaining({ message: jasmine.stringContaining('ClickHouse URL is malformed.'), - }) + }), ) }) @@ -77,7 +77,7 @@ describe('[Node.js] createClient', () => { enabled: true, socket_ttl: 1000, retry_on_expired_socket: true, - } + }, ) expect(createConnectionStub).toHaveBeenCalledTimes(1) }) diff --git a/packages/client-node/__tests__/unit/node_config.test.ts b/packages/client-node/__tests__/unit/node_config.test.ts index 8b5c631f..f03c606b 100644 --- a/packages/client-node/__tests__/unit/node_config.test.ts +++ b/packages/client-node/__tests__/unit/node_config.test.ts @@ -18,7 +18,7 @@ describe('[Node.js] Config implementation details', () => { [ 'keep_alive_retry_on_expired_socket=true', 'keep_alive_socket_ttl=1000', - ].join('&') + ].join('&'), ) const config: BaseClickHouseClientConfigOptions = { keep_alive: { @@ -86,7 +86,7 @@ describe('[Node.js] Config implementation details', () => { const fakeConnection = { test: true } as unknown as NodeBaseConnection beforeEach(() => { createConnectionStub = spyOn(c, 'createConnection').and.returnValue( - fakeConnection + fakeConnection, ) }) @@ -102,7 +102,7 @@ describe('[Node.js] Config implementation details', () => { enabled: true, socket_ttl: 2500, retry_on_expired_socket: false, - } + }, ) expect(createConnectionStub).toHaveBeenCalledTimes(1) expect(res).toEqual(fakeConnection) @@ -126,7 +126,7 @@ describe('[Node.js] Config implementation details', () => { enabled: true, socket_ttl: 2500, retry_on_expired_socket: false, - } + }, ) expect(createConnectionStub).toHaveBeenCalledTimes(1) expect(res).toEqual(fakeConnection) @@ -154,7 +154,7 @@ describe('[Node.js] Config implementation details', () => { enabled: true, socket_ttl: 2500, retry_on_expired_socket: false, - } + }, ) expect(createConnectionStub).toHaveBeenCalledTimes(1) expect(res).toEqual(fakeConnection) @@ -183,7 +183,7 @@ describe('[Node.js] Config implementation details', () => { enabled: false, socket_ttl: 42_000, retry_on_expired_socket: true, - } + }, ) 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 a5c219ef..b12066c4 100644 --- a/packages/client-node/__tests__/unit/node_connection.test.ts +++ b/packages/client-node/__tests__/unit/node_connection.test.ts @@ -136,7 +136,7 @@ describe('[Node.js] Connection', () => { await expectAsync(selectPromise).toBeRejectedWith( jasmine.objectContaining({ message: 'Unexpected encoding: br', - }) + }), ) }) @@ -162,7 +162,7 @@ describe('[Node.js] Connection', () => { headers: { 'content-encoding': 'gzip', }, - }) + }), ) const readStream = async () => { @@ -176,7 +176,7 @@ describe('[Node.js] Connection', () => { jasmine.objectContaining({ message: 'incorrect header check', code: 'Z_DATA_ERROR', - }) + }), ) }) }) @@ -234,7 +234,7 @@ describe('[Node.js] Connection', () => { const myHttpAdapter = new MyTestHttpAdapter() 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)\)$/ + /^clickhouse-js\/[0-9\\.]+-?(?:(alpha|beta)\d*)? \(lv:nodejs\/v[0-9\\.]+?; os:(?:linux|darwin|win32)\)$/, ) }) @@ -242,7 +242,7 @@ describe('[Node.js] Connection', () => { const myHttpAdapter = new MyTestHttpAdapter('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)\)$/ + /^MyFancyApp clickhouse-js\/[0-9\\.]+-?(?:(alpha|beta)\d*)? \(lv:nodejs\/v[0-9\\.]+?; os:(?:linux|darwin|win32)\)$/, ) }) }) @@ -459,7 +459,7 @@ describe('[Node.js] Connection', () => { }) function buildHttpAdapter( - config: Partial + config: Partial, ): NodeHttpConnection { return new NodeHttpConnection({ ...{ @@ -491,7 +491,7 @@ describe('[Node.js] Connection', () => { async function assertQueryResult( { stream, query_id }: ConnQueryResult, - expectedResponseBody: any + expectedResponseBody: any, ) { expect(await getAsText(stream)).toBe(expectedResponseBody) assertQueryId(query_id) @@ -515,7 +515,7 @@ class MyTestHttpAdapter extends NodeBaseConnection { retry_on_expired_socket: true, }, } as NodeConnectionParams, - {} as Http.Agent + {} as Http.Agent, ) } protected createClientRequest(): Http.ClientRequest { 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 2301269b..a6b053e5 100644 --- a/packages/client-node/__tests__/unit/node_result_set.test.ts +++ b/packages/client-node/__tests__/unit/node_result_set.test.ts @@ -53,7 +53,7 @@ describe('[Node.js] ResultSet', () => { const rs = new ResultSet( Stream.Readable.from([Buffer.from('{"foo":"bar"}\n')]), 'JSONEachRow', - guid() + guid(), ) const allRows: Row[] = [] for await (const rows of rs.stream()) { @@ -74,7 +74,7 @@ describe('[Node.js] ResultSet', () => { Buffer.from('{"qaz":"qux"}\n'), ]), 'JSONEachRow', - guid() + guid(), ) } }) 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 b45548af..cb55b28b 100644 --- a/packages/client-node/__tests__/unit/node_user_agent.test.ts +++ b/packages/client-node/__tests__/unit/node_user_agent.test.ts @@ -14,14 +14,14 @@ 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() 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 012f61c9..94e95a45 100644 --- a/packages/client-node/__tests__/unit/node_values_encoder.test.ts +++ b/packages/client-node/__tests__/unit/node_values_encoder.test.ts @@ -59,26 +59,26 @@ describe('[Node.js] ValuesEncoder', () => { objectFormats.forEach((format) => { expect(() => - encoder.validateInsertValues(objectModeStream, format as DataFormat) + encoder.validateInsertValues(objectModeStream, format as DataFormat), ).not.toThrow() expect(() => - encoder.validateInsertValues(rawStream, format as DataFormat) + encoder.validateInsertValues(rawStream, format as DataFormat), ).toThrow( jasmine.objectContaining({ message: jasmine.stringContaining('with enabled object mode'), - }) + }), ) }) rawFormats.forEach((format) => { expect(() => - encoder.validateInsertValues(objectModeStream, format as DataFormat) + encoder.validateInsertValues(objectModeStream, format as DataFormat), ).toThrow( jasmine.objectContaining({ message: jasmine.stringContaining('with disabled object mode'), - }) + }), ) expect(() => - encoder.validateInsertValues(rawStream, format as DataFormat) + encoder.validateInsertValues(rawStream, format as DataFormat), ).not.toThrow() }) }) @@ -91,7 +91,7 @@ describe('[Node.js] ValuesEncoder', () => { rawFormats.forEach((format) => { // should be exactly the same object (no duplicate instances) expect(encoder.encodeValues(values, format as DataFormat)).toEqual( - values + values, ) }) }) @@ -155,7 +155,7 @@ describe('[Node.js] ValuesEncoder', () => { 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' + 'Cannot encode values of type number with JSON format', ) }) }) diff --git a/packages/client-node/__tests__/utils/http_stubs.ts b/packages/client-node/__tests__/utils/http_stubs.ts index fdce42ad..76e0b7c3 100644 --- a/packages/client-node/__tests__/utils/http_stubs.ts +++ b/packages/client-node/__tests__/utils/http_stubs.ts @@ -43,20 +43,20 @@ export function stubClientRequest() { export function emitResponseBody( request: Http.ClientRequest, - body: string | Buffer | undefined + body: string | Buffer | undefined, ) { request.emit( 'response', buildIncomingMessage({ body, - }) + }), ) } export async function emitCompressedBody( request: ClientRequest, body: string | Buffer, - encoding = 'gzip' + encoding = 'gzip', ) { const compressedBody = await gzip(body) request.emit( @@ -66,6 +66,6 @@ export async function emitCompressedBody( headers: { 'content-encoding': encoding, }, - }) + }), ) } diff --git a/packages/client-node/__tests__/utils/node_client.ts b/packages/client-node/__tests__/utils/node_client.ts index cd09736a..ff536d86 100644 --- a/packages/client-node/__tests__/utils/node_client.ts +++ b/packages/client-node/__tests__/utils/node_client.ts @@ -4,7 +4,7 @@ import type { NodeClickHouseClient } from '../../src' import { type BaseClickHouseClientConfigOptions } from '../../src' export function createNodeTestClient( - config: BaseClickHouseClientConfigOptions = {} + config: BaseClickHouseClientConfigOptions = {}, ): NodeClickHouseClient { return createTestClient(config) as NodeClickHouseClient } diff --git a/packages/client-node/src/client.ts b/packages/client-node/src/client.ts index 2f6bac32..e80e459d 100644 --- a/packages/client-node/src/client.ts +++ b/packages/client-node/src/client.ts @@ -11,14 +11,14 @@ import type { ResultSet } from './result_set' export class NodeClickHouseClient extends ClickHouseClient { /** See the base implementation: {@link ClickHouseClient.query} */ query( - params: QueryParamsWithFormat + params: QueryParamsWithFormat, ): Promise> { return super.query(params) as Promise> } } export function createClient( - config?: NodeClickHouseClientConfigOptions + config?: NodeClickHouseClientConfigOptions, ): NodeClickHouseClient { return new ClickHouseClient({ impl: NodeConfigImpl, diff --git a/packages/client-node/src/config.ts b/packages/client-node/src/config.ts index b2d2ac36..6e99dd29 100644 --- a/packages/client-node/src/config.ts +++ b/packages/client-node/src/config.ts @@ -90,7 +90,7 @@ export const NodeConfigImpl: Required< }, make_connection: ( nodeConfig: NodeClickHouseClientConfigOptions, - params: ConnectionParams + params: ConnectionParams, ) => { let tls: TLSParams | undefined = undefined if (nodeConfig.tls !== undefined) { @@ -118,7 +118,7 @@ export const NodeConfigImpl: Required< make_result_set: (( stream: Stream.Readable, format: DataFormat, - query_id: string + query_id: string, ) => new ResultSet(stream, format, query_id)) as any, close_stream: async (stream) => { stream.destroy() diff --git a/packages/client-node/src/connection/create_connection.ts b/packages/client-node/src/connection/create_connection.ts index 515b1432..f10f77a4 100644 --- a/packages/client-node/src/connection/create_connection.ts +++ b/packages/client-node/src/connection/create_connection.ts @@ -9,7 +9,7 @@ import { NodeHttpsConnection } from './node_https_connection' export function createConnection( params: ConnectionParams, tls: NodeConnectionParams['tls'], - keep_alive: NodeConnectionParams['keep_alive'] + keep_alive: NodeConnectionParams['keep_alive'], ): NodeBaseConnection { switch (params.url.protocol) { case 'http:': diff --git a/packages/client-node/src/connection/node_base_connection.ts b/packages/client-node/src/connection/node_base_connection.ts index 805671c4..efcebd46 100644 --- a/packages/client-node/src/connection/node_base_connection.ts +++ b/packages/client-node/src/connection/node_base_connection.ts @@ -77,7 +77,7 @@ export abstract class NodeBaseConnection >() protected constructor( protected readonly params: NodeConnectionParams, - protected readonly agent: Http.Agent + protected readonly agent: Http.Agent, ) { this.logger = params.log_writer this.retry_expired_sockets = @@ -85,18 +85,18 @@ export abstract class NodeBaseConnection this.headers = this.buildDefaultHeaders( params.username, params.password, - params.http_headers + params.http_headers, ) } protected buildDefaultHeaders( username: string, password: string, - http_headers?: Record + http_headers?: Record, ): Http.OutgoingHttpHeaders { return { Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString( - 'base64' + 'base64', )}`, 'User-Agent': getUserAgent(this.params.application_id), ...http_headers, @@ -104,12 +104,12 @@ export abstract class NodeBaseConnection } protected abstract createClientRequest( - params: RequestParams + params: RequestParams, ): Http.ClientRequest private async request( params: RequestParams, - retryCount = 0 + retryCount = 0, ): Promise { try { return await this._request(params) @@ -140,7 +140,7 @@ export abstract class NodeBaseConnection } const onResponse = async ( - _response: Http.IncomingMessage + _response: Http.IncomingMessage, ): Promise => { this.logResponse(request, params, _response, start) @@ -318,12 +318,12 @@ export abstract class NodeBaseConnection } async query( - params: ConnBaseQueryParams + params: ConnBaseQueryParams, ): Promise> { const query_id = getQueryId(params.query_id) const clickhouse_settings = withHttpSettings( params.clickhouse_settings, - this.params.compression.decompress_response + this.params.compression.decompress_response, ) const searchParams = toSearchParams({ database: this.params.database, @@ -348,7 +348,7 @@ export abstract class NodeBaseConnection } async exec( - params: ConnBaseQueryParams + params: ConnBaseQueryParams, ): Promise> { const query_id = getQueryId(params.query_id) const searchParams = toSearchParams({ @@ -375,7 +375,7 @@ export abstract class NodeBaseConnection } async insert( - params: ConnInsertParams + params: ConnInsertParams, ): Promise { const query_id = getQueryId(params.query_id) const searchParams = toSearchParams({ @@ -410,7 +410,7 @@ export abstract class NodeBaseConnection request: Http.ClientRequest, params: RequestParams, response: Http.IncomingMessage, - startTimestamp: number + startTimestamp: number, ) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { authorization, host, ...headers } = request.getHeaders() @@ -465,7 +465,7 @@ export abstract class NodeBaseConnection } private parseSummary( - response: Http.IncomingMessage + response: Http.IncomingMessage, ): ClickHouseSummary | undefined { const summaryHeader = response.headers['x-clickhouse-summary'] if (typeof summaryHeader === 'string') { @@ -500,7 +500,7 @@ function decompressResponse(response: Http.IncomingMessage): // eslint-disable-next-line no-console console.error(err) } - } + }, ), } } else if (encoding !== undefined) { diff --git a/packages/client-node/src/connection/node_https_connection.ts b/packages/client-node/src/connection/node_https_connection.ts index 01172be1..6a521672 100644 --- a/packages/client-node/src/connection/node_https_connection.ts +++ b/packages/client-node/src/connection/node_https_connection.ts @@ -22,7 +22,7 @@ export class NodeHttpsConnection extends NodeBaseConnection { protected override buildDefaultHeaders( username: string, password: string, - additional_headers?: Record + additional_headers?: Record, ): Http.OutgoingHttpHeaders { if (this.params.tls?.type === 'Mutual') { return { diff --git a/packages/client-node/src/result_set.ts b/packages/client-node/src/result_set.ts index 6613feee..a9962842 100644 --- a/packages/client-node/src/result_set.ts +++ b/packages/client-node/src/result_set.ts @@ -29,7 +29,7 @@ export type StreamReadable = Omit & { on(event: 'unpipe', listener: (src: Readable) => void): Stream.Readable on( event: string | symbol, - listener: (...args: any[]) => void + listener: (...args: any[]) => void, ): Stream.Readable } @@ -39,7 +39,7 @@ export class ResultSet constructor( private _stream: Stream.Readable, private readonly format: Format, - public readonly query_id: string + public readonly query_id: string, ) {} /** See {@link BaseResultSet.text} */ @@ -76,7 +76,7 @@ export class ResultSet transform( chunk: Buffer, _encoding: BufferEncoding, - callback: TransformCallback + callback: TransformCallback, ) { const rows: Row[] = [] let lastIdx = 0 @@ -88,7 +88,7 @@ export class ResultSet if (incompleteChunks.length > 0) { text = Buffer.concat( [...incompleteChunks, chunk.subarray(0, idx)], - incompleteChunks.reduce((sz, buf) => sz + buf.length, 0) + idx + incompleteChunks.reduce((sz, buf) => sz + buf.length, 0) + idx, ).toString() incompleteChunks = [] } else { @@ -138,7 +138,7 @@ export class ResultSet // eslint-disable-next-line no-console console.error(err) } - } + }, ) return pipeline as any } diff --git a/packages/client-node/src/utils/encoder.ts b/packages/client-node/src/utils/encoder.ts index 9b4d4c0e..fadc60c5 100644 --- a/packages/client-node/src/utils/encoder.ts +++ b/packages/client-node/src/utils/encoder.ts @@ -10,7 +10,7 @@ import { isStream, mapStream } from './stream' export class NodeValuesEncoder implements ValuesEncoder { encodeValues( values: InsertValues, - format: DataFormat + format: DataFormat, ): string | Stream.Readable { if (isStream(values)) { // TSV/CSV/CustomSeparated formats don't require additional serialization @@ -21,7 +21,7 @@ export class NodeValuesEncoder implements ValuesEncoder { return Stream.pipeline( values, mapStream((value) => encodeJSON(value, format)), - pipelineCb + pipelineCb, ) } // JSON* arrays @@ -33,13 +33,13 @@ export class NodeValuesEncoder implements ValuesEncoder { return encodeJSON(values, format) } throw new Error( - `Cannot encode values of type ${typeof values} with ${format} format` + `Cannot encode values of type ${typeof values} with ${format} format`, ) } validateInsertValues( values: InsertValues, - format: DataFormat + format: DataFormat, ): void { if ( !Array.isArray(values) && @@ -48,7 +48,7 @@ export class NodeValuesEncoder implements ValuesEncoder { ) { throw new Error( 'Insert expected "values" to be an array, a stream of values or a JSON object, ' + - `got: ${typeof values}` + `got: ${typeof values}`, ) } @@ -56,12 +56,12 @@ export class NodeValuesEncoder implements ValuesEncoder { if (isSupportedRawFormat(format)) { if (values.readableObjectMode) { throw new Error( - `Insert for ${format} expected Readable Stream with disabled object mode.` + `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.` + `Insert for ${format} expected Readable Stream with enabled object mode.`, ) } } diff --git a/packages/client-node/src/utils/stream.ts b/packages/client-node/src/utils/stream.ts index 65dcb552..279769e3 100644 --- a/packages/client-node/src/utils/stream.ts +++ b/packages/client-node/src/utils/stream.ts @@ -18,7 +18,7 @@ export async function getAsText(stream: Stream.Readable): Promise { } export function mapStream( - mapper: (input: unknown) => string + mapper: (input: unknown) => string, ): Stream.Transform { return new Stream.Transform({ objectMode: true, 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 66d8cba5..ceeb5bcd 100644 --- a/packages/client-web/__tests__/integration/web_abort_request.test.ts +++ b/packages/client-web/__tests__/integration/web_abort_request.test.ts @@ -39,7 +39,7 @@ describe('[Web] abort request streaming', () => { jasmine.objectContaining({ // Chrome = The user aborted a request; FF = The operation was aborted message: jasmine.stringContaining('aborted'), - }) + }), ) }) @@ -67,7 +67,7 @@ describe('[Web] abort request streaming', () => { await expectAsync(selectPromise).toBeRejectedWith( jasmine.objectContaining({ message: jasmine.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 8ee83429..20dddd19 100644 --- a/packages/client-web/__tests__/integration/web_client.test.ts +++ b/packages/client-web/__tests__/integration/web_client.test.ts @@ -5,7 +5,7 @@ describe('[Web] Client', () => { let fetchSpy: jasmine.Spy beforeEach(() => { fetchSpy = spyOn(window, 'fetch').and.returnValue( - Promise.resolve(new Response()) + Promise.resolve(new Response()), ) }) 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 3f16761a..c79d1ed8 100644 --- a/packages/client-web/__tests__/integration/web_error_parsing.test.ts +++ b/packages/client-web/__tests__/integration/web_error_parsing.test.ts @@ -8,12 +8,12 @@ describe('[Web] errors parsing', () => { await expectAsync( client.query({ query: 'SELECT * FROM system.numbers LIMIT 3', - }) + }), ).toBeRejectedWith( // Chrome = Failed to fetch; FF = NetworkError when attempting to fetch resource jasmine.objectContaining({ message: jasmine.stringContaining('to fetch'), - }) + }), ) }) }) diff --git a/packages/client-web/__tests__/integration/web_ping.test.ts b/packages/client-web/__tests__/integration/web_ping.test.ts index 567cc8ea..0d172a01 100644 --- a/packages/client-web/__tests__/integration/web_ping.test.ts +++ b/packages/client-web/__tests__/integration/web_ping.test.ts @@ -18,7 +18,7 @@ describe('[Web] ping', () => { // Chrome = Failed to fetch; FF = NetworkError when attempting to fetch resource jasmine.objectContaining({ message: jasmine.stringContaining('to fetch'), - }) + }), ) }) }) 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 63723d97..f3e02830 100644 --- a/packages/client-web/__tests__/integration/web_select_streaming.test.ts +++ b/packages/client-web/__tests__/integration/web_select_streaming.test.ts @@ -15,14 +15,14 @@ describe('[Web] SELECT streaming', () => { await expectAsync(fn()).toBeRejectedWith( jasmine.objectContaining({ message: 'Stream has been already consumed', - }) + }), ) } function assertAlreadyConsumed(fn: () => T) { expect(fn).toThrow( jasmine.objectContaining({ message: 'Stream has been already consumed', - }) + }), ) } it('should consume a JSON response only once', async () => { @@ -73,7 +73,7 @@ describe('[Web] SELECT streaming', () => { expect(() => result.stream()).toThrow( jasmine.objectContaining({ message: jasmine.stringContaining('JSON format is not streamable'), - }) + }), ) }) }) @@ -202,7 +202,7 @@ describe('[Web] SELECT streaming', () => { }) async function rowsJsonValues( - stream: ReadableStream + stream: ReadableStream, ): Promise { const result: T[] = [] const reader = stream.getReader() diff --git a/packages/client-web/__tests__/unit/web_client.test.ts b/packages/client-web/__tests__/unit/web_client.test.ts index 4b894905..01a07448 100644 --- a/packages/client-web/__tests__/unit/web_client.test.ts +++ b/packages/client-web/__tests__/unit/web_client.test.ts @@ -6,7 +6,7 @@ describe('[Web] createClient', () => { expect(() => createClient({ url: 'foo' })).toThrow( jasmine.objectContaining({ message: jasmine.stringContaining('ClickHouse URL is malformed.'), - }) + }), ) }) 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 bba01f1e..2c9918eb 100644 --- a/packages/client-web/__tests__/unit/web_result_set.test.ts +++ b/packages/client-web/__tests__/unit/web_result_set.test.ts @@ -56,7 +56,7 @@ describe('[Web] ResultSet', () => { }, }), 'JSONEachRow', - guid() + guid(), ) const allRows: Row[] = [] @@ -86,7 +86,7 @@ describe('[Web] ResultSet', () => { }, }), 'JSONEachRow', - guid() + guid(), ) } }) diff --git a/packages/client-web/src/client.ts b/packages/client-web/src/client.ts index d03aac77..89b67f7e 100644 --- a/packages/client-web/src/client.ts +++ b/packages/client-web/src/client.ts @@ -20,21 +20,21 @@ export type WebClickHouseClient = Omit & { insert( params: Omit, 'values'> & { values: ReadonlyArray | InputJSON | InputJSONObjectEachRow - } + }, ): Promise } class WebClickHouseClientImpl extends ClickHouseClient { /** See the base implementation: {@link ClickHouseClient.query} */ query( - params: QueryParamsWithFormat + params: QueryParamsWithFormat, ): Promise> { return super.query(params) as Promise> } } export function createClient( - config?: WebClickHouseClientConfigOptions + config?: WebClickHouseClientConfigOptions, ): WebClickHouseClient { return new WebClickHouseClientImpl({ impl: WebImpl, diff --git a/packages/client-web/src/config.ts b/packages/client-web/src/config.ts index f149e6d4..af8b7360 100644 --- a/packages/client-web/src/config.ts +++ b/packages/client-web/src/config.ts @@ -15,7 +15,7 @@ export const WebImpl: ImplementationDetails['impl'] = { make_result_set: (( stream: ReadableStream, format: DataFormat, - query_id: string + query_id: string, ) => new ResultSet(stream, format, query_id)) as any, values_encoder: new WebValuesEncoder(), close_stream: (stream) => stream.cancel(), diff --git a/packages/client-web/src/connection/web_connection.ts b/packages/client-web/src/connection/web_connection.ts index 9e4167da..9d0e3f7a 100644 --- a/packages/client-web/src/connection/web_connection.ts +++ b/packages/client-web/src/connection/web_connection.ts @@ -36,12 +36,12 @@ export class WebConnection implements Connection { } async query( - params: ConnBaseQueryParams + params: ConnBaseQueryParams, ): Promise>> { const query_id = getQueryId(params.query_id) const clickhouse_settings = withHttpSettings( params.clickhouse_settings, - this.params.compression.decompress_response + this.params.compression.decompress_response, ) const searchParams = toSearchParams({ database: this.params.database, @@ -62,7 +62,7 @@ export class WebConnection implements Connection { } async exec( - params: ConnBaseQueryParams + params: ConnBaseQueryParams, ): Promise>> { const query_id = getQueryId(params.query_id) const searchParams = toSearchParams({ @@ -84,7 +84,7 @@ export class WebConnection implements Connection { } async insert( - params: WebInsertParams + params: WebInsertParams, ): Promise { const query_id = getQueryId(params.query_id) const searchParams = toSearchParams({ @@ -188,8 +188,8 @@ export class WebConnection implements Connection { } else { return Promise.reject( parseError( - await getAsText(response.body || new ReadableStream()) - ) + await getAsText(response.body || new ReadableStream()), + ), ) } } catch (err) { diff --git a/packages/client-web/src/result_set.ts b/packages/client-web/src/result_set.ts index e7ee3ec3..5b51d529 100644 --- a/packages/client-web/src/result_set.ts +++ b/packages/client-web/src/result_set.ts @@ -15,7 +15,7 @@ export class ResultSet constructor( private _stream: ReadableStream, private readonly format: Format, - public readonly query_id: string + public readonly query_id: string, ) {} /** See {@link BaseResultSet.text} */ diff --git a/packages/client-web/src/utils/encoder.ts b/packages/client-web/src/utils/encoder.ts index e31ca535..0f7f6ea7 100644 --- a/packages/client-web/src/utils/encoder.ts +++ b/packages/client-web/src/utils/encoder.ts @@ -9,7 +9,7 @@ import { isStream } from './stream' export class WebValuesEncoder implements ValuesEncoder { encodeValues( values: InsertValues, - format: DataFormat + format: DataFormat, ): string | ReadableStream { throwIfStream(values) // JSON* arrays @@ -21,7 +21,7 @@ export class WebValuesEncoder implements ValuesEncoder { return encodeJSON(values, format) } throw new Error( - `Cannot encode values of type ${typeof values} with ${format} format` + `Cannot encode values of type ${typeof values} with ${format} format`, ) } @@ -30,7 +30,7 @@ export class WebValuesEncoder implements ValuesEncoder { if (!Array.isArray(values) && typeof values !== 'object') { throw new Error( 'Insert expected "values" to be an array or a JSON object, ' + - `got: ${typeof values}` + `got: ${typeof values}`, ) } } @@ -39,7 +39,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', ) } } From 98e4e7cbc640bac1e54c95168775c919ad42173a Mon Sep 17 00:00:00 2001 From: slvrtrn Date: Mon, 25 Mar 2024 19:54:43 +0100 Subject: [PATCH 10/16] Fix lint --- examples/async_insert.ts | 2 +- examples/async_insert_without_waiting.ts | 2 +- .../client-node/__tests__/integration/node_keep_alive.test.ts | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/async_insert.ts b/examples/async_insert.ts index 6307a825..7fc8f7ef 100644 --- a/examples/async_insert.ts +++ b/examples/async_insert.ts @@ -81,7 +81,7 @@ void (async () => { query: `SELECT count(*) AS count FROM ${table}`, format: 'JSONEachRow', }) - const [{ count }] = await resultSet.json<[{ count: string }]>() + const [{ count }] = await resultSet.json<{ count: string }>() // It is expected to have 10k records in the table. console.info('Select count result:', count) })() diff --git a/examples/async_insert_without_waiting.ts b/examples/async_insert_without_waiting.ts index e841d291..9f656795 100644 --- a/examples/async_insert_without_waiting.ts +++ b/examples/async_insert_without_waiting.ts @@ -97,7 +97,7 @@ void (async () => { query: `SELECT count(*) AS count FROM ${tableName}`, format: 'JSONEachRow', }) - const [{ count }] = await resultSet.json<[{ count: string }]>() + const [{ count }] = await resultSet.json<{ count: string }>() console.log( 'Rows inserted so far:', `${rowsInserted};`, 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 7c41bcf5..466a8297 100644 --- a/packages/client-node/__tests__/integration/node_keep_alive.test.ts +++ b/packages/client-node/__tests__/integration/node_keep_alive.test.ts @@ -1,3 +1,4 @@ +import { ClickHouseLogLevel } from '@clickhouse/client-common' import { createSimpleTable } from '@test/fixtures/simple_table' import { guid, sleep } from '@test/utils' import type { NodeClickHouseClient } from '../../src' From 317226d2b12078260227b748ef2f09e0fa871109 Mon Sep 17 00:00:00 2001 From: slvrtrn Date: Mon, 25 Mar 2024 20:01:37 +0100 Subject: [PATCH 11/16] Fix package exports --- .../__tests__/integration/node_client.test.ts | 4 ++-- .../__tests__/integration/node_keep_alive.test.ts | 4 ++-- .../integration/node_max_open_connections.test.ts | 4 ++-- packages/client-node/__tests__/utils/node_client.ts | 10 ++++++---- packages/client-node/src/index.ts | 7 +++---- packages/client-web/src/index.ts | 5 ++--- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/client-node/__tests__/integration/node_client.test.ts b/packages/client-node/__tests__/integration/node_client.test.ts index f61e42be..18e0281d 100644 --- a/packages/client-node/__tests__/integration/node_client.test.ts +++ b/packages/client-node/__tests__/integration/node_client.test.ts @@ -1,5 +1,5 @@ import Http from 'http' -import type { NodeClickHouseClient } from '../../src' +import type { ClickHouseClient } from '../../src' import { createClient } from '../../src' import { emitResponseBody, stubClientRequest } from '../utils/http_stubs' @@ -110,7 +110,7 @@ describe('[Node.js] Client', () => { }) }) - async function query(client: NodeClickHouseClient) { + async function query(client: ClickHouseClient) { const selectPromise = client.query({ query: 'SELECT * FROM system.numbers LIMIT 5', }) 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 466a8297..8a47247b 100644 --- a/packages/client-node/__tests__/integration/node_keep_alive.test.ts +++ b/packages/client-node/__tests__/integration/node_keep_alive.test.ts @@ -1,7 +1,7 @@ import { ClickHouseLogLevel } from '@clickhouse/client-common' import { createSimpleTable } from '@test/fixtures/simple_table' import { guid, sleep } from '@test/utils' -import type { NodeClickHouseClient } from '../../src' +import type { ClickHouseClient } from '../../src' import type { NodeClickHouseClientConfigOptions } from '../../src/config' import { createNodeTestClient } from '../utils/node_client' @@ -11,7 +11,7 @@ import { createNodeTestClient } from '../utils/node_client' * To be revisited in https://github.com/ClickHouse/clickhouse-js/issues/177 */ xdescribe('[Node.js] Keep Alive', () => { - let client: NodeClickHouseClient + 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() 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 cf336099..71276697 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,10 +1,10 @@ import { createSimpleTable } from '@test/fixtures/simple_table' import { guid, sleep } from '@test/utils' -import type { NodeClickHouseClient } from '../../src' +import type { ClickHouseClient } from '../../src' import { createNodeTestClient } from '../utils/node_client' describe('[Node.js] max_open_connections config', () => { - let client: NodeClickHouseClient + let client: ClickHouseClient let results: number[] = [] afterEach(async () => { diff --git a/packages/client-node/__tests__/utils/node_client.ts b/packages/client-node/__tests__/utils/node_client.ts index ff536d86..ea383c7f 100644 --- a/packages/client-node/__tests__/utils/node_client.ts +++ b/packages/client-node/__tests__/utils/node_client.ts @@ -1,10 +1,12 @@ import { createTestClient } from '@test/utils' import type Stream from 'stream' -import type { NodeClickHouseClient } from '../../src' -import { type BaseClickHouseClientConfigOptions } from '../../src' +import type { + BaseClickHouseClientConfigOptions, + ClickHouseClient, +} from '../../src' export function createNodeTestClient( config: BaseClickHouseClientConfigOptions = {}, -): NodeClickHouseClient { - return createTestClient(config) as NodeClickHouseClient +): ClickHouseClient { + return createTestClient(config) as ClickHouseClient } diff --git a/packages/client-node/src/index.ts b/packages/client-node/src/index.ts index 88bf6ee3..d7aca42d 100644 --- a/packages/client-node/src/index.ts +++ b/packages/client-node/src/index.ts @@ -1,7 +1,7 @@ -export type { NodeClickHouseClient } from './client' +export type { NodeClickHouseClient as ClickHouseClient } from './client' export { createClient } from './client' -export { NodeClickHouseClientConfigOptions as ClickHouseClientConfigOptions } from './config' -export { ResultSet, StreamReadable } from './result_set' +export { type NodeClickHouseClientConfigOptions as ClickHouseClientConfigOptions } from './config' +export { ResultSet, type StreamReadable } from './result_set' /** Re-export @clickhouse/client-common types */ export { @@ -28,7 +28,6 @@ export { type InputJSONObjectEachRow, type BaseResultSet, type PingResult, - ClickHouseClient, ClickHouseError, ClickHouseLogLevel, SettingsMap, diff --git a/packages/client-web/src/index.ts b/packages/client-web/src/index.ts index 8fc23e5d..342d1902 100644 --- a/packages/client-web/src/index.ts +++ b/packages/client-web/src/index.ts @@ -1,6 +1,6 @@ -export type { WebClickHouseClient } from './client' +export type { WebClickHouseClient as ClickHouseClient } from './client' export { createClient } from './client' -export { WebClickHouseClientConfigOptions as ClickHouseClientConfigOptions } from './config' +export { type WebClickHouseClientConfigOptions as ClickHouseClientConfigOptions } from './config' export { ResultSet } from './result_set' /** Re-export @clickhouse/client-common types */ @@ -28,7 +28,6 @@ export { type InputJSONObjectEachRow, type BaseResultSet, type PingResult, - ClickHouseClient, ClickHouseError, ClickHouseLogLevel, SettingsMap, From 9ab5ab63b7ce03ad3ee3b56f2394654c6b295185 Mon Sep 17 00:00:00 2001 From: slvrtrn Date: Mon, 25 Mar 2024 21:36:20 +0100 Subject: [PATCH 12/16] Add expect-type tests --- .eslintrc.json | 3 +- .scripts/jasmine.sh | 2 +- package.json | 1 + .../__tests__/fixtures/table_with_fields.ts | 3 +- .../node_query_format_types.test.ts | 318 ++++++++++++++++++ .../__tests__/utils/node_client.ts | 7 +- packages/client-node/src/client.ts | 2 +- packages/client-node/src/result_set.ts | 9 +- packages/client-web/src/client.ts | 9 +- 9 files changed, 336 insertions(+), 18 deletions(-) create mode 100644 packages/client-node/__tests__/integration/node_query_format_types.test.ts diff --git a/.eslintrc.json b/.eslintrc.json index c3fd0ecd..9a0ec5ee 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -8,11 +8,12 @@ "env": { "node": true }, - "plugins": ["@typescript-eslint", "prettier"], + "plugins": ["@typescript-eslint", "prettier", "eslint-plugin-expect-type"], "extends": [ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", + "plugin:expect-type/recommended", "prettier" ], "rules": { diff --git a/.scripts/jasmine.sh b/.scripts/jasmine.sh index dca0989e..1bbf3b29 100755 --- a/.scripts/jasmine.sh +++ b/.scripts/jasmine.sh @@ -1,2 +1,2 @@ #!/bin/bash -ts-node -r tsconfig-paths/register --project=tsconfig.dev.json node_modules/jasmine/bin/jasmine --config=$1 +ts-node -r tsconfig-paths/register --transpileOnly --project=tsconfig.dev.json node_modules/jasmine/bin/jasmine --config=$1 diff --git a/package.json b/package.json index 9f8c4178..f59c750f 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "apache-arrow": "^15.0.2", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", + "eslint-plugin-expect-type": "^0.3.0", "eslint-plugin-prettier": "^5.1.3", "husky": "^9.0.11", "jasmine": "^5.1.0", diff --git a/packages/client-common/__tests__/fixtures/table_with_fields.ts b/packages/client-common/__tests__/fixtures/table_with_fields.ts index d19054cd..40a7f823 100644 --- a/packages/client-common/__tests__/fixtures/table_with_fields.ts +++ b/packages/client-common/__tests__/fixtures/table_with_fields.ts @@ -8,8 +8,9 @@ export async function createTableWithFields( client: ClickHouseClient, fields: string, clickhouse_settings?: ClickHouseSettings, + table_name?: string, ): Promise { - const tableName = `test_table__${guid()}` + const tableName = table_name ?? `test_table__${guid()}` await createTable( client, (env) => { 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 new file mode 100644 index 00000000..45972862 --- /dev/null +++ b/packages/client-node/__tests__/integration/node_query_format_types.test.ts @@ -0,0 +1,318 @@ +import type { + ClickHouseClient as BaseClickHouseClient, + DataFormat, +} from '@clickhouse/client-common' +import { createTableWithFields } from '@test/fixtures/table_with_fields' +import { guid } from '@test/utils' +import type { ClickHouseClient } from '../../src' +import { createNodeTestClient } from '../utils/node_client' + +// Ignored and used only as a source for ESLint checks with $ExpectType +// See also: https://www.npmjs.com/package/eslint-plugin-expect-type +xdescribe('[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() + await createTableWithFields( + client as BaseClickHouseClient, + '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] }, + ], + format: 'JSONEachRow', + }) + }) + afterAll(async () => { + await client.close() + }) + + describe('Streamable JSON formats', () => { + it('should infer types for JSONEachRow', async () => { + // $ExpectType ResultSet<"JSONEachRow"> + const rs = await client.query({ + query, + format: 'JSONEachRow', + }) + // $ExpectType unknown[] + await rs.json() + // $ExpectType Data[] + await rs.json() + // $ExpectType string + await rs.text() + // $ExpectType StreamReadable[]> + const stream = rs.stream() + + // stream + on('data') + await new Promise((resolve, reject) => { + stream + .on( + 'data', + // $ExpectType (rows: Row[]) => void + (rows) => { + rows.forEach( + // $ExpectType (row: Row) => void + (row) => { + // $ExpectType unknown + row.json() + // $ExpectType Data + row.json() + // $ExpectType string + row.text + }, + ) + }, + ) + .on('end', resolve) + .on('error', reject) + }) + + // stream + async iterator + for await (const rows of stream) { + rows.forEach((row) => { + // $ExpectType unknown + row.json() + // $ExpectType Data + row.json() + // $ExpectType string + row.text + }) + } + }) + + /** + * TODO: the rest of the streamable JSON formats + * 'JSONStringsEachRow', + * 'JSONCompactEachRow', + * 'JSONCompactStringsEachRow', + * 'JSONCompactEachRowWithNames', + * 'JSONCompactEachRowWithNamesAndTypes', + * 'JSONCompactStringsEachRowWithNames', + * 'JSONCompactStringsEachRowWithNamesAndTypes' + */ + }) + + 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() + // $ExpectType ResponseJSON + await rs.json() + // $ExpectType string + await rs.text() + // $ExpectType never + rs.stream() + }) + + it('should infer types for JSON', async () => { + // $ExpectType ResultSet<"JSON"> + const rs = await client.query({ + query, + format: 'JSON', + }) + // $ExpectType ResponseJSON + await rs.json() + // $ExpectType ResponseJSON + await rs.json() + // $ExpectType string + await rs.text() + // $ExpectType never + rs.stream() + }) + + it('should infer types for JSONObjectEachRow', async () => { + // $ExpectType ResultSet<"JSONObjectEachRow"> + const rs = await client.query({ + query, + format: 'JSONObjectEachRow', + }) + // $ExpectType Record + await rs.json() + // $ExpectType Record + await rs.json() + // $ExpectType string + await rs.text() + // $ExpectType never + rs.stream() + }) + + /** + * TODO: the rest of the single document JSON formats + * 'JSONStrings', + * 'JSONCompact', + * 'JSONCompactStrings', + * 'JSONColumnsWithMetadata', + */ + }) + + describe('Raw formats', () => { + it('should infer types for CSV', async () => { + // $ExpectType ResultSet<"CSV"> + const rs = await client.query({ + query, + format: 'CSV', + }) + // $ExpectType never + await rs.json() + // $ExpectType never + await rs.json() + // $ExpectType string + await rs.text() + // $ExpectType StreamReadable[]> + const stream = rs.stream() + + // stream + on('data') + await new Promise((resolve, reject) => { + stream + .on( + 'data', + // $ExpectType (rows: Row[]) => void + (rows) => { + rows.forEach( + // $ExpectType (row: Row) => void + (row) => { + // $ExpectType never + row.json() + // $ExpectType never + row.json() + // $ExpectType string + row.text + }, + ) + }, + ) + .on('end', resolve) + .on('error', reject) + }) + + // stream + async iterator + for await (const rows of stream) { + rows.forEach( + // $ExpectType (row: Row) => void + (row) => { + // $ExpectType never + row.json() + // $ExpectType never + row.json() + // $ExpectType string + row.text + }, + ) + } + }) + + /** + * TODO: the rest of the raw formats + * 'CSVWithNames', + * 'CSVWithNamesAndTypes', + * 'TabSeparated', + * 'TabSeparatedRaw', + * 'TabSeparatedWithNames', + * 'TabSeparatedWithNamesAndTypes', + * 'CustomSeparated', + * 'CustomSeparatedWithNames', + * 'CustomSeparatedWithNamesAndTypes', + * 'Parquet', + */ + }) + + describe('Type inference with ambiguous format variants', () => { + // FIXME: 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 () => { + function runQuery(format: 'JSON' | 'JSONEachRow') { + return client.query({ + query, + format, + }) + } + // $ExpectType ResultSet<"JSONEachRow" | "JSON"> + const rs = await runQuery('JSON') + // $ExpectType unknown[] | ResponseJSON + await rs.json() + // $ExpectType Data[] | ResponseJSON + await rs.json() + // $ExpectType string + await rs.text() + // $ExpectType StreamReadable[]> + rs.stream() + }) + + it('should infer types for JSON or JSONEachRow (with extra type parameter)', async () => { + function runQuery(format: F) { + return client.query({ + query, + format, + }) + } + // $ExpectType ResultSet<"JSON"> + const rs = await runQuery('JSON') + // $ExpectType ResponseJSON + await rs.json() + // $ExpectType ResponseJSON + await rs.json() + // $ExpectType string + await rs.text() + // $ExpectType never + rs.stream() + + // $ExpectType ResultSet<"JSONEachRow"> + const rs2 = await runQuery('JSONEachRow') + // $ExpectType unknown[] + await rs2.json() + // $ExpectType Data[] + await rs2.json() + // $ExpectType string + await rs2.text() + // $ExpectType StreamReadable[]> + rs2.stream() + }) + + 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 DataFormat usage) + function runQuery(format: DataFormat) { + return client.query({ + query, + format, + }) + } + + // ResultSet falls back to all possible formats (ResultSet type prints all possible formats) + // $ExpectType ResultSet<"JSONEachRow" | "JSONStringsEachRow" | "JSONCompactEachRow" | "JSONCompactStringsEachRow" | "JSONCompactEachRowWithNames" | "JSONCompactEachRowWithNamesAndTypes" | "JSONCompactStringsEachRowWithNames" | "JSONCompactStringsEachRowWithNamesAndTypes" | "JSON" | "JSONStrings" | "JSONCompact" | "JSONCompactStrings" | "JSONColumnsWithMetadata" | "JSONObjectEachRow" | "CSV" | "CSVWithNames" | "CSVWithNamesAndTypes" | "TabSeparated" | "TabSeparatedRaw" | "TabSeparatedWithNames" | "TabSeparatedWithNamesAndTypes" | "CustomSeparated" | "CustomSeparatedWithNames" | "CustomSeparatedWithNamesAndTypes" | "Parquet"> + const rs = await runQuery('JSON') + + // All possible JSON variants are now allowed + // $ExpectType unknown[] | ResponseJSON | Record + await rs.json() + // $ExpectType Data[] | ResponseJSON | Record + await rs.json() + // $ExpectType string + await rs.text() + // Stream is still allowed (can't be inferred, so it is not "never") + const stream = rs.stream() + for await (const rows of stream) { + rows.forEach((row) => { + // $ExpectType unknown + row.json() + // $ExpectType Data + row.json() + // $ExpectType string + row.text + }) + } + }) + }) +}) + +type Data = { id: number; name: string; sku: number[] } diff --git a/packages/client-node/__tests__/utils/node_client.ts b/packages/client-node/__tests__/utils/node_client.ts index ea383c7f..293b46cf 100644 --- a/packages/client-node/__tests__/utils/node_client.ts +++ b/packages/client-node/__tests__/utils/node_client.ts @@ -1,12 +1,9 @@ import { createTestClient } from '@test/utils' import type Stream from 'stream' -import type { - BaseClickHouseClientConfigOptions, - ClickHouseClient, -} from '../../src' +import type { ClickHouseClient, ClickHouseClientConfigOptions } from '../../src' export function createNodeTestClient( - config: BaseClickHouseClientConfigOptions = {}, + config: ClickHouseClientConfigOptions = {}, ): ClickHouseClient { return createTestClient(config) as ClickHouseClient } diff --git a/packages/client-node/src/client.ts b/packages/client-node/src/client.ts index e80e459d..6f73bf14 100644 --- a/packages/client-node/src/client.ts +++ b/packages/client-node/src/client.ts @@ -9,7 +9,7 @@ import { NodeConfigImpl } from './config' import type { ResultSet } from './result_set' export class NodeClickHouseClient extends ClickHouseClient { - /** See the base implementation: {@link ClickHouseClient.query} */ + /** See {@link ClickHouseClient.query}. */ query( params: QueryParamsWithFormat, ): Promise> { diff --git a/packages/client-node/src/result_set.ts b/packages/client-node/src/result_set.ts index a9962842..b28f8bc0 100644 --- a/packages/client-node/src/result_set.ts +++ b/packages/client-node/src/result_set.ts @@ -13,9 +13,10 @@ import { getAsText } from './utils' const NEWLINE = 0x0a as const -/** {@link Stream.Readable} with additional types for the `on(data)` method. +/** {@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](): AsyncIterableIterator on(event: 'data', listener: (chunk: T) => void): Stream.Readable on(event: 'close', listener: () => void): Stream.Readable on(event: 'drain', listener: () => void): Stream.Readable @@ -42,7 +43,7 @@ export class ResultSet public readonly query_id: string, ) {} - /** See {@link BaseResultSet.text} */ + /** See {@link BaseResultSet.text}. */ async text(): Promise { if (this._stream.readableEnded) { throw Error(streamAlreadyConsumedMessage) @@ -50,7 +51,7 @@ export class ResultSet return (await getAsText(this._stream)).toString() } - /** See {@link BaseResultSet.json} */ + /** See {@link BaseResultSet.json}. */ async json(): Promise> { if (this._stream.readableEnded) { throw Error(streamAlreadyConsumedMessage) @@ -58,7 +59,7 @@ export class ResultSet return decode(await this.text(), this.format) } - /** See {@link BaseResultSet.stream} */ + /** See {@link BaseResultSet.stream}. */ stream(): Format extends StreamableDataFormat ? StreamReadable[]> : never { diff --git a/packages/client-web/src/client.ts b/packages/client-web/src/client.ts index 89b67f7e..8954e2f2 100644 --- a/packages/client-web/src/client.ts +++ b/packages/client-web/src/client.ts @@ -12,11 +12,10 @@ import { WebImpl } from './config' import type { ResultSet } from './result_set' export type WebClickHouseClient = Omit & { - /** - * See the base implementation: {@link ClickHouseClient.insert} + /** See {@link ClickHouseClient.insert}. * - * ReadableStream is removed from possible insert values - * until it is supported by all major web platforms */ + * ReadableStream is removed from possible insert values + * until it is supported by all major web platforms. */ insert( params: Omit, 'values'> & { values: ReadonlyArray | InputJSON | InputJSONObjectEachRow @@ -25,7 +24,7 @@ export type WebClickHouseClient = Omit & { } class WebClickHouseClientImpl extends ClickHouseClient { - /** See the base implementation: {@link ClickHouseClient.query} */ + /** See {@link ClickHouseClient.query}. */ query( params: QueryParamsWithFormat, ): Promise> { From 3d9227adcef5dc66418cf4b61447fa5ece77d55a Mon Sep 17 00:00:00 2001 From: slvrtrn Date: Mon, 25 Mar 2024 21:46:39 +0100 Subject: [PATCH 13/16] Fix ESLint assertion --- package.json | 2 +- .../node_query_format_types.test.ts | 21 +++++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index f59c750f..4174a092 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "lint-staged": { "*.ts": [ "prettier --write", - "eslint --fix" + "npm run lint:fix" ], "*.json": [ "prettier --write" 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 45972862..7601ff3d 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 @@ -77,14 +77,17 @@ xdescribe('[Node.js] Query and ResultSet types', () => { // stream + async iterator for await (const rows of stream) { - rows.forEach((row) => { - // $ExpectType unknown - row.json() - // $ExpectType Data - row.json() - // $ExpectType string - row.text - }) + rows.forEach( + // $ExpectType (row: Row) => void + (row) => { + // $ExpectType unknown + row.json() + // $ExpectType Data + row.json() + // $ExpectType string + row.text + }, + ) } }) @@ -293,7 +296,7 @@ xdescribe('[Node.js] Query and ResultSet types', () => { const rs = await runQuery('JSON') // All possible JSON variants are now allowed - // $ExpectType unknown[] | ResponseJSON | Record + // $ExpectType unknown[] | Record | ResponseJSON await rs.json() // $ExpectType Data[] | ResponseJSON | Record await rs.json() From ee57200672fab738c5d278b8712af04b6d41f7e1 Mon Sep 17 00:00:00 2001 From: slvrtrn Date: Mon, 25 Mar 2024 21:52:08 +0100 Subject: [PATCH 14/16] Assert typed stream --- .../node_query_format_types.test.ts | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) 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 7601ff3d..2a97cdc1 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 @@ -89,6 +89,46 @@ xdescribe('[Node.js] Query and ResultSet types', () => { }, ) } + + // stream + T hint + on('data') + const streamTyped = rs.stream() + await new Promise((resolve, reject) => { + streamTyped + .on( + 'data', + // $ExpectType (rows: Row[]) => void + (rows) => { + rows.forEach( + // $ExpectType (row: Row) => void + (row) => { + // $ExpectType Data + row.json() + // $ExpectType Data + row.json() + // $ExpectType string + row.text + }, + ) + }, + ) + .on('end', resolve) + .on('error', reject) + }) + + // stream + T hint + async iterator + for await (const rows of streamTyped) { + rows.forEach( + // $ExpectType (row: Row) => void + (row) => { + // $ExpectType Data + row.json() + // $ExpectType Data + row.json() + // $ExpectType string + row.text + }, + ) + } }) /** @@ -297,7 +337,7 @@ xdescribe('[Node.js] Query and ResultSet types', () => { // All possible JSON variants are now allowed // $ExpectType unknown[] | Record | ResponseJSON - await rs.json() + await rs.json() // in the IDE sometimes the type is reported in a different order // $ExpectType Data[] | ResponseJSON | Record await rs.json() // $ExpectType string From 1b2378a943c2961efac6157808a4bb865e27caae Mon Sep 17 00:00:00 2001 From: slvrtrn Date: Mon, 25 Mar 2024 22:37:37 +0100 Subject: [PATCH 15/16] Bump version, update CHANGELOG/examples --- CHANGELOG.md | 97 ++++++++++++++------------- examples/README.md | 1 + examples/url_configuration.ts | 2 + packages/client-common/src/version.ts | 2 +- packages/client-node/src/version.ts | 2 +- packages/client-web/src/version.ts | 2 +- 6 files changed, 55 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a04dc173..4c068551 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,42 +51,26 @@ NB: this is not necessary if a user has `READONLY = 2` mode as it allows to modi See also: [readonly documentation](https://clickhouse.com/docs/en/operations/settings/permissions-for-queries#readonly). -- (TypeScript only) `ResultSet` and `Row` are now more strictly typed, according to the format used during the `query` call. See "New features" section for more details. -- (TypeScript only) Instead of using `ClickHouseClient`, a corresponding type name (`NodeClickHouseClient` or `WebClickHouseClient`) should be used when providing a _type hint_ for your client instance. NB: you should still use `createClient` factory function provided in the package. For example: - -```ts -// pre-1.0.0 (Node.js) -class MyClickHouseService { - private client: ClickHouseClient - constructor() { - this.client = createClient({ ... }) - } -} - -// 1.0.0 (Node.js) -class MyClickHouseService { - private client: NodeClickHouseClient - constructor() { - this.client = createClient({ ... }) - } -} -``` +- (TypeScript only) `ResultSet` and `Row` are now more strictly typed, according to the format used during the `query` call. See [this section](#advanced-typescript-support-for-query--resultset) for more details. +- (TypeScript only) Both Node.js and Web versions now uniformly export correct `ClickHouseClient` and `ClickHouseClientConfigOptions` types, specific to each implementation. Exported `ClickHouseClient` now does not have a `Stream` type parameter, as it was unintended to expose it there. NB: you should still use `createClient` factory function provided in the package. ## New features -- Advanced TypeScript support for `query` + `ResultSet`. Client will now try its best to figure out the shape of the data based on the DataFormat literal specified to the `query` call, as well as which methods are allowed to be called on the `ResultSet`. +### Advanced TypeScript support for `query` + `ResultSet` + +Client will now try its best to figure out the shape of the data based on the DataFormat literal specified to the `query` call, as well as which methods are allowed to be called on the `ResultSet`. Live demo (see the full description below): -[Screencast from 2024-03-09 08-10-26.webm](https://github.com/ClickHouse/clickhouse-js/assets/3175289/b66afcb2-3a10-4411-af59-51d2754c417e) +[Screencast](https://github.com/ClickHouse/clickhouse-js/assets/3175289/b66afcb2-3a10-4411-af59-51d2754c417e) Complete reference: | Format | `ResultSet.json()` | `ResultSet.stream()` | Stream data | `Row.json()` | | ------------------------------- | --------------------- | --------------------------- | ----------------- | --------------- | | JSON | ResponseJSON\ | never | never | never | -| JSONObjectsEachRow | Record\ | never | never | never | -| JSON\*EachRow | Array\ | Stream\\>\> | Array\\> | T | +| JSONObjectEachRow | Record\ | never | never | never | +| All other JSON\*EachRow | Array\ | Stream\\>\> | Array\\> | T | | CSV/TSV/CustomSeparated/Parquet | never | Stream\\>\> | Array\\> | never | 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: @@ -94,7 +78,8 @@ By default, `T` (which represents `JSONType`) is still `unknown`. However, consi ```ts type Data = { foo: string } -const resultSet = await client.query('SELECT * FROM table', { +const resultSet = await client.query({ + query: 'SELECT * FROM my_table', format: 'JSONObjectsEachRow', }) @@ -109,7 +94,8 @@ const resultNew = resultSet.json() This is even more handy in case of streaming on the Node.js platform: ```ts -const resultSet = await client.query('SELECT * FROM table', { +const resultSet = await client.query({ + query: 'SELECT * FROM my_table', format: 'JSONEachRow', }) @@ -129,17 +115,28 @@ streamNew.on('data', (rows: Row[]) => { // `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() -// `rows` inferred as an Array> instead of `any` +// `rows` are inferred as an Array> instead of `any` 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 }) }) + +// 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 + }) +} ``` -Calling `ResultSet.stream` is not allowed for certain data formats, such as `JSON` and `JSONObjectsEachRow`. 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: +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', { @@ -163,7 +160,7 @@ There is one currently known limitation: as the general shape of the data and th ```ts // assuming that `queryParams` has `JSONObjectsEachRow` format inside async function runQuery( - queryParams: QueryParams + queryParams: QueryParams, ): Promise> { const resultSet = await client.query(queryParams) // type hint here will provide a union of all known shapes instead of a specific one @@ -175,35 +172,35 @@ In this case, as it is _likely_ that you already know the desired format in adva ```ts async function runQuery( - queryParams: QueryParams + queryParams: QueryParams, ): Promise> { const resultSet = await client.query({ ...queryParams, format: 'JSONObjectsEachRow', }) - return resultSet.json() // TS understands that it is a Record now + return resultSet.json() // TS understands that it is a Record now } ``` +### URL configuration + - Added `url` configuration parameter. It is intended to replace the deprecated `host`, which was already supposed to be passed as a valid URL. -- Added `http_headers` configuration parameter as a direct replacement for `additional_headers`. Functionally, it is the same, and the change is purely cosmetic, as we'd like to leave an option to implement TCP connection in the future open. - It is now possible to configure most of the client instance parameters with a URL. The URL format is `http[s]://[username:password@]hostname:port[/database][?param1=value1¶m2=value2]`. In almost every case, the name of a particular parameter reflects its path in the config options interface, with a few exceptions. The following parameters are supported: -| Parameter | Type | -| --------------------------------------------------- | ----------------------------------------------------------------- | -| `readonly` | boolean. See below [1]. | -| `application_id` | non-empty string. | -| `session_id` | non-empty string. | -| `request_timeout` | non-negative number. | -| `max_open_connections` | non-negative number, greater than zero. | -| `compression_request` | boolean. | -| `compression_response` | boolean. | -| `log_level` | allowed values: `OFF`, `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`. | -| `keep_alive_enabled` | boolean. | -| `clickhouse_setting_*` or `ch_*` | see below [2]. | -| `http_header_*` | see below [3]. | -| (Node.js only) `keep_alive_socket_ttl` | non-negative number. | -| (Node.js only) `keep_alive_retry_on_expired_socket` | boolean. | +| Parameter | Type | +| ------------------------------------------- | ----------------------------------------------------------------- | +| `readonly` | boolean. See below [1]. | +| `application_id` | non-empty string. | +| `session_id` | non-empty string. | +| `request_timeout` | non-negative number. | +| `max_open_connections` | non-negative number, greater than zero. | +| `compression_request` | boolean. | +| `compression_response` | boolean. | +| `log_level` | allowed values: `OFF`, `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`. | +| `keep_alive_enabled` | boolean. | +| `clickhouse_setting_*` or `ch_*` | see below [2]. | +| `http_header_*` | see below [3]. | +| (Node.js only) `keep_alive_idle_socket_ttl` | non-negative number. | [1] For booleans, valid values will be `true`/`1` and `false`/`0`. @@ -237,6 +234,10 @@ Currently not supported via URL: See also: [URL configuration example](./examples/url_configuration.ts). +### Miscellaneous + +- Added `http_headers` configuration parameter as a direct replacement for `additional_headers`. Functionally, it is the same, and the change is purely cosmetic, as we'd like to leave an option to implement TCP connection in the future open. + ## 0.3.0 (Node.js only) This release primarily focuses on improving the Keep-Alive mechanism's reliability on the client side. @@ -483,7 +484,7 @@ 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' + 'CREATE TABLE foo (id String) ENGINE Memory', ) stream.destroy() diff --git a/examples/README.md b/examples/README.md index 56d6bcf2..87ffcfd2 100644 --- a/examples/README.md +++ b/examples/README.md @@ -11,6 +11,7 @@ We aim to cover various scenarios of client usage with these examples. #### General usage +- [url_configuration.ts](url_configuration.ts) - client configuration using the URL parameters. - [clickhouse_settings.ts](clickhouse_settings.ts) - ClickHouse settings on the client side, both global and per operation. - [ping.ts](ping.ts) - sample checks if the server can be reached. - [abort_request.ts](abort_request.ts) - cancelling an outgoing request or a read-only query. diff --git a/examples/url_configuration.ts b/examples/url_configuration.ts index cca24dbe..c332e01f 100644 --- a/examples/url_configuration.ts +++ b/examples/url_configuration.ts @@ -34,6 +34,8 @@ void (async () => { 'log_level=TRACE', // sets keep_alive.enabled = false 'keep_alive_enabled=false', + // (Node.js only) sets 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', diff --git a/packages/client-common/src/version.ts b/packages/client-common/src/version.ts index cab1d0b4..5976b8b3 100644 --- a/packages/client-common/src/version.ts +++ b/packages/client-common/src/version.ts @@ -1 +1 @@ -export default '0.3.0' +export default '1.0.0' diff --git a/packages/client-node/src/version.ts b/packages/client-node/src/version.ts index cab1d0b4..5976b8b3 100644 --- a/packages/client-node/src/version.ts +++ b/packages/client-node/src/version.ts @@ -1 +1 @@ -export default '0.3.0' +export default '1.0.0' diff --git a/packages/client-web/src/version.ts b/packages/client-web/src/version.ts index cab1d0b4..5976b8b3 100644 --- a/packages/client-web/src/version.ts +++ b/packages/client-web/src/version.ts @@ -1 +1 @@ -export default '0.3.0' +export default '1.0.0' From 95e7c01d112a1dd8c4b127afbfb5a20f06a440db Mon Sep 17 00:00:00 2001 From: slvrtrn Date: Tue, 26 Mar 2024 03:46:17 +0100 Subject: [PATCH 16/16] Fix scary ResultSet type in case of failed type inference --- packages/client-common/src/client.ts | 11 +- packages/client-common/src/index.ts | 10 +- packages/client-common/src/result.ts | 27 +- packages/client-common/src/ts_utils.ts | 8 + .../node_query_format_types.test.ts | 315 ++++++++++++++++-- packages/client-node/src/client.ts | 11 +- packages/client-node/src/index.ts | 5 +- packages/client-node/src/result_set.ts | 10 +- packages/client-web/src/client.ts | 11 +- packages/client-web/src/index.ts | 5 +- packages/client-web/src/result_set.ts | 10 +- 11 files changed, 375 insertions(+), 48 deletions(-) create mode 100644 packages/client-common/src/ts_utils.ts diff --git a/packages/client-common/src/client.ts b/packages/client-common/src/client.ts index 849bf176..44de1a08 100644 --- a/packages/client-common/src/client.ts +++ b/packages/client-common/src/client.ts @@ -3,6 +3,7 @@ import type { ClickHouseSettings, Connection, ConnExecResult, + IsSame, MakeResultSet, WithClickHouseSummary, } from '@clickhouse/client-common' @@ -43,6 +44,14 @@ export type QueryParamsWithFormat = Omit< '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, + * and avoiding generated types that include all possible DataFormat literal values. */ +export type QueryResult = + IsSame extends true + ? BaseResultSet + : BaseResultSet + export interface ExecParams extends BaseQueryParams { /** Statement to execute. */ query: string @@ -145,7 +154,7 @@ export class ClickHouseClient { */ async query( params: QueryParamsWithFormat, - ): Promise> { + ): Promise> { const format = params.format ?? 'JSON' const query = formatQuery(params.query, format) const { stream, query_id } = await this.connection.query({ diff --git a/packages/client-common/src/index.ts b/packages/client-common/src/index.ts index 9ca1cab3..b0ce9e38 100644 --- a/packages/client-common/src/index.ts +++ b/packages/client-common/src/index.ts @@ -2,6 +2,7 @@ export { type BaseQueryParams, type QueryParams, + type QueryResult, type ExecParams, type InsertParams, type InsertValues, @@ -13,7 +14,13 @@ export { type PingResult, } from './client' export { type BaseClickHouseClientConfigOptions } from './config' -export type { Row, BaseResultSet, ResultJSONType, RowJSONType } from './result' +export type { + Row, + BaseResultSet, + ResultJSONType, + RowJSONType, + ResultStream, +} from './result' export { type DataFormat } from './data_formatter' export { ClickHouseError } from './error' export { @@ -84,3 +91,4 @@ export { formatQueryParams, } from './data_formatter' export type { QueryParamsWithFormat } from './client' +export type { IsSame } from './ts_utils' diff --git a/packages/client-common/src/result.ts b/packages/client-common/src/result.ts index 2a6257aa..150156da 100644 --- a/packages/client-common/src/result.ts +++ b/packages/client-common/src/result.ts @@ -4,23 +4,38 @@ import type { RawDataFormat, RecordsJSONFormat, SingleDocumentJSONFormat, + StreamableDataFormat, StreamableJSONDataFormat, } from './data_formatter' +export type ResultStream = + // JSON*EachRow (except JSONObjectEachRow), CSV, TSV etc. + Format extends StreamableDataFormat + ? Stream + : // JSON formats represented as an object { data, meta, statistics, ... } + Format extends SingleDocumentJSONFormat + ? never + : // JSON formats represented as a Record + Format extends RecordsJSONFormat + ? never + : // If we fail to infer the literal type, allow to obtain the stream + Stream + export type ResultJSONType = - // JSON*EachRow formats + // JSON*EachRow formats except JSONObjectEachRow F extends StreamableJSONDataFormat ? T[] : // JSON formats with known layout { data, meta, statistics, ... } F extends SingleDocumentJSONFormat ? ResponseJSON - : // JSON formats returned as a Record + : // JSON formats represented as a Record F extends RecordsJSONFormat ? Record : // CSV, TSV etc. - cannot be represented as JSON F extends RawDataFormat ? never - : T // happens only when Format could not be inferred from a literal + : // happens only when Format could not be inferred from a literal + T[] | Record | ResponseJSON export type RowJSONType = // JSON*EachRow formats @@ -46,7 +61,7 @@ export interface Row< json(): RowJSONType } -export interface BaseResultSet { +export interface BaseResultSet { /** * The method waits for all the rows to be fully loaded * and returns the result as a string. @@ -94,7 +109,7 @@ export interface BaseResultSet { * * CustomSeparatedWithNamesAndTypes * * Parquet * - * Formats that CANNOT be streamed: + * Formats that CANNOT be streamed (the method returns "never" in TS): * * JSON * * JSONStrings * * JSONCompact @@ -111,7 +126,7 @@ export interface BaseResultSet { * and if the underlying stream was already consumed * by calling the other methods. */ - stream(): Stream + stream(): ResultStream /** Close the underlying stream. */ close(): void diff --git a/packages/client-common/src/ts_utils.ts b/packages/client-common/src/ts_utils.ts new file mode 100644 index 00000000..f72a18a6 --- /dev/null +++ b/packages/client-common/src/ts_utils.ts @@ -0,0 +1,8 @@ +/** Adjusted from https://stackoverflow.com/a/72801672/4575540. + * Useful for checking if we could not infer a concrete literal type + * (i.e. if instead of 'JSONEachRow' or other literal we just get a generic {@link DataFormat} as an argument). */ +export type IsSame = [A] extends [B] + ? B extends A + ? true + : false + : false 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 2a97cdc1..0fb6ee81 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 @@ -76,7 +76,10 @@ xdescribe('[Node.js] Query and ResultSet types', () => { }) // stream + async iterator - for await (const rows of stream) { + for await (const _rows of stream) { + // $ExpectType Row[] + const rows = _rows + rows.length // avoid unused variable warning (rows reassigned for type assertion) rows.forEach( // $ExpectType (row: Row) => void (row) => { @@ -116,7 +119,10 @@ xdescribe('[Node.js] Query and ResultSet types', () => { }) // stream + T hint + async iterator - for await (const rows of streamTyped) { + for await (const _rows of streamTyped) { + // $ExpectType Row[] + const rows = _rows + rows.length // avoid unused variable warning (rows reassigned for type assertion) rows.forEach( // $ExpectType (row: Row) => void (row) => { @@ -131,10 +137,132 @@ xdescribe('[Node.js] Query and ResultSet types', () => { } }) + 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') { + return client.query({ + query, + format, + }) + } + + // ResultSet cannot infer the type from the literal, so it falls back to both possible formats. + // However, these are both streamable, both can use JSON features, and both have the same data layout. + + //// JSONCompactEachRow + + // $ExpectType ResultSet<"JSONEachRow" | "JSONCompactEachRow"> + const rs = await runQuery('JSONCompactEachRow') + // $ExpectType unknown[] + await rs.json() + // $ExpectType Data[] + await rs.json() + // $ExpectType string + await rs.text() + // $ExpectType StreamReadable[]> + const stream = rs.stream() + + // stream + on('data') + await new Promise((resolve, reject) => { + stream + .on( + 'data', + // $ExpectType (rows: Row[]) => void + (rows) => { + rows.forEach( + // $ExpectType (row: Row) => void + (row) => { + // $ExpectType unknown + row.json() + // $ExpectType Data + row.json() + // $ExpectType string + row.text + }, + ) + }, + ) + .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) + rows.forEach( + // $ExpectType (row: Row) => void + (row) => { + // $ExpectType unknown + row.json() + // $ExpectType Data + row.json() + // $ExpectType string + row.text + }, + ) + } + + //// JSONEachRow + + // $ExpectType ResultSet<"JSONEachRow" | "JSONCompactEachRow"> + const rs2 = await runQuery('JSONEachRow') + // $ExpectType unknown[] + await rs2.json() + // $ExpectType Data[] + await rs2.json() + // $ExpectType string + await rs2.text() + // $ExpectType StreamReadable[]> + const stream2 = rs2.stream() + + // stream + on('data') + await new Promise((resolve, reject) => { + stream2 + .on( + 'data', + // $ExpectType (rows: Row[]) => void + (rows) => { + rows.forEach( + // $ExpectType (row: Row) => void + (row) => { + // $ExpectType unknown + row.json() + // $ExpectType Data + row.json() + // $ExpectType string + row.text + }, + ) + }, + ) + .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) + rows.forEach( + // $ExpectType (row: Row) => void + (row) => { + // $ExpectType unknown + row.json() + // $ExpectType Data + row.json() + // $ExpectType string + row.text + }, + ) + } + }) + /** - * TODO: the rest of the streamable JSON formats + * Not covered, but should behave similarly: * 'JSONStringsEachRow', - * 'JSONCompactEachRow', * 'JSONCompactStringsEachRow', * 'JSONCompactEachRowWithNames', * 'JSONCompactEachRowWithNamesAndTypes', @@ -192,7 +320,7 @@ xdescribe('[Node.js] Query and ResultSet types', () => { }) /** - * TODO: the rest of the single document JSON formats + * Not covered, but should behave similarly: * 'JSONStrings', * 'JSONCompact', * 'JSONCompactStrings', @@ -241,7 +369,10 @@ xdescribe('[Node.js] Query and ResultSet types', () => { }) // stream + async iterator - for await (const rows of stream) { + for await (const _rows of stream) { + // $ExpectType Row[] + const rows = _rows + rows.length // avoid unused variable warning (rows reassigned for type assertion) rows.forEach( // $ExpectType (row: Row) => void (row) => { @@ -256,11 +387,133 @@ xdescribe('[Node.js] Query and ResultSet types', () => { } }) + 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') { + return client.query({ + query, + format, + }) + } + + // ResultSet cannot infer the type from the literal, so it falls back to both possible formats. + // However, these are both streamable, and both cannot use JSON features. + + //// CSV + + // $ExpectType ResultSet<"CSV" | "TabSeparated"> + const rs = await runQuery('CSV') + // $ExpectType never + await rs.json() + // $ExpectType never + await rs.json() + // $ExpectType string + await rs.text() + // $ExpectType StreamReadable[]> + const stream = rs.stream() + + // stream + on('data') + await new Promise((resolve, reject) => { + stream + .on( + 'data', + // $ExpectType (rows: Row[]) => void + (rows) => { + rows.forEach( + // $ExpectType (row: Row) => void + (row) => { + // $ExpectType never + row.json() + // $ExpectType never + row.json() + // $ExpectType string + row.text + }, + ) + }, + ) + .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) + rows.forEach( + // $ExpectType (row: Row) => void + (row) => { + // $ExpectType never + row.json() + // $ExpectType never + row.json() + // $ExpectType string + row.text + }, + ) + } + + //// TabSeparated + + // $ExpectType ResultSet<"CSV" | "TabSeparated"> + const rs2 = await runQuery('TabSeparated') + // $ExpectType never + await rs2.json() + // $ExpectType never + await rs2.json() + // $ExpectType string + await rs2.text() + // $ExpectType StreamReadable[]> + const stream2 = rs2.stream() + + // stream + on('data') + await new Promise((resolve, reject) => { + stream2 + .on( + 'data', + // $ExpectType (rows: Row[]) => void + (rows) => { + rows.forEach( + // $ExpectType (row: Row) => void + (row) => { + // $ExpectType never + row.json() + // $ExpectType never + row.json() + // $ExpectType string + row.text + }, + ) + }, + ) + .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) + rows.forEach( + // $ExpectType (row: Row) => void + (row) => { + // $ExpectType never + row.json() + // $ExpectType never + row.json() + // $ExpectType string + row.text + }, + ) + } + }) + /** - * TODO: the rest of the raw formats + * Not covered, but should behave similarly: * 'CSVWithNames', * 'CSVWithNamesAndTypes', - * 'TabSeparated', * 'TabSeparatedRaw', * 'TabSeparatedWithNames', * 'TabSeparatedWithNamesAndTypes', @@ -272,14 +525,18 @@ xdescribe('[Node.js] Query and ResultSet types', () => { }) describe('Type inference with ambiguous format variants', () => { - // FIXME: Maybe there is a way to infer the format without an extra type parameter? + // 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 () => { - function runQuery(format: 'JSON' | 'JSONEachRow') { + // $ExpectType (format: "JSONEachRow" | "JSON") => Promise> + function runQuery(format: 'JSONEachRow' | 'JSON') { 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 ResultSet<"JSONEachRow" | "JSON"> const rs = await runQuery('JSON') // $ExpectType unknown[] | ResponseJSON @@ -288,11 +545,12 @@ xdescribe('[Node.js] Query and ResultSet types', () => { await rs.json() // $ExpectType string await rs.text() - // $ExpectType StreamReadable[]> + // $ExpectType StreamReadable[]> rs.stream() }) 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, @@ -323,7 +581,8 @@ xdescribe('[Node.js] Query and ResultSet types', () => { }) 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 DataFormat usage) + // 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, @@ -331,28 +590,36 @@ xdescribe('[Node.js] Query and ResultSet types', () => { }) } - // ResultSet falls back to all possible formats (ResultSet type prints all possible formats) - // $ExpectType ResultSet<"JSONEachRow" | "JSONStringsEachRow" | "JSONCompactEachRow" | "JSONCompactStringsEachRow" | "JSONCompactEachRowWithNames" | "JSONCompactEachRowWithNamesAndTypes" | "JSONCompactStringsEachRowWithNames" | "JSONCompactStringsEachRowWithNamesAndTypes" | "JSON" | "JSONStrings" | "JSONCompact" | "JSONCompactStrings" | "JSONColumnsWithMetadata" | "JSONObjectEachRow" | "CSV" | "CSVWithNames" | "CSVWithNamesAndTypes" | "TabSeparated" | "TabSeparatedRaw" | "TabSeparatedWithNames" | "TabSeparatedWithNamesAndTypes" | "CustomSeparated" | "CustomSeparatedWithNames" | "CustomSeparatedWithNamesAndTypes" | "Parquet"> + // 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') // All possible JSON variants are now allowed // $ExpectType unknown[] | Record | ResponseJSON - await rs.json() // in the IDE sometimes the type is reported in a different order + await rs.json() // IDE error here, different type order // $ExpectType Data[] | ResponseJSON | Record await rs.json() // $ExpectType string await rs.text() // Stream is still allowed (can't be inferred, so it is not "never") + // $ExpectType StreamReadable[]> const stream = rs.stream() - for await (const rows of stream) { - rows.forEach((row) => { - // $ExpectType unknown - row.json() - // $ExpectType Data - row.json() - // $ExpectType string - row.text - }) + for await (const _rows of stream) { + // $ExpectType Row[] + 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() + // $ExpectType Data + row.json() + // $ExpectType string + row.text + }, + ) } }) }) diff --git a/packages/client-node/src/client.ts b/packages/client-node/src/client.ts index 6f73bf14..9dcc61d2 100644 --- a/packages/client-node/src/client.ts +++ b/packages/client-node/src/client.ts @@ -1,5 +1,6 @@ import type { DataFormat, + IsSame, QueryParamsWithFormat, } from '@clickhouse/client-common' import { ClickHouseClient } from '@clickhouse/client-common' @@ -8,11 +9,19 @@ 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, + * and avoiding generated types that include all possible DataFormat literal values. */ +export type QueryResult = + IsSame extends true + ? ResultSet + : ResultSet + export class NodeClickHouseClient extends ClickHouseClient { /** See {@link ClickHouseClient.query}. */ query( params: QueryParamsWithFormat, - ): Promise> { + ): Promise> { return super.query(params) as Promise> } } diff --git a/packages/client-node/src/index.ts b/packages/client-node/src/index.ts index d7aca42d..261dd2c5 100644 --- a/packages/client-node/src/index.ts +++ b/packages/client-node/src/index.ts @@ -1,4 +1,7 @@ -export type { NodeClickHouseClient as ClickHouseClient } from './client' +export type { + NodeClickHouseClient as ClickHouseClient, + QueryResult, +} from './client' export { createClient } from './client' export { type NodeClickHouseClientConfigOptions as ClickHouseClientConfigOptions } from './config' export { ResultSet, type StreamReadable } from './result_set' diff --git a/packages/client-node/src/result_set.ts b/packages/client-node/src/result_set.ts index b28f8bc0..ef05dc59 100644 --- a/packages/client-node/src/result_set.ts +++ b/packages/client-node/src/result_set.ts @@ -2,8 +2,8 @@ import type { BaseResultSet, DataFormat, ResultJSONType, + ResultStream, Row, - StreamableDataFormat, } from '@clickhouse/client-common' import { decode, validateStreamFormat } from '@clickhouse/client-common' import { Buffer } from 'buffer' @@ -34,7 +34,7 @@ export type StreamReadable = Omit & { ): Stream.Readable } -export class ResultSet +export class ResultSet implements BaseResultSet { constructor( @@ -56,13 +56,11 @@ export class ResultSet if (this._stream.readableEnded) { throw Error(streamAlreadyConsumedMessage) } - return decode(await this.text(), this.format) + return decode(await this.text(), this.format as DataFormat) } /** See {@link BaseResultSet.stream}. */ - stream(): Format extends StreamableDataFormat - ? StreamReadable[]> - : never { + stream(): ResultStream[]>> { // If the underlying stream has already ended by calling `text` or `json`, // Stream.pipeline will create a new empty stream // but without "readableEnded" flag set to true diff --git a/packages/client-web/src/client.ts b/packages/client-web/src/client.ts index 8954e2f2..9bda2ae9 100644 --- a/packages/client-web/src/client.ts +++ b/packages/client-web/src/client.ts @@ -4,6 +4,7 @@ import type { InputJSONObjectEachRow, InsertParams, InsertResult, + IsSame, QueryParamsWithFormat, } from '@clickhouse/client-common' import { ClickHouseClient } from '@clickhouse/client-common' @@ -11,6 +12,14 @@ 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, + * and avoiding generated types that include all possible DataFormat literal values. */ +export type QueryResult = + IsSame extends true + ? ResultSet + : ResultSet + export type WebClickHouseClient = Omit & { /** See {@link ClickHouseClient.insert}. * @@ -27,7 +36,7 @@ class WebClickHouseClientImpl extends ClickHouseClient { /** See {@link ClickHouseClient.query}. */ query( params: QueryParamsWithFormat, - ): Promise> { + ): Promise> { return super.query(params) as Promise> } } diff --git a/packages/client-web/src/index.ts b/packages/client-web/src/index.ts index 342d1902..8a93656a 100644 --- a/packages/client-web/src/index.ts +++ b/packages/client-web/src/index.ts @@ -1,4 +1,7 @@ -export type { WebClickHouseClient as ClickHouseClient } from './client' +export type { + WebClickHouseClient as ClickHouseClient, + QueryResult, +} from './client' export { createClient } from './client' export { type WebClickHouseClientConfigOptions as ClickHouseClientConfigOptions } from './config' export { ResultSet } from './result_set' diff --git a/packages/client-web/src/result_set.ts b/packages/client-web/src/result_set.ts index 5b51d529..1832a1df 100644 --- a/packages/client-web/src/result_set.ts +++ b/packages/client-web/src/result_set.ts @@ -2,13 +2,13 @@ import type { BaseResultSet, DataFormat, ResultJSONType, + ResultStream, Row, } from '@clickhouse/client-common' import { decode, validateStreamFormat } from '@clickhouse/client-common' -import type { StreamableDataFormat } from '@clickhouse/client-common/src/data_formatter' import { getAsText } from './utils' -export class ResultSet +export class ResultSet implements BaseResultSet, Format> { private isAlreadyConsumed = false @@ -27,13 +27,11 @@ export class ResultSet /** See {@link BaseResultSet.json} */ async json(): Promise> { const text = await this.text() - return decode(text, this.format) + return decode(text, this.format as DataFormat) } /** See {@link BaseResultSet.stream} */ - stream(): Format extends StreamableDataFormat - ? ReadableStream[]> - : never { + stream(): ResultStream[]>> { this.markAsConsumed() validateStreamFormat(this.format)