SeaQL / SeaQL/sea-orm

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

Open
#3,183 9 comments 0 reactions 0 assignees View on GitHub

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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.