From eb5f6c06e5e514c1d88892393a0c02cbca4ae6d9 Mon Sep 17 00:00:00 2001 From: davidda Date: Sat, 5 Sep 2026 15:56:16 +0200 Subject: [PATCH] fix(schema-compiler): correct MSSQL pushdown null ordering --- .../src/adapter/MssqlQuery.ts | 2 +- .../test/unit/mssql-query.test.ts | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/cubejs-schema-compiler/src/adapter/MssqlQuery.ts b/packages/cubejs-schema-compiler/src/adapter/MssqlQuery.ts index 0f9503df9d391..066f98797d8a5 100644 --- a/packages/cubejs-schema-compiler/src/adapter/MssqlQuery.ts +++ b/packages/cubejs-schema-compiler/src/adapter/MssqlQuery.ts @@ -312,7 +312,7 @@ export class MssqlQuery extends BaseQuery { // MSSQL uses + for string concatenation instead of || templates.expressions.concat_strings = '{{ strings | join(\' + \' ) }}'; // NOTE: this template contains a comma; two order expressions are being generated - templates.expressions.sort = '{{ expr }} IS NULL {% if nulls_first %}DESC{% else %}ASC{% endif %}, {{ expr }} {% if asc %}ASC{% else %}DESC{% endif %}'; + templates.expressions.sort = 'CASE WHEN {{ expr }} IS NULL THEN 1 ELSE 0 END {% if nulls_first %}DESC{% else %}ASC{% endif %}, {{ expr }} {% if asc %}ASC{% else %}DESC{% endif %}'; // Timestamp constants arrive as ISO-8601 UTC strings ('2021-01-01T00:00:00.000Z'); // CONVERT style 127 is defined as exactly this format (yyyy-mm-ddThh:mi:ss.mmmZ, // "ISO8601 with time zone Z"). The base template renders the value bare, which is 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 8dd435f19f506..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,6 +96,25 @@ describe('MssqlQuery', () => { const joinedSchemaCompilers = prepareJsCompiler(createJoinedCubesSchema()); + it('renders a scalar null discriminator for SQL API pushdown sorting', async () => { + await compiler.compile(); + + const query = new MssqlQuery({ joinGraph, cubeEvaluator, compiler }, { + measures: ['visitors.count'], + dimensions: ['visitors.source'], + }); + + // SQL API pushdown uses expressions.sort, rather than the regular query's + // order_by template. T-SQL rejects a bare IS NULL predicate in ORDER BY and + // can report the subsequent FETCH NEXT clause as the failing syntax. + const { sort } = query.sqlTemplates().expressions; + expect(sort).toContain('CASE WHEN {{ expr }} IS NULL THEN 1 ELSE 0 END'); + // The discriminator's direction depends on null placement, independently + // of the value's direction. NULL maps to 1, so DESC puts NULLs first. + expect(sort).toContain('END {% if nulls_first %}DESC{% else %}ASC{% endif %},'); + expect(sort).toContain('{{ expr }} {% if asc %}ASC{% else %}DESC{% endif %}'); + }); + it('should group by the created_at field on the calculated granularity for unbounded trailing windows', () => compiler.compile().then(() => { const query = new MssqlQuery(