Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
2c0d9ce
add cache option to the query
KSDaemon Sep 12, 2025
5b4b45d
Pass CacheMode from /cubesql to backend
KSDaemon Sep 15, 2025
79b8dd7
imject CacheMode into more places
KSDaemon Sep 15, 2025
372e4c8
update normalizeQuery with cache mode
KSDaemon Sep 15, 2025
289957f
pass cache mode within graphql
KSDaemon Sep 15, 2025
507fd0e
pass new cache mode in sqlApiLoad in API GW
KSDaemon Sep 15, 2025
00f506f
fix types imports
KSDaemon Sep 15, 2025
9abee17
update preAggs to use cache option instead of renewQuery
KSDaemon Sep 15, 2025
8a72260
code polish
KSDaemon Sep 15, 2025
2a017e8
comments with types
KSDaemon Sep 15, 2025
2f22df2
fix query type
KSDaemon Sep 15, 2025
323dc40
set default cacheMode = 'stale-if-slow' in normalize()
KSDaemon Sep 15, 2025
1bf3d62
more types and polish
KSDaemon Sep 15, 2025
4a34088
backbone code for 'stale-if-slow' & 'stale-while-revalidate'
KSDaemon Sep 15, 2025
7490dfe
make query cache aware of queryBody.cache === 'must-revalidate'
KSDaemon Sep 15, 2025
be23c52
First attempt to implement 'no-cache' scenario
KSDaemon Sep 15, 2025
dbfdcbf
add cache to open api spec and regenerate rust client
KSDaemon Sep 16, 2025
d0c2671
pass cache mode to cubeScan
KSDaemon Sep 16, 2025
2d5adbc
cargo clippy/fmt
KSDaemon Sep 16, 2025
6c857c1
Implement background refresh
KSDaemon Sep 17, 2025
34fcf6a
add cache mode descriptions
KSDaemon Sep 17, 2025
d62a495
remove query cache mode from normalize query
KSDaemon Sep 18, 2025
2278a2b
pass cacheMode to getSqlResponseInternal
KSDaemon Sep 18, 2025
e82a230
remove obsolete
KSDaemon Sep 18, 2025
25229ec
add cacheMode as input param in orchestratorApi
KSDaemon Sep 18, 2025
ce573bd
open api spec fix
KSDaemon Sep 18, 2025
219ba57
fix cubesql after introducing cacheMode
KSDaemon Sep 18, 2025
1932339
rename cache → cacheMode
KSDaemon Sep 18, 2025
7c41e0a
clean up obsolete
KSDaemon Sep 18, 2025
e719434
pass cache_mode from SqlApiLoadPayload
KSDaemon Sep 18, 2025
cd33cd4
fix important
KSDaemon Sep 18, 2025
ce2d218
move 'no-cache' variant into queryCache.cachedQueryResult()
KSDaemon Sep 19, 2025
99c9ca6
remove cacheMode from client query body types (it's incorrect)
KSDaemon Sep 19, 2025
62a7ae3
switch RefreshScheduler to use cacheMode instead of renewQuery
KSDaemon Sep 19, 2025
495ba44
remove obsolete continueWait flag
KSDaemon Sep 19, 2025
df2e63b
fix refresh scheduler
KSDaemon Sep 19, 2025
6a325e4
add fallback to renewQuery in api gw
KSDaemon Sep 24, 2025
bc3224e
fix tests
KSDaemon Sep 15, 2025
72d6144
Docs
igorlukanin Sep 24, 2025
6a4c79d
Deprecation
igorlukanin Sep 24, 2025
b724bf2
refactor api gw: move copy/paste into this.normalizeCacheMode()
KSDaemon Sep 25, 2025
c4e6fed
fix tests snapshots
KSDaemon Sep 25, 2025
047a838
return cacheMode into query object
KSDaemon Oct 16, 2025
8e8fd01
fix tests
KSDaemon Oct 16, 2025
70af110
some cleanup (removed renewQuery)
KSDaemon Oct 16, 2025
eb31ea9
update cacheMod in graphql
KSDaemon Oct 16, 2025
d9fc946
return back cache as public prop in query
KSDaemon Oct 16, 2025
6701631
fix
KSDaemon Oct 16, 2025
5ebef32
lint fix
KSDaemon Oct 16, 2025
ee3132d
fix subscribe()
KSDaemon Oct 16, 2025
76453da
remove cache from subscribe
KSDaemon Oct 16, 2025
57c8066
fix CacheMode serialization
KSDaemon Oct 16, 2025
bc52b41
refactor: move normalizeQueryCacheMode to normalize
KSDaemon Oct 24, 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 @@ -484,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"
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.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",
Expand Down
61 changes: 36 additions & 25 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 All @@ -50,7 +52,9 @@ import {
PreAggJob,
PreAggJobStatusItem,
PreAggJobStatusResponse,
SqlApiRequest, MetaResponseResultFn,
SqlApiRequest,
MetaResponseResultFn,
RequestQuery,
} from './types/request';
import {
CheckAuthInternalOptions,
Expand Down Expand Up @@ -177,7 +181,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 +321,7 @@ class ApiGateway {
context: req.context,
res: this.resToResultFn(res),
queryType: req.query.queryType,
cacheMode: req.query.cache,
});
}));

Expand All @@ -320,7 +331,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,
});
}));

Expand All @@ -329,7 +341,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,
});
}));

Expand Down Expand Up @@ -425,7 +438,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 @@ -1200,6 +1213,7 @@ class ApiGateway {
context: RequestContext,
persistent = false,
memberExpressions: boolean = false,
cacheMode?: CacheMode,
): Promise<[QueryType, NormalizedQuery[], NormalizedQuery[]]> {
let query = this.parseQueryParam(inputQuery);

Expand Down Expand Up @@ -1240,7 +1254,7 @@ class ApiGateway {
}

return {
normalizedQuery: (normalizeQuery(currentQuery, persistent)),
normalizedQuery: (normalizeQuery(currentQuery, persistent, cacheMode)),
hasExpressionsInQuery
};
});
Expand Down Expand Up @@ -1285,13 +1299,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');
}

Expand Down Expand Up @@ -1637,12 +1651,11 @@ class ApiGateway {
normalizedQuery: NormalizedQuery,
sqlQuery: any,
): Promise<ResultWrapper> {
const queries = [{
const queries: QueryBody[] = [{
...sqlQuery,
query: sqlQuery.sql[0],
values: sqlQuery.sql[1],
continueWait: true,
renewQuery: normalizedQuery.renewQuery,
cacheMode: normalizedQuery.cacheMode,
requestId: context.requestId,
context,
persistent: false,
Expand All @@ -1665,8 +1678,7 @@ class ApiGateway {
...totalQuery,
query: totalQuery.sql[0],
values: totalQuery.sql[1],
continueWait: true,
renewQuery: normalizedTotal.renewQuery,
cacheMode: normalizedTotal.cacheMode,
requestId: context.requestId,
context
});
Expand Down Expand Up @@ -1782,12 +1794,11 @@ 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 @@ -1826,6 +1837,7 @@ class ApiGateway {
context,
res,
apiType = 'rest',
cacheMode,
...props
} = request;
const requestStarted = new Date();
Expand All @@ -1847,7 +1859,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 &&
Expand Down Expand Up @@ -1941,6 +1953,7 @@ class ApiGateway {
const {
context,
res,
cacheMode,
} = request;
const requestStarted = new Date();

Expand All @@ -1955,7 +1968,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, {
Expand All @@ -1970,17 +1983,16 @@ 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 +2007,10 @@ 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 Expand Up @@ -2133,7 +2144,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');
}
Expand Down
Loading
Loading