SeaQL/sea-orm · error

item is a full model

Error message

item is a full model

What it means

In establish_links (used when diffing existing links for has_many/many-to-many relations), the code takes each leftover model and calls leftover.get_primary_key_value().expect("item is a full model"). This expect documents the API contract: get_primary_key_value only yields Some for a fully-loaded Model (not a partial ActiveModel), so the panic means a non-full model reached a code path that requires a complete row with all primary key fields populated.

Source

Thrown at sea-orm-sync/src/entity/active_model.rs:1284

        let via_key = get_key_from_active_model(&right.from_col, &via)?;
        if !leftover.iter().any(|t| t.1 == via_key) {
            // if not already exist, save for insert
            via_models.push(via);
        }
        if delete_leftover {
            all_keys.insert(via_key);
        }
    }

    if delete_leftover {
        let mut to_delete = Vec::new();
        let mut to_delete_am = Vec::new();
        for (leftover, key) in leftover {
            if !all_keys.contains(&key) {
                to_delete.push(
                    leftover
                        .get_primary_key_value()
                        .expect("item is a full model"),
                );
                to_delete_am.push(leftover);
            }
        }
        if !to_delete.is_empty() {
            // run before_delete hooks
            for am in to_delete_am.clone() {
                am.before_delete(db)?;
            }
            if db.support_returning() {
                let deleted = J::delete_many()
                    .filter_by_value_tuples(&to_delete, db.get_database_backend())
                    .exec_with_returning(db)?;
                // run after_delete hooks with the returned value if possible
                for am in deleted {
                    let am = am.into_active_model();
                    let _ = am.after_delete(db)?;
                }

View on GitHub (pinned to e29bcd1b41)

Solutions

  1. Ensure every model passed to establish_links is a fully loaded Model (from Entity::find()) or ActiveModel::from(model) with primary keys set.
  2. Load the related rows from the database first (find().one/all) instead of constructing ActiveModels manually, then pass them to the link API.
  3. If using partial models, set the primary key fields explicitly (ActiveValue::Set(pk)) before calling the API.
  4. Handle the case at the type level: prefer APIs accepting Model over ActiveModel when you cannot guarantee completeness.

Example fix

// before: partial ActiveModel without primary key reaches establish_links
let am = post::ActiveModel { title: Set("hi".into()), ..Default::default() };
establish_links(am, ...);

// after: load the full model first
let full = post::Entity::find_by_id(post_id).one(db).await?.unwrap();
establish_links(full.into_active_model(), ...);
Defensive patterns

Strategy: type-guard

Validate before calling

// Only pass fully-loaded models into link-establishing APIs
fn assert_full_model<M: ModelTrait>(m: &M) {
    let pk: Vec<_> = M::PrimaryKey::iter().map(|c| m.get(&c)).collect();
    assert!(pk.iter().all(|v| !matches!(v, ActiveValue::NotSet)),
        "model passed to establish_links has unset primary key fields");
}

Type guard

fn has_primary_key_set<M: ModelTrait>(m: &M) -> bool {
    M::PrimaryKey::iter().all(|c| !matches!(m.get(&c), ActiveValue::NotSet))
}

Try / catch

// expect() panics are not catchable via Result; prefer loading full models:
match Entity::find_by_id(id).one(db).await? {
    Some(full_model) => establish_links(full_model.into_active_model(), ...),
    None => return Err(DbErr::RecordNotFound(format!("id {} not found", id))),
}

Prevention

When it happens

Trigger: Calling establish_links (or higher-level APIs that diff and prune links, e.g. replacing a relation's item set) while supplying ActiveModel values that are not full Models — e.g. partial updates with unset primary key fields, or NotSet primary keys on models passed into the link-replacement API.

Common situations: Passing ActiveModel::default() or partially-built models (from UpdateMany/Unchanged of incomplete rows) into relation-establishing helpers; using models loaded with select-only subsets of columns so primary key fields are absent; constructing ActiveModels by hand and calling save/link APIs that expect full models.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of SeaQL/sea-orm@e29bcd1b41 (2026-09-10). Data as JSON: /api/errors/6fc51e434060bfda. Report an issue: GitHub.