Skip to content

v2.19.0

Choose a tag to compare

@github-actions github-actions released this 09 Oct 09:58
· 29 commits to main since this release
Immutable release. Only release title and notes can be modified.
1398768

Changed

  • _updated_at is a BEFORE UPDATE … FOR EACH ROW trigger that assigns NEW, not a statement trigger that issues a second UPDATE (#80, ADR 0038). The old form re-UPDATEd every row through dynamic SQL, so each row was written twice, the plan was never cached, and the follow-up was itself rewritten by the table's ON UPDATE rules: on a consumer's staging database one single-row UPDATE of a table with six cascade rules became 35 statements over 13 tables, 466 of 576 ms spent in updated_at_trigger. It now assigns NEW._updated_at := NOW() on the row being written, so one UPDATE writes each row once and RETURNING _updated_at reads the stamped value. Always assigned, as before: a caller still cannot set the column. Existing projects: the next makeguitarmigrations emits one function migration (stamp_updated_at(), in TRIGGER_FUNCTION_APP) and one replacement migration per app that swaps each updated_at_trigger with CREATE OR REPLACE TRIGGER (SHARE ROW EXCLUSIVE, not ACCESS EXCLUSIVE, on the app's tables until it commits: writes block, reads do not, so set lock_timeout on a large app), reversing to the statement trigger; makemigrations --check is red until it does. The frozen set_updated_at() stays in a migrated database, called by nothing, and a table not yet regenerated keeps the old trigger and works. The MTI parent trigger (set_parent_updated_at) is unchanged.
  • The self-referential cascade no longer leaves _updated_at stale below its first level. The new trigger carries no WHEN (pg_trigger_depth() = 0), so the cascade children of every level a raw DELETE reaches are stamped, as the ORM collector already did; the two paths now agree. delete() therefore takes its fast path for a self-referential key, which it used to stand aside for on this account.
  • The cascade and the owned rule are no longer ON UPDATE rules (#80, ADR 0039). A rule is expanded and planned on every UPDATE of its table, whether or not _deleted_at moves: one rename of a Band was twelve statements over eight tables, and each cascade level multiplies it. Each cascade key is now an archive arm of its owner's existing statement trigger, beside its revive arm, behind a first EXISTS that a plain UPDATE stops at, so the same rename is one statement. Flat and joined forms; it reads the owner's before image for the key and the after image for the stamp, keeping _deleted_at IS NULL and the parent's own timestamp. The owned rule is retired and its sweep, which stamped a superset, carries the work alone. Existing projects: the next makeguitarmigrations emits, in each app hosting an owner, the owner's new soft_delete_cascade_on_* trigger (cascade arms and revive arms in one), a retirement of the revive-only soft_delete_revive_on_* trigger it supersedes, and a retirement of every cascade rule and owned rule it recorded, each reversing to what it dropped; makemigrations --check is red until it does. A rule is dropped only where the trigger carries its arm, so one whose arm is refused keeps cascading. The migration takes ACCESS EXCLUSIVE on each owner table (the revive-only trigger's DROP) and SHARE ROW EXCLUSIVE on the others until it commits: set lock_timeout on a large app.
  • A statement archiving a row while rewriting its primary key is refused (feature_not_supported) where a live child holds a key of a live row the statement read (so a pk reused inside it is caught too), as the self-referential cascade has been since 2.8.0: the rule read old. per row, the arm pairs a row across the statement on its primary key. Rewrite the key and archive in separate statements; a key rewrite with nothing live below it, or with no archive, is untouched.
  • Without FORCE ROW LEVEL SECURITY the cascade no longer reaches a child the scope cannot see. A rule's action ran as the table owner, who is exempt from row-level security unless forced, so it archived cross-tenant children; the arm runs as the invoker and leaves them live, as under FORCE (the default) both always did. Only GUITARS_RLS_FORCE = False, the staged-retrofit state, differs.
  • What a plpgsql arm gives up that a rule recorded. A rule depended on every table and column it named, so a RemoveField or DeleteModel of a child failed at migrate. An arm records no dependency: dropping or renaming a child's table or key column leaves the owner's trigger naming what is gone and fails an archive of that owner until makeguitarmigrations re-emits it (--check is red). DISABLE TRIGGER and session_replication_role = replica now switch a cascade off, where a rule survived both.
  • A table or column named with $$ gets another dollar-quote tag in the owner trigger and the owned sweep ($guitars$), where an arm spliced into $$ would close it early. No key is left without its cascade, and no recorded rule is retired for want of one; every migration written so far keeps $$.
  • sweepowned follows a relation the database holds an owned rule or a sweep for, since a 2.19.0 database holds no owned rule. A scan no longer flags an app for the digest guard when it reads a trigger-family retirement, which would have waived it for ever after the one-time retirement of every owned rule (ADR 0021 point 9).