Skip to content

Commit 7cb98a4

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

File tree

5 files changed

+83
-11
lines changed

5 files changed

+83
-11
lines changed

packages/cubejs-clickhouse-driver/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"integration:clickhouse": "jest dist/test"
2828
},
2929
"dependencies": {
30+
"@clickhouse/client-common": "^1.8.0",
3031
"@cubejs-backend/apla-clickhouse": "^1.7",
3132
"@cubejs-backend/base-driver": "1.1.3",
3233
"@cubejs-backend/shared": "1.1.3",

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

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ import {
2323
} from '@cubejs-backend/base-driver';
2424
import genericPool, { Pool } from 'generic-pool';
2525
import { v4 as uuidv4 } from 'uuid';
26+
// TODO get rid of sqlstring completely
2627
import sqlstring from 'sqlstring';
28+
import { formatQueryParams } from '@clickhouse/client-common';
2729

2830
import { HydrationStream, transformRow } from './HydrationStream';
2931

@@ -222,12 +224,40 @@ export class ClickHouseDriver extends BaseDriver implements DriverInterface {
222224
true;
223225
}
224226

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

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

232262
return this.withConnection((connection, queryId) => connection.querying(formattedQuery, {
233263
dataObjects: true,
@@ -241,6 +271,9 @@ export class ClickHouseDriver extends BaseDriver implements DriverInterface {
241271
//
242272
//
243273
...(this.readOnlyMode ? {} : { join_use_nulls: 1 }),
274+
275+
// Add parameter values to query string
276+
...paramsValues,
244277
}
245278
}));
246279
}
@@ -309,6 +342,15 @@ export class ClickHouseDriver extends BaseDriver implements DriverInterface {
309342
return [{ schema_name: this.config.queryOptions.database }];
310343
}
311344

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

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

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,23 @@ describe('ClickHouseDriver', () => {
5858
[]
5959
);
6060

61-
await driver.query('INSERT INTO test.types_test VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)', [
61+
function mkPlaceholdersTuple(len: number): string {
62+
const parts = new Array(len).fill('').map((_, idx) => driver.param(idx));
63+
return `(${parts.join(',')})`;
64+
}
65+
66+
async function insert(table: string, values: Array<unknown>): Promise<void> {
67+
const placeholders = mkPlaceholdersTuple(values.length);
68+
await driver.query(`INSERT INTO ${table} VALUES ${placeholders}`, values);
69+
}
70+
71+
await insert('test.types_test', [
6272
'2020-01-01', '2020-01-01 00:00:00', '2020-01-01 00:00:00.000', '2020-01-01 00:00:00.000000', '2020-01-01 00:00:00.000000000', 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.01, 1.01, 1.01, 'hello', 'world'
6373
]);
64-
await driver.query('INSERT INTO test.types_test VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)', [
74+
await insert('test.types_test', [
6575
'2020-01-02', '2020-01-02 00:00:00', '2020-01-02 00:00:00.123', '2020-01-02 00:00:00.123456', '2020-01-02 00:00:00.123456789', 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2.02, 2.02, 2.02, 'hello', 'world'
6676
]);
67-
await driver.query('INSERT INTO test.types_test VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)', [
77+
await insert('test.types_test', [
6878
'2020-01-03', '2020-01-03 00:00:00', '2020-01-03 00:00:00.234', '2020-01-03 00:00:00.234567', '2020-01-03 00:00:00.234567890', 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3.03, 3.03, 3.03, 'hello', 'world'
6979
]);
7080
});
@@ -205,8 +215,8 @@ describe('ClickHouseDriver', () => {
205215
try {
206216
await driver.createSchemaIfNotExists(name);
207217
await driver.query(`CREATE TABLE ${name}.test (x Int32, s String) ENGINE Log`, []);
208-
await driver.query(`INSERT INTO ${name}.test VALUES (?, ?), (?, ?), (?, ?)`, [1, 'str1', 2, 'str2', 3, 'str3']);
209-
const values = await driver.query(`SELECT * FROM ${name}.test WHERE x = ?`, [2]);
218+
await driver.query(`INSERT INTO ${name}.test VALUES ({p0:Int32}, {p1:String}), ({p2:Int32}, {p3:String}), ({p4:Int32}, {p5:String})`, [1, 'str1', 2, 'str2', 3, 'str3']);
219+
const values = await driver.query(`SELECT * FROM ${name}.test WHERE x = {p0:Int32}`, [2]);
210220
expect(values).toEqual([{ x: '2', s: 'str2' }]);
211221
} finally {
212222
await driver.query(`DROP DATABASE ${name}`, []);
@@ -220,10 +230,10 @@ describe('ClickHouseDriver', () => {
220230
try {
221231
await driver.createSchemaIfNotExists(name);
222232
await driver.query(`CREATE TABLE ${name}.a (x Int32, s String) ENGINE Log`, []);
223-
await driver.query(`INSERT INTO ${name}.a VALUES (?, ?), (?, ?), (?, ?)`, [1, 'str1', 2, 'str2', 3, 'str3']);
233+
await driver.query(`INSERT INTO ${name}.a VALUES ({p0:Int32}, {p1:String}), ({p2:Int32}, {p3:String}), ({p4:Int32}, {p5:String})`, [1, 'str1', 2, 'str2', 3, 'str3']);
224234

225235
await driver.query(`CREATE TABLE ${name}.b (x Int32, s String) ENGINE Log`, []);
226-
await driver.query(`INSERT INTO ${name}.b VALUES (?, ?), (?, ?), (?, ?)`, [2, 'str2', 3, 'str3', 4, 'str4']);
236+
await driver.query(`INSERT INTO ${name}.b VALUES ({p0:Int32}, {p1:String}), ({p2:Int32}, {p3:String}), ({p4:Int32}, {p5:String})`, [2, 'str2', 3, 'str3', 4, 'str4']);
227237

228238
const values = await driver.query(`SELECT * FROM ${name}.a LEFT OUTER JOIN ${name}.b ON a.x = b.x`, []);
229239
expect(values).toEqual([
@@ -245,7 +255,7 @@ describe('ClickHouseDriver', () => {
245255

246256
it('datetime with specific timezone', async () => {
247257
await doWithDriver(async (driver) => {
248-
const rows = await driver.query('SELECT toDateTime(?, \'Asia/Istanbul\') as dt', [
258+
const rows = await driver.query('SELECT toDateTime({p0:String}, \'Asia/Istanbul\') as dt', [
249259
'2020-01-01 00:00:00'
250260
]);
251261
expect(rows).toEqual([{

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

Lines changed: 14 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,6 +15,13 @@ 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') ? '%' : '';
@@ -31,6 +39,10 @@ class ClickHouseFilter extends BaseFilter {
3139
}
3240

3341
export class ClickHouseQuery extends BaseQuery {
42+
public newParamAllocator(expressionParams) {
43+
return new ClickHouseParamAllocator(expressionParams);
44+
}
45+
3446
public newFilter(filter) {
3547
return new ClickHouseFilter(this, filter);
3648
}
@@ -268,6 +280,8 @@ export class ClickHouseQuery extends BaseQuery {
268280

269281
public sqlTemplates() {
270282
const templates = super.sqlTemplates();
283+
// TODO not every param is a string
284+
templates.params.param = '{p{{ param_index }}:String}';
271285
templates.functions.DATETRUNC = 'DATE_TRUNC({{ args_concat }})';
272286
// TODO: Introduce additional filter in jinja? or parseDateTimeBestEffort?
273287
// https://github.com/ClickHouse/ClickHouse/issues/19351

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4222,6 +4222,11 @@
42224222
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
42234223
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
42244224

4225+
"@clickhouse/client-common@^1.8.0":
4226+
version "1.8.0"
4227+
resolved "https://registry.yarnpkg.com/@clickhouse/client-common/-/client-common-1.8.0.tgz#6775c4f1f4b56f393cbacec0a1b544dafcf8d5ed"
4228+
integrity sha512-aQgH0UODGuFHfL8rgeLSrGCoh3NCoNUs0tFGl0o79iyfASfvWtT/K/X3RM0QJpXXOgXpB//T2nD5XvCFtdk32w==
4229+
42254230
"@codemirror/highlight@^0.19.0":
42264231
version "0.19.6"
42274232
resolved "https://registry.yarnpkg.com/@codemirror/highlight/-/highlight-0.19.6.tgz#7f2e066f83f5649e8e0748a3abe0aaeaf64b8ac2"

0 commit comments

Comments
 (0)