5 Commits

Author SHA1 Message Date
davidskrundz 3bfebe424a Add tests around the new entity format 2025-11-02 10:38:53 -07:00
davidskrundz 9b868c98de Update sea-orm to 2.0.0-rc.16 2025-10-28 21:41:08 -06:00
davidskrundz 9cf1f1f0c5 Support IP addresses 2025-10-20 10:49:10 -06:00
davidskrundz 302612281f Update sea-orm to 2.0.0-rc.10 2025-10-13 23:18:16 -06:00
davidskrundz 96f260d02d std -> core 2025-10-13 21:30:39 -06:00
9 changed files with 702 additions and 276 deletions
Generated
+288 -250
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -35,8 +35,8 @@ overflow-checks = true
strip = "debuginfo"
[workspace.dependencies]
sea-orm = { version = "2.0.0-rc.7", default-features = false }
sea-orm-migration = { version = "2.0.0-rc.7", 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 }
+2 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "seamantic"
version = "0.0.6"
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"] }
+15 -5
View File
@@ -3,7 +3,7 @@
use core::time::Duration;
use sea_orm::sea_query::{ArrayType, Nullable, ValueType, ValueTypeErr};
use sea_orm::{ColIdx, ColumnType, QueryResult, TryGetError, TryGetable, Value};
use sea_orm::{ColIdx, ColumnType, DbErr, QueryResult, TryFromU64, TryGetError, TryGetable, Value};
// "u64 unsupported by sqlx-sqlite", so use i64 as the bit representation
type SeaOrmRepr = i64;
@@ -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)]
@@ -40,7 +40,7 @@ impl ValueType for Seconds {
}
fn type_name() -> String {
core::any::type_name::<Duration>().to_string()
core::any::type_name::<Self>().to_string()
}
fn array_type() -> ArrayType {
@@ -67,6 +67,15 @@ impl TryGetable for Seconds {
}
}
impl TryFromU64 for Seconds {
fn try_from_u64(n: u64) -> Result<Self, DbErr> {
SeaOrmRepr::try_from_u64(n)
.map(|i| DurationRepr::from_ne_bytes(i.to_ne_bytes()))
.map(Duration::from_secs)
.map(Self)
}
}
impl Nullable for Seconds {
fn null() -> Value {
SeaOrmRepr::null()
@@ -76,8 +85,8 @@ impl Nullable for Seconds {
#[cfg(test)]
mod tests {
use sea_orm::{
ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EnumIter,
PrimaryKeyTrait,
ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait,
EnumIter, PrimaryKeyTrait,
};
use super::Seconds;
@@ -88,6 +97,7 @@ mod tests {
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
id: u8,
#[sea_orm(primary_key, auto_increment = false)]
seconds: Seconds,
nullable: Option<Seconds>,
}
+3 -3
View File
@@ -78,7 +78,7 @@ impl<T> Id<T> {
}
impl<T> fmt::Debug for Id<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Id")
.field("T", &core::any::type_name::<T>())
.field("id", &self.id)
@@ -140,8 +140,8 @@ impl<T> Nullable for Id<T> {
#[cfg(test)]
mod tests {
use sea_orm::{
ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EnumIter,
PrimaryKeyTrait,
ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait,
EnumIter, PrimaryKeyTrait,
};
use super::Id;
+179
View File
@@ -0,0 +1,179 @@
//! [IpAddr], [Ipv4Addr], and [Ipv6Addr] utilities
use core::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use sea_orm::sea_query::{ArrayType, Nullable, ValueType, ValueTypeErr};
use sea_orm::{ColIdx, ColumnType, DbErr, QueryResult, TryFromU64, TryGetError, TryGetable, Value};
type SeaOrmRepr = String;
/// Wrapper around [IpAddr]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[repr(transparent)]
pub struct IpAddress(pub IpAddr);
/// Wrapper around [IpAddr]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[repr(transparent)]
pub struct Ipv4Address(pub Ipv4Addr);
/// Wrapper around [IpAddr]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[repr(transparent)]
pub struct Ipv6Address(pub Ipv6Addr);
macro_rules! impl_addr {
($t:ty, $inner:ty, $repr:ty) => {
impl From<$inner> for $t {
fn from(value: $inner) -> Self {
Self(value)
}
}
impl From<$t> for $inner {
fn from(value: $t) -> Self {
value.0
}
}
impl ValueType for $t {
fn try_from(v: Value) -> Result<Self, ValueTypeErr> {
<SeaOrmRepr as ValueType>::try_from(v)
.and_then(|s| s.parse().map_err(|_| ValueTypeErr))
.map(Self)
}
fn type_name() -> String {
core::any::type_name::<Self>().to_string()
}
fn array_type() -> ArrayType {
SeaOrmRepr::array_type()
}
fn column_type() -> ColumnType {
SeaOrmRepr::column_type()
}
}
impl From<$t> for Value {
fn from(value: $t) -> Self {
value.0.to_string().into()
}
}
impl TryGetable for $t {
fn try_get_by<I: ColIdx>(res: &QueryResult, index: I) -> Result<Self, TryGetError> {
SeaOrmRepr::try_get_by(res, index)
.and_then(|s| s.parse().map_err(|_| TryGetError::Null(Self::type_name())))
.map(Self)
}
}
impl TryFromU64 for $t {
fn try_from_u64(n: u64) -> Result<Self, DbErr> {
SeaOrmRepr::try_from_u64(n)
.and_then(|s| {
s.parse()
.map_err(|_| DbErr::ConvertFromU64(core::any::type_name::<Self>()))
})
.map(Self)
}
}
impl Nullable for $t {
fn null() -> Value {
SeaOrmRepr::null()
}
}
};
}
impl_addr!(IpAddress, IpAddr, SeaOrmRepr);
impl_addr!(Ipv4Address, Ipv4Addr, SeaOrmRepr);
impl_addr!(Ipv6Address, Ipv6Addr, SeaOrmRepr);
#[cfg(test)]
mod tests {
mod ipaddress {
use sea_orm::{
ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait,
EnumIter, PrimaryKeyTrait,
};
use super::super::IpAddress;
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "ipaddress")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
id: u8,
#[sea_orm(primary_key, auto_increment = false)]
addr: IpAddress,
nullable: Option<IpAddress>,
}
impl ActiveModelBehavior for ActiveModel {}
#[allow(dead_code)]
#[derive(Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
}
mod ipv4address {
use sea_orm::{
ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait,
EnumIter, PrimaryKeyTrait,
};
use super::super::Ipv4Address;
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "ipv4address")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
id: u8,
#[sea_orm(primary_key, auto_increment = false)]
addr: Ipv4Address,
nullable: Option<Ipv4Address>,
}
impl ActiveModelBehavior for ActiveModel {}
#[allow(dead_code)]
#[derive(Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
}
mod ipv6address {
use sea_orm::{
ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait,
EnumIter, PrimaryKeyTrait,
};
use super::super::Ipv6Address;
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "ipv6address")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
id: u8,
#[sea_orm(primary_key, auto_increment = false)]
addr: Ipv6Address,
nullable: Option<Ipv6Address>,
}
impl ActiveModelBehavior for ActiveModel {}
#[allow(dead_code)]
#[derive(Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
}
}
+1
View File
@@ -2,4 +2,5 @@
pub mod duration;
pub mod id;
pub mod ip;
pub mod path;
+14 -4
View File
@@ -9,7 +9,7 @@ use std::os::unix::ffi::OsStringExt as _;
compile_error!("PathBytes is not supported on Windows");
use sea_orm::sea_query::{ArrayType, Nullable, ValueType, ValueTypeErr};
use sea_orm::{ColIdx, ColumnType, QueryResult, TryGetError, TryGetable, Value};
use sea_orm::{ColIdx, ColumnType, DbErr, QueryResult, TryFromU64, TryGetError, TryGetable, Value};
type SeaOrmRepr = Vec<u8>;
@@ -47,7 +47,7 @@ impl ValueType for PathBytes {
}
fn type_name() -> String {
core::any::type_name::<PathBuf>().to_string()
core::any::type_name::<Self>().to_string()
}
fn array_type() -> ArrayType {
@@ -74,6 +74,15 @@ impl TryGetable for PathBytes {
}
}
impl TryFromU64 for PathBytes {
fn try_from_u64(n: u64) -> Result<Self, DbErr> {
SeaOrmRepr::try_from_u64(n)
.map(OsString::from_vec)
.map(PathBuf::from)
.map(Self)
}
}
impl Nullable for PathBytes {
fn null() -> Value {
SeaOrmRepr::null()
@@ -83,8 +92,8 @@ impl Nullable for PathBytes {
#[cfg(test)]
mod tests {
use sea_orm::{
ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EnumIter,
PrimaryKeyTrait,
ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait,
EnumIter, PrimaryKeyTrait,
};
use super::PathBytes;
@@ -95,6 +104,7 @@ mod tests {
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
id: u8,
#[sea_orm(primary_key, auto_increment = false)]
path: PathBytes,
nullable: Option<PathBytes>,
}
+198 -11
View File
@@ -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<Model>,
/// }
/// impl ActiveModelBehavior for ActiveModel {}
pub fn sqlite_rowid_alias<T: IntoIden>(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<T: IntoIden>(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<Model>,
}
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")