Skip to content

Commit 1e37d1f

Browse files
authored
chore(tesseract): MSSQL driver tests (cube-js#10588)
1 parent e94781a commit 1e37d1f

5 files changed

Lines changed: 1154 additions & 82 deletions

File tree

.github/workflows/drivers-tests.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,8 @@ jobs:
296296
use_tesseract_sql_planner: true
297297
- database: clickhouse
298298
use_tesseract_sql_planner: true
299+
- database: mssql
300+
use_tesseract_sql_planner: true
299301
fail-fast: false
300302

301303
steps:

packages/cubejs-schema-compiler/src/adapter/MssqlQuery.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -268,11 +268,15 @@ export class MssqlQuery extends BaseQuery {
268268
const templates = super.sqlTemplates();
269269
templates.functions.LEAST = 'LEAST({{ args_concat }})';
270270
templates.functions.GREATEST = 'GREATEST({{ args_concat }})';
271+
// MSSQL ROUND requires 2 arguments: ROUND(number, length)
272+
templates.functions.ROUND = 'ROUND({{ args_concat }}{% if args | length < 2 %}, 0{% endif %})';
271273
// NOTE: MSSQL does not support DISTINCT clause. No workaround is available
272274
delete templates.functions.STRING_AGG;
273275
// PERCENTILE_CONT works but requires PARTITION BY
274276
delete templates.functions.PERCENTILECONT;
275277
delete templates.expressions.ilike;
278+
// MSSQL uses + for string concatenation instead of ||
279+
templates.expressions.concat_strings = '{{ strings | join(\' + \' ) }}';
276280
// NOTE: this template contains a comma; two order expressions are being generated
277281
templates.expressions.sort = '{{ expr }} IS NULL {% if nulls_first %}DESC{% else %}ASC{% endif %}, {{ expr }} {% if asc %}ASC{% else %}DESC{% endif %}';
278282
templates.types.string = 'VARCHAR';
@@ -298,11 +302,37 @@ export class MssqlQuery extends BaseQuery {
298302
'{% if not loop.last %}, {% endif %}' +
299303
'{% endfor %}' +
300304
') AS dates (date_from, date_to)';
305+
// MSSQL uses recursive CTE for time series generation.
306+
// The template body becomes content of `time_series AS (...)` CTE,
307+
// so it self-references `time_series` for recursion.
308+
templates.statements.generated_time_series_select =
309+
'SELECT CAST({{ start }} AS DATETIME2) AS date_from,\n' +
310+
' DATEADD(MILLISECOND, -1, DATEADD({{ minimal_time_unit }}, 1, CAST({{ start }} AS DATETIME2))) AS date_to\n' +
311+
'UNION ALL\n' +
312+
'SELECT DATEADD({{ minimal_time_unit }}, 1, date_from),\n' +
313+
' DATEADD(MILLISECOND, -1, DATEADD({{ minimal_time_unit }}, 1, DATEADD({{ minimal_time_unit }}, 1, date_from)))\n' +
314+
'FROM time_series\n' +
315+
'WHERE DATEADD({{ minimal_time_unit }}, 1, date_from) <= CAST({{ end }} AS DATETIME2)';
316+
317+
templates.statements.generated_time_series_with_cte_range_source =
318+
'SELECT {{ range_source }}.{{ min_name }} AS date_from,\n' +
319+
' DATEADD(MILLISECOND, -1, DATEADD({{ minimal_time_unit }}, 1, {{ range_source }}.{{ min_name }})) AS date_to,\n' +
320+
' {{ range_source }}.{{ max_name }} AS max_date\n' +
321+
'FROM {{ range_source }}\n' +
322+
'UNION ALL\n' +
323+
'SELECT DATEADD({{ minimal_time_unit }}, 1, date_from),\n' +
324+
' DATEADD(MILLISECOND, -1, DATEADD({{ minimal_time_unit }}, 1, DATEADD({{ minimal_time_unit }}, 1, date_from))),\n' +
325+
' max_date\n' +
326+
'FROM time_series\n' +
327+
'WHERE DATEADD({{ minimal_time_unit }}, 1, date_from) <= max_date';
328+
301329
// MSSQL uses OFFSET/FETCH instead of LIMIT/OFFSET
330+
templates.tesseract.ilike = 'LOWER({{ expr }}) {% if negated %}NOT {% endif %}LIKE LOWER({{ pattern }})';
331+
templates.filters.like_pattern = 'CONCAT({% if start_wild %}\'%\'{% else %}\'\'{% endif %}, LOWER({{ value }}), {% if end_wild %}\'%\'{% else %}\'\'{% endif %})';
302332
templates.statements.select = '{% if ctes %} WITH \n' +
303333
'{{ ctes | join(\',\n\') }}\n' +
304334
'{% endif %}' +
305-
'SELECT {% if distinct %}DISTINCT {% endif %}' +
335+
'SELECT {% if limit is not none and not order_by %}TOP {{ limit }} {% endif %}{% if distinct %}DISTINCT {% endif %}' +
306336
'{{ select_concat | map(attribute=\'aliased\') | join(\', \') }} {% if from %}\n' +
307337
'FROM (\n' +
308338
'{{ from | indent(2, true) }}\n' +
@@ -312,8 +342,9 @@ export class MssqlQuery extends BaseQuery {
312342
'{% if filter %}\nWHERE {{ filter }}{% endif %}' +
313343
'{% if group_by %}\nGROUP BY {{ group_by }}{% endif %}' +
314344
'{% if having %}\nHAVING {{ having }}{% endif %}' +
315-
'{% if order_by %}\nORDER BY {{ order_by | map(attribute=\'expr\') | join(\', \') }}\nOFFSET {% if offset is not none %}{{ offset }}{% else %}0{% endif %} ROWS{% endif %}' +
316-
'{% if limit is not none %}\nFETCH NEXT {{ limit }} ROWS ONLY{% endif %}';
345+
'{% if order_by %}\nORDER BY {{ order_by | map(attribute=\'expr\') | join(\', \') }}\nOFFSET {% if offset is not none %}{{ offset }}{% else %}0{% endif %} ROWS' +
346+
'\nFETCH NEXT {% if limit is not none %}{{ limit }}{% else %}2147483647{% endif %} ROWS ONLY{% endif %}' +
347+
'{% if ctes %}\nOPTION (MAXRECURSION 0){% endif %}';
317348
return templates;
318349
}
319350
}

packages/cubejs-testing-drivers/fixtures/_schemas.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@
441441
{
442442
"name": "percentageOfTotalForStatus",
443443
"type": "number",
444-
"sql": "ROUND(100 * {totalProfit} / NULLIF({totalProfitForStatus}, 0))",
444+
"sql": "ROUND(100 * {totalProfit} / NULLIF({totalProfitForStatus}, 0), 0)",
445445
"multi_stage": true
446446
},
447447
{

packages/cubejs-testing-drivers/fixtures/mssql.json

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,5 +183,74 @@
183183
"---------------------------------------",
184184
"SQL API: Rolling Window YTD (year + month + day + date_trunc equal)",
185185
"SQL API: Rolling Window YTD (year + month + day + date_trunc IN)"
186+
],
187+
"tesseractSkip": [
188+
"---------------------------------------",
189+
"SKIPPED FOR MS SQL (total not supported)",
190+
"---------------------------------------",
191+
"querying Customers: dimensions + total",
192+
"querying Customers: dimensions + order + limit + total",
193+
"querying Customers: dimensions + order + total + offset",
194+
"querying Customers: dimensions + order + limit + total + offset",
195+
"querying Products: dimensions + order + total",
196+
"querying Products: dimensions + order + limit + total",
197+
"querying ECommerce: dimensions + total",
198+
"querying ECommerce: dimensions + order + limit + total",
199+
"querying ECommerce: dimensions + order + total + offset",
200+
"querying ECommerce: dimensions + order + limit + total + offset",
201+
202+
"---------------------------------------",
203+
"SKIPPED FOR ALL ",
204+
"---------------------------------------",
205+
"querying Products: dimensions -- doesn't work wo ordering",
206+
"querying ECommerce: total quantity, avg discount, total sales, total profit by product + order + total -- rounding in athena",
207+
"querying ECommerce: total sales, total profit by month + order (date) + total -- doesn't work with the BigQuery",
208+
"querying ECommerce: total quantity, avg discount, total sales, total profit by product + order + total -- noisy test",
209+
"querying BigECommerce: null sum",
210+
"querying BigECommerce: null boolean",
211+
"---------------------------------------",
212+
"SKIPPED SQL API (Need work)",
213+
"---------------------------------------",
214+
"SQL API: reuse params",
215+
"SQL API: post-aggregate percentage of total",
216+
"SQL API: powerbi min max ungrouped flag",
217+
"SQL API: powerbi min max push down",
218+
"SQL API: Simple Rollup",
219+
"SQL API: Complex Rollup",
220+
"SQL API: Nested Rollup",
221+
"SQL API: Rollup with aliases",
222+
"SQL API: Rollup over exprs",
223+
"SQL API: Nested Rollup with aliases",
224+
"SQL API: Nested Rollup over asterisk",
225+
"SQL API: Extended nested Rollup over asterisk",
226+
"SQL API: ungrouped pre-agg",
227+
"SQL API: NULLS FIRST/LAST SQL push down",
228+
"SQL API: SQL push down push to cube quoted alias",
229+
"SQL API: Date/time comparison with SQL push down",
230+
"SQL API: Date/time comparison with date_trunc with SQL push down",
231+
232+
"---------------------------------------",
233+
"Error during rewrite: Can't detect Cube query and it may be not supported yet.",
234+
"---------------------------------------",
235+
"SQL API: Rolling Window YTD (year + month + day + date_trunc equal)",
236+
"SQL API: Rolling Window YTD (year + month + day + date_trunc IN)",
237+
"---------------------------------------",
238+
"Switch dimensions",
239+
"---------------------------------------",
240+
"querying SwitchSourceTest: simple cross join",
241+
"querying SwitchSourceTest: full cross join",
242+
"querying SwitchSourceTest: filter by switch dimensions",
243+
244+
"-------",
245+
"querying BigECommerce: rolling window YTD (month + week)",
246+
"querying BigECommerce: rolling window YTD (month + week + no gran)",
247+
"querying BigECommerce: rolling window YTD without granularity",
248+
"querying BigECommerce with Retail Calendar: totalCountRetailMonthAgo",
249+
"querying BigECommerce with Retail Calendar: totalCountRetailWeekAgo",
250+
"SQL API: Timeshift measure from cube",
251+
"querying BigECommerce: rolling window YTD (month + week + day + no gran)",
252+
"SQL API: Timeshift measure from cube",
253+
"querying custom granularities ECommerce: count by two_mo_by_feb + no dimension + rollingCountByLeading without date range",
254+
"querying BigECommerce: rolling window YTD (month + week + day)"
186255
]
187256
}

0 commit comments

Comments
 (0)