Skip to content

fix(query): render named-timezone timestamps - #283

Merged
anoop-narang merged 2 commits into
mainfrom
fix/timestamptz-renders-as-null
Sep 3, 2026
Merged

anoop-narang merged 2 commits into
mainfrom
fix/timestamptz-renders-as-null

Conversation

@anoop-narang

Copy link
Copy Markdown
Contributor

Summary

A timestamp column whose type carries a named IANA timezone (UTC, America/New_York) renders as null when a result is fetched, instead of showing its value. Fixed-offset (+00:00) and zone-less timestamps were unaffected, so a neighbouring column in the same row looked fine.

The data was never wrong — only the rendering.

Cause

Results are fetched as Arrow IPC and formatted client-side, so a timestamp arrives as a raw integer plus a timezone recorded in the Arrow schema. Turning that into text for a named zone requires a timezone database. arrow was declared with default-features = false, so none was compiled in and ArrayFormatter::try_new returned an error for those values:

ArrayFormatter::try_new(col, &opts)
    .map(|f| Value::String(f.value(row).to_string()))
    .unwrap_or(Value::Null)          // error became a null cell

A fixed offset parses arithmetically and a zone-less timestamp needs no lookup, which is why only named zones broke.

Regressed in 50e3439, which moved result fetching from JSON to Arrow — before that the service formatted every value and sent strings, so the client never had to resolve a timezone.

Fix

  1. Enable arrow's chrono-tz feature so named zones resolve.
  2. Stop converting a formatting failure into null. The cell is already checked for null above that arm, so reaching it means a present value could not be rendered — reporting null asserts the opposite and is indistinguishable from a real null to anything reading the output. The shape is kept (callers rely on one value per column) and the failure now goes to stderr, deduplicated once per run so a large result cannot flood the terminal.

Verification

Against the live API, fetching the same stored result with the released build and this one:

columns:      ts_utc                ts_ny                       ts_naive             n

0.29.0        NULL                  NULL                        2026-01-01T12:00:00  7
this branch   2026-01-01T12:00:00Z  2026-01-01T12:00:00-05:00   2026-01-01T12:00:00  7
served inline 2026-01-01T12:00:00Z  2026-01-01T12:00:00-05:00   2026-01-01T12:00:00  7

The client now matches what the service returns when it formats the same query inline.

Tests

  • named_timezone_timestamps_render_instead_of_nulling — named zones across several time units, plus the fixed-offset and zone-less forms kept as controls so a regression narrows to the named-zone case, and a genuine null still rendering as null.
  • no_column_type_silently_renders_as_null — sweeps the temporal and decimal types a query can return (Date32/64, Time32/64, Duration, Decimal128, all four Timestamp units in naive, UTC, fixed-offset and named-zone forms) and asserts none renders a present value as null. This guards the wider hazard rather than just this instance: every type without an explicit arm falls through to the same formatter.

Both fail without the feature (Timestamp(s, UTC) has a value at row 0 but rendered as null) and pass with it. cargo fmt, cargo clippy --all-targets -- -D warnings and the full suite pass.

Results are fetched as Arrow IPC and formatted client-side, so a
timestamp arrives as a raw integer plus a timezone recorded in the
schema. Formatting one whose zone is a named IANA zone ("UTC",
"America/New_York") needs a timezone database; arrow was built with
default features off, so none was compiled in and its formatter
returned an error for those values. The cell then reported them as
null.

Only named zones were affected: a fixed offset ("+00:00") and a
zone-less timestamp both format without a timezone database, so a
neighbouring column in the same row looked correct and the loss was
easy to miss. Nothing was wrong with the data — the same query served
inline, formatted by the service, returned the values correctly.

Enable arrow's chrono-tz feature, and stop turning a formatting
failure into a null: the cell has already been checked for null above,
so reaching that arm means a present value could not be rendered, and
reporting it as null asserts the opposite. The output shape is kept
(callers rely on one value per column) and the failure is reported on
stderr instead, once per run so a large result cannot flood it.

Two tests cover it: named zones across several time units, and a
sweep over the temporal and decimal types a query can return
asserting none of them render a present value as null. Both fail
without the feature.

Regressed in 50e3439, which moved result fetching from JSON to Arrow.
@anoop-narang
anoop-narang requested a review from a team as a code owner September 3, 2026 09:04
@anoop-narang
anoop-narang requested review from zfarrell and removed request for a team September 3, 2026 09:04
Comment thread src/commands/query.rs
Comment thread src/commands/query.rs Outdated
claude[bot]
claude Bot previously approved these changes Sep 3, 2026

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix and tests match the described cause. Two non-blocking nits inline. CI / test was still pending at review time, so the new tests are unverified here.

@codecov

codecov Bot commented Sep 3, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.52174% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/commands/query.rs 96.52% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

Two review points.

The warn-once flag was global, so a result with two unformattable
column types reported only the first and the second went back to
nulling silently — the exact hazard this removes. Key the set on the
data type, which is what the failure is a property of.

`try_new` only reports a type Arrow cannot format at all. A single
out-of-range value inside a formattable type fails later, in the
value formatter, and the plain `Display` impl under the default
`safe: true` writes the text "ERROR: <msg>" into the cell as if it
were data (and panics when the inner write fails). Render the value
through `try_to_string` so one bad value degrades the same way a bad
type does.

Test covers it: an out-of-range Timestamp(Second) must not panic and
must not put an error message in the data. Without the change it
fails with "ERROR: Cast error: Failed to convert ... to datetime".

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both prior nits are addressed: the warning is now keyed per data type, and the per-value path uses try_to_string instead of Display. No further findings.

Note: CI / test and CI / fmt were still pending at review time, so the new try_to_string call is not confirmed to compile from this review.

@anoop-narang
anoop-narang merged commit 7afcbc1 into main Sep 3, 2026
13 checks passed
@anoop-narang
anoop-narang deleted the fix/timestamptz-renders-as-null branch September 3, 2026 09:11
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