Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,6 @@ Also requires the M [deployment tier](/product/administration/pricing#deployment

</InfoBox>

<WarningBox>

The DAX API is currently in preview. Please [contact us](https://cube.dev/contact) to enable it for your account.

</WarningBox>

Read below about the DAX API [configuration](#configuration),
[authentication](#authentication), and [using it](#using-dax-api-with-power-bi) with Power BI.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,6 @@ on configuration and features for specific tools:
/>
</Grid>

<InfoBox>

Previously, Semantic Layer Sync provided a way to connect to [Microsoft Power BI][ref-powerbi].
Currently, it's recommended to use the [DAX API][ref-dax-api].

</InfoBox>

## Creating syncs

You can create a new sync by navigating to <Btn>IDE → Integrations</Btn>
Expand Down Expand Up @@ -381,8 +374,6 @@ on, i.e., your development mode branch, shared branch, or main branch.

[ref-data-model]: /product/data-modeling/overview
[ref-sql-api]: /product/apis-integrations/sql-api
[ref-powerbi]: /product/configuration/visualization-tools/powerbi
[ref-dax-api]: /product/apis-integrations/dax-api
[ref-config-file]: /product/configuration#configuration-options
[ref-config-contexts]: /product/configuration/reference/config#scheduledrefreshcontexts
[ref-config-schemaversion]: /product/configuration/reference/config#schema_version
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,9 @@ Webinar recording: [Instant Insights Unlocked with Cube & Metabase](https://cube

</InfoBox>

<LoomVideo url="https://www.loom.com/embed/38752bb23e5244d1a43675795c470a53" />

## Connect from Cube Cloud

It is recommended to use [Semantic Layer Sync][ref-sls] to connect Cube Cloud to
Metabase. It automatically synchronizes the [data model][ref-data-model] with Metabase.
You can connect Cube Cloud to Metabase using the [SQL API][ref-sql-api].

Navigate to the [Integrations](/product/workspace/integrations#connect-specific-tools)
page, click <Btn>Connect to Cube</Btn>, and choose <Btn>Metabase</Btn> to get
Expand Down Expand Up @@ -82,6 +79,3 @@ well.
[ref-sql-api]: /product/apis-integrations/sql-api
[metabase-oss]: https://github.com/metabase/metabase
[metabase]: https://www.metabase.com
[ref-sls]: /product/apis-integrations/semantic-layer-sync
[ref-sql-api]: /product/apis-integrations/sql-api
[ref-data-model]: /product/data-modeling/overview
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ export class LocalCacheDriver implements CacheDriverInterface {
// Nothing to do
}

public reset(): void {
for (const key of Object.keys(this.store)) {
delete this.store[key];
}
}

public async testConnection(): Promise<void> {
// Nothing to do
}
Expand Down
38 changes: 29 additions & 9 deletions packages/cubejs-query-orchestrator/src/orchestrator/QueryCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ type CacheEntry = {
time: number;
result: any;
renewalKey: string;
requestId?: string;
};

export interface QueryCacheOptions {
Expand Down Expand Up @@ -301,6 +302,7 @@ export class QueryCache {
requestId: queryBody.requestId,
dataSource: queryBody.dataSource,
persistent: queryBody.persistent,
skipRefreshKeyWaitForRenew: true,
}
);
}
Expand Down Expand Up @@ -405,6 +407,12 @@ export class QueryCache {
return key;
}

public static extractRequestUUID(requestId: string): string {
const idx = requestId.lastIndexOf('-span-');

return idx !== -1 ? requestId.substring(0, idx) : requestId;
}

protected static replaceAll(replaceThis, withThis, inThis) {
withThis = withThis.replace(/\$/g, '$$$$');
return inThis.replace(
Expand Down Expand Up @@ -903,7 +911,8 @@ export class QueryCache {
const result = {
time: (new Date()).getTime(),
result: res,
renewalKey
renewalKey,
requestId: options.requestId,
};
return this
.cacheDriver
Expand Down Expand Up @@ -1003,14 +1012,24 @@ export class QueryCache {
primaryQuery,
renewCycle
});
if (
renewalKey && (
!renewalThreshold ||
!parsedResult.time ||
renewedAgo > renewalThreshold * 1000 ||
parsedResult.renewalKey !== renewalKey
)
) {

const isExpired = !renewalThreshold || !parsedResult.time || renewedAgo > renewalThreshold * 1000;
const isKeyMismatch = renewalKey && parsedResult.renewalKey !== renewalKey;
const isSameRequest = options.requestId && parsedResult.requestId &&
QueryCache.extractRequestUUID(parsedResult.requestId) === QueryCache.extractRequestUUID(options.requestId);

// Continue-wait cycle: result was produced by our request,
// refreshKey changed during execution — return cached, refresh in background.
// Skip for renewCycle — it must always fetch fresh data to keep cache up-to-date.
if (isSameRequest && !renewCycle && (isExpired || isKeyMismatch)) {
this.logger('Same request cache hit (background refresh)', { cacheKey, renewalThreshold, requestId: options.requestId, spanId, primaryQuery, renewCycle });
fetchNew().catch(e => {
if (!(e instanceof ContinueWaitError)) {
this.logger('Error renewing', { cacheKey, error: e.stack || e, requestId: options.requestId, spanId, primaryQuery, renewCycle });
}
});
} else if (renewalKey && (isExpired || isKeyMismatch)) {
// Cache expired or refreshKey changed — need to refresh
if (options.waitForRenew) {
this.logger('Waiting for renew', { cacheKey, renewalThreshold, requestId: options.requestId, spanId, primaryQuery, renewCycle });
return fetchNew();
Expand All @@ -1023,6 +1042,7 @@ export class QueryCache {
});
}
}

this.logger('Using cache for', { cacheKey, requestId: options.requestId, spanId, primaryQuery, renewCycle });
if (options.useInMemory && renewedAgo + inMemoryCacheDisablePeriod <= renewalThreshold * 1000) {
this.memoryCache.set(redisKey, parsedResult);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable global-require */
import R from 'ramda';
import {
BUILD_RANGE_END_LOCAL,
BUILD_RANGE_START_LOCAL,
FROM_PARTITION_RANGE,
TO_PARTITION_RANGE
} from '@cubejs-backend/shared';
import { PreAggregationPartitionRangeLoader, PreAggregations, version } from '../../src';
import crypto from 'crypto';

import { PreAggregationPartitionRangeLoader, PreAggregations, QueryCache, LocalCacheDriver, version } from '../../src';

class MockDriver {
public tables: string[] = [];
Expand Down Expand Up @@ -123,34 +123,32 @@ describe('PreAggregations', () => {
let mockExternalDriverFactory: (() => Promise<MockDriver>) | null = null;
let queryCache: any = null;

const basicQuery: any = {
const defaultCacheKeyQuery: [string, any[], Record<string, any>] = ['SELECT date_trunc(\'hour\', (NOW()::timestamptz AT TIME ZONE \'UTC\')) as current_hour', [], {
renewalThreshold: 10,
external: false,
}];

const createBasicQuery = (overrides: Record<string, any> = {}): any => ({
query: 'SELECT "orders__created_at_week" "orders__created_at_week", sum("orders__count") "orders__count" FROM (SELECT * FROM stb_pre_aggregations.orders_number_and_count20191101) as partition_union WHERE ("orders__created_at_week" >= ($1::timestamptz::timestamptz AT TIME ZONE \'UTC\') AND "orders__created_at_week" <= ($2::timestamptz::timestamptz AT TIME ZONE \'UTC\')) GROUP BY 1 ORDER BY 1 ASC LIMIT 10000',
values: ['2019-11-01T00:00:00Z', '2019-11-30T23:59:59Z'],
cacheKeyQueries: {
renewalThreshold: 21600,
queries: [['SELECT date_trunc(\'hour\', (NOW()::timestamptz AT TIME ZONE \'UTC\')) as current_hour', [], {
renewalThreshold: 10,
external: false,
}]]
queries: [defaultCacheKeyQuery]
},
preAggregations: [{
preAggregationsSchema: 'stb_pre_aggregations',
tableName: 'stb_pre_aggregations.orders_number_and_count20191101',
loadSql: ['CREATE TABLE stb_pre_aggregations.orders_number_and_count20191101 AS SELECT\n date_trunc(\'week\', ("orders".created_at::timestamptz AT TIME ZONE \'UTC\')) "orders__created_at_week", count("orders".id) "orders__count", sum("orders".number) "orders__number"\n FROM\n public.orders AS "orders"\n WHERE ("orders".created_at >= $1::timestamptz AND "orders".created_at <= $2::timestamptz) GROUP BY 1', ['2019-11-01T00:00:00Z', '2019-11-30T23:59:59Z']],
invalidateKeyQueries: [['SELECT date_trunc(\'hour\', (NOW()::timestamptz AT TIME ZONE \'UTC\')) as current_hour', [], {
renewalThreshold: 10,
external: false,
}]]
invalidateKeyQueries: [defaultCacheKeyQuery],
}],
requestId: 'basic'
};
requestId: 'basic',
...overrides,
});

const basicQueryExternal = R.clone(basicQuery);
basicQueryExternal.preAggregations[0].external = true;
const basicQueryWithRenew = R.clone(basicQuery);
basicQueryWithRenew.renewQuery = true;
const basicQueryExternalWithRenew = R.clone(basicQueryExternal);
basicQueryExternalWithRenew.renewQuery = true;
const basicQuery: any = createBasicQuery();
const basicQueryExternal = createBasicQuery({ preAggregations: [{ ...basicQuery.preAggregations[0], external: true }] });
const basicQueryWithRenew = createBasicQuery({ renewQuery: true });
const basicQueryExternalWithRenew = createBasicQuery({ preAggregations: [{ ...basicQuery.preAggregations[0], external: true }], renewQuery: true });

beforeEach(() => {
mockDriver = new MockDriver();
Expand All @@ -167,24 +165,22 @@ describe('PreAggregations', () => {
return driver;
};

jest.resetModules();

// Dynamic require after resetModules to ensure fresh module state
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { QueryCache } = require('../../src/orchestrator/QueryCache');
queryCache = new QueryCache(
'TEST',
mockDriverFactory as any,
// eslint-disable-next-line @typescript-eslint/no-empty-function
() => {},
{
cacheAndQueueDriver: 'memory',
queueOptions: () => ({
queueOptions: async () => ({
executionTimeout: 1,
concurrency: 2,
}),
},
);

// Reset the shared in-memory cache store between tests
(queryCache.getCacheDriver() as LocalCacheDriver).reset();
});

describe('loadAllPreAggregationsIfNeeded', () => {
Expand All @@ -198,7 +194,7 @@ describe('PreAggregations', () => {
() => {},
queryCache!,
{
queueOptions: () => ({
queueOptions: async () => ({
executionTimeout: 1,
concurrency: 2,
}),
Expand All @@ -225,7 +221,7 @@ describe('PreAggregations', () => {
() => {},
queryCache!,
{
queueOptions: () => ({
queueOptions: async () => ({
executionTimeout: 1,
concurrency: 2,
}),
Expand All @@ -252,7 +248,7 @@ describe('PreAggregations', () => {
() => {},
queryCache!,
{
queueOptions: () => ({
queueOptions: async () => ({
executionTimeout: 1,
concurrency: 2,
}),
Expand All @@ -279,7 +275,7 @@ describe('PreAggregations', () => {
() => {},
queryCache!,
{
queueOptions: () => ({
queueOptions: async () => ({
executionTimeout: 1,
concurrency: 2,
}),
Expand Down Expand Up @@ -310,7 +306,7 @@ describe('PreAggregations', () => {
() => {},
queryCache!,
{
queueOptions: () => ({
queueOptions: async () => ({
executionTimeout: 1,
concurrency: 2,
}),
Expand Down Expand Up @@ -344,7 +340,7 @@ describe('PreAggregations', () => {
() => {},
queryCache!,
{
queueOptions: () => ({
queueOptions: async () => ({
executionTimeout: 1,
concurrency: 2,
}),
Expand Down Expand Up @@ -395,7 +391,7 @@ describe('PreAggregations', () => {
() => {},
queryCache!,
{
queueOptions: () => ({
queueOptions: async () => ({
executionTimeout: 1,
concurrency: 2,
}),
Expand Down Expand Up @@ -932,8 +928,6 @@ describe('PreAggregations', () => {
// Old implementation (before the unsigned shift fix)
// This would hang on certain inputs, but for inputs that don't trigger the bug,
// it should produce the same results as the new implementation
const crypto = require('crypto');

function oldVersion(cacheKey: any): string | null {
let result = '';

Expand Down
Loading
Loading