diff --git a/CHANGELOG.md b/CHANGELOG.md index 5380bfe90f..b94f5b45ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,10 +30,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). * Added `DatabaseConnection::close` https://github.com/SeaQL/sea-orm/pull/1236 * Refactor schema module to expose functions for database alteration https://github.com/SeaQL/sea-orm/pull/1256 * Added `is_null` getter for `ColumnDef` struct https://github.com/SeaQL/sea-orm/pull/1381 -* Postgres insert many will throw `RecordNotInserted` error if non of them are being inserted https://github.com/SeaQL/sea-orm/pull/1021 * `MockDatabase::append_exec_results()`, `MockDatabase::append_query_results()`, `MockDatabase::append_exec_errors()` and `MockDatabase::append_query_errors()` take any types implemented `IntoIterator` trait https://github.com/SeaQL/sea-orm/pull/1367 * `find_by_id` and `delete_by_id` take any Into primary key value https://github.com/SeaQL/sea-orm/pull/1362 * Added `ActiveValue::reset` to convert `Unchanged` into `Set` https://github.com/SeaQL/sea-orm/pull/1177 +* Generate compact entity with `#[sea_orm(column_type = "JsonBinary")]` macro attribute https://github.com/SeaQL/sea-orm/pull/1346 ### Upgrades @@ -47,11 +47,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/). * Removed dependency when not needed https://github.com/SeaQL/sea-orm/pull/1213 * Changed all version = "^x.y.z" into version = "x.y.z" and disabled default features and enable only the needed ones https://github.com/SeaQL/sea-orm/pull/1300 * Cleanup the use of `vec!` macros https://github.com/SeaQL/sea-orm/pull/1367 +* Fixed a small typo https://github.com/SeaQL/sea-orm/pull/1391 ### Bug Fixes * Fixes `DeriveColumn` (by qualifying `IdenStatic::as_str`) https://github.com/SeaQL/sea-orm/pull/1280 * Prevent returning connections to pool with a positive transaction depth https://github.com/SeaQL/sea-orm/pull/1283 +* Postgres insert many will throw `RecordNotInserted` error if non of them are being inserted https://github.com/SeaQL/sea-orm/pull/1021 + * Fixes inserting active models by `insert_many` with `on_conflict` and `do_nothing` panics if no rows are inserted on Postgres https://github.com/SeaQL/sea-orm/issues/899 +* Don't call `last_insert_id` if not needed https://github.com/SeaQL/sea-orm/pull/1403 + * Fixes hitting 'negative last_insert_rowid' panic with Sqlite https://github.com/SeaQL/sea-orm/issues/1357 ### Breaking changes @@ -83,6 +88,13 @@ impl ActiveModelBehavior for ActiveModel { } ``` +## 0.10.7 - 2023-01-19 + +### Bug Fixes + +* Inserting active models by `insert_many` with `on_conflict` and `do_nothing` panics if no rows are inserted on Postgres https://github.com/SeaQL/sea-orm/issues/899 +* Hitting 'negative last_insert_rowid' panic with Sqlite https://github.com/SeaQL/sea-orm/issues/1357 + ## 0.10.6 - 2022-12-23 ### Enhancements diff --git a/examples/jsonrpsee_example/api/Cargo.toml b/examples/jsonrpsee_example/api/Cargo.toml index 9e98a03c3a..6440e00417 100644 --- a/examples/jsonrpsee_example/api/Cargo.toml +++ b/examples/jsonrpsee_example/api/Cargo.toml @@ -16,4 +16,4 @@ migration = { path = "../migration" } anyhow = "1.0.52" async-trait = "0.1.52" log = { version = "0.4", features = ["std"] } -simplelog = "*" +simplelog = "0.12" diff --git a/issues/1357/Cargo.toml b/issues/1357/Cargo.toml new file mode 100644 index 0000000000..55e7bfc336 --- /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 0000000000..3ad25f9ff7 --- /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 0000000000..f7488beb40 --- /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/sea-orm-codegen/src/entity/column.rs b/sea-orm-codegen/src/entity/column.rs index bcc1932b3a..fecb71fa44 100644 --- a/sea-orm-codegen/src/entity/column.rs +++ b/sea-orm-codegen/src/entity/column.rs @@ -94,6 +94,7 @@ impl Column { ColumnType::Decimal(Some((p, s))) => Some(format!("Decimal(Some(({}, {})))", p, s)), ColumnType::Money(Some((p, s))) => Some(format!("Money(Some({}, {}))", p, s)), ColumnType::Text => Some("Text".to_owned()), + ColumnType::JsonBinary => Some("JsonBinary".to_owned()), ColumnType::Custom(iden) => { Some(format!("Custom(\"{}\".to_owned())", iden.to_string())) } diff --git a/sea-orm-codegen/src/entity/writer.rs b/sea-orm-codegen/src/entity/writer.rs index 21568380b2..e14883e154 100644 --- a/sea-orm-codegen/src/entity/writer.rs +++ b/sea-orm-codegen/src/entity/writer.rs @@ -1957,4 +1957,117 @@ mod tests { Ok(expected.to_string()) } + + #[test] + fn test_gen_postgres() -> io::Result<()> { + let entities = vec![ + // This tests that the JsonBinary column type is annotated + // correctly in compact entity form. More information can be found + // in this issue: + // + // https://github.com/SeaQL/sea-orm/issues/1344 + Entity { + table_name: "task".to_owned(), + columns: vec![ + Column { + name: "id".to_owned(), + col_type: ColumnType::Integer, + auto_increment: true, + not_null: true, + unique: false, + }, + Column { + name: "payload".to_owned(), + col_type: ColumnType::Json, + auto_increment: false, + not_null: true, + unique: false, + }, + Column { + name: "payload_binary".to_owned(), + col_type: ColumnType::JsonBinary, + auto_increment: false, + not_null: true, + unique: false, + }, + ], + relations: vec![], + conjunct_relations: vec![], + primary_keys: vec![PrimaryKey { + name: "id".to_owned(), + }], + }, + ]; + const ENTITY_FILES: [&str; 1] = [include_str!("../../tests/postgres/binary_json.rs")]; + + const ENTITY_FILES_EXPANDED: [&str; 1] = + [include_str!("../../tests/postgres/binary_json_expanded.rs")]; + + assert_eq!(entities.len(), ENTITY_FILES.len()); + + for (i, entity) in entities.iter().enumerate() { + assert_eq!( + parse_from_file(ENTITY_FILES[i].as_bytes())?.to_string(), + EntityWriter::gen_compact_code_blocks( + entity, + &crate::WithSerde::None, + &crate::DateTimeCrate::Chrono, + &None, + false, + false, + &TokenStream::new(), + &TokenStream::new(), + ) + .into_iter() + .skip(1) + .fold(TokenStream::new(), |mut acc, tok| { + acc.extend(tok); + acc + }) + .to_string() + ); + assert_eq!( + parse_from_file(ENTITY_FILES[i].as_bytes())?.to_string(), + EntityWriter::gen_compact_code_blocks( + entity, + &crate::WithSerde::None, + &crate::DateTimeCrate::Chrono, + &Some("public".to_owned()), + false, + false, + &TokenStream::new(), + &TokenStream::new(), + ) + .into_iter() + .skip(1) + .fold(TokenStream::new(), |mut acc, tok| { + acc.extend(tok); + acc + }) + .to_string() + ); + assert_eq!( + parse_from_file(ENTITY_FILES_EXPANDED[i].as_bytes())?.to_string(), + EntityWriter::gen_expanded_code_blocks( + entity, + &crate::WithSerde::None, + &crate::DateTimeCrate::Chrono, + &Some("schema_name".to_owned()), + false, + false, + &TokenStream::new(), + &TokenStream::new(), + ) + .into_iter() + .skip(1) + .fold(TokenStream::new(), |mut acc, tok| { + acc.extend(tok); + acc + }) + .to_string() + ); + } + + Ok(()) + } } diff --git a/sea-orm-codegen/tests/postgres/binary_json.rs b/sea-orm-codegen/tests/postgres/binary_json.rs new file mode 100644 index 0000000000..3d7cf95075 --- /dev/null +++ b/sea-orm-codegen/tests/postgres/binary_json.rs @@ -0,0 +1,21 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 +//! +//! This file tests that the JsonBinary column type is annotated correctly is +//! compact entity form. More information can be found in this issue: +//! +//! https://github.com/SeaQL/sea-orm/issues/1344 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "task")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub payload: Json, + #[sea_orm(column_type = "JsonBinary")] + pub payload_binary: Json, +} +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/postgres/binary_json_expanded.rs b/sea-orm-codegen/tests/postgres/binary_json_expanded.rs new file mode 100644 index 0000000000..543e77f1f6 --- /dev/null +++ b/sea-orm-codegen/tests/postgres/binary_json_expanded.rs @@ -0,0 +1,65 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 +//! +//! This file tests that the JsonBinary column type is annotated correctly is +//! expanded entity form. More information can be found in this issue: +//! +//! https://github.com/SeaQL/sea-orm/issues/1344 + +use sea_orm::entity::prelude::*; + +#[derive(Copy, Clone, Default, Debug, DeriveEntity)] +pub struct Entity; +impl EntityName for Entity { + fn schema_name(&self) -> Option< &str > { + Some("schema_name") + } + fn table_name(&self) -> &str { + "task" + } +} + +#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Eq)] +pub struct Model { + pub id: i32, + pub payload: Json, + pub payload_binary: Json, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] +pub enum Column { + Id, + Payload, + PayloadBinary, +} + +#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] +pub enum PrimaryKey { + Id, +} + +impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + fn auto_increment() -> bool { + true + } +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation {} +impl ColumnTrait for Column { + type EntityName = Entity; + fn def(&self) -> ColumnDef { + match self { + Self::Id => ColumnType::Integer.def(), + // This is the part that is being tested. + Self::Payload => ColumnType::Json.def(), + Self::PayloadBinary => ColumnType::JsonBinary.def(), + } + } +} +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + panic!("No RelationDef") + } +} +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/error.rs b/src/error.rs index 7988bd1b8a..2ed87ffdca 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 3da4ec8a34..f3a7150de5 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 796cd36290..c4c988ba9d 100644 --- a/src/executor/insert.rs +++ b/src/executor/insert.rs @@ -137,29 +137,37 @@ where { type PrimaryKey = <::Entity as EntityTrait>::PrimaryKey; type ValueTypeOf = 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::::iter() .map(|col| col.to_string()) .collect::>(); - 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::::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::::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 }) } diff --git a/src/executor/select.rs b/src/executor/select.rs index fbae345161..c36753f1eb 100644 --- a/src/executor/select.rs +++ b/src/executor/select.rs @@ -70,7 +70,7 @@ where model: PhantomData, } -/// Defines a type to get two Modelss +/// Defines a type to get two Models #[derive(Clone, Debug)] pub struct SelectTwoModel where diff --git a/tests/string_primary_key_tests.rs b/tests/string_primary_key_tests.rs index 94d1ae554c..9ed4df4763 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 ad874e79c8..748d5b0a33 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(()) }