diff --git a/packages/cubejs-clickhouse-driver/src/ClickHouseDriver.ts b/packages/cubejs-clickhouse-driver/src/ClickHouseDriver.ts index 465e398afaf8e..e34102722606c 100644 --- a/packages/cubejs-clickhouse-driver/src/ClickHouseDriver.ts +++ b/packages/cubejs-clickhouse-driver/src/ClickHouseDriver.ts @@ -257,7 +257,7 @@ export class ClickHouseDriver extends BaseDriver implements DriverInterface { } protected queryResponse(query: string, values: unknown[]): Promise>> { - const formattedQuery = sqlstring.format(query, values); + const formattedQuery = query.replace(/___ClickHouseParam_(\d+)___/g, (_, idx) => sqlstring.escape(values[idx])); return this.withCancel(async (connection, queryId, signal) => { try { @@ -306,6 +306,10 @@ export class ClickHouseDriver extends BaseDriver implements DriverInterface { await this.client.close(); } + public param(paramIndex: number): string { + return `___ClickHouseParam_${paramIndex}___`; + } + public informationSchemaQuery() { return ` SELECT name as column_name, @@ -362,7 +366,7 @@ export class ClickHouseDriver extends BaseDriver implements DriverInterface { const queryId = uuidv4(); try { - const formattedQuery = sqlstring.format(query, values); + const formattedQuery = query.replace(/___ClickHouseParam_(\d+)___/g, (_, idx) => sqlstring.escape(values[idx])); const format = 'JSONCompactEachRowWithNamesAndTypes'; @@ -486,7 +490,7 @@ export class ClickHouseDriver extends BaseDriver implements DriverInterface { } public getTablesQuery(schemaName: string): Promise { - return this.query('SELECT name as table_name FROM system.tables WHERE database = ?', [schemaName]); + return this.query('SELECT name as table_name FROM system.tables WHERE database = ___ClickHouseParam_0___', [schemaName]); } public override async dropTable(tableName: string, _options?: QueryOptions): Promise { @@ -598,7 +602,7 @@ export class ClickHouseDriver extends BaseDriver implements DriverInterface { const { bucketName, path } = this.parseBucketUrl(this.config.exportBucket.bucketName); const exportPrefix = path ? `${path}/${uuidv4()}` : uuidv4(); - const formattedQuery = sqlstring.format(` + const formattedQuery = ` INSERT INTO FUNCTION s3( 'https://${bucketName}.s3.${this.config.exportBucket.region}.amazonaws.com/${exportPrefix}/export.csv.gz', @@ -607,7 +611,7 @@ export class ClickHouseDriver extends BaseDriver implements DriverInterface { 'CSV' ) ${sql} - `, params); + `.replace(/___ClickHouseParam_(\d+)___/g, (_, idx) => sqlstring.escape(params[idx])); await this.command(formattedQuery); diff --git a/packages/cubejs-clickhouse-driver/test/ClickHouseDriver.test.ts b/packages/cubejs-clickhouse-driver/test/ClickHouseDriver.test.ts index 9db85c8ea7cf1..1cf4a895d0ce3 100644 --- a/packages/cubejs-clickhouse-driver/test/ClickHouseDriver.test.ts +++ b/packages/cubejs-clickhouse-driver/test/ClickHouseDriver.test.ts @@ -210,7 +210,7 @@ describe('ClickHouseDriver', () => { await driver.createSchemaIfNotExists(name); await driver.command(`CREATE TABLE ${name}.test (x Int32, s String) ENGINE Log`); await driver.insert(`${name}.test`, [[1, 'str1'], [2, 'str2'], [3, 'str3']]); - const values = await driver.query(`SELECT * FROM ${name}.test WHERE x = ?`, [2]); + const values = await driver.query(`SELECT * FROM ${name}.test WHERE x = ___ClickHouseParam_0___`, [2]); expect(values).toEqual([{ x: '2', s: 'str2' }]); } finally { await driver.command(`DROP DATABASE ${name}`); @@ -249,7 +249,7 @@ describe('ClickHouseDriver', () => { it('datetime with specific timezone', async () => { await doWithDriver(async (driver) => { - const rows = await driver.query('SELECT toDateTime(?, \'Asia/Istanbul\') as dt', [ + const rows = await driver.query('SELECT toDateTime(___ClickHouseParam_0___, \'Asia/Istanbul\') as dt', [ '2020-01-01 00:00:00' ]); expect(rows).toEqual([{ diff --git a/packages/cubejs-schema-compiler/src/adapter/ClickHouseQuery.ts b/packages/cubejs-schema-compiler/src/adapter/ClickHouseQuery.ts index 0c392ea888c00..88611ea467d59 100644 --- a/packages/cubejs-schema-compiler/src/adapter/ClickHouseQuery.ts +++ b/packages/cubejs-schema-compiler/src/adapter/ClickHouseQuery.ts @@ -3,6 +3,7 @@ import { BaseQuery } from './BaseQuery'; import { BaseFilter } from './BaseFilter'; import { UserError } from '../compiler/UserError'; import { BaseTimeDimension } from './BaseTimeDimension'; +import { ParamAllocator } from './ParamAllocator'; const GRANULARITY_TO_INTERVAL = { day: 'Day', @@ -30,7 +31,17 @@ class ClickHouseFilter extends BaseFilter { } } +class ClickHouseParamAllocator extends ParamAllocator { + public paramPlaceHolder(paramIndex) { + return `___ClickHouseParam_${paramIndex}___`; + } +} + export class ClickHouseQuery extends BaseQuery { + public newParamAllocator(expressionParams) { + return new ClickHouseParamAllocator(expressionParams); + } + public newFilter(filter) { return new ClickHouseFilter(this, filter); } diff --git a/packages/cubejs-schema-compiler/test/integration/clickhouse/ClickHouseDbRunner.ts b/packages/cubejs-schema-compiler/test/integration/clickhouse/ClickHouseDbRunner.ts index 67138b027802e..00f6504903917 100644 --- a/packages/cubejs-schema-compiler/test/integration/clickhouse/ClickHouseDbRunner.ts +++ b/packages/cubejs-schema-compiler/test/integration/clickhouse/ClickHouseDbRunner.ts @@ -2,7 +2,7 @@ import { createClient } from '@clickhouse/client'; import type { ClickHouseClient, ResponseJSON } from '@clickhouse/client'; import { GenericContainer } from 'testcontainers'; import type { StartedTestContainer } from 'testcontainers'; -import { format as formatSql } from 'sqlstring'; +import { escape } from 'sqlstring'; import { v4 as uuidv4 } from 'uuid'; import moment from 'moment'; @@ -158,7 +158,7 @@ export class ClickHouseDbRunner extends BaseDbRunner { const requests = queries .map(async ([query, params]) => { const resultSet = await clickHouse.query({ - query: formatSql(query, params), + query: query.replace(/___ClickHouseParam_(\d+)___/g, (_, idx) => escape(params[idx])), format: 'JSON', clickhouse_settings: { join_use_nulls: 1,