Skip to content

Nested ActiveModelEx::save() fails to insert a new has_many child with a composite, non-auto-increment primary key #3183

Description

@ziimakc

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!

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions