Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 43 additions & 8 deletions packages/cubejs-schema-compiler/src/compiler/YamlCompiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,12 @@ export class YamlCompiler {
private transformYamlCubeObj(cubeObj, errorsReport: ErrorReporter) {
camelizeCube(cubeObj);

cubeObj.measures = this.yamlArrayToObj(cubeObj.measures || [], 'measure', errorsReport);
cubeObj.dimensions = this.yamlArrayToObj(cubeObj.dimensions || [], 'dimension', errorsReport);
cubeObj.segments = this.yamlArrayToObj(cubeObj.segments || [], 'segment', errorsReport);
cubeObj.preAggregations = this.yamlArrayToObj(cubeObj.preAggregations || [], 'preAggregation', errorsReport);
cubeObj.hierarchies = this.yamlArrayToObj(cubeObj.hierarchies || [], 'hierarchies', errorsReport);
const ctx = { cubeName: cubeObj.name };
cubeObj.measures = this.yamlArrayToObj(cubeObj.measures || [], 'measure', errorsReport, ctx);
cubeObj.dimensions = this.yamlArrayToObj(cubeObj.dimensions || [], 'dimension', errorsReport, ctx);
cubeObj.segments = this.yamlArrayToObj(cubeObj.segments || [], 'segment', errorsReport, ctx);
cubeObj.preAggregations = this.yamlArrayToObj(cubeObj.preAggregations || [], 'preAggregation', errorsReport, ctx);
cubeObj.hierarchies = this.yamlArrayToObj(cubeObj.hierarchies || [], 'hierarchies', errorsReport, ctx);

cubeObj.joins = cubeObj.joins || []; // For edge cases where joins are not defined/null
if (!Array.isArray(cubeObj.joins)) {
Expand Down Expand Up @@ -315,12 +316,40 @@ export class YamlCompiler {
return body?.expression;
}

private yamlArrayToObj(yamlArray, memberType: string, errorsReport: ErrorReporter) {
private yamlArrayToObj(
yamlArray,
memberType: string,
errorsReport: ErrorReporter,
ctx: { cubeName: string; parent?: { type: string; name: string } }
) {
if (!Array.isArray(yamlArray)) {
errorsReport.error(`${memberType}s must be defined as array`);
return {};
}

// Check for duplicate names
const names = yamlArray
.map(item => item?.name)
.filter(name => name != null);

const seen = new Set<string>();

for (const name of names) {
if (seen.has(name)) {
if (ctx.parent) {
errorsReport.error(
`Found duplicate ${memberType} '${name}' in ${ctx.parent.type} '${ctx.parent.name}' in cube '${ctx.cubeName}'.`
);
} else {
errorsReport.error(
`Member names must be unique within a cube. Found duplicate ${memberType} '${name}' in cube '${ctx.cubeName}'.`
);
}
}

seen.add(name);
}

const remapped = yamlArray.map(({ name, indexes, granularities, ...rest }) => {
if (!name) {
errorsReport.error(`name isn't defined for ${memberType}: ${JSON.stringify(rest)}`);
Expand All @@ -329,12 +358,18 @@ export class YamlCompiler {

const res = { [name]: {} };
if (memberType === 'preAggregation' && indexes) {
indexes = this.yamlArrayToObj(indexes || [], `${memberType}.index`, errorsReport);
indexes = this.yamlArrayToObj(indexes || [], 'preAggregation.index', errorsReport, {
cubeName: ctx.cubeName,
parent: { type: 'pre-aggregation', name }
});
res[name] = { indexes, ...res[name] };
}

if (memberType === 'dimension' && granularities) {
granularities = this.yamlArrayToObj(granularities || [], `${memberType}.granularity`, errorsReport);
granularities = this.yamlArrayToObj(granularities || [], 'dimension.granularity', errorsReport, {
cubeName: ctx.cubeName,
parent: { type: 'time dimension', name }
});
res[name] = { granularities, ...res[name] };
}

Expand Down
179 changes: 179 additions & 0 deletions packages/cubejs-schema-compiler/test/unit/yaml-schema.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,185 @@
import { prepareYamlCompiler } from './PrepareCompiler';

describe('Yaml Schema Testing', () => {
describe('Duplicate member detection', () => {
it('detects duplicate measures', async () => {
const { compiler } = prepareYamlCompiler(`
cubes:
- name: orders
sql_table: orders
dimensions:
- name: id
sql: id
type: number
primary_key: true
measures:
- name: count
type: count
- name: count
type: count
`);

try {
await compiler.compile();
throw new Error('compile must return an error');
} catch (e: any) {
expect(e.message).toContain("Found duplicate measure 'count' in cube 'orders'");
}
});

it('detects duplicate dimensions', async () => {
const { compiler } = prepareYamlCompiler(`
cubes:
- name: orders
sql_table: orders
dimensions:
- name: id
sql: id
type: number
primary_key: true
- name: id
sql: id
type: number
`);

try {
await compiler.compile();
throw new Error('compile must return an error');
} catch (e: any) {
expect(e.message).toContain("Found duplicate dimension 'id' in cube 'orders'");
}
});

it('detects duplicate segments', async () => {
const { compiler } = prepareYamlCompiler(`
cubes:
- name: orders
sql_table: orders
dimensions:
- name: id
sql: id
type: number
primary_key: true
segments:
- name: active
sql: "{CUBE}.status = 'active'"
- name: active
sql: "{CUBE}.status = 'active'"
`);

try {
await compiler.compile();
throw new Error('compile must return an error');
} catch (e: any) {
expect(e.message).toContain("Found duplicate segment 'active' in cube 'orders'");
}
});

it('detects multiple duplicates', async () => {
const { compiler } = prepareYamlCompiler(`
cubes:
- name: orders
sql_table: orders
dimensions:
- name: id
sql: id
type: number
primary_key: true
measures:
- name: count
type: count
- name: count
type: count
- name: total
type: sum
sql: amount
- name: total
type: sum
sql: amount
`);

try {
await compiler.compile();
throw new Error('compile must return an error');
} catch (e: any) {
expect(e.message).toContain("Found duplicate measure 'count' in cube 'orders'");
expect(e.message).toContain("Found duplicate measure 'total' in cube 'orders'");
}
});

it('detects duplicate pre-aggregation indexes', async () => {
const { compiler } = prepareYamlCompiler(`
cubes:
- name: orders
sql_table: orders
dimensions:
- name: id
sql: id
type: number
primary_key: true
- name: status
sql: status
type: string
measures:
- name: count
type: count
pre_aggregations:
- name: main
measures:
- count
dimensions:
- status
indexes:
- name: status_idx
columns:
- status
- name: status_idx
columns:
- id
`);

try {
await compiler.compile();
throw new Error('compile must return an error');
} catch (e: any) {
expect(e.message).toContain("Found duplicate preAggregation.index 'status_idx' in pre-aggregation 'main' in cube 'orders'");
}
});

it('detects duplicate dimension granularities', async () => {
const { compiler } = prepareYamlCompiler(`
cubes:
- name: orders
sql_table: orders
dimensions:
- name: id
sql: id
type: number
primary_key: true
- name: created_at
sql: created_at
type: time
granularities:
- name: fiscal_year
interval: 1 year
origin: "2024-04-01"
- name: fiscal_year
interval: 1 year
origin: "2024-01-01"
measures:
- name: count
type: count
`);

try {
await compiler.compile();
throw new Error('compile must return an error');
} catch (e: any) {
expect(e.message).toContain("Found duplicate dimension.granularity 'fiscal_year' in time dimension 'created_at' in cube 'orders'");
}
});
});

it('members must be defined as arrays', async () => {
const { compiler } = prepareYamlCompiler(
`
Expand Down
Loading