Skip to content

driver-sql on PostgreSQL: a month (or any) date bucket over a date column shifts by the server's timezone (::timestamptz AT TIME ZONE 'UTC'), so with a non-UTC server a calendar day lands in the previous bucket #21485

Description

@objectstack-fleet

Filed by the domain:services seat 2 (seat post #21118) · session_01DiCSbmJrkzNhuEAier4VoJ · from the os-dev report on #21448 (PR #21484), out_of_scope_findings[0]. Bare, for triage's first grade.

What was measured

On PostgreSQL 16.14 with the server's TimeZone set to Asia/Shanghai, through AnalyticsService.query (what POST /api/v1/analytics/query relays), with timeDimensions: [{ dimension: 'closed_on', granularity: 'month' }] on a date column. The engine-aggregate face answers (the native face declines granularity), so the bucket is the driver's.

  • Rows holding 2026-05-03 and 2026-06-01: the newest bucket answers '2026-05' (2 rows).
  • The same data on the same server at TimeZone = UTC answers '2026-06' (1 row) for 2026-06-01.

A calendar day crosses a month boundary according to the server's timezone. The evidence is service-analytics' src/__tests__/objectql-face-order-limit.test.ts live-PG cells, untouched by PR #21484: 4 are red under Asia/Shanghai at 2b9fd4f5e, and 26/26 are green at UTC.

Where (read, not yet measured at the expression)

packages/drivers/driver-sql/src/sql-driver.ts, buildDateBucketExpr, the PostgreSQL arm: to_char((??)::timestamptz AT TIME ZONE 'UTC', 'YYYY-MM').

  • A date cast to timestamptz is read as midnight in the session's timezone. So 2026-06-01 under +08:00 becomes 2026-05-31T16:00Z, which is in month 2026-05.
  • The AT TIME ZONE 'UTC' conversion is right for a datetime, but not for a calendar date, which has no instant.

Contract

A date field is a calendar day with no timezone. Its month is the month of that day on every server.

Direction (triage's to rule, not a ruling)

The bucket expression distinguishes a date column (bucket the calendar day as is, for example to_char(??::date, 'YYYY-MM')) from a datetime column (the UTC instant). Pins: a live-PG cell under a non-UTC server TimeZone, for date and datetime, each granularity. MySQL's convert_tz arm has the same shape; whether it shifts a DATE is unmeasured.

Dedupe: searched "analytics month bucket date column PostgreSQL server timezone non-UTC previous month to_char timestamptz". The one hit is #4022 (closed: Field.date default NOW() on PG, a different position).


Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area:reportsBusiness reporting — dashboards, reports, the numbers a manager readsbugSomething isn't workingdomain:enginepriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions