diff --git a/packages/cubejs-clickhouse-driver/package.json b/packages/cubejs-clickhouse-driver/package.json index 7f30360694fbc..5a98e20933168 100644 --- a/packages/cubejs-clickhouse-driver/package.json +++ b/packages/cubejs-clickhouse-driver/package.json @@ -27,7 +27,7 @@ "integration:clickhouse": "jest dist/test" }, "dependencies": { - "@cubejs-backend/apla-clickhouse": "^1.7", + "@clickhouse/client": "^1.7.0", "@cubejs-backend/base-driver": "1.1.4", "@cubejs-backend/shared": "1.1.4", "generic-pool": "^3.6.0", diff --git a/packages/cubejs-clickhouse-driver/src/ClickHouseDriver.ts b/packages/cubejs-clickhouse-driver/src/ClickHouseDriver.ts index 4cd1c5049426b..d7c752f3bc2a5 100644 --- a/packages/cubejs-clickhouse-driver/src/ClickHouseDriver.ts +++ b/packages/cubejs-clickhouse-driver/src/ClickHouseDriver.ts @@ -18,16 +18,20 @@ import { QuerySchemasResult, StreamOptions, StreamTableDataWithTypes, + TableQueryResult, TableStructure, UnloadOptions, } from '@cubejs-backend/base-driver'; -import genericPool, { Pool } from 'generic-pool'; + +import { Readable } from 'node:stream'; +import { ClickHouseClient, createClient } from '@clickhouse/client'; +import type { ResponseJSON } from '@clickhouse/client'; +import genericPool from 'generic-pool'; +import type { Factory, Pool } from 'generic-pool'; import { v4 as uuidv4 } from 'uuid'; import sqlstring from 'sqlstring'; -import { HydrationStream, transformRow } from './HydrationStream'; - -const ClickHouse = require('@cubejs-backend/apla-clickhouse'); +import { transformRow, transformStreamRow } from './HydrationStream'; const ClickhouseTypeToGeneric: Record = { enum: 'text', @@ -54,14 +58,31 @@ const ClickhouseTypeToGeneric: Record = { enum16: 'text', }; -interface ClickHouseDriverOptions { +export interface ClickHouseDriverOptions { host?: string, port?: string, auth?: string, protocol?: string, database?: string, readOnly?: boolean, + // TODO how to treat this in a BC way? queryOptions?: object, + + /** + * Data source name. + */ + dataSource?: string, + + /** + * Max pool size value for the [cube]<-->[db] pool. + */ + maxPoolSize?: number, + + /** + * Time to wait for a response from a connection after validation + * request before determining it as not valid. Default - 10000 ms. + */ + testConnectionTimeout?: number, } interface ClickhouseDriverExportRequiredAWS { @@ -86,7 +107,9 @@ export class ClickHouseDriver extends BaseDriver implements DriverInterface { return 5; } - protected readonly pool: Pool; + protected readonly connectionFactory: Factory; + + protected readonly pool: Pool; protected readonly readOnlyMode: boolean; @@ -96,23 +119,7 @@ export class ClickHouseDriver extends BaseDriver implements DriverInterface { * Class constructor. */ public constructor( - config: ClickHouseDriverOptions & { - /** - * Data source name. - */ - dataSource?: string, - - /** - * Max pool size value for the [cube]<-->[db] pool. - */ - maxPoolSize?: number, - - /** - * Time to wait for a response from a connection after validation - * request before determining it as not valid. Default - 10000 ms. - */ - testConnectionTimeout?: number, - } = {}, + config: ClickHouseDriverOptions = {}, ) { super({ testConnectionTimeout: config.testConnectionTimeout, @@ -122,19 +129,36 @@ export class ClickHouseDriver extends BaseDriver implements DriverInterface { config.dataSource || assertDataSource('default'); + // TODO recheck everything in config for new driver + const host = config.host ?? getEnv('dbHost', { dataSource }); + const port = config.port ?? getEnv('dbPort', { dataSource }) ?? 8123; + const protocol = config.protocol ?? getEnv('dbSsl', { dataSource }) ? 'https:' : 'http:'; + // TODO proper value here, with proper back compat, and treating protocol + const url = `${protocol}//${host}:${port}`; + // TODO drop this + console.log('ClickHouseDriver will use url', url); + + // TODO parse username and apssword from config.auth, from a string like this: + // `${getEnv('dbUser', { dataSource })}:${getEnv('dbPass', { dataSource })}` + + const username = getEnv('dbUser', { dataSource }); + const password = getEnv('dbPass', { dataSource }); this.config = { - host: getEnv('dbHost', { dataSource }), - port: getEnv('dbPort', { dataSource }), - auth: - getEnv('dbUser', { dataSource }) || - getEnv('dbPass', { dataSource }) - ? `${ - getEnv('dbUser', { dataSource }) - }:${ - getEnv('dbPass', { dataSource }) - }` - : '', - protocol: getEnv('dbSsl', { dataSource }) ? 'https:' : 'http:', + // host: getEnv('dbHost', { dataSource }), + // port: getEnv('dbPort', { dataSource }), + url, + // auth: + // getEnv('dbUser', { dataSource }) || + // getEnv('dbPass', { dataSource }) + // ? `${ + // getEnv('dbUser', { dataSource }) + // }:${ + // getEnv('dbPass', { dataSource }) + // }` + // : '', + username, + password, + // protocol: getEnv('dbSsl', { dataSource }) ? 'https:' : 'http:', queryOptions: { database: getEnv('dbName', { dataSource }) || @@ -148,10 +172,17 @@ export class ClickHouseDriver extends BaseDriver implements DriverInterface { this.readOnlyMode = getEnv('clickhouseReadOnly', { dataSource }) === 'true'; - this.pool = genericPool.createPool({ - create: async () => new ClickHouse({ - ...this.config, - queryOptions: { + this.connectionFactory = { + create: async () => createClient({ + // ...this.config, + + url: this.config.url, + username: this.config.username, + password: this.config.password, + + database: this.config.queryOptions.database, + session_id: uuidv4(), + clickhouse_settings: { // // // If ClickHouse user's permissions are restricted with "readonly = 1", @@ -160,54 +191,72 @@ export class ClickHouseDriver extends BaseDriver implements DriverInterface { // // ...(this.readOnlyMode ? {} : { join_use_nulls: 1 }), - session_id: uuidv4(), - ...this.config.queryOptions, - } + }, + + // TODO max_open_connections vs generic pool + max_open_connections: 1, }), - destroy: () => Promise.resolve() - }, { - min: 0, - max: - config.maxPoolSize || - getEnv('dbMaxPoolSize', { dataSource }) || - 8, - evictionRunIntervalMillis: 10000, - softIdleTimeoutMillis: 30000, - idleTimeoutMillis: 30000, - acquireTimeoutMillis: 20000 + validate: async (client) => { + const result = await client.ping(); + if (!result.success) { + this.databasePoolError(result.error); + } + return result.success; + }, + destroy: (client) => client.close(), + }; + + // TODO @clickhouse/client have internal pool, that does NOT guarantee same connection, and can break with temp tables. Disable it? + this.pool = genericPool.createPool( + this.connectionFactory, + { + min: 0, + max: config.maxPoolSize || getEnv("dbMaxPoolSize", { dataSource }) || 8, + evictionRunIntervalMillis: 10000, + softIdleTimeoutMillis: 30000, + idleTimeoutMillis: 30000, + acquireTimeoutMillis: 20000, + } + ); + + // https://github.com/coopernurse/node-pool/blob/ee5db9ddb54ce3a142fde3500116b393d4f2f755/README.md#L220-L226 + this.pool.on('factoryCreateError', (err) => { + this.databasePoolError(err); + }); + this.pool.on('factoryDestroyError', (err) => { + this.databasePoolError(err); }); } - protected withConnection(fn: (con: any, queryId: string) => Promise) { - const self = this; - const connectionPromise = this.pool.acquire(); + protected withConnection(fn: (con: ClickHouseClient, queryId: string) => Promise): Promise { + console.log("withConnection call"); const queryId = uuidv4(); - let cancelled = false; - const cancelObj: any = {}; - - const promise: any = connectionPromise.then((connection: any) => { - cancelObj.cancel = async () => { - cancelled = true; - await self.withConnection(async conn => { - await conn.querying(`KILL QUERY WHERE query_id = '${queryId}'`); + const abortController = new AbortController(); + const { signal } = abortController; + + const promise = (async () => { + const connection = await this.pool.acquire(); + try { + signal.throwIfAborted(); + // TODO pass signal deeper, new driver supports abort signal, but does not do autokill + const result = await fn(connection, queryId); + signal.throwIfAborted(); + return result; + } finally { + await this.pool.release(connection); + } + })(); + // TODO why do we need this? + (promise as any).cancel = async () => { + abortController.abort(); + // TODO kill is sent thru same pool, which can be busy, use separate pool/connection. + await this.withConnection(async conn => { + await conn.command({ + query: `KILL QUERY WHERE query_id = '${queryId}'`, }); - }; - return fn(connection, queryId) - .then(res => this.pool.release(connection).then(() => { - if (cancelled) { - throw new Error('Query cancelled'); - } - return res; - })) - .catch((err) => this.pool.release(connection).then(() => { - if (cancelled) { - throw new Error('Query cancelled'); - } - throw err; - })); - }); - promise.cancel = () => cancelObj.cancel(); + }); + }; return promise; } @@ -222,41 +271,108 @@ export class ClickHouseDriver extends BaseDriver implements DriverInterface { true; } - public async query(query: string, values: unknown[]) { - return this.queryResponse(query, values).then((res: any) => this.normaliseResponse(res)); + public async query(query: string, values: unknown[]): Promise { + try { + const response = await this.queryResponse(query, values); + return this.normaliseResponse(response); + } catch (e) { + const newError = new Error(`Query failed: ${query}`, { cause: e }); + console.log(newError); + throw newError; + } } - protected queryResponse(query: string, values: unknown[]) { + protected queryResponse(query: string, values: unknown[]): Promise>> { + // TODO why do we even have inserts coming to query? is it accepted contract, or just tests misbehaviour? + const isInsert = query.trim().match(/^INSERT/i); + + console.log('ClickHouse queryResponse call', query); + const formattedQuery = sqlstring.format(query, values); - return this.withConnection((connection, queryId) => connection.querying(formattedQuery, { - dataObjects: true, - queryOptions: { + console.log('ClickHouse queryResponse prepared', formattedQuery); + + return this.withConnection(async (connection, queryId) => { + // if (formattedQuery.startsWith("CREATE TABLE")) { + // + // } + + if (isInsert) { + // INSERT queries does not work with `query` method due to query preparation (like adding FORMAT clause) + // And `insert` wants to construct query on its own + await connection.command({ + query: formattedQuery, + query_id: queryId, + clickhouse_settings: { + // + // + // If ClickHouse user's permissions are restricted with "readonly = 1", + // change settings queries are not allowed. Thus, "join_use_nulls" setting + // can not be changed + // + // + ...(this.readOnlyMode ? {} : { join_use_nulls: 1 }), + }, + }); + + return { + data: [], + }; + } + + const resultSet = await connection.query({ + query: formattedQuery, query_id: queryId, - // - // - // If ClickHouse user's permissions are restricted with "readonly = 1", - // change settings queries are not allowed. Thus, "join_use_nulls" setting - // can not be changed - // - // - ...(this.readOnlyMode ? {} : { join_use_nulls: 1 }), + format: 'JSON', + clickhouse_settings: { + // + // + // If ClickHouse user's permissions are restricted with "readonly = 1", + // change settings queries are not allowed. Thus, "join_use_nulls" setting + // can not be changed + // + // + ...(this.readOnlyMode ? {} : { join_use_nulls: 1 }), + }, + }); + console.log("queryResponse resultSet", query, resultSet.query_id, resultSet.response_headers); + + if (resultSet.response_headers['x-clickhouse-format'] !== 'JSON') { + // TODO explain why and when this happens + const results = await resultSet.text(); + console.log("queryResponse text results", query, results); + console.log("queryResponse text results JSON", JSON.stringify(results)); + return { + data: [], + }; + } else { + // We used format JSON, so we expect each row to be Record with column names as keys + const results = await resultSet.json>(); + console.log("queryResponse json results", query, results); + console.log("queryResponse json results JSON", JSON.stringify(results)); + return results; } - })); + + // 'content-type': 'text/plain; charset=UTF-8', + // vs + // 'content-type': 'application/json; charset=UTF-8', + // 'x-clickhouse-format': 'JSON', + }); } - protected normaliseResponse(res: any) { + protected normaliseResponse(res: ResponseJSON>): Array { if (res.data) { - const meta = res.meta.reduce( - (state: any, element: any) => ({ [element.name]: element, ...state }), + const meta = (res.meta ?? []).reduce>( + (state, element) => ({ [element.name]: element, ...state }), {} ); - res.data.forEach((row: any) => { + // TODO maybe use row-based format here as well? + res.data.forEach((row) => { transformRow(row, meta); }); } - return res.data; + return res.data as Array; } public async release() { @@ -315,51 +431,87 @@ export class ClickHouseDriver extends BaseDriver implements DriverInterface { // eslint-disable-next-line @typescript-eslint/no-unused-vars { highWaterMark }: StreamOptions ): Promise { - // eslint-disable-next-line no-underscore-dangle - const conn = await ( this.pool)._factory.create(); + console.log('ClickHouse stream call', query, values); + + const conn = await this.connectionFactory.create(); try { const formattedQuery = sqlstring.format(query, values); - return await new Promise((resolve, reject) => { - const options = { - queryOptions: { - query_id: uuidv4(), - // - // - // If ClickHouse user's permissions are restricted with "readonly = 1", - // change settings queries are not allowed. Thus, "join_use_nulls" setting - // can not be changed - // - // - ...(this.readOnlyMode ? {} : { join_use_nulls: 1 }), - } - }; + console.log('ClickHouse stream prepared', formattedQuery); - const originalStream = conn.query(formattedQuery, options, (err: Error | null, result: any) => { - if (err) { - reject(err); - } else { - const rowStream = new HydrationStream(result.meta); - originalStream.pipe(rowStream); - - resolve({ - rowStream, - types: result.meta.map((field: any) => ({ - name: field.name, - type: this.toGenericType(field.type), - })), - release: async () => { - // eslint-disable-next-line no-underscore-dangle - await ( this.pool)._factory.destroy(conn); - } - }); - } - }); + const resultSet = await conn.query({ + query: formattedQuery, + query_id: uuidv4(), + format: 'JSONCompactEachRowWithNamesAndTypes', + clickhouse_settings: { + // + // + // If ClickHouse user's permissions are restricted with "readonly = 1", + // change settings queries are not allowed. Thus, "join_use_nulls" setting + // can not be changed + // + // + ...(this.readOnlyMode ? {} : { join_use_nulls: 1 }), + } }); + // Array is okay, because we use fixed JSONCompactEachRowWithNamesAndTypes format + // And each row after first two will look like this: [42, "hello", [0,1]] + // https://clickhouse.com/docs/en/interfaces/formats#jsoncompacteachrowwithnamesandtypes + const resultSetStream = resultSet.stream>(); + + // TODO types are missing for this call + // const iter = resultSetStream[Symbol.asyncIterator](); + + const allRowsIter = (async function* allRowsIter() { + for await (const rowsBatch of resultSetStream) { + for (const row of rowsBatch) { + console.log('ClickHouse stream got row', formattedQuery, row); + yield row.json(); + } + } + }()); + + const first = await allRowsIter.next(); + if (first.done) { + throw new Error('Unexpected stream end before row with names'); + } + // TODO as + const names = first.value as Array; + const second = await allRowsIter.next(); + if (second.done) { + throw new Error('Unexpected stream end before row with types'); + } + // TODO as + const types = second.value as Array; + + if (names.length !== types.length) { + throw new Error(`Unexpected names and types length mismatch; names ${names.length} vs types ${types.length}`); + } + + const dataRowsIter = (async function* () { + for await (const row of allRowsIter) { + // TODO as + yield transformStreamRow(row as Array, names, types); + } + }()); + const rowStream = Readable.from(dataRowsIter); + + return { + rowStream, + types: names.map((name, idx) => { + const type = types[idx]; + return { + name, + type: this.toGenericType(type), + }; + }), + release: async () => { + await this.connectionFactory.destroy(conn); + } + }; } catch (e) { - // eslint-disable-next-line no-underscore-dangle - await ( this.pool)._factory.destroy(conn); + await this.connectionFactory.destroy(conn); throw e; } @@ -378,7 +530,7 @@ export class ClickHouseDriver extends BaseDriver implements DriverInterface { return { rows: this.normaliseResponse(response), - types: response.meta.map((field: any) => ({ + types: (response.meta ?? []).map((field) => ({ name: field.name, type: this.toGenericType(field.type), })), @@ -415,7 +567,7 @@ export class ClickHouseDriver extends BaseDriver implements DriverInterface { await this.query(`CREATE DATABASE IF NOT EXISTS ${schemaName}`, []); } - public getTablesQuery(schemaName: string) { + public getTablesQuery(schemaName: string): Promise { return this.query('SELECT name as table_name FROM system.tables WHERE database = ?', [schemaName]); } @@ -473,12 +625,19 @@ export class ClickHouseDriver extends BaseDriver implements DriverInterface { * Returns an array of queried fields meta info. */ public async queryColumnTypes(sql: string, params: unknown[]): Promise { - const columns = await this.query(`DESCRIBE ${sql}`, params); + // For DESCRIBE we expect that each row would have special structure + // See https://clickhouse.com/docs/en/sql-reference/statements/describe-table + // TODO complete this type + type DescribeRow = { + name: string, + type: string + }; + const columns = await this.query(`DESCRIBE ${sql}`, params); if (!columns) { throw new Error('Unable to describe table'); } - return columns.map((column: any) => ({ + return columns.map((column) => ({ name: column.name, type: this.toGenericType(column.type), })); diff --git a/packages/cubejs-clickhouse-driver/src/HydrationStream.ts b/packages/cubejs-clickhouse-driver/src/HydrationStream.ts index cb7bcc97dd5e5..71ce704ca69d9 100644 --- a/packages/cubejs-clickhouse-driver/src/HydrationStream.ts +++ b/packages/cubejs-clickhouse-driver/src/HydrationStream.ts @@ -1,41 +1,63 @@ -import stream, { TransformCallback } from 'stream'; +// import stream, { TransformCallback } from 'stream'; import * as moment from 'moment'; +function transformValue(type: string, value: unknown) { + if (value !== null) { + if (type.includes('DateTime64')) { + return moment.utc(value).format(moment.HTML5_FMT.DATETIME_LOCAL_MS); + } else if (type.includes('DateTime') /** Can be DateTime or DateTime('timezone') */) { + // expect DateTime to always be string + const valueStr = value as string; + return `${valueStr.substring(0, 10)}T${valueStr.substring(11, 22)}.000`; + } else if (type.includes('Date')) { + return `${value}T00:00:00.000`; + } else if (type.includes('Int') + || type.includes('Float') + || type.includes('Decimal') + ) { + // convert all numbers into strings + return `${value}`; + } + } + + return value; +} + // ClickHouse returns DateTime as strings in format "YYYY-DD-MM HH:MM:SS" // cube.js expects them in format "YYYY-DD-MMTHH:MM:SS.000", so translate them based on the metadata returned // // ClickHouse returns some number types as js numbers, others as js string, normalise them all to strings -export function transformRow(row: Record, meta: any) { +export function transformRow(row: Record, meta: any) { for (const [fieldName, value] of Object.entries(row)) { - if (value !== null) { - const metaForField = meta[fieldName]; - if (metaForField.type.includes('DateTime64')) { - row[fieldName] = moment.utc(value).format(moment.HTML5_FMT.DATETIME_LOCAL_MS); - } else if (metaForField.type.includes('DateTime') /** Can be DateTime or DateTime('timezone') */) { - row[fieldName] = `${value.substring(0, 10)}T${value.substring(11, 22)}.000`; - } else if (metaForField.type.includes('Date')) { - row[fieldName] = `${value}T00:00:00.000`; - } else if (metaForField.type.includes('Int') - || metaForField.type.includes('Float') - || metaForField.type.includes('Decimal') - ) { - // convert all numbers into strings - row[fieldName] = `${value}`; - } - } + const metaForField = meta[fieldName]; + row[fieldName] = transformValue(metaForField.type, value); } } -export class HydrationStream extends stream.Transform { - public constructor(meta: any) { - super({ - objectMode: true, - transform(row: any[], encoding: BufferEncoding, callback: TransformCallback) { - transformRow(row, meta); - - this.push(row); - callback(); - } - }); +export function transformStreamRow(row: Array, names: Array, types: Array): Record { + if (row.length !== names.length) { + throw new Error(`Unexpected row and names/types length mismatch; row ${row.length} vs names ${names.length}`); } + + return row.reduce>((rowObj, value, idx) => { + const name = names[idx]; + const type = types[idx]; + rowObj[name] = transformValue(type, value); + return rowObj; + }, Object.create(null)); } + +// // TODO get rid of streams in favor of async iter +// export class HydrationStream extends stream.Transform { +// public constructor(meta: any) { +// super({ +// objectMode: true, +// transform(row: any[], encoding: BufferEncoding, callback: TransformCallback) { +// transformRow(row, meta); +// +// this.push(row); +// callback(); +// } +// }); +// } +// } diff --git a/packages/cubejs-clickhouse-driver/test/ClickHouseDriver.test.ts b/packages/cubejs-clickhouse-driver/test/ClickHouseDriver.test.ts index c501bd3568b22..e1b987ae60058 100644 --- a/packages/cubejs-clickhouse-driver/test/ClickHouseDriver.test.ts +++ b/packages/cubejs-clickhouse-driver/test/ClickHouseDriver.test.ts @@ -2,18 +2,23 @@ import { ClickhouseDBRunner } from '@cubejs-backend/testing-shared'; import { streamToArray } from '@cubejs-backend/shared'; import { ClickHouseDriver } from '../src'; +import type { ClickHouseDriverOptions } from '../src'; describe('ClickHouseDriver', () => { jest.setTimeout(20 * 1000); let container: any; - let config: any; + let config: ClickHouseDriverOptions; const doWithDriver = async (cb: (driver: ClickHouseDriver) => Promise) => { const driver = new ClickHouseDriver(config); try { await cb(driver); + } catch (e) { + const newError = new Error('doWithDriver failed', { cause: e }); + console.log(newError); + throw newError; } finally { await driver.release(); } @@ -307,9 +312,9 @@ describe('ClickHouseDriver', () => { { name: 'enum16', type: 'text' }, ]); expect(await streamToArray(tableData.rowStream as any)).toEqual([ - ['2020-01-01T00:00:00.000', '2020-01-01T00:00:00.000', '2020-01-01T00:00:00.000', '2020-01-01T00:00:00.000', '2020-01-01T00:00:00.000', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1.01', '1.01', '1.01', 'hello', 'world'], - ['2020-01-02T00:00:00.000', '2020-01-02T00:00:00.000', '2020-01-02T00:00:00.123', '2020-01-02T00:00:00.123', '2020-01-02T00:00:00.123', '2', '2', '2', '2', '2', '2', '2', '2', '2', '2', '2.02', '2.02', '2.02', 'hello', 'world'], - ['2020-01-03T00:00:00.000', '2020-01-03T00:00:00.000', '2020-01-03T00:00:00.234', '2020-01-03T00:00:00.234', '2020-01-03T00:00:00.234', '3', '3', '3', '3', '3', '3', '3', '3', '3', '3', '3.03', '3.03', '3.03', 'hello', 'world'], + { date: '2020-01-01T00:00:00.000', datetime: '2020-01-01T00:00:00.000', datetime64_millis: '2020-01-01T00:00:00.000', datetime64_micros: '2020-01-01T00:00:00.000', datetime64_nanos: '2020-01-01T00:00:00.000', int8: '1', int16: '1', int32: '1', int64: '1', uint8: '1', uint16: '1', uint32: '1', uint64: '1', float32: '1', float64: '1', decimal32: '1.01', decimal64: '1.01', decimal128: '1.01', enum8: 'hello', enum16: 'world' }, + { date: '2020-01-02T00:00:00.000', datetime: '2020-01-02T00:00:00.000', datetime64_millis: '2020-01-02T00:00:00.123', datetime64_micros: '2020-01-02T00:00:00.123', datetime64_nanos: '2020-01-02T00:00:00.123', int8: '2', int16: '2', int32: '2', int64: '2', uint8: '2', uint16: '2', uint32: '2', uint64: '2', float32: '2', float64: '2', decimal32: '2.02', decimal64: '2.02', decimal128: '2.02', enum8: 'hello', enum16: 'world' }, + { date: '2020-01-03T00:00:00.000', datetime: '2020-01-03T00:00:00.000', datetime64_millis: '2020-01-03T00:00:00.234', datetime64_micros: '2020-01-03T00:00:00.234', datetime64_nanos: '2020-01-03T00:00:00.234', int8: '3', int16: '3', int32: '3', int64: '3', uint8: '3', uint16: '3', uint32: '3', uint64: '3', float32: '3', float64: '3', decimal32: '3.03', decimal64: '3.03', decimal128: '3.03', enum8: 'hello', enum16: 'world' }, ]); } finally { // @ts-ignore diff --git a/yarn.lock b/yarn.lock index 52b737c54400a..bbc90068904a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4222,6 +4222,18 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@clickhouse/client-common@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@clickhouse/client-common/-/client-common-1.7.0.tgz#4d0315158d275ea8d55ed8e04d69871832f4d8ba" + integrity sha512-RkHYf23/wyv/6C0KcVD4nRX4JAn/Y+9AZBQPlrSId2JwXsmAnjDkkKpuPLwZPNVH6J3BkW+y8bQCEk3VHQzArw== + +"@clickhouse/client@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@clickhouse/client/-/client-1.7.0.tgz#a6b7b72db825162b1f54c2fe383f349dbf437fbd" + integrity sha512-2aESIFRbSPWEZIU++sXt1RYWgEKZH75C3jyXLcRBeafMDjq7bKV2AX1X9n9xscN+Y4VvnkBzkjFxcbuqFSBk6w== + dependencies: + "@clickhouse/client-common" "1.7.0" + "@codemirror/highlight@^0.19.0": version "0.19.6" resolved "https://registry.yarnpkg.com/@codemirror/highlight/-/highlight-0.19.6.tgz#7f2e066f83f5649e8e0748a3abe0aaeaf64b8ac2" @@ -4353,7 +4365,7 @@ tiny-invariant "^1.3.3" valid-url "^1.0.9" -"@cubejs-backend/apla-clickhouse@^1.7", "@cubejs-backend/apla-clickhouse@^1.7.0": +"@cubejs-backend/apla-clickhouse@^1.7.0": version "1.7.0" resolved "https://registry.yarnpkg.com/@cubejs-backend/apla-clickhouse/-/apla-clickhouse-1.7.0.tgz#6359f46c56492d1704d18be0210c7546fdac5f5e" integrity sha512-qwXapTC/qosA6RprElRjnl8gmlDQaxtJPtbgcdjyNvkmiyao1HI+w5QkjHWCiVm6aTzE0gjFr6/2y87TZ9fojg==