Repository navigation
Commit 5b5cff1
fix(service-analytics): a preset dateRange on a date field counts the reference zone's calendar days, east of UTC too (#27271)
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgKURKuRGF1ewEVVZKx7U1i4)_
Fixes #27172
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:报表的预设日期范围(如“本季度”)落在 `date` 类型字段上时,按参考时区的日历日计数;`datetime`
字段仍按时刻比较。两条查询策略、钻取范围、草稿预览共用同一处取界。
- 为什么改:东八区等 UTC 以东的租户,“本季度”会多算 6 月 30 日、漏掉 9 月 30 日,报表数字静默出错(西半球恰好正确)。
- 风险与回滚:只影响“预设范围 × 已声明为 date 的字段”;显式起止日期、datetime 字段、类型未知字段行为不变。回滚即还原本
PR。
- 先看:`packages/services/service-analytics/src/dataset-executor.ts`(新增
`dateRangeWindowBounds`,复用既有
`inclusiveCalendarDayWindow`)、`strategies/native-sql-strategy.ts` 与
`strategies/objectql-strategy.ts` 的预设分支、`drill-window.ts`。
- 两分钟验证:`pnpm --filter @objectstack/service-analytics exec vitest run
src/__tests__/dataset-drill-window.test.ts`,预期 26 条全过,其中上海 date 字段
this_quarter 两条策略都只列 7/1–9/30 的记录。
- 需要你做的:无,席位落地。
## What changes
A preset `dateRange` (`this_quarter`, `last_30_days`, …) resolves to the
reference zone's midnight instants. On a `date` column those instants
were compared as they are, and the column reads an instant by its
leading UTC day (`temporalStorageForm`), so east of UTC the window slid
one day early: in `Asia/Shanghai`, `this_quarter` counted 2026-06-30 and
dropped 2026-09-30, on both strategies (measured red below). West of UTC
the leading UTC day happens to be the zone's own day.
The triage rule (6465987107): a `date` field compares calendar days in
the reference zone (ADR-0053), a `datetime` compares instants, and the
executor's existing whole-day rule is used, not a new resolver.
- **The existing rule, reused.** `inclusiveCalendarDayWindow` in
`packages/services/service-analytics/src/dataset-executor.ts` already
projects a lowered window onto the calendar days it covers in the
reference zone (through `bucketDateKey`, the exact inverse of the
`zonedDateStartToUtcMs` the preset resolver renders bounds with);
`compareTo` shifts exactly that projection. No new resolver and no new
calendar arithmetic.
- **One spelling for every face.** New `dateRangeWindowBounds(window,
timezone, dateColumn)` beside it: a resolved preset on a `date` column
becomes the inclusive day pair `{ $gte: first, $lte: last }`; on any
other column it stays `{ $gte, $lt }` on the instants; an explicit `[a,
b]` stays `{ $gte, $lte }` bound for bound. It is called by
`NativeSQLStrategy` (compile), `ObjectQLStrategy` (execute and the SQL
echo), `drillWindowBounds` (so drill and count stay on one lowering, as
#27169 set up) and the draft preview's `evaluateAnalyticsQueryOverRows`.
- **Which column is `date`.** New `declaredDateColumn(ctx, target)` in
`strategies/filter-normalizer.ts`, asked of the same `declaredFieldType`
hook and the same storage target as `declaredDatetimeLowering`. A column
whose type the host cannot name answers `false` and keeps the instants
it was filtered by before.
- The `[gte, lt)` drill bound for a `date` window now reads `2026-07-01`
.. `2026-10-01` in Shanghai, the same days the count uses.
Bounded in-place fix, declared: the draft preview face
(`preview-evaluator.ts`, inside the claim's file surface) had the same
defect, measured red on `4d3a077f` (Shanghai counted Jun 30 and dropped
Sep 30); it takes the same helper, pinned by its own test.
## Tests
- `dataset-drill-window.test.ts`: the real-engine case "stage ×
this_quarter on the date field, Shanghai" now asserts the ids (`won: d2
d3 d6 d8`, `lost: d10 d4`) instead of parity only, on both strategies;
added Los Angeles × `date` (west, unchanged days) and Los Angeles ×
`datetime` (instants: d2 out, d5 in), each also checking the drill lists
exactly what the row counted. A unit case pins the engine bounds (`{
$gte: '2026-07-01', $lte: '2026-09-30' }`) and the drill range for
Shanghai and Los Angeles.
- New `preview-preset-date-field.test.ts`: the draft preview on a `date`
field selects Jul 1 through Sep 30 in Shanghai, Los Angeles and UTC; a
`datetime` field keeps the zone's instants.
## Acceptance notes
- `driver-memory`'s cube face (`MemoryAnalyticsService`) is outside this
claim's file surface and has the mirror-image defect: measured
in-process on this branch, `this_quarter` on a `date` field (declared or
not) answers Jul 1 and Sep 30 in Shanghai (right, by text-prefix
ordering of a bare day against the instant) but Sep 30 and Oct 1 in Los
Angeles (drops Jul 1, counts Oct 1). Reported to the seat for its own
card; not touched here.
- An explicit `[a, b]` window keeps the engine's UTC calendar-day
reading on a `datetime` column (ADR-0053 D-D), unchanged and still
pinned.
- A field whose type the host cannot name keeps the instants (the drill
already omits its window under a non-UTC zone).
## Verification
All at head `c9135bdb` on this branch, base `origin/main` `4d3a077f`.
<details><summary>Red first, on base sources (fix restored to
origin/main 4d3a077, tests from this branch)</summary>
```
dataset-drill-window.test.ts: Tests 3 failed | 23 passed (26)
x [#27172] a date field counts and drills the zone's calendar days (Asia/Shanghai):
received { $gte: '2026-06-30T16:00:00.000Z', $lt: '2026-09-30T16:00:00.000Z' }
x stage x this_quarter on the date field, Shanghai (native) won +d1, lost -d4
x stage x this_quarter on the date field, Shanghai (objectql) won +d1, lost -d4
preview-preset-date-field.test.ts (preview-evaluator.ts at 4d3a077): Tests 1 failed | 1 passed
Asia/Shanghai: expected [jul01, sep30], received [jul01, jun30]
```
</details>
<details><summary>Green at c9135bd</summary>
```
pnpm --filter @objectstack/service-analytics test Test Files 195 passed (195) · Tests 4705 passed | 282 skipped VERDICT command-exit 0
pnpm --filter @objectstack/service-analytics typecheck VERDICT command-exit 0
eslint --no-inline-config on the 8 touched files exit 0
```
</details>
<details><summary>Ablation, once (scripts/ablation-replace.mjs; restored
to the HEAD blob 41872f4, git diff HEAD empty)</summary>
Mutation in `dateRangeWindowBounds`: `if (!dateColumn) return { $gte:
window.start, $lt: window.end };` became `if (dateColumn || !dateColumn)
…` (the date arm never taken). Anchor 1 to 0, blob 41872f4 to
6b76ccd50391.
```
Tests 4 failed | 24 passed (28)
x a date field selects Jul 1 through Sep 30, east and west of UTC (preview)
x [#27172] a date field counts and drills the zone's calendar days, east and west of UTC
x stage x this_quarter on the date field, Shanghai (native)
x stage x this_quarter on the date field, Shanghai (objectql)
```
The Los Angeles date and datetime cases stay green under the mutation,
as expected: west of UTC the leading UTC day was already the zone's day.
</details>
<details><summary>Derived gates (dispatch-gates --commands, 60 families)
at c9135bd</summary>
```
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/check-tenant-audit-census.mjs :: exit 0
node scripts/check-tenant-audit-census.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 3
pnpm check:engine-double-contract :: exit 0
pnpm check:error-status-conformance :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
```
`check:dual-build-cjs-loads` exit 3 is PREREQUISITE NOT MET (it reads
every package's dist; only this package's dependency closure was built):
NOT MEASURED, CI's. `check-plugin-teardown-shape --self-test` first
exited 3 on the shallow clone (its pinned fixture commit was
unreachable); after fetching that commit it exits 0, as recorded.
Reconciliation, `dispatch-gates --ran`: 60 derived, 59 run, 1
NOT-MEASURED (derived from the recorded exit 3), 0 UNRUN.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6Caqg6E9ggEvoUcYsbq)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 38bbc2f commit 5b5cff1
9 files changed
Lines changed: 236 additions & 26 deletions
File tree
- .changeset
- packages/services/service-analytics/src
- __tests__
- strategies
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
| 35 | + | |
35 | 36 | | |
36 | 37 | | |
37 | 38 | | |
| |||
233 | 234 | | |
234 | 235 | | |
235 | 236 | | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
236 | 252 | | |
237 | 253 | | |
238 | 254 | | |
| |||
366 | 382 | | |
367 | 383 | | |
368 | 384 | | |
369 | | - | |
370 | | - | |
| 385 | + | |
371 | 386 | | |
372 | 387 | | |
373 | 388 | | |
| |||
393 | 408 | | |
394 | 409 | | |
395 | 410 | | |
396 | | - | |
397 | | - | |
398 | | - | |
399 | | - | |
400 | | - | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
401 | 415 | | |
402 | 416 | | |
403 | | - | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
404 | 433 | | |
405 | 434 | | |
406 | 435 | | |
| |||
417 | 446 | | |
418 | 447 | | |
419 | 448 | | |
420 | | - | |
| 449 | + | |
421 | 450 | | |
422 | 451 | | |
423 | 452 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
702 | 702 | | |
703 | 703 | | |
704 | 704 | | |
| 705 | + | |
| 706 | + | |
| 707 | + | |
| 708 | + | |
| 709 | + | |
| 710 | + | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
| 714 | + | |
| 715 | + | |
| 716 | + | |
| 717 | + | |
| 718 | + | |
| 719 | + | |
| 720 | + | |
| 721 | + | |
| 722 | + | |
| 723 | + | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
| 730 | + | |
| 731 | + | |
| 732 | + | |
| 733 | + | |
| 734 | + | |
| 735 | + | |
| 736 | + | |
| 737 | + | |
| 738 | + | |
| 739 | + | |
| 740 | + | |
| 741 | + | |
| 742 | + | |
| 743 | + | |
705 | 744 | | |
706 | 745 | | |
707 | 746 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
28 | | - | |
29 | | - | |
30 | | - | |
31 | | - | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
32 | 34 | | |
33 | 35 | | |
34 | 36 | | |
| |||
42 | 44 | | |
43 | 45 | | |
44 | 46 | | |
45 | | - | |
| 47 | + | |
46 | 48 | | |
47 | 49 | | |
48 | 50 | | |
| |||
74 | 76 | | |
75 | 77 | | |
76 | 78 | | |
77 | | - | |
78 | | - | |
79 | | - | |
| 79 | + | |
80 | 80 | | |
81 | 81 | | |
82 | 82 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
41 | 41 | | |
42 | 42 | | |
43 | 43 | | |
| 44 | + | |
44 | 45 | | |
45 | 46 | | |
46 | 47 | | |
| |||
826 | 827 | | |
827 | 828 | | |
828 | 829 | | |
829 | | - | |
| 830 | + | |
| 831 | + | |
| 832 | + | |
| 833 | + | |
830 | 834 | | |
831 | 835 | | |
832 | 836 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2331 | 2331 | | |
2332 | 2332 | | |
2333 | 2333 | | |
| 2334 | + | |
| 2335 | + | |
| 2336 | + | |
| 2337 | + | |
| 2338 | + | |
| 2339 | + | |
| 2340 | + | |
| 2341 | + | |
| 2342 | + | |
| 2343 | + | |
| 2344 | + | |
| 2345 | + | |
| 2346 | + | |
| 2347 | + | |
| 2348 | + | |
| 2349 | + | |
| 2350 | + | |
| 2351 | + | |
| 2352 | + | |
| 2353 | + | |
| 2354 | + | |
2334 | 2355 | | |
2335 | 2356 | | |
2336 | 2357 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| 9 | + | |
9 | 10 | | |
10 | 11 | | |
11 | 12 | | |
| |||
51 | 52 | | |
52 | 53 | | |
53 | 54 | | |
| 55 | + | |
54 | 56 | | |
55 | 57 | | |
56 | 58 | | |
| |||
1334 | 1336 | | |
1335 | 1337 | | |
1336 | 1338 | | |
| 1339 | + | |
| 1340 | + | |
| 1341 | + | |
| 1342 | + | |
| 1343 | + | |
| 1344 | + | |
1337 | 1345 | | |
1338 | 1346 | | |
1339 | 1347 | | |
| |||
1450 | 1458 | | |
1451 | 1459 | | |
1452 | 1460 | | |
1453 | | - | |
| 1461 | + | |
| 1462 | + | |
| 1463 | + | |
| 1464 | + | |
| 1465 | + | |
| 1466 | + | |
| 1467 | + | |
1454 | 1468 | | |
1455 | 1469 | | |
1456 | 1470 | | |
| |||
0 commit comments