Skip to content

belongs_to detach: null the FK if nullable, clean error if not#3127

Merged
tyt2y3 merged 3 commits into
masterfrom
feat/belongs-to-detach-clean-error
Jul 8, 2026
Merged

belongs_to detach: null the FK if nullable, clean error if not#3127
tyt2y3 merged 3 commits into
masterfrom
feat/belongs-to-detach-clean-error

Conversation

@tyt2y3

@tyt2y3 tyt2y3 commented Jul 7, 2026

Copy link
Copy Markdown
Member

Detaching a belongs_to relation via the generated delete_<field>() builder (ActiveHasOne::Delete) was silently ignored on save: take() maps Delete to None, so the belongs_to action did nothing and the FK was never touched. So cake.delete_bakery().save(db) was a no-op.

This is the deliberately-minimal fix, keeping the single ActiveHasOne<E> type — nullability drives behavior at runtime, rather than being pushed into the type parameter (which is the direction #3118 explored; closing that in favor of this).

New Behavior

delete_<field>() on a belongs_to now:

  • nullable FK → nulls the column (detaches), keeping both rows.
  • non-nullable FK → returns a clean DbErr::Type ("relation cannot be detached") instead of attempting an UPDATE that the database's NOT NULL constraint would reject with a raw driver error.
  • never-set FK (a freshly-built ActiveModel) → no-op (inserts NULL), rather than AttrNotSet.

has_one detach (delete/orphan the child) is unchanged.

tyt2y3 added 2 commits July 7, 2026 23:53
`delete_<field>()` on a belongs_to (`ActiveHasOne::Delete`) was silently ignored on
save — `take()` mapped `Delete` to `None`, so the branch did nothing and the foreign
key was never touched.

Handle it in the belongs_to save action: `clear_parent_key::<Related>()` nulls this
row's FK when the column is nullable, and returns `false` when it is not — in which
case we return a clean `DbErr::Type` ("relation cannot be detached") instead of
attempting an UPDATE that a raw NOT NULL constraint would reject. Composite FKs are
handled by the relation def. Also make `clear_key_on_active_model` a no-op when the
FK column was never set (a freshly-built ActiveModel) rather than erroring, so
`delete_<field>()` on a fresh model just inserts NULL.

Keeps the single `ActiveHasOne` type — nullability drives behavior at runtime rather
than being encoded in the type.

Adds tests for nullable detach, non-nullable clean error, and detach-on-unset no-op.
@Huliiiiii

Huliiiiii commented Jul 8, 2026

Copy link
Copy Markdown
Member

I think a runtime error is much worse than an operation that is impossible to express. A foreign key is just a nullable ID column. Set(Option<M>) is more consistent.

When two belongs_to fields share a target entity (e.g. `user_follower.user` +
`user_follower.follower`, both -> `user`), there is no unique `Related<E>` to key
the FK write by, so the save action's `entity_count == 1` guard dropped them
entirely — nested writes silently no-op'd.

Route such relations through the relation-keyed helpers instead: capture their
relation variant at detection time (the explicit `relation_enum`, else the default
inferred the same way the loader/model_ex does) and use `set_parent_key_for` /
`clear_parent_key_for(Relation::X)`. Unique-target belongs_to keep the entity-keyed
path unchanged. Adds `ActiveModelTrait::clear_parent_key_for` (the relation-keyed
counterpart to `clear_parent_key`, mirroring `set_parent_key_for`).

Adds `test_belongs_to_duplicate_target`.
@tyt2y3

tyt2y3 commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

I think we can have BelongsTo<E>/BelongsTo<Option<E>> to pair with belongs_to for better compile-time guarantee. but it's a trade-off in terms of complexity. HasOne should be usable as is, and shouldn't need HasOne<Option<E>>.

so I guess we can continue to experiment on #3118 with something like:

    #[sea_orm(belongs_to, from = "BakeryId", to = "Id")]
    pub bakery: BelongsTo<Option<super::bakery::Entity>>,

@tyt2y3 tyt2y3 merged commit f0121e0 into master Jul 8, 2026
39 checks passed
@tyt2y3 tyt2y3 deleted the feat/belongs-to-detach-clean-error branch July 8, 2026 11:30
@tyt2y3 tyt2y3 mentioned this pull request Jul 12, 2026
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