{"record":{"id":"6fc51e434060bfda","repo":"SeaQL/sea-orm","slug":"item-is-a-full-model","errorCode":null,"errorMessage":"item is a full model","messagePattern":"item is a full model","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"sea-orm-sync/src/entity/active_model.rs","lineNumber":1284,"sourceCode":"        let via_key = get_key_from_active_model(&right.from_col, &via)?;\n        if !leftover.iter().any(|t| t.1 == via_key) {\n            // if not already exist, save for insert\n            via_models.push(via);\n        }\n        if delete_leftover {\n            all_keys.insert(via_key);\n        }\n    }\n\n    if delete_leftover {\n        let mut to_delete = Vec::new();\n        let mut to_delete_am = Vec::new();\n        for (leftover, key) in leftover {\n            if !all_keys.contains(&key) {\n                to_delete.push(\n                    leftover\n                        .get_primary_key_value()\n                        .expect(\"item is a full model\"),\n                );\n                to_delete_am.push(leftover);\n            }\n        }\n        if !to_delete.is_empty() {\n            // run before_delete hooks\n            for am in to_delete_am.clone() {\n                am.before_delete(db)?;\n            }\n            if db.support_returning() {\n                let deleted = J::delete_many()\n                    .filter_by_value_tuples(&to_delete, db.get_database_backend())\n                    .exec_with_returning(db)?;\n                // run after_delete hooks with the returned value if possible\n                for am in deleted {\n                    let am = am.into_active_model();\n                    let _ = am.after_delete(db)?;\n                }","sourceCodeStart":1266,"sourceCodeEnd":1302,"githubUrl":"https://github.com/SeaQL/sea-orm/blob/e29bcd1b417c41a553b386fe94511d7c64a1c8ec/sea-orm-sync/src/entity/active_model.rs#L1266-L1302","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Ensure every model passed to establish_links is a fully loaded Model (from Entity::find()) or ActiveModel::from(model) with primary keys set.","Load the related rows from the database first (find().one/all) instead of constructing ActiveModels manually, then pass them to the link API.","If using partial models, set the primary key fields explicitly (ActiveValue::Set(pk)) before calling the API.","Handle the case at the type level: prefer APIs accepting Model over ActiveModel when you cannot guarantee completeness."],"exampleFix":"// before: partial ActiveModel without primary key reaches establish_links\nlet am = post::ActiveModel { title: Set(\"hi\".into()), ..Default::default() };\nestablish_links(am, ...);\n\n// after: load the full model first\nlet full = post::Entity::find_by_id(post_id).one(db).await?.unwrap();\nestablish_links(full.into_active_model(), ...);","handlingStrategy":"type-guard","validationCode":"// Only pass fully-loaded models into link-establishing APIs\nfn assert_full_model<M: ModelTrait>(m: &M) {\n    let pk: Vec<_> = M::PrimaryKey::iter().map(|c| m.get(&c)).collect();\n    assert!(pk.iter().all(|v| !matches!(v, ActiveValue::NotSet)),\n        \"model passed to establish_links has unset primary key fields\");\n}","typeGuard":"fn has_primary_key_set<M: ModelTrait>(m: &M) -> bool {\n    M::PrimaryKey::iter().all(|c| !matches!(m.get(&c), ActiveValue::NotSet))\n}","tryCatchPattern":"// expect() panics are not catchable via Result; prefer loading full models:\nmatch Entity::find_by_id(id).one(db).await? {\n    Some(full_model) => establish_links(full_model.into_active_model(), ...),\n    None => return Err(DbErr::RecordNotFound(format!(\"id {} not found\", id))),\n}","preventionTips":["Load models with Entity::find() before passing them to relation APIs instead of hand-building ActiveModels","Never call establish_links with ActiveModel::default() or partial updates","Set primary keys explicitly (ActiveValue::Set) when constructing ActiveModels manually","Avoid SelectColumns/partial selects for models destined for link-diff APIs"],"tags":["panic","active-model","primary-key","relations","api-misuse"],"backgroundTag":"invalid-state-transition","analyzedSha":"e29bcd1b417c41a553b386fe94511d7c64a1c8ec","analyzedAt":"2026-09-10T11:31:52.468Z","contentChangedAt":"2026-09-10T11:31:52.468Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}