AshReferentialActions

Copy Markdown View Source

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_related from cascade_has_many and cascade_has_one
  • rejects an archive while a live restrict_has_many/has_one record exists
  • generates a private nilify update action and clears live nilify_has_many/has_one foreign 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?: true

AshReferentialActions 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