Skip to content

Vendors the conformance corpus and runs every case under pg_regress - #8

Merged
johnnyt merged 1 commit into
mainfrom
uxpg-bem-vendor-corpus-runner
Oct 25, 2026
Merged

johnnyt merged 1 commit into
mainfrom
uxpg-bem-vendor-corpus-runner

Conversation

@johnnyt

@johnnyt johnnyt commented Oct 25, 2026

Copy link
Copy Markdown
Member

Bead: uxpg-bem

What this changes

  • The corpus, vendored. conformance/ holds uxid-ex's conformance corpus at the tag v2.10.0 (commit 57235777cb29c70f751552ce7aa20ab5fe71fb74): the five case files, manifest.json, the six files under schema/ and the corpus README.md, each read from the tag's objects and compared with cmp against git show v2.10.0:conformance/<path> (all equal, and each blob id equals the tag's). The generator source under the tag's conformance/source/ is not vendored: a port does not need it to run the cases. conformance/SOURCE.json records repo, tag, sha and corpus_hash. .gitattributes marks conformance/** as -text, so no checkout converts line endings under the hash.

  • The hash and count check. scripts/corpus-check.sh is POSIX sh using only POSIX utilities (no jq: the manifest is one line of sorted-key JSON, read with sed and tr; SHA-256 from sha256sum, shasum or openssl, whichever is installed). It checks that SOURCE.json has exactly its four keys and names the repo, tag and commit the script expects; that the manifest is corpus version 1 with SOURCE.json's hash and entries of exactly case_count, file and kind; and that each case file is case_count lines, each ending in a newline, none empty, each with a case id starting with its kind. It then recomputes corpus_hash over the case files' bytes in the manifest's order. Any mismatch exits non-zero, and nothing rewrites a hash or a count. The gate's corpus step now runs it under sh with no fallback for a missing script, and CI's gate job runs the gate.

  • The runner. test/sql/conformance.sql (added to REGRESS) loads each file with psql's \copy into a staging table as CSV, with quote byte 0x01 and delimiter byte 0x02. No case file contains either byte, so each line arrives whole as one text value, with client_encoding set to UTF-8 because some lines carry non-ASCII bytes. It checks the loaded lines hash back to corpus_hash and that each file's line count matches its case_count. A runner defined only in the test (pg_temp functions; the extension script is unchanged) then runs every case, one output row per case in manifest order, with no skip list:

    • encode through uxid_encode with the case's fixed rand;
    • deterministic through uxid_from;
    • decode through uxid_decode_at with the clock injected at the window's first millisecond, at 1700000000000 and at its last millisecond (the corpus README's window), plus uxid_decode_error returning NULL;
    • invalid decode through uxid_decode_at (SQLSTATE 22P02, message exactly uxid: <reason>) and uxid_decode_error; invalid generate through uxid_check_request (22023, uxid: <name>: ...), an absent option passed as NULL, its default;
    • monotonic through uxid_monotonic_step with initial_seed, steps and overflow_seed handed in as values (the overflow seed at most once, and checked as drawn exactly when the case carries one), each id through uxid_encode, lower case.

    A case carrying a key the runner does not know (at the top, in its input, in its expected answer) fails with that key named. A JSON null prefix reaches the functions as SQL NULL, which they read as no prefix. The output prints 96 rows (35 encode, 12 deterministic, 18 decode, 25 invalid, 6 monotonic), every one pass, then a tally row. A last query runs the runner on crafted lines that are not the corpus's, so the expected output pins the reason on a failing row: an unknown key in each of five places, a case id of another kind, a line that is not JSON, a wrong answer, a refusal of another name, and a null prefix (which passes).

Paths

The \copy paths are relative: make installcheck runs pg_regress, and so psql, at the top of the tree, and the test does \cd conformance before the loads and \cd .. after. That holds in the local gate's container copy of the tree, and in CI's installcheck jobs in the pgxn/pgxn-tools container, which run make installcheck in the checkout. The loads use COPY's older option spelling (CSV QUOTE E'\x01' DELIMITER E'\x02'), which keeps each \copy line inside the lint's line width. psql does not interpolate variables in a \copy line, so the line cannot be shortened that way.

Evidence

  • Full gate (mise run gate) green on PostgreSQL 18.6: 17 of 17 regression tests pass, sqlfluff lint clean, the corpus check prints corpus: v2.10.0 (57235777cb29c70f751552ce7aa20ab5fe71fb74), 5 case files, 96 cases, sha256:4f7572c8c1a60134b2955f02b34c82a839d0c3ab95d4218cc2df5a85eeb2bebb. The commit's tree is byte-identical to the tree that gate ran green on, so it was committed without a second gate run.
  • The script passes sh -n and runs clean under macOS /bin/sh, under /bin/dash, and under the gate image's /bin/sh (dash). Run on a copy of the corpus with one change at a time, it exits 1 naming the fault for each of: a trailing byte, one changed case byte, a wrong case_count, a wrong sha in SOURCE.json, an extra SOURCE.json key, an extra manifest entry key, a missing case file, corpus_version 2, and both hashes rewritten to the same wrong value.
  • Sabotage, each noted beside its query as a -- sabotage: comment. Every mutation turned the conformance test red on an assertion and was restored byte-equal: the deterministic marker (12 deterministic rows fail), the compact time's window step (decode/s-compact-guard-last-valid fails), the decode refusal's SQLSTATE (16 decode-refusal rows fail), the monotonic step rule's <= (monotonic/xs-compact-overflow-clock-back fails), the CSV quote byte on one load (the hash check reads f and 35 rows fail), the runner's unknown-key check (the probe reasons go empty), and the last decode clock moved one past the window (decode/s-compact-guard-last-valid fails).

Earlier tests

The per-function regression files that carry corpus cases by hand (encode, from, decode, decode_refusals, check_request, monotonic_step) stay. They pin each function's behaviour with their own sabotage notes. The runner now runs every corpus case as well, so as corpus coverage they overlap; whether any should go is left to review. Two comments that said "until the corpus runner runs them" now name conformance.sql.

Provenance

  • Beyond the corpus, the script and the test: .gitattributes (conformance/** -text), the gate's corpus step in mise.toml (runs under sh, no fallback), Makefile (REGRESS), the two test comments above with their expected output, and CLAUDE.md, whose sentences about the corpus now describe the tree as it is.
  • No changelog fragment: changelog.d/README.md says tests and gate changes take none.

The corpus, not the reference's code, is the arbiter of conformance,
so the case files, manifest.json, the schemas and the corpus README
are copied byte for byte from uxid-ex at v2.10.0, with the repo, tag,
commit and corpus_hash in conformance/SOURCE.json. The generator
source under the tag's conformance/source/ is not vendored: a port
does not need it to run the cases. The corpus is marked -text so no
checkout converts its line endings.

scripts/corpus-check.sh, POSIX sh with no jq, recomputes corpus_hash
over the case files in the manifest's order and counts each file's
lines against its case_count; the gate now runs it under sh.

The conformance regression test loads each file with psql's \copy as
CSV with quote and delimiter bytes no case contains, checks the
loaded lines hash to corpus_hash, and runs all 96 cases through a
runner defined in the test, one row per case in manifest order.

Refs: uxpg-bem
@johnnyt
johnnyt merged commit 9cc15db into main Oct 25, 2026
10 checks passed
@johnnyt
johnnyt deleted the uxpg-bem-vendor-corpus-runner branch October 25, 2026 17:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant