#![cfg(feature = "async")]
use roomrs::{BuildAsyncExt, MigrationPolicy, dao, database, entity};
#[entity(table = "todos")]
#[derive(Debug, Clone, PartialEq)]
struct Todo {
#[pk(autoincrement)]
id: i64,
title: String,
done: bool,
}
#[dao]
trait TodoDao {
#[insert]
fn add(&self, t: &Todo) -> roomrs::Result<i64>;
#[query("SELECT * FROM todos WHERE id = :id")]
fn find(&self, id: i64) -> roomrs::Result<Option<Todo>>;
#[query("SELECT * FROM todos WHERE done = :done ORDER BY id")]
fn by_done(&self, done: bool) -> roomrs::Result<Vec<Todo>>;
#[update("UPDATE todos SET done = :done WHERE id = :id")]
fn set_done(&self, id: i64, done: bool) -> roomrs::Result<u64>;
#[delete("DELETE FROM todos WHERE id = :id")]
fn remove(&self, id: i64) -> roomrs::Result<u64>;
}
#[database(entities(Todo), daos(TodoDao), version = 1)]
struct Db;
async fn crud_roundtrip(dir: &tempfile::TempDir) -> roomrs::Result<()> {
let db = Db::builder()
.sqlite(dir.path().join("a.db"))
.migrate(MigrationPolicy::Auto)
.build_async()
.await?;
let h = db.run_async();
let dao = h.todo_dao();
let id = dao
.add(&Todo {
id: 0,
title: "비동기".into(),
done: false,
})
.await?;
assert!(id > 0);
let t = dao.find(id).await?.expect("존재해야 함");
assert_eq!(t.title, "비동기");
dao.set_done(id, true).await?;
assert!(dao.find(id).await?.unwrap().done);
assert_eq!(dao.by_done(false).await?.len(), 0);
let cnt: i64 = h.query_scalar("SELECT COUNT(*) FROM todos", ()).await?;
assert_eq!(cnt, 1);
use DbTxDaos as _;
h.transaction(|tx| {
tx.todo_dao().add(&Todo {
id: 0,
title: "tx".into(),
done: false,
})?;
Ok(())
})
.await?;
let cnt: i64 = h.query_scalar("SELECT COUNT(*) FROM todos", ()).await?;
assert_eq!(cnt, 2);
dao.remove(id).await?;
assert!(dao.find(id).await?.is_none());
Ok(())
}
#[test]
fn executor_tokio() {
let dir = tempfile::tempdir().unwrap();
tokio::runtime::Builder::new_multi_thread()
.build()
.unwrap()
.block_on(crud_roundtrip(&dir))
.unwrap();
}
#[cfg(not(feature = "tokio"))]
#[test]
fn executor_smol() {
let dir = tempfile::tempdir().unwrap();
smol::block_on(crud_roundtrip(&dir)).unwrap();
}
#[cfg(not(feature = "tokio"))]
#[test]
fn executor_futures() {
let dir = tempfile::tempdir().unwrap();
futures::executor::block_on(crud_roundtrip(&dir)).unwrap();
}
#[test]
fn futures_are_send_spawnable() {
let dir = tempfile::tempdir().unwrap();
let rt = tokio::runtime::Builder::new_multi_thread().build().unwrap();
rt.block_on(async {
let db = Db::builder()
.sqlite(dir.path().join("s.db"))
.build_async()
.await
.unwrap();
let handle = tokio::spawn(async move {
let h = db.run_async();
h.todo_dao()
.add(&Todo {
id: 0,
title: "spawned".into(),
done: false,
})
.await
});
let id = handle.await.expect("join").expect("insert");
assert!(id > 0);
});
}
#[cfg(feature = "tokio")]
#[test]
fn generated_dao_future_is_directly_spawnable() {
let dir = tempfile::tempdir().unwrap();
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let db = Db::builder()
.sqlite(dir.path().join("direct-spawn.db"))
.build_async()
.await
.unwrap();
let dao = db.run_async().todo_dao();
let future = dao.find(1);
drop(dao);
let _ = tokio::spawn(future).await.unwrap().unwrap();
});
}
#[cfg(not(feature = "tokio"))]
#[test]
fn panicking_job_is_isolated() {
let dir = tempfile::tempdir().unwrap();
futures::executor::block_on(async {
let db = Db::builder()
.sqlite(dir.path().join("p.db"))
.migrate(MigrationPolicy::Auto)
.build_async()
.await
.unwrap();
let h = db.run_async();
for _ in 0..64 {
let res: roomrs::Result<i64> = h.run(|_| panic!("테스트 패닉")).await;
assert!(matches!(res, Err(roomrs::Error::Internal(_))));
}
let id = h
.todo_dao()
.add(&Todo {
id: 0,
title: "생존".into(),
done: false,
})
.await
.unwrap();
assert!(id > 0);
});
}
#[cfg(feature = "tokio")]
#[test]
fn tokio_feature_works_outside_runtime() {
let dir = tempfile::tempdir().unwrap();
futures::executor::block_on(crud_roundtrip(&dir)).unwrap();
}
#[cfg(feature = "tokio")]
#[test]
fn tokio_panicking_job_is_isolated() {
let dir = tempfile::tempdir().unwrap();
tokio::runtime::Builder::new_multi_thread()
.build()
.unwrap()
.block_on(async {
let db = Db::builder()
.sqlite(dir.path().join("tp.db"))
.migrate(MigrationPolicy::Auto)
.build_async()
.await
.unwrap();
let h = db.run_async();
for _ in 0..16 {
let res: roomrs::Result<i64> = h.run(|_| panic!("테스트 패닉")).await;
assert!(matches!(res, Err(roomrs::Error::Internal(_))));
}
let id = h
.todo_dao()
.add(&Todo {
id: 0,
title: "생존".into(),
done: false,
})
.await
.unwrap();
assert!(id > 0);
});
}
#[cfg(feature = "tokio")]
#[test]
fn tokio_cancelled_future_is_safe() {
let dir = tempfile::tempdir().unwrap();
tokio::runtime::Builder::new_multi_thread()
.build()
.unwrap()
.block_on(async {
let db = Db::builder()
.sqlite(dir.path().join("tc.db"))
.build_async()
.await
.unwrap();
let h = db.run_async();
drop(h.todo_dao().add(&Todo {
id: 0,
title: "취소0".into(),
done: false,
}));
{
let dao = h.todo_dao();
let todo = Todo {
id: 0,
title: "취소1".into(),
done: false,
};
let fut = dao.add(&todo);
futures::pin_mut!(fut);
let _ = futures::poll!(fut.as_mut());
}
let mut landed = false;
for _ in 0..200 {
let cnt: i64 = h
.query_scalar("SELECT COUNT(*) FROM todos", ())
.await
.unwrap();
if cnt == 1 {
landed = true;
break;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
assert!(landed, "취소된 쓰기가 완료되지 않음");
let id = h
.todo_dao()
.add(&Todo {
id: 0,
title: "생존".into(),
done: false,
})
.await
.unwrap();
assert!(id > 0);
let cnt: i64 = h
.query_scalar("SELECT COUNT(*) FROM todos", ())
.await
.unwrap();
assert_eq!(cnt, 2);
});
}
#[cfg(not(feature = "tokio"))]
#[test]
fn cancelled_future_is_safe() {
let dir = tempfile::tempdir().unwrap();
smol::block_on(async {
let db = Db::builder()
.sqlite(dir.path().join("c.db"))
.build_async()
.await
.unwrap();
let h = db.run_async();
drop(h.todo_dao().add(&Todo {
id: 0,
title: "취소1".into(),
done: false,
}));
let id = h
.todo_dao()
.add(&Todo {
id: 0,
title: "생존".into(),
done: false,
})
.await
.unwrap();
assert!(id > 0);
});
}