Skip to content

Commit cfdd523

Browse files
authored
feat: use clickhouse client lib by default for queries (#776)
Still uses the old fetch method on local mode Ref: HDX-1630 Ref: HDX-1653
1 parent 8dc83c3 commit cfdd523

File tree

4 files changed

+110
-106
lines changed

4 files changed

+110
-106
lines changed

.changeset/forty-hounds-grin.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@hyperdx/common-utils": patch
3+
"@hyperdx/app": patch
4+
---
5+
6+
feat: clickhouse queries are by default conducted through the clickhouse library via POST request. localMode still uses GET for CORS purposes

packages/app/src/BenchmarkPage.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
useQueryState,
88
} from 'nuqs';
99
import { useForm } from 'react-hook-form';
10+
import { DataFormat } from '@clickhouse/client-common';
1011
import { DisplayType } from '@hyperdx/common-utils/dist/types';
1112
import {
1213
Button,
@@ -56,7 +57,7 @@ function useBenchmarkQueryIds({
5657
.query({
5758
query: shuffledQueries[j],
5859
connectionId: connections[j],
59-
format: 'NULL',
60+
format: 'NULL' as DataFormat, // clickhouse doesn't have this under the client-js lib for some reason
6061
clickhouse_settings: {
6162
min_bytes_to_use_direct_io: '1',
6263
use_query_cache: 0,
@@ -133,7 +134,7 @@ function useIndexes(
133134
clickhouseClient
134135
.query({
135136
query: `EXPLAIN indexes=1, json=1, description = 0 ${query}`,
136-
format: 'TSVRaw',
137+
format: 'TabSeparatedRaw',
137138
connectionId: connections[i],
138139
})
139140
.then(res => res.text())

packages/app/src/sessions.ts

+8-45
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ class FatalError extends Error {}
240240
class TimeoutError extends Error {}
241241
const EventStreamContentType = 'text/event-stream';
242242

243-
async function* streamToAsyncIterator<T>(
243+
async function* streamToAsyncIterator<T = any>(
244244
stream: ReadableStream<T>,
245245
): AsyncIterableIterator<T> {
246246
const reader = stream.getReader();
@@ -372,52 +372,15 @@ export function useRRWebEventStream(
372372
metadata,
373373
);
374374

375-
// TODO: Change ClickhouseClient class to use this under the hood,
376-
// and refactor this to use ClickhouseClient.query. Also change pathname
377-
// in createClient to PROXY_CLICKHOUSE_HOST instead
378375
const format = 'JSONEachRow';
379-
const queryFn = async () => {
380-
if (IS_LOCAL_MODE) {
381-
const localConnections = getLocalConnections();
382-
const localModeUrl = new URL(localConnections[0].host);
383-
localModeUrl.username = localConnections[0].username;
384-
localModeUrl.password = localConnections[0].password;
385-
386-
const clickhouseClient = getClickhouseClient();
387-
return clickhouseClient.query({
388-
query: query.sql,
389-
query_params: query.params,
390-
format,
391-
});
392-
} else {
393-
const clickhouseClient = createClient({
394-
clickhouse_settings: {
395-
add_http_cors_header: IS_LOCAL_MODE ? 1 : 0,
396-
cancel_http_readonly_queries_on_client_close: 1,
397-
date_time_output_format: 'iso',
398-
wait_end_of_query: 0,
399-
},
400-
http_headers: { 'x-hyperdx-connection-id': source.connection },
401-
keep_alive: {
402-
enabled: true,
403-
},
404-
url: window.location.origin,
405-
pathname: '/api/clickhouse-proxy',
406-
compression: {
407-
response: true,
408-
},
409-
});
410-
411-
return clickhouseClient.query({
412-
query: query.sql,
413-
query_params: query.params,
414-
format,
415-
});
416-
}
417-
};
418-
419376
const fetchPromise = (async () => {
420-
const resultSet = await queryFn();
377+
const clickhouseClient = getClickhouseClient();
378+
const resultSet = await clickhouseClient.query({
379+
query: query.sql,
380+
query_params: query.params,
381+
format,
382+
connectionId: source.connection,
383+
});
421384

422385
let forFunc: (data: any) => void;
423386
if (onEvent) {

packages/common-utils/src/clickhouse.ts

+93-59
Original file line numberDiff line numberDiff line change
@@ -345,47 +345,23 @@ export class ClickhouseClient {
345345
}
346346

347347
// https://github.com/ClickHouse/clickhouse-js/blob/1ebdd39203730bb99fad4c88eac35d9a5e96b34a/packages/client-web/src/connection/web_connection.ts#L151
348-
async query<T extends DataFormat>({
348+
async query<Format extends DataFormat>({
349349
query,
350-
format = 'JSON',
350+
format = 'JSON' as Format,
351351
query_params = {},
352352
abort_signal,
353353
clickhouse_settings,
354354
connectionId,
355355
queryId,
356356
}: {
357357
query: string;
358-
format?: string;
358+
format?: Format;
359359
abort_signal?: AbortSignal;
360360
query_params?: Record<string, any>;
361361
clickhouse_settings?: Record<string, any>;
362362
connectionId?: string;
363363
queryId?: string;
364-
}): Promise<BaseResultSet<ReadableStream, T>> {
365-
const isLocalMode = this.username != null && this.password != null;
366-
const includeCredentials = !isLocalMode;
367-
const includeCorsHeader = isLocalMode;
368-
369-
const searchParams = new URLSearchParams([
370-
...(includeCorsHeader ? [['add_http_cors_header', '1']] : []),
371-
['query', query],
372-
['default_format', format],
373-
['date_time_output_format', 'iso'],
374-
['wait_end_of_query', '0'],
375-
['cancel_http_readonly_queries_on_client_close', '1'],
376-
...(this.username ? [['user', this.username]] : []),
377-
...(this.password ? [['password', this.password]] : []),
378-
...(queryId ? [['query_id', queryId]] : []),
379-
...Object.entries(query_params).map(([key, value]) => [
380-
`param_${key}`,
381-
value,
382-
]),
383-
...Object.entries(clickhouse_settings ?? {}).map(([key, value]) => [
384-
key,
385-
value,
386-
]),
387-
]);
388-
364+
}): Promise<BaseResultSet<ReadableStream, Format>> {
389365
let debugSql = '';
390366
try {
391367
debugSql = parameterizedQueryToSql({ sql: query, params: query_params });
@@ -402,38 +378,96 @@ export class ClickhouseClient {
402378

403379
if (isBrowser) {
404380
// TODO: check if we can use the client-web directly
405-
const { ResultSet } = await import('@clickhouse/client-web');
406-
407-
const headers = {};
408-
if (!isLocalMode && connectionId) {
409-
headers['x-hyperdx-connection-id'] = connectionId;
410-
}
411-
// https://github.com/ClickHouse/clickhouse-js/blob/1ebdd39203730bb99fad4c88eac35d9a5e96b34a/packages/client-web/src/connection/web_connection.ts#L200C7-L200C23
412-
const response = await fetch(`${this.host}/?${searchParams.toString()}`, {
413-
...(includeCredentials ? { credentials: 'include' } : {}),
414-
signal: abort_signal,
415-
method: 'GET',
416-
headers,
417-
});
381+
const { createClient, ResultSet } = await import(
382+
'@clickhouse/client-web'
383+
);
418384

419-
// TODO: Send command to CH to cancel query on abort_signal
420-
if (!response.ok) {
421-
if (!isSuccessfulResponse(response.status)) {
422-
const text = await response.text();
423-
throw new ClickHouseQueryError(`${text}`, debugSql);
385+
const isLocalMode = this.username != null && this.password != null;
386+
if (isLocalMode) {
387+
// LocalMode may potentially interact directly with a db, so it needs to
388+
// send a get request. @clickhouse/client-web does not currently support
389+
// querying via GET
390+
const includeCredentials = !isLocalMode;
391+
const includeCorsHeader = isLocalMode;
392+
393+
const searchParams = new URLSearchParams([
394+
...(includeCorsHeader ? [['add_http_cors_header', '1']] : []),
395+
['query', query],
396+
['default_format', format],
397+
['date_time_output_format', 'iso'],
398+
['wait_end_of_query', '0'],
399+
['cancel_http_readonly_queries_on_client_close', '1'],
400+
...(this.username ? [['user', this.username]] : []),
401+
...(this.password ? [['password', this.password]] : []),
402+
...(queryId ? [['query_id', queryId]] : []),
403+
...Object.entries(query_params).map(([key, value]) => [
404+
`param_${key}`,
405+
value,
406+
]),
407+
...Object.entries(clickhouse_settings ?? {}).map(([key, value]) => [
408+
key,
409+
value,
410+
]),
411+
]);
412+
const headers = {};
413+
if (!isLocalMode && connectionId) {
414+
headers['x-hyperdx-connection-id'] = connectionId;
415+
}
416+
// https://github.com/ClickHouse/clickhouse-js/blob/1ebdd39203730bb99fad4c88eac35d9a5e96b34a/packages/client-web/src/connection/web_connection.ts#L200C7-L200C23
417+
const response = await fetch(
418+
`${this.host}/?${searchParams.toString()}`,
419+
{
420+
...(includeCredentials ? { credentials: 'include' } : {}),
421+
signal: abort_signal,
422+
method: 'GET',
423+
headers,
424+
},
425+
);
426+
427+
// TODO: Send command to CH to cancel query on abort_signal
428+
if (!response.ok) {
429+
if (!isSuccessfulResponse(response.status)) {
430+
const text = await response.text();
431+
throw new ClickHouseQueryError(`${text}`, debugSql);
432+
}
424433
}
425-
}
426434

427-
if (response.body == null) {
428-
// TODO: Handle empty responses better?
429-
throw new Error('Unexpected empty response from ClickHouse');
435+
if (response.body == null) {
436+
// TODO: Handle empty responses better?
437+
throw new Error('Unexpected empty response from ClickHouse');
438+
}
439+
return new ResultSet<Format>(
440+
response.body,
441+
format,
442+
queryId ?? '',
443+
getResponseHeaders(response),
444+
);
445+
} else {
446+
if (connectionId === undefined) {
447+
throw new Error('ConnectionId must be defined');
448+
}
449+
const clickhouseClient = createClient({
450+
url: window.origin,
451+
pathname: this.host,
452+
http_headers: { 'x-hyperdx-connection-id': connectionId },
453+
clickhouse_settings: {
454+
date_time_output_format: 'iso',
455+
wait_end_of_query: 0,
456+
cancel_http_readonly_queries_on_client_close: 1,
457+
},
458+
compression: {
459+
response: true,
460+
},
461+
});
462+
return clickhouseClient.query<Format>({
463+
query,
464+
query_params,
465+
format,
466+
abort_signal,
467+
clickhouse_settings,
468+
query_id: queryId,
469+
}) as Promise<BaseResultSet<ReadableStream, Format>>;
430470
}
431-
return new ResultSet<T>(
432-
response.body,
433-
format as T,
434-
queryId ?? '',
435-
getResponseHeaders(response),
436-
);
437471
} else if (isNode) {
438472
const { createClient } = await import('@clickhouse/client');
439473
const _client = createClient({
@@ -448,14 +482,14 @@ export class ClickhouseClient {
448482
});
449483

450484
// TODO: Custom error handling
451-
return _client.query({
485+
return _client.query<Format>({
452486
query,
453487
query_params,
454-
format: format as T,
488+
format,
455489
abort_signal,
456490
clickhouse_settings,
457491
query_id: queryId,
458-
}) as unknown as BaseResultSet<any, T>;
492+
}) as unknown as Promise<BaseResultSet<ReadableStream, Format>>;
459493
} else {
460494
throw new Error(
461495
'ClickhouseClient is only supported in the browser or node environment',

0 commit comments

Comments
 (0)