From 490d3c454cf6d1888d305800283359c2166c2c9b Mon Sep 17 00:00:00 2001 From: Gaurav Singh Date: Sat, 31 Oct 2026 01:46:04 +0530 Subject: [PATCH v1] Mark age(timestamptz, timestamptz) as STABLE Its result depends on the TimeZone setting, but it was marked IMMUTABLE. That let postgres_fdw ship it to the remote server, which runs with TimeZone = GMT, and return wrong rows. Do not backpatch, since this can break existing indexes, generated columns and partition keys that use the function. Reported-by: Gaurav Singh Author: Gaurav Singh Discussion: https://postgr.es/m/CAEcQ1badDTrQM8qjdkMOy4Bs5xP2EwKopK=3O_-DSatkrsz2KA@mail.gmail.com --- .../postgres_fdw/expected/postgres_fdw.out | 24 +++++++++++++++++++ contrib/postgres_fdw/sql/postgres_fdw.sql | 12 ++++++++++ src/include/catalog/pg_proc.dat | 2 +- 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 115ebc7925f..82ae8396ef3 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -1176,6 +1176,30 @@ SELECT * FROM ft1 WHERE CASE c3 COLLATE "C" WHEN c6 THEN true ELSE c3 < 'bar' EN Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" (4 rows) +-- age(timestamptz, timestamptz) depends on TimeZone, so it can't be shipped: +-- the remote session runs with TimeZone = GMT +CREATE TABLE loct_age (s timestamptz, e timestamptz); +INSERT INTO loct_age VALUES ('2026-01-31 00:00+00', '2026-03-01 00:00+00'); +CREATE FOREIGN TABLE ft_age (s timestamptz, e timestamptz) + SERVER loopback OPTIONS (table_name 'loct_age'); +EXPLAIN (VERBOSE, COSTS OFF) +SELECT age(e, s) FROM ft_age WHERE age(e, s) = '29 days'; + QUERY PLAN +------------------------------------------------------------- + Foreign Scan on public.ft_age + Output: age(e, s) + Filter: (age(ft_age.e, ft_age.s) = '@ 29 days'::interval) + Remote SQL: SELECT s, e FROM public.loct_age +(4 rows) + +SELECT age(e, s) FROM ft_age WHERE age(e, s) = '29 days'; + age +----------- + @ 29 days +(1 row) + +DROP FOREIGN TABLE ft_age; +DROP TABLE loct_age; -- Test array type conversion pushdown SET plan_cache_mode = force_generic_plan; PREPARE s(varchar[]) AS SELECT count(*) FROM ft2 WHERE c6 = ANY ($1); diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index 77c9a25a890..85352704690 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -453,6 +453,18 @@ SELECT * FROM ft1 WHERE CASE c3 WHEN c6 THEN true ELSE c3 < 'bar' END; EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 WHERE CASE c3 COLLATE "C" WHEN c6 THEN true ELSE c3 < 'bar' END; +-- age(timestamptz, timestamptz) depends on TimeZone, so it can't be shipped: +-- the remote session runs with TimeZone = GMT +CREATE TABLE loct_age (s timestamptz, e timestamptz); +INSERT INTO loct_age VALUES ('2026-01-31 00:00+00', '2026-03-01 00:00+00'); +CREATE FOREIGN TABLE ft_age (s timestamptz, e timestamptz) + SERVER loopback OPTIONS (table_name 'loct_age'); +EXPLAIN (VERBOSE, COSTS OFF) +SELECT age(e, s) FROM ft_age WHERE age(e, s) = '29 days'; +SELECT age(e, s) FROM ft_age WHERE age(e, s) = '29 days'; +DROP FOREIGN TABLE ft_age; +DROP TABLE loct_age; + -- Test array type conversion pushdown SET plan_cache_mode = force_generic_plan; PREPARE s(varchar[]) AS SELECT count(*) FROM ft2 WHERE c6 = ANY ($1); diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 1f21a8eb6d4..ce90b271ffb 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -2562,7 +2562,7 @@ proname => 'interval_larger', prorettype => 'interval', proargtypes => 'interval interval', prosrc => 'interval_larger' }, { oid => '1199', descr => 'date difference preserving months and years', - proname => 'age', prorettype => 'interval', + proname => 'age', provolatile => 's', prorettype => 'interval', proargtypes => 'timestamptz timestamptz', prosrc => 'timestamptz_age' }, # OIDS 1200 - 1299 -- 2.39.5 (Apple Git-154)