Skip to content

Commit 8c9cddd

Browse files
committed
add topologicalSort to cubes/views during compile cube symbols
1 parent 94d5205 commit 8c9cddd

File tree

3 files changed

+157
-36
lines changed

3 files changed

+157
-36
lines changed

packages/cubejs-backend-shared/src/env.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ const variables: Record<string, (...args: any) => any> = {
233233
transpilationNative: () => get('CUBEJS_TRANSPILATION_NATIVE')
234234
.default('false')
235235
.asBoolStrict(),
236-
caseInsensitiveDuplicateCheck: () => get('CUBEJS_CASE_INSENSITIVE_DUPLICATE_CHECK')
236+
caseInsensitiveDuplicatesCheck: () => get('CUBEJS_CASE_INSENSITIVE_DUPLICATES_CHECK')
237237
.default('false')
238238
.asBoolStrict(),
239239

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

Lines changed: 97 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { camelize } from 'inflection';
44

55
import { UserError } from './UserError';
66
import { DynamicReference } from './DynamicReference';
7-
import { camelizeCube } from './utils';
7+
import { camelizeCube, topologicalSort } from './utils';
88
import { BaseQuery } from '../adapter';
99

1010
import type { ErrorReporter } from './ErrorReporter';
@@ -34,7 +34,7 @@ interface SplitViews {
3434
const FunctionRegex = /function\s+\w+\(([A-Za-z0-9_,]*)|\(([\s\S]*?)\)\s*=>|\(?(\w+)\)?\s*=>/;
3535
export const CONTEXT_SYMBOLS = {
3636
SECURITY_CONTEXT: 'securityContext',
37-
// SECURITY_CONTEXT has been deprecated, however security_context (lowecase)
37+
// SECURITY_CONTEXT has been deprecated, however security_context (lowercase)
3838
// is allowed in RBAC policies for query-time attribute matching
3939
security_context: 'securityContext',
4040
securityContext: 'securityContext',
@@ -45,6 +45,22 @@ export const CONTEXT_SYMBOLS = {
4545

4646
export const CURRENT_CUBE_CONSTANTS = ['CUBE', 'TABLE'];
4747

48+
export type CubeDef = any;
49+
50+
export type GraphNode = {
51+
cubeDef: CubeDef;
52+
name: string;
53+
};
54+
55+
export type ReferenceNode = {
56+
name: string;
57+
};
58+
59+
/**
60+
* Treat it as GraphNode depends on ReferenceNode
61+
*/
62+
export type GraphEdge = [GraphNode, ReferenceNode?];
63+
4864
export class CubeSymbols {
4965
public symbols: Record<string | symbol, any>;
5066

@@ -60,7 +76,9 @@ export class CubeSymbols {
6076

6177
private resolveSymbolsCallContext: any;
6278

63-
private readonly duplicateCheckerFn: (cube: any, memberType: string, memberName: string) => boolean;
79+
private readonly viewDuplicateCheckerFn: (cube: any, memberType: string, memberName: string) => boolean;
80+
81+
private readonly cubeDuplicateNamesCheckerFn: (cube: any) => string[];
6482

6583
public constructor(evaluateViews = false) {
6684
this.symbols = {};
@@ -70,26 +88,22 @@ export class CubeSymbols {
7088
this.cubeList = [];
7189
this.evaluateViews = evaluateViews;
7290

73-
if (getEnv('caseInsensitiveDuplicateCheck')) {
74-
this.duplicateCheckerFn = this.duplicateCheckerCaseInsensitive;
91+
if (getEnv('caseInsensitiveDuplicatesCheck')) {
92+
this.cubeDuplicateNamesCheckerFn = this.cubeDuplicateNamesCheckerCaseInsensitive;
93+
this.viewDuplicateCheckerFn = this.viewDuplicateCheckerCaseInsensitive;
7594
} else {
76-
this.duplicateCheckerFn = this.duplicateCheckerCaseSensitive;
95+
this.cubeDuplicateNamesCheckerFn = this.cubeDuplicateNamesCheckerCaseSensitive;
96+
this.viewDuplicateCheckerFn = this.viewDuplicateCheckerCaseSensitive;
7797
}
7898
}
7999

80100
public compile(cubes: CubeDefinition[], errorReporter: ErrorReporter) {
81-
// @ts-ignore
82-
this.cubeDefinitions = R.pipe(
83-
// @ts-ignore
84-
R.map((c: CubeDefinition) => [c.name, c]),
85-
R.fromPairs
86-
// @ts-ignore
87-
)(cubes);
88-
this.cubeList = cubes.map(c => (c.name ? this.getCubeDefinition(c.name) : this.createCube(c)));
89-
// TODO support actual dependency sorting to allow using views inside views
90-
const sortedByDependency = R.pipe(
91-
R.sortBy((c: CubeDefinition) => !!c.isView),
92-
)(cubes);
101+
this.cubeDefinitions = Object.fromEntries(cubes.map((c) => [c.name, c]));
102+
this.cubeList = cubes.map(c => this.getCubeDefinition(c.name));
103+
104+
// Sorting matters only for views evaluation
105+
const sortedByDependency = this.evaluateViews ? topologicalSort(this.prepareDepsGraph(cubes)) : cubes;
106+
93107
for (const cube of sortedByDependency) {
94108
const splitViews: SplitViews = {};
95109
this.symbols[cube.name] = this.transform(cube.name, errorReporter.inContext(`${cube.name} cube`), splitViews);
@@ -101,6 +115,35 @@ export class CubeSymbols {
101115
}
102116
}
103117

118+
private prepareDepsGraph(cubes: CubeDefinition[]): GraphEdge[] {
119+
const graph = new Map<string, GraphEdge>();
120+
121+
for (const cube of cubes) {
122+
if (!cube.isView) { // Cubes are independent
123+
graph.set(`${cube.name}-none`, [{ cubeDef: cube, name: cube.name }]);
124+
} else {
125+
cube.cubes?.forEach(c => {
126+
const jp = c.joinPath || c.join_path;
127+
if (jp) {
128+
// It's enough to ref the very first level, as everything else will be evaluated on its own
129+
const cubeJoinPath = this.funcArguments(jp)[0]; // View is not camelized yet
130+
graph.set(`${cube.name}-${cubeJoinPath}`, [{ cubeDef: cube, name: cube.name }, { name: cubeJoinPath }]);
131+
}
132+
});
133+
134+
// Legacy-style includes
135+
if (typeof cube.includes === 'function') {
136+
const refs = this.funcArguments(cube.includes);
137+
refs.forEach(ref => {
138+
graph.set(`${cube.name}-${ref}`, [{ cubeDef: cube, name: cube.name }, { name: ref }]);
139+
});
140+
}
141+
}
142+
}
143+
144+
return Array.from(graph.values());
145+
}
146+
104147
public getCubeDefinition(cubeName: string) {
105148
if (!this.builtCubes[cubeName]) {
106149
const cubeDefinition = this.cubeDefinitions[cubeName];
@@ -201,31 +244,50 @@ export class CubeSymbols {
201244
this.prepareIncludes(cube, errorReporter, splitViews);
202245
}
203246

204-
const duplicateNames = R.compose(
205-
R.map((nameToDefinitions: any) => nameToDefinitions[0]),
247+
const duplicateNames = this.cubeDuplicateNamesCheckerFn(cube);
248+
249+
if (duplicateNames.length > 0) {
250+
errorReporter.error(`${duplicateNames.join(', ')} defined more than once`);
251+
}
252+
253+
return {
254+
cubeName: () => cube.name,
255+
cubeObj: () => cube,
256+
...cube.measures || {},
257+
...cube.dimensions || {},
258+
...cube.segments || {},
259+
...cube.preAggregations || {}
260+
};
261+
}
262+
263+
private cubeDuplicateNamesCheckerCaseSensitive(cube: any): string[] {
264+
// @ts-ignore
265+
return R.compose(
266+
R.map(([name]) => name),
206267
R.toPairs,
207268
R.filter((definitionsByName: any) => definitionsByName.length > 1),
208-
R.groupBy((nameToDefinition: any) => nameToDefinition[0]),
269+
R.groupBy(([name]: any) => name),
209270
R.unnest,
210271
R.map(R.toPairs),
211272
// @ts-ignore
212273
R.filter((v: any) => !!v)
213274
// @ts-ignore
214275
)([cube.measures, cube.dimensions, cube.segments, cube.preAggregations, cube.hierarchies]);
276+
}
215277

278+
private cubeDuplicateNamesCheckerCaseInsensitive(cube: any): string[] {
216279
// @ts-ignore
217-
if (duplicateNames.length > 0) {
280+
return R.compose(
281+
R.map(([name]) => name),
282+
R.toPairs,
283+
R.filter((definitionsByName: any) => definitionsByName.length > 1),
284+
R.groupBy(([name]: any) => name.toLowerCase()),
285+
R.unnest,
286+
R.map(R.toPairs),
218287
// @ts-ignore
219-
errorReporter.error(`${duplicateNames.join(', ')} defined more than once`);
220-
}
221-
222-
return Object.assign(
223-
{ cubeName: () => cube.name, cubeObj: () => cube },
224-
cube.measures || {},
225-
cube.dimensions || {},
226-
cube.segments || {},
227-
cube.preAggregations || {}
228-
);
288+
R.filter((v: any) => !!v)
289+
// @ts-ignore
290+
)([cube.measures, cube.dimensions, cube.segments, cube.preAggregations, cube.hierarchies]);
229291
}
230292

231293
private camelCaseTypes(obj: Object) {
@@ -369,19 +431,19 @@ export class CubeSymbols {
369431

370432
protected applyIncludeMembers(includeMembers: any[], cube: CubeDefinition, type: string, errorReporter: ErrorReporter) {
371433
for (const [memberName, memberDefinition] of includeMembers) {
372-
if (this.duplicateCheckerFn(cube, type, memberName)) {
434+
if (this.viewDuplicateCheckerFn(cube, type, memberName)) {
373435
errorReporter.error(`Included member '${memberName}' conflicts with existing member of '${cube.name}'. Please consider excluding this member or assigning it an alias.`);
374436
} else if (type !== 'hierarchies') {
375437
cube[type][memberName] = memberDefinition;
376438
}
377439
}
378440
}
379441

380-
private duplicateCheckerCaseSensitive(cube: any, memberType: string, memberName: string): boolean {
442+
private viewDuplicateCheckerCaseSensitive(cube: any, memberType: string, memberName: string): boolean {
381443
return cube[memberType][memberName];
382444
}
383445

384-
private duplicateCheckerCaseInsensitive(cube: any, memberType: string, memberName: string): boolean {
446+
private viewDuplicateCheckerCaseInsensitive(cube: any, memberType: string, memberName: string): boolean {
385447
return Object.keys(cube[memberType]).map(v => v.toLowerCase()).includes(memberName.toLowerCase());
386448
}
387449

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { camelize } from 'inflection';
2+
import { CubeDef, GraphEdge } from './CubeSymbols';
23

34
// It's a map where key - is a level and value - is a map of properties on this level to ignore camelization
45
const IGNORE_CAMELIZE = {
@@ -52,3 +53,61 @@ export function camelizeCube(cube: any): unknown {
5253

5354
return cube;
5455
}
56+
57+
export function topologicalSort(edges: GraphEdge[]): CubeDef[] {
58+
const graph = new Map();
59+
const outDegree = new Map();
60+
61+
for (const [from, to] of edges) {
62+
if (!graph.has(from.name)) {
63+
graph.set(from.name, { cubeDef: from.cubeDef, from: [] });
64+
outDegree.set(from.name, 0);
65+
} else {
66+
const n = graph.get(from.name);
67+
if (!n.cubeDef) {
68+
n.cubeDef = from.cubeDef;
69+
}
70+
}
71+
72+
if (to) {
73+
if (!graph.has(to.name)) {
74+
graph.set(to.name, { from: [from.name] });
75+
outDegree.set(to.name, 0);
76+
} else {
77+
const n = graph.get(to.name);
78+
n.from.push(from.name);
79+
}
80+
81+
outDegree.set(from.name, (outDegree.get(from.name) || 0) + 1);
82+
}
83+
}
84+
85+
const queue: string[] = [...outDegree.entries()].filter(([_, deg]) => deg === 0).map(([name]) => name);
86+
87+
const sorted: CubeDef[] = [];
88+
89+
while (queue.length) {
90+
const nodeName = queue.shift();
91+
if (nodeName === undefined) {
92+
break;
93+
}
94+
95+
const node = graph.get(nodeName);
96+
97+
sorted.push(node.cubeDef);
98+
99+
for (const neighbor of node.from) {
100+
outDegree.set(neighbor, outDegree.get(neighbor) - 1);
101+
if (outDegree.get(neighbor) === 0) {
102+
queue.push(neighbor);
103+
}
104+
}
105+
}
106+
107+
if (sorted.length !== graph.size) {
108+
const remainingNodes = [...graph.keys()].filter(node => !sorted.includes(node));
109+
throw new Error(`Cyclical dependence detected! Potential problems with ${remainingNodes.join(', ')}.`);
110+
}
111+
112+
return sorted;
113+
}

0 commit comments

Comments
 (0)