#![cfg(test)]
#![cfg(target_pointer_width = "64")] #![expect(clippy::panic, clippy::panic_in_result_fn)]
use oxigraph::model::vocab::rdf;
use oxigraph::model::{Literal, NamedNode, NamedNodeRef, QuadRef};
use oxigraph::sparql::{QueryResults, SparqlEvaluator};
use oxigraph::store::Store;
use serde_json::Value;
const N_PERSONS: usize = 20;
const ORDERS_PER_PERSON: usize = 20;
const QUERY: &str = "
PREFIX schema: <http://schema.org/>
PREFIX ex: <http://example.org/>
SELECT * WHERE {
?c a schema:Person ;
schema:addressCountry ?country ;
ex:segment ?segment .
OPTIONAL {
?order a schema:Order ;
schema:customer ?c ;
schema:totalPrice ?total .
}
}
";
#[test]
fn optional_on_foreign_key_is_not_quadratic() -> Result<(), Box<dyn std::error::Error>> {
let store = Store::new()?;
let person_type = NamedNodeRef::new("http://schema.org/Person")?;
let order_type = NamedNodeRef::new("http://schema.org/Order")?;
let address_country = NamedNodeRef::new("http://schema.org/addressCountry")?;
let segment = NamedNodeRef::new("http://example.org/segment")?;
let customer = NamedNodeRef::new("http://schema.org/customer")?;
let total_price = NamedNodeRef::new("http://schema.org/totalPrice")?;
let country = Literal::new_simple_literal("FR");
let seg = Literal::new_simple_literal("retail");
let persons: Vec<NamedNode> = (0..N_PERSONS)
.map(|i| NamedNode::new(format!("http://example.org/person/{i}")).unwrap())
.collect();
for p in &persons {
store.insert(QuadRef::new(
p,
rdf::TYPE,
person_type,
oxigraph::model::GraphNameRef::DefaultGraph,
))?;
store.insert(QuadRef::new(
p,
address_country,
country.as_ref(),
oxigraph::model::GraphNameRef::DefaultGraph,
))?;
store.insert(QuadRef::new(
p,
segment,
seg.as_ref(),
oxigraph::model::GraphNameRef::DefaultGraph,
))?;
}
let mut order_idx = 0;
for p in &persons {
for _ in 0..ORDERS_PER_PERSON {
let order = NamedNode::new(format!("http://example.org/order/{order_idx}"))?;
order_idx += 1;
let price = Literal::new_simple_literal(format!("{}", 10 + order_idx));
store.insert(QuadRef::new(
&order,
rdf::TYPE,
order_type,
oxigraph::model::GraphNameRef::DefaultGraph,
))?;
store.insert(QuadRef::new(
&order,
customer,
p.as_ref(),
oxigraph::model::GraphNameRef::DefaultGraph,
))?;
store.insert(QuadRef::new(
&order,
total_price,
price.as_ref(),
oxigraph::model::GraphNameRef::DefaultGraph,
))?;
}
}
let total_orders = N_PERSONS * ORDERS_PER_PERSON;
let quadratic_floor = N_PERSONS * total_orders;
let (results, explanation) = SparqlEvaluator::new()
.parse_query(QUERY)?
.on_store(&store)
.compute_statistics()
.explain();
let QueryResults::Solutions(solutions) = results? else {
panic!("expected solutions");
};
let row_count = solutions.count();
assert_eq!(row_count, total_orders, "result row count mismatch");
let mut buf = Vec::new();
explanation.write_in_json(&mut buf)?;
let json: Value = serde_json::from_slice(&buf)?;
let plan = &json["plan"];
let for_loop = find_label(plan, "ForLoopLeftJoin").unwrap_or_else(|| {
panic!("expected the OPTIONAL to be rewritten as ForLoopLeftJoin (Lateral); plan was:\n{plan:#}")
});
let max_quad_pattern_work = max_quad_pattern_exec_count(for_loop);
assert!(
max_quad_pattern_work < (quadratic_floor / 4) as u64,
"max inner QuadPattern exec_count = {max_quad_pattern_work} is in the quadratic regime (floor = {quadratic_floor}); plan:\n{plan:#}"
);
Ok(())
}
fn max_quad_pattern_exec_count(node: &Value) -> u64 {
let label = node.get("name").and_then(Value::as_str).unwrap_or("");
let here = if label.starts_with("QuadPattern(") {
node.get("number of results")
.and_then(Value::as_u64)
.unwrap_or(0)
} else {
0
};
let children = node
.get("children")
.and_then(Value::as_array)
.map_or(0, |c| {
c.iter().map(max_quad_pattern_exec_count).max().unwrap_or(0)
});
here.max(children)
}
fn find_label<'a>(node: &'a Value, needle: &str) -> Option<&'a Value> {
if node
.get("name")
.and_then(Value::as_str)
.is_some_and(|n| n.contains(needle))
{
return Some(node);
}
node.get("children")
.and_then(Value::as_array)?
.iter()
.find_map(|c| find_label(c, needle))
}