Skip to content

fix(timezone): shift cell raw to project TZ for filter/drill/underlying actions - #22895

Merged
almeidabbm merged 6 commits into
mainfrom
05-11-fix_timezone_use_project_tz_for_filter-by-date-cell_raw_value
May 11, 2026
Merged

fix(timezone): shift cell raw to project TZ for filter/drill/underlying actions#22895
almeidabbm merged 6 commits into
mainfrom
05-11-fix_timezone_use_project_tz_for_filter-by-date-cell_raw_value

Conversation

@almeidabbm

@almeidabbm almeidabbm commented May 11, 2026

Copy link
Copy Markdown
Contributor

Summary

With EnableTimezoneSupport on and a non-UTC positive-offset project timezone (e.g. Europe/Paris), three cell actions on time-interval DATE dimensions (_month, _week, …) produced filters off by one bucket:

  • "Filter by" in the results table
  • Drill into (drill-down)
  • View underlying data

Example: cell displays 2024-11 in Paris but the filter targets 2024-10. Negative offsets were unaffected.

The row's raw value is a UTC instant aligned to the project-TZ bucket midnight (post DATE_TRUNC round-trip — Paris-Nov is 2024-10-31T23:00:00Z). The three actions fed raw straight into filter rules and the subsequent date extraction happened in UTC.

Fix

Shift the UTC instant into the resolved project TZ before extracting YYYY-MM-DD, gated on timeIntervalBaseDimensionType === TIMESTAMP. DATE-base intervals (pure calendar values, anchored at UTC midnight) must not be shifted — that would push e.g. Mar 1 → Feb 28 on negative offsets.

  • getFilterRuleWithDefaultValue (common) takes an optional timezone.
  • MetricQueryDataProvider carries resolvedTimezone; CellContextMenu, DrillDownModal, UnderlyingDataModal consume it.
  • New shared helper normalizeCellRawForFilter for the drill-down + underlying-data paths.

Before

Drill-down

before_fix_drill.mp4

Filter by

before_fix_filter.mp4

Underlying data

before_fix_underlying_data.mp4

After

Drill-down

after_fix_drill.mp4

Filter by

after_fix_filter.mp4

Underlying data

after_fix_underlying_data.mp4

Closes GLITCH-431

almeidabbm commented May 11, 2026

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@linear

linear Bot commented May 11, 2026

Copy link
Copy Markdown

GLITCH-431

@almeidabbm almeidabbm self-assigned this May 11, 2026
@almeidabbm
almeidabbm marked this pull request as ready for review May 11, 2026 10:29
@github-actions

github-actions Bot commented May 11, 2026

Copy link
Copy Markdown

🧪 Test Selection

✅ Tests that will run

Test Description
Preview Environment Deploys a preview environment for testing
Frontend E2E Tests Runs Cypress app tests
Backend API Tests Runs Vitest API tests
CLI Tests Runs CLI integration and dbt version tests

⏭️ Tests skipped (no relevant file changes detected)

Test How to trigger manually
Timezone Tests Add test-timezone to PR description

Tip: Add test-all to your PR description to run all tests.

@github-actions

Copy link
Copy Markdown

Your preview environment pr-22895 has been deployed.

Preview environment endpoints are available at:

@github-actions

github-actions Bot commented May 11, 2026

Copy link
Copy Markdown

Preview Environment

🌐 URL: https://lightdash-preview-pr-22895.lightdash.okteto.dev

📋 Logs: View in GCP Console

🔧 SSH: ./scripts/okteto-ssh.sh 22895


const addFilter = useCallback(
(field: FilterableField, value: any) => {
(field: FilterableField, value: any, timezone?: string) => {

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.

The value parameter is typed as any in the addFilter callback. TypeScript v5 best practices require proper typing — use FilterableItem or unknown with a type guard instead of any to maintain type safety.

Suggested change
(field: FilterableField, value: any, timezone?: string) => {
(field: FilterableField, value: unknown, timezone?: string) => {

Spotted by Graphite (based on custom rule: packages/frontend rules)

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

@almeidabbm
almeidabbm force-pushed the 05-11-fix_timezone_use_project_tz_for_filter-by-date-cell_raw_value branch from 7cb32f7 to 9cf08c4 Compare May 11, 2026 11:02
…ng actions

Filter-by, drill-down and view-underlying-data on time-interval DATE
dimensions were using the row's raw UTC instant verbatim. On positive
offsets (e.g. Europe/Paris) the UTC calendar date is one day earlier
than the displayed bucket, so filters resolved to the previous month.

Shift the UTC instant into the resolved project TZ before extracting
YYYY-MM-DD, gated on timeIntervalBaseDimensionType === TIMESTAMP so
DATE-base intervals (pure calendar values) are left alone.
Tighten the gate in getFilterRuleWithDefaultValue and
normalizeCellRawForFilter so plain DATE columns (no timeInterval) no
longer get TZ-shifted on Filter-by / drill / underlying-data actions.
Previously the gate was "not DATE-base", which caught plain DATE dims
too and silently shifted their calendar date back a day in negative-
offset projects.
Drop the (fieldId, explore) lookup pair and accept Item | undefined.
Two of the three call sites already had the field in scope; the
remaining DrillDown site resolves it once with findFieldByIdInExplore.
…getFieldsFromMetricQuery

Hoist the cell-raw filter normalization helper into @lightdash/common alongside
shouldShiftItemTimezone so the spreadsheet and filter paths share a single home.

In DrillDownModal.combineFilters, replace per-iteration findFieldByIdInExplore
calls with a single getFieldsFromMetricQuery lookup.
@almeidabbm
almeidabbm force-pushed the 05-11-fix_timezone_use_project_tz_for_filter-by-date-cell_raw_value branch from 01f2b2a to 69808db Compare May 11, 2026 11:33
…taProvider

Coalesce once inside the provider so call sites can pass the nullable
executeQueryResponse.resolvedTimezone directly without ?? undefined.
…Filter

Keeps flag-off / no-project-tz path bit-identical to pre-fix behavior
by returning rawValue unchanged when timezone is missing, instead of
reformatting the raw ISO instant to YYYY-MM-DD via .tz('UTC').
@almeidabbm
almeidabbm merged commit da52176 into main May 11, 2026
73 of 77 checks passed
@almeidabbm
almeidabbm deleted the 05-11-fix_timezone_use_project_tz_for_filter-by-date-cell_raw_value branch May 11, 2026 14:25
lightdash-bot pushed a commit that referenced this pull request May 11, 2026
## [0.2907.2](0.2907.1...0.2907.2) (2026-05-11)

### Bug Fixes

* **timezone:** shift cell raw to project TZ for filter/drill/underlying actions ([#22895](#22895)) ([da52176](da52176))
@lightdash-bot

Copy link
Copy Markdown
Collaborator

🎉 This PR is included in version 0.2907.2 🎉

The release is available on:

Your semantic-release bot 📦🚀

pimmey added a commit that referenced this pull request Jun 16, 2026
…dicate collapse

GLITCH-452 makes day-or-coarser TIMESTAMP-base truncs compile to a real DATE,
so they are calendar values and the drill/filter path no longer shifts them.
Update the stale #22895 tests (which fed the old UTC-instant representation and
expected a project-tz shift) to the post-452 model: bare cast dates are not
shifted, and a legacy UTC-instant value is read at UTC.

Relates: GLITCH-452

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pimmey added a commit that referenced this pull request Jun 16, 2026
…H-452) (#24251)

* feat(common): cast day-or-coarser DATE_TRUNCs to DATE in getSqlForTruncatedDate

Adds an opt-in castDayGrainToDate param: day-or-coarser truncations cast the project-wall-clock value to DATE (dropping the toUTC round-trip-back); sub-day grains stay TIMESTAMP. Dormant until the query builder passes the flag (behind EnableTimezoneSupport).

Relates: GLITCH-452

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(backend): wire day-or-coarser DATE cast through MetricQueryBuilder

getTimezoneAwareDimensionSql passes castDayGrainToDate=true to getSqlForTruncatedDate (reached only when useTimezoneAwareDateTrunc is on), so day-or-coarser SELECT and WHERE-LHS expressions emit CAST(... AS DATE). Adds a MetricQueryBuilder integration test.

Relates: GLITCH-452

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(common): emit bare date literals for day-grain filters

Day-or-coarser dims now compile to a real DATE LHS (CAST(... AS DATE)), so their filter literals and relative-filter boundaries stay bare across warehouses — wrapping them as timestamptz would re-introduce the tz drift the cast removes. Updates the filter-literal-wrapping test suite to the new bare contract; adds a MetricQueryBuilder integration test.

Relates: GLITCH-452

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(common): collapse calendar-value predicates so day-grain DATE no longer shifts

isCalendarValueDimension is now true for any DATE-typed field and shouldShiftItemTimezone false for them, since day-or-coarser truncs compile to a real DATE (the single update point GLITCH-450 consolidated). formatItemValue, spreadsheet exports, drill-filter normalization and the ECharts category-date axis all stop shifting day-grain DATE values — fixing the negative-offset off-by-one. Tests updated to the no-shift contract.

Relates: GLITCH-452

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(common): emit YYYY-MM-DD raw values for DATE fields in tz-aware mode

formatRawValue gains an optional timezone param: when a display timezone is resolved (tz-aware mode on), DATE fields emit a bare YYYY-MM-DD raw matching the warehouse type; undefined keeps the legacy ISO output (byte-identical, flag-off). Threaded via the timezone formatRow already passes, plus formatRawRows for subtotals (resolving the gated displayTimezone once) and the pivot-values path. The value is never shifted — the tz is only a mode signal.

Relates: GLITCH-452

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(timezones): split MIN/MAX out of GLITCH-452 to GLITCH-499

The design doc's MIN/MAX section described the wrong failure mode (a
date-string fall-through). The real issue: MAX/MIN over a DATE value is
tz-shifted because the formatter branch catches both the JS Date and the
ISO-midnight string and shifts them. Fixing it needs a metric-level type
signal, which is out of scope for 452 — split to GLITCH-499 and corrected
the analysis in the Scope list and §3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(timezones): update timezone-handling.md for the GLITCH-452 DATE cast

Day-or-coarser TIMESTAMP-base truncations now compile to a real DATE
(CAST of the truncated wall-clock), so the engineer-facing reference is
updated across: SELECT grouping (per-grain output type), filter parity +
bare-date literals, result formatting + the removed correction layer,
the raw wire value, cell-actions (normalizeCellRawForFilter is now an
inert no-op since the predicate collapse), the ECharts shift scope, and
the overview/vision diagrams. Sub-day grains still return a UTC instant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(common): cast day-grain to DATE via DATE(expr, tz) on BigQuery

BigQuery's TIMESTAMP_TRUNC returns the tz-midnight value as a UTC instant,
so CAST(... AS DATE) read its UTC date and bucketed day-or-coarser grains a
day early in positive-offset project timezones (Europe/Asia/Australia). Use
DATE(expr, tz) on BigQuery to read the calendar date in the project tz; other
adapters truncate to wall-clock, so CAST AS DATE stays correct there.

Found via cross-warehouse execution testing: Asia/Tokyo bucketed a 15:00Z row
to the previous day on BigQuery, while Snowflake/Databricks/Trino were correct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(timezones): update GLITCH-452 design doc with implementation outcomes

Status → implemented/in-review; add an "Implementation status & outcomes"
section (BigQuery DATE(expr,tz) per-adapter fix, 6/6 cross-warehouse pass,
downstream surface validated, normalizeCellRawForFilter now inert). Resolve
the §1 "decision to confirm" lines (date-zoom not cast → GLITCH-505;
per-adapter cast required + verified) and mark the §5 tests, risks, and
docs-to-update items done. Follow-ups: GLITCH-499 / GLITCH-503 / GLITCH-505.

Relates: GLITCH-452

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(timezones): record downstream blast-radius audit in GLITCH-452 design doc

Audit verdicts (all flag-gated): custom dimensions, conditional formatting,
result caching, saved filters, underlying-data/drill, sorting & totals all
inert. One genuine breaking case — raw-SQL table calcs with TIMESTAMP-specific
ops on truncated date dims (GLITCH-506). Latent/pre-existing noted: MIN/MAX-on-
date in CF (GLITCH-499) and the PoP range pre-filter coercion (GLITCH-505).

Relates: GLITCH-452

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(common): align createFilterRuleFromField date tests with the predicate collapse

GLITCH-452 makes day-or-coarser TIMESTAMP-base truncs compile to a real DATE,
so they are calendar values and the drill/filter path no longer shifts them.
Update the stale #22895 tests (which fed the old UTC-instant representation and
expected a project-tz shift) to the post-452 model: bare cast dates are not
shifted, and a legacy UTC-instant value is read at UTC.

Relates: GLITCH-452

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(backend): DATE-base-TS Excel cells are not shifted post-cast (GLITCH-452)

The predicate collapse makes day-or-coarser TIMESTAMP-base truncs real DATEs
(calendar values), so convertRowToExcel no longer shifts them. Update the stale
test that fed the old UTC-instant and expected a project-tz shift: feed the bare
cast date and assert no shift, matching the native-DATE case.

Relates: GLITCH-452

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(api-tests): add timezone boundary-crossing battery (GLITCH-452)

Adds day/month/year/DST boundary-crossing tests over the timezone_test boundary
rows (11-18) at offsets that flip a boundary — year (#17 → 2023 in NY), month
(#18 → Feb in Tokyo/Kiritimati+14), Tokyo-midnight day (#11), fractional
St_Johns (-3:30), and US spring/fall DST days. Asserts the bare-date raw bucket
to rule out double conversions. Verified 12/12 against a flag-enabled backend.

Adds an eventIds scope + getRawBucketMap helper to support boundary-row queries.

Relates: GLITCH-452

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(frontend): DATE-base-TS day buckets are not axis-shifted post-cast (GLITCH-452)

After merging main, resolveAxisTimezone (which gates on shouldShiftItemTimezone)
no longer shifts a DATE day bucket derived from a TIMESTAMP base — the cast makes
it a real DATE calendar value. Flip main's stale test to expect no shift, matching
the native-DATE case.

Relates: GLITCH-452

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(backend): DATE-base-TS CSV cells are not shifted post-cast (GLITCH-452)

Last of the stale correction-layer tests: a day-or-coarser TIMESTAMP-base trunc
is now a real DATE (calendar value), so convertRowToCsv does not shift it. Flip
the expectation to no-shift, matching the native-DATE case. Verified against the
full backend/common/frontend suites — no remaining stale tests.

Relates: GLITCH-452

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lightdash-bot pushed a commit that referenced this pull request Jun 16, 2026
# [0.3174.0](0.3173.0...0.3174.0) (2026-06-16)

### Features

* **timezone:** cast day-or-coarser DATE_TRUNCs to a real DATE (GLITCH-452) ([#24251](#24251)) ([e149743](e149743)), closes [#22895](#22895) [#17](#17) [#18](#18) [#11](#11)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants