Skip to content

Misleading "attempting to insert struct" error on delete when a foreign-key relationship is not declared #745

@barnabasJ

Description

@barnabasJ

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

  1. Have a resource A and a resource B whose table has a foreign key to A's table.
  2. 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).
  3. Insert an A and a B referencing it, with no ON DELETE cascade at the database level.
  4. 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:

  1. 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.
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions