diff --git a/docs/content/product/_meta.js b/docs/content/product/_meta.js index dbd4abc57641b..52cfdd3cdafbb 100644 --- a/docs/content/product/_meta.js +++ b/docs/content/product/_meta.js @@ -1,7 +1,7 @@ export default { "introduction": "Introduction", "getting-started": "Getting started", - "configuration": "Data Sources", + "configuration": "Configuration", "data-modeling": "Data modeling", "exploration": "Explore & Analyze", "presentation": "Present & Share", diff --git a/docs/content/product/administration/deployment/auto-suspension.mdx b/docs/content/product/administration/deployment/auto-suspension.mdx index 7c5ed1186cf29..ef9eb1bf6831a 100644 --- a/docs/content/product/administration/deployment/auto-suspension.mdx +++ b/docs/content/product/administration/deployment/auto-suspension.mdx @@ -57,15 +57,18 @@ from running. - [Monitoring integrations][ref-monitoring] are also suspended, which prevents the export of metrics and logs. -When a deployment is resumed from auto-suspension: +When a deployment is [resumed](#resuming-a-suspended-deployment) from auto-suspension: - [Data model][ref-data-model] compilation would need to be done from scratch. It applies to all tenants in case [multitenancy][ref-multitenancy] is set up. -Consequently, one or more queries served after a deployment is resumed from +Consequently, one or more requests served after a deployment is resumed from auto-suspension are likely to have suboptimal performance. - [Refresh worker][ref-refresh-worker] would need to refresh all pre-aggregations that became stale during the suspension, competing for the query queue with API instances and compromising the end-user experience. +- Until the deployment is fully resumed, the requests will be served by transient, +on-demand API instances with limited performance. There are no guarantees for the +[version][ref-cube-version] of Cube these API instances will be running. ## Configuration @@ -120,4 +123,5 @@ response times to be significantly longer than usual. [ref-multitenancy]: /product/configuration/multitenancy [self-effects]: #effects-on-experience [ref-refresh-worker]: /product/deployment#refresh-worker -[ref-sls]: /product/apis-integrations/semantic-layer-sync#on-schedule \ No newline at end of file +[ref-sls]: /product/apis-integrations/semantic-layer-sync#on-schedule +[ref-cube-version]: /product/administration/deployment/deployments#cube-version \ No newline at end of file diff --git a/package.json b/package.json index 15665cfbdda6d..be2953f2990fd 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "postcss": "^8.2.8", "prettier": "^2.0.5", "rimraf": "^3.0.2", - "rollup": "2.53.1", + "rollup": "2.80.0", "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-tsconfig-paths": "^1.5.2", "typescript": "~5.2.2" diff --git a/packages/cubejs-client-core/src/index.ts b/packages/cubejs-client-core/src/index.ts index 1bc9665b0a7f2..50ff869809445 100644 --- a/packages/cubejs-client-core/src/index.ts +++ b/packages/cubejs-client-core/src/index.ts @@ -46,6 +46,10 @@ export type LoadMethodOptions = { * Function that receives `ProgressResult` on each `Continue wait` message. */ progressCallback?(result: ProgressResult): void; + /** + * Server-side cache policy for query execution. Does not control client-side caching. + */ + cache?: CacheMode; /** * AbortSignal to cancel requests */ @@ -113,10 +117,6 @@ export type CubeSqlOptions = LoadMethodOptions & { * Query timeout in milliseconds */ timeout?: number; - /** - * Cache mode for query execution - */ - cache?: CacheMode; }; export type CubeSqlSchemaColumn = { @@ -577,13 +577,20 @@ class CubeApi { */ public load>(query: QueryType, options?: LoadMethodOptions, callback?: CallableFunction, responseFormat: ResponseFormat = 'default') { [query, options] = this.prepareQueryOptions(query, options, responseFormat); + + const params: Record = { + query, + queryType: 'multi', + signal: options?.signal, + baseRequestId: options?.baseRequestId, + }; + + if (options?.cache) { + params.cache = options.cache; + } + return this.loadMethod( - () => this.request('load', { - query, - queryType: 'multi', - signal: options?.signal, - baseRequestId: options?.baseRequestId, - }), + () => this.request('load', params), (response: any) => this.loadResponseInternal(response, options), options, callback @@ -726,14 +733,19 @@ class CubeApi { public cubeSql(sqlQuery: string, options?: CubeSqlOptions, callback?: LoadMethodCallback): Promise | UnsubscribeObj { return this.loadMethod( () => { - const request = this.request('cubesql', { + const cubesqlParams: Record = { query: sqlQuery, - cache: options?.cache, method: 'POST', signal: options?.signal, fetchTimeout: options?.timeout, baseRequestId: options?.baseRequestId, - }); + }; + + if (options?.cache) { + cubesqlParams.cache = options.cache; + } + + const request = this.request('cubesql', cubesqlParams); return request; }, diff --git a/packages/cubejs-client-core/test/CubeApi.test.ts b/packages/cubejs-client-core/test/CubeApi.test.ts index 65c1373ad9dfd..5c111c288a0fc 100644 --- a/packages/cubejs-client-core/test/CubeApi.test.ts +++ b/packages/cubejs-client-core/test/CubeApi.test.ts @@ -124,6 +124,46 @@ describe('CubeApi Load', () => { expect(res.rawData()).toEqual(DescriptiveQueryResponse.results[0].data); }); + test('simple query + { cache: "no-cache" }', async () => { + const requestSpy = jest.spyOn(HttpTransport.prototype, 'request').mockImplementation(() => ({ + subscribe: (cb) => Promise.resolve(cb({ + status: 200, + text: () => Promise.resolve(JSON.stringify(DescriptiveQueryResponse)), + json: () => Promise.resolve(DescriptiveQueryResponse) + } as any, + async () => undefined as any)) + })); + + const cubeApi = new CubeApi('token', { + apiUrl: 'http://localhost:4000/cubejs-api/v1', + }); + + const res = await cubeApi.load(DescriptiveQueryRequest as Query, { cache: 'no-cache' }); + expect(res).toBeInstanceOf(ResultSet); + expect(requestSpy).toHaveBeenCalled(); + expect(requestSpy.mock.calls[0]?.[1]?.cache).toBe('no-cache'); + }); + + test('simple query + { cache: "must-revalidate" }', async () => { + const requestSpy = jest.spyOn(HttpTransport.prototype, 'request').mockImplementation(() => ({ + subscribe: (cb) => Promise.resolve(cb({ + status: 200, + text: () => Promise.resolve(JSON.stringify(DescriptiveQueryResponse)), + json: () => Promise.resolve(DescriptiveQueryResponse) + } as any, + async () => undefined as any)) + })); + + const cubeApi = new CubeApi('token', { + apiUrl: 'http://localhost:4000/cubejs-api/v1', + }); + + const res = await cubeApi.load(DescriptiveQueryRequest as Query, { cache: 'must-revalidate' }); + expect(res).toBeInstanceOf(ResultSet); + expect(requestSpy).toHaveBeenCalled(); + expect(requestSpy.mock.calls[0]?.[1]?.cache).toBe('must-revalidate'); + }); + test('2 queries + compact response format', async () => { // Create a spy on the request method jest.spyOn(HttpTransport.prototype, 'request').mockImplementation(() => ({ diff --git a/packages/cubejs-client-react/index.d.ts b/packages/cubejs-client-react/index.d.ts index 05c815e1efe63..40525d9597d4f 100644 --- a/packages/cubejs-client-react/index.d.ts +++ b/packages/cubejs-client-react/index.d.ts @@ -32,6 +32,7 @@ declare module '@cubejs-client/react' { BinaryOperator, DeeplyReadonly, QueryRecordType, + CacheMode, } from '@cubejs-client/core'; type CubeProviderOptions = { @@ -139,6 +140,10 @@ declare module '@cubejs-client/react' { * `CubeApi` instance to use */ cubeApi?: CubeApi; + /** + * Server-side cache policy for query execution. Does not control client-side caching. + */ + cache?: CacheMode; /** * Output of this function will be rendered by the `QueryRenderer` */ @@ -477,6 +482,10 @@ declare module '@cubejs-client/react' { * If enabled, all members of the 'number' type will be automatically converted to numerical values on the client side */ castNumerics?: boolean; + /** + * Server-side cache policy for query execution. Does not control client-side caching. + */ + cache?: CacheMode; }; type UseCubeQueryResult = { diff --git a/packages/cubejs-client-react/src/QueryRenderer.jsx b/packages/cubejs-client-react/src/QueryRenderer.jsx index 500c8abb3d190..11220c44b94e9 100644 --- a/packages/cubejs-client-react/src/QueryRenderer.jsx +++ b/packages/cubejs-client-react/src/QueryRenderer.jsx @@ -14,7 +14,8 @@ export default class QueryRenderer extends React.Component { queries: null, loadSql: null, updateOnlyOnStateChange: false, - resetResultSetOnChange: true + resetResultSetOnChange: true, + cache: null, }; // @deprecated use `isQueryPresent` from `@cubejs-client/core` @@ -71,7 +72,7 @@ export default class QueryRenderer extends React.Component { } load(query) { - const { resetResultSetOnChange } = this.props; + const { resetResultSetOnChange, cache } = this.props; this.setState({ isLoading: true, error: null, @@ -81,6 +82,12 @@ export default class QueryRenderer extends React.Component { const { loadSql } = this.props; const cubeApi = this.cubeApi(); + const loadOptions = { + mutexObj: this.mutexObj, + mutexKey: 'query', + ...(cache ? { cache } : {}), + }; + if (query && isQueryPresent(query)) { if (loadSql === 'only') { cubeApi.sql(query, { mutexObj: this.mutexObj, mutexKey: 'sql' }) @@ -93,7 +100,7 @@ export default class QueryRenderer extends React.Component { } else if (loadSql) { Promise.all([ cubeApi.sql(query, { mutexObj: this.mutexObj, mutexKey: 'sql' }), - cubeApi.load(query, { mutexObj: this.mutexObj, mutexKey: 'query' }) + cubeApi.load(query, loadOptions) ]).then(([sqlQuery, resultSet]) => this.setState({ sqlQuery, resultSet, error: null, isLoading: false })) @@ -103,7 +110,7 @@ export default class QueryRenderer extends React.Component { isLoading: false })); } else { - cubeApi.load(query, { mutexObj: this.mutexObj, mutexKey: 'query' }) + cubeApi.load(query, loadOptions) .then(resultSet => this.setState({ resultSet, error: null, isLoading: false })) .catch(error => this.setState({ ...(resetResultSetOnChange ? { resultSet: null } : {}), @@ -116,7 +123,7 @@ export default class QueryRenderer extends React.Component { loadQueries(queries) { const cubeApi = this.cubeApi(); - const { resetResultSetOnChange } = this.props; + const { resetResultSetOnChange, cache } = this.props; this.setState({ isLoading: true, ...(resetResultSetOnChange ? { resultSet: null } : {}), @@ -124,7 +131,11 @@ export default class QueryRenderer extends React.Component { }); const resultPromises = Promise.all(toPairs(queries).map( - ([name, query]) => cubeApi.load(query, { mutexObj: this.mutexObj, mutexKey: name }).then(r => [name, r]) + ([name, query]) => cubeApi.load(query, { + mutexObj: this.mutexObj, + mutexKey: name, + ...(cache ? { cache } : {}), + }).then(r => [name, r]) )); resultPromises diff --git a/packages/cubejs-client-react/src/hooks/cube-query.js b/packages/cubejs-client-react/src/hooks/cube-query.js index c2f60ae9bcf27..d27ad02554f55 100644 --- a/packages/cubejs-client-react/src/hooks/cube-query.js +++ b/packages/cubejs-client-react/src/hooks/cube-query.js @@ -37,7 +37,8 @@ export function useCubeQuery(query, options = {}) { mutexObj: mutexRef.current, mutexKey: 'query', progressCallback, - castNumerics: Boolean(typeof options.castNumerics === 'boolean' ? options.castNumerics : context?.options?.castNumerics) + castNumerics: Boolean(typeof options.castNumerics === 'boolean' ? options.castNumerics : context?.options?.castNumerics), + ...(options.cache ? { cache: options.cache } : {}), }); setResultSet(response); @@ -85,6 +86,7 @@ export function useCubeQuery(query, options = {}) { mutexObj: mutexRef.current, mutexKey: 'query', progressCallback, + ...(options.cache ? { cache: options.cache } : {}), }, (e, result) => { if (e) { diff --git a/yarn.lock b/yarn.lock index 0a54b7e60f0e4..34b5b430c7d86 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8523,10 +8523,10 @@ "@types/estree" "*" "@types/json-schema" "*" -"@types/estree@*": - version "0.0.50" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83" - integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw== +"@types/estree@*", "@types/estree@1.0.7", "@types/estree@^1.0.0", "@types/estree@^1.0.5", "@types/estree@^1.0.6": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8" + integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== "@types/estree@0.0.39": version "0.0.39" @@ -8538,16 +8538,6 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== -"@types/estree@1.0.7", "@types/estree@^1.0.0", "@types/estree@^1.0.5": - version "1.0.7" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8" - integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== - -"@types/estree@^1.0.6": - version "1.0.6" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" - integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== - "@types/express-serve-static-core@*": version "4.17.26" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.26.tgz#5d9a8eeecb9d5f9d7fc1d85f541512a84638ae88" @@ -10650,22 +10640,13 @@ axe-core@^4.3.5: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.3.5.tgz#78d6911ba317a8262bfee292aeafcc1e04b49cc5" integrity sha512-WKTW1+xAzhMS5dJsxWkliixlO/PqC4VhmO9T4juNYcaTg9jzWiJsou6m5pxWYGfigWbwzJWeFY6z47a+4neRXA== -axios@^1.11.0, axios@^1.8.1, axios@^1.8.3: - version "1.12.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.12.0.tgz#11248459be05a5ee493485628fa0e4323d0abfc3" - integrity sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg== +axios@^1.11.0, axios@^1.12.0, axios@^1.8.1, axios@^1.8.3: + version "1.13.5" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.13.5.tgz#5e464688fa127e11a660a2c49441c009f6567a43" + integrity sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q== dependencies: - follow-redirects "^1.15.6" - form-data "^4.0.4" - proxy-from-env "^1.1.0" - -axios@^1.12.0: - version "1.13.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.13.2.tgz#9ada120b7b5ab24509553ec3e40123521117f687" - integrity sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA== - dependencies: - follow-redirects "^1.15.6" - form-data "^4.0.4" + follow-redirects "^1.15.11" + form-data "^4.0.5" proxy-from-env "^1.1.0" axobject-query@^2.2.0: @@ -10932,9 +10913,9 @@ baseline-browser-mapping@^2.8.25: integrity sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw== basic-ftp@^5.0.2: - version "5.0.5" - resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.0.5.tgz#14a474f5fffecca1f4f406f1c26b18f800225ac0" - integrity sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg== + version "5.2.0" + resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.2.0.tgz#7c2dff63c918bde60e6bad1f2ff93dcf5137a40a" + integrity sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw== batch@0.6.1: version "0.6.1" @@ -11077,14 +11058,14 @@ bmp-js@^0.1.0: integrity sha1-4Fpj95amwf8l9Hcex62twUjAcjM= bn.js@^4.0.0, bn.js@^4.11.9: - version "4.12.0" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" - integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== + version "4.12.3" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.3.tgz#2cc2c679188eb35b006f2d0d4710bed8437a769e" + integrity sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g== bn.js@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" - integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== + version "5.2.3" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.3.tgz#16a9e409616b23fef3ccbedb8d42f13bff80295e" + integrity sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w== body-parser@1.20.3, body-parser@^1.19.0: version "1.20.3" @@ -15160,10 +15141,10 @@ focus-lock@^1.3.5: dependencies: tslib "^2.0.3" -follow-redirects@^1.0.0, follow-redirects@^1.15.3, follow-redirects@^1.15.6: - version "1.15.9" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" - integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== +follow-redirects@^1.0.0, follow-redirects@^1.15.11, follow-redirects@^1.15.3: + version "1.15.11" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340" + integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== for-each@^0.3.3: version "0.3.3" @@ -15211,10 +15192,10 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -form-data@^4.0.0, form-data@^4.0.4, form-data@~4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4" - integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow== +form-data@^4.0.0, form-data@^4.0.5, form-data@~4.0.0: + version "4.0.5" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053" + integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w== dependencies: asynckit "^0.4.0" combined-stream "^1.0.8" @@ -23737,10 +23718,10 @@ rollup-plugin-tsconfig-paths@^1.5.2: dependencies: typescript-paths "^1.5.1" -rollup@2.53.1: - version "2.53.1" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.53.1.tgz#b60439efd1eb41bdb56630509bd99aae78b575d3" - integrity sha512-yiTCvcYXZEulNWNlEONOQVlhXA/hgxjelFSjNcrwAAIfYx/xqjSHwqg/cCaWOyFRKr+IQBaXwt723m8tCaIUiw== +rollup@2.80.0: + version "2.80.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.80.0.tgz#a82efc15b748e986a7c76f0f771221b1fa108a2c" + integrity sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ== optionalDependencies: fsevents "~2.3.2"