Skip to content

fix: a join with a WHERE that names no column no longer fails in the join runtime filter (#1437) - #1440

Merged
jdatcmd merged 1 commit into
mainfrom
fix/1437-runtime-filter-gate
Oct 15, 2026
Merged

jdatcmd merged 1 commit into
mainfrom
fix/1437-runtime-filter-gate

Conversation

@jdatcmd

@jdatcmd jdatcmd commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator

Fixes #1437.

The bug

A join to a columnar fact table failed when its WHERE named no column, for example (SELECT false), (SELECT true) or now() < '2000-01-01'. EXPLAIN failed too.

  • Release build: ERROR: pgcolumnar runtime filter expected a core Hash Join.
  • Assert build: a backend abort, 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 Result over the join. The coordinator expected the HashJoin itself as its child. linitial_node cast the Result before the IsA check, which is what aborts on an assert build.

The fix (src/columnar_runtime_filter.c)

Evidence

Red first, both twins, PG17:

tree result
main the first gated query aborts the backend; every later arm fails in recovery
the cast fixed, no decline exactly the four gate arms fail with expected a core Hash Join, against heap's 0|| and 200|639900|25600; pytest gets InternalError_
this PR 55/55 shell, 58 pytest checks

Green, PG15 to 19: shell 55/55 on each major; pytest 73 checks (test_join_runtime_filter.py plus test_native_agg.py) on each major.

Premises, so the arms cannot pass on nothing:

  • the same join with no gate takes the coordinator;
  • heap's true-gate answer holds the matching rows.

lib.sh's check accepts 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 gate with --registered (298 suites) and --against origin/main returns rc 0, with the ceiling 224 -> 224. orphan-scan reports 0 orphans.

Guards. 438 pass. Collection gives 604 cluster tests, the new count.

For the reviewer

🤖 Generated with Claude Code

…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>
@jdatcmd

jdatcmd commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator Author

@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 OffgridwithJD left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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;
  • IN and EXISTS semi 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A join with a WHERE that references no column fails: "pgcolumnar runtime filter expected a core Hash Join"

2 participants