#![cfg(all(feature = "sync", feature = "serverless"))]
use turso_orm_driver::{ConnectOptions, ConnectionTrait, Database, Statement, TransactionTrait};
fn credentials() -> Option<(String, String)> {
let url = std::env::var("TURSO_DATABASE_URL").ok()?;
let token = std::env::var("TURSO_AUTH_TOKEN").ok()?;
Some((url, token))
}
async fn count(db: &Database, table: &str) -> i64 {
db.query_one(Statement::from_string(format!(
"SELECT COUNT(*) AS n FROM {table}"
)))
.await
.expect("count")
.expect("row")
.get("n")
.expect("n")
}
#[tokio::test]
async fn replica_pushes_and_pulls() {
let Some((url, token)) = credentials() else {
eprintln!("TURSO_DATABASE_URL and TURSO_AUTH_TOKEN not set; skipping");
return;
};
let dir = tempfile::tempdir().expect("tempdir");
let replica = Database::connect(
ConnectOptions::sync(dir.path().join("replica.db"), url.clone()).auth_token(token.clone()),
)
.await
.expect("connect replica");
let remote = Database::connect(ConnectOptions::remote(url).auth_token(token))
.await
.expect("connect remote");
let table = format!("t_orm_sync_{}", std::process::id());
replica
.execute_unprepared(&format!(
"CREATE TABLE {table} (id INTEGER PRIMARY KEY, name TEXT NOT NULL)"
))
.await
.expect("create");
replica
.execute_unprepared(&format!("INSERT INTO {table} (name) VALUES ('alice')"))
.await
.expect("insert locally");
let before = replica.stats().await.expect("stats");
assert!(before.pending_operations >= 1, "{before:?}");
assert!(before.last_push_unix_time.is_none(), "{before:?}");
replica.push().await.expect("push");
assert_eq!(count(&remote, &table).await, 1);
let after = replica.stats().await.expect("stats");
assert_eq!(after.pending_operations, 0, "{after:?}");
assert!(after.last_push_unix_time.is_some(), "{after:?}");
assert!(after.network_sent_bytes > before.network_sent_bytes);
replica.checkpoint().await.expect("checkpoint");
remote
.execute_unprepared(&format!("INSERT INTO {table} (name) VALUES ('bob')"))
.await
.expect("insert remotely");
assert!(replica.pull().await.expect("pull"));
assert_eq!(count(&replica, &table).await, 2);
assert!(!replica.pull().await.expect("second pull"));
assert!(remote.push().await.is_err());
assert!(remote.stats().await.is_err());
let txn = replica.begin().await.expect("begin");
txn.execute_unprepared(&format!("INSERT INTO {table} (name) VALUES ('carol')"))
.await
.expect("insert in txn");
let writer = {
let replica = replica.clone();
let table = table.clone();
tokio::spawn(async move {
replica
.execute_unprepared(&format!("INSERT INTO {table} (name) VALUES ('dave')"))
.await
})
};
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
txn.commit().await.expect("commit");
writer
.await
.expect("join")
.expect("the writer waited for the lock");
assert_eq!(count(&replica, &table).await, 4);
replica.push().await.expect("push before the drop");
remote
.execute_unprepared(&format!("DROP TABLE {table}"))
.await
.expect("drop");
replica.pull().await.expect("pull the drop");
}
#[tokio::test]
async fn replica_sync_runs_rounds() {
use std::time::Duration;
use turso_orm_driver::{Checkpoint, ReplicaSync, SyncPolicy};
let Some((url, token)) = credentials() else {
eprintln!("TURSO_DATABASE_URL and TURSO_AUTH_TOKEN not set; skipping");
return;
};
let dir = tempfile::tempdir().expect("tempdir");
let replica = Database::connect(
ConnectOptions::sync(dir.path().join("replica.db"), url.clone()).auth_token(token.clone()),
)
.await
.expect("connect replica");
let remote = Database::connect(ConnectOptions::remote(url).auth_token(token))
.await
.expect("connect remote");
let table = format!("t_orm_sync_task_{}", std::process::id());
replica
.execute_unprepared(&format!(
"CREATE TABLE {table} (id INTEGER PRIMARY KEY, name TEXT NOT NULL)"
))
.await
.expect("create");
let policy = SyncPolicy::new()
.interval(Duration::from_secs(1))
.checkpoint(Checkpoint::AfterPush);
let sync = ReplicaSync::spawn(replica.clone(), policy).expect("spawn");
assert!(matches!(
ReplicaSync::spawn(replica.clone(), SyncPolicy::new()),
Err(turso_orm_driver::Error::Misuse(_))
));
replica
.execute_unprepared(&format!("INSERT INTO {table} (name) VALUES ('alice')"))
.await
.expect("insert locally");
let mut rounds = sync.subscribe();
tokio::time::timeout(Duration::from_secs(10), rounds.changed())
.await
.expect("a round within ten seconds")
.expect("the task is alive");
let status = sync.status();
assert_eq!(status.rounds, 1, "{status:?}");
assert_eq!(status.consecutive_failures, 0, "{status:?}");
assert!(status.checkpointed, "{status:?}");
assert!(status.stats.is_some(), "{status:?}");
assert_eq!(count(&remote, &table).await, 1);
remote
.execute_unprepared(&format!("INSERT INTO {table} (name) VALUES ('bob')"))
.await
.expect("insert remotely");
let status = sync.trigger().await.expect("trigger");
assert!(status.pulled, "{status:?}");
assert_eq!(count(&replica, &table).await, 2);
replica
.execute_unprepared(&format!("INSERT INTO {table} (name) VALUES ('carol')"))
.await
.expect("insert before shutdown");
sync.shutdown().await.expect("shutdown");
assert_eq!(count(&remote, &table).await, 3);
let again = ReplicaSync::spawn(replica.clone(), SyncPolicy::new()).expect("spawn again");
drop(again);
remote
.execute_unprepared(&format!("DROP TABLE {table}"))
.await
.expect("drop");
replica.pull().await.expect("pull the drop");
}