What's wrong
GET /queries returns the 250 most recent queries each carrying its complete result set, and the renderer holds all of them forever.
query-runner.ts:291-300 — list is a bare client.select().from(queriesTable).orderBy(desc(queriedAt)).limit(250). Full rows, no projection.
schemas.ts:255-264 — QueryDto.result: NullOr(QueryResultDto), and QueryResultDto carries the full rows array.
adapter.ts:3 — maxResultRows = 10_000.
So one boot fetch can pull up to 250 complete result sets into renderer memory.
Nothing removes a row and nothing refetches to server truth. The repo-wide utils.write* grep shows writeDelete only for databases (mutations.ts:125) and worksheets (mutations.ts:143) — queries is written at collections.ts:79, queries.ts:209 and mutations.ts:57 and never deleted. No invalidateQueries({queryKey: queryKeys.queries}) exists anywhere (all five invalidate sites are worksheets/databases), and query-client.ts:7 sets refetchOnWindowFocus: false.
Nobody needs the payload
What the renderer actually consumes from this list:
- the single latest query of the open worksheet (
App.tsx:119-128, a filter+sort over the whole array)
- per-worksheet message metadata —
queriedAt, finishedAt, error, result.rowCount, result.truncated (use-worksheet-messages.ts:31-59)
The active worksheet's live result is separately re-fetched by the poller (queries.ts:161-180) and also arrives on the POST /queries response (collections.ts:67-79). The full row payload in the list is needed by nobody.
Proposed change
GET /queries stops carrying result.rows.
That collapses "250 complete result sets held forever" into "a bounded list of small rows plus the current result" by deleting a field, not by adding a policy — and it is exactly what CLAUDE.md's own memory rule asks for: "projections that exclude large fields."
Scope
src/glue/api/schemas.ts (a list-row variant of QueryDto without result.rows) and query-runner.ts:291-300.
Sequencing: this is a QueryDto contract change. Land it on top of the QueryDto status-discriminant work (see that issue), which rewrites the same type — this is a projection over that union, not a competing shape. Its independently-shippable first half (deleting the dead top-level truncated) is the natural prerequisite.
What was narrowed from the original finding
Two corrections against the first framing, both worth keeping honest:
- "Unbounded" overstates it. Growth beyond the 250-row boot fetch is one row per query the user runs in this window. The accurate claim is a large fixed boot payload plus session-proportional growth, not a leak.
- Adding eviction to
collections.ts was rejected as the fix — it adds a retention policy rather than removing a representation. Keep it as a fallback only if the projection turns out to be blocked.
Risks
Any renderer read of result.rows off a list row breaks. Today only the latest-query path does, and it is already backed by the per-id fetch — verify that before landing. The messages log is derived and capped at 200 (use-worksheet-messages.ts:17), so it is unaffected. test-utils.tsx:58-62 seeds collections from the query cache, and fixtures carrying full results keep working.
Validation
Existing App.test.tsx ("running a query"), the use-worksheet-messages tests, and the ResultsPane tests. Add a server test asserting the list response omits rows while GET /queries/:id still returns them.
Found in a codebase-wide simplification audit (F-S20-b). Confidence: medium — evidence exact, value depends on a contract change shared with the QueryDto union work.
What's wrong
GET /queriesreturns the 250 most recent queries each carrying its complete result set, and the renderer holds all of them forever.query-runner.ts:291-300—listis a bareclient.select().from(queriesTable).orderBy(desc(queriedAt)).limit(250). Full rows, no projection.schemas.ts:255-264—QueryDto.result: NullOr(QueryResultDto), andQueryResultDtocarries the fullrowsarray.adapter.ts:3—maxResultRows = 10_000.So one boot fetch can pull up to 250 complete result sets into renderer memory.
Nothing removes a row and nothing refetches to server truth. The repo-wide
utils.write*grep showswriteDeleteonly fordatabases(mutations.ts:125) andworksheets(mutations.ts:143) —queriesis written atcollections.ts:79,queries.ts:209andmutations.ts:57and never deleted. NoinvalidateQueries({queryKey: queryKeys.queries})exists anywhere (all five invalidate sites areworksheets/databases), andquery-client.ts:7setsrefetchOnWindowFocus: false.Nobody needs the payload
What the renderer actually consumes from this list:
App.tsx:119-128, a filter+sort over the whole array)queriedAt,finishedAt,error,result.rowCount,result.truncated(use-worksheet-messages.ts:31-59)The active worksheet's live result is separately re-fetched by the poller (
queries.ts:161-180) and also arrives on thePOST /queriesresponse (collections.ts:67-79). The full row payload in the list is needed by nobody.Proposed change
GET /queriesstops carryingresult.rows.That collapses "250 complete result sets held forever" into "a bounded list of small rows plus the current result" by deleting a field, not by adding a policy — and it is exactly what CLAUDE.md's own memory rule asks for: "projections that exclude large fields."
Scope
src/glue/api/schemas.ts(a list-row variant ofQueryDtowithoutresult.rows) andquery-runner.ts:291-300.Sequencing: this is a
QueryDtocontract change. Land it on top of theQueryDtostatus-discriminant work (see that issue), which rewrites the same type — this is a projection over that union, not a competing shape. Its independently-shippable first half (deleting the dead top-leveltruncated) is the natural prerequisite.What was narrowed from the original finding
Two corrections against the first framing, both worth keeping honest:
collections.tswas rejected as the fix — it adds a retention policy rather than removing a representation. Keep it as a fallback only if the projection turns out to be blocked.Risks
Any renderer read of
result.rowsoff a list row breaks. Today only the latest-query path does, and it is already backed by the per-id fetch — verify that before landing. The messages log is derived and capped at 200 (use-worksheet-messages.ts:17), so it is unaffected.test-utils.tsx:58-62seeds collections from the query cache, and fixtures carrying full results keep working.Validation
Existing
App.test.tsx("running a query"), theuse-worksheet-messagestests, and theResultsPanetests. Add a server test asserting the list response omitsrowswhileGET /queries/:idstill returns them.Found in a codebase-wide simplification audit (F-S20-b). Confidence: medium — evidence exact, value depends on a contract change shared with the QueryDto union work.