1use serde::{Deserialize, Serialize};
18use shifty_algebra::render::{path_to_string, shape_to_string};
19use shifty_algebra::{
20 NamedNode, Path, Schema, Selector, Shape, ShapeArena, ShapeId, SparqlTarget, Term,
21};
22use std::collections::BTreeSet;
23use std::collections::HashMap;
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub enum FocusSource {
28 SubjectsOf(NamedNode),
30 ObjectsOf(NamedNode),
32 Node(Term),
34 PathToConst { path: Path, target: Term },
37 ScanFilter { path: Path, qualifier: ShapeId },
40 Sparql(SparqlTarget),
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub struct StatementPlan {
46 pub source: FocusSource,
47 pub shape: ShapeId,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51pub struct PhysicalPlan {
52 pub arena: ShapeArena,
54 pub statements: Vec<StatementPlan>,
55 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
58 pub names: HashMap<ShapeId, String>,
59}
60
61pub fn plan(schema: &Schema) -> PhysicalPlan {
63 plan_with_flags(schema, true, true)
64}
65
66pub fn plan_no_seeding(schema: &Schema) -> PhysicalPlan {
70 plan_with_flags(schema, false, true)
71}
72
73pub fn plan_no_sort(schema: &Schema) -> PhysicalPlan {
77 plan_with_flags(schema, true, false)
78}
79
80fn plan_with_flags(schema: &Schema, seeding: bool, sort: bool) -> PhysicalPlan {
81 let mut arena = schema.arena.clone();
82
83 if sort {
84 let costs = compute_costs(&arena);
85 for i in 0..arena.len() {
86 let id = ShapeId(i as u32);
87 let reordered = match arena.get(id).clone() {
88 Shape::And(cs) => Some(Shape::And(sort_by_cost(cs, &costs))),
89 Shape::Or(cs) => Some(Shape::Or(sort_by_cost(cs, &costs))),
90 _ => None,
91 };
92 if let Some(s) = reordered {
93 arena.set(id, s);
94 }
95 }
96 }
97
98 let statements = schema
99 .statements
100 .iter()
101 .map(|st| StatementPlan {
102 source: if seeding {
103 plan_selector(&arena, &st.selector)
104 } else {
105 plan_selector_no_seeding(&arena, &st.selector)
106 },
107 shape: st.shape,
108 })
109 .collect();
110
111 arena.debug_assert_finalized();
112 PhysicalPlan {
113 arena,
114 statements,
115 names: schema.names.clone(),
116 }
117}
118
119fn sort_by_cost(mut cs: Vec<ShapeId>, costs: &[u64]) -> Vec<ShapeId> {
120 cs.sort_by_key(|c| (costs[c.0 as usize], c.0));
121 cs
122}
123
124fn plan_selector(arena: &ShapeArena, sel: &Selector) -> FocusSource {
125 match sel {
126 Selector::HasOut(p) => FocusSource::SubjectsOf(p.clone()),
127 Selector::HasIn(p) => FocusSource::ObjectsOf(p.clone()),
128 Selector::IsConst(c) => FocusSource::Node(c.clone()),
129 Selector::HasPath(path, qual) => match arena.get(*qual) {
130 Shape::TestConst(c) => FocusSource::PathToConst {
131 path: path.clone(),
132 target: c.clone(),
133 },
134 _ => FocusSource::ScanFilter {
135 path: path.clone(),
136 qualifier: *qual,
137 },
138 },
139 Selector::Sparql(target) => FocusSource::Sparql(target.clone()),
140 }
141}
142
143fn plan_selector_no_seeding(_arena: &ShapeArena, sel: &Selector) -> FocusSource {
148 match sel {
149 Selector::HasOut(p) => FocusSource::SubjectsOf(p.clone()),
150 Selector::HasIn(p) => FocusSource::ObjectsOf(p.clone()),
151 Selector::IsConst(c) => FocusSource::Node(c.clone()),
152 Selector::HasPath(path, qual) => FocusSource::ScanFilter {
153 path: path.clone(),
154 qualifier: *qual,
155 },
156 Selector::Sparql(target) => FocusSource::Sparql(target.clone()),
157 }
158}
159
160const C_CLOSED: u64 = 4;
163const C_PAIR: u64 = 2;
164const C_SPARQL: u64 = 100;
165const C_STAR: u64 = 10;
166const C_RECURSIVE: u64 = 50;
167
168pub fn compute_costs(arena: &ShapeArena) -> Vec<u64> {
170 let mut memo = vec![None; arena.len()];
171 let mut computing = vec![false; arena.len()];
172 for i in 0..arena.len() {
173 cost_of(arena, ShapeId(i as u32), &mut memo, &mut computing);
174 }
175 memo.into_iter().map(|c| c.unwrap_or(0)).collect()
176}
177
178fn cost_of(
179 arena: &ShapeArena,
180 id: ShapeId,
181 memo: &mut [Option<u64>],
182 computing: &mut [bool],
183) -> u64 {
184 let i = id.0 as usize;
185 if let Some(c) = memo[i] {
186 return c;
187 }
188 if computing[i] {
189 return C_RECURSIVE; }
191 computing[i] = true;
192 let cost = match arena.get(id).clone() {
193 Shape::Annotated { shape, .. } => cost_of(arena, shape, memo, computing),
194 Shape::Top | Shape::Pending => 0,
195 Shape::TestConst(_) | Shape::TestKind(_) | Shape::TestType(_) => 1,
196 Shape::Closed(_) => C_CLOSED,
197 Shape::Eq(p, _) | Shape::Disj(p, _) | Shape::Lt(p, _) | Shape::Le(p, _) => {
198 C_PAIR + path_cost(&p)
199 }
200 Shape::UniqueLang(p) => 1 + path_cost(&p),
201 Shape::Not(c) => cost_of(arena, c, memo, computing),
202 Shape::And(cs) | Shape::Or(cs) => cs
203 .iter()
204 .map(|c| cost_of(arena, *c, memo, computing))
205 .sum::<u64>()
206 .max(1),
207 Shape::Count {
208 path, qualifier, ..
209 } => {
210 let q = cost_of(arena, qualifier, memo, computing);
211 path_cost(&path) * (1 + q)
212 }
213 Shape::Sparql(_) => C_SPARQL,
214 Shape::Expression(_) => C_SPARQL,
218 };
219 computing[i] = false;
220 memo[i] = Some(cost);
221 cost
222}
223
224fn path_cost(p: &Path) -> u64 {
225 match p {
226 Path::Id => 0,
227 Path::Pred(_) => 1,
228 Path::Inverse(inner) => 1 + path_cost(inner),
229 Path::Seq(ps) | Path::Alt(ps) => ps.iter().map(path_cost).sum::<u64>().max(1),
230 Path::Star(inner) => C_STAR * (1 + path_cost(inner)),
231 }
232}
233
234pub fn plan_to_text(plan: &PhysicalPlan) -> String {
238 let mut out = String::new();
239 out.push_str(&format!("plan: {} statement(s)\n", plan.statements.len()));
240 for (i, st) in plan.statements.iter().enumerate() {
241 out.push_str(&format!(
242 " [{i}] {} ⇒ @{}\n",
243 focus_to_string(&st.source),
244 st.shape.0
245 ));
246 }
247
248 let costs = compute_costs(&plan.arena);
249 let reachable = reachable_shapes(plan);
250 out.push_str("shapes (cost-ordered):\n");
251 for id in &reachable {
252 out.push_str(&format!(
253 " @{} [cost {}] = {}\n",
254 id.0,
255 costs[id.0 as usize],
256 shape_to_string(&plan.arena, *id),
257 ));
258 }
259 out
260}
261
262fn focus_to_string(source: &FocusSource) -> String {
263 match source {
264 FocusSource::SubjectsOf(p) => format!("subjectsOf({p})"),
265 FocusSource::ObjectsOf(p) => format!("objectsOf({p})"),
266 FocusSource::Node(c) => format!("node({c})"),
267 FocusSource::PathToConst { path, target } => {
268 format!("seed {target} ⟵ {}", path_to_string(path))
269 }
270 FocusSource::ScanFilter { path, qualifier } => {
271 format!("scan ∃ {} . @{}", path_to_string(path), qualifier.0)
272 }
273 FocusSource::Sparql(_) => "sparql{…}".to_string(),
274 }
275}
276
277fn reachable_shapes(plan: &PhysicalPlan) -> BTreeSet<ShapeId> {
278 let mut stack: Vec<ShapeId> = Vec::new();
279 for st in &plan.statements {
280 stack.push(st.shape);
281 if let FocusSource::ScanFilter { qualifier, .. } = &st.source {
282 stack.push(*qualifier);
283 }
284 }
285 let mut seen = BTreeSet::new();
286 while let Some(id) = stack.pop() {
287 if seen.insert(id) {
288 match plan.arena.get(id) {
289 Shape::Annotated { shape, .. } => stack.push(*shape),
290 Shape::Not(c) => stack.push(*c),
291 Shape::And(cs) | Shape::Or(cs) => stack.extend(cs.iter().copied()),
292 Shape::Count { qualifier, .. } => stack.push(*qualifier),
293 Shape::Expression(e) => e.referenced_shapes(&mut stack),
294 _ => {}
295 }
296 }
297 }
298 seen
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304 use shifty_algebra::{NodeKindSet, Statement};
305
306 fn nn(s: &str) -> NamedNode {
307 NamedNode::new(s).unwrap()
308 }
309
310 fn schema_with(arena: ShapeArena, selector: Selector, shape: ShapeId) -> Schema {
311 Schema {
312 arena,
313 statements: vec![Statement { selector, shape }],
314 rules: Vec::new(),
315 names: Default::default(),
316 }
317 }
318
319 #[test]
320 fn reorders_and_cheap_first() {
321 let mut a = ShapeArena::new();
323 let kind = a.insert(Shape::TestKind(NodeKindSet::IRI));
324 let top = a.insert(Shape::Top);
325 let star = Path::star(Path::Pred(nn("http://ex/p")));
326 let count = a.insert(Shape::Count {
327 path: star,
328 min: Some(1),
329 max: None,
330 qualifier: top,
331 });
332 let and = a.insert(Shape::And(vec![count, kind])); let p = plan(&schema_with(
334 a,
335 Selector::IsConst(Term::NamedNode(nn("http://ex/x"))),
336 and,
337 ));
338 match p.arena.get(and) {
339 Shape::And(cs) => assert_eq!(cs, &vec![kind, count]), other => panic!("expected And, got {other:?}"),
341 }
342 }
343
344 #[test]
345 fn class_target_seeds_from_constant() {
346 let mut a = ShapeArena::new();
348 let class = Term::NamedNode(nn("http://ex/Person"));
349 let test = a.insert(Shape::TestConst(class.clone()));
350 let path = Path::seq(vec![
351 Path::Pred(nn("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")),
352 Path::star(Path::Pred(nn(
353 "http://www.w3.org/2000/01/rdf-schema#subClassOf",
354 ))),
355 ]);
356 let root = a.insert(Shape::TestKind(NodeKindSet::IRI));
357 let p = plan(&schema_with(a, Selector::HasPath(path.clone(), test), root));
358 assert_eq!(
359 p.statements[0].source,
360 FocusSource::PathToConst {
361 path,
362 target: class
363 }
364 );
365 }
366
367 #[test]
368 fn simple_selectors_compile() {
369 let mut a = ShapeArena::new();
370 let root = a.insert(Shape::Top);
371 let p = plan(&schema_with(a, Selector::HasOut(nn("http://ex/q")), root));
372 assert_eq!(
373 p.statements[0].source,
374 FocusSource::SubjectsOf(nn("http://ex/q"))
375 );
376 }
377}