Skip to content

Introduce BelongsTo#3133

Open
tyt2y3 wants to merge 6 commits into
masterfrom
belongs-to-opt-in
Open

Introduce BelongsTo#3133
tyt2y3 wants to merge 6 commits into
masterfrom
belongs-to-opt-in

Conversation

@tyt2y3

@tyt2y3 tyt2y3 commented Jul 12, 2026

Copy link
Copy Markdown
Member

Brings the BelongsTo relation type from #3118 onto master, but makes it opt-in so existing belongs_to code keeps compiling:

  • belongs_to now accepts the recommended BelongsTo<E> / BelongsTo<Option<E>> (compile-time cardinality, paired with ActiveBelongsTo) or the legacy HasOne<E> field type. A HasOne-typed belongs_to generates the pre-BelongsTo ActiveHasOne write path — it sets this row's own foreign key on save, with relation-keyed disambiguation for duplicate targets (two belongs_to to the same entity).
  • Reverts belongs_to detach: null the FK if nullable, clean error if not #3127 ("belongs_to detach: null the FK if nullable, error if not"), which was merged but never shipped, in favour of Support non-null HasOne #3118's approach.
  • Fixes action() skipping the insert of a brand-new row that has no column explicitly Set (e.g. an auto-increment-only primary key), which previously left the generated key unset.

Backward compatibility

BelongsTo is the recommended type for belongs_to; HasOne<E> remains supported as a legacy field type. Existing entities, examples, and doctests are unaffected.

Tests

  • New blogger_legacy fixtures (a HasOne-belongs_to mirror of blogger) + belongs_to_legacy_tests: nested writes (has_one + belongs_to, including a brand-new nested parent), many-to-many through a junction, duplicate-target belongs_to, and the entity loader — all exercising the legacy HasOne path.
  • Full sqlite suite green: integration 256, lib 256, doctests 130; sea-orm-sync (rusqlite) green.

Notes

  • sea-orm-sync regenerated via build-tools/make-sync.sh; this also resyncs runtime files that had drifted from src.
  • Basing on Support non-null HasOne #3118 inherits its HasOne / ActiveHasOne internal reshape relative to rc.42; the backward-compatibility work here targets belongs_to specifically.

tyt2y3 and others added 6 commits July 12, 2026 19:13
* Rename `Has*Model` to `ActiveHas*`

* Support non-null HasOne

* Fix E0034 ambiguity: unify HasOne::loaded / ActiveHasOne::set constructors

The per-cardinality inherent impls each defined a same-named associated
constructor (loaded / set), so the unqualified paths HasOne::loaded and
ActiveHasOne::set were ambiguous (E0034) at every call site — the E: EntityTrait
where-bound does not disambiguate associated-function path resolution. The crate's
own tests and doctests failed to compile as a result.

Define loaded / set once on the cardinality-generic impl, dispatching arg-wrapping
through IntoHasOneLoaded / IntoActiveHasOneSet helper traits (one impl for the
required E case, one for the optional Option<E> case). Bare model -> required,
Option<model> -> optional, resolved unambiguously.

Also fixes an entity_loader test that passed a bare model to a nullable relation
(cake.bakery is HasOne<Option<..>>), which the ambiguity had masked.

* Support nested writes for duplicate-target belongs_to; clear() no-op on unset FK

Two fixes to nested-ActiveModel save behavior:

1. belongs_to relations that share a target entity with another relation used to
   silently generate no save/detach code (the action codegen was gated on the
   target entity being unique), so their nested writes were no-ops. They now always
   generate: a belongs_to with an explicit relation_enum, or a non-unique target,
   is keyed by its relation variant via `set_parent_key_for` / `clear_parent_key_for`
   (the default variant is inferred the same way the loader does); a unique target
   keeps the entity-keyed path. Fixes e.g. `user_follower` (two belongs_to to `user`).

2. `clear_<field>()` on a nullable belongs_to whose FK column was never set (a
   freshly-built ActiveModel) returned `DbErr::AttrNotSet`; it is now a no-op, since
   an unset nullable FK is already effectively clear.

Adds `test_belongs_to_duplicate_target` and `test_clear_unset_belongs_to_is_noop`.

* Revert now-redundant turbofish to bare HasOne::loaded / ActiveHasOne::set

With the E0034 root-cause fix in place, the unqualified constructors resolve
unambiguously, so the per-site `::<Option<E>>::` turbofish is no longer needed.
Revert it in the doc example, lib test modules, into_active_model bodies, and the
integration tests so users see the clean bare-call form. The macro-generated code
keeps its explicit turbofish (internal, not user-facing).

* Fix ActiveModelEx optional belongs_to clearing

* Split HasOne and BelongsTo

---------

Co-authored-by: Chris Tsang <chris.2y3@outlook.com>
`belongs_to` now accepts `HasOne<E>` (legacy) in addition to the recommended
`BelongsTo<E>` / `BelongsTo<Option<E>>`, so entities written against the earlier
API keep compiling. A `HasOne`-typed `belongs_to` generates the pre-`BelongsTo`
`ActiveHasOne` write path — it sets this row's own foreign key on save, with
relation-keyed disambiguation for duplicate targets (two `belongs_to` to the
same entity). `BelongsTo` remains the recommended type.

Also fix `action()` skipping the insert of a brand-new row that has no column
explicitly `Set` (e.g. an auto-increment-only primary key), which left the
generated key unset and failed the subsequent model conversion. A genuinely new
row (no primary key set) now always inserts, while an unchanged row that already
has a primary key is still left untouched.
`derive_model_tests` and `entity_loader_tests` referenced `HasOne` where the
fixtures now declare `BelongsTo`, so they failed to compile once a database
backend feature enabled the test bodies. Update the import and constructor to
`BelongsTo`.
Add `blogger_legacy`, a mirror of the `blogger` fixtures whose `belongs_to`
fields keep the legacy `HasOne<E>` type, and `belongs_to_legacy_tests` covering
nested writes (has_one + belongs_to, including a brand-new nested parent),
many-to-many through a junction, duplicate-target `belongs_to`, and the entity
loader. Document the `BelongsTo` / `HasOne` split in the changelog.
Run `build-tools/make-sync.sh` to bring `sea-orm-sync` in line with `src/` and
`tests/`: propagate the legacy `HasOne` belongs_to support and the new
`blogger_legacy` / `belongs_to_legacy_tests`. This also resyncs runtime files
that had drifted from `src` (e.g. `ActiveHasOne`/`ActiveHasMany`/`ActiveBelongsTo`
derives, and an un-stripped `async fn` in the entity loader).
@tyt2y3 tyt2y3 requested a review from Huliiiiii July 12, 2026 23:55
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