seamantic 0.0.6

A library to enhance SeaORM
Documentation
use sea_orm_migration::schema::{integer_null, string};
use sea_orm_migration::sea_query::{ColumnDef, IntoIden};

/// Sets the column to be an alias for SQLite's rowid.
///
/// Required conditions:
/// - This column must *not* be auto_increment
/// - There cannot be other primary_key columns in the table
///
/// When using `DeriveEntityModel`, the type must be `i64` (or equivalent)
/// and should be tagged with:
///
/// `#[sea_orm(primary_key, auto_increment = false)]`
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
pub fn sqlite_case_insensitive_string<T: IntoIden>(name: T) -> ColumnDef {
	string(name).extra("COLLATE NOCASE").take()
}

#[cfg(test)]
mod tests {
	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 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");
		Database::connect(options).await.expect("Database::connect")
	}

	#[tokio::test]
	async fn test_sqlite_rowid_alias() {
		struct Migrator;
		impl MigratorTrait for Migrator {
			fn migrations() -> Vec<Box<dyn MigrationTrait>> {
				vec![Box::new(Migration)]
			}
		}

		#[derive(Iden)]
		pub(super) enum TestTable {
			Table,
			Id,
		}
		#[derive(DeriveMigrationName)]
		struct Migration;
		#[async_trait]
		impl MigrationTrait for Migration {
			async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
				manager
					.create_table(
						Table::create()
							.table(TestTable::Table)
							.col(sqlite_rowid_alias(TestTable::Id))
							.to_owned(),
					)
					.await?;

				Ok(())
			}
		}

		#[derive(Debug, Clone, DeriveEntityModel)]
		#[sea_orm(table_name = "test_table")]
		pub struct Model {
			#[sea_orm(primary_key, auto_increment = false)]
			id: Id<Model>,
		}
		#[derive(Debug, EnumIter)]
		pub enum Relation {}
		impl RelationTrait for Relation {
			fn def(&self) -> RelationDef {
				panic!("No RelationDef")
			}
		}
		impl ActiveModelBehavior for ActiveModel {}

		let db = new_memory_db().await;
		Migrator::up(&db, None).await.expect("up");

		// 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);
		}
	}

	#[tokio::test]
	async fn test_sqlite_case_insensitive_string() {
		struct Migrator;
		impl MigratorTrait for Migrator {
			fn migrations() -> Vec<Box<dyn MigrationTrait>> {
				vec![Box::new(Migration)]
			}
		}

		#[derive(Iden)]
		pub(super) enum TestTable {
			Table,
			Id,
			CiStr,
		}
		#[derive(DeriveMigrationName)]
		struct Migration;
		#[async_trait]
		impl MigrationTrait for Migration {
			async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
				manager
					.create_table(
						Table::create()
							.table(TestTable::Table)
							.col(sqlite_rowid_alias(TestTable::Id))
							.col(sqlite_case_insensitive_string(TestTable::CiStr))
							.to_owned(),
					)
					.await?;

				Ok(())
			}
		}

		#[derive(Debug, Clone, DeriveEntityModel)]
		#[sea_orm(table_name = "test_table")]
		pub struct Model {
			#[sea_orm(primary_key, auto_increment = false)]
			id: i64,
			ci_str: String,
		}
		#[derive(Debug, EnumIter)]
		pub enum Relation {}
		impl RelationTrait for Relation {
			fn def(&self) -> RelationDef {
				panic!("No RelationDef")
			}
		}
		impl ActiveModelBehavior for ActiveModel {}

		let db = new_memory_db().await;
		Migrator::up(&db, None).await.expect("up");

		let i = 50;

		// Insert a lowercase string
		{
			let model = ActiveModel {
				id: Set(i),
				ci_str: Set("abcd".to_owned()),
			};
			model.insert(&db).await.expect("insert");
		}

		// Query by uppercase string
		{
			let model = Entity::find()
				.filter(Column::CiStr.contains("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.ci_str, "abcd");
		}
	}
}