Contents
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
EXPLAINran afterSET ROLEto its author, and a folded function ranRESET ROLE,SET SESSION AUTHORIZATION DEFAULTorset_config('role', ...)and was the runner again, then ranCOPY ... TO PROGRAM(external audit, round 5). TheEXPLAINnow runs in a temporarySECURITY DEFINERfunction 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.shadds S1, each way back red on 1.1.8 with its control.
1.1.8 – 2026-10-09
- F-12: a
plan_guardschema created by someone else is refused.CREATE EXTENSIONused 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 aplan_guardschema 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()andpg_stat_reset()as the role runningverify(); a session advisory lock stayed in that session; andpg_cancel_backend()of its own backend abortedverify()andsync_stash()for every baseline. A role needed only INSERT onbaselines.baselines.captured_bynow records the author – set by a trigger to whoever writes or rewrites the query, another name accepted only from a role that maySET ROLEto it – and the sealedEXPLAINruns afterSET ROLEto that author. What needs more than the author has is that baseline’serror; 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_pathno longer leaks into the caller’s session (F-01). 1.1.4 applied it withset_config(..., false)inside helpers with aSETclause and said the clause would restore the caller’s path; a plainSEToverrides the clause and outlives it. Afterverify()the session kept the baseline’s path, and its nextcapture()recorded that path and blessed a plan of another schema’s table. - Every
EXPLAINof stored text runs sealed (F-02, F-03, F-14). The planner folds anIMMUTABLEfunction with constant arguments, so a stored query ran code as whoever ranverify(), 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 everyEXPLAINin a subtransaction switched to read-only and always rolled back, applies the baseline’s path andcompute_query_idinside it withset_config(..., true), and parses underpg_catalog, pg_temp.query_id_for()no longer leavescompute_query_id = onin the caller’s transaction. - A role that is not a superuser can capture (F-04) when
pg_plan_adviceis preloaded: a refusedLOADof 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_TEMPispg_temp(a path fromset_config()orALTER ROLE ... SETis 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_logis append-only (F-13), as documented:UPDATE,DELETEandTRUNCATEare refused by triggers.test/cluster.shpreloadspg_plan_adviceandpg_stash_advicewhere 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()andsync_stash()planned the stored query under the search_path of whoever ran them, where PostgreSQL searchespg_tempfirst. A temporary table named like a watched one was planned instead: a stable plan came backdriftedand wrote a false drift intodrift_log, and a real drift (an index under an approved seq scan) came backok. A baseline captured with its schema on the path came backerrorfrom pg_cron’s session, adrift_logrow per run. capture()records the caller’s path in the new columnbaselines.search_path; every re-plan applies it withpg_templast, scoped to the call. Baselines captured before 1.1.4 keep the caller’s path, also withpg_templast, and the upgrade says how many there are.META.jsonrequires pg_living_assertions 0.5.5 forwatch(): the first release that runs a check withpg_templast.test/pg_temp.shandtest/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.