Refuse to compare a measurement nobody took (#418) - #422
Conversation
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
left a comment
There was a problem hiding this comment.
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.333If 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
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 The part I had not seen until you laid it out is that the same commit made it That is worth more than the fix. The helper cannot depend on each call site remembering, The other twoThe inversion algebra is right and the comment stays. On the GateRe-gating on the bench once the ClickBench run for #421 finishes. Those are timings, and |
jdatcmd
left a comment
There was a problem hiding this comment.
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.
…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.
…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
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.
Closes #418. @jdatcmd for review.
The shape
check "$label" "$a" "$b"with both sides empty compares""with""and printsPASS. 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.shpiped its buffer count throughbc,bcwas absent on onemachine, 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, becausegot [] want []is the message that cost the time.check_numcheck_ratiopgc_require_toolscheck_ratiousesawkrather thanbc.bcis not in a base install and its absenceis what started this;
awkis required by POSIX.Proved by removal, since a guard nobody tests is a guard that quietly stops working
harness_selftest.shgives each helper what it must reject. All thirteen pass on fivemajors:
The first line is deliberate. Plain
checkstill passes on two empty strings, and pinningthat is what stops someone "fixing"
checkand quietly changing every suite in the tree.The runner, which you asked me to add here
run_all_versions.shprintedALL VERSIONS PASSEDand exited 0 having built nothing.Same box, same defaults, before and after:
It now counts versions run, prints that count in the summary, fails at zero, and says
VERSIONS RUN PASSED (n of m)rather thanALL VERSIONS PASSEDwhen only some werepresent. The CI invocations pass one
pg_config, so one of one still readsALL VERSIONS PASSEDand nothing downstream changes.audit.shMy note in the issue said it exits non-zero while printing no
FAILline. That was notquite right and the real cause is worse. It runs under
set -euo pipefailwith itsverdict 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 TABLEwithCOLLATE "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:column_projection.shThe suite that surfaced this adopts
check_ratioand dropsbc.Gate
Five-major assert matrix on the bench, fresh clone,
git clean -fdx:Zero failing suites.
Not in scope
No sweep of the other suites. They can adopt
check_numas they are touched, which iswhat the issue proposed. @jdatcmd, the local guard you added to
agree()in #420 is thefirst candidate:
check_numcovers it, and dropping the local one keeps a single shape.🤖 Generated with Claude Code