Explicit referential actions for Ash relationships.
Why
belongs_to, has_many, and has_one describe cardinality but not lifecycle. A related record may be owned, may prevent deletion, may lose its foreign key, or may require no lifecycle behavior. AshReferentialActions makes that choice mandatory and derives soft-archive and optional PostgreSQL behavior from it.
DSL
Declare the same action on both sides of an attributable relationship:
# A comment is owned by its post
cascade_belongs_to :post, Post, allow_nil?: false
cascade_has_many :comments, Comment
# A product cannot disappear while an order item references it
restrict_belongs_to :product, Product, allow_nil?: false
restrict_has_many :order_items, OrderItem
# A task remains when its assignee disappears
nilify_belongs_to :assignee, User, allow_nil?: true
nilify_has_many :assigned_tasks, Task, destination_attribute: :assignee_id
# Normal relationship with no lifecycle behavior
do_nothing_has_many :published_posts, Post, filter: expr(published)Resources using the extension cannot declare plain attributable belongs_to, has_many, or has_one. Use one of cascade_*, restrict_*, nilify_*, or do_nothing_*.
Plain reverse relationships to destination resources that do not use AshReferentialActions are exempt. This primarily supports relationships generated by extensions such as AshPaperTrail.
Use normal Ash relationship options. nilify_belongs_to requires allow_nil?: true.
Adapters
Soft archive
use Ash.Resource,
extensions: [AshReferentialActions.Archival]The archival adapter also installs AshArchival.Resource and:
- generates
archive_relatedfromcascade_has_manyandcascade_has_one - rejects an archive while a live
restrict_has_many/has_onerecord exists - generates a private nilify update action and clears live
nilify_has_many/has_oneforeign keys - rejects new cascade/restrict/nilify references to archived targets
- validates cascade destinations and ordering
PostgreSQL physical delete
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
extensions: [AshReferentialActions.Postgres]The PostgreSQL adapter generates migration reference behavior:
- cascade ->
on_delete: :delete - restrict ->
on_delete: :restrict - nilify ->
on_delete: :nilify
Do not enable this adapter merely because an application uses PostgreSQL. Applications that only soft-archive records should normally use the archival adapter alone and retain restrictive database foreign keys as a safety net.
The adapters can be combined when both soft archive and physical delete must share the same semantics.
Generated nilify action
For:
nilify_belongs_to :assignee, User, allow_nil?: trueAshReferentialActions generates a private update action named from the source attribute, for example:
:__ash_referential_actions_nilify_assignee_id__The action accepts no input and only sets assignee_id to nil. The target's archival change invokes it through the matching reverse relationship with Ash's cascade_update change.
Guarantees
Compile-time verification rejects:
- unmarked attributable relationships
- missing or mismatched forward/reverse actions
- nilify relationships whose foreign key is non-nullable
- filtered or manual lifecycle reverse relationships
- lifecycle targets that do not use AshReferentialActions
- invalid cascade destinations or cascade order
- PostgreSQL reference behavior that conflicts with the declared action