From 6de5f9c62efdc06a859f5f3449a4d322185a613c Mon Sep 17 00:00:00 2001 From: Andrei Pechkurov Date: Mon, 5 Jan 2026 15:48:52 +0200 Subject: [PATCH] fix(cubejs-questdb-driver): unsupported HAVING clause --- .../cubejs-questdb-driver/src/QuestQuery.ts | 16 ++++++++++++ .../test/QuestQuery.test.ts | 25 +++++++++++++++++++ .../src/adapter/BaseQuery.js | 10 ++++---- .../src/adapter/HiveQuery.ts | 5 ++-- .../src/adapter/PreAggregations.ts | 18 ++++++------- .../src/db-container-runners/questdb.ts | 2 +- 6 files changed, 59 insertions(+), 17 deletions(-) diff --git a/packages/cubejs-questdb-driver/src/QuestQuery.ts b/packages/cubejs-questdb-driver/src/QuestQuery.ts index b4096bb5ddbfd..89a4d20c00878 100644 --- a/packages/cubejs-questdb-driver/src/QuestQuery.ts +++ b/packages/cubejs-questdb-driver/src/QuestQuery.ts @@ -1,3 +1,4 @@ +import R from 'ramda'; import { BaseFilter, BaseQuery, @@ -91,6 +92,21 @@ export class QuestQuery extends BaseQuery { .join(' AND '); } + public baseHaving(query: string, filters: BaseFilter[]) { + // QuestDB doesn't support HAVING syntax. + // `( ) WHERE ` should be used instead. + + if (filters.length > 0) { + let filter = filters.map(t => t.filterToWhere()).filter(R.identity).map(f => `(${f})`).join(' AND '); + // Replace measures with their aliases in the filter. + this.measures.forEach((m) => { + filter = filter.replace(m.measureSql(), m.aliasName()); + }); + return `SELECT * FROM (${query}) WHERE ${filter}`; + } + return query; + } + public renderSqlMeasure(name: string, evaluateSql: string, symbol: any, cubeName: string, parentMeasure: string, orderBySql: string[]): string { // QuestDB doesn't support COUNT(column_name) syntax. // COUNT() or COUNT(*) should be used instead. diff --git a/packages/cubejs-questdb-driver/test/QuestQuery.test.ts b/packages/cubejs-questdb-driver/test/QuestQuery.test.ts index 77c2d33e6cce1..a0d7e496b4a63 100644 --- a/packages/cubejs-questdb-driver/test/QuestQuery.test.ts +++ b/packages/cubejs-questdb-driver/test/QuestQuery.test.ts @@ -138,4 +138,29 @@ describe('QuestQuery', () => { expect(queryAndParams[0]).toContain('ILIKE \'%\' || $1 || \'%\''); })); + + it('test having filter', + () => compiler.compile().then(() => { + const query = new QuestQuery({ joinGraph, cubeEvaluator, compiler }, { + dimensions: ['visitors.name'], + measures: ['visitors.count'], + filters: [ + { + member: 'visitors.count', + operator: 'gt', + values: ['42'] + }, + ], + }); + + const queryAndParams = query.buildSqlAndParams(); + + const expected = 'SELECT * FROM (SELECT\n' + + ' "visitors".name "visitors__name", count(*) "visitors__count"\n' + + ' FROM\n' + + ' visitors AS "visitors" GROUP BY "visitors__name") WHERE ("visitors__count" > $1) ORDER BY "visitors__count" DESC'; + expect(queryAndParams[0]).toEqual(expected); + const expectedParams = ['42']; + expect(queryAndParams[1]).toEqual(expectedParams); + })); }); diff --git a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js index 1b2074bcf9ce5..e8277a2c2a6fd 100644 --- a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js +++ b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js @@ -1228,9 +1228,9 @@ export class BaseQuery { if (this.multiStageQuery) { return `${commonQuery} ${this.baseWhere(this.allFilters.concat(inlineWhereConditions))}`; } - return `${commonQuery} ${this.baseWhere(this.allFilters.concat(inlineWhereConditions))}` + - this.groupByClause() + - this.baseHaving(this.measureFilters) + + const query = `${commonQuery} ${this.baseWhere(this.allFilters.concat(inlineWhereConditions))}` + + this.groupByClause(); + return this.baseHaving(query, this.measureFilters) + this.orderBy() + this.groupByDimensionLimit(); } @@ -1834,9 +1834,9 @@ export class BaseQuery { return filterClause.length ? ` WHERE ${filterClause.join(' AND ')}` : ''; } - baseHaving(filters) { + baseHaving(query, filters) { const filterClause = filters.map(t => t.filterToWhere()).filter(R.identity).map(f => `(${f})`); - return filterClause.length ? ` HAVING ${filterClause.join(' AND ')}` : ''; + return filterClause.length ? query + ` HAVING ${filterClause.join(' AND ')}` : query; } timeStampInClientTz(dateParam) { diff --git a/packages/cubejs-schema-compiler/src/adapter/HiveQuery.ts b/packages/cubejs-schema-compiler/src/adapter/HiveQuery.ts index 5cadce3d2d905..479039c82da18 100644 --- a/packages/cubejs-schema-compiler/src/adapter/HiveQuery.ts +++ b/packages/cubejs-schema-compiler/src/adapter/HiveQuery.ts @@ -71,8 +71,9 @@ export class HiveQuery extends BaseQuery { ungroupedAliases: R.fromPairs(this.forSelect().map((m: any) => [m.measure || m.dimension, m.aliasName()])) } ); - return `SELECT ${select} FROM (${ungrouped}) AS ${this.escapeColumnName('hive_wrapper')} - ${this.groupByClause()}${this.baseHaving(this.measureFilters)}${this.orderBy()}${this.groupByDimensionLimit()}`; + const query = `SELECT ${select} FROM (${ungrouped}) AS ${this.escapeColumnName('hive_wrapper')} + ${this.groupByClause()}`; + return this.baseHaving(query, this.measureFilters) + this.orderBy() + this.groupByDimensionLimit(); } public seriesSql(timeDimension) { diff --git a/packages/cubejs-schema-compiler/src/adapter/PreAggregations.ts b/packages/cubejs-schema-compiler/src/adapter/PreAggregations.ts index 015ed0bea7745..134c0e8dedc65 100644 --- a/packages/cubejs-schema-compiler/src/adapter/PreAggregations.ts +++ b/packages/cubejs-schema-compiler/src/adapter/PreAggregations.ts @@ -1543,15 +1543,15 @@ export class PreAggregations { }; return this.query.evaluateSymbolSqlWithContext( - // eslint-disable-next-line prefer-template - () => `SELECT ${this.query.selectAllDimensionsAndMeasures(measures)} FROM ${from} ${this.query.baseWhere(replacedFilters)}` + - this.query.groupByClause() + - ( - isFullSimpleQuery ? - this.query.baseHaving(this.query.measureFilters) + - this.query.orderBy() + - this.query.groupByDimensionLimit() : '' - ), + () => { + // eslint-disable-next-line prefer-template + const query = `SELECT ${this.query.selectAllDimensionsAndMeasures(measures)} FROM ${from} ${this.query.baseWhere(replacedFilters)}` + + this.query.groupByClause(); + return isFullSimpleQuery ? + this.query.baseHaving(query, this.query.measureFilters) + + this.query.orderBy() + + this.query.groupByDimensionLimit() : query; + }, { renderedReference, rollupQuery: true, diff --git a/packages/cubejs-testing-shared/src/db-container-runners/questdb.ts b/packages/cubejs-testing-shared/src/db-container-runners/questdb.ts index 021cb2535c2b9..73a8bb6bd8ab0 100644 --- a/packages/cubejs-testing-shared/src/db-container-runners/questdb.ts +++ b/packages/cubejs-testing-shared/src/db-container-runners/questdb.ts @@ -4,7 +4,7 @@ import { DbRunnerAbstract, DBRunnerContainerOptions } from './db-runner.abstract export class QuestDBRunner extends DbRunnerAbstract { public static startContainer(options: DBRunnerContainerOptions) { - const version = process.env.TEST_QUEST_DB_VERSION || options.version || '8.0.3'; + const version = process.env.TEST_QUEST_DB_VERSION || options.version || '9.2.3'; const container = new GenericContainer(`questdb/questdb:${version}`) .withExposedPorts(8812)