Repository navigation
pg_statement_stats: the pg_stat_statements_info epoch read is gated on the relation's existence, not the server major — the collector runs again on clusters whose extension is below 1.9 or outside public (#3818) - #3824
Merged
Conversation
…n the relation's existence, not the server major — the collector runs again on clusters whose extension is below 1.9 or outside public (#3818) The #3694 epoch column read pg_stat_statements_info behind `postgresMajorVersion >= 14`, conflating the engine major with the extension's catalog version: the view is created by the extension's 1.9 update script, and a 14+ engine upgraded in place keeps the extension at 1.8 until ALTER EXTENSION ... UPDATE is run by hand. On 23 of 50 clusters in an upgraded fleet the whole collector failed 42P01 on that one column for 25 hours with its base view readable. Measured on postgres:13/14/16 with the extension at 1.8, 1.9 and 1.10 (in a schema off the search_path): a static FROM of the view raises at parse analysis whatever WHERE or CASE surrounds it, so the read is query_to_xml of SQL text - resolved at execution - behind a to_regclass test on the name qualified with the extension's own schema from pg_extension. NULL at 1.8 with no error, filled after UPDATE, filled outside public, InitPlan once per statement. Ordinal 27 stays timestamptz; ReadAsync is unchanged. The fault mapping learns the difference between the extension's base object and a companion it gains at a later version: PgExtensionDependency.Companions, declared by pg_statement_stats for pg_stat_statements_info at 1.9, and a PostgresFaultOutcome arm that names the object the server named, says the extension IS installed, and gives ALTER EXTENSION ... UPDATE as the remedy - recorded as the general non-fatal bucket, never EXTENSION_MISSING, which every health surface reads as an optional module at rest. Pins in both test projects; a gated live-target proof (DARLING_TEST_PG_TARGET) that ran against the rig at 1.8 -> UPDATE -> schema ext.
erikdarlingdata
enabled auto-merge (squash)
September 20, 2026 18:40
This was referenced Sep 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The mechanism
#3694 appended the statements epoch to
pg_statement_statsas a scalar subquery overpublic.pg_stat_statements_info, gated onpostgresMajorVersion >= 14with the comment "extension 1.9, the same release as toplevel, so the same major guards both". That gate conflates the engine major with the extension's catalog version.pg_stat_statements_infois created by the extension's 1.8→1.9 update script, and a 14+ engine whose database was upgraded in place or restored keepspg_stat_statementsat 1.8 until someone runsALTER EXTENSION pg_stat_statements UPDATE— RDS and Aurora do not run it for you. On 23 of 50 PostgreSQL clusters in an upgraded fleet the whole collector failed42P01on that one column every cycle for ~25 hours, its base view readable the entire time, and the stored status said the extension was not installed. The hard-codedpublic.was the second failure mode: a relocatable extension lives whereverCREATE EXTENSION ... SCHEMAput it.What was measured, and why the fix has the shape it has
The issue proposed
(SELECT i.stats_reset FROM pg_stat_statements_info AS i WHERE to_regclass('pg_stat_statements_info') IS NOT NULL)as "a subquery that yields NULL when the relation is absent and never raises". That claim is false, and this PR records the measurement: onpostgres:14withCREATE EXTENSION pg_stat_statements VERSION '1.8'it raises42P01— a FROM item is resolved at parse analysis, before any WHERE runs. So doesCASE WHEN to_regclass(...) IS NULL THEN NULL ELSE (SELECT stats_reset FROM pg_stat_statements_info) END: CASE short-circuits evaluation, not name resolution.Only a read whose relation name is resolved at execution can be conditional on the relation existing, and core PostgreSQL has exactly one function that runs SQL text and returns its rows:
query_to_xml. The shipped form, nowPgStatementStatsCollector.StatementsEpochSql:The schema comes from
pg_extensionrather than the search_path, deliberately: the issue's unqualifiedto_regclass('pg_stat_statements_info')read NULL on the PG16 rig with the extension created in a schema off the path, which would have left the epoch dark there for a different reason. Measured onpostgres:13,14and16containers (desktop-linux): extension at 1.8 → NULL, no error, on 13 and 14;ALTER EXTENSION UPDATEto 1.9 → filled, and equal to a direct read of the view; 1.10 created in schemaextoff the search_path → filled; extension not created at all → NULL (the outer read fails first on its base object, which is the fault theEXTENSION_MISSINGsentence is for).EXPLAINshowsInitPlan 1 (returns $0)— once per statement, not per row.query_to_xmlrenders timestamptz as2026-09-20T17:54:26.803822+00:00, which the timestamptz input routine accepts, so ordinal 27 keeps its type andReadAsyncis untouched.The
>= 14guard stays ontoplevelas a proxy with its limit written down: measured on the same PG14 rig at 1.8,topleveldoes not exist either (42703) — the column belongs to the extension's view, not the engine. A column reference cannot be made conditional without a probe, the failing case is loud (42703 → Unclassified → ERROR) rather than silently wrong, and the failing production population is Aurora, whoseaurora_stat_statements()carries the column regardless. The comment says all of this; the brief's premise that "the server major is correct there" is not, and is reported as such.The status text: a missing companion is not the extension missing
PostgresFaultOutcome's #3240 arm recorded every 42P01/42883 on a collector that declares an extension asEXTENSION_MISSING— "the pg_stat_statements extension this collector reads is not installed on this target", withCREATE EXTENSIONand ashared_preload_librariesrestart as the remedy. All three false here.PgExtensionDependencygainsCompanions: objects the extension gains at a later version, which the collector reads beside its base object.pg_statement_statsdeclarespg_stat_statements_infoat1.9. A new arm, checked first because it is the narrower claim, reads the object the server named off the message (relation "…" does not exist/function …(— Npgsql's relation/function fields are not consulted because PostgreSQL fills them for constraint errors, not unresolved names) and, when it is a declared companion, records the general non-fatal bucket with a sentence that names the object, says the extension is installed in the named database, gives both producing states andALTER EXTENSION pg_stat_statements UPDATEas the remedy — neverEXTENSION_MISSING, whose status word every health surface reads as an optional module at rest. The inference "the base object resolved" rests on PostgreSQL resolving FROM before the select list and naming the first object it cannot find; the declaration's doc comment names that as the property a declarer must hold.Before:
relation "public.pg_stat_statements_info" does not exist (SQLSTATE 42P01) — the pg_stat_statements extension this collector reads is not installed on this target. This is NOT a missing grant, and no GRANT will change it: run CREATE EXTENSION pg_stat_statements in database 'appdb' … The module also has to be in shared_preload_libraries first, which takes a server restart …After:
relation "public.pg_stat_statements_info" does not exist (SQLSTATE 42P01) — the missing object is pg_stat_statements_info, which is NOT the pg_stat_statements extension's base object: that resolved, so the extension IS installed in database 'appdb' and this is not the extension missing. pg_stat_statements_info is created by the extension's 1.9 update script, so either the extension is present at a catalog version below 1.9 (… runs ALTER EXTENSION pg_stat_statements UPDATE in database 'appdb' — a statement, with no restart and no shared_preload_libraries change; RDS and Aurora do not run it for you) or the object is installed outside the schema the query text names (… check pg_extension.extversion and extnamespace). This is NOT a missing grant …What this does NOT do
FROM public.pg_stat_statements(Self-hosted PostgreSQL has no top-queries answer at all: pg_statement_stats is Aurora-only #2625's shape) — on a non-Aurora target with the extension outsidepublic, the base read still fails and lands on the (correct)EXTENSION_MISSINGarm.toplevelruntime-safe on vanilla 14+ at extension ≤ 1.8; see above. A follow-up could read it as(to_jsonb(s)->>'toplevel')::boolean, resolved at execution, at a per-row serialization cost — not for an urgent lane.ReadAsyncread. Lite has no PostgreSQL targets (McpEngineCapability.cs:60); its pins cover the shared collector.Tests
Lite.Tests/PgStatementStatsFlavorTests: the epoch column is the existence-gated constant on every major (13/14/17) and both flavors; noNULL::timestamp with time zone AS statements_stats_resetarm remains; no staticFROM … pg_stat_statements_info; the gate reads the extension's own schema and resolves the relation at execution; both CASE arms typed; the companion declaration (name, version, lowercase, bare, read by both flavors' text, and no other collector declares one). The Aurora pin that forbade\bpg_stat_statements\bis refined to a FROM of the view, since the epoch now names the extension as anextnameliteral.Darling.Tests/PostgresFaultOutcomeTests: companion 42P01 (qualified and bare) →PERMISSIONS, the sentence above, none of the three lies; base object missing (view 42P01, Aurora function 42883, and the bare "boom" every other pin sends) →EXTENSION_MISSINGas before; the companion arm is scoped to the declaring collector and falls back on an unknown database;MissingObjectNamedByreads the last segment of relation and function messages and returns null for anything else.Darling.Tests/PgStatementStatsInfoGateLiveTests(new, gated onDARLING_TEST_PG_TARGET— a superuser connection to a PostgreSQL withpg_stat_statementspreloaded; skips in CI, whose live cluster is the store): at 1.8 the epoch read is NULL alone and inside the collector's whole statement over a readable base view; afterUPDATEthe collector's ownBuildQuerytext fills it with the same instant a direct read returns, Kind Utc; created in schemaextoff the search_path it still fills. Ran green against thepostgres:14rig.PgExtensionDependencyContractTests; 37/37 in a Lite.Tests harness (flavor, delta-skip, Aurora-gate scan); the BRIEF census list (337 tests, 1 known Mac-only PresentationFramework red inLivePostgresCollectionHygieneTests, identical on clean dev); fullDarling.Tests11,868 with 188 platform reds (WPF/PresentationCore, SqlClient native config, Windows paths, PowerShell scripts), none in the touched area. Builds-c Release -p:EnableWindowsTargeting=truewith 0 warnings on every touched project.Closes #3818