Skip to content

Latest commit

 

History

31 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

AshReferentialActions

Explicit referential actions for Ash relationships.

Installation

Add the dependency to your mix.exs:

{:ash_referential_actions, "~> 0.1.1"}

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

The live-target guard batches its reads for bulk creates and streamed bulk updates: one read before and one after each batch, per guarded relationship, domain, and tenant with keys to check. For example, 5,000 creates with one relationship and batch_size: 100 require 100 guard reads. Required read-action pagination does not truncate the keys being checked, and PostgreSQL reads retain FOR SHARE locks.

Batches with record hooks, managed relationships, action-level after_batch, or resource-level before_batch callbacks retain individual guard checks to preserve hook ordering. Ordinary resource-level changes (including direct attribute changes and after_batch callbacks) do not disable batching. The guard reserves its original hook positions until before_batch, so hooks installed by global changes cannot move ahead of it. Single actions retain their existing hooks. Atomic updates that do not change guarded keys do not run guard reads. Ash's transaction and error options continue to determine partial success and rollback behavior.

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

About

Explicit cascade, restrict, nilify, and view semantics for Ash relationships

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages