From 3bfebe424a7f43a6f9d3fb10d635d38634b350a4 Mon Sep 17 00:00:00 2001 From: David Skrundz Date: Sun, 2 Nov 2025 10:38:53 -0700 Subject: [PATCH] Add tests around the new entity format --- Cargo.lock | 42 ++++--- Cargo.toml | 4 +- seamantic/Cargo.toml | 3 +- seamantic/src/model/duration.rs | 2 +- seamantic/src/schema/sqlite.rs | 209 ++++++++++++++++++++++++++++++-- 5 files changed, 229 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5b77ecc..3a2aaa5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,9 +115,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.43" +version = "1.2.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" +checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" dependencies = [ "find-msvc-tools", "shlex", @@ -669,6 +669,15 @@ dependencies = [ "syn", ] +[[package]] +name = "inventory" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" +dependencies = [ + "rustversion", +] + [[package]] name = "itertools" version = "0.14.0" @@ -1046,14 +1055,15 @@ dependencies = [ [[package]] name = "sea-orm" -version = "2.0.0-rc.16" +version = "2.0.0-rc.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6265b8190c0898db442eeb8333f277de801742f0072f984601a10bb3145237d3" +checksum = "296a092e16fbbcc3a9969eae3915ef11024b98a43f3b77ff5199bc626c462d7a" dependencies = [ "async-stream", "async-trait", "derive_more", "futures-util", + "inventory", "itertools", "log", "ouroboros", @@ -1071,9 +1081,9 @@ dependencies = [ [[package]] name = "sea-orm-cli" -version = "2.0.0-rc.16" +version = "2.0.0-rc.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d9971a23ea89ab63b48d19923871415358f07a1db72b4dd9a40b6cae625c32" +checksum = "301f7ace977d940474b47a154a5fc453714d13e5d53bdb077330c6fc6212a31b" dependencies = [ "chrono", "glob", @@ -1089,9 +1099,9 @@ dependencies = [ [[package]] name = "sea-orm-macros" -version = "2.0.0-rc.16" +version = "2.0.0-rc.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c54373b4c855be7a5b0691e1fcbdd1d5bd25a9a54efdcf922f281356a7129808" +checksum = "e56ee8be52d15a801dc62a5a30d83dc718762401f7c7945b7f2730ef385b8b3d" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -1103,9 +1113,9 @@ dependencies = [ [[package]] name = "sea-orm-migration" -version = "2.0.0-rc.16" +version = "2.0.0-rc.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82dbb9a68e0d64b5f5493f2924958e2149f1d4827bb381100d75886d1b1df281" +checksum = "b7444940e5b7db32f55dea6d04cfb312480e45ae07753b1391311eccd5753475" dependencies = [ "async-trait", "sea-orm", @@ -1177,7 +1187,7 @@ dependencies = [ [[package]] name = "seamantic" -version = "0.0.9" +version = "0.0.10" dependencies = [ "sea-orm", "sea-orm-migration", @@ -1590,9 +1600,9 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-ident" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-xid" @@ -1693,14 +1703,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.3", + "webpki-roots 1.0.4", ] [[package]] name = "webpki-roots" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" dependencies = [ "rustls-pki-types", ] diff --git a/Cargo.toml b/Cargo.toml index 4672645..b5117e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,8 +35,8 @@ overflow-checks = true strip = "debuginfo" [workspace.dependencies] -sea-orm = { version = "2.0.0-rc.16", default-features = false } -sea-orm-migration = { version = "2.0.0-rc.16", default-features = false } +sea-orm = { version = "2.0.0-rc.17", default-features = false } +sea-orm-migration = { version = "2.0.0-rc.17", default-features = false } serde = { version = "^1", default-features = false } serde_test = { version = "^1", default-features = false } diff --git a/seamantic/Cargo.toml b/seamantic/Cargo.toml index 8504f5a..2e76e61 100644 --- a/seamantic/Cargo.toml +++ b/seamantic/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "seamantic" -version = "0.0.9" +version = "0.0.10" categories = [] description = "A library to enhance SeaORM" @@ -35,6 +35,7 @@ sea-orm-migration = { workspace = true } serde = { workspace = true, features = ["derive", "std"], optional = true } [dev-dependencies] +sea-orm = { workspace = true, features = ["entity-registry", "schema-sync"] } sea-orm-migration = { workspace = true, features = ["runtime-tokio-rustls"] } serde_test = { workspace = true } tokio = { workspace = true, features = ["rt", "macros"] } diff --git a/seamantic/src/model/duration.rs b/seamantic/src/model/duration.rs index 6461986..a99c518 100644 --- a/seamantic/src/model/duration.rs +++ b/seamantic/src/model/duration.rs @@ -13,7 +13,7 @@ type DurationRepr = u64; /// /// ### Warning: /// Sub-second precision will be lost -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(transparent))] #[repr(transparent)] diff --git a/seamantic/src/schema/sqlite.rs b/seamantic/src/schema/sqlite.rs index 4b6ee76..1b9a3bf 100644 --- a/seamantic/src/schema/sqlite.rs +++ b/seamantic/src/schema/sqlite.rs @@ -11,34 +11,221 @@ use sea_orm_migration::sea_query::{ColumnDef, IntoIden}; /// and should be tagged with: /// /// `#[sea_orm(primary_key, auto_increment = false)]` +/// +/// When using the new entity format: +/// +/// use sea_orm::entity::prelude::*; +/// use seamantic::model::id::Id; +/// +/// #[sea_orm::model] +/// #[derive(Debug, Clone, DeriveEntityModel)] +/// #[sea_orm(table_name = "rowid_test")] +/// pub struct Model { +/// #[sea_orm(column_type = "Integer", primary_key, nullable, auto_increment = false)] +/// id: Id, +/// } +/// impl ActiveModelBehavior for ActiveModel {} pub fn sqlite_rowid_alias(name: T) -> ColumnDef { integer_null(name).primary_key().take() } /// Set the column to be a case insensitive string +/// +/// When using the new entity format: +/// +/// use sea_orm::entity::prelude::*; +/// use seamantic::model::id::Id; +/// +/// #[sea_orm::model] +/// #[derive(Debug, Clone, DeriveEntityModel)] +/// #[sea_orm(table_name = "nocase_test")] +/// pub struct Model { +/// #[sea_orm(primary_key, auto_increment = false)] +/// id: i64, +/// #[sea_orm(extra = "COLLATE NOCASE")] +/// nocase: String, +/// } +/// impl ActiveModelBehavior for ActiveModel {} pub fn sqlite_case_insensitive_string(name: T) -> ColumnDef { string(name).extra("COLLATE NOCASE").take() } +#[cfg(test)] +mod entity_tests { + use sea_orm::{ConnectOptions, Database, DatabaseConnection}; + + pub async fn new_initialized_memory_db() -> DatabaseConnection { + let options = ConnectOptions::new("sqlite::memory:"); + + let db = Database::connect(options) + .await + .expect("Database::connect()"); + db.get_schema_registry("seamantic::schema::sqlite::*") + .sync(&db) + .await + .expect("db.get_schema_registry().sync()"); + db + } + + mod rowid_test { + use sea_orm::ActiveValue::{NotSet, Set}; + use sea_orm::entity::prelude::*; + + use crate::model::id::{Id, SeaOrmRepr}; + + #[sea_orm::model] + #[derive(Debug, Clone, DeriveEntityModel)] + #[sea_orm(table_name = "rowid_test")] + pub struct Model { + #[sea_orm(column_type = "Integer", primary_key, nullable, auto_increment = false)] + id: Id, + } + impl ActiveModelBehavior for ActiveModel {} + + #[tokio::test] + async fn test_sqlite_rowid_alias() { + let db = super::new_initialized_memory_db().await; + + // Starts at 1 and increments + for i in 1..=3 { + let model = ActiveModel { id: NotSet }; + let model = model.insert(&db).await.expect("insert"); + assert_eq!(model.id.into_raw(), i); + } + + // Delete the top number and re-add + for i in 3..=3 { + let model = ActiveModel { + id: Set(Id::from_raw(i)), + }; + model.delete(&db).await.expect("delete"); + let model = ActiveModel { id: NotSet }; + let model = model.insert(&db).await.expect("insert"); + assert_eq!(model.id.into_raw(), i); + } + + // Jump to 100 and increment + for i in 100..=103 { + let model = ActiveModel { + id: Set(Id::from_raw(i)), + }; + let model = model.insert(&db).await.expect("insert"); + assert_eq!(model.id.into_raw(), i); + } + + // Continue to increment + for i in 104..=104 { + let model = ActiveModel { id: NotSet }; + let model = model.insert(&db).await.expect("insert"); + assert_eq!(model.id.into_raw(), i); + } + + // Jump to SeaOrmRepr::MAX and increment + for i in SeaOrmRepr::MAX..=SeaOrmRepr::MAX { + let model = ActiveModel { + id: Set(Id::from_raw(i)), + }; + let model = model.insert(&db).await.expect("insert"); + assert_eq!(model.id.into_raw(), i); + } + + // Next ones are random around the center + for _ in 0..3 { + let model = ActiveModel { id: NotSet }; + let model = model.insert(&db).await.expect("insert"); + assert!(model.id.into_raw() > 0); + } + + // Zero ID is valid + for i in 0..=0 { + let model = ActiveModel { + id: Set(Id::from_raw(i)), + }; + let model = model.insert(&db).await.expect("insert"); + assert_eq!(model.id.into_raw(), i); + } + + // Negative ID is valid + for i in SeaOrmRepr::MIN..=(SeaOrmRepr::MIN + 3) { + let model = ActiveModel { + id: Set(Id::from_raw(i)), + }; + let model = model.insert(&db).await.expect("insert"); + assert_eq!(model.id.into_raw(), i); + } + } + } + + mod nocase_test { + use sea_orm::ActiveValue::Set; + use sea_orm::entity::prelude::*; + + #[sea_orm::model] + #[derive(Debug, Clone, DeriveEntityModel)] + #[sea_orm(table_name = "nocase_test")] + pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + id: i64, + case: String, + #[sea_orm(extra = "COLLATE NOCASE")] + nocase: String, + } + impl ActiveModelBehavior for ActiveModel {} + + #[tokio::test] + async fn test_sqlite_case_insensitive_string() { + let db = super::new_initialized_memory_db().await; + + let i = 50; + + // Insert a lowercase string + { + let model = ActiveModel { + id: Set(i), + case: Set("abcd".to_owned()), + nocase: Set("abcd".to_owned()), + }; + model.insert(&db).await.expect("insert"); + } + + // Query by uppercase string + { + Entity::find() + .filter(Column::Case.eq("ABCD")) + .one(&db) + .await + .expect("find by case insensitive string") + .ok_or(()) + .expect_err("find by case insensitive string"); + + let model = Entity::find() + .filter(Column::Nocase.eq("ABCD")) + .one(&db) + .await + .expect("find by case insensitive string") + .expect("find by case insensitive string"); + assert_eq!(model.id, i); + // The string should be read back as-is + assert_eq!(model.nocase, "abcd"); + } + } + } +} + #[cfg(test)] mod tests { + use sea_orm::ActiveValue::{NotSet, Set}; + use sea_orm::entity::prelude::*; + use sea_orm::{ConnectOptions, Database, DatabaseConnection}; use sea_orm_migration::async_trait::async_trait; - use sea_orm_migration::sea_orm::QueryFilter; - use sea_orm_migration::sea_orm::{ - ActiveModelBehavior, ActiveModelTrait, ActiveValue::NotSet, ActiveValue::Set, ColumnTrait, - ConnectOptions, Database, DatabaseConnection, DbErr, DeriveEntityModel, - DeriveMigrationName, DerivePrimaryKey, EntityTrait, EnumIter, PrimaryKeyTrait, RelationDef, - RelationTrait, - }; - use sea_orm_migration::sea_query::{self, Iden, Table}; - use sea_orm_migration::{MigrationTrait, MigratorTrait, SchemaManager}; + use sea_orm_migration::prelude::*; use crate::model::id::{Id, SeaOrmRepr}; use super::{sqlite_case_insensitive_string, sqlite_rowid_alias}; async fn new_memory_db() -> DatabaseConnection { - let options = ConnectOptions::new("sqlite:/tmp/db?mode=memory"); + let options = ConnectOptions::new("sqlite::memory:"); Database::connect(options).await.expect("Database::connect") } @@ -228,7 +415,7 @@ mod tests { // Query by uppercase string { let model = Entity::find() - .filter(Column::CiStr.contains("ABCD")) + .filter(Column::CiStr.eq("ABCD")) .one(&db) .await .expect("find by case insensitive string")