Files
hq/04-ISSUES/193-the-store-seats-read-only-query-is-read-only-by-convention/00-report.md
T
jschoubben b13ef1be81 Issues 192 and 193: the console reaches a person only by hand; the store's read-only query is not
192: no provision says where the console is, its port was never assigned, and nothing
owns a person's agent configuration since the predecessor left. 193: the query verb wraps
the caller's text in a read-only transaction the text can end, and its rows come back
keyed by BEGIN.
2026-10-02 00:02:51 +02:00

2.9 KiB

status, opened, located-in, fixed-by, amended-design
status opened located-in fixed-by amended-design
located 2026-10-02
mesh-catalog modules/postgres/client.ts (readOnlyQuery)

193 — The store seat's read-only query is read-only by convention, and its answer is unreadable

What was observed

Asking the store seat's query verb for a count through the console returned this. Rows are each wrapped in an object under a key named BEGIN: the column name, then the value, then the word ROLLBACK. A query returning nothing gave the column name and ROLLBACK alone. The answer to select count(*) as n from <table> was:

rows: [ {BEGIN: "n"}, {BEGIN: "46"}, {BEGIN: "ROLLBACK"} ]

A reader can work it out. A program cannot, and a query with two columns loses which value belongs to which.

Why this is here

The cause is the same line that makes the query read-only. The holder's tool sends BEGIN TRANSACTION READ ONLY; <the caller's statement>; ROLLBACK; to the command-line client as one string. The client prints a command tag for each of the three statements, and the parser takes the first line, BEGIN, as the header.

And it is not read-only. ADR 0159 decided the store seat's query verb is "one read-only statement against one database". The only thing enforcing that is the wrapping transaction, and the caller's statement is pasted inside it as text. A statement that begins by ending the transaction (a commit, then anything) runs whatever follows it outside the read-only transaction, with the holder's own role, the administrative one that creates every consumer's role and database. A rule stated in a decision and enforced by string concatenation is enforced by nothing.

This is read from the code, not tried against the live store, and it should not be tried there. The lab bed is where it gets proven.

Every caller with invokes on the store seat's query can do this. The console has invokes: ["*"], so that includes anyone logged in on a machine running the console.

What a fix looks like

  • One statement, refused otherwise. Send the caller's statement alone, through the client's single-statement path (the extended protocol takes one statement per call and refuses more). The read-only property then comes from the session, not from text around the statement.
  • Read-only by role, not by transaction. Run the verb as a role that can only read, granted pg_read_all_data, not as the administrative role. A statement that escapes every wrapper still cannot write.
  • Rows as rows. Parse the client's output with the column names it returns, or use a driver instead of the command-line client, so a row is an object keyed by its columns.
  • The check 0159 lacks: a test that sends a commit followed by a write and asserts the write is refused and nothing changed. Another asserts a two-column row comes back keyed by both columns.