From 375751aa514b4429527d9e1e37da68d29d63d99a Mon Sep 17 00:00:00 2001 From: Tender Wang Date: Wed, 11 Nov 2026 10:20:27 +0800 Subject: [PATCH v2] Skip SJE if jointree containing VOLATILE function --- src/backend/optimizer/plan/analyzejoins.c | 38 +++++++++++++++++++++++ src/test/regress/expected/join.out | 29 +++++++++++++++++ src/test/regress/sql/join.sql | 19 ++++++++++++ 3 files changed, 86 insertions(+) diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c index 25a863efbde..d68a4bc5d73 100644 --- a/src/backend/optimizer/plan/analyzejoins.c +++ b/src/backend/optimizer/plan/analyzejoins.c @@ -1757,6 +1757,35 @@ match_unique_clauses(RelOptInfo *outer, List *uclauses, Index relid) return true; } +/* + * Check whether a relation has any restriction clauses containing volatile + * functions. Self-join elimination can rewrite or relocate such clauses, + * which could change the number or timing of their evaluations. + */ +static bool +rel_has_volatile_quals(RelOptInfo *rel) +{ + ListCell *lc; + + foreach(lc, rel->baserestrictinfo) + { + RestrictInfo *rinfo = lfirst_node(RestrictInfo, lc); + + if (contain_volatile_functions((Node *) rinfo)) + return true; + } + + foreach(lc, rel->joininfo) + { + RestrictInfo *rinfo = lfirst_node(RestrictInfo, lc); + + if (contain_volatile_functions((Node *) rinfo)) + return true; + } + + return false; +} + /* * Find and remove unique self-joins in a group of base relations that have * the same Oid. @@ -1804,6 +1833,15 @@ remove_self_joins_one_group(PlannerInfo *root, Relids relids) Assert(root->simple_rte_array[k]->relid == root->simple_rte_array[r]->relid); + /* + * Removing either relation would rewrite or relocate its restriction + * clauses. Do not do that if any of them contain volatile functions, + * since their evaluation count or timing is observable. + */ + if (rel_has_volatile_quals(rrel) || + rel_has_volatile_quals(krel)) + continue; + /* * It is impossible to eliminate the join of two relations if they * are not on the same side of every outer join. Otherwise, the diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index 7773852e61b..314636903bf 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -13073,3 +13073,32 @@ WHERE s.x = s.c; (1 row) ROLLBACK; +-- Self-join elimination must preserve separately observable evaluations of +-- volatile quals, even when Var replacement makes the quals identical. +BEGIN; +CREATE TEMP TABLE sje_volatile (id int PRIMARY KEY); +CREATE TEMP SEQUENCE sje_volatile_seq; +INSERT INTO sje_volatile VALUES (1); +SELECT setval('sje_volatile_seq', 1, false); + setval +-------- + 1 +(1 row) + +SELECT count(*) +FROM sje_volatile a +JOIN sje_volatile b ON a.id = b.id +WHERE (nextval('sje_volatile_seq') + a.id) % 2 = 0 + AND (nextval('sje_volatile_seq') + b.id) % 2 = 0; + count +------- + 0 +(1 row) + +SELECT last_value FROM sje_volatile_seq; + last_value +------------ + 2 +(1 row) + +ROLLBACK; diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql index a49d343ec32..efae6809099 100644 --- a/src/test/regress/sql/join.sql +++ b/src/test/regress/sql/join.sql @@ -4978,3 +4978,22 @@ FROM (VALUES (1)) AS v(z) LEFT JOIN WHERE s.x = s.c; ROLLBACK; + +-- Self-join elimination must preserve separately observable evaluations of +-- volatile quals, even when Var replacement makes the quals identical. +BEGIN; + +CREATE TEMP TABLE sje_volatile (id int PRIMARY KEY); +CREATE TEMP SEQUENCE sje_volatile_seq; +INSERT INTO sje_volatile VALUES (1); + +SELECT setval('sje_volatile_seq', 1, false); +SELECT count(*) +FROM sje_volatile a +JOIN sje_volatile b ON a.id = b.id +WHERE (nextval('sje_volatile_seq') + a.id) % 2 = 0 + AND (nextval('sje_volatile_seq') + b.id) % 2 = 0; + +SELECT last_value FROM sje_volatile_seq; + +ROLLBACK; -- 2.43.0