diff --git a/contrib/btree_gin/btree_gin.c b/contrib/btree_gin/btree_gin.c index 8dfbaa4781d..4fb38ac22e4 100644 --- a/contrib/btree_gin/btree_gin.c +++ b/contrib/btree_gin/btree_gin.c @@ -513,6 +513,34 @@ cvt_timestamptz_timestamp(Datum input) result = timestamptz2timestamp_safe(val, (Node *) &escontext); /* We can ignore errors, since result is useful as-is */ + + /* + * It is critical that result <= val, else the index scan can begin after + * potentially-matching index entries. There are two known ways that this + * might fail to be true: + * + * 1. In the to-be-repeated hour of a fall-back DST transition, the + * ambiguous converted timestamp will be interpreted by <= as the later + * (standard-time) timestamptz, thus comparing later than the given val. + * + * 2. timestamptz2timestamp_safe will return +Infinity if the input is + * close to the range limit of timestamptz and its rotated value would + * exceed the limit of timestamp. (In itself this might not cause any + * failures, but if #1 also applies then it possibly could. In any case + * we don't want to be repeatedly subtracting an hour from +Infinity.) + * + * To fix both things, first replace +Infinity with the maximum finite + * timestamp, and then back up an hour at a time until we have a result + * that compares <= val. (It won't hurt if we end with a result that is + * slightly less than the nominal lower limit of timestamp.) This may + * result in scanning more index entries than we really need to, but we + * won't miss any we need. + */ + if (TIMESTAMP_IS_NOEND(result)) + result = END_TIMESTAMP; + while (timestamp_cmp_timestamptz_internal(result, val) > 0) + result -= USECS_PER_HOUR; + return TimestampGetDatum(result); } diff --git a/contrib/btree_gin/expected/timestamp.out b/contrib/btree_gin/expected/timestamp.out index b7565285e68..6e972b56602 100644 --- a/contrib/btree_gin/expected/timestamp.out +++ b/contrib/btree_gin/expected/timestamp.out @@ -343,3 +343,29 @@ SELECT * FROM test_timestamp WHERE i>'4714-11-23 17:00 BC'::timestamptz ORDER BY infinity (7 rows) +-- Check that we aren't fooled by ambiguous timestamps near a DST transition +INSERT INTO test_timestamp VALUES + ('2026-11-01 00:50'), ('2026-11-01 01:10'), ('2026-11-01 01:20'), + ('2026-11-01 01:40'), ('2026-11-01 02:10'); +SET TimeZone = 'America/New_York'; +SELECT * FROM test_timestamp WHERE i>='2026-11-01 01:30-04'::timestamptz ORDER BY i; + i +-------------------------- + Sun Nov 01 01:10:00 2026 + Sun Nov 01 01:20:00 2026 + Sun Nov 01 01:40:00 2026 + Sun Nov 01 02:10:00 2026 + infinity +(5 rows) + +SELECT * FROM test_timestamp WHERE i>'2026-11-01 01:30-04'::timestamptz ORDER BY i; + i +-------------------------- + Sun Nov 01 01:10:00 2026 + Sun Nov 01 01:20:00 2026 + Sun Nov 01 01:40:00 2026 + Sun Nov 01 02:10:00 2026 + infinity +(5 rows) + +RESET TimeZone; diff --git a/contrib/btree_gin/sql/timestamp.sql b/contrib/btree_gin/sql/timestamp.sql index 1ee4edb5ea4..750ab6d2336 100644 --- a/contrib/btree_gin/sql/timestamp.sql +++ b/contrib/btree_gin/sql/timestamp.sql @@ -71,3 +71,12 @@ SELECT * FROM test_timestamp WHERE i>'infinity'::timestamptz ORDER BY i; -- This PST timestamptz will underflow if converted to timestamp SELECT * FROM test_timestamp WHERE i<='4714-11-23 17:00 BC'::timestamptz ORDER BY i; SELECT * FROM test_timestamp WHERE i>'4714-11-23 17:00 BC'::timestamptz ORDER BY i; + +-- Check that we aren't fooled by ambiguous timestamps near a DST transition +INSERT INTO test_timestamp VALUES + ('2026-11-01 00:50'), ('2026-11-01 01:10'), ('2026-11-01 01:20'), + ('2026-11-01 01:40'), ('2026-11-01 02:10'); +SET TimeZone = 'America/New_York'; +SELECT * FROM test_timestamp WHERE i>='2026-11-01 01:30-04'::timestamptz ORDER BY i; +SELECT * FROM test_timestamp WHERE i>'2026-11-01 01:30-04'::timestamptz ORDER BY i; +RESET TimeZone;