[PG19] Stale state after the join removal restart gives wrong results

Started by shihao zhong2 days ago7 messageshackers
Beta feature

Hackorum builds and tests every patch posted to the lists, not only commitfest submissions. This is Hackorum's own CI rather than the PostgreSQL project's, and it is still under testing - please report anything that looks wrong.

won't retrysuccessCI history

This thread has been committed, so CI has stopped here. Anything below is the last result it produced.

You can run a PostgreSQL built from this patch straight from Docker, with no checkout and no build:

docker run --rm -p 5432:5432 ghcr.io/hackorum-dev/postgres-patch:t254166
psql -h localhost -U postgres

Built from patchset v6 (message #6), October 14, 2026 at 04:19 AM.

Every patchset is also pushed to a branch of our PostgreSQL fork, so you can check out the same tree CI built. Without a PostgreSQL checkout:

git clone --branch t254166_6 https://github.com/hackorum-dev/postgres.git

In a checkout you already have, add the fork once:

git remote add hackorum https://github.com/hackorum-dev/postgres.git

then, for this patchset and every later one:

git fetch hackorum t254166_6 && git checkout t254166_6

Patchset v6 (message #6) is on t254166_6

Jump to latest
#1shihao zhong
zhong950419@gmail.com

Hi,

This is High 1 from my list in [1]/messages/by-id/CAGRkXqSFdjqZP_kwRz7dv6LHX-CSrkDnynNXgntRsCEnQTKxmg@mail.gmail.com.

Since 2ebf25e7d70, query_planner() jumps back to its restart label
after it removes a join and runs standard_qp_callback() again. Two
things from the first pass are left behind. Both are on master, on
REL_19_STABLE and on REL_18_STABLE (9f25197bf27), 18.6 does not have
them.

1. adjust_group_pathkeys_for_groupagg() sets aggpresorted on the
aggregates it picks and nothing clears it. When self join elimination
removes a rel and the second pass picks a different presorted group,
the stale flag makes the executor skip the sort.

create table sj (id int primary key, p int, q int);
insert into sj select i, i % 3, i from generate_series(1, 30) i;
select count(distinct a.p), count(distinct a.q), sum(distinct a2.q)
from sj a join sj a2 on a.id = a2.id;

master returns 29 for the first column, the answer is 3.
enable_self_join_elimination = off gives 3. 0001 clears the flag at
the top of that function.

2. A removed rel keeps its tablesample. When the sample argument
refers to another rel that the second pass then removes,
ChangeVarNodes hits Assert(context->new_index != INVALID_VAR) in
rewriteManip.c. Non assert builds write INVALID_VAR into a dead rte
and seem fine. Richard looked at the rte fields in [2]/messages/by-id/CAMbWs4-wxRAFx8RcsRBJ6mawf7mbOp9J-ms5n3Di0U8T=SxCHg@mail.gmail.com and thought
tablesample was safe, this is the case that is not. 0002 clears
rte->tablesample next to securityQuals.

0003 adds both cases to join.sql. The patches apply to master,
REL_19_STABLE and REL_18_STABLE.

[1]: /messages/by-id/CAGRkXqSFdjqZP_kwRz7dv6LHX-CSrkDnynNXgntRsCEnQTKxmg@mail.gmail.com
/messages/by-id/CAGRkXqSFdjqZP_kwRz7dv6LHX-CSrkDnynNXgntRsCEnQTKxmg@mail.gmail.com
[2]: /messages/by-id/CAMbWs4-wxRAFx8RcsRBJ6mawf7mbOp9J-ms5n3Di0U8T=SxCHg@mail.gmail.com
/messages/by-id/CAMbWs4-wxRAFx8RcsRBJ6mawf7mbOp9J-ms5n3Di0U8T=SxCHg@mail.gmail.com

Thanks,
Shihao

Attachments:

t254166_1
v1-0002-Clear-tablesample-of-a-removed-rel-in-join-remova.patchapplication/octet-stream; name=v1-0002-Clear-tablesample-of-a-removed-rel-in-join-remova.patchDownload+1-1
v1-0003-Add-tests-for-planner-restart-after-join-removal.patchapplication/octet-stream; name=v1-0003-Add-tests-for-planner-restart-after-join-removal.patchDownload+52-1
v1-0001-Reset-aggpresorted-when-the-planner-restarts-afte.patchapplication/octet-stream; name=v1-0001-Reset-aggpresorted-when-the-planner-restarts-afte.patchDownload+13-1
#2shihao zhong
zhong950419@gmail.com
In reply to: shihao zhong (#1)
Re: [PG19] Stale state after the join removal restart gives wrong results

master returns 29 for the first column, the answer is 3.
enable_self_join_elimination = off gives 3. 0001 clears the flag at
the top of that function.

master(wrong) returns 30, not 29.

Thanks,
Shihao

#3Tom Lane
tgl@sss.pgh.pa.us
In reply to: shihao zhong (#1)
Re: [PG19] Stale state after the join removal restart gives wrong results

shihao zhong <zhong950419@gmail.com> writes:

1. adjust_group_pathkeys_for_groupagg() sets aggpresorted on the
aggregates it picks and nothing clears it. When self join elimination
removes a rel and the second pass picks a different presorted group,
the stale flag makes the executor skip the sort.

Hmm. I've not looked at exactly what that function does, but just
going by the name, why is it being called inside the restart loop
rather than later? I feel like anything to do with identifying
pathkeys could/should wait till after join removal.

2. A removed rel keeps its tablesample. When the sample argument
refers to another rel that the second pass then removes,
ChangeVarNodes hits Assert(context->new_index != INVALID_VAR) in
rewriteManip.c. Non assert builds write INVALID_VAR into a dead rte
and seem fine. Richard looked at the rte fields in [2] and thought
tablesample was safe, this is the case that is not. 0002 clears
rte->tablesample next to securityQuals.

Good catch, although I kind of wonder why we're allowing lateral
refs in tablesample clauses in the first place.

regards, tom lane

#4Tom Lane
tgl@sss.pgh.pa.us
In reply to: Tom Lane (#3)
Re: [PG19] Stale state after the join removal restart gives wrong results

I wrote:

shihao zhong <zhong950419@gmail.com> writes:

1. adjust_group_pathkeys_for_groupagg() sets aggpresorted on the
aggregates it picks and nothing clears it. When self join elimination
removes a rel and the second pass picks a different presorted group,
the stale flag makes the executor skip the sort.

Hmm. I've not looked at exactly what that function does, but just
going by the name, why is it being called inside the restart loop
rather than later? I feel like anything to do with identifying
pathkeys could/should wait till after join removal.

Concretely, the attached fixes the given test case and passes
check-world; but I've not tried harder than that to break it.

regards, tom lane

Attachments:

t254166_4
wip-delay-calling-qp_callback.patchtext/x-diff; charset=us-ascii; name=wip-delay-calling-qp_callback.patchDownload+7-7
#5shihao zhong
zhong950419@gmail.com
In reply to: Tom Lane (#4)
Re: [PG19] Stale state after the join removal restart gives wrong results

Hi Tom,

Tom Lane <tgl@sss.pgh.pa.us> writes:

Concretely, the attached fixes the given test case and passes
check-world; but I've not tried harder than that to break it.

I like this better than my 0001, and I could not break it. I kept
0001 small only because of where 19 is. If you think moving the call
is right for 19 and 18 too, I am fine with that.

I did a simple fuzz test generating 2200 random self join queries giving
the same results as 18.6 and the same plans as master. Tests pass
on REL_19_STABLE and REL_18_STABLE too.

Good catch, although I kind of wonder why we're allowing lateral
refs in tablesample clauses in the first place.

0002 is still needed, the Assert fires with only your patch.

I also hit one more of that kind. Self join elimination leaves the
removed rel's securityQuals in its rte, now pointing at the kept rel.
When a later pass removes the kept rel too, the same Assert fires.

create table r (k int primary key, v int);
alter table r enable row level security;
create policy p on r using (v > 0);
create table x (k int primary key);
create role u1;
grant select on r, x to u1;
set role u1;
select x.k from x left join (r a join r b on a.k = b.k) on x.k = a.k;

I will send a patch to address ^^^^^ later today.

Thanks,
Shihao

#6shihao zhong
zhong950419@gmail.com
In reply to: shihao zhong (#5)
Re: [PG19] Stale state after the join removal restart gives wrong results

Hi Tom,

Thanks for committing 6b3f8857aa8. Both Asserts I mentioned still
fire on master, so here is v2.

0001 is my old 0002. It clears tablesample of a rel removed by outer
join removal. If you would rather forbid lateral refs in TABLESAMPLE,
I am happy to write that.
0001 would still be needed for the back branches.

0002 clears securityQuals of a rel removed by self join elimination.
The kept rel has the same quals because SJE requires the same
checkAsUser and the same row mark type, so RLS still filters.

0003 adds both cases to join.sql.

The tablesample Assert also fires on the REL_16_STABLE and
REL_17_STABLE heads, since 2ebf25e7d70 went back to 16.

Thanks,
Shihao

Attachments:

t254166_6
v2-0001-Clear-tablesample-of-a-removed-rel-in-join-remova.patchapplication/octet-stream; name=v2-0001-Clear-tablesample-of-a-removed-rel-in-join-remova.patchDownload+1-1
v2-0003-Add-tests-for-planner-restart-after-join-removal.patchapplication/octet-stream; name=v2-0003-Add-tests-for-planner-restart-after-join-removal.patchDownload+62-1
v2-0002-Clear-securityQuals-of-a-rel-removed-by-self-join.patchapplication/octet-stream; name=v2-0002-Clear-securityQuals-of-a-rel-removed-by-self-join.patchDownload+7-1
#7Tom Lane
tgl@sss.pgh.pa.us
In reply to: shihao zhong (#6)
Re: [PG19] Stale state after the join removal restart gives wrong results

shihao zhong <zhong950419@gmail.com> writes:

Thanks for committing 6b3f8857aa8. Both Asserts I mentioned still
fire on master, so here is v2.

Right, pushed. I concluded that the test cases probably weren't
worth the cycles forevermore, so didn't include them.

regards, tom lane