fix(timezone): shift cell raw to project TZ for filter/drill/underlying actions - #22895
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
🧪 Test Selection✅ Tests that will run
⏭️ Tests skipped (no relevant file changes detected)
|
|
Your preview environment pr-22895 has been deployed. Preview environment endpoints are available at: |
Preview Environment🌐 URL: https://lightdash-preview-pr-22895.lightdash.okteto.dev 📋 Logs: View in GCP Console 🔧 SSH: |
|
|
||
| const addFilter = useCallback( | ||
| (field: FilterableField, value: any) => { | ||
| (field: FilterableField, value: any, timezone?: string) => { |
There was a problem hiding this comment.
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.
| (field: FilterableField, value: any, timezone?: string) => { | |
| (field: FilterableField, value: unknown, timezone?: string) => { |
Spotted by Graphite (based on custom rule: packages/frontend rules)
Is this helpful? React 👍 or 👎 to let us know.
7cb32f7 to
9cf08c4
Compare
…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.
01f2b2a to
69808db
Compare
…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').
## [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))
|
🎉 This PR is included in version 0.2907.2 🎉 The release is available on:
Your semantic-release bot 📦🚀 |
…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>
…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>

Summary
With
EnableTimezoneSupporton 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:Example: cell displays
2024-11in Paris but the filter targets2024-10. Negative offsets were unaffected.The row's
rawvalue is a UTC instant aligned to the project-TZ bucket midnight (post DATE_TRUNC round-trip — Paris-Nov is2024-10-31T23:00:00Z). The three actions fedrawstraight 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 ontimeIntervalBaseDimensionType === 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 optionaltimezone.MetricQueryDataProvidercarriesresolvedTimezone;CellContextMenu,DrillDownModal,UnderlyingDataModalconsume it.normalizeCellRawForFilterfor 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