Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
43a0e67
Update resolveSymbol & cubeReferenceProxy to support deep properties …
KSDaemon Sep 2, 2024
5735e49
Update evaluateSymbolSql to support deep properties resolution (proxi…
KSDaemon Sep 2, 2024
edf7101
add unit tests for proxied time dimension granularities
KSDaemon Sep 2, 2024
984acef
refactor: remove cubePropertyReferenceProxy and move required logic p…
KSDaemon Sep 6, 2024
fbcf746
fix func types in CubeEvaluator
KSDaemon Sep 9, 2024
43e089a
Add support for proxying predefined granularities
KSDaemon Sep 9, 2024
50e72f0
Add tests for proxying predefined granularities
KSDaemon Sep 9, 2024
9a0e163
fix proxying granularities, referenced from different cubes
KSDaemon Sep 9, 2024
95ff61e
add tests for proxying granularities, referenced from different cubes
KSDaemon Sep 9, 2024
df38e8b
Add check for undefined during custom granularities tests
KSDaemon Sep 9, 2024
4b8e0a6
fix proxying items, referenced from different cubes/views
KSDaemon Sep 9, 2024
dcac470
rename internalPropertyName -> subPropertyName in evaluateSymbolSql
KSDaemon Sep 13, 2024
8eb7ab9
fix after merge/rebase
KSDaemon Sep 13, 2024
9a79d94
Add comments in evaluateSymbolSql
KSDaemon Sep 13, 2024
1dbd2c6
create resolveSubProperty for unified way of subproperty resolving
KSDaemon Sep 13, 2024
39f4a91
fix naming
KSDaemon Sep 13, 2024
288f445
Add usefull comments
KSDaemon Sep 13, 2024
7691704
Add usefull comments
KSDaemon Sep 13, 2024
7515861
implement proxied time dim granularity evaluation within the BaseQuer…
KSDaemon Sep 13, 2024
d7e990c
lint fix
KSDaemon Sep 13, 2024
bd449d8
refactoring in resolveGranularity flow
KSDaemon Sep 14, 2024
8db5799
reverted back to dimensionSql()
KSDaemon Sep 15, 2024
ec955a2
fix populating granularities in the views
KSDaemon Sep 15, 2024
9ce2f5e
add unit tests for custom granularities in views
KSDaemon Sep 15, 2024
dadb108
add integration tests for proxied granularity
KSDaemon Sep 15, 2024
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
22 changes: 20 additions & 2 deletions packages/cubejs-schema-compiler/src/adapter/BaseQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -2191,12 +2191,20 @@ export class BaseQuery {
return this.evaluateSymbolContext || {};
}

evaluateSymbolSql(cubeName, name, symbol, memberExpressionType) {
evaluateSymbolSql(cubeName, name, symbol, memberExpressionType, subPropertyName) {
const isMemberExpr = !!memberExpressionType;
if (!memberExpressionType) {
this.pushMemberNameForCollectionIfNecessary(cubeName, name);
}
const memberPathArray = [cubeName, name];
// Member path needs to be expanded to granularity if subPropertyName is provided.
// Without this: infinite recursion with maximum call stack size exceeded.
// During resolving within dimensionSql() the same symbol is pushed into the stack.
// This would not be needed when the subProperty evaluation will be here and no
// call to dimensionSql().
if (subPropertyName && symbol.type === 'time') {
memberPathArray.push('granularities', subPropertyName);
}
const memberPath = this.cubeEvaluator.pathFromArray(memberPathArray);
let type = memberExpressionType;
if (!type) {
Expand Down Expand Up @@ -2301,8 +2309,18 @@ export class BaseQuery {
'\',\'',
this.autoPrefixAndEvaluateSql(cubeName, symbol.longitude.sql, isMemberExpr)
]);
} else if (symbol.type === 'time' && subPropertyName) {
// TODO: Beware! memberExpression && shiftInterval are not supported with the current implementation.
// Ideally this should be implemented (at least partially) here + inside cube symbol evaluation logic.
// As now `dimensionSql()` is recursively calling `evaluateSymbolSql()` which is not good.
const td = this.newTimeDimension({
dimension: this.cubeEvaluator.pathFromArray([cubeName, name]),
granularity: subPropertyName
});
return td.dimensionSql();
} else {
let res = this.autoPrefixAndEvaluateSql(cubeName, symbol.sql, isMemberExpr);

if (symbol.shiftInterval) {
res = `(${this.addTimestampInterval(res, symbol.shiftInterval)})`;
}
Expand Down Expand Up @@ -2364,7 +2382,7 @@ export class BaseQuery {
}
return self.evaluateSymbolSql(nextCubeName, name, resolvedSymbol);
}, {
sqlResolveFn: options.sqlResolveFn || ((symbol, cube, n) => self.evaluateSymbolSql(cube, n, symbol)),
sqlResolveFn: options.sqlResolveFn || ((symbol, cube, propName, subPropName) => self.evaluateSymbolSql(cube, propName, symbol, false, subPropName)),
cubeAliasFn: self.cubeAlias.bind(self),
contextSymbols: this.parametrizedContextSymbols(),
query: this,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ export class Granularity {
const customGranularity = this.query.cacheValue(
['customGranularity', timeDimension.dimension, this.granularity],
() => query.cubeEvaluator
.byPath('dimensions', timeDimension.dimension)
.granularities?.[this.granularity]
.resolveGranularity([...query.cubeEvaluator.parsePath('dimensions', timeDimension.dimension), 'granularities', this.granularity])
);

if (!customGranularity) {
Expand Down
16 changes: 8 additions & 8 deletions packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export class CubeEvaluator extends CubeSymbols {
for (const cube of validCubes) {
this.evaluatedCubes[cube.name] = this.prepareCube(cube, errorReporter);
}

this.byFileName = R.groupBy(v => v.fileName, validCubes);
this.primaryKeys = R.fromPairs(
validCubes.map((v) => {
Expand Down Expand Up @@ -128,21 +128,21 @@ export class CubeEvaluator extends CubeSymbols {
if (cube.isView && (cube.includedMembers || []).length) {
const includedCubeNames: string[] = R.uniq(cube.includedMembers.map(it => it.memberPath.split('.')[0]));
const includedMemberPaths: string[] = R.uniq(cube.includedMembers.map(it => it.memberPath));

if (!cube.hierarchies) {
for (const cubeName of includedCubeNames) {
const { hierarchies } = this.evaluatedCubes[cubeName] || {};

if (Array.isArray(hierarchies) && hierarchies.length) {
const filteredHierarchies = hierarchies.map(it => {
const levels = it.levels.filter(level => includedMemberPaths.includes(level));

return {
...it,
levels
};
}).filter(it => it.levels.length);

cube.hierarchies = [...(cube.hierarchies || []), ...filteredHierarchies];
}
}
Expand Down Expand Up @@ -420,15 +420,15 @@ export class CubeEvaluator extends CubeSymbols {
return Object.keys(this.evaluatedCubes);
}

public isMeasure(measurePath: string): boolean {
public isMeasure(measurePath: string | string[]): boolean {
return this.isInstanceOfType('measures', measurePath);
}

public isDimension(path: string): boolean {
public isDimension(path: string | string[]): boolean {
return this.isInstanceOfType('dimensions', path);
}

public isSegment(path: string): boolean {
public isSegment(path: string | string[]): boolean {
return this.isInstanceOfType('segments', path);
}

Expand Down
74 changes: 60 additions & 14 deletions packages/cubejs-schema-compiler/src/compiler/CubeSymbols.js
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ export class CubeSymbols {
};
} else if (type === 'dimensions') {
memberDefinition = {
...(resolvedMember.granularities ? { granularities: resolvedMember.granularities } : {}),
sql,
type: resolvedMember.type,
meta: resolvedMember.meta,
Expand Down Expand Up @@ -507,25 +508,42 @@ export class CubeSymbols {
return symbol;
}

let cube = this.isCurrentCube(name) && this.symbols[cubeName] || this.symbols[name];
if (sqlResolveFn && cube) {
cube = this.cubeReferenceProxy(
this.isCurrentCube(name) ? cubeName : name,
collectJoinHints ? [] : undefined
);
// In proxied subProperty flow `name` will be set to parent dimension|measure name,
// so there will be no cube = this.symbols[cubeName : name] found, but potentially
// during cube definition evaluation some other deeper subProperty may be requested.
// To distinguish such cases we pass the right now requested property name to
// cubeReferenceProxy, so later if subProperty is requested we'll have all the required
// information to construct the response.
let cube = this.symbols[this.isCurrentCube(name) ? cubeName : name];
if (sqlResolveFn) {
if (cube) {
cube = this.cubeReferenceProxy(
this.isCurrentCube(name) ? cubeName : name,
collectJoinHints ? [] : undefined
);
} else if (this.symbols[cubeName]?.[name]) {
cube = this.cubeReferenceProxy(
cubeName,
undefined,
name
);
}
}

return cube || (this.symbols[cubeName] && this.symbols[cubeName][name]);
}

cubeReferenceProxy(cubeName, joinHints) {
cubeReferenceProxy(cubeName, joinHints, refProperty) {
if (joinHints) {
joinHints = joinHints.concat(cubeName);
}
const self = this;
const { sqlResolveFn, cubeAliasFn, query, cubeReferencesUsed } = self.resolveSymbolsCallContext || {};
return new Proxy({}, {
get: (v, propertyName) => {
if (propertyName === '_objectWithResolvedProperties') {
return true;
}
if (propertyName === '__cubeName') {
return cubeName;
}
Expand All @@ -538,6 +556,13 @@ export class CubeSymbols {
return undefined;
}
if (propertyName === 'toString') {
if (refProperty) {
return () => this.withSymbolsCallContext(
() => sqlResolveFn(cube[refProperty], cubeName, refProperty),
{ ...this.resolveSymbolsCallContext, joinHints }
);
}

return () => {
if (query) {
query.pushCubeNameForCollectionIfNecessary(cube.cubeName());
Expand All @@ -555,28 +580,49 @@ export class CubeSymbols {
if (propertyName === 'sql') {
return () => query.cubeSql(cube.cubeName());
}
if (propertyName === '_objectWithResolvedProperties') {
return true;
}
if (cube[propertyName]) {
if (refProperty &&
cube[refProperty].type === 'time' &&
self.resolveGranularity([cubeName, refProperty, 'granularities', propertyName], cube)
) {
return {
toString: () => this.withSymbolsCallContext(
() => sqlResolveFn(cube[propertyName], cubeName, propertyName),
{ ...this.resolveSymbolsCallContext, joinHints },
() => sqlResolveFn(cube[refProperty], cubeName, refProperty, propertyName),
{ ...this.resolveSymbolsCallContext },
),
};
}
if (cube[propertyName]) {
return this.cubeReferenceProxy(cubeName, joinHints, propertyName);
}
if (self.symbols[propertyName]) {
return this.cubeReferenceProxy(propertyName, joinHints);
}
if (typeof propertyName === 'string') {
throw new UserError(`${cubeName}.${propertyName} cannot be resolved. There's no such member or cube.`);
throw new UserError(`${cubeName}${refProperty ? `.${refProperty}` : ''}.${propertyName} cannot be resolved. There's no such member or cube.`);
}
return undefined;
}
});
}

/**
* Tries to resolve Granularity object.
* For predefined granularity it constructs it on the fly.
* @param {string|string[]} path
* @param [refCube] Optional cube object to operate on
*/
resolveGranularity(path, refCube) {
const [cubeName, dimName, gr, granName] = Array.isArray(path) ? path : path.split('.');
const cube = refCube || this.symbols[cubeName];

// Predefined granularity
if (typeof granName === 'string' && /^(second|minute|hour|day|week|month|quarter|year)$/i.test(granName)) {
return { interval: `1 ${granName}` };
}

return cube && cube[dimName] && cube[dimName][gr] && cube[dimName][gr][granName];
}

isCurrentCube(name) {
return CURRENT_CUBE_CONSTANTS.indexOf(name) >= 0;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ describe('Custom Granularities', () => {
sql: status
type: string

- name: createdAtHalfYear
sql: "{createdAt.half_year}"
type: string

- name: createdAt
sql: created_at
type: time
Expand Down Expand Up @@ -72,6 +76,12 @@ describe('Custom Granularities', () => {
type: count
rolling_window:
trailing: unbounded

views:
- name: orders_view
cubes:
- join_path: orders
includes: "*"
`);

it('works with half_year custom granularity w/o dimensions query', async () => dbRunner.runQueryTest(
Expand Down Expand Up @@ -111,6 +121,115 @@ describe('Custom Granularities', () => {
{ joinGraph, cubeEvaluator, compiler }
));

it('works with proxied createdAtHalfYear custom granularity as dimension query', async () => dbRunner.runQueryTest(
{
measures: ['orders.count'],
timeDimensions: [{
dimension: 'orders.createdAt',
dateRange: ['2024-01-01', '2025-12-31']
}],
dimensions: ['orders.createdAtHalfYear'],
filters: [],
timezone: 'Europe/London'
},
[
{
orders__count: '13',
orders__created_at_half_year: '2024-01-01T00:00:00.000Z',
},
{
orders__count: '13',
orders__created_at_half_year: '2024-07-01T00:00:00.000Z',
},
{
orders__count: '13',
orders__created_at_half_year: '2025-01-01T00:00:00.000Z',
},
{
orders__count: '13',
orders__created_at_half_year: '2025-07-01T00:00:00.000Z',
},
{
orders__count: '1',
orders__created_at_half_year: '2026-01-01T00:00:00.000Z',
},
],
{ joinGraph, cubeEvaluator, compiler }
));

it('works with half_year custom granularity w/o dimensions querying view', async () => dbRunner.runQueryTest(
{
measures: ['orders_view.count'],
timeDimensions: [{
dimension: 'orders_view.createdAt',
granularity: 'half_year',
dateRange: ['2024-01-01', '2025-12-31']
}],
dimensions: [],
filters: [],
timezone: 'Europe/London'
},
[
{
orders_view__count: '13',
orders_view__created_at_half_year: '2024-01-01T00:00:00.000Z',
},
{
orders_view__count: '13',
orders_view__created_at_half_year: '2024-07-01T00:00:00.000Z',
},
{
orders_view__count: '13',
orders_view__created_at_half_year: '2025-01-01T00:00:00.000Z',
},
{
orders_view__count: '13',
orders_view__created_at_half_year: '2025-07-01T00:00:00.000Z',
},
{
orders_view__count: '1',
orders_view__created_at_half_year: '2026-01-01T00:00:00.000Z',
},
],
{ joinGraph, cubeEvaluator, compiler }
));

it('works with proxied createdAtHalfYear custom granularity as dimension querying view', async () => dbRunner.runQueryTest(
{
measures: ['orders_view.count'],
timeDimensions: [{
dimension: 'orders_view.createdAt',
dateRange: ['2024-01-01', '2025-12-31']
}],
dimensions: ['orders_view.createdAtHalfYear'],
filters: [],
timezone: 'Europe/London'
},
[
{
orders_view__count: '13',
orders_view__created_at_half_year: '2024-01-01T00:00:00.000Z',
},
{
orders_view__count: '13',
orders_view__created_at_half_year: '2024-07-01T00:00:00.000Z',
},
{
orders_view__count: '13',
orders_view__created_at_half_year: '2025-01-01T00:00:00.000Z',
},
{
orders_view__count: '13',
orders_view__created_at_half_year: '2025-07-01T00:00:00.000Z',
},
{
orders_view__count: '1',
orders_view__created_at_half_year: '2026-01-01T00:00:00.000Z',
},
],
{ joinGraph, cubeEvaluator, compiler }
));

it('works with half_year_by_1st_april custom granularity w/o dimensions query', async () => dbRunner.runQueryTest(
{
measures: ['orders.count'],
Expand Down
Loading
Loading