Skip to content

PostgreSQL config change history sees per-database and per-role overrides: changed, set and reset, server-wide rows unchanged (#3937) - #3957

Merged
erikdarlingdata merged 4 commits into
devfrom
fix/3937-config-change-feed-scoped
Sep 23, 2026
Merged

erikdarlingdata merged 4 commits into
devfrom
fix/3937-config-change-feed-scoped

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Closes #3937. This is a derivative of #3691 (V138, the per-database and per-role override rows in pg_server_config).

What a PostgreSQL operator now learns

get_pg_server_config_changes, and its web twin, now reports ALTER DATABASE … SET and ALTER ROLE … SET history. Before this change it went silent on overrides. V138 had to exclude override rows from the server-wide LAG, because a same-name override in the same partition would invent a change on every snapshot. That exclusion was correct, and it left the feed blind to overrides. Even a per-scope LAG could only see a value moving on an override that already existed. It could never see one being SET, because a first row is hidden by prev_time IS NOT NULL, or RESET, because an absent row is never read.

The design

  • The server-wide read is the same statement. ConfigChangesSql changed in its comment only. Its text with comments stripped is pinned verbatim against the pre-get_pg_server_config_changes is blind to per-database and per-role overrides: an ALTER DATABASE … SET or ALTER ROLE … SET change never appears (derivative of #3691 / V138) #3937 statement, so its rows and their order cannot move.

  • The scoped read is a second statement, ScopedConfigChangesSql, not a UNION ALL in the first. Folding it in would have changed the server-wide read's text, its row order and its census classification. As a second statement it is the census's second override-selecting read, carrying the positive arm on its own FROM.

  • It walks the SERVER's snapshot sequence, not the key's. snapshots pairs each windowed snapshot instant with the one before it, and each (name, database_name, role_name) key is compared across that pair:

    • present in both with a different value is changed;
    • present only in the later snapshot is set, with old_value null;
    • present only in the earlier snapshot is reset, with new_value null.

    The instants are read off every row, not off the override rows alone. RESETting the last override leaves a snapshot that holds no overrides, and that pair still has to exist.

  • The baseline cut. A set is reported only when the snapshot it is compared against was taken at or after V138's darling_schema_version.applied_at. Overrides present on the first post-V138 snapshot are baseline: the collector started seeing them, nobody set them then. A store with no V138 stamp reports no SETs, which is the conservative failure. Value changes and RESETs need no cut.

  • Bounds. Both snapshots of every pair sit inside [$2, $3], and both reads are server_id = $1 plus the window. No index was added, and neither read is on the alert path.

  • Payload. A server-wide row keeps the SAME anonymous shape, byte for byte. A scoped row is a JsonObject carrying changed_at, name, only the scope names it has (database_name and/or role_name), then change_kind, old_value, new_value and source. McpHelpers.JsonOptions writes nulls, so a shared shape would have put "database_name": null on every server-wide row. scoped_changes_note is attached only when a scoped row is on the page. A page without one goes through the original serialize path unchanged.

  • Row order. GetConfigChangesAsync asks both statements for the full limit and merges them newest first. At one collection_time the server-wide rows come first. The tool's limit + 1 over-fetch therefore still observes truncation over the union.

Inherent limits (not defects)

  • A key that is reset and then set again between two hourly snapshots is invisible. So is a value that moves and moves back.
  • changed_at is the snapshot that first saw the new state, as it already was for server-wide rows.
  • The read is two round-trips, both bounded by server_id and the window.

Pins

  • (i) PgServerConfigTests.TheServerWideChangeRead_IsTheStatementItWasBeforeTheScopedFeed: raw-text pin, comments stripped, against the pre-get_pg_server_config_changes is blind to per-database and per-role overrides: an ALTER DATABASE … SET or ALTER ROLE … SET change never appears (derivative of #3691 / V138) #3937 statement verbatim.

  • (ii)–(v) PgServerConfigOverrideLivePostgresTests.TheChangeFeed_ReportsOverrideChangesSetsAndResets_AndLeavesTheServerWideRowsUnchanged (live). It plants a four-snapshot history straddling the real V138 applied_at:

    • t0 is pre-V138;
    • t1 is the FIRST snapshot at or after the stamp and already carries two overrides, which must NOT be reported (iv);
    • t2 moves the database override's value (ii, changed), moves server-wide work_mem, and adds a database+role override (iii, set with a null old_value);
    • t3 drops the role override (v, reset with a null new_value).

    It asserts exactly four rows in order: the reset, then the server-wide change ahead of the scoped rows at t2. Then it checks the tool:

  • (vi) PgServerConfigScopeRungTests.EveryProductReadOfTheConfigTable_…: the census goes from 10 to 12 reads with the reason. ScopedConfigChangesSql's snapshots subquery and overrides CTE are both aliased FROM pg_server_config AS c, and both are classified "selects". New assertions pin exactly two FROMs, the positive arm on the CTE, no negative arm anywhere, and the arm inside the census's statement window. The prose moves from "ten" to "twelve" and from "eleventh" to "thirteenth".

  • DarlingPgReadSqlParsesLiveTests: ScopedConfigChangesSql is parse-checked live by reflection, and the three PgConfigChangeKind spellings join NotQueryFields because they are values, not statements.

Reviewer checklist

  1. ScopedConfigChangesSql: the set arm's s.prev_time >= (SELECT applied_at … version = 138), the reset arm's NOT EXISTS against the NEXT snapshot, and IS NOT DISTINCT FROM on both scope columns.
  2. The GetConfigChangesAsync merge: server-wide rows are taken first on equal collection_time, and the merge is capped at limit.
  3. The tool: the rows.All(server-wide) branch returns the original JsonSerializer.Serialize(page, …) path.

Tests run (macOS harness, in-worktree, net10.0, live against timescale/timescaledb:2.28.1-pg18, migrated)

255 tests, 0 failed, 0 skipped, with DARLING_TEST_PG set:

  • McpPayloadContractCensusTests: 64
  • McpPageContractTests: 62
  • DocCommentHygieneTests: 48
  • TsqlConventionGuardTests: 28
  • LiveCleanupConversionRatchetTests: 24
  • PgServerConfigTests: 16
  • PgServerConfigScopeRungTests: 16, a copy with the Viewer probe fact stripped
  • McpPageContractLivePostgresTests: 8
  • PgServerConfigOverrideLivePostgresTests: 6
  • LivePostgresCollectionHygieneTests: 6
  • DarlingPgReadSqlParsesLiveTests: 6
  • RepoFileAdoptionTests: 4

The runner counts rows, including theory cases. The Viewer-dependent probe fact and every Lite/Viewer census first run on Windows CI.

CHANGELOG entry

…override changes - SET, RESET and value changes over the server's snapshot sequence, V138 applied_at as the baseline cut (#3937)
…ange reader merges the server-wide and scoped reads newest first (#3937)
…ws byte-identical, scoped rows name only their scope plus change_kind, the note attached only when a scoped row is present (#3937)
…entical, live SET / baseline / changed / RESET over a V138-straddling history, tool payload shapes, reader census 10 -> 12 (#3937)
@erikdarlingdata
erikdarlingdata enabled auto-merge (squash) September 23, 2026 00:40
@erikdarlingdata
erikdarlingdata merged commit e368719 into dev Sep 23, 2026
15 of 16 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/3937-config-change-feed-scoped branch September 23, 2026 00:40
erikdarlingdata added a commit that referenced this pull request Sep 23, 2026
…3920, #3927, #3931, #3932, #3940, #3942, #3946, #3947, #3950, #3952, #3955, #3956, #3957, #3964, #3965, #3966, #3968, #3972, #3975, #3979, #3980, #3981, #3983, #3984, #3985) (#3989)

The wave's fix PRs deliberately carried no CHANGELOG edits (parallel-agent hot-spot protocol); each agent reported its entry and this commit lands them together, byte-verified against origin/dev.


Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant