|
26 | 26 | * - another grid dimension is filled per combination that holds a row; |
27 | 27 | * - a non-UTC zone: the keys the fill writes are the keys the query writes; |
28 | 28 | * - the controls — a full window, a top-N, an order on a measure, no time |
29 | | - * dimension, `compareTo` — come back as they always have; |
30 | | - * - the bound: a fill past `MAX_ZERO_FILLED_GRID_ROWS` does not run at all. |
| 29 | + * dimension — come back as they always have; |
| 30 | + * - the bound: a fill past `MAX_ZERO_FILLED_GRID_ROWS` does not run at all; |
| 31 | + * - #27111, `compareTo`: the same fill, over the PRIMARY window, adds the |
| 32 | + * buckets empty in both windows with `0` in both the measure and its |
| 33 | + * `__compare`; a bucket either window holds is unchanged, and the guards |
| 34 | + * (no axis `dateRange`, a `limit`, an order on a `__compare`) hold. |
31 | 35 | */ |
32 | 36 |
|
33 | 37 | import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; |
@@ -354,29 +358,160 @@ describe('[#26893] controls — answers the fill must not change', () => { |
354 | 358 | expect(res.rows.map((r) => `${r.owner}|${r.opp_count}`).sort()).toEqual(['ann|10', 'bob|5']); |
355 | 359 | }); |
356 | 360 |
|
357 | | - it('compareTo keeps its shape: its empty buckets are not filled', async () => { |
| 361 | + it(`a fill past ${MAX_ZERO_FILLED_GRID_ROWS} rows does not run at all — never a partial fill`, async () => { |
358 | 362 | const res = await run({ |
359 | 363 | dimensions: ['close_date'], |
360 | | - measures: ['total_amount'], |
| 364 | + measures: ['opp_count'], |
| 365 | + dateGranularity: 'day', |
| 366 | + runtimeFilter: WON, |
| 367 | + timeDimensions: [{ dimension: 'close_date', dateRange: ['2017-01-01', '2026-12-31'] }], |
| 368 | + }); |
| 369 | + expect(res.rows).toHaveLength(DEALS.filter((d) => d.stage === 'closed_won').length); |
| 370 | + }); |
| 371 | +}); |
| 372 | + |
| 373 | +describe('[#27111] compareTo: a bucket empty in both windows is filled, over the primary window', () => { |
| 374 | + it('May–August vs the previous year: May and August come back, with 0 in the measure and in its __compare', async () => { |
| 375 | + const res = await run({ |
| 376 | + dimensions: ['close_date'], |
| 377 | + measures: ['opp_count', 'total_amount', 'avg_amount'], |
361 | 378 | runtimeFilter: WON, |
362 | 379 | timeDimensions: [{ dimension: 'close_date', dateRange: ['2026-05-01', '2026-08-31'] }], |
363 | 380 | compareTo: { kind: 'previousYear' }, |
364 | 381 | }); |
365 | | - // May and August hold nothing in either window (2025-05..08 holds no |
366 | | - // deal), and stay absent: the fill does not run under `compareTo`. |
367 | | - expect(series(res.rows)).toEqual([['2026-06', 70], ['2026-07', 80]]); |
368 | | - expect(res.rows.map((r) => r.total_amount__compare)).toEqual([0, 0]); |
| 382 | + // 2025-05..08 holds no deal, and 2026 holds June and July only. |
| 383 | + expect(series(res.rows)).toEqual([['2026-05', 0], ['2026-06', 70], ['2026-07', 80], ['2026-08', 0]]); |
| 384 | + expect(res.rows.map((r) => r.total_amount__compare)).toEqual([0, 0, 0, 0]); |
| 385 | + for (const month of ['2026-05', '2026-08']) { |
| 386 | + const row = res.rows.find((r) => r.close_date === month)!; |
| 387 | + expect(row.opp_count).toBe(0); |
| 388 | + expect(row.opp_count__compare).toBe(0); |
| 389 | + // avg is an absent key on both sides, the way an empty group reads it. |
| 390 | + expect('avg_amount' in row).toBe(false); |
| 391 | + expect('avg_amount__compare' in row).toBe(false); |
| 392 | + } |
369 | 393 | }); |
| 394 | +}); |
370 | 395 |
|
371 | | - it(`a fill past ${MAX_ZERO_FILLED_GRID_ROWS} rows does not run at all — never a partial fill`, async () => { |
| 396 | +/** |
| 397 | + * A compare-only fixture, its own stage: March–August 2026 against |
| 398 | + * March–August 2025, one month in each of the four states. |
| 399 | + * |
| 400 | + * | month | 2026 (primary) | 2025 (compare) | |
| 401 | + * |:--|:--|:--| |
| 402 | + * | 03 | 1 deal | 1 deal | |
| 403 | + * | 04 | 1 deal | — | |
| 404 | + * | 05 | — | 1 deal | |
| 405 | + * | 06 | — | — | |
| 406 | + * | 07 | 1 deal | — | |
| 407 | + * | 08 | — | — | |
| 408 | + * |
| 409 | + * `closed_at` dates the same deals for the window-only anchor case: 2026's |
| 410 | + * deals close in 2026 and 2025's in 2025, so anchoring on `closed_at` while |
| 411 | + * the grid buckets `close_date` gives a series with no axis window. |
| 412 | + */ |
| 413 | +const CMP = { stage: 'cmp_probe' }; |
| 414 | +const CMP_DEALS = [ |
| 415 | + { id: 'c1', close_date: '2026-03-10', closed_at: '2026-03-10T12:00:00.000Z', amount: 4 }, |
| 416 | + { id: 'c2', close_date: '2026-04-10', closed_at: '2026-04-10T12:00:00.000Z', amount: 8 }, |
| 417 | + { id: 'c3', close_date: '2026-07-10', closed_at: '2026-07-10T12:00:00.000Z', amount: 16 }, |
| 418 | + { id: 'c4', close_date: '2025-03-10', closed_at: '2025-03-10T12:00:00.000Z', amount: 1 }, |
| 419 | + { id: 'c5', close_date: '2025-05-10', closed_at: '2025-05-10T12:00:00.000Z', amount: 2 }, |
| 420 | +]; |
| 421 | +const CMP_WINDOW: [string, string] = ['2026-03-01', '2026-08-31']; |
| 422 | +const cells = (rows: Record<string, unknown>[]) => |
| 423 | + rows.map((r) => `${r.close_date}|${r.opp_count}|${r.opp_count__compare}`); |
| 424 | + |
| 425 | +describe('[#27111] compareTo: only the both-empty buckets are added; every other answer is unchanged', () => { |
| 426 | + beforeAll(async () => { |
| 427 | + for (const deal of CMP_DEALS) await engine.insert(DEAL, { ...deal, ...CMP, owner: 'ann' } as any); |
| 428 | + }); |
| 429 | + |
| 430 | + it('a bucket either window holds keeps the merge\'s values; June and August are added at 0 / 0', async () => { |
| 431 | + const res = await run({ |
| 432 | + dimensions: ['close_date'], |
| 433 | + measures: ['opp_count', 'total_amount'], |
| 434 | + runtimeFilter: CMP, |
| 435 | + timeDimensions: [{ dimension: 'close_date', dateRange: CMP_WINDOW }], |
| 436 | + compareTo: { kind: 'previousYear' }, |
| 437 | + }); |
| 438 | + expect(cells(res.rows)).toEqual([ |
| 439 | + '2026-03|1|1', '2026-04|1|0', '2026-05|0|1', '2026-06|0|0', '2026-07|1|0', '2026-08|0|0', |
| 440 | + ]); |
| 441 | + expect(res.rows.map((r) => [r.total_amount, r.total_amount__compare].map(Number))).toEqual([ |
| 442 | + [4, 1], [8, 0], [0, 2], [0, 0], [16, 0], [0, 0], |
| 443 | + ]); |
| 444 | + }); |
| 445 | + |
| 446 | + it('the primary columns are the plain series\' answer: one fill rule', async () => { |
| 447 | + const selection: DatasetSelection = { |
| 448 | + dimensions: ['close_date'], |
| 449 | + measures: ['opp_count', 'total_amount'], |
| 450 | + runtimeFilter: CMP, |
| 451 | + timeDimensions: [{ dimension: 'close_date', dateRange: CMP_WINDOW }], |
| 452 | + }; |
| 453 | + const plain = await run(selection); |
| 454 | + const compared = await run({ ...selection, compareTo: { kind: 'previousYear' } }); |
| 455 | + const primary = (rows: Record<string, unknown>[]) => |
| 456 | + rows.map((r) => [r.close_date, r.opp_count, Number(r.total_amount)]); |
| 457 | + expect(primary(compared.rows)).toEqual(primary(plain.rows)); |
| 458 | + expect(plain.rows.map((r) => r.close_date)).toEqual([ |
| 459 | + '2026-03', '2026-04', '2026-05', '2026-06', '2026-07', '2026-08', |
| 460 | + ]); |
| 461 | + }); |
| 462 | + |
| 463 | + it('previousPeriod aligns by ordinal and fills the same way', async () => { |
| 464 | + // June–August 2026 against March–May 2026: 03↔06, 04↔07, 05↔08. |
372 | 465 | const res = await run({ |
373 | 466 | dimensions: ['close_date'], |
374 | 467 | measures: ['opp_count'], |
375 | | - dateGranularity: 'day', |
376 | | - runtimeFilter: WON, |
377 | | - timeDimensions: [{ dimension: 'close_date', dateRange: ['2017-01-01', '2026-12-31'] }], |
| 468 | + runtimeFilter: CMP, |
| 469 | + timeDimensions: [{ dimension: 'close_date', dateRange: ['2026-06-01', '2026-08-31'] }], |
| 470 | + compareTo: { kind: 'previousPeriod' }, |
378 | 471 | }); |
379 | | - expect(res.rows).toHaveLength(DEALS.filter((d) => d.stage === 'closed_won').length); |
| 472 | + expect(cells(res.rows)).toEqual(['2026-06|0|1', '2026-07|1|1', '2026-08|0|0']); |
| 473 | + }); |
| 474 | + |
| 475 | + it('an axis with no dateRange of its own (a window-only anchor) fills interior gaps only, no edges', async () => { |
| 476 | + const res = await run({ |
| 477 | + dimensions: ['close_date'], |
| 478 | + measures: ['opp_count'], |
| 479 | + runtimeFilter: CMP, |
| 480 | + timeDimensions: [{ dimension: 'closed_at', dateRange: ['2026-01-01', '2026-12-31'] }], |
| 481 | + compareTo: { kind: 'previousYear', dimension: 'closed_at' }, |
| 482 | + }); |
| 483 | + // The anchor is not a grid column, so the comparison rows key by their |
| 484 | + // own close_date (2025-03, 2025-05). Between the first and last bucket |
| 485 | + // the grid holds, every month; nothing before 2025-03 or after 2026-07. |
| 486 | + expect(cells(res.rows)).toEqual([ |
| 487 | + '2025-03|0|1', '2025-04|0|0', '2025-05|0|1', '2025-06|0|0', '2025-07|0|0', '2025-08|0|0', |
| 488 | + '2025-09|0|0', '2025-10|0|0', '2025-11|0|0', '2025-12|0|0', '2026-01|0|0', '2026-02|0|0', |
| 489 | + '2026-03|1|0', '2026-04|1|0', '2026-05|0|0', '2026-06|0|0', '2026-07|1|0', |
| 490 | + ]); |
| 491 | + }); |
| 492 | + |
| 493 | + it('a limit with compareTo is not filled', async () => { |
| 494 | + const res = await run({ |
| 495 | + dimensions: ['close_date'], |
| 496 | + measures: ['opp_count'], |
| 497 | + runtimeFilter: CMP, |
| 498 | + timeDimensions: [{ dimension: 'close_date', dateRange: CMP_WINDOW }], |
| 499 | + compareTo: { kind: 'previousYear' }, |
| 500 | + limit: 10, |
| 501 | + }); |
| 502 | + expect(cells(res.rows)).toEqual(['2026-03|1|1', '2026-04|1|0', '2026-05|0|1', '2026-07|1|0']); |
| 503 | + }); |
| 504 | + |
| 505 | + it('an order on a __compare column is not filled', async () => { |
| 506 | + const res = await run({ |
| 507 | + dimensions: ['close_date'], |
| 508 | + measures: ['opp_count'], |
| 509 | + runtimeFilter: CMP, |
| 510 | + timeDimensions: [{ dimension: 'close_date', dateRange: CMP_WINDOW }], |
| 511 | + compareTo: { kind: 'previousYear' }, |
| 512 | + order: { opp_count__compare: 'desc' }, |
| 513 | + }); |
| 514 | + expect(res.rows.map((r) => r.close_date).sort()).toEqual(['2026-03', '2026-04', '2026-05', '2026-07']); |
380 | 515 | }); |
381 | 516 | }); |
382 | 517 |
|
|
0 commit comments