Skip to content

Commit df8161c

Browse files
authored
feat: Allow to specify key for dimensions in schema (#10270)
1 parent 930f98b commit df8161c

File tree

10 files changed

+424
-3
lines changed

10 files changed

+424
-3
lines changed

packages/cubejs-api-gateway/openspec.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ components:
147147
$ref: "#/components/schemas/V1CubeMetaFormat"
148148
order:
149149
$ref: "#/components/schemas/V1CubeMetaDimensionOrder"
150+
key:
151+
type: "string"
152+
description: "Key reference for the dimension"
150153
V1CubeMetaDimensionOrder:
151154
type: "string"
152155
enum:

packages/cubejs-client-core/src/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import Meta from './Meta';
22
import { TimeDimensionGranularity } from './time';
3-
import { TransportOptions } from './HttpTransport';
43

54
export type QueryOrder = 'asc' | 'desc' | 'none';
65

@@ -27,7 +26,7 @@ export type Annotation = {
2726
shortTitle: string;
2827
type: string;
2928
meta?: any;
30-
format?: DimensionFormat;
29+
format?: DimensionFormat | MeasureFormat;
3130
drillMembers?: any[];
3231
drillMembersGrouped?: any;
3332
granularity?: GranularityAnnotation;
@@ -396,6 +395,7 @@ export type BaseCubeDimension = BaseCubeMember & {
396395
primaryKey?: boolean;
397396
suggestFilterValues: boolean;
398397
format?: DimensionFormat;
398+
key?: string;
399399
};
400400

401401
export type CubeTimeDimension = BaseCubeDimension &

packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ export type DimensionDefinition = {
3737
multiStage?: boolean;
3838
shiftInterval?: string;
3939
order?: 'asc' | 'desc';
40+
key?: (...args: any[]) => ToString;
41+
keyReference?: string;
4042
};
4143

4244
export type TimeShiftDefinition = {

packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export type CubeSymbolDefinition = {
3636
timeShift?: TimeshiftDefinition[];
3737
format?: string;
3838
order?: 'asc' | 'desc';
39+
key?: (...args: any[]) => ToString;
40+
keyReference?: string;
3941
};
4042

4143
export type HierarchyDefinition = {
@@ -275,9 +277,16 @@ export class CubeSymbols implements TranspilerSymbolResolver, CompilerInterface
275277
const sortedByDependency = R.pipe(
276278
R.sortBy((c: CubeDefinition) => !!c.isView),
277279
)(cubes);
280+
278281
for (const cube of sortedByDependency) {
279282
const splitViews: SplitViews = {};
283+
280284
this.symbols[cube.name] = this.transform(cube.name, errorReporter.inContext(`${cube.name} cube`), splitViews);
285+
286+
if (!cube.isView) {
287+
this.evaluateDimensionKeys(this.getCubeDefinition(cube.name), errorReporter.inContext(`${cube.name} cube`));
288+
}
289+
281290
for (const viewName of Object.keys(splitViews)) {
282291
// TODO can we define it when cubeList is defined?
283292
this.cubeList.push(splitViews[viewName]);
@@ -542,6 +551,45 @@ export class CubeSymbols implements TranspilerSymbolResolver, CompilerInterface
542551
});
543552
}
544553

554+
private evaluateDimensionKeys(cube: CubeDefinition, errorReporter: ErrorReporter) {
555+
const dimensions = cube.dimensions || {};
556+
557+
// eslint-disable-next-line no-restricted-syntax
558+
for (const [dimensionName, dimension] of Object.entries(dimensions)) {
559+
if (dimension.key) {
560+
const keyReference = this.evaluateReference(
561+
cube.name,
562+
dimension.key,
563+
`Dimension '${cube.name}.${dimensionName}' key`
564+
);
565+
566+
const [refCubeName, refDimensionName] = keyReference.split('.');
567+
568+
if (refCubeName !== cube.name) {
569+
errorReporter.error(
570+
`Dimension '${cube.name}.${dimensionName}' has a key that references dimension '${keyReference}' ` +
571+
'from a different cube. Key must reference a dimension within the same cube.'
572+
);
573+
} else if (!dimensions[refDimensionName]) {
574+
errorReporter.error(
575+
`Dimension '${cube.name}.${dimensionName}' references key dimension '${refDimensionName}' ` +
576+
`which does not exist in cube '${cube.name}'.`
577+
);
578+
} else {
579+
const referencedDimension = dimensions[refDimensionName];
580+
if (referencedDimension.key) {
581+
errorReporter.error(
582+
`Dimension '${cube.name}.${dimensionName}' references '${keyReference}' as its key, ` +
583+
`but '${keyReference}' already defines its own key. Nested keys are not allowed.`
584+
);
585+
} else {
586+
dimension.keyReference = keyReference;
587+
}
588+
}
589+
}
590+
}
591+
}
592+
545593
protected transformPreAggregations(preAggregations: Object) {
546594
// eslint-disable-next-line no-restricted-syntax
547595
for (const preAggregation of Object.values(preAggregations)) {
@@ -836,6 +884,23 @@ export class CubeSymbols implements TranspilerSymbolResolver, CompilerInterface
836884
return this.symbols[cubeName]?.cubeObj()?.[type]?.[memberName];
837885
}
838886

887+
protected processKeyReferenceForView(
888+
keyReference: string,
889+
viewName: string,
890+
viewAllMembers: ViewResolvedMember[],
891+
dimensionName: string
892+
): { keyReference: string } {
893+
const viewKeyMember = viewAllMembers.find(v => v.member === keyReference);
894+
895+
if (!viewKeyMember) {
896+
throw new UserError(
897+
`Dimension '${dimensionName}' has key '${keyReference}' but the key dimension is not included in view '${viewName}'`
898+
);
899+
}
900+
901+
return { keyReference: `${viewName}.${viewKeyMember.name}` };
902+
}
903+
839904
protected generateIncludeMembers(members: any[], type: string, targetCube: CubeDefinitionExtended, viewAllMembers: ViewResolvedMember[]) {
840905
return members.map(memberRef => {
841906
const path = memberRef.member.split('.');
@@ -900,6 +965,7 @@ export class CubeSymbols implements TranspilerSymbolResolver, CompilerInterface
900965
format: memberRef.override?.format || resolvedMember.format,
901966
...(resolvedMember.granularities ? { granularities: resolvedMember.granularities } : {}),
902967
...(resolvedMember.multiStage && { multiStage: resolvedMember.multiStage }),
968+
...(resolvedMember.keyReference && this.processKeyReferenceForView(resolvedMember.keyReference, targetCube.name, viewAllMembers, memberRef.member)),
903969
};
904970
} else if (type === 'segments') {
905971
memberDefinition = {
@@ -994,6 +1060,19 @@ export class CubeSymbols implements TranspilerSymbolResolver, CompilerInterface
9941060
return options.originalSorting ? references : R.sortBy(R.identity, references) as any;
9951061
}
9961062

1063+
public evaluateReference(
1064+
cube: string,
1065+
referencesFn: (...args: Array<unknown>) => ToString,
1066+
context: string
1067+
): string {
1068+
const result = this.evaluateReferences(cube, referencesFn);
1069+
if (Array.isArray(result)) {
1070+
throw new UserError(`${context} must be a single reference, not an array`);
1071+
}
1072+
1073+
return result;
1074+
}
1075+
9971076
public pathFromArray(array: string[]): string {
9981077
return array.join('.');
9991078
}

packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export interface ExtendedCubeSymbolDefinition extends CubeSymbolDefinition {
3939
drillMemberReferences?: any;
4040
cumulative?: boolean;
4141
aggType?: string;
42+
keyReference?: string;
4243
}
4344

4445
interface ExtendedCubeDefinition extends CubeDefinitionExtended {
@@ -98,6 +99,7 @@ export type DimensionConfig = {
9899
aliasMember?: string;
99100
granularities?: GranularityDefinition[];
100101
order?: 'asc' | 'desc';
102+
key?: string;
101103
};
102104

103105
export type SegmentConfig = {
@@ -276,6 +278,7 @@ export class CubeToMetaTransformer implements CompilerInterface {
276278
}))
277279
: undefined,
278280
order: extendedDimDef.order,
281+
key: extendedDimDef.keyReference,
279282
};
280283
}),
281284
segments: Object.entries(extendedCube.segments || {}).map((nameToSegment: [string, any]) => {

packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,8 @@ const BaseDimensionWithoutSubQuery = {
279279
}),
280280
meta: Joi.any(),
281281
order: Joi.string().valid('asc', 'desc'),
282+
key: Joi.func(),
283+
keyReference: Joi.string(),
282284
values: Joi.when('type', {
283285
is: 'switch',
284286
then: Joi.array().items(Joi.string()),

packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export const transpiledFieldsPatterns: Array<RegExp> = [
2020
/^measures\.[_a-zA-Z][_a-zA-Z0-9]*\.(timeShift|time_shift)\.[0-9]+\.(timeDimension|time_dimension)$/,
2121
/^measures\.[_a-zA-Z][_a-zA-Z0-9]*\.(reduceBy|reduce_by|groupBy|group_by|addGroupBy|add_group_by)$/,
2222
/^(measures|dimensions)\.[_a-zA-Z][_a-zA-Z0-9]*\.case\.switch$/,
23-
/^dimensions\.[_a-zA-Z][_a-zA-Z0-9]*\.(reduceBy|reduce_by|groupBy|group_by|addGroupBy|add_group_by)$/,
23+
/^dimensions\.[_a-zA-Z][_a-zA-Z0-9]*\.(reduceBy|reduce_by|groupBy|group_by|addGroupBy|add_group_by|key)$/,
2424
/^(preAggregations|pre_aggregations)\.[_a-zA-Z][_a-zA-Z0-9]*\.indexes\.[_a-zA-Z][_a-zA-Z0-9]*\.columns$/,
2525
/^(preAggregations|pre_aggregations)\.[_a-zA-Z][_a-zA-Z0-9]*\.(timeDimensionReference|timeDimension|time_dimension|segments|dimensions|measures|rollups|segmentReferences|dimensionReferences|measureReferences|rollupReferences)$/,
2626
/^(preAggregations|pre_aggregations)\.[_a-zA-Z][_a-zA-Z0-9]*\.(timeDimensions|time_dimensions)\.\d+\.dimension$/,

packages/cubejs-schema-compiler/test/unit/__snapshots__/views.test.ts.snap

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Object {
1111
"format": "imageUrl",
1212
"granularities": undefined,
1313
"isVisible": true,
14+
"key": undefined,
1415
"meta": Object {
1516
"key": "Meta.key for CubeA.id",
1617
},
@@ -29,6 +30,7 @@ Object {
2930
"format": "imageUrl",
3031
"granularities": undefined,
3132
"isVisible": true,
33+
"key": undefined,
3234
"meta": Object {
3335
"key": "Meta.key for CubeB.other_id",
3436
},
@@ -112,6 +114,7 @@ Object {
112114
"format": "imageUrl",
113115
"granularities": undefined,
114116
"isVisible": true,
117+
"key": undefined,
115118
"meta": Object {
116119
"key": "Meta.key for CubeB.other_id",
117120
},
@@ -195,6 +198,7 @@ Object {
195198
"format": "imageUrl",
196199
"granularities": undefined,
197200
"isVisible": true,
201+
"key": undefined,
198202
"meta": Object {
199203
"key": "Meta.key for CubeA.id",
200204
},
@@ -213,6 +217,7 @@ Object {
213217
"format": "imageUrl",
214218
"granularities": undefined,
215219
"isVisible": true,
220+
"key": undefined,
216221
"meta": Object {
217222
"key": "Meta.key for CubeB.id",
218223
},
@@ -231,6 +236,7 @@ Object {
231236
"format": "imageUrl",
232237
"granularities": undefined,
233238
"isVisible": true,
239+
"key": undefined,
234240
"meta": Object {
235241
"key": "Meta.key for CubeB.other_id",
236242
},
@@ -314,6 +320,7 @@ Object {
314320
"format": "imageUrl",
315321
"granularities": undefined,
316322
"isVisible": true,
323+
"key": undefined,
317324
"meta": Object {
318325
"key": "Meta.key for CubeB.other_id",
319326
},

0 commit comments

Comments
 (0)