use std::borrow::Cow;
use std::sync::Arc;
use surrealdb_kvs::TransactionType::{Read, Write};
use crate::catalog::providers::CatalogProvider;
use crate::catalog::record::{Record, RecordType};
use crate::catalog::{DatabaseId, NamespaceId};
use crate::dbs::{Capabilities, Session};
use crate::expr::Dir;
use crate::key::schema::{AnyKey, GraphDirPrefix, GraphKey, KeyKind, RecordKey};
use crate::kvs::Datastore;
use crate::val::{Array, Object, RecordId, RecordIdKey, Strand, TableName, Value};
async fn mem_ds() -> Arc<Datastore> {
Datastore::builder()
.without_maintenance_tasks()
.with_capabilities(Capabilities::all())
.build_with_path("memory")
.await
.unwrap()
}
fn rid(table: &str, key: &str) -> RecordId {
RecordId {
table: TableName::from(table),
key: RecordIdKey::String(Strand::new(key)),
}
}
fn rid_array(rids: &[&RecordId]) -> Value {
Value::Array(Array::from(
rids.iter().map(|r| Value::RecordId((*r).clone())).collect::<Vec<_>>(),
))
}
async fn run(ds: &Datastore, session: &Session, sql: &str) -> Value {
let responses = ds.execute(sql, session, None).await.unwrap();
let mut last = Value::None;
for response in responses {
last = response.result.unwrap_or_else(|e| panic!("{sql} failed: {e}")).into();
}
last
}
async fn adjacency_kinds(
ds: &Datastore,
ns: NamespaceId,
db: DatabaseId,
vertex: &RecordId,
dir: Dir,
) -> Vec<KeyKind> {
let range = GraphDirPrefix {
ns,
db,
tb: Cow::Borrowed(&vertex.table),
id: Cow::Borrowed(&vertex.key),
dir,
}
.range()
.unwrap();
let tx = ds.transaction(Read).await.unwrap();
let keys = tx.keys_raw(range, u32::MAX, 0, None).await.unwrap();
let _ = tx.cancel().await;
keys.iter()
.map(|k| AnyKey::decode(k).unwrap_or_else(|| panic!("unrecognised key {k:?}")).kind())
.collect()
}
async fn seed_v1_edge(
ds: &Datastore,
ns: NamespaceId,
db: DatabaseId,
edge: &RecordId,
l: &RecordId,
r: &RecordId,
) {
let ltr = GraphKey {
ns,
db,
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 etl = GraphKey {
ns,
db,
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,
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),
};
let rtl = GraphKey {
ns,
db,
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(), Value::RecordId(edge.clone()));
data.insert("in".to_string(), Value::RecordId(l.clone()));
data.insert("out".to_string(), Value::RecordId(r.clone()));
let mut record = Record::new(Value::Object(data));
record.set_record_type(RecordType::Edge {
variant: 1,
});
let tx = ds.transaction(Write).await.unwrap();
tx.set_key(<r, &()).await.unwrap();
tx.set_key(&etl, &()).await.unwrap();
tx.set_key(&etr, &()).await.unwrap();
tx.set_key(&rtl, &()).await.unwrap();
tx.set_key(
&RecordKey {
ns,
db,
tb: Cow::Borrowed(&edge.table),
id: Cow::Borrowed(&edge.key),
},
&record,
)
.await
.unwrap();
tx.commit().await.unwrap();
}
#[tokio::test]
async fn re_relating_a_v1_edge_replaces_both_legacy_vertex_keys() {
let ds = mem_ds().await;
let db_def = {
let tx = ds.transaction(Write).await.unwrap();
let db_def = tx.ensure_ns_db(None, "test", "test").await.unwrap();
tx.commit().await.unwrap();
db_def
};
let ns = db_def.namespace_id;
let db = db_def.database_id;
let session = Session::owner().with_ns("test").with_db("test");
run(
&ds,
&session,
"DEFINE TABLE person;
DEFINE TABLE knows TYPE RELATION IN person OUT person;
CREATE person:alice;
CREATE person:bob;",
)
.await;
let alice = rid("person", "alice");
let bob = rid("person", "bob");
let edge = rid("knows", "k1");
seed_v1_edge(&ds, ns, db, &edge, &alice, &bob).await;
assert_eq!(
run(&ds, &session, "SELECT VALUE ->knows->person FROM ONLY person:alice;").await,
rid_array(&[&bob]),
);
assert_eq!(
run(&ds, &session, "SELECT VALUE <-knows<-person FROM ONLY person:bob;").await,
rid_array(&[&alice]),
);
run(&ds, &session, "RELATE OR UPDATE person:alice->knows:k1->person:bob SET since = 2020;")
.await;
assert_eq!(adjacency_kinds(&ds, ns, db, &alice, Dir::Out).await, vec![KeyKind::GraphPointer]);
assert_eq!(
adjacency_kinds(&ds, ns, db, &bob, Dir::In).await,
vec![KeyKind::GraphPointer],
"the legacy (out-vertex, In, edge) key must not survive the migration",
);
assert_eq!(adjacency_kinds(&ds, ns, db, &alice, Dir::In).await, vec![]);
assert_eq!(adjacency_kinds(&ds, ns, db, &bob, Dir::Out).await, vec![]);
assert_eq!(adjacency_kinds(&ds, ns, db, &edge, Dir::In).await, vec![KeyKind::Graph]);
assert_eq!(adjacency_kinds(&ds, ns, db, &edge, Dir::Out).await, vec![KeyKind::Graph]);
assert_eq!(
run(&ds, &session, "SELECT VALUE ->knows FROM ONLY person:alice;").await,
rid_array(&[&edge]),
);
assert_eq!(
run(&ds, &session, "SELECT VALUE <-knows FROM ONLY person:bob;").await,
rid_array(&[&edge]),
"a surviving legacy key shares the pointer key's scan range and duplicates the edge",
);
assert_eq!(
run(&ds, &session, "SELECT VALUE ->knows->person FROM ONLY person:alice;").await,
rid_array(&[&bob]),
);
assert_eq!(
run(&ds, &session, "SELECT VALUE <-knows<-person FROM ONLY person:bob;").await,
rid_array(&[&alice]),
);
run(&ds, &session, "DELETE knows:k1;").await;
for dir in [Dir::In, Dir::Out] {
assert_eq!(adjacency_kinds(&ds, ns, db, &alice, dir).await, vec![]);
assert_eq!(adjacency_kinds(&ds, ns, db, &bob, dir).await, vec![]);
assert_eq!(adjacency_kinds(&ds, ns, db, &edge, dir).await, vec![]);
}
assert_eq!(
run(&ds, &session, "SELECT VALUE ->knows FROM ONLY person:alice;").await,
rid_array(&[]),
);
assert_eq!(
run(&ds, &session, "SELECT VALUE <-knows FROM ONLY person:bob;").await,
rid_array(&[]),
);
}
#[tokio::test]
async fn re_relating_a_v1_self_loop_edge_migrates_cleanly() {
let ds = mem_ds().await;
let db_def = {
let tx = ds.transaction(Write).await.unwrap();
let db_def = tx.ensure_ns_db(None, "test", "test").await.unwrap();
tx.commit().await.unwrap();
db_def
};
let ns = db_def.namespace_id;
let db = db_def.database_id;
let session = Session::owner().with_ns("test").with_db("test");
run(
&ds,
&session,
"DEFINE TABLE person;
DEFINE TABLE likes TYPE RELATION IN person OUT person;
CREATE person:alice;",
)
.await;
let alice = rid("person", "alice");
let edge = rid("likes", "k1");
seed_v1_edge(&ds, ns, db, &edge, &alice, &alice).await;
assert_eq!(
run(&ds, &session, "SELECT VALUE ->likes->person FROM ONLY person:alice;").await,
rid_array(&[&alice]),
);
run(&ds, &session, "RELATE OR UPDATE person:alice->likes:k1->person:alice SET since = 2020;")
.await;
assert_eq!(adjacency_kinds(&ds, ns, db, &alice, Dir::Out).await, vec![KeyKind::GraphPointer]);
assert_eq!(adjacency_kinds(&ds, ns, db, &alice, Dir::In).await, vec![KeyKind::GraphPointer]);
assert_eq!(
run(&ds, &session, "SELECT VALUE <-likes FROM ONLY person:alice;").await,
rid_array(&[&edge]),
);
run(&ds, &session, "DELETE likes:k1;").await;
for dir in [Dir::In, Dir::Out] {
assert_eq!(adjacency_kinds(&ds, ns, db, &alice, dir).await, vec![]);
assert_eq!(adjacency_kinds(&ds, ns, db, &edge, dir).await, vec![]);
}
}