diff --git a/packages/cubejs-backend-shared/src/helpers.ts b/packages/cubejs-backend-shared/src/helpers.ts index b9b925fd6fd0b..9d38edf484e05 100644 --- a/packages/cubejs-backend-shared/src/helpers.ts +++ b/packages/cubejs-backend-shared/src/helpers.ts @@ -74,3 +74,7 @@ export async function streamToArray(stream: Readable): Promise { export async function oldStreamToArray(stream: NodeJS.ReadableStream): Promise { return streamToArray(new Readable().wrap(stream)); } + +export function notEmpty(value: T | null | undefined): value is T { + return value !== null && value !== undefined; +} diff --git a/packages/cubejs-schema-compiler/src/scaffolding/ScaffoldingSchema.ts b/packages/cubejs-schema-compiler/src/scaffolding/ScaffoldingSchema.ts index 48c1a3863472b..77c7fa42a0e8e 100644 --- a/packages/cubejs-schema-compiler/src/scaffolding/ScaffoldingSchema.ts +++ b/packages/cubejs-schema-compiler/src/scaffolding/ScaffoldingSchema.ts @@ -1,5 +1,6 @@ import inflection from 'inflection'; import R from 'ramda'; +import { notEmpty } from '@cubejs-backend/shared'; import { UserError } from '../compiler'; import { toSnakeCase } from './utils'; @@ -26,7 +27,7 @@ export type Dimension = { export type TableName = string | [string, string]; -type JoinRelationship = 'hasOne' | 'hasMany' | 'belongsTo'; +export type JoinRelationship = 'hasOne' | 'hasMany' | 'belongsTo'; type ColumnsToJoin = { cubeToJoin: string; @@ -45,11 +46,13 @@ export type CubeDescriptorMember = { isPrimaryKey?: boolean; }; -type Join = { +export type Join = { thisTableColumn: string; + thisTableColumnIncludedAsDimension?: boolean; tableName: TableName; cubeToJoin: string; columnToJoin: string; + columnToJoinIncludedAsDimension?: boolean; relationship: JoinRelationship; }; @@ -234,7 +237,7 @@ export class ScaffoldingSchema { }; } - protected parseTableName(tableName) { + protected parseTableName(tableName: TableName) { let schemaAndTable; if (Array.isArray(tableName)) { schemaAndTable = tableName; @@ -247,7 +250,7 @@ export class ScaffoldingSchema { return schemaAndTable; } - protected dimensions(tableDefinition): Dimension[] { + protected dimensions(tableDefinition: ColumnData[]): Dimension[] { return this.dimensionColumns(tableDefinition).map(column => { const res: Dimension = { name: column.name, @@ -280,7 +283,7 @@ export class ScaffoldingSchema { return !column.name.match(new RegExp(idRegex, 'i')) && !!MEASURE_DICTIONARY.find(word => this.fixCase(column.name).endsWith(word)); } - protected dimensionColumns(tableDefinition: any) { + protected dimensionColumns(tableDefinition: ColumnData[]): Array { const dimensionColumns = tableDefinition.filter( column => !column.name.startsWith('_') && ['string', 'boolean'].includes(this.columnType(column)) || column.attributes?.includes('primaryKey') || @@ -307,7 +310,7 @@ export class ScaffoldingSchema { return value.toLocaleLowerCase(); } - protected joins(tableName: TableName, tableDefinition: ColumnData[]) { + protected joins(tableName: TableName, tableDefinition: ColumnData[]): Join[] { const cubeName = (name: string) => (this.options.snakeCase ? toSnakeCase(name) : inflection.camelize(name)); return R.unnest(tableDefinition @@ -336,7 +339,7 @@ export class ScaffoldingSchema { return null; } - columnsToJoin = tablesToJoin.map(definition => { + columnsToJoin = tablesToJoin.map(definition => { if (tableName === definition.tableName) { return null; } @@ -350,14 +353,14 @@ export class ScaffoldingSchema { columnToJoin: columnForJoin.name, tableName: definition.tableName }; - }).filter(R.identity); + }).filter(notEmpty); } if (!columnsToJoin.length) { return null; } - return columnsToJoin.map(columnToJoin => ({ + return columnsToJoin.map(columnToJoin => ({ thisTableColumn: column.name, tableName: columnToJoin.tableName, cubeToJoin: columnToJoin.cubeToJoin, @@ -365,7 +368,7 @@ export class ScaffoldingSchema { relationship: 'belongsTo' })); }) - .filter(R.identity)) as Join[]; + .filter(notEmpty)); } protected timeColumnIndex(column): number { diff --git a/packages/cubejs-schema-compiler/src/scaffolding/formatters/BaseSchemaFormatter.ts b/packages/cubejs-schema-compiler/src/scaffolding/formatters/BaseSchemaFormatter.ts index 02227ed87dcbd..1c49e6d567e26 100644 --- a/packages/cubejs-schema-compiler/src/scaffolding/formatters/BaseSchemaFormatter.ts +++ b/packages/cubejs-schema-compiler/src/scaffolding/formatters/BaseSchemaFormatter.ts @@ -3,6 +3,7 @@ import { CubeMembers, SchemaContext } from '../ScaffoldingTemplate'; import { CubeDescriptor, DatabaseSchema, + Dimension, MemberType, ScaffoldingSchema, TableName, @@ -51,21 +52,39 @@ export abstract class BaseSchemaFormatter { tableNames: TableName[], schemaContext: SchemaContext = {} ): SchemaFile[] { - const schemaForTables = this.scaffoldingSchema.generateForTables( + const tableSchemas = this.scaffoldingSchema.generateForTables( tableNames.map((n) => this.scaffoldingSchema.resolveTableName(n)) ); - return schemaForTables.map((tableSchema) => ({ - fileName: `${tableSchema.cube}.${this.fileExtension()}`, - content: this.renderFile(this.schemaDescriptorForTable(tableSchema, schemaContext)), - })); + return this.generateFilesByTableSchemas(tableSchemas, schemaContext); } public generateFilesByCubeDescriptors( cubeDescriptors: CubeDescriptor[], schemaContext: SchemaContext = {} ): SchemaFile[] { - return this.schemaForTablesByCubeDescriptors(cubeDescriptors).map((tableSchema) => ({ + return this.generateFilesByTableSchemas(this.tableSchemasByCubeDescriptors(cubeDescriptors), schemaContext); + } + + protected generateFilesByTableSchemas(tableSchemas: TableSchema[], schemaContext: SchemaContext = {}): SchemaFile[] { + const cubeToDimensionNamesMap = new Map( + tableSchemas.map(tableSchema => [tableSchema.cube, tableSchema.dimensions.map(d => d.name)]) + ); + + tableSchemas = tableSchemas.map((tableSchema) => { + const updatedJoins = tableSchema.joins.map((join) => ({ + ...join, + thisTableColumnIncludedAsDimension: !!cubeToDimensionNamesMap.get(tableSchema.cube)?.includes(join.thisTableColumn), + columnToJoinIncludedAsDimension: !!cubeToDimensionNamesMap.get(join.cubeToJoin)?.includes(join.columnToJoin) + })); + + return { + ...tableSchema, + joins: updatedJoins + }; + }); + + return tableSchemas.map((tableSchema) => ({ fileName: `${tableSchema.cube}.${this.fileExtension()}`, content: this.renderFile(this.schemaDescriptorForTable(tableSchema, schemaContext)), })); @@ -85,7 +104,7 @@ export abstract class BaseSchemaFormatter { : undefined; } - protected memberName(member) { + protected memberName(member: { title: string }) { const title = member.title.replace(/[^A-Za-z0-9]+/g, '_').toLowerCase(); if (this.options.snakeCase) { @@ -106,7 +125,7 @@ export abstract class BaseSchemaFormatter { return !!name.match(/^[a-z0-9_]+$/); } - public schemaDescriptorForTable(tableSchema: TableSchema, schemaContext: SchemaContext = {}) { + protected schemaDescriptorForTable(tableSchema: TableSchema, schemaContext: SchemaContext = {}) { let table = `${ tableSchema.schema?.length ? `${this.escapeName(tableSchema.schema)}.` : '' }${this.escapeName(tableSchema.table)}`; @@ -130,23 +149,39 @@ export abstract class BaseSchemaFormatter { sql: `SELECT * FROM ${table}`, }; - return { - cube: tableSchema.cube, - ...sqlOption, - ...dataSourceProp, + // Try to use dimension refs if possible + // Source and target columns must be included in the respective cubes as dimensions + // {CUBE.dimension_name} = {other_cube.other_dimension_name} + // instead of + // {CUBE}.dimension_name = {other_cube}.other_dimension_name + const joins = tableSchema.joins + .map((j) => { + const thisTableColumnRef = j.thisTableColumnIncludedAsDimension + ? this.cubeReference(`CUBE.${this.memberName({ title: j.thisTableColumn })}`) + : `${this.cubeReference('CUBE')}.${this.escapeName( + j.thisTableColumn + )}`; + const columnToJoinRef = j.columnToJoinIncludedAsDimension + ? this.cubeReference(`${j.cubeToJoin}.${this.memberName({ title: j.columnToJoin })}`) + : `${this.cubeReference(j.cubeToJoin)}.${this.escapeName(j.columnToJoin)}`; - joins: tableSchema.joins - .map((j) => ({ + return ({ [j.cubeToJoin]: { - sql: `${this.cubeReference('CUBE')}.${this.escapeName( - j.thisTableColumn - )} = ${this.cubeReference(j.cubeToJoin)}.${this.escapeName(j.columnToJoin)}`, + sql: `${thisTableColumnRef} = ${columnToJoinRef}`, relationship: this.options.snakeCase ? (JOIN_RELATIONSHIP_MAP[j.relationship] ?? j.relationship) : j.relationship, }, - })) - .reduce((a, b) => ({ ...a, ...b }), {}), + }); + }) + .reduce((a, b) => ({ ...a, ...b }), {}); + + return { + cube: tableSchema.cube, + ...sqlOption, + ...dataSourceProp, + + joins, dimensions: tableSchema.dimensions.sort((a) => (a.isPrimaryKey ? -1 : 0)) .map((m) => ({ [this.memberName(m)]: { @@ -189,7 +224,7 @@ export abstract class BaseSchemaFormatter { }; } - protected schemaForTablesByCubeDescriptors(cubeDescriptors: CubeDescriptor[]) { + protected tableSchemasByCubeDescriptors(cubeDescriptors: CubeDescriptor[]) { const tableNames = cubeDescriptors.map(({ tableName }) => tableName); const generatedSchemaForTables = this.scaffoldingSchema.generateForTables( tableNames.map((n) => this.scaffoldingSchema.resolveTableName(n)) diff --git a/packages/cubejs-schema-compiler/test/unit/__snapshots__/scaffolding-template.test.ts.snap b/packages/cubejs-schema-compiler/test/unit/__snapshots__/scaffolding-template.test.ts.snap new file mode 100644 index 0000000000000..5bd2bf80a8ccd --- /dev/null +++ b/packages/cubejs-schema-compiler/test/unit/__snapshots__/scaffolding-template.test.ts.snap @@ -0,0 +1,693 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ScaffoldingTemplate JavaScript formatter big query nested fields: orders.js 1`] = ` +"cube(\`orders\`, { + sql_table: \`public.orders\`, + + joins: { + + }, + + dimensions: { + id: { + sql: \`id\`, + type: \`number\`, + primary_key: true + }, + + some_dimension_inside: { + sql: \`\${CUBE}.some.dimension.inside\`, + type: \`string\`, + title: \`Some.dimension.inside\` + } + }, + + measures: { + count: { + type: \`count\` + } + }, + + pre_aggregations: { + // Pre-aggregation definitions go here. + // Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started + } +}); +" +`; + +exports[`ScaffoldingTemplate JavaScript formatter escaping back tick: some_orders.js 1`] = ` +"cube(\`some_orders\`, { + sql_table: \`public.\\\\\`someOrders\\\\\`\`, + + joins: { + + }, + + dimensions: { + id: { + sql: \`id\`, + type: \`number\`, + primary_key: true + }, + + somedimension: { + sql: \`\${CUBE}.\\\\\`someDimension\\\\\`\`, + type: \`string\` + } + }, + + measures: { + count: { + type: \`count\` + }, + + amount: { + sql: \`amount\`, + type: \`sum\` + } + }, + + pre_aggregations: { + // Pre-aggregation definitions go here. + // Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started + } +}); +" +`; + +exports[`ScaffoldingTemplate JavaScript formatter should add options if passed: orders.js 1`] = ` +"cube(\`orders\`, { + sql_table: \`public.orders\`, + + data_source: \`testDataSource\`, + + joins: { + + }, + + dimensions: { + id: { + sql: \`id\`, + type: \`number\`, + primary_key: true + }, + + some_dimension_inside: { + sql: \`\${CUBE}.some.dimension.inside\`, + type: \`string\`, + title: \`Some.dimension.inside\` + } + }, + + measures: { + count: { + type: \`count\` + } + }, + + pre_aggregations: { + // Pre-aggregation definitions go here. + // Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started + } +}); +" +`; + +exports[`ScaffoldingTemplate JavaScript formatter template with snake case: accounts.js 1`] = ` +"cube(\`accounts\`, { + sql_table: \`public.accounts\`, + + joins: { + + }, + + dimensions: { + id: { + sql: \`id\`, + type: \`number\`, + primary_key: true + }, + + username: { + sql: \`username\`, + type: \`string\` + }, + + password: { + sql: \`password\`, + type: \`string\` + } + }, + + measures: { + count: { + type: \`count\` + }, + + failure_count: { + sql: \`failure_count\`, + type: \`sum\` + } + }, + + pre_aggregations: { + // Pre-aggregation definitions go here. + // Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started + } +}); +" +`; + +exports[`ScaffoldingTemplate JavaScript formatter template with snake case: customers.js 1`] = ` +"cube(\`customers\`, { + sql_table: \`public.customers\`, + + joins: { + accounts: { + sql: \`\${CUBE}.account_id = \${accounts.id}\`, + relationship: \`many_to_one\` + } + }, + + dimensions: { + id: { + sql: \`id\`, + type: \`number\`, + primary_key: true + }, + + name: { + sql: \`name\`, + type: \`string\` + } + }, + + measures: { + count: { + type: \`count\` + }, + + visit_count: { + sql: \`visit_count\`, + type: \`sum\` + } + }, + + pre_aggregations: { + // Pre-aggregation definitions go here. + // Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started + } +}); +" +`; + +exports[`ScaffoldingTemplate JavaScript formatter template with snake case: orders.js 1`] = ` +"cube(\`orders\`, { + sql_table: \`public.orders\`, + + joins: { + customers: { + sql: \`\${CUBE}.\\"customerId\\" = \${customers.id}\`, + relationship: \`many_to_one\` + } + }, + + dimensions: { + id: { + sql: \`id\`, + type: \`number\`, + primary_key: true + } + }, + + measures: { + count: { + type: \`count\` + }, + + amount: { + sql: \`amount\`, + type: \`sum\` + } + }, + + pre_aggregations: { + // Pre-aggregation definitions go here. + // Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started + } +}); +" +`; + +exports[`ScaffoldingTemplate JavaScript formatter template: Accounts.js 1`] = ` +"cube(\`Accounts\`, { + sql: \`SELECT * FROM public.accounts\`, + + joins: { + + }, + + dimensions: { + id: { + sql: \`id\`, + type: \`number\`, + primaryKey: true + }, + + username: { + sql: \`username\`, + type: \`string\` + }, + + password: { + sql: \`password\`, + type: \`string\` + } + }, + + measures: { + count: { + type: \`count\` + }, + + failureCount: { + sql: \`failure_count\`, + type: \`sum\` + } + }, + + preAggregations: { + // Pre-aggregation definitions go here. + // Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started + } +}); +" +`; + +exports[`ScaffoldingTemplate JavaScript formatter template: Customers.js 1`] = ` +"cube(\`Customers\`, { + sql: \`SELECT * FROM public.customers\`, + + joins: { + Accounts: { + sql: \`\${CUBE}.account_id = \${Accounts.id}\`, + relationship: \`belongsTo\` + } + }, + + dimensions: { + id: { + sql: \`id\`, + type: \`number\`, + primaryKey: true + }, + + name: { + sql: \`name\`, + type: \`string\` + } + }, + + measures: { + count: { + type: \`count\` + }, + + visitCount: { + sql: \`visit_count\`, + type: \`sum\` + } + }, + + preAggregations: { + // Pre-aggregation definitions go here. + // Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started + } +}); +" +`; + +exports[`ScaffoldingTemplate JavaScript formatter template: Orders.js 1`] = ` +"cube(\`Orders\`, { + sql: \`SELECT * FROM public.orders\`, + + joins: { + Customers: { + sql: \`\${CUBE}.\\"customerId\\" = \${Customers.id}\`, + relationship: \`belongsTo\` + } + }, + + dimensions: { + id: { + sql: \`id\`, + type: \`number\`, + primaryKey: true + } + }, + + measures: { + count: { + type: \`count\` + }, + + amount: { + sql: \`amount\`, + type: \`sum\` + } + }, + + preAggregations: { + // Pre-aggregation definitions go here. + // Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started + } +}); +" +`; + +exports[`ScaffoldingTemplate JavaScript formatter uses dimension refs instead of table columns for join sql: customers.js 1`] = ` +"cube(\`customers\`, { + sql_table: \`public.customers\`, + + joins: { + + }, + + dimensions: { + id: { + sql: \`id\`, + type: \`string\`, + primary_key: true + }, + + name: { + sql: \`name\`, + type: \`string\` + } + }, + + measures: { + count: { + type: \`count\` + } + }, + + pre_aggregations: { + // Pre-aggregation definitions go here. + // Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started + } +}); +" +`; + +exports[`ScaffoldingTemplate JavaScript formatter uses dimension refs instead of table columns for join sql: orders.js 1`] = ` +"cube(\`orders\`, { + sql_table: \`public.orders\`, + + joins: { + customers: { + sql: \`\${CUBE.customerkey} = \${customers.id}\`, + relationship: \`many_to_one\` + } + }, + + dimensions: { + id: { + sql: \`id\`, + type: \`number\`, + primary_key: true + }, + + test: { + sql: \`test\`, + type: \`number\`, + primary_key: true + }, + + customerkey: { + sql: \`\${CUBE}.\\"customerKey\\"\`, + type: \`string\` + } + }, + + measures: { + count: { + type: \`count\` + }, + + amount: { + sql: \`amount\`, + type: \`sum\` + } + }, + + pre_aggregations: { + // Pre-aggregation definitions go here. + // Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started + } +}); +" +`; + +exports[`ScaffoldingTemplate Yaml formatter generates schema for MySQL driver: accounts.yml 1`] = ` +"cubes: + - name: accounts + sql_table: public.accounts + + joins: [] + + dimensions: + - name: id + sql: id + type: number + primary_key: true + + - name: username + sql: username + type: string + + - name: password + sql: password + type: string + + measures: + - name: count + type: count + + - name: failure_count + sql: failure_count + type: sum + + pre_aggregations: + # Pre-aggregation definitions go here. + # Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started + +" +`; + +exports[`ScaffoldingTemplate Yaml formatter generates schema for base driver: accounts.yml 1`] = ` +"cubes: + - name: accounts + sql_table: public.accounts + + joins: [] + + dimensions: + - name: id + sql: id + type: number + primary_key: true + + - name: username + sql: username + type: string + + - name: password + sql: password + type: string + + measures: + - name: count + type: count + + - name: failure_count + sql: failure_count + type: sum + + pre_aggregations: + # Pre-aggregation definitions go here. + # Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started + +" +`; + +exports[`ScaffoldingTemplate Yaml formatter generates schema for base driver: customers.yml 1`] = ` +"cubes: + - name: customers + sql_table: public.customers + + joins: + - name: accounts + sql: \\"{CUBE}.account_id = {accounts.id}\\" + relationship: many_to_one + + dimensions: + - name: id + sql: id + type: number + primary_key: true + + - name: name + sql: name + type: string + + measures: + - name: count + type: count + + - name: visit_count + sql: visit_count + type: sum + + pre_aggregations: + # Pre-aggregation definitions go here. + # Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started + +" +`; + +exports[`ScaffoldingTemplate Yaml formatter generates schema for base driver: orders.yml 1`] = ` +"cubes: + - name: orders + sql_table: public.orders + + joins: + - name: customers + sql: \\"{CUBE}.\\\\\\"customerId\\\\\\" = {customers.id}\\" + relationship: many_to_one + + dimensions: + - name: id + sql: id + type: number + primary_key: true + + measures: + - name: count + type: count + + - name: amount + sql: amount + type: sum + + pre_aggregations: + # Pre-aggregation definitions go here. + # Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started + +" +`; + +exports[`ScaffoldingTemplate Yaml formatter generates schema with a catalog: accounts.yml 1`] = ` +"cubes: + - name: accounts + sql_table: hello_catalog.public.accounts + + joins: [] + + dimensions: + - name: id + sql: id + type: number + primary_key: true + + - name: username + sql: username + type: string + + - name: password + sql: password + type: string + + measures: + - name: count + type: count + + - name: failure_count + sql: failure_count + type: sum + + pre_aggregations: + # Pre-aggregation definitions go here. + # Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started + +" +`; + +exports[`ScaffoldingTemplate Yaml formatter uses dimension refs instead of table columns for join sql: customers.yml 1`] = ` +"cubes: + - name: customers + sql_table: public.customers + + joins: [] + + dimensions: + - name: id + sql: id + type: string + primary_key: true + + - name: name + sql: name + type: string + + measures: + - name: count + type: count + + pre_aggregations: + # Pre-aggregation definitions go here. + # Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started + +" +`; + +exports[`ScaffoldingTemplate Yaml formatter uses dimension refs instead of table columns for join sql: orders.yml 1`] = ` +"cubes: + - name: orders + sql_table: public.orders + + joins: + - name: customers + sql: \\"{CUBE.customerkey} = {customers.id}\\" + relationship: many_to_one + + dimensions: + - name: id + sql: id + type: number + primary_key: true + + - name: test + sql: test + type: number + primary_key: true + + - name: customerkey + sql: \\"{CUBE}.\\\\\\"customerKey\\\\\\"\\" + type: string + + measures: + - name: count + type: count + + - name: amount + sql: amount + type: sum + + pre_aggregations: + # Pre-aggregation definitions go here. + # Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started + +" +`; diff --git a/packages/cubejs-schema-compiler/test/unit/scaffolding-template.test.ts b/packages/cubejs-schema-compiler/test/unit/scaffolding-template.test.ts index 4a9961c13d6da..03c225b2617e2 100644 --- a/packages/cubejs-schema-compiler/test/unit/scaffolding-template.test.ts +++ b/packages/cubejs-schema-compiler/test/unit/scaffolding-template.test.ts @@ -91,145 +91,95 @@ const dbSchema = { }, }; +const schemasWithPrimaryAndForeignKeys = { + public: { + orders: [ + { + name: 'test', + type: 'integer', + attributes: ['primaryKey'] + }, + { + name: 'id', + type: 'integer', + attributes: [] + }, + { + name: 'amount', + type: 'integer', + attributes: [] + }, + { + name: 'customerKey', + type: 'string', + attributes: [], + foreign_keys: [ + { + target_table: 'customers', + target_column: 'id' + } + ] + } + ], + customers: [ + { + name: 'id', + type: 'string', + attributes: [] + }, + { + name: 'name', + type: 'character varying', + attributes: [] + }, + { + name: 'account_id', + type: 'integer', + attributes: [] + } + ], + accounts: [ + { + name: 'id', + type: 'integer', + attributes: [] + }, + { + name: 'username', + type: 'character varying', + attributes: [] + }, + { + name: 'password', + type: 'character varying', + attributes: ['primaryKey'] + }, + { + name: 'failure_count', + type: 'integer', + attributes: [] + }, + { + name: 'account_status', + type: 'character varying', + attributes: [] + } + ], + } +}; + describe('ScaffoldingTemplate', () => { describe('JavaScript formatter', () => { it('template', () => { const template = new ScaffoldingTemplate(dbSchema, driver); - expect( - template.generateFilesByTableNames([ - 'public.orders', - ['public', 'customers'], - 'public.accounts', - ]) - ).toEqual([ - { - fileName: 'Orders.js', - content: `cube(\`Orders\`, { - sql: \`SELECT * FROM public.orders\`, - - joins: { - Customers: { - sql: \`\${CUBE}."customerId" = \${Customers}.id\`, - relationship: \`belongsTo\` - } - }, - - dimensions: { - id: { - sql: \`id\`, - type: \`number\`, - primaryKey: true - } - }, - - measures: { - count: { - type: \`count\` - }, - - amount: { - sql: \`amount\`, - type: \`sum\` - } - }, - - preAggregations: { - // Pre-aggregation definitions go here. - // Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started - } -}); -`, - }, - { - fileName: 'Customers.js', - content: `cube(\`Customers\`, { - sql: \`SELECT * FROM public.customers\`, - - joins: { - Accounts: { - sql: \`\${CUBE}.account_id = \${Accounts}.id\`, - relationship: \`belongsTo\` - } - }, - - dimensions: { - id: { - sql: \`id\`, - type: \`number\`, - primaryKey: true - }, - - name: { - sql: \`name\`, - type: \`string\` - } - }, - - measures: { - count: { - type: \`count\` - }, - - visitCount: { - sql: \`visit_count\`, - type: \`sum\` - } - }, - - preAggregations: { - // Pre-aggregation definitions go here. - // Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started - } -}); -`, - }, - { - fileName: 'Accounts.js', - content: `cube(\`Accounts\`, { - sql: \`SELECT * FROM public.accounts\`, - - joins: { - - }, - - dimensions: { - id: { - sql: \`id\`, - type: \`number\`, - primaryKey: true - }, - - username: { - sql: \`username\`, - type: \`string\` - }, - - password: { - sql: \`password\`, - type: \`string\` - } - }, - - measures: { - count: { - type: \`count\` - }, - - failureCount: { - sql: \`failure_count\`, - type: \`sum\` - } - }, - - preAggregations: { - // Pre-aggregation definitions go here. - // Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started - } -}); -`, - }, - ]); + template.generateFilesByTableNames([ + 'public.orders', + ['public', 'customers'], + 'public.accounts', + ]).forEach((it) => { + expect(it.content).toMatchSnapshot(it.fileName); + }); }); it('template with snake case', () => { @@ -237,140 +187,13 @@ describe('ScaffoldingTemplate', () => { snakeCase: true, }); - expect( - template.generateFilesByTableNames([ - 'public.orders', - ['public', 'customers'], - 'public.accounts', - ]) - ).toEqual([ - { - fileName: 'orders.js', - content: `cube(\`orders\`, { - sql_table: \`public.orders\`, - - joins: { - customers: { - sql: \`\${CUBE}."customerId" = \${customers}.id\`, - relationship: \`many_to_one\` - } - }, - - dimensions: { - id: { - sql: \`id\`, - type: \`number\`, - primary_key: true - } - }, - - measures: { - count: { - type: \`count\` - }, - - amount: { - sql: \`amount\`, - type: \`sum\` - } - }, - - pre_aggregations: { - // Pre-aggregation definitions go here. - // Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started - } -}); -`, - }, - { - fileName: 'customers.js', - content: `cube(\`customers\`, { - sql_table: \`public.customers\`, - - joins: { - accounts: { - sql: \`\${CUBE}.account_id = \${accounts}.id\`, - relationship: \`many_to_one\` - } - }, - - dimensions: { - id: { - sql: \`id\`, - type: \`number\`, - primary_key: true - }, - - name: { - sql: \`name\`, - type: \`string\` - } - }, - - measures: { - count: { - type: \`count\` - }, - - visit_count: { - sql: \`visit_count\`, - type: \`sum\` - } - }, - - pre_aggregations: { - // Pre-aggregation definitions go here. - // Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started - } -}); -`, - }, - { - fileName: 'accounts.js', - content: `cube(\`accounts\`, { - sql_table: \`public.accounts\`, - - joins: { - - }, - - dimensions: { - id: { - sql: \`id\`, - type: \`number\`, - primary_key: true - }, - - username: { - sql: \`username\`, - type: \`string\` - }, - - password: { - sql: \`password\`, - type: \`string\` - } - }, - - measures: { - count: { - type: \`count\` - }, - - failure_count: { - sql: \`failure_count\`, - type: \`sum\` - } - }, - - pre_aggregations: { - // Pre-aggregation definitions go here. - // Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started - } -}); -`, - }, - ]); + template.generateFilesByTableNames([ + 'public.orders', + ['public', 'customers'], + 'public.accounts', + ]).forEach((it) => { + expect(it.content).toMatchSnapshot(it.fileName); + }); }); it('escaping back tick', () => { @@ -402,50 +225,9 @@ describe('ScaffoldingTemplate', () => { } ); - expect(template.generateFilesByTableNames(['public.someOrders'])).toEqual( - [ - { - fileName: 'some_orders.js', - content: `cube(\`some_orders\`, { - sql_table: \`public.\\\`someOrders\\\`\`, - - joins: { - - }, - - dimensions: { - id: { - sql: \`id\`, - type: \`number\`, - primary_key: true - }, - - somedimension: { - sql: \`\${CUBE}.\\\`someDimension\\\`\`, - type: \`string\` - } - }, - - measures: { - count: { - type: \`count\` - }, - - amount: { - sql: \`amount\`, - type: \`sum\` - } - }, - - pre_aggregations: { - // Pre-aggregation definitions go here. - // Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started - } -}); -`, - }, - ] - ); + template.generateFilesByTableNames(['public.someOrders']).forEach((it) => { + expect(it.content).toMatchSnapshot(it.fileName); + }); }); it('big query nested fields', () => { @@ -471,44 +253,9 @@ describe('ScaffoldingTemplate', () => { snakeCase: true } ); - expect(template.generateFilesByTableNames(['public.orders'])).toEqual([ - { - fileName: 'orders.js', - content: `cube(\`orders\`, { - sql_table: \`public.orders\`, - - joins: { - - }, - - dimensions: { - id: { - sql: \`id\`, - type: \`number\`, - primary_key: true - }, - - some_dimension_inside: { - sql: \`\${CUBE}.some.dimension.inside\`, - type: \`string\`, - title: \`Some.dimension.inside\` - } - }, - - measures: { - count: { - type: \`count\` - } - }, - - pre_aggregations: { - // Pre-aggregation definitions go here. - // Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started - } -}); -`, - }, - ]); + + template.generateFilesByTableNames(['public.orders']) + .forEach((it) => expect(it.content).toMatchSnapshot(it.fileName)); }); it('should add options if passed', () => { @@ -539,48 +286,24 @@ describe('ScaffoldingTemplate', () => { } ); - expect( - template.generateFilesByTableNames(['public.orders'], schemaContext) - ).toEqual([ + template.generateFilesByTableNames(['public.orders'], schemaContext).forEach((it) => { + expect(it.content).toMatchSnapshot(it.fileName); + }); + }); + + it('uses dimension refs instead of table columns for join sql', () => { + const template = new ScaffoldingTemplate( + schemasWithPrimaryAndForeignKeys, + driver, { - fileName: 'orders.js', - content: `cube(\`orders\`, { - sql_table: \`public.orders\`, - - data_source: \`testDataSource\`, - - joins: { - - }, - - dimensions: { - id: { - sql: \`id\`, - type: \`number\`, - primary_key: true - }, - - some_dimension_inside: { - sql: \`\${CUBE}.some.dimension.inside\`, - type: \`string\`, - title: \`Some.dimension.inside\` - } - }, - - measures: { - count: { - type: \`count\` - } - }, - - pre_aggregations: { - // Pre-aggregation definitions go here. - // Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started - } -}); -`, - }, - ]); + format: SchemaFormat.JavaScript, + snakeCase: true, + } + ); + + template.generateFilesByTableNames(['public.orders', 'public.customers']).forEach((it) => { + expect(it.content).toMatchSnapshot(it.fileName); + }); }); }); @@ -591,116 +314,13 @@ describe('ScaffoldingTemplate', () => { snakeCase: true }); - expect( - template.generateFilesByTableNames([ - 'public.orders', - ['public', 'customers'], - 'public.accounts', - ]) - ).toEqual([ - { - fileName: 'orders.yml', - content: `cubes: - - name: orders - sql_table: public.orders - - joins: - - name: customers - sql: "{CUBE}.\\"customerId\\" = {customers}.id" - relationship: many_to_one - - dimensions: - - name: id - sql: id - type: number - primary_key: true - - measures: - - name: count - type: count - - - name: amount - sql: amount - type: sum - - pre_aggregations: - # Pre-aggregation definitions go here. - # Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started - -`, - }, - { - fileName: 'customers.yml', - content: `cubes: - - name: customers - sql_table: public.customers - - joins: - - name: accounts - sql: "{CUBE}.account_id = {accounts}.id" - relationship: many_to_one - - dimensions: - - name: id - sql: id - type: number - primary_key: true - - - name: name - sql: name - type: string - - measures: - - name: count - type: count - - - name: visit_count - sql: visit_count - type: sum - - pre_aggregations: - # Pre-aggregation definitions go here. - # Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started - -`, - }, - { - fileName: 'accounts.yml', - content: `cubes: - - name: accounts - sql_table: public.accounts - - joins: [] - - dimensions: - - name: id - sql: id - type: number - primary_key: true - - - name: username - sql: username - type: string - - - name: password - sql: password - type: string - - measures: - - name: count - type: count - - - name: failure_count - sql: failure_count - type: sum - - pre_aggregations: - # Pre-aggregation definitions go here. - # Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started - -`, - }, - ]); + template.generateFilesByTableNames([ + 'public.orders', + ['public', 'customers'], + 'public.accounts', + ]).forEach((it) => { + expect(it.content).toMatchSnapshot(it.fileName); + }); }); it('generates schema for MySQL driver', () => { @@ -717,46 +337,11 @@ describe('ScaffoldingTemplate', () => { } ); - expect(template.generateFilesByTableNames(['public.accounts'])).toEqual([ - { - fileName: 'accounts.yml', - content: `cubes: - - name: accounts - sql_table: public.accounts - - joins: [] - - dimensions: - - name: id - sql: id - type: number - primary_key: true - - - name: username - sql: username - type: string - - - name: password - sql: password - type: string - - measures: - - name: count - type: count - - - name: failure_count - sql: failure_count - type: sum - - pre_aggregations: - # Pre-aggregation definitions go here. - # Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started - -`, - }, - ]); + template.generateFilesByTableNames(['public.accounts']).forEach((it) => { + expect(it.content).toMatchSnapshot(it.fileName); + }); }); - + it('generates schema with a catalog', () => { const template = new ScaffoldingTemplate( { @@ -772,44 +357,24 @@ describe('ScaffoldingTemplate', () => { } ); - expect(template.generateFilesByTableNames(['public.accounts'])).toEqual([ - { - fileName: 'accounts.yml', - content: `cubes: - - name: accounts - sql_table: hello_catalog.public.accounts - - joins: [] - - dimensions: - - name: id - sql: id - type: number - primary_key: true - - - name: username - sql: username - type: string - - - name: password - sql: password - type: string - - measures: - - name: count - type: count - - - name: failure_count - sql: failure_count - type: sum + template.generateFilesByTableNames(['public.accounts']).forEach((it) => { + expect(it.content).toMatchSnapshot(it.fileName); + }); + }); - pre_aggregations: - # Pre-aggregation definitions go here. - # Learn more in the documentation: https://cube.dev/docs/caching/pre-aggregations/getting-started + it('uses dimension refs instead of table columns for join sql', () => { + const template = new ScaffoldingTemplate( + schemasWithPrimaryAndForeignKeys, + driver, + { + format: SchemaFormat.Yaml, + snakeCase: true, + } + ); -`, - }, - ]); + template.generateFilesByTableNames(['public.orders', 'public.customers']).forEach((it) => { + expect(it.content).toMatchSnapshot(it.fileName); + }); }); }); }); diff --git a/packages/cubejs-schema-compiler/test/unit/utils.test.ts b/packages/cubejs-schema-compiler/test/unit/utils.test.ts index 86b782a3fb0dd..1f2d8878c2c25 100644 --- a/packages/cubejs-schema-compiler/test/unit/utils.test.ts +++ b/packages/cubejs-schema-compiler/test/unit/utils.test.ts @@ -1,6 +1,13 @@ import { camelizeCube } from '../../src/compiler/utils'; +import { toSnakeCase } from '../../src/scaffolding/utils'; describe('Test Utils', () => { + it('toSnakeCase', () => { + expect(toSnakeCase('customerkey')).toEqual('customerkey'); + expect(toSnakeCase('customerKey')).toEqual('customer_key'); + expect(toSnakeCase('customer_key')).toEqual('customer_key'); + }); + it('camelizeObject (js)', () => { const res = camelizeCube({ sql_table: 'tbl',