assertion failure with unique index + partitioning + join

Started by Alexander Kuzmenkov4 days ago10 messageshackers
Jump to latest
#1Alexander Kuzmenkov
akuzmenkov@tigerdata.com

Hi hackers,

While I was running some random testing on the TimescaleDB extension, I
noticed that I can easily hit an assertion failure in plain Postgres
REL_18_STABLE. I haven't analyzed it at all, but thought I should report
this:

CREATE TABLE t (a bool UNIQUE) PARTITION BY LIST (a);
CREATE TABLE p PARTITION OF t DEFAULT;

SELECT
FROM (
SELECT *
FROM t
LEFT JOIN t r USING (a)
)
WHERE a
GROUP BY ();

TRAP: failed Assert("bms_is_member(i, root->outer_join_rels)"), File:
"../pg/src/backend/optimizer/path/equivclass.c", Line: 3631, PID: 1813021

Stack trace:
#6 0x000055d44911f565 in get_eclass_indexes_for_relids
(root=root@entry=0x55d45c0a2d58,
relids=relids@entry=0x55d45c0af3f0) at
../pg/src/backend/optimizer/path/equivclass.c:3631
#7 0x000055d44911f338 in get_common_eclass_indexes (root=0x55d45c0a2d58,
relids1=0x55d45c0a47e8, relids2=0x55d45c0af3f0) at
../pg/src/backend/optimizer/path/equivclass.c:3662
#8 generate_join_implied_equalities (root=root@entry=0x55d45c0a2d58,
join_relids=join_relids@entry=0x55d45c0af520,
outer_relids=outer_relids@entry=0x55d45c0af3f0,
inner_rel=inner_rel@entry=0x55d45c0a5c28,
sjinfo=sjinfo@entry=0x0) at
../pg/src/backend/optimizer/path/equivclass.c:1599
#9 0x000055d44917a42f in get_baserel_parampathinfo
(root=root@entry=0x55d45c0a2d58,
baserel=baserel@entry=0x55d45c0a5c28, required_outer=0x55d45c0af3f0) at
../pg/src/backend/optimizer/util/relnode.c:1590
#10 0x000055d44916d0ab in create_index_path (root=root@entry=0x55d45c0a2d58,
index=index@entry=0x55d45c0ad048, indexclauses=0x55d45c0af4b0,
indexorderbys=0x0, indexorderbycols=indexorderbycols@entry=0x0,
pathkeys=0x0, indexscandir=ForwardScanDirection, indexonly=<optimized out>,
required_outer=0x55d45c0af3f0, loop_count=<error reading variable: That
operation is not available on integers of more than 8 bytes.>,
loop_count@entry=1, partial_path=<optimized out>) at
../pg/src/backend/optimizer/util/pathnode.c:1070
#11 0x000055d449126dae in build_index_paths (root=root@entry=0x55d45c0a2d58,
rel=rel@entry=0x55d45c0a5c28, index=index@entry=0x55d45c0ad048,
clauses=clauses@entry=0x7ffc6f09da70, useful_predicate=false,
scantype=scantype@entry=ST_ANYSCAN, skip_nonnative_saop=0x7ffc6f09da37) at
../pg/src/backend/optimizer/path/indxpath.c:969
#12 0x000055d449123cc2 in get_index_paths (root=root@entry=0x55d45c0a2d58,
rel=rel@entry=0x55d45c0a5c28, index=0x6, index@entry=0x55d45c0ad048,
clauses=clauses@entry=0x7ffc6f09da70,
bitindexpaths=bitindexpaths@entry=0x7ffc6f09ddb0)
at ../pg/src/backend/optimizer/path/indxpath.c:728
#13 0x000055d449123700 in create_index_paths (root=0x55d45c0a2d58,
rel=0x55d45c0a5c28) at ../pg/src/backend/optimizer/path/indxpath.c:285
#14 0x000055d4491141ce in set_rel_pathlist (root=root@entry=0x55d45c0a2d58,
rel=rel@entry=0x55d45c0a5c28, rti=5, rte=0x55d45c0a5660) at
../pg/src/backend/optimizer/path/allpaths.c:499
#15 0x000055d449114053 in set_append_rel_pathlist (root=0x55d45c0a2d58,
rel=0x55d45c0a4cd0, rti=2, rte=0x55d45bff3dc8) at
../pg/src/backend/optimizer/path/allpaths.c:1290
#16 set_rel_pathlist (root=root@entry=0x55d45c0a2d58, rel=0x55d45c0a4cd0,
rti=rti@entry=2, rte=0x55d45bff3dc8) at
../pg/src/backend/optimizer/path/allpaths.c:479
#17 0x000055d44910fd69 in set_base_rel_pathlists (root=0x55d45c0a2d58) at
../pg/src/backend/optimizer/path/allpaths.c:351
#18 make_one_rel (root=root@entry=0x55d45c0a2d58,
joinlist=joinlist@entry=0x55d45c0ad6b8)
at ../pg/src/backend/optimizer/path/allpaths.c:221
#19 0x000055d449144f89 in query_planner (root=0x55d45c0a2d58,
qp_callback=<optimized out>, qp_extra=qp_extra@entry=0x7ffc6f09df48) at
../pg/src/backend/optimizer/plan/planmain.c:288
#20 0x000055d44914817e in grouping_planner (root=root@entry=0x55d45c0a2d58,
tuple_fraction=<optimized out>, tuple_fraction@entry=0,
setops=setops@entry=0x0)
at ../pg/src/backend/optimizer/plan/planner.c:1896
#21 0x000055d449146ae2 in subquery_planner (glob=glob@entry=0x55d45bfbc1e8,
parse=parse@entry=0x55d45bfbc2f8, parent_root=parent_root@entry=0x0,
hasRecursion=false, tuple_fraction=<error reading variable: That operation
is not available on integers of more than 8 bytes.>, setops=setops@entry=0x0)
at ../pg/src/backend/optimizer/plan/planner.c:1274
#22 0x000055d44914519a in standard_planner (parse=0x55d45bfbc2f8,
query_string=<optimized out>, cursorOptions=2048, boundParams=<optimized
out>) at ../pg/src/backend/optimizer/plan/planner.c:453
#23 0x000055d44914503e in planner (parse=0x1baa1d, parse@entry=0x55d45bfbc2f8,
query_string=0x1baa1d <error: Cannot access memory at address 0x1baa1d>,
query_string@entry=0x55d45bfbaba0 "SELECT", ' ' <repeats 91 times>, "\n
FROM (\n SELECT *", ' ' <repeats 79 times>..., cursorOptions=6,
cursorOptions@entry=2048, boundParams=0x7f05ac2969fc
<__GI___pthread_kill+300>, boundParams@entry=0x0) at
../pg/src/backend/optimizer/plan/planner.c:313

Best regards
Alexander Kuzmenkov

#2Tender Wang
tndrwang@gmail.com
In reply to: Alexander Kuzmenkov (#1)
Re: assertion failure with unique index + partitioning + join

Alexander Kuzmenkov <akuzmenkov@tigerdata.com> 于2026年6月15日周一 23:39写道:

Hi hackers,

While I was running some random testing on the TimescaleDB extension, I noticed that I can easily hit an assertion failure in plain Postgres REL_18_STABLE. I haven't analyzed it at all, but thought I should report this:

CREATE TABLE t (a bool UNIQUE) PARTITION BY LIST (a);
CREATE TABLE p PARTITION OF t DEFAULT;
SELECT
FROM (
SELECT *
FROM t
LEFT JOIN t r USING (a)
)
WHERE a
GROUP BY ();

TRAP: failed Assert("bms_is_member(i, root->outer_join_rels)"), File: "../pg/src/backend/optimizer/path/equivclass.c", Line: 3631, PID: 1813021

I can reproduce this issue on HEAD.
The root->outer_join_rels is empty, and the i is 3(rel: t r).
The left join can be removed in remove_useless_joins(), in which
root->outer_join_rels is changed to be empty.
But the from->qual in this case has already been distributed to its rel.
(gdb) pgprint brel->baserestrictinfo
RestrictInfo [is_pushed_down=true can_join=false pseudoconstant=false
has_clone=false is_clone=false leakproof=false
has_volatile=VOLATILITY_UNKNOWN security_level=0
num_base_rels=1 rinfo_serial=1 eval_cost={startup = -1,
per_tuple = 0} norm_selec=-1 outer_selec=-1 outer_is_left=false
hashjoinoperator=0 left_bucketsize=-1
right_bucketsize=-1 left_mcvfreq=-1 right_mcvfreq=-1
left_hasheqoperator=0 right_hasheqoperator=0]
[clause]
PlaceHolderVar [phid=1 phlevelsup=0]
[phexpr] Var [varno=2 varattno=1 vartype=16
varreturningtype=VAR_RETURNING_DEFAULT varnosyn=2 varattnosyn=1]
[phrels] Bitmapset [4 3 2]
[clause_relids] Bitmapset [2]
[required_relids] Bitmapset [2]

It was distributed to (rel:2, table t), but I think "[phrels]
Bitmapset [4 3 2]" is incorrect. After leftjoin is removed, we no
longer need rel=4 (outerjoin) and rel=3 (t r).
But the qual has already been distributed to the (rel: 2) when
deconstruct_jointree (). Then the qual is inherited by its child(table
p)
It seems that we have no logic to process this distributed
restrictinfo in remove_useless_joins().

I need to dig deeper to find out how to fix it.

--
Thanks,
Tender Wang

#3Tender Wang
tndrwang@gmail.com
In reply to: Tender Wang (#2)
Re: assertion failure with unique index + partitioning + join

Hi all,

Tender Wang <tndrwang@gmail.com> 于2026年6月16日周二 09:58写道:

I can reproduce this issue on HEAD.
The root->outer_join_rels is empty, and the i is 3(rel: t r).
The left join can be removed in remove_useless_joins(), in which
root->outer_join_rels is changed to be empty.
But the from->qual in this case has already been distributed to its rel.
(gdb) pgprint brel->baserestrictinfo
RestrictInfo [is_pushed_down=true can_join=false pseudoconstant=false
has_clone=false is_clone=false leakproof=false
has_volatile=VOLATILITY_UNKNOWN security_level=0
num_base_rels=1 rinfo_serial=1 eval_cost={startup = -1,
per_tuple = 0} norm_selec=-1 outer_selec=-1 outer_is_left=false
hashjoinoperator=0 left_bucketsize=-1
right_bucketsize=-1 left_mcvfreq=-1 right_mcvfreq=-1
left_hasheqoperator=0 right_hasheqoperator=0]
[clause]
PlaceHolderVar [phid=1 phlevelsup=0]
[phexpr] Var [varno=2 varattno=1 vartype=16
varreturningtype=VAR_RETURNING_DEFAULT varnosyn=2 varattnosyn=1]
[phrels] Bitmapset [4 3 2]
[clause_relids] Bitmapset [2]
[required_relids] Bitmapset [2]
I need to dig deeper to find out how to fix it.

I tried on the old version (pg1212), I got the correct plan, no crash.
And after pulling up the subquery, no PlaceHolderVar. I found after
commit cc5d98525d43,

commit cc5d98525d43c22b98f360ef0f2c8d7dc57f04dc
Author: Richard Guo <rguo@postgresql.org>
Date: Thu Mar 13 16:36:03 2025 +0900

Fix incorrect handling of subquery pullup

the from->qual will include PHV, because this query contains groupingsets.
I reverted this commit, I got correct plan, too:
postgres=# explain SELECT
FROM (
SELECT *
FROM t
LEFT JOIN t r USING (a)
) ss
WHERE a
GROUP BY ();
QUERY PLAN
------------------------------------------
Result (cost=0.00..0.01 rows=1 width=0)
(1 row)

I added Richard to the cc list. He may know more about this.

--
Thanks,
Tender Wang

#4Tender Wang
tndrwang@gmail.com
In reply to: Tender Wang (#3)
Re: assertion failure with unique index + partitioning + join

Tender Wang <tndrwang@gmail.com> 于2026年6月16日周二 12:06写道:

Hi all,

Tender Wang <tndrwang@gmail.com> 于2026年6月16日周二 09:58写道:

I can reproduce this issue on HEAD.
The root->outer_join_rels is empty, and the i is 3(rel: t r).
The left join can be removed in remove_useless_joins(), in which
root->outer_join_rels is changed to be empty.
But the from->qual in this case has already been distributed to its rel.
(gdb) pgprint brel->baserestrictinfo
RestrictInfo [is_pushed_down=true can_join=false pseudoconstant=false
has_clone=false is_clone=false leakproof=false
has_volatile=VOLATILITY_UNKNOWN security_level=0
num_base_rels=1 rinfo_serial=1 eval_cost={startup = -1,
per_tuple = 0} norm_selec=-1 outer_selec=-1 outer_is_left=false
hashjoinoperator=0 left_bucketsize=-1
right_bucketsize=-1 left_mcvfreq=-1 right_mcvfreq=-1
left_hasheqoperator=0 right_hasheqoperator=0]
[clause]
PlaceHolderVar [phid=1 phlevelsup=0]
[phexpr] Var [varno=2 varattno=1 vartype=16
varreturningtype=VAR_RETURNING_DEFAULT varnosyn=2 varattnosyn=1]
[phrels] Bitmapset [4 3 2]
[clause_relids] Bitmapset [2]
[required_relids] Bitmapset [2]
I need to dig deeper to find out how to fix it.

I tried on the old version (pg1212), I got the correct plan, no crash.
And after pulling up the subquery, no PlaceHolderVar. I found after
commit cc5d98525d43,

commit cc5d98525d43c22b98f360ef0f2c8d7dc57f04dc
Author: Richard Guo <rguo@postgresql.org>
Date: Thu Mar 13 16:36:03 2025 +0900

Fix incorrect handling of subquery pullup

the from->qual will include PHV, because this query contains groupingsets.
I reverted this commit, I got correct plan, too:
postgres=# explain SELECT
FROM (
SELECT *
FROM t
LEFT JOIN t r USING (a)
) ss
WHERE a
GROUP BY ();
QUERY PLAN
------------------------------------------
Result (cost=0.00..0.01 rows=1 width=0)
(1 row)

I added Richard to the cc list. He may know more about this.

In apply_child_basequals(), the restricintinfo->clause_relids of the
child is different from the parent's.
The parent's restricintinfo->clause_relids only contains rel:2, but
the child contains (3,4,5).
The difference is due to pull_varnos_walker() when processing the
PlaceholdVar case.

I do a quick fix as below:
diff --git a/src/backend/optimizer/util/inherit.c
b/src/backend/optimizer/util/inherit.c
index 6a7b9edff3f..fcf0ad7a187 100644
--- a/src/backend/optimizer/util/inherit.c
+++ b/src/backend/optimizer/util/inherit.c
@@ -861,6 +861,21 @@ apply_child_basequals(PlannerInfo *root,
RelOptInfo *parentrel,
                ListCell   *lc2;
                Assert(IsA(rinfo, RestrictInfo));
+               if (IsA(rinfo->clause, PlaceHolderVar))
+               {
+                       PlaceHolderVar *phv = (PlaceHolderVar *) rinfo->clause;
+                       PlaceHolderInfo *phinfo = NULL;
+                       if (phv->phlevelsup == 0)
+                       {
+                               if (phv->phid < root->placeholder_array_size)
+                                       phinfo =
root->placeholder_array[phv->phid];
+                       }
+                       if (phinfo != NULL)
+                       {
+                               phv->phrels = bms_copy(phinfo->ph_var->phrels);
+                       }
+
+               }

The issue has gone, and regression tests all pass.
But this is very simply fixed.
What do you think?

--
Thanks,
Tender Wang

#5Richard Guo
guofenglinux@gmail.com
In reply to: Tender Wang (#2)
Re: assertion failure with unique index + partitioning + join

On Tue, Jun 16, 2026 at 10:58 AM Tender Wang <tndrwang@gmail.com> wrote:

It was distributed to (rel:2, table t), but I think "[phrels]
Bitmapset [4 3 2]" is incorrect. After leftjoin is removed, we no
longer need rel=4 (outerjoin) and rel=3 (t r).
But the qual has already been distributed to the (rel: 2) when
deconstruct_jointree (). Then the qual is inherited by its child(table
p)
It seems that we have no logic to process this distributed
restrictinfo in remove_useless_joins().

When removing the left join, remove_rel_from_query() fixes up the
relid sets of RestrictInfos and EquivalenceMembers, and the canonical
PlaceHolderVars, but it does not rewrite the PlaceHolderVars embedded
in clause and EC-member expressions. This is usually fine, because
later processing consults those relid sets rather than the embedded
PHVs.

The trouble is that such an expression can later be translated for an
appendrel child and have its relids recomputed by pull_varnos(). If
the embedded PlaceHolderVar's phrels still mentions the removed
relation, pull_varnos() folds it back in, so the rebuilt clause
references a no-longer-existent rel. That produces a parameterized
path keyed on the removed relation, which trips the assertion.

I think we can fix it by stripping the removed relids from the PHVs in
surviving rels' baserestrictinfo and in EquivalenceClass member
expressions. See attached.

(I went back and forth on instead making pull_varnos() robust to the
stale phrels, but that turned out fragile: it can't reliably
distinguish a removed relid from a not-yet-built appendrel child, and
it broke unrelated partitionwise-join plans.)

Thoughts?

- Richard

Attachments:

v1-0001-Strip-removed-relation-references-from-PlaceHolde.patchapplication/octet-stream; name=v1-0001-Strip-removed-relation-references-from-PlaceHolde.patchDownload+163-11
#6Tender Wang
tndrwang@gmail.com
In reply to: Richard Guo (#5)
Re: assertion failure with unique index + partitioning + join

Richard Guo <guofenglinux@gmail.com> 于2026年6月16日周二 16:27写道:

On Tue, Jun 16, 2026 at 10:58 AM Tender Wang <tndrwang@gmail.com> wrote:

It was distributed to (rel:2, table t), but I think "[phrels]
Bitmapset [4 3 2]" is incorrect. After leftjoin is removed, we no
longer need rel=4 (outerjoin) and rel=3 (t r).
But the qual has already been distributed to the (rel: 2) when
deconstruct_jointree (). Then the qual is inherited by its child(table
p)
It seems that we have no logic to process this distributed
restrictinfo in remove_useless_joins().

When removing the left join, remove_rel_from_query() fixes up the
relid sets of RestrictInfos and EquivalenceMembers, and the canonical
PlaceHolderVars, but it does not rewrite the PlaceHolderVars embedded
in clause and EC-member expressions. This is usually fine, because
later processing consults those relid sets rather than the embedded
PHVs.

The trouble is that such an expression can later be translated for an
appendrel child and have its relids recomputed by pull_varnos(). If
the embedded PlaceHolderVar's phrels still mentions the removed
relation, pull_varnos() folds it back in, so the rebuilt clause
references a no-longer-existent rel. That produces a parameterized
path keyed on the removed relation, which trips the assertion.

Yes, I came to the same conclusion.

I think we can fix it by stripping the removed relids from the PHVs in
surviving rels' baserestrictinfo and in EquivalenceClass member
expressions. See attached.

The fix I proposed was somewhat specific to this particular issue,
whereas your patch takes a more general approach.
I took a quick pass over the patch and didn't notice any obvious issues.
I won't have time to review it in detail today, but I'll try to take a
closer look tomorrow morning.

(I went back and forth on instead making pull_varnos() robust to the
stale phrels, but that turned out fragile: it can't reliably
distinguish a removed relid from a not-yet-built appendrel child, and
it broke unrelated partitionwise-join plans.)

Interesting, I hadn't thought of that approach.

--
Thanks,
Tender Wang

#7Tender Wang
tndrwang@gmail.com
In reply to: Richard Guo (#5)
Re: assertion failure with unique index + partitioning + join

Hi all,

Richard Guo <guofenglinux@gmail.com> 于2026年6月16日周二 16:27写道:

I think we can fix it by stripping the removed relids from the PHVs in
surviving rels' baserestrictinfo and in EquivalenceClass member
expressions. See attached.

I spent some time reviewing the patch in detail.
A couple of minor suggestions:
In remove_rel_from_eclass(), we have
...
if (!IsA(em->em_expr, Var))
em->em_expr = (Expr *)
remove_rel_from_phvs((Node *) em->em_expr, relid, ojrelid);
...
"where s.id = 1" in the test case, the Const node seems to be able to skip, too.

Other than the minor suggestions above, the patch looks good to me.

I have another question, although it's unrelated to this bug.
The logic in remove_rel_from_query() makes me a bit uneasy. It
currently has to handle both self-join and outer-join cases,
and it seems possible that inner-join-specific handling could end up
being added there in the future as well.
The function appears to be taking on multiple distinct
responsibilities, which makes the overall design feel somewhat
awkward.

Perhaps this could be improved as part of your future inner-join removal patch.

--
Thanks,
Tender Wang

#8Richard Guo
guofenglinux@gmail.com
In reply to: Tender Wang (#7)
Re: assertion failure with unique index + partitioning + join

On Wed, Jun 17, 2026 at 11:00 AM Tender Wang <tndrwang@gmail.com> wrote:

In remove_rel_from_eclass(), we have
...
if (!IsA(em->em_expr, Var))
em->em_expr = (Expr *)
remove_rel_from_phvs((Node *) em->em_expr, relid, ojrelid);
...
"where s.id = 1" in the test case, the Const node seems to be able to skip, too.

Good point. Patch updated.

I have another question, although it's unrelated to this bug.
The logic in remove_rel_from_query() makes me a bit uneasy. It
currently has to handle both self-join and outer-join cases,
and it seems possible that inner-join-specific handling could end up
being added there in the future as well.
The function appears to be taking on multiple distinct
responsibilities, which makes the overall design feel somewhat
awkward.

Yeah. remove_rel_from_query() was originally written for left-join
removal only. The self-join elimination (SJE) commit grafted
self-join removal onto it, which made it do more than one thing within
one function. Commit 20efbdffe cleaned it up, clarifying the
separation between the left-join removal and self-join elimination
code paths within it. That cleanup was also meant to make it better
structured for adding new types of join removal, such as inner-join
removal, in the future.

That said, I agree that the current shape is not ideal. A better
structure might be to make remove_rel_from_query() handle only the
bookkeeping that is common to all removal types, and push the
type-specific work into the per-type callers. I think that is worth
pursuing as separate future work, kept apart from the current bug fix.

- Richard

Attachments:

v2-0001-Strip-removed-relation-references-from-PlaceHolde.patchapplication/octet-stream; name=v2-0001-Strip-removed-relation-references-from-PlaceHolde.patchDownload+165-11
#9Tender Wang
tndrwang@gmail.com
In reply to: Richard Guo (#8)
Re: assertion failure with unique index + partitioning + join

Richard Guo <guofenglinux@gmail.com> 于2026年6月17日周三 13:51写道:

On Wed, Jun 17, 2026 at 11:00 AM Tender Wang <tndrwang@gmail.com> wrote:

In remove_rel_from_eclass(), we have
...
if (!IsA(em->em_expr, Var))
em->em_expr = (Expr *)
remove_rel_from_phvs((Node *) em->em_expr, relid, ojrelid);
...
"where s.id = 1" in the test case, the Const node seems to be able to skip, too.

Good point. Patch updated.

In remove_rel_from_phvs_mutator(), we have:

else if (IsA(node, Query))
{
Query *newnode;

...
}

I want to find what kind of SQL can enter the above else-if block.
So I added "assert(0)" to the else-if block. But no regression test crashed.
We test rel->baserestrictinfo here. Is it possible that the clause
includes a Query structure?
I know some xxx_mutator() functions in the planner have the same logic
processing pattern.
Is there a need to add a test case to cover the above code?

Yeah. remove_rel_from_query() was originally written for left-join
removal only. The self-join elimination (SJE) commit grafted
self-join removal onto it, which made it do more than one thing within
one function. Commit 20efbdffe cleaned it up, clarifying the
separation between the left-join removal and self-join elimination
code paths within it. That cleanup was also meant to make it better
structured for adding new types of join removal, such as inner-join
removal, in the future.

That said, I agree that the current shape is not ideal. A better
structure might be to make remove_rel_from_query() handle only the
bookkeeping that is common to all removal types, and push the
type-specific work into the per-type callers. I think that is worth
pursuing as separate future work, kept apart from the current bug fix.

Agree.

--
Thanks,
Tender Wang

#10Richard Guo
guofenglinux@gmail.com
In reply to: Tender Wang (#9)
Re: assertion failure with unique index + partitioning + join

On Thu, Jun 18, 2026 at 10:01 PM Tender Wang <tndrwang@gmail.com> wrote:

In remove_rel_from_phvs_mutator(), we have:

else if (IsA(node, Query))
{
Query *newnode;

...
}

I want to find what kind of SQL can enter the above else-if block.
So I added "assert(0)" to the else-if block. But no regression test crashed.
We test rel->baserestrictinfo here. Is it possible that the clause
includes a Query structure?

Good point. I considered this, and the answer is no. Any SubLink
that could contain a Query has already been expanded into a SubPlan by
the time we reach left-join removal, so we never encounter a Query
here.

I think what I had in mind when I wrote that branch was a SubLink
wrapped in a PHV with phlevelsup > 0, since SS_process_sublinks does
not recurse into the arguments of such an outer-level PHV. But that
case cannot arise here either: at left-join removal we only ever see
phlevelsup == 0 PHVs, because upper-level ones have already been
replaced with Params.

So we can drop the 'else if' branch. And we can remove sublevels_up
from remove_rel_from_phvs_context, and that makes it a one-field
struct, so we can remove the struct as well and just pass the Relids
directly.

Patch updated.

- Richard

Attachments:

v3-0001-Strip-removed-relation-references-from-PlaceHolde.patchapplication/octet-stream; name=v3-0001-Strip-removed-relation-references-from-PlaceHolde.patchDownload+142-11