use std::iter;
use surrealdb::opt::Config;
use surrealdb::opt::auth::{Database, Namespace};
use surrealdb::types::RecordId;
use surrealdb_types::SurrealValue;
use ulid::Ulid;
use super::CreateDb;
#[derive(Debug, Clone, PartialEq, SurrealValue)]
struct TestRecord {
id: RecordId,
value: i32,
}
pub async fn clone_creates_new_session(new_db: impl CreateDb) {
let config = Config::new();
let (permit, db) = new_db.create_db(config).await;
let ns1 = Ulid::new().to_string();
let db1_name = Ulid::new().to_string();
db.use_ns(&ns1).use_db(&db1_name).await.unwrap();
db.query(format!(
"DEFINE NAMESPACE IF NOT EXISTS `{ns1}`; USE NS `{ns1}`; DEFINE DATABASE IF NOT EXISTS `{db1_name}`"
))
.await
.unwrap()
.check()
.unwrap();
let table = format!("t{}", Ulid::new());
db.query(format!("CREATE {table}:test SET value = 1")).await.unwrap().check().unwrap();
let db2 = db.clone();
let ns2 = Ulid::new().to_string();
let db2_name = Ulid::new().to_string();
db2.use_ns(&ns2).use_db(&db2_name).await.unwrap();
db2.query(format!("DEFINE NAMESPACE IF NOT EXISTS `{ns2}`; USE NS `{ns2}`; DEFINE DATABASE IF NOT EXISTS `{db2_name}`; DEFINE TABLE `{table}`"))
.await
.unwrap()
.check()
.unwrap();
let records: Vec<TestRecord> =
db2.query(format!("SELECT * FROM {table}:test")).await.unwrap().take(0).unwrap();
assert_eq!(records.len(), 0, "Cloned client should see empty database");
let records: Vec<TestRecord> =
db.query(format!("SELECT * FROM {table}:test")).await.unwrap().take(0).unwrap();
assert_eq!(records.len(), 1, "Original client should see its record");
assert_eq!(records[0].value, 1);
drop(permit);
}
pub async fn clone_then_immediate_query(new_db: impl CreateDb) {
let config = Config::new();
let (permit, db) = new_db.create_db(config).await;
let ns = Ulid::new().to_string();
let dbn = Ulid::new().to_string();
db.use_ns(&ns).use_db(&dbn).await.unwrap();
for _ in 0..50 {
let db2 = db.clone();
db2.use_ns(&ns).use_db(&dbn).await.unwrap();
db2.query("RETURN 1").await.unwrap().check().unwrap();
}
drop(permit);
}
pub async fn multiple_namespaces_databases(new_db: impl CreateDb) {
let config = Config::new();
let (permit, db) = new_db.create_db(config).await;
let client1 = db.clone();
let ns1 = Ulid::new().to_string();
let db1 = Ulid::new().to_string();
client1.use_ns(&ns1).use_db(&db1).await.unwrap();
client1
.query(format!(
"DEFINE NAMESPACE IF NOT EXISTS `{ns1}`; USE NS `{ns1}`; DEFINE DATABASE IF NOT EXISTS `{db1}`"
))
.await
.unwrap()
.check()
.unwrap();
let client2 = db.clone();
let ns2 = Ulid::new().to_string();
let db2 = Ulid::new().to_string();
client2.use_ns(&ns2).use_db(&db2).await.unwrap();
client2
.query(format!(
"DEFINE NAMESPACE IF NOT EXISTS `{ns2}`; USE NS `{ns2}`; DEFINE DATABASE IF NOT EXISTS `{db2}`"
))
.await
.unwrap()
.check()
.unwrap();
let client3 = db.clone();
let db3 = Ulid::new().to_string();
client3.use_ns(&ns1).use_db(&db3).await.unwrap();
client3.query(format!("DEFINE DATABASE IF NOT EXISTS `{db3}`")).await.unwrap().check().unwrap();
let table = format!("t{}", Ulid::new());
client1.query(format!("CREATE {table}:test SET value = 100")).await.unwrap().check().unwrap();
client2.query(format!("CREATE {table}:test SET value = 200")).await.unwrap().check().unwrap();
client3.query(format!("CREATE {table}:test SET value = 300")).await.unwrap().check().unwrap();
let records: Vec<TestRecord> =
client1.query(format!("SELECT * FROM {table}:test")).await.unwrap().take(0).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].value, 100, "Client 1 should see value 100");
let records: Vec<TestRecord> =
client2.query(format!("SELECT * FROM {table}:test")).await.unwrap().take(0).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].value, 200, "Client 2 should see value 200");
let records: Vec<TestRecord> =
client3.query(format!("SELECT * FROM {table}:test")).await.unwrap().take(0).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].value, 300, "Client 3 should see value 300");
drop(permit);
}
pub async fn session_variables_isolated(new_db: impl CreateDb) {
let config = Config::new();
let (permit, db) = new_db.create_db(config).await;
let ns = Ulid::new().to_string();
let db_name = Ulid::new().to_string();
let client1 = db.clone();
client1.use_ns(&ns).use_db(&db_name).await.unwrap();
client1
.query(format!(
"DEFINE NAMESPACE IF NOT EXISTS `{ns}`; USE NS `{ns}`; DEFINE DATABASE IF NOT EXISTS `{db_name}`"
))
.await
.unwrap()
.check()
.unwrap();
let client2 = db.clone();
client2.use_ns(&ns).use_db(&db_name).await.unwrap();
client1.set("my_var", 111).await.unwrap();
client2.set("my_var", 222).await.unwrap();
let table = format!("t{}", Ulid::new());
client1
.query(format!("CREATE {table}:client1 SET value = $my_var"))
.await
.unwrap()
.check()
.unwrap();
client2
.query(format!("CREATE {table}:client2 SET value = $my_var"))
.await
.unwrap()
.check()
.unwrap();
let records: Vec<TestRecord> =
client1.query(format!("SELECT * FROM {table}:client1")).await.unwrap().take(0).unwrap();
assert_eq!(records[0].value, 111, "Client 1 should use its own variable");
let records: Vec<TestRecord> =
client2.query(format!("SELECT * FROM {table}:client2")).await.unwrap().take(0).unwrap();
assert_eq!(records[0].value, 222, "Client 2 should use its own variable");
client1.unset("my_var").await.unwrap();
client1
.query(format!("CREATE {table}:test1 SET value = $my_var OR 0"))
.await
.unwrap()
.check()
.unwrap();
let records: Vec<TestRecord> =
client1.query(format!("SELECT * FROM {table}:test1")).await.unwrap().take(0).unwrap();
assert_eq!(records[0].value, 0, "Client 1's variable should be unset (NONE coalesces to 0)");
client2
.query(format!("CREATE {table}:test2 SET value = $my_var"))
.await
.unwrap()
.check()
.unwrap();
let records: Vec<TestRecord> =
client2.query(format!("SELECT * FROM {table}:test2")).await.unwrap().take(0).unwrap();
assert_eq!(records[0].value, 222, "Client 2 should still have its variable");
drop(permit);
}
#[cfg(not(feature = "protocol-http"))]
pub async fn session_transactions_isolated(new_db: impl CreateDb) {
let config = Config::new();
let (permit, db) = new_db.create_db(config).await;
let ns = Ulid::new().to_string();
let db_name = Ulid::new().to_string();
let table = format!("t{}", Ulid::new());
db.use_ns(&ns).use_db(&db_name).await.unwrap();
db.query(format!("DEFINE TABLE {table}")).await.unwrap().check().unwrap();
let client1 = db.clone();
client1.use_ns(&ns).use_db(&db_name).await.unwrap();
let client2 = db.clone();
client2.use_ns(&ns).use_db(&db_name).await.unwrap();
let tx1 = client1.begin().await.unwrap();
tx1.query(format!("CREATE {table}:tx1 SET value = 100")).await.unwrap().check().unwrap();
let records: Vec<TestRecord> =
client2.query(format!("SELECT * FROM {table}:tx1")).await.unwrap().take(0).unwrap();
assert_eq!(records.len(), 0, "Client 2 should not see uncommitted transaction data");
let client1 = tx1.commit().await.unwrap();
let records: Vec<TestRecord> =
client2.query(format!("SELECT * FROM {table}:tx1")).await.unwrap().take(0).unwrap();
assert_eq!(records.len(), 1, "Client 2 should see committed transaction data");
assert_eq!(records[0].value, 100);
let tx2 = client2.begin().await.unwrap();
tx2.query(format!("CREATE {table}:tx2 SET value = 200")).await.unwrap().check().unwrap();
let records: Vec<TestRecord> =
client1.query(format!("SELECT * FROM {table}:tx2")).await.unwrap().take(0).unwrap();
assert_eq!(records.len(), 0, "Client 1 should not see uncommitted transaction data");
let _client2 = tx2.cancel().await.unwrap();
let records: Vec<TestRecord> =
client1.query(format!("SELECT * FROM {table}:tx2")).await.unwrap().take(0).unwrap();
assert_eq!(records.len(), 0, "Client 1 should not see cancelled transaction data");
drop(permit);
}
pub async fn session_authentication_isolated(new_db: impl CreateDb) {
let config = Config::new();
let (permit, db) = new_db.create_db(config).await;
let ns = Ulid::new().to_string();
let db_name = Ulid::new().to_string();
let table = format!("t{}", Ulid::new());
db.use_ns(&ns).use_db(&db_name).await.unwrap();
db.query(format!(
"
DEFINE NAMESPACE IF NOT EXISTS `{ns}`;
USE NS `{ns}`;
DEFINE DATABASE IF NOT EXISTS `{db_name}`;
DEFINE TABLE {table};
DEFINE USER ns_user ON NAMESPACE PASSWORD 'ns_pass' ROLES OWNER;
DEFINE USER db_user ON DATABASE PASSWORD 'db_pass' ROLES OWNER;
",
))
.await
.unwrap()
.check()
.unwrap();
let client1 = db.clone();
client1
.signin(Namespace {
namespace: ns.clone(),
username: "ns_user".to_string(),
password: "ns_pass".to_string(),
})
.await
.unwrap();
let client2 = db.clone();
client2
.signin(Database {
namespace: ns.clone(),
database: db_name.clone(),
username: "db_user".to_string(),
password: "db_pass".to_string(),
})
.await
.unwrap();
client1.use_ns(&ns).use_db(&db_name).await.unwrap();
client2.use_ns(&ns).use_db(&db_name).await.unwrap();
client1
.query(format!("CREATE {table}:from_ns_user SET value = 1"))
.await
.unwrap()
.check()
.unwrap();
client2
.query(format!("CREATE {table}:from_db_user SET value = 2"))
.await
.unwrap()
.check()
.unwrap();
client1.invalidate().await.unwrap();
let result = client1.query(format!("CREATE {table}:test SET value = 3; RETURN $auth")).await;
assert!(
result.is_err() || result.unwrap().check().is_err(),
"Client 1 should be unauthenticated after invalidate"
);
client2
.query(format!("CREATE {table}:still_works SET value = 4"))
.await
.unwrap()
.check()
.unwrap();
drop(permit);
}
pub async fn mixed_session_operations(new_db: impl CreateDb) {
let config = Config::new();
let (permit, db) = new_db.create_db(config).await;
let client1 = db.clone();
let ns1 = Ulid::new().to_string();
let db1 = Ulid::new().to_string();
client1.use_ns(&ns1).use_db(&db1).await.unwrap();
client1
.query(format!(
"DEFINE NAMESPACE IF NOT EXISTS `{ns1}`; USE NS `{ns1}`; DEFINE DATABASE IF NOT EXISTS `{db1}`"
))
.await
.unwrap()
.check()
.unwrap();
client1.set("multiplier", 10).await.unwrap();
let client2 = db.clone();
let ns2 = Ulid::new().to_string();
let db2 = Ulid::new().to_string();
client2.use_ns(&ns2).use_db(&db2).await.unwrap();
client2
.query(format!(
"DEFINE NAMESPACE IF NOT EXISTS `{ns2}`; USE NS `{ns2}`; DEFINE DATABASE IF NOT EXISTS `{db2}`"
))
.await
.unwrap()
.check()
.unwrap();
client2.set("multiplier", 20).await.unwrap();
let client3 = db.clone();
client3.use_ns(&ns1).use_db(&db1).await.unwrap(); client3.set("multiplier", 30).await.unwrap();
let table = format!("t{}", Ulid::new());
client1.query(format!("DEFINE TABLE {table}")).await.unwrap().check().unwrap();
client2.query(format!("DEFINE TABLE {table}")).await.unwrap().check().unwrap();
let h1 = tokio::spawn({
let client = client1.clone(); let table = table.clone();
async move {
for i in 1..=5 {
client
.query(format!("CREATE {table}:{i} SET value = {i} * $multiplier"))
.await
.unwrap()
.check()
.unwrap();
}
}
});
let h2 = tokio::spawn({
let client = client2.clone(); let table = table.clone();
async move {
for i in 1..=5 {
client
.query(format!("CREATE {table}:{i} SET value = {i} * $multiplier"))
.await
.unwrap()
.check()
.unwrap();
}
}
});
let h3 = tokio::spawn({
let client = client3.clone(); let table = table.clone();
async move {
for i in 6..=10 {
client
.query(format!("CREATE {table}:{i} SET value = {i} * $multiplier"))
.await
.unwrap()
.check()
.unwrap();
}
}
});
tokio::try_join!(h1, h2, h3).unwrap();
let records: Vec<TestRecord> = client1
.query(format!("SELECT * FROM {table} ORDER BY value"))
.await
.unwrap()
.take(0)
.unwrap();
assert_eq!(records.len(), 10); assert_eq!(records[0].value, 10);
assert_eq!(records[4].value, 50);
assert_eq!(records[5].value, 180);
assert_eq!(records[9].value, 300);
let records: Vec<TestRecord> = client2
.query(format!("SELECT * FROM {table} ORDER BY value"))
.await
.unwrap()
.take(0)
.unwrap();
assert_eq!(records.len(), 5);
assert_eq!(records[0].value, 20); assert_eq!(records[4].value, 100);
drop(permit);
}
pub async fn query_variables_with_session_vars(new_db: impl CreateDb) {
let config = Config::new();
let (permit, db) = new_db.create_db(config).await;
let ns = Ulid::new().to_string();
let db_name = Ulid::new().to_string();
let client1 = db.clone();
client1.use_ns(&ns).use_db(&db_name).await.unwrap();
client1
.query(format!(
"DEFINE NAMESPACE IF NOT EXISTS `{ns}`; USE NS `{ns}`; DEFINE DATABASE IF NOT EXISTS `{db_name}`"
))
.await
.unwrap()
.check()
.unwrap();
client1.set("session_value", 100).await.unwrap();
let client2 = db.clone();
client2.use_ns(&ns).use_db(&db_name).await.unwrap();
client2.set("session_value", 200).await.unwrap();
let table = format!("t{}", Ulid::new());
#[derive(Debug, SurrealValue)]
struct RecordWithVars {
id: RecordId,
session: i32,
query: i32,
}
let records: Vec<RecordWithVars> = client1
.query(format!("CREATE {table}:test1 SET session = $session_value, query = $query_value"))
.bind(("query_value", 111))
.await
.unwrap()
.take(0)
.unwrap();
assert_eq!(records[0].session, 100, "Should use client1's session variable");
assert_eq!(records[0].query, 111, "Should use query-specific variable");
let records: Vec<RecordWithVars> = client2
.query(format!("CREATE {table}:test2 SET session = $session_value, query = $query_value"))
.bind(("query_value", 222))
.await
.unwrap()
.take(0)
.unwrap();
assert_eq!(records[0].session, 200, "Should use client2's session variable");
assert_eq!(records[0].query, 222, "Should use query-specific variable");
drop(permit);
}
pub async fn shared_connection_isolated_sessions(new_db: impl CreateDb) {
let config = Config::new();
let (permit, db) = new_db.create_db(config).await;
let clients: Vec<_> = iter::repeat_n(db, 5).collect();
let mut handles = vec![];
for (i, client) in clients.iter().enumerate() {
let client = client.clone();
let handle = tokio::spawn(async move {
let ns = Ulid::new().to_string();
let db_name = Ulid::new().to_string();
let table = format!("t{}", Ulid::new());
client.use_ns(&ns).use_db(&db_name).await.unwrap();
client
.query(format!(
"DEFINE NAMESPACE IF NOT EXISTS `{ns}`; USE NS `{ns}`; DEFINE DATABASE IF NOT EXISTS `{db_name}`"
))
.await
.unwrap()
.check()
.unwrap();
client.set("client_id", i as i32).await.unwrap();
for j in 0..10 {
client
.query(format!("CREATE {table}:{j} SET value = {j} + $client_id"))
.await
.unwrap()
.check()
.unwrap();
}
let records: Vec<TestRecord> = client
.query(format!("SELECT * FROM {table} ORDER BY value"))
.await
.unwrap()
.take(0)
.unwrap();
assert_eq!(records.len(), 10);
assert_eq!(records[0].value, i as i32);
(ns, db_name, table)
});
handles.push(handle);
}
for handle in handles {
handle.await.unwrap();
}
drop(permit);
}
define_include_tests!(
session_isolation => {
#[test_log::test(tokio::test)]
clone_creates_new_session,
#[test_log::test(tokio::test)]
clone_then_immediate_query,
#[test_log::test(tokio::test)]
multiple_namespaces_databases,
#[test_log::test(tokio::test)]
session_variables_isolated,
#[test_log::test(tokio::test)]
#[cfg(not(feature = "protocol-http"))]
session_transactions_isolated,
#[test_log::test(tokio::test)]
session_authentication_isolated,
#[test_log::test(tokio::test)]
mixed_session_operations,
#[test_log::test(tokio::test)]
query_variables_with_session_vars,
#[test_log::test(tokio::test)]
shared_connection_isolated_sessions,
}
);