Does validations in update actions always require you to load the fields the validations are working on

Hi,

I am adding complex validations to my resources. Often they need to use aggregates or calculations to validate things.

For instance, I might want to ensure that that a Topic has no open Issues before it it closed:

update :close do
  validate attribute_equals(:open_issue_count, 0)
end

aggregates do
  count :open_issue_count, :open_issues
end

Then in my form or where ever I want to perform the update, I would:

topic = Topic.get_by_id!(123)
Topic.close!(topic) <=== BOOM

close! fails because it does not have the aggregate open_issue_count loaded.
That means that the call-site code needs to know which fields need to be loaded before it can validate, and introducing new validations break existing code because they down know they need the fields loaded.

topic = Topic.get_by_id!(123, load: [:open_issue_count])
Topic.close!(topic)

Can I postpone some of the validation until it is processed in the data layer?
Is there some obvious pattern I am missing?

See this related question and LMK if that clarifies. You can refer to aggregates in the expressions of an atomic validation:

Good read thanks.

The above example was a contrived one.

In my case the two related resources are not in the same data layer. I must admit my understanding of how the atomic validations work is still foggy, but I assume they are compiled down to SQL and executed in the Data Layer. Since one of my calculations fetches data from a git repo I assume atomic validations that won’t work?

Ah, then yes in that case you will need to load the data up front :slight_smile:

You can avoid duplicative work by adding a change before the validation that loads the data. And you can also push validations off to before_action hooks:

change fn changeset, _ -> 
  %{changeset | data: Ash.load!(changeset.data, ....)}
end

validate one()
validate two()
validate something_expensive(), before_action?: true

Is there any reason why i wouldn’t do the load directly in the validation, like:

defmodule Validations.HasCommits do
  use Ash.Resource.Validation
  require Ash.Query

  @impl true
  def validate(changeset, _opts, _context) do
    %{master_sha: sha} = Ash.load!(changeset.data, [:master_sha], lazy?: true, reuse_values?: true) 
    if sha == nil do
      {:error, field: :base, message: "must have documents"}
    else
      :ok
    end
  end

  @impl true
  def atomic(changeset, opts, context) do
    {:not_atomic, "This calculation uses git"}
  end
end

I assume I have to set require_atomic? false on the action since it cannot be fully atomic anymore?

Not at all :slight_smile:

1 Like