Skip to content

Commit c73b368

Browse files
authored
feat(schema-compiler): Introduce support for sqlTable (syntactic sugar) (#6360)
1 parent 4731f4a commit c73b368

File tree

12 files changed

+196
-42
lines changed

12 files changed

+196
-42
lines changed

packages/cubejs-schema-compiler/src/adapter/BaseQuery.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1418,11 +1418,18 @@ class BaseQuery {
14181418
}
14191419
return this.preAggregations.originalSqlPreAggregationTable(foundPreAggregation);
14201420
}
1421-
const evaluatedSql = this.evaluateSql(cube, this.cubeEvaluator.cubeFromPath(cube).sql);
1421+
1422+
const fromPath = this.cubeEvaluator.cubeFromPath(cube);
1423+
if (fromPath.sqlTable) {
1424+
return this.evaluateSql(cube, fromPath.sqlTable);
1425+
}
1426+
1427+
const evaluatedSql = this.evaluateSql(cube, fromPath.sql);
14221428
const selectAsterisk = evaluatedSql.match(/^\s*select\s+\*\s+from\s+([a-zA-Z0-9_\-`".*]+)\s*$/i);
14231429
if (selectAsterisk) {
14241430
return selectAsterisk[1];
14251431
}
1432+
14261433
return `(${evaluatedSql})`;
14271434
}
14281435

packages/cubejs-schema-compiler/src/adapter/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export * from './CubeStoreQuery';
1515
// Base queries that can be re-used across different drivers
1616
export * from './MysqlQuery';
1717
export * from './PostgresQuery';
18+
export * from './MssqlQuery';
1819

1920
// Candidates to move from this package to drivers packages
2021
// export * from './PrestodbQuery';

packages/cubejs-schema-compiler/src/compiler/CompilerCache.js renamed to packages/cubejs-schema-compiler/src/compiler/CompilerCache.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,29 @@ import LRUCache from 'lru-cache';
22
import { QueryCache } from '../adapter/QueryCache';
33

44
export class CompilerCache extends QueryCache {
5-
constructor({ maxQueryCacheSize, maxQueryCacheAge }) {
5+
protected readonly queryCache: LRUCache<string, QueryCache>;
6+
7+
public constructor({ maxQueryCacheSize, maxQueryCacheAge }) {
68
super();
9+
710
this.queryCache = new LRUCache({
811
max: maxQueryCacheSize || 10000,
912
maxAge: (maxQueryCacheAge * 1000) || 1000 * 60 * 10,
1013
updateAgeOnGet: true
1114
});
1215
}
1316

14-
getQueryCache(key) {
17+
public getQueryCache(key: unknown): QueryCache {
1518
const keyString = JSON.stringify(key);
16-
if (!this.queryCache.get(keyString)) {
17-
this.queryCache.set(keyString, new QueryCache());
19+
20+
const exist = this.queryCache.get(keyString);
21+
if (exist) {
22+
return exist;
1823
}
19-
return this.queryCache.get(keyString);
24+
25+
const result = new QueryCache();
26+
this.queryCache.set(keyString, result);
27+
28+
return result;
2029
}
2130
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,9 @@ export class CubeEvaluator extends CubeSymbols {
9797
transformMembers(members, cube, errorReporter) {
9898
members = members || {};
9999
for (const memberName of Object.keys(members)) {
100-
const member = members[memberName];
101100
let ownedByCube = true;
101+
102+
const member = members[memberName];
102103
if (member.sql && !member.subQuery) {
103104
const funcArgs = this.funcArguments(member.sql);
104105
const cubeReferences = this.collectUsedCubeReferences(cube.name, member.sql);
@@ -112,9 +113,11 @@ export class CubeEvaluator extends CubeSymbols {
112113
errorReporter.error(`Member '${cube.name}.${memberName}' references foreign cubes: ${foreignCubes.join(', ')}. Please split and move this definition to corresponding cubes.`);
113114
}
114115
}
116+
115117
if (ownedByCube && cube.isView) {
116118
errorReporter.error(`View '${cube.name}' defines own member '${cube.name}.${memberName}'. Please move this member definition to one of the cubes.`);
117119
}
120+
118121
members[memberName] = { ...members[memberName], ownedByCube };
119122
}
120123
}

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

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -450,17 +450,8 @@ const MeasuresSchema = Joi.object().pattern(identifierRegex, Joi.alternatives().
450450
* and update CubePropContextTranspiler.transpiledFieldsPatterns
451451
**************************** */
452452

453-
const cubeSchema = Joi.object().keys({
453+
const baseSchema = {
454454
name: identifier,
455-
sql: Joi.alternatives().conditional(
456-
Joi.ref('..isView'), [
457-
{
458-
is: true,
459-
then: Joi.forbidden(),
460-
otherwise: Joi.func().required()
461-
}
462-
]
463-
),
464455
refreshKey: CubeRefreshKeySchema,
465456
fileName: Joi.string().required(),
466457
extends: Joi.func(),
@@ -470,7 +461,6 @@ const cubeSchema = Joi.object().keys({
470461
dataSource: Joi.string(),
471462
description: Joi.string(),
472463
rewriteQueries: Joi.boolean().strict(),
473-
isView: Joi.boolean().strict(),
474464
shown: Joi.boolean().strict(),
475465
joins: Joi.object().pattern(identifierRegex, Joi.object().keys({
476466
sql: Joi.func().required(),
@@ -521,6 +511,17 @@ const cubeSchema = Joi.object().keys({
521511
preAggregations: PreAggregationsAlternatives,
522512
includes: Joi.func(),
523513
excludes: Joi.func(),
514+
};
515+
516+
const cubeSchema = inherit(baseSchema, {
517+
sql: Joi.func(),
518+
sqlTable: Joi.func(),
519+
}).xor('sql', 'sqlTable').messages({
520+
'object.xor': 'You must use either sql or sqlTable within a model, but not both'
521+
});
522+
523+
const viewSchema = inherit(baseSchema, {
524+
isView: Joi.boolean().strict(),
524525
});
525526

526527
function formatErrorMessageFromDetails(explain, d) {
@@ -578,7 +579,7 @@ function collectFunctionFieldsPatterns(patterns, path, o) {
578579

579580
export function functionFieldsPatterns() {
580581
const functionPatterns = new Set();
581-
collectFunctionFieldsPatterns(functionPatterns, '', cubeSchema);
582+
collectFunctionFieldsPatterns(functionPatterns, '', { ...cubeSchema, ...viewSchema });
582583
return Array.from(functionPatterns);
583584
}
584585

@@ -595,7 +596,7 @@ export class CubeValidator {
595596
}
596597

597598
validate(cube, errorReporter) {
598-
const result = cubeSchema.validate(cube);
599+
const result = cube.isView ? viewSchema.validate(cube) : cubeSchema.validate(cube);
599600

600601
if (result.error != null) {
601602
errorReporter.error(formatErrorMessage(result.error), result.error);

packages/cubejs-schema-compiler/src/compiler/DataSchemaCompiler.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ export class DataSchemaCompiler {
181181
if (err) {
182182
errorsReport.error(err.toString());
183183
}
184+
184185
try {
185186
vm.runInNewContext(file.content, {
186187
view: (name, cube) => (

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export class YamlCompiler {
9494
for (const p of transpiledFieldsPatterns) {
9595
const fullPath = propertyPath.join('.');
9696
if (fullPath.match(p)) {
97-
if (typeof obj === 'string' && propertyPath[propertyPath.length - 1] === 'sql') {
97+
if (typeof obj === 'string' && ['sql', 'sqlTable'].includes(propertyPath[propertyPath.length - 1])) {
9898
return this.parsePythonIntoArrowFunction(`f"${this.escapeDoubleQuotes(obj)}"`, cubeName, obj, errorsReport);
9999
} else if (typeof obj === 'string') {
100100
return this.parsePythonIntoArrowFunction(obj, cubeName, obj, errorsReport);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { CubeDictionary } from '../CubeDictionary';
1010
export const transpiledFieldsPatterns: Array<RegExp> = [
1111
/\.sql$/,
1212
/sql$/,
13+
/sqlTable$/,
1314
/^measures\.[_a-zA-Z][_a-zA-Z0-9]*\.(drillMemberReferences|drillMembers)$/,
1415
/^preAggregations\.[_a-zA-Z][_a-zA-Z0-9]*\.indexes\.[_a-zA-Z][_a-zA-Z0-9]*\.columns$/,
1516
/^preAggregations\.[_a-zA-Z][_a-zA-Z0-9]*\.(timeDimensionReference|timeDimension|segments|dimensions|measures|rollups|segmentReferences|dimensionReferences|measureReferences|rollupReferences)$/,

packages/cubejs-schema-compiler/test/unit/base-query.test.ts

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,53 @@
11
import moment from 'moment-timezone';
2-
import { UserError } from '../../src/compiler/UserError';
3-
import { PostgresQuery } from '../../src/adapter/PostgresQuery';
4-
import { prepareCompiler } from './PrepareCompiler';
5-
import { MssqlQuery } from '../../src/adapter/MssqlQuery';
6-
import { BaseQuery } from '../../src';
7-
import { createCubeSchema, createJoinedCubesSchema } from './utils';
2+
import { BaseQuery, PostgresQuery, MssqlQuery, UserError } from '../../src';
3+
import { prepareCompiler, prepareYamlCompiler } from './PrepareCompiler';
4+
import { createCubeSchema, createCubeSchemaYaml, createJoinedCubesSchema } from './utils';
85

96
describe('SQL Generation', () => {
10-
describe('Common', () => {
7+
describe('Common - Yaml - syntax sugar', () => {
8+
const compilers = /** @type Compilers */ prepareYamlCompiler(
9+
createCubeSchemaYaml({ name: 'cards', sqlTable: 'card_tbl' })
10+
);
11+
12+
it('Simple query', async () => {
13+
await compilers.compiler.compile();
14+
15+
const query = new PostgresQuery(compilers, {
16+
measures: [
17+
'cards.count'
18+
],
19+
timeDimensions: [],
20+
filters: [],
21+
});
22+
const queryAndParams = query.buildSqlAndParams();
23+
expect(queryAndParams[0]).toContain('card_tbl');
24+
});
25+
});
26+
27+
describe('Common - JS - syntax sugar', () => {
28+
const compilers = /** @type Compilers */ prepareCompiler(
29+
createCubeSchema({
30+
name: 'cards',
31+
sqlTable: 'card_tbl'
32+
})
33+
);
34+
35+
it('Simple query', async () => {
36+
await compilers.compiler.compile();
37+
38+
const query = new PostgresQuery(compilers, {
39+
measures: [
40+
'cards.count'
41+
],
42+
timeDimensions: [],
43+
filters: [],
44+
});
45+
const queryAndParams = query.buildSqlAndParams();
46+
expect(queryAndParams[0]).toContain('card_tbl');
47+
});
48+
});
49+
50+
describe('Common - JS', () => {
1151
const compilers = /** @type Compilers */ prepareCompiler(
1252
createCubeSchema({
1353
name: 'cards',

packages/cubejs-schema-compiler/test/unit/cube-validator.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,77 @@ describe('Cube Validation', () => {
1515
console.log('CubePropContextTranspiler.transpiledFieldsPatterns =', transpiledFieldsPatterns);
1616
});
1717

18+
it('cube defined with sql - correct', async () => {
19+
const cubeValidator = new CubeValidator(new CubeSymbols());
20+
const cube = {
21+
name: 'name',
22+
sql: () => 'SELECT * FROM public.Users',
23+
fileName: 'fileName',
24+
};
25+
26+
const validationResult = cubeValidator.validate(cube, {
27+
error: (message, e) => {
28+
console.log(message);
29+
}
30+
});
31+
32+
expect(validationResult.error).toBeFalsy();
33+
});
34+
35+
it('cube defined with sqlTable - correct', async () => {
36+
const cubeValidator = new CubeValidator(new CubeSymbols());
37+
const cube = {
38+
name: 'name',
39+
sqlTable: () => 'public.Users',
40+
fileName: 'fileName',
41+
};
42+
43+
const validationResult = cubeValidator.validate(cube, {
44+
error: (message, e) => {
45+
console.log(message);
46+
}
47+
});
48+
49+
expect(validationResult.error).toBeFalsy();
50+
});
51+
52+
it('cube defined with sql and sqlTable - fail', async () => {
53+
const cubeValidator = new CubeValidator(new CubeSymbols());
54+
const cube = {
55+
name: 'name',
56+
sql: () => 'SELECT * FROM public.Users',
57+
sqlTable: () => 'public.Users',
58+
fileName: 'fileName',
59+
};
60+
61+
const validationResult = cubeValidator.validate(cube, {
62+
error: (message, e) => {
63+
console.log(message);
64+
expect(message).toContain('You must use either sql or sqlTable within a model, but not both');
65+
}
66+
});
67+
68+
expect(validationResult.error).toBeTruthy();
69+
});
70+
71+
it('view defined by includes - correct', async () => {
72+
const cubeValidator = new CubeValidator(new CubeSymbols());
73+
const cube = {
74+
name: 'name',
75+
// it's a hidden field which we use internally
76+
isView: true,
77+
fileName: 'fileName',
78+
};
79+
80+
const validationResult = cubeValidator.validate(cube, {
81+
error: (message, e) => {
82+
console.log(message);
83+
}
84+
});
85+
86+
expect(validationResult.error).toBeFalsy();
87+
});
88+
1889
it('refreshKey alternatives', async () => {
1990
const cubeValidator = new CubeValidator(new CubeSymbols());
2091
const cube = {

0 commit comments

Comments
 (0)