Skip to content

Commit 1cfe4c2

Browse files
authored
feat(schema-compiler): Support adding pre-aggregations in Rollup Designer for YAML-based models (#9500)
* prepare schema converter for yaml * add yaml package * fix: do not write all files while you need only one * feat(schema-compiler): Support adding pre-aggregations in Rollup Designer for YAML-based models * process and write only one required file * add basic test for CubeSchemaConverter * test for yaml pre-agg gen * add note * more tests * rename test * fix tests
1 parent 096fb90 commit 1cfe4c2

File tree

8 files changed

+1633
-51
lines changed

8 files changed

+1633
-51
lines changed

packages/cubejs-schema-compiler/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@
5555
"ramda": "^0.27.2",
5656
"syntax-error": "^1.3.0",
5757
"uuid": "^8.3.2",
58-
"workerpool": "^9.2.0"
58+
"workerpool": "^9.2.0",
59+
"yaml": "^2.7.1"
5960
},
6061
"devDependencies": {
6162
"@clickhouse/client": "^1.7.0",

packages/cubejs-schema-compiler/src/compiler/converters/CubePreAggregationConverter.ts

+62-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { parse } from '@babel/parser';
22
import * as t from '@babel/types';
3+
import YAML, { isMap, isScalar, Scalar, YAMLMap, YAMLSeq, Pair, parseDocument } from 'yaml';
34

45
import { UserError } from '../UserError';
56

6-
import { AstByCubeName, CubeConverterInterface } from './CubeSchemaConverter';
7+
import { AstByCubeName, JsSet, CubeConverterInterface, YamlSet } from './CubeSchemaConverter';
78

89
export type PreAggregationDefinition = {
910
cubeName: string;
@@ -15,8 +16,20 @@ export class CubePreAggregationConverter implements CubeConverterInterface {
1516
public constructor(protected preAggregationDefinition: PreAggregationDefinition) {}
1617

1718
public convert(astByCubeName: AstByCubeName): void {
18-
const { cubeName, preAggregationName, code } = this.preAggregationDefinition;
19-
const { cubeDefinition } = astByCubeName[cubeName];
19+
const { cubeName } = this.preAggregationDefinition;
20+
21+
const cubeDefSet = astByCubeName[cubeName];
22+
23+
if ('ast' in cubeDefSet) {
24+
this.convertJS(cubeDefSet);
25+
} else {
26+
this.convertYaml(cubeDefSet);
27+
}
28+
}
29+
30+
protected convertJS(cubeDefSet: JsSet) {
31+
const { preAggregationName, code } = this.preAggregationDefinition;
32+
const { cubeDefinition } = cubeDefSet;
2033

2134
let preAggregationNode: t.ObjectExpression | null = null;
2235
const preAggregationAst = parse(`(${code})`);
@@ -64,4 +77,50 @@ export class CubePreAggregationConverter implements CubeConverterInterface {
6477
);
6578
}
6679
}
80+
81+
protected convertYaml(cubeDefSet: YamlSet) {
82+
const { preAggregationName, code } = this.preAggregationDefinition;
83+
const { cubeDefinition } = cubeDefSet;
84+
85+
const preAggDoc = YAML.parseDocument(code);
86+
const preAggNode = preAggDoc.contents;
87+
88+
if (!preAggNode || !isMap(preAggNode)) {
89+
throw new UserError('Pre-aggregation YAML must be a map/object');
90+
}
91+
92+
const preAggsPair = cubeDefinition.items.find(
93+
(pair: Pair) => isScalar(pair.key) && (pair.key.value === 'pre_aggregations' || pair.key.value === 'preAggregations')
94+
);
95+
96+
if (preAggsPair) {
97+
const seq = preAggsPair.value;
98+
if (!YAML.isSeq(seq)) {
99+
throw new UserError('\'pre_aggregations\' must be a sequence');
100+
}
101+
102+
const exists = seq.items.some(item => {
103+
if (isMap(item)) {
104+
const namePair = item.items.find(
105+
(pair: Pair) => isScalar(pair.key) && pair.key.value === 'name'
106+
);
107+
return namePair && isScalar(namePair.value) && namePair.value.value === preAggregationName;
108+
}
109+
return false;
110+
});
111+
112+
if (exists) {
113+
throw new UserError(`Pre-aggregation '${preAggregationName}' is already defined`);
114+
}
115+
116+
seq.items.push(preAggNode);
117+
} else {
118+
const newSeq = new YAMLSeq();
119+
newSeq.items.push(preAggNode);
120+
121+
cubeDefinition.items.push(
122+
new Pair(new Scalar('pre_aggregations'), newSeq)
123+
);
124+
}
125+
}
67126
}

packages/cubejs-schema-compiler/src/compiler/converters/CubeSchemaConverter.ts

+121-41
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,23 @@ import generator from '@babel/generator';
22
import { parse } from '@babel/parser';
33
import traverse from '@babel/traverse';
44
import * as t from '@babel/types';
5+
import YAML, { isMap, isSeq } from 'yaml';
56

6-
export type AstSet = {
7+
export type JsSet = {
78
fileName: string;
89
ast: t.File;
910
cubeDefinition: t.ObjectExpression;
1011
};
1112

12-
export type AstByCubeName = Record<string, AstSet>;
13+
export type YamlSet = {
14+
fileName: string;
15+
yaml: YAML.Document;
16+
cubeDefinition: YAML.YAMLMap;
17+
};
18+
19+
const JINJA_SYNTAX = /{%|%}|{{|}}/ig;
20+
21+
export type AstByCubeName = Record<string, (JsSet | YamlSet)>;
1322

1423
export interface CubeConverterInterface {
1524
convert(astByCubeName: AstByCubeName): void;
@@ -27,47 +36,112 @@ export class CubeSchemaConverter {
2736

2837
public constructor(protected fileRepository: any, protected converters: CubeConverterInterface[]) {}
2938

30-
protected async prepare(): Promise<void> {
39+
/**
40+
* Parse Schema files from the repository and create a mapping of cube names to schema files.
41+
* If optional `cubeName` parameter is passed - only file with asked cube is parsed and stored.
42+
* @param cubeName
43+
* @protected
44+
*/
45+
protected async prepare(cubeName?: string): Promise<void> {
3146
this.dataSchemaFiles = await this.fileRepository.dataSchemaFiles();
3247

3348
this.dataSchemaFiles.forEach((schemaFile) => {
34-
const ast = this.parse(schemaFile);
35-
36-
traverse(ast, {
37-
CallExpression: (path) => {
38-
if (t.isIdentifier(path.node.callee)) {
39-
const args = path.get('arguments');
40-
41-
if (path.node.callee.name === 'cube') {
42-
if (args?.[args.length - 1]) {
43-
let cubeName: string | undefined;
44-
45-
if (args[0].node.type === 'StringLiteral' && args[0].node.value) {
46-
cubeName = args[0].node.value;
47-
} else if (args[0].node.type === 'TemplateLiteral' && args[0].node.quasis?.[0]?.value.cooked) {
48-
cubeName = args[0].node.quasis?.[0]?.value.cooked;
49-
}
50-
51-
if (cubeName == null) {
52-
throw new Error(`Error parsing ${schemaFile.fileName}`);
53-
}
54-
55-
if (t.isObjectExpression(args[1]?.node) && ast != null) {
56-
this.parsedFiles[cubeName] = {
57-
fileName: schemaFile.fileName,
58-
ast,
59-
cubeDefinition: args[1].node,
60-
};
61-
}
49+
if (schemaFile.fileName.endsWith('.js')) {
50+
this.transformJS(schemaFile, cubeName);
51+
} else if ((schemaFile.fileName.endsWith('.yml') || schemaFile.fileName.endsWith('.yaml')) && !schemaFile.content.match(JINJA_SYNTAX)) {
52+
// Jinja-templated data models are not supported in Rollup Designer yet, so we're ignoring them,
53+
// and if user has chosen the cube from such file - it won't be found during generation.
54+
this.transformYaml(schemaFile, cubeName);
55+
}
56+
});
57+
}
58+
59+
protected transformYaml(schemaFile: SchemaFile, filterCubeName?: string) {
60+
if (!schemaFile.content.trim()) {
61+
return;
62+
}
63+
64+
const yamlDoc = YAML.parseDocument(schemaFile.content);
65+
if (!yamlDoc?.contents) {
66+
return;
67+
}
68+
69+
const root = yamlDoc.contents;
70+
71+
if (!isMap(root)) {
72+
return;
73+
}
74+
75+
const cubesPair = root.items.find((item) => {
76+
const key = item.key as YAML.Scalar;
77+
return key?.value === 'cubes';
78+
});
79+
80+
if (!cubesPair || !isSeq(cubesPair.value)) {
81+
return;
82+
}
83+
84+
for (const cubeNode of cubesPair.value.items) {
85+
if (isMap(cubeNode)) {
86+
const cubeNamePair = cubeNode.items.find((item) => {
87+
const key = item.key as YAML.Scalar;
88+
return key?.value === 'name';
89+
});
90+
91+
const cubeName = (cubeNamePair?.value as YAML.Scalar).value;
92+
93+
if (cubeName && typeof cubeName === 'string' && (!filterCubeName || cubeName === filterCubeName)) {
94+
this.parsedFiles[cubeName] = {
95+
fileName: schemaFile.fileName,
96+
yaml: yamlDoc,
97+
cubeDefinition: cubeNode,
98+
};
99+
100+
if (cubeName === filterCubeName) {
101+
return;
102+
}
103+
}
104+
}
105+
}
106+
}
107+
108+
protected transformJS(schemaFile: SchemaFile, filterCubeName?: string) {
109+
const ast = this.parseJS(schemaFile);
110+
111+
traverse(ast, {
112+
CallExpression: (path) => {
113+
if (t.isIdentifier(path.node.callee)) {
114+
const args = path.get('arguments');
115+
116+
if (path.node.callee.name === 'cube') {
117+
if (args?.[args.length - 1]) {
118+
let cubeName: string | undefined;
119+
120+
if (args[0].node.type === 'StringLiteral' && args[0].node.value) {
121+
cubeName = args[0].node.value;
122+
} else if (args[0].node.type === 'TemplateLiteral' && args[0].node.quasis?.[0]?.value.cooked) {
123+
cubeName = args[0].node.quasis?.[0]?.value.cooked;
124+
}
125+
126+
if (cubeName == null) {
127+
throw new Error(`Error parsing ${schemaFile.fileName}`);
128+
}
129+
130+
if (t.isObjectExpression(args[1]?.node) && ast != null && (!filterCubeName || cubeName === filterCubeName)) {
131+
this.parsedFiles[cubeName] = {
132+
fileName: schemaFile.fileName,
133+
ast,
134+
cubeDefinition: args[1].node,
135+
};
62136
}
63137
}
64138
}
65-
},
66-
});
139+
}
140+
},
67141
});
68142
}
69143

70-
protected parse(file: SchemaFile) {
144+
protected parseJS(file: SchemaFile) {
71145
try {
72146
return parse(file.content, {
73147
sourceFilename: file.fileName,
@@ -86,19 +160,25 @@ export class CubeSchemaConverter {
86160
}
87161
}
88162

89-
public async generate() {
90-
await this.prepare();
163+
public async generate(cubeName?: string) {
164+
await this.prepare(cubeName);
91165

92166
this.converters.forEach((converter) => {
93167
converter.convert(this.parsedFiles);
94168
});
95169
}
96170

97171
public getSourceFiles() {
98-
return Object.entries(this.parsedFiles).map(([cubeName, file]) => ({
99-
cubeName,
100-
fileName: file.fileName,
101-
source: generator(file.ast, {}).code,
102-
}));
172+
return Object.entries(this.parsedFiles).map(([cubeName, file]) => {
173+
const source = 'ast' in file
174+
? generator(file.ast, {}).code
175+
: String(file.yaml);
176+
177+
return {
178+
cubeName,
179+
fileName: file.fileName,
180+
source,
181+
};
182+
});
103183
}
104184
}

0 commit comments

Comments
 (0)