Skip to main content

rete_core/sparql/
partition.rs

1//! Running one query as many smaller ones, split by the values of one
2//! variable — so a long extraction can go in short, resumable chunks (the
3//! Claude Desktop extension's `partition_by`).
4//!
5//! The split is exact when every solution binds the variable: each solution
6//! then belongs to exactly one chunk, the one holding its value, so the
7//! chunks' solutions together are the query's. [`plan`] checks that, refuses
8//! the solution modifiers a single chunk cannot apply (ORDER BY, an aggregate
9//! over more than one key), and writes the **keys query**: the variable's
10//! distinct values from a required `VALUES` list on it when the query has one,
11//! else over the one required triple pattern that binds it with constants and
12//! is cheapest to scan. That is a superset of the values the solutions take —
13//! a key with no solution just makes an empty chunk — and listing it reads
14//! nothing or one range scan, where the query itself may be one read per
15//! entity. [`seed`] then cuts the query's own `VALUES` list down to a chunk's
16//! keys, or places them as `VALUES` on the first required pattern block that
17//! binds the variable, where the engine's VALUES pushdown turns each key into
18//! an index lookup.
19
20use 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/// How to run a query as chunks split by one variable: see [`crate::partition_plan`].
36#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
37pub struct PartitionPlan {
38    /// The variable split on, without its `?`.
39    pub var: String,
40    /// `"select"` or `"construct"`.
41    pub form: &'static str,
42    /// `SELECT DISTINCT ?var …`: every value the variable takes in a solution,
43    /// and possibly more (a key with no solution makes an empty chunk).
44    pub keys_query: String,
45    /// Where the keys query lists them from: `"values"`, a required `VALUES`
46    /// list on the variable (no read); `"pattern"`, the cheapest required
47    /// triple pattern binding it with constants alone (one range scan);
48    /// `"where"`, the whole WHERE clause, when neither exists — then listing
49    /// them may cost as much as the query.
50    pub keys_from: &'static str,
51    /// The query's top-level OFFSET and LIMIT. Each chunk drops them; they
52    /// apply once, to the chunks' solutions taken in key order.
53    pub offset: usize,
54    pub limit: Option<usize>,
55    /// Whether two chunks can yield the same row, so the merged rows need
56    /// deduplicating: a DISTINCT that does not project the variable, and every
57    /// CONSTRUCT (a graph is a set of triples).
58    pub dedupe: bool,
59}
60
61/// `?w`, `$w` or `w` → `w`.
62pub(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
70/// The query's top-level solution modifiers, peeled off its algebra, and the
71/// WHERE pattern under them.
72struct Shape<'q> {
73    form: &'static str,
74    offset: usize,
75    limit: Option<usize>,
76    distinct: bool,
77    /// The top-level projection (a CONSTRUCT has none).
78    projected: Option<&'q [Variable]>,
79    ordered: bool,
80    /// GROUP BY's variables, when the query aggregates.
81    group: Option<&'q [Variable]>,
82    /// Variables a top-level select expression `(… AS ?v)` assigns.
83    computed: Vec<&'q Variable>,
84    where_: &'q GraphPattern,
85}
86
87/// Peel the top-level modifiers in the order spargebra nests them — Slice,
88/// Distinct/Reduced, Project, OrderBy, select expressions and HAVING, Group —
89/// each at most once and only in that order, so a nested sub-SELECT's own
90/// modifiers are never mistaken for the query's.
91fn 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; // the group's inner is the WHERE pattern
157        }
158    }
159    s.where_ = p;
160    Ok(s)
161}
162
163/// Refuse a split that would change the answer. Shared by [`plan`] and the
164/// chunk evaluation, so a chunk is never run on a query its plan refused.
165pub(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
204/// Plan running `query` as chunks split by `?var`: the checks of [`check`],
205/// then the keys query. Reads only the tile directory, to pick the cheapest
206/// pattern to list the keys from — never tile data.
207pub(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    // The cheapest by tile bytes; with no estimate (a multi-graph FROM), the
225    // first, as written.
226    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
279/// The variables bound in EVERY solution of `p` — conservative: when unsure,
280/// a variable is left out (and a split by it refused).
281fn 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        // A sub-SELECT shows only what it projects.
339        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        // SERVICE answers from elsewhere; anything newer: nothing is certain.
358        _ => {}
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
379/// The required triple patterns of `p` whose only variable is `var`: each
380/// one must hold for every solution, so the values of `var` it matches are a
381/// superset of the keys. Not looked for under GRAPH (the pattern means
382/// something else outside it), UNION, OPTIONAL's or MINUS's right side, or a
383/// sub-SELECT (whose `?var` may be another variable of the same name).
384fn 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
414/// The values of `var` in a required `VALUES` list that binds it in every row
415/// (in order, without repeats): the solutions can take no other, so they are
416/// the keys, listed without a read. The shortest such list when there are
417/// several. Searched where [`core_patterns`] searches.
418fn 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()?; // an UNDEF: not every row binds it
429                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
449/// A blank node in a pattern is an unnamed variable.
450fn 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
458/// `plan` with a chunk's `keys` seeded in. When the query has its own required
459/// `VALUES` list on `var`, that list is cut down to the keys — the chunk is
460/// then the query a caller would write for those keys alone. Otherwise
461/// `Join(VALUES, bgp)` on the first required pattern block that mentions
462/// `var`, where the engine's VALUES pushdown substitutes each key into the
463/// scan; around the whole plan when no such block exists (still exact, as
464/// `var` is certain — just a hash join).
465pub(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
479/// The first required `VALUES` list on `var`, keeping only the rows whose
480/// value is one of `keys`. Only when every key is found in it as written: a
481/// key spelled otherwise (a literal escaped differently from the list), or
482/// one the list lacks, leaves the seeding to [`seed_in`], which matches terms
483/// rather than text.
484fn 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
544/// Row `row`'s value in column `i`, if bound.
545fn cell(row: &[Option<String>], i: usize) -> Option<&str> {
546    row.get(i).and_then(|t| t.as_deref())
547}