Repository navigation
v2.19.0
·
29 commits
to main
since this release
Immutable
release. Only release title and notes can be modified.
Changed
_updated_atis aBEFORE UPDATE … FOR EACH ROWtrigger that assignsNEW, not a statement trigger that issues a secondUPDATE(#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'sON UPDATErules: on a consumer's staging database one single-rowUPDATEof a table with six cascade rules became 35 statements over 13 tables, 466 of 576 ms spent inupdated_at_trigger. It now assignsNEW._updated_at := NOW()on the row being written, so oneUPDATEwrites each row once andRETURNING _updated_atreads the stamped value. Always assigned, as before: a caller still cannot set the column. Existing projects: the nextmakeguitarmigrationsemits one function migration (stamp_updated_at(), inTRIGGER_FUNCTION_APP) and one replacement migration per app that swaps eachupdated_at_triggerwithCREATE OR REPLACE TRIGGER(SHARE ROW EXCLUSIVE, notACCESS EXCLUSIVE, on the app's tables until it commits: writes block, reads do not, so setlock_timeouton a large app), reversing to the statement trigger;makemigrations --checkis red until it does. The frozenset_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_atstale below its first level. The new trigger carries noWHEN (pg_trigger_depth() = 0), so the cascade children of every level a rawDELETEreaches 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 UPDATErules (#80, ADR 0039). A rule is expanded and planned on everyUPDATEof its table, whether or not_deleted_atmoves: one rename of aBandwas 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 firstEXISTSthat a plainUPDATEstops 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 NULLand the parent's own timestamp. The owned rule is retired and its sweep, which stamped a superset, carries the work alone. Existing projects: the nextmakeguitarmigrationsemits, in each app hosting an owner, the owner's newsoft_delete_cascade_on_*trigger (cascade arms and revive arms in one), a retirement of the revive-onlysoft_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 --checkis 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 takesACCESS EXCLUSIVEon each owner table (the revive-only trigger'sDROP) andSHARE ROW EXCLUSIVEon the others until it commits: setlock_timeouton 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 readold.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 SECURITYthe 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 underFORCE(the default) both always did. OnlyGUITARS_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
RemoveFieldorDeleteModelof a child failed atmigrate. 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 untilmakeguitarmigrationsre-emits it (--checkis red).DISABLE TRIGGERandsession_replication_role = replicanow 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$$. sweepownedfollows 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).