diff --git a/test/check_ledger.tsv b/test/check_ledger.tsv index 218e6a82f..e27f39989 100644 --- a/test/check_ledger.tsv +++ b/test/check_ledger.tsv @@ -2331,7 +2331,7 @@ harness_selftest 480-an-unrunnable-record-names-its-check premise: and it compar harness_selftest 480-an-unrunnable-record-names-its-check premise: that clean run had a refusal site to be silent ABOUT 15;16;17;18;19 never - harness_selftest 480-an-unrunnable-record-names-its-check premise: the sweep found unrunnable sites at all 15;16;17;18;19 never - harness_selftest 480-an-unrunnable-record-names-its-check premise: the sweep tool is present 15;16;17;18;19 never - -harness_selftest 480-an-unrunnable-record-names-its-check the refusal emitters are the two the tree defines 15;16;17;18;19 never - +harness_selftest 480-an-unrunnable-record-names-its-check the refusal emitters are the ones the tree defines 15;16;17;18;19 never - harness_selftest 480-an-unrunnable-record-names-its-check the sweep EXITS non-zero when it reports a mismatch 15;16;17;18;19 never - harness_selftest 480-an-unrunnable-record-names-its-check the sweep REPORTS a name the file records only in its refusal branch 15;16;17;18;19 never - harness_selftest 490-a-one-line-body-ends-at-its-own-brace a body that never closes is NAMED rather than silently swallowing the file 15;16;17;18;19 never - @@ -2537,6 +2537,190 @@ harness_selftest 640-every-suite-job-installs-the-extensions the detector names harness_selftest 640-every-suite-job-installs-the-extensions the detector names a workflow in which it finds no suite job 15;16;17;18;19 2026-10-14 640's _ej_missing without its no-suite-job guard harness_selftest 640-every-suite-job-installs-the-extensions the detector names a workflow it cannot read 15;16;17;18;19 2026-10-14 640's _ej_missing without its unreadable-file guard harness_selftest 640-every-suite-job-installs-the-extensions the suite-running jobs are exactly the five this part knows 15;16;17;18;19 2026-10-14 640's job scan no longer counts a cluster job that runs pytest +hilbert_curve hilbert_curve C0 premise: the battery encoded every case and every point 15;16;17;18;19 never - +hilbert_curve hilbert_curve C0 premise: the encoder was called once per point 15;16;17;18;19 never - +hilbert_curve hilbert_curve C1 the Hilbert index set is exactly the contiguous range 15;16;17;18;19 2026-10-21 columnar_curve.c: the transpose clears the top bit of X[0], so curve indices collide +hilbert_curve hilbert_curve C2 consecutive points in memcmp key order are unit-adjacent 15;16;17;18;19 2026-10-21 columnar_curve.c: the transpose clears the top bit of X[0], so curve indices collide +hilbert_curve hilbert_curve C3 every dyadic sub-cube occupies a contiguous run of indices 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave emits the bit rounds lowest first, a permutation whose dyadic sub-cubes are scattered +hilbert_curve hilbert_curve C4a premise: the bridge covered every case and every point 15;16;17;18;19 never - +hilbert_curve hilbert_curve C4a the full-width key prefix does not move when the low bits change 15;16;17;18;19 2026-10-21 columnar_curve.c: the lowest bit of the last transposed word is folded into the top bit of the first +hilbert_curve hilbert_curve C4a the full-width key prefixes are exactly the index range 15;16;17;18;19 2026-10-21 columnar_curve.c: the transpose clears the top bit of X[0], so curve indices collide +hilbert_curve hilbert_curve C4b consecutive keys decode to unit-adjacent points, re-encoded by the code under test 15;16;17;18;19 2026-10-21 columnar_curve.c: a correct transpose that flips bit 0 of X[0] when ncols == 8 and the low byte of X[3] is 0x5a +hilbert_curve hilbert_curve C4b control: a mutated inverse breaks every round trip the clean one made 15;16;17;18;19 2026-10-21 columnar_curve.c: a correct transpose that flips bit 0 of X[0] when ncols == 8 and the low byte of X[3] is 0x5a +hilbert_curve hilbert_curve C4b premise: 3,000 DISTINCT full-width keys per ncols were round-tripped 15;16;17;18;19 never - +hilbert_curve hilbert_curve C4b premise: consecutive key pairs were formed 15;16;17;18;19 never - +hilbert_curve hilbert_curve C4b the published inverse round-trips full-width keys 15;16;17;18;19 2026-10-21 columnar_curve.c: a correct transpose that flips bit 0 of X[0] when ncols == 8 and the low byte of X[3] is 0x5a +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=1 asym 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=1 lowbit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=1 max 15;16;17;18;19 2026-10-21 columnar_curve.c: the final mask t loses its lowest bit, so only bit 0 of each transposed word moves +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=1 mixed 15;16;17;18;19 2026-10-21 columnar_curve.c: the final mask t loses its lowest bit, so only bit 0 of each transposed word moves +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=1 tie 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=1 topbit0 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=1 unit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=1 zero 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=2 asym 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=2 lowbit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=2 max 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=2 mixed 15;16;17;18;19 2026-10-21 columnar_curve.c: the final mask t loses its lowest bit, so only bit 0 of each transposed word moves +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=2 tie 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=2 topbit0 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=2 unit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=2 zero 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=3 asym 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=3 lowbit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=3 max 15;16;17;18;19 2026-10-21 columnar_curve.c: the final mask t loses its lowest bit, so only bit 0 of each transposed word moves +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=3 mixed 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=3 tie 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=3 topbit0 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=3 unit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=3 zero 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=4 asym 15;16;17;18;19 2026-10-21 columnar_curve.c: the final mask t loses its lowest bit, so only bit 0 of each transposed word moves +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=4 lowbit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=4 max 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=4 mixed 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=4 tie 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=4 topbit0 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=4 unit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=4 zero 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=5 asym 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=5 lowbit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=5 max 15;16;17;18;19 2026-10-21 columnar_curve.c: the final mask t loses its lowest bit, so only bit 0 of each transposed word moves +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=5 mixed 15;16;17;18;19 2026-10-21 columnar_curve.c: the final mask t loses its lowest bit, so only bit 0 of each transposed word moves +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=5 tie 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=5 topbit0 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=5 unit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=5 zero 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=6 asym 15;16;17;18;19 2026-10-21 columnar_curve.c: the final mask t loses its lowest bit, so only bit 0 of each transposed word moves +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=6 lowbit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=6 max 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=6 mixed 15;16;17;18;19 2026-10-21 columnar_curve.c: the final mask t loses its lowest bit, so only bit 0 of each transposed word moves +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=6 tie 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=6 topbit0 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=6 unit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=6 zero 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=7 asym 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=7 lowbit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=7 max 15;16;17;18;19 2026-10-21 columnar_curve.c: the final mask t loses its lowest bit, so only bit 0 of each transposed word moves +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=7 mixed 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=7 tie 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=7 topbit0 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=7 unit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=7 zero 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=8 asym 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=8 lowbit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=8 max 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=8 mixed 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=8 tie 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=8 topbit0 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=8 unit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve C5 golden Hilbert keys: ncols=8 zero 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve C5 the pack bit order: pack(0, 2^63) starts 0x40 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 the pack bit order: pack(2^63, 0) starts 0x80 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C5 the pack establishes exactly 8*ncols bytes: ncols=1 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave writes one byte past the 8*ncols-byte key +hilbert_curve hilbert_curve C5 the pack establishes exactly 8*ncols bytes: ncols=2 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave writes one byte past the 8*ncols-byte key +hilbert_curve hilbert_curve C5 the pack establishes exactly 8*ncols bytes: ncols=3 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave writes one byte past the 8*ncols-byte key +hilbert_curve hilbert_curve C5 the pack establishes exactly 8*ncols bytes: ncols=4 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave writes one byte past the 8*ncols-byte key +hilbert_curve hilbert_curve C5 the pack establishes exactly 8*ncols bytes: ncols=5 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave writes one byte past the 8*ncols-byte key +hilbert_curve hilbert_curve C5 the pack establishes exactly 8*ncols bytes: ncols=6 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave writes one byte past the 8*ncols-byte key +hilbert_curve hilbert_curve C5 the pack establishes exactly 8*ncols bytes: ncols=7 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave writes one byte past the 8*ncols-byte key +hilbert_curve hilbert_curve C5 the pack establishes exactly 8*ncols bytes: ncols=8 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave writes one byte past the 8*ncols-byte key +hilbert_curve hilbert_curve C5 the pack sets every bit of the key for an all-ones vector: ncols=1 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave writes one byte past the 8*ncols-byte key +hilbert_curve hilbert_curve C5 the pack sets every bit of the key for an all-ones vector: ncols=2 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave writes one byte past the 8*ncols-byte key +hilbert_curve hilbert_curve C5 the pack sets every bit of the key for an all-ones vector: ncols=3 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave writes one byte past the 8*ncols-byte key +hilbert_curve hilbert_curve C5 the pack sets every bit of the key for an all-ones vector: ncols=4 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave writes one byte past the 8*ncols-byte key +hilbert_curve hilbert_curve C5 the pack sets every bit of the key for an all-ones vector: ncols=5 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave writes one byte past the 8*ncols-byte key +hilbert_curve hilbert_curve C5 the pack sets every bit of the key for an all-ones vector: ncols=6 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave writes one byte past the 8*ncols-byte key +hilbert_curve hilbert_curve C5 the pack sets every bit of the key for an all-ones vector: ncols=7 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave writes one byte past the 8*ncols-byte key +hilbert_curve hilbert_curve C5 the pack sets every bit of the key for an all-ones vector: ncols=8 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave writes one byte past the 8*ncols-byte key +hilbert_curve hilbert_curve C5 the pack wrote nothing past the key: ncols=1 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave writes one byte past the 8*ncols-byte key +hilbert_curve hilbert_curve C5 the pack wrote nothing past the key: ncols=2 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave writes one byte past the 8*ncols-byte key +hilbert_curve hilbert_curve C5 the pack wrote nothing past the key: ncols=3 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave writes one byte past the 8*ncols-byte key +hilbert_curve hilbert_curve C5 the pack wrote nothing past the key: ncols=4 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave writes one byte past the 8*ncols-byte key +hilbert_curve hilbert_curve C5 the pack wrote nothing past the key: ncols=5 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave writes one byte past the 8*ncols-byte key +hilbert_curve hilbert_curve C5 the pack wrote nothing past the key: ncols=6 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave writes one byte past the 8*ncols-byte key +hilbert_curve hilbert_curve C5 the pack wrote nothing past the key: ncols=7 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave writes one byte past the 8*ncols-byte key +hilbert_curve hilbert_curve C5 the pack wrote nothing past the key: ncols=8 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave writes one byte past the 8*ncols-byte key +hilbert_curve hilbert_curve C6 ncols == 1 agrees with Z-order over 200,000 random values 15;16;17;18;19 2026-10-21 columnar_curve.c: the final mask t loses its lowest bit, so only bit 0 of each transposed word moves +hilbert_curve hilbert_curve C6 ncols == 1 is the big-endian ordinal over 200,000 random values 15;16;17;18;19 2026-10-21 columnar_curve.c: the final mask t loses its lowest bit, so only bit 0 of each transposed word moves +hilbert_curve hilbert_curve C6 ncols == 1 is the big-endian ordinal: 0 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C6 ncols == 1 is the big-endian ordinal: 1 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C6 ncols == 1 is the big-endian ordinal: 2^63 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C6 ncols == 1 is the big-endian ordinal: UINT64_MAX 15;16;17;18;19 2026-10-21 columnar_curve.c: the final mask t loses its lowest bit, so only bit 0 of each transposed word moves +hilbert_curve hilbert_curve C6 premise: the comparison loop ran 200,000 times 15;16;17;18;19 never - +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=1 asym 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=1 lowbit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=1 max 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave stops after 32 of its 64 rounds +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=1 mixed 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave emits the bit rounds lowest first, a permutation whose dyadic sub-cubes are scattered +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=1 tie 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=1 topbit0 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=1 unit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=1 zero 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=2 asym 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=2 lowbit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=2 max 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave stops after 32 of its 64 rounds +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=2 mixed 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=2 tie 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=2 topbit0 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=2 unit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=2 zero 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=3 asym 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=3 lowbit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=3 max 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave stops after 32 of its 64 rounds +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=3 mixed 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=3 tie 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=3 topbit0 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=3 unit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=3 zero 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=4 asym 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=4 lowbit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=4 max 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave stops after 32 of its 64 rounds +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=4 mixed 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave emits the bit rounds lowest first, a permutation whose dyadic sub-cubes are scattered +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=4 tie 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=4 topbit0 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=4 unit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=4 zero 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=5 asym 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=5 lowbit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=5 max 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave stops after 32 of its 64 rounds +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=5 mixed 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave emits the bit rounds lowest first, a permutation whose dyadic sub-cubes are scattered +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=5 tie 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=5 topbit0 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=5 unit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=5 zero 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=6 asym 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=6 lowbit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=6 max 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave stops after 32 of its 64 rounds +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=6 mixed 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave emits the bit rounds lowest first, a permutation whose dyadic sub-cubes are scattered +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=6 tie 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=6 topbit0 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=6 unit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=6 zero 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes one byte short of the key +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=7 asym 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=7 lowbit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=7 max 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave stops after 32 of its 64 rounds +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=7 mixed 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=7 tie 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=7 topbit0 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=7 unit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=7 zero 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=8 asym 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=8 lowbit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=8 max 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave stops after 32 of its 64 rounds +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=8 mixed 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=8 tie 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=8 topbit0 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=8 unit 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve C7 the Z-order pack is byte-for-byte what it was: ncols=8 zero 15;16;17;18;19 2026-10-21 columnar_curve.c: cluster_pack_interleave zeroes at most 48 bytes, so a 7- or 8-column key is not fully established +hilbert_curve hilbert_curve INSTRUMENT C0 premise: the control battery encoded every case and every point 15;16;17;18;19 never - +hilbert_curve hilbert_curve INSTRUMENT C0 premise: the control encoders were called once per point 15;16;17;18;19 never - +hilbert_curve hilbert_curve INSTRUMENT C1 control: Z-order is a permutation of the index range 15;16;17;18;19 never - +hilbert_curve hilbert_curve INSTRUMENT C1 control: a truncating encoder FAILS the permutation test in every case 15;16;17;18;19 never - +hilbert_curve hilbert_curve INSTRUMENT C1 control: the serpentine is a permutation of the index range 15;16;17;18;19 never - +hilbert_curve hilbert_curve INSTRUMENT C2 control: Z-order FAILS unit-step adjacency in every multi-column case 15;16;17;18;19 never - +hilbert_curve hilbert_curve INSTRUMENT C2 control: the serpentine PASSES unit-step adjacency 15;16;17;18;19 never - +hilbert_curve hilbert_curve INSTRUMENT C3 control: Z-order PASSES dyadic contiguity 15;16;17;18;19 never - +hilbert_curve hilbert_curve INSTRUMENT C3 control: the serpentine FAILS dyadic contiguity in every nested case 15;16;17;18;19 never - +hilbert_curve hilbert_curve INSTRUMENT the control's own pack reproduces the frozen Z-order bytes 15;16;17;18;19 never - iceberg_catalog iceberg_catalog a dangling current-snapshot-id is refused (data_corrupted XX001) 15;16;17;18;19 2026-10-21 a dangling current-snapshot-id reads as no current snapshot iceberg_catalog iceberg_catalog a non-Iceberg JSON object is refused (invalid_parameter_value 22023) 15;16;17;18;19 2026-10-21 any JSON object is read as Iceberg table metadata iceberg_catalog iceberg_catalog a pg_read_server_files member who is not superuser may read the catalog 15;16;17;18;19 2026-10-21 the Iceberg SRF gate admits only superusers, refusing every pg_read_server_files member @@ -2549,6 +2733,81 @@ iceberg_catalog iceberg_catalog malformed JSON is rejected (22P02), backend surv iceberg_catalog iceberg_catalog resolves the named current snapshot, not snapshots[0] 15;16;17;18;19 2026-10-21 the resolver returns the first snapshot rather than the one named current iceberg_catalog iceberg_catalog the decoy snapshot is not returned 15;16;17;18;19 2026-10-21 the resolver returns the first snapshot rather than the one named current iceberg_catalog iceberg_catalog the first snapshot has no parent (NULL) 15;16;17;18;19 never - +iceberg_deletes iceberg_deletes ...and the boundary refusal names the escape 15;16;17;18;19 2026-10-21 columnar_iceberg.c: ice_rebase does not hold a recorded path under the recorded table location +iceberg_deletes iceberg_deletes ...and the negative-pos refusal names the negative pos 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a negative position-delete pos is not refused +iceberg_deletes iceberg_deletes ...and the null-pos refusal names pos 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a position-delete row with a null pos is not refused (the n1 || n2 guard skipped) +iceberg_deletes iceberg_deletes ...and the refusal names the type 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a timestamp equality-delete column is mapped to text instead of refused +iceberg_deletes iceberg_deletes a cross-spec partition-scoped delete applies to nothing (5 rows) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a partition-scoped equality delete applies across partition spec ids +iceberg_deletes iceberg_deletes a data file lacking partition_spec_id is refused under a partitioned delete (XX001) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a data file without partition_spec_id is not refused under a partition-scoped delete +iceberg_deletes iceberg_deletes a delete older than the data (lower seq) does not apply (5 rows) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a position delete applies whatever its sequence number +iceberg_deletes iceberg_deletes a delete scoped to an empty partition deletes nothing (5 rows) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a partition-scoped equality delete ignores the partition value +iceberg_deletes iceberg_deletes a dotdot-escaping delete path is refused (22023) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: ice_rebase does not re-check containment after canonicalize_path +iceberg_deletes iceberg_deletes a manifest list without partition_spec_id is refused for equality deletes (XX001) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: an equality delete whose manifest list carries no partition_spec_id is not refused +iceberg_deletes iceberg_deletes a never-applicable equality delete is skipped, not validated (5 rows) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: an equality delete at the oldest data file's sequence number is not skipped before validation +iceberg_deletes iceberg_deletes a never-applicable equality delete without equality_ids is skipped (5 rows) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: an equality delete at the oldest data file's sequence number is not skipped before validation +iceberg_deletes iceberg_deletes a null delete value matches only null data values 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a null equality-delete value matches any data value (comparing the null Datum kills the backend) +iceberg_deletes iceberg_deletes a partition spec without its fields key is refused (XX001) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a partition spec without a fields array is not refused (a NULL deref kills the backend) +iceberg_deletes iceberg_deletes a partition-scoped delete with an uncomparable partition value is refused (0A000) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a partition-scoped equality delete with an uncomparable partition tuple is not refused +iceberg_deletes iceberg_deletes a partition-scoped equality delete applies only to its partition 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a partition-scoped equality delete ignores the partition value +iceberg_deletes iceberg_deletes a position delete naming a different data file deletes nothing (5 rows) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a position delete applies to every data file, not only the one it names +iceberg_deletes iceberg_deletes a position delete of ordinal 0 deletes the first row (id 1 gone, 4 remain) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a position delete applies only when strictly newer than the data (> where the spec says >=) +iceberg_deletes iceberg_deletes a position delete with a negative pos is refused (XX001, not cast inert) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a negative position-delete pos is not refused +iceberg_deletes iceberg_deletes a position delete with a null pos is refused (XX001, not silently dropped) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a position-delete row with a null pos is not refused (the n1 || n2 guard skipped) +iceberg_deletes iceberg_deletes a position delete with an inherited (null-entry) sequence number applies 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a position delete applies only when strictly newer than the data (> where the spec says >=) +iceberg_deletes iceberg_deletes a v1 manifest without a sequence_number column reads (default 0, 5 rows) 15;16;17;18;19 never - +iceberg_deletes iceberg_deletes an EXISTING entry with a null sequence number is refused (XX001) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: an EXISTING (status 0) manifest entry with a null sequence number is not refused +iceberg_deletes iceberg_deletes an equality column missing from the data file errors (22023) 15;16;17;18;19 never - +iceberg_deletes iceberg_deletes an equality delete at an equal sequence number does not apply 15;16;17;18;19 2026-10-21 columnar_iceberg.c: an equality delete applies at an equal sequence number (both the pre-skip and the per-file strict < relaxed to <=) +iceberg_deletes iceberg_deletes an equality delete on an unsupported column type is refused (0A000) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a timestamp equality-delete column is mapped to text instead of refused +iceberg_deletes iceberg_deletes an equality delete without equality_ids is refused (XX001) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: an equality delete without equality_ids is not refused +iceberg_deletes iceberg_deletes an equality-delete path outside the table location is refused (22023) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: ice_rebase does not hold a recorded path under the recorded table location +iceberg_deletes iceberg_deletes an equality_ids value beyond int32 refuses the manifest (XX001) 15;16;17;18;19 2026-10-21 columnar_avro.c: an equality_ids value beyond int32 is not refused (truncated) +iceberg_deletes iceberg_deletes backend still up after the audit arms 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a partition spec without a fields array is not refused (a NULL deref kills the backend) +iceberg_deletes iceberg_deletes backend still up after the bad-sequence refusal 15;16;17;18;19 never - +iceberg_deletes iceberg_deletes backend still up after the equality applications 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a null equality-delete value matches any data value (comparing the null Datum kills the backend) +iceberg_deletes iceberg_deletes backend still up after the equality refusals 15;16;17;18;19 never - +iceberg_deletes iceberg_deletes backend still up after the negative-pos refusal 15;16;17;18;19 never - +iceberg_deletes iceberg_deletes backend still up after the null-pos refusal 15;16;17;18;19 2026-10-21 columnar_iceberg.c: abort() in place of the null-pos refusal +iceberg_deletes iceberg_deletes columns beyond equality_ids are ignored for matching 15;16;17;18;19 2026-10-21 columnar_iceberg.c: no equality-delete row ever matches +iceberg_deletes iceberg_deletes equality delete drops matching rows (survivors == oracle) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: no equality-delete row ever matches +iceberg_deletes iceberg_deletes equality deletes apply when the delete column is not projected 15;16;17;18;19 2026-10-21 columnar_iceberg.c: no equality-delete row ever matches +iceberg_deletes iceberg_deletes exactly the two deleted rows were removed (5 - 2 = 3) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a position delete applies only when strictly newer than the data (> where the spec says >=) +iceberg_deletes iceberg_deletes multi-column equality matches all columns (AND, not OR) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: an equality delete compares only its first equality column (multi-column match becomes first-column) +iceberg_deletes iceberg_deletes multiple equality-delete files apply their union 15;16;17;18;19 2026-10-21 columnar_iceberg.c: no equality-delete row ever matches +iceberg_deletes iceberg_deletes position and equality deletes combine 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a position delete applies only when strictly newer than the data (> where the spec says >=) +iceberg_deletes iceberg_deletes position deletes apply under column projection (id only -> 1,3,5) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a position delete applies only when strictly newer than the data (> where the spec says >=) +iceberg_deletes iceberg_deletes position deletes drop the listed rows (surviving == oracle) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a position delete applies only when strictly newer than the data (> where the spec says >=) +iceberg_deletes iceberg_deletes the data file has 5 rows before deletes 15;16;17;18;19 never - +iceberg_deletes iceberg_deletes the deleted ids (2, 4) are gone 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a position delete applies only when strictly newer than the data (> where the spec says >=) +iceberg_deletes iceberg_deletes the null-region row is gone, the non-null rows remain 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a null equality-delete value matches any data value (comparing the null Datum kills the backend) +iceberg_deletes iceberg_deletes the us-partition rows survive an eu-scoped delete 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a partition-scoped equality delete ignores the partition value +iceberg_deletes iceberg_deletes the would-be-deleted ids survive when the delete is older than the data 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a position delete applies whatever its sequence number +iceberg_dv iceberg_dv ...and the refusal names the compressed footer 15;16;17;18;19 2026-10-21 columnar_puffin.c: a compressed Puffin footer (flags bit 0) is not refused +iceberg_dv iceberg_dv ...and the refusal names the format version 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a deletion vector in a format-version 2 table is not refused +iceberg_dv iceberg_dv a DV blob declaring a compression codec is refused (XX001) 15;16;17;18;19 2026-10-21 columnar_puffin.c: a deletion-vector blob may declare a compression codec +iceberg_dv iceberg_dv a DV entry without referenced_data_file is refused (XX001) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a DV without referenced_data_file is not refused (a NULL path reaches strcmp and the backend dies) +iceberg_dv iceberg_dv a DV whose offset+size overflows int64 is refused, not read (XX001) 15;16;17;18;19 2026-10-21 columnar_puffin.c: a DV's offset and size are not bounded by the blob region (a wild read kills the backend) +iceberg_dv iceberg_dv a DV with run containers and a high-bucket position decodes 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a DV applies only when strictly newer than the data (> where the spec says >=) +iceberg_dv iceberg_dv a Puffin path outside the table location is refused (22023) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: ice_open_path does not hold a path under the table location +iceberg_dv iceberg_dv a bitset-container DV decodes 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a DV applies only when strictly newer than the data (> where the spec says >=) +iceberg_dv iceberg_dv a compressed Puffin footer is refused (0A000) 15;16;17;18;19 2026-10-21 columnar_puffin.c: a compressed Puffin footer (flags bit 0) is not refused +iceberg_dv iceberg_dv a corrupted DV CRC is refused (XX001) 15;16;17;18;19 2026-10-21 columnar_puffin.c: a deletion vector's CRC-32 is not checked +iceberg_dv iceberg_dv a deletion vector at an equal sequence number applies (survivors == oracle) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a DV applies only when strictly newer than the data (> where the spec says >=) +iceberg_dv iceberg_dv a deletion vector in a format-version 2 table is refused (0A000) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a deletion vector in a format-version 2 table is not refused +iceberg_dv iceberg_dv a deletion vector older than the data does not apply 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a DV applies whatever its sequence number (dv->seq >= d->seq always true) +iceberg_dv iceberg_dv a manifest/footer offset mismatch is refused (XX001) 15;16;17;18;19 2026-10-21 columnar_puffin.c: the Puffin footer's blob need not match the manifest's offset and length (the !found refusal skipped) +iceberg_dv iceberg_dv a record_count that disagrees with the DV cardinality is refused (XX001) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a manifest record_count that disagrees with the DV cardinality is not refused +iceberg_dv iceberg_dv a run container overflowing its 16-bit range is refused (XX001) 15;16;17;18;19 2026-10-21 columnar_puffin.c: a run container whose start+length passes 0x10000 is not refused +iceberg_dv iceberg_dv a run-optimized DV (cookie 12347) decodes 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a DV applies only when strictly newer than the data (> where the spec says >=) +iceberg_dv iceberg_dv a wrong DV blob magic is refused (XX001) 15;16;17;18;19 2026-10-21 columnar_puffin.c: a deletion vector's magic sequence is not checked +iceberg_dv iceberg_dv an aliased duplicate DV (scheme vs no scheme) is refused (XX001) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: the one-DV-per-data-file check compares paths without stripping the file:// scheme +iceberg_dv iceberg_dv an applicable DV supersedes position-delete files for its data file 15;16;17;18;19 2026-10-21 columnar_iceberg.c: position-delete files apply even when a DV applies (no supersede) +iceberg_dv iceberg_dv backend still up after the DV applications 15;16;17;18;19 never - +iceberg_dv iceberg_dv backend still up after the DV refusals 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a DV without referenced_data_file is not refused (a NULL path reaches strcmp and the backend dies) +iceberg_dv iceberg_dv backend still up after the audit arms 15;16;17;18;19 2026-10-21 columnar_puffin.c: a DV's offset and size are not bounded by the blob region (a wild read kills the backend) +iceberg_dv iceberg_dv supersede is per data file, not global 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a DV applies to every data file, not only the one it references +iceberg_dv iceberg_dv two DV blobs in one Puffin file each apply to their own data file 15;16;17;18;19 2026-10-21 columnar_iceberg.c: a DV applies to every data file, not only the one it references +iceberg_dv iceberg_dv two DV entries for one data file are refused (XX001) 15;16;17;18;19 2026-10-21 columnar_iceberg.c: two DVs for one data file are not refused iceberg_fdw_estimate iceberg_fdw_estimate a filtered scan estimates fewer rows than the whole table (selectivity applied) 15;16;17;18;19 2026-10-21 the Iceberg FDW sets rows directly, so the scan's selectivity never applies iceberg_fdw_estimate iceberg_fdw_estimate backend alive 15;16;17;18;19 never - iceberg_fdw_estimate iceberg_fdw_estimate the FDW estimates a positive row count from the manifests 15;16;17;18;19 never - @@ -2636,24 +2895,47 @@ iceberg_objstore iceberg_objstore name mapping reads an id-less file over s3 (5 iceberg_objstore iceberg_objstore position deletes read over s3 (surviving == filesystem oracle) 15;16;17;18;19 2026-10-21 columnar_objstore_module.c: os_check_endpoint_allowed refuses every endpoint, the listed one included iceberg_objstore iceberg_objstore premise: sigv4 fixture server is up 15;16;17;18;19 never - iceberg_objstore iceberg_objstore the position-deleted ids are gone over s3 15;16;17;18;19 2026-10-21 columnar_objstore_module.c: os_check_endpoint_allowed refuses every endpoint, the listed one included +iceberg_rest iceberg_rest a catalog endpoint off the allow-list is refused (42501) 15;16;17;18;19 2026-10-21 objstore/columnar_objstore_module.c: every address reads as link-local and the allow-list is gone +iceberg_rest iceberg_rest a link-local catalog host is refused even if allow-listed (42501) 15;16;17;18;19 2026-10-21 objstore/columnar_objstore_module.c: the allow-list refuses every endpoint and the link-local refusal is gone +iceberg_rest iceberg_rest a non-object catalog config is refused, not walked (XX001) 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: a non-object /v1/config body is not refused +iceberg_rest iceberg_rest a wrong bearer token is refused (28000) 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: a catalog 401/403 is not mapped to 28000 +iceberg_rest iceberg_rest an anonymous request to a token-protected catalog is refused (28000) 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: a catalog 401/403 is not mapped to 28000 +iceberg_rest iceberg_rest an over-cap catalog response is refused (54000) 15;16;17;18;19 2026-10-21 objstore/columnar_objstore_module.c: os_slurp_body does not refuse an advertised length over the cap +iceberg_rest iceberg_rest an unknown table surfaces a clean not-found (42P01) 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: a catalog 404 is not mapped to 42P01 +iceberg_rest iceberg_rest backend still up after the REST refusals 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: the backend aborts on a catalog 404 (abort() before the 42P01 refusal) +iceberg_rest iceberg_rest loadTable resolves the current metadata-location 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: no Authorization header is ever sent +iceberg_rest iceberg_rest premise: rest fixture server is up 15;16;17;18;19 never - +iceberg_rest iceberg_rest premise: the PG server log holds lines to search 15;16;17;18;19 never - +iceberg_rest iceberg_rest the loadTable request carried an Authorization header 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: the bearer token also travels in the request URL's query string +iceberg_rest iceberg_rest the token never appears in the server (PG) log 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: rest_get_json logs the bearer token (elog LOG) +iceberg_rest iceberg_rest the token value never appears in the catalog request log 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: the bearer token also travels in the request URL's query string +iceberg_rest_scan iceberg_rest_scan backend still up after the REST read 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: the backend aborts on a catalog 404 (abort() before the 42P01 refusal) +iceberg_rest_scan iceberg_rest_scan iceberg_rest_namespaces lists the catalog's namespaces 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: iceberg_rest_namespaces drops the catalog's last namespace +iceberg_rest_scan iceberg_rest_scan iceberg_rest_scan matches iceberg_scan of the same table (same oracle) 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: no Authorization header is ever sent +iceberg_rest_scan iceberg_rest_scan iceberg_rest_scan of an unknown table is a clean not-found (42P01) 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: a catalog 404 is not mapped to 42P01 +iceberg_rest_scan iceberg_rest_scan iceberg_rest_scan projects and reorders by field id (amount, id) 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: no Authorization header is ever sent +iceberg_rest_scan iceberg_rest_scan iceberg_rest_scan returns the five current-snapshot rows 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: no Authorization header is ever sent +iceberg_rest_scan iceberg_rest_scan iceberg_rest_tables lists a namespace's tables 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: no Authorization header is ever sent +iceberg_rest_scan iceberg_rest_scan premise: rest fixture server is up 15;16;17;18;19 never - +iceberg_rest_scan iceberg_rest_scan the catalog reports the relocated metadata location 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: no Authorization header is ever sent iceberg_rest_server iceberg_rest_server a catalog_uri option on the USER MAPPING is rejected (HV00D) 15;16;17;18;19 never - -iceberg_rest_server iceberg_rest_server a half OAuth2 credential (id, no secret) is refused before any request (28000) 15;16;17;18;19 never - -iceberg_rest_server iceberg_rest_server a link-local oauth_token_uri is refused by the allow-list (42501) 15;16;17;18;19 never - +iceberg_rest_server iceberg_rest_server a half OAuth2 credential (id, no secret) is refused before any request (28000) 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: a half OAuth2 credential (id, no secret) is not refused, so the mint runs with a NULL secret and the backend dies +iceberg_rest_server iceberg_rest_server a link-local oauth_token_uri is refused by the allow-list (42501) 15;16;17;18;19 2026-10-21 objstore/columnar_objstore_module.c: an endpoint off pgcolumnar.objstore_allowed_endpoints is not refused iceberg_rest_server iceberg_rest_server a non-superuser with no token mapping is refused (28000) despite the environment token, before any request 15;16;17;18;19 2026-10-15 the REST resolver letting every role borrow the environment token iceberg_rest_server iceberg_rest_server a server with an OAuth2 mapping reads by a minted bearer 15;16;17;18;19 never - iceberg_rest_server iceberg_rest_server a token option on the SERVER is rejected (HV00D) 15;16;17;18;19 never - iceberg_rest_server iceberg_rest_server a warehouse option on the SERVER is accepted (not HV00D) 15;16;17;18;19 never - -iceberg_rest_server iceberg_rest_server a warehouse-configured server resolves a table location 15;16;17;18;19 never - +iceberg_rest_server iceberg_rest_server a warehouse-configured server resolves a table location 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: the USER MAPPING token option is ignored iceberg_rest_server iceberg_rest_server a wrong OAuth2 client secret is refused (28000) 15;16;17;18;19 never - -iceberg_rest_server iceberg_rest_server a wrong mapping token is refused (28000) 15;16;17;18;19 never - +iceberg_rest_server iceberg_rest_server a wrong mapping token is refused (28000) 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: abort() in place of the catalog's 401/403 refusal iceberg_rest_server iceberg_rest_server an oauth option on the SERVER is rejected (HV00D) 15;16;17;18;19 never - -iceberg_rest_server iceberg_rest_server an off-allow-list oauth_token_uri host is refused (42501) 15;16;17;18;19 never - +iceberg_rest_server iceberg_rest_server an off-allow-list oauth_token_uri host is refused (42501) 15;16;17;18;19 2026-10-21 objstore/columnar_objstore_module.c: an endpoint off pgcolumnar.objstore_allowed_endpoints is not refused iceberg_rest_server iceberg_rest_server and it cannot mark its own mapping credentials_required false (42501) 15;16;17;18;19 2026-10-15 the catalog validator letting a non-superuser set credentials_required false -iceberg_rest_server iceberg_rest_server backend still up 15;16;17;18;19 never - +iceberg_rest_server iceberg_rest_server backend still up 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: a half OAuth2 credential (id, no secret) is not refused, so the mint runs with a NULL secret and the backend dies iceberg_rest_server iceberg_rest_server control: with credentials_required false set by a superuser, the same role reads by the environment token 15;16;17;18;19 never - -iceberg_rest_server iceberg_rest_server iceberg_rest_table_location('cat',...) reads via the mapping token 15;16;17;18;19 never - +iceberg_rest_server iceberg_rest_server iceberg_rest_table_location('cat',...) reads via the mapping token 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: the USER MAPPING token option is ignored iceberg_rest_server iceberg_rest_server loadTable then carried the minted bearer (Authorization present) 15;16;17;18;19 never - -iceberg_rest_server iceberg_rest_server no mapping and no env token is refused, not bypassed (28000) 15;16;17;18;19 never - +iceberg_rest_server iceberg_rest_server no mapping and no env token is refused, not bypassed (28000) 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: abort() in place of the catalog's 401/403 refusal iceberg_rest_server iceberg_rest_server oauth options are accepted on a USER MAPPING (no error) 15;16;17;18;19 never - iceberg_rest_server iceberg_rest_server premise: rest fixture up 15;16;17;18;19 never - iceberg_rest_server iceberg_rest_server premise: the PG server log holds lines to search, for the client secret 15;16;17;18;19 never - @@ -2663,8 +2945,18 @@ iceberg_rest_server iceberg_rest_server the catalog-URI form still works with th iceberg_rest_server iceberg_rest_server the client secret never appears in the PG server log 15;16;17;18;19 never - iceberg_rest_server iceberg_rest_server the client secret never appears in the fixture request log 15;16;17;18;19 never - iceberg_rest_server iceberg_rest_server the mapping token never appears in the PG server log 15;16;17;18;19 never - -iceberg_rest_server iceberg_rest_server the request carried an Authorization header 15;16;17;18;19 never - -iceberg_rest_server iceberg_rest_server the warehouse was sent on GET /v1/config (?warehouse=wh1) 15;16;17;18;19 never - +iceberg_rest_server iceberg_rest_server the request carried an Authorization header 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: the USER MAPPING token option is ignored +iceberg_rest_server iceberg_rest_server the warehouse was sent on GET /v1/config (?warehouse=wh1) 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: the warehouse server option is never sent on GET /v1/config +iceberg_rest_vended iceberg_rest_vended a table vending no creds still reads with ambient (cfg NULL path) 15;16;17;18;19 never - +iceberg_rest_vended iceberg_rest_vended a wrong vended secret is refused by S3, not bypassed (28000) 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: vended credentials are never built, so every read falls back to ambient +iceberg_rest_vended iceberg_rest_vended backend still up after the vended-credential arms 15;16;17;18;19 2026-10-21 objstore/columnar_objstore_module.c: abort() in place of the HTTP 403 refusal +iceberg_rest_vended iceberg_rest_vended iceberg_rest_scan reads via vended credentials (no ambient creds set) 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: vended credentials are never built, so every read falls back to ambient +iceberg_rest_vended iceberg_rest_vended premise: the PG server log holds lines to search 15;16;17;18;19 never - +iceberg_rest_vended iceberg_rest_vended premise: the SigV4 S3 fixture is up 15;16;17;18;19 never - +iceberg_rest_vended iceberg_rest_vended storage-credentials longest-prefix match selects the right creds 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: rest_vended_cfg never reads the storage-credentials array and signs with the flat config +iceberg_rest_vended iceberg_rest_vended the vended secret never appears in the PG server log 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: the vended secret is written to the server log +iceberg_rest_vended iceberg_rest_vended the vended secret never appears in the S3 request log 15;16;17;18;19 never - +iceberg_rest_vended iceberg_rest_vended vended creds for an off-allow-list endpoint are refused (42501) 15;16;17;18;19 2026-10-21 columnar_iceberg_rest.c: vended credentials are never built, so every read falls back to ambient iceberg_scan iceberg_scan a column renamed in the schema still reads by field id (amt2 = amount) 15;16;17;18;19 2026-10-21 iceberg_scan resolves output columns by position rather than field id iceberg_scan iceberg_scan a pg_read_server_files member who is not superuser may scan 15;16;17;18;19 2026-10-21 iceberg_scan admits only superusers, refusing every pg_read_server_files member iceberg_scan iceberg_scan an output column not in the schema is refused (42703) 15;16;17;18;19 2026-10-21 iceberg_scan does not refuse an output column the schema lacks @@ -2876,6 +3168,74 @@ local_open_race_free local_open_race_free the partition coordinator never stats local_open_race_free local_open_race_free the partition coordinator opens O_NONBLOCK (a FIFO cannot block its open) 15;16;17;18;19 2026-10-21 the partition coordinator opens without O_NONBLOCK local_open_race_free local_open_race_free the race-free opener exists 15;16;17;18;19 never - local_open_race_free local_open_race_free the racy stat-before-open helper is gone 15;16;17;18;19 2026-10-21 the racy PgColumnarRejectNonRegularFile helper comes back +logical_decoding_cdc_recipe logical_decoding_cdc_recipe CONTROL: the columnar table itself still emits nothing 15;16;17;18;19 never - +logical_decoding_cdc_recipe logical_decoding_cdc_recipe a DELETE captures the OLD values, which are gone from the table 15;16;17;18;19 2026-10-21 docs/user-guide.md: the recipe's trigger fires on INSERT OR UPDATE only, not DELETE +logical_decoding_cdc_recipe logical_decoding_cdc_recipe a bulk load writes exactly one capture row per loaded row 15;16;17;18;19 never - +logical_decoding_cdc_recipe logical_decoding_cdc_recipe a rolled-back transaction captures nothing 15;16;17;18;19 never - +logical_decoding_cdc_recipe logical_decoding_cdc_recipe a row trigger on a COLUMNAR table fires once per inserted row 15;16;17;18;19 never - +logical_decoding_cdc_recipe logical_decoding_cdc_recipe an UPDATE captures exactly one row 15;16;17;18;19 2026-10-21 columnar_tableam.c: pgcolumnar_tuple_update leaves the old row version live +logical_decoding_cdc_recipe logical_decoding_cdc_recipe and it is recorded as UPDATE with the NEW value 15;16;17;18;19 2026-10-21 columnar_tableam.c: pgcolumnar_tuple_update writes its new row version with every column null +logical_decoding_cdc_recipe logical_decoding_cdc_recipe and it saw the same values the table holds 15;16;17;18;19 never - +logical_decoding_cdc_recipe logical_decoding_cdc_recipe and leaves no row in the columnar table either 15;16;17;18;19 never - +logical_decoding_cdc_recipe logical_decoding_cdc_recipe and the DELETE as a DELETE 15;16;17;18;19 2026-10-21 docs/user-guide.md: the recipe's trigger fires on INSERT OR UPDATE only, not DELETE +logical_decoding_cdc_recipe logical_decoding_cdc_recipe and the rolled-back row is not in the stream 15;16;17;18;19 never - +logical_decoding_cdc_recipe logical_decoding_cdc_recipe and the row really is gone from the columnar table 15;16;17;18;19 2026-10-21 columnar_tableam.c: pgcolumnar_tuple_delete marks nothing deleted +logical_decoding_cdc_recipe logical_decoding_cdc_recipe inside the transaction the row was captured and is in the columnar table, before the rollback 15;16;17;18;19 never - +logical_decoding_cdc_recipe logical_decoding_cdc_recipe nor the storage's internal INSERT 15;16;17;18;19 2026-10-21 docs/user-guide.md: the recipe's capture records every non-DELETE change as an INSERT +logical_decoding_cdc_recipe logical_decoding_cdc_recipe one decoded capture row per captured change 15;16;17;18;19 never - +logical_decoding_cdc_recipe logical_decoding_cdc_recipe premise: FOR ALL TABLES really does include pgcolumnar's internal tables 15;16;17;18;19 never - +logical_decoding_cdc_recipe logical_decoding_cdc_recipe premise: and it is the whole block, not a fragment 15;16;17;18;19 never - +logical_decoding_cdc_recipe logical_decoding_cdc_recipe premise: events is a COLUMNAR table 15;16;17;18;19 never - +logical_decoding_cdc_recipe logical_decoding_cdc_recipe premise: events_cdc is a HEAP table 15;16;17;18;19 never - +logical_decoding_cdc_recipe logical_decoding_cdc_recipe premise: the extracted recipe contains [CREATE FUNCTION events_capture] 15;16;17;18;19 never - +logical_decoding_cdc_recipe logical_decoding_cdc_recipe premise: the extracted recipe contains [CREATE TABLE events] 15;16;17;18;19 never - +logical_decoding_cdc_recipe logical_decoding_cdc_recipe premise: the extracted recipe contains [CREATE TABLE events_cdc] 15;16;17;18;19 never - +logical_decoding_cdc_recipe logical_decoding_cdc_recipe premise: the extracted recipe contains [CREATE TRIGGER events_cdc_t] 15;16;17;18;19 never - +logical_decoding_cdc_recipe logical_decoding_cdc_recipe premise: the extracted recipe contains [OLD.id] 15;16;17;18;19 never - +logical_decoding_cdc_recipe logical_decoding_cdc_recipe premise: the extracted recipe contains [TG_OP = 'DELETE'] 15;16;17;18;19 never - +logical_decoding_cdc_recipe logical_decoding_cdc_recipe premise: the extracted recipe contains [USING pgcolumnar] 15;16;17;18;19 never - +logical_decoding_cdc_recipe logical_decoding_cdc_recipe premise: the extraction is not empty 15;16;17;18;19 never - +logical_decoding_cdc_recipe logical_decoding_cdc_recipe premise: the guide is where this suite expects it 15;16;17;18;19 never - +logical_decoding_cdc_recipe logical_decoding_cdc_recipe premise: the identical insert COMMITTED does capture, so the arm above is real 15;16;17;18;19 never - +logical_decoding_cdc_recipe logical_decoding_cdc_recipe premise: the same matcher finds the INSERTs, so a 1 above is a real match 15;16;17;18;19 never - +logical_decoding_cdc_recipe logical_decoding_cdc_recipe premise: the server is running at wal_level=logical 15;16;17;18;19 never - +logical_decoding_cdc_recipe logical_decoding_cdc_recipe premise: the slot decoded something at all 15;16;17;18;19 never - +logical_decoding_cdc_recipe logical_decoding_cdc_recipe premise: the slot exists and is logical 15;16;17;18;19 never - +logical_decoding_cdc_recipe logical_decoding_cdc_recipe the capture table IS carried by the slot 15;16;17;18;19 never - +logical_decoding_cdc_recipe logical_decoding_cdc_recipe the decoded stream carries the UPDATE as an UPDATE 15;16;17;18;19 2026-10-21 columnar_tableam.c: pgcolumnar_tuple_update leaves the old row version live +logical_decoding_cdc_recipe logical_decoding_cdc_recipe the update did NOT leak the storage's internal DELETE 15;16;17;18;19 never - +logical_decoding_source logical_decoding_source 100x the rows does not give 100x the internal records 15;16;17;18;19 never - +logical_decoding_source logical_decoding_source CONTROL: the heap table's 50 inserts ARE decoded 15;16;17;18;19 never - +logical_decoding_source logical_decoding_source CONTROL: the second slot decodes the heap table's 5000 inserts 15;16;17;18;19 never - +logical_decoding_source logical_decoding_source a columnar table emits no decodable change 15;16;17;18;19 never - +logical_decoding_source logical_decoding_source although the rows are there 15;16;17;18;19 never - +logical_decoding_source logical_decoding_source and it too emits no decodable change of its own 15;16;17;18;19 never - +logical_decoding_source logical_decoding_source premise: both tables hold the same 50 rows 15;16;17;18;19 never - +logical_decoding_source logical_decoding_source premise: the 100x table really holds 100x the rows 15;16;17;18;19 never - +logical_decoding_source logical_decoding_source premise: the server is running at wal_level=logical 15;16;17;18;19 never - +logical_decoding_source logical_decoding_source premise: the slot decoded something at all 15;16;17;18;19 never - +logical_decoding_source logical_decoding_source premise: the slot exists and is logical 15;16;17;18;19 never - +logical_decoding_source logical_decoding_source the slot carries pgcolumnar's internal catalog instead 15;16;17;18;19 never - +logical_subscriber logical_subscriber an ordinary SELECT ... FOR NO KEY UPDATE is not told about replication 15;16;17;18;19 2026-10-21 columnar_tableam.c: pgcolumnar_tuple_lock raises the replication error for every row lock, not only the apply worker's +logical_subscriber logical_subscriber an ordinary SELECT ... FOR NO KEY UPDATE keeps the generic message 15;16;17;18;19 2026-10-21 columnar_tableam.c: pgcolumnar_tuple_lock raises the replication error for every row lock, not only the apply worker's +logical_subscriber logical_subscriber an ordinary SELECT ... FOR SHARE is not told about replication 15;16;17;18;19 2026-10-21 columnar_tableam.c: pgcolumnar_tuple_lock raises the replication error for every row lock, not only the apply worker's +logical_subscriber logical_subscriber an ordinary SELECT ... FOR SHARE keeps the generic message 15;16;17;18;19 2026-10-21 columnar_tableam.c: pgcolumnar_tuple_lock raises the replication error for every row lock, not only the apply worker's +logical_subscriber logical_subscriber an ordinary SELECT ... FOR UPDATE is not told about replication 15;16;17;18;19 2026-10-21 columnar_tableam.c: pgcolumnar_tuple_lock raises the replication error for every row lock, not only the apply worker's +logical_subscriber logical_subscriber an ordinary SELECT ... FOR UPDATE keeps the generic message 15;16;17;18;19 2026-10-21 columnar_tableam.c: pgcolumnar_tuple_lock raises the replication error for every row lock, not only the apply worker's +logical_subscriber logical_subscriber insert-only: initial sync copied every row 15;16;17;18;19 2026-10-21 columnar_tableam.c: pgcolumnar_multi_insert raises in a logical worker, so initial sync's COPY fails +logical_subscriber logical_subscriber insert-only: no apply errors 15;16;17;18;19 2026-10-21 columnar_tableam.c: pgcolumnar_tuple_insert raises in a logical worker, so streamed INSERTs fail +logical_subscriber logical_subscriber insert-only: streamed INSERTs applied 15;16;17;18;19 2026-10-21 columnar_tableam.c: pgcolumnar_tuple_insert raises in a logical worker, so streamed INSERTs fail +logical_subscriber logical_subscriber insert-only: the UPDATE is skipped by design, not applied 15;16;17;18;19 2026-10-21 columnar_tableam.c: pgcolumnar_tuple_insert raises in a logical worker, so streamed INSERTs fail +logical_subscriber logical_subscriber it retries rather than giving up, so the subscription is wedged 15;16;17;18;19 2026-10-21 columnar_tableam.c: pgcolumnar_tuple_lock never takes the IsLogicalWorker() branch, so the apply worker gets the generic row-locking error +logical_subscriber logical_subscriber premise: the lock fixture really is columnar 15;16;17;18;19 never - +logical_subscriber logical_subscriber premise: the publisher has logical decoding on 15;16;17;18;19 never - +logical_subscriber logical_subscriber premise: the subscriber cluster answers 15;16;17;18;19 never - +logical_subscriber logical_subscriber premise: the subscriber loaded pgcolumnar 15;16;17;18;19 never - +logical_subscriber logical_subscriber the error does NOT say only 'row locking is not supported' 15;16;17;18;19 2026-10-21 columnar_tableam.c: pgcolumnar_tuple_lock never takes the IsLogicalWorker() branch, so the apply worker gets the generic row-locking error +logical_subscriber logical_subscriber the error names logical replication, not row locking 15;16;17;18;19 2026-10-21 columnar_tableam.c: pgcolumnar_tuple_lock never takes the IsLogicalWorker() branch, so the apply worker gets the generic row-locking error +logical_subscriber logical_subscriber the hint names the insert-only publication 15;16;17;18;19 2026-10-21 columnar_tableam.c: the apply worker's hint stops naming CREATE PUBLICATION ... WITH (publish = 'insert') +logical_subscriber logical_subscriber the second subscription synced before the UPDATE 15;16;17;18;19 2026-10-21 columnar_tableam.c: pgcolumnar_multi_insert raises in a logical worker, so initial sync's COPY fails +logical_subscriber logical_subscriber the wedge is real: the row is still unchanged on the subscriber 15;16;17;18;19 2026-10-21 columnar_tableam.c: pgcolumnar_multi_insert raises in a logical worker, so initial sync's COPY fails maintenance_crash maintenance_crash after a crash with compact committed, the index reads the rows the scan reads 15;16;17;18;19 2026-10-16 pgcolumnar_index_fetch_tuple finds no row (columnar_tableam.c) maintenance_crash maintenance_crash after a crash with compact committed, the table is as compact left it, with the same rows 15;16;17;18;19 never - maintenance_crash maintenance_crash after a crash with compact uncommitted, the index reads the rows the scan reads 15;16;17;18;19 2026-10-16 pgcolumnar_index_fetch_tuple finds no row (columnar_tableam.c) diff --git a/test/check_ledger_budget.txt b/test/check_ledger_budget.txt index 2970fed4d..2b5e6b626 100644 --- a/test/check_ledger_budget.txt +++ b/test/check_ledger_budget.txt @@ -15,7 +15,7 @@ # # Adding a check to a suite that is already covered does not move it, which is # what makes it safe to bound. -suites_not_covered 102 +suites_not_covered 93 # # checks_never_observed_red -- A CENSUS. NOT a ceiling, and it must not become # one. @@ -5439,4 +5439,187 @@ suites_not_covered 102 # never 3883 + 1 = 3884 # dated 2282 + 4 = 2286 # 3884 + 2286 == 6170 -checks_never_observed_red 3884 +# #1341, iceberg_dv seeded, with its twin (OffgridwithJD): 3884 -> 3885, and +# suites_not_covered 102 -> 101. The suite had no rows. It enters with 26 checks from +# five-major runs (all green on each of 15-19, shell and twin alike), after two fixes. +# "a manifest/footer offset mismatch is refused (XX001)" read the SQLSTATE alone, and +# several guards here raise XX001: with the offset match removed a later XX001 refused +# the same file, and main's shell passed 26/26. Every refusal now reads its own guard's +# message. And the three "backend still up" checks read SELECT 1, which answers again +# after crash recovery: with the referenced_data_file check removed a backend died on +# the arm, and main passed the survival check. They now also read the server log's +# count of killed backends, and each refusal waits for the server first, so a crash +# reddens its own arm and its section's survival check and nothing else. 25 are dated by +# seventeen mutations, each reddening the same names in both twins. 1 arrives never: +# the survival check after the DV applications, which no mutation crashed. +# +# ledger rows 6170 + 26 = 6196 +# never 3884 + 1 = 3885 +# dated 2286 + 25 = 2311 +# 3885 + 2311 == 6196 +# +# #1341, logical_decoding_source seeded, with its twin (OffgridwithJD): 3885 -> 3897, +# and suites_not_covered 101 -> 100. The suite had no rows. It enters with its 12 +# checks, all green on each of 15-19 in shell and twin alike. The missing-plugin name +# pgc_skip records is now a literal both twins share, but it is a skip, not a check, so +# it has no row: CI has test_decoding and never emits it. After one fix. +# The second slot had no heap control: created after the 5000-row insert, it decoded +# nothing, and "it too emits no decodable change" and "100x the rows does not give 100x +# the internal records" (0 < anything) both passed, 11/11 on main. A heap table now writes +# through that slot in the same window and must decode its 5000 inserts, and the ratio +# check refuses a slot that carried no internal records. Under that harness mutation both +# are red in both twins. No source mutation reddens a check: the claim is that pgcolumnar +# writes nothing logical decoding can read, and a default chunk group of 100 rows still +# keeps the internal records following groups (114 for 50 groups, under the 10x bound). +# 0 are dated. 12 arrive never. +# +# ledger rows 6196 + 12 = 6208 +# never 3885 + 12 = 3897 +# dated 2311 + 0 = 2311 +# 3897 + 2311 == 6208 +# +# #1341, iceberg_deletes seeded, with its twin (OffgridwithJD): 3897 -> 3903, and +# suites_not_covered 100 -> 99. The suite had no rows. It enters with 49 checks from +# five-major runs (all green on each of 15-19, shell and twin alike), after one fix. +# The six "backend still up" checks read SELECT 1, which answers again after crash +# recovery: with abort() in place of the null-pos refusal, or the partition spec's +# fields guard removed (a segfault), main's shell passed the survival check once +# recovery had finished. They now also read the server log's count of killed +# backends, and every reading waits for the server first, so a crash reddens its own +# arm and its section's survival check, not the two arms after it. Every refusal now +# also reads its own guard's message (XX001 and 22023 are shared), and the oracle +# comparisons refuse an empty side. 43 are dated by twenty-three mutations, each reddening +# the same names in both twins. The equal-sequence equality check is dated only by +# relaxing both of its layered guards: the per-file strict < alone passed, because the +# pre-skip at the oldest data file's sequence number already drops that delete. +# 6 arrive never. +# +# ledger rows 6208 + 49 = 6257 +# never 3897 + 6 = 3903 +# dated 2311 + 43 = 2354 +# 3903 + 2354 == 6257 +# +# #1341, logical_subscriber seeded, with its twin (OffgridwithJD): 3903 -> 3907, +# and suites_not_covered 99 -> 98. The suite had no rows. It enters with 20 checks +# from five-major runs (all green on each of 15-19, shell and twin alike), after four fixes. +# With the insert-only subscription disabled before its UPDATE, main's shell passed 20/20: +# "skipped by design" and "no apply errors" read after a fixed sleep, and nothing applied +# is nothing applied wrongly. An INSERT after the UPDATE must now arrive first. With no +# UPDATE sent, "does NOT say only 'row locking'" and "the wedge is real" passed; the first +# now needs the replication error in the log, the second reads publisher -> subscriber. +# With the lock statements failing on a missing table, the three "is not told about +# replication" checks passed; they now need the generic error first. And the second +# subscription's sync check lost its premise prefix: COPY failing in a logical worker +# reddens it. The lock loop is unrolled so the twin grader reads each name. +# 16 are dated by five mutations, each reddening the same names in both twins. 4 +# arrive never. +# +# ledger rows 6257 + 20 = 6277 +# never 3903 + 4 = 3907 +# dated 2354 + 16 = 2370 +# 3907 + 2370 == 6277 +# +# #1341, logical_decoding_cdc_recipe seeded, with its twin (OffgridwithJD): 3907 -> 3936, +# and suites_not_covered 98 -> 97. The suite had no rows. It enters with its 36 checks from five-major +# runs (all green on each of 15-19, shell and twin alike), after two fixes. The +# missing-plugin name pgc_skip records is a skip, not a check, so it has no row: CI has +# test_decoding and never emits it. The rollback arm read the capture count +# and the row after ROLLBACK only, so a rolled-back INSERT that never ran passed both arms: +# with it made a duplicate key, main passed 35/35. A reading inside the transaction, before +# the rollback, now requires the row captured and present. And "it saw the same values the +# table holds" compared two readings with check, so with the server gone "" = "" passed; it +# is check_text. The recipe-contains premises are unrolled into literal names. Three +# table-AM mutations and two mutations of the guide's own recipe date checks, each red by the +# same names in both twins. 7 are dated. 29 arrive never. +# +# ledger rows 6277 + 36 = 6313 +# never 3907 + 29 = 3936 +# dated 2370 + 7 = 2377 +# 3936 + 2377 == 6313 +# +# #1341, iceberg_rest seeded, with its twin (OffgridwithJD): 3936 -> 3938, and +# suites_not_covered 97 -> 96. The suite had no rows. It enters with 14 checks from +# five-major runs (all green on each of 15-19, shell and twin alike), after two fixes. +# The 42501 arms read the SQLSTATE alone, and two guards raise 42501: with the allow-list +# refusing everything and the link-local refusal gone, "a link-local catalog host is +# refused" passed on the allow-list's 42501, and with every address read as link-local +# and the allow-list gone, "a catalog endpoint off the allow-list is refused" passed on +# the link-local guard's. Every refusal now reads its own guard's message. And "backend +# still up after the REST refusals" read SELECT 1, which answers again after crash +# recovery: with the backend aborting on the catalog's 404, main passed it. It now also +# reads the server log's count of killed backends. 12 are dated by eleven mutations, +# each reddening the same names in both twins. 2 arrive never: the two premises. +# +# ledger rows 6313 + 14 = 6327 +# never 3936 + 2 = 3938 +# dated 2377 + 12 = 2389 +# 3938 + 2389 == 6327 +# +# #1341, iceberg_rest_scan seeded, with its twin (OffgridwithJD): 3938 -> 3939, and +# suites_not_covered 96 -> 95. The suite had no rows. It enters with 9 checks from +# five-major runs (all green on each of 15-19, shell and twin alike), after one fix. +# "backend still up after the REST read" read SELECT 1, which answers again after crash +# recovery: with the backend aborting on the catalog's 404 and the probe 3s later, main +# passed it. It now also reads the server log's count of killed backends, and the +# not-found arm reads the catalog's own 404 message. 8 are dated by five mutations, +# each reddening the same names in both twins. 1 arrives never: the premise. +# +# ledger rows 6327 + 9 = 6336 +# never 3938 + 1 = 3939 +# dated 2389 + 8 = 2397 +# 3939 + 2397 == 6336 +# +# #1341, iceberg_rest_vended seeded, with its twin (OffgridwithJD): 3939 -> 3943, and +# suites_not_covered 95 -> 94. The suite had no rows. It enters with 10 checks from +# five-major runs (all green on each of 15-19, shell and twin alike), after three fixes. +# The longest-prefix arm passed with the storage-credentials array never read: the +# catalog's flat config carried the same right credentials, so the fixture's flat config +# is now wrong too. The wrong-secret arm read 28000 alone, which "no credentials" also +# raises: with vended credentials never built, it passed; it now needs the 403 message. +# The survival check read SELECT 1: with abort() in place of the 403 refusal it passed; +# it now also reads the server log's count of killed backends. 6 are dated by six +# mutations, each reddening the same names in both twins. 4 arrive never. +# +# ledger rows 6336 + 10 = 6346 +# never 3939 + 4 = 3943 +# dated 2397 + 6 = 2403 +# 3943 + 2403 == 6346 +# +# #1341, iceberg_rest_server's twin (OffgridwithJD): 3943 -> 3933. The suite +# already had its 29 rows; none is added. Four fixes in the shell, mirrored in +# the twin. "backend still up" read SELECT 1: with the half-credential guard removed +# (the backend segfaults) or abort() in place of the 401 refusal, it passed; it now +# also reads the server log's count of killed backends, and every reading waits for +# the server first. The two "accepted" arms read an empty SQLSTATE, which a stopped +# server also gives, so with the server stopped both passed; a statement that ran now +# reads "ok". "refused by the allow-list (42501)" read the SQLSTATE alone, and the +# link-local guard raises 42501 too: with the allow-list removed it passed. Every +# refusal now needs its own message, and the half-credential arm also counts the +# fixture's requests. 10 more rows are dated by five mutations, each reddening the +# same names in both twins. +# +# ledger rows 6346 + 0 = 6346 +# never 3943 - 10 = 3933 +# dated 2403 + 10 = 2413 +# 3933 + 2413 == 6346 +# +# #1341, hilbert_curve seeded, with its twin (OffgridwithJD): 3933 -> 3949, and +# suites_not_covered 94 -> 93. The suite had no rows. It enters with its 184 +# checks from five-major runs, 184/184 in the shell and in the twin on each of 15-19. +# Seventeen mutations of src/columnar_curve.c each reddened checks in the shell on +# main, and between them every arm but the six premises and the ten INSTRUMENT +# controls, which link nothing from src/ by design. Two shell changes. The battery +# now fills every key buffer with 0xa5 before the pack writes it: under a memset one +# byte short, which goldens went red depended on what the previous call had left in +# the buffer (121 red on main, against a stable 124), and the first use of a buffer +# read uninitialized memory. And the two list-taking helpers are written out as loops +# at their call sites, so compare_to_bash reads the suite instead of refusing it, +# which it did as the tree's one refused suite. The seventeen date 168 rows, each +# reddening the same names in the shell and the twin. 16 arrive never: the six +# premises and the ten controls. +# +# ledger rows 6346 + 184 = 6530 +# never 3933 + 16 = 3949 +# dated 2413 + 168 = 2581 +# 3949 + 2581 == 6530 +checks_never_observed_red 3949 diff --git a/test/hilbert_curve.sh b/test/hilbert_curve.sh index 1e814886c..7dab0744e 100755 --- a/test/hilbert_curve.sh +++ b/test/hilbert_curve.sh @@ -335,20 +335,13 @@ battery_arm_names() { } BATTERY_ARMS="$(battery_arm_names)" -# Report a whole group of arms in one state, so a run that could not build -# still lists what it did not measure. -arms_unrunnable() { # arms_unrunnable "LIST" REASON DETAIL - local _a - while IFS= read -r _a; do - [ -n "$_a" ] && check_unrunnable "$_a" "$2" "$3" - done <<< "$1" -} -arms_failed() { # arms_failed "LIST" DETAIL - local _a - while IFS= read -r _a; do - [ -n "$_a" ] && pgc_fail "$_a" "$2" - done <<< "$1" -} +# A run that could not build reports a whole group of arms in one state, so it +# still lists what it did not measure. The loops that do it are written out at each +# site rather than in a helper taking the list: a helper that reaches a recorder +# through a loop variable is a name slot compare_to_bash cannot resolve, and it +# refused this whole suite for that (#1341). At the top level the same loop is read +# as one unnamed call, which the grader drops, and the literal arms below are what +# it grades. # A CC INHERITED FROM THE ENVIRONMENT IS NOT A COMPILER UNTIL IT COMPILES. # @@ -376,8 +369,9 @@ if [ "$cc_usable" != yes ]; then sed 's/^/ /' "$WORK/smoke.log" 2>/dev/null _why="CC=$CC cannot build C" fi - arms_unrunnable "$CONTROL_ARMS" MISSING_DEPENDENCY "$_why" - arms_unrunnable "$BATTERY_ARMS" MISSING_DEPENDENCY "$_why" + while IFS= read -r _a; do + [ -n "$_a" ] && check_unrunnable "$_a" MISSING_DEPENDENCY "$_why" + done <<< "$CONTROL_ARMS"$'\n'"$BATTERY_ARMS" pgc_summary fi echo "-- compiler: $CC ($("$CC" --version 2>/dev/null | head -1))" @@ -1178,7 +1172,9 @@ if "$CC" -O2 -Wall -I"$WORK" -o "$WORK/controls" "$WORK/controls.c" >"$CTLLOG" 2 else echo "---- the control program would not build or run ----" sed 's/^/ /' "$CTLLOG" - arms_failed "$CONTROL_ARMS" "the control program would not build or run" + while IFS= read -r _a; do + [ -n "$_a" ] && pgc_fail "$_a" "the control program would not build or run" + done <<< "$CONTROL_ARMS" fi echo "-- instrument arms: $((PGC_CHECKS - _pre_checks)) reported" \ "($((PGC_PASSED - _pre_passed)) passed, $((PGC_FAILED - _pre_failed)) failed);" \ @@ -1214,6 +1210,22 @@ cat > "$WORK/battery.c" <<'CEOF' static void (*const pin_transpose) (uint64 *, int) = cluster_hilbert_transpose; static void (*const pin_pack) (const uint64 *, int, unsigned char *) = cluster_pack_interleave; +/* + * Every key buffer is filled with 0xa5 before the pack writes it. A byte the pack + * never establishes then reads 0xa5 -- the same in every run and in the pytest + * twin -- rather than whatever the previous key left there, or, on the first use of + * a buffer, uninitialized memory. Measured: under a pack whose memset stops one + * byte short, which goldens went red depended on what the previous call had left + * in that byte. The C5 extent arms below fill their own buffers with 0x00 and 0xff + * on purpose and call the pack directly; nothing else does. + */ +static void +pk(const uint64 *ord, int ncols, unsigned char *out) +{ + memset(out, 0xa5, (size_t) ncols * 8); + cluster_pack_interleave(ord, ncols, out); +} + /* Deterministic, so two runs of this suite compare the same points. */ static unsigned rs = 987654321u; static unsigned @@ -1332,7 +1344,7 @@ enc_hilbert(const unsigned *c, int ncols, int b, unsigned char *key) for (j = 0; j < ncols; j++) X[j] = (uint64) c[j] << (64 - b); cluster_hilbert_transpose(X, ncols); - cluster_pack_interleave(X, ncols, key); + pk(X, ncols, key); } int @@ -1431,9 +1443,9 @@ main(void) Xf[j] = Xb[j] | (rnd64() & ((((uint64) 1) << (64 - b)) - 1)); } cluster_hilbert_transpose(Xb, ncols); - cluster_pack_interleave(Xb, ncols, kb); + pk(Xb, ncols, kb); cluster_hilbert_transpose(Xf, ncols); - cluster_pack_interleave(Xf, ncols, kf); + pk(Xf, ncols, kf); points += 1; ixf = pgc_key_index(kf, ncols, b); if (pgc_key_index(kb, ncols, b) != ixf) @@ -1494,7 +1506,7 @@ main(void) untranspose(A0, ncols, 0); memcpy(B, A0, sizeof(uint64) * ncols); cluster_hilbert_transpose(B, ncols); - cluster_pack_interleave(B, ncols, kk); + pk(B, ncols, kk); trips++; ok0 = (memcmp(kk, key, (size_t) ncols * 8) == 0); if (!ok0) @@ -1505,7 +1517,7 @@ main(void) untranspose(A1, ncols, 1); memcpy(B, A1, sizeof(uint64) * ncols); cluster_hilbert_transpose(B, ncols); - cluster_pack_interleave(B, ncols, kk); + pk(B, ncols, kk); if (memcmp(kk, key, (size_t) ncols * 8) != 0) mutbad++; @@ -1547,7 +1559,7 @@ main(void) */ memcpy(B, A1, sizeof(uint64) * ncols); cluster_hilbert_transpose(B, ncols); - cluster_pack_interleave(B, ncols, kk); + pk(B, ncols, kk); ok1 = (memcmp(kk, key2, (size_t) ncols * 8) == 0); adj++; @@ -1599,7 +1611,7 @@ main(void) pgc_makevec(k, ncols, X); memcpy(Y, X, sizeof(uint64) * ncols); cluster_hilbert_transpose(Y, ncols); - cluster_pack_interleave(Y, ncols, key); + pk(Y, ncols, key); pgc_hex(key, ncols * 8, h); printf("golden_n%d_%s=%s\n", ncols, pgc_vecname[k], h); } @@ -1609,12 +1621,12 @@ main(void) v[0] = (uint64) 1 << 63; v[1] = 0; - cluster_pack_interleave(v, 2, key); + pk(v, 2, key); pgc_hex(key, 16, h); printf("packpin_hi=%s\n", h); v[0] = 0; v[1] = (uint64) 1 << 63; - cluster_pack_interleave(v, 2, key); + pk(v, 2, key); pgc_hex(key, 16, h); printf("packpin_lo=%s\n", h); } @@ -1703,7 +1715,7 @@ main(void) { X[0] = vals[k]; cluster_hilbert_transpose(X, 1); - cluster_pack_interleave(X, 1, key); + pk(X, 1, key); pgc_hex(key, 8, h); printf("ident_n1_%s=%s\n", nm[k], h); } @@ -1728,9 +1740,9 @@ main(void) X[0] = x; cluster_hilbert_transpose(X, 1); - cluster_pack_interleave(X, 1, kh); + pk(X, 1, kh); Y[0] = x; - cluster_pack_interleave(Y, 1, kz); + pk(Y, 1, kz); for (q = 0; q < 8; q++) be[q] = (unsigned char) (x >> (56 - 8 * q)); n6++; @@ -1749,7 +1761,7 @@ main(void) for (k = 0; k < PGC_NVEC; k++) { pgc_makevec(k, ncols, X); - cluster_pack_interleave(X, ncols, key); + pk(X, ncols, key); pgc_hex(key, ncols * 8, h); printf("zorder_n%d_%s=%s\n", ncols, pgc_vecname[k], h); } @@ -1766,7 +1778,9 @@ if [ ! -f "$CURVE_C" ] || [ ! -f "$CURVE_H" ]; then [ -f "$CURVE_C" ] || _missing="$_missing src/columnar_curve.c" [ -f "$CURVE_H" ] || _missing="$_missing src/columnar_curve.h" echo "-- the code under test is absent:$_missing" - arms_failed "$BATTERY_ARMS" "the code under test is absent:$_missing" + while IFS= read -r _a; do + [ -n "$_a" ] && pgc_fail "$_a" "the code under test is absent:$_missing" + done <<< "$BATTERY_ARMS" else cp "$CURVE_C" "$CURVE_H" "$WORK/" if "$CC" -O2 -Wall -Werror=incompatible-pointer-types -I"$WORK" \ @@ -1777,7 +1791,9 @@ else else echo "---- the battery would not build or run against $CURVE_C ----" sed 's/^/ /' "$BATLOG" | head -40 - arms_failed "$BATTERY_ARMS" "the battery would not build or run; see the log above" + while IFS= read -r _a; do + [ -n "$_a" ] && pgc_fail "$_a" "the battery would not build or run; see the log above" + done <<< "$BATTERY_ARMS" fi fi diff --git a/test/iceberg_deletes.sh b/test/iceberg_deletes.sh index b9e88a875..413959ea7 100755 --- a/test/iceberg_deletes.sh +++ b/test/iceberg_deletes.sh @@ -15,22 +15,73 @@ set -uo pipefail . "$(dirname "${BASH_SOURCE[0]}")/lib.sh" pgc_setup "${1:-/usr/local/pg17/bin/pg_config}" -sqlstate_of() { - env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postgres \ - -d "$PGC_DB" -qtA 2>&1 < the SQLSTATE when SQL is refused with PHRASE in its message; +# "SQLSTATE (another refusal: MESSAGE)" when a different guard refused it; "no +# error" when it ran; nothing when the server could not be asked. Each guard here +# has its own message, and several share a SQLSTATE (XX001 and 22023), so an arm +# counts only its own guard's refusal. +refusal() { + local out st msg + await_up + out="$(env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postgres \ + -d "$PGC_DB" -qtA 2>&1 <&1 < wait, at most 60 s, until the server answers. A refusal arm that kills a +# backend sends the server through crash recovery; without the wait, the next arms' +# readings fail on recovery and red as collateral, more or fewer of them by timing +# (with the partition spec's fields guard removed, the backend segfaults and two +# later arms went red). The crash itself is still caught: survived() counts it. +await_up() { + local i + for i in $(seq 1 120); do + [ "$(q 'SELECT 1')" = 1 ] && return 0 + sleep 0.5 + done +} + +# Backends killed by a signal, from the server log: a number, or nothing when the +# log can't be read. A fresh connection answers again after crash recovery, so +# "SELECT 1" alone passed with a backend killed by a refusal arm: with abort() in +# place of the null-pos refusal, or the fields guard removed (a segfault), and the +# check run once recovery had finished. The survival checks below also require +# that this count has not moved since their section began. +crashes() { + local _n + _n="$(pgc_pg "grep -c 'terminated by signal' '$PGC_LOGFILE'" 2>/dev/null)" + case "$_n" in '' | *[!0-9]*) ;; *) echo "$_n" ;; esac +} +survived() { # survived BEFORE -> "1" when the server answers and no backend died since + local _up _now + await_up + _up="$(q 'SELECT 1')"; _now="$(crashes)" + if ! [[ "$1" =~ ^[0-9]+$ && "$_now" =~ ^[0-9]+$ ]]; then echo "no (crash count unreadable)" + elif [ "$_up" = 1 ] && [ "$_now" = "$1" ]; then echo 1 + else echo "no ([$_up], $(( _now - $1 )) backend crashes)"; fi +} + FX="$(dirname "${BASH_SOURCE[0]}")/fixtures/iceberg" WHD="$FX/warehouse_del" [ -f "$WHD/db/t/metadata/apply.metadata.json" ] || pgc_skip fixture "delete fixture is missing" @@ -44,7 +95,7 @@ chmod -R u+rwX "$DEST" MDIR="$DEST/db/t/metadata" # the full data, ignoring deletes, is 5 rows -- the baseline the delete removes from -check "premise: the data file has 5 rows before deletes" \ +check "the data file has 5 rows before deletes" \ "$(q "SELECT count(*) FROM pgcolumnar.read_parquet('$DEST/db/t/data/data.parquet') AS t(id bigint, region text, amount int)")" "5" @@ -55,20 +106,24 @@ o = json.load(open(sys.argv[1])) print("\n".join("%d|%s|%d" % (r["id"], r["region"], r["amount"]) for r in o["surviving"])) PY )" -check "position deletes drop the listed rows (surviving == oracle)" \ +await_up +check_text "position deletes drop the listed rows (surviving == oracle)" \ "$(q "SELECT id || '|' || region || '|' || amount FROM pgcolumnar.iceberg_scan('$MDIR/apply.metadata.json') AS t(id bigint, region text, amount int) ORDER BY id")" \ "$ORACLE" +await_up check "the deleted ids (2, 4) are gone" \ "$(q "SELECT count(*) FROM pgcolumnar.iceberg_scan('$MDIR/apply.metadata.json') AS t(id bigint, region text, amount int) WHERE id IN (2,4)")" "0" +await_up check "exactly the two deleted rows were removed (5 - 2 = 3)" \ "$(q "SELECT count(*) FROM pgcolumnar.iceberg_scan('$MDIR/apply.metadata.json') AS t(id bigint, region text, amount int)")" "3" # deletes are by row ordinal, independent of column projection: reading only one # column must still drop the same rows (the ordinal is the row index, not a # column value) +await_up check "position deletes apply under column projection (id only -> 1,3,5)" \ "$(q "SELECT string_agg(id::text, ',' ORDER BY id) FROM pgcolumnar.iceberg_scan('$MDIR/apply.metadata.json') AS t(id bigint)")" \ @@ -79,9 +134,11 @@ check "position deletes apply under column projection (id only -> 1,3,5)" \ # which the spec says applies (data_seq <= delete_seq) -- the boundary that a # strict > would wrongly exclude. This arm is the other side: a delete OLDER than # the data (seq 4 < data seq 5) must NOT apply, so all five rows survive. +await_up check "a delete older than the data (lower seq) does not apply (5 rows)" \ "$(q "SELECT count(*) FROM pgcolumnar.iceberg_scan('$MDIR/noapply.metadata.json') AS t(id bigint, region text, amount int)")" "5" +await_up check "the would-be-deleted ids survive when the delete is older than the data" \ "$(q "SELECT count(*) FROM pgcolumnar.iceberg_scan('$MDIR/noapply.metadata.json') AS t(id bigint, region text, amount int) WHERE id IN (2,4)")" "2" @@ -90,6 +147,7 @@ check "the would-be-deleted ids survive when the delete is older than the data" # real writers record a null entry sequence number and inherit the manifest's; # this variant's delete entry is null, inheriting seq 5 from the manifest, so it # must still apply over the seq-5 data (3 rows survive), proving inheritance. +await_up check "a position delete with an inherited (null-entry) sequence number applies" \ "$(q "SELECT count(*) FROM pgcolumnar.iceberg_scan('$MDIR/inherit.metadata.json') AS t(id bigint, region text, amount int)")" "3" @@ -98,6 +156,7 @@ check "a position delete with an inherited (null-entry) sequence number applies" # the delete is applicable by sequence, but its rows target another data file, so # none of data.parquet's rows may be dropped (proves the path match excludes, not # just includes -- a basename-only or always-true match would wrongly delete). +await_up check "a position delete naming a different data file deletes nothing (5 rows)" \ "$(q "SELECT count(*) FROM pgcolumnar.iceberg_scan('$MDIR/wrongpath.metadata.json') AS t(id bigint, region text, amount int)")" "5" @@ -106,24 +165,27 @@ check "a position delete naming a different data file deletes nothing (5 rows)" # On current main a null pos is silently dropped (the `!n1 && !n2` filter) and a # negative pos is cast to a huge uint64 that matches nothing: the scan returns # the phantom row(s) with NO error. The reader must instead refuse (XX001). +CRASHES_BEFORE="$(crashes)" check "a position delete with a null pos is refused (XX001, not silently dropped)" \ - "$(sqlstate_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/nullpos.metadata.json') - AS t(id bigint, region text, amount int)")" "XX001" + "$(refusal "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/nullpos.metadata.json') + AS t(id bigint, region text, amount int)" "has a null pos")" "XX001" check "...and the null-pos refusal names pos" \ "$(errmsg_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/nullpos.metadata.json') AS t(id bigint, region text, amount int)" | grep -c "null pos")" "1" -check "backend still up after the null-pos refusal" "$(q 'SELECT 1')" "1" +check "backend still up after the null-pos refusal" "$(survived "$CRASHES_BEFORE")" "1" +CRASHES_BEFORE="$(crashes)" check "a position delete with a negative pos is refused (XX001, not cast inert)" \ - "$(sqlstate_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/negpos.metadata.json') - AS t(id bigint, region text, amount int)")" "XX001" + "$(refusal "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/negpos.metadata.json') + AS t(id bigint, region text, amount int)" "has a negative pos")" "XX001" check "...and the negative-pos refusal names the negative pos" \ "$(errmsg_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/negpos.metadata.json') AS t(id bigint, region text, amount int)" | grep -c "negative pos")" "1" -check "backend still up after the negative-pos refusal" "$(q 'SELECT 1')" "1" +check "backend still up after the negative-pos refusal" "$(survived "$CRASHES_BEFORE")" "1" # RISK control: pos == 0 is a LEGITIMATE delete of the first row (id 1). It must # apply, not be rejected -- a `pos <= 0` guard would wrongly turn this red. +await_up check "a position delete of ordinal 0 deletes the first row (id 1 gone, 4 remain)" \ "$(q "SELECT string_agg(id::text, ',' ORDER BY id) FROM pgcolumnar.iceberg_scan('$MDIR/zeropos.metadata.json') @@ -144,50 +206,62 @@ eq_ids_of() { AS t(id bigint, region text, amount int)" } -check "equality delete drops matching rows (survivors == oracle)" \ +CRASHES_BEFORE="$(crashes)" +await_up +check_text "equality delete drops matching rows (survivors == oracle)" \ "$(eq_ids_of eqapply)" "$(eq_oracle eqapply)" # THE boundary that distinguishes the equality rule (strict <) from the position # rule (<=): the same delete file at a sequence number EQUAL to the data's must # delete NOTHING (an equality delete never touches same-commit data) -check "an equality delete at an equal sequence number does not apply" \ +await_up +check_text "an equality delete at an equal sequence number does not apply" \ "$(eq_ids_of eqboundary)" "$(eq_oracle eqboundary)" # multi-column equality is an AND: (3,'eu') matches no row (3 is 'us'), so only # (4,'us') deletes -check "multi-column equality matches all columns (AND, not OR)" \ +await_up +check_text "multi-column equality matches all columns (AND, not OR)" \ "$(eq_ids_of eqmulti)" "$(eq_oracle eqmulti)" # only the equality_ids columns define the match; the delete file's extra amount # column carries a wrong value that must be ignored -check "columns beyond equality_ids are ignored for matching" \ +await_up +check_text "columns beyond equality_ids are ignored for matching" \ "$(eq_ids_of eqextra)" "$(eq_oracle eqextra)" # null matches null (IS NULL semantics), and ONLY null: id 6 (region NULL) goes, # id 7 ('eu') stays -check "a null delete value matches only null data values" \ +await_up +check_text "a null delete value matches only null data values" \ "$(eq_ids_of eqnull)" "$(eq_oracle eqnull)" +await_up check "the null-region row is gone, the non-null rows remain" \ "$(q "SELECT count(*) FROM pgcolumnar.iceberg_scan('$MDIR/eqnull.metadata.json') AS t(id bigint, region text, amount int) WHERE region IS NULL")" "0" # two delete files with different equality_ids in one manifest: both apply -check "multiple equality-delete files apply their union" \ +await_up +check_text "multiple equality-delete files apply their union" \ "$(eq_ids_of eqtwo)" "$(eq_oracle eqtwo)" # position and equality deletes in one snapshot: the skip sets merge -check "position and equality deletes combine" \ +await_up +check_text "position and equality deletes combine" \ "$(eq_ids_of eqmixed)" "$(eq_oracle eqmixed)" # the delete columns come from the table schema, not the output list: a scan # projecting only amount must still drop the id-matched rows +await_up check "equality deletes apply when the delete column is not projected" \ "$(q "SELECT string_agg(amount::text, ',' ORDER BY amount) FROM pgcolumnar.iceberg_scan('$MDIR/eqapply.metadata.json') AS t(amount int)")" \ "10,30,50" -check "backend still up after the equality applications" "$(q 'SELECT 1')" "1" +check "backend still up after the equality applications" "$(survived "$CRASHES_BEFORE")" "1" # ---- 4b: equality refusal and corruption arms ------------------------------- # content=2 with null equality_ids is corrupt metadata (the spec requires it) +CRASHES_BEFORE="$(crashes)" check "an equality delete without equality_ids is refused (XX001)" \ - "$(sqlstate_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/eqnoids.metadata.json') - AS t(id bigint, region text, amount int)")" "XX001" + "$(refusal "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/eqnoids.metadata.json') + AS t(id bigint, region text, amount int)" "has no equality_ids")" "XX001" # the pre-4b 'equality' fixture also lacks equality_ids, but its delete sits at # the data's own sequence number, so under the strict-< rule it can never apply # and is skipped before validation: the data reads untouched +await_up check "a never-applicable equality delete without equality_ids is skipped (5 rows)" \ "$(q "SELECT count(*) FROM pgcolumnar.iceberg_scan('$MDIR/equality.metadata.json') AS t(id bigint, region text, amount int)")" "5" @@ -209,13 +283,16 @@ o = json.load(open(sys.argv[1])) print(",".join(str(i) for i in o["part_surviving"][sys.argv[2]])) PY } -check "a partition-scoped equality delete applies only to its partition" \ +await_up +check_text "a partition-scoped equality delete applies only to its partition" \ "$(part_ids eqpart_apply)" "$(part_oracle eqpart_apply)" +await_up check "the us-partition rows survive an eu-scoped delete" \ "$(q "SELECT count(*) FROM pgcolumnar.iceberg_scan('$MDIR/eqpart_apply.metadata.json') AS t(id bigint, region text, grp int) WHERE region = 'us'")" "3" # a delete scoped to a partition (region=zz) that no data file is in applies to # nothing -- a legitimate no-op, not an error and not a global delete +await_up check "a delete scoped to an empty partition deletes nothing (5 rows)" \ "$(q "SELECT count(*) FROM pgcolumnar.iceberg_scan('$MDIR/eqpart_nomatch.metadata.json') AS t(id bigint, region text, grp int)")" "5" @@ -223,23 +300,24 @@ check "a delete scoped to an empty partition deletes nothing (5 rows)" \ # partition-scoped delete, would silently under-delete; refuse it (the mirror # of the delete-side has_spec_id check) check "a data file lacking partition_spec_id is refused under a partitioned delete (XX001)" \ - "$(sqlstate_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/eqpart_datanospec.metadata.json') - AS t(id bigint, region text, grp int)")" "XX001" + "$(refusal "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/eqpart_datanospec.metadata.json') + AS t(id bigint, region text, grp int)" "carries no partition_spec_id, which a partition-scoped")" "XX001" # a delete written under a partition spec no data file uses (partition # evolution): per the spec a partitioned delete never crosses spec ids, so it # applies to nothing -- a no-op, all rows survive, never a global over-delete +await_up check "a cross-spec partition-scoped delete applies to nothing (5 rows)" \ "$(q "SELECT count(*) FROM pgcolumnar.iceberg_scan('$MDIR/eqpart_crossspec.metadata.json') AS t(id bigint, region text, grp int)")" "5" # a partition tuple the reader cannot compare exactly (a double) is refused check "a partition-scoped delete with an uncomparable partition value is refused (0A000)" \ - "$(sqlstate_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/eqpart_incomparable.metadata.json') - AS t(id bigint, region text, grp int)")" "0A000" + "$(refusal "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/eqpart_incomparable.metadata.json') + AS t(id bigint, region text, grp int)" "has a partition tuple this reader cannot compare")" "0A000" # an equality column of a type with no supported mapping (timestamp) is refused # before any file is opened check "an equality delete on an unsupported column type is refused (0A000)" \ - "$(sqlstate_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/eqtype.metadata.json') - AS t(id bigint, region text, amount int)")" "0A000" + "$(refusal "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/eqtype.metadata.json') + AS t(id bigint, region text, amount int)" "is not supported")" "0A000" check "...and the refusal names the type" \ "$(errmsg_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/eqtype.metadata.json') AS t(id bigint, region text, amount int)" | grep -c "type")" "1" @@ -247,65 +325,69 @@ check "...and the refusal names the type" \ # data file errors loudly (the reader's missing-field-id error), never silently # skips the delete check "an equality column missing from the data file errors (22023)" \ - "$(sqlstate_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/eqmissing.metadata.json') - AS t(id bigint, region text, amount int)")" "22023" + "$(refusal "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/eqmissing.metadata.json') + AS t(id bigint, region text, amount int)" "no Parquet column has field id")" "22023" # a delete-file path outside the table root is stopped by the path boundary # (22023 from the boundary, BEFORE the file is opened; if the boundary were # gone, /etc/hostname would be read and fail differently) check "an equality-delete path outside the table location is refused (22023)" \ - "$(sqlstate_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/eqescape.metadata.json') - AS t(id bigint, region text, amount int)")" "22023" + "$(refusal "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/eqescape.metadata.json') + AS t(id bigint, region text, amount int)" "is not under the table location")" "22023" check "...and the boundary refusal names the escape" \ "$(errmsg_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/eqescape.metadata.json') AS t(id bigint, region text, amount int)" | grep -c "table location")" "1" # a path under the table root that climbs out with ".." is refused by the # containment (canonicalize collapses it and the re-check fails) check "a dotdot-escaping delete path is refused (22023)" \ - "$(sqlstate_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/eqdotdot.metadata.json') - AS t(id bigint, region text, amount int)")" "22023" -check "backend still up after the equality refusals" "$(q 'SELECT 1')" "1" + "$(refusal "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/eqdotdot.metadata.json') + AS t(id bigint, region text, amount int)" "escapes the table location")" "22023" +check "backend still up after the equality refusals" "$(survived "$CRASHES_BEFORE")" "1" # ---- 4b audit arms (multi-agent adversarial audit findings) ----------------- # an equality_ids value beyond int32 must refuse the manifest (XX001); a silent # 32-bit truncation would alias field 2^32+2 onto the real field 2 and delete # rows the manifest never named +CRASHES_BEFORE="$(crashes)" check "an equality_ids value beyond int32 refuses the manifest (XX001)" \ - "$(sqlstate_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/eqbigid.metadata.json') - AS t(id bigint, region text, amount int)")" "XX001" + "$(refusal "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/eqbigid.metadata.json') + AS t(id bigint, region text, amount int)" "corrupt Avro manifest entry")" "XX001" # a manifest list omitting partition_spec_id leaves an equality delete's scope # undecidable; defaulting it to 0 (unpartitioned) would defeat the # partition-scope guard, so the reader must refuse (XX001) check "a manifest list without partition_spec_id is refused for equality deletes (XX001)" \ - "$(sqlstate_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/eqnospec.metadata.json') - AS t(id bigint, region text, amount int)")" "XX001" + "$(refusal "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/eqnospec.metadata.json') + AS t(id bigint, region text, amount int)" "the manifest list carries no partition_spec_id")" "XX001" # a partition-specs entry matching the delete's spec id but lacking the # required "fields" key: treating it as unpartitioned would silently globalize # the delete, so it is corrupt metadata (XX001) check "a partition spec without its fields key is refused (XX001)" \ - "$(sqlstate_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/eqnofields.metadata.json') - AS t(id bigint, region text, amount int)")" "XX001" + "$(refusal "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/eqnofields.metadata.json') + AS t(id bigint, region text, amount int)" "has no fields array")" "XX001" # an equality delete that can never apply (its sequence number exceeds no data # file's) is skipped per spec, not validated: this variant's delete has an # unsupported column type (field 8, timestamp) at the data's own sequence # number, and must NOT refuse -- all five rows read +await_up check "a never-applicable equality delete is skipped, not validated (5 rows)" \ "$(q "SELECT count(*) FROM pgcolumnar.iceberg_scan('$MDIR/eqstaletype.metadata.json') AS t(id bigint, region text, amount int)")" "5" # a v1-shaped manifest (no sequence_number column in the entry schema) defaults # every file's sequence number to 0 per spec, even for EXISTING entries -- the # opposite of badseq, whose v2 schema HAS the column and carries a null +await_up check "a v1 manifest without a sequence_number column reads (default 0, 5 rows)" \ "$(q "SELECT count(*) FROM pgcolumnar.iceberg_scan('$MDIR/v1seq.metadata.json') AS t(id bigint, region text, amount int)")" "5" -check "backend still up after the audit arms" "$(q 'SELECT 1')" "1" +check "backend still up after the audit arms" "$(survived "$CRASHES_BEFORE")" "1" # ---- spec: inheritance is ADDED-only; a null seq on an EXISTING entry is bad -- # the spec inherits a null sequence number only for status-1 (ADDED) entries; an # EXISTING (status 0) entry with a null sequence number is corrupt, and silently # inheriting the too-new manifest number could keep rows a delete should remove. +CRASHES_BEFORE="$(crashes)" check "an EXISTING entry with a null sequence number is refused (XX001)" \ - "$(sqlstate_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/badseq.metadata.json') - AS t(id bigint, region text, amount int)")" "XX001" -check "backend still up after the bad-sequence refusal" "$(q 'SELECT 1')" "1" + "$(refusal "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/badseq.metadata.json') + AS t(id bigint, region text, amount int)" "has no sequence number")" "XX001" +check "backend still up after the bad-sequence refusal" "$(survived "$CRASHES_BEFORE")" "1" pgc_summary diff --git a/test/iceberg_dv.sh b/test/iceberg_dv.sh index d9df48ca8..5f8d80677 100755 --- a/test/iceberg_dv.sh +++ b/test/iceberg_dv.sh @@ -16,22 +16,71 @@ set -uo pipefail . "$(dirname "${BASH_SOURCE[0]}")/lib.sh" pgc_setup "${1:-/usr/local/pg17/bin/pg_config}" -sqlstate_of() { - env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postgres \ - -d "$PGC_DB" -qtA 2>&1 < the SQLSTATE when SQL is refused with PHRASE in its message; +# "SQLSTATE (another refusal: MESSAGE)" when a different guard refused it; "no +# error" when it ran; nothing when the server could not be asked. An arm used to +# read the SQLSTATE alone, and several guards here raise XX001: with the +# manifest/footer offset match removed, a later XX001 refused the same file and +# that arm still passed. +refusal() { + local out st msg + await_up + out="$(env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postgres \ + -d "$PGC_DB" -qtA 2>&1 <&1 < wait, at most 60 s, until the server answers. A refusal arm that kills a +# backend sends the server through crash recovery; without the wait, the next arms' +# readings fail on recovery and red as collateral, more or fewer of them by timing. +# The crash itself is still caught: survived() counts it. +await_up() { + local i + for i in $(seq 1 120); do + [ "$(q 'SELECT 1')" = 1 ] && return 0 + sleep 0.5 + done +} + +# Backends killed by a signal, from the server log: a number, or nothing when the +# log can't be read. A fresh connection answers again after crash recovery, so +# "SELECT 1" alone passed with a backend killed by a refusal arm (no +# referenced_data_file check: a NULL path reached strcmp); the survival checks +# below also require that this count has not moved. +crashes() { + local _n + _n="$(pgc_pg "grep -c 'terminated by signal' '$PGC_LOGFILE'" 2>/dev/null)" + case "$_n" in '' | *[!0-9]*) ;; *) echo "$_n" ;; esac +} +survived() { # survived BEFORE -> "1" when the server answers and no backend died since + local _up _now + _up="$(q 'SELECT 1')"; _now="$(crashes)" + if ! [[ "$1" =~ ^[0-9]+$ && "$_now" =~ ^[0-9]+$ ]]; then echo "no (crash count unreadable)" + elif [ "$_up" = 1 ] && [ "$_now" = "$1" ]; then echo 1 + else echo "no ([$_up], $(( _now - $1 )) backend crashes)"; fi +} + FX="$(dirname "${BASH_SOURCE[0]}")/fixtures/iceberg" WHD="$FX/warehouse_del" [ -f "$WHD/db/t/metadata/dvapply.metadata.json" ] || pgc_skip fixture "deletion-vector fixtures are missing" @@ -60,100 +109,103 @@ dv_ids_of() { # ---- deletion vectors are applied ------------------------------------------- # THE <= boundary: a DV at the data file's own sequence number applies (same # rule as 4a position-delete files; a strict > would wrongly exclude it) -check "a deletion vector at an equal sequence number applies (survivors == oracle)" \ +CRASHES_BEFORE="$(crashes)" +check_text "a deletion vector at an equal sequence number applies (survivors == oracle)" \ "$(dv_ids_of dvapply)" "$(dv_oracle dvapply)" # the other side: a DV strictly older than the data applies to nothing -check "a deletion vector older than the data does not apply" \ +check_text "a deletion vector older than the data does not apply" \ "$(dv_ids_of dvnoapply)" "$(dv_oracle dvnoapply)" # the spec's supersede rule: a position-delete FILE applies only when no DV # must be applied to the data file. The fixture's DV drops ordinal 1 and its # (also applicable) Parquet posdel names ordinals 1 and 3; a UNION would drop # both, supersede drops only ordinal 1. -check "an applicable DV supersedes position-delete files for its data file" \ +check_text "an applicable DV supersedes position-delete files for its data file" \ "$(dv_ids_of dvsupersede)" "$(dv_oracle dvsupersede)" # supersede is per data file: a posdel targeting a DIFFERENT data file still # applies alongside the DV -check "supersede is per data file, not global" \ +check_text "supersede is per data file, not global" \ "$(dv_ids_of dvother)" "$(dv_oracle dvother)" # roaring coverage: run-form containers plus a second 64-bit bucket -check "a DV with run containers and a high-bucket position decodes" \ +check_text "a DV with run containers and a high-bucket position decodes" \ "$(dv_ids_of dvwide)" "$(dv_oracle dvwide)" # cookie 12347 (run containers marked in the run bitset) -check "a run-optimized DV (cookie 12347) decodes" \ +check_text "a run-optimized DV (cookie 12347) decodes" \ "$(dv_ids_of dvrun)" "$(dv_oracle dvrun)" # a bitset container (cardinality above 4096) -check "a bitset-container DV decodes" \ +check_text "a bitset-container DV decodes" \ "$(dv_ids_of dvbitset)" "$(dv_oracle dvbitset)" # one Puffin file carrying two DV blobs for two data files -check "two DV blobs in one Puffin file each apply to their own data file" \ +check_text "two DV blobs in one Puffin file each apply to their own data file" \ "$(dv_ids_of dvtwo)" "$(dv_oracle dvtwo)" -check "backend still up after the DV applications" "$(q 'SELECT 1')" "1" +check "backend still up after the DV applications" "$(survived "$CRASHES_BEFORE")" "1" # ---- refusal and corruption arms -------------------------------------------- +CRASHES_BEFORE="$(crashes)" # two DV entries referencing one data file: the spec says scan results are # undefined and readers may raise; we raise rather than guess check "two DV entries for one data file are refused (XX001)" \ - "$(sqlstate_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/dvdup.metadata.json') - AS t(id bigint, region text, amount int)")" "XX001" + "$(refusal "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/dvdup.metadata.json') + AS t(id bigint, region text, amount int)" "two deletion vectors reference data file")" "XX001" check "a corrupted DV CRC is refused (XX001)" \ - "$(sqlstate_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/dvbadcrc.metadata.json') - AS t(id bigint, region text, amount int)")" "XX001" + "$(refusal "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/dvbadcrc.metadata.json') + AS t(id bigint, region text, amount int)" "fails its CRC-32 check")" "XX001" check "a wrong DV blob magic is refused (XX001)" \ - "$(sqlstate_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/dvbadmagic.metadata.json') - AS t(id bigint, region text, amount int)")" "XX001" + "$(refusal "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/dvbadmagic.metadata.json') + AS t(id bigint, region text, amount int)" "has a bad magic sequence")" "XX001" # the manifest's content_offset must exactly match the footer's blob offset check "a manifest/footer offset mismatch is refused (XX001)" \ - "$(sqlstate_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/dvoffmismatch.metadata.json') - AS t(id bigint, region text, amount int)")" "XX001" + "$(refusal "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/dvoffmismatch.metadata.json') + AS t(id bigint, region text, amount int)" "has no blob matching the manifest's offset")" "XX001" # referenced_data_file is required for DVs check "a DV entry without referenced_data_file is refused (XX001)" \ - "$(sqlstate_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/dvnoref.metadata.json') - AS t(id bigint, region text, amount int)")" "XX001" + "$(refusal "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/dvnoref.metadata.json') + AS t(id bigint, region text, amount int)" "has no referenced_data_file")" "XX001" # record_count is defined as the DV's cardinality check "a record_count that disagrees with the DV cardinality is refused (XX001)" \ - "$(sqlstate_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/dvbadcount.metadata.json') - AS t(id bigint, region text, amount int)")" "XX001" + "$(refusal "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/dvbadcount.metadata.json') + AS t(id bigint, region text, amount int)" "positions but its manifest entry records")" "XX001" # deletion-vector-v1 blobs must not declare a compression codec check "a DV blob declaring a compression codec is refused (XX001)" \ - "$(sqlstate_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/dvcompressed.metadata.json') - AS t(id bigint, region text, amount int)")" "XX001" + "$(refusal "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/dvcompressed.metadata.json') + AS t(id bigint, region text, amount int)" "declares a compression codec")" "XX001" # a compressed Puffin footer is refused loudly, never misparsed check "a compressed Puffin footer is refused (0A000)" \ - "$(sqlstate_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/dvflags.metadata.json') - AS t(id bigint, region text, amount int)")" "0A000" + "$(refusal "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/dvflags.metadata.json') + AS t(id bigint, region text, amount int)" "has a compressed Puffin footer")" "0A000" check "...and the refusal names the compressed footer" \ "$(errmsg_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/dvflags.metadata.json') AS t(id bigint, region text, amount int)" | grep -c "compressed")" "1" # deletion vectors are v3-only check "a deletion vector in a format-version 2 table is refused (0A000)" \ - "$(sqlstate_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/dvv2.metadata.json') - AS t(id bigint, region text, amount int)")" "0A000" + "$(refusal "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/dvv2.metadata.json') + AS t(id bigint, region text, amount int)" "is a deletion vector, which format-version")" "0A000" check "...and the refusal names the format version" \ "$(errmsg_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/dvv2.metadata.json') AS t(id bigint, region text, amount int)" | grep -c "format-version")" "1" # a Puffin path outside the table root is stopped by the path boundary check "a Puffin path outside the table location is refused (22023)" \ - "$(sqlstate_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/dvescape.metadata.json') - AS t(id bigint, region text, amount int)")" "22023" -check "backend still up after the DV refusals" "$(q 'SELECT 1')" "1" + "$(refusal "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/dvescape.metadata.json') + AS t(id bigint, region text, amount int)" "is not under the table location")" "22023" +check "backend still up after the DV refusals" "$(survived "$CRASHES_BEFORE")" "1" # ---- audit arms (multi-agent adversarial audit findings) -------------------- +CRASHES_BEFORE="$(crashes)" # content_offset + content_size chosen to overflow int64: a bounds check that # adds the operands would wrap negative and read a wild pointer; testing each # operand against the file size refuses it cleanly check "a DV whose offset+size overflows int64 is refused, not read (XX001)" \ - "$(sqlstate_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/dvbigoff.metadata.json') - AS t(id bigint, region text, amount int)")" "XX001" + "$(refusal "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/dvbigoff.metadata.json') + AS t(id bigint, region text, amount int)" "lies outside the file's blob region")" "XX001" # a run container whose start+length exceeds the 16-bit container range would # fabricate positions in a neighboring container; refuse rather than corrupt check "a run container overflowing its 16-bit range is refused (XX001)" \ - "$(sqlstate_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/dvrunoverflow.metadata.json') - AS t(id bigint, region text, amount int)")" "XX001" + "$(refusal "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/dvrunoverflow.metadata.json') + AS t(id bigint, region text, amount int)" "overflows its 16-bit range")" "XX001" # two DV entries for one data file recorded with and without the file:// scheme: # the one-DV-per-file check strips the scheme, so the aliased pair is caught check "an aliased duplicate DV (scheme vs no scheme) is refused (XX001)" \ - "$(sqlstate_of "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/dvdupscheme.metadata.json') - AS t(id bigint, region text, amount int)")" "XX001" -check "backend still up after the audit arms" "$(q 'SELECT 1')" "1" + "$(refusal "SELECT * FROM pgcolumnar.iceberg_scan('$MDIR/dvdupscheme.metadata.json') + AS t(id bigint, region text, amount int)" "two deletion vectors reference data file")" "XX001" +check "backend still up after the audit arms" "$(survived "$CRASHES_BEFORE")" "1" pgc_summary diff --git a/test/iceberg_rest.sh b/test/iceberg_rest.sh index 8383f5d34..a97ac65b2 100755 --- a/test/iceberg_rest.sh +++ b/test/iceberg_rest.sh @@ -41,12 +41,42 @@ rest_teardown() { } trap rest_teardown EXIT INT TERM -sqlstate_of() { - env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postgres \ - -d "$PGC_DB" -qtA 2>&1 < "