Nested ActiveModelEx::save() fails to insert a new has_many child with a composite, non-auto-increment primary key
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 9.9k
- Forks
- 734
- Avg merge
- 6h 36m
- Merged PRs (30d)
- 8
Description
Description
Nested ActiveModelEx::save() (SeaORM 2.0) picks insert-vs-update per related row via is_update(), which just checks whether the primary key columns are Set/Unchanged. For an entity with a composite, non-auto-increment primary key (e.g. a join/ownership table keyed by (parent_id, tag)), the key is always fully set when building a new row, so a brand-new child is misdiagnosed as an update. The resulting UPDATE matches zero rows and save() fails with RecordNotFound, instead of inserting the row.
This works fine for auto-increment primary keys (a fresh row naturally has NotSet on the pk), but composite non-auto-increment keys are a common shape for has_many children (join tables, per-user records, etc.), so save() can't be used to add a new child to an already-persisted parent in that case. The same misdiagnosis also hits any entity whose (single-column) primary key is a UUID assigned application-side rather than by the database — a very common pattern — since that pk is likewise always Set on a fresh row, not NotSet.
Steps to reproduce
use sea_orm::entity::prelude::*;
use sea_orm::{ConnectionTrait, Database, Schema};
mod parent {
use sea_orm::entity::prelude::*;
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "parent")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: i32,
#[sea_orm(has_many)]
pub children: HasMany<super::child::Entity>,
}
impl ActiveModelBehavior for ActiveModel {}
}
mod child {
use sea_orm::entity::prelude::*;
// Composite, non-auto-increment primary key.
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "child")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub parent_id: i32,
#[sea_orm(primary_key, auto_increment = false)]
pub tag: i32,
#[sea_orm(belongs_to, from = "parent_id", to = "id")]
pub parent: HasOne<super::parent::Entity>,
}
impl ActiveModelBehavior for ActiveModel {}
}
#[tokio::main]
async fn main() -> Result<(), DbErr> {
let db = Database::connect("sqlite::memory:").await?;
let builder = db.get_database_backend();
let schema = Schema::new(builder);
db.execute(&schema.create_table_from_entity(parent::Entity)).await?;
db.execute(&schema.create_table_from_entity(child::Entity)).await?;
parent::ActiveModel::builder().set_id(1).insert(&db).await?;
let loaded = parent::Entity::load()
.filter(parent::COLUMN.id.eq(1))
.one(&db)
.await?
.expect("parent exists");
let mut active: parent::ActiveModelEx = loaded.into();
active.children.push(child::ActiveModel::builder().set_tag(42));
let result = active.save(&db).await; // <- fails
println!("{:?}", result.map(|_| ()));
Ok(())
}
Cargo.toml dependency: sea-orm = { version = "=2.0.2", features = ["sqlx-sqlite", "runtime-tokio-rustls", "macros"] }
Expected behaviour
The new child row is INSERTed.
Actual behaviour
Err(RecordNotFound Error: Failed to find updated item)
No row is inserted; save() returns an error instead.
Versions
- sea-orm: 2.0.2
- rustc: latest stable
Happy to help narrow this down further if useful — thanks for the 2.0 nested-save feature, it's great for the auto-increment case!
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at the SeaORM 2.0 ActiveModelEx::save() path and its is_update() check, then run the SQLite reproduction from the issue. Trace how a newly built child with composite or application-assigned keys is classified, and verify that the expected result is an INSERT with no RecordNotFound error.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust, sqlite
- Domain
- database
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 58/100