Skip to content

Commit b1aedfa

Browse files
vasilev-alexmarianore-muttdata
authored andcommitted
feat(schema-compiler): generate join sql using dimension refs when po… (cube-js#9413)
* feat(schema-compiler): generate join sql using dimension refs when possible * tests * lint * name fix
1 parent 434a5c7 commit b1aedfa

File tree

6 files changed

+913
-606
lines changed

6 files changed

+913
-606
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,7 @@ export async function streamToArray<T>(stream: Readable): Promise<T[]> {
7474
export async function oldStreamToArray<T>(stream: NodeJS.ReadableStream): Promise<T[]> {
7575
return streamToArray(new Readable().wrap(stream));
7676
}
77+
78+
export function notEmpty<T>(value: T | null | undefined): value is T {
79+
return value !== null && value !== undefined;
80+
}

packages/cubejs-schema-compiler/src/scaffolding/ScaffoldingSchema.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import inflection from 'inflection';
22
import R from 'ramda';
3+
import { notEmpty } from '@cubejs-backend/shared';
34
import { UserError } from '../compiler';
45
import { toSnakeCase } from './utils';
56

@@ -26,7 +27,7 @@ export type Dimension = {
2627

2728
export type TableName = string | [string, string];
2829

29-
type JoinRelationship = 'hasOne' | 'hasMany' | 'belongsTo';
30+
export type JoinRelationship = 'hasOne' | 'hasMany' | 'belongsTo';
3031

3132
type ColumnsToJoin = {
3233
cubeToJoin: string;
@@ -45,11 +46,13 @@ export type CubeDescriptorMember = {
4546
isPrimaryKey?: boolean;
4647
};
4748

48-
type Join = {
49+
export type Join = {
4950
thisTableColumn: string;
51+
thisTableColumnIncludedAsDimension?: boolean;
5052
tableName: TableName;
5153
cubeToJoin: string;
5254
columnToJoin: string;
55+
columnToJoinIncludedAsDimension?: boolean;
5356
relationship: JoinRelationship;
5457
};
5558

@@ -234,7 +237,7 @@ export class ScaffoldingSchema {
234237
};
235238
}
236239

237-
protected parseTableName(tableName) {
240+
protected parseTableName(tableName: TableName) {
238241
let schemaAndTable;
239242
if (Array.isArray(tableName)) {
240243
schemaAndTable = tableName;
@@ -247,7 +250,7 @@ export class ScaffoldingSchema {
247250
return schemaAndTable;
248251
}
249252

250-
protected dimensions(tableDefinition): Dimension[] {
253+
protected dimensions(tableDefinition: ColumnData[]): Dimension[] {
251254
return this.dimensionColumns(tableDefinition).map(column => {
252255
const res: Dimension = {
253256
name: column.name,
@@ -280,7 +283,7 @@ export class ScaffoldingSchema {
280283
return !column.name.match(new RegExp(idRegex, 'i')) && !!MEASURE_DICTIONARY.find(word => this.fixCase(column.name).endsWith(word));
281284
}
282285

283-
protected dimensionColumns(tableDefinition: any) {
286+
protected dimensionColumns(tableDefinition: ColumnData[]): Array<ColumnData & { columnType?: string }> {
284287
const dimensionColumns = tableDefinition.filter(
285288
column => !column.name.startsWith('_') && ['string', 'boolean'].includes(this.columnType(column)) ||
286289
column.attributes?.includes('primaryKey') ||
@@ -307,7 +310,7 @@ export class ScaffoldingSchema {
307310
return value.toLocaleLowerCase();
308311
}
309312

310-
protected joins(tableName: TableName, tableDefinition: ColumnData[]) {
313+
protected joins(tableName: TableName, tableDefinition: ColumnData[]): Join[] {
311314
const cubeName = (name: string) => (this.options.snakeCase ? toSnakeCase(name) : inflection.camelize(name));
312315

313316
return R.unnest(tableDefinition
@@ -336,7 +339,7 @@ export class ScaffoldingSchema {
336339
return null;
337340
}
338341

339-
columnsToJoin = tablesToJoin.map<any>(definition => {
342+
columnsToJoin = tablesToJoin.map(definition => {
340343
if (tableName === definition.tableName) {
341344
return null;
342345
}
@@ -350,22 +353,22 @@ export class ScaffoldingSchema {
350353
columnToJoin: columnForJoin.name,
351354
tableName: definition.tableName
352355
};
353-
}).filter(R.identity);
356+
}).filter(notEmpty);
354357
}
355358

356359
if (!columnsToJoin.length) {
357360
return null;
358361
}
359362

360-
return columnsToJoin.map(columnToJoin => ({
363+
return columnsToJoin.map<Join>(columnToJoin => ({
361364
thisTableColumn: column.name,
362365
tableName: columnToJoin.tableName,
363366
cubeToJoin: columnToJoin.cubeToJoin,
364367
columnToJoin: columnToJoin.columnToJoin,
365368
relationship: 'belongsTo'
366369
}));
367370
})
368-
.filter(R.identity)) as Join[];
371+
.filter(notEmpty));
369372
}
370373

371374
protected timeColumnIndex(column): number {

packages/cubejs-schema-compiler/src/scaffolding/formatters/BaseSchemaFormatter.ts

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { CubeMembers, SchemaContext } from '../ScaffoldingTemplate';
33
import {
44
CubeDescriptor,
55
DatabaseSchema,
6+
Dimension,
67
MemberType,
78
ScaffoldingSchema,
89
TableName,
@@ -51,21 +52,39 @@ export abstract class BaseSchemaFormatter {
5152
tableNames: TableName[],
5253
schemaContext: SchemaContext = {}
5354
): SchemaFile[] {
54-
const schemaForTables = this.scaffoldingSchema.generateForTables(
55+
const tableSchemas = this.scaffoldingSchema.generateForTables(
5556
tableNames.map((n) => this.scaffoldingSchema.resolveTableName(n))
5657
);
5758

58-
return schemaForTables.map((tableSchema) => ({
59-
fileName: `${tableSchema.cube}.${this.fileExtension()}`,
60-
content: this.renderFile(this.schemaDescriptorForTable(tableSchema, schemaContext)),
61-
}));
59+
return this.generateFilesByTableSchemas(tableSchemas, schemaContext);
6260
}
6361

6462
public generateFilesByCubeDescriptors(
6563
cubeDescriptors: CubeDescriptor[],
6664
schemaContext: SchemaContext = {}
6765
): SchemaFile[] {
68-
return this.schemaForTablesByCubeDescriptors(cubeDescriptors).map((tableSchema) => ({
66+
return this.generateFilesByTableSchemas(this.tableSchemasByCubeDescriptors(cubeDescriptors), schemaContext);
67+
}
68+
69+
protected generateFilesByTableSchemas(tableSchemas: TableSchema[], schemaContext: SchemaContext = {}): SchemaFile[] {
70+
const cubeToDimensionNamesMap = new Map(
71+
tableSchemas.map(tableSchema => [tableSchema.cube, tableSchema.dimensions.map(d => d.name)])
72+
);
73+
74+
tableSchemas = tableSchemas.map((tableSchema) => {
75+
const updatedJoins = tableSchema.joins.map((join) => ({
76+
...join,
77+
thisTableColumnIncludedAsDimension: !!cubeToDimensionNamesMap.get(tableSchema.cube)?.includes(join.thisTableColumn),
78+
columnToJoinIncludedAsDimension: !!cubeToDimensionNamesMap.get(join.cubeToJoin)?.includes(join.columnToJoin)
79+
}));
80+
81+
return {
82+
...tableSchema,
83+
joins: updatedJoins
84+
};
85+
});
86+
87+
return tableSchemas.map((tableSchema) => ({
6988
fileName: `${tableSchema.cube}.${this.fileExtension()}`,
7089
content: this.renderFile(this.schemaDescriptorForTable(tableSchema, schemaContext)),
7190
}));
@@ -85,7 +104,7 @@ export abstract class BaseSchemaFormatter {
85104
: undefined;
86105
}
87106

88-
protected memberName(member) {
107+
protected memberName(member: { title: string }) {
89108
const title = member.title.replace(/[^A-Za-z0-9]+/g, '_').toLowerCase();
90109

91110
if (this.options.snakeCase) {
@@ -106,7 +125,7 @@ export abstract class BaseSchemaFormatter {
106125
return !!name.match(/^[a-z0-9_]+$/);
107126
}
108127

109-
public schemaDescriptorForTable(tableSchema: TableSchema, schemaContext: SchemaContext = {}) {
128+
protected schemaDescriptorForTable(tableSchema: TableSchema, schemaContext: SchemaContext = {}) {
110129
let table = `${
111130
tableSchema.schema?.length ? `${this.escapeName(tableSchema.schema)}.` : ''
112131
}${this.escapeName(tableSchema.table)}`;
@@ -130,23 +149,39 @@ export abstract class BaseSchemaFormatter {
130149
sql: `SELECT * FROM ${table}`,
131150
};
132151

133-
return {
134-
cube: tableSchema.cube,
135-
...sqlOption,
136-
...dataSourceProp,
152+
// Try to use dimension refs if possible
153+
// Source and target columns must be included in the respective cubes as dimensions
154+
// {CUBE.dimension_name} = {other_cube.other_dimension_name}
155+
// instead of
156+
// {CUBE}.dimension_name = {other_cube}.other_dimension_name
157+
const joins = tableSchema.joins
158+
.map((j) => {
159+
const thisTableColumnRef = j.thisTableColumnIncludedAsDimension
160+
? this.cubeReference(`CUBE.${this.memberName({ title: j.thisTableColumn })}`)
161+
: `${this.cubeReference('CUBE')}.${this.escapeName(
162+
j.thisTableColumn
163+
)}`;
164+
const columnToJoinRef = j.columnToJoinIncludedAsDimension
165+
? this.cubeReference(`${j.cubeToJoin}.${this.memberName({ title: j.columnToJoin })}`)
166+
: `${this.cubeReference(j.cubeToJoin)}.${this.escapeName(j.columnToJoin)}`;
137167

138-
joins: tableSchema.joins
139-
.map((j) => ({
168+
return ({
140169
[j.cubeToJoin]: {
141-
sql: `${this.cubeReference('CUBE')}.${this.escapeName(
142-
j.thisTableColumn
143-
)} = ${this.cubeReference(j.cubeToJoin)}.${this.escapeName(j.columnToJoin)}`,
170+
sql: `${thisTableColumnRef} = ${columnToJoinRef}`,
144171
relationship: this.options.snakeCase
145172
? (JOIN_RELATIONSHIP_MAP[j.relationship] ?? j.relationship)
146173
: j.relationship,
147174
},
148-
}))
149-
.reduce((a, b) => ({ ...a, ...b }), {}),
175+
});
176+
})
177+
.reduce((a, b) => ({ ...a, ...b }), {});
178+
179+
return {
180+
cube: tableSchema.cube,
181+
...sqlOption,
182+
...dataSourceProp,
183+
184+
joins,
150185
dimensions: tableSchema.dimensions.sort((a) => (a.isPrimaryKey ? -1 : 0))
151186
.map((m) => ({
152187
[this.memberName(m)]: {
@@ -189,7 +224,7 @@ export abstract class BaseSchemaFormatter {
189224
};
190225
}
191226

192-
protected schemaForTablesByCubeDescriptors(cubeDescriptors: CubeDescriptor[]) {
227+
protected tableSchemasByCubeDescriptors(cubeDescriptors: CubeDescriptor[]) {
193228
const tableNames = cubeDescriptors.map(({ tableName }) => tableName);
194229
const generatedSchemaForTables = this.scaffoldingSchema.generateForTables(
195230
tableNames.map((n) => this.scaffoldingSchema.resolveTableName(n))

0 commit comments

Comments
 (0)