From 2c0d9ce99a06063a52b021c7b509fe919b7e5c72 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Fri, 12 Sep 2025 19:12:33 +0300 Subject: [PATCH 01/53] add cache option to the query --- packages/cubejs-api-gateway/src/query.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/cubejs-api-gateway/src/query.js b/packages/cubejs-api-gateway/src/query.js index b9320d128168f..37e056ca0f5da 100644 --- a/packages/cubejs-api-gateway/src/query.js +++ b/packages/cubejs-api-gateway/src/query.js @@ -187,7 +187,9 @@ const querySchema = Joi.object().keys({ limit: Joi.number().integer().strict().min(0), offset: Joi.number().integer().strict().min(0), total: Joi.boolean(), + // @deprecated renewQuery: Joi.boolean(), + cache: Joi.valid('stale-if-slow', 'stale-while-revalidate', 'must-revalidate', 'no-cache').default('stale-if-slow'), ungrouped: Joi.boolean(), responseFormat: Joi.valid('default', 'compact'), subqueryJoins: Joi.array().items(subqueryJoin), From 5b4b45d363e914e39f93b8b7840f53e32da38c0f Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Mon, 15 Sep 2025 11:00:12 +0300 Subject: [PATCH 02/53] Pass CacheMode from /cubesql to backend --- packages/cubejs-api-gateway/src/gateway.ts | 2 +- packages/cubejs-api-gateway/src/sql-server.ts | 5 +++-- packages/cubejs-backend-native/js/index.ts | 6 ++++-- packages/cubejs-backend-native/src/node_export.rs | 4 ++++ 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/cubejs-api-gateway/src/gateway.ts b/packages/cubejs-api-gateway/src/gateway.ts index 8e2cfce793f26..b6af914bad688 100644 --- a/packages/cubejs-api-gateway/src/gateway.ts +++ b/packages/cubejs-api-gateway/src/gateway.ts @@ -425,7 +425,7 @@ class ApiGateway { try { await this.assertApiScope('data', req.context?.securityContext); - await this.sqlServer.execSql(req.body.query, res, req.context?.securityContext); + await this.sqlServer.execSql(req.body.query, res, req.context?.securityContext, req.body.cache); } catch (e: any) { this.handleError({ e, diff --git a/packages/cubejs-api-gateway/src/sql-server.ts b/packages/cubejs-api-gateway/src/sql-server.ts index cd3a6eeff0d05..dfdf6eaea7b3c 100644 --- a/packages/cubejs-api-gateway/src/sql-server.ts +++ b/packages/cubejs-api-gateway/src/sql-server.ts @@ -8,6 +8,7 @@ import { Request as NativeRequest, LoadRequestMeta, Sql4SqlResponse, + CacheMode, } from '@cubejs-backend/native'; import type { ShutdownMode } from '@cubejs-backend/native'; import { displayCLIWarning, getEnv } from '@cubejs-backend/shared'; @@ -65,8 +66,8 @@ export class SQLServer { throw new Error('Native api gateway is not enabled'); } - public async execSql(sqlQuery: string, stream: any, securityContext?: any) { - await execSql(this.sqlInterfaceInstance!, sqlQuery, stream, securityContext); + public async execSql(sqlQuery: string, stream: any, securityContext?: any, cacheMode?: CacheMode) { + await execSql(this.sqlInterfaceInstance!, sqlQuery, stream, securityContext, cacheMode); } public async sql4sql(sqlQuery: string, disablePostProcessing: boolean, securityContext?: unknown): Promise { diff --git a/packages/cubejs-backend-native/js/index.ts b/packages/cubejs-backend-native/js/index.ts index a7343a76cf8b2..3777f48d6527e 100644 --- a/packages/cubejs-backend-native/js/index.ts +++ b/packages/cubejs-backend-native/js/index.ts @@ -429,16 +429,18 @@ export const registerInterface = async (options: SQLInterfaceOptions): Promise => { const native = loadNative(); await native.shutdownInterface(instance, shutdownMode); }; -export const execSql = async (instance: SqlInterfaceInstance, sqlQuery: string, stream: any, securityContext?: any): Promise => { +export const execSql = async (instance: SqlInterfaceInstance, sqlQuery: string, stream: any, securityContext?: any, cacheMode: CacheMode = 'stale-if-slow'): Promise => { const native = loadNative(); - await native.execSql(instance, sqlQuery, stream, securityContext ? JSON.stringify(securityContext) : null); + await native.execSql(instance, sqlQuery, stream, securityContext ? JSON.stringify(securityContext) : null, cacheMode); }; // TODO parse result from native code diff --git a/packages/cubejs-backend-native/src/node_export.rs b/packages/cubejs-backend-native/src/node_export.rs index 7f1c38861b8ff..499717ec090e0 100644 --- a/packages/cubejs-backend-native/src/node_export.rs +++ b/packages/cubejs-backend-native/src/node_export.rs @@ -223,6 +223,7 @@ async fn handle_sql_query( channel: Arc, stream_methods: WritableStreamMethods, sql_query: &str, + cache_mode: &str, ) -> Result<(), CubeError> { let span_id = Some(Arc::new(SpanId::new( Uuid::new_v4().to_string(), @@ -424,6 +425,8 @@ fn exec_sql(mut cx: FunctionContext) -> JsResult { Err(_) => None, }; + let cache_mode = cx.argument::(4)?.value(&mut cx); + let js_stream_on_fn = Arc::new( node_stream .get::(&mut cx, "on")? @@ -471,6 +474,7 @@ fn exec_sql(mut cx: FunctionContext) -> JsResult { channel.clone(), stream_methods, &sql_query, + &cache_mode, ) .await; From 79b8dd791e4a1a327b89664eb33a3cdb43f3599a Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Mon, 15 Sep 2025 11:17:19 +0300 Subject: [PATCH 03/53] imject CacheMode into more places --- packages/cubejs-api-gateway/src/types/query.ts | 3 +++ packages/cubejs-client-core/src/types.ts | 3 +++ packages/cubejs-client-dx/index.d.ts | 4 ++++ .../cubejs-query-orchestrator/src/orchestrator/QueryCache.ts | 5 +++++ 4 files changed, 15 insertions(+) diff --git a/packages/cubejs-api-gateway/src/types/query.ts b/packages/cubejs-api-gateway/src/types/query.ts index 7c470a6aaa27c..5ce7900b12e70 100644 --- a/packages/cubejs-api-gateway/src/types/query.ts +++ b/packages/cubejs-api-gateway/src/types/query.ts @@ -12,6 +12,7 @@ import { QueryTimeDimensionGranularity, } from './strings'; import { ResultType } from './enums'; +import { CacheMode } from '@cubejs-backend/native'; /** * Query base filter definition. @@ -139,7 +140,9 @@ interface Query { totalQuery?: boolean; order?: any; timezone?: string; + // @deprecated renewQuery?: boolean; + cache?: CacheMode; ungrouped?: boolean; responseFormat?: ResultType; diff --git a/packages/cubejs-client-core/src/types.ts b/packages/cubejs-client-core/src/types.ts index 58f5adba4c24a..35af0feb65213 100644 --- a/packages/cubejs-client-core/src/types.ts +++ b/packages/cubejs-client-core/src/types.ts @@ -1,6 +1,7 @@ import Meta from './Meta'; import { TimeDimensionGranularity } from './time'; import { TransportOptions } from './HttpTransport'; +import { CacheMode } from '@cubejs-backend/native'; export type QueryOrder = 'asc' | 'desc' | 'none'; @@ -113,7 +114,9 @@ export interface Query { offset?: number; order?: TQueryOrderObject | TQueryOrderArray; timezone?: string; + // @deprecated renewQuery?: boolean; + cache?: CacheMode; ungrouped?: boolean; responseFormat?: 'compact' | 'default'; total?: boolean; diff --git a/packages/cubejs-client-dx/index.d.ts b/packages/cubejs-client-dx/index.d.ts index 9e61ed1fe0de6..59d15176c6be4 100644 --- a/packages/cubejs-client-dx/index.d.ts +++ b/packages/cubejs-client-dx/index.d.ts @@ -1,3 +1,5 @@ +import { CacheMode } from '@cubejs-backend/native'; + declare module "@cubejs-client/core" { export type IntrospectedMeasureName = import('./generated').IntrospectedMeasureName; @@ -19,7 +21,9 @@ declare module "@cubejs-client/core" { offset?: number; order?: IntrospectedTQueryOrderObject | IntrospectedTQueryOrderArray; timezone?: string; + // @deprecated renewQuery?: boolean; + cache?: CacheMode; ungrouped?: boolean; } diff --git a/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts b/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts index a492e974aa510..b34a246330a16 100644 --- a/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts +++ b/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts @@ -19,6 +19,7 @@ import { DriverFactory, DriverFactoryByDataSource } from './DriverFactory'; import { LoadPreAggregationResult, PreAggregationDescription } from './PreAggregations'; import { getCacheHash } from './utils'; import { CacheAndQueryDriverType, MetadataOperationType } from './QueryOrchestrator'; +import { CacheMode } from '@cubejs-backend/native'; type QueryOptions = { external?: boolean; @@ -46,7 +47,9 @@ export type Query = { preAggregations?: PreAggregationDescription[]; groupedPartitionPreAggregations?: PreAggregationDescription[][]; preAggregationsLoadCacheByDataSource?: any; + // @deprecated renewQuery?: boolean; + cache?: CacheMode; compilerCacheFn?: (subKey: string[], cacheFn: () => T) => T; }; @@ -56,7 +59,9 @@ export type QueryBody = { query?: string; values?: string[]; continueWait?: boolean; + // @deprecated renewQuery?: boolean; + cache?: CacheMode; requestId?: string; external?: boolean; isJob?: boolean; From 372e4c841a495828c620dbc409743895a4ec8b7e Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Mon, 15 Sep 2025 11:24:28 +0300 Subject: [PATCH 04/53] update normalizeQuery with cache mode --- packages/cubejs-api-gateway/src/query.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/cubejs-api-gateway/src/query.js b/packages/cubejs-api-gateway/src/query.js index 37e056ca0f5da..03bf9b2fbe4be 100644 --- a/packages/cubejs-api-gateway/src/query.js +++ b/packages/cubejs-api-gateway/src/query.js @@ -336,8 +336,15 @@ const normalizeQuery = (query, persistent) => { newLimit = query.limit; } + let cacheMode = query.cache; + // Convert deprecated renewQuery option to new cache mode + if (query.renewQuery && !query.cache) { + cacheMode = 'must-revalidate'; + } + return { ...query, + ...{ cache: cacheMode }, ...(query.order ? { order: normalizeQueryOrder(query.order) } : {}), limit: newLimit, timezone, From 289957f88d1ad15be787558e958246437d5e8b35 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Mon, 15 Sep 2025 11:29:50 +0300 Subject: [PATCH 05/53] pass cache mode within graphql --- packages/cubejs-api-gateway/src/graphql.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/cubejs-api-gateway/src/graphql.ts b/packages/cubejs-api-gateway/src/graphql.ts index b00a9d4738b4e..d35039cfe00a0 100644 --- a/packages/cubejs-api-gateway/src/graphql.ts +++ b/packages/cubejs-api-gateway/src/graphql.ts @@ -363,7 +363,7 @@ function parseDates(result: any) { } export function getJsonQuery(metaConfig: any, args: Record, infos: GraphQLResolveInfo) { - const { where, limit, offset, timezone, orderBy, renewQuery, ungrouped } = args; + const { where, limit, offset, timezone, orderBy, renewQuery, ungrouped, cache } = args; const measures: string[] = []; const dimensions: string[] = []; @@ -461,6 +461,7 @@ export function getJsonQuery(metaConfig: any, args: Record, infos: ...(timezone && { timezone }), ...(filters.length && { filters }), ...(renewQuery && { renewQuery }), + ...(cache && { cache }), ...(ungrouped && { ungrouped }), }; } @@ -639,6 +640,7 @@ export function makeSchema(metaConfig: any): GraphQLSchema { offset: intArg(), timezone: stringArg(), renewQuery: booleanArg(), + cache: stringArg(), ungrouped: booleanArg(), orderBy: arg({ type: 'RootOrderByInput' From 507fd0ea20dda8409855841074b91f0403e5fd5d Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Mon, 15 Sep 2025 12:04:06 +0300 Subject: [PATCH 06/53] pass new cache mode in sqlApiLoad in API GW --- packages/cubejs-api-gateway/src/gateway.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/cubejs-api-gateway/src/gateway.ts b/packages/cubejs-api-gateway/src/gateway.ts index b6af914bad688..5eea64423dbef 100644 --- a/packages/cubejs-api-gateway/src/gateway.ts +++ b/packages/cubejs-api-gateway/src/gateway.ts @@ -1643,6 +1643,7 @@ class ApiGateway { values: sqlQuery.sql[1], continueWait: true, renewQuery: normalizedQuery.renewQuery, + cache: normalizedQuery.cache, requestId: context.requestId, context, persistent: false, @@ -1667,6 +1668,7 @@ class ApiGateway { values: totalQuery.sql[1], continueWait: true, renewQuery: normalizedTotal.renewQuery, + cache: normalizedTotal.cache, requestId: context.requestId, context }); @@ -1788,6 +1790,7 @@ class ApiGateway { values: sqlQuery.sql[1], continueWait: true, renewQuery: false, + cache: 'stale-if-slow', requestId: context.requestId, context, persistent: true, @@ -1981,6 +1984,7 @@ class ApiGateway { values: sqlQuery.values || sqlQuery.sql[1], continueWait: true, renewQuery: false, + cache: 'stale-if-slow', requestId: context.requestId, context, persistent: true, @@ -2000,6 +2004,7 @@ class ApiGateway { values: request.sqlQuery[1], continueWait: true, renewQuery: normalizedQueries[0].renewQuery, + cache: normalizedQueries[0].cache, requestId: context.requestId, context, ...sqlQueries[0], From 00f506fe28a710aa5f552dbd0192a9fd174a27e9 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Mon, 15 Sep 2025 12:32:44 +0300 Subject: [PATCH 07/53] fix types imports --- packages/cubejs-api-gateway/src/sql-server.ts | 3 +-- packages/cubejs-api-gateway/src/types/query.ts | 2 +- packages/cubejs-backend-native/js/index.ts | 3 +-- packages/cubejs-backend-shared/src/index.ts | 1 + packages/cubejs-backend-shared/src/shared-types.ts | 1 + packages/cubejs-client-core/src/types.ts | 3 ++- packages/cubejs-client-dx/index.d.ts | 2 +- .../cubejs-query-orchestrator/src/orchestrator/QueryCache.ts | 2 +- 8 files changed, 9 insertions(+), 8 deletions(-) create mode 100644 packages/cubejs-backend-shared/src/shared-types.ts diff --git a/packages/cubejs-api-gateway/src/sql-server.ts b/packages/cubejs-api-gateway/src/sql-server.ts index dfdf6eaea7b3c..b38b9ec7a15c5 100644 --- a/packages/cubejs-api-gateway/src/sql-server.ts +++ b/packages/cubejs-api-gateway/src/sql-server.ts @@ -8,10 +8,9 @@ import { Request as NativeRequest, LoadRequestMeta, Sql4SqlResponse, - CacheMode, } from '@cubejs-backend/native'; import type { ShutdownMode } from '@cubejs-backend/native'; -import { displayCLIWarning, getEnv } from '@cubejs-backend/shared'; +import { displayCLIWarning, getEnv, CacheMode } from '@cubejs-backend/shared'; import * as crypto from 'crypto'; import type { ApiGateway } from './gateway'; diff --git a/packages/cubejs-api-gateway/src/types/query.ts b/packages/cubejs-api-gateway/src/types/query.ts index 5ce7900b12e70..6f83c4d04b14d 100644 --- a/packages/cubejs-api-gateway/src/types/query.ts +++ b/packages/cubejs-api-gateway/src/types/query.ts @@ -5,6 +5,7 @@ * Network query data types definition. */ +import { CacheMode } from '@cubejs-backend/shared'; import { Member, TimeMember, @@ -12,7 +13,6 @@ import { QueryTimeDimensionGranularity, } from './strings'; import { ResultType } from './enums'; -import { CacheMode } from '@cubejs-backend/native'; /** * Query base filter definition. diff --git a/packages/cubejs-backend-native/js/index.ts b/packages/cubejs-backend-native/js/index.ts index 3777f48d6527e..57bf80e4f2c92 100644 --- a/packages/cubejs-backend-native/js/index.ts +++ b/packages/cubejs-backend-native/js/index.ts @@ -3,6 +3,7 @@ import fs from 'fs'; import path from 'path'; import { Writable } from 'stream'; import type { Request as ExpressRequest } from 'express'; +import { CacheMode } from '@cubejs-backend/shared'; import { ResultWrapper } from './ResultWrapper'; export * from './ResultWrapper'; @@ -429,8 +430,6 @@ export const registerInterface = async (options: SQLInterfaceOptions): Promise => { const native = loadNative(); diff --git a/packages/cubejs-backend-shared/src/index.ts b/packages/cubejs-backend-shared/src/index.ts index ac7ff477ac440..29f89efa7d72e 100644 --- a/packages/cubejs-backend-shared/src/index.ts +++ b/packages/cubejs-backend-shared/src/index.ts @@ -13,6 +13,7 @@ export * from './convert'; export * from './helpers'; export * from './machine-id'; export * from './type-helpers'; +export * from './shared-types'; export * from './http-utils'; export * from './cli'; export * from './proxy'; diff --git a/packages/cubejs-backend-shared/src/shared-types.ts b/packages/cubejs-backend-shared/src/shared-types.ts new file mode 100644 index 0000000000000..f47ad6eb351f2 --- /dev/null +++ b/packages/cubejs-backend-shared/src/shared-types.ts @@ -0,0 +1 @@ +export type CacheMode = 'stale-if-slow' | 'stale-while-revalidate' | 'must-revalidate' | 'no-cache'; diff --git a/packages/cubejs-client-core/src/types.ts b/packages/cubejs-client-core/src/types.ts index 35af0feb65213..6e807479f9d91 100644 --- a/packages/cubejs-client-core/src/types.ts +++ b/packages/cubejs-client-core/src/types.ts @@ -1,13 +1,14 @@ import Meta from './Meta'; import { TimeDimensionGranularity } from './time'; import { TransportOptions } from './HttpTransport'; -import { CacheMode } from '@cubejs-backend/native'; export type QueryOrder = 'asc' | 'desc' | 'none'; export type TQueryOrderObject = { [key: string]: QueryOrder }; export type TQueryOrderArray = Array<[string, QueryOrder]>; +export type CacheMode = 'stale-if-slow' | 'stale-while-revalidate' | 'must-revalidate' | 'no-cache'; + export type GranularityAnnotation = { name: string; title: string; diff --git a/packages/cubejs-client-dx/index.d.ts b/packages/cubejs-client-dx/index.d.ts index 59d15176c6be4..7e6d108a3d0fb 100644 --- a/packages/cubejs-client-dx/index.d.ts +++ b/packages/cubejs-client-dx/index.d.ts @@ -1,4 +1,4 @@ -import { CacheMode } from '@cubejs-backend/native'; +import { CacheMode } from '@cubejs-backend/shared'; declare module "@cubejs-client/core" { diff --git a/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts b/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts index b34a246330a16..aacd79f8cf42a 100644 --- a/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts +++ b/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts @@ -19,7 +19,7 @@ import { DriverFactory, DriverFactoryByDataSource } from './DriverFactory'; import { LoadPreAggregationResult, PreAggregationDescription } from './PreAggregations'; import { getCacheHash } from './utils'; import { CacheAndQueryDriverType, MetadataOperationType } from './QueryOrchestrator'; -import { CacheMode } from '@cubejs-backend/native'; +import { CacheMode } from '@cubejs-backend/shared'; type QueryOptions = { external?: boolean; From 9abee17b47083e668f3721ce26eb8fd62f205b5c Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Mon, 15 Sep 2025 14:04:41 +0300 Subject: [PATCH 08/53] update preAggs to use cache option instead of renewQuery --- .../src/orchestrator/PreAggregations.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cubejs-query-orchestrator/src/orchestrator/PreAggregations.ts b/packages/cubejs-query-orchestrator/src/orchestrator/PreAggregations.ts index c8a6722cc9049..e8048586b072e 100644 --- a/packages/cubejs-query-orchestrator/src/orchestrator/PreAggregations.ts +++ b/packages/cubejs-query-orchestrator/src/orchestrator/PreAggregations.ts @@ -498,7 +498,7 @@ export class PreAggregations { maxPartitions: this.options.maxPartitions, maxSourceRowLimit: this.options.maxSourceRowLimit, isJob: queryBody.isJob, - waitForRenew: queryBody.renewQuery, + waitForRenew: queryBody.cache !== undefined ? queryBody.cache === 'must-revalidate' : queryBody.renewQuery, // TODO workaround to avoid continuous waiting on building pre-aggregation dependencies forceBuild: i === preAggregations.length - 1 ? queryBody.forceBuildPreAggregations : false, requestId: queryBody.requestId, @@ -603,7 +603,7 @@ export class PreAggregations { { maxPartitions: this.options.maxPartitions, maxSourceRowLimit: this.options.maxSourceRowLimit, - waitForRenew: queryBody.renewQuery, + waitForRenew: queryBody.cache !== undefined ? queryBody.cache === 'must-revalidate' : queryBody.renewQuery, requestId: queryBody.requestId, externalRefresh: this.externalRefresh, compilerCacheFn: queryBody.compilerCacheFn, From 8a722606b62120e43a4d01da14350a0fcb2a1556 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Mon, 15 Sep 2025 14:04:53 +0300 Subject: [PATCH 09/53] code polish --- .../src/orchestrator/QueryCache.ts | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts b/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts index aacd79f8cf42a..478dd5410c8b8 100644 --- a/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts +++ b/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts @@ -2,7 +2,13 @@ import crypto from 'crypto'; import csvWriter from 'csv-write-stream'; import { LRUCache } from 'lru-cache'; import { pipeline } from 'stream'; -import { AsyncDebounce, getEnv, MaybeCancelablePromise, streamToArray } from '@cubejs-backend/shared'; +import { + AsyncDebounce, + getEnv, + MaybeCancelablePromise, + streamToArray, + CacheMode, +} from '@cubejs-backend/shared'; import { CubeStoreCacheDriver, CubeStoreDriver } from '@cubejs-backend/cubestore-driver'; import { BaseDriver, @@ -19,7 +25,23 @@ import { DriverFactory, DriverFactoryByDataSource } from './DriverFactory'; import { LoadPreAggregationResult, PreAggregationDescription } from './PreAggregations'; import { getCacheHash } from './utils'; import { CacheAndQueryDriverType, MetadataOperationType } from './QueryOrchestrator'; -import { CacheMode } from '@cubejs-backend/shared'; + +export type CacheQueryResultOptions = { + renewalThreshold?: number, + renewalKey?: any, + priority?: number, + external?: boolean, + requestId?: string, + dataSource: string, + waitForRenew?: boolean, + forceNoCache?: boolean, + useInMemory?: boolean, + useCsvQuery?: boolean, + lambdaTypes?: TableStructure, + persistent?: boolean, + primaryQuery?: boolean, + renewCycle?: boolean, +}; type QueryOptions = { external?: boolean; @@ -848,22 +870,7 @@ export class QueryCache { values: string[], cacheKey: CacheKey, expiration: number, - options: { - renewalThreshold?: number, - renewalKey?: any, - priority?: number, - external?: boolean, - requestId?: string, - dataSource: string, - waitForRenew?: boolean, - forceNoCache?: boolean, - useInMemory?: boolean, - useCsvQuery?: boolean, - lambdaTypes?: TableStructure, - persistent?: boolean, - primaryQuery?: boolean, - renewCycle?: boolean, - } + options: CacheQueryResultOptions, ) { const spanId = crypto.randomBytes(16).toString('hex'); options = options || { dataSource: 'default' }; From 2a017e804837a222b1d7c0c3ca1904bb3686f42b Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Mon, 15 Sep 2025 14:24:43 +0300 Subject: [PATCH 10/53] comments with types --- packages/cubejs-api-gateway/src/gateway.ts | 6 ++++++ packages/cubejs-server-core/src/core/CompilerApi.js | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/cubejs-api-gateway/src/gateway.ts b/packages/cubejs-api-gateway/src/gateway.ts index 5eea64423dbef..66530d6907c6c 100644 --- a/packages/cubejs-api-gateway/src/gateway.ts +++ b/packages/cubejs-api-gateway/src/gateway.ts @@ -177,7 +177,13 @@ class ApiGateway { public constructor( protected readonly apiSecret: string, + /** + * It actually returns a Promise + */ protected readonly compilerApi: (ctx: RequestContext) => Promise, + /** + * It actually returns a Promise + */ protected readonly adapterApi: (ctx: RequestContext) => Promise, protected readonly logger: any, protected readonly options: ApiGatewayOptions, diff --git a/packages/cubejs-server-core/src/core/CompilerApi.js b/packages/cubejs-server-core/src/core/CompilerApi.js index b00a9b92927ab..dfe6027c5eecb 100644 --- a/packages/cubejs-server-core/src/core/CompilerApi.js +++ b/packages/cubejs-server-core/src/core/CompilerApi.js @@ -504,7 +504,7 @@ export class CompilerApi { /** * - * @param {unknown} filter + * @param {unknown|undefined} filter * @returns {Promise>} */ async preAggregations(filter) { From 2f22df23c9ff789bbc72deef098e4cc74c89e4f8 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Mon, 15 Sep 2025 14:34:39 +0300 Subject: [PATCH 11/53] fix query type --- .../cubejs-query-orchestrator/src/orchestrator/QueryCache.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts b/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts index 478dd5410c8b8..0f4264c6964a4 100644 --- a/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts +++ b/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts @@ -80,6 +80,8 @@ export type QueryBody = { persistent?: boolean; query?: string; values?: string[]; + loadRefreshKeysOnly?: boolean; + scheduledRefresh?: boolean; continueWait?: boolean; // @deprecated renewQuery?: boolean; From 323dc40184585c13a22c791e93d5ce6368a9f459 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Mon, 15 Sep 2025 16:41:50 +0300 Subject: [PATCH 12/53] set default cacheMode = 'stale-if-slow' in normalize() --- packages/cubejs-api-gateway/src/query.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/cubejs-api-gateway/src/query.js b/packages/cubejs-api-gateway/src/query.js index 03bf9b2fbe4be..4495d32434657 100644 --- a/packages/cubejs-api-gateway/src/query.js +++ b/packages/cubejs-api-gateway/src/query.js @@ -340,6 +340,8 @@ const normalizeQuery = (query, persistent) => { // Convert deprecated renewQuery option to new cache mode if (query.renewQuery && !query.cache) { cacheMode = 'must-revalidate'; + } else if (!query.renewQuery && !query.cache) { + cacheMode = 'stale-if-slow'; } return { From 1bf3d6257906113f543e337d34c509ef0c451fdf Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Mon, 15 Sep 2025 17:11:12 +0300 Subject: [PATCH 13/53] more types and polish --- packages/cubejs-server-core/src/core/OrchestratorApi.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/cubejs-server-core/src/core/OrchestratorApi.ts b/packages/cubejs-server-core/src/core/OrchestratorApi.ts index 77caf0d556c3f..fef9dc7233dc1 100644 --- a/packages/cubejs-server-core/src/core/OrchestratorApi.ts +++ b/packages/cubejs-server-core/src/core/OrchestratorApi.ts @@ -81,7 +81,7 @@ export class OrchestratorApi { requestId: query.requestId }); - let fetchQueryPromise = query.loadRefreshKeysOnly + let fetchQueryPromise: Promise = query.loadRefreshKeysOnly ? this.orchestrator.loadRefreshKeys(query) : this.orchestrator.fetchQuery(query); @@ -120,7 +120,7 @@ export class OrchestratorApi { return data; } catch (err) { - if ((err instanceof pt.TimeoutError || err instanceof ContinueWaitError)) { + if (err instanceof pt.TimeoutError || err instanceof ContinueWaitError) { this.logger('Continue wait', { duration: ((new Date()).getTime() - startQueryTime), query: queryForLog, @@ -131,6 +131,7 @@ export class OrchestratorApi { const fromCache = await this .orchestrator .resultFromCacheIfExists(query); + if ( !query.renewQuery && fromCache && From 4a3408855e5304290ef3e6a25df82014706ac861 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Mon, 15 Sep 2025 17:17:53 +0300 Subject: [PATCH 14/53] backbone code for 'stale-if-slow' & 'stale-while-revalidate' --- .../src/core/OrchestratorApi.ts | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/cubejs-server-core/src/core/OrchestratorApi.ts b/packages/cubejs-server-core/src/core/OrchestratorApi.ts index fef9dc7233dc1..49a01896f9912 100644 --- a/packages/cubejs-server-core/src/core/OrchestratorApi.ts +++ b/packages/cubejs-server-core/src/core/OrchestratorApi.ts @@ -128,15 +128,18 @@ export class OrchestratorApi { requestId: query.requestId }); + if (query.scheduledRefresh) { + throw { + error: 'Continue wait', + stage: null + }; + } + const fromCache = await this .orchestrator .resultFromCacheIfExists(query); - if ( - !query.renewQuery && - fromCache && - !query.scheduledRefresh - ) { + if (query.cache === 'stale-if-slow' && fromCache) { this.logger('Slow Query Warning', { query: queryForLog, requestId: query.requestId, @@ -151,11 +154,17 @@ export class OrchestratorApi { }; } + if (query.cache === 'stale-while-revalidate' && fromCache) { + // TODO: Run background refresh + return { + ...fromCache, + slowQuery: true + }; + } + throw { error: 'Continue wait', - stage: !query.scheduledRefresh - ? await this.orchestrator.queryStage(query) - : null + stage: await this.orchestrator.queryStage(query) }; } From 7490dfe3800d4d5a978c097b799ed2a88a3b4834 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Mon, 15 Sep 2025 17:43:56 +0300 Subject: [PATCH 15/53] make query cache aware of queryBody.cache === 'must-revalidate' --- .../cubejs-query-orchestrator/src/orchestrator/QueryCache.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts b/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts index 0f4264c6964a4..8313f438635bf 100644 --- a/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts +++ b/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts @@ -281,7 +281,8 @@ export class QueryCache { } } - if (queryBody.renewQuery) { + // renewQuery has been deprecated, but keeping it for now + if (queryBody.cache === 'must-revalidate' || queryBody.renewQuery) { this.logger('Requested renew', { cacheKey, requestId: queryBody.requestId }); return this.renewQuery( query, From be23c5221a9d65b51fee5f008e142b75225286a3 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Mon, 15 Sep 2025 17:45:03 +0300 Subject: [PATCH 16/53] First attempt to implement 'no-cache' scenario --- .../src/orchestrator/QueryOrchestrator.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/cubejs-query-orchestrator/src/orchestrator/QueryOrchestrator.ts b/packages/cubejs-query-orchestrator/src/orchestrator/QueryOrchestrator.ts index fe53c59a55888..f55657f4a17c4 100644 --- a/packages/cubejs-query-orchestrator/src/orchestrator/QueryOrchestrator.ts +++ b/packages/cubejs-query-orchestrator/src/orchestrator/QueryOrchestrator.ts @@ -210,6 +210,25 @@ export class QueryOrchestrator { * @throw ContinueWaitError */ public async fetchQuery(queryBody: QueryBody): Promise { + if (queryBody.query && queryBody.cache === 'no-cache') { + const result = await this.queryCache.cachedQueryResult( + { ...queryBody, forceNoCache: true }, + [] + ); + + if (result instanceof QueryStream) { + return result; + } + + return { + ...result, + dataSource: queryBody.dataSource, + external: queryBody.external, + usedPreAggregations: {}, + lastRefreshTime: new Date(), + }; + } + const { preAggregationsTablesToTempTables, values, From dbfdcbf2a9e1c7d55f6a99de4e8e6d6bdecc30f7 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Tue, 16 Sep 2025 10:29:47 +0300 Subject: [PATCH 17/53] add cache to open api spec and regenerate rust client --- packages/cubejs-api-gateway/openspec.yml | 7 +++++++ .../cubeclient/.openapi-generator/VERSION | 2 +- rust/cubesql/cubeclient/src/models/mod.rs | 1 + .../src/models/v1_load_request_query.rs | 21 +++++++++++++++++++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/cubejs-api-gateway/openspec.yml b/packages/cubejs-api-gateway/openspec.yml index 62b62852054b4..d4da1d397efde 100644 --- a/packages/cubejs-api-gateway/openspec.yml +++ b/packages/cubejs-api-gateway/openspec.yml @@ -467,6 +467,13 @@ components: type: "array" items: $ref: "#/components/schemas/V1LoadRequestQueryFilterItem" + cache: + type: "string" + enum: + - stale-if-slow + - stale-while-revalidate + - must-revalidate + - no-cache ungrouped: type: "boolean" # vector of (subquery sql: string, join condition: member expression, join type: enum) diff --git a/rust/cubesql/cubeclient/.openapi-generator/VERSION b/rust/cubesql/cubeclient/.openapi-generator/VERSION index e465da43155f4..368fd8fd8d784 100644 --- a/rust/cubesql/cubeclient/.openapi-generator/VERSION +++ b/rust/cubesql/cubeclient/.openapi-generator/VERSION @@ -1 +1 @@ -7.14.0 +7.15.0 diff --git a/rust/cubesql/cubeclient/src/models/mod.rs b/rust/cubesql/cubeclient/src/models/mod.rs index 96884d6be2587..9a5a7886d0bdd 100644 --- a/rust/cubesql/cubeclient/src/models/mod.rs +++ b/rust/cubesql/cubeclient/src/models/mod.rs @@ -32,6 +32,7 @@ pub mod v1_load_request; pub use self::v1_load_request::V1LoadRequest; pub mod v1_load_request_query; pub use self::v1_load_request_query::V1LoadRequestQuery; +pub use self::v1_load_request_query::Cache; pub mod v1_load_request_query_filter_base; pub use self::v1_load_request_query_filter_base::V1LoadRequestQueryFilterBase; pub mod v1_load_request_query_filter_item; diff --git a/rust/cubesql/cubeclient/src/models/v1_load_request_query.rs b/rust/cubesql/cubeclient/src/models/v1_load_request_query.rs index ae966f9c65160..f6f56fc30fb32 100644 --- a/rust/cubesql/cubeclient/src/models/v1_load_request_query.rs +++ b/rust/cubesql/cubeclient/src/models/v1_load_request_query.rs @@ -29,6 +29,8 @@ pub struct V1LoadRequestQuery { pub offset: Option, #[serde(rename = "filters", skip_serializing_if = "Option::is_none")] pub filters: Option>, + #[serde(rename = "cache", skip_serializing_if = "Option::is_none")] + pub cache: Option, #[serde(rename = "ungrouped", skip_serializing_if = "Option::is_none")] pub ungrouped: Option, #[serde(rename = "subqueryJoins", skip_serializing_if = "Option::is_none")] @@ -48,9 +50,28 @@ impl V1LoadRequestQuery { limit: None, offset: None, filters: None, + cache: None, ungrouped: None, subquery_joins: None, join_hints: None, } } } +/// +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub enum Cache { + #[serde(rename = "stale-if-slow")] + StaleIfSlow, + #[serde(rename = "stale-while-revalidate")] + StaleWhileRevalidate, + #[serde(rename = "must-revalidate")] + MustRevalidate, + #[serde(rename = "no-cache")] + NoCache, +} + +impl Default for Cache { + fn default() -> Cache { + Self::StaleIfSlow + } +} From d0c267129260d42fc11c43a2c8c19fba0390dd6d Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Tue, 16 Sep 2025 10:49:13 +0300 Subject: [PATCH 18/53] pass cache mode to cubeScan --- .../cubejs-backend-native/src/node_export.rs | 11 ++++++++ rust/cubesql/cubeclient/src/models/mod.rs | 2 +- rust/cubesql/cubesql/src/compile/builder.rs | 1 + .../cubesql/src/compile/engine/df/wrapper.rs | 1 + .../cubesql/src/compile/rewrite/converter.rs | 24 ++++++++++++++++- rust/cubesql/cubesql/src/sql/session.rs | 26 +++++++++++++++++++ 6 files changed, 63 insertions(+), 2 deletions(-) diff --git a/packages/cubejs-backend-native/src/node_export.rs b/packages/cubejs-backend-native/src/node_export.rs index 499717ec090e0..37221146d8ff4 100644 --- a/packages/cubejs-backend-native/src/node_export.rs +++ b/packages/cubejs-backend-native/src/node_export.rs @@ -253,6 +253,17 @@ async fn handle_sql_query( .await?; } + let cache_enum = cache_mode.parse().map_err(|e| CubeError::user(e))?; + + { + let mut cm = session + .state + .cache_mode + .write() + .expect("failed to unlock session cache_mode for change"); + *cm = Some(cache_enum); + } + let session_clone = Arc::clone(&session); let span_id_clone = span_id.clone(); diff --git a/rust/cubesql/cubeclient/src/models/mod.rs b/rust/cubesql/cubeclient/src/models/mod.rs index 9a5a7886d0bdd..4fbc3a8adbcc0 100644 --- a/rust/cubesql/cubeclient/src/models/mod.rs +++ b/rust/cubesql/cubeclient/src/models/mod.rs @@ -31,8 +31,8 @@ pub use self::v1_error::V1Error; pub mod v1_load_request; pub use self::v1_load_request::V1LoadRequest; pub mod v1_load_request_query; -pub use self::v1_load_request_query::V1LoadRequestQuery; pub use self::v1_load_request_query::Cache; +pub use self::v1_load_request_query::V1LoadRequestQuery; pub mod v1_load_request_query_filter_base; pub use self::v1_load_request_query_filter_base::V1LoadRequestQueryFilterBase; pub mod v1_load_request_query_filter_item; diff --git a/rust/cubesql/cubesql/src/compile/builder.rs b/rust/cubesql/cubesql/src/compile/builder.rs index 7fdc2e86bc908..bc8f1b5ec094e 100644 --- a/rust/cubesql/cubesql/src/compile/builder.rs +++ b/rust/cubesql/cubesql/src/compile/builder.rs @@ -150,6 +150,7 @@ impl QueryBuilder { } else { None }, + cache: None, ungrouped: None, subquery_joins: None, join_hints: None, diff --git a/rust/cubesql/cubesql/src/compile/engine/df/wrapper.rs b/rust/cubesql/cubesql/src/compile/engine/df/wrapper.rs index 4b31eeb658169..1632f739d5fdb 100644 --- a/rust/cubesql/cubesql/src/compile/engine/df/wrapper.rs +++ b/rust/cubesql/cubesql/src/compile/engine/df/wrapper.rs @@ -3452,6 +3452,7 @@ impl WrappedSelectNode { } else { None }, + cache: None, // TODO is it okay to just override limit? limit: if let Some(limit) = self.limit { Some(limit as i32) diff --git a/rust/cubesql/cubesql/src/compile/rewrite/converter.rs b/rust/cubesql/cubesql/src/compile/rewrite/converter.rs index 3717d2e99d45d..08d50a4bd66d3 100644 --- a/rust/cubesql/cubesql/src/compile/rewrite/converter.rs +++ b/rust/cubesql/cubesql/src/compile/rewrite/converter.rs @@ -1,4 +1,5 @@ pub use super::rewriter::CubeRunner; +use crate::sql::session::CacheMode; use crate::{ compile::{ engine::df::{ @@ -38,7 +39,8 @@ use crate::{ CubeError, }; use cubeclient::models::{ - V1LoadRequestQuery, V1LoadRequestQueryFilterItem, V1LoadRequestQueryTimeDimension, + Cache as V1LoadRequestCache, V1LoadRequestQuery, V1LoadRequestQueryFilterItem, + V1LoadRequestQueryTimeDimension, }; use datafusion::{ arrow::datatypes::{DataType, TimeUnit}, @@ -1582,6 +1584,26 @@ impl LanguageToLogicalPlanConverter { let mut query_time_dimensions = Vec::new(); let mut query_order = Vec::new(); let mut query_dimensions = Vec::new(); + let cache_mode = &*self + .cube_context + .session_state + .cache_mode + .read() + .expect("failed to read lock for session cache_mode"); + + let v1_cache_mode = match cache_mode { + None => None, + Some(m) => match m { + CacheMode::StaleIfSlow => Some(V1LoadRequestCache::StaleIfSlow), + CacheMode::StaleWhileRevalidate => { + Some(V1LoadRequestCache::StaleWhileRevalidate) + } + CacheMode::MustRevalidate => Some(V1LoadRequestCache::MustRevalidate), + CacheMode::NoCache => Some(V1LoadRequestCache::NoCache), + }, + }; + + query.cache = v1_cache_mode; for m in members { match m { diff --git a/rust/cubesql/cubesql/src/sql/session.rs b/rust/cubesql/cubesql/src/sql/session.rs index 4c08e4b2f3aaa..97bb7b8145c67 100644 --- a/rust/cubesql/cubesql/src/sql/session.rs +++ b/rust/cubesql/cubesql/src/sql/session.rs @@ -1,6 +1,7 @@ use datafusion::scalar::ScalarValue; use log::trace; use rand::Rng; +use std::str::FromStr; use std::{ collections::HashMap, sync::{Arc, LazyLock, RwLock as RwLockSync, Weak}, @@ -53,6 +54,28 @@ pub enum QueryState { }, } +#[derive(Debug)] +pub enum CacheMode { + StaleIfSlow, + StaleWhileRevalidate, + MustRevalidate, + NoCache, +} + +impl FromStr for CacheMode { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "stale-if-slow" => Ok(Self::StaleIfSlow), + "stale-while-revalidate" => Ok(Self::StaleWhileRevalidate), + "must-revalidate" => Ok(Self::MustRevalidate), + "no-cache" => Ok(Self::NoCache), + other => Err(format!("Unknown cache mode: {}", other)), + } + } +} + #[derive(Debug)] pub struct SessionState { // connection id, immutable @@ -89,6 +112,8 @@ pub struct SessionState { pub statements: RWLockAsync>, auth_context_expiration: Duration, + + pub cache_mode: RwLockSync>, } impl SessionState { @@ -120,6 +145,7 @@ impl SessionState { query: RwLockSync::new(QueryState::None), statements: RWLockAsync::new(HashMap::new()), auth_context_expiration, + cache_mode: RwLockSync::new(None), } } From 2d5adbc8630f7ecd617ea6944be98ee5356694a3 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Tue, 16 Sep 2025 10:52:27 +0300 Subject: [PATCH 19/53] cargo clippy/fmt --- packages/cubejs-backend-native/src/node_export.rs | 2 +- rust/cubesql/cubeclient/src/models/v1_load_request_query.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cubejs-backend-native/src/node_export.rs b/packages/cubejs-backend-native/src/node_export.rs index 37221146d8ff4..42470dc7f9a4c 100644 --- a/packages/cubejs-backend-native/src/node_export.rs +++ b/packages/cubejs-backend-native/src/node_export.rs @@ -253,7 +253,7 @@ async fn handle_sql_query( .await?; } - let cache_enum = cache_mode.parse().map_err(|e| CubeError::user(e))?; + let cache_enum = cache_mode.parse().map_err(CubeError::user)?; { let mut cm = session diff --git a/rust/cubesql/cubeclient/src/models/v1_load_request_query.rs b/rust/cubesql/cubeclient/src/models/v1_load_request_query.rs index f6f56fc30fb32..0f97e3d3b9f14 100644 --- a/rust/cubesql/cubeclient/src/models/v1_load_request_query.rs +++ b/rust/cubesql/cubeclient/src/models/v1_load_request_query.rs @@ -57,7 +57,7 @@ impl V1LoadRequestQuery { } } } -/// + #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] pub enum Cache { #[serde(rename = "stale-if-slow")] From 6c857c1be8c2f18674da1bf0a1ee456295a956b9 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Wed, 17 Sep 2025 12:37:00 +0300 Subject: [PATCH 20/53] Implement background refresh --- .../src/orchestrator/QueryCache.ts | 36 +++++++++++++++++++ .../src/orchestrator/QueryOrchestrator.ts | 23 ++++++++++++ .../src/core/OrchestratorApi.ts | 10 +++++- 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts b/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts index 8313f438635bf..c8992aeb0e403 100644 --- a/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts +++ b/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts @@ -760,6 +760,42 @@ export class QueryCache { }); } + public async startBackgroundRefreshForQuery(queryBody: QueryBody, preAggregationsTablesToTempTables: PreAggTableToTempTable[]) { + const replacePreAggregationTableNames = + (queryAndParams: string | QueryWithParams) => ( + QueryCache.replacePreAggregationTableNames( + queryAndParams, + preAggregationsTablesToTempTables, + ) + ); + + const query = replacePreAggregationTableNames(queryBody.query); + const { values } = queryBody; + + const cacheKeyQueries = this + .cacheKeyQueriesFrom(queryBody) + .map(replacePreAggregationTableNames); + + const renewalThreshold = queryBody.cacheKeyQueries?.renewalThreshold; + const expireSecs = this.getExpireSecs(queryBody); + const cacheKey = QueryCache.queryCacheKey(queryBody); + + this.startRenewCycle( + query, + values, + cacheKeyQueries, + expireSecs, + cacheKey, + renewalThreshold, + { + external: queryBody.external, + requestId: queryBody.requestId, + dataSource: queryBody.dataSource, + persistent: false, // We don't need stream back as there will be no consumer + } + ); + } + public renewQuery( query: string | QueryWithParams, values: string[], diff --git a/packages/cubejs-query-orchestrator/src/orchestrator/QueryOrchestrator.ts b/packages/cubejs-query-orchestrator/src/orchestrator/QueryOrchestrator.ts index f55657f4a17c4..4041af964e31e 100644 --- a/packages/cubejs-query-orchestrator/src/orchestrator/QueryOrchestrator.ts +++ b/packages/cubejs-query-orchestrator/src/orchestrator/QueryOrchestrator.ts @@ -143,6 +143,29 @@ export class QueryOrchestrator { return this.preAggregations; } + /** + * Starts background refresh cycle for a query. + */ + public async startBackgroundRefresh(queryBody: QueryBody): Promise { + if (!queryBody.query) { + return; + } + + const { + preAggregationsTablesToTempTables, + values, + } = await this.preAggregations.loadAllPreAggregationsIfNeeded(queryBody); + + if (values) { + queryBody = { + ...queryBody, + values + }; + } + + await this.queryCache.startBackgroundRefreshForQuery(queryBody, preAggregationsTablesToTempTables); + } + /** * Force reconcile queue logic to be executed. */ diff --git a/packages/cubejs-server-core/src/core/OrchestratorApi.ts b/packages/cubejs-server-core/src/core/OrchestratorApi.ts index 49a01896f9912..48e9aed08a6af 100644 --- a/packages/cubejs-server-core/src/core/OrchestratorApi.ts +++ b/packages/cubejs-server-core/src/core/OrchestratorApi.ts @@ -155,7 +155,15 @@ export class OrchestratorApi { } if (query.cache === 'stale-while-revalidate' && fromCache) { - // TODO: Run background refresh + // Start background refresh + this.orchestrator.startBackgroundRefresh(query).catch(e => { + this.logger('Error starting background refresh', { + query: queryForLog, + requestId: query.requestId, + error: ((e as Error).stack || e) + }); + }); + return { ...fromCache, slowQuery: true From 34fcf6ae50f2e3696d29a2f3a56be2800f0ecbaa Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Wed, 17 Sep 2025 15:48:02 +0300 Subject: [PATCH 21/53] add cache mode descriptions --- .../cubejs-backend-shared/src/shared-types.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/cubejs-backend-shared/src/shared-types.ts b/packages/cubejs-backend-shared/src/shared-types.ts index f47ad6eb351f2..52eb4fc09db74 100644 --- a/packages/cubejs-backend-shared/src/shared-types.ts +++ b/packages/cubejs-backend-shared/src/shared-types.ts @@ -1 +1,22 @@ +/* +stale-if-slow (default) — equivalent to previously used renewQuery: false + If refresh keys are up-to-date, returns the value from cache + If refresh keys are expired, tries to return the value from the database + Returns fresh value from the database if the query executed in the database until the first “Continue wait” interval is reached + Returns stale value from cache otherwise + +stale-while-revalidate — AKA “backgroundRefresh” + If refresh keys are up-to-date, returns the value from cache + If refresh keys are expired, returns stale data from cache + Updates the cache in background + +must-revalidate — equivalent to previously used renewQuery: true + If refresh keys are up-to-date, returns the value from cache + If refresh keys are expired, tries to return the value from the database + Returns fresh value from the database even if it takes minutes and many “Continue wait” intervals + +no-cache — AKA “forceRefresh” + Skips refresh key checks + Returns fresh data from the database, even if it takes minutes and many “Continue wait” intervals +*/ export type CacheMode = 'stale-if-slow' | 'stale-while-revalidate' | 'must-revalidate' | 'no-cache'; From d62a495e0a93632ecb0979ec94a3c925ca2b224a Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Thu, 18 Sep 2025 11:40:37 +0300 Subject: [PATCH 22/53] remove query cache mode from normalize query --- packages/cubejs-api-gateway/src/query.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/cubejs-api-gateway/src/query.js b/packages/cubejs-api-gateway/src/query.js index 4495d32434657..37e056ca0f5da 100644 --- a/packages/cubejs-api-gateway/src/query.js +++ b/packages/cubejs-api-gateway/src/query.js @@ -336,17 +336,8 @@ const normalizeQuery = (query, persistent) => { newLimit = query.limit; } - let cacheMode = query.cache; - // Convert deprecated renewQuery option to new cache mode - if (query.renewQuery && !query.cache) { - cacheMode = 'must-revalidate'; - } else if (!query.renewQuery && !query.cache) { - cacheMode = 'stale-if-slow'; - } - return { ...query, - ...{ cache: cacheMode }, ...(query.order ? { order: normalizeQueryOrder(query.order) } : {}), limit: newLimit, timezone, From 2278a2b83f6e9159c510e069706d21efc799381d Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Thu, 18 Sep 2025 11:46:51 +0300 Subject: [PATCH 23/53] pass cacheMode to getSqlResponseInternal --- packages/cubejs-api-gateway/src/gateway.ts | 9 +++++++-- packages/cubejs-api-gateway/src/graphql.ts | 1 + packages/cubejs-api-gateway/src/types/request.ts | 2 ++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/cubejs-api-gateway/src/gateway.ts b/packages/cubejs-api-gateway/src/gateway.ts index 66530d6907c6c..0892c972c454d 100644 --- a/packages/cubejs-api-gateway/src/gateway.ts +++ b/packages/cubejs-api-gateway/src/gateway.ts @@ -12,6 +12,7 @@ import { getRealType, parseUtcIntoLocalDate, QueryAlias, + CacheMode, } from '@cubejs-backend/shared'; import { ResultArrayWrapper, @@ -317,6 +318,7 @@ class ApiGateway { context: req.context, res: this.resToResultFn(res), queryType: req.query.queryType, + cacheMode: req.query.cache, }); })); @@ -326,7 +328,8 @@ class ApiGateway { query: req.body.query, context: req.context, res: this.resToResultFn(res), - queryType: req.body.queryType + queryType: req.body.queryType, + cacheMode: req.body.cache, }); })); @@ -335,7 +338,8 @@ class ApiGateway { query: req.query.query, context: req.context, res: this.resToResultFn(res), - queryType: req.query.queryType + queryType: req.query.queryType, + cacheMode: req.query.cache, }); })); @@ -1642,6 +1646,7 @@ class ApiGateway { context: RequestContext, normalizedQuery: NormalizedQuery, sqlQuery: any, + cacheMode: CacheMode = 'stale-if-slow', ): Promise { const queries = [{ ...sqlQuery, diff --git a/packages/cubejs-api-gateway/src/graphql.ts b/packages/cubejs-api-gateway/src/graphql.ts index d35039cfe00a0..b9002bf4ade5d 100644 --- a/packages/cubejs-api-gateway/src/graphql.ts +++ b/packages/cubejs-api-gateway/src/graphql.ts @@ -653,6 +653,7 @@ export function makeSchema(metaConfig: any): GraphQLSchema { apiGateway.load({ query, queryType: QueryType.REGULAR_QUERY, + ...(query.cache ? { cacheMode: query.cache } : {}), context: req.context, res: async (message) => { if (message.error) { diff --git a/packages/cubejs-api-gateway/src/types/request.ts b/packages/cubejs-api-gateway/src/types/request.ts index 9ff0ba90121b6..21a7685f9226b 100644 --- a/packages/cubejs-api-gateway/src/types/request.ts +++ b/packages/cubejs-api-gateway/src/types/request.ts @@ -7,6 +7,7 @@ import type { Request as ExpressRequest } from 'express'; import type { DataResult } from '@cubejs-backend/native'; +import { CacheMode } from '@cubejs-backend/shared'; import { RequestType, ApiType, ResultType } from './strings'; import { Query } from './query'; @@ -133,6 +134,7 @@ type QueryRequest = BaseRequest & { memberExpressions?: boolean; disableExternalPreAggregations?: boolean; disableLimitEnforcing?: boolean; + cacheMode?: CacheMode; }; type SqlApiRequest = BaseRequest & { From e82a230b4146d5e957e4ca00f2da89353eb1d737 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Thu, 18 Sep 2025 11:49:09 +0300 Subject: [PATCH 24/53] remove obsolete --- packages/cubejs-api-gateway/src/gateway.ts | 5 ----- packages/cubejs-api-gateway/src/query.js | 1 - packages/cubejs-api-gateway/src/types/query.ts | 2 -- 3 files changed, 8 deletions(-) diff --git a/packages/cubejs-api-gateway/src/gateway.ts b/packages/cubejs-api-gateway/src/gateway.ts index 0892c972c454d..0489c5669522f 100644 --- a/packages/cubejs-api-gateway/src/gateway.ts +++ b/packages/cubejs-api-gateway/src/gateway.ts @@ -1654,7 +1654,6 @@ class ApiGateway { values: sqlQuery.sql[1], continueWait: true, renewQuery: normalizedQuery.renewQuery, - cache: normalizedQuery.cache, requestId: context.requestId, context, persistent: false, @@ -1679,7 +1678,6 @@ class ApiGateway { values: totalQuery.sql[1], continueWait: true, renewQuery: normalizedTotal.renewQuery, - cache: normalizedTotal.cache, requestId: context.requestId, context }); @@ -1801,7 +1799,6 @@ class ApiGateway { values: sqlQuery.sql[1], continueWait: true, renewQuery: false, - cache: 'stale-if-slow', requestId: context.requestId, context, persistent: true, @@ -1995,7 +1992,6 @@ class ApiGateway { values: sqlQuery.values || sqlQuery.sql[1], continueWait: true, renewQuery: false, - cache: 'stale-if-slow', requestId: context.requestId, context, persistent: true, @@ -2015,7 +2011,6 @@ class ApiGateway { values: request.sqlQuery[1], continueWait: true, renewQuery: normalizedQueries[0].renewQuery, - cache: normalizedQueries[0].cache, requestId: context.requestId, context, ...sqlQueries[0], diff --git a/packages/cubejs-api-gateway/src/query.js b/packages/cubejs-api-gateway/src/query.js index 37e056ca0f5da..8bee96aa83802 100644 --- a/packages/cubejs-api-gateway/src/query.js +++ b/packages/cubejs-api-gateway/src/query.js @@ -189,7 +189,6 @@ const querySchema = Joi.object().keys({ total: Joi.boolean(), // @deprecated renewQuery: Joi.boolean(), - cache: Joi.valid('stale-if-slow', 'stale-while-revalidate', 'must-revalidate', 'no-cache').default('stale-if-slow'), ungrouped: Joi.boolean(), responseFormat: Joi.valid('default', 'compact'), subqueryJoins: Joi.array().items(subqueryJoin), diff --git a/packages/cubejs-api-gateway/src/types/query.ts b/packages/cubejs-api-gateway/src/types/query.ts index 6f83c4d04b14d..ef8deb3bd8c89 100644 --- a/packages/cubejs-api-gateway/src/types/query.ts +++ b/packages/cubejs-api-gateway/src/types/query.ts @@ -5,7 +5,6 @@ * Network query data types definition. */ -import { CacheMode } from '@cubejs-backend/shared'; import { Member, TimeMember, @@ -142,7 +141,6 @@ interface Query { timezone?: string; // @deprecated renewQuery?: boolean; - cache?: CacheMode; ungrouped?: boolean; responseFormat?: ResultType; From 25229eca77963d0d5ee85402f5a4b00f55d85558 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Thu, 18 Sep 2025 12:01:38 +0300 Subject: [PATCH 25/53] add cacheMode as input param in orchestratorApi --- packages/cubejs-api-gateway/src/gateway.ts | 3 ++- packages/cubejs-server-core/src/core/OrchestratorApi.ts | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/cubejs-api-gateway/src/gateway.ts b/packages/cubejs-api-gateway/src/gateway.ts index 0489c5669522f..b3f04324b58a6 100644 --- a/packages/cubejs-api-gateway/src/gateway.ts +++ b/packages/cubejs-api-gateway/src/gateway.ts @@ -1685,7 +1685,7 @@ class ApiGateway { const [response, total] = await Promise.all( queries.map(async (query) => { const res = await (await this.getAdapterApi(context)) - .executeQuery(query); + .executeQuery(query, cacheMode); return res; }) ); @@ -1891,6 +1891,7 @@ class ApiGateway { context, normalizedQuery, sqlQueries[index], + props.cacheMode, ); const annotation = prepareAnnotation( diff --git a/packages/cubejs-server-core/src/core/OrchestratorApi.ts b/packages/cubejs-server-core/src/core/OrchestratorApi.ts index 48e9aed08a6af..ae49578b1364d 100644 --- a/packages/cubejs-server-core/src/core/OrchestratorApi.ts +++ b/packages/cubejs-server-core/src/core/OrchestratorApi.ts @@ -11,6 +11,7 @@ import { } from '@cubejs-backend/query-orchestrator'; import { DatabaseType, RequestContext } from './types'; +import { CacheMode } from '@cubejs-backend/shared'; export interface OrchestratorApiOptions extends QueryOrchestratorOptions { contextToDbType: (dataSource: string) => Promise; @@ -60,7 +61,7 @@ export class OrchestratorApi { * * @throw Error */ - public async streamQuery(query: QueryBody): Promise { + public async streamQuery(query: QueryBody, cacheMode: CacheMode = 'stale-if-slow'): Promise { // TODO merge with fetchQuery return this.orchestrator.streamQuery(query); } @@ -70,7 +71,7 @@ export class OrchestratorApi { * less than `continueWaitTimeout` seconds, throw `ContinueWaitError` * error otherwise. */ - public async executeQuery(query: QueryBody) { + public async executeQuery(query: QueryBody, cacheMode: CacheMode = 'stale-if-slow') { const queryForLog = query.query?.replace(/\s+/g, ' '); const startQueryTime = (new Date()).getTime(); From ce573bda793066f9aa5c47e1c54eee6555b34b79 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Thu, 18 Sep 2025 12:32:03 +0300 Subject: [PATCH 26/53] open api spec fix --- packages/cubejs-api-gateway/openspec.yml | 14 ++++++------- rust/cubesql/cubeclient/src/models/mod.rs | 2 +- .../cubeclient/src/models/v1_load_request.rs | 21 +++++++++++++++++++ .../src/models/v1_load_request_query.rs | 21 ------------------- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/packages/cubejs-api-gateway/openspec.yml b/packages/cubejs-api-gateway/openspec.yml index d4da1d397efde..681978404dc42 100644 --- a/packages/cubejs-api-gateway/openspec.yml +++ b/packages/cubejs-api-gateway/openspec.yml @@ -467,13 +467,6 @@ components: type: "array" items: $ref: "#/components/schemas/V1LoadRequestQueryFilterItem" - cache: - type: "string" - enum: - - stale-if-slow - - stale-while-revalidate - - must-revalidate - - no-cache ungrouped: type: "boolean" # vector of (subquery sql: string, join condition: member expression, join type: enum) @@ -491,6 +484,13 @@ components: properties: queryType: type: "string" + cache: + type: "string" + enum: + - stale-if-slow + - stale-while-revalidate + - must-revalidate + - no-cache query: type: "object" $ref: "#/components/schemas/V1LoadRequestQuery" diff --git a/rust/cubesql/cubeclient/src/models/mod.rs b/rust/cubesql/cubeclient/src/models/mod.rs index 4fbc3a8adbcc0..5ee41ef768ef0 100644 --- a/rust/cubesql/cubeclient/src/models/mod.rs +++ b/rust/cubesql/cubeclient/src/models/mod.rs @@ -29,9 +29,9 @@ pub use self::v1_cube_meta_type::V1CubeMetaType; pub mod v1_error; pub use self::v1_error::V1Error; pub mod v1_load_request; +pub use self::v1_load_request::Cache; pub use self::v1_load_request::V1LoadRequest; pub mod v1_load_request_query; -pub use self::v1_load_request_query::Cache; pub use self::v1_load_request_query::V1LoadRequestQuery; pub mod v1_load_request_query_filter_base; pub use self::v1_load_request_query_filter_base::V1LoadRequestQueryFilterBase; diff --git a/rust/cubesql/cubeclient/src/models/v1_load_request.rs b/rust/cubesql/cubeclient/src/models/v1_load_request.rs index c9d0c5e28fedb..8363498c4fa63 100644 --- a/rust/cubesql/cubeclient/src/models/v1_load_request.rs +++ b/rust/cubesql/cubeclient/src/models/v1_load_request.rs @@ -15,6 +15,8 @@ use serde::{Deserialize, Serialize}; pub struct V1LoadRequest { #[serde(rename = "queryType", skip_serializing_if = "Option::is_none")] pub query_type: Option, + #[serde(rename = "cache", skip_serializing_if = "Option::is_none")] + pub cache: Option, #[serde(rename = "query", skip_serializing_if = "Option::is_none")] pub query: Option, } @@ -23,7 +25,26 @@ impl V1LoadRequest { pub fn new() -> V1LoadRequest { V1LoadRequest { query_type: None, + cache: None, query: None, } } } + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub enum Cache { + #[serde(rename = "stale-if-slow")] + StaleIfSlow, + #[serde(rename = "stale-while-revalidate")] + StaleWhileRevalidate, + #[serde(rename = "must-revalidate")] + MustRevalidate, + #[serde(rename = "no-cache")] + NoCache, +} + +impl Default for Cache { + fn default() -> Cache { + Self::StaleIfSlow + } +} diff --git a/rust/cubesql/cubeclient/src/models/v1_load_request_query.rs b/rust/cubesql/cubeclient/src/models/v1_load_request_query.rs index 0f97e3d3b9f14..ae966f9c65160 100644 --- a/rust/cubesql/cubeclient/src/models/v1_load_request_query.rs +++ b/rust/cubesql/cubeclient/src/models/v1_load_request_query.rs @@ -29,8 +29,6 @@ pub struct V1LoadRequestQuery { pub offset: Option, #[serde(rename = "filters", skip_serializing_if = "Option::is_none")] pub filters: Option>, - #[serde(rename = "cache", skip_serializing_if = "Option::is_none")] - pub cache: Option, #[serde(rename = "ungrouped", skip_serializing_if = "Option::is_none")] pub ungrouped: Option, #[serde(rename = "subqueryJoins", skip_serializing_if = "Option::is_none")] @@ -50,28 +48,9 @@ impl V1LoadRequestQuery { limit: None, offset: None, filters: None, - cache: None, ungrouped: None, subquery_joins: None, join_hints: None, } } } - -#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] -pub enum Cache { - #[serde(rename = "stale-if-slow")] - StaleIfSlow, - #[serde(rename = "stale-while-revalidate")] - StaleWhileRevalidate, - #[serde(rename = "must-revalidate")] - MustRevalidate, - #[serde(rename = "no-cache")] - NoCache, -} - -impl Default for Cache { - fn default() -> Cache { - Self::StaleIfSlow - } -} From 219ba5730b291bff27e99205ac2e107cd6ce1b99 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Thu, 18 Sep 2025 12:48:43 +0300 Subject: [PATCH 27/53] fix cubesql after introducing cacheMode --- rust/cubesql/cubesql/src/compile/builder.rs | 1 - .../cubesql/src/compile/engine/df/scan.rs | 70 +++++++++++++------ .../cubesql/src/compile/engine/df/wrapper.rs | 1 - .../cubesql/src/compile/rewrite/converter.rs | 32 +++------ rust/cubesql/cubesql/src/compile/test/mod.rs | 2 + rust/cubesql/cubesql/src/sql/session.rs | 24 +------ rust/cubesql/cubesql/src/transport/mod.rs | 1 + rust/cubesql/cubesql/src/transport/service.rs | 17 +++++ 8 files changed, 78 insertions(+), 70 deletions(-) diff --git a/rust/cubesql/cubesql/src/compile/builder.rs b/rust/cubesql/cubesql/src/compile/builder.rs index bc8f1b5ec094e..7fdc2e86bc908 100644 --- a/rust/cubesql/cubesql/src/compile/builder.rs +++ b/rust/cubesql/cubesql/src/compile/builder.rs @@ -150,7 +150,6 @@ impl QueryBuilder { } else { None }, - cache: None, ungrouped: None, subquery_joins: None, join_hints: None, diff --git a/rust/cubesql/cubesql/src/compile/engine/df/scan.rs b/rust/cubesql/cubesql/src/compile/engine/df/scan.rs index cf6e6af5f28e4..0f13ba6981a95 100644 --- a/rust/cubesql/cubesql/src/compile/engine/df/scan.rs +++ b/rust/cubesql/cubesql/src/compile/engine/df/scan.rs @@ -1,4 +1,16 @@ +use crate::compile::date_parser::parse_date_str; +use crate::{ + compile::{ + engine::df::wrapper::{CubeScanWrappedSqlNode, CubeScanWrapperNode, SqlQuery}, + test::find_cube_scans_deep_search, + }, + config::ConfigObj, + sql::AuthContextRef, + transport::{CubeStreamReceiver, LoadRequestMeta, SpanId, TransportService}, + CubeError, +}; use async_trait::async_trait; +use chrono::{Datelike, NaiveDate}; use cubeclient::models::{V1LoadRequestQuery, V1LoadResponse}; pub use datafusion::{ arrow::{ @@ -18,28 +30,6 @@ pub use datafusion::{ Partitioning, PhysicalPlanner, RecordBatchStream, SendableRecordBatchStream, Statistics, }, }; -use futures::Stream; -use log::warn; -use std::{ - any::Any, - borrow::Cow, - fmt, - sync::Arc, - task::{Context, Poll}, -}; - -use crate::compile::date_parser::parse_date_str; -use crate::{ - compile::{ - engine::df::wrapper::{CubeScanWrappedSqlNode, CubeScanWrapperNode, SqlQuery}, - test::find_cube_scans_deep_search, - }, - config::ConfigObj, - sql::AuthContextRef, - transport::{CubeStreamReceiver, LoadRequestMeta, SpanId, TransportService}, - CubeError, -}; -use chrono::{Datelike, NaiveDate}; use datafusion::{ arrow::{ array::{ @@ -51,7 +41,17 @@ use datafusion::{ execution::context::TaskContext, scalar::ScalarValue, }; +use futures::Stream; +use log::warn; use serde_json::Value; +use std::str::FromStr; +use std::{ + any::Any, + borrow::Cow, + fmt, + sync::Arc, + task::{Context, Poll}, +}; #[derive(Debug, Clone, Eq, PartialEq)] pub struct RegularMember { @@ -79,10 +79,33 @@ impl MemberField { } } +#[derive(Debug, Clone)] +pub enum CacheMode { + StaleIfSlow, + StaleWhileRevalidate, + MustRevalidate, + NoCache, +} + +impl FromStr for CacheMode { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + match s { + "stale-if-slow" => Ok(Self::StaleIfSlow), + "stale-while-revalidate" => Ok(Self::StaleWhileRevalidate), + "must-revalidate" => Ok(Self::MustRevalidate), + "no-cache" => Ok(Self::NoCache), + other => Err(format!("Unknown cache mode: {}", other)), + } + } +} + #[derive(Debug, Clone)] pub struct CubeScanOptions { pub change_user: Option, pub max_records: Option, + pub cache_mode: Option, } #[derive(Debug, Clone)] @@ -682,6 +705,7 @@ async fn load_data( meta, schema, member_fields, + options.cache_mode, ) .await .map_err(|err| ArrowError::ComputeError(err.to_string()))?; @@ -1192,6 +1216,7 @@ mod tests { _meta_fields: LoadRequestMeta, schema: SchemaRef, member_fields: Vec, + _cache_mode: Option, ) -> Result, CubeError> { let response = r#" { @@ -1317,6 +1342,7 @@ mod tests { options: CubeScanOptions { change_user: None, max_records: None, + cache_mode: None, }, transport: get_test_transport(), meta: get_test_load_meta(DatabaseProtocol::PostgreSQL), diff --git a/rust/cubesql/cubesql/src/compile/engine/df/wrapper.rs b/rust/cubesql/cubesql/src/compile/engine/df/wrapper.rs index 1632f739d5fdb..4b31eeb658169 100644 --- a/rust/cubesql/cubesql/src/compile/engine/df/wrapper.rs +++ b/rust/cubesql/cubesql/src/compile/engine/df/wrapper.rs @@ -3452,7 +3452,6 @@ impl WrappedSelectNode { } else { None }, - cache: None, // TODO is it okay to just override limit? limit: if let Some(limit) = self.limit { Some(limit as i32) diff --git a/rust/cubesql/cubesql/src/compile/rewrite/converter.rs b/rust/cubesql/cubesql/src/compile/rewrite/converter.rs index 08d50a4bd66d3..68b715d99f4ad 100644 --- a/rust/cubesql/cubesql/src/compile/rewrite/converter.rs +++ b/rust/cubesql/cubesql/src/compile/rewrite/converter.rs @@ -1,5 +1,4 @@ pub use super::rewriter::CubeRunner; -use crate::sql::session::CacheMode; use crate::{ compile::{ engine::df::{ @@ -39,8 +38,7 @@ use crate::{ CubeError, }; use cubeclient::models::{ - Cache as V1LoadRequestCache, V1LoadRequestQuery, V1LoadRequestQueryFilterItem, - V1LoadRequestQueryTimeDimension, + V1LoadRequestQuery, V1LoadRequestQueryFilterItem, V1LoadRequestQueryTimeDimension, }; use datafusion::{ arrow::datatypes::{DataType, TimeUnit}, @@ -1584,26 +1582,6 @@ impl LanguageToLogicalPlanConverter { let mut query_time_dimensions = Vec::new(); let mut query_order = Vec::new(); let mut query_dimensions = Vec::new(); - let cache_mode = &*self - .cube_context - .session_state - .cache_mode - .read() - .expect("failed to read lock for session cache_mode"); - - let v1_cache_mode = match cache_mode { - None => None, - Some(m) => match m { - CacheMode::StaleIfSlow => Some(V1LoadRequestCache::StaleIfSlow), - CacheMode::StaleWhileRevalidate => { - Some(V1LoadRequestCache::StaleWhileRevalidate) - } - CacheMode::MustRevalidate => Some(V1LoadRequestCache::MustRevalidate), - CacheMode::NoCache => Some(V1LoadRequestCache::NoCache), - }, - }; - - query.cache = v1_cache_mode; for m in members { match m { @@ -2077,6 +2055,13 @@ impl LanguageToLogicalPlanConverter { let member_fields = fields.iter().map(|(_, m)| m.clone()).collect(); + let cache_mode = &*self + .cube_context + .session_state + .cache_mode + .read() + .expect("failed to read lock for session cache_mode"); + let node = Arc::new(CubeScanNode::new( Arc::new(DFSchema::new_with_metadata( fields.into_iter().map(|(f, _)| f).collect(), @@ -2088,6 +2073,7 @@ impl LanguageToLogicalPlanConverter { CubeScanOptions { change_user, max_records, + cache_mode: cache_mode.clone(), }, alias_to_cube.into_iter().map(|(_, c)| c).unique().collect(), self.span_id.clone(), diff --git a/rust/cubesql/cubesql/src/compile/test/mod.rs b/rust/cubesql/cubesql/src/compile/test/mod.rs index 9e78921bf910f..7efc5fdb0ab94 100644 --- a/rust/cubesql/cubesql/src/compile/test/mod.rs +++ b/rust/cubesql/cubesql/src/compile/test/mod.rs @@ -47,6 +47,7 @@ pub mod test_user_change; #[cfg(test)] pub mod test_wrapper; pub mod utils; +use crate::compile::engine::df::scan::CacheMode; use crate::compile::{ arrow::record_batch::RecordBatch, engine::df::scan::convert_transport_response, }; @@ -911,6 +912,7 @@ impl TransportService for TestConnectionTransport { meta: LoadRequestMeta, schema: SchemaRef, member_fields: Vec, + _cache_mode: Option, ) -> Result, CubeError> { { let mut calls = self.load_calls.lock().await; diff --git a/rust/cubesql/cubesql/src/sql/session.rs b/rust/cubesql/cubesql/src/sql/session.rs index 97bb7b8145c67..a51b4cf6fe6d3 100644 --- a/rust/cubesql/cubesql/src/sql/session.rs +++ b/rust/cubesql/cubesql/src/sql/session.rs @@ -1,7 +1,6 @@ use datafusion::scalar::ScalarValue; use log::trace; use rand::Rng; -use std::str::FromStr; use std::{ collections::HashMap, sync::{Arc, LazyLock, RwLock as RwLockSync, Weak}, @@ -10,6 +9,7 @@ use std::{ use tokio_util::sync::CancellationToken; use super::{server_manager::ServerManager, session_manager::SessionManager, AuthContextRef}; +use crate::compile::engine::df::scan::CacheMode; use crate::{ compile::{ DatabaseProtocol, DatabaseProtocolDetails, DatabaseVariable, DatabaseVariables, @@ -54,28 +54,6 @@ pub enum QueryState { }, } -#[derive(Debug)] -pub enum CacheMode { - StaleIfSlow, - StaleWhileRevalidate, - MustRevalidate, - NoCache, -} - -impl FromStr for CacheMode { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "stale-if-slow" => Ok(Self::StaleIfSlow), - "stale-while-revalidate" => Ok(Self::StaleWhileRevalidate), - "must-revalidate" => Ok(Self::MustRevalidate), - "no-cache" => Ok(Self::NoCache), - other => Err(format!("Unknown cache mode: {}", other)), - } - } -} - #[derive(Debug)] pub struct SessionState { // connection id, immutable diff --git a/rust/cubesql/cubesql/src/transport/mod.rs b/rust/cubesql/cubesql/src/transport/mod.rs index 305abe26b20a8..0ae565f2b51dc 100644 --- a/rust/cubesql/cubesql/src/transport/mod.rs +++ b/rust/cubesql/cubesql/src/transport/mod.rs @@ -22,6 +22,7 @@ pub type CubeMetaFormat = cubeclient::models::V1CubeMetaFormat; pub type TransportLoadResponse = cubeclient::models::V1LoadResponse; pub type TransportLoadRequestQuery = cubeclient::models::V1LoadRequestQuery; pub type TransportLoadRequest = cubeclient::models::V1LoadRequest; +pub type TransportLoadRequestCacheMode = cubeclient::models::Cache; pub type TransportMetaResponse = cubeclient::models::V1MetaResponse; pub type TransportError = cubeclient::models::V1Error; diff --git a/rust/cubesql/cubesql/src/transport/service.rs b/rust/cubesql/cubesql/src/transport/service.rs index 8b5a9dabdf20f..e6e5e32591f7d 100644 --- a/rust/cubesql/cubesql/src/transport/service.rs +++ b/rust/cubesql/cubesql/src/transport/service.rs @@ -25,6 +25,8 @@ use tokio::{ }; use uuid::Uuid; +use crate::compile::engine::df::scan::CacheMode; +use crate::transport::TransportLoadRequestCacheMode; use crate::{ compile::{ engine::df::{ @@ -142,6 +144,7 @@ pub trait TransportService: Send + Sync + Debug { meta_fields: LoadRequestMeta, schema: SchemaRef, member_fields: Vec, + cache_mode: Option, ) -> Result, CubeError>; async fn load_stream( @@ -282,6 +285,7 @@ impl TransportService for HttpTransport { meta: LoadRequestMeta, schema: SchemaRef, member_fields: Vec, + cache_mode: Option, ) -> Result, CubeError> { if meta.change_user().is_some() { return Err(CubeError::internal( @@ -290,10 +294,23 @@ impl TransportService for HttpTransport { )); } + let cache_mode = match cache_mode { + None => None, + Some(m) => match m { + CacheMode::StaleIfSlow => Some(TransportLoadRequestCacheMode::StaleIfSlow), + CacheMode::StaleWhileRevalidate => { + Some(TransportLoadRequestCacheMode::StaleWhileRevalidate) + } + CacheMode::MustRevalidate => Some(TransportLoadRequestCacheMode::MustRevalidate), + CacheMode::NoCache => Some(TransportLoadRequestCacheMode::NoCache), + }, + }; + // TODO: support meta_fields for HTTP let request = TransportLoadRequest { query: Some(query), query_type: Some("multi".to_string()), + cache: cache_mode, }; let response = cube_api::load_v1(&self.get_client_config_for_ctx(ctx), Some(request)).await?; From 193233922e52b27ca04a6913f33ee68181e3145f Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Thu, 18 Sep 2025 13:11:30 +0300 Subject: [PATCH 28/53] =?UTF-8?q?rename=20cache=20=E2=86=92=20cacheMode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cubejs-api-gateway/src/gateway.ts | 20 ++++++++++++------- packages/cubejs-api-gateway/src/sql-server.ts | 2 ++ .../cubejs-api-gateway/src/types/request.ts | 1 + .../src/orchestrator/PreAggregations.ts | 4 ++-- .../src/orchestrator/QueryCache.ts | 6 +++--- .../src/orchestrator/QueryOrchestrator.ts | 4 ++-- .../src/core/OrchestratorApi.ts | 8 ++++---- 7 files changed, 27 insertions(+), 18 deletions(-) diff --git a/packages/cubejs-api-gateway/src/gateway.ts b/packages/cubejs-api-gateway/src/gateway.ts index b3f04324b58a6..0a8382e8246ca 100644 --- a/packages/cubejs-api-gateway/src/gateway.ts +++ b/packages/cubejs-api-gateway/src/gateway.ts @@ -29,6 +29,7 @@ import type { } from 'express'; import { createProxyMiddleware } from 'http-proxy-middleware'; +import { QueryBody } from '@cubejs-backend/query-orchestrator'; import { QueryType, ApiScopes, @@ -734,7 +735,7 @@ class ApiGateway { const preAggregationPartitionsWithoutError = preAggregationPartitions.filter(p => !p?.errors?.length); const versionEntriesResult = preAggregationPartitions && - await orchestratorApi.getPreAggregationVersionEntries( + orchestratorApi.getPreAggregationVersionEntries( context, preAggregationPartitionsWithoutError, compilerApi.preAggregationsSchema @@ -1648,12 +1649,13 @@ class ApiGateway { sqlQuery: any, cacheMode: CacheMode = 'stale-if-slow', ): Promise { - const queries = [{ + const queries: QueryBody[] = [{ ...sqlQuery, query: sqlQuery.sql[0], values: sqlQuery.sql[1], continueWait: true, renewQuery: normalizedQuery.renewQuery, + cacheMode, requestId: context.requestId, context, persistent: false, @@ -1678,6 +1680,7 @@ class ApiGateway { values: totalQuery.sql[1], continueWait: true, renewQuery: normalizedTotal.renewQuery, + cacheMode, requestId: context.requestId, context }); @@ -1685,7 +1688,7 @@ class ApiGateway { const [response, total] = await Promise.all( queries.map(async (query) => { const res = await (await this.getAdapterApi(context)) - .executeQuery(query, cacheMode); + .executeQuery(query); return res; }) ); @@ -1793,12 +1796,13 @@ class ApiGateway { this.log({ type: 'Load Request', query, streaming: true }, context); const [, normalizedQueries] = await this.getNormalizedQueries(query, context, true); const sqlQuery = (await this.getSqlQueriesInternal(context, normalizedQueries))[0]; - const q = { + const q: QueryBody = { ...sqlQuery, query: sqlQuery.sql[0], values: sqlQuery.sql[1], continueWait: true, renewQuery: false, + cacheMode: 'stale-if-slow', requestId: context.requestId, context, persistent: true, @@ -1982,17 +1986,18 @@ class ApiGateway { normalizedQueries.map(q => ({ ...q, disableExternalPreAggregations: request.sqlQuery })) ); - let results; + let results: any[]; let slowQuery = false; const streamResponse = async (sqlQuery) => { - const q = { + const q: QueryBody = { ...sqlQuery, query: sqlQuery.query || sqlQuery.sql[0], values: sqlQuery.values || sqlQuery.sql[1], continueWait: true, renewQuery: false, + cacheMode: 'stale-if-slow', requestId: context.requestId, context, persistent: true, @@ -2007,11 +2012,12 @@ class ApiGateway { }; if (request.sqlQuery) { - const finalQuery = { + const finalQuery: QueryBody = { query: request.sqlQuery[0], values: request.sqlQuery[1], continueWait: true, renewQuery: normalizedQueries[0].renewQuery, + cacheMode: request.cacheMode, requestId: context.requestId, context, ...sqlQueries[0], diff --git a/packages/cubejs-api-gateway/src/sql-server.ts b/packages/cubejs-api-gateway/src/sql-server.ts index b38b9ec7a15c5..68988026407ec 100644 --- a/packages/cubejs-api-gateway/src/sql-server.ts +++ b/packages/cubejs-api-gateway/src/sql-server.ts @@ -210,6 +210,8 @@ export class SQLServer { sqlApiLoad: async ({ request, session, query, queryKey, sqlQuery, streaming }) => { const context = await contextByRequest(request, session); + // XXX: Should we pass cacheMode somehow? + // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { try { diff --git a/packages/cubejs-api-gateway/src/types/request.ts b/packages/cubejs-api-gateway/src/types/request.ts index 21a7685f9226b..ed9310b5e95da 100644 --- a/packages/cubejs-api-gateway/src/types/request.ts +++ b/packages/cubejs-api-gateway/src/types/request.ts @@ -144,6 +144,7 @@ type SqlApiRequest = BaseRequest & { queryKey: any; streaming?: boolean; memberExpressions?: boolean; + cacheMode?: CacheMode; }; /** diff --git a/packages/cubejs-query-orchestrator/src/orchestrator/PreAggregations.ts b/packages/cubejs-query-orchestrator/src/orchestrator/PreAggregations.ts index e8048586b072e..14c1150bb2c69 100644 --- a/packages/cubejs-query-orchestrator/src/orchestrator/PreAggregations.ts +++ b/packages/cubejs-query-orchestrator/src/orchestrator/PreAggregations.ts @@ -498,7 +498,7 @@ export class PreAggregations { maxPartitions: this.options.maxPartitions, maxSourceRowLimit: this.options.maxSourceRowLimit, isJob: queryBody.isJob, - waitForRenew: queryBody.cache !== undefined ? queryBody.cache === 'must-revalidate' : queryBody.renewQuery, + waitForRenew: queryBody.cacheMode !== undefined ? queryBody.cacheMode === 'must-revalidate' : queryBody.renewQuery, // TODO workaround to avoid continuous waiting on building pre-aggregation dependencies forceBuild: i === preAggregations.length - 1 ? queryBody.forceBuildPreAggregations : false, requestId: queryBody.requestId, @@ -603,7 +603,7 @@ export class PreAggregations { { maxPartitions: this.options.maxPartitions, maxSourceRowLimit: this.options.maxSourceRowLimit, - waitForRenew: queryBody.cache !== undefined ? queryBody.cache === 'must-revalidate' : queryBody.renewQuery, + waitForRenew: queryBody.cacheMode !== undefined ? queryBody.cacheMode === 'must-revalidate' : queryBody.renewQuery, requestId: queryBody.requestId, externalRefresh: this.externalRefresh, compilerCacheFn: queryBody.compilerCacheFn, diff --git a/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts b/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts index c8992aeb0e403..d9debda64d251 100644 --- a/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts +++ b/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts @@ -71,7 +71,7 @@ export type Query = { preAggregationsLoadCacheByDataSource?: any; // @deprecated renewQuery?: boolean; - cache?: CacheMode; + cacheMode?: CacheMode; compilerCacheFn?: (subKey: string[], cacheFn: () => T) => T; }; @@ -85,7 +85,7 @@ export type QueryBody = { continueWait?: boolean; // @deprecated renewQuery?: boolean; - cache?: CacheMode; + cacheMode?: CacheMode; requestId?: string; external?: boolean; isJob?: boolean; @@ -282,7 +282,7 @@ export class QueryCache { } // renewQuery has been deprecated, but keeping it for now - if (queryBody.cache === 'must-revalidate' || queryBody.renewQuery) { + if (queryBody.cacheMode === 'must-revalidate' || queryBody.renewQuery) { this.logger('Requested renew', { cacheKey, requestId: queryBody.requestId }); return this.renewQuery( query, diff --git a/packages/cubejs-query-orchestrator/src/orchestrator/QueryOrchestrator.ts b/packages/cubejs-query-orchestrator/src/orchestrator/QueryOrchestrator.ts index 4041af964e31e..59b515160be36 100644 --- a/packages/cubejs-query-orchestrator/src/orchestrator/QueryOrchestrator.ts +++ b/packages/cubejs-query-orchestrator/src/orchestrator/QueryOrchestrator.ts @@ -1,6 +1,6 @@ import * as stream from 'stream'; import R from 'ramda'; -import { getEnv } from '@cubejs-backend/shared'; +import { CacheMode, getEnv } from '@cubejs-backend/shared'; import { CubeStoreDriver } from '@cubejs-backend/cubestore-driver'; import { QuerySchemasResult, @@ -233,7 +233,7 @@ export class QueryOrchestrator { * @throw ContinueWaitError */ public async fetchQuery(queryBody: QueryBody): Promise { - if (queryBody.query && queryBody.cache === 'no-cache') { + if (queryBody.query && queryBody.cacheMode === 'no-cache') { const result = await this.queryCache.cachedQueryResult( { ...queryBody, forceNoCache: true }, [] diff --git a/packages/cubejs-server-core/src/core/OrchestratorApi.ts b/packages/cubejs-server-core/src/core/OrchestratorApi.ts index ae49578b1364d..1e2bbb42b337c 100644 --- a/packages/cubejs-server-core/src/core/OrchestratorApi.ts +++ b/packages/cubejs-server-core/src/core/OrchestratorApi.ts @@ -61,7 +61,7 @@ export class OrchestratorApi { * * @throw Error */ - public async streamQuery(query: QueryBody, cacheMode: CacheMode = 'stale-if-slow'): Promise { + public async streamQuery(query: QueryBody): Promise { // TODO merge with fetchQuery return this.orchestrator.streamQuery(query); } @@ -71,7 +71,7 @@ export class OrchestratorApi { * less than `continueWaitTimeout` seconds, throw `ContinueWaitError` * error otherwise. */ - public async executeQuery(query: QueryBody, cacheMode: CacheMode = 'stale-if-slow') { + public async executeQuery(query: QueryBody) { const queryForLog = query.query?.replace(/\s+/g, ' '); const startQueryTime = (new Date()).getTime(); @@ -140,7 +140,7 @@ export class OrchestratorApi { .orchestrator .resultFromCacheIfExists(query); - if (query.cache === 'stale-if-slow' && fromCache) { + if (query.cacheMode === 'stale-if-slow' && fromCache) { this.logger('Slow Query Warning', { query: queryForLog, requestId: query.requestId, @@ -155,7 +155,7 @@ export class OrchestratorApi { }; } - if (query.cache === 'stale-while-revalidate' && fromCache) { + if (query.cacheMode === 'stale-while-revalidate' && fromCache) { // Start background refresh this.orchestrator.startBackgroundRefresh(query).catch(e => { this.logger('Error starting background refresh', { From 7c41e0a31891826ff1ab74a48f436dddf9740962 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Thu, 18 Sep 2025 13:42:01 +0300 Subject: [PATCH 29/53] clean up obsolete --- .../src/orchestrator/QueryCache.ts | 38 +------------------ .../src/orchestrator/QueryOrchestrator.ts | 23 ----------- .../src/core/OrchestratorApi.ts | 18 +-------- 3 files changed, 2 insertions(+), 77 deletions(-) diff --git a/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts b/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts index d9debda64d251..1046724986024 100644 --- a/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts +++ b/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts @@ -300,7 +300,7 @@ export class QueryCache { ); } - if (!this.options.backgroundRenew) { + if (!this.options.backgroundRenew && queryBody.cacheMode !== 'stale-while-revalidate') { const resultPromise = this.renewQuery( query, values, @@ -760,42 +760,6 @@ export class QueryCache { }); } - public async startBackgroundRefreshForQuery(queryBody: QueryBody, preAggregationsTablesToTempTables: PreAggTableToTempTable[]) { - const replacePreAggregationTableNames = - (queryAndParams: string | QueryWithParams) => ( - QueryCache.replacePreAggregationTableNames( - queryAndParams, - preAggregationsTablesToTempTables, - ) - ); - - const query = replacePreAggregationTableNames(queryBody.query); - const { values } = queryBody; - - const cacheKeyQueries = this - .cacheKeyQueriesFrom(queryBody) - .map(replacePreAggregationTableNames); - - const renewalThreshold = queryBody.cacheKeyQueries?.renewalThreshold; - const expireSecs = this.getExpireSecs(queryBody); - const cacheKey = QueryCache.queryCacheKey(queryBody); - - this.startRenewCycle( - query, - values, - cacheKeyQueries, - expireSecs, - cacheKey, - renewalThreshold, - { - external: queryBody.external, - requestId: queryBody.requestId, - dataSource: queryBody.dataSource, - persistent: false, // We don't need stream back as there will be no consumer - } - ); - } - public renewQuery( query: string | QueryWithParams, values: string[], diff --git a/packages/cubejs-query-orchestrator/src/orchestrator/QueryOrchestrator.ts b/packages/cubejs-query-orchestrator/src/orchestrator/QueryOrchestrator.ts index 59b515160be36..4dd1c8e5cde18 100644 --- a/packages/cubejs-query-orchestrator/src/orchestrator/QueryOrchestrator.ts +++ b/packages/cubejs-query-orchestrator/src/orchestrator/QueryOrchestrator.ts @@ -143,29 +143,6 @@ export class QueryOrchestrator { return this.preAggregations; } - /** - * Starts background refresh cycle for a query. - */ - public async startBackgroundRefresh(queryBody: QueryBody): Promise { - if (!queryBody.query) { - return; - } - - const { - preAggregationsTablesToTempTables, - values, - } = await this.preAggregations.loadAllPreAggregationsIfNeeded(queryBody); - - if (values) { - queryBody = { - ...queryBody, - values - }; - } - - await this.queryCache.startBackgroundRefreshForQuery(queryBody, preAggregationsTablesToTempTables); - } - /** * Force reconcile queue logic to be executed. */ diff --git a/packages/cubejs-server-core/src/core/OrchestratorApi.ts b/packages/cubejs-server-core/src/core/OrchestratorApi.ts index 1e2bbb42b337c..9c6a150db871c 100644 --- a/packages/cubejs-server-core/src/core/OrchestratorApi.ts +++ b/packages/cubejs-server-core/src/core/OrchestratorApi.ts @@ -140,7 +140,7 @@ export class OrchestratorApi { .orchestrator .resultFromCacheIfExists(query); - if (query.cacheMode === 'stale-if-slow' && fromCache) { + if ((query.cacheMode === 'stale-if-slow' || query.cacheMode === 'stale-while-revalidate') && fromCache) { this.logger('Slow Query Warning', { query: queryForLog, requestId: query.requestId, @@ -155,22 +155,6 @@ export class OrchestratorApi { }; } - if (query.cacheMode === 'stale-while-revalidate' && fromCache) { - // Start background refresh - this.orchestrator.startBackgroundRefresh(query).catch(e => { - this.logger('Error starting background refresh', { - query: queryForLog, - requestId: query.requestId, - error: ((e as Error).stack || e) - }); - }); - - return { - ...fromCache, - slowQuery: true - }; - } - throw { error: 'Continue wait', stage: await this.orchestrator.queryStage(query) From e71943420457e9e70876a21ea344fb5141e58705 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Thu, 18 Sep 2025 14:24:15 +0300 Subject: [PATCH 30/53] pass cache_mode from SqlApiLoadPayload --- packages/cubejs-api-gateway/src/sql-server.ts | 5 ++--- packages/cubejs-backend-native/js/index.ts | 13 +++++++------ packages/cubejs-backend-native/src/transport.rs | 8 +++++++- .../cubejs-server-core/src/core/OrchestratorApi.ts | 1 - rust/cubesql/cubesql/src/compile/engine/df/scan.rs | 3 ++- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/cubejs-api-gateway/src/sql-server.ts b/packages/cubejs-api-gateway/src/sql-server.ts index 68988026407ec..8384547bf9149 100644 --- a/packages/cubejs-api-gateway/src/sql-server.ts +++ b/packages/cubejs-api-gateway/src/sql-server.ts @@ -207,11 +207,9 @@ export class SQLServer { } }); }, - sqlApiLoad: async ({ request, session, query, queryKey, sqlQuery, streaming }) => { + sqlApiLoad: async ({ request, session, query, queryKey, sqlQuery, streaming, cacheMode }) => { const context = await contextByRequest(request, session); - // XXX: Should we pass cacheMode somehow? - // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { try { @@ -220,6 +218,7 @@ export class SQLServer { query, sqlQuery, streaming, + cacheMode, context, memberExpressions: true, res: (response) => { diff --git a/packages/cubejs-backend-native/js/index.ts b/packages/cubejs-backend-native/js/index.ts index 57bf80e4f2c92..36e3cd8a20296 100644 --- a/packages/cubejs-backend-native/js/index.ts +++ b/packages/cubejs-backend-native/js/index.ts @@ -78,12 +78,13 @@ export interface SqlPayload { } export interface SqlApiLoadPayload { - request: Request, - session: SessionContext, - query: any, - queryKey: any, - sqlQuery: any, - streaming: boolean, + request: Request; + session: SessionContext; + query: any; + queryKey: any; + sqlQuery: any; + streaming: boolean; + cacheMode: CacheMode; } export interface LogLoadEventPayload { diff --git a/packages/cubejs-backend-native/src/transport.rs b/packages/cubejs-backend-native/src/transport.rs index 0331f4e78add3..eaf048d126763 100644 --- a/packages/cubejs-backend-native/src/transport.rs +++ b/packages/cubejs-backend-native/src/transport.rs @@ -13,7 +13,7 @@ use crate::{ }; use async_trait::async_trait; use cubesql::compile::engine::df::scan::{ - convert_transport_response, transform_response, MemberField, RecordBatch, SchemaRef, + convert_transport_response, transform_response, CacheMode, MemberField, RecordBatch, SchemaRef, }; use cubesql::compile::engine::df::wrapper::SqlQuery; use cubesql::transport::{ @@ -91,6 +91,8 @@ struct LoadRequest { streaming: bool, #[serde(rename = "queryKey", skip_serializing_if = "Option::is_none")] query_key: Option, + #[serde(rename = "cacheMode", skip_serializing_if = "Option::is_none")] + cache_mode: Option, } #[derive(Debug, Serialize)] @@ -287,6 +289,7 @@ impl TransportService for NodeBridgeTransport { member_to_alias, expression_params, streaming: false, + cache_mode: None, })?; let response: serde_json::Value = call_js_with_channel_as_callback( @@ -338,6 +341,7 @@ impl TransportService for NodeBridgeTransport { meta: LoadRequestMeta, schema: SchemaRef, member_fields: Vec, + cache_mode: Option, ) -> Result, CubeError> { trace!("[transport] Request ->"); @@ -371,6 +375,7 @@ impl TransportService for NodeBridgeTransport { member_to_alias: None, expression_params: None, streaming: false, + cache_mode: cache_mode.clone(), })?; let result = call_raw_js_with_channel_as_callback( @@ -527,6 +532,7 @@ impl TransportService for NodeBridgeTransport { member_to_alias: None, expression_params: None, streaming: true, + cache_mode: None, })?; let res = call_js_with_stream_as_callback( diff --git a/packages/cubejs-server-core/src/core/OrchestratorApi.ts b/packages/cubejs-server-core/src/core/OrchestratorApi.ts index 9c6a150db871c..f2385c41af251 100644 --- a/packages/cubejs-server-core/src/core/OrchestratorApi.ts +++ b/packages/cubejs-server-core/src/core/OrchestratorApi.ts @@ -11,7 +11,6 @@ import { } from '@cubejs-backend/query-orchestrator'; import { DatabaseType, RequestContext } from './types'; -import { CacheMode } from '@cubejs-backend/shared'; export interface OrchestratorApiOptions extends QueryOrchestratorOptions { contextToDbType: (dataSource: string) => Promise; diff --git a/rust/cubesql/cubesql/src/compile/engine/df/scan.rs b/rust/cubesql/cubesql/src/compile/engine/df/scan.rs index 0f13ba6981a95..db87dca4cef9a 100644 --- a/rust/cubesql/cubesql/src/compile/engine/df/scan.rs +++ b/rust/cubesql/cubesql/src/compile/engine/df/scan.rs @@ -43,6 +43,7 @@ use datafusion::{ }; use futures::Stream; use log::warn; +use serde::Serialize; use serde_json::Value; use std::str::FromStr; use std::{ @@ -79,7 +80,7 @@ impl MemberField { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub enum CacheMode { StaleIfSlow, StaleWhileRevalidate, From cd33cd42cb2fecebccb52cf6c9f4417000445aba Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Thu, 18 Sep 2025 16:32:39 +0300 Subject: [PATCH 31/53] fix important --- packages/cubejs-api-gateway/src/gateway.ts | 2 +- packages/cubejs-server-core/src/core/OrchestratorApi.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cubejs-api-gateway/src/gateway.ts b/packages/cubejs-api-gateway/src/gateway.ts index 0a8382e8246ca..da2283880d0a9 100644 --- a/packages/cubejs-api-gateway/src/gateway.ts +++ b/packages/cubejs-api-gateway/src/gateway.ts @@ -735,7 +735,7 @@ class ApiGateway { const preAggregationPartitionsWithoutError = preAggregationPartitions.filter(p => !p?.errors?.length); const versionEntriesResult = preAggregationPartitions && - orchestratorApi.getPreAggregationVersionEntries( + await orchestratorApi.getPreAggregationVersionEntries( context, preAggregationPartitionsWithoutError, compilerApi.preAggregationsSchema diff --git a/packages/cubejs-server-core/src/core/OrchestratorApi.ts b/packages/cubejs-server-core/src/core/OrchestratorApi.ts index f2385c41af251..e715e55a9be69 100644 --- a/packages/cubejs-server-core/src/core/OrchestratorApi.ts +++ b/packages/cubejs-server-core/src/core/OrchestratorApi.ts @@ -262,7 +262,7 @@ export class OrchestratorApi { this.seenDataSources[dataSource] = true; } - public getPreAggregationVersionEntries(context: RequestContext, preAggregations, preAggregationsSchema) { + public getPreAggregationVersionEntries(context: RequestContext, preAggregations, preAggregationsSchema): Promise { return this.orchestrator.getPreAggregationVersionEntries( preAggregations, preAggregationsSchema, From ce2d218d3195fa39cff929d09acb08e198b0cdce Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Fri, 19 Sep 2025 12:03:12 +0300 Subject: [PATCH 32/53] move 'no-cache' variant into queryCache.cachedQueryResult() --- .../src/orchestrator/QueryCache.ts | 2 +- .../src/orchestrator/QueryOrchestrator.ts | 19 ------------------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts b/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts index 1046724986024..dc1498f16766b 100644 --- a/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts +++ b/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts @@ -231,7 +231,7 @@ export class QueryCache { queuePriority = queryBody.queuePriority; } - const forceNoCache = queryBody.forceNoCache || false; + const forceNoCache = queryBody.forceNoCache || (queryBody.cacheMode === 'no-cache') || false; const { values } = queryBody; diff --git a/packages/cubejs-query-orchestrator/src/orchestrator/QueryOrchestrator.ts b/packages/cubejs-query-orchestrator/src/orchestrator/QueryOrchestrator.ts index 4dd1c8e5cde18..bd72793003781 100644 --- a/packages/cubejs-query-orchestrator/src/orchestrator/QueryOrchestrator.ts +++ b/packages/cubejs-query-orchestrator/src/orchestrator/QueryOrchestrator.ts @@ -210,25 +210,6 @@ export class QueryOrchestrator { * @throw ContinueWaitError */ public async fetchQuery(queryBody: QueryBody): Promise { - if (queryBody.query && queryBody.cacheMode === 'no-cache') { - const result = await this.queryCache.cachedQueryResult( - { ...queryBody, forceNoCache: true }, - [] - ); - - if (result instanceof QueryStream) { - return result; - } - - return { - ...result, - dataSource: queryBody.dataSource, - external: queryBody.external, - usedPreAggregations: {}, - lastRefreshTime: new Date(), - }; - } - const { preAggregationsTablesToTempTables, values, From 99c9ca67633d608627cee39a51158005b4b6ec19 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Fri, 19 Sep 2025 12:34:18 +0300 Subject: [PATCH 33/53] remove cacheMode from client query body types (it's incorrect) --- packages/cubejs-client-core/src/types.ts | 3 --- packages/cubejs-client-dx/index.d.ts | 3 --- 2 files changed, 6 deletions(-) diff --git a/packages/cubejs-client-core/src/types.ts b/packages/cubejs-client-core/src/types.ts index 6e807479f9d91..400de81044e93 100644 --- a/packages/cubejs-client-core/src/types.ts +++ b/packages/cubejs-client-core/src/types.ts @@ -7,8 +7,6 @@ export type QueryOrder = 'asc' | 'desc' | 'none'; export type TQueryOrderObject = { [key: string]: QueryOrder }; export type TQueryOrderArray = Array<[string, QueryOrder]>; -export type CacheMode = 'stale-if-slow' | 'stale-while-revalidate' | 'must-revalidate' | 'no-cache'; - export type GranularityAnnotation = { name: string; title: string; @@ -117,7 +115,6 @@ export interface Query { timezone?: string; // @deprecated renewQuery?: boolean; - cache?: CacheMode; ungrouped?: boolean; responseFormat?: 'compact' | 'default'; total?: boolean; diff --git a/packages/cubejs-client-dx/index.d.ts b/packages/cubejs-client-dx/index.d.ts index 7e6d108a3d0fb..c37dcb86385c0 100644 --- a/packages/cubejs-client-dx/index.d.ts +++ b/packages/cubejs-client-dx/index.d.ts @@ -1,5 +1,3 @@ -import { CacheMode } from '@cubejs-backend/shared'; - declare module "@cubejs-client/core" { export type IntrospectedMeasureName = import('./generated').IntrospectedMeasureName; @@ -23,7 +21,6 @@ declare module "@cubejs-client/core" { timezone?: string; // @deprecated renewQuery?: boolean; - cache?: CacheMode; ungrouped?: boolean; } From 62a7ae3aab5d5e2f98604a9abeecfba64e1ff34e Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Fri, 19 Sep 2025 12:34:52 +0300 Subject: [PATCH 34/53] switch RefreshScheduler to use cacheMode instead of renewQuery --- packages/cubejs-server-core/src/core/RefreshScheduler.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cubejs-server-core/src/core/RefreshScheduler.ts b/packages/cubejs-server-core/src/core/RefreshScheduler.ts index d9d947ec5b1b5..d6e7ec74ab48e 100644 --- a/packages/cubejs-server-core/src/core/RefreshScheduler.ts +++ b/packages/cubejs-server-core/src/core/RefreshScheduler.ts @@ -369,7 +369,7 @@ export class RefreshScheduler { sql: null, preAggregations: [], continueWait: true, - renewQuery: true, + cacheMode: 'must-revalidate', requestId: context.requestId, scheduledRefresh: true, loadRefreshKeysOnly: true, @@ -580,7 +580,7 @@ export class RefreshScheduler { priority: preAggregationsWarmup ? 1 : queryCursor - queries.length })), continueWait: true, - renewQuery: true, + cacheMode: 'must-revalidate', requestId: context.requestId, timezone: timezones[timezoneCursor], scheduledRefresh: true, @@ -645,7 +645,7 @@ export class RefreshScheduler { await orchestratorApi.executeQuery({ preAggregations: dependencies.concat([partition]), continueWait: true, - renewQuery: true, + cacheMode: 'must-revalidate', forceBuildPreAggregations: queryingOptions.forceBuildPreAggregations ?? true, orphanedTimeout: 60 * 60, requestId: context.requestId, @@ -739,7 +739,7 @@ export class RefreshScheduler { const job = await orchestratorApi.executeQuery({ preAggregations: dependencies.concat([partition]), continueWait: true, - renewQuery: false, + cacheMode: 'stale-while-revalidate', forceBuildPreAggregations: true, orphanedTimeout: 60 * 60, requestId: context.requestId, From 495ba44c0649701936fed5f68507882cd1598c35 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Fri, 19 Sep 2025 12:45:15 +0300 Subject: [PATCH 35/53] remove obsolete continueWait flag --- packages/cubejs-api-gateway/src/gateway.ts | 5 ----- .../src/orchestrator/QueryCache.ts | 1 - .../src/orchestrator/QueryOrchestrator.ts | 1 - packages/cubejs-server-core/src/core/RefreshScheduler.ts | 6 +----- 4 files changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/cubejs-api-gateway/src/gateway.ts b/packages/cubejs-api-gateway/src/gateway.ts index da2283880d0a9..bdf7060eed093 100644 --- a/packages/cubejs-api-gateway/src/gateway.ts +++ b/packages/cubejs-api-gateway/src/gateway.ts @@ -1653,7 +1653,6 @@ class ApiGateway { ...sqlQuery, query: sqlQuery.sql[0], values: sqlQuery.sql[1], - continueWait: true, renewQuery: normalizedQuery.renewQuery, cacheMode, requestId: context.requestId, @@ -1678,7 +1677,6 @@ class ApiGateway { ...totalQuery, query: totalQuery.sql[0], values: totalQuery.sql[1], - continueWait: true, renewQuery: normalizedTotal.renewQuery, cacheMode, requestId: context.requestId, @@ -1800,7 +1798,6 @@ class ApiGateway { ...sqlQuery, query: sqlQuery.sql[0], values: sqlQuery.sql[1], - continueWait: true, renewQuery: false, cacheMode: 'stale-if-slow', requestId: context.requestId, @@ -1995,7 +1992,6 @@ class ApiGateway { ...sqlQuery, query: sqlQuery.query || sqlQuery.sql[0], values: sqlQuery.values || sqlQuery.sql[1], - continueWait: true, renewQuery: false, cacheMode: 'stale-if-slow', requestId: context.requestId, @@ -2015,7 +2011,6 @@ class ApiGateway { const finalQuery: QueryBody = { query: request.sqlQuery[0], values: request.sqlQuery[1], - continueWait: true, renewQuery: normalizedQueries[0].renewQuery, cacheMode: request.cacheMode, requestId: context.requestId, diff --git a/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts b/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts index dc1498f16766b..0887aac625065 100644 --- a/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts +++ b/packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts @@ -82,7 +82,6 @@ export type QueryBody = { values?: string[]; loadRefreshKeysOnly?: boolean; scheduledRefresh?: boolean; - continueWait?: boolean; // @deprecated renewQuery?: boolean; cacheMode?: CacheMode; diff --git a/packages/cubejs-query-orchestrator/src/orchestrator/QueryOrchestrator.ts b/packages/cubejs-query-orchestrator/src/orchestrator/QueryOrchestrator.ts index bd72793003781..f682955ae2331 100644 --- a/packages/cubejs-query-orchestrator/src/orchestrator/QueryOrchestrator.ts +++ b/packages/cubejs-query-orchestrator/src/orchestrator/QueryOrchestrator.ts @@ -408,7 +408,6 @@ export class QueryOrchestrator { const data = await this.fetchQuery({ dataSource: preAggregation.dataSource, - continueWait: true, query, external, preAggregations: [ diff --git a/packages/cubejs-server-core/src/core/RefreshScheduler.ts b/packages/cubejs-server-core/src/core/RefreshScheduler.ts index d6e7ec74ab48e..43a4ac9faf7b1 100644 --- a/packages/cubejs-server-core/src/core/RefreshScheduler.ts +++ b/packages/cubejs-server-core/src/core/RefreshScheduler.ts @@ -368,7 +368,6 @@ export class RefreshScheduler { ...sqlQuery, sql: null, preAggregations: [], - continueWait: true, cacheMode: 'must-revalidate', requestId: context.requestId, scheduledRefresh: true, @@ -579,7 +578,6 @@ export class RefreshScheduler { ...partition, priority: preAggregationsWarmup ? 1 : queryCursor - queries.length })), - continueWait: true, cacheMode: 'must-revalidate', requestId: context.requestId, timezone: timezones[timezoneCursor], @@ -644,7 +642,6 @@ export class RefreshScheduler { Promise.all(partitions.map(async (partition) => { await orchestratorApi.executeQuery({ preAggregations: dependencies.concat([partition]), - continueWait: true, cacheMode: 'must-revalidate', forceBuildPreAggregations: queryingOptions.forceBuildPreAggregations ?? true, orphanedTimeout: 60 * 60, @@ -738,8 +735,7 @@ export class RefreshScheduler { async (partition): Promise => { const job = await orchestratorApi.executeQuery({ preAggregations: dependencies.concat([partition]), - continueWait: true, - cacheMode: 'stale-while-revalidate', + cacheMode: 'must-revalidate', forceBuildPreAggregations: true, orphanedTimeout: 60 * 60, requestId: context.requestId, From df2e63bf121f1f82a7ac6ed4024a97fb9298bb34 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Fri, 19 Sep 2025 13:47:20 +0300 Subject: [PATCH 36/53] fix refresh scheduler --- packages/cubejs-server-core/src/core/RefreshScheduler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cubejs-server-core/src/core/RefreshScheduler.ts b/packages/cubejs-server-core/src/core/RefreshScheduler.ts index 43a4ac9faf7b1..50b0d9b8dc924 100644 --- a/packages/cubejs-server-core/src/core/RefreshScheduler.ts +++ b/packages/cubejs-server-core/src/core/RefreshScheduler.ts @@ -735,7 +735,7 @@ export class RefreshScheduler { async (partition): Promise => { const job = await orchestratorApi.executeQuery({ preAggregations: dependencies.concat([partition]), - cacheMode: 'must-revalidate', + cacheMode: 'stale-if-slow', forceBuildPreAggregations: true, orphanedTimeout: 60 * 60, requestId: context.requestId, From 6a325e480e18375b957fcabb188d2cc095503578 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Wed, 24 Sep 2025 16:15:03 +0300 Subject: [PATCH 37/53] add fallback to renewQuery in api gw --- packages/cubejs-api-gateway/src/gateway.ts | 39 ++++++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/cubejs-api-gateway/src/gateway.ts b/packages/cubejs-api-gateway/src/gateway.ts index bdf7060eed093..5cdd521e584da 100644 --- a/packages/cubejs-api-gateway/src/gateway.ts +++ b/packages/cubejs-api-gateway/src/gateway.ts @@ -314,33 +314,66 @@ class ApiGateway { *************************************************************** */ app.get(`${this.basePath}/v1/load`, userMiddlewares, userAsyncHandler(async (req: any, res) => { + let cacheMode: CacheMode | undefined; + + // TODO: Drop this fallback to renewQuery when it will be removed + if (req.query.cache !== undefined) { + cacheMode = req.query.cache; + } else if (req.query.query?.renewQuery !== undefined) { + cacheMode = req.query.query.renewQuery === true + ? 'must-revalidate' + : 'stale-if-slow'; + } + await this.load({ query: req.query.query, context: req.context, res: this.resToResultFn(res), queryType: req.query.queryType, - cacheMode: req.query.cache, + cacheMode, }); })); const jsonParser = bodyParser.json({ limit: '1mb' }); app.post(`${this.basePath}/v1/load`, jsonParser, userMiddlewares, userAsyncHandler(async (req, res) => { + let cacheMode: CacheMode | undefined; + + // TODO: Drop this fallback to renewQuery when it will be removed + if (req.query.cache !== undefined) { + cacheMode = req.body.cache; + } else if (req.body.query?.renewQuery !== undefined) { + cacheMode = req.body.query.renewQuery === true + ? 'must-revalidate' + : 'stale-if-slow'; + } + await this.load({ query: req.body.query, context: req.context, res: this.resToResultFn(res), queryType: req.body.queryType, - cacheMode: req.body.cache, + cacheMode, }); })); app.get(`${this.basePath}/v1/subscribe`, userMiddlewares, userAsyncHandler(async (req: any, res) => { + let cacheMode: CacheMode | undefined; + + // TODO: Drop this fallback to renewQuery when it will be removed + if (req.query.cache !== undefined) { + cacheMode = req.query.cache; + } else if (req.query.query?.renewQuery !== undefined) { + cacheMode = req.query.query.renewQuery === true + ? 'must-revalidate' + : 'stale-if-slow'; + } + await this.load({ query: req.query.query, context: req.context, res: this.resToResultFn(res), queryType: req.query.queryType, - cacheMode: req.query.cache, + cacheMode, }); })); From bc3224e862f9da2f05ed0ddab9fb562c7dd76771 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Mon, 15 Sep 2025 18:29:24 +0300 Subject: [PATCH 38/53] fix tests --- .../cubejs-api-gateway/test/index.test.ts | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/cubejs-api-gateway/test/index.test.ts b/packages/cubejs-api-gateway/test/index.test.ts index 2a8d5f03bab0f..fa7d6b8c0ee52 100644 --- a/packages/cubejs-api-gateway/test/index.test.ts +++ b/packages/cubejs-api-gateway/test/index.test.ts @@ -384,7 +384,8 @@ describe('API Gateway', () => { limit: 10000, dimensions: [], timeDimensions: [], - queryType: 'regularQuery' + queryType: 'regularQuery', + cache: 'stale-if-slow', } ], queryOrder: [{ id: 'desc' }], @@ -396,7 +397,8 @@ describe('API Gateway', () => { limit: 10000, dimensions: [], timeDimensions: [], - queryType: 'regularQuery' + queryType: 'regularQuery', + cache: 'stale-if-slow', }, transformedQueries: [null] }); @@ -461,7 +463,8 @@ describe('API Gateway', () => { limit: 10000, dimensions: [], timeDimensions: [], - queryType: 'regularQuery' + queryType: 'regularQuery', + cache: 'stale-if-slow', } ]); } @@ -518,7 +521,8 @@ describe('API Gateway', () => { limit: 2, dimensions: [], timeDimensions: [], - queryType: 'regularQuery' + queryType: 'regularQuery', + cache: 'stale-if-slow', } ], queryOrder: [{ id: 'desc' }], @@ -530,7 +534,8 @@ describe('API Gateway', () => { limit: 2, dimensions: [], timeDimensions: [], - queryType: 'regularQuery' + queryType: 'regularQuery', + cache: 'stale-if-slow', }, transformedQueries: [null] }); @@ -564,7 +569,8 @@ describe('API Gateway', () => { limit: 10000, dimensions: [], timeDimensions: [], - queryType: 'regularQuery' + queryType: 'regularQuery', + cache: 'stale-if-slow', } ], queryOrder: [{ id: 'desc' }], @@ -577,7 +583,8 @@ describe('API Gateway', () => { limit: 10000, dimensions: [], timeDimensions: [], - queryType: 'regularQuery' + queryType: 'regularQuery', + cache: 'stale-if-slow', }, transformedQueries: [null] }); From 72d6144a0a47540adf3a200a1a253c898c20480c Mon Sep 17 00:00:00 2001 From: Igor Lukanin Date: Wed, 24 Sep 2025 15:16:12 +0200 Subject: [PATCH 39/53] Docs --- .../product/apis-integrations/rest-api.mdx | 21 +++++++++++++++---- .../rest-api/query-format.mdx | 11 ---------- .../apis-integrations/rest-api/reference.mdx | 19 ++++++++++------- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/docs/pages/product/apis-integrations/rest-api.mdx b/docs/pages/product/apis-integrations/rest-api.mdx index 98b1e1d9a0cb7..607d19272472e 100644 --- a/docs/pages/product/apis-integrations/rest-api.mdx +++ b/docs/pages/product/apis-integrations/rest-api.mdx @@ -161,7 +161,7 @@ accessible for everyone. | API scope | REST API endpoints | Accessible by default? | | --- | --- | --- | | `meta` | [`/v1/meta`][ref-ref-meta] | ✅ Yes | -| `data` | [`/v1/load`][ref-ref-load] | ✅ Yes | +| `data` | [`/v1/load`][ref-ref-load], [`/v1/cubesql`][ref-ref-cubesql] | ✅ Yes | | `graphql` | `/graphql` | ✅ Yes | | `sql` | [`/v1/sql`][ref-ref-sql] | ✅ Yes | | `jobs` | [`/v1/pre-aggregations/jobs`][ref-ref-paj] | ❌ No | @@ -248,9 +248,20 @@ should be unique for each separate request. `spanId` should define user interaction span such us `Continue wait` retry cycle and it's value shouldn't change during one single interaction. -## Troubleshooting +## Cache control -### `Continue wait` +[`/v1/load`][ref-ref-load] and [`/v1/cubesql`][ref-ref-cubesql] endpoints of the REST API +allow to control the cache behavior. The following querying strategies with regards to +the cache are supported: + +| Strategy | Description | +| --- | --- | +| `stale-if-slow` | If [refresh keys][ref-refresh-keys] are up-to-date, returns cached value. If expired, tries to return fresh value from the data source. If the data source query is slow (hits [`Continue wait`](#continue-wait)), returns stale value from cache. | +| `stale-while-revalidate`| If [refresh keys][ref-refresh-keys] are up-to-date, returns cached value. If expired, returns stale data from cache and updates cache in background. | +| `must-revalidate` | If [refresh keys][ref-refresh-keys] are up-to-date, returns cached value. If expired, always waits for fresh value from the data source, even if slow (hits one or more [`Continue wait`](#continue-wait) intervals). | +| `no-cache` | Skips [refresh key][ref-refresh-keys] checks. Always returns fresh data from the data source, regardless of cache or query performance. | + +## `Continue wait` If the request takes too long to be processed, the REST API responds with `{ "error": "Continue wait" }` and the status code 200. @@ -295,6 +306,7 @@ warehouse][ref-data-warehouses]. [ref-ref-load]: /product/apis-integrations/rest-api/reference#base_pathv1load [ref-ref-meta]: /product/apis-integrations/rest-api/reference#base_pathv1meta [ref-ref-sql]: /product/apis-integrations/rest-api/reference#base_pathv1sql +[ref-ref-cubesql]: /product/apis-integrations/rest-api/reference#base_pathv1cubesql [ref-ref-paj]: /product/apis-integrations/rest-api/reference#base_pathv1pre-aggregationsjobs [ref-security-context]: /product/auth/context [ref-graphql-api]: /product/apis-integrations/graphql-api @@ -313,4 +325,5 @@ warehouse][ref-data-warehouses]. [ref-traditional-databases]: /product/configuration/data-sources#transactional-databases [ref-pre-aggregations]: /product/caching/using-pre-aggregations [ref-javascript-sdk]: /product/apis-integrations/javascript-sdk -[ref-recipe-real-time-data-fetch]: /product/apis-integrations/recipes/real-time-data-fetch \ No newline at end of file +[ref-recipe-real-time-data-fetch]: /product/apis-integrations/recipes/real-time-data-fetch +[ref-refresh-keys]: /product/data-modeling/reference/cube#refresh_key \ No newline at end of file diff --git a/docs/pages/product/apis-integrations/rest-api/query-format.mdx b/docs/pages/product/apis-integrations/rest-api/query-format.mdx index 12840981bbd74..17683e70f1d0e 100644 --- a/docs/pages/product/apis-integrations/rest-api/query-format.mdx +++ b/docs/pages/product/apis-integrations/rest-api/query-format.mdx @@ -41,17 +41,6 @@ The default value is `false`. - `timezone`: A [time zone][ref-time-zone] for your query. You can set the desired time zone in the [TZ Database Name](https://en.wikipedia.org/wiki/Tz_database) format, e.g., `America/Los_Angeles`. -- `renewQuery`: If `renewQuery` is set to `true`, Cube will renew all - [`refreshKey`][ref-schema-ref-preaggs-refreshkey] for queries and query - results in the foreground. However, if the - [`refreshKey`][ref-schema-ref-preaggs-refreshkey] (or - [`refreshKey.every`][ref-schema-ref-preaggs-refreshkey-every]) doesn't - indicate that there's a need for an update this setting has no effect. The - default value is `false`. - > **NOTE**: Cube provides only eventual consistency guarantee. Using a small - > [`refreshKey.every`][ref-schema-ref-preaggs-refreshkey-every] value together - > with `renewQuery` to achieve immediate consistency can lead to endless - > refresh loops and overall system instability. - `ungrouped`: If set to `true`, Cube will run an [ungrouped query][ref-ungrouped-query]. - `joinHints`: Query-time [join hints][ref-join-hints], provided as an array of diff --git a/docs/pages/product/apis-integrations/rest-api/reference.mdx b/docs/pages/product/apis-integrations/rest-api/reference.mdx index 8e30162750ee8..00c7c2357225f 100644 --- a/docs/pages/product/apis-integrations/rest-api/reference.mdx +++ b/docs/pages/product/apis-integrations/rest-api/reference.mdx @@ -13,10 +13,11 @@ By default, it's `/cubejs-api`. Run the query to the REST API and get the results. -| Parameter | Description | -| ----------- | --------------------------------------------------------------------------------------------------------------------- | -| `query` | Either a single URL encoded Cube [Query](/product/apis-integrations/rest-api/query-format), or an array of queries | -| `queryType` | If multiple queries are passed in `query` for [data blending][ref-recipes-data-blending], this must be set to `multi` | +| Parameter | Description | Required | +| ----------- | --------------------------------------------------------------------------------------------------------------------- | --- | +| `query` | Either a single URL encoded Cube [Query](/product/apis-integrations/rest-api/query-format), or an array of queries | ✅ Yes | +| `queryType` | If multiple queries are passed in `query` for [data blending][ref-recipes-data-blending], this must be set to `multi` | ❌ No | +| `cache` | See [cache control][ref-cache-control]. `stale-if-slow` by default | ❌ No | Response @@ -319,9 +320,10 @@ This endpoint is part of the [SQL API][ref-sql-api]. -| Parameter | Description | -| --- | --- | -| `query` | The SQL query to run. | +| Parameter | Description | Required | +| --- | --- | --- | +| `query` | The SQL query to run. | ✅ Yes | +| `cache` | See [cache control][ref-cache-control]. `stale-if-slow` by default | ❌ No | Response: a stream of newline-delimited JSON objects. The first object contains the `schema` property with column names and types. The following objects contain @@ -639,4 +641,5 @@ Keep-Alive: timeout=5 [ref-query-wpd]: /product/apis-integrations/queries#query-with-pushdown [ref-sql-api]: /product/apis-integrations/sql-api [ref-orchestration-api]: /product/apis-integrations/orchestration-api -[ref-folders]: /product/data-modeling/reference/view#folders \ No newline at end of file +[ref-folders]: /product/data-modeling/reference/view#folders +[ref-cache-control]: /product/apis-integrations/rest-api#cache-control \ No newline at end of file From 6a4c79d7ae317ef6691a5082edb71b1b3299fc6d Mon Sep 17 00:00:00 2001 From: Igor Lukanin Date: Wed, 24 Sep 2025 15:16:21 +0200 Subject: [PATCH 40/53] Deprecation --- DEPRECATION.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/DEPRECATION.md b/DEPRECATION.md index 1ee5c4f14295a..c55794741cc52 100644 --- a/DEPRECATION.md +++ b/DEPRECATION.md @@ -64,8 +64,9 @@ features: | Removed | [`initApp` hook](#initapp-hook) | v0.35.0 | v0.35.0 | | Removed | [`/v1/run-scheduled-refresh` REST API endpoint](#v1run-scheduled-refresh-rest-api-endpoint) | v0.35.0 | v0.36.0 | | Removed | [Node.js 18](#nodejs-18) | v0.36.0 | v1.3.0 | -| Deprecated | [`CUBEJS_SCHEDULED_REFRESH_CONCURRENCY`](#cubejs_scheduled_refresh_concurrency) | v1.2.7 | | +| Deprecated | [`CUBEJS_SCHEDULED_REFRESH_CONCURRENCY`](#cubejs_scheduled_refresh_concurrency) | v1.2.7 | | | Deprecated | [Node.js 20](#nodejs-20) | v1.3.0 | | +| Deprecated | [`renewQuery` parameter of the `/v1/load` endpoint](#renewquery-parameter-of-the-v1load-endpoint) | v1.3.73 | | ### Node.js 8 @@ -412,3 +413,8 @@ This environment variable was renamed to [`CUBEJS_SCHEDULED_REFRESH_QUERIES_PER_ Node.js 20 is in maintenance mode from [November 22, 2024][link-nodejs-eol]. This means no more new features, only security updates. Please upgrade to Node.js 22 or higher. + +### `renewQuery` parameter of the `/v1/load` endpoint + +This parameter is deprecated and will be removed in future releases. See [cache control](https://cube.dev/docs/product/apis-integrations/rest-api#cache-control) +options and use the new `cache` parameter of the `/v1/load` endpoint instead. \ No newline at end of file From b724bf21d8d7cbf20726b7420f6fa99675cd0a40 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Thu, 25 Sep 2025 10:26:05 +0300 Subject: [PATCH 41/53] refactor api gw: move copy/paste into this.normalizeCacheMode() --- packages/cubejs-api-gateway/src/gateway.ts | 52 +++++++--------------- 1 file changed, 16 insertions(+), 36 deletions(-) diff --git a/packages/cubejs-api-gateway/src/gateway.ts b/packages/cubejs-api-gateway/src/gateway.ts index 5cdd521e584da..d02135496e40d 100644 --- a/packages/cubejs-api-gateway/src/gateway.ts +++ b/packages/cubejs-api-gateway/src/gateway.ts @@ -314,66 +314,33 @@ class ApiGateway { *************************************************************** */ app.get(`${this.basePath}/v1/load`, userMiddlewares, userAsyncHandler(async (req: any, res) => { - let cacheMode: CacheMode | undefined; - - // TODO: Drop this fallback to renewQuery when it will be removed - if (req.query.cache !== undefined) { - cacheMode = req.query.cache; - } else if (req.query.query?.renewQuery !== undefined) { - cacheMode = req.query.query.renewQuery === true - ? 'must-revalidate' - : 'stale-if-slow'; - } - await this.load({ query: req.query.query, context: req.context, res: this.resToResultFn(res), queryType: req.query.queryType, - cacheMode, + cacheMode: this.normalizeCacheMode(req.query.query, req.query.cache), }); })); const jsonParser = bodyParser.json({ limit: '1mb' }); app.post(`${this.basePath}/v1/load`, jsonParser, userMiddlewares, userAsyncHandler(async (req, res) => { - let cacheMode: CacheMode | undefined; - - // TODO: Drop this fallback to renewQuery when it will be removed - if (req.query.cache !== undefined) { - cacheMode = req.body.cache; - } else if (req.body.query?.renewQuery !== undefined) { - cacheMode = req.body.query.renewQuery === true - ? 'must-revalidate' - : 'stale-if-slow'; - } - await this.load({ query: req.body.query, context: req.context, res: this.resToResultFn(res), queryType: req.body.queryType, - cacheMode, + cacheMode: this.normalizeCacheMode(req.body.query, req.body.cache), }); })); app.get(`${this.basePath}/v1/subscribe`, userMiddlewares, userAsyncHandler(async (req: any, res) => { - let cacheMode: CacheMode | undefined; - - // TODO: Drop this fallback to renewQuery when it will be removed - if (req.query.cache !== undefined) { - cacheMode = req.query.cache; - } else if (req.query.query?.renewQuery !== undefined) { - cacheMode = req.query.query.renewQuery === true - ? 'must-revalidate' - : 'stale-if-slow'; - } - await this.load({ query: req.query.query, context: req.context, res: this.resToResultFn(res), queryType: req.query.queryType, - cacheMode, + cacheMode: this.normalizeCacheMode(req.query.query, req.query.cache), }); })); @@ -620,6 +587,19 @@ class ApiGateway { return requestStarted && (new Date().getTime() - requestStarted.getTime()); } + // TODO: Drop this when renewQuery will be removed + private normalizeCacheMode(query, cache: string): CacheMode { + if (cache !== undefined) { + return cache as CacheMode; + } else if (query?.renewQuery !== undefined) { + return query.renewQuery === true + ? 'must-revalidate' + : 'stale-if-slow'; + } + + return 'stale-if-slow'; + } + private filterVisibleItemsInMeta(context: RequestContext, cubes: any[]) { const isDevMode = getEnv('devMode'); function visibilityFilter(item) { From c4e6feda0604791bbfd2d7f7f1e40c31347a703a Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Thu, 25 Sep 2025 12:46:15 +0300 Subject: [PATCH 42/53] fix tests snapshots --- .../cubejs-api-gateway/test/index.test.ts | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/packages/cubejs-api-gateway/test/index.test.ts b/packages/cubejs-api-gateway/test/index.test.ts index fa7d6b8c0ee52..2a8d5f03bab0f 100644 --- a/packages/cubejs-api-gateway/test/index.test.ts +++ b/packages/cubejs-api-gateway/test/index.test.ts @@ -384,8 +384,7 @@ describe('API Gateway', () => { limit: 10000, dimensions: [], timeDimensions: [], - queryType: 'regularQuery', - cache: 'stale-if-slow', + queryType: 'regularQuery' } ], queryOrder: [{ id: 'desc' }], @@ -397,8 +396,7 @@ describe('API Gateway', () => { limit: 10000, dimensions: [], timeDimensions: [], - queryType: 'regularQuery', - cache: 'stale-if-slow', + queryType: 'regularQuery' }, transformedQueries: [null] }); @@ -463,8 +461,7 @@ describe('API Gateway', () => { limit: 10000, dimensions: [], timeDimensions: [], - queryType: 'regularQuery', - cache: 'stale-if-slow', + queryType: 'regularQuery' } ]); } @@ -521,8 +518,7 @@ describe('API Gateway', () => { limit: 2, dimensions: [], timeDimensions: [], - queryType: 'regularQuery', - cache: 'stale-if-slow', + queryType: 'regularQuery' } ], queryOrder: [{ id: 'desc' }], @@ -534,8 +530,7 @@ describe('API Gateway', () => { limit: 2, dimensions: [], timeDimensions: [], - queryType: 'regularQuery', - cache: 'stale-if-slow', + queryType: 'regularQuery' }, transformedQueries: [null] }); @@ -569,8 +564,7 @@ describe('API Gateway', () => { limit: 10000, dimensions: [], timeDimensions: [], - queryType: 'regularQuery', - cache: 'stale-if-slow', + queryType: 'regularQuery' } ], queryOrder: [{ id: 'desc' }], @@ -583,8 +577,7 @@ describe('API Gateway', () => { limit: 10000, dimensions: [], timeDimensions: [], - queryType: 'regularQuery', - cache: 'stale-if-slow', + queryType: 'regularQuery' }, transformedQueries: [null] }); From 047a838b4b0fff90c29f6572b8b7a2bbc525e74a Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Thu, 16 Oct 2025 16:07:52 +0300 Subject: [PATCH 43/53] return cacheMode into query object --- packages/cubejs-api-gateway/src/gateway.ts | 55 +++++++++++-------- packages/cubejs-api-gateway/src/query.js | 1 + .../cubejs-api-gateway/src/types/query.ts | 2 + .../cubejs-api-gateway/src/types/request.ts | 9 ++- 4 files changed, 44 insertions(+), 23 deletions(-) diff --git a/packages/cubejs-api-gateway/src/gateway.ts b/packages/cubejs-api-gateway/src/gateway.ts index d02135496e40d..85383a9e72dde 100644 --- a/packages/cubejs-api-gateway/src/gateway.ts +++ b/packages/cubejs-api-gateway/src/gateway.ts @@ -52,7 +52,9 @@ import { PreAggJob, PreAggJobStatusItem, PreAggJobStatusResponse, - SqlApiRequest, MetaResponseResultFn, + SqlApiRequest, + MetaResponseResultFn, + RequestQuery, } from './types/request'; import { CheckAuthInternalOptions, @@ -319,7 +321,7 @@ class ApiGateway { context: req.context, res: this.resToResultFn(res), queryType: req.query.queryType, - cacheMode: this.normalizeCacheMode(req.query.query, req.query.cache), + cacheMode: req.query.cache, }); })); @@ -330,7 +332,7 @@ class ApiGateway { context: req.context, res: this.resToResultFn(res), queryType: req.body.queryType, - cacheMode: this.normalizeCacheMode(req.body.query, req.body.cache), + cacheMode: req.body.cache, }); })); @@ -340,7 +342,7 @@ class ApiGateway { context: req.context, res: this.resToResultFn(res), queryType: req.query.queryType, - cacheMode: this.normalizeCacheMode(req.query.query, req.query.cache), + cacheMode: req.query.cache, }); })); @@ -587,17 +589,22 @@ class ApiGateway { return requestStarted && (new Date().getTime() - requestStarted.getTime()); } - // TODO: Drop this when renewQuery will be removed - private normalizeCacheMode(query, cache: string): CacheMode { - if (cache !== undefined) { - return cache as CacheMode; - } else if (query?.renewQuery !== undefined) { - return query.renewQuery === true + private normalizeQueryCacheMode(query: Query, cacheMode: CacheMode | undefined): Query { + if (cacheMode !== undefined) { + query.cacheMode = cacheMode; + } else if (!query.cacheMode && query?.renewQuery !== undefined) { + // TODO: Drop this when renewQuery will be removed + query.cacheMode = query.renewQuery === true ? 'must-revalidate' : 'stale-if-slow'; + } else if (!query.cacheMode) { + query.cacheMode = 'stale-if-slow'; } - return 'stale-if-slow'; + // TODO: Drop this when renewQuery will be removed + query.renewQuery = undefined; + + return query; } private filterVisibleItemsInMeta(context: RequestContext, cubes: any[]) { @@ -1224,8 +1231,11 @@ class ApiGateway { context: RequestContext, persistent = false, memberExpressions: boolean = false, + cacheMode?: CacheMode, ): Promise<[QueryType, NormalizedQuery[], NormalizedQuery[]]> { let query = this.parseQueryParam(inputQuery); + query = Array.isArray(query) ? query.map(q => this.normalizeQueryCacheMode(q, cacheMode)) + : this.normalizeQueryCacheMode(query, cacheMode); let queryType: QueryType = QueryTypeEnum.REGULAR_QUERY; if (!Array.isArray(query)) { @@ -1309,13 +1319,13 @@ class ApiGateway { type: 'Query Rewrite completed', queryRewriteId, normalizedQueries, - duration: new Date().getTime() - startTime, + duration: Date.now() - startTime, query }, context); normalizedQueries = normalizedQueries.map(q => remapToQueryAdapterFormat(q)); - if (normalizedQueries.find((currentQuery) => !currentQuery)) { + if (normalizedQueries.some((currentQuery) => !currentQuery)) { throw new Error('queryTransformer returned null query. Please check your queryTransformer implementation'); } @@ -1660,14 +1670,13 @@ class ApiGateway { context: RequestContext, normalizedQuery: NormalizedQuery, sqlQuery: any, - cacheMode: CacheMode = 'stale-if-slow', ): Promise { const queries: QueryBody[] = [{ ...sqlQuery, query: sqlQuery.sql[0], values: sqlQuery.sql[1], renewQuery: normalizedQuery.renewQuery, - cacheMode, + cacheMode: normalizedQuery.cacheMode, requestId: context.requestId, context, persistent: false, @@ -1691,7 +1700,7 @@ class ApiGateway { query: totalQuery.sql[0], values: totalQuery.sql[1], renewQuery: normalizedTotal.renewQuery, - cacheMode, + cacheMode: normalizedTotal.cacheMode, requestId: context.requestId, context }); @@ -1851,6 +1860,7 @@ class ApiGateway { context, res, apiType = 'rest', + cacheMode, ...props } = request; const requestStarted = new Date(); @@ -1872,7 +1882,7 @@ class ApiGateway { }, context); const [queryType, normalizedQueries] = - await this.getNormalizedQueries(query, context); + await this.getNormalizedQueries(query, context, false, false, cacheMode); if ( queryType !== QueryTypeEnum.REGULAR_QUERY && @@ -1905,7 +1915,6 @@ class ApiGateway { context, normalizedQuery, sqlQueries[index], - props.cacheMode, ); const annotation = prepareAnnotation( @@ -1967,6 +1976,7 @@ class ApiGateway { const { context, res, + cacheMode, } = request; const requestStarted = new Date(); @@ -1981,7 +1991,7 @@ class ApiGateway { } const [queryType, normalizedQueries] = - await this.getNormalizedQueries(query, context, request.streaming, request.memberExpressions); + await this.getNormalizedQueries(query, context, request.streaming, request.memberExpressions, cacheMode); const compilerApi = await this.getCompilerApi(context); let metaConfigResult = await compilerApi.metaConfig(request.context, { @@ -2099,7 +2109,7 @@ class ApiGateway { } public async subscribe({ - query, context, res, subscribe, subscriptionState, queryType, apiType + query, context, res, subscribe, subscriptionState, queryType, apiType, cacheMode }) { const requestStarted = new Date(); try { @@ -2112,7 +2122,7 @@ class ApiGateway { let error: any = null; if (!subscribe) { - await this.load({ query, context, res, queryType, apiType }); + await this.load({ query, context, res, queryType, apiType, cacheMode }); return; } @@ -2129,6 +2139,7 @@ class ApiGateway { }, queryType, apiType, + cacheMode, }); const state = await subscriptionState(); if (result && (!state || JSON.stringify(state.result) !== JSON.stringify(result))) { @@ -2159,7 +2170,7 @@ class ApiGateway { }; } - protected parseQueryParam(query): Query | Query[] { + protected parseQueryParam(query: RequestQuery | 'undefined'): Query | Query[] { if (!query || query === 'undefined') { throw new UserError('Query param is required'); } diff --git a/packages/cubejs-api-gateway/src/query.js b/packages/cubejs-api-gateway/src/query.js index 8bee96aa83802..324010c66376a 100644 --- a/packages/cubejs-api-gateway/src/query.js +++ b/packages/cubejs-api-gateway/src/query.js @@ -189,6 +189,7 @@ const querySchema = Joi.object().keys({ total: Joi.boolean(), // @deprecated renewQuery: Joi.boolean(), + cacheMode: Joi.valid('stale-if-slow', 'stale-while-revalidate', 'must-revalidate', 'no-cache'), ungrouped: Joi.boolean(), responseFormat: Joi.valid('default', 'compact'), subqueryJoins: Joi.array().items(subqueryJoin), diff --git a/packages/cubejs-api-gateway/src/types/query.ts b/packages/cubejs-api-gateway/src/types/query.ts index ef8deb3bd8c89..8f7ce8ad386af 100644 --- a/packages/cubejs-api-gateway/src/types/query.ts +++ b/packages/cubejs-api-gateway/src/types/query.ts @@ -12,6 +12,7 @@ import { QueryTimeDimensionGranularity, } from './strings'; import { ResultType } from './enums'; +import { CacheMode } from '@cubejs-backend/shared'; /** * Query base filter definition. @@ -141,6 +142,7 @@ interface Query { timezone?: string; // @deprecated renewQuery?: boolean; + cacheMode?: CacheMode; ungrouped?: boolean; responseFormat?: ResultType; diff --git a/packages/cubejs-api-gateway/src/types/request.ts b/packages/cubejs-api-gateway/src/types/request.ts index ed9310b5e95da..16e97558a7e0b 100644 --- a/packages/cubejs-api-gateway/src/types/request.ts +++ b/packages/cubejs-api-gateway/src/types/request.ts @@ -120,11 +120,17 @@ type BaseRequest = { res: ResponseResultFn }; +type RequestQuery = Record | Record[] & { + renewQuery?: boolean; + cacheMode?: CacheMode; + cache?: CacheMode; +}; + /** * Data query HTTP request parameters map data type. */ type QueryRequest = BaseRequest & { - query: Record | Record[]; + query: RequestQuery; queryType?: RequestType; apiType?: ApiType; resType?: ResultType @@ -221,6 +227,7 @@ export { ResponseResultFn, MetaResponseResultFn, BaseRequest, + RequestQuery, QueryRequest, PreAggsJobsRequest, PreAggsSelector, From 8e8fd01764e536128d296cb90c027ff9366da636 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Thu, 16 Oct 2025 16:07:57 +0300 Subject: [PATCH 44/53] fix tests --- packages/cubejs-api-gateway/test/index.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/cubejs-api-gateway/test/index.test.ts b/packages/cubejs-api-gateway/test/index.test.ts index 2a8d5f03bab0f..78fa66e2d3e43 100644 --- a/packages/cubejs-api-gateway/test/index.test.ts +++ b/packages/cubejs-api-gateway/test/index.test.ts @@ -377,6 +377,7 @@ describe('API Gateway', () => { queryType: 'regularQuery', normalizedQueries: [ { + cacheMode: 'stale-if-slow', measures: ['Foo.bar'], timezone: 'UTC', filters: [], @@ -389,6 +390,7 @@ describe('API Gateway', () => { ], queryOrder: [{ id: 'desc' }], pivotQuery: { + cacheMode: 'stale-if-slow', measures: ['Foo.bar'], timezone: 'UTC', filters: [], @@ -436,6 +438,7 @@ describe('API Gateway', () => { (res) => { expect(res.body.normalizedQueries).toStrictEqual([ { + cacheMode: 'stale-if-slow', measures: ['Foo.bar'], timezone: 'UTC', filters: [{ @@ -511,6 +514,7 @@ describe('API Gateway', () => { queryType: 'regularQuery', normalizedQueries: [ { + cacheMode: 'stale-if-slow', measures: ['Foo.bar'], timezone: 'UTC', filters: [], @@ -523,6 +527,7 @@ describe('API Gateway', () => { ], queryOrder: [{ id: 'desc' }], pivotQuery: { + cacheMode: 'stale-if-slow', measures: ['Foo.bar'], timezone: 'UTC', filters: [], @@ -556,6 +561,7 @@ describe('API Gateway', () => { queryType: 'regularQuery', normalizedQueries: [ { + cacheMode: 'stale-if-slow', measures: ['Foo.bar'], order: [{ id: 'Foo.bar', desc: true }], timezone: 'UTC', @@ -569,6 +575,7 @@ describe('API Gateway', () => { ], queryOrder: [{ id: 'desc' }], pivotQuery: { + cacheMode: 'stale-if-slow', measures: ['Foo.bar'], order: [{ id: 'Foo.bar', desc: true }], timezone: 'UTC', From 70af110cb9baa3dabc3845ed87d3c28795a17366 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Thu, 16 Oct 2025 16:14:38 +0300 Subject: [PATCH 45/53] some cleanup (removed renewQuery) --- packages/cubejs-api-gateway/src/gateway.ts | 5 ----- packages/cubejs-api-gateway/src/types/request.ts | 1 - 2 files changed, 6 deletions(-) diff --git a/packages/cubejs-api-gateway/src/gateway.ts b/packages/cubejs-api-gateway/src/gateway.ts index 85383a9e72dde..7d575d1c2150d 100644 --- a/packages/cubejs-api-gateway/src/gateway.ts +++ b/packages/cubejs-api-gateway/src/gateway.ts @@ -1675,7 +1675,6 @@ class ApiGateway { ...sqlQuery, query: sqlQuery.sql[0], values: sqlQuery.sql[1], - renewQuery: normalizedQuery.renewQuery, cacheMode: normalizedQuery.cacheMode, requestId: context.requestId, context, @@ -1699,7 +1698,6 @@ class ApiGateway { ...totalQuery, query: totalQuery.sql[0], values: totalQuery.sql[1], - renewQuery: normalizedTotal.renewQuery, cacheMode: normalizedTotal.cacheMode, requestId: context.requestId, context @@ -1820,7 +1818,6 @@ class ApiGateway { ...sqlQuery, query: sqlQuery.sql[0], values: sqlQuery.sql[1], - renewQuery: false, cacheMode: 'stale-if-slow', requestId: context.requestId, context, @@ -2015,7 +2012,6 @@ class ApiGateway { ...sqlQuery, query: sqlQuery.query || sqlQuery.sql[0], values: sqlQuery.values || sqlQuery.sql[1], - renewQuery: false, cacheMode: 'stale-if-slow', requestId: context.requestId, context, @@ -2034,7 +2030,6 @@ class ApiGateway { const finalQuery: QueryBody = { query: request.sqlQuery[0], values: request.sqlQuery[1], - renewQuery: normalizedQueries[0].renewQuery, cacheMode: request.cacheMode, requestId: context.requestId, context, diff --git a/packages/cubejs-api-gateway/src/types/request.ts b/packages/cubejs-api-gateway/src/types/request.ts index 16e97558a7e0b..024352615bba7 100644 --- a/packages/cubejs-api-gateway/src/types/request.ts +++ b/packages/cubejs-api-gateway/src/types/request.ts @@ -123,7 +123,6 @@ type BaseRequest = { type RequestQuery = Record | Record[] & { renewQuery?: boolean; cacheMode?: CacheMode; - cache?: CacheMode; }; /** From eb31ea9bb86e251cb550134b362919a89bce0e09 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Thu, 16 Oct 2025 16:14:53 +0300 Subject: [PATCH 46/53] update cacheMod in graphql --- packages/cubejs-api-gateway/src/graphql.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cubejs-api-gateway/src/graphql.ts b/packages/cubejs-api-gateway/src/graphql.ts index b9002bf4ade5d..46c23d3e0708b 100644 --- a/packages/cubejs-api-gateway/src/graphql.ts +++ b/packages/cubejs-api-gateway/src/graphql.ts @@ -653,7 +653,7 @@ export function makeSchema(metaConfig: any): GraphQLSchema { apiGateway.load({ query, queryType: QueryType.REGULAR_QUERY, - ...(query.cache ? { cacheMode: query.cache } : {}), + ...(query.cache ? { cache: query.cache } : {}), context: req.context, res: async (message) => { if (message.error) { From d9fc9467371956fdc493b542260b5abc24adaada Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Thu, 16 Oct 2025 16:23:41 +0300 Subject: [PATCH 47/53] return back cache as public prop in query --- packages/cubejs-api-gateway/src/gateway.ts | 5 +++-- packages/cubejs-api-gateway/src/query.js | 1 + packages/cubejs-api-gateway/src/types/query.ts | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/cubejs-api-gateway/src/gateway.ts b/packages/cubejs-api-gateway/src/gateway.ts index 7d575d1c2150d..93cc6374e44be 100644 --- a/packages/cubejs-api-gateway/src/gateway.ts +++ b/packages/cubejs-api-gateway/src/gateway.ts @@ -592,17 +592,18 @@ class ApiGateway { private normalizeQueryCacheMode(query: Query, cacheMode: CacheMode | undefined): Query { if (cacheMode !== undefined) { query.cacheMode = cacheMode; - } else if (!query.cacheMode && query?.renewQuery !== undefined) { + } else if (!query.cache && query?.renewQuery !== undefined) { // TODO: Drop this when renewQuery will be removed query.cacheMode = query.renewQuery === true ? 'must-revalidate' : 'stale-if-slow'; - } else if (!query.cacheMode) { + } else if (!query.cache) { query.cacheMode = 'stale-if-slow'; } // TODO: Drop this when renewQuery will be removed query.renewQuery = undefined; + query.cache = undefined; return query; } diff --git a/packages/cubejs-api-gateway/src/query.js b/packages/cubejs-api-gateway/src/query.js index 324010c66376a..074bcd6fed734 100644 --- a/packages/cubejs-api-gateway/src/query.js +++ b/packages/cubejs-api-gateway/src/query.js @@ -190,6 +190,7 @@ const querySchema = Joi.object().keys({ // @deprecated renewQuery: Joi.boolean(), cacheMode: Joi.valid('stale-if-slow', 'stale-while-revalidate', 'must-revalidate', 'no-cache'), + cache: Joi.valid('stale-if-slow', 'stale-while-revalidate', 'must-revalidate', 'no-cache'), ungrouped: Joi.boolean(), responseFormat: Joi.valid('default', 'compact'), subqueryJoins: Joi.array().items(subqueryJoin), diff --git a/packages/cubejs-api-gateway/src/types/query.ts b/packages/cubejs-api-gateway/src/types/query.ts index 8f7ce8ad386af..07d263afb9547 100644 --- a/packages/cubejs-api-gateway/src/types/query.ts +++ b/packages/cubejs-api-gateway/src/types/query.ts @@ -142,7 +142,8 @@ interface Query { timezone?: string; // @deprecated renewQuery?: boolean; - cacheMode?: CacheMode; + cacheMode?: CacheMode; // used after query normalization + cache?: CacheMode; // Used in public interface ungrouped?: boolean; responseFormat?: ResultType; From 67016310158d3c8d479bfd9edd26803caf0ddce5 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Thu, 16 Oct 2025 16:40:29 +0300 Subject: [PATCH 48/53] fix --- packages/cubejs-api-gateway/src/gateway.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/cubejs-api-gateway/src/gateway.ts b/packages/cubejs-api-gateway/src/gateway.ts index 93cc6374e44be..9cc9bf4a93e0b 100644 --- a/packages/cubejs-api-gateway/src/gateway.ts +++ b/packages/cubejs-api-gateway/src/gateway.ts @@ -599,6 +599,8 @@ class ApiGateway { : 'stale-if-slow'; } else if (!query.cache) { query.cacheMode = 'stale-if-slow'; + } else { + query.cacheMode = query.cache; } // TODO: Drop this when renewQuery will be removed From 5ebef32a53e398c27d5e0152254bb00bc2599cb4 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Thu, 16 Oct 2025 17:19:05 +0300 Subject: [PATCH 49/53] lint fix # Conflicts: # packages/cubejs-api-gateway/package.json --- packages/cubejs-api-gateway/package.json | 1 + packages/cubejs-api-gateway/src/types/query.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/cubejs-api-gateway/package.json b/packages/cubejs-api-gateway/package.json index 0e6ba3ddf66af..35ac4b20c5ace 100644 --- a/packages/cubejs-api-gateway/package.json +++ b/packages/cubejs-api-gateway/package.json @@ -29,6 +29,7 @@ "dependencies": { "@cubejs-backend/native": "1.3.82", "@cubejs-backend/shared": "1.3.82", + "@cubejs-backend/query-orchestrator": "1.3.82", "@ungap/structured-clone": "^0.3.4", "assert-never": "^1.4.0", "body-parser": "^1.19.0", diff --git a/packages/cubejs-api-gateway/src/types/query.ts b/packages/cubejs-api-gateway/src/types/query.ts index 07d263afb9547..86c06c59ffce3 100644 --- a/packages/cubejs-api-gateway/src/types/query.ts +++ b/packages/cubejs-api-gateway/src/types/query.ts @@ -5,6 +5,7 @@ * Network query data types definition. */ +import { CacheMode } from '@cubejs-backend/shared'; import { Member, TimeMember, @@ -12,7 +13,6 @@ import { QueryTimeDimensionGranularity, } from './strings'; import { ResultType } from './enums'; -import { CacheMode } from '@cubejs-backend/shared'; /** * Query base filter definition. From ee3132dc7240af875df82d2e8e2cfec0321415a1 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Thu, 16 Oct 2025 17:31:07 +0300 Subject: [PATCH 50/53] fix subscribe() --- packages/cubejs-api-gateway/src/gateway.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cubejs-api-gateway/src/gateway.ts b/packages/cubejs-api-gateway/src/gateway.ts index 9cc9bf4a93e0b..47cedcacb70f7 100644 --- a/packages/cubejs-api-gateway/src/gateway.ts +++ b/packages/cubejs-api-gateway/src/gateway.ts @@ -2107,7 +2107,7 @@ class ApiGateway { } public async subscribe({ - query, context, res, subscribe, subscriptionState, queryType, apiType, cacheMode + query, context, res, subscribe, subscriptionState, queryType, apiType, cache }) { const requestStarted = new Date(); try { @@ -2120,7 +2120,7 @@ class ApiGateway { let error: any = null; if (!subscribe) { - await this.load({ query, context, res, queryType, apiType, cacheMode }); + await this.load({ query, context, res, queryType, apiType, cacheMode: cache }); return; } @@ -2137,7 +2137,7 @@ class ApiGateway { }, queryType, apiType, - cacheMode, + cacheMode: cache, }); const state = await subscriptionState(); if (result && (!state || JSON.stringify(state.result) !== JSON.stringify(result))) { From 76453da5750a1cee9fe35d8ed1eb90e55a99834f Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Thu, 16 Oct 2025 18:01:36 +0300 Subject: [PATCH 51/53] remove cache from subscribe --- packages/cubejs-api-gateway/src/gateway.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/cubejs-api-gateway/src/gateway.ts b/packages/cubejs-api-gateway/src/gateway.ts index 47cedcacb70f7..85a5904950afc 100644 --- a/packages/cubejs-api-gateway/src/gateway.ts +++ b/packages/cubejs-api-gateway/src/gateway.ts @@ -2107,7 +2107,7 @@ class ApiGateway { } public async subscribe({ - query, context, res, subscribe, subscriptionState, queryType, apiType, cache + query, context, res, subscribe, subscriptionState, queryType, apiType }) { const requestStarted = new Date(); try { @@ -2120,7 +2120,7 @@ class ApiGateway { let error: any = null; if (!subscribe) { - await this.load({ query, context, res, queryType, apiType, cacheMode: cache }); + await this.load({ query, context, res, queryType, apiType }); return; } @@ -2137,7 +2137,6 @@ class ApiGateway { }, queryType, apiType, - cacheMode: cache, }); const state = await subscriptionState(); if (result && (!state || JSON.stringify(state.result) !== JSON.stringify(result))) { From 57c806695da5435321b4aedd2e5f3f97edf9c9b1 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Thu, 16 Oct 2025 19:01:17 +0300 Subject: [PATCH 52/53] fix CacheMode serialization --- rust/cubesql/cubesql/src/compile/engine/df/scan.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rust/cubesql/cubesql/src/compile/engine/df/scan.rs b/rust/cubesql/cubesql/src/compile/engine/df/scan.rs index db87dca4cef9a..250b116afbba2 100644 --- a/rust/cubesql/cubesql/src/compile/engine/df/scan.rs +++ b/rust/cubesql/cubesql/src/compile/engine/df/scan.rs @@ -82,9 +82,13 @@ impl MemberField { #[derive(Debug, Clone, Serialize)] pub enum CacheMode { + #[serde(rename = "stale-if-slow")] StaleIfSlow, + #[serde(rename = "stale-while-revalidate")] StaleWhileRevalidate, + #[serde(rename = "must-revalidate")] MustRevalidate, + #[serde(rename = "no-cache")] NoCache, } From bc52b41eee56a2f551558ea01abfaa79bda4fa17 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Fri, 24 Oct 2025 19:28:56 +0300 Subject: [PATCH 53/53] refactor: move normalizeQueryCacheMode to normalize --- packages/cubejs-api-gateway/src/gateway.ts | 25 +--------------- packages/cubejs-api-gateway/src/query.js | 33 ++++++++++++++++++++-- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/packages/cubejs-api-gateway/src/gateway.ts b/packages/cubejs-api-gateway/src/gateway.ts index 85a5904950afc..d195676897baf 100644 --- a/packages/cubejs-api-gateway/src/gateway.ts +++ b/packages/cubejs-api-gateway/src/gateway.ts @@ -589,27 +589,6 @@ class ApiGateway { return requestStarted && (new Date().getTime() - requestStarted.getTime()); } - private normalizeQueryCacheMode(query: Query, cacheMode: CacheMode | undefined): Query { - if (cacheMode !== undefined) { - query.cacheMode = cacheMode; - } else if (!query.cache && query?.renewQuery !== undefined) { - // TODO: Drop this when renewQuery will be removed - query.cacheMode = query.renewQuery === true - ? 'must-revalidate' - : 'stale-if-slow'; - } else if (!query.cache) { - query.cacheMode = 'stale-if-slow'; - } else { - query.cacheMode = query.cache; - } - - // TODO: Drop this when renewQuery will be removed - query.renewQuery = undefined; - query.cache = undefined; - - return query; - } - private filterVisibleItemsInMeta(context: RequestContext, cubes: any[]) { const isDevMode = getEnv('devMode'); function visibilityFilter(item) { @@ -1237,8 +1216,6 @@ class ApiGateway { cacheMode?: CacheMode, ): Promise<[QueryType, NormalizedQuery[], NormalizedQuery[]]> { let query = this.parseQueryParam(inputQuery); - query = Array.isArray(query) ? query.map(q => this.normalizeQueryCacheMode(q, cacheMode)) - : this.normalizeQueryCacheMode(query, cacheMode); let queryType: QueryType = QueryTypeEnum.REGULAR_QUERY; if (!Array.isArray(query)) { @@ -1277,7 +1254,7 @@ class ApiGateway { } return { - normalizedQuery: (normalizeQuery(currentQuery, persistent)), + normalizedQuery: (normalizeQuery(currentQuery, persistent, cacheMode)), hasExpressionsInQuery }; }); diff --git a/packages/cubejs-api-gateway/src/query.js b/packages/cubejs-api-gateway/src/query.js index 074bcd6fed734..e260d0855d90c 100644 --- a/packages/cubejs-api-gateway/src/query.js +++ b/packages/cubejs-api-gateway/src/query.js @@ -291,14 +291,43 @@ function parseInputMemberExpression(expression) { return expression; } +/** + * + * @param {Query} query + * @param {CacheMode} cacheMode + * @return {Query} + */ +function normalizeQueryCacheMode(query, cacheMode) { + if (cacheMode !== undefined) { + query.cacheMode = cacheMode; + } else if (!query.cache && query?.renewQuery !== undefined) { + // TODO: Drop this when renewQuery will be removed + query.cacheMode = query.renewQuery === true + ? 'must-revalidate' + : 'stale-if-slow'; + } else if (!query.cache) { + query.cacheMode = 'stale-if-slow'; + } else { + query.cacheMode = query.cache; + } + + // TODO: Drop this when renewQuery will be removed + query.renewQuery = undefined; + query.cache = undefined; + + return query; +} + /** * Normalize incoming network query. * @param {Query} query * @param {boolean} persistent + * @param {CacheMode} [cacheMode] * @throws {UserError} - * @returns {NormalizedQuery} + * @returns {import('./types/query').NormalizedQuery} */ -const normalizeQuery = (query, persistent) => { +const normalizeQuery = (query, persistent, cacheMode) => { + query = normalizeQueryCacheMode(query); const { error } = querySchema.validate(query); if (error) { throw new UserError(`Invalid query format: ${error.message || error.toString()}`);