It seems we still need to check whether a variable-free qual comes from somewhere that is below the nullable side of an outer join before we decide that it can be evaluated at join domain level, just like we did before. So I wonder if we can add a 'below_outer_join' flag in JoinTreeItem, fill its value during deconstruct_recurse, and check it in distribute_qual_to_rels()
It occurs to me that we can leverage JoinDomain to tell if we are below the nullable side of a higher-level outer join if the clause is not an outer-join clause. If we are belew outer join, the current JoinDomain is supposed to be a proper subset of top JoinDomain. Otherwise the current JoinDomain must be equal to top JoinDomain. And that leads to a fix as code changes below.
--- a/src/backend/optimizer/plan/initsplan.c +++ b/src/backend/optimizer/plan/initsplan.c @@ -2269,8 +2269,16 @@ distribute_qual_to_rels(PlannerInfo *root, Node *clause, } else { - /* eval at join domain level */ - relids = bms_copy(jtitem->jdomain->jd_relids); + JoinDomain *top_jdomain = + linitial_node(JoinDomain, root->join_domains); + + /* + * eval at original syntactic level if we are below an outer join, + * otherwise eval at join domain level, which is actually the top + * of tree + */ + relids = jtitem->jdomain == top_jdomain ? + bms_copy(jtitem->jdomain->jd_relids) : bms_copy(qualscope);