Changelog

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

0.5.8 – 2026-10-09

  • A check runs as the role that declared it (external audit: F9, F6; the same class as pg_plan_guard’s PG-S1). Up to 0.5.7 it ran with the privileges of whoever called run() – documented, and demonstrated by test/privilegios.sh reading the owner’s secret through a trusted role’s check. The seal bounded writes to the database and nothing else: COPY ... TO PROGRAM is a read, so a check ran a program as the caller; a session advisory lock stayed in the caller’s session; and a check that cancelled its own backend aborted run_all() for every assertion. Inside the seal the evaluator now does SET ROLE to declared_by (not when that is the current user), so a check can do what its author could and no more; what needs more is that assertion’s erroring, and cancelling the caller’s backend is refused the same way. Advisory locks taken in the seal are released (test/sql/read_only.sql measured the lock held until 0.5.7).
  • declared_by cannot be forged at insert: a trigger accepts a name other than the declaring role only from a role that may SET ROLE to it (a superuser restoring a dump).
  • Behaviour change for callers. A caller must be able to SET ROLE to each author; a superuser can. A SECURITY DEFINER caller – pg_agent_gate binding an assertion – runs the checks its owner declared; the others are erroring, with the reason.
  • test/audit.sh: the F9/F6 teeth, and test/privilegios.sh inverted – red on 0.5.7 with their controls green.

0.5.7 – 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 0.5.6 -> 0.5.7 changes no object.

0.5.6 – 2026-10-08

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

  • The recorded search_path no longer stays in the caller’s session (F1). run() applied it with set_config(..., false), and the 0.5.5 comment said the function’s SET clause would restore it on exit. It does not: a plain SET inside a function with a SET clause overrides the clause and persists after the function. After run_all() a runner’s next unqualified call reached a function in a schema the author of an assertion wrote, and a SECURITY DEFINER wrapper with its own SET search_path continued under the author’s path once run() returned.
  • run()’s bookkeeping no longer runs under the author’s path (F2). With a recorded path of evil, pg_catalog, the author’s clock_timestamp() ran as the runner in a session that only called run_all().
  • Both closed in one place: the path is applied inside _evaluate’s sealed subtransaction with set_config(..., true), right after read-only is switched on, and the rollback that undoes the check undoes it too. _evaluate has its own SET search_path = pg_catalog, pg_temp for everything outside the seal, and the calls around the check are schema-qualified. run() no longer touches the path.
  • An unparsable recorded path is that assertion erroring instead of run_all() raising for everyone (F7), and the path is split the way PostgreSQL splits it: 0.5.5 broke a quoted schema name containing a comma (F15), and did not recognise an unquoted PG_TEMP as pg_temp. SET stores the path lower-cased, but a path set with set_config() or ALTER ROLE ... SET is recorded as written, and with PG_TEMP first a temporary table of the evaluating session answered for the check.
  • A forged verdict cannot be pinned (F3). The latest verdict was the one with the latest checked_at, and a role allowed to run checks needs INSERT on checks: a row dated 'infinity' outranked every honest check forever. The latest verdict is now the last row written (by id), and checked_at must be finite. Such a role can still write a row; it lasts until the next honest check (README).
  • An assertion is not edited in place, all of it (F4). The trigger compared five columns; search_path, declared_by, why_changed and id are fixed now, and a retirement is written once – not undone, not re-dated, its reason not rewritten.
  • A NULL reason no longer passes the checks that make retiring and replacing cost one (F5).
  • Two concurrent replacements of one assertion no longer both retire it, the second reason overwriting the first (F11): the predecessor is locked.
  • The upgrade adds the new constraints NOT VALID and validates them; an installation already holding rows they refuse upgrades, gets a WARNING naming them, and keeps them, since the record is append-only.

0.5.5 – 2026-10-08

  • A temporary table of the session that evaluates an assertion can no longer change what it reads. PostgreSQL searches pg_temp first for tables whenever search_path does not name it, and no path here named it. run() evaluated the check under the declarer’s path ("$user", public), so from cuentas read the evaluating session’s pg_temp.cuentas; and run() looked the assertion up with FROM assertions, so a temporary assertions with a forged row – a failing assertion’s name, the id of one that holds – made it answer holds. retire() and run_all() read and wrote assertions the same way. That matters when the check runs in someone else’s session with the owner’s rights: a SECURITY DEFINER function of the owner that calls run(), which is what pg_agent_gate does inside an agent’s commit. Measured on 0.5.4 with the real table broken (test/pg_temp.sh, make check-pgtemp): both ways answered holds, and the record said holds. Now every function names pg_temp last, run(), run_all() and retire() name the registry by its schema, and the declared path is applied with set_config() – with any pg_temp in it moved to the end – instead of being concatenated into a SET statement.

0.5.4 – 2026-10-06

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

0.5.3

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.

0.5.2

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

0.5.1 and earlier

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