Skip to content

Assert the node before believing the numbers read from it - #203

Merged
jdatcmd merged 1 commit into
jdatcmd:mainfrom
ChronicallyJD:test/plan-shape-assertions
Jul 28, 2026
Merged

Assert the node before believing the numbers read from it#203
jdatcmd merged 1 commit into
jdatcmd:mainfrom
ChronicallyJD:test/plan-shape-assertions

Conversation

@ChronicallyJD

Copy link
Copy Markdown
Collaborator

Taking the node-marker instrument you asked for on #198, with your placement requirement: the assertion is the first check in each section, before any number is read.

The failure this instruments

Your own suite, from the #198 thread: a section headed "the aggregate path reports through a different function" ran a query with a qual. The vectorized aggregate path declines those, so the base custom scan answered instead. Both halves of the file measured the same code path, one of them under a heading saying otherwise, both passed, and the mutation run failed exactly the checks it was expected to fail — so nothing in the process caught it.

That is the gap worth closing generally: a mutation run proves a check discriminates, not that it discriminates on the thing in its name.

The instrument

Each node reports its own marker in EXPLAIN, and the two are disjoint:

node marker
scalar custom scan Columnar Projected Columns, Columnar Total Columns
vectorized aggregate Columnar Vectorized Aggregates

So each assertion is a single positive grep rather than an absence test. That matters: an absence test passes for a plan that fell back to a seq scan, which has no Columnar lines to be absent.

native_agg.sh is the one that most needed it

Eleven parity checks compare a columnar answer against a heap oracle. A fallback plan returns exactly the same answers — that is what makes it a correct fallback. So those eleven cannot tell the metadata path from the path that replaces it, and eleven of them passing says nothing about which one ran.

The node assertion already existed in that file. It ran twelfth. It now runs first.

pushdown_report.sh and native_vecskip.sh read counters straight out of a plan they never checked. Both now assert the node first. pushdown_report.sh additionally asserts that the setting under test is not itself changing which node runs, since every comparison in that file is between two plans and a plan change would make them incomparable.

Verified by taking the node away, not by argument

A GUC rather than a code edit, because the failure being guarded against is a plan change and a GUC is the most faithful way to produce one.

enable_custom_scan = off:

===== pushdown_report =====
FAIL  the plan under test is a columnar custom scan: got [no] want [yes]
FAIL  and it is still one with pushdown off: got [no] want [yes]
FAIL  pushdown on reports a pushed-down filter: got [] want [1]

===== native_vecskip =====
FAIL  the plan under test is a columnar custom scan: got [no] want [yes]
PASS  row count
PASS  single row group
PASS  range parity          <-- still passes with the node gone
FAIL  vectors removed > 0: got [no] want [yes]

enable_vectorization = off:

===== native_agg =====
FAIL  count(*) uses the metadata agg node: got [no] want [yes]
FAIL  sum/min/max uses the metadata agg node: got [no] want [yes]
PASS  count(*)
PASS  count(col,nulls)
PASS  sum(int4)
PASS  sum(int4,nulls)

Those four passes are the point. They agree with the heap oracle because the fallback is correct, and before this change they were the first thing the file reported — four green lines describing a path that was not running.

Gate

  • Build preflight: 15.18, 16.14, 17.6, 18.4, 19beta2 — zero warnings.
  • Full suite on the -O2 --enable-cassert build: ALL VERSIONS PASSED on PG18 and PG19, with pushdown_report=PASS, native_vecskip=PASS and native_agg=PASS in both.

Test-only change; no source touched.

Not done here

I stopped at the three files you named. Other suites read EXPLAIN counters — the parquet pushdown and index-only ones at least — and the same argument applies to any check that names a path. Worth a sweep, but a sweep is a different review from this one, and I would rather you see the shape on three files first.

A check that names a code path has to assert that path ran, and has to do it
before it reads anything out of the plan.

This is the instrument for the failure jdatcmd found in his own suite while
reviewing jdatcmd#198: a section headed "the aggregate path reports through a
different function" ran a query with a qual, the vectorized aggregate path
declines those, and the base custom scan answered instead. Both halves of
the file measured the same code path, one of them under a heading saying
otherwise, both passed, and the mutation run failed exactly the checks it
was expected to fail -- so nothing in the process caught it. A mutation run
proves a check discriminates, not that it discriminates on the thing in its
name.

Each node reports its own marker in EXPLAIN, and the two are disjoint:

  scalar custom scan    Columnar Projected Columns, Columnar Total Columns
  vectorized aggregate  Columnar Vectorized Aggregates

so each assertion is a single positive grep rather than an absence test. An
absence test would pass for a plan that fell back to a seq scan, which has
no Columnar lines to be absent.

native_agg.sh is the one that most needed it. Eleven parity checks compare
a columnar answer against a heap oracle, and a fallback plan returns exactly
the same answers -- that is what makes it a correct fallback. So those
eleven cannot tell the metadata path from the path that replaces it. The
node assertion existed but ran twelfth. It now runs first.

pushdown_report.sh and native_vecskip.sh read counters straight out of a
plan they never checked; both now assert the node first, and
pushdown_report also asserts that the setting under test is not itself
changing which node runs, since every comparison in that file is between
two plans.

Verified by taking the node away rather than by argument. With
enable_custom_scan off, pushdown_report and native_vecskip fail on the node
assertion first. With enable_vectorization off, native_agg fails both node
assertions first and then passes count(*), count(col), sum(int4) and
sum(int4,nulls) against the heap oracle -- which is the whole point: those
four agree because the fallback is correct, and before this change they
were the first thing the file reported.
@jdatcmd

jdatcmd commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Verified and merging. The placement is right in all three files, checked rather than taken:

native_agg.sh:70       check "count(*) uses the metadata agg node"
native_vecskip.sh:55   check "the plan under test is a columnar custom scan"
pushdown_report.sh:66  check "the plan under test is a columnar custom scan"

First in each, before any number is read.

The native_agg.sh observation is the one worth keeping

The assertion already existed in that file and ran twelfth. That is worse than not having it, in a specific way: eleven parity checks would pass and be believed, and the check that could have explained why they were meaningless fired after the reader had already drawn a conclusion. Moving it is a one-line change that alters what the file can tell you.

And your reason is exact: a fallback plan returns the same answers -- that is what makes it a correct fallback -- so eleven parity checks cannot distinguish the metadata path from the path that replaces it. Eleven passing says nothing about which ran. That generalises past this file, and it is the sharpest statement of the trap I have seen either of us write:

A mutation run proves a check discriminates, not that it discriminates on the thing in its name.

Verification by GUC

Right instrument for the reason you give: the failure being guarded against is a plan change, so producing one with enable_custom_scan = off is faithful where a code edit would not be. And the line that proves the point is this one:

PASS  range parity          <-- still passes with the node gone

That is the whole argument in one line of output.

Positive markers, not absence tests

Agreed and worth having stated in the PR: an absence test passes for a plan that fell back to a seq scan, which has no Columnar lines to be absent. Asserting Columnar Vectorized Aggregates is present is a different claim from asserting Projected Columns is missing, and only the first one fails when the node disappears entirely.

pushdown_report.sh additionally asserting that the setting under test does not itself change the node is the part I would not have thought to ask for. Every comparison in that file is between two plans, and a setting that silently changed which node ran would make them incomparable while both halves still reported numbers.

This closes the gap my suite had. Thank you for taking it.

@jdatcmd
jdatcmd merged commit c8c72ff into jdatcmd:main Jul 28, 2026
jdatcmd pushed a commit that referenced this pull request Jul 28, 2026
#203 asserted the node in the three files that were asked for, and named the
rest as a sweep left undone. This is the rest of it.

Six more suites read a counter out of EXPLAIN and assert on the number:

  native_skip, native_bloom, native_cluster, native_recluster,
  sorted_projection, write_minmax_fastpath

Those counters -- "Columnar Chunk Groups Removed by Filter" and "Columnar
Vectors Skipped" -- exist only on the columnar custom scan. When the planner
stops choosing it the read returns nothing, and the check fails describing
skipping, or bloom, or clustering: anything except the plan change that
actually happened.

The helper moves to test/lib.sh as pgc_is_columnar_scan rather than being
copied six times, and uses EXPLAIN without ANALYZE, since the marker line
comes from the plan and not the run -- so an assertion costs a plan and does
not execute the query. It greps positively for the node's own marker rather
than testing for absence: a plan that fell back to a sequential scan has no
Columnar lines to be absent, so an absence test would pass for exactly the
case worth catching.

Two suites in the survey are deliberately left alone:

- analyze_stats reads "Rows Removed by Filter", a generic executor counter,
  and compares the same query with and without an index. Its comment says
  the comparison is independent of plan shape, and it is; asserting a node
  there would contradict what the check is for.
- audit greps for "Pushed-Down Filters: 1" and requires the value. That
  cannot pass without the node, so only the failure message would improve,
  and not enough to justify touching that file.

Verified by taking the node away. With enable_custom_scan off, every one of
the six fails its node assertion ahead of the counter checks that assertion
guards, while the parity checks around them keep passing -- which is the
point: parity cannot see a plan change, so before this the first red line in
each file described the wrong thing.
jdatcmd added a commit that referenced this pull request Jul 28, 2026
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.

2 participants