Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
6508566
add cache option to the query
KSDaemon Sep 12, 2025
47f0d6f
Pass CacheMode from /cubesql to backend
KSDaemon Sep 15, 2025
e9dfa6b
imject CacheMode into more places
KSDaemon Sep 15, 2025
f7ace8c
update normalizeQuery with cache mode
KSDaemon Sep 15, 2025
4b5d412
pass cache mode within graphql
KSDaemon Sep 15, 2025
b127dc5
pass new cache mode in sqlApiLoad in API GW
KSDaemon Sep 15, 2025
958fd0e
fix types imports
KSDaemon Sep 15, 2025
932b9a5
update preAggs to use cache option instead of renewQuery
KSDaemon Sep 15, 2025
5406704
code polish
KSDaemon Sep 15, 2025
68b8641
comments with types
KSDaemon Sep 15, 2025
c8360d6
fix query type
KSDaemon Sep 15, 2025
0ed9a38
set default cacheMode = 'stale-if-slow' in normalize()
KSDaemon Sep 15, 2025
f68b285
more types and polish
KSDaemon Sep 15, 2025
093a949
backbone code for 'stale-if-slow' & 'stale-while-revalidate'
KSDaemon Sep 15, 2025
43e3a74
make query cache aware of queryBody.cache === 'must-revalidate'
KSDaemon Sep 15, 2025
281b076
First attempt to implement 'no-cache' scenario
KSDaemon Sep 15, 2025
965f0d4
add cache to open api spec and regenerate rust client
KSDaemon Sep 16, 2025
db4ba87
pass cache mode to cubeScan
KSDaemon Sep 16, 2025
7193430
cargo clippy/fmt
KSDaemon Sep 16, 2025
a0f785c
Implement background refresh
KSDaemon Sep 17, 2025
76eab86
add cache mode descriptions
KSDaemon Sep 17, 2025
96269e3
remove query cache mode from normalize query
KSDaemon Sep 18, 2025
5ce7a97
pass cacheMode to getSqlResponseInternal
KSDaemon Sep 18, 2025
4f0d7af
remove obsolete
KSDaemon Sep 18, 2025
a3102da
add cacheMode as input param in orchestratorApi
KSDaemon Sep 18, 2025
a8bbcbc
open api spec fix
KSDaemon Sep 18, 2025
0dc490f
fix cubesql after introducing cacheMode
KSDaemon Sep 18, 2025
b5d8e17
rename cache → cacheMode
KSDaemon Sep 18, 2025
f68a42d
add @cubejs-backend/query-orchestrator to deps of api gw (because of …
KSDaemon Sep 18, 2025
6def5be
clean up obsolete
KSDaemon Sep 18, 2025
414daab
pass cache_mode from SqlApiLoadPayload
KSDaemon Sep 18, 2025
ada177d
fix important
KSDaemon Sep 18, 2025
2b1d388
move 'no-cache' variant into queryCache.cachedQueryResult()
KSDaemon Sep 19, 2025
a2828e6
remove cacheMode from client query body types (it's incorrect)
KSDaemon Sep 19, 2025
690b02e
switch RefreshScheduler to use cacheMode instead of renewQuery
KSDaemon Sep 19, 2025
0b6d741
remove obsolete continueWait flag
KSDaemon Sep 19, 2025
c7380a5
fix refresh scheduler
KSDaemon Sep 19, 2025
8812bd1
add fallback to renewQuery in api gw
KSDaemon Sep 24, 2025
99f4bf8
fix tests
KSDaemon Sep 15, 2025
5d14e10
Docs
igorlukanin Sep 24, 2025
c0bb227
Deprecation
igorlukanin Sep 24, 2025
775dc60
refactor api gw: move copy/paste into this.normalizeCacheMode()
KSDaemon Sep 25, 2025
c04b566
fix tests snapshots
KSDaemon Sep 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion DEPRECATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
21 changes: 17 additions & 4 deletions docs/pages/product/apis-integrations/rest-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
[ref-recipe-real-time-data-fetch]: /product/apis-integrations/recipes/real-time-data-fetch
[ref-refresh-keys]: /product/data-modeling/reference/cube#refresh_key
11 changes: 0 additions & 11 deletions docs/pages/product/apis-integrations/rest-api/query-format.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 11 additions & 8 deletions docs/pages/product/apis-integrations/rest-api/reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -319,9 +320,10 @@ This endpoint is part of the [SQL API][ref-sql-api].

</InfoBox>

| 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
Expand Down Expand Up @@ -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
[ref-folders]: /product/data-modeling/reference/view#folders
[ref-cache-control]: /product/apis-integrations/rest-api#cache-control
7 changes: 7 additions & 0 deletions packages/cubejs-api-gateway/openspec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,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"
1 change: 1 addition & 0 deletions packages/cubejs-api-gateway/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"dependencies": {
"@cubejs-backend/native": "1.3.74",
"@cubejs-backend/shared": "1.3.74",
"@cubejs-backend/query-orchestrator": "1.3.74",
"@ungap/structured-clone": "^0.3.4",
"assert-never": "^1.4.0",
"body-parser": "^1.19.0",
Expand Down
52 changes: 39 additions & 13 deletions packages/cubejs-api-gateway/src/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
getRealType,
parseUtcIntoLocalDate,
QueryAlias,
CacheMode,
} from '@cubejs-backend/shared';
import {
ResultArrayWrapper,
Expand All @@ -28,6 +29,7 @@ import type {
} from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';

import { QueryBody } from '@cubejs-backend/query-orchestrator';
import {
QueryType,
ApiScopes,
Expand Down Expand Up @@ -177,7 +179,13 @@ class ApiGateway {

public constructor(
protected readonly apiSecret: string,
/**
* It actually returns a Promise<CompilerApi>
*/
protected readonly compilerApi: (ctx: RequestContext) => Promise<any>,
/**
* It actually returns a Promise<OrchestratorApi>
*/
protected readonly adapterApi: (ctx: RequestContext) => Promise<any>,
protected readonly logger: any,
protected readonly options: ApiGatewayOptions,
Expand Down Expand Up @@ -311,6 +319,7 @@ class ApiGateway {
context: req.context,
res: this.resToResultFn(res),
queryType: req.query.queryType,
cacheMode: this.normalizeCacheMode(req.query.query, req.query.cache),
});
}));

Expand All @@ -320,7 +329,8 @@ class ApiGateway {
query: req.body.query,
context: req.context,
res: this.resToResultFn(res),
queryType: req.body.queryType
queryType: req.body.queryType,
cacheMode: this.normalizeCacheMode(req.body.query, req.body.cache),
});
}));

Expand All @@ -329,7 +339,8 @@ class ApiGateway {
query: req.query.query,
context: req.context,
res: this.resToResultFn(res),
queryType: req.query.queryType
queryType: req.query.queryType,
cacheMode: this.normalizeCacheMode(req.query.query, req.query.cache),
});
}));

Expand Down Expand Up @@ -425,7 +436,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,
Expand Down Expand Up @@ -576,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) {
Expand Down Expand Up @@ -1636,13 +1660,14 @@ class ApiGateway {
context: RequestContext,
normalizedQuery: NormalizedQuery,
sqlQuery: any,
cacheMode: CacheMode = 'stale-if-slow',
): Promise<ResultWrapper> {
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,
Expand All @@ -1665,8 +1690,8 @@ class ApiGateway {
...totalQuery,
query: totalQuery.sql[0],
values: totalQuery.sql[1],
continueWait: true,
renewQuery: normalizedTotal.renewQuery,
cacheMode,
requestId: context.requestId,
context
});
Expand Down Expand Up @@ -1782,12 +1807,12 @@ 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,
Expand Down Expand Up @@ -1880,6 +1905,7 @@ class ApiGateway {
context,
normalizedQuery,
sqlQueries[index],
props.cacheMode,
);

const annotation = prepareAnnotation(
Expand Down Expand Up @@ -1970,17 +1996,17 @@ 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,
Expand All @@ -1995,11 +2021,11 @@ 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],
Expand Down
5 changes: 4 additions & 1 deletion packages/cubejs-api-gateway/src/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ function parseDates(result: any) {
}

export function getJsonQuery(metaConfig: any, args: Record<string, any>, 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[] = [];
Expand Down Expand Up @@ -461,6 +461,7 @@ export function getJsonQuery(metaConfig: any, args: Record<string, any>, infos:
...(timezone && { timezone }),
...(filters.length && { filters }),
...(renewQuery && { renewQuery }),
...(cache && { cache }),
...(ungrouped && { ungrouped }),
};
}
Expand Down Expand Up @@ -639,6 +640,7 @@ export function makeSchema(metaConfig: any): GraphQLSchema {
offset: intArg(),
timezone: stringArg(),
renewQuery: booleanArg(),
cache: stringArg(),
ungrouped: booleanArg(),
orderBy: arg({
type: 'RootOrderByInput'
Expand All @@ -651,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) {
Expand Down
1 change: 1 addition & 0 deletions packages/cubejs-api-gateway/src/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ 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(),
ungrouped: Joi.boolean(),
responseFormat: Joi.valid('default', 'compact'),
Expand Down
9 changes: 5 additions & 4 deletions packages/cubejs-api-gateway/src/sql-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
Sql4SqlResponse,
} 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';
Expand Down Expand Up @@ -65,8 +65,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<Sql4SqlResponse> {
Expand Down Expand Up @@ -207,7 +207,7 @@ 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);

// eslint-disable-next-line no-async-promise-executor
Expand All @@ -218,6 +218,7 @@ export class SQLServer {
query,
sqlQuery,
streaming,
cacheMode,
context,
memberExpressions: true,
res: (response) => {
Expand Down
Loading
Loading