Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions documentation/dsls/DSL-Ash.Resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -1110,7 +1110,7 @@ validate present([:first_name, :last_name], at_least: 1)
| [`only_when_valid?`](#actions-action-validate-only_when_valid?){: #actions-action-validate-only_when_valid? } | `boolean` | `false` | If the validation should only run on valid changesets. Useful for expensive validations or validations that depend on valid data. |
| [`message`](#actions-action-validate-message){: #actions-action-validate-message } | `String.t` | | If provided, overrides any message set by the validation error |
| [`description`](#actions-action-validate-description){: #actions-action-validate-description } | `String.t` | | An optional description for the validation |
| [`before_action?`](#actions-action-validate-before_action?){: #actions-action-validate-before_action? } | `boolean` | `false` | If set to `true`, the validation will be run in a before_action hook |
| [`before_action?`](#actions-action-validate-before_action?){: #actions-action-validate-before_action? } | `boolean` | `false` | If set to `true`, the validation will be run in a before_action hook, i.e. when the action is executed (inside the transaction) rather than when the changeset is built. Useful for validations that run queries or are otherwise expensive. Cannot be used with atomic actions. |



Expand Down Expand Up @@ -1311,7 +1311,7 @@ validate changing(:email)
| [`only_when_valid?`](#actions-create-validate-only_when_valid?){: #actions-create-validate-only_when_valid? } | `boolean` | `false` | If the validation should only run on valid changesets. Useful for expensive validations or validations that depend on valid data. |
| [`message`](#actions-create-validate-message){: #actions-create-validate-message } | `String.t` | | If provided, overrides any message set by the validation error |
| [`description`](#actions-create-validate-description){: #actions-create-validate-description } | `String.t` | | An optional description for the validation |
| [`before_action?`](#actions-create-validate-before_action?){: #actions-create-validate-before_action? } | `boolean` | `false` | If set to `true`, the validation will be run in a before_action hook |
| [`before_action?`](#actions-create-validate-before_action?){: #actions-create-validate-before_action? } | `boolean` | `false` | If set to `true`, the validation will be run in a before_action hook, i.e. when the action is executed (inside the transaction) rather than when the changeset is built. Useful for validations that run queries or are otherwise expensive. Cannot be used with atomic actions. |
| [`always_atomic?`](#actions-create-validate-always_atomic?){: #actions-create-validate-always_atomic? } | `boolean` | `false` | By default, validations are only run atomically if all changes will be run atomically or if there is no `validate/3` callback defined. Set this to `true` to run it atomically always. |


Expand Down Expand Up @@ -1637,7 +1637,7 @@ validate present([:first_name, :last_name], at_least: 1)
| [`only_when_valid?`](#actions-read-validate-only_when_valid?){: #actions-read-validate-only_when_valid? } | `boolean` | `false` | If the validation should only run on valid changesets. Useful for expensive validations or validations that depend on valid data. |
| [`message`](#actions-read-validate-message){: #actions-read-validate-message } | `String.t` | | If provided, overrides any message set by the validation error |
| [`description`](#actions-read-validate-description){: #actions-read-validate-description } | `String.t` | | An optional description for the validation |
| [`before_action?`](#actions-read-validate-before_action?){: #actions-read-validate-before_action? } | `boolean` | `false` | If set to `true`, the validation will be run in a before_action hook |
| [`before_action?`](#actions-read-validate-before_action?){: #actions-read-validate-before_action? } | `boolean` | `false` | If set to `true`, the validation will be run in a before_action hook, i.e. when the action is executed (inside the transaction) rather than when the changeset is built. Useful for validations that run queries or are otherwise expensive. Cannot be used with atomic actions. |



Expand Down Expand Up @@ -1947,7 +1947,7 @@ validate changing(:email)
| [`only_when_valid?`](#actions-update-validate-only_when_valid?){: #actions-update-validate-only_when_valid? } | `boolean` | `false` | If the validation should only run on valid changesets. Useful for expensive validations or validations that depend on valid data. |
| [`message`](#actions-update-validate-message){: #actions-update-validate-message } | `String.t` | | If provided, overrides any message set by the validation error |
| [`description`](#actions-update-validate-description){: #actions-update-validate-description } | `String.t` | | An optional description for the validation |
| [`before_action?`](#actions-update-validate-before_action?){: #actions-update-validate-before_action? } | `boolean` | `false` | If set to `true`, the validation will be run in a before_action hook |
| [`before_action?`](#actions-update-validate-before_action?){: #actions-update-validate-before_action? } | `boolean` | `false` | If set to `true`, the validation will be run in a before_action hook, i.e. when the action is executed (inside the transaction) rather than when the changeset is built. Useful for validations that run queries or are otherwise expensive. Cannot be used with atomic actions. |
| [`always_atomic?`](#actions-update-validate-always_atomic?){: #actions-update-validate-always_atomic? } | `boolean` | `false` | By default, validations are only run atomically if all changes will be run atomically or if there is no `validate/3` callback defined. Set this to `true` to run it atomically always. |


Expand Down Expand Up @@ -2240,7 +2240,7 @@ validate changing(:email)
| [`only_when_valid?`](#actions-destroy-validate-only_when_valid?){: #actions-destroy-validate-only_when_valid? } | `boolean` | `false` | If the validation should only run on valid changesets. Useful for expensive validations or validations that depend on valid data. |
| [`message`](#actions-destroy-validate-message){: #actions-destroy-validate-message } | `String.t` | | If provided, overrides any message set by the validation error |
| [`description`](#actions-destroy-validate-description){: #actions-destroy-validate-description } | `String.t` | | An optional description for the validation |
| [`before_action?`](#actions-destroy-validate-before_action?){: #actions-destroy-validate-before_action? } | `boolean` | `false` | If set to `true`, the validation will be run in a before_action hook |
| [`before_action?`](#actions-destroy-validate-before_action?){: #actions-destroy-validate-before_action? } | `boolean` | `false` | If set to `true`, the validation will be run in a before_action hook, i.e. when the action is executed (inside the transaction) rather than when the changeset is built. Useful for validations that run queries or are otherwise expensive. Cannot be used with atomic actions. |
| [`always_atomic?`](#actions-destroy-validate-always_atomic?){: #actions-destroy-validate-always_atomic? } | `boolean` | `false` | By default, validations are only run atomically if all changes will be run atomically or if there is no `validate/3` callback defined. Set this to `true` to run it atomically always. |


Expand Down Expand Up @@ -3027,7 +3027,7 @@ validate present([:first_name, :last_name], at_least: 1)
| [`only_when_valid?`](#validations-validate-only_when_valid?){: #validations-validate-only_when_valid? } | `boolean` | `false` | If the validation should only run on valid changesets. Useful for expensive validations or validations that depend on valid data. |
| [`message`](#validations-validate-message){: #validations-validate-message } | `String.t` | | If provided, overrides any message set by the validation error |
| [`description`](#validations-validate-description){: #validations-validate-description } | `String.t` | | An optional description for the validation |
| [`before_action?`](#validations-validate-before_action?){: #validations-validate-before_action? } | `boolean` | `false` | If set to `true`, the validation will be run in a before_action hook |
| [`before_action?`](#validations-validate-before_action?){: #validations-validate-before_action? } | `boolean` | `false` | If set to `true`, the validation will be run in a before_action hook, i.e. when the action is executed (inside the transaction) rather than when the changeset is built. Useful for validations that run queries or are otherwise expensive. Cannot be used with atomic actions. |
| [`always_atomic?`](#validations-validate-always_atomic?){: #validations-validate-always_atomic? } | `boolean` | `false` | By default, validations are only run atomically if all changes will be run atomically or if there is no `validate/3` callback defined. Set this to `true` to run it atomically always. |


Expand Down Expand Up @@ -3185,7 +3185,7 @@ validate changing(:email)
| [`only_when_valid?`](#pipelines-pipeline-validate-only_when_valid?){: #pipelines-pipeline-validate-only_when_valid? } | `boolean` | `false` | If the validation should only run on valid changesets. Useful for expensive validations or validations that depend on valid data. |
| [`message`](#pipelines-pipeline-validate-message){: #pipelines-pipeline-validate-message } | `String.t` | | If provided, overrides any message set by the validation error |
| [`description`](#pipelines-pipeline-validate-description){: #pipelines-pipeline-validate-description } | `String.t` | | An optional description for the validation |
| [`before_action?`](#pipelines-pipeline-validate-before_action?){: #pipelines-pipeline-validate-before_action? } | `boolean` | `false` | If set to `true`, the validation will be run in a before_action hook |
| [`before_action?`](#pipelines-pipeline-validate-before_action?){: #pipelines-pipeline-validate-before_action? } | `boolean` | `false` | If set to `true`, the validation will be run in a before_action hook, i.e. when the action is executed (inside the transaction) rather than when the changeset is built. Useful for validations that run queries or are otherwise expensive. Cannot be used with atomic actions. |
| [`always_atomic?`](#pipelines-pipeline-validate-always_atomic?){: #pipelines-pipeline-validate-always_atomic? } | `boolean` | `false` | By default, validations are only run atomically if all changes will be run atomically or if there is no `validate/3` callback defined. Set this to `true` to run it atomically always. |


Expand Down
2 changes: 1 addition & 1 deletion documentation/topics/actions/actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -620,7 +620,7 @@ The hooks execute in the following order (as of Ash 3.0+):

#### Transaction Boundaries
- **Outside Transaction**: `around_transaction`, `before_transaction`, `after_transaction`
- **Inside Transaction**: Action preparations/validations/changes, Global preparations/validations/changes, `around_action`, `before_action`, `after_action`
- **Inside Transaction**: `around_action`, `before_action`, `after_action`, and any validations marked with `before_action? true` (or all global validations when the action has `delay_global_validations? true`)

#### Error Handling
- `after_action` only runs on successful operations
Expand Down
32 changes: 32 additions & 0 deletions documentation/topics/resources/validations.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,38 @@ validations do
end
```

## before_action? Option

By default, validations run while the changeset, query, or action input is being *built*, i.e. inside `Ash.Changeset.for_create/4`, `Ash.Changeset.for_update/4`, `Ash.Query.for_read/4` and friends. That happens before the action is executed and outside of any transaction. Setting `before_action? true` defers a validation into a `before_action` hook instead, so it runs when the action is actually executed with `Ash.create/2`, `Ash.update/2`, etc.

```elixir
actions do
update :assign_worker do
argument :worker_id, :uuid, allow_nil?: false

# Runs a query, so only run it on execution, inside the transaction
validate MyApp.Validations.WorkerIsActive do
before_action? true
only_when_valid? true
end
end
end
```

There are two reasons to do this:

1. **The validation is expensive, and changesets are built more often than they are executed.** A validation that runs a query or calls an external service will otherwise run every time the changeset is built. For example, `AshPhoenix.Form.validate/3` rebuilds the changeset on every form change event, so a querying validation would hit the database on every keystroke. With `before_action? true` it only runs on submit.

2. **The validation needs to see the state at execution time.** `before_action` hooks run *inside* the data layer transaction, immediately before the data layer operation. A check made at build time can be stale by the time the action runs. Running inside the transaction means the validation observes the same state as the write that follows it. Note that this does not by itself lock rows, so for guarantees like uniqueness you still want an identity or a database constraint.

Things to keep in mind:

- **Errors surface later.** A `before_action?` validation is not run by `Ash.Changeset.for_create/4` and friends, so its errors will not be shown during live form validation, only once the action is submitted.
- **It cannot be atomic.** Because it runs Elixir code inside a hook, an update or destroy action with `require_atomic? true` will fail with an error explaining this. Bulk actions fall back to the `:stream` strategy. Set `require_atomic? false` on the action if you need this.
- **It runs after `before_transaction` and `around_transaction` hooks.** Those hooks run outside the transaction, before any `before_action?` validation. See the action lifecycle in `d:Ash.Resource.Dsl.actions` for the full hook order.
- **`only_when_valid?` is checked at hook time.** Combining both options, as in the example above, skips the expensive check if any earlier validation or change has already added an error.
- **`delay_global_validations?`** on an action is equivalent to setting `before_action? true` on every validation from the global `validations` block for that action.

## Atomic Validations

To make a validation atomic, you have to implement the `c:Ash.Resource.Validation.atomic/3` callback. This callback returns an atomic instruction, or a list of atomic instructions, or an error/indication that the validation cannot be done atomically. For our `IsPrime` example above, this would look something like:
Expand Down
3 changes: 2 additions & 1 deletion lib/ash/resource/validation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@ defmodule Ash.Resource.Validation do
before_action?: [
type: :boolean,
default: false,
doc: "If set to `true`, the validation will be run in a before_action hook"
doc:
"If set to `true`, the validation will be run in a before_action hook, i.e. when the action is executed (inside the transaction) rather than when the changeset is built. Useful for validations that run queries or are otherwise expensive. Cannot be used with atomic actions."
],
always_atomic?: [
type: :boolean,
Expand Down
Loading