diff --git a/packages/cubejs-schema-compiler/src/adapter/MssqlQuery.ts b/packages/cubejs-schema-compiler/src/adapter/MssqlQuery.ts index 066f98797d8a5..0a41ef8097b75 100644 --- a/packages/cubejs-schema-compiler/src/adapter/MssqlQuery.ts +++ b/packages/cubejs-schema-compiler/src/adapter/MssqlQuery.ts @@ -384,6 +384,7 @@ export class MssqlQuery extends BaseQuery { ') AS {{ from_alias }}{% elif from_prepared %}\n' + 'FROM {{ from_prepared }}' + '{% endif %}' + + '{% for join in joins %}\n{{ join }}{% endfor %}' + '{% if filter %}\nWHERE {{ filter }}{% endif %}' + '{% if group_by %}\nGROUP BY {{ group_by }}{% endif %}' + '{% if having %}\nHAVING {{ having }}{% endif %}' + diff --git a/packages/cubejs-schema-compiler/test/unit/allDialects.ts b/packages/cubejs-schema-compiler/test/unit/allDialects.ts index 8b94bdfc6709f..1b9a38a476584 100644 --- a/packages/cubejs-schema-compiler/test/unit/allDialects.ts +++ b/packages/cubejs-schema-compiler/test/unit/allDialects.ts @@ -9,8 +9,9 @@ const ADAPTER_DIR = path.join(__dirname, '..', '..', 'src', 'adapter'); const MIN_DIALECTS = 10; /** - * Every dialect, read off the adapter directory rather than listed by hand, so a dialect - * added later cannot quietly escape an invariant asserted over all of them. + * Every dialect in the schema-compiler adapter directory, discovered rather than listed + * by hand. Driver packages carry Query subclasses outside this scan; for a given template + * invariant the ones that matter are those redefining or deleting that template. * * Paired with its name, which is what a caller needs to report which dialect failed. */ diff --git a/packages/cubejs-schema-compiler/test/unit/select-template.test.ts b/packages/cubejs-schema-compiler/test/unit/select-template.test.ts new file mode 100644 index 0000000000000..e1bf4da3450e4 --- /dev/null +++ b/packages/cubejs-schema-compiler/test/unit/select-template.test.ts @@ -0,0 +1,25 @@ +import { allDialects } from './allDialects'; + +// The SQL API supplies already-rendered joins to statements/select; Tesseract folds +// joins into FROM instead. Guard the loop here because query snapshots do not cover it. +describe('statements/select', () => { + it.each(allDialects())('%s renders joins after both FROM branches and before WHERE', (_name, QueryClass) => { + // These templates are defined unconditionally, so no compiled model is needed. + const { select } = QueryClass.prototype.sqlTemplates.call(Object.create(QueryClass.prototype)).statements; + const joins = /\{%-?\s*for\s+join\s+in\s+joins\s*-?%\}\s*\{\{-?\s*join\s*-?\}\}\s*\{%-?\s*endfor\s*-?%\}/; + const fromAlias = /\{\{-?\s*from_alias\s*-?\}\}\s*\{%-?\s*elif\s+from_prepared\s*-?%\}/; + const fromPrepared = /FROM\s+\{\{-?\s*from_prepared\s*-?\}\}\s*\{%-?\s*endif\s*-?%\}/; + const filter = /\{%-?\s*if\s+filter\s*-?%\}\s*WHERE\b/; + + expect(select).toMatch(joins); + expect(select).toMatch(fromAlias); + expect(select).toMatch(fromPrepared); + expect(select).toMatch(filter); + + const fromEnd = select.match(fromPrepared)!; + const joinsEnd = select.match(joins)!; + expect(select.search(fromPrepared)).toBeGreaterThan(select.search(fromAlias)); + expect(select.search(joins)).toBeGreaterThanOrEqual(fromEnd.index! + fromEnd[0].length); + expect(select.search(filter)).toBeGreaterThanOrEqual(joinsEnd.index! + joinsEnd[0].length); + }); +});