Skip to content

Commit 581bdc7

Browse files
committed
tests for cubeSymbols compilation
1 parent 9a8dc4e commit 581bdc7

File tree

2 files changed

+273
-1
lines changed

2 files changed

+273
-1
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { BaseQuery } from '../adapter';
99

1010
import type { ErrorReporter } from './ErrorReporter';
1111

12-
interface CubeDefinition {
12+
export interface CubeDefinition {
1313
name: string;
1414
extends?: string;
1515
measures?: Record<string, any>;
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
import * as process from 'node:process';
2+
import { CubeSymbols, CubeDefinition } from '../../src/compiler/CubeSymbols';
3+
import { ErrorReporter } from '../../src/compiler/ErrorReporter';
4+
5+
class ConsoleErrorReporter extends ErrorReporter {
6+
public error(message: any, _e: any) {
7+
console.log(message);
8+
}
9+
}
10+
11+
/**
12+
* Topological sort in CubeSymbols.compile() should correctly
13+
* order cubes and views in a way that views depend on cubes will be processed after dependencies
14+
*/
15+
const cubeDefs: CubeDefinition[] = [
16+
{
17+
name: 'users_view',
18+
isView: true,
19+
cubes: [
20+
{ join_path: (users) => 'users', includes: '*' },
21+
{ joinPath: () => 'clients', includes: '*' },
22+
]
23+
},
24+
{
25+
name: 'users',
26+
measures: {
27+
count: { type: 'count', sql: () => 'sql' },
28+
sum: { type: 'sum', sql: () => 'sql' },
29+
},
30+
dimensions: {
31+
userId: { type: 'number', sql: () => 'user_id' },
32+
name: { type: 'string', sql: () => 'user_name' },
33+
createdAt: { type: 'time', sql: () => 'created_at' },
34+
},
35+
joins: {
36+
checkins: { relationship: 'hasMany', sql: (CUBE) => `${CUBE}.id = checkins.id` },
37+
clients: { relationship: 'hasMany', sql: (CUBE) => `${CUBE}.id = clients.id` }
38+
},
39+
preAggregations: {
40+
main: {}
41+
}
42+
},
43+
{
44+
name: 'view_with_view_as_cube',
45+
isView: true,
46+
cubes: [
47+
{ join_path: () => 'emails', includes: '*' },
48+
{ joinPath: () => 'users_view', includes: ['userId'] },
49+
]
50+
},
51+
{
52+
name: 'clients',
53+
measures: {
54+
Count: { type: 'count', sql: () => 'sql' },
55+
Sum: { type: 'sum', sql: () => 'sql' },
56+
},
57+
dimensions: {
58+
UserId: { type: 'number', sql: () => 'user_id' },
59+
Name: { type: 'string', sql: () => 'user_name' },
60+
CreatedAt: { type: 'time', sql: () => 'created_at' },
61+
},
62+
},
63+
{
64+
name: 'emails',
65+
measures: {
66+
CountMail: { type: 'count', sql: () => 'sql' },
67+
SumMail: { type: 'sum', sql: () => 'sql' },
68+
},
69+
dimensions: {
70+
mailId: { type: 'number', sql: () => 'user_id' },
71+
Address: { type: 'string', sql: () => 'email' },
72+
MailCreatedAt: { type: 'time', sql: () => 'created_at' },
73+
},
74+
},
75+
];
76+
77+
describe('Cube Symbols Compiler', () => {
78+
it('disallows members of different types with the same name (case sensitive)', () => {
79+
process.env.CUBEJS_CASE_INSENSITIVE_DUPLICATES_CHECK = 'false';
80+
81+
const reporter = new ConsoleErrorReporter();
82+
let compiler = new CubeSymbols();
83+
84+
let cubeDefsTest: CubeDefinition[] = [
85+
{
86+
name: 'users',
87+
measures: {
88+
count: { type: 'count', sql: () => 'sql' },
89+
sum: { type: 'sum', sql: () => 'sql' },
90+
},
91+
dimensions: {
92+
userId: { type: 'number', sql: () => 'user_id' },
93+
Sum: { type: 'string', sql: () => 'user_name' },
94+
createdAt: { type: 'time', sql: () => 'created_at' },
95+
}
96+
}
97+
];
98+
99+
compiler.compile(cubeDefsTest, reporter);
100+
reporter.throwIfAny(); // should not throw in this case
101+
102+
compiler = new CubeSymbols();
103+
cubeDefsTest = [
104+
{
105+
name: 'users',
106+
measures: {
107+
count: { type: 'count', sql: () => 'sql' },
108+
sum: { type: 'sum', sql: () => 'sql' },
109+
},
110+
dimensions: {
111+
userId: { type: 'number', sql: () => 'user_id' },
112+
sum: { type: 'string', sql: () => 'user_name' },
113+
createdAt: { type: 'time', sql: () => 'created_at' },
114+
}
115+
}
116+
];
117+
118+
compiler.compile(cubeDefsTest, reporter);
119+
expect(() => reporter.throwIfAny()).toThrow(/sum defined more than once/);
120+
});
121+
122+
it('disallows members of different types with the same name (case insensitive)', () => {
123+
process.env.CUBEJS_CASE_INSENSITIVE_DUPLICATES_CHECK = 'true';
124+
125+
const reporter = new ConsoleErrorReporter();
126+
const compiler = new CubeSymbols();
127+
128+
const cubeDefsTest: CubeDefinition[] = [
129+
{
130+
name: 'users',
131+
measures: {
132+
count: { type: 'count', sql: () => 'sql' },
133+
sum: { type: 'sum', sql: () => 'sql' },
134+
},
135+
dimensions: {
136+
userId: { type: 'number', sql: () => 'user_id' },
137+
Sum: { type: 'string', sql: () => 'user_name' },
138+
createdAt: { type: 'time', sql: () => 'created_at' },
139+
}
140+
}
141+
];
142+
143+
compiler.compile(cubeDefsTest, reporter);
144+
expect(() => reporter.throwIfAny()).toThrow(/sum defined more than once/);
145+
});
146+
147+
it('compiles correct cubes and views (case sensitive)', () => {
148+
process.env.CUBEJS_CASE_INSENSITIVE_DUPLICATES_CHECK = 'false';
149+
150+
const reporter = new ConsoleErrorReporter();
151+
let compiler = new CubeSymbols();
152+
153+
compiler.compile(cubeDefs, reporter);
154+
reporter.throwIfAny();
155+
156+
// and with compileViews
157+
compiler = new CubeSymbols(true);
158+
compiler.compile(cubeDefs, reporter);
159+
reporter.throwIfAny();
160+
});
161+
162+
it('throws error for duplicates with case insensitive flag', () => {
163+
process.env.CUBEJS_CASE_INSENSITIVE_DUPLICATES_CHECK = 'true';
164+
165+
const reporter = new ConsoleErrorReporter();
166+
let compiler = new CubeSymbols();
167+
168+
compiler.compile(cubeDefs, reporter);
169+
reporter.throwIfAny(); // should not throw at this stage
170+
171+
// and with compileViews
172+
compiler = new CubeSymbols(true);
173+
compiler.compile(cubeDefs, reporter);
174+
expect(() => reporter.throwIfAny()).toThrow(/users_view cube.*conflicts with existing member/);
175+
});
176+
177+
it('throws error for including non-existing member in view\'s cube', () => {
178+
process.env.CUBEJS_CASE_INSENSITIVE_DUPLICATES_CHECK = 'false';
179+
180+
const reporter = new ConsoleErrorReporter();
181+
const compiler = new CubeSymbols(true);
182+
183+
const cubeDefsTest: CubeDefinition[] = [
184+
{
185+
name: 'users',
186+
measures: {
187+
count: { type: 'count', sql: () => 'sql' },
188+
sum: { type: 'sum', sql: () => 'sql' },
189+
},
190+
dimensions: {
191+
userId: { type: 'number', sql: () => 'user_id' },
192+
Sum: { type: 'string', sql: () => 'user_name' },
193+
createdAt: { type: 'time', sql: () => 'created_at' },
194+
}
195+
},
196+
{
197+
name: 'users_view',
198+
isView: true,
199+
cubes: [
200+
{ join_path: (users) => 'users', includes: ['sum', 'non-existent'] },
201+
]
202+
},
203+
];
204+
205+
compiler.compile(cubeDefsTest, reporter);
206+
expect(() => reporter.throwIfAny()).toThrow(/Member 'non-existent' is included in 'users_view' but not defined in any cube/);
207+
});
208+
209+
it('throws error for using paths in view\'s cube includes members', () => {
210+
process.env.CUBEJS_CASE_INSENSITIVE_DUPLICATES_CHECK = 'false';
211+
212+
const reporter = new ConsoleErrorReporter();
213+
const compiler = new CubeSymbols(true);
214+
215+
const cubeDefsTest: CubeDefinition[] = [
216+
{
217+
name: 'users',
218+
measures: {
219+
count: { type: 'count', sql: () => 'sql' },
220+
sum: { type: 'sum', sql: () => 'sql' },
221+
},
222+
dimensions: {
223+
userId: { type: 'number', sql: () => 'user_id' },
224+
Sum: { type: 'string', sql: () => 'user_name' },
225+
createdAt: { type: 'time', sql: () => 'created_at' },
226+
}
227+
},
228+
{
229+
name: 'users_view',
230+
isView: true,
231+
cubes: [
232+
{ join_path: (users) => 'users', includes: ['sum', 'some.other.non-existent'] },
233+
]
234+
},
235+
];
236+
237+
compiler.compile(cubeDefsTest, reporter);
238+
expect(() => reporter.throwIfAny()).toThrow(/Paths aren't allowed in cube includes but 'some.other.non-existent' provided as include member/);
239+
});
240+
241+
it('throws error for using paths in view\'s cube includes members', () => {
242+
process.env.CUBEJS_CASE_INSENSITIVE_DUPLICATES_CHECK = 'false';
243+
244+
const reporter = new ConsoleErrorReporter();
245+
const compiler = new CubeSymbols(true);
246+
247+
const cubeDefsTest: CubeDefinition[] = [
248+
{
249+
name: 'users',
250+
measures: {
251+
count: { type: 'count', sql: () => 'sql' },
252+
sum: { type: 'sum', sql: () => 'sql' },
253+
},
254+
dimensions: {
255+
userId: { type: 'number', sql: () => 'user_id' },
256+
Sum: { type: 'string', sql: () => 'user_name' },
257+
createdAt: { type: 'time', sql: () => 'created_at' },
258+
}
259+
},
260+
{
261+
name: 'users_view',
262+
isView: true,
263+
cubes: [
264+
{ join_path: (users) => 'users', includes: '*', excludes: ['some.other.non-existent'] },
265+
]
266+
},
267+
];
268+
269+
compiler.compile(cubeDefsTest, reporter);
270+
expect(() => reporter.throwIfAny()).toThrow(/Paths aren't allowed in cube excludes but 'some.other.non-existent' provided as exclude member/);
271+
});
272+
});

0 commit comments

Comments
 (0)