Skip to content

feat(analytics): timezone-aware date bucketing (ADR-0053 Phase 2) (#1982) - #2001

Merged
os-zhuang merged 1 commit into
mainfrom
feat/tz-analytics-bucketing-1982
Jun 17, 2026
Merged

feat(analytics): timezone-aware date bucketing (ADR-0053 Phase 2) (#1982)#2001
os-zhuang merged 1 commit into
mainfrom
feat/tz-analytics-bucketing-1982

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Part of ADR-0053 Phase 2 (Slice 5 of 6 — compute-tz).Closes#1982. Design: #1975 · Parent: #1928.

Makes analytics date bucketing timezone-aware: day/week/month/quarter/year buckets resolve on a reference timezone's calendar days, so a row near a tz day-boundary lands in the bucket a user in that zone would expect — identically on SQLite and Postgres.

Decision D2 — bucket in-memory, uniformly

Native driver date bucketing (date_trunc) is UTC-only: SQLite has no tz database and MySQL needs tz tables loaded, so pushing tz-aware bucketing down splits boundaries per dialect. So engine.aggregate({ timezone })forces the in-memory aggregation path when a non-UTC reference tz is set — the date-range where still goes to the driver (only matching rows are fetched), but bucketing runs uniformly in JS. UTC / unset keeps the native driver fast path unchanged.

Changes

  • @objectstack/core — new shared calendarPartsInTz / calendarPartsInTzOrUtc util (the y/m/d an instant falls on in a zone). DST-safe via Intl.DateTimeFormat().formatToParts() — never hand-rolled offset math. Falls back to the UTC calendar day for an unset / 'UTC' / invalid zone. Lives in core because both objectql and service-analytics need it and both already depend on core.
  • @objectstack/objectqlbucketDateValue / applyInMemoryAggregation take a timezone; engine.aggregate routes non-UTC tz through the in-memory path and threads tz down.
  • @objectstack/specEngineAggregateOptions.timezone + StrategyContext.executeAggregate({ timezone }).
  • @objectstack/service-analyticsObjectQLStrategy forwards query.timezone; the auto-bridge passes it to engine.aggregate; the draft-preview evaluator's bucketDate is tz-aware. formatDateBucket stays UTC by design (it re-labels values already bucketed upstream; re-applying tz there would shift a correct day bucket).

Acceptance criteria

  • Day/week/month/quarter buckets align to the reference tz, identically on SQLite and Postgres (uniform in-memory path).
  • tz unset / 'UTC' → DB-side fast path, behavior unchanged.
  • Test: a row near a tz day-boundary lands in the correct bucket under a non-UTC reference tz (week boundary crossing a Monday covered too).

Tests

  • in-memory-aggregation.test.ts — tz day/month/quarter/week bucketing + grouping; UTC/invalid fallback.
  • engine-aggregate-timezone.test.ts — routing: UTC/unset take native path, non-UTC forces in-memory with correct buckets.
  • preview-evaluator.test.tsbucketDate reference-zone resolution.
  • Full suites green: objectql 645, core 284, service-analytics 125. DTS builds type-check clean.

Depends on

Slices 1 (timezone resolver #1978) and 3 (#1980) — both merged.

🤖 Generated with Claude Code

Day/week/month/quarter/year buckets resolve on a reference timezone's
calendar days, so a row near a tz day-boundary lands in the bucket a user
in that zone expects — identically on SQLite and Postgres.
Per decision D2, non-UTC bucketing runs in-memory uniformly rather than
emitting dialect-specific `date_trunc … AT TIME ZONE` (SQLite/MySQL lack
loaded tz data → cross-driver boundary skew). `engine.aggregate({ timezone })`
forces the in-memory path for a non-UTC zone; the date-range `where` still
goes to the driver. UTC / unset keeps the native fast path unchanged.
- New shared DST-safe `calendarPartsInTz`/`calendarPartsInTzOrUtc` in
@objectstack/core (Intl-based; falls back to UTC for unset/UTC/invalid).
- Thread the reference tz: EngineAggregateOptions → analytics executeAggregate
bridge / ObjectQLStrategy → applyInMemoryAggregation → bucketDateValue, plus
the draft-preview evaluator's bucketDate.
- formatDateBucket stays UTC by design (re-labels already-bucketed values).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercelBot commented Jun 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJun 17, 2026 4:32am

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:data tests tooling size/m labels Jun 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/core, @objectstack/objectql, packages/services, @objectstack/spec.

98 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/cloud-artifact-api.mdx(via packages/spec)
  • content/docs/concepts/cluster-semantics.mdx(via @objectstack/spec)
  • content/docs/concepts/core/index.mdx(via @objectstack/core)
  • content/docs/concepts/core/plugins.mdx(via @objectstack/core)
  • content/docs/concepts/core/services.mdx(via @objectstack/core, @objectstack/objectql)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/implementation-status.mdx(via @objectstack/core, @objectstack/objectql, @objectstack/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/objectql, packages/spec)
  • content/docs/concepts/north-star.mdx(via packages/core, packages/spec)
  • content/docs/concepts/packages.mdx(via @objectstack/core, @objectstack/objectql, @objectstack/spec)
  • content/docs/concepts/setup-app.mdx(via @objectstack/spec)
  • content/docs/concepts/skills.mdx(via @objectstack/spec)
  • content/docs/concepts/webhook-delivery.mdx(via @objectstack/core, @objectstack/spec)
  • content/docs/getting-started/architecture.mdx(via @objectstack/spec)
  • content/docs/getting-started/cli.mdx(via @objectstack/spec)
  • content/docs/getting-started/core-concepts.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/guides/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/guides/ai-capabilities.mdx(via @objectstack/core, @objectstack/spec)
  • content/docs/guides/airtable-dashboard-analysis.mdx(via @objectstack/spec)
  • content/docs/guides/analytics-datasets.mdx(via @objectstack/spec)
  • content/docs/guides/api-reference.mdx(via @objectstack/spec)
  • content/docs/guides/authentication.mdx(via @objectstack/core, @objectstack/objectql)
  • content/docs/guides/business-logic.mdx(via @objectstack/spec)
  • content/docs/guides/cheatsheets/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/guides/cheatsheets/error-catalog.mdx(via @objectstack/spec)
  • content/docs/guides/cheatsheets/field-type-gallery.mdx(via @objectstack/spec)
  • content/docs/guides/cheatsheets/field-validation-rules.mdx(via @objectstack/spec)
  • content/docs/guides/cheatsheets/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/guides/cheatsheets/protocol-diagram.mdx(via packages/spec)
  • content/docs/guides/cheatsheets/query-cheat-sheet.mdx(via @objectstack/spec)
  • content/docs/guides/cheatsheets/quick-reference.mdx(via @objectstack/spec)
  • content/docs/guides/client-sdk.mdx(via @objectstack/spec)
  • content/docs/guides/common-patterns.mdx(via @objectstack/spec)
  • content/docs/guides/contracts/auth-service.mdx(via packages/spec)
  • content/docs/guides/contracts/cache-service.mdx(via packages/spec)
  • content/docs/guides/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/guides/contracts/index.mdx(via @objectstack/core, @objectstack/spec)
  • content/docs/guides/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/guides/contracts/storage-service.mdx(via packages/spec)
  • content/docs/guides/data-modeling.mdx(via @objectstack/spec)
  • content/docs/guides/deployment-vercel.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/guides/driver-configuration.mdx(via @objectstack/spec)
  • content/docs/guides/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/guides/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/guides/formula.mdx(via packages/objectql, @objectstack/spec)
  • content/docs/guides/hook-bodies.mdx(via packages/spec)
  • content/docs/guides/kernel-services.mdx(via @objectstack/core, @objectstack/objectql, @objectstack/spec)
  • content/docs/guides/metadata/dashboard.mdx(via @objectstack/spec)
  • content/docs/guides/metadata/field.mdx(via @objectstack/spec)
  • content/docs/guides/metadata/flow.mdx(via @objectstack/spec)
  • content/docs/guides/metadata/index.mdx(via @objectstack/spec)
  • content/docs/guides/metadata/object.mdx(via @objectstack/spec)
  • content/docs/guides/metadata/validation.mdx(via @objectstack/spec)
  • content/docs/guides/metadata/workflow.mdx(via @objectstack/spec)
  • content/docs/guides/objectql-migration.mdx(via @objectstack/core, @objectstack/objectql)
  • content/docs/guides/packages.mdx(via @objectstack/core, @objectstack/objectql, packages/services, @objectstack/spec)
  • content/docs/guides/plugin-development.mdx(via @objectstack/core, @objectstack/spec)
  • content/docs/guides/plugins.mdx(via @objectstack/core, @objectstack/objectql, @objectstack/spec)
  • content/docs/guides/project-scoping.mdx(via @objectstack/spec)
  • content/docs/guides/public-forms.mdx(via @objectstack/spec)
  • content/docs/guides/runtime-services/audit-service.mdx(via packages/services)
  • content/docs/guides/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/guides/runtime-services/examples.mdx(via @objectstack/core)
  • content/docs/guides/runtime-services/index.mdx(via packages/services, packages/spec)
  • content/docs/guides/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/guides/runtime-services/settings-service.mdx(via packages/services)
  • content/docs/guides/runtime-services/sharing-service.mdx(via packages/spec)
  • content/docs/guides/runtime-services/storage-service.mdx(via packages/spec)
  • content/docs/guides/security.mdx(via @objectstack/spec)
  • content/docs/guides/seed-data.mdx(via @objectstack/spec)
  • content/docs/guides/skills.mdx(via @objectstack/spec)
  • content/docs/guides/standards.mdx(via @objectstack/spec)
  • content/docs/guides/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectos/config-resolution.mdx(via @objectstack/core, @objectstack/spec)
  • content/docs/protocol/objectos/i18n-standard.mdx(via packages/services, @objectstack/spec)
  • content/docs/protocol/objectos/index.mdx(via @objectstack/core, @objectstack/objectql)
  • content/docs/protocol/objectos/lifecycle.mdx(via @objectstack/core, @objectstack/spec)
  • content/docs/protocol/objectos/plugin-spec.mdx(via @objectstack/core, @objectstack/spec)
  • content/docs/protocol/objectos/runtime-capabilities.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via packages/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via packages/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/objectql, @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang merged commit 601cc11 into mainJun 17, 2026
15 checks passed
@os-zhuang
os-zhuang deleted the feat/tz-analytics-bucketing-1982 branch June 17, 2026 04:38
os-zhuang added a commit that referenced this pull request Jul 18, 2026
Add acceptance tests for the timezone-aware today()/daysFromNow()/daysAgo()
functions (compute-tz core of ADR-0053 Phase 2, decision D1). The
implementation already shipped (#1998/#2001/#2006); these lock the issue's
criteria and pin the DST-boundary + equality behavior:
- AC1: today() at 2026-06-16T02:00Z in America/Los_Angeles == UTC-midnight of
2026-06-15.
- AC3: reference tz unset vs 'UTC' is byte-for-byte the pre-Phase-2 behavior
for all three functions.
- AC2: calendar days are correct across both 2026 US DST transitions
(spring-forward Mar 8, fall-back Nov 1); a Field.datetime instant compares
equal to daysFromNow(n) across DST; a Field.date string matches via the
hydration-safe idioms (ordering operators, date(), daysBetween()).
- A characterization guard documents the known cel-js equality limitation:
a bare `date-string == today()` silently returns false because cel-js's
isEqual hard-codes `string == X` to false. This is timezone-independent and
cross-cutting; the fix belongs in the data layer (hydrate date fields to Date
where field types are known) and is tracked as a separate follow-up.
Test-only; no changeset (no functional change).
Claude-Session: https://claude.ai/code/session_01SuiM565BZ3TR1VD3prMguB
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:datasize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ADR-0053 Phase 2 · Slice 5: timezone-aware analytics date bucketing

1 participant

@os-zhuang