Port sorted_pathkeys to pytest, all 110 names (#432) - #1067
Conversation
`test/sorted_pathkeys.sh` pins one decision: when a columnar scan may hand the
planner PATHKEYS, the promise that its rows already arrive in a stated order. The
planner then drops the Sort above the scan and nothing downstream re-checks, so a
wrong promise is not a slow plan, it is WRONG ROWS.
The port keeps the original's three shapes rather than reorganising by feature: a
CLAIM arm (the Sort goes), a REFUSAL arm (something made the claim untrue and the
Sort comes back), and an ANSWER arm (the rows themselves, against a heap table
built from the same data). The ANSWER arm is not a duplicate of its CLAIM arm --
dropping the Sort is only correct if the rows arrive sorted anyway, and a plan
check alone cannot say whether they did.
literal matches: 110 | template matches: 0 | missing: 0
VERDICT: every bash property is covered
ONE ARM NEEDED A CLUSTER SETTING, NOT A WORKAROUND. `pgcolumnar.parallel_copy`
prepares one transaction per worker, and `max_prepared_transactions` cannot be
raised without restarting the postmaster. The default is 0, so asking for fewer
workers does not help: any number is one too many. `pgc_cluster` now sets it where
it writes `postgresql.conf`, at the value `lib.sh` gives this suite through
`PGC_EXTRA_CONF`.
Refusing that arm with `expect.cannot_run` was written first and then measured and
rejected, for two reasons. It loses three of the bash suite's names outright,
because `cannot_run` records under the REASON CODE rather than under a name (commandprompt#1040
phase 0b). And it turns the `pytest (cluster tests)` job RED: an unrunnable check
exits 67, the job runs pytest under `set -euo pipefail`, and no file in that half
had ever produced one. Measured: that leg exits 0 today with zero unrun, so this
file would have been the first to break it. The arm now asserts `pg_prepared_xacts`
is empty afterwards, because a leaked prepared transaction holds its locks and the
cluster is session-scoped -- it would not fail this test, it would wedge every file
after it.
TWO DOCS DEFECTS FOUND AND FIXED HERE, both of them mine.
`## 37. test_iceberg_fdw.py` reached main sitting directly above `## 38.`, with the
Iceberg body attached to the userinfo heading: my commandprompt#1066 merge put the new section
into the gap between another heading and its body. commandprompt#1066's own arm cannot see it,
because it asks whether each file is NAMED by a numbered heading and a heading with
no body is still a heading. `test_docs_cover_the_corpus.py` now refuses a numbered
section with no body, with a planted control, and the section order matches the
bodies.
`compare_to_bash.py`'s corpus arm called a WRAPPED name fabricated. A name too long
for one line is written as adjacent literals and Python joins them at parse time, so
the joined name is text the file contains but not text `in src` can find. The arm
exists to catch a reader that CONSTRUCTS a name, so it now collapses the file's own
concatenation and keeps exactly that guarantee: an f-string name still yields a `{}`
template, which the collapse does not rescue. Both directions are asserted.
VERIFIED, on pg16a in the audit container:
guard leg 342 collected, 878 checks, 0 fail rc=0
cluster leg 373 collected, 1025 checks, 0 fail, 0 unrun rc=0
grader 110 literal, 0 missing
Both counts re-derived BY COLLECTION on this tree, never by adding a delta.
MUTATION PROOF, each asserting it applied before it was believed:
M1 collation refusal removed -> 3 arms red, naming the collation properties
M2 enable_sorted_pathkeys GUC ignored -> 2 arms red, the off switch and a
planning-buffers control
restored, source byte-identical -> 127 checks, 48 passed, 0 fail
Closes nothing on its own; advances commandprompt#432.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012RSw4qMHS7ByE7PY8Ns4cs
linuxhikerpm
left a comment
There was a problem hiding this comment.
The port is one-for-one (110 literal names), and the two extras are real defects the coverage arms could not see: a numbered TESTS.md heading with no body, and a wrapped assertion name that in src called fabricated. Raising max_prepared_transactions in pgc_cluster matches what lib.sh already does via PGC_EXTRA_CONF; refusing the arm would have dropped three names and exited 67 under the cluster job. CI is green. Not merging.
commandprompt#1067 already records this, in the section about `sorted_pathkeys`. That is the wrong place for it: somebody about to write an unrunnable arm reads the assertion vocabulary, not a port's section forty pages down. Moved to "The third state, and the hole it left", immediately after the paragraph about `cannot_run` being the layer's largest hole, because it is the next thing true about that hatch: - a run with one unrunnable check exits 67 (EXIT_INCOMPLETE) - `pytest (cluster tests)` runs pytest bare under `set -euo pipefail` - so the job goes red on a check that did exactly what it should - measured: that leg exits 0 with 0 unrun today, so nothing absorbs it and the next such arm is the first With the order to try: remove the precondition if the harness controls it; weigh that refusing loses every bash name the arm would carry (`cannot_run` records under the REASON CODE, commandprompt#1040 phase 0b); only then refuse, and say so in the PR. Raised by @jdatcmd's other session, whose point was that whoever writes the next legitimately-unrunnable cluster arm inherits this and it was not written down anywhere they would look. Cross-referenced to commandprompt#1015, which is the same shape one level out: a truthful "could not evaluate" sharing a channel with "something is wrong". No code change. Docs only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012RSw4qMHS7ByE7PY8Ns4cs
|
Follow-up |
|
Adversarial review of the stack, since #1069 sits on this one. Verified independently: The hazard is in how this merges, not in the code.
Stacking it deliberately for deterministic TESTS.md numbering is the right call — both PRs would otherwise claim section 40, which is exactly #1024's collision. Worth noting that #1024's arm now catches that, so the stack is belt-and-braces rather than the only protection. One thing I could not check without a build and am relying on you for: the mutation proofs (the #875 latch in both directions, the planner arms with |
Self-review found it, and the stronger instrument then found a second thing I
had not looked for.
WHAT IT WAS. Eight ANSWER arms compared the columnar and heap results as
expect.text("same" if columnar == heap else "DIFFERENT", "same", name)
which is a hash comparison wearing a Python spelling: a mismatch says "these
two strings differ" and nothing about WHICH row. `expect.ordered_rows` exists
for exactly this, names the position, and is what the layer's own docstring
says to use -- "a mismatch names the position, where a hash mismatch only says
two hashes differ".
WHAT IT THEN CAUGHT. `ordered_rows` refuses a sequence whose elements are all
identical, because the reverse reads the same and an ordering claim about it
cannot fail. Switching the eight over immediately reddened one:
and the first row matches heap: order cannot be observed in these
sequences. Every element is the same, so the reverse ordering is
identical and the claim asserts nothing beyond what rows() already asserts.
That arm is `... ORDER BY k LIMIT 1`. One row. It was decorative -- it sat in
the half of the suite I defended in the PR body as the half that catches a
wrong ordering claim, and it asserted nothing about ordering.
It is a VALUE claim, not an ordering one: the minimum under the ordering, which
is worth asserting and is caught by `rows`. So ANSWERS now carries a third
field saying whether a template can carry an ordering claim, DECLARED rather
than sniffed from the data at runtime -- deciding per call by looking at the
rows is how an ordering claim silently becomes a value one, which is the
failure the two instruments exist to keep apart.
Parity and counts unchanged, re-verified rather than assumed:
grader 110 literal | 0 template | 0 missing
file 127 checks, 48 passed, 0 fail
(the parametrize arity changed, and the class-3 name reader still reads
all eight names, which is the thing that arity change could have broken)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012RSw4qMHS7ByE7PY8Ns4cs
`test/projections.sh` pins the multiple-projections DDL, catalog and read path. A
projection is a second copy of some columns, and every property is about the copy
staying honest: it holds the rows the base holds, it loses the rows the base loses,
it survives a vacuum that renumbers every row underneath it, and the planner reads
it only when it can answer the whole query from it. A wrong projection is a WRONG
ANSWER rather than a slow one, because nothing downstream re-checks.
literal matches: 75 | template matches: 0 | missing: 0
VERDICT: every bash property is covered
THE PORT IS STRICTLY STRONGER IN ONE PLACE, and a reader comparing the two should
know which way. The original's `expect_fail` runs the statement and passes when it
errors AT ALL, so a misspelt table name satisfies every one of its eight refusal
arms. The port asserts the SQLSTATE, and every code was MEASURED against this build
rather than guessed:
duplicate name 42710 add on heap table 42809
unknown column 42703 drop base 22023
empty columns 22023 drop unknown 42704
duplicate column 42701 read_projection base 42704
sort key not in columns 22023
The names are the bash suite's; the assertions are not.
Three further mechanism changes assert the same property by a stronger means. The
EXPLAIN grep becomes a typed JSON field, reading the projection NAME rather than its
presence -- `grep -c 'Columnar Projection: pc'` is a substring test, the plan carries
the value. `pgc_set_hash` becomes `expect.row_set`, order-blind by declaration rather
than by construction. The second MVCC session becomes a second connection rather than
a background psql on a fifo polled 200 times at 0.1s, which removes the wait rather
than shortening it; the original's two arms that exist only to name that timeout are
carried as the positive facts they are the negative of.
Arrays are cast `::text` in SQL. psycopg returns a PostgreSQL array as a python list,
so `{1,2,3}` arrives as `[1, 2, 3]` and a transcribed comparison fails for a reason
that has nothing to do with projections.
VERIFIED, on pg16a in the audit container, both legs as CI runs them:
guard leg 342 collected, 0 fail rc=0
cluster leg 406 collected, 0 fail, 0 unrun rc=0
grader 75 literal, 0 missing
Both counts re-derived BY COLLECTION on this tree, never by adding a delta.
MUTATION PROOF, each asserting it applied before its result was believed:
M1 the commandprompt#875 projection-writer reset removed
-> mid-transaction add: got 105 want 116, which is the defect's own
signature (the back-fill plus the pre-add write, missing the 11 after)
-> mid-transaction drop: orphan projection storage, got 1 want 0
M2 the planner never chooses a projection
-> covering query: got None want 'pc'
-> after vacuum: got None want 'pvp'
which is what proves the port reads the NAME, not the presence
restored, both sources byte-identical -> 75 checks, 33 passed, 0 fail
STACKED ON commandprompt#1067 so the TESTS.md section numbering is deterministic: that PR takes
section 40 and this takes 41. Based on main both would claim 40 and one would have to
renumber whichever merged second.
Advances commandprompt#432.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012RSw4qMHS7ByE7PY8Ns4cs
Same class as the ANSWER-half fix on commandprompt#1067, found by the same self-review. expect.text(str(_rows(...)), str(_rows(...)), "aggregate over projection scan matches oracle") Comparing the repr of two lists reports "these two strings differ" and nothing about which row. `expect.rows` names the differing row, which is the whole reason this layer exists rather than comparing hashes. The bash original compares two psql outputs as text, so this is the port being stronger rather than different -- the same move the SQLSTATE arms make. Re-verified rather than assumed: grader 75 literal | 0 template | 0 missing file 75 checks, 33 passed, 0 fail Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012RSw4qMHS7ByE7PY8Ns4cs
|
Adversarial self-review, at jd's request. I found a real defect in this PR and have fixed it in I do not approve my own PRs; this is a finding, not a sign-off. The ANSWER half asserted order with the wrong instrumentEight arms compared the columnar and heap results like this: expect.text("same" if columnar == heap else "DIFFERENT", "same", name)That is a hash comparison wearing a Python spelling. A failure says these two strings differ and nothing about which row — which is precisely what this layer's The grader cannot see this. It compares names, and all 110 names were present and correct. A port can carry every name and assert something weaker than its twin, and nothing in the parity tooling looks at strength — which is the argument for reading a port rather than trusting its verdict. Switching to
|
linuxhikerpm
left a comment
There was a problem hiding this comment.
Re-review on HEAD ef2dc712 (previous approval was 8b75f849). The two follow-ups belong: exit-67 is now documented next to cannot_run, and the ANSWER half uses ordered_rows with a declared third field so LIMIT 1 is a value claim (expect.rows) rather than a decorative ordering claim. That is the instrument the layer's own docstring asks for, and it caught a real no-op.
110 literal names, pgc_cluster sets max_prepared_transactions=8 to match lib.sh, empty-section and wrapped-name arms are real defects the old coverage could not see. CI 14/14 green. Not merging.
CHANGELOG only, and the two entries are independent -- kept both. Counts re-derived BY COLLECTION on the merged tree rather than assumed unchanged, because commandprompt#1070 touched test/ and a count that happens to still be right is not the same as a count that was checked: guard 342 collected (tracked 342) cluster 373 collected (tracked 373) Both legs on the merged tree, pg16a: guard leg 342 collected, 879 checks, 0 fail rc=0 cluster leg 373 collected, 1025 checks, 0 fail rc=0 This branch adds no bash checks, so commandprompt#1070's newly-armed orphan-scan has nothing to say about it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012RSw4qMHS7ByE7PY8Ns4cs
linuxhikerpm
left a comment
There was a problem hiding this comment.
Re-reviewed HEAD ef2dc712 after #1070 landed on main.
The port is still the right one: CLAIM/REFUSAL/ANSWER are paired, the LIMIT-1 arm uses expect.rows rather than a vacuous ordered_rows on a single identical row, and max_prepared_transactions is set in pgc_cluster so the parallel_copy names stay runnable. CI is green.
This PR now conflicts with main (CHANGELOG / TESTS.md / expected_tests from #1070). That is mechanical, not a product defect. Rebase before it can land; keep landing it before #1069.
No conflicts. Counts re-derived BY COLLECTION on the merged tree:
guard 342 collected (tracked 342)
cluster 406 collected (tracked 406)
Both legs, pg16a:
guard leg 342 collected, 880 checks, 0 fail rc=0
cluster leg 406 collected, 1100 checks, 0 fail rc=0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012RSw4qMHS7ByE7PY8Ns4cs
linuxhikerpm
left a comment
There was a problem hiding this comment.
Re-reviewed HEAD 47a78e6d (merge of #1070). Same port as before: CLAIM/REFUSAL/ANSWER pairing, LIMIT-1 uses expect.rows, max_prepared_transactions in pgc_cluster. cluster_tests 373 is a collection re-derive, not a delta on 325. CI is green on the jobs that have finished.
test/sorted_pathkeys.shpins one decision: when a columnar scan may hand the plannerPATHKEYS, the promise that its rows already arrive in a stated order. The planner then
drops the Sort above the scan and nothing downstream re-checks, so a wrong promise is
not a slow plan, it is wrong rows.
The port keeps the original's three shapes rather than reorganising by feature:
collation change) and the Sort must come back.
An ANSWER arm is not a duplicate of its CLAIM arm. Dropping the Sort is only correct if
the rows arrive sorted anyway, and a plan check alone cannot say whether they did.
One arm needed a cluster setting, not a workaround
pgcolumnar.parallel_copyprepares one transaction per worker, andmax_prepared_transactionscannot be raised without restarting the postmaster. Thedefault is 0, so asking for fewer workers does not help: any number is one too many.
pgc_clusternow sets it where it writespostgresql.conf, at the valuelib.shgives this suite through
PGC_EXTRA_CONF.I wrote the refusal first —
expect.cannot_runwith the reason, and the suite declaredin
INCOMPLETE— then measured it and rejected it, for two reasons:cannot_runrecords under theREASON CODE rather than under a name (compare_to_bash.py reads 5 of lib.sh's 8 check helpers, hiding 2 unported properties in hilbert_locality (#432) #1040 phase 0b), so those names cannot be
emitted at all.
pytest (cluster tests)red. An unrunnable check exits 67, that job runspytest under
set -euo pipefail, and no file in that half had ever produced one.Measured, with my file removed: the leg exits 0 today with 0 unrun. This file would
have been the first to break it.
The arm now asserts
pg_prepared_xactsis empty afterwards. A leaked preparedtransaction holds its locks until someone resolves it and the cluster is session-scoped,
so a leak would not fail this test — it would wedge every file that runs after it.
Two docs defects found and fixed here, both mine
A numbered section with no body.
## 37. test_iceberg_fdw.pyreachedmainsittingdirectly above
## 38., with the Iceberg body attached to the userinfo heading — my#1066 merge put the new section into the gap between another heading and its body.
#1066's own arm cannot see this: it asks whether each file is NAMED by a numbered
heading, and a heading with no body is still a heading.
test_docs_cover_the_corpus.pynow refuses a numbered section with no body, with a planted control, and the section
order matches the bodies again.
A wrapped name called fabricated.
compare_to_bash.py's corpus arm asserts everyname its loop reader returns is text the file contains. A name too long for one line is
written as adjacent literals and Python joins them at parse time, so a joined name is
text the file contains but not text
in srccan find — this port has two. The arm existsto catch a reader that CONSTRUCTS a name, so it now collapses the file's own
concatenation and keeps exactly that guarantee. Both directions are asserted: collapsing
rescues a split literal, and an f-string name still yields a
{}template, which thecollapse does not rescue.
Verified
On
/usr/local/pg16ain the audit container, both legs exactly as CI runs them:Both counts in
expected_tests.txtre-derived by collection on this tree, never byadding a delta.
Mutation proof
Each mutation asserts it applied before its result is believed — a silent no-match
mutation is green and proves nothing.
enable_sorted_pathkeysignoredAdvances #432. I do not merge this.
🤖 Generated with Claude Code
https://claude.ai/code/session_012RSw4qMHS7ByE7PY8Ns4cs