AshReferentialActions.Changes.EnsureNotRestricted (ash_referential_actions v0.1.0)

Copy Markdown View Source

Runtime guard added to every destroy action of a resource that declares restrict_has_many/has_one: the destroy (hard delete, or archive when the resource is archival) is rejected while live restricting records exist.

Restricting records are enumerated through the resource's reverse relationships and counted via each relationship's declared read_action (or the referring resource's primary read — a verifier rejects a filtered primary read as the default), so action-level read filters cannot silently hide live restricting records. Archival's global is_nil(archived_at) preparation still applies — an archived restricting record does not block. Inside a transaction, restricting records archived earlier in the same transaction are already invisible, which lets an reference cascade pass deterministically as long as it archives restricting records before the restricted resource (see the archive_last option of AshReferentialActions.Archival).

Before counting, the record being destroyed is locked FOR UPDATE where the data layer supports it, pairing with the FOR SHARE lock AshReferentialActions.Changes.EnsureTargetLive takes on the same row. That closes the window where a lock is created concurrently with a destroy. On data layers without lock support (e.g. ETS) both stay plain application-level checks and the race remains.