1use std::collections::{BTreeSet, HashSet};
21
22use serde::Serialize;
23use spargebra::algebra::GraphPattern;
24use spargebra::term::{
25 GroundTerm, NamedNodePattern, TermPattern, TriplePattern as SpTriplePattern, Variable,
26};
27use spargebra::Query;
28
29use super::eval::scan_estimate;
30use super::lower::{lower_select, parse_query};
31use super::{Plan, SparqlError};
32use crate::bgp::PatternTerm;
33use crate::file::Rete;
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
37pub struct PartitionPlan {
38 pub var: String,
40 pub form: &'static str,
42 pub keys_query: String,
45 pub keys_from: &'static str,
51 pub offset: usize,
54 pub limit: Option<usize>,
55 pub dedupe: bool,
59}
60
61pub(super) fn var_name(var: &str) -> &str {
63 var.trim().trim_start_matches(['?', '$'])
64}
65
66fn refuse(var: &str, reason: impl Into<String>) -> SparqlError {
67 SparqlError::Partition(format!("by ?{var}: {}", reason.into()))
68}
69
70struct Shape<'q> {
73 form: &'static str,
74 offset: usize,
75 limit: Option<usize>,
76 distinct: bool,
77 projected: Option<&'q [Variable]>,
79 ordered: bool,
80 group: Option<&'q [Variable]>,
82 computed: Vec<&'q Variable>,
84 where_: &'q GraphPattern,
85}
86
87fn shape(query: &Query) -> Result<Shape<'_>, &'static str> {
92 let (form, pattern) = match query {
93 Query::Select { pattern, .. } => ("select", pattern),
94 Query::Construct { pattern, .. } => ("construct", pattern),
95 Query::Ask { .. } => {
96 return Err("an ASK answer is one boolean — there is nothing to split")
97 }
98 Query::Describe { .. } => {
99 return Err("split a SELECT of the resources instead of a DESCRIBE")
100 }
101 };
102 let mut s = Shape {
103 form,
104 offset: 0,
105 limit: None,
106 distinct: false,
107 projected: None,
108 ordered: false,
109 group: None,
110 computed: Vec::new(),
111 where_: pattern,
112 };
113 let mut stage = 0;
114 let mut p: &GraphPattern = pattern;
115 loop {
116 let (next, inner): (u8, &GraphPattern) = match p {
117 GraphPattern::Slice {
118 inner,
119 start,
120 length,
121 } if stage < 1 => {
122 s.offset = *start;
123 s.limit = *length;
124 (1, &**inner)
125 }
126 GraphPattern::Distinct { inner } | GraphPattern::Reduced { inner } if stage < 2 => {
127 s.distinct = true;
128 (2, &**inner)
129 }
130 GraphPattern::Project { inner, variables } if stage < 3 => {
131 s.projected = Some(variables);
132 (3, &**inner)
133 }
134 GraphPattern::OrderBy { inner, .. } if stage < 4 => {
135 s.ordered = true;
136 (4, &**inner)
137 }
138 GraphPattern::Extend {
139 inner, variable, ..
140 } if stage <= 5 => {
141 s.computed.push(variable);
142 (5, &**inner)
143 }
144 GraphPattern::Filter { inner, .. } if stage <= 5 => (5, &**inner),
145 GraphPattern::Group {
146 inner, variables, ..
147 } if stage < 6 => {
148 s.group = Some(variables);
149 (6, &**inner)
150 }
151 _ => break,
152 };
153 stage = next;
154 p = inner;
155 if stage == 6 {
156 break; }
158 }
159 s.where_ = p;
160 Ok(s)
161}
162
163pub(super) fn check(query: &Query, var: &str) -> Result<(), SparqlError> {
166 checked(query, var).map(|_| ())
167}
168
169fn checked<'q>(query: &'q Query, var: &str) -> Result<Shape<'q>, SparqlError> {
170 let s = shape(query).map_err(|r| refuse(var, r))?;
171 if s.ordered {
172 return Err(refuse(
173 var,
174 "ORDER BY needs every row at once. Drop it here and sort the result afterwards (or \
175 sort each chunk's rows yourself)",
176 ));
177 }
178 if let Some(by) = s.group {
179 if !by.iter().any(|v| v.as_str() == var) {
180 return Err(refuse(
181 var,
182 "an aggregate over more than one value of the variable would be computed per \
183 chunk. Split by a GROUP BY variable, or aggregate after collecting the rows",
184 ));
185 }
186 }
187 if s.computed.iter().any(|v| v.as_str() == var) {
188 return Err(refuse(
189 var,
190 "the variable is computed by a select expression, not matched",
191 ));
192 }
193 if !certain(s.where_).contains(var) {
194 return Err(refuse(
195 var,
196 "some solutions may leave the variable unbound (it is matched only inside OPTIONAL, \
197 one side of UNION, a BIND, a SERVICE or a sub-SELECT that hides it), and those rows \
198 would belong to no chunk. Split by a variable every row has",
199 ));
200 }
201 Ok(s)
202}
203
204pub(super) fn plan(rete: &Rete, query: &str, var: &str) -> Result<PartitionPlan, SparqlError> {
208 let var = var_name(var);
209 if var.is_empty() || Variable::new(var).is_err() {
210 return Err(SparqlError::Partition(format!(
211 "by \"{var}\": not a variable name"
212 )));
213 }
214 let parsed = parse_query(query)?;
215 let s = checked(&parsed, var)?;
216 let dataset = match &parsed {
217 Query::Select { dataset, .. } | Query::Construct { dataset, .. } => dataset.clone(),
218 _ => None,
219 };
220 let v = Variable::new_unchecked(var);
221
222 let mut core = Vec::new();
223 core_patterns(s.where_, var, &mut core);
224 let cost = |tp: &SpTriplePattern| -> Option<u64> {
227 let one = GraphPattern::Bgp {
228 patterns: vec![tp.clone()],
229 };
230 let sel = lower_select(&one, &dataset).ok()?;
231 scan_estimate(rete, &sel)
232 };
233 let best = core
234 .iter()
235 .min_by_key(|tp| cost(tp).unwrap_or(u64::MAX))
236 .copied();
237 let (keys_from, body) = match (values_keys(s.where_, var), best) {
238 (Some(listed), _) => (
239 "values",
240 GraphPattern::Values {
241 variables: vec![v.clone()],
242 bindings: listed.into_iter().map(|t| vec![Some(t)]).collect(),
243 },
244 ),
245 (None, Some(tp)) => (
246 "pattern",
247 GraphPattern::Bgp {
248 patterns: vec![tp.clone()],
249 },
250 ),
251 (None, None) => ("where", s.where_.clone()),
252 };
253 let keys_query = Query::Select {
254 dataset,
255 pattern: GraphPattern::Distinct {
256 inner: Box::new(GraphPattern::Project {
257 inner: Box::new(body),
258 variables: vec![v],
259 }),
260 },
261 base_iri: None,
262 }
263 .to_string();
264
265 let projects_var = s
266 .projected
267 .is_none_or(|vs| vs.iter().any(|x| x.as_str() == var));
268 Ok(PartitionPlan {
269 var: var.to_string(),
270 form: s.form,
271 keys_query,
272 keys_from,
273 offset: s.offset,
274 limit: s.limit,
275 dedupe: s.form == "construct" || (s.distinct && !projects_var),
276 })
277}
278
279fn certain(p: &GraphPattern) -> BTreeSet<String> {
282 let mut out = BTreeSet::new();
283 match p {
284 GraphPattern::Bgp { patterns } => {
285 for tp in patterns {
286 term_vars(&tp.subject, &mut out);
287 if let NamedNodePattern::Variable(v) = &tp.predicate {
288 out.insert(v.as_str().to_string());
289 }
290 term_vars(&tp.object, &mut out);
291 }
292 }
293 GraphPattern::Path {
294 subject, object, ..
295 } => {
296 term_vars(subject, &mut out);
297 term_vars(object, &mut out);
298 }
299 GraphPattern::Join { left, right } => {
300 out = certain(left);
301 out.extend(certain(right));
302 }
303 GraphPattern::LeftJoin { left, .. } | GraphPattern::Minus { left, .. } => {
304 out = certain(left)
305 }
306 GraphPattern::Filter { inner, .. }
307 | GraphPattern::Extend { inner, .. }
308 | GraphPattern::Distinct { inner }
309 | GraphPattern::Reduced { inner }
310 | GraphPattern::Slice { inner, .. }
311 | GraphPattern::OrderBy { inner, .. } => out = certain(inner),
312 GraphPattern::Union { left, right } => {
313 let r = certain(right);
314 out = certain(left)
315 .into_iter()
316 .filter(|v| r.contains(v))
317 .collect();
318 }
319 GraphPattern::Graph { name, inner } => {
320 out = certain(inner);
321 if let NamedNodePattern::Variable(v) = name {
322 out.insert(v.as_str().to_string());
323 }
324 }
325 GraphPattern::Values {
326 variables,
327 bindings,
328 } => {
329 for (i, v) in variables.iter().enumerate() {
330 if bindings
331 .iter()
332 .all(|row| row.get(i).is_some_and(Option::is_some))
333 {
334 out.insert(v.as_str().to_string());
335 }
336 }
337 }
338 GraphPattern::Project { inner, variables } => {
340 let c = certain(inner);
341 out = variables
342 .iter()
343 .map(|v| v.as_str().to_string())
344 .filter(|v| c.contains(v))
345 .collect();
346 }
347 GraphPattern::Group {
348 inner, variables, ..
349 } => {
350 let c = certain(inner);
351 out = variables
352 .iter()
353 .map(|v| v.as_str().to_string())
354 .filter(|v| c.contains(v))
355 .collect();
356 }
357 _ => {}
359 }
360 out
361}
362
363fn term_vars(t: &TermPattern, out: &mut BTreeSet<String>) {
364 match t {
365 TermPattern::Variable(v) => {
366 out.insert(v.as_str().to_string());
367 }
368 TermPattern::Triple(tp) => {
369 term_vars(&tp.subject, out);
370 if let NamedNodePattern::Variable(v) = &tp.predicate {
371 out.insert(v.as_str().to_string());
372 }
373 term_vars(&tp.object, out);
374 }
375 _ => {}
376 }
377}
378
379fn core_patterns<'q>(p: &'q GraphPattern, var: &str, out: &mut Vec<&'q SpTriplePattern>) {
385 match p {
386 GraphPattern::Bgp { patterns } => {
387 for tp in patterns {
388 let mut vars = BTreeSet::new();
389 let mut blank = false;
390 for t in [&tp.subject, &tp.object] {
391 term_vars(t, &mut vars);
392 blank |= has_blank(t);
393 }
394 if let NamedNodePattern::Variable(v) = &tp.predicate {
395 vars.insert(v.as_str().to_string());
396 }
397 if !blank && vars.len() == 1 && vars.contains(var) {
398 out.push(tp);
399 }
400 }
401 }
402 GraphPattern::Join { left, right } => {
403 core_patterns(left, var, out);
404 core_patterns(right, var, out);
405 }
406 GraphPattern::LeftJoin { left, .. }
407 | GraphPattern::Minus { left, .. }
408 | GraphPattern::Filter { inner: left, .. }
409 | GraphPattern::Extend { inner: left, .. } => core_patterns(left, var, out),
410 _ => {}
411 }
412}
413
414fn values_keys(p: &GraphPattern, var: &str) -> Option<Vec<GroundTerm>> {
419 match p {
420 GraphPattern::Values {
421 variables,
422 bindings,
423 } => {
424 let i = variables.iter().position(|v| v.as_str() == var)?;
425 let mut seen = HashSet::new();
426 let mut out = Vec::new();
427 for row in bindings {
428 let term = row.get(i)?.as_ref()?; if seen.insert(term.to_string()) {
430 out.push(term.clone());
431 }
432 }
433 Some(out)
434 }
435 GraphPattern::Join { left, right } => {
436 match (values_keys(left, var), values_keys(right, var)) {
437 (Some(a), Some(b)) => Some(if a.len() <= b.len() { a } else { b }),
438 (a, b) => a.or(b),
439 }
440 }
441 GraphPattern::LeftJoin { left, .. }
442 | GraphPattern::Minus { left, .. }
443 | GraphPattern::Filter { inner: left, .. }
444 | GraphPattern::Extend { inner: left, .. } => values_keys(left, var),
445 _ => None,
446 }
447}
448
449fn has_blank(t: &TermPattern) -> bool {
451 match t {
452 TermPattern::BlankNode(_) => true,
453 TermPattern::Triple(tp) => has_blank(&tp.subject) || has_blank(&tp.object),
454 _ => false,
455 }
456}
457
458pub(super) fn seed(plan: Plan, var: &str, keys: &[String]) -> Plan {
466 if let Some(narrowed) = narrow_values(&plan, var, keys) {
467 return narrowed;
468 }
469 let values = Plan::Values(
470 vec![var.to_string()],
471 keys.iter().map(|k| vec![Some(k.clone())]).collect(),
472 );
473 match seed_in(&plan, var, &values) {
474 Some(seeded) => seeded,
475 None => Plan::Join(Box::new(values), Box::new(plan)),
476 }
477}
478
479fn narrow_values(plan: &Plan, var: &str, keys: &[String]) -> Option<Plan> {
485 match plan {
486 Plan::Values(vars, rows) => {
487 let i = vars.iter().position(|v| v == var)?;
488 let wanted: HashSet<&str> = keys.iter().map(String::as_str).collect();
489 let kept: Vec<Vec<Option<String>>> = rows
490 .iter()
491 .filter(|row| cell(row, i).is_some_and(|t| wanted.contains(t)))
492 .cloned()
493 .collect();
494 let found: HashSet<&str> = kept.iter().filter_map(|row| cell(row, i)).collect();
495 (found.len() == wanted.len()).then(|| Plan::Values(vars.clone(), kept))
496 }
497 Plan::Join(l, r) => match narrow_values(l, var, keys) {
498 Some(nl) => Some(Plan::Join(Box::new(nl), r.clone())),
499 None => narrow_values(r, var, keys).map(|nr| Plan::Join(l.clone(), Box::new(nr))),
500 },
501 Plan::LeftJoin(l, r, c) => {
502 narrow_values(l, var, keys).map(|nl| Plan::LeftJoin(Box::new(nl), r.clone(), c.clone()))
503 }
504 Plan::Minus(l, r) => {
505 narrow_values(l, var, keys).map(|nl| Plan::Minus(Box::new(nl), r.clone()))
506 }
507 Plan::Filter(e, inner) => {
508 narrow_values(inner, var, keys).map(|ni| Plan::Filter(e.clone(), Box::new(ni)))
509 }
510 Plan::Extend(v, e, inner) => narrow_values(inner, var, keys)
511 .map(|ni| Plan::Extend(v.clone(), e.clone(), Box::new(ni))),
512 _ => None,
513 }
514}
515
516fn seed_in(plan: &Plan, var: &str, values: &Plan) -> Option<Plan> {
517 match plan {
518 Plan::Bgp(patterns) => patterns
519 .iter()
520 .any(|p| {
521 [&p.s, &p.p, &p.o]
522 .into_iter()
523 .any(|t| matches!(t, PatternTerm::Var(v) if v == var))
524 })
525 .then(|| Plan::Join(Box::new(values.clone()), Box::new(plan.clone()))),
526 Plan::Join(l, r) => match seed_in(l, var, values) {
527 Some(nl) => Some(Plan::Join(Box::new(nl), r.clone())),
528 None => seed_in(r, var, values).map(|nr| Plan::Join(l.clone(), Box::new(nr))),
529 },
530 Plan::LeftJoin(l, r, c) => {
531 seed_in(l, var, values).map(|nl| Plan::LeftJoin(Box::new(nl), r.clone(), c.clone()))
532 }
533 Plan::Minus(l, r) => seed_in(l, var, values).map(|nl| Plan::Minus(Box::new(nl), r.clone())),
534 Plan::Filter(e, inner) => {
535 seed_in(inner, var, values).map(|ni| Plan::Filter(e.clone(), Box::new(ni)))
536 }
537 Plan::Extend(v, e, inner) => {
538 seed_in(inner, var, values).map(|ni| Plan::Extend(v.clone(), e.clone(), Box::new(ni)))
539 }
540 _ => None,
541 }
542}
543
544fn cell(row: &[Option<String>], i: usize) -> Option<&str> {
546 row.get(i).and_then(|t| t.as_deref())
547}