use std::fmt::Write as _;
use rete_core::{eval_query, QueryOutput, Rete};
pub const DEFAULT_BASE: &str = "http://ex/";
#[derive(Debug, thiserror::Error)]
#[error("Cypher error: {0}")]
pub struct CypherError(pub String);
impl CypherError {
fn new(msg: impl Into<String>) -> Self {
CypherError(msg.into())
}
}
type Result<T> = std::result::Result<T, CypherError>;
#[derive(Debug, Clone, PartialEq, Eq)]
struct Node {
var: Option<String>,
label: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Dir {
Forward,
Reverse,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Rel {
rel_type: String,
dir: Dir,
var_length: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Path {
first: Node,
steps: Vec<(Rel, Node)>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CmpOp {
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
}
impl CmpOp {
fn sparql(self) -> &'static str {
match self {
CmpOp::Eq => "=",
CmpOp::Ne => "!=",
CmpOp::Lt => "<",
CmpOp::Le => "<=",
CmpOp::Gt => ">",
CmpOp::Ge => ">=",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Lhs {
Prop(String, String),
Ident(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Value {
Number(String),
Str(String),
Iri(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Condition {
lhs: Lhs,
op: CmpOp,
value: Value,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BoolOp {
And,
Or,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum ReturnItem {
Var(String),
Prop(String, String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct CypherQuery {
paths: Vec<Path>,
where_: Vec<Condition>,
where_op: BoolOp,
returns: Vec<ReturnItem>,
limit: Option<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Tok {
Ident(String),
Str(String),
Num(String),
Iri(String),
Punct(String),
}
fn tokenize(src: &str) -> Result<Vec<Tok>> {
let chars: Vec<char> = src.chars().collect();
let mut i = 0;
let mut toks = Vec::new();
while i < chars.len() {
let c = chars[i];
if c.is_whitespace() {
i += 1;
continue;
}
if c == '<' {
if let Some(end) = find_iri_end(&chars, i) {
let iri: String = chars[i..=end].iter().collect();
toks.push(Tok::Iri(iri));
i = end + 1;
continue;
}
if i + 1 < chars.len() && (chars[i + 1] == '-') {
toks.push(Tok::Punct("<-".into()));
i += 2;
continue;
}
if i + 1 < chars.len() && (chars[i + 1] == '=') {
toks.push(Tok::Punct("<=".into()));
i += 2;
continue;
}
if i + 1 < chars.len() && (chars[i + 1] == '>') {
toks.push(Tok::Punct("<>".into()));
i += 2;
continue;
}
toks.push(Tok::Punct("<".into()));
i += 1;
continue;
}
if c == '-' {
if i + 1 < chars.len() && chars[i + 1] == '>' {
toks.push(Tok::Punct("->".into()));
i += 2;
} else {
toks.push(Tok::Punct("-".into()));
i += 1;
}
continue;
}
if c == '>' {
if i + 1 < chars.len() && chars[i + 1] == '=' {
toks.push(Tok::Punct(">=".into()));
i += 2;
} else {
toks.push(Tok::Punct(">".into()));
i += 1;
}
continue;
}
if c == '!' {
if i + 1 < chars.len() && chars[i + 1] == '=' {
toks.push(Tok::Punct("!=".into()));
i += 2;
continue;
}
return Err(CypherError::new("unexpected '!' (did you mean '!='?)"));
}
if c == '"' || c == '\'' {
let quote = c;
let mut s = String::new();
i += 1;
let mut closed = false;
while i < chars.len() {
if chars[i] == '\\' && i + 1 < chars.len() {
s.push(chars[i + 1]);
i += 2;
continue;
}
if chars[i] == quote {
closed = true;
i += 1;
break;
}
s.push(chars[i]);
i += 1;
}
if !closed {
return Err(CypherError::new("unterminated string literal"));
}
toks.push(Tok::Str(s));
continue;
}
if c.is_ascii_digit() || (c == '-' && i + 1 < chars.len() && chars[i + 1].is_ascii_digit())
{
let start = i;
i += 1;
while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
i += 1;
}
toks.push(Tok::Num(chars[start..i].iter().collect()));
continue;
}
if c.is_alphabetic() || c == '_' {
let start = i;
while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
i += 1;
}
toks.push(Tok::Ident(chars[start..i].iter().collect()));
continue;
}
if "()[]{}:,.*=".contains(c) {
toks.push(Tok::Punct(c.to_string()));
i += 1;
continue;
}
return Err(CypherError::new(format!("unexpected character {c:?}")));
}
Ok(toks)
}
fn find_iri_end(chars: &[char], start: usize) -> Option<usize> {
let mut j = start + 1;
if j >= chars.len() {
return None;
}
if chars[j] == '-' || chars[j] == '=' || chars[j] == '>' {
return None;
}
while j < chars.len() {
match chars[j] {
'>' => return Some(j),
c if c.is_whitespace() => return None,
_ => j += 1,
}
}
None
}
struct Parser {
toks: Vec<Tok>,
pos: usize,
}
impl Parser {
fn new(toks: Vec<Tok>) -> Self {
Parser { toks, pos: 0 }
}
fn peek(&self) -> Option<&Tok> {
self.toks.get(self.pos)
}
fn next(&mut self) -> Option<Tok> {
let t = self.toks.get(self.pos).cloned();
if t.is_some() {
self.pos += 1;
}
t
}
fn eat_keyword(&mut self, kw: &str) -> bool {
if let Some(Tok::Ident(s)) = self.peek() {
if s.eq_ignore_ascii_case(kw) {
self.pos += 1;
return true;
}
}
false
}
fn is_keyword(&self, kw: &str) -> bool {
matches!(self.peek(), Some(Tok::Ident(s)) if s.eq_ignore_ascii_case(kw))
}
fn eat_punct(&mut self, p: &str) -> bool {
if matches!(self.peek(), Some(Tok::Punct(s)) if s == p) {
self.pos += 1;
true
} else {
false
}
}
fn expect_punct(&mut self, p: &str) -> Result<()> {
if self.eat_punct(p) {
Ok(())
} else {
Err(CypherError::new(format!(
"expected {p:?}, found {}",
self.describe_next()
)))
}
}
fn describe_next(&self) -> String {
match self.peek() {
None => "end of input".to_string(),
Some(Tok::Ident(s)) => format!("{s:?}"),
Some(Tok::Str(s)) => format!("string {s:?}"),
Some(Tok::Num(s)) => format!("number {s:?}"),
Some(Tok::Iri(s)) => format!("IRI {s}"),
Some(Tok::Punct(s)) => format!("{s:?}"),
}
}
fn ident(&mut self) -> Result<String> {
match self.next() {
Some(Tok::Ident(s)) => Ok(s),
other => Err(CypherError::new(format!(
"expected a name, found {}",
describe(other.as_ref())
))),
}
}
fn parse_query(&mut self) -> Result<CypherQuery> {
for unsupported in ["CREATE", "MERGE", "SET", "DELETE", "WITH"] {
if self.is_keyword(unsupported) {
return Err(CypherError::new(format!(
"{unsupported} is not supported (this is a read-only Cypher subset)"
)));
}
}
if self.is_keyword("OPTIONAL") {
return Err(CypherError::new("OPTIONAL MATCH is not supported"));
}
if !self.eat_keyword("MATCH") {
return Err(CypherError::new(format!(
"expected MATCH, found {}",
self.describe_next()
)));
}
let paths = self.parse_paths()?;
let mut where_ = Vec::new();
let mut where_op = BoolOp::And;
if self.eat_keyword("WHERE") {
let (conds, op) = self.parse_where()?;
where_ = conds;
where_op = op;
}
if !self.eat_keyword("RETURN") {
return Err(CypherError::new(format!(
"expected RETURN, found {}",
self.describe_next()
)));
}
let returns = self.parse_return()?;
let mut limit = None;
if self.eat_keyword("LIMIT") {
match self.next() {
Some(Tok::Num(n)) => {
let n: usize = n
.parse()
.map_err(|_| CypherError::new(format!("invalid LIMIT value {n:?}")))?;
limit = Some(n);
}
other => {
return Err(CypherError::new(format!(
"expected a number after LIMIT, found {}",
describe(other.as_ref())
)))
}
}
}
if let Some(t) = self.peek() {
return Err(CypherError::new(format!(
"unexpected trailing input: {}",
describe(Some(t))
)));
}
Ok(CypherQuery {
paths,
where_,
where_op,
returns,
limit,
})
}
fn parse_paths(&mut self) -> Result<Vec<Path>> {
let mut paths = vec![self.parse_path()?];
while self.eat_punct(",") {
paths.push(self.parse_path()?);
}
Ok(paths)
}
fn parse_path(&mut self) -> Result<Path> {
let first = self.parse_node()?;
let mut steps = Vec::new();
while self.at_rel_start() {
let rel = self.parse_rel()?;
let node = self.parse_node()?;
steps.push((rel, node));
}
Ok(Path { first, steps })
}
fn at_rel_start(&self) -> bool {
matches!(self.peek(), Some(Tok::Punct(s)) if s == "-" || s == "<-")
}
fn parse_node(&mut self) -> Result<Node> {
self.expect_punct("(")?;
let mut var = None;
let mut label = None;
if let Some(Tok::Ident(_)) = self.peek() {
var = Some(self.ident()?);
}
if self.eat_punct(":") {
label = Some(self.ident()?);
if self.eat_punct(":") {
return Err(CypherError::new(
"multiple labels per node are not supported",
));
}
}
if matches!(self.peek(), Some(Tok::Punct(s)) if s == "{") {
return Err(CypherError::new(
"inline node properties {{...}} are not supported",
));
}
self.expect_punct(")")?;
Ok(Node { var, label })
}
fn parse_rel(&mut self) -> Result<Rel> {
let dir = if self.eat_punct("<-") {
Dir::Reverse
} else if self.eat_punct("-") {
Dir::Forward
} else {
return Err(CypherError::new(format!(
"expected a relationship, found {}",
self.describe_next()
)));
};
self.expect_punct("[")?;
if let Some(Tok::Ident(_)) = self.peek() {
return Err(CypherError::new(
"relationship variables ([r:REL]) are not supported; use [:REL]",
));
}
self.expect_punct(":")?;
let rel_type = self.ident()?;
let var_length = self.eat_punct("*");
if var_length && matches!(self.peek(), Some(Tok::Num(_))) {
return Err(CypherError::new(
"bounded variable-length (*N..M) is not supported; use * (one-or-more)",
));
}
if matches!(self.peek(), Some(Tok::Punct(s)) if s == "{") {
return Err(CypherError::new(
"relationship properties ([:REL {{...}}]) are not supported",
));
}
self.expect_punct("]")?;
match dir {
Dir::Forward => self.expect_punct("->")?,
Dir::Reverse => self.expect_punct("-")?,
}
Ok(Rel {
rel_type,
dir,
var_length,
})
}
fn parse_where(&mut self) -> Result<(Vec<Condition>, BoolOp)> {
let mut conds = vec![self.parse_condition()?];
let mut op: Option<BoolOp> = None;
loop {
let next_op = if self.eat_keyword("AND") {
BoolOp::And
} else if self.eat_keyword("OR") {
BoolOp::Or
} else {
break;
};
if let Some(prev) = op {
if prev != next_op {
return Err(CypherError::new(
"mixing AND and OR is not supported; use one connective",
));
}
}
op = Some(next_op);
conds.push(self.parse_condition()?);
}
Ok((conds, op.unwrap_or(BoolOp::And)))
}
fn parse_condition(&mut self) -> Result<Condition> {
let var = self.ident()?;
let lhs = if self.eat_punct(".") {
let prop = self.ident()?;
Lhs::Prop(var, prop)
} else {
Lhs::Ident(var)
};
let op = self.parse_cmp_op()?;
let value = self.parse_value()?;
if matches!(lhs, Lhs::Ident(_)) && !matches!(op, CmpOp::Eq | CmpOp::Ne) {
return Err(CypherError::new(
"node identity supports only = and <> comparisons",
));
}
Ok(Condition { lhs, op, value })
}
fn parse_cmp_op(&mut self) -> Result<CmpOp> {
let op = match self.peek() {
Some(Tok::Punct(s)) => match s.as_str() {
"=" => CmpOp::Eq,
"<>" | "!=" => CmpOp::Ne,
"<" => CmpOp::Lt,
"<=" => CmpOp::Le,
">" => CmpOp::Gt,
">=" => CmpOp::Ge,
other => {
return Err(CypherError::new(format!(
"expected a comparison operator, found {other:?}"
)))
}
},
other => {
return Err(CypherError::new(format!(
"expected a comparison operator, found {}",
describe(other)
)))
}
};
self.pos += 1;
Ok(op)
}
fn parse_value(&mut self) -> Result<Value> {
match self.next() {
Some(Tok::Num(n)) => Ok(Value::Number(n)),
Some(Tok::Str(s)) => Ok(Value::Str(s)),
Some(Tok::Iri(iri)) => Ok(Value::Iri(iri)),
other => Err(CypherError::new(format!(
"expected a value (number, string, or <iri>), found {}",
describe(other.as_ref())
))),
}
}
fn parse_return(&mut self) -> Result<Vec<ReturnItem>> {
if self.eat_punct("*") {
return Err(CypherError::new(
"RETURN * is not supported; list variables explicitly",
));
}
let mut items = vec![self.parse_return_item()?];
while self.eat_punct(",") {
items.push(self.parse_return_item()?);
}
Ok(items)
}
fn parse_return_item(&mut self) -> Result<ReturnItem> {
let var = self.ident()?;
if self.eat_punct(".") {
let prop = self.ident()?;
Ok(ReturnItem::Prop(var, prop))
} else {
Ok(ReturnItem::Var(var))
}
}
}
fn describe(t: Option<&Tok>) -> String {
match t {
None => "end of input".to_string(),
Some(Tok::Ident(s)) => format!("{s:?}"),
Some(Tok::Str(s)) => format!("string {s:?}"),
Some(Tok::Num(s)) => format!("number {s:?}"),
Some(Tok::Iri(s)) => format!("IRI {s}"),
Some(Tok::Punct(s)) => format!("{s:?}"),
}
}
fn iri(base: &str, name: &str) -> String {
format!("<{base}{name}>")
}
struct VarGen {
counter: usize,
}
impl VarGen {
fn new() -> Self {
VarGen { counter: 0 }
}
fn fresh(&mut self, hint: &str) -> String {
let v = format!("__{hint}{}", self.counter);
self.counter += 1;
v
}
}
fn translate(q: &CypherQuery, base: &str) -> Result<String> {
let mut gen = VarGen::new();
let mut triples: Vec<String> = Vec::new();
let node_var = |n: &Node, gen: &mut VarGen| -> String {
match &n.var {
Some(v) => format!("?{v}"),
None => format!("?{}", gen.fresh("n")),
}
};
for path in &q.paths {
let mut prev_var = node_var(&path.first, &mut gen);
emit_node_label(&path.first, &prev_var, base, &mut triples);
for (rel, node) in &path.steps {
let cur_var = node_var(node, &mut gen);
emit_node_label(node, &cur_var, base, &mut triples);
let pred = iri(base, &rel.rel_type);
let pred = if rel.var_length {
format!("{pred}+")
} else {
pred
};
match rel.dir {
Dir::Forward => triples.push(format!("{prev_var} {pred} {cur_var} .")),
Dir::Reverse => triples.push(format!("{cur_var} {pred} {prev_var} .")),
}
prev_var = cur_var;
}
}
let mut projection: Vec<String> = Vec::new();
for item in &q.returns {
match item {
ReturnItem::Var(v) => projection.push(format!("?{v}")),
ReturnItem::Prop(v, prop) => {
let pvar = format!("?{v}_{prop}");
triples.push(format!("?{v} {} {pvar} .", iri(base, prop)));
projection.push(pvar);
}
}
}
let mut filters: Vec<String> = Vec::new();
for cond in &q.where_ {
match &cond.lhs {
Lhs::Prop(v, prop) => {
let pvar = format!("?__w_{v}_{prop}");
triples.push(format!("?{v} {} {pvar} .", iri(base, prop)));
filters.push(format!(
"{pvar} {} {}",
cond.op.sparql(),
value_to_sparql(&cond.value)
));
}
Lhs::Ident(v) => {
filters.push(format!(
"?{v} {} {}",
cond.op.sparql(),
value_to_sparql(&cond.value)
));
}
}
}
let mut sparql = String::new();
write!(sparql, "SELECT {} WHERE {{", projection.join(" ")).unwrap();
for t in &triples {
write!(sparql, " {t}").unwrap();
}
if !filters.is_empty() {
let joiner = match q.where_op {
BoolOp::And => " && ",
BoolOp::Or => " || ",
};
write!(sparql, " FILTER({})", filters.join(joiner)).unwrap();
}
sparql.push_str(" }");
if let Some(n) = q.limit {
write!(sparql, " LIMIT {n}").unwrap();
}
Ok(sparql)
}
fn emit_node_label(node: &Node, var: &str, base: &str, triples: &mut Vec<String>) {
if let Some(label) = &node.label {
triples.push(format!("{var} a {} .", iri(base, label)));
}
}
fn value_to_sparql(v: &Value) -> String {
match v {
Value::Number(n) => n.clone(),
Value::Str(s) => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")),
Value::Iri(iri) => iri.clone(),
}
}
pub fn cypher_to_sparql(cypher: &str, base: &str) -> Result<String> {
let toks = tokenize(cypher)?;
if toks.is_empty() {
return Err(CypherError::new("empty query"));
}
let mut parser = Parser::new(toks);
let query = parser.parse_query()?;
translate(&query, base)
}
pub fn eval_cypher(rete: &Rete, cypher: &str, base: &str) -> Result<QueryOutput> {
let sparql = cypher_to_sparql(cypher, base)?;
eval_query(rete, &sparql)
.map_err(|e| CypherError::new(format!("{e} (translated to: {sparql})")))
}
#[cfg(test)]
mod tests {
use super::*;
fn tr(cypher: &str) -> String {
cypher_to_sparql(cypher, DEFAULT_BASE).unwrap()
}
#[test]
fn translate_labeled_node_match() {
assert_eq!(
tr("MATCH (a:Library) RETURN a"),
"SELECT ?a WHERE { ?a a <http://ex/Library> . }"
);
}
#[test]
fn translate_relationship() {
assert_eq!(
tr("MATCH (a)-[:dependsOn]->(b) RETURN a, b"),
"SELECT ?a ?b WHERE { ?a <http://ex/dependsOn> ?b . }"
);
}
#[test]
fn translate_reverse_relationship() {
assert_eq!(
tr("MATCH (a)<-[:dependsOn]-(b) RETURN a, b"),
"SELECT ?a ?b WHERE { ?b <http://ex/dependsOn> ?a . }"
);
}
#[test]
fn translate_variable_length_to_plus_path() {
assert_eq!(
tr("MATCH (a)-[:dependsOn*]->(b) RETURN a, b"),
"SELECT ?a ?b WHERE { ?a <http://ex/dependsOn>+ ?b . }"
);
}
#[test]
fn translate_where_filter() {
assert_eq!(
tr("MATCH (a:Person) WHERE a.age > 30 RETURN a"),
"SELECT ?a WHERE { ?a a <http://ex/Person> . ?a <http://ex/age> ?__w_a_age . \
FILTER(?__w_a_age > 30) }"
);
}
#[test]
fn translate_identity_filter() {
assert_eq!(
tr("MATCH (a)-[:knows]->(b) WHERE a = <http://ex/Alice> RETURN b"),
"SELECT ?b WHERE { ?a <http://ex/knows> ?b . FILTER(?a = <http://ex/Alice>) }"
);
}
#[test]
fn translate_string_property_filter() {
assert_eq!(
tr(r#"MATCH (a) WHERE a.name = "Alice" RETURN a"#),
"SELECT ?a WHERE { ?a <http://ex/name> ?__w_a_name . FILTER(?__w_a_name = \"Alice\") }"
);
}
#[test]
fn translate_return_property_and_limit() {
assert_eq!(
tr("MATCH (a:Person) RETURN a, a.name LIMIT 5"),
"SELECT ?a ?a_name WHERE { ?a a <http://ex/Person> . ?a <http://ex/name> ?a_name . } \
LIMIT 5"
);
}
#[test]
fn translate_and_conditions() {
assert_eq!(
tr("MATCH (a) WHERE a.age > 18 AND a.age < 65 RETURN a"),
"SELECT ?a WHERE { ?a <http://ex/age> ?__w_a_age . ?a <http://ex/age> ?__w_a_age . \
FILTER(?__w_a_age > 18 && ?__w_a_age < 65) }"
);
}
#[test]
fn translate_multiple_patterns() {
assert_eq!(
tr("MATCH (a)-[:knows]->(b), (b)-[:knows]->(c) RETURN a, c"),
"SELECT ?a ?c WHERE { ?a <http://ex/knows> ?b . ?b <http://ex/knows> ?c . }"
);
}
#[test]
fn custom_base_iri() {
assert_eq!(
cypher_to_sparql("MATCH (a:Library) RETURN a", "http://my/").unwrap(),
"SELECT ?a WHERE { ?a a <http://my/Library> . }"
);
}
#[test]
fn rejects_writes() {
let err = cypher_to_sparql("CREATE (a:Person) RETURN a", DEFAULT_BASE).unwrap_err();
assert!(err.to_string().contains("CREATE"), "{err}");
}
#[test]
fn rejects_relationship_variable() {
let err = cypher_to_sparql("MATCH (a)-[r:knows]->(b) RETURN a", DEFAULT_BASE).unwrap_err();
assert!(err.to_string().contains("relationship variable"), "{err}");
}
#[test]
fn rejects_relationship_property() {
let err = cypher_to_sparql(
"MATCH (a)-[:knows {since: 2020}]->(b) RETURN a",
DEFAULT_BASE,
)
.unwrap_err();
assert!(err.to_string().contains("relationship properties"), "{err}");
}
#[test]
fn rejects_multiple_labels() {
let err = cypher_to_sparql("MATCH (a:Person:Admin) RETURN a", DEFAULT_BASE).unwrap_err();
assert!(err.to_string().contains("multiple labels"), "{err}");
}
#[test]
fn rejects_missing_return() {
let err = cypher_to_sparql("MATCH (a:Person)", DEFAULT_BASE).unwrap_err();
assert!(err.to_string().contains("RETURN"), "{err}");
}
#[test]
fn rejects_garbage() {
let err = cypher_to_sparql("not a query at all", DEFAULT_BASE).unwrap_err();
assert!(err.to_string().contains("MATCH"), "{err}");
}
#[test]
fn rejects_optional_match() {
let err = cypher_to_sparql("OPTIONAL MATCH (a) RETURN a", DEFAULT_BASE).unwrap_err();
assert!(err.to_string().contains("OPTIONAL"), "{err}");
}
fn build_deps() -> Vec<u8> {
use rete_core::{
build_pyramid_meta, write_dataset, DictionaryBuilder, GraphIndexBuilder,
DEFAULT_TILE_BUDGET,
};
let triples_nt: &[(&str, &str, &str)] = &[
(
"<http://ex/app>",
"<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>",
"<http://ex/Application>",
),
(
"<http://ex/web>",
"<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>",
"<http://ex/Library>",
),
(
"<http://ex/auth>",
"<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>",
"<http://ex/Library>",
),
(
"<http://ex/logging>",
"<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>",
"<http://ex/Library>",
),
(
"<http://ex/log4x>",
"<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>",
"<http://ex/Library>",
),
(
"<http://ex/log4x>",
"<http://ex/hasVulnerability>",
"<http://ex/CVE-2099-0001>",
),
(
"<http://ex/app>",
"<http://ex/dependsOn>",
"<http://ex/web>",
),
(
"<http://ex/app>",
"<http://ex/dependsOn>",
"<http://ex/auth>",
),
(
"<http://ex/web>",
"<http://ex/dependsOn>",
"<http://ex/logging>",
),
(
"<http://ex/auth>",
"<http://ex/dependsOn>",
"<http://ex/logging>",
),
(
"<http://ex/logging>",
"<http://ex/dependsOn>",
"<http://ex/log4x>",
),
];
let mut db = DictionaryBuilder::new();
for (s, p, o) in triples_nt {
db.observe(s, p, o);
}
let dict = db.build();
let mut def = GraphIndexBuilder::new();
let mut encoded = Vec::new();
for (s, p, o) in triples_nt {
let t = dict.encode(s, p, o).unwrap();
def.push(t);
encoded.push(t);
}
let (meta, levels) = build_pyramid_meta(&dict, &encoded, DEFAULT_TILE_BUDGET);
write_dataset(&dict, &def.build(), &[], false, &meta, levels)
}
fn rows(out: &QueryOutput) -> Vec<std::collections::BTreeMap<String, String>> {
match out {
QueryOutput::Select(_, sols) => sols
.iter()
.map(|b| b.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
.collect(),
_ => panic!("expected SELECT output"),
}
}
#[test]
fn end_to_end_labeled_match() {
let bytes = build_deps();
let rete = Rete::open(&bytes).unwrap();
let out = eval_cypher(&rete, "MATCH (a:Application) RETURN a", DEFAULT_BASE).unwrap();
let r = rows(&out);
assert_eq!(r.len(), 1);
assert_eq!(r[0]["a"], "<http://ex/app>");
}
#[test]
fn end_to_end_variable_length_path() {
let bytes = build_deps();
let rete = Rete::open(&bytes).unwrap();
let out = eval_cypher(
&rete,
"MATCH (a)-[:dependsOn*]->(b) WHERE b = <http://ex/log4x> RETURN a",
DEFAULT_BASE,
)
.unwrap();
let mut got: Vec<String> = rows(&out).into_iter().map(|m| m["a"].clone()).collect();
got.sort();
assert_eq!(
got,
vec![
"<http://ex/app>".to_string(),
"<http://ex/auth>".to_string(),
"<http://ex/logging>".to_string(),
"<http://ex/web>".to_string(),
]
);
}
#[test]
fn end_to_end_direct_relationship() {
let bytes = build_deps();
let rete = Rete::open(&bytes).unwrap();
let out = eval_cypher(
&rete,
"MATCH (a)-[:dependsOn]->(b) WHERE a = <http://ex/app> RETURN b",
DEFAULT_BASE,
)
.unwrap();
let mut got: Vec<String> = rows(&out).into_iter().map(|m| m["b"].clone()).collect();
got.sort();
assert_eq!(
got,
vec![
"<http://ex/auth>".to_string(),
"<http://ex/web>".to_string()
]
);
}
}