Skip to content

Commit a4b6915

Browse files
committed
[WIP] feat(clickhouse): Use ClickHouse query with parameters via HTTP interface
1 parent 8e815c2 commit a4b6915

File tree

2 files changed

+62
-3
lines changed

2 files changed

+62
-3
lines changed

packages/cubejs-clickhouse-driver/src/ClickHouseDriver.ts

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -222,12 +222,41 @@ export class ClickHouseDriver extends BaseDriver implements DriverInterface {
222222
true;
223223
}
224224

225-
public async query(query: string, values: unknown[]) {
225+
public async query(query: string, values?: unknown[]) {
226226
return this.queryResponse(query, values).then((res: any) => this.normaliseResponse(res));
227227
}
228228

229-
protected queryResponse(query: string, values: unknown[]) {
230-
const formattedQuery = sqlstring.format(query, values);
229+
protected queryResponse(query: string, values?: unknown[]) {
230+
// todo drop this
231+
console.log('queryResponse call', query, values);
232+
233+
// const formattedQuery = sqlstring.format(query, values);
234+
const formattedQuery = query;
235+
236+
// `queryOptions` object will be merged into query params via querystring.stringify
237+
// https://github.com/cube-js/apla-node-clickhouse/blob/5a6577fc97ba6911171753fc65b2cd2f6170f2f7/src/clickhouse.js#L347-L348
238+
// https://github.com/cube-js/apla-node-clickhouse/blob/5a6577fc97ba6911171753fc65b2cd2f6170f2f7/src/clickhouse.js#L265-L266
239+
// https://github.com/cube-js/apla-node-clickhouse/blob/5a6577fc97ba6911171753fc65b2cd2f6170f2f7/src/clickhouse.js#L336-L338
240+
// https://github.com/cube-js/apla-node-clickhouse/blob/5a6577fc97ba6911171753fc65b2cd2f6170f2f7/src/clickhouse.js#L173-L175
241+
242+
// We can use `toSearchParams` or `formatQueryParams` from `@clickhouse/client-common` to prepare params, and extract only interesting ones
243+
// Beware - these functions marked as "For implementations usage only - should not be re-exported", so, probably, ot could be moved or disappear completely
244+
// https://github.com/ClickHouse/clickhouse-js/blob/a15cce93545c792852e34c05ce31954c75d11486/packages/client-common/src/utils/url.ts#L57-L61
245+
246+
// HTTP interface itself is documented, so it should be fine
247+
// https://clickhouse.com/docs/en/interfaces/cli#cli-queries-with-parameters
248+
// https://clickhouse.com/docs/en/interfaces/http#cli-queries-with-parameters
249+
250+
const paramsValues = Object.fromEntries((values ?? []).map((value, idx) => {
251+
const paramName = this.paramName(idx);
252+
const paramKey = `param_${paramName}`;
253+
// TODO prepare value
254+
const preparedValue = value;
255+
return [paramKey, preparedValue];
256+
}));
257+
258+
// todo drop this
259+
console.log('queryResponse prepared', formattedQuery, paramsValues);
231260

232261
return this.withConnection((connection, queryId) => connection.querying(formattedQuery, {
233262
dataObjects: true,
@@ -241,6 +270,9 @@ export class ClickHouseDriver extends BaseDriver implements DriverInterface {
241270
//
242271
//
243272
...(this.readOnlyMode ? {} : { join_use_nulls: 1 }),
273+
274+
// Add parameter values to query string
275+
...paramsValues,
244276
}
245277
}));
246278
}
@@ -309,6 +341,15 @@ export class ClickHouseDriver extends BaseDriver implements DriverInterface {
309341
return [{ schema_name: this.config.queryOptions.database }];
310342
}
311343

344+
protected paramName(paramIndex: number): string {
345+
return `p${paramIndex}`;
346+
}
347+
348+
public param(paramIndex: number): string {
349+
// TODO not always string
350+
return `{${this.paramName(paramIndex)}:String}`;
351+
}
352+
312353
public async stream(
313354
query: string,
314355
values: unknown[],

packages/cubejs-schema-compiler/src/adapter/ClickHouseQuery.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { BaseQuery } from './BaseQuery';
33
import { BaseFilter } from './BaseFilter';
44
import { UserError } from '../compiler/UserError';
55
import { BaseTimeDimension } from './BaseTimeDimension';
6+
import { ParamAllocator } from './ParamAllocator';
67

78
const GRANULARITY_TO_INTERVAL = {
89
day: 'Day',
@@ -14,13 +15,22 @@ const GRANULARITY_TO_INTERVAL = {
1415
year: 'Year',
1516
};
1617

18+
class ClickHouseParamAllocator extends ParamAllocator {
19+
public paramPlaceHolder(paramIndex: number): string {
20+
// TODO not always string
21+
return `{p${paramIndex}:String}`;
22+
}
23+
}
24+
1725
class ClickHouseFilter extends BaseFilter {
1826
public likeIgnoreCase(column, not, param, type) {
1927
const p = (!type || type === 'contains' || type === 'ends') ? '%' : '';
2028
const s = (!type || type === 'contains' || type === 'starts') ? '%' : '';
2129
return `lower(${column}) ${not ? 'NOT' : ''} LIKE CONCAT('${p}', lower(${this.allocateParam(param)}), '${s}')`;
2230
}
2331

32+
// todo replace ? with proper params
33+
// todo how to get index for this?
2434
public castParameter() {
2535
if (this.measure || this.definition().type === 'number') {
2636
// TODO here can be measure type of string actually
@@ -31,6 +41,10 @@ class ClickHouseFilter extends BaseFilter {
3141
}
3242

3343
export class ClickHouseQuery extends BaseQuery {
44+
public newParamAllocator(expressionParams) {
45+
return new ClickHouseParamAllocator(expressionParams);
46+
}
47+
3448
public newFilter(filter) {
3549
return new ClickHouseFilter(this, filter);
3650
}
@@ -39,6 +53,8 @@ export class ClickHouseQuery extends BaseQuery {
3953
return `\`${name}\``;
4054
}
4155

56+
// TODO override timeStampParam
57+
4258
public convertTz(field) {
4359
//
4460
// field yields a Date or a DateTime so add in the extra toDateTime64 to support the Date case
@@ -268,6 +284,8 @@ export class ClickHouseQuery extends BaseQuery {
268284

269285
public sqlTemplates() {
270286
const templates = super.sqlTemplates();
287+
// TODO not every param is a string
288+
templates.params.param = '{p{{ param_index }}:String}';
271289
templates.functions.DATETRUNC = 'DATE_TRUNC({{ args_concat }})';
272290
// TODO: Introduce additional filter in jinja? or parseDateTimeBestEffort?
273291
// https://github.com/ClickHouse/ClickHouse/issues/19351

0 commit comments

Comments
 (0)