Skip to content

Engine-kind capability: the PostgreSQL AppliesTo axis, and the reads that still cannot answer #2532

Closed

Description

@erikdarlingdata

Two deferrals from #2531 (the #2530 foundation), both about the same mechanism, both filed rather than smuggled in because each needs a guard that does not exist yet.

1. The PostgreSQL AppliesTo axis, and the guard it needs first

#2531 taught CollectorEngineCapability the engine-KIND axis, but only the engine half of the dispatch gate: "a SQL Server collector is never sent at a PostgreSQL target, and vice versa". The PostgreSQL collectors' own AppliesTo gates still answer unavailable, exactly as msdb access does on the SQL Server side.

That leaves a real claim on the table. Now that the store records aurora-postgres distinctly from postgres, Aurora-ness is fixed by the engine kind the way IsAzureSqlDb / IsAzureManagedInstance are fixed by the engine edition. So pg_wait_stats — which reads aurora_stat_system_waits(), a surface core PostgreSQL has in no version — is a genuine permanent gap on a stock PostgreSQL target, and today its read tells the operator to go check that collection is running.

The mechanism is the exact mirror of the edition axis:

  • a TargetsWithEngineKind(engineKind) sweep, with IsAurora fixed by the kind and PostgresMajorVersion / PostgresVersionNum / IsInRecovery varied (all three are fixable — an upgrade moves a floor, a writer connection moves recovery state — so a permanence claim over them would be the On Azure SQL DB, twelve reads tell you to go start a capture that cannot exist there #2511 over-claim one engine over);
  • IsCollectedOnEngineKind asking CollectorCatalog.AppliesTo over that sweep instead of EngineMatches alone;
  • capture-path prose entries for the PostgreSQL collectors that are genuinely gated, so the message is specific rather than "the data this read is served from".

Do not ship the sweep without the guard. CollectorEngineCapabilitySweepDimensionTests.EveryFactASqlServerGateReads_IsVariedBySweepOrFixedByEdition (#2518) exists because a sweep that leaves a gated fact at its CLR default reports a permanent engine gap for a collector that runs perfectly well — silently, because the gap set still looks plausible. It decodes the IL of every SQL Server definition's AppliesTo and fails if any fact the sweep never varies is read by one. A PostgreSQL twin is the prerequisite, and the machinery in that file already generalises: FactsReadBySqlServerGates filters by TargetEngine, so a PostgreSQL variant is a filter change plus a role table keyed off TargetsWithEngineKind.

Measurement that decides whether it is worth doing at all: the fleet today is entirely Aurora, so the stock-PostgreSQL claim buys nothing until a non-Aurora target is monitored. The recovery-state and version-floor halves are already correctly declined. The claim to check before starting is whether any monitored PostgreSQL target is not Aurora — if none is and none is planned, this is theory.

2. Only some reads can answer at all

#2511 wired the capability helper into the reads whose collectors are gated off on Azure SQL Database — twelve collectors' worth. That was the right scope for the edition axis, because a collector that runs on every SQL Server had nothing to say.

The kind axis changes the arithmetic completely: on a PostgreSQL target, every SQL Server read is a permanent gap, not just twelve. So get_database_config, get_wait_stats, get_top_queries_by_cpu and the rest still answer:

unavailable — No database configuration data available. The config collector may not have run yet.

...on a target where that collector will never run. EngineCapabilityMissLivePostgresTests.APostgresTarget_AnswersNotCollectedNamingTheEngine_WhileAnUnprobedServerKeepsItsMiss asserts this current behaviour explicitly, so the gap cannot be mistaken later for a regression.

Options, with the trade-off each carries:

  1. Wire every SQL Server read to the helper. Correct and mechanical, but it is dozens of files and each call site is a hand edit — precisely the shape that goes 90% done and stays that way. EngineCapabilityReadWiringTests.BothSkus_WireTheSameReadsToTheSameCollectors would need to grow into a completeness ratchet ("every read naming a collector consults the helper") or the last few will never land.
  2. Do nothing, and let the UI make it moot. Once PostgreSQL targets have no UI on either SKU, and both UIs render SQL Server tabs at them #2530's UI waves land, a human pointed at a PostgreSQL target sees PostgreSQL tabs and never asks a SQL Server read. But an agent over MCP has no tabs — it asks by name — and agents are half the product's surface, so this only closes the human half.
  3. Answer at the resolver instead of per read. One gate where a server is resolved, rather than N call sites. Cheapest by far, and the risk is exactly the one DarlingEngineCapability's "called only on the miss path" note guards against: a server whose registry row says one engine while its collected rows say another would get a confident explanation instead of its data. Only safe if it stays on the miss path.

Option 3 looks right on cost and needs the miss-path property proved rather than assumed.

Activity

  1. erikdarlingdata commented on Aug 22, 2026

    @erikdarlingdata
    OwnerAuthor

    Both halves are up, as two PRs against dev rather than one, because the read wiring is the user-visible half and should not have waited on the derivation guard.

    Neither depends on the other. Their only overlap is CapturePathByCollector (adjacent entries) and EngineCapabilityMissTests.cs, and the test-file hunks are byte-identical in both branches on purpose, so whichever lands first the second needs a one-hunk rebase on the dictionary and nothing else.

    What each option cost, since the issue asked

    Option 1 (wire every read) is what #2537 does, and the fear that it "goes 90% done and stays that way" was right about the shape but wrong about the cost: the transformation is uniform enough to be applied mechanically and then checked by the pins, and the result is 72 Darling reads and 71 Lite reads, up from 17 each. The completeness ratchet the issue asked for is EveryWiredRead_NamesARealCollectorWhoseCapabilityBranchCanFire plus the new converse on the prose table — an entry must be gated by its own gate somewhere or named by a wired read, so neither side can drift alone.

    Option 3 (answer at the resolver) was not taken, and the reason is the one the issue named: the miss-path property. Answering where a server is resolved means answering before the read, and DarlingEngineCapability's "called only on the miss path" note exists because a server whose registry row says one engine while its collected rows say another must get its DATA, not a confident explanation of why it cannot have any. The resolver cannot know that the read is about to come back empty. Per-call-site is more edits and it is the only version that keeps the property structurally rather than by argument.

    Option 2 (let the UI make it moot) closes the human half only, as the issue said. The agent half is what #2537 closes — including the part that is not a call site: the miss-vocabulary paragraph in both SKUs' MCP instructions explained the permanent engine gap with one example, Azure SQL Database, because that was the only shape producing one when #2511 wrote it. It now also says that a PostgreSQL target collects none of the SQL Server signals and that the get_pg_* family is what answers there, which is the only place an agent can learn it, and it is pinned in McpMissMessageParityPinTests so the two copies cannot drift.

    What is deliberately left, and it is the only residue

    Six reads roll up a dozen collectors each and can name none of them: analyze_server, get_analysis_facts, compare_analysis, get_daily_summary, get_daily_summary_range, get_server_summary. Wiring one of them to a representative collector would be a specific message about the wrong thing. The honest answer is a rollup-level one — every input to this read is a SQL Server collector, so on this engine there is nothing to roll up — which is a second message shape and a second entry point on CollectorEngineCapability, not a call-site edit.

    The measurement that should decide it: whether a PostgreSQL target's analyze_server is actually asked. get_daily_summary and analyze_server are the two highest-traffic reads on the SQL Server side, so the prior is that they are; if the fleet's MCP logs say otherwise, this is not worth a second message shape.

    Three groups beyond that are not residue and should not be re-opened as one:

    • get_collection_health and get_collection_log are served by collection_log, which a PostgreSQL target genuinely populates. Their messages are about collection and are correct there.
    • list_servers, get_fleet_overview, get_store_metrics, the alert tools and the custom-view tools resolve no single server, so there is no engine to speak for.
    • get_wait_trend and get_perfmon_trend's "not a wait type / counter this server collects" branches are only reached once the collector HAS produced rows, so the engine cannot be in question. Their sibling "nothing collected at all" branches are wired.

    The claim this issue said to check first

    The claim to check before starting is whether any monitored PostgreSQL target is not Aurora — if none is and none is planned, this is theory.

    I could not re-measure the fleet from this session, so I am recording that rather than asserting it. On the stated all-Aurora fleet the stock-PostgreSQL claim buys nothing today. What #2536 buys immediately is the rest of it: the eight get_pg_* reads had never asked the capability question at all, and five of them answer a miss with a bespoke all-clear rather than unavailable — a SQL Server target asked get_pg_xmin_horizon was told "nothing is holding back the xmin horizon", and get_pg_autovacuum_health reported "no table on this server has dead tuples". Those are not weak answers about a mechanism the engine lacks; they are fabricated clean bills of health, and an agent asking by name is exactly who got them.

    And the guard is worth having whether or not the claim is: EveryFactAPostgresGateReads_IsVariedBySweepOrFixedByKind fires the moment a PostgreSQL gate is written on a fact the sweep leaves at its CLR default, which is the failure that would otherwise announce a permanent gap for a collector that runs. Proven red by fixing IsInRecovery at false — pg_autovacuum_stats is then reported as a permanent gap on both PostgreSQL tokens and the guard names it.

    One thing in the issue that turned out not to hold

    The issue expected the PostgreSQL twin to be "a filter change plus a role table keyed off TargetsWithEngineKind", and the filter change was exactly that — FactsReadBySqlServerGates became FactsReadByGatesOf(engine). The role table was not: RoleOf was hard-wired to the edition sweep, and SweepRole.FixedByEdition is the wrong name once a second axis exists. It is now parameterised over the shape families each axis produces, with the member renamed FixedByAxis, so the two axes share one piece of arithmetic instead of two that agree today.

  2. added 2 commits that reference this issue on Aug 22, 2026
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