Skip to content

GET /queries ships 250 full result sets the renderer never reads #91

Description

@Artmann

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-300list is a bare client.select().from(queriesTable).orderBy(desc(queriedAt)).limit(250). Full rows, no projection.
  • schemas.ts:255-264QueryDto.result: NullOr(QueryResultDto), and QueryResultDto carries the full rows array.
  • adapter.ts:3maxResultRows = 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:

  1. "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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions