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 let mut arena = schema.arena.clone();
64 let costs = compute_costs(&arena);
65
66 for i in 0..arena.len() {
68 let id = ShapeId(i as u32);
69 let reordered = match arena.get(id).clone() {
70 Shape::And(cs) => Some(Shape::And(sort_by_cost(cs, &costs))),
71 Shape::Or(cs) => Some(Shape::Or(sort_by_cost(cs, &costs))),
72 _ => None,
73 };
74 if let Some(s) = reordered {
75 arena.set(id, s);
76 }
77 }
78
79 let statements = schema
80 .statements
81 .iter()
82 .map(|st| StatementPlan {
83 source: plan_selector(&arena, &st.selector),
84 shape: st.shape,
85 })
86 .collect();
87
88 arena.debug_assert_finalized();
89 PhysicalPlan {
90 arena,
91 statements,
92 names: schema.names.clone(),
93 }
94}
95
96fn sort_by_cost(mut cs: Vec<ShapeId>, costs: &[u64]) -> Vec<ShapeId> {
97 cs.sort_by_key(|c| (costs[c.0 as usize], c.0));
98 cs
99}
100
101fn plan_selector(arena: &ShapeArena, sel: &Selector) -> FocusSource {
102 match sel {
103 Selector::HasOut(p) => FocusSource::SubjectsOf(p.clone()),
104 Selector::HasIn(p) => FocusSource::ObjectsOf(p.clone()),
105 Selector::IsConst(c) => FocusSource::Node(c.clone()),
106 Selector::HasPath(path, qual) => match arena.get(*qual) {
107 Shape::TestConst(c) => FocusSource::PathToConst {
108 path: path.clone(),
109 target: c.clone(),
110 },
111 _ => FocusSource::ScanFilter {
112 path: path.clone(),
113 qualifier: *qual,
114 },
115 },
116 Selector::Sparql(target) => FocusSource::Sparql(target.clone()),
117 }
118}
119
120const C_CLOSED: u64 = 4;
123const C_PAIR: u64 = 2;
124const C_SPARQL: u64 = 100;
125const C_STAR: u64 = 10;
126const C_RECURSIVE: u64 = 50;
127
128pub fn compute_costs(arena: &ShapeArena) -> Vec<u64> {
130 let mut memo = vec![None; arena.len()];
131 let mut computing = vec![false; arena.len()];
132 for i in 0..arena.len() {
133 cost_of(arena, ShapeId(i as u32), &mut memo, &mut computing);
134 }
135 memo.into_iter().map(|c| c.unwrap_or(0)).collect()
136}
137
138fn cost_of(
139 arena: &ShapeArena,
140 id: ShapeId,
141 memo: &mut [Option<u64>],
142 computing: &mut [bool],
143) -> u64 {
144 let i = id.0 as usize;
145 if let Some(c) = memo[i] {
146 return c;
147 }
148 if computing[i] {
149 return C_RECURSIVE; }
151 computing[i] = true;
152 let cost = match arena.get(id).clone() {
153 Shape::Annotated { shape, .. } => cost_of(arena, shape, memo, computing),
154 Shape::Top | Shape::Pending => 0,
155 Shape::TestConst(_) | Shape::TestKind(_) | Shape::TestType(_) => 1,
156 Shape::Closed(_) => C_CLOSED,
157 Shape::Eq(p, _) | Shape::Disj(p, _) | Shape::Lt(p, _) | Shape::Le(p, _) => {
158 C_PAIR + path_cost(&p)
159 }
160 Shape::UniqueLang(p) => 1 + path_cost(&p),
161 Shape::Not(c) => cost_of(arena, c, memo, computing),
162 Shape::And(cs) | Shape::Or(cs) => cs
163 .iter()
164 .map(|c| cost_of(arena, *c, memo, computing))
165 .sum::<u64>()
166 .max(1),
167 Shape::Count {
168 path, qualifier, ..
169 } => {
170 let q = cost_of(arena, qualifier, memo, computing);
171 path_cost(&path) * (1 + q)
172 }
173 Shape::Sparql(_) => C_SPARQL,
174 Shape::Expression(_) => C_SPARQL,
178 };
179 computing[i] = false;
180 memo[i] = Some(cost);
181 cost
182}
183
184fn path_cost(p: &Path) -> u64 {
185 match p {
186 Path::Id => 0,
187 Path::Pred(_) => 1,
188 Path::Inverse(inner) => 1 + path_cost(inner),
189 Path::Seq(ps) | Path::Alt(ps) => ps.iter().map(path_cost).sum::<u64>().max(1),
190 Path::Star(inner) => C_STAR * (1 + path_cost(inner)),
191 }
192}
193
194pub fn plan_to_text(plan: &PhysicalPlan) -> String {
198 let mut out = String::new();
199 out.push_str(&format!("plan: {} statement(s)\n", plan.statements.len()));
200 for (i, st) in plan.statements.iter().enumerate() {
201 out.push_str(&format!(
202 " [{i}] {} ⇒ @{}\n",
203 focus_to_string(&st.source),
204 st.shape.0
205 ));
206 }
207
208 let costs = compute_costs(&plan.arena);
209 let reachable = reachable_shapes(plan);
210 out.push_str("shapes (cost-ordered):\n");
211 for id in &reachable {
212 out.push_str(&format!(
213 " @{} [cost {}] = {}\n",
214 id.0,
215 costs[id.0 as usize],
216 shape_to_string(&plan.arena, *id),
217 ));
218 }
219 out
220}
221
222fn focus_to_string(source: &FocusSource) -> String {
223 match source {
224 FocusSource::SubjectsOf(p) => format!("subjectsOf({p})"),
225 FocusSource::ObjectsOf(p) => format!("objectsOf({p})"),
226 FocusSource::Node(c) => format!("node({c})"),
227 FocusSource::PathToConst { path, target } => {
228 format!("seed {target} ⟵ {}", path_to_string(path))
229 }
230 FocusSource::ScanFilter { path, qualifier } => {
231 format!("scan ∃ {} . @{}", path_to_string(path), qualifier.0)
232 }
233 FocusSource::Sparql(_) => "sparql{…}".to_string(),
234 }
235}
236
237fn reachable_shapes(plan: &PhysicalPlan) -> BTreeSet<ShapeId> {
238 let mut stack: Vec<ShapeId> = Vec::new();
239 for st in &plan.statements {
240 stack.push(st.shape);
241 if let FocusSource::ScanFilter { qualifier, .. } = &st.source {
242 stack.push(*qualifier);
243 }
244 }
245 let mut seen = BTreeSet::new();
246 while let Some(id) = stack.pop() {
247 if seen.insert(id) {
248 match plan.arena.get(id) {
249 Shape::Annotated { shape, .. } => stack.push(*shape),
250 Shape::Not(c) => stack.push(*c),
251 Shape::And(cs) | Shape::Or(cs) => stack.extend(cs.iter().copied()),
252 Shape::Count { qualifier, .. } => stack.push(*qualifier),
253 Shape::Expression(e) => e.referenced_shapes(&mut stack),
254 _ => {}
255 }
256 }
257 }
258 seen
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use shifty_algebra::{NodeKindSet, Statement};
265
266 fn nn(s: &str) -> NamedNode {
267 NamedNode::new(s).unwrap()
268 }
269
270 fn schema_with(arena: ShapeArena, selector: Selector, shape: ShapeId) -> Schema {
271 Schema {
272 arena,
273 statements: vec![Statement { selector, shape }],
274 rules: Vec::new(),
275 names: Default::default(),
276 }
277 }
278
279 #[test]
280 fn reorders_and_cheap_first() {
281 let mut a = ShapeArena::new();
283 let kind = a.insert(Shape::TestKind(NodeKindSet::IRI));
284 let top = a.insert(Shape::Top);
285 let star = Path::star(Path::Pred(nn("http://ex/p")));
286 let count = a.insert(Shape::Count {
287 path: star,
288 min: Some(1),
289 max: None,
290 qualifier: top,
291 });
292 let and = a.insert(Shape::And(vec![count, kind])); let p = plan(&schema_with(
294 a,
295 Selector::IsConst(Term::NamedNode(nn("http://ex/x"))),
296 and,
297 ));
298 match p.arena.get(and) {
299 Shape::And(cs) => assert_eq!(cs, &vec![kind, count]), other => panic!("expected And, got {other:?}"),
301 }
302 }
303
304 #[test]
305 fn class_target_seeds_from_constant() {
306 let mut a = ShapeArena::new();
308 let class = Term::NamedNode(nn("http://ex/Person"));
309 let test = a.insert(Shape::TestConst(class.clone()));
310 let path = Path::seq(vec![
311 Path::Pred(nn("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")),
312 Path::star(Path::Pred(nn(
313 "http://www.w3.org/2000/01/rdf-schema#subClassOf",
314 ))),
315 ]);
316 let root = a.insert(Shape::TestKind(NodeKindSet::IRI));
317 let p = plan(&schema_with(a, Selector::HasPath(path.clone(), test), root));
318 assert_eq!(
319 p.statements[0].source,
320 FocusSource::PathToConst {
321 path,
322 target: class
323 }
324 );
325 }
326
327 #[test]
328 fn simple_selectors_compile() {
329 let mut a = ShapeArena::new();
330 let root = a.insert(Shape::Top);
331 let p = plan(&schema_with(a, Selector::HasOut(nn("http://ex/q")), root));
332 assert_eq!(
333 p.statements[0].source,
334 FocusSource::SubjectsOf(nn("http://ex/q"))
335 );
336 }
337}