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 };
175 computing[i] = false;
176 memo[i] = Some(cost);
177 cost
178}
179
180fn path_cost(p: &Path) -> u64 {
181 match p {
182 Path::Id => 0,
183 Path::Pred(_) => 1,
184 Path::Inverse(inner) => 1 + path_cost(inner),
185 Path::Seq(ps) | Path::Alt(ps) => ps.iter().map(path_cost).sum::<u64>().max(1),
186 Path::Star(inner) => C_STAR * (1 + path_cost(inner)),
187 }
188}
189
190pub fn plan_to_text(plan: &PhysicalPlan) -> String {
194 let mut out = String::new();
195 out.push_str(&format!("plan: {} statement(s)\n", plan.statements.len()));
196 for (i, st) in plan.statements.iter().enumerate() {
197 out.push_str(&format!(
198 " [{i}] {} ⇒ @{}\n",
199 focus_to_string(&st.source),
200 st.shape.0
201 ));
202 }
203
204 let costs = compute_costs(&plan.arena);
205 let reachable = reachable_shapes(plan);
206 out.push_str("shapes (cost-ordered):\n");
207 for id in &reachable {
208 out.push_str(&format!(
209 " @{} [cost {}] = {}\n",
210 id.0,
211 costs[id.0 as usize],
212 shape_to_string(&plan.arena, *id),
213 ));
214 }
215 out
216}
217
218fn focus_to_string(source: &FocusSource) -> String {
219 match source {
220 FocusSource::SubjectsOf(p) => format!("subjectsOf({p})"),
221 FocusSource::ObjectsOf(p) => format!("objectsOf({p})"),
222 FocusSource::Node(c) => format!("node({c})"),
223 FocusSource::PathToConst { path, target } => {
224 format!("seed {target} ⟵ {}", path_to_string(path))
225 }
226 FocusSource::ScanFilter { path, qualifier } => {
227 format!("scan ∃ {} . @{}", path_to_string(path), qualifier.0)
228 }
229 FocusSource::Sparql(_) => "sparql{…}".to_string(),
230 }
231}
232
233fn reachable_shapes(plan: &PhysicalPlan) -> BTreeSet<ShapeId> {
234 let mut stack: Vec<ShapeId> = Vec::new();
235 for st in &plan.statements {
236 stack.push(st.shape);
237 if let FocusSource::ScanFilter { qualifier, .. } = &st.source {
238 stack.push(*qualifier);
239 }
240 }
241 let mut seen = BTreeSet::new();
242 while let Some(id) = stack.pop() {
243 if seen.insert(id) {
244 match plan.arena.get(id) {
245 Shape::Annotated { shape, .. } => stack.push(*shape),
246 Shape::Not(c) => stack.push(*c),
247 Shape::And(cs) | Shape::Or(cs) => stack.extend(cs.iter().copied()),
248 Shape::Count { qualifier, .. } => stack.push(*qualifier),
249 _ => {}
250 }
251 }
252 }
253 seen
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259 use shifty_algebra::{NodeKindSet, Statement};
260
261 fn nn(s: &str) -> NamedNode {
262 NamedNode::new(s).unwrap()
263 }
264
265 fn schema_with(arena: ShapeArena, selector: Selector, shape: ShapeId) -> Schema {
266 Schema {
267 arena,
268 statements: vec![Statement { selector, shape }],
269 rules: Vec::new(),
270 names: Default::default(),
271 }
272 }
273
274 #[test]
275 fn reorders_and_cheap_first() {
276 let mut a = ShapeArena::new();
278 let kind = a.insert(Shape::TestKind(NodeKindSet::IRI));
279 let top = a.insert(Shape::Top);
280 let star = Path::star(Path::Pred(nn("http://ex/p")));
281 let count = a.insert(Shape::Count {
282 path: star,
283 min: Some(1),
284 max: None,
285 qualifier: top,
286 });
287 let and = a.insert(Shape::And(vec![count, kind])); let p = plan(&schema_with(
289 a,
290 Selector::IsConst(Term::NamedNode(nn("http://ex/x"))),
291 and,
292 ));
293 match p.arena.get(and) {
294 Shape::And(cs) => assert_eq!(cs, &vec![kind, count]), other => panic!("expected And, got {other:?}"),
296 }
297 }
298
299 #[test]
300 fn class_target_seeds_from_constant() {
301 let mut a = ShapeArena::new();
303 let class = Term::NamedNode(nn("http://ex/Person"));
304 let test = a.insert(Shape::TestConst(class.clone()));
305 let path = Path::seq(vec![
306 Path::Pred(nn("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")),
307 Path::star(Path::Pred(nn(
308 "http://www.w3.org/2000/01/rdf-schema#subClassOf",
309 ))),
310 ]);
311 let root = a.insert(Shape::TestKind(NodeKindSet::IRI));
312 let p = plan(&schema_with(a, Selector::HasPath(path.clone(), test), root));
313 assert_eq!(
314 p.statements[0].source,
315 FocusSource::PathToConst {
316 path,
317 target: class
318 }
319 );
320 }
321
322 #[test]
323 fn simple_selectors_compile() {
324 let mut a = ShapeArena::new();
325 let root = a.insert(Shape::Top);
326 let p = plan(&schema_with(a, Selector::HasOut(nn("http://ex/q")), root));
327 assert_eq!(
328 p.statements[0].source,
329 FocusSource::SubjectsOf(nn("http://ex/q"))
330 );
331 }
332}