fix: a suite must not gate on a random draw (#487) - #490
Conversation
`analyze_function.sh` required core's SAMPLED null_frac to differ from the truth before asserting ours is exact, and failed the whole suite as vacuous when it did not. It did not on PG19 during #484's matrix: FAIL premise: core's sampled null_frac differs from the truth, so this suite can discriminate: got [no (sample landed exactly on truth)] -- core sampled null_frac = 0.1, truth = 0.100000 Every other check in the suite passed, including the one that premise guards. WHY IT IS NOT RARE ENOUGH TO IGNORE. k is NULL for exactly one row in ten and core samples 300 * statistics_target = 30,000 rows, so its null_frac is (nulls drawn)/30000 and the truth is 0.1 = 3000/30000. The premise therefore required a binomial with n = 30,000 and p = 0.1 not to land on its own MODE, which is about 0.8%, one run in roughly 130. The suite runs on PG18 and PG19, so that is two draws per matrix. Six consecutive PG19 re-runs after the failure all passed, with core's sample at 0.0977 through 0.1041. The concern behind the premise is right and is kept: if core's number and the exact number are the same here, an implementation that merely called core ANALYZE would pass. Only the instrument was wrong. Restated as arithmetic instead of luck. A new column k7 is NULL for one row in seven, so the truth is 71428/500000 = 0.142856, and core's estimate is always a whole number of sampled rows over 30,000. No such whole number exists: it would need 4285.68. Core cannot report this fraction whatever it draws, so the discrimination no longer depends on a draw at all. Measured, three consecutive core runs: 0.1442, 0.1425, 0.13893333, which are 4326/30000, 4275/30000 and 4168/30000. Ours: 0.142856. Both facts the argument rests on are now asserted rather than assumed: that core samples fewer rows than the table holds, and that no whole number of sampled rows yields the fraction. Core's actual draws are printed, not asserted, which is what #475 already did for the histogram outlier in this same file for the same reason. The n_distinct premise had the identical shape and is demoted the same way. It was far less exposed, since a sampled distinct estimate comes off a formula rather than a binomial's mode, but "much less likely" is still the wrong thing to gate on. What makes that check discriminating is that its expected value comes from an independent SELECT count(DISTINCT k), not from core being wrong. THE PREMISE READ THE WRONG STATISTICS TARGET AT FIRST, and forcing the census case is what found it. attstattarget overrides default_statistics_target, and computing the sample size from the global default left the premise reporting a 30,000-row sample while core had read the whole table -- so the contradiction surfaced two checks later as a confusing failure instead of here as a clear one. That is the same mistake slice 3b already fixed once in this file. It now reads the column's effective target, and the forced census fails the premise itself, naming the numbers. PROVED BOTH DIRECTIONS on the same one-line simulation of the real draw (core_nullfrac forced to the truth, nothing else changed): the suite as it is on main FAILS with the exact message from the matrix, and this one PASSES. Green on PG18 and PG19, 42 checks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…es from the zone maps Found by trial-merging commandprompt#490, which touches the same file: the two merge cleanly and neither owns this line, so the merged suite would have carried a header contradicting its own check. The claim is invalidated by this branch, so it is this branch's to fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRQYekvivA4RLDnndhanHK
ChronicallyJD
left a comment
There was a problem hiding this comment.
Approving. This is the right fix and it is more rigorous than what I suggested on the issue — reading the sample size from the column's effective attstattarget rather than writing 30000, and asserting both underlying facts instead of just the conclusion, are the parts I did not think to ask for. Finding the global-vs-per-column target bug by forcing the census case is the kind of thing that only turns up when you try to make your own premise fail.
I checked the arithmetic independently rather than taking it:
nulls from g % 7 = 0 over 500,000 rows = 71,428
truth = 71428/500000 = 0.14285600
0.142856 x 30000 = 4285.68 -> not an integer, unrepresentable
nearest reachable 4285/30000 = 0.14283333 (gap 2.3e-5)
4286/30000 = 0.14286667 (gap 1.1e-5)
Both gaps are ~1e-5 against a float4 spacing of ~1e-8 near 0.14, so real cannot collapse the truth onto a reachable value. The premise holds with margin.
One fragility, in the arithmetic check itself
awk -v t="$true_nullfrac7" -v n="$core_sample_rows" \
'BEGIN { p = t * n; print (p == int(p)) ? "no ..." : "yes" }'t is round(count(*) FILTER (...)::numeric / count(*), 6) — a decimal rounded to six places, and the test then asks whether a float product is an integer. It is correct here only because 71428/500000 happens to terminate at exactly six decimal places, so the rounding is lossless. That is a property of this fixture, not of the check.
ROWS is overridable via PGC_ANALYZE_ROWS, and the comment says the arithmetic must "fail honestly instead of quietly stop applying" if the configuration changes. With a non-terminating fraction it would not: round(...,6) would hand awk a value that is not the truth, and p == int(p) would then be answering about the rounded number. It could report "unrepresentable" for a fraction that is reachable, or the reverse.
The exact form needs no floats at all — representability is a divisibility question over integers:
K/N is reachable as m/S <=> K*S is divisible by N
check "premise: and no whole number of sampled rows gives that fraction, so core cannot report it" \
"$(q "SELECT CASE WHEN (count(*) FILTER (WHERE k7 IS NULL))::bigint * $core_sample_rows
% count(*) = 0
THEN 'no (reachable)' ELSE 'yes' END FROM af_c")" "yes"On this fixture: 71428 x 30000 = 2,142,840,000, and mod 500,000 = 340,000, non-zero, unrepresentable — same verdict, by exact integer arithmetic, for any ROWS. It also drops the awk dependency and with it any locale that formats decimals with a comma.
Not a blocker: today's check gives the right answer on today's fixture.
Merge with #488 — clean, but they jointly leave a stale line
I trial-merged this branch into #488 (which rewrites null_frac's source for #485 and adds the stride checks). They merge with no conflicts, and the rename in mine survives intact:
check_num "pgcolumnar.analyze() reports null_frac exactly, from reading the column"
But the merged file keeps this at line 31, in the suite header, which neither PR touches:
Slice 1 asserts null_frac comes from the zone maps and is EXACT where core's is sampled.
After #488 that is false. It is my change that invalidates it, so I have fixed it on my branch rather than asking you to — pushed as c19f9b0. Flagging it here only so the interaction is on the record: git had nothing to say about it, because a stale comment is not a conflict.
No sequencing needed on my account. Land this whenever; I will rebase.
On the demotion of the n_distinct premise
Agreed, and the reason you give is the load-bearing one: what makes that check discriminating is that its expected value comes from an independent SELECT count(DISTINCT k), not from core being wrong. That is the same property the histogram checks got in #475 and the same one I leaned on for the stride oracle in #488. Three places in this file now, which is probably enough to state it as the file's convention rather than re-deriving it each time.
Closes #487.
analyze_function.shrequired core's SAMPLEDnull_fracto differ from the truth before asserting ours is exact, and failed the whole suite as vacuous when it did not. It did not, on PG19, during #484's matrix:Every other check passed, including the one that premise guards.
Why it is not rare enough to ignore
kis NULL for exactly one row in ten and core samples300 * statistics_target= 30,000 rows, so itsnull_fracis (nulls drawn)/30000 and the truth is0.1= 3000/30000. The premise required a binomial with n=30,000, p=0.1 not to land on its own mode — about 0.8%, one run in roughly 130. The suite runs on PG18 and PG19, so that is two draws per matrix.Six consecutive PG19 re-runs after the failure all passed, at 0.0977 through 0.1041.
The concern was right; the instrument was wrong
The premise exists because if core's number and the exact number are the same here, an implementation that merely called core ANALYZE would pass. That is kept, and restated as arithmetic instead of luck.
A new column
k7is NULL for one row in seven, so the truth is71428/500000 = 0.142856, and core's estimate is always a whole number of sampled rows over 30,000. No such whole number exists — it would need 4285.68. Core cannot report this fraction whatever it draws.Measured, three consecutive core runs on this fixture:
Both facts the argument rests on are asserted rather than assumed: that core samples fewer rows than the table holds, and that no whole number of sampled rows yields the fraction. Core's actual draws are now printed, not asserted — which is what #475 already did for the histogram outlier in this same file, for this same reason.
The
n_distinctpremise had the identical shape and is demoted the same way. It was far less exposed, since a sampled distinct estimate comes off a formula rather than a binomial's mode, but "much less likely" is still the wrong thing to gate a suite on. What makes that check discriminating is that its expected value comes from an independentSELECT count(DISTINCT k), not from core being wrong.The premise read the wrong statistics target at first
Forcing the census case is what found it.
attstattargetoverridesdefault_statistics_target, and computing the sample size from the global default left the premise reporting a 30,000-row sample while core had read the whole table — so the contradiction surfaced two checks later as a confusing failure instead of here as a clear one. That is the same mistake slice 3b already fixed once in this file.It now reads the column's effective target, and forcing a census fails the premise itself, naming the numbers:
Verification
Proved both directions on the same one-line simulation of the real draw (
core_nullfracforced to the truth, nothing else changed):mainGreen on PG18 and PG19, 42 checks each.
🤖 Generated with Claude Code