From ba4ab5272cab6b9e1e2e020795f4bd348da5a71e Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Mon, 27 Jul 2026 14:05:37 -0700 Subject: [PATCH 1/2] fix: convert_timezone must use to_local_time(), not the Postgres pattern The double-AT TIME ZONE + cast-to-naive-timestamp pattern silently loses the shift on DataFusion: the final cast re-renders the UTC instant rather than the target zone's wall clock. to_local_time() yields the local wall clock and is DST-aware. Verified live: 2024-01-01 12:00 UTC -> 04:00 (PST) and 2024-07-01 12:00 UTC -> 05:00 (PDT). Found by review on hotdata-dev/jaffle-shop#7. --- dbt/include/hotdata/macros/utils.sql | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/dbt/include/hotdata/macros/utils.sql b/dbt/include/hotdata/macros/utils.sql index 5ccd413..6bd4a7c 100644 --- a/dbt/include/hotdata/macros/utils.sql +++ b/dbt/include/hotdata/macros/utils.sql @@ -13,12 +13,16 @@ {%- endmacro %} {% macro hotdata__convert_timezone(column, target_tz, source_tz) -%} - {#- dbt_date dispatch hook: DataFusion supports the Postgres double - AT TIME ZONE pattern, so delegate to it. -#} - cast( - cast({{ column }} as timestamp) - at time zone '{{ source_tz }}' at time zone '{{ target_tz }}' - as timestamp + {#- + dbt_date dispatch hook (packages need a root-project shim or dispatch + config to reach it). NOT the Postgres double-AT TIME ZONE pattern: on + DataFusion the final cast back to a naive timestamp re-renders the UTC + instant, silently losing the shift. to_local_time() yields the target + zone's wall clock (DST-aware). + -#} + to_local_time( + (cast({{ column }} as timestamp) at time zone '{{ source_tz }}') + at time zone '{{ target_tz }}' ) {%- endmacro %} From 5951bb6dae30bc32b3813aad6537740de93a54ce Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Mon, 27 Jul 2026 14:11:31 -0700 Subject: [PATCH 2/2] Address review: default falsy source_tz to UTC; changelog entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit source_tz itself works — verified live that AT TIME ZONE on a naive timestamp localizes it as the source zone's wall clock (12:00 America/New_York -> 17:00 UTC, -> 09:00 LA). The earlier round-trip loss was in the cast-to-naive rendering step, not AT TIME ZONE. --- CHANGELOG.md | 6 ++++++ dbt/include/hotdata/macros/utils.sql | 11 +++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12c0788..9240801 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- `convert_timezone` returned the UTC instant unshifted; it now converts via + `to_local_time()` (DST-aware, non-UTC sources verified) and defaults a + falsy `source_tz` to UTC. + ## [0.1.0] - 2026-07-27 ### Added diff --git a/dbt/include/hotdata/macros/utils.sql b/dbt/include/hotdata/macros/utils.sql index 6bd4a7c..35688e6 100644 --- a/dbt/include/hotdata/macros/utils.sql +++ b/dbt/include/hotdata/macros/utils.sql @@ -15,11 +15,14 @@ {% macro hotdata__convert_timezone(column, target_tz, source_tz) -%} {#- dbt_date dispatch hook (packages need a root-project shim or dispatch - config to reach it). NOT the Postgres double-AT TIME ZONE pattern: on - DataFusion the final cast back to a naive timestamp re-renders the UTC - instant, silently losing the shift. to_local_time() yields the target - zone's wall clock (DST-aware). + config to reach it). NOT the Postgres double-AT TIME ZONE + cast-to-naive + pattern: on DataFusion that final cast re-renders the UTC instant, + silently losing the shift. Here AT TIME ZONE on a naive timestamp + localizes it as the source zone's wall clock (12:00 @ America/New_York + -> 12:00-05:00, verified — non-UTC sources convert correctly), and + to_local_time() renders the target zone's wall clock (DST-aware). -#} + {%- set source_tz = source_tz or 'UTC' -%} to_local_time( (cast({{ column }} as timestamp) at time zone '{{ source_tz }}') at time zone '{{ target_tz }}'