#![allow(clippy::unwrap_used)]
use std::sync::Arc;
use surrealdb_kvs::TransactionType::Write;
use surrealdb_types::Value;
use crate::catalog::providers::DatabaseProvider;
use crate::dbs::Session;
use crate::expr::Dir;
use crate::idx::adjacency::fold_scope;
use crate::kvs::Datastore;
use crate::val::RecordId;
async fn ds() -> Arc<Datastore> {
Datastore::builder().without_maintenance_tasks().build_with_path("memory").await.unwrap()
}
async fn run(ds: &Datastore, ses: &Session, sql: &str) -> Vec<Value> {
ds.execute(sql, ses, None)
.await
.unwrap()
.into_iter()
.map(|response| response.result.unwrap())
.collect()
}
async fn fold_vertex(ds: &Datastore, rid: &RecordId) {
for dir in [Dir::In, Dir::Out] {
loop {
let txn = Arc::new(ds.transaction(Write).await.unwrap());
let db = txn.get_db_by_name("test", "test", None).await.unwrap().unwrap();
let mut env = ds.setup_ctx().unwrap();
env.set_transaction(Arc::clone(&txn));
let env = env.freeze();
let outcome =
fold_scope(&env, db.namespace_id, db.database_id, "test", "test", rid, dir, 1024)
.await
.unwrap();
txn.commit().await.unwrap();
if !outcome.has_more {
break;
}
}
}
}
async fn assert_degrees(ds: &Datastore, ses: &Session, cases: &[(&str, i64)]) {
for (query, expected) in cases {
let got = run(ds, ses, query).await.remove(0);
assert_eq!(
got,
run(ds, ses, &format!("RETURN {{ degree: {expected} }};")).await.remove(0),
"query `{query}`"
);
}
}
#[tokio::test]
async fn degrees_are_exact_in_every_adjacency_state() {
let ds = ds().await;
let ses = Session::owner().with_ns("test").with_db("test");
run(
&ds,
&ses,
"DEFINE NAMESPACE test;
DEFINE DATABASE test;
DEFINE TABLE person SCHEMALESS;
DEFINE TABLE cached_person SCHEMALESS INLINE EDGES 32;
DEFINE TABLE knows TYPE RELATION;
DEFINE TABLE likes TYPE RELATION;
CREATE |person:1..=12| RETURN NONE;
CREATE cached_person:1 RETURN NONE;
FOR $n IN 2..=8 { RELATE person:1->knows->(type::record('person', $n)) SET id = type::record('knows', $n) RETURN NONE; };
FOR $n IN 9..=11 { RELATE person:1->likes->(type::record('person', $n)) RETURN NONE; };
FOR $n IN 2..=6 { RELATE cached_person:1->knows->(type::record('person', $n)) RETURN NONE; };",
)
.await;
let cases: &[(&str, i64)] = &[
("SELECT count(->knows) AS degree FROM ONLY person:1;", 7),
("SELECT count(->likes) AS degree FROM ONLY person:1;", 3),
("SELECT count(->?) AS degree FROM ONLY person:1;", 10),
("SELECT count(<-knows) AS degree FROM ONLY person:5;", 2),
("SELECT count(<->knows) AS degree FROM ONLY person:5;", 2),
("SELECT count(->knows) AS degree FROM ONLY person:12;", 0),
];
assert_degrees(&ds, &ses, cases).await;
assert_degrees(&ds, &ses, &[("SELECT count(->knows) AS degree FROM ONLY cached_person:1;", 5)])
.await;
fold_vertex(&ds, &RecordId::new("person".into(), 1)).await;
assert_degrees(&ds, &ses, cases).await;
run(&ds, &ses, "RELATE person:1->knows->person:12;").await;
assert_degrees(&ds, &ses, &[("SELECT count(->knows) AS degree FROM ONLY person:1;", 8)]).await;
run(&ds, &ses, "DELETE knows:3;").await;
assert_degrees(&ds, &ses, &[("SELECT count(->knows) AS degree FROM ONLY person:1;", 7)]).await;
run(&ds, &ses, "DELETE knows:4; RELATE person:1->knows->person:4 SET id = knows:4;").await;
assert_degrees(&ds, &ses, &[("SELECT count(->knows) AS degree FROM ONLY person:1;", 7)]).await;
fold_vertex(&ds, &RecordId::new("person".into(), 1)).await;
assert_degrees(&ds, &ses, &[("SELECT count(->knows) AS degree FROM ONLY person:1;", 7)]).await;
}
#[tokio::test]
async fn numeric_blocks_count_like_the_evaluator() {
use surrealdb_cnf::ConfigMap;
let ds = Datastore::builder()
.without_maintenance_tasks()
.with_config(ConfigMap::empty().with_key_value("graph_numeric_ids", "true"))
.build_with_path("memory")
.await
.unwrap();
let ses = Session::owner().with_ns("test").with_db("test");
run(
&ds,
&ses,
"DEFINE NAMESPACE test;
DEFINE DATABASE test;
DEFINE TABLE person SCHEMALESS;
DEFINE TABLE company SCHEMALESS;
DEFINE TABLE works_at TYPE RELATION;
CREATE person:1 RETURN NONE;
CREATE |company:1..=100| RETURN NONE;
FOR $n IN 1..=100 { RELATE person:1->works_at->(type::record('company', $n)) RETURN NONE; };",
)
.await;
let cases: &[(&str, i64)] = &[
("SELECT count(->works_at) AS degree FROM ONLY person:1;", 100),
("SELECT array::len(->works_at) AS degree FROM ONLY person:1;", 100),
];
assert_degrees(&ds, &ses, cases).await;
fold_vertex(&ds, &RecordId::new("person".into(), 1)).await;
assert_degrees(&ds, &ses, cases).await;
run(&ds, &ses, "REMOVE TABLE company;").await;
let fast =
run(&ds, &ses, "SELECT count(->works_at) AS degree FROM ONLY person:1;").await.remove(0);
let slow = run(&ds, &ses, "SELECT array::len(->works_at) AS degree FROM ONLY person:1;")
.await
.remove(0);
assert_eq!(fast, slow, "the degree fast path disagrees with the evaluator after REMOVE TABLE");
}
#[tokio::test]
async fn permission_checked_sessions_keep_the_evaluator_path() {
let ds = ds().await;
let owner = Session::owner().with_ns("test").with_db("test");
run(
&ds,
&owner,
"DEFINE NAMESPACE test;
DEFINE DATABASE test;
DEFINE ACCESS user ON DATABASE TYPE RECORD;
DEFINE TABLE person SCHEMALESS PERMISSIONS FULL;
DEFINE TABLE knows TYPE RELATION PERMISSIONS NONE;
CREATE |person:1..=4| RETURN NONE;
RELATE person:1->knows->person:2;
RELATE person:1->knows->person:3;",
)
.await;
assert_degrees(&ds, &owner, &[("SELECT count(->knows) AS degree FROM ONLY person:1;", 2)])
.await;
let user = Session::for_record(
"test",
"test",
"user",
Value::RecordId(surrealdb_types::RecordId::new(
"person",
surrealdb_types::RecordIdKey::Number(1),
)),
);
assert_degrees(&ds, &user, &[("SELECT count(->knows) AS degree FROM ONLY person:1;", 0)]).await;
}