Describe the bug
When a record is deleted and a foreign-key constraint fires because of dangling related rows, AshPostgres surfaces an Ecto.ConstraintError whose message claims the constraint failed "when attempting to insert struct" — even though the operation was a delete and nothing is being inserted.
The misleading wording comes from lib/data_layer.ex where the generic Postgrex.Error branch of handle_raised_error/4 calls constraints_to_errors(:insert, ...) regardless of the real operation:
https://github.com/ash-project/ash_postgres/blob/main/lib/data_layer.ex#L2872
If the resource doesn't have a matching foreign_key_constraint registered on the changeset, no user_constraint matches inside constraints_to_errors/5, so the nil -> fallback raises Ecto.ConstraintError.exception(action: :insert, ...) and that's what bubbles up to the caller.
For deletes, add_related_foreign_key_constraints/3 is responsible for registering the relevant FK constraints, but it only walks declared relationships (and only finds an FK when both the source's relationship to the destination and the destination's back-relation exist — see the existing TODO comment at lib/data_layer.ex:3110-3111). When the relationship isn't declared on either side, no constraint is registered, the fallback fires, and the misleading "insert struct" error reaches the user.
Reproduction
- Have a resource
A and a resource B whose table has a foreign key to A's table.
- Don't declare the relationship between them on the Ash side (so
add_related_foreign_key_constraints/3 won't register the FK on the changeset for deletes).
- Insert an
A and a B referencing it, with no ON DELETE cascade at the database level.
- Try to permanently delete the
A.
Expected: an Ash error explaining that related records still reference this row, and (ideally) pointing at the relevant resolutions — declaring the relationship + using cascade_destroy, or setting references do reference :a, on_delete: :delete end so PostgreSQL handles it.
Actual:
```
{:error, %Ash.Error.Unknown{
errors: [%Ash.Error.Unknown.UnknownError{
error: "** (Ecto.ConstraintError) constraint error when attempting to insert struct: "
_
fkey" (foreign_key_constraint)..."
}]
}}
```
Suggested fix
To produce a useful error message, the error reporting code needs to know which operation was being performed. Concretely:
- Thread the actual operation (
:create | :update | :delete) through handle_raised_error/4 from each call site so it reaches the generic Postgrex.Error clause. Today that clause has no way to know the operation, so it hardcodes :insert.
- Call sites that need to pass the operation:
lib/data_layer.ex L1868 (create/update), L2136 (destroy), L3730 (bulk update). The :bulk_create clause at L2823-2839 already implies :insert and can stay as-is.
- Use that operation in
constraints_to_errors/5:
- As the
action: field for the nil -> fallback (L2938-2944) so the wording matches reality (e.g. "... when attempting to delete struct" instead of "... insert struct").
- When the operation is
:delete and the violated constraint is a foreign key with no matching user_constraint, return a friendlier Ash error (e.g. Ash.Error.Changes.InvalidChanges or a dedicated FK-violation error) that names the constraint, rather than raising the raw Ecto.ConstraintError.
Environment
- AshPostgres: main (line numbers above are against
4be82c75)
- PostgreSQL: any supported version
Describe the bug
When a record is deleted and a foreign-key constraint fires because of dangling related rows, AshPostgres surfaces an
Ecto.ConstraintErrorwhose message claims the constraint failed "when attempting to insert struct" — even though the operation was a delete and nothing is being inserted.The misleading wording comes from
lib/data_layer.exwhere the genericPostgrex.Errorbranch ofhandle_raised_error/4callsconstraints_to_errors(:insert, ...)regardless of the real operation:https://github.com/ash-project/ash_postgres/blob/main/lib/data_layer.ex#L2872
If the resource doesn't have a matching
foreign_key_constraintregistered on the changeset, nouser_constraintmatches insideconstraints_to_errors/5, so thenil ->fallback raisesEcto.ConstraintError.exception(action: :insert, ...)and that's what bubbles up to the caller.For deletes,
add_related_foreign_key_constraints/3is responsible for registering the relevant FK constraints, but it only walks declared relationships (and only finds an FK when both the source's relationship to the destination and the destination's back-relation exist — see the existing TODO comment atlib/data_layer.ex:3110-3111). When the relationship isn't declared on either side, no constraint is registered, the fallback fires, and the misleading "insert struct" error reaches the user.Reproduction
Aand a resourceBwhose table has a foreign key toA's table.add_related_foreign_key_constraints/3won't register the FK on the changeset for deletes).Aand aBreferencing it, with noON DELETEcascade at the database level.A.Expected: an Ash error explaining that related records still reference this row, and (ideally) pointing at the relevant resolutions — declaring the relationship + using
cascade_destroy, or settingreferences do reference :a, on_delete: :delete endso PostgreSQL handles it.Actual:
```
_fkey" (foreign_key_constraint)..."{:error, %Ash.Error.Unknown{
errors: [%Ash.Error.Unknown.UnknownError{
error: "** (Ecto.ConstraintError) constraint error when attempting to insert struct: "
}]
}}
```
Suggested fix
To produce a useful error message, the error reporting code needs to know which operation was being performed. Concretely:
:create | :update | :delete) throughhandle_raised_error/4from each call site so it reaches the genericPostgrex.Errorclause. Today that clause has no way to know the operation, so it hardcodes:insert.lib/data_layer.exL1868 (create/update), L2136 (destroy), L3730 (bulk update). The:bulk_createclause at L2823-2839 already implies:insertand can stay as-is.constraints_to_errors/5:action:field for thenil ->fallback (L2938-2944) so the wording matches reality (e.g."... when attempting to delete struct"instead of"... insert struct").:deleteand the violated constraint is a foreign key with no matchinguser_constraint, return a friendlier Ash error (e.g.Ash.Error.Changes.InvalidChangesor a dedicated FK-violation error) that names the constraint, rather than raising the rawEcto.ConstraintError.Environment
4be82c75)