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

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jun 17, 2026 4:32am

Request Review

@github-actions github-actions Bot 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 main Jun 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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/m tests tooling

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