Skip to content

Read PostgreSQL plans: the MCP must answer with a plan, not a pointer to one #2567

Description

@erikdarlingdata

Child of #2538, blocked on #2566 (storage) and #2565 (mechanism).

The reads that make captured plans useful, once there are any.

What it has to do

Behaviour when there is no plan

This is most of the work, and the machinery now exists. The honest answers are all different and must not collapse into one:

Two Query Store reads were previously guessing in prose between exactly this kind of ambiguity ("Query Store may not be enabled on target databases"), which #2557 replaced with measured facts. Do not reintroduce the pattern here.

Aurora-only, probably

If the mechanism chosen in #2565 depends on pg_stat_statements, this read inherits its AppliesTo gate — and per #2547 the Aurora-only reads are shown on stock PostgreSQL with the not_collected sentence, not hidden. Follow that.

Activity

  1. erikdarlingdata commented on Aug 24, 2026

    @erikdarlingdata
    OwnerAuthor

    Still blocked on storage, but #2565 is answered and it changes one of the three no-plan cases listed here — worth recording before anyone builds this.

    The mechanism is a collector running EXPLAIN (GENERIC_PLAN, FORMAT JSON) over statements already in pg_stat_statements, because auto_explain writes to the server log and a SQL connection cannot read it (measurements on #2565).

    Consequences for this read:

    The middle case does not exist as written. "Capture is configured but this query never crossed the threshold" is an auto_explain concept — a duration threshold the server applies. We capture on OUR schedule, for statements WE select from pg_stat_statements. So the honest middle case becomes "this statement is in the store but we have not explained it yet", which is a different sentence and a different fix (widen what the collector explains, or wait a cycle). Reusing the threshold wording would describe a mechanism we do not use.

    A fourth case appears, and it is the one most likely to bite. EXPLAIN can fail per-statement where collection is otherwise healthy: the statement references an object the monitoring login cannot see, or is a utility statement EXPLAIN refuses, or GENERIC_PLAN rejects it because the parameters are not in a position the planner can generalise. That is per-statement, not per-server, so it cannot be reported as a server-level precondition — it belongs on the row.

    And the plan we return is not the plan that ran. GENERIC_PLAN is what the planner would choose for an unknown parameter. For a parameter-sensitive workload it can differ materially from the executed plan. The read must say so in its own words — an agent with no viewer will otherwise treat it as observed truth, which is exactly the "guessing in prose" failure this issue warns against, arriving from the opposite direction.

    What is unchanged and confirmed: queryid serializes as a string (#2548/#2553); the spine is pg_statement_stats' queryid; and per #2547 an Aurora-gated read is SHOWN on stock PostgreSQL with the not_collected sentence rather than hidden.

    Retention wording is now precise: #2566's recommendation puts PostgreSQL plans in their own dimension registered under the existing plan_content_retention_days, so "the plan existed and was aged out" is a real, nameable state with a knob to cite — not a guess.

  2. erikdarlingdata commented on Aug 24, 2026

    @erikdarlingdata
    OwnerAuthor

    Half-unblocked: #2565 is closed (mechanism is auto_explain, threshold-driven). #2566 is still open but now has a recommendation on it. Two things from the measurement work change this issue directly.

    The "no plan" list is missing the state that will dominate

    This issue names three honest answers and is right that they must not collapse. There are five, and the two missing ones are the ones a first implementation will get wrong.

    4. The plan was captured and cannot be attributed. auto_explain puts no query identifier in the plan — measured on PostgreSQL 17, even with compute_query_id=on. The id exists only in the log line prefix, and only when log_line_prefix carries %Q, where it matches pg_stat_statements.queryid exactly. Without it, plans are being written and none of them can be joined to a queryid — so a read keyed on queryid finds nothing while the server is doing everything asked of it.

    This is worse than the other four states because every other signal says capture is working. It is now a plan_attribution facet on pg_plan_capture_readiness (#2584, merged), so this read can distinguish it and give the specific remedy, which is a log_line_prefix edit and needs no restart.

    5. The plan aged out of the SERVER log before we collected it, which is distinct from our own retention. On this Aurora fleet rds.log_retention_period is 4320 minutes (3 days), and plans come back through the RDS log API. A plan older than that is gone at the source and no plan_content_retention_days setting of ours affects it. Two different retention boundaries, two different sentences — collapsing them would tell someone to raise a knob that cannot help.

    So the vocabulary this read needs is: not-configured (precondition), configured-but-unattributable (precondition, different remedy), never-crossed-the-threshold (genuinely fine), aged-out-of-the-server-log, aged-out-of-our-store. All five are now measurable rather than guessable, which is the standard #2557 set.

    The queryid-as-string point is more load-bearing here than elsewhere

    Confirmed in the raw data: pg_stat_statements.queryid for a trivial query came back as -4828029293864693941. That is 19 digits and negative — comfortably past 2^53, so JSON.parse mangles it. This read's entire spine is that value, so the #2548/#2553 string-on-the-wire rule is not a nicety here; get it wrong and the join silently returns the wrong plan rather than no plan.

    One shape consequence from #2566

    My recommendation there is to store the plan and the queryid and discard Query Text — because auto_explain logs literals verbatim, and that is both a data-handling exposure and the reason plan dedup cannot work on the raw blob.

    If that is accepted, this read cannot echo the captured statement text back and must resolve it from pg_stat_statements via the queryid. That is better anyway — the text from there is normalised and literal-free — but it makes the join mandatory rather than convenient, so the read cannot be built to work without it.

    Still blocked on #2566 for storage shape.

  3. erikdarlingdata commented on Aug 25, 2026

    @erikdarlingdata
    OwnerAuthor

    Shipped in #2614.

    get_pg_plans returns the plan as navigable JSON — not an id, not an opaque string. #2538's constraint is met: an agent has no viewer to follow a reference into.

    Every requirement this issue listed:

    One thing this issue anticipated that turned out differently: it expected the read might be Aurora-only. It is the reverse. The mechanism is auto_explain, which writes to the server log, and reading that log needs pg_read_server_files plus an explicit GRANT EXECUTE ON FUNCTION pg_read_file — impossible on Aurora and RDS, which have no filesystem. So this is a self-hosted capability that is dark on Aurora, and the capability phrase names the log rather than the engine.

    Nothing here re-derives redaction: plans are stripped at collection (#2566), so there is no un-redacted copy in the store for a read to leak.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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