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!
Description
Nested
ActiveModelEx::save()(SeaORM 2.0) picks insert-vs-update per related row viais_update(), which just checks whether the primary key columns areSet/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 resultingUPDATEmatches zero rows andsave()fails withRecordNotFound, instead of inserting the row.This works fine for auto-increment primary keys (a fresh row naturally has
NotSeton the pk), but composite non-auto-increment keys are a common shape forhas_manychildren (join tables, per-user records, etc.), sosave()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 alwaysSeton a fresh row, notNotSet.Steps to reproduce
Cargo.tomldependency:sea-orm = { version = "=2.0.2", features = ["sqlx-sqlite", "runtime-tokio-rustls", "macros"] }Expected behaviour
The new child row is
INSERTed.Actual behaviour
No row is inserted;
save()returns an error instead.Versions
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!