From 6edc8faaf1c8d7e4ecfa59cfa258fe87c761cfd3 Mon Sep 17 00:00:00 2001 From: davidda Date: Sat, 5 Sep 2026 16:38:20 +0200 Subject: [PATCH 1/5] fix(schema-compiler): render MSSQL pushdown joins --- .../src/adapter/MssqlQuery.ts | 1 + .../test/unit/mssql-query.test.ts | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) 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/mssql-query.test.ts b/packages/cubejs-schema-compiler/test/unit/mssql-query.test.ts index dfce42f18ef94..9a1c9d7642f9e 100644 --- a/packages/cubejs-schema-compiler/test/unit/mssql-query.test.ts +++ b/packages/cubejs-schema-compiler/test/unit/mssql-query.test.ts @@ -96,6 +96,23 @@ describe('MssqlQuery', () => { const joinedSchemaCompilers = prepareJsCompiler(createJoinedCubesSchema()); + it('renders SQL API pushdown joins after FROM and before WHERE', async () => { + await compiler.compile(); + + const query = new MssqlQuery({ joinGraph, cubeEvaluator, compiler }, { + measures: ['visitors.count'], + }); + + // The SQL API supplies already-rendered joins to statements.select. + // Omitting this loop leaves projected columns referring to absent aliases. + const { select } = query.sqlTemplates().statements; + const joins = '{% for join in joins %}\n{{ join }}{% endfor %}'; + expect(select).toContain(joins); + expect(select.indexOf(joins)).toBeGreaterThan(select.indexOf('FROM {{ from_prepared }}')); + expect(select.indexOf(joins)).toBeGreaterThan(select.indexOf(') AS {{ from_alias }}')); + expect(select.indexOf(joins)).toBeLessThan(select.indexOf('{% if filter %}')); + }); + it('renders a scalar null discriminator for SQL API pushdown sorting', async () => { await compiler.compile(); From 38a1a01507556e1208cca0c2ed31c19f8dd26ab4 Mon Sep 17 00:00:00 2001 From: davidda Date: Tue, 15 Sep 2026 16:47:31 +0200 Subject: [PATCH 2/5] test(schema-compiler): cover select joins across dialects --- .../test/unit/mssql-query.test.ts | 17 ------------- .../test/unit/select-template.test.ts | 25 +++++++++++++++++++ 2 files changed, 25 insertions(+), 17 deletions(-) create mode 100644 packages/cubejs-schema-compiler/test/unit/select-template.test.ts diff --git a/packages/cubejs-schema-compiler/test/unit/mssql-query.test.ts b/packages/cubejs-schema-compiler/test/unit/mssql-query.test.ts index 9a1c9d7642f9e..dfce42f18ef94 100644 --- a/packages/cubejs-schema-compiler/test/unit/mssql-query.test.ts +++ b/packages/cubejs-schema-compiler/test/unit/mssql-query.test.ts @@ -96,23 +96,6 @@ describe('MssqlQuery', () => { const joinedSchemaCompilers = prepareJsCompiler(createJoinedCubesSchema()); - it('renders SQL API pushdown joins after FROM and before WHERE', async () => { - await compiler.compile(); - - const query = new MssqlQuery({ joinGraph, cubeEvaluator, compiler }, { - measures: ['visitors.count'], - }); - - // The SQL API supplies already-rendered joins to statements.select. - // Omitting this loop leaves projected columns referring to absent aliases. - const { select } = query.sqlTemplates().statements; - const joins = '{% for join in joins %}\n{{ join }}{% endfor %}'; - expect(select).toContain(joins); - expect(select.indexOf(joins)).toBeGreaterThan(select.indexOf('FROM {{ from_prepared }}')); - expect(select.indexOf(joins)).toBeGreaterThan(select.indexOf(') AS {{ from_alias }}')); - expect(select.indexOf(joins)).toBeLessThan(select.indexOf('{% if filter %}')); - }); - it('renders a scalar null discriminator for SQL API pushdown sorting', async () => { await compiler.compile(); 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); + }); +}); From e1a804a989fb037a38743929ca2200d09b797f8e Mon Sep 17 00:00:00 2001 From: davidda Date: Wed, 16 Sep 2026 10:10:03 +0200 Subject: [PATCH 3/5] docs(schema-compiler): clarify dialect test discovery scope --- packages/cubejs-schema-compiler/test/unit/allDialects.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/cubejs-schema-compiler/test/unit/allDialects.ts b/packages/cubejs-schema-compiler/test/unit/allDialects.ts index 8b94bdfc6709f..e619e7e7bb841 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-package overrides, including cubejs-questdb-driver's QuestQuery and + * cubejs-pinot-driver's PinotQuery, are outside this scan. * * Paired with its name, which is what a caller needs to report which dialect failed. */ From 5c6a6f0125c9dd31f7d946cda881e307dc3f09d7 Mon Sep 17 00:00:00 2001 From: David Dahlen <4145824+davidda@users.noreply.github.com> Date: Thu, 17 Sep 2026 09:20:26 +0200 Subject: [PATCH 4/5] Update packages/cubejs-schema-compiler/test/unit/allDialects.ts Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- packages/cubejs-schema-compiler/test/unit/allDialects.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cubejs-schema-compiler/test/unit/allDialects.ts b/packages/cubejs-schema-compiler/test/unit/allDialects.ts index e619e7e7bb841..02c7d4c463318 100644 --- a/packages/cubejs-schema-compiler/test/unit/allDialects.ts +++ b/packages/cubejs-schema-compiler/test/unit/allDialects.ts @@ -10,8 +10,8 @@ const MIN_DIALECTS = 10; /** * Every dialect in the schema-compiler adapter directory, discovered rather than listed - * by hand. Driver-package overrides, including cubejs-questdb-driver's QuestQuery and - * cubejs-pinot-driver's PinotQuery, are outside this scan. + * by hand. Driver packages carry Query subclasses outside this scan; the ones that matter + * to a template invariant are those redefining it — today QuestQuery and PinotQuery. * * Paired with its name, which is what a caller needs to report which dialect failed. */ From 3747c8474f84d26f157af0a56c3d78c344a13484 Mon Sep 17 00:00:00 2001 From: davidda Date: Thu, 17 Sep 2026 11:31:15 +0200 Subject: [PATCH 5/5] docs(schema-compiler): clarify template invariant exclusions --- packages/cubejs-schema-compiler/test/unit/allDialects.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cubejs-schema-compiler/test/unit/allDialects.ts b/packages/cubejs-schema-compiler/test/unit/allDialects.ts index 02c7d4c463318..1b9a38a476584 100644 --- a/packages/cubejs-schema-compiler/test/unit/allDialects.ts +++ b/packages/cubejs-schema-compiler/test/unit/allDialects.ts @@ -10,8 +10,8 @@ const MIN_DIALECTS = 10; /** * Every dialect in the schema-compiler adapter directory, discovered rather than listed - * by hand. Driver packages carry Query subclasses outside this scan; the ones that matter - * to a template invariant are those redefining it — today QuestQuery and PinotQuery. + * 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. */