Skip to content

Refuse to compare a measurement nobody took (#418) - #422

Merged
jdatcmd merged 2 commits into
commandprompt:mainfrom
ChronicallyJD:fix/418-empty-measurements
Aug 5, 2026
Merged

Refuse to compare a measurement nobody took (#418)#422
jdatcmd merged 2 commits into
commandprompt:mainfrom
ChronicallyJD:fix/418-empty-measurements

Conversation

@ChronicallyJD

Copy link
Copy Markdown
Collaborator

Closes #418. @jdatcmd for review.

The shape

check "$label" "$a" "$b" with both sides empty compares "" with "" and prints
PASS. Every way a measurement goes missing produces exactly that: a tool that is not
installed, a grep that matched no line, a psql against a cluster that is down, a
substitution that expanded to nothing.

column_projection.sh piped its buffer count through bc, bc was absent on one
machine, and it failed only because one side happened to be non-empty. I produced two
green checks the same day that measured nothing at all.

What is here

Three assertions in lib.sh, each naming the reason rather than the subject, because
got [] want [] is the message that cost the time.

helper refuses
check_num a side that is not a number
check_ratio an empty side, and a zero denominator, before dividing
pgc_require_tools a missing tool, once at the top, rather than an empty string three checks later

check_ratio uses awk rather than bc. bc is not in a base install and its absence
is what started this; awk is required by POSIX.

Proved by removal, since a guard nobody tests is a guard that quietly stops working

harness_selftest.sh gives each helper what it must reject. All thirteen pass on five
majors:

PASS  check compares two empty strings and passes, which is why the rest exist
PASS  check_num refuses two empty strings
PASS  check_num refuses a psql error message
PASS  check_num refuses the word a yes/no check would produce
PASS  check_num still compares two real numbers
PASS  check_num still fails two unequal numbers
PASS  check_num accepts a decimal and a sign
PASS  check_ratio refuses an empty measurement
PASS  check_ratio refuses a zero denominator rather than dividing by it
PASS  check_ratio passes a ratio inside its bound
PASS  check_ratio fails a ratio outside its bound
PASS  pgc_require_tools passes on tools that exist
PASS  pgc_require_tools fails on one that does not

The first line is deliberate. Plain check still passes on two empty strings, and pinning
that is what stops someone "fixing" check and quietly changing every suite in the tree.

The runner, which you asked me to add here

run_all_versions.sh printed ALL VERSIONS PASSED and exited 0 having built nothing.
Same box, same defaults, before and after:

BEFORE (main)         exit=0   ALL VERSIONS PASSED            5 SKIP (not executable)
AFTER  (this branch)  exit=1   NO VERSIONS RAN: every configured pg_config was missing
                               versions run: 0 of 5 configured
                               configured: /usr/local/pg15/bin/pg_config ...

It now counts versions run, prints that count in the summary, fails at zero, and says
VERSIONS RUN PASSED (n of m) rather than ALL VERSIONS PASSED when only some were
present. The CI invocations pass one pg_config, so one of one still reads
ALL VERSIONS PASSED and nothing downstream changes.

audit.sh

My note in the issue said it exits non-zero while printing no FAIL line. That was not
quite right and the real cause is worse. It runs under set -euo pipefail with its
verdict at the bottom, so an unguarded failure ends the run before the verdict, and
every check below the failure silently did not run. Observed when a CREATE TABLE with
COLLATE "en_US" met a box without that locale: five PASS lines, one ERROR, no verdict.

An EXIT trap now says so. It does not change the exit status and it cannot turn an abort
into a pass. Proved by injecting an unguarded false:

exit=1
AUDIT TEST ABORTED (exit 1) before reaching its verdict.
The checks after the failing command did not run.

column_projection.sh

The suite that surfaced this adopts check_ratio and drops bc.

Gate

Five-major assert matrix on the bench, fresh clone, git clean -fdx:

ALL VERSIONS PASSED
  versions run: 5 of 5 configured
  PASS  PG15   PASS  PG16   PASS  PG17   PASS  PG18   PASS  PG19
  harness_selftest=PASS x5   column_projection=PASS x5   audit=PASS x5

Zero failing suites.

Not in scope

No sweep of the other suites. They can adopt check_num as they are touched, which is
what the issue proposed. @jdatcmd, the local guard you added to agree() in #420 is the
first candidate: check_num covers it, and dropping the local one keeps a single shape.

🤖 Generated with Claude Code

check "$label" "$a" "$b" with both sides empty compares "" with "" and prints
PASS. Every way a measurement goes missing produces exactly that: a tool that is
not installed, a grep that matched no line, a psql against a cluster that is
down, a substitution that expanded to nothing.

That is not hypothetical. column_projection.sh piped its buffer count through
bc, bc was absent on one machine, and the checks failed only because one side
happened to be non-empty. Two green checks were produced the same day that
measured nothing at all.

lib.sh gains three assertions, all of which name the reason rather than the
subject. "got [] want []" is the message that cost the time.

  check_num          both sides must look like a number
  check_ratio        a over b within a bound, refusing an empty side and a zero
                     denominator, with awk rather than bc, because bc is not in
                     a base install and its absence is what started this
  pgc_require_tools  one clear line at the top instead of an empty string three
                     checks later

They are load-bearing now, so harness_selftest.sh proves each one by giving it
what it must reject: two empty strings, a psql error message, a yes, a zero
denominator. It also pins that plain check DOES pass on two empty strings, since
that is the behaviour the rest of this exists to work around.

run_all_versions.sh printed ALL VERSIONS PASSED and exited 0 when every
configured pg_config was missing. The defaults name /usr/local/pg15 through
pg19; a box whose assert builds are pg15a through pg19a skips all five, and that
output then gets pasted into a pull request as the gate. It now reports how many
versions ran, fails when that is zero, and says VERSIONS RUN PASSED rather than
ALL VERSIONS PASSED when only some were present. One config in, one config run,
so the CI invocations are unaffected.

audit.sh runs under set -e with its verdict at the bottom, so an unguarded
failure ended the run non-zero having printed neither a FAIL line nor a verdict,
and every check below the abort silently did not run. Observed when a CREATE
TABLE with COLLATE "en_US" met a box without that locale. An EXIT trap now says
the suite aborted before reaching its verdict. It does not change the status.

column_projection.sh, the suite that surfaced this, adopts the helpers and drops
bc.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqprqkCXuH8SegiZejE1Tw

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

Verified, and it is good work. One hole survives in the new helper, and it is the

same hole one level down.

I checked the claims rather than reading them.

The runner guard, removal-proved

Same command, same box, only the branch differing:

--- main ---
exit=0
  SKIP   /nonexistent/bin/pg_config
ALL VERSIONS PASSED

--- this PR ---
exit=1
  versions run: 0 of 1 configured
NO VERSIONS RAN: every configured pg_config was missing or not executable.

That is the most valuable thing in the PR. ALL VERSIONS PASSED, exit 0, having built
nothing, is the line we paste into pull requests as the gate. Both of us have quoted that
runner this week.

harness_selftest.sh runs 27 checks and all 13 new probes pass on PG18. _probe running
each in a subshell so a deliberate failure does not corrupt PGC_FAIL is the right shape.

Blocking: check_ratio passes a zero numerator, and the comment says it does not

# A zero numerator is reported rather than passed, because "the thing we measured
# cost nothing" is nearly always "the thing we measured did not happen", which is
# the same defect one layer down.

I agree with every word. The code does not do it:

$ check_ratio "projected read touched ZERO buffers (a=0)" 0 900 0.33
PASS  projected read touched ZERO buffers (a=0) (0.00x, bound 0.33x, from a=0 b=900)

Only the denominator is checked. And this is not theoretical, because of the other change
in the same PR:

grep -oE '(hit|read)=[0-9]+' | cut -d= -f2 |
awk '{ n += $1 } END { print n + 0 }'

bufs() now returns 0 where it used to return the empty string. That was the right
call for the premise check, which tests -gt 1000. But it converts the failure mode from
one check_ratio rejects (empty) into one it accepts (zero), and then:

check_ratio "one column costs less than a third of reading all of them" \
	"$B_ON" "$B_OFF" 0.333

If EXPLAIN's Buffers: line ever changes shape, or the query errors, or the grep stops
matching, B_ON is 0, the ratio is 0.00, and it passes. A projected read that touched
no buffers at all reports success. That is exactly #418, moved rather than closed, in the
very check that started this.

B_OFF is protected only by the premise above it, and only because that premise happens
to exist here. The helper should not depend on each call site remembering.

Add the check the comment already promises, and a probe for it beside the other twelve:

check "check_ratio refuses a zero numerator" \
	"$(_probe check_ratio "zero numerator" "0" "100" "0.5")" "1"

If a legitimate call site ever needs to permit zero, that is worth an explicit
check_ratio_allow_zero rather than a silently permissive default.

Two smaller things, neither blocking

The inversion in column_projection.sh is correct, and I checked the algebra rather
than assuming: B_ALL > B_OFF * 8 / 10 is B_OFF / B_ALL < 1.25. Worth keeping the
comment, since the next reader will do that division too.

audit.sh's trap is right and the observed case is worth having in the file. One
note: trap ... EXIT fires on a normal exit too, which is why AUDIT_REACHED_END exists.
That works. It is also worth knowing the trap does not fire for SIGKILL, so a suite the
OOM killer takes still ends silently. Not worth chasing; worth not being surprised by.

On scope

You said you were not proposing a sweep of every suite, and I agree. check_num and
check_ratio earn their place by being adopted as files are touched.

I have a local non-empty guard in native_index_projection.sh's agree() on #420 that
should become check_num once this lands. I will do that rather than leave two mechanisms.

Fix the zero numerator and this has my approval.

…used both (commandprompt#418)

The comment said a zero numerator is reported rather than passed. The code
checked the denominator. A measurement of zero is inside every bound, so it
passed.

That is not a stray inconsistency, because the same commit made it reachable.
column_projection.sh's bufs() now sums with awk and returns 0 where it used to
return the empty string. The empty string was a failure mode check_ratio
rejects; zero was one it accepted. So the projected-read check would have
reported success on a read that touched no buffers at all, which is commandprompt#418 moved
rather than closed, inside the check that started it.

Either side being zero now fails, and harness_selftest.sh probes the numerator
case beside the eleven others. The exact call jdatcmd quoted:

  before  PASS  ... (0.00x, bound 0.33x, from a=0 b=900)
  after   FAIL  ... a side of the ratio is zero, so nothing was measured

A call site that genuinely needs to permit zero should say so under its own
name rather than get it by default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqprqkCXuH8SegiZejE1Tw
@ChronicallyJD

Copy link
Copy Markdown
Collaborator Author

You are right, and the way it was wrong is worse than a stray inconsistency.

Fixed in 97a783f. Either side being zero now fails, and the numerator case is probed
beside the other eleven. Your exact call:

before  PASS  projected read touched ZERO buffers (a=0) (0.00x, bound 0.33x, from a=0 b=900)
after   FAIL  projected read touched ZERO buffers (a=0): a side of the ratio is zero,
              so nothing was measured: a=[0] b=[900]

The part I had not seen until you laid it out is that the same commit made it
reachable
. Changing bufs() from paste -sd+ | bc to an awk sum was right for the
premise check, and it converted the empty string, which check_ratio rejects, into zero,
which it accepted. So the one call site this whole issue came from would have reported
success on a read that touched no buffers. I closed #418 in the helper and reopened it in
the caller, in one commit, while writing a comment that said I had not.

That is worth more than the fix. The helper cannot depend on each call site remembering,
which is the argument for the helper existing at all, and I wrote a default that required
exactly that.

The other two

The inversion algebra is right and the comment stays. On the audit.sh trap: agreed
about SIGKILL, and I would rather not chase it. A suite the OOM killer takes exits 137
and the runner records the suite as failed, so the loss is the verdict line and not the
result.

Gate

Re-gating on the bench once the ClickBench run for #421 finishes. Those are timings, and
the matrix runs six suites at once, so starting the matrix underneath them would make
both untrustworthy. I will post the five-major result rather than say it passed.

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

Fixed and verified. Approving.

The case I raised now fails:

FAIL  projected read touched ZERO buffers: a side of the ratio is zero, so nothing was measured: a=[0] b=[900]
FAIL  zero denominator: a side of the ratio is zero, so nothing was measured: a=[100] b=[0]
PASS  inside bound  (0.10x, bound 0.5x, from a=10 b=100)
FAIL  outside bound: 0.90x exceeds the 0.5x bound (a=90 b=100)

One condition covering both sides, rather than a second branch, and the message names the
reason instead of the subject. harness_selftest.sh is 28 checks, all green on PG18, with
the new probe among them.

I also ran column_projection.sh, the suite that started this, since it is the one whose
call sites changed shape:

PASS  premise: the unprojected read is large enough to measure (off: 2524)
PASS  one column costs less than a third of reading all of them (0.04x, bound 0.333x, from a=109 b=2524)
PASS  SELECT * reads as much as projection-off does (1.00x, bound 1.25x, from a=2524 b=2524)
35 checks, PASSED

The from a=... b=... in the pass line is worth more than it looks. 0.04x against a
0.333x bound tells the next person the check has margin; the old boolean form told them
only that it did not fail.

The comment now records that the helper's first version claimed both sides and checked
one, and why the bc-to-awk change in the same commit made that reachable. Leaving
that in is the right call. It is the second time in this PR that a comment described a
property the code did not have, and the file now says so about itself.

What I said I would do

I have a local non-empty guard in native_index_projection.sh's agree() on #420, added
before this existed. Once this lands I will replace it with the shared helper so there is
one mechanism, as the comment above it already promises.

Not blocking, for whenever check_ratio is next touched

pgc_is_number accepts 5. and .5 but rejects 1e5. Fine for buffer counts and
timings, and stricter is the right default here. Worth knowing before someone feeds it a
psql float that came back in exponent form, which EXPLAIN's JSON output can do for
large costs.

Merging once CI is green.

@jdatcmd
jdatcmd merged commit 9779220 into commandprompt:main Aug 5, 2026
11 checks passed
jdatcmd added a commit to ChronicallyJD/pgcolumnar that referenced this pull request Aug 5, 2026
…ndprompt#418)

I said on commandprompt#422 and commandprompt#420 that I would replace native_index_projection.sh's local
non-empty guard with check_num once the shared helpers landed. That was wrong,
and finding out why exposed the larger half of commandprompt#418.

check_num requires a NUMBER. An md5 over an ordered result is not one:

  FAIL  two identical md5 hashes: not a measurement, so nothing was compared:
        got [9dd4e461268c8034f5c8564e155c67a6] want [9dd4e461268c8034f5c8564e155c67a6]

So a suite comparing a non-numeric oracle has nothing to reach for and falls back
to plain check, where "" equals "" and prints PASS. That is not a corner: 35
places in this tree compare an md5(string_agg(...)) oracle, across audit,
concurrency, arrow_import, encode_invariants, encode_effort and column_projection,
and every one is a down cluster or an errored query away from comparing nothing
with nothing.

check_text asserts presence rather than shape, which is the most a shared helper
can know. A caller that knows the shape should still say so: agree() keeps its
32-hex-character test, because "not empty" would accept a psql error message.

harness_selftest.sh gains five probes, including the one that records why this
exists at all -- check_num refusing an md5 -- so the next person does not repeat
my mistake by adopting the wrong helper. 33 checks green on PG18;
native_index_projection.sh 35 green.
ChronicallyJD added a commit to ChronicallyJD/pgcolumnar that referenced this pull request Aug 5, 2026
…ndprompt#401)

    case "$h$c" in *ERR*|"") ;; *) r=$(awk ... a / b) ;; esac

"$h$c" is empty only when both are. One empty side concatenates to a non-empty
string, falls through, and divides:

    heap=[1500] col=[    ]  ->  0.00     reads as a 100 percent win
    heap=[    ] col=[ 800]  ->  inf

0.00 is the dangerous one, in a table whose whole purpose is to be quoted
somewhere without the run log beside it. timed() returns empty on every path the
ERROR branch does not catch: a cancelled query, a lost connection, or psql
changing its timing format.

Replaced with a ratio() that tests each side separately, refuses ERR, and
refuses a zero denominator. It is self-tested before any number is printed,
because a ratio helper that silently passes a half-empty pair is worse than
none. bench/ does not source test/lib.sh, so this is the local counterpart of
check_ratio from commandprompt#418 and commandprompt#422.

Same class as the bc failure that started commandprompt#418, and the third time this shape
has turned up in my own work today.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqprqkCXuH8SegiZejE1Tw
jdatcmd pushed a commit that referenced this pull request Aug 6, 2026
pgc_summary decided the verdict from PGC_FAIL alone. PGC_CHECKS was printed and
never read. So a suite that asserted nothing printed PASSED and exited 0, which
is indistinguishable from one that ran four hundred checks, and run_all_versions
reads only the exit code.

Fifteen suites do exactly that whenever pyarrow is absent:

    arrow_export arrow_nested parquet_export parquet_import parquet_nested
    fuzz_arrow fuzz_parquet native_parquet_codecs native_parquet_hardening
    native_parquet_multifile native_parquet_partition native_parquet_projection
    native_parquet_pushdown native_parquet_streaming native_parquet_units

That is the whole Arrow and Parquet surface including both fuzzers, and the run
says PASSED beside every one of them. It is the same defect #422 fixed one level
up, where a matrix reported ALL VERSIONS PASSED having run none of them.

Skipped is now its own state rather than a kind of pass. 0 passed, 1 failed,
2 ran no checks. Making it a failure instead would have been wrong and would have
reddened the gate on the first run: PostgreSQL 15 has no WITHOUT OVERLAPS for
temporal.sh to test, and a box without pg_isolation_regress is a supported
configuration. A red everyone knows to ignore is a red nobody reads.

Measured, on one box, same tree, same build:

    pyarrow present                     rc=0  PASSED    19 checks
    pyarrow absent                      rc=2  SKIPPED    0 checks
    pyarrow absent, PGC_REQUIRE_PYARROW=1  rc=1  FAILED
    native_roundtrip (control)          rc=0  PASSED     8 checks
    harness_selftest                    rc=0  PASSED    33 checks

The first two rows have to disagree. Before this they were both PASSED.

PGC_REQUIRE_PYARROW follows PGC_REQUIRE_ISOLATION, which isolation.sh has used
for this exact purpose since it was written, rather than inventing a second
convention. Whether CI sets it is a policy call and is not made here.

All four consumers had to learn the third state, because every one of them
treated non-zero as failure: run_all_versions.sh, run_san.sh, run_coverage.sh and
devloop.sh. run_all_versions now prints "suites that ran: n of m" per major and
fails at zero. run_san fails if every suite it ran was a skip, because five of its
default subset are pyarrow-gated and it would otherwise report a sanitizer gate
passing having sanitized nothing.

The PGC_SKIP_TIMING synthetic result now writes 2 rather than 0. Those suites do
not run, and recording them as passes was the same claim, written by the driver.

native_repack.sh and pg19_vacuum_options.sh printed their own SKIPPED line and
exited 0 without calling pgc_summary, so no change inside pgc_summary could reach
them. They now route through it and report the same state as everything else.

Not fixed here, and worth naming: arrow_import, native_parquet_flba,
native_parquet_schema and parquet_nested_import skip only a SECTION when pyarrow
is missing and keep their other checks. They will still report a genuine pass
having lost real coverage. This closes the zero-check gap, not the pyarrow gap.

Running the suites this way immediately found a second instance: temporal.sh
reports zero checks on PG18 on my bench, where the feature does exist, because
btree_gist is not installed there. It has been reporting PASSED.
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.

The harness compares empty measurements: check "" "" passes, and four suites can produce one

2 participants