Skip to content

docs: split T3 marks, add online-safety column, state the unix philosophy - #57

Merged
Kiran01bm merged 3 commits into
mainfrom
kiran01bm/capabilities-t3-marks
Aug 24, 2026
Merged

docs: split T3 marks, add online-safety column, state the unix philosophy#57
Kiran01bm merged 3 commits into
mainfrom
kiran01bm/capabilities-t3-marks

Conversation

@Kiran01bm

@Kiran01bm Kiran01bm commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

docs/capabilities.md rendered every T3 row with the same ❌, so the page visually read as "pg-sprite doesn't support a bunch of things" when most of those rows are deliberate scope boundaries — operations that either have no online-safety problem to solve or belong to a different tool class. This PR makes the distinction visual and explicit, and states the underlying design stance — the Unix philosophy — loudly in the prominent positioning docs.

What

  • T3 rows now carry one of three marks:
    • ⚪ no online-safety problem to solve — no table scan, no rewrite; at most a brief catalog lock. Rows where that lock lands on a live table (a trigger, a view swap, a greenfield foreign key) carry an explicit queue caveat: run it under a lock_timeout.
    • 🔵 a different tool class owns it — data-change runners, provisioning/IaC, convergence planners, expand/contract frameworks, replication provisioning.
    • ❌ no online mechanism exists — PostgreSQL provides no online pattern to build on; the only rows where "unsupported" is the honest reading (EXCLUDE constraints, USING INDEX on a partitioned parent). The tier table's T3 definition carries all three categories.
  • Every matrix table gains an "Online-safety problem?" column: "Yes" for rows an online engine must solve; "No" cells name the tool class users should reach for.
  • Rows that carried one mark for two different reasons are split so each mark means exactly one thing: views/matview DDL (⚪) vs REFRESH MATERIALIZED VIEW (🔵, a data operation), CREATE OR REPLACE FUNCTION (⚪, no relation lock) vs CREATE TRIGGER (⚪ with a queue caveat — SHARE ROW EXCLUSIVE on the table), standalone sequences (⚪) vs publications/subscriptions (🔵, replication provisioning).
  • A one-line summary under the matrix heading makes the page's claim skimmable and checkable: 51 operations — 17 supported today, 18 planned behind a typed refusal, 14 out of scope by design, 2 with no online mechanism in PostgreSQL.
  • Unix philosophy called out explicitly — "do one thing, and do it perfectly", where the one thing is changing the shape of live PostgreSQL tables under concurrent load:
    • docs/vision.md: a dedicated paragraph after the intro, reconciling "one tool for all schema changes" with "do one thing" (depth across every table-shape change, never sprawl across object types), plus a framing line opening "What pg-sprite is not".
    • README.md: a paragraph after the planner positioning, pointing at capabilities.md.
    • docs/capabilities.md: a framing line in "What pg-sprite is — and why it exists" — the page is the map of where the one thing ends.
  • The positioning prose describes what exists: the engine refuses out-of-scope work with a typed reason, and this page names the tool class that owns each job — today's refusal text is one undifferentiated unsupported-statement reason, and the docs no longer claim otherwise. Carrying the classification into the refusal detail itself is a natural engine-side follow-up.

Why

The capabilities page is the doc we point users at for "does pg-sprite support this?". A scope boundary presented with the same mark as a hard limitation misrepresents the tool: only two rows in the whole matrix are genuinely impossible today, and the rest of T3 is design intent. Naming the Unix philosophy in the vision and README makes that intent legible before a user ever reaches the matrix, and the new column answers the follow-up question ("then what do I use?") inline.

…ophy

A wall of ❌ read as "unsupported" when most T3 rows are scope
boundaries, not gaps. T3 now carries three marks — ⚪ (no online-safety
problem to solve), 🔵 (a different tool class owns it), ❌ (no online
mechanism exists in PostgreSQL) — and every matrix table gains an
"Online-safety problem?" column whose "No" cells name the tool class
users should reach for instead.

The same boundary is now stated loudly as the design stance it is:
vision.md, README.md, and capabilities.md call out the Unix philosophy —
do one thing (online table-shape change under concurrent load) and do
it perfectly — so scope limits read as intent, not missing features.
@Kiran01bm Kiran01bm changed the title docs: split T3 marks, add online-safety column, state the Unix philosophy docs: split T3 marks, add online-safety column Aug 23, 2026
@Kiran01bm Kiran01bm changed the title docs: split T3 marks, add online-safety column docs: split T3 marks, add online-safety column, state the Unix philosophy Aug 23, 2026
@Kiran01bm
Kiran01bm marked this pull request as ready for review August 23, 2026 20:46
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@Kiran01bm Kiran01bm changed the title docs: split T3 marks, add online-safety column, state the Unix philosophy docs: split T3 marks, add online-safety column, state the unix philosophy Aug 23, 2026
@aparajon

Copy link
Copy Markdown
Collaborator

🤖 Adversarial correctness review, requested by @aparajon and performed by their agent. Reviewed at head e594499, in a worktree, against a live PostgreSQL 17. A docs PR that re-marks rows as "already safe to run directly — run it through owner tooling or psql" is making lock-behavior claims, so I took the ⚪ rows as the brief and measured what each operation actually locks, then checked whether the engine really does what the new prose says it does.

Verdict: the re-marking is the right change and 8 of the 11 re-marks are exactly right — I diffed every row and confirmed nothing was silently re-tiered, no ✅/🟡 row was touched, and the "only two rows are genuinely impossible" claim is precisely true. Two things to fix before merge. Three ⚪ rows send users to psql for operations that take a write-blocking or reader-blocking lock on a live table — CREATE TRIGGER takes ShareRowExclusive on the table, CREATE OR REPLACE VIEW takes ACCESS EXCLUSIVE on the view, and a greenfield CREATE TABLE ... REFERENCES takes ShareRowExclusive on the referenced table. And the new sentence in three places — "the engine says so with a typed refusal that names the tool class" — is not true today: a trigger, a grant, a view, and an UPDATE all come back with the same undifferentiated message. Neither is hard to fix, and the second one the engine already knows how to do.

Findings

1. Three ⚪ rows grant permission to run something that locks a live table. The mark is defined as "the operation is already safe to run directly: transactional catalog work, or bootstrap on an object nothing reads yet" — a strong claim, and the strongest thing this page now says. Measured on PG 17, holding each statement open in a transaction and reading pg_locks:

⚪ row statement lock actually taken
Triggers and PL/pgSQL function bodies CREATE TRIGGER … ON live_table ShareRowExclusiveLock on the table — blocks every writer
Views, materialized views CREATE OR REPLACE VIEW AccessExclusiveLock on the view — blocks every reader
Views, materialized views REFRESH MATERIALIZED VIEW AccessExclusiveLock for the whole rebuild
Greenfield CREATE TABLE apply CREATE TABLE child (… REFERENCES parent) ShareRowExclusiveLock on parent
(control) Triggers and PL/pgSQL function bodies CREATE OR REPLACE FUNCTION no relation lock at all — genuinely ⚪

None of these is catalog work on an object nothing reads yet; each one queues behind in-flight sessions on a table an application is using, and while it waits it blocks everything behind it. The page already knows this hazard by name: the CREATE TABLE ... PARTITION OF row is T1 with a typed warning precisely because it "takes a brief ACCESS EXCLUSIVE on the parent and queues behind long-running queries" — the same fact treated as worth warning about in one row and as nonexistent in another. And the advice compounds it in a way the page argues against elsewhere: the "Why typed refusal, not passthrough" section makes the case that the engine's value on exactly these statements is "unsafe DDL under a bounded lock budget, which raw psql does not give you" — so ⚪ currently routes users to the tool the page says is worse for this. Because ⚪ is new permission where there used to be an undifferentiated ❌, getting it wrong is worse than the mark it replaces: a reader who took the ❌ as "not my problem" will take ⚪ as "go ahead". The cheapest fix that keeps the taxonomy intact is to tighten the definition to what it is really claiming — no scan and no rewrite, a brief catalog lock — and give these rows the same one-line queue caveat the partition row already carries.

2. The engine does not name the tool class, and this PR says it does in the three most prominent places. README.md and docs/vision.md both add "the engine says so with a typed refusal that names the tool class", and capabilities.md states the invariant as "the refusal says out-of-scope and names the tool class that owns the job". What the engine actually returns at this head:

CREATE TRIGGER …  → refused / unsupported-statement
                    "only ALTER TABLE and CREATE INDEX statements are supported
                     by the imperative front door"
GRANT SELECT …    → refused / unsupported-statement   (identical detail)
UPDATE …          → refused / unsupported-statement   (identical detail)
CREATE OR REPLACE VIEW … → refused / unsupported-statement   (identical detail)

One message for four different tool classes, naming none of them, and carrying no trace of the ⚪/🔵/❌ distinction this PR exists to draw. The gap is worth closing rather than papering over, because the engine already demonstrates the shape you want — CREATE TABLE returns a genuinely useful refusal that names the right door and fills in safer_idiom:

"detail": "migrate changes an existing table; to converge a table onto a desired-state
           CREATE TABLE, use the declarative front door — diff the desired schema
           against the live database",
"safer_idiom": "pg-sprite diff --desired schema.sql"

Either soften the three sentences to describe this page rather than the engine, or carry the classification into the refusal detail. The second is better and is the natural follow-up: the page's own closing rule is that a support question it cannot answer is a bug in the page — the mirror of that is a promise the page makes that the product does not keep.

3. Three rows carry one mark for two different reasons — the same conflation this PR removes between rows, kept inside a row. "Triggers and PL/pgSQL function bodies" pairs the one operation with no relation lock at all with one that blocks writers. "Views, materialized views" pairs a brief catalog swap with a rebuild that holds ACCESS EXCLUSIVE for as long as the query takes — and the row's own text calls that refresh "a data operation", which is 🔵 by this PR's taxonomy, not ⚪. "Standalone sequences, publications/subscriptions" pairs genuinely transactional sequence DDL with what the row itself calls "replication provisioning" — again 🔵 by the taxonomy, and ALTER PUBLICATION … ADD TABLE takes ShareUpdateExclusive on the table besides. Splitting these three rows costs three table lines and makes every mark on the page mean exactly one thing, which is the whole premise.

4. (nit) The tier table still defines T3 as two categories, one line above the three marks. T3 reads "No online-safety problem to solve, or solving it belongs to a different tool class" — which excludes ❌, a T3 mark whose own column says "Yes — unsolvable today". The tier table is the definition users read first; it should carry all three.

Action items

  1. (Finding 1) Tighten the ⚪ definition to what it actually warrants (brief catalog lock, no scan, no rewrite) and add the queue caveat that the PARTITION OF row already carries to the trigger, view, and greenfield-FK rows — a user following ⚪ to psql should know they are taking a lock that waits.
  2. (Finding 2) Make the refusal name the tool class, following the CREATE TABLE refusal's shape — or, if that lands later, reword the three "the engine says so" sentences in README.md, vision.md, and capabilities.md to describe the page instead.
  3. (Finding 3) Split the trigger/function, view/matview, and sequence/publication rows so no row carries two marks' worth of meaning.
  4. (Finding 4) Add the third category to the T3 row of the tier table.
  5. (optional) The greenfield row is scoped as "bootstrap an empty database", but the greenfield path is reached whenever the table does not exist — including a new table added to a populated production database, which is the common case and the one where the referenced-table lock bites. Worth widening the parenthetical.

Verified (tried to break, couldn't)

The re-marking is disciplined: I extracted every status mark from both revisions and compared them, and the change is exactly 11 rows moving off ❌ (6 → ⚪, 5 → 🔵) with no row added, dropped, renamed, or re-tiered anywhere else — no ✅ or 🟡 row was touched while the column was inserted, which is the thing that would have hidden a real capability claim inside a formatting change. The counts back the body precisely: 17 ✅, 18 🟡, 6 ⚪, 5 🔵, and exactly 2 ❌, so "only two rows in the whole matrix are genuinely impossible today" is literally true rather than rhetorically true. Both survivors are correctly classified — PostgreSQL offers no NOT VALID/USING INDEX equivalent for EXCLUDE builds, and no supported version adopts an index on a partitioned parent — and the safety column's third value ("Yes — unsolvable today") is covered by the legend's parenthetical rather than being an undefined mark. All five 🔵 rows are unambiguous, and the tool classes they name are the right ones. On the ⚪ rows I could not fault: enum/domain creation, extension creation, sequence DDL, and CREATE OR REPLACE FUNCTION are transactional catalog work and take no lock on any live table — I checked the function case directly rather than assuming it. The vision.md paragraph does the hardest thing in the PR well: reconciling "one tool for all schema changes" with "do one thing" as depth-not-breadth is a real argument, not a slogan, and the "What pg-sprite is not" framing line makes the section read as one boundary seen from several angles instead of a list of apologies. Docs-only diff, so the code jobs correctly skip; DCO, changes, and all-green pass. Leak check clean. Process note: the only bot comment is Codex reporting it is out of quota, so no automated review ran.

This review was generated by Claude Code (claude-opus-5).

@aparajon

Copy link
Copy Markdown
Collaborator

🤖 Second pass, same head (e594499), through the two lenses @aparajon asks pg-sprite changes to be judged on: how easily an outside team adopts this, and the seam an orchestrator embedding the engine consumes. Correctness findings are in the comment above; nothing here blocks.

Lens 1 — OSS adoption

This is the highest-leverage doc change in the repo, because it fixes the single sentence an evaluator forms in the first thirty seconds. Before this PR, a reader scrolling the matrix saw thirteen ❌ marks and concluded "narrow tool, lots of gaps". After it, they see two — and the other eleven read as a scope statement, which is the thing pg-sprite is actually unusual for having thought about. The insight that carries the PR is that ❌ was doing two incompatible jobs (a limitation and a boundary) and that a reader cannot tell them apart, so the page was misrepresenting the tool to exactly the audience it was written for. The "Online-safety problem?" column then answers the question the old page left hanging — a reader who hits a boundary now learns what to reach for instead, in the same row, rather than closing the tab.

The change I'd make next is not more rows, it's a number at the top. Nobody reads 48 rows, and nobody quotes them in the Slack thread where the adoption decision actually happens — they quote one line. The page now has that line available for the first time and doesn't print it: 48 operations — 17 supported today, 18 planned with a typed refusal, 11 out of scope by design, 2 with no online mechanism in PostgreSQL. Put that immediately under the matrix heading, and the page's argument survives being skimmed, screenshotted, or pasted into a thread. That framing also does something the prose can't: "2 of 48" is a defensible, checkable claim, and it invites the reader to go verify it rather than trust the tone.

Second: this page and the buzz replay in #58 now measure the same question from opposite directions, and neither knows about the other. #58 replayed a real service's history and found that 25 of its 32 typed refusals were CREATE TABLE — which, by this page's new taxonomy, is not a limitation at all. That is this PR's thesis, confirmed empirically against somebody else's schema, and it is the most persuasive evidence the project has for the claim it is making here. The two artifacts should cite each other: this page gets a line pointing at the replay as measured evidence that the boundary is where it says it is, and the replay's bucket summary adopts these marks so its output stops reporting scope boundaries and capability gaps in one number. Right now a reader can come away from #58 believing the engine handles half of a real workload and from this page believing it handles nearly all of it, and both readings come from the same underlying facts.

Lens 2 — the seam an orchestrator consumes

The taxonomy this PR introduces is a classification the engine could emit, and today it exists only as prose in a markdown file — which is why finding 2 in the comment above is a drift that shipped on day one. Compare the discipline already applied one layer down: #53 added verdict.Reasons() plus a docs test that pins every reason token to a row in the refusal-reason table, so a rename fails the build. The ⚪/🔵/❌ distinction has no such anchor — no enum, no test, no engine field — so the page and the product can disagree freely, and they already do: four different tool classes come back as one unsupported-statement with one message. The fix that makes the prose true by construction is small and follows the existing pattern: an out-of-scope class on the refusal (no-online-safety-problem / different-tool-class / no-online-mechanism), with the tool class as a field where one applies, pinned to this page's marks by the same style of docs test. That is worth doing for its own sake rather than to satisfy the doc — an embedder that wants to tell a user "this is a data change, use your data-change runner" currently has to string-match a sentence that is identical across every case.

For a consumer, the more consequential distinction here is that ⚪ and 🔵 are routable and ❌ is not. An orchestrator receiving a 🔵 knows the change is real work that belongs somewhere else and can say so; a ⚪ means "hand this to the owner's own tooling, unchanged"; an ❌ means stop, there is nothing anyone can do online. Those are three different UX outcomes in a pull-request comment or a CI gate, and today they are one exit code with one reason token. The exit-code contract stays right as it is — the page correctly guards "0 means online-safe" as the product — but a sub-classification underneath exit 2 is the piece a driver needs to turn a refusal into an instruction. Worth designing now, while the taxonomy is fresh and only eleven rows carry it, rather than after consumers have started parsing detail strings.

One thing worth keeping exactly as it is: the decision to state the philosophy in vision.md and README.md rather than only in the matrix, and specifically the sentence reconciling "one tool for all schema changes" with "do one thing" as depth across every table-shape change, never sprawl across object types. That is the argument that makes the eleven out-of-scope rows read as conviction rather than as a backlog, and it is the part a competing tool cannot copy by adding features. It will be tempting to soften it the first time a user asks why the engine won't create their enum; don't.

This review was generated by Claude Code (claude-opus-5).

@aparajon aparajon 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 — the re-marking is the right change and the taxonomy is a real improvement. Action items 1 and 2 from the review above are worth landing before merge (three ⚪ rows send users to psql for locks that block writers or readers, and the "names the tool class" claim isn't true of the engine today). Approving so the fix can land as follow-up commits rather than a re-review round.

This review was generated by Claude Code (claude-opus-5).

The ⚪ mark overclaimed: triggers, view replaces, and greenfield FKs take
brief locks on live tables that queue behind long-running queries. Tighten
the definition to no-scan/no-rewrite, add queue caveats, split rows that
carried one mark for two reasons, and stop claiming the engine names the
tool class (this page does; refusals are undifferentiated today).
@Kiran01bm

Copy link
Copy Markdown
Collaborator Author

Review response from Kiran's (@Kiran01bm) code review assessment agent (Amp / Claude Opus 4.5)

Summary: all five correctness action items (four findings + the optional fifth) are fixed in the follow-up commit; the engine-side refusal classification both passes recommend is accepted and deferred to follow-up PRs, now designed and in flight.

# Finding Status Explanation
1 Three ⚪ rows send users to psql for operations that take a write/reader-blocking lock on a live table fixed ⚪ definition tightened to "no scan, no rewrite; at most a brief catalog lock"; the trigger, view-replace, and greenfield-FK rows now carry the same queue caveat as the PARTITION OF row, with lock_timeout guidance
2 "The engine says so with a typed refusal that names the tool class" is not true of the engine today fixed (docs) / deferred (engine) All three sentences (README, vision, capabilities invariant) reworded: the engine refuses with a typed reason, and this page names the tool class — the invariant paragraph now states plainly that today's refusal text is one undifferentiated unsupported-statement reason. Carrying the classification into the refusal itself is deferred as a follow-up (see lens items below)
3 Three rows carry one mark for two different reasons fixed Split into six rows: views/matview DDL (⚪, with queue caveat) vs REFRESH MATERIALIZED VIEW (🔵, data operation); CREATE OR REPLACE FUNCTION (⚪, no relation lock) vs CREATE TRIGGER (⚪, SHARE ROW EXCLUSIVE caveat); standalone sequences (⚪) vs publications/subscriptions (🔵, with the SHARE UPDATE EXCLUSIVE note). Matrix is now 51 rows: 17 ✅ / 18 🟡 / 7 ⚪ / 7 🔵 / 2 ❌
4 Tier table defines T3 as two categories, excluding ❌ fixed T3 row now carries all three categories, including "PostgreSQL offers no online mechanism to build on"
5 (optional) Greenfield scope reads as "empty database" but fires whenever the table doesn't exist fixed Parenthetical widened to "a fresh database or a new table in a live one"; the row also gained the referenced-table lock caveat from finding 1
L1 Nobody quotes 48 rows — put the number at the top fixed One-line summary added directly under the matrix heading: 51 operations — 17 supported today, 18 planned behind a typed refusal, 14 out of scope by design, 2 with no online mechanism in PostgreSQL
L2 This page and the corpus-replay work should cite each other deferred The replay PR is still open; linking it from this page now would land a dead path if this PR merges first. The cross-cite goes in when the replay lands
L3 The taxonomy exists only as prose — carry it into the engine as a typed out-of-scope class + tool-class field, pinned by a docs test deferred Accepted, designed, and split into follow-up PRs: statement-family recognition at the parse boundary (the gate already switches on real grammar node types), then additive out_of_scope / tool_class verdict fields with closed-set functions pinned to this page's marks by the same style of docs test the plan-report vocabulary uses
L4 ⚪/🔵/❌ are three different routable outcomes under exit 2 deferred Same follow-up as L3: the fields are additive under the unchanged exit-code contract and reason token, so existing consumers keep working while orchestrators gain the routing distinction
L5 Keep the vision.md depth-not-breadth sentence exactly as it is no action Kept verbatim

@Kiran01bm
Kiran01bm merged commit 7888823 into main Aug 24, 2026
8 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.

2 participants