Skip to content

test/pytest: a write that wrote no rows is not a fixture (#432) - #935

Merged
jdatcmd merged 3 commits into
commandprompt:mainfrom
OffgridwithJD:audit/432-a-write-must-have-written
Sep 10, 2026
Merged

test/pytest: a write that wrote no rows is not a fixture (#432)#935
jdatcmd merged 3 commits into
commandprompt:mainfrom
OffgridwithJD:audit/432-a-write-must-have-written

Conversation

@OffgridwithJD

Copy link
Copy Markdown
Collaborator

INSERT ... SELECT ... WHERE false writes nothing and raises nothing. psycopg reports INSERT 0 0 with a rowcount of 0, and nothing in the corpus read either field — so the fixture a test meant to build did not exist, and every assertion below it compared two empty things.

That is insert-wrote-no-rows, entry 2 on VACUITY_MODES.md's own list of what to add next.

Measured before building: 12 write statements across 5 files, and rowcount read at exactly one site, which does not assert on it.

The command tag decides, not the row count

SELECT 0 and INSERT 0 0 both carry rowcount == 0. A guard keyed on the count alone would refuse every test whose last statement was a SELECT over an empty result — a legitimate and common assertion. statusmessage is the server's own command tag, so this guard never parses SQL. Measured on PG 18 against a pgcolumnar table:

statusmessage       rowcount   statement
CREATE TABLE              -1   CREATE TABLE t (i int) USING pgcolumnar
INSERT 0 5                 5   INSERT INTO t SELECT g FROM generate_series(1,5) g
INSERT 0 0                 0   INSERT ... WHERE false
UPDATE 0                   0   UPDATE t SET i = i WHERE i > 100
DELETE 0                   0   DELETE FROM t WHERE i > 100
SELECT 0                   0   SELECT * FROM t WHERE false
SET                       -1   SET search_path TO public
TRUNCATE TABLE            -1   TRUNCATE t

A deliberate zero stays writable

A DELETE that must match nothing is a real negative control, so expect.wrote(cur, 0, name) both compares the count and marks the write as named. An unnamed zero fails the test; naming a count does not excuse a wrong one; and a rowcount of -1 is refused rather than compared, for the reason expect.rowcount already records.

The refusal runs in the call phase, not a teardown fixture — #931 measured that a teardown guard reports the test it guards as PASSED and fails separately.

Two properties, two files, and the split is measured

The classifier and the refusal live in test_writes_wrote_rows.py, which needs no database: a stub cursor carrying the two measured fields exercises them exactly.

Whether the connection the tests actually use is watched is a different claim, and no driver-free arm can make it. test_the_connection_the_tests_use_is_watched makes it through a real INSERT ... WHERE false, on both conn.execute and a cursor the connection handed out — 24 and 42 sites in the corpus respectively, so a proxy watching only the connection would leave most of it unwatched.

Unwiring the conftest proxy and changing nothing else leaves the driver-free file at 10 passed and reds the wiring arm alone. That is #917's defect in miniature: its pytest twin tested the reconciler's body and left the runner's CALL to it uncovered, so removing the call kept the pytest half green while the shell half went red.

Prove by removal

Five mutations, each applied by exact string match with the file asserted to still parse:

mutation driver-free wiring arm
control 10 passed 9 passed
no refusal 2 failed 9 passed
command tag ignored 2 failed 1 failed
every statement treated as a write 4 failed 1 failed
connection not wrapped 10 passed 1 failed
acknowledgement not recorded 1 failed 1 failed

False-positive budget first

A guard that reddens a legitimate write is worse than the mode it closes. The full corpus with a cluster is 249 passed, and none of the 12 write sites already in the corpus reddened.

Two of my own arms were wrong

Recorded because each produced a green that meant nothing.

An arm asserting only failed=1 passed before the feature existed. The inner test called a function not yet written, got an AttributeError, and the outer arm read that as the comparison failing. Naming the numbers is what makes the red the right red.

A multi-word pattern can straddle pytest's word wrap. expect.refusal anchors each pattern to one E line, and pytest wraps a long traceback line — so matching wrote no rows failed against a message that contained it, which reads exactly like "the guard did not fire". The refusal now leads with the mode's own kebab-case id, which is one token and cannot be split.

Docs in the same commit

VACUITY_MODES.md moves the mode to section 2 and keeps a back-reference where it was. Its stated totals go 27 → 28 refused and 45 → 44 not refused — checked by the corpus's own arms, which is how I learned the numbers rather than deciding them. TESTS.md gains section 22, and the section-18 paragraph that listed this gap is rewritten rather than deleted, because a reader who knew the gap needs to find out where it went.

Gate

harness_selftest   588 checks, 588 passed + 0 failed + 0 unrunnable, PASSED
driver-free job    10 files, 171 passed, psycopg absent from the venv
full corpus        249 passed with a cluster on pg18a

Based on d0ac4f5d (current main).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a

OffgridwithJD and others added 2 commits September 10, 2026 20:39
…pt#432)

`INSERT ... SELECT ... WHERE false` writes nothing and raises nothing. psycopg
reports `INSERT 0 0` with a `rowcount` of 0, and nothing in the corpus read either
field -- so the fixture a test meant to build did not exist, and every assertion
below it compared two empty things. That is `insert-wrote-no-rows`, entry 2 on
VACUITY_MODES.md's own list of what to add next.

Measured before building: 12 write statements across 5 files, and `rowcount` read
at exactly one site, which does not assert on it.

THE COMMAND TAG DECIDES, NOT THE ROW COUNT. `SELECT 0` and `INSERT 0 0` both carry
`rowcount == 0`, so a guard keyed on the count alone would refuse every test whose
last statement was a SELECT over an empty result -- a legitimate and common
assertion. `statusmessage` is the server's own command tag, so this guard never
parses SQL. Measured on PG 18 against a pgcolumnar table:

    statusmessage       rowcount   statement
    CREATE TABLE              -1   CREATE TABLE t (i int) USING pgcolumnar
    INSERT 0 5                 5   INSERT INTO t SELECT g FROM generate_series(1,5) g
    INSERT 0 0                 0   INSERT ... WHERE false
    UPDATE 0                   0   UPDATE t SET i = i WHERE i > 100
    DELETE 0                   0   DELETE FROM t WHERE i > 100
    SELECT 0                   0   SELECT * FROM t WHERE false
    SET                       -1   SET search_path TO public
    TRUNCATE TABLE            -1   TRUNCATE t

A DELIBERATE ZERO STAYS WRITABLE. A DELETE that must match nothing is a real
negative control, so `expect.wrote(cur, 0, name)` both compares the count and marks
the write as named. An unnamed zero fails the test; naming a count does not excuse
a wrong one; and a `rowcount` of -1 is refused rather than compared, for the reason
`expect.rowcount` already records.

IN THE CALL PHASE, NOT A TEARDOWN. commandprompt#931 measured that a guard run as a teardown
fixture reports the test it guards as PASSED and fails separately, so a reader sees
a green test beside an error.

TWO PROPERTIES, TWO FILES, AND THE SPLIT IS MEASURED RATHER THAN ASSERTED. The
classifier and the refusal live in test_writes_wrote_rows.py, which needs no
database: a stub cursor carrying the two measured fields exercises them exactly.
Whether the connection the tests actually use is watched is a different claim that
no driver-free arm can make, and test_the_connection_the_tests_use_is_watched makes
it through a real `INSERT ... WHERE false`, on both `conn.execute` and a cursor the
connection handed out -- 24 and 42 sites in the corpus, so a proxy watching only the
connection would leave most of it unwatched.

Unwiring the conftest proxy and changing nothing else leaves the driver-free file at
10 passed and reds the wiring arm alone. That is commandprompt#917's defect in miniature: its
pytest twin tested the reconciler's body and left the runner's CALL to it
uncovered, so removing the call kept the pytest half green at 9 passed while the
shell half went red by one.

PROVE BY REMOVAL, five mutations, each applied by exact string match with the file
asserted to still parse:

    control                       free 10 passed    wiring 9 passed
    no refusal                    free  2 failed    wiring 9 passed
    command tag ignored           free  2 failed    wiring 1 failed
    every statement is a write    free  4 failed    wiring 1 failed
    connection not wrapped        free 10 PASSED    wiring 1 failed
    acknowledgement not recorded  free  1 failed    wiring 1 failed

FALSE-POSITIVE BUDGET FIRST, because a guard that reddens a legitimate write is
worse than the mode: the full corpus with a cluster is 249 passed, and none of the
12 write sites already in the corpus reddened.

TWO OF MY OWN ARMS WERE WRONG, recorded because each produced a green that meant
nothing:

  * An arm asserting only `failed=1` passed before the feature existed. The inner
    test called a function not yet written, got an AttributeError, and the outer arm
    read that as the comparison failing. Naming the numbers is what makes the red
    the right red.
  * A multi-word pattern can straddle pytest's word wrap. `expect.refusal` anchors
    each pattern to one `E` line and pytest wraps a long traceback line, so matching
    "wrote no rows" failed against a message containing it -- which reads exactly
    like "the guard did not fire". The refusal now leads with the mode's own
    kebab-case id, which is one token and cannot be split.

Docs carried in the same commit, per jd's rule. VACUITY_MODES.md moves the mode to
section 2 and keeps a back-reference where it was, and its stated totals go 27 -> 28
refused and 45 -> 44 not refused -- checked by the corpus's own arms, which is how I
learned the numbers rather than deciding them. TESTS.md gains section 22, and the
paragraph in section 18 that listed this gap is rewritten rather than deleted,
because a reader who knew the gap needs to find out where it went.

Gate on this tree:
    harness_selftest   588 checks, 588 passed + 0 failed + 0 unrunnable, PASSED
    driver-free job    10 files, 171 passed, psycopg absent from the venv
    full corpus        249 passed with a cluster on pg18a

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
commandprompt#432)

Both were found by asking what the new guard ACCEPTS rather than what it refuses,
and the first version was green with both.

`expect.wrote` ACCEPTED A SELECT. A query matching nothing reports `SELECT 0` with
`rowcount == 0`, so `expect.wrote(cur, 0, name)` compared 0 with 0 and passed --
asserting "this write wrote no rows" about a statement that is not a write. It reads
as a deliberate zero and pins nothing, which is this layer's own subject appearing
inside the assertion written to close it. A tag outside the write vocabulary is now
refused, with `not-a-write` as the token.

THE REFUSAL NUMBERED THE WRONG THING. It enumerated the empty writes it was about to
print, so `commandprompt#1` meant "the first one I am complaining about" and identified no
statement: a reader counting writes in the source went to the wrong line. The ordinal
is now the write's position among ALL the test's writes, assigned when it is recorded.

AND THAT ONE PRODUCED AN ARM THAT COULD NOT DISCRIMINATE, which is the part worth
keeping. My first version of the arm used two writes, where the old numbering and the
new one both print `commandprompt#1` -- so it passed against the defect it was written for. It now
uses three writes with the empty one SECOND: the filtered numbering says `commandprompt#1`, the
absolute one says `commandprompt#2`, and only one of those sends a reader to the right statement.

An acknowledgement is now matched by the CURSOR that ran the statement rather than by
`(tag, count)`. Two writes can carry the same tag and the same count -- one
accidental, one deliberate -- and matching on the pair acknowledged whichever came
first, marking the accidental one as named and reporting the deliberate one instead.
The write is stamped on the cursor in `note_write`, defensively, because a psycopg
cursor may refuse a new attribute; the value match stays as the fallback, which is
why the ordinal had to become absolute rather than relative to the survivors.

Prove by removal, now eight mutations, each applied by exact string match with the
file asserted to still parse:

                                      driver-free      wiring arm
    control                           13 passed        9 passed
    no refusal                         3 failed        9 passed
    command tag ignored                2 failed        1 failed
    every statement is a write         3 failed        1 failed
    connection not wrapped            13 PASSED        1 failed
    acknowledgement not recorded       1 failed        9 passed
    non-write accepted                 1 failed        9 passed
    acknowledge by (tag, count) only   1 failed        9 passed
    ordinal relative to survivors      1 failed        9 passed

The fifth row is still the one that matters: unwiring the connection leaves every
driver-free arm green and reds the wiring arm alone, which is why they are two files.

Gate:
    harness_selftest   588 checks, 588 passed + 0 failed + 0 unrunnable, PASSED
    driver-free job    10 files, 174 passed, psycopg absent from the venv
    full corpus        252 passed with a cluster on pg18a

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
@OffgridwithJD

Copy link
Copy Markdown
Collaborator Author

Pushed 29834e3b: two holes I found by attacking this guard after it was green. Both were present in 5467382a, which CI passed 13/13 — so neither was caught by anything except looking for what the guard accepts.

expect.wrote accepted a SELECT

A query matching nothing reports SELECT 0 with rowcount == 0, so expect.wrote(cur, 0, name) compared 0 with 0 and passed — asserting "this write wrote no rows" about a statement that is not a write at all. It reads as a deliberate zero and pins nothing.

That is this layer's own subject appearing inside the assertion written to close it, which is the second time today a guard of mine carried the defect it guards against. A tag outside the write vocabulary is now refused, with not-a-write as the token.

The refusal numbered the wrong thing

It enumerated the empty writes it was about to print, so #1 meant "the first one I am complaining about" and identified no statement. A reader counting writes in the source went to the wrong line. The ordinal is now the write's position among all the test's writes, assigned when it is recorded.

And that one produced an arm that could not discriminate. My first version used two writes — where the old numbering and the new one both print #1, so the arm passed against the very defect it was written for. It now uses three writes with the empty one second: the filtered numbering says #1, the absolute one says #2, and only one of those sends a reader to the right statement.

An acknowledgement is also matched by the cursor that ran the statement rather than by (tag, count). Two writes can carry the same tag and the same count — one accidental, one deliberate — and matching on the pair acknowledged whichever came first, marking the accidental one as named and reporting the deliberate one instead. The write is stamped on the cursor in note_write, defensively, because a psycopg cursor may refuse a new attribute; the value match stays as the fallback, which is why the ordinal had to become absolute rather than relative to the survivors.

Prove by removal, now eight mutations

Each applied by exact string match, with the file asserted to still parse:

mutation driver-free wiring arm
control 13 passed 9 passed
no refusal 3 failed 9 passed
command tag ignored 2 failed 1 failed
every statement treated as a write 3 failed 1 failed
connection not wrapped 13 passed 1 failed
acknowledgement not recorded 1 failed 9 passed
non-write accepted 1 failed 9 passed
acknowledge by (tag, count) only 1 failed 9 passed
ordinal relative to the survivors 1 failed 9 passed

Row five is still the one that matters: unwiring the connection leaves every driver-free arm green and reds the wiring arm alone, which is why they are two files.

Gate

harness_selftest   588 checks, 588 passed + 0 failed + 0 unrunnable, PASSED
driver-free job    10 files, 174 passed, psycopg absent from the venv
full corpus        252 passed with a cluster on pg18a

Two things I would most like attacked

Since I found these by attacking rather than by running:

  1. Whether statusmessage can be absent or reworded on a path this corpus uses. I measured PG 18 only. The guard returns early on a falsy statusmessage, so the failure direction is a silent non-record rather than a false red — which is the worse direction for a guard.
  2. executemany. It reports the count of the last batch, and the corpus has no call sites today, so the arm for it would be an instrument with nothing exercising it. I recorded the path rather than guessing its semantics.

@jdatcmd jdatcmd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes at 29834e3b. You found both of my findings before I could post them, and the third one you found on your own — wrote() passing on a SELECT 0 — I had missed entirely. But the identity fix does not work against the real driver, and it takes the ordinal fix down with it.

Blocking: the identity path cannot run, and its arms cannot see that

note_write stamps self._cur, the raw psycopg cursor. A psycopg cursor cannot take a new attribute, so the stamp is swallowed by the except (AttributeError, TypeError): pass on every real write:

connection.cursor()        stampable=FALSE   (Cursor: AttributeError)
conn.execute() return      stampable=FALSE   (Cursor: AttributeError)
ServerCursor               stampable=FALSE   (ServerCursor: AttributeError)

And the object the test holds is a _WatchedCursor, whose __getattr__ forwards _pgc_write to the raw cursor, which never got one.

End to end against a real PostgreSQL 17, two identical zero-row INSERTs through _WatchedConnection, naming the second:

writes recorded:            [(1, INSERT, 0), (2, INSERT, 0)]
real cursor carries stamp?  False
what the test holds:        _WatchedCursor
getattr(c2, '_pgc_write'):  None            <- falls back to value matching
write #1 acknowledged=True                  <- the one I did NOT name
write #2 acknowledged=False
-> refusal names statement [2]; the unnamed one is #1

So wrote() still matches by value, exactly as before the commit — and the absolute ordinal now points at the wrong statement in precisely the case it was added for. The two fixes are load-bearing on each other: the ordinal is only trustworthy when the acknowledgement is by identity, and it never is.

The arms cannot catch this because the stub is stampable and the real cursor is not:

the arms' _Cur stub    stampable=True
a real psycopg Cursor  stampable=FALSE

13 passed. They prove the identity mechanism on an object that differs from the real one in exactly the respect under test — which is the shape your own _WatchedCursor docstring warns about, a proxy standing in for a thing that behaves differently.

The fix looks small: stamp the object the caller receives rather than the one psycopg owns. execute already returns self, and _WatchedCursor is an ordinary object with a __dict__, so stamping the proxy lands and __getattr__ never intercepts an attribute the instance actually has. What is worth more than the fix is an arm that would have caught it — the cluster-bound wiring test is the natural home, since it is the one place a real cursor exists, and it is already the arm that exists to prove the call site rather than the function.

The verdict itself is not wrong today. I checked separately: because value matching requires equal tag and equal count, a mis-assignment can only happen between indistinguishable writes, so the refusal still fires the right number of times. What breaks is which statement the message names.

What I verified and could not fault

Your first question, answered wider than you asked. You measured PG 18; I measured PG 15.18 and PG 17.10, twelve shapes each, through psycopg 3.3.5. statusmessage is never absent and the wording is byte-identical across both:

INSERT 0 0 / UPDATE 0 / DELETE 0 / MERGE 0     rowcount 0   guarded
SELECT 0                                        rowcount 0   NOT guarded
CREATE TABLE                                    rowcount -1  NOT guarded

Your central design claim, proven by mutation rather than argued. Removing the tag check:

                          with the tag              count-keyed
SELECT over empty result  recorded=0  refuse=False  recorded=1  refuse=TRUE
zero-row INSERT           recorded=1  refuse=True   recorded=1  refuse=True

The count-keyed version refuses a legitimate SELECT over an empty result. The tag is the right key, and not-a-write closing the wrote() side of the same hole is the better half of this commit — a SELECT 0 compared 0 with 0 and passed, asserting "this write wrote no rows" about a statement that is not a write. I did not find that one.

The ordinal fix is correct on the identity path. Four writes, only the third empty: the message now says #3 INSERT where it used to say #1.

Four shapes write rows and escape the tag, measured on both majors — a data-modifying CTE and a SELECT of an inserting function both report SELECT, a DO block reports DO, a CALL reports CALL. Zero occur in the corpus (0 files for each; the corpus writes are 15 INSERT, 14 COPY, 2 DELETE, 1 UPDATE), so this is a residual to state rather than a gap to close.

Doc arithmetic 53/53, the new file declared, membership_report [], cluster-free 11 = job runnable 10 + driver-dependent 1, driver-free job 10 files / 171 passed.

The wiring arm remains the best part, and it is the #917 lesson applied by name — "proving a function and proving its call site are two proofs, and the second is the one that goes missing." This finding is that sentence coming true one level down: the stamp is a call site too.

@jdatcmd found that the identity fix could not run against the real driver, and that
it took the ordinal fix down with it. Both are right and the second is the part that
mattered: the two rest on each other, so with the stamp swallowed the refusal named
the wrong statement in exactly the case the ordinal was added for.

`note_write` stamped the RAW psycopg cursor. A `psycopg.Cursor` cannot take a new
attribute, so the stamp was swallowed by its own `except` on every real write, and the
object the test holds is a `_WatchedCursor` whose `__getattr__` forwards the lookup to
the raw cursor that never got one. Measured through the driver:

    connection.cursor()        stampable=False   (Cursor: AttributeError)
    conn.execute() return      stampable=False   (Cursor: AttributeError)
    ServerCursor               stampable=False   (ServerCursor: AttributeError)
    the arms' _Cur stub        stampable=True

THE ARMS COULD NOT SEE IT, and that is the lesson rather than the bug. They proved the
identity mechanism on an object that differs from the real one in exactly the respect
under test -- the hazard `_WatchedCursor`'s own docstring names, arriving in the arms
written to guard it. The fix stamps the object the CALLER receives; the stamp on the
raw cursor stays, because it is what the layer's own stub-driven arms use, and its
comment now says what it reaches and what it does not.

AND MY FIRST ARM FOR IT PINNED NEITHER SITE. There are two stamps -- one in
`_WatchedCursor.execute`, one in `_WatchedConnection.execute` -- and the arm exercised
only the connection path, so removing the cursor-path stamp left it green. Found by
mutating the fix rather than by reading it. The arm now drives both paths with
INDISTINGUISHABLE writes, which is the only case where identity and value matching can
disagree: the existing wiring arm uses the cursor path with different counts, where
value matching finds the right write whether or not the stamp lands.

Prove by removal, ten mutations, each by exact string with a parse assertion:

                                      driver-free      cluster arms
    control                           13 passed        10 passed
    no refusal                         3 failed        10 passed
    command tag ignored                2 failed         2 failed
    every statement is a write         3 failed         2 failed
    connection not wrapped            13 passed         2 failed
    acknowledgement not recorded       1 failed         1 failed
    non-write accepted                 1 failed        10 passed
    acknowledge by (tag, count) only   1 failed         1 failed
    ordinal relative to survivors      1 failed        10 passed
    cursor-path stamp removed         13 passed         1 failed
    connection-path stamp removed     13 passed         1 failed

Three rows now red ONLY the cluster side, which is the argument for the split stated
three times over: a driver-free arm cannot see wiring, and cannot see an attribute a
real object refuses.

RESIDUAL RECORDED, MEASURED BY @jdatcmd ON TWO MAJORS. `statusmessage` is never absent
and its wording is byte-identical on PG 15.18 and PG 17.10 across twelve shapes. Four
shapes write rows while reporting a non-write tag -- a data-modifying CTE and a SELECT
of an inserting function both report SELECT, a DO block reports DO, a CALL reports
CALL -- and ZERO occur in the corpus, whose writes are 15 INSERT, 14 COPY, 2 DELETE and
1 UPDATE. So it is stated rather than closed: an arm for a shape nothing uses is an
instrument with nothing exercising it.

Gate:
    harness_selftest   588 checks, 588 passed + 0 failed + 0 unrunnable, PASSED
    driver-free job    10 files, 174 passed, psycopg absent from the venv
    full corpus        253 passed with a cluster on pg18a

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
@OffgridwithJD

Copy link
Copy Markdown
Collaborator Author

Fixed at 7c5d85b5. You were right on both counts, and the second one — that the two fixes rest on each other — is the part I had not seen.

The stamp now lands on the object the caller receives. The stamp on the raw cursor stays, because it is what the layer's stub-driven arms use, and its comment says what it reaches and what it does not rather than implying it works everywhere.

And my first arm for it pinned neither site

I wrote the cluster-bound arm you suggested, it went red before the fix and green after, and it was still wrong: there are two stamp sites — _WatchedCursor.execute and _WatchedConnection.execute — and my arm drove only the connection path. Removing the cursor-path stamp left it green.

Found by mutating the fix rather than by reading it, which is the only reason I know. The arm now drives both paths with indistinguishable writes, since that is the only case where identity and value matching can disagree — the existing wiring arm uses the cursor path with different counts, where value matching finds the right write whether or not the stamp lands. That is why it never caught this.

Ten mutations, and three of them red only the cluster side

mutation driver-free cluster arms
control 13 passed 10 passed
no refusal 3 failed 10 passed
command tag ignored 2 failed 2 failed
every statement treated as a write 3 failed 2 failed
connection not wrapped 13 passed 2 failed
acknowledgement not recorded 1 failed 1 failed
non-write accepted 1 failed 10 passed
acknowledge by (tag, count) only 1 failed 1 failed
ordinal relative to the survivors 1 failed 10 passed
cursor-path stamp removed 13 passed 1 failed
connection-path stamp removed 13 passed 1 failed

Rows 5, 10 and 11 are the argument for the split stated three times: a driver-free arm cannot see wiring, and it cannot see an attribute that a real object refuses.

Your residual is in the docs, attributed

statusmessage never absent and byte-identical on PG 15.18 and PG 17.10 across twelve shapes is a better premise than the one I shipped, which was PG 18 only. And the four shapes that write rows while reporting a non-write tag — data-modifying CTE, SELECT of an inserting function, DO, CALL — are recorded in TESTS.md as a stated residual with your measurement that zero occur in the corpus (15 INSERT, 14 COPY, 2 DELETE, 1 UPDATE). An arm for a shape nothing uses would be an instrument with nothing exercising it.

Your mutation of my central claim is better evidence than my prose for it. I argued the tag is the right key; you showed the count-keyed version refuses a legitimate SELECT over an empty result, which is the argument rather than a restatement of it.

Gate

harness_selftest   588 checks, 588 passed + 0 failed + 0 unrunnable, PASSED
driver-free job    10 files, 174 passed, psycopg absent from the venv
full corpus        253 passed with a cluster on pg18a

One thing still worth attacking, since you found the last one by asking what the arms could not see: the acknowledgement now depends on the proxy being the object a test passes to wrote(). A test that reaches through to cur._cur, or that keeps a cursor across two execute calls and names the earlier one, gets the fallback. The second case is now covered; the first is not, and I do not think it should be — but I would rather you told me that than assumed it.

@jdatcmd jdatcmd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at 7c5d85b5. My finding is closed, and I verified it the way it was found — against a real PostgreSQL 17 through _WatchedConnection, not against stubs.

The stamp lands now, on both paths:

via conn.execute       stamp=yes  refusal names [1]  -> correct
via cursor.execute     stamp=yes  refusal names [1]  -> correct

Naming the second of two indistinguishable zero-row writes acknowledges the second, and the refusal names statement #1 — the genuinely unnamed one. Before, the stamp was None, naming the second acknowledged the first, and the refusal named #2. Stamping the proxy rather than the cursor psycopg owns is the right fix, and initialising self._pgc_write = None in __init__ so absence is distinguishable from a cursor that has not run is the detail that makes getattr(..., None) honest.

Your own finding reproduces, and both sites are independently load-bearing. I removed each stamp in turn, leaving note_write called so only the stamp is under test:

stamp removed at 798 (cursor path)      conn path correct   cursor path WRONG, names [2]
stamp removed at 834 (connection path)  conn path WRONG     cursor path correct

So each site is observable only on its own path — which is exactly why an arm driving one of them pinned neither. Worth stating for the record: all three stamp removals leave the driver-free file at 13 passed, because the stub is stampable so identity works there regardless. The cluster side is the only place this property exists at all.

The new arm drives both paths with indistinguishable writes, asserts the stamp is a _Write, asserts its absolute ordinal is 2, and asserts acknowledgement lands on the second while leaving the first. The comment records why the pre-existing wiring arm could not have caught it — it uses the cursor path with distinguishable counts, where value matching finds the right write whether or not the stamp lands. That sentence is the useful part of the whole change.

not-a-write is the finding I missed and it is the better half of the earlier commit: wrote() on a SELECT 0 compared 0 with 0 and passed, asserting "this write wrote no rows" about a statement that is not a write — this layer's own subject appearing inside the assertion meant to close it.

My residual is recorded accurately, including the measurement and its limits: four shapes write rows and report a non-write tag, zero occur in the corpus, and "writing an arm for a shape nothing uses would be an instrument with nothing exercising it" is the right reason not to close it.

State at this head: 13 arms passed, the driver-free job 10 files / 174 passed, membership_report [].

Taking your _comparisons() question, since you asked me to choose the shape

You are right that wrote(cur, want, name) is excluded correctly but by accident — the derivation keys on the first two parameter names, and a future assertion whose first parameter is not called got would be excluded just as silently.

I do not think a declared exclusion list is the shape, though, because it is a hand-maintained list of names and this layer's whole argument against _WRITE_TAGS-style lists is that they go stale invisibly. What I would rather have is the same trick you used for the write tags: make the exclusion derivable and then assert the partition. Every public method lands in exactly one of selected / excluded, each excluded one carries a stated reason next to its signature, and an arm prints inputs == selected + excluded and fails when a method appears that matches neither rule. That converts "excluded because nobody named a parameter got" into "excluded because its left side is a cursor, and here is the arm that says so".

That is #937-adjacent work rather than this PR's, and it is your file as much as mine now, so I will open it as its own issue unless you would rather carry it.

@jdatcmd
jdatcmd merged commit 7988299 into commandprompt:main Sep 10, 2026
13 checks passed
jdatcmd added a commit that referenced this pull request Sep 10, 2026
#926 and #935 landed while this waited.

CHANGELOG: both sides append at the top of the same section and neither replaces
anything, so the union is the resolution.

TESTS.md: one region, and this side of it is EMPTY. This branch deleted its
section 22 in aa07d47, and main added its own -- #935's
test_writes_wrote_rows.py. Main's is the only content, so the resolution is to
take it. 22 headings against 22 TOC entries, every anchor equal to GitHub's
derivation, contiguous 1..22.

selftest 080 goes from 15 checks to 53 because #926 landed, which is the number
that branch reports and not a change of this one.

On the composed tree: 350 53/53, 400 81/81, 080 53/53, shellcheck rc=0 over the
whole harness, the driver-free job 10 files 174 passed, membership_report [].

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Unuuvh3fRR67SceiGpfeeK
jdatcmd added a commit that referenced this pull request Sep 10, 2026
#926 and #935 landed on main and #923 dropped its coupled pytest twin, so all
three reached this branch at once.

NO_CLUSTER: the base removed test_check_results_are_machine_readable.py, this
branch had added test_mutation_ledger.py, and the conflict spanned both. Kept
the ledger entry and dropped the deleted file's. Its comment block was OUTSIDE
the conflict region and survived as six orphaned lines above an unrelated entry
-- removed, and the module asserted to still parse and to hold 11 entries with
the deleted file absent. That is the orphaned-heading shape this repository has
been bitten by before, and git will not point at it.

TESTS.md: two regions. In the TOC this branch had 22 (the deleted file) and 23
(the ledger) while main had its own 22, test_writes_wrote_rows.py; in the body
this side opened with six orphaned lines of the deleted section before the
ledger's. Composed as main's 22 followed by this branch's 23. 23 headings
against 23 TOC entries, every anchor equal to GitHub's derivation, contiguous
1..23.

THE LEDGER IS REGENERATED AGAIN, and #926 is why: it took selftest 080 from 15
checks to 53, and the gate refuses a check it has never seen. From a real run of
the composed tree:

    harness_selftest.sh: PASSED, rc=0
    checks run: 773 | accounting: 773 passed + 0 failed + 0 unrunnable + 0 skipped
    ledger: 734 rows -> 772 | never=772, ever red=0
    log triples not in the ledger: 0

All 772 rows carry five fields, none ends in a tab, and the budget's asserted
census follows to 772.

Gates on the composed tree: 350 53/53, 400 81/81, 410 96 checks 0 failed,
080 53/53, shellcheck rc=0, the driver-free job 11 files 183 passed,
membership_report [].

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Unuuvh3fRR67SceiGpfeeK
OffgridwithJD pushed a commit to OffgridwithJD/pgcolumnar that referenced this pull request Sep 10, 2026
# Conflicts:
#	test/pytest/VACUITY_MODES.md
jdatcmd pushed a commit that referenced this pull request Sep 11, 2026
jdatcmd pushed a commit that referenced this pull request Sep 11, 2026
…her branch

#935 closed `insert-wrote-no-rows` and moved the refused total 27 -> 28. This branch
closes `raises-catches-setup` and moved it 27 -> 28 as well. The merge was CLEAN and
therefore wrong: git took one 28 where the answer is 29, because the number is a
property of both closures rather than of either.

That is the exact failure TESTS.md's own header records about counts in this document
-- "a claim whose correct value is a function of the MERGE rather than of either
branch, so it collided on essentially every rebase" -- and the reason the counts are
checked by arms instead of trusted. The arms named every number:

    refused today      28 -> 29
    not refused        44 -> 43
    demonstrated       48 -> 47   (section 3's sentence)
    TESTS.md prose     28 -> 29, and 44/43 -> 43/42
    README.md          28 -> 29

I did not decide any of them; each one is what `test_the_prose_totals_match_the_counted_modes`
and its siblings reported against the ids on disk.

Gate on the merged tree:
    harness_selftest   588 checks, 588 passed + 0 failed + 0 unrunnable, PASSED
    driver-free job    10 files, 174 passed, psycopg absent from the venv
    full corpus        257 passed with a cluster on pg18a
    the raises scan    zero offences over the corpus, all 9 compound kinds recognised

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
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.

2 participants