fix(storage): pin PostgreSQL sessions to UTC timezone - #1018
Conversation
Storage compares plain timestamp columns against server-side now(), which evaluates through the session TimeZone. On a server whose default is not UTC, lease-expiry and staleness predicates would skew. Inject timezone=UTC at connection setup unless the DSN sets one explicitly, mirroring the sslmode handling.
There was a problem hiding this comment.
Pull request overview
Pins SchemaBot-managed PostgreSQL sessions to timezone=UTC (unless explicitly set in the DSN) so storage comparisons between timestamp columns and server-side now() are consistent regardless of the server’s default TimeZone.
Changes:
- Default
pgxruntime paramtimezone=UTCis applied during connection config parsing unless already present in the DSN. - Unit test added to verify both DSN forms default to UTC and that explicit DSN timezone overrides.
- Integration test added to validate the session reports UTC even when the database default timezone is set to a non-UTC value.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| pkg/postgresconn/postgresconn.go | Sets a default timezone=UTC runtime param during connectionConfig() parsing (DSN timezone still wins). |
| pkg/postgresconn/postgresconn_test.go | Adds unit coverage for UTC default and explicit timezone override across URL + keyword DSN forms. |
| pkg/postgresconn/postgresconn_integration_test.go | Adds integration coverage asserting Open() yields a UTC session even when DB default timezone is non-UTC. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
PostgreSQL matches GUC names case-insensitively but pgx preserves DSN key case, so an explicit ?TimeZone=... would otherwise coexist with the injected pin in the startup packet in nondeterministic map order. PGTZ is documented and covered as an explicit libpq fallback that skips the pin.
A fresh unpinned session must observe the ALTER DATABASE timezone default first; otherwise the UTC assertion on the pinned pool holds on any UTC-default server even if Open never pinned anything.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
morgo
left a comment
There was a problem hiding this comment.
🤖 Approving on Morgan's behalf (automated review).
Clean and PG-only (postgresconn; the MySQL path is structurally untouched). The pin is correctly scoped: it applies only when no timezone GUC is already present under case-insensitive matching — which dodges the real trap here, since pgx preserves DSN key case and a blind write would put both TimeZone=... and timezone=UTC into the startup packet in nondeterministic map order. PGTZ arriving via libpq fallback semantics counts as explicit and is honored. The motivation is load-bearing (storage's lease-expiry/staleness predicates compare plain timestamps against session-timezone-dependent now()), and the tests are exemplary — both DSN forms, both GUC spellings with a no-duplicate assertion, PGTZ honored, and an integration test that first proves a fresh unpinned session observes the non-UTC database default before asserting the pinned session reads UTC.
No nits.
Pins SchemaBot-managed PostgreSQL sessions to
timezone=UTCunless the DSN sets one explicitly.Why
Storage compares plain
timestampcolumns against server-sidenow(), which evaluates through the session TimeZone. On a server whose default TimeZone is not UTC, lease-expiry and staleness predicates would skew — and the UTC default of common container images means tests can't catch it. Pinning the session at connection setup makes the comparisons consistent across pods regardless of server configuration.What
postgresconninjectstimezone=UTCinto the connection config after DSN parsing, for both DSN forms and the credential-reload path alike. An explicittimezonein the DSN wins, in either DSN form and under any GUC-name spelling.Two related gaps are tracked as internal follow-ups, out of scope here:
mysqlconnpins no sessiontime_zone, so the same skew class remains live for MySQL lease/staleness predicates — follow-up injectstime_zone='+00:00'unless the DSN sets one.received_atis written from client-sidetime.Now()(zone discarded by pgx) and diffed against servernow()in the backlog-age metric — follow-up writes it with the dialect'sCurrentTimestamp.Before / after