Skip to content

docs: test_advisory_lock_class understated its own removal proof - #1160

Merged
jdatcmd merged 1 commit into
commandprompt:mainfrom
OffgridwithJD:fix/1157-two-arms
Sep 19, 2026
Merged

jdatcmd merged 1 commit into
commandprompt:mainfrom
OffgridwithJD:fix/1157-two-arms

Conversation

@OffgridwithJD

Copy link
Copy Markdown
Collaborator

Closes #1157. My docstring claimed less than the file does, and the claim was reasoned
rather than run.

I wrote that the shell suite's discovery makes its contention arm "inherit the error".
@jdatcmd ran it instead of reading it, and found the port reddens two arms, not one.
I verified that here before writing this, on f12caa2 with the object forced to rebuild:

CONTROL   7 pass + 0 fail                      .so 0ab2accb47ef
MUTATED   two arms red                         .so 29a7f55eeb71
          the lock an insert takes is not in a SQL-reachable class:
              got 'reachable [(16572, 77, 2)]' want 'unreachable'
          a user advisory lock on that tag does not block a columnar insert

and the shell suite under the same mutation:
          advisory_lock_class.sh: PASSED       7 passed + 0 failed

The accurate statement is that the discovery feeds the contention arm: one wrong
premise disables both, and both come back when the premise is fixed. That is worse than
one vacuous arm rather than the same thing, because two arms move with the defect and both
keep looking like evidence.

And the rebuild caveat, which is the reason the proof is reproducible at all (#1158)

PGXS emits no header dependencies unless PostgreSQL was configured with --enable-depend,
and it was not here — so changing this #define rebuilds nothing, the run measures a
stale object, and it reports a clean pass that reads exactly like "not load-bearing". The
.so hash not moving is the only tell.
That is now in the docstring beside the
measurement, because the next person to re-run this proof will hit it otherwise.

Scope

Comment only. No assertion, no check name, no count changes — git show --stat is one
file and the docs-coverage guard confirms nothing moved.

What I ran, and what I did not

full pytest corpus, PG18   822 passed   2403 pass + 0 fail + 0 unrun   rc=0

I did not run the five-major gate for this one. The change is a docstring; it cannot
fail differently on PG15 than on PG19, and a twelve-minute matrix would buy a number
rather than a fact. Saying so rather than quoting five greens I did not need is the point
— the earlier ports in this file got the full gate because they could differ by major.

🤖 Generated with Claude Code

https://claude.ai/code/session_012RSw4qMHS7ByE7PY8Ns4cs

@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.

The substance is right and I verified it before saying so. One line is wrong, and it is the line whose whole job is telling the next person how to reproduce.

The correction: rm -f src/columnar_unique.o does not work

    `rm -f src/columnar_unique.o` first.

It changes nothing. Measured from a known clean state, PG 17, with set -x:

make clean; make                        md5 pgcolumnar.so  c67dd162c44cd86cbacf03dd2286b862
edit the #define 103 -> 2
rm -f src/columnar_unique.o
make                                    files compiled: 0
                                        md5 pgcolumnar.so  c67dd162c44cd86cbacf03dd2286b862
                                        src/columnar_unique.o  OBJECT_STILL_MISSING

make compiles nothing, does not recreate the deleted object, and leaves the shared library byte-identical. Its entire output is the objstore submake saying Nothing to be done for 'all'. So the recipe hands the reader the same stale object the caveat is warning them about — and because the mutation "ran", it produces the clean pass that reads as "not load-bearing". The caveat would recreate the bug it documents.

src/columnar_unique.o is in OBJS (Makefile line 6ff), and the explicit targets do work:

make src/columnar_unique.o    -> gcc ... -c -o src/columnar_unique.o src/columnar_unique.c
make pgcolumnar.so            -> gcc ... -shared -o pgcolumnar.so <35 objects>

So the object is reachable; plain all does not reach it. I have not chased why all-lib is considered satisfied with a prerequisite missing, and I am not going to guess at it in a review.

make clean is what I verified end to end, twice, in both directions:

clean build, 103         c67dd162c44cd86cbacf03dd2286b862
make clean, mutate to 2  9649379be700b359bea375f70799708e   <- two arms red
make clean, back to 103  c67dd162c44cd86cbacf03dd2286b862   <- 7 shell / 12 pytest green

So the line should say make clean first. If you want the cheap version, make src/columnar_unique.o && make pgcolumnar.so is measured to work; rm plus plain make is not.

This is a bigger finding than the docstring. A deleted object listed in OBJS is not rebuilt by make, which means incremental builds here are not merely missing header dependencies — they do not reliably rebuild from a missing object either. That is very likely why #1158 survived: nobody depends on incremental correctness because nothing incremental works, so everyone make cleans out of habit and the header gap never surfaced. I will add it to #1158 rather than expand this PR.

Everything else, checked rather than read

two arms, not one        confirmed here: `reachable [(16520, 77, 2)]` and
                         `BLOCKED by the user lock`, on .so 9649379b
the shell suite green    confirmed: 7 passed + 0 failed + 0 unrunnable, PASSED
"the discovery FEEDS     correct, and better than "inherits": one wrong premise
 the contention arm"     disables both, both return when it is fixed

Your last sentence is the one I would have wanted and did not write: two arms moving together with the defect is worse than one vacuous arm, because both keep looking like evidence. That is the general form and it belongs in the file.

On not running the five-major gate: agreed, and the reasoning is right. A docstring cannot fail differently by major, and quoting five greens you did not need is the number-without-a-fact you have been objecting to. Saying so on the PR is what makes it a decision rather than an omission.

Requesting changes on the one line only.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK

…mandprompt#1157)

The docstring said the shell suite's discovery makes the contention arm
"inherit the error". That was reasoned, not run, and it undersells the file:
under the mutation TWO arms redden, not one.

    advisory_lock_class.sh    7 passed + 0 failed        PASSED
    this file                 TWO arms red, .so 0ab2accb47ef -> 29a7f55eeb71

Both `the lock an insert takes is not in a SQL-reachable class` and `a user
advisory lock on that tag does not block a columnar insert` go red, because the
discovery FEEDS the contention arm: one wrong premise disables both, and both
come back when it is fixed. Two arms moving together with the defect is worse
than one vacuous arm, since both keep looking like evidence.

Also records the rebuild caveat (commandprompt#1158): PGXS emits no header dependencies
unless PostgreSQL was configured with --enable-depend, and it was not here, so
changing this #define rebuilds nothing and the run measures a stale object --
a clean pass that reads exactly like "not load-bearing". The .so hash not moving
is the only tell.

Comment only; no assertion, name or count changes.

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

Copy link
Copy Markdown
Collaborator Author

You are right, and the line did recreate the bug it documents. Corrected in 91f4e18.

I measured all three cells here before changing the wording, because the fix depends
on which of them actually works. PG17, clean tree, header mutated each time:

rm -f obj; make                 0 files compiled, object still MISSING
                                .so f7eac4d19c7f4762 -- UNCHANGED
rm -f obj; touch the .c; make   2 files compiled, .so 812d9b3c5c9ddd4c
make clean; make                             .so 812d9b3c5c9ddd4c

Restoring the header and rebuilding returns f7eac4d19c7f4762 exactly, so the experiment
is reversible and the two mutant cells agree on the byte.

Two things fall out of that, and the second is the one worth keeping.

My runs were valid and my caveat was not. Every mutation script I used did
rm -f obj and touch the .c; the touch is what rebuilt, and the .so hash moving
is how I knew. When I wrote the caveat I recorded the half I had reasoned was doing the
work rather than the pair I had actually run — the same failure as the docstring this PR
exists to fix, in the same file, in the commit that fixes it.

And touch .c is measured-equivalent to make clean for this change: cells 2 and 3
produce the identical library. So the cheap path is real, but the comment now points at
make clean anyway, because it does not require knowing which object the header reached —
and a reader who has to work that out is one inference away from the stale-object trap
again.

On your larger finding

A deleted object listed in OBJS is not rebuilt by make, and that is not a
header-dependency problem.
My cell 1 reproduces it exactly: zero compiles, object still
missing afterwards, library untouched. -MMD -MP would not have helped, because the
dependency was never the thing that failed — the rule did. Both have to work before an
incremental build can be trusted for a removal proof, and I would put cell 1 in #1158
verbatim, since "make does not recreate a deleted prerequisite" is a claim people will
disbelieve without the transcript.

Your explanation for the lifetime is better than my framing was. I said the failure mode
is a clean PASS so nobody had a reason to look. You have the other half: incremental
builds here fail in more than one way, so make clean is already everyone's reflex, and
a habit that hides a bug is why the bug never had to be found. That also revises the
damage estimate downward in a way I could not have argued — many proofs were probably run
clean by reflex, which is luck rather than design and recorded nowhere, and is one more
reason not to sweep the history.

Not chasing why all-lib is satisfied with a missing prerequisite was the right call.
Handing over the measurement and stopping is more useful than a mechanism either of us
guessed at, and the two all: rules are a good enough pointer for whoever picks it up.

@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 91f4e187. The line is fixed, and you went past what I asked for in a way that is worth keeping.

I reproduced the new claim rather than reading it

Same experiment on my tree. My hashes differ from yours because the .so fingerprint is a function of the build directory — the relation is what transfers, and it holds exactly:

baseline, header at 103                     c67dd162c44cd86c
mutate to 2; rm -f obj; touch the .c; make  compiled 2 files   9649379be700b359
mutate to 2; make clean; make                                  9649379be700b359

Byte-identical, and rm alone still compiles nothing. So rm -f obj; touch the .c is a measured equivalent to a clean build, and your table is right in both directions.

That is a better answer than the one I gave. I established that rm alone fails and stopped at "use make clean"; you established what the shortcut actually costs and why, which is the difference between a rule and a reason. Your closing clause is the part I would keep — make clean is what to reach for because it does not depend on knowing which object the header reached. That is the real hazard with the shortcut: it is correct only if you correctly guessed the reachable set, and a header with two consumers silently gives you a half-rebuilt library.

The sentence that earns the docstring

rm -f src/columnar_unique.o IS NOT ENOUGH, AND THIS COMMENT SAID IT WAS.

Leaving the wrong advice named in the text that replaces it is right. A reader who saw the old version elsewhere — in a review, in a branch, in their own notes — needs to know it was wrong, and a silent correction does not tell them.

The rest, re-checked at this head

two arms, not one       confirmed: `reachable [(16520, 77, 2)]` and `BLOCKED by the user lock`
shell suite green       confirmed: 7 passed + 0 failed + 0 unrunnable, PASSED
"discovery FEEDS the    correct, and the right word -- one wrong premise disables
 contention arm"        both, both return when it is fixed
CI at 91f4e187          14 of 14, non-green 0, CLEAN

Still agreed on skipping the five-major gate for a docstring, for the reason you gave.

One thing for #1158 rather than this PR: your touch result narrows what the fix has to do. -MMD -MP gives correct header dependencies, and the touch case shows make does act on a .c timestamp — so the remaining gap is specifically that a missing object listed in OBJS does not itself trigger a rebuild. That is a smaller and more precise defect than "incremental builds do not work", which is how I had been describing it. I will correct that on the issue.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK

@jdatcmd
jdatcmd merged commit c050f84 into commandprompt:main Sep 19, 2026
14 checks passed
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.

advisory_lock_class.sh passes on the #430 regression it exists to catch

2 participants