Skip to content

feat: Add attribute_always_select? option to belongs_to relationships#2627

Open
emadshaaban92 wants to merge 1 commit intoash-project:mainfrom
emadshaaban92:belong-to-attribute-always-select
Open

feat: Add attribute_always_select? option to belongs_to relationships#2627
emadshaaban92 wants to merge 1 commit intoash-project:mainfrom
emadshaaban92:belong-to-attribute-always-select

Conversation

@emadshaaban92
Copy link
Contributor

Contributor checklist

Leave anything that you believe does not apply unchecked.

  • I accept the AI Policy, or AI was not used in the creation of this PR.
  • Bug fixes include regression tests
  • Chores
  • Documentation changes
  • Features include unit/acceptance tests
  • Refactoring
  • Update dependencies

Problem

When reading records with a limited select, foreign key attributes from belongs_to relationships get masked as Ash.NotLoaded. If you then call an update action on that record, any change with a where [present(:foreign_key)] guard will silently skip — even though the value exists in the database.

Example

Consider a Task resource that optionally belongs to a Project:

belongs_to :project, MyApp.PM.Project, allow_nil?: true

When a task is completed, we want to check if the entire project is now done and update its status — but only for project-linked tasks:

update :complete do
  change transition_state(:completed)

  change MyApp.PM.Task.Changes.AutoCompleteProject do
    where [present(:project_id)]
  end
end

In a dashboard LiveView, tasks are loaded with a limited select for performance:

Task
|> Ash.Query.select([:id, :state, :title, :due_at])
|> Ash.read!()

When a user marks a task as complete, the action runs on the record as-is. Since project_id wasn't in the select, it's Ash.NotLoaded, present? returns false, and the AutoCompleteProject change is silently skipped — even for tasks that do belong to a project.

The current workaround is to either:

  • Always remember to include the FK in every query's select (error-prone)
  • Define the attribute manually with always_select? true and set define_attribute? false (verbose)

Solution

Adds attribute_always_select? to belongs_to, consistent with existing attribute_writable? and attribute_public? options:

belongs_to :project, MyApp.PM.Project,
  allow_nil?: true,
  attribute_always_select?: true

Alternative / Future Consideration

ensure_loaded or preload on actions

A complementary approach would be an ensure_loaded option that loads specific fields on the record before running validations and changes. Unlike attribute_always_select? (which is attribute-scoped and global), ensure_loaded would generalize to calculations, aggregates, and relationships, and could be scoped to specific actions.

update :complete do
  ensure_loaded [:project_id]

  change transition_state(:completed)

  change MyApp.PM.Task.Changes.AutoCompleteProject do
    where [present(:project_id)]
  end
end

Note: Code in this PR is hand-written. The PR description was generated with AI assistance.

@zachdaniel
Copy link
Contributor

There is an option called action_select which you can use to ensure certain attributes are selected on the base record. Have you tried that? This change is useful anyway though 😄

@zachdaniel
Copy link
Contributor

Please make sure to run mix spark.cheat_sheets locally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants