Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,14 @@ true until the next version shipped.
vectorized aggregate answered from the zone maps and never applied the one-time
filter. It now leaves such a query to the ordinary plan. Found by OffgridwithJD in
review (#1435). `test/native_agg.sh` and `test_native_agg.py`.
- **A join to a columnar table with a WHERE that names no column no longer fails.**
`SELECT count(*) FROM fact JOIN dim USING (k) WHERE (SELECT false)`, or a gate on
`now()`, failed with `pgcolumnar runtime filter expected a core Hash Join`, EXPLAIN
included, and on a build with assertions it aborted the backend. The planner puts a
gating Result over such a join, and the join runtime filter expected the Hash Join
itself. The runtime filter now leaves such a join to the ordinary plan. Found by
OffgridwithJD (#1437). `test/native_join_runtime_filter.sh` and
`test_join_runtime_filter.py`.

- **A concurrent `UPDATE` of the same row now fails with 40001 on a table with a
unique index, as documented, not 23505 (#1421).** Two sessions updating one row of
Expand Down
29 changes: 27 additions & 2 deletions src/columnar_runtime_filter.c
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ typedef struct PgColumnarRuntimeTapState

static set_join_pathlist_hook_type previous_set_join_pathlist_hook = NULL;

static bool PgColumnarHasGatingQual(List *restrictinfos);
static Plan *PgColumnarPlanRuntimeFilterPath(PlannerInfo *root,
RelOptInfo *rel,
CustomPath *bestPath,
Expand Down Expand Up @@ -339,7 +340,8 @@ PgColumnarSetJoinPathlist(PlannerInfo *root,
continue;

hashPath = (HashPath *) path;
if (path->param_info != NULL ||
if (PgColumnarHasGatingQual(hashPath->jpath.joinrestrictinfo) ||
path->param_info != NULL ||
path->parallel_aware ||
path->parallel_workers != 0 ||
hashPath->jpath.innerjoinpath->param_info != NULL ||
Expand Down Expand Up @@ -430,6 +432,27 @@ PgColumnarSetJoinPathlist(PlannerInfo *root,
add_path(joinrel, &customPath->path);
}

/*
* A gating qual is a pseudoconstant clause, one that names no column of the
* join, such as WHERE (SELECT false) or WHERE now() < '2000-01-01'. The planner
* puts a Result with a One-Time Filter over a join that carries one, so the
* coordinator's child would be that Result rather than the Hash Join it wraps
* (#1437). The path declines such a join, as the vectorized aggregates do.
* has_pseudoconstant_clauses() would say the same, but PG17 removed it.
*/
static bool
PgColumnarHasGatingQual(List *restrictinfos)
{
ListCell *cell;

foreach(cell, restrictinfos)
{
if (lfirst_node(RestrictInfo, cell)->pseudoconstant)
return true;
}
return false;
}

static AttrNumber
PgColumnarFindKeyResno(Plan *plan, Index varno, AttrNumber attno)
{
Expand Down Expand Up @@ -469,7 +492,9 @@ PgColumnarPlanRuntimeFilterPath(PlannerInfo *root,
if (list_length(customPlans) != 1)
elog(ERROR, "pgcolumnar runtime filter expected one core join plan");

joinPlan = linitial_node(HashJoin, customPlans);
/* linitial, not linitial_node: a cast that fails is an assertion abort on
* an assert build, and this check is what must report the wrong shape */
joinPlan = (HashJoin *) linitial(customPlans);
if (!IsA(joinPlan, HashJoin) || !IsA(innerPlan(joinPlan), Hash))
elog(ERROR, "pgcolumnar runtime filter expected a core Hash Join");

Expand Down
8 changes: 8 additions & 0 deletions test/check_ledger.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -3464,3 +3464,11 @@ vm_privilege vm_privilege vm entry point is covered by this boundary test: vm_se
vm_privilege vm_privilege vm_is_visible refuses a heap relation even for its owner (#748) 15;16;17;18;19 never -
vm_privilege vm_privilege vm_selftest did not report success to the unprivileged role 15;16;17;18;19 2026-10-14 vm_selftest without its owner check
vm_privilege vm_privilege vm_selftest refuses a heap relation even for its owner (#748) 15;16;17;18;19 never -
native_join_runtime_filter native_join_runtime_filter premise: with no gate, the heap-dimension join has the coordinator 15;16;17;18;19 never -
native_join_runtime_filter native_join_runtime_filter premise: heap's answer under a true gate over a heap dimension holds 200 rows 15;16;17;18;19 never -
native_join_runtime_filter native_join_runtime_filter a (SELECT false) gate over a heap dimension equals heap 15;16;17;18;19 2026-10-15 the join runtime filter took a join carrying a gating qual and failed on the Result over it (the tree before #1437)
native_join_runtime_filter native_join_runtime_filter a (SELECT true) gate over a heap dimension equals heap 15;16;17;18;19 2026-10-15 the join runtime filter took a join carrying a gating qual and failed on the Result over it (the tree before #1437)
native_join_runtime_filter native_join_runtime_filter premise: with no gate, the columnar-dimension join has the coordinator 15;16;17;18;19 never -
native_join_runtime_filter native_join_runtime_filter premise: heap's answer under a true gate over a columnar dimension holds 200 rows 15;16;17;18;19 never -
native_join_runtime_filter native_join_runtime_filter a (SELECT false) gate over a columnar dimension equals heap 15;16;17;18;19 2026-10-15 the join runtime filter took a join carrying a gating qual and failed on the Result over it (the tree before #1437)
native_join_runtime_filter native_join_runtime_filter a (SELECT true) gate over a columnar dimension equals heap 15;16;17;18;19 2026-10-15 the join runtime filter took a join carrying a gating qual and failed on the Result over it (the tree before #1437)
14 changes: 13 additions & 1 deletion test/check_ledger_budget.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2791,4 +2791,16 @@ suites_not_covered 224
# never 2596 + 2 = 2598
# dated 865 + 3 = 868
# 2598 + 868 == 3466
checks_never_observed_red 2598
#
# #1437, a join with a WHERE that names no column failed in the join runtime filter
# (jdatcmd): 2598 -> 2602. native_join_runtime_filter gains eight checks from
# five-major runs (55/55 on each of 15-19). The four gate arms are dated on PG17 by
# the tree before the fix with only the asserting cast removed, where each failed
# with 'expected a core Hash Join' against heap's 0|| and 200|639900|25600 (main's
# own tree aborts the backend at the first one). The four premises arrive never.
#
# ledger rows 3466 + 8 = 3474
# never 2598 + 4 = 2602
# dated 868 + 4 = 872
# 2602 + 872 == 3474
checks_never_observed_red 2602
38 changes: 38 additions & 0 deletions test/native_join_runtime_filter.sh
Original file line number Diff line number Diff line change
Expand Up @@ -314,4 +314,42 @@ SQLFQ="SELECT count(*),sum(fact_fq.k),sum(fact_fq.extra) FROM fact_fq JOIN dim_f
onfq="$(pc "${rf_on}SET max_parallel_workers_per_gather=0;SET enable_nestloop=off;SET enable_mergejoin=off;EXPLAIN(ANALYZE,TIMING off,SUMMARY off)$SQLFQ")"
check "fact-qual plan has coordinator" "$(grep -c 'Columnar Runtime Filter Coordinator' <<<"$onfq")" 1
check "fact-qual conjunction equals heap" "$(pc "${rf_on}$SQLFQ"|tail -1)" "$(q "SELECT count(*),sum(heap_fq.k),sum(heap_fq.extra) FROM heap_fq JOIN dim_fq ON heap_fq.k=dim_fq.k WHERE heap_fq.k > 100 AND heap_fq.extra > 40")"
# A gating qual, a WHERE that names no column (#1437). The planner puts a
# Result with a One-Time Filter over the join, so the coordinator's child was a
# Result rather than the Hash Join it expects, and every such query failed with
# "expected a core Hash Join", EXPLAIN included. A true gate must give heap's
# rows and a false one none, over a heap and a columnar dimension. The premises
# show the same join with no gate takes the coordinator, so the gate is what the
# arms vary. Independent of the pytest twin: different names, key range, and
# row count.
q "$(cat <<'SQL'
CREATE TABLE dim_gh(k int);
INSERT INTO dim_gh SELECT g FROM generate_series(3100, 3299) g;
CREATE TABLE dim_gc(k int) USING pgcolumnar;
INSERT INTO dim_gc SELECT g FROM generate_series(3100, 3299) g;
CREATE TABLE fact_g(k int, payload text) USING pgcolumnar;
SELECT pgcolumnar.set_options($t$fact_g$t$, stripe_row_limit => 1000);
INSERT INTO fact_g SELECT g, repeat(md5(g::text), 4) FROM generate_series(1, 15000) g;
CREATE TABLE heap_g AS SELECT * FROM fact_g;
ANALYZE dim_gh;
ANALYZE dim_gc;
ANALYZE fact_g;
SQL
)" >/dev/null
for dim in dim_gh dim_gc; do
kind=heap; [ "$dim" = dim_gc ] && kind=columnar
SQLG="SELECT count(*),sum(fact_g.k),sum(length(fact_g.payload)) FROM fact_g JOIN $dim ON fact_g.k=$dim.k"
ong="$(pc "${rf_on}SET max_parallel_workers_per_gather=0;SET enable_nestloop=off;SET enable_mergejoin=off;EXPLAIN(ANALYZE,TIMING off,SUMMARY off)$SQLG")"
check "premise: with no gate, the $kind-dimension join has the coordinator" \
"$(grep -c 'Columnar Runtime Filter Coordinator' <<<"$ong")" 1
# the reference must hold rows, or a server that answered nothing on both
# sides would pass the comparisons below as equal
check "premise: heap's answer under a true gate over a $kind dimension holds 200 rows" \
"$(q "SELECT count(*) FROM heap_g JOIN $dim ON heap_g.k=$dim.k WHERE (SELECT true)")" 200
for gate in false true; do
check "a (SELECT $gate) gate over a $kind dimension equals heap" \
"$(pc "${rf_on}SET max_parallel_workers_per_gather=0;SET enable_nestloop=off;SET enable_mergejoin=off;$SQLG WHERE (SELECT $gate)" | tail -1)" \
"$(q "SELECT count(*),sum(heap_g.k),sum(length(heap_g.payload)) FROM heap_g JOIN $dim ON heap_g.k=$dim.k WHERE (SELECT $gate)")"
done
done
pgc_summary
9 changes: 9 additions & 0 deletions test/pytest/TESTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3023,6 +3023,15 @@ A non-key fact-table qual with late materialization off. The attach used to
force the two-pass path with only the join key decoded, so the qual dropped
every row. Heap is the oracle. Independent of the shell conjunction arm.

### `test_a_gating_qual_over_the_join_matches_heap`

A WHERE that names no column, `(SELECT false)` or `(SELECT true)`, over a join to a
heap and to a columnar dimension (#1437). The planner puts a gating Result over such
a join, and the coordinator expected a Hash Join there: a release build raised an
ERROR and an assert build aborted the backend. Each answer must be heap's, with
premises that the same join with no gate takes the coordinator and that heap's
true-gate answer holds rows. Independent of the shell arms.

## 26. test_check_records.py: every counted assertion is a record

#937, first phase. The shell harness makes counting and recording the same call,
Expand Down
4 changes: 3 additions & 1 deletion test/pytest/expected_tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -727,4 +727,6 @@ guard_tests 438
# cluster_tests 601 -> 602 for #1341's test_type_matrix.py, one test, by collection
# cluster_tests 602 -> 603 for #1435's test_a_where_with_no_column_in_it_is_applied in
# test_native_agg.py, by collection over the cluster files on this tree.
cluster_tests 603
# cluster_tests 603 -> 604 for #1437's test_a_gating_qual_over_the_join_matches_heap in
# test_join_runtime_filter.py, by collection over the cluster files on this tree.
cluster_tests 604
50 changes: 50 additions & 0 deletions test/pytest/test_join_runtime_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -719,3 +719,53 @@ def test_fact_qual_with_late_mat_off_matches_heap(pgc_conn, expect):
name="late-mat-off fact qual still uses the runtime coordinator",
)
expect.rows([got], [heap], "late-mat-off fact qual equals heap")


def test_a_gating_qual_over_the_join_matches_heap(pgc_conn, expect):
"""A WHERE that names no column puts a gating Result over the join (#1437).

The coordinator expected its child to be the Hash Join, so the Result made
every such query fail, EXPLAIN included: an ERROR on a release build, and a
failed cast assertion that aborted the backend on an assert build. A true
gate must give heap's rows and a false one none, over a heap and a columnar
dimension. The premises show the same join with no gate takes the
coordinator. Independently of native_join_runtime_filter.sh: different
names, key range and row count.
"""
with pgc_conn.cursor() as cur:
cur.execute(
"""
CREATE TABLE dgh(k int);
INSERT INTO dgh SELECT g FROM generate_series(6400, 6549) g;
CREATE TABLE dgc(k int) USING pgcolumnar;
INSERT INTO dgc SELECT g FROM generate_series(6400, 6549) g;
CREATE TABLE fg(k int, payload text) USING pgcolumnar;
SELECT pgcolumnar.set_options($t$fg$t$, stripe_row_limit => 1000);
INSERT INTO fg SELECT g, repeat(md5(g::text), 3)
FROM generate_series(1, 13000) g;
CREATE TABLE hg AS SELECT * FROM fg;
ANALYZE dgh;
ANALYZE dgc;
ANALYZE fg
"""
)
for dim, kind in (("dgh", "heap"), ("dgc", "columnar")):
sql = (f"SELECT count(*), sum(fg.k), sum(length(fg.payload)) "
f"FROM fg JOIN {dim} ON fg.k = {dim}.k")
expect.num(
_plan(pgc_conn, sql, on=True).count("Columnar Runtime Filter Coordinator"), 1,
f"premise: with no gate, the {kind}-dimension join has the coordinator")
with pgc_conn.cursor() as cur:
cur.execute(f"SELECT count(*) FROM hg JOIN {dim} ON hg.k = {dim}.k WHERE (SELECT true)")
expect.num(cur.fetchone()[0], 150,
f"premise: heap's answer under a true gate over a {kind} dimension holds 150 rows")
for gate in ("false", "true"):
with pgc_conn.cursor() as cur:
cur.execute(f"{sql} WHERE (SELECT {gate})")
got = cur.fetchone()
cur.execute(
f"SELECT count(*), sum(hg.k), sum(length(hg.payload)) "
f"FROM hg JOIN {dim} ON hg.k = {dim}.k WHERE (SELECT {gate})")
heap = cur.fetchone()
expect.rows([got], [heap],
f"a (SELECT {gate}) gate over a {kind} dimension equals heap")
Loading