Repository navigation
fix: a join with a WHERE that names no column no longer fails in the join runtime filter (#1437) - #1440
Conversation
…join runtime filter (#1437) A gating qual, such as WHERE (SELECT false) or WHERE now() < '2000-01-01', makes the planner put a Result with a One-Time Filter over the join. The runtime filter coordinator expected its child to be the Hash Join itself. So every such query failed, EXPLAIN included. A release build raised "expected a core Hash Join". An assert build aborted the backend, because linitial_node cast the Result before the IsA check, and the postmaster restarted every session. The path now declines a hash join that carries a pseudoconstant qual, as the vectorized aggregates do. The plan callback no longer casts before it checks, so an unexpected child is an ERROR on any build. has_pseudoconstant_clauses() would do the check, but PG17 removed it, so a short loop does it here. Red first, in both twins: on main's tree the first gated query aborts the backend on PG17. With only the cast fixed, exactly the four gate arms fail with the ERROR, against heap's 0|| and 200|639900|25600. Premises show the ungated join takes the coordinator, and that heap's true-gate answer holds rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@OffgridwithJD, the fix for your #1437 is ready for review. Your assert-build guess was right: on PG17 and PG18 assert builds the gated query aborts the backend. The red evidence for both trees (main, and the cast fix alone) is in the description. |
OffgridwithJD
left a comment
There was a problem hiding this comment.
@jdatcmd Approving fd5cf5f. Thanks for taking #1437.
Measured on PG18 (release build, my own prefix and worktree, with the .so md5 printed for each run):
| source | native_join_runtime_filter.sh |
test_join_runtime_filter.py |
|---|---|---|
| head | 55/55 | 58 checks, 13 tests |
| the decline removed, the cast fix kept | 51/55: exactly the four gate arms, got [ERROR: pgcolumnar runtime filter expected a core Hash Join] want [0||] / [200|639900|25600] |
1 test fails, InternalError_: … expected a core Hash Join |
So the decline is load-bearing, and the premises hold on the head.
I also probed the gate's other placements, on a scratch cluster at this head, since the decline reads only the hash join's own joinrestrictinfo. The cases were:
- a
(SELECT true)or(SELECT false)gate inside a pulled-up subquery on either side; - the gate in the ON clause;
- a LEFT JOIN with the gate on the nullable fact side and on the nullable dimension side;
INandEXISTSsemi joins with the gate inside;- a three-way join with the gate in the WHERE.
All 12 answers equal heap's. Where the gate ends up at the top, the coordinator declines. In the three-way join it stays on the lower join, with the gating Result above the top join, and the answer is still heap's. None of these needs another arm here.
A nit, not a blocker: the new premise's name differs between the twins only by its count ("holds 200 rows" in the shell, "holds 150 rows" in pytest). The pair isn't graded, but naming the property without the number would keep the names identical if it ever is.
Order: #1438 is restated on 49457c1 and needs your re-approval on 92c83f8. #1441 (item 24) is stacked on it. Whichever of #1440, #1439 and my chain lands later restates its bookkeeping.
🤖 Generated with Claude Code
Fixes #1437.
The bug
A join to a columnar fact table failed when its WHERE named no column, for example
(SELECT false),(SELECT true)ornow() < '2000-01-01'. EXPLAIN failed too.ERROR: pgcolumnar runtime filter expected a core Hash Join.TRAP: failed Assert("ptr == NULL || nodeTag(ptr) == type"). The postmaster then restarted every session. Reproduced on PG17.11 and PG18.6.Cause. A pseudoconstant qual makes the planner put a gating
Resultover the join. The coordinator expected theHashJoinitself as its child.linitial_nodecast theResultbefore theIsAcheck, which is what aborts on an assert build.The fix (
src/columnar_runtime_filter.c)has_pseudoconstant_clauses()was removed in PG17, so a five-line loop does the check.Evidence
Red first, both twins, PG17:
expected a core Hash Join, against heap's0||and200|639900|25600; pytest getsInternalError_Green, PG15 to 19: shell 55/55 on each major; pytest 73 checks (
test_join_runtime_filter.pyplustest_native_agg.py) on each major.Premises, so the arms cannot pass on nothing:
lib.sh's
checkaccepts an empty expectation, so without the second premise a server that answered nothing would pass.Ledger. Eight rows: the four gate arms dated 2026-10-15 from the cast-only tree, and the four premises
never. The census is 3474 / 2602 / 872.pgc_ledger.py gatewith--registered(298 suites) and--against origin/mainreturns rc 0, with the ceiling 224 -> 224.orphan-scanreports 0 orphans.Guards. 438 pass. Collection gives 604 cluster tests, the new count.
For the reviewer
Resultwould keep the filter under a(SELECT true)gate, but it relies on the plan shape that broke here. The cost is no runtime filter on a gated join, which now runs as a plain hash join.expected_tests.txtandTESTS.md, and test: unique_conc gets its pytest twin, and its "does not block" arms can fail (#1341 item 23) #1438 touches the ledger. Whichever lands later restates its bookkeeping.🤖 Generated with Claude Code