From 3ae815960a62af2fbfe4b51e3ac2a76eadf1e3c9 Mon Sep 17 00:00:00 2001 From: Alexandre Felipe Date: Sun, 30 Aug 2026 10:51:07 +0100 Subject: [PATCH-v1] date_trunc redotz for hours and minutes When truncating a timestamp, one can cross timezone transitions, unless the local time after the transition is at time hh:00:00 or the offset change is a multiple of 3600, date_trunc requires handle the timezone transitions. trunc_timestamptz_internal already handled this when truncating dates (day or lager units), mediated by a flag redotz. This commit simply sets redotz for the time part (minute or larger units). The issue can be observed with t = '1916-07-27 22:26:08+00' and timezone 'Europe/Athens' prior to this commit. This happens because the input timestamptz is 1916-07-28 00:26:08 at Europe/Athens, just after a transition, subtracting 26 min 8 sec, causes the timestamp to be interpreted at a different offset. --- src/backend/utils/adt/timestamp.c | 6 +++++- src/test/regress/expected/timestamptz.out | 22 ++++++++++++++++++++++ src/test/regress/sql/timestamptz.sql | 9 +++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c index 9c17ba2f905..4eb0c6876f3 100644 --- a/src/backend/utils/adt/timestamp.c +++ b/src/backend/utils/adt/timestamp.c @@ -5055,13 +5055,17 @@ timestamptz_trunc_internal(text *units, TimestampTz timestamp, pg_tz *tzp) pg_fallthrough; case DTK_DAY: tm->tm_hour = 0; - redotz = true; /* for all cases >= DAY */ pg_fallthrough; case DTK_HOUR: tm->tm_min = 0; pg_fallthrough; case DTK_MINUTE: tm->tm_sec = 0; + /* + * Truncating anything larger than a second it might + * cross a time transition. + */ + redotz = true; pg_fallthrough; case DTK_SECOND: fsec = 0; diff --git a/src/test/regress/expected/timestamptz.out b/src/test/regress/expected/timestamptz.out index 5dc8a621f6c..8d2e3e10a94 100644 --- a/src/test/regress/expected/timestamptz.out +++ b/src/test/regress/expected/timestamptz.out @@ -800,6 +800,28 @@ SELECT date_trunc( 'week', timestamp with time zone 'infinity', 'GMT') AS inf_zo SELECT date_trunc('ago', timestamp with time zone 'infinity', 'GMT') AS invalid_zone_trunc; ERROR: unit "ago" not recognized for type timestamp with time zone +SET timezone to 'UTC'; +SELECT unit, side, + timezone(z, date_trunc(unit, t::timestamptz, z)) as "local", + timezone('UTC', date_trunc(unit, t::timestamptz, z)) as "UTC" +FROM (VALUES ('before', '1916-07-27 22:26:07.987654+00', 'Europe/Athens'), + ('after', '1916-07-27 22:26:08.123456+00', 'Europe/Athens')) t1(side, t,z), + (VALUES ('month'), ('day'), ('hour'), ('minute'), ('sec')) t2(unit); + unit | side | local | UTC +--------+--------+--------------------------+-------------------------- + month | before | Sat Jul 01 00:00:00 1916 | Fri Jun 30 22:25:08 1916 + month | after | Sat Jul 01 00:00:00 1916 | Fri Jun 30 22:25:08 1916 + day | before | Fri Jul 28 00:00:00 1916 | Thu Jul 27 22:25:08 1916 + day | after | Fri Jul 28 00:00:00 1916 | Thu Jul 27 22:25:08 1916 + hour | before | Fri Jul 28 00:00:00 1916 | Thu Jul 27 22:25:08 1916 + hour | after | Fri Jul 28 00:00:00 1916 | Thu Jul 27 22:25:08 1916 + minute | before | Fri Jul 28 00:00:00 1916 | Thu Jul 27 22:25:08 1916 + minute | after | Fri Jul 28 00:51:08 1916 | Thu Jul 27 22:51:08 1916 + sec | before | Fri Jul 28 00:00:59 1916 | Thu Jul 27 22:26:07 1916 + sec | after | Fri Jul 28 00:26:08 1916 | Thu Jul 27 22:26:08 1916 +(10 rows) + +RESET timezone; -- verify date_bin behaves the same as date_trunc for relevant intervals SELECT str, diff --git a/src/test/regress/sql/timestamptz.sql b/src/test/regress/sql/timestamptz.sql index 6ace851d169..6d308644680 100644 --- a/src/test/regress/sql/timestamptz.sql +++ b/src/test/regress/sql/timestamptz.sql @@ -229,6 +229,15 @@ SELECT date_trunc('timezone', timestamp with time zone 'infinity', 'GMT') AS not SELECT date_trunc( 'week', timestamp with time zone 'infinity', 'GMT') AS inf_zone_trunc; SELECT date_trunc('ago', timestamp with time zone 'infinity', 'GMT') AS invalid_zone_trunc; +SET timezone to 'UTC'; +SELECT unit, side, + timezone(z, date_trunc(unit, t::timestamptz, z)) as "local", + timezone('UTC', date_trunc(unit, t::timestamptz, z)) as "UTC" +FROM (VALUES ('before', '1916-07-27 22:26:07.987654+00', 'Europe/Athens'), + ('after', '1916-07-27 22:26:08.123456+00', 'Europe/Athens')) t1(side, t,z), + (VALUES ('month'), ('day'), ('hour'), ('minute'), ('sec')) t2(unit); +RESET timezone; + -- verify date_bin behaves the same as date_trunc for relevant intervals SELECT str, -- 2.53.0