Changelog

Versions are released on PGXN. Each upgrade script (pg_plan_guard--OLD--NEW.sql) documents, in its own header, exactly what changed and why; that is the authoritative per-version record.

1.1.9 – 2026-10-09

  • A baseline cannot leave the role it is planned as. From 1.1.7 its EXPLAIN ran after SET ROLE to its author, and a folded function ran RESET ROLE, SET SESSION AUTHORIZATION DEFAULT or set_config('role', ...) and was the runner again, then ran COPY ... TO PROGRAM (external audit, round 5). The EXPLAIN now runs in a temporary SECURITY DEFINER function the author owns, created and rolled back inside the seal; there PostgreSQL refuses to change role or session authorization at all. Skipped only where it cannot change anything: capturing one’s own query costs what it cost. test/audit.sh adds S1, each way back red on 1.1.8 with its control.

1.1.8 – 2026-10-09

  • F-12: a plan_guard schema created by someone else is refused. CREATE EXTENSION used it, and its owner – any role with CREATE on the database – could drop it, and the extension with every baseline and the drift history (external audit of 1.1.4). The install and every upgrade refuse a plan_guard schema owned by a role that is neither the installer nor a superuser (test/audit.sh, red on 1.1.7).

1.1.7 – 2026-10-09

  • A baseline is planned as the role that wrote it (PG-S1, external audit round 4). The 1.1.5 seal (read-only, rolled back) stops writes to the database, not what is not one: a function the planner folds ran COPY ... TO PROGRAM (measured: a file created by the server’s OS user), pg_switch_wal(), pg_create_restore_point() and pg_stat_reset() as the role running verify(); a session advisory lock stayed in that session; and pg_cancel_backend() of its own backend aborted verify() and sync_stash() for every baseline. A role needed only INSERT on baselines. baselines.captured_by now records the author – set by a trigger to whoever writes or rewrites the query, another name accepted only from a role that may SET ROLE to it – and the sealed EXPLAIN runs after SET ROLE to that author. What needs more than the author has is that baseline’s error; advisory locks taken in the seal are released.
  • A baseline captured before 1.1.7 is refused until captured again (state error, “no recorded author”); the upgrade names them. Running it as the caller is the hole this closes, so the default stays closed.
  • test/audit.sh: the PG-S1 teeth, red on 1.1.6 with their controls green.

1.1.6 – 2026-10-08

  • Metadata only. The PGXN description is two sentences now; the longer explanation it carried is in this README. No code changed: the upgrade script 1.1.5 -> 1.1.6 changes no object.

1.1.5 – 2026-10-08

From an external audit of 1.1.4, each finding measured on 1.1.4 before it was changed (test/audit.sh, make check-audit, in make check-suites: every tooth red on 1.1.4 with its control green).

  • The baseline’s search_path no longer leaks into the caller’s session (F-01). 1.1.4 applied it with set_config(..., false) inside helpers with a SET clause and said the clause would restore the caller’s path; a plain SET overrides the clause and outlives it. After verify() the session kept the baseline’s path, and its next capture() recorded that path and blessed a plan of another schema’s table.
  • Every EXPLAIN of stored text runs sealed (F-02, F-03, F-14). The planner folds an IMMUTABLE function with constant arguments, so a stored query ran code as whoever ran verify(), and kept what it did; and the advice was parsed with an unqualified || under the baseline’s path, so an operator in a schema on it ran too. One function, _explain_lines(), now runs every EXPLAIN in a subtransaction switched to read-only and always rolled back, applies the baseline’s path and compute_query_id inside it with set_config(..., true), and parses under pg_catalog, pg_temp. query_id_for() no longer leaves compute_query_id = on in the caller’s transaction.
  • A role that is not a superuser can capture (F-04) when pg_plan_advice is preloaded: a refused LOAD of a loaded library is not an error any more.
  • A baseline that cannot be planned is logged on the transition (F-05), like a drift, not on every run.
  • The identity sequences travel with pg_dump (F-06). After a restore they started again at 1, and the first drift or capture died on a duplicate key. The upgrade also moves them past the ids an earlier restore left.
  • Re-capturing a name forgets the old statement’s query_id (F-07), and sync_stash() always computes it again – and goes on past a baseline it cannot plan, instead of aborting on the first one with everything it had done.
  • The recorded path is read the way PostgreSQL reads it (F-10): an unquoted PG_TEMP is pg_temp (a path from set_config() or ALTER ROLE ... SET is recorded as written), and a quoted schema name with a comma in it is one name.
  • Only the lines after the last “Generated Plan Advice:” header are the advice (F-11): a query whose text contained the header leaked plan lines into it.
  • drift_log is append-only (F-13), as documented: UPDATE, DELETE and TRUNCATE are refused by triggers.
  • test/cluster.sh preloads pg_plan_advice and pg_stash_advice where they exist, as a server running this extension has them.

1.1.4 – 2026-10-08

  • A baseline is re-planned against the tables its author meant. Up to 1.1.3 verify() and sync_stash() planned the stored query under the search_path of whoever ran them, where PostgreSQL searches pg_temp first. A temporary table named like a watched one was planned instead: a stable plan came back drifted and wrote a false drift into drift_log, and a real drift (an index under an approved seq scan) came back ok. A baseline captured with its schema on the path came back error from pg_cron’s session, a drift_log row per run.
  • capture() records the caller’s path in the new column baselines.search_path; every re-plan applies it with pg_temp last, scoped to the call. Baselines captured before 1.1.4 keep the caller’s path, also with pg_temp last, and the upgrade says how many there are.
  • META.json requires pg_living_assertions 0.5.5 for watch(): the first release that runs a check with pg_temp last.
  • test/pg_temp.sh and test/cluster.sh (make check-pgtemp): 4 FAIL on 1.1.3 with their controls green, 12/12 on 1.1.4 including the upgrade, PostgreSQL 19beta2.

1.1.3 – 2026-10-06

  • License: Apache License 2.0, replacing the PostgreSQL License, from this release on. Every version up to and including 1.1.2, already published, stays under the PostgreSQL License it was released with. No code changed.

1.1.2

Completes the copyright and licensing files: the copyright holder’s full legal name in LICENSE and README, and a per-file SPDX header on every SQL source file. No schema change.

1.1.1

No schema change. Adds project governance and legal files (NOTICE, AUTHORS, SECURITY, CONTRIBUTING, TRADEMARK). The database objects are byte-for-byte those of 1.1; the 1.1--1.1.1 upgrade is empty on purpose.

1.1.0 and earlier

See the header of each pg_plan_guard--*--*.sql upgrade script and the release notes on PGXN.