Skip to content

fix(genesis-writer): emit every timestamp in UTC - #488

Merged
rickyrombo merged 1 commit into
mainfrom
fix/genesis-writer-utc-timestamps
Aug 10, 2026
Merged

fix(genesis-writer): emit every timestamp in UTC#488
rickyrombo merged 1 commit into
mainfrom
fix/genesis-writer-utc-timestamps

Conversation

@rickyrombo

Copy link
Copy Markdown
Contributor

The writer renders timestamps with whatever zone pgx decoded them into. For most columns that is UTC, but not all of them.

The measurement

The source schema mixes types. Seven columns the writer reads are timestamp with time zoneplaylist_tracks.created_at/updated_at, email_access and encrypted_emails created_at/updated_at, and users.last_active_at — and pgx decodes those into time.Local. Run against the production snapshot on a PDT host:

Source column Type Decoded location .Format(RFC3339)
playlist_tracks.created_at timestamptz Local 2024-02-26T18:00:34-08:00
tracks.created_at timestamp UTC 2024-10-31T22:02:26Z

What it does and does not cost

Not data corruption. Both strings name the same instant, and the indexer normalizes on read — parseReleaseDate calls .UTC() before storing. Existing migrated data is correct.

It costs reproducibility. The same source row emits different bytes — and therefore a different transaction hash — on a UTC host than on a developer's laptop. A genesis artifact should not depend on the machine that produced it. It also leaves correctness resting on every downstream consumer normalizing, forever, while the producer emits a host-dependent representation.

Change

.UTC() at all 17 formatting sites, no helper. The call is self-explanatory where it appears, and a helper is one more thing to remember to use.

Two sites already had .UTC() (from the tombstone carry in #485) and were left at one call, not two.

The test

A guard that scans the package for any .Format(time.RFC3339) whose receiver does not end in .UTC(), reporting file, line and expression.

Source-level on purpose: most metadata is built inside per-entity closures that cannot be called in isolation, so a behavioural test would cover whichever single path it happened to reach — while the failure actually worth catching is a new emission site added later without .UTC(). It also fails if it finds zero formatting sites, so it cannot silently stop guarding anything.

A second test pins the premise — that a Local-zone time formats with an offset and .UTC() removes it while naming the same instant.

Negative check. Removing .UTC() from one site:

--- FAIL: TestAllEmittedTimestampsAreUTC
    1 timestamp(s) emitted without .UTC(); on a non-UTC host these render
    with a local offset and the artifact stops being reproducible:
      entities_user.go:110  CreatedAt: u.CreatedAt.Format(time.RFC3339),

Verification

go build, go vet, gofmt -l, and the package tests all clean. No stray binary committed.

🤖 Generated with Claude Code

The source schema mixes column types. Most timestamps are `timestamp
without time zone`, which pgx decodes with a UTC location, but seven of
the columns this writer reads are `timestamp with time zone`:
playlist_tracks created_at/updated_at, email_access and encrypted_emails
created_at/updated_at, and users.last_active_at. pgx decodes those into
time.Local.

Measured against the production snapshot on a PDT host:

  playlist_tracks.created_at (timestamptz)  ->  2024-02-26T18:00:34-08:00
  tracks.created_at          (timestamp)    ->  2024-10-31T22:02:26Z

Both name the same instant, and the indexer normalizes on read --
parseReleaseDate calls .UTC() before storing -- so this is not data
corruption today. What it costs is reproducibility: the same source row
emits different bytes, and therefore a different transaction hash, on a
UTC host than on a developer's laptop. A genesis artifact should not
depend on the machine that produced it.

Applies .UTC() at all 17 formatting sites, including inside the
fmtCreatedAt helper, which covers a further seven call sites in the
social and event writers.

The guard test scans the package for a .Format(time.RFC3339) whose
receiver does not end in .UTC(). It is a source-level check on purpose.
Most metadata is built inside per-entity closures that cannot be called
in isolation, so a behavioural test would only cover whichever path it
happened to reach, while the failure worth catching is a *new* emission
site added later without .UTC(). The test also fails if it finds no
formatting sites at all, so it cannot quietly stop guarding anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rickyrombo
rickyrombo force-pushed the fix/genesis-writer-utc-timestamps branch from 075a758 to 7166cea Compare August 10, 2026 17:48
@rickyrombo
rickyrombo merged commit 3a877e1 into main Aug 10, 2026
3 checks passed
@rickyrombo
rickyrombo deleted the fix/genesis-writer-utc-timestamps branch August 10, 2026 17:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant