Repository: SeaQL/sea-orm (rust)

Issue:
Hitting 'negative last_insert_rowid' panic with Sqlite
## Description

I have a table with a primary ID that is a hash, stored as a signed integer (since unsigned 64 bit is not supported). Auto-increment is turned off for that table.
When storing a new element, if the hash as signed is negative, then sea-orm panics on that line: https://github.com/SeaQL/sea-orm/blob/master/src/executor/execute.rs#L43

## Steps to Reproduce

- Create an SQlite table with an i64 primary key: https://github.com/nitnelave/lldap/blob/c64d32e2c0cc698c61ca40e8364ab0cd7934cd46/server/src/domain/model/jwt_refresh_storage.rs#L12
- Insert a negative value.

### Expected Behavior

Since auto-increment is turned off for that table, `last_insert_id` should probably not be called to avoid a panic.

### Actual Behavior

Panic.

### Reproduces How Often

Always.

## Versions

Using sea-orm 0.10.3 (no other version override). Running on alpine, using the bundled Sqlite.

## Additional Information

The query, for reference: https://github.com/nitnelave/lldap/blob/c64d32e2c0cc698c61ca40e8364ab0cd7934cd46/server/src/infra/sql_backend_handler.rs#L49

Pull request description:
Don't call last_insert_id if not needed
Fixes #1357 

Patch:
diff --git a/issues/1357/Cargo.toml b/issues/1357/Cargo.toml
new file mode 100644
index 00000000..55e7bfc3
--- /dev/null
+++ b/issues/1357/Cargo.toml
@@ -0,0 +1,18 @@
+[workspace]
+# A separate workspace
+
+[package]
+name = "sea-orm-issues-1357"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[dependencies]
+anyhow = "1"
+serde = "1"
+tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
+
+[dependencies.sea-orm]
+path = "../../"
+default-features = false
+features = ["macros", "runtime-tokio-native-tls", "sqlx-sqlite"]
diff --git a/issues/1357/src/entity.rs b/issues/1357/src/entity.rs
new file mode 100644
index 00000000..3ad25f9f
--- /dev/null
+++ b/issues/1357/src/entity.rs
@@ -0,0 +1,14 @@
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
+#[sea_orm(table_name = "pool")]
+pub struct Model {
+    #[sea_orm(primary_key, auto_increment = false)]
+    pub id: i64,
+    pub name: String,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {}
+
+impl ActiveModelBehavior for ActiveModel {}
diff --git a/issues/1357/src/main.rs b/issues/1357/src/main.rs
new file mode 100644
index 00000000..f7488beb
--- /dev/null
+++ b/issues/1357/src/main.rs
@@ -0,0 +1,42 @@
+use anyhow::Result;
+use sea_orm::{ConnectionTrait, Database, EntityTrait, IntoActiveModel, Schema};
+
+mod entity;
+
+use entity::*;
+
+#[tokio::main]
+async fn main() -> Result<()> {
+    let db = Database::connect("sqlite::memory:").await.unwrap();
+
+    let builder = db.get_database_backend();
+    let schema = Schema::new(builder);
+    let stmt = schema.create_table_from_entity(Entity);
+    db.execute(builder.build(&stmt)).await?;
+
+    let model = Model {
+        id: 100,
+        name: "Hello".to_owned(),
+    };
+
+    let res = Entity::insert(model.clone().into_active_model())
+        .exec(&db)
+        .await?;
+
+    assert_eq!(Entity::find().one(&db).await?, Some(model.clone()));
+    assert_eq!(res.last_insert_id, model.id);
+
+    let model = Model {
+        id: -10,
+        name: "World".to_owned(),
+    };
+
+    let res = Entity::insert(model.clone().into_active_model())
+        .exec(&db)
+        .await?;
+
+    assert_eq!(Entity::find().one(&db).await?, Some(model.clone()));
+    assert_eq!(res.last_insert_id, model.id);
+
+    Ok(())
+}
diff --git a/src/error.rs b/src/error.rs
index 7988bd1b..2ed87ffd 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -58,8 +58,8 @@ pub enum DbErr {
     /// None of the records are being inserted into the database,
     /// if you insert with upsert expression that means
     /// all of them conflict with existing records in the database
-    #[error("RecordNotInserted Error: {0}")]
-    RecordNotInserted(String),
+    #[error("None of the records are being inserted")]
+    RecordNotInserted,
 }
 
 /// Runtime error
diff --git a/src/executor/execute.rs b/src/executor/execute.rs
index 3da4ec8a..f3a7150d 100644
--- a/src/executor/execute.rs
+++ b/src/executor/execute.rs
@@ -52,7 +52,7 @@ impl ExecResult {
         }
     }
 
-    /// Get the number of rows affedted by the operation
+    /// Get the number of rows affected by the operation
     pub fn rows_affected(&self) -> u64 {
         match &self.result {
             #[cfg(feature = "sqlx-mysql")]
diff --git a/src/executor/insert.rs b/src/executor/insert.rs
index 796cd362..c4c988ba 100644
--- a/src/executor/insert.rs
+++ b/src/executor/insert.rs
@@ -137,29 +137,37 @@ where
 {
     type PrimaryKey<A> = <<A as ActiveModelTrait>::Entity as EntityTrait>::PrimaryKey;
     type ValueTypeOf<A> = <PrimaryKey<A> as PrimaryKeyTrait>::ValueType;
-    let last_insert_id_opt = match db.support_returning() {
-        true => {
+
+    let last_insert_id = match (primary_key, db.support_returning()) {
+        (Some(value_tuple), _) => {
+            let res = db.execute(statement).await?;
+            if res.rows_affected() == 0 {
+                return Err(DbErr::RecordNotInserted);
+            }
+            FromValueTuple::from_value_tuple(value_tuple)
+        }
+        (None, true) => {
+            let mut rows = db.query_all(statement).await?;
+            let row = match rows.pop() {
+                Some(row) => row,
+                None => return Err(DbErr::RecordNotInserted),
+            };
             let cols = PrimaryKey::<A>::iter()
                 .map(|col| col.to_string())
                 .collect::<Vec<_>>();
-            let rows = db.query_all(statement).await?;
-            let res = rows.last().ok_or_else(|| {
-                DbErr::RecordNotInserted("None of the records are being inserted".to_owned())
-            })?;
-            res.try_get_many("", cols.as_ref()).ok()
+            row.try_get_many("", cols.as_ref())
+                .map_err(|_| DbErr::UnpackInsertId)?
         }
-        false => {
-            let last_insert_id = db.execute(statement).await?.last_insert_id();
-            ValueTypeOf::<A>::try_from_u64(last_insert_id).ok()
+        (None, false) => {
+            let res = db.execute(statement).await?;
+            if res.rows_affected() == 0 {
+                return Err(DbErr::RecordNotInserted);
+            }
+            let last_insert_id = res.last_insert_id();
+            ValueTypeOf::<A>::try_from_u64(last_insert_id).map_err(|_| DbErr::UnpackInsertId)?
         }
     };
-    let last_insert_id = match primary_key {
-        Some(value_tuple) => FromValueTuple::from_value_tuple(value_tuple),
-        None => match last_insert_id_opt {
-            Some(last_insert_id) => last_insert_id,
-            None => return Err(DbErr::UnpackInsertId),
-        },
-    };
+
     Ok(InsertResult { last_insert_id })
 }
 


Test patch:
diff --git a/tests/string_primary_key_tests.rs b/tests/string_primary_key_tests.rs
index 94d1ae55..9ed4df47 100644
--- a/tests/string_primary_key_tests.rs
+++ b/tests/string_primary_key_tests.rs
@@ -2,7 +2,7 @@ pub mod common;
 
 pub use common::{features::*, setup::*, TestContext};
 use pretty_assertions::assert_eq;
-use sea_orm::{entity::prelude::*, entity::*, DatabaseConnection};
+use sea_orm::{entity::prelude::*, entity::*, sea_query::OnConflict, DatabaseConnection};
 use serde_json::json;
 
 #[sea_orm_macros::test]
@@ -30,7 +30,7 @@ pub async fn insert_and_delete_repository(db: &DatabaseConnection) -> Result<(),
     }
     .into_active_model();
 
-    let result = repository.insert(db).await?;
+    let result = repository.clone().insert(db).await?;
 
     assert_eq!(
         result,
@@ -42,6 +42,17 @@ pub async fn insert_and_delete_repository(db: &DatabaseConnection) -> Result<(),
         }
     );
 
+    #[cfg(any(feature = "sqlx-sqlite", feature = "sqlx-postgres"))]
+    {
+        let err = Repository::insert(repository)
+            // MySQL does not support DO NOTHING, we might workaround that later
+            .on_conflict(OnConflict::new().do_nothing().to_owned())
+            .exec(db)
+            .await;
+
+        assert_eq!(err.err(), Some(DbErr::RecordNotInserted));
+    }
+
     result.delete(db).await?;
 
     assert_eq!(
diff --git a/tests/upsert_tests.rs b/tests/upsert_tests.rs
index ad874e79..748d5b0a 100644
--- a/tests/upsert_tests.rs
+++ b/tests/upsert_tests.rs
@@ -54,12 +54,7 @@ pub async fn create_insert_default(db: &DatabaseConnection) -> Result<(), DbErr>
     .exec(db)
     .await;
 
-    assert_eq!(
-        res.err(),
-        Some(DbErr::RecordNotInserted(
-            "None of the records are being inserted".to_owned()
-        ))
-    );
+    assert_eq!(res.err(), Some(DbErr::RecordNotInserted));
 
     Ok(())
 }