Skip to content

Commit 1785a1e

Browse files
committed
feat(schema-compiler): generate join sql using dimension refs when possible
1 parent 7d890ff commit 1785a1e

File tree

3 files changed

+70
-29
lines changed

3 files changed

+70
-29
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: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -51,21 +51,39 @@ export abstract class BaseSchemaFormatter {
5151
tableNames: TableName[],
5252
schemaContext: SchemaContext = {}
5353
): SchemaFile[] {
54-
const schemaForTables = this.scaffoldingSchema.generateForTables(
54+
const tableSchemas = this.scaffoldingSchema.generateForTables(
5555
tableNames.map((n) => this.scaffoldingSchema.resolveTableName(n))
5656
);
5757

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

6461
public generateFilesByCubeDescriptors(
6562
cubeDescriptors: CubeDescriptor[],
6663
schemaContext: SchemaContext = {}
6764
): SchemaFile[] {
68-
return this.schemaForTablesByCubeDescriptors(cubeDescriptors).map((tableSchema) => ({
65+
return this.generateFilesByTableSchemas(this.tableSchemasByCubeDescriptors(cubeDescriptors), schemaContext);
66+
}
67+
68+
protected generateFilesByTableSchemas(tableSchemas: TableSchema[], schemaContext: SchemaContext = {}): SchemaFile[] {
69+
const cubeToDimensionNamesMap = new Map(
70+
tableSchemas.map(tableSchema => [tableSchema.cube, tableSchema.dimensions.map(d => d.name)])
71+
);
72+
73+
tableSchemas = tableSchemas.map((tableSchema) => {
74+
const updatedJoins = tableSchema.joins.map((join) => ({
75+
...join,
76+
thisTableColumnIncludedAsDimension: !!cubeToDimensionNamesMap.get(tableSchema.cube)?.includes(join.thisTableColumn),
77+
columnToJoinIncludedAsDimension: !!cubeToDimensionNamesMap.get(join.cubeToJoin)?.includes(join.columnToJoin)
78+
}));
79+
80+
return {
81+
...tableSchema,
82+
joins: updatedJoins
83+
};
84+
});
85+
86+
return tableSchemas.map((tableSchema) => ({
6987
fileName: `${tableSchema.cube}.${this.fileExtension()}`,
7088
content: this.renderFile(this.schemaDescriptorForTable(tableSchema, schemaContext)),
7189
}));
@@ -106,7 +124,7 @@ export abstract class BaseSchemaFormatter {
106124
return !!name.match(/^[a-z0-9_]+$/);
107125
}
108126

109-
public schemaDescriptorForTable(tableSchema: TableSchema, schemaContext: SchemaContext = {}) {
127+
protected schemaDescriptorForTable(tableSchema: TableSchema, schemaContext: SchemaContext = {}) {
110128
let table = `${
111129
tableSchema.schema?.length ? `${this.escapeName(tableSchema.schema)}.` : ''
112130
}${this.escapeName(tableSchema.table)}`;
@@ -130,23 +148,39 @@ export abstract class BaseSchemaFormatter {
130148
sql: `SELECT * FROM ${table}`,
131149
};
132150

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

138-
joins: tableSchema.joins
139-
.map((j) => ({
167+
return ({
140168
[j.cubeToJoin]: {
141-
sql: `${this.cubeReference('CUBE')}.${this.escapeName(
142-
j.thisTableColumn
143-
)} = ${this.cubeReference(j.cubeToJoin)}.${this.escapeName(j.columnToJoin)}`,
169+
sql: `${thisTableColumnRef} = ${columnToJoinRef}`,
144170
relationship: this.options.snakeCase
145171
? (JOIN_RELATIONSHIP_MAP[j.relationship] ?? j.relationship)
146172
: j.relationship,
147173
},
148-
}))
149-
.reduce((a, b) => ({ ...a, ...b }), {}),
174+
});
175+
})
176+
.reduce((a, b) => ({ ...a, ...b }), {});
177+
178+
return {
179+
cube: tableSchema.cube,
180+
...sqlOption,
181+
...dataSourceProp,
182+
183+
joins,
150184
dimensions: tableSchema.dimensions.sort((a) => (a.isPrimaryKey ? -1 : 0))
151185
.map((m) => ({
152186
[this.memberName(m)]: {
@@ -189,7 +223,7 @@ export abstract class BaseSchemaFormatter {
189223
};
190224
}
191225

192-
protected schemaForTablesByCubeDescriptors(cubeDescriptors: CubeDescriptor[]) {
226+
protected tableSchemasByCubeDescriptors(cubeDescriptors: CubeDescriptor[]) {
193227
const tableNames = cubeDescriptors.map(({ tableName }) => tableName);
194228
const generatedSchemaForTables = this.scaffoldingSchema.generateForTables(
195229
tableNames.map((n) => this.scaffoldingSchema.resolveTableName(n))

0 commit comments

Comments
 (0)