#![allow(clippy::unwrap_used)]
use std::borrow::Cow;
use std::sync::Arc;
use surrealdb_kvs::TransactionType::Write;
use surrealdb_types::Value;
use crate::catalog::providers::DatabaseProvider;
use crate::catalog::record::{Record, RecordType};
use crate::dbs::Session;
use crate::expr::Dir;
use crate::key::schema::{GraphKey, RecordKey};
use crate::kvs::Datastore;
use crate::val::{Object, RecordId, RecordIdKey, TableName};
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 assert_cases(ds: &Datastore, ses: &Session, cases: &[(&str, &str)]) {
for (query, expected) in cases {
let got = run(ds, ses, query).await.remove(0);
assert_eq!(got, run(ds, ses, &format!("RETURN {expected};")).await.remove(0), "`{query}`");
}
}
async fn analyze_text(ds: &Datastore, ses: &Session, query: &str) -> String {
match run(ds, ses, query).await.remove(0) {
Value::String(text) => text,
other => panic!("expected EXPLAIN ANALYZE to answer a string, got {other:?}"),
}
}
async fn seed(ds: &Datastore, ses: &Session) {
run(
ds,
ses,
"DEFINE NAMESPACE test;
DEFINE DATABASE test;
DEFINE TABLE person SCHEMALESS;
DEFINE TABLE doc SCHEMALESS;
DEFINE TABLE wrote TYPE RELATION;
DEFINE INDEX idx_flag ON person FIELDS flag;
DEFINE INDEX idx_kind ON doc FIELDS kind;
FOR $n IN 1..=8 { CREATE type::record('person', $n) SET flag = $n % 2 == 0 RETURN NONE; };
CREATE doc:x SET kind = 'paper' RETURN NONE;
CREATE doc:y SET kind = 'paper' RETURN NONE;
FOR $n IN 1..=4 { RELATE (type::record('person', $n))->wrote->doc:x RETURN NONE; };
RELATE person:1->wrote->doc:y RETURN NONE;",
)
.await;
}
#[tokio::test]
async fn semijoins_fuse_and_answer_exactly() {
let ds = ds().await;
let ses = Session::owner().with_ns("test").with_db("test");
seed(&ds, &ses).await;
assert_cases(
&ds,
&ses,
&[
(
"SELECT VALUE id FROM person WHERE flag = true AND ->wrote->doc CONTAINS doc:x;",
"[person:2, person:4]",
),
(
"SELECT VALUE id FROM person WHERE flag = true AND doc:x INSIDE ->wrote->doc;",
"[person:2, person:4]",
),
(
"SELECT VALUE id FROM doc WHERE kind = 'paper' AND <-wrote<-person CONTAINS person:1;",
"[doc:x, doc:y]",
),
(
"SELECT VALUE id FROM person WHERE flag = true AND ->wrote->doc CONTAINS doc:y;",
"[]",
),
],
)
.await;
let plan = surrealdb_types::ToSql::to_sql(
&run(
&ds,
&ses,
"EXPLAIN SELECT VALUE id FROM person WHERE flag = true AND ->wrote->doc CONTAINS doc:x;",
)
.await
.remove(0),
);
assert!(plan.contains("BitmapGraphScan"), "expected the graph branch in: {plan}");
assert!(plan.contains("BitmapAnd"), "expected an intersection in: {plan}");
for declined in [
"EXPLAIN SELECT VALUE id FROM person WHERE ->wrote->doc CONTAINS doc:x;",
"EXPLAIN SELECT VALUE id FROM person WHERE flag = true AND ->wrote->doc->wrote->doc CONTAINS doc:x;",
"EXPLAIN SELECT VALUE id FROM person WHERE flag = true AND <->wrote<->doc CONTAINS doc:x;",
"EXPLAIN SELECT VALUE id FROM person WHERE flag = true AND ->wrote->doc CONTAINS person:1;",
] {
let plan = surrealdb_types::ToSql::to_sql(&run(&ds, &ses, declined).await.remove(0));
assert!(
!plan.contains("BitmapGraphScan"),
"unexpected graph branch in `{declined}`: {plan}"
);
}
}
#[tokio::test]
async fn a_unique_anchor_keeps_the_streaming_plan() {
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 doc SCHEMALESS;
DEFINE TABLE wrote TYPE RELATION;
DEFINE INDEX idx_email ON person FIELDS email UNIQUE;
FOR $n IN 1..=8 { CREATE type::record('person', $n) SET email = 'p' + <string>$n + '@x' RETURN NONE; };
CREATE doc:x RETURN NONE;
FOR $n IN 1..=4 { RELATE (type::record('person', $n))->wrote->doc:x RETURN NONE; };",
)
.await;
assert_cases(
&ds,
&ses,
&[
(
"SELECT VALUE id FROM person WHERE email = 'p2@x' AND ->wrote->doc CONTAINS doc:x;",
"[person:2]",
),
(
"SELECT VALUE id FROM person WHERE email = 'p6@x' AND ->wrote->doc CONTAINS doc:x;",
"[]",
),
],
)
.await;
let plan = surrealdb_types::ToSql::to_sql(
&run(
&ds,
&ses,
"EXPLAIN SELECT VALUE id FROM person WHERE email = 'p2@x' AND ->wrote->doc CONTAINS doc:x;",
)
.await
.remove(0),
);
assert!(
!plan.contains("BitmapGraphScan") && !plan.contains("BitmapResolve"),
"a unique anchor must keep the streaming plan: {plan}"
);
}
#[tokio::test]
async fn a_hub_anchor_drops_the_graph_branch_against_a_small_intersection() {
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 doc SCHEMALESS;
DEFINE TABLE wrote TYPE RELATION;
DEFINE INDEX idx_flag ON person FIELDS flag;
FOR $n IN 1..=100 { CREATE type::record('person', $n) SET flag = $n == 1 RETURN NONE; };
CREATE doc:hub RETURN NONE;
FOR $n IN 1..=100 { RELATE (type::record('person', $n))->wrote->doc:hub RETURN NONE; };",
)
.await;
assert_cases(
&ds,
&ses,
&[(
"SELECT VALUE id FROM person WHERE flag = true AND ->wrote->doc CONTAINS doc:hub;",
"[person:1]",
)],
)
.await;
let mut redacted = Session::owner().with_ns("test").with_db("test");
redacted.redact_volatile_explain_attrs = true;
let plan = analyze_text(
&ds,
&redacted,
"EXPLAIN ANALYZE SELECT VALUE id FROM person WHERE flag = true AND ->wrote->doc CONTAINS doc:hub;",
)
.await;
let line = plan
.lines()
.find(|l| l.contains("BitmapGraphScan"))
.unwrap_or_else(|| panic!("expected a BitmapGraphScan node in: {plan}"));
assert!(
line.contains("rows: 0") && line.contains("dropped: true"),
"the hub branch must be cut off by the proportional budget: {line}"
);
}
async fn seed_v1_edge(ds: &Datastore, edge: &RecordId, l: &RecordId, r: &RecordId) {
let txn = Arc::new(ds.transaction(Write).await.unwrap());
let db = txn.get_db_by_name("test", "test", None).await.unwrap().unwrap();
let (ns, dbid) = (db.namespace_id, db.database_id);
let ltr = GraphKey {
ns,
db: dbid,
tb: Cow::Borrowed(&l.table),
id: Cow::Borrowed(&l.key),
dir: Dir::Out,
foreign_table: Cow::Borrowed(&edge.table),
foreign_key: Cow::Borrowed(&edge.key),
};
let rtl = GraphKey {
ns,
db: dbid,
tb: Cow::Borrowed(&r.table),
id: Cow::Borrowed(&r.key),
dir: Dir::In,
foreign_table: Cow::Borrowed(&edge.table),
foreign_key: Cow::Borrowed(&edge.key),
};
let mut data = Object::default();
data.insert("id".to_string(), crate::val::Value::RecordId(edge.clone()));
data.insert("in".to_string(), crate::val::Value::RecordId(l.clone()));
data.insert("out".to_string(), crate::val::Value::RecordId(r.clone()));
let mut record = Record::new(crate::val::Value::Object(data));
record.set_record_type(RecordType::Edge {
variant: 1,
});
let etl = GraphKey {
ns,
db: dbid,
tb: Cow::Borrowed(&edge.table),
id: Cow::Borrowed(&edge.key),
dir: Dir::In,
foreign_table: Cow::Borrowed(&l.table),
foreign_key: Cow::Borrowed(&l.key),
};
let etr = GraphKey {
ns,
db: dbid,
tb: Cow::Borrowed(&edge.table),
id: Cow::Borrowed(&edge.key),
dir: Dir::Out,
foreign_table: Cow::Borrowed(&r.table),
foreign_key: Cow::Borrowed(&r.key),
};
txn.set_key(<r, &()).await.unwrap();
txn.set_key(&rtl, &()).await.unwrap();
txn.set_key(&etl, &()).await.unwrap();
txn.set_key(&etr, &()).await.unwrap();
txn.set_key(
&RecordKey {
ns,
db: dbid,
tb: Cow::Borrowed(&edge.table),
id: Cow::Borrowed(&edge.key),
},
&record,
)
.await
.unwrap();
txn.commit().await.unwrap();
}
#[tokio::test]
async fn a_legacy_key_drops_the_branch_not_the_rows() {
let ds = ds().await;
let ses = Session::owner().with_ns("test").with_db("test");
seed(&ds, &ses).await;
let edge = RecordId::new(TableName::from("wrote"), RecordIdKey::String("legacy".into()));
let l = RecordId::new(TableName::from("person"), 6);
let r = RecordId::new(TableName::from("doc"), RecordIdKey::String("x".into()));
seed_v1_edge(&ds, &edge, &l, &r).await;
assert_cases(
&ds,
&ses,
&[(
"SELECT VALUE id FROM person WHERE flag = true AND ->wrote->doc CONTAINS doc:x;",
"[person:2, person:4, person:6]",
)],
)
.await;
let mut redacted = Session::owner().with_ns("test").with_db("test");
redacted.redact_volatile_explain_attrs = true;
let plan = analyze_text(
&ds,
&redacted,
"EXPLAIN ANALYZE SELECT VALUE id FROM person WHERE flag = true AND ->wrote->doc CONTAINS doc:x;",
)
.await;
let line = plan
.lines()
.find(|l| l.contains("BitmapGraphScan"))
.unwrap_or_else(|| panic!("expected a BitmapGraphScan node in: {plan}"));
assert!(
line.contains("rows: 0") && line.contains("dropped: true"),
"a legacy key must drop the branch observably: {line}"
);
}
#[tokio::test]
async fn a_select_denied_edge_table_declines_the_bitmap_leaf() {
let ds = ds().await;
let owner = Session::owner().with_ns("test").with_db("test");
run(
&ds,
&owner,
"DEFINE NAMESPACE test;
DEFINE DATABASE test;
DEFINE TABLE person SCHEMALESS PERMISSIONS FULL;
DEFINE TABLE doc SCHEMALESS PERMISSIONS FULL;
DEFINE TABLE wrote TYPE RELATION PERMISSIONS FOR select NONE;
DEFINE INDEX idx_flag ON person FIELDS flag;
FOR $n IN 1..=8 { CREATE type::record('person', $n) SET flag = $n % 2 == 0 RETURN NONE; };
CREATE doc:x SET kind = 'paper' RETURN NONE;
FOR $n IN 1..=4 { RELATE (type::record('person', $n))->wrote->doc:x RETURN NONE; };",
)
.await;
let mut owner = owner;
owner.redact_volatile_explain_attrs = true;
let mut low = Session::for_record(
"test",
"test",
"user",
crate::types::PublicValue::String("person:1".to_owned()),
);
low.redact_volatile_explain_attrs = true;
let query = "EXPLAIN ANALYZE SELECT VALUE id FROM person WHERE flag = true AND \
->wrote->doc CONTAINS doc:x;";
let owner_plan = analyze_text(&ds, &owner, query).await;
let owner_line = owner_plan
.lines()
.find(|l| l.contains("BitmapGraphScan"))
.unwrap_or_else(|| panic!("expected a BitmapGraphScan node in: {owner_plan}"));
assert!(
owner_line.contains("rows: 4"),
"expected the true reachable-set count in: {owner_line}"
);
let low_plan = analyze_text(&ds, &low, query).await;
if let Some(low_line) = low_plan.lines().find(|l| l.contains("BitmapGraphScan")) {
assert!(
low_line.contains("rows: 0"),
"a select-denied session must not see the true reachable-set count: {low_line}"
);
assert!(
low_line.contains("dropped: true"),
"a declined leaf must be reported as dropped, not as empty: {low_line}"
);
}
assert!(
!owner_line.contains("dropped"),
"a completed leaf must not be reported as dropped: {owner_line}"
);
assert_cases(
&ds,
&low,
&[("SELECT VALUE id FROM person WHERE flag = true AND ->wrote->doc CONTAINS doc:x;", "[]")],
)
.await;
}