#![cfg(any(feature = "rusqlite", feature = "turso", feature = "libsql"))]
use drizzle::core::expr::*;
use drizzle::sqlite::prelude::*;
#[derive(SQLiteEnum, PartialEq, Clone, Default, Debug)]
pub enum UserRole {
#[default]
Guest,
Member,
Admin,
}
#[derive(SQLiteEnum, Default, Debug, Clone, PartialEq)]
pub enum AccountStatus {
Suspended = -1,
#[default]
Inactive = 3,
Active,
}
#[derive(SQLiteEnum, Default, Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i64)]
enum SharedJobStatus {
#[default]
Queued = 0,
Complete = 1,
}
#[SQLiteTable]
struct UserAccount {
#[column(PRIMARY, AUTOINCREMENT)]
id: i64,
name: String,
#[column(ENUM)]
role: UserRole,
#[column(integer, ENUM)]
status: AccountStatus,
}
#[SQLiteTable]
struct PrimaryJob {
#[column(PRIMARY, AUTOINCREMENT)]
id: i64,
status: SharedJobStatus,
}
#[SQLiteTable]
struct SecondaryJob {
#[column(PRIMARY, AUTOINCREMENT)]
id: i64,
status: SharedJobStatus,
}
#[SQLiteTable]
struct NullableJob {
#[column(PRIMARY, AUTOINCREMENT)]
id: i64,
#[column(integer, ENUM)]
status: Option<SharedJobStatus>,
}
#[derive(SQLiteSchema)]
pub struct Schema {
user_account: UserAccount,
primary_job: PrimaryJob,
secondary_job: SecondaryJob,
nullable_job: NullableJob,
}
#[test]
fn test_enum() {
let role = UserRole::Admin;
let status = AccountStatus::Active;
assert_eq!(format!("{}", role), "Admin");
assert_eq!(format!("{}", status), "Active");
assert_eq!("Member".parse::<UserRole>().unwrap(), UserRole::Member);
assert_eq!(
"Suspended".parse::<AccountStatus>().unwrap(),
AccountStatus::Suspended
);
assert!(SharedJobStatus::decode(SQLiteValueRef::Text("Complete")).is_err());
assert!(UserRole::decode(SQLiteValueRef::Integer(0)).is_err());
}
#[cfg(feature = "query")]
#[test]
fn enum_query_codec_rejects_wrong_storage_class() {
let integer_as_text = drizzle::core::serde_json::json!({
"$drizzle_storage": "text",
"$drizzle_value": "Complete",
});
let text_as_integer = drizzle::core::serde_json::json!({
"$drizzle_storage": "integer",
"$drizzle_value": 0,
});
assert!(SharedJobStatus::decode_json(&integer_as_text).is_err());
assert!(UserRole::decode_json(&text_as_integer).is_err());
}
#[test]
fn test_table_generation() {
let _table = UserAccount::new();
let insert_model = InsertUserAccount::new("test", UserRole::Member, AccountStatus::Suspended);
let update_model = UpdateUserAccount::default();
let _insert_with_role = insert_model.with_role(UserRole::Member);
let _update_with_status = update_model.with_status(AccountStatus::Suspended);
let table_ref = &<UserAccount as drizzle::core::DrizzleTable>::TABLE_REF;
assert!(!table_ref.columns.is_empty());
}
#[test]
fn repr_enum_without_column_marker_uses_integer_ddl_in_multiple_tables() {
assert!(
PrimaryJob::create_table_sql().contains("`status` INTEGER NOT NULL"),
"primary table must use the enum's INTEGER storage authority"
);
assert!(
SecondaryJob::create_table_sql().contains("`status` INTEGER NOT NULL"),
"secondary table must use the same enum's INTEGER storage authority"
);
assert_eq!(
<PrimaryJob as drizzle::core::DrizzleTable>::TABLE_REF.columns[1].sql_type,
"INTEGER"
);
}
#[cfg(feature = "rusqlite")]
#[test]
fn repr_enum_rusqlite_parameter_binds_as_integer() {
let connection = rusqlite::Connection::open_in_memory().unwrap();
connection
.execute_batch("CREATE TABLE jobs (status INTEGER NOT NULL) STRICT")
.unwrap();
connection
.execute(
"INSERT INTO jobs (status) VALUES (?1)",
rusqlite::params![SharedJobStatus::Complete],
)
.unwrap();
let storage_class: String = connection
.query_row("SELECT typeof(status) FROM jobs", [], |row| row.get(0))
.unwrap();
assert_eq!(storage_class, "integer");
assert!(
connection
.query_row("SELECT 'Complete'", [], |row| {
row.get::<_, SharedJobStatus>(0)
})
.is_err()
);
}
#[cfg(feature = "libsql")]
#[tokio::test]
async fn nullable_libsql_enum_rejects_invalid_non_null_value() {
let database = libsql::Builder::new_local(":memory:")
.build()
.await
.unwrap();
let connection = database.connect().unwrap();
connection
.execute(
"CREATE TABLE nullable_job (id INTEGER PRIMARY KEY, status INTEGER)",
(),
)
.await
.unwrap();
connection
.execute("INSERT INTO nullable_job VALUES (1, 99)", ())
.await
.unwrap();
connection
.execute("INSERT INTO nullable_job VALUES (2, NULL)", ())
.await
.unwrap();
let mut rows = connection
.query("SELECT id, status FROM nullable_job WHERE id = 1", ())
.await
.unwrap();
let row = rows.next().await.unwrap().unwrap();
let error = SelectNullableJob::try_from(&row).expect_err("99 is not a valid enum value");
assert!(
error.to_string().contains("99"),
"unexpected error: {error}"
);
let mut rows = connection
.query("SELECT id, status FROM nullable_job WHERE id = 2", ())
.await
.unwrap();
let row = rows.next().await.unwrap().unwrap();
let decoded = SelectNullableJob::try_from(&row).unwrap();
assert_eq!(decoded.status, None);
}
#[allow(dead_code)]
#[derive(Debug, SQLiteFromRow)]
struct UserAccountResult {
id: i64,
name: String,
role: UserRole, status: AccountStatus, }
#[drizzle::test]
fn test_enum_database_roundtrip(db: &mut TestDb<Schema>) {
let Schema { user_account, .. } = schema;
let test_users = vec![
InsertUserAccount::new("guest_user", UserRole::Guest, AccountStatus::Inactive),
InsertUserAccount::new("member_user", UserRole::Member, AccountStatus::Active),
InsertUserAccount::new("admin_user", UserRole::Admin, AccountStatus::Suspended),
];
let inserted = db.insert(user_account).values(test_users).execute();
assert_eq!(inserted, 3);
let results: Vec<UserAccountResult> = db
.select((
user_account.id,
user_account.name,
user_account.role,
user_account.status,
))
.from(user_account)
.all();
assert_eq!(results.len(), 3);
let guest = results.iter().find(|u| u.name == "guest_user").unwrap();
assert_eq!(guest.role, UserRole::Guest);
assert_eq!(guest.status, AccountStatus::Inactive);
let member = results.iter().find(|u| u.name == "member_user").unwrap();
assert_eq!(member.role, UserRole::Member);
assert_eq!(member.status, AccountStatus::Active);
let admin = results.iter().find(|u| u.name == "admin_user").unwrap();
assert_eq!(admin.role, UserRole::Admin);
assert_eq!(admin.status, AccountStatus::Suspended);
let admin_users: Vec<UserAccountResult> = db
.select((
user_account.id,
user_account.name,
user_account.role,
user_account.status,
))
.from(user_account)
.r#where(eq(UserAccount::role, UserRole::Admin))
.all();
assert_eq!(admin_users.len(), 1);
assert_eq!(admin_users[0].name, "admin_user");
let suspended_users: Vec<UserAccountResult> = db
.select((
user_account.id,
user_account.name,
user_account.role,
user_account.status,
))
.from(user_account)
.r#where(eq(UserAccount::status, AccountStatus::Suspended))
.all();
assert_eq!(suspended_users.len(), 1);
assert_eq!(suspended_users[0].name, "admin_user");
}
#[drizzle::test]
fn repr_enum_shared_by_two_tables_filters_and_round_trips(db: &mut TestDb<Schema>) {
let Schema {
primary_job,
secondary_job,
..
} = schema;
let primary_inserted = db
.insert(primary_job)
.values([
InsertPrimaryJob::new(SharedJobStatus::Queued),
InsertPrimaryJob::new(SharedJobStatus::Complete),
])
.execute();
let secondary_inserted = db
.insert(secondary_job)
.values([
InsertSecondaryJob::new(SharedJobStatus::Complete),
InsertSecondaryJob::new(SharedJobStatus::Queued),
])
.execute();
assert_eq!(primary_inserted, 2);
assert_eq!(secondary_inserted, 2);
let primary: Vec<SelectPrimaryJob> = db
.select(())
.from(primary_job)
.r#where(eq(primary_job.status, SharedJobStatus::Complete))
.all();
let secondary: Vec<SelectSecondaryJob> = db
.select(())
.from(secondary_job)
.r#where(eq(secondary_job.status, SharedJobStatus::Queued))
.all();
let tuple_rows: Vec<(i64, SharedJobStatus)> = db
.select((primary_job.id, primary_job.status))
.from(primary_job)
.r#where(in_array(
primary_job.status,
[SharedJobStatus::Queued, SharedJobStatus::Complete],
))
.order_by([asc(primary_job.id)])
.all();
let excluded_rows: Vec<(i64, SharedJobStatus)> = db
.select((primary_job.id, primary_job.status))
.from(primary_job)
.r#where(not_in_array(
primary_job.status,
[SharedJobStatus::Complete],
))
.all();
assert_eq!(primary.len(), 1);
assert_eq!(primary[0].status, SharedJobStatus::Complete);
assert_eq!(secondary.len(), 1);
assert_eq!(secondary[0].status, SharedJobStatus::Queued);
assert_eq!(tuple_rows.len(), 2);
assert_eq!(tuple_rows[0].1, SharedJobStatus::Queued);
assert_eq!(tuple_rows[1].1, SharedJobStatus::Complete);
assert_eq!(excluded_rows.len(), 1);
assert_eq!(excluded_rows[0].1, SharedJobStatus::Queued);
}
#[cfg(feature = "query")]
#[drizzle::test]
fn marker_free_enum_query_uses_enum_owned_decoder(db: &mut TestDb<Schema>) {
let Schema { primary_job, .. } = schema;
db.insert(primary_job)
.values([InsertPrimaryJob::new(SharedJobStatus::Complete)])
.execute();
let rows = db.query(primary_job).find_many();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].status, SharedJobStatus::Complete);
}