Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/async_insert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ClickHouseError } from '@clickhouse/client-common' // or '@clickhouse/c
// See https://clickhouse.com/docs/en/optimize/asynchronous-inserts
void (async () => {
const client = createClient({
host: process.env['CLICKHOUSE_HOST'], // defaults to 'http://localhost:8123'
url: process.env['CLICKHOUSE_URL'], // defaults to 'http://localhost:8123'
password: process.env['CLICKHOUSE_PASSWORD'], // defaults to an empty string
max_open_connections: 10,
clickhouse_settings: {
Expand Down
2 changes: 1 addition & 1 deletion examples/async_insert_without_waiting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { EventEmitter } from 'events'
// See https://clickhouse.com/docs/en/optimize/asynchronous-inserts
void (async () => {
const client = createClient({
host: process.env['CLICKHOUSE_HOST'], // defaults to 'http://localhost:8123'
url: process.env['CLICKHOUSE_URL'], // defaults to 'http://localhost:8123'
password: process.env['CLICKHOUSE_PASSWORD'], // defaults to an empty string
max_open_connections: 10,
clickhouse_settings: {
Expand Down
6 changes: 3 additions & 3 deletions examples/ping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ void (async () => {

async function existingHostPing() {
const client = createClient({
host: process.env['CLICKHOUSE_HOST'], // defaults to 'http://localhost:8123'
url: process.env['CLICKHOUSE_URL'], // defaults to 'http://localhost:8123'
password: process.env['CLICKHOUSE_PASSWORD'], // defaults to an empty string
})
const pingResult = await client.ping()
Expand All @@ -36,7 +36,7 @@ async function existingHostPing() {

async function nonExistingHostPing() {
const client = createClient({
host: 'http://localhost:8100', // non-existing host
url: 'http://localhost:8100', // non-existing host
request_timeout: 50, // low request_timeout to speed up the example
})
// Ping does not throw an error; instead, { success: false; error: Error } is returned.
Expand All @@ -56,7 +56,7 @@ async function nonExistingHostPing() {
async function timeoutPing() {
const server = startSlowHTTPServer()
const client = createClient({
host: 'http://localhost:18123',
url: 'http://localhost:18123',
request_timeout: 50, // low request_timeout to speed up the example
})
// Ping does not throw an error; instead, { success: false; error: Error } is returned.
Expand Down
31 changes: 24 additions & 7 deletions packages/client-common/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export type InsertResult = {
/**
* Indicates whether the INSERT statement was executed on the server.
* Will be `false` if there was no data to insert.
* For example: if {@link InsertParams.values} was an empty array,
* For example, if {@link InsertParams.values} was an empty array,
* the client does not send any requests to the server, and {@link executed} is false.
*/
executed: boolean
Expand All @@ -115,7 +115,6 @@ export type InsertResult = {
WithResponseHeaders

export type ExecResult<Stream> = ConnExecResult<Stream>
export type PingResult = ConnPingResult

/** {@link except} field contains a non-empty list of columns to exclude when generating `(* EXCEPT (...))` clause */
export interface InsertColumnsExcept {
Expand Down Expand Up @@ -145,6 +144,19 @@ export interface InsertParams<Stream = unknown, T = unknown>
columns?: NonEmptyArray<string> | InsertColumnsExcept
}

/** Parameters for the health-check request - using the built-in `/ping` endpoint. */
export type PingParamsWithEndpoint = { select: false } & Pick<
BaseQueryParams,
'abort_signal' | 'http_headers'
>
/** Parameters for the health-check request - using a SELECT query. */
export type PingParamsWithSelectQuery = { select: true } & Omit<
BaseQueryParams,
'query_params'
>
export type PingParams = PingParamsWithEndpoint | PingParamsWithSelectQuery
export type PingResult = ConnPingResult

export class ClickHouseClient<Stream = unknown> {
private readonly clientClickHouseSettings: ClickHouseSettings
private readonly connectionParams: ConnectionParams
Expand Down Expand Up @@ -221,7 +233,7 @@ export class ClickHouseClient<Stream = unknown> {
/**
* It should be used for statements that do not have any output,
* when the format clause is not applicable, or when you are not interested in the response at all.
* Response stream is destroyed immediately as we do not expect useful information there.
* The response stream is destroyed immediately as we do not expect useful information there.
* Examples of such statements are DDLs or custom inserts.
*
* @note if you have a custom query that does not work with {@link ClickHouseClient.query},
Expand Down Expand Up @@ -284,11 +296,16 @@ export class ClickHouseClient<Stream = unknown> {
}

/**
* Health-check request. It does not throw if an error occurs -
* the error is returned inside the result object.
* A health-check request. It does not throw if an error occurs - the error is returned inside the result object.
*
* By default, Node.js version uses the built-in `/ping` endpoint, which does not verify credentials.
* Optionally, it can be switched to a `SELECT` query (see {@link PingParamsWithSelectQuery}).
* In that case, the server will verify the credentials.
*
* **NOTE**: Since the `/ping` endpoint does not support CORS, the Web version always uses a `SELECT` query.
*/
async ping(): Promise<PingResult> {
return await this.connection.ping()
async ping(params?: PingParams): Promise<PingResult> {
return await this.connection.ping(params ?? { select: false })
}

/**
Expand Down
11 changes: 8 additions & 3 deletions packages/client-common/src/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ export interface ConnBaseQueryParams {
http_headers?: Record<string, string>
}

export type ConnPingParams = { select: boolean } & Omit<
ConnBaseQueryParams,
'query' | 'query_params'
>

export interface ConnInsertParams<Stream> extends ConnBaseQueryParams {
values: string | Stream
}
Expand Down Expand Up @@ -72,10 +77,10 @@ export type ConnPingResult =
export type ConnOperation = 'Ping' | 'Query' | 'Insert' | 'Exec' | 'Command'

export interface Connection<Stream> {
ping(): Promise<ConnPingResult>
close(): Promise<void>
ping(params: ConnPingParams): Promise<ConnPingResult>
query(params: ConnBaseQueryParams): Promise<ConnQueryResult<Stream>>
insert(params: ConnInsertParams<Stream>): Promise<ConnInsertResult>
exec(params: ConnExecParams<Stream>): Promise<ConnExecResult<Stream>>
command(params: ConnBaseQueryParams): Promise<ConnCommandResult>
exec(params: ConnExecParams<Stream>): Promise<ConnExecResult<Stream>>
close(): Promise<void>
}
4 changes: 4 additions & 0 deletions packages/client-common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ export {
type ExecResult,
type InsertResult,
type PingResult,
type PingParams,
type PingParamsWithSelectQuery,
type PingParamsWithEndpoint,
} from './client'
export { type BaseClickHouseClientConfigOptions } from './config'
export type {
Expand Down Expand Up @@ -127,6 +130,7 @@ export type {
ConnPingResult,
ConnCommandResult,
ConnOperation,
ConnPingParams,
} from './connection'
export type { QueryParamsWithFormat } from './client'
export type { IsSame } from './ts_utils'
4 changes: 2 additions & 2 deletions packages/client-common/src/utils/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function transformUrl({
}

type ToSearchParamsOptions = {
database: string
database: string | undefined
clickhouse_settings?: ClickHouseSettings
query_params?: Record<string, unknown>
query?: string
Expand Down Expand Up @@ -69,7 +69,7 @@ export function toSearchParams({
}
}

if (database !== 'default') {
if (database !== undefined && database !== 'default') {
params.set('database', database)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ describe('[Node.js] logger support', () => {
},
})
await client.ping()
// logs[0] are about current log level
// logs[0] are about the current log level
expect(logs[1]).toEqual(
jasmine.objectContaining({
message: 'Ping: got a response from ClickHouse',
Expand Down
32 changes: 31 additions & 1 deletion packages/client-node/__tests__/integration/node_ping.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import type { ClickHouseClient } from '@clickhouse/client-common'
import type {
ClickHouseClient,
ClickHouseError,
} from '@clickhouse/client-common'
import { createTestClient } from '@test/utils'

describe('[Node.js] ping', () => {
let client: ClickHouseClient

afterEach(async () => {
await client.close()
})

it('does not swallow a client error', async () => {
client = createTestClient({
url: 'http://localhost:3333',
Expand All @@ -20,4 +25,29 @@ describe('[Node.js] ping', () => {
}),
)
})

it('ignores credentials by default', async () => {
client = createTestClient({
username: 'wrong',
})
const response = await client.ping()
expect(response.success).toBe(true)
})

it('checks credentials when select query mode is enabled', async () => {
client = createTestClient({
username: 'wrong',
})
const response = await client.ping({
select: true,
})
expect(response.success).toBe(false)

const err = (response as unknown as { error: ClickHouseError }).error
expect(err.code).toEqual('516')
expect(err.type).toEqual('AUTHENTICATION_FAILED')
expect(err.message).toEqual(
jasmine.stringContaining('Authentication failed'),
)
})
})
58 changes: 43 additions & 15 deletions packages/client-node/src/connection/node_base_connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
transformUrl,
withHttpSettings,
} from '@clickhouse/client-common'
import { type ConnPingParams } from '@clickhouse/client-common'
import crypto from 'crypto'
import type Http from 'http'
import type * as net from 'net'
Expand Down Expand Up @@ -107,34 +108,59 @@ export abstract class NodeBaseConnection
this.idleSocketTTL = params.keep_alive.idle_socket_ttl
}

async ping(): Promise<ConnPingResult> {
const abortController = new AbortController()
async ping(params: ConnPingParams): Promise<ConnPingResult> {
const query_id = this.getQueryId(params.query_id)
const { controller, controllerCleanup } = this.getAbortController(params)
let result: RequestResult
try {
const { stream } = await this.request(
{
method: 'GET',
url: transformUrl({ url: this.params.url, pathname: '/ping' }),
abort_signal: abortController.signal,
headers: this.buildRequestHeaders(),
query: 'ping',
},
'Ping',
)
await drainStream(stream)
if (params.select) {
const searchParams = toSearchParams({
database: undefined,
query: PingQuery,
query_id,
})
result = await this.request(
{
method: 'GET',
url: transformUrl({ url: this.params.url, searchParams }),
query: PingQuery,
abort_signal: controller.signal,
headers: this.buildRequestHeaders(),
},
'Ping',
)
} else {
result = await this.request(
{
method: 'GET',
url: transformUrl({ url: this.params.url, pathname: '/ping' }),
abort_signal: controller.signal,
headers: this.buildRequestHeaders(),
query: 'ping',
},
'Ping',
)
}
await drainStream(result.stream)
return { success: true }
} catch (error) {
// it is used to ensure that the outgoing request is terminated,
// and we don't get unhandled error propagation later
abortController.abort('Ping failed')
controller.abort('Ping failed')
// not an error, as this might be semi-expected
this.logger.warn({
message: this.httpRequestErrorMessage('Ping'),
err: error as Error,
args: {
query_id,
},
})
return {
success: false,
error: error as Error, // should NOT be propagated to the user
}
} finally {
controllerCleanup()
}
}

Expand Down Expand Up @@ -320,7 +346,7 @@ export abstract class NodeBaseConnection
}

// a wrapper over the user's Signal to terminate the failed requests
private getAbortController(params: ConnBaseQueryParams): {
private getAbortController(params: { abort_signal?: AbortSignal }): {
controller: AbortController
controllerCleanup: () => void
} {
Expand Down Expand Up @@ -764,3 +790,5 @@ type RunExecParams = ConnBaseQueryParams & {
values?: ConnExecParams<Stream.Readable>['values']
decompress_response_stream?: boolean
}

const PingQuery = `SELECT 'ping'`
24 changes: 23 additions & 1 deletion packages/client-web/__tests__/integration/web_ping.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import type { ClickHouseClient } from '@clickhouse/client-common'
import type {
ClickHouseClient,
ClickHouseError,
} from '@clickhouse/client-common'
import { createTestClient } from '@test/utils'

describe('[Web] ping', () => {
let client: ClickHouseClient

afterEach(async () => {
await client.close()
})

it('does not swallow a client error', async () => {
client = createTestClient({
url: 'http://localhost:3333',
Expand All @@ -21,4 +26,21 @@ describe('[Web] ping', () => {
}),
)
})

it('checks credentials by default', async () => {
client = createTestClient({
username: 'wrong',
})
const response = await client.ping({
select: false, // ignored
})
expect(response.success).toBe(false)

const err = (response as unknown as { error: ClickHouseError }).error
expect(err.code).toEqual('516')
expect(err.type).toEqual('AUTHENTICATION_FAILED')
expect(err.message).toEqual(
jasmine.stringContaining('Authentication failed'),
)
})
})
Loading