Skip to content

Commit 8821a8d

Browse files
committed
test(service-analytics): a duration sum's empty-group 0 is answered in the statement, and the all-NULL group sorts and pages as it
Main now answers a measure's empty-group identity in the native statement (COALESCE) instead of folding it after the rows come back, and orders with NULLS LAST. The duration measure's statement takes the same wrap, so: - the exact-SQL cases expect COALESCE(SUM(...), 0) on the filtered sum; - the presentation case's client hands back the statement's 0; - the live ordering case no longer leaves out group c (no record carrying both columns): ascending it pages as b (-3599.25), c (0), a (91800) on SQLite and PostgreSQL alike. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AKPixnkg91qCkQCVFDJuqW
1 parent b0bf2f6 commit 8821a8d

2 files changed

Lines changed: 31 additions & 19 deletions

File tree

‎packages/services/service-analytics/src/__tests__/dataset-duration-measure.test.ts‎

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -264,21 +264,29 @@ for (const cell of CELLS) {
264264
const total = await services.native!.queryDataset(DATASET, { measures: ['avg_hours', 'total_seconds'] } as any);
265265
expect(total.rows).toEqual([{ avg_hours: aggregate(ROWS, 'avg', 'hours'), total_seconds: aggregate(ROWS, 'sum', 'seconds') }]);
266266

267-
// Group `c` is left out: its sum is SQL NULL until the strategy folds it
268-
// to 0, and the dialects order a NULL at opposite ends.
269-
const run = await counted(() =>
270-
services.native!.queryDataset(DATASET, {
271-
measures: ['total_seconds'],
272-
dimensions: ['category'],
273-
runtimeFilter: { category: { $ne: 'c' } },
274-
order: { total_seconds: 'asc' },
275-
limit: 1,
276-
} as any),
277-
);
278-
expect(run.error, run.error?.message).toBeUndefined();
279-
expect(run.rawSql).toBe(1);
267+
// [#25907] Group `c` has no record carrying both columns. Its sum is
268+
// answered as 0 in the statement, so it sorts and pages as 0 on both
269+
// dialects: ascending it is b (-3599.25), c (0), a (91800). Folded after
270+
// the rows came back, SQLite ordered the NULL first and PostgreSQL last.
271+
const page = (offset: number) =>
272+
counted(() =>
273+
services.native!.queryDataset(DATASET, {
274+
measures: ['total_seconds'],
275+
dimensions: ['category'],
276+
order: { total_seconds: 'asc' },
277+
limit: 1,
278+
offset,
279+
} as any),
280+
);
281+
const first = await page(0);
282+
expect(first.error, first.error?.message).toBeUndefined();
283+
expect(first.rawSql).toBe(1);
280284
expect(reads.statements[reads.statements.length - 1]).toContain('ORDER BY "total_seconds" ASC');
281-
expect(run.value!.rows).toEqual([{ category: 'b', total_seconds: -3599.25 }]);
285+
expect(reads.statements[reads.statements.length - 1]).toContain('COALESCE(SUM(');
286+
expect(first.value!.rows).toEqual([{ category: 'b', total_seconds: -3599.25 }]);
287+
const second = await page(1);
288+
expect(second.error, second.error?.message).toBeUndefined();
289+
expect(second.value!.rows).toEqual([{ category: 'c', total_seconds: 0 }]);
282290
});
283291

284292
it('the cube door serves the registered dataset by name, with the same numbers', async () => {

‎packages/services/service-analytics/src/__tests__/native-sql-duration-measure.test.ts‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,19 +68,21 @@ describe('[#25778] NativeSQLStrategy — the duration measure as a statement', (
6868
[
6969
'sqlite',
7070
'AVG(((ROUND((julianday(resolved_at) - julianday(opened_at)) * 86400000.0) / 1000.0) / 3600.0)) AS "avg_hours"',
71-
'SUM(CASE WHEN priority = $1 THEN (ROUND((julianday(resolved_at) - julianday(opened_at)) * 86400000.0) / 1000.0) END) AS "total_seconds"',
71+
'COALESCE(SUM(CASE WHEN priority = $1 THEN (ROUND((julianday(resolved_at) - julianday(opened_at)) * 86400000.0) / 1000.0) END), 0) AS "total_seconds"',
7272
],
7373
[
7474
'postgres',
7575
'AVG((CAST(EXTRACT(EPOCH FROM (resolved_at - opened_at)) AS DOUBLE PRECISION) / 3600.0)) AS "avg_hours"',
76-
'SUM(CASE WHEN priority = $1 THEN CAST(EXTRACT(EPOCH FROM (resolved_at - opened_at)) AS DOUBLE PRECISION) END) AS "total_seconds"',
76+
'COALESCE(SUM(CASE WHEN priority = $1 THEN CAST(EXTRACT(EPOCH FROM (resolved_at - opened_at)) AS DOUBLE PRECISION) END), 0) AS "total_seconds"',
7777
],
7878
[
7979
'mysql',
8080
'AVG(((TIMESTAMPDIFF(MICROSECOND, opened_at, resolved_at) / 1.0E6) / 3600.0)) AS "avg_hours"',
81-
'SUM(CASE WHEN priority = $1 THEN (TIMESTAMPDIFF(MICROSECOND, opened_at, resolved_at) / 1.0E6) END) AS "total_seconds"',
81+
'COALESCE(SUM(CASE WHEN priority = $1 THEN (TIMESTAMPDIFF(MICROSECOND, opened_at, resolved_at) / 1.0E6) END), 0) AS "total_seconds"',
8282
],
8383
])('%s: the per-dialect difference, divided by the unit, under the aggregate and its measure filter', async (dialect, avg, filteredSum) => {
84+
// [#25907] The `sum` answers its empty-group identity `0` in the statement,
85+
// where the order and the window read it; the `avg` has none and keeps NULL.
8486
const { sql, params } = await native.generateSql(q(), nativeCtx({ dialect }));
8587
expect(sql).toBe(`SELECT category AS "category", ${avg}, ${filteredSum} FROM "${OBJECT}" GROUP BY category`);
8688
expect(params).toEqual(['high']);
@@ -110,10 +112,12 @@ describe('[#25778] NativeSQLStrategy — the duration measure as a statement', (
110112
for (const dialect of ['sqlite', 'postgres', 'mysql']) expect(native.canHandle(q(), nativeCtx({ dialect })), dialect).toBe(true);
111113
});
112114

113-
it('presents a client string as a number, and folds an empty sum to 0 while an empty avg stays null', async () => {
115+
it('presents a client string as a number; an empty sum arrives as the statement\'s 0 and an empty avg stays null', async () => {
116+
// [#25907] Category `c` has no record carrying both columns: the statement
117+
// answered `COALESCE(SUM(…), 0)` (above), so the client hands back `'0'`.
114118
const rows = [
115119
{ category: 'a', avg_hours: '12.75', total_seconds: '91800' },
116-
{ category: 'c', avg_hours: null, total_seconds: null },
120+
{ category: 'c', avg_hours: null, total_seconds: '0' },
117121
];
118122
const res = await native.execute(q(), nativeCtx({ dialect: 'postgres', rows }));
119123
expect(res.rows).toEqual([

0 commit comments

Comments
 (0)