Skip to main content

ikigai_script/
sparql.rs

1//! **SPARQL as a script language**: a stored query is a script whose language is SPARQL.
2//!
3//! A query gets everything a Lisp script gets (versions, the three authorities, run records,
4//! the manifold) and three things of its own, all read from the PARSED text, never from a
5//! caller's argument:
6//!
7//! - **its form**: SELECT, ASK, CONSTRUCT and DESCRIBE run as reads (`…:result`); an UPDATE
8//!   runs as a write (`…:runs`). See [`Form`].
9//! - **its parameters**, declared in the query's leading comment block and bound as typed
10//!   RDF terms by substitution into the parsed algebra, never by splicing text. See
11//!   [`Parameter`] and [`bind`].
12//! - **its authority**, derived from the graphs it names: `urn:cap:store:read:graph:<g>` for
13//!   each graph a query reads, `urn:cap:store:write:graph:<g>` (and the read token when it
14//!   has a `WHERE`) for the one graph an update writes. See [`Analysis::requires`].
15//!
16//! # Declaring parameters
17//!
18//! In the comment block before the query's first token, one line per parameter:
19//!
20//! ```text
21//! # @param <name> <type> [required | optional | default <value>] [-- <summary>]
22//! ```
23//!
24//! - `<name>` is the query variable it binds (`?days` or `$days` in the text), ASCII letters,
25//!   digits and `_`. `as`, `name` and `content` are taken by the doors.
26//! - `<type>` is an XSD datatype (`xsd:integer`, `xsd:dateTime`, …: see [`DATATYPES`]),
27//!   bound as a typed literal after its lexical form is checked; or a class (`<iri>`, or
28//!   `rdfs:Resource` for "any IRI"), bound as an IRI.
29//! - Bare means required; `default <value>` makes it optional with that value (a value with
30//!   a space in it is a JSON string, `"like this"`).
31//!
32//! ```
33//! use ikigai_script::sparql::{analyze, Form, SparqlDoor};
34//!
35//! // (`##` is how rustdoc spells a line that starts with `#`.)
36//! let text = r#"
37//! ## Items untouched for a while.
38//! ## @param days xsd:integer default 7 -- days without a touch
39//! ## @param repo <https://ikigai-rs.dev/ns#Repo> optional
40//! PREFIX ledger: <https://ikigai-rs.dev/ns/ledger#>
41//! SELECT ?item WHERE {
42//!   GRAPH <urn:iki:ledger:default> {
43//!     ?item ledger:age ?age ; ledger:repo ?repo FILTER(?age > ?days)
44//!   }
45//! }"#;
46//! let analysis = analyze(text, &SparqlDoor::store()).unwrap();
47//! assert_eq!(analysis.form, Form::Select);
48//! assert_eq!(analysis.parameters[0].name, "days");
49//! assert_eq!(analysis.parameters[0].default.as_deref(), Some("7"));
50//! assert!(!analysis.parameters[1].required);
51//! // Authority comes from the graphs the text names, never from the author.
52//! assert_eq!(
53//!     analysis.requires().into_iter().collect::<Vec<_>>(),
54//!     vec!["urn:cap:store:read:graph:urn:iki:ledger:default".to_string()]
55//! );
56//! ```
57//!
58//! # The dataset, and why some queries are refused at publish
59//!
60//! Evaluation is a sub-request to the host's store through its graph-scoped doors
61//! (`urn:iki:store:graph-{select,ask,construct,describe}` and `…:graph-update`), so the
62//! store's bounds and tenancy apply unchanged. Those doors take ONE set of graphs `G`: the
63//! default graph is the merge of `G` and the named graphs are `G` (ikigai-store's
64//! `src/scope.rs`). A script's `G` is every graph its text names: `FROM`, `FROM NAMED` and
65//! `GRAPH <iri>` all add to it, and the clauses themselves are removed from the text the
66//! store sees (that door refuses them, because `graph=` IS the dataset).
67//!
68//! That is one dataset where SPARQL has two, so a query whose answer would CHANGE under it
69//! is refused at publish rather than answered differently:
70//!
71//! - a query that reads the default graph (a pattern outside every `GRAPH` block, or a
72//!   `DESCRIBE`) must name its whole dataset with `FROM` (or the host gives one,
73//!   [`SparqlDoor::default_graphs`]): a `FROM NAMED` or `GRAPH <iri>` graph outside it would
74//!   join the merge its bare patterns read, and a `GRAPH ?g` would pull the caller's whole
75//!   readable union into it;
76//! - a query that names no graph at all and has no variable `GRAPH` reads nothing.
77//!
78//! A `GRAPH ?g` whose graph the text does not fix runs over **the caller's readable union**
79//! (and never more): the graphs the run may read, computed per run.
80
81use std::collections::{BTreeMap, BTreeSet};
82
83use ikigai_core::{Error, Result};
84use serde::{Deserialize, Serialize};
85use spargebra::algebra::{
86    AggregateExpression, Expression, GraphPattern, GraphTarget, OrderExpression, QueryDataset,
87};
88use spargebra::term::{
89    GraphName, GraphNamePattern, GroundQuadPattern, GroundTermPattern, Literal, NamedNode,
90    NamedNodePattern, QuadPattern, TermPattern, TriplePattern, Variable,
91};
92use spargebra::{GraphUpdateOperation, Query, SparqlParser, Update};
93
94use crate::limits;
95
96/// The store's per-graph read scope, as a family: "holds SOME grant under this prefix".
97/// A query that reads the caller's readable union (`GRAPH ?g`) declares it.
98pub const CAP_READ_GRAPH: &str = "urn:cap:store:read:graph:*";
99/// The prefix of every per-graph read grant.
100pub const READ_GRAPH_PREFIX: &str = "urn:cap:store:read:graph:";
101/// The prefix of every per-graph write grant.
102pub const WRITE_GRAPH_PREFIX: &str = "urn:cap:store:write:graph:";
103/// The store's broad read scope: what enumerating every graph needs when the run's
104/// authority is root (see `urn:iki:store:graphs`).
105pub const CAP_STORE_READ: &str = "urn:cap:store:read";
106
107/// The exact grant to read graph `iri` through the store's scoped doors.
108pub fn cap_read_graph(iri: &str) -> String {
109    format!("{READ_GRAPH_PREFIX}{iri}")
110}
111
112/// The exact grant to write graph `iri` through `urn:iki:store:graph-update`.
113pub fn cap_write_graph(iri: &str) -> String {
114    format!("{WRITE_GRAPH_PREFIX}{iri}")
115}
116
117const XSD: &str = "http://www.w3.org/2001/XMLSchema#";
118const RDFS_RESOURCE: &str = "http://www.w3.org/2000/01/rdf-schema#Resource";
119
120/// The XSD datatypes a parameter may declare, by local name. A value's lexical form is
121/// checked against its type and bound in its canonical form, so `007` binds as `7`.
122pub const DATATYPES: [&str; 13] = [
123    "string",
124    "boolean",
125    "integer",
126    "decimal",
127    "double",
128    "float",
129    "date",
130    "dateTime",
131    "time",
132    "duration",
133    "dayTimeDuration",
134    "yearMonthDuration",
135    "anyURI",
136];
137
138/// Names a parameter may not take: each is an argument a door already reads.
139pub const RESERVED_PARAMETERS: [&str; 3] = ["as", "name", "content"];
140
141/// The result faces of SELECT and ASK, default first.
142pub const RESULT_FACES: [&str; 4] = [
143    "application/sparql-results+json",
144    "application/sparql-results+xml",
145    "text/csv",
146    "text/tab-separated-values",
147];
148/// The faces of CONSTRUCT and DESCRIBE, default first.
149pub const GRAPH_FACES: [&str; 2] = ["text/turtle", "application/n-triples"];
150
151// ---------------------------------------------------------------------------------------
152// The host's door
153// ---------------------------------------------------------------------------------------
154
155/// Where a host evaluates SPARQL scripts: the store's graph-scoped doors, under a prefix.
156///
157/// A host decides this, as it decides which evaluator a Lisp script reaches. Evaluation is
158/// always a sub-request, so the store's bounds (the pre-parse nesting bound, the sized
159/// stack, any time budget) and its tenancy apply to every run; this crate never evaluates
160/// a query itself.
161///
162/// ```
163/// use ikigai_script::sparql::{Form, SparqlDoor};
164/// let door = SparqlDoor::store();
165/// assert_eq!(door.query_iri(Form::Select), "urn:iki:store:graph-select");
166/// assert_eq!(door.query_iri(Form::Update), "urn:iki:store:graph-update");
167/// assert_eq!(door.graphs_iri(), "urn:iki:store:graphs");
168/// ```
169#[derive(Clone, Debug, PartialEq, Eq)]
170#[non_exhaustive]
171pub struct SparqlDoor {
172    prefix: String,
173    default_graphs: BTreeSet<String>,
174}
175
176impl SparqlDoor {
177    /// `ikigai-store` bound at its own names, `urn:iki:store:`, with no default dataset.
178    pub fn store() -> SparqlDoor {
179        SparqlDoor::store_at("urn:iki:store:")
180    }
181
182    /// `ikigai-store`'s doors under another prefix (a host that mounts the store elsewhere).
183    /// The prefix ends in `:`.
184    pub fn store_at(prefix: impl Into<String>) -> SparqlDoor {
185        SparqlDoor {
186            prefix: prefix.into(),
187            default_graphs: BTreeSet::new(),
188        }
189    }
190
191    /// The graphs a query reads when it reads the default graph and names none with
192    /// `FROM`: this host's default dataset. They are part of what such a query DERIVES, so
193    /// its runs need their read grants like any other graph's. ⚠ Changing them changes what
194    /// a published query reads at its next run; republish to restate its `requires`.
195    pub fn default_graphs<I, S>(mut self, graphs: I) -> SparqlDoor
196    where
197        I: IntoIterator<Item = S>,
198        S: Into<String>,
199    {
200        self.default_graphs = graphs.into_iter().map(Into::into).collect();
201        self
202    }
203
204    /// The door a query of `form` is issued to: `{prefix}graph-{form}`.
205    pub fn query_iri(&self, form: Form) -> String {
206        format!("{}graph-{}", self.prefix, form.as_str())
207    }
208
209    /// The door that lists which graphs a caller may read: `{prefix}graphs`.
210    pub fn graphs_iri(&self) -> String {
211        format!("{}graphs", self.prefix)
212    }
213
214    /// The host's default dataset.
215    pub fn defaults(&self) -> &BTreeSet<String> {
216        &self.default_graphs
217    }
218}
219
220// ---------------------------------------------------------------------------------------
221// Forms and parameters
222// ---------------------------------------------------------------------------------------
223
224/// What a SPARQL script is, read from its parsed text.
225#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
226#[serde(rename_all = "lowercase")]
227pub enum Form {
228    /// SELECT: a result set.
229    Select,
230    /// ASK: a boolean result.
231    Ask,
232    /// CONSTRUCT: a graph.
233    Construct,
234    /// DESCRIBE: a graph.
235    Describe,
236    /// An UPDATE: a write.
237    Update,
238}
239
240impl Form {
241    /// The word the store's doors and every face use.
242    pub fn as_str(self) -> &'static str {
243        match self {
244            Form::Select => "select",
245            Form::Ask => "ask",
246            Form::Construct => "construct",
247            Form::Describe => "describe",
248            Form::Update => "update",
249        }
250    }
251
252    /// Whether running it is a READ (`…:result`); an update is a write (`…:runs` only).
253    pub fn is_read(self) -> bool {
254        self != Form::Update
255    }
256
257    /// The faces a run of this form can answer in, default first: the SPARQL 1.1 result
258    /// formats for SELECT and ASK, Turtle and N-Triples for CONSTRUCT and DESCRIBE, plain
259    /// text for an update (the store's report).
260    pub fn faces(self) -> &'static [&'static str] {
261        match self {
262            Form::Select | Form::Ask => &RESULT_FACES,
263            Form::Construct | Form::Describe => &GRAPH_FACES,
264            Form::Update => &["text/plain"],
265        }
266    }
267}
268
269/// What a parameter's value is.
270#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
271#[serde(rename_all = "lowercase", tag = "kind", content = "iri")]
272pub enum ParamType {
273    /// A typed literal of this XSD datatype (its full IRI).
274    Datatype(String),
275    /// An IRI naming an instance of this class (its full IRI; `rdfs:Resource` for any).
276    Class(String),
277}
278
279impl ParamType {
280    /// The IRI an `ArgSpec` carries as its `class`.
281    pub fn iri(&self) -> &str {
282        match self {
283            ParamType::Datatype(iri) | ParamType::Class(iri) => iri,
284        }
285    }
286}
287
288/// One declared parameter of a SPARQL script.
289#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
290pub struct Parameter {
291    /// The variable it binds, and the argument a run passes it as.
292    pub name: String,
293    /// What its value is.
294    #[serde(rename = "type")]
295    pub kind: ParamType,
296    /// Whether a run must pass it.
297    pub required: bool,
298    /// The value an absent argument takes, in its lexical form.
299    pub default: Option<String>,
300    /// What it means, for the catalog.
301    pub summary: Option<String>,
302}
303
304/// A parameter's value as an RDF term.
305#[derive(Clone, Debug, PartialEq, Eq)]
306pub enum Value {
307    /// An IRI (a class-typed parameter).
308    Iri(NamedNode),
309    /// A typed literal (a datatype-typed parameter).
310    Literal(Literal),
311}
312
313impl Parameter {
314    /// Check `lexical` against this parameter's type and make it a term: a typed literal
315    /// in its canonical form, or an IRI. Refused with `InvalidArgument` naming the
316    /// parameter when it is not one.
317    ///
318    /// ```
319    /// use ikigai_script::sparql::{ParamType, Parameter, Value};
320    /// let days = Parameter {
321    ///     name: "days".into(),
322    ///     kind: ParamType::Datatype("http://www.w3.org/2001/XMLSchema#integer".into()),
323    ///     required: true,
324    ///     default: None,
325    ///     summary: None,
326    /// };
327    /// match days.value("007").unwrap() {
328    ///     Value::Literal(l) => assert_eq!(l.to_string(), "\"7\"^^<http://www.w3.org/2001/XMLSchema#integer>"),
329    ///     other => panic!("{other:?}"),
330    /// }
331    /// assert!(days.value("seven").is_err());
332    /// // Whatever a string parameter holds, it is ONE literal: the serializer escapes it.
333    /// let text = Parameter { kind: ParamType::Datatype("http://www.w3.org/2001/XMLSchema#string".into()), ..days };
334    /// match text.value("\" } ; DROP ALL ; #").unwrap() {
335    ///     Value::Literal(l) => assert_eq!(l.value(), "\" } ; DROP ALL ; #"),
336    ///     other => panic!("{other:?}"),
337    /// }
338    /// ```
339    pub fn value(&self, lexical: &str) -> Result<Value> {
340        let bad = |detail: String| Error::InvalidArgument {
341            name: self.name.clone(),
342            detail,
343        };
344        match &self.kind {
345            ParamType::Class(class) => {
346                NamedNode::new(lexical.trim()).map(Value::Iri).map_err(|e| {
347                    bad(format!(
348                        "`{lexical}` is not an IRI ({e}); this parameter names an instance of \
349                         <{class}>"
350                    ))
351                })
352            }
353            ParamType::Datatype(datatype) => {
354                let local = datatype.strip_prefix(XSD).unwrap_or(datatype);
355                let canonical = canonical(local, lexical)
356                    .ok_or_else(|| bad(format!("`{lexical}` is not a valid xsd:{local}")))?;
357                Ok(Value::Literal(Literal::new_typed_literal(
358                    canonical,
359                    NamedNode::new_unchecked(datatype.clone()),
360                )))
361            }
362        }
363    }
364}
365
366/// The canonical lexical form of `lexical` as `xsd:{local}`, or `None` when it is not one.
367fn canonical(local: &str, lexical: &str) -> Option<String> {
368    use oxsdatatypes::{
369        Boolean, Date, DateTime, DayTimeDuration, Decimal, Double, Duration, Float, Integer, Time,
370        YearMonthDuration,
371    };
372    // XSD's whitespace facet is `collapse` for every type here but string.
373    let t = lexical.trim();
374    match local {
375        "string" => Some(lexical.to_string()),
376        "anyURI" => Some(t.to_string()),
377        "boolean" => t.parse::<Boolean>().ok().map(|v| v.to_string()),
378        "integer" => t.parse::<Integer>().ok().map(|v| v.to_string()),
379        "decimal" => t.parse::<Decimal>().ok().map(|v| v.to_string()),
380        "double" => t.parse::<Double>().ok().map(|v| v.to_string()),
381        "float" => t.parse::<Float>().ok().map(|v| v.to_string()),
382        "date" => t.parse::<Date>().ok().map(|v| v.to_string()),
383        "dateTime" => t.parse::<DateTime>().ok().map(|v| v.to_string()),
384        "time" => t.parse::<Time>().ok().map(|v| v.to_string()),
385        "duration" => t.parse::<Duration>().ok().map(|v| v.to_string()),
386        "dayTimeDuration" => t.parse::<DayTimeDuration>().ok().map(|v| v.to_string()),
387        "yearMonthDuration" => t.parse::<YearMonthDuration>().ok().map(|v| v.to_string()),
388        _ => None,
389    }
390}
391
392fn invalid(detail: impl Into<String>) -> Error {
393    Error::InvalidArgument {
394        name: "content".to_string(),
395        detail: detail.into(),
396    }
397}
398
399/// The parameters declared in `source`'s leading comment block.
400///
401/// Only that block declares: before the first token, a `#` line is a comment and nothing
402/// else. A declaration-shaped comment after it is refused rather than ignored, because an
403/// ignored declaration leaves its variable unbound and the query silently answers a
404/// different question.
405pub fn parameters(source: &str) -> Result<Vec<Parameter>> {
406    let mut out: Vec<Parameter> = Vec::new();
407    let mut header = true;
408    for (number, line) in source.lines().enumerate() {
409        let trimmed = line.trim();
410        let comment = trimmed.strip_prefix('#').map(str::trim_start);
411        if header && trimmed.is_empty() {
412            continue;
413        }
414        match comment {
415            Some(body) if header => {
416                if let Some(rest) = body.strip_prefix("@param") {
417                    if !rest.starts_with(char::is_whitespace) {
418                        return Err(invalid(format!(
419                            "line {}: `@param` needs a name and a type",
420                            number + 1
421                        )));
422                    }
423                    let parameter = declaration(rest, number + 1)?;
424                    if out.iter().any(|p| p.name == parameter.name) {
425                        return Err(invalid(format!(
426                            "line {}: `{}` is declared twice",
427                            number + 1,
428                            parameter.name
429                        )));
430                    }
431                    out.push(parameter);
432                }
433            }
434            Some(body) if body.starts_with("@param") => {
435                return Err(invalid(format!(
436                    "line {}: a parameter is declared only in the comment block before the \
437                     query's first token; this one would be ignored, and its variable left \
438                     unbound",
439                    number + 1
440                )));
441            }
442            _ => header = false,
443        }
444    }
445    Ok(out)
446}
447
448/// One `@param` line, after the keyword.
449fn declaration(rest: &str, line: usize) -> Result<Parameter> {
450    let bad = |detail: String| invalid(format!("line {line}: {detail}"));
451    let mut tokens = Tokens { rest: rest.trim() };
452    let name = tokens
453        .word()
454        .ok_or_else(|| bad("`@param` needs a name and a type".to_string()))?;
455    let name = name.strip_prefix(['?', '$']).unwrap_or(&name).to_string();
456    let valid = name
457        .chars()
458        .next()
459        .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
460        && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
461    if !valid {
462        return Err(bad(format!(
463            "`{name}` is not a parameter name: ASCII letters, digits and `_`, not starting \
464             with a digit"
465        )));
466    }
467    if RESERVED_PARAMETERS.contains(&name.as_str()) {
468        return Err(bad(format!(
469            "`{name}` is an argument a door already reads ({}); choose another name",
470            RESERVED_PARAMETERS.join(", ")
471        )));
472    }
473    let kind = tokens
474        .word()
475        .ok_or_else(|| {
476            bad(format!(
477                "`{name}` needs a type: `xsd:integer`, `<class IRI>`, …"
478            ))
479        })
480        .and_then(|t| param_type(&t).map_err(bad))?;
481    let mut parameter = Parameter {
482        name,
483        kind,
484        required: true,
485        default: None,
486        summary: None,
487    };
488    while let Some(word) = tokens.word() {
489        match word.as_str() {
490            "required" => parameter.required = true,
491            "optional" => parameter.required = false,
492            "default" => {
493                let value = tokens
494                    .value()
495                    .map_err(bad)?
496                    .ok_or_else(|| bad("`default` needs a value".to_string()))?;
497                // The default must be a value of its own type: checked here, at publish.
498                parameter.value(&value)?;
499                parameter.default = Some(value);
500                parameter.required = false;
501            }
502            "--" => {
503                let summary = tokens.rest.trim();
504                if !summary.is_empty() {
505                    parameter.summary = Some(summary.to_string());
506                }
507                break;
508            }
509            other => {
510                return Err(bad(format!(
511                    "`{other}` is not `required`, `optional`, `default <value>` or \
512                     `-- <summary>`"
513                )))
514            }
515        }
516    }
517    Ok(parameter)
518}
519
520fn param_type(token: &str) -> std::result::Result<ParamType, String> {
521    if let Some(local) = token.strip_prefix("xsd:") {
522        return datatype(&format!("{XSD}{local}"));
523    }
524    if token == "rdfs:Resource" {
525        return Ok(ParamType::Class(RDFS_RESOURCE.to_string()));
526    }
527    if let Some(iri) = token.strip_prefix('<').and_then(|t| t.strip_suffix('>')) {
528        NamedNode::new(iri).map_err(|e| format!("`{token}` is not an IRI: {e}"))?;
529        if iri.starts_with(XSD) {
530            return datatype(iri);
531        }
532        return Ok(ParamType::Class(iri.to_string()));
533    }
534    Err(format!(
535        "`{token}` is not a type: an XSD datatype (`xsd:integer`, one of {}), a class \
536         (`<iri>`), or `rdfs:Resource` for any IRI",
537        DATATYPES.join(", ")
538    ))
539}
540
541fn datatype(iri: &str) -> std::result::Result<ParamType, String> {
542    let local = iri.strip_prefix(XSD).unwrap_or(iri);
543    if DATATYPES.contains(&local) {
544        Ok(ParamType::Datatype(iri.to_string()))
545    } else {
546        Err(format!(
547            "xsd:{local} is not a parameter datatype here; one of {}",
548            DATATYPES.join(", ")
549        ))
550    }
551}
552
553/// A tiny tokenizer for a declaration line: whitespace-separated words, and a value that
554/// may be a JSON string.
555struct Tokens<'a> {
556    rest: &'a str,
557}
558
559impl Tokens<'_> {
560    fn word(&mut self) -> Option<String> {
561        let rest = self.rest.trim_start();
562        if rest.is_empty() {
563            return None;
564        }
565        let end = rest.find(char::is_whitespace).unwrap_or(rest.len());
566        let (word, tail) = rest.split_at(end);
567        self.rest = tail;
568        Some(word.to_string())
569    }
570
571    fn value(&mut self) -> std::result::Result<Option<String>, String> {
572        let rest = self.rest.trim_start();
573        if !rest.starts_with('"') {
574            return Ok(self.word());
575        }
576        // A JSON string: find its end by letting serde read exactly one value.
577        let mut stream = serde_json::Deserializer::from_str(rest).into_iter::<String>();
578        match stream.next() {
579            Some(Ok(value)) => {
580                self.rest = &rest[stream.byte_offset()..];
581                Ok(Some(value))
582            }
583            _ => Err(format!(
584                "`{rest}` does not start with a complete JSON string"
585            )),
586        }
587    }
588}
589
590// ---------------------------------------------------------------------------------------
591// Analysis
592// ---------------------------------------------------------------------------------------
593
594/// What a SPARQL script's text says about itself, read from the parsed query.
595#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
596pub struct Analysis {
597    /// Its form.
598    pub form: Form,
599    /// Its declared parameters, in declaration order.
600    pub parameters: Vec<Parameter>,
601    /// For a query: the graphs it reads that the text fixes. For an update: the one graph
602    /// it writes.
603    pub graphs: BTreeSet<String>,
604    /// Whether it also reads a graph only a run decides (`GRAPH ?g`): the caller's
605    /// readable union.
606    #[serde(rename = "anyGraph")]
607    pub any_graph: bool,
608    /// For an update: whether it reads its graph too (it has a `WHERE`).
609    #[serde(rename = "updateReads")]
610    pub update_reads: bool,
611}
612
613impl Analysis {
614    /// The capability its runs need, DERIVED from the text: never declared by an author.
615    ///
616    /// A query: `urn:cap:store:read:graph:<g>` for each graph it fixes, and the family
617    /// [`CAP_READ_GRAPH`] when it reads the caller's readable union. An update:
618    /// `urn:cap:store:write:graph:<g>` for its graph, and `urn:cap:store:read:graph:<g>`
619    /// when it has a `WHERE` (the store's own rule).
620    pub fn requires(&self) -> BTreeSet<String> {
621        let mut out = BTreeSet::new();
622        for graph in &self.graphs {
623            if self.form == Form::Update {
624                out.insert(cap_write_graph(graph));
625                if self.update_reads {
626                    out.insert(cap_read_graph(graph));
627                }
628            } else {
629                out.insert(cap_read_graph(graph));
630            }
631        }
632        if self.any_graph {
633            out.insert(CAP_READ_GRAPH.to_string());
634        }
635        out
636    }
637}
638
639/// Analyze a SPARQL script: bounded before it is parsed, then parsed (on a stack sized for
640/// it) for its form, parameters, dataset and derived authority. Every refusal is an
641/// `InvalidArgument` on `content` that says what to write instead.
642pub fn analyze(source: &str, door: &SparqlDoor) -> Result<Analysis> {
643    // ★ Bounded before the parser sees a byte (ledger #915): the parser is recursive, and
644    // a stack overflow aborts the whole host, not this request.
645    limits::check_sparql(source, "content")?;
646    let parameters = parameters(source)?;
647    limits::on_sparql_stack(source, || {
648        let parsed = parse(source)?;
649        let shape = shape(&parsed, door)?;
650        check_parameters(&parameters, &shape)?;
651        Ok(Analysis {
652            form: parsed.form(),
653            parameters,
654            graphs: shape.graphs,
655            any_graph: shape.any_graph,
656            update_reads: shape.update_reads,
657        })
658    })
659}
660
661/// The text the store is sent, and the dataset it runs over, after binding.
662#[derive(Clone, Debug, PartialEq, Eq)]
663pub struct Bound {
664    /// Its form.
665    pub form: Form,
666    /// The query or update, serialized from the bound algebra, with no dataset clauses.
667    pub text: String,
668    /// The graphs the bound text fixes (a parameter naming a graph is one of them now).
669    pub graphs: BTreeSet<String>,
670    /// Whether it still reads a graph only the run decides.
671    pub any_graph: bool,
672}
673
674/// Bind `values` into `source` and serialize it for the store.
675///
676/// ★ **Never string interpolation.** The text is parsed, each parameter's variable is
677/// replaced by its term IN THE ALGEBRA (in patterns, paths, expressions and templates; a
678/// projected parameter is bound in the projection, `(term AS ?p)`), and the store is sent
679/// what spargebra serializes from that algebra, which writes a literal as one escaped
680/// token whatever it holds. A value cannot close a string, open a pattern or start a
681/// second operation, because it is never text to a parser. A parameter without a value
682/// (optional, no default) stays an unbound variable.
683pub fn bind(source: &str, door: &SparqlDoor, values: &BTreeMap<String, Value>) -> Result<Bound> {
684    limits::check_sparql(source, "content")?;
685    limits::on_sparql_stack(source, || {
686        let mut parsed = parse(source)?;
687        let subst = Subst { values };
688        match &mut parsed {
689            Parsed::Query(query) => subst.query(query)?,
690            Parsed::Update(update) => subst.update(update)?,
691        }
692        let shape = shape(&parsed, door)?;
693        let text = match &mut parsed {
694            Parsed::Query(query) => {
695                // The store's scoped door refuses dataset clauses: `graph=` IS the dataset,
696                // and it is `shape.graphs`, which the clauses contributed to.
697                *dataset_of(query) = None;
698                query.to_string()
699            }
700            Parsed::Update(update) => update.to_string(),
701        };
702        Ok(Bound {
703            form: parsed.form(),
704            text,
705            graphs: shape.graphs,
706            any_graph: shape.any_graph,
707        })
708    })
709}
710
711enum Parsed {
712    Query(Box<Query>),
713    Update(Update),
714}
715
716impl Parsed {
717    fn form(&self) -> Form {
718        match self {
719            Parsed::Query(query) => match **query {
720                Query::Select { .. } => Form::Select,
721                Query::Ask { .. } => Form::Ask,
722                Query::Construct { .. } => Form::Construct,
723                Query::Describe { .. } => Form::Describe,
724            },
725            Parsed::Update(_) => Form::Update,
726        }
727    }
728}
729
730/// Parse as a query, then as an update. The FORM comes from which one parses: there is no
731/// argument a caller could use to say otherwise.
732fn parse(source: &str) -> Result<Parsed> {
733    let query_error = match SparqlParser::new().parse_query(source) {
734        Ok(query) => return Ok(Parsed::Query(Box::new(query))),
735        Err(e) => e,
736    };
737    match SparqlParser::new().parse_update(source) {
738        Ok(update) => Ok(Parsed::Update(update)),
739        Err(update_error) => Err(invalid(format!(
740            "not a SPARQL query ({query_error}) nor an update ({update_error})"
741        ))),
742    }
743}
744
745fn dataset_of(query: &mut Query) -> &mut Option<QueryDataset> {
746    match query {
747        Query::Select { dataset, .. }
748        | Query::Ask { dataset, .. }
749        | Query::Construct { dataset, .. }
750        | Query::Describe { dataset, .. } => dataset,
751    }
752}
753
754/// What a walk of the algebra found.
755#[derive(Default)]
756struct Scan {
757    /// A triple pattern or path outside every `GRAPH` block.
758    reads_default: bool,
759    /// `GRAPH <iri>`.
760    graphs: BTreeSet<String>,
761    /// `GRAPH ?v`.
762    graph_variables: BTreeSet<String>,
763    /// Every variable the text uses.
764    used: BTreeSet<String>,
765    /// Variables the text BINDS or groups by: `BIND … AS`, `VALUES`, `GROUP BY`, an
766    /// aggregate's name. A parameter cannot be one.
767    bound: BTreeSet<String>,
768    /// Variables in a position only an IRI can fill: a predicate or a graph name.
769    iri_only: BTreeSet<String>,
770    /// A `SERVICE` clause.
771    service: bool,
772}
773
774/// The dataset and the shape a query or update has.
775struct Shape {
776    graphs: BTreeSet<String>,
777    any_graph: bool,
778    update_reads: bool,
779    scan: Scan,
780}
781
782fn shape(parsed: &Parsed, door: &SparqlDoor) -> Result<Shape> {
783    match parsed {
784        Parsed::Query(query) => query_shape(query, door),
785        Parsed::Update(update) => update_shape(update),
786    }
787}
788
789fn query_shape(query: &Query, door: &SparqlDoor) -> Result<Shape> {
790    let mut scan = Scan::default();
791    let (dataset, describe) = match query {
792        Query::Select {
793            dataset, pattern, ..
794        }
795        | Query::Ask {
796            dataset, pattern, ..
797        } => {
798            scan.pattern(pattern, false);
799            (dataset, false)
800        }
801        Query::Construct {
802            template,
803            dataset,
804            pattern,
805            ..
806        } => {
807            for triple in template {
808                scan.triple(triple, true);
809            }
810            scan.pattern(pattern, false);
811            (dataset, false)
812        }
813        Query::Describe {
814            dataset, pattern, ..
815        } => {
816            scan.pattern(pattern, false);
817            (dataset, true)
818        }
819    };
820    if scan.service {
821        return Err(invalid(
822            "a script cannot use SERVICE: the store refuses federation, and a run's authority \
823             is derived from graphs this store holds",
824        ));
825    }
826    let from: BTreeSet<String> = dataset
827        .iter()
828        .flat_map(|d| d.default.iter())
829        .map(|g| g.as_str().to_string())
830        .collect();
831    let from_named: BTreeSet<String> = dataset
832        .iter()
833        .flat_map(|d| d.named.iter().flatten())
834        .map(|g| g.as_str().to_string())
835        .collect();
836    let any_graph = !scan.graph_variables.is_empty();
837    // ⚠ DESCRIBE collects from the DEFAULT graph (spareval's DescribeIterator), so it
838    // reads the default graph whatever its pattern says.
839    let reads_default = scan.reads_default || describe;
840    let graphs = if reads_default {
841        let base = if from.is_empty() {
842            door.defaults().clone()
843        } else {
844            from.clone()
845        };
846        if base.is_empty() {
847            return Err(invalid(
848                "this query reads the default graph (a pattern outside every GRAPH block, or \
849                 a DESCRIBE) but names no graph with FROM, and this host gives no default \
850                 dataset. Name the graphs it reads with FROM <…>, or put its patterns in \
851                 GRAPH <…> blocks",
852            ));
853        }
854        let extra: Vec<&String> = from_named
855            .iter()
856            .chain(scan.graphs.iter())
857            .filter(|g| !base.contains(*g))
858            .collect();
859        if !extra.is_empty() {
860            return Err(invalid(format!(
861                "this query reads the default graph, which a script's dataset makes the merge \
862                 of EVERY graph it names; it also names {} outside its FROM set ({}), so its \
863                 bare patterns would read them too. Add them to FROM, or put the bare \
864                 patterns in GRAPH blocks",
865                extra
866                    .iter()
867                    .map(|g| format!("<{g}>"))
868                    .collect::<Vec<_>>()
869                    .join(", "),
870                base.iter()
871                    .map(|g| format!("<{g}>"))
872                    .collect::<Vec<_>>()
873                    .join(", ")
874            )));
875        }
876        if any_graph {
877            return Err(invalid(
878                "this query reads the default graph and also has a GRAPH ?variable, which \
879                 runs over the caller's readable union: in a script's one dataset the bare \
880                 patterns would read that whole union too. Put the bare patterns in GRAPH \
881                 blocks, or fix the variable graph with GRAPH <…>",
882            ));
883        }
884        base
885    } else {
886        let mut all = from;
887        all.extend(from_named);
888        all.extend(scan.graphs.iter().cloned());
889        if all.is_empty() && !any_graph {
890            return Err(invalid(
891                "this query names no graph (no FROM, FROM NAMED or GRAPH), so it reads \
892                 nothing; name the graphs it reads",
893            ));
894        }
895        all
896    };
897    Ok(Shape {
898        graphs,
899        any_graph,
900        update_reads: false,
901        scan,
902    })
903}
904
905fn update_shape(update: &Update) -> Result<Shape> {
906    let mut scan = Scan::default();
907    let mut targets: BTreeSet<String> = BTreeSet::new();
908    let mut reads: BTreeSet<String> = BTreeSet::new();
909    let mut has_where = false;
910    let unnamed_target = || {
911        invalid(
912            "an update script names the graph it writes: CLEAR GRAPH <…> or DROP GRAPH <…>, \
913             not DEFAULT, NAMED or ALL",
914        )
915    };
916    for operation in &update.operations {
917        match operation {
918            GraphUpdateOperation::InsertData { data } => {
919                for quad in data {
920                    match &quad.graph_name {
921                        GraphName::NamedNode(g) => targets.insert(g.as_str().to_string()),
922                        GraphName::DefaultGraph => return Err(writes_default()),
923                    };
924                }
925            }
926            GraphUpdateOperation::DeleteData { data } => {
927                for quad in data {
928                    match &quad.graph_name {
929                        GraphName::NamedNode(g) => targets.insert(g.as_str().to_string()),
930                        GraphName::DefaultGraph => return Err(writes_default()),
931                    };
932                }
933            }
934            GraphUpdateOperation::DeleteInsert {
935                delete,
936                insert,
937                using,
938                pattern,
939            } => {
940                has_where = true;
941                for quad in delete {
942                    scan.ground_quad(quad);
943                    template_target(&quad.graph_name, &mut targets)?;
944                }
945                for quad in insert {
946                    scan.quad(quad);
947                    template_target(&quad.graph_name, &mut targets)?;
948                }
949                let mut inner = Scan::default();
950                inner.pattern(pattern, false);
951                let using_default: Vec<String> = using
952                    .iter()
953                    .flat_map(|d| d.default.iter())
954                    .map(|g| g.as_str().to_string())
955                    .collect();
956                if inner.reads_default && using_default.is_empty() {
957                    return Err(invalid(
958                        "this update's WHERE reads the default graph, which the store's scoped \
959                         door does not hold: match inside GRAPH <…>, or name the graph with \
960                         USING <…> or WITH <…>",
961                    ));
962                }
963                if !inner.graph_variables.is_empty() {
964                    return Err(invalid(
965                        "an update script's WHERE cannot use GRAPH ?variable: the graph it reads \
966                         is part of its authority, so the text names it",
967                    ));
968                }
969                reads.extend(using_default);
970                reads.extend(
971                    using
972                        .iter()
973                        .flat_map(|d| d.named.iter().flatten())
974                        .map(|g| g.as_str().to_string()),
975                );
976                reads.extend(inner.graphs.iter().cloned());
977                scan.absorb(inner);
978            }
979            GraphUpdateOperation::Load { .. } => {
980                return Err(invalid(
981                    "a script cannot LOAD: the store fetches nothing over the network, and \
982                     an update script writes only what its text says",
983                ))
984            }
985            GraphUpdateOperation::Clear { graph, .. }
986            | GraphUpdateOperation::Drop { graph, .. } => match graph {
987                GraphTarget::NamedNode(g) => {
988                    targets.insert(g.as_str().to_string());
989                }
990                _ => return Err(unnamed_target()),
991            },
992            GraphUpdateOperation::Create { graph, .. } => {
993                targets.insert(graph.as_str().to_string());
994            }
995        }
996    }
997    if scan.service {
998        return Err(invalid(
999            "a script cannot use SERVICE: the store refuses federation",
1000        ));
1001    }
1002    let mut all = targets.clone();
1003    all.extend(reads);
1004    if all.len() != 1 || targets.len() != 1 {
1005        return Err(invalid(format!(
1006            "an update script writes exactly one named graph and reads no other (the store's \
1007             scoped door confines it to one); this one names {}",
1008            if all.is_empty() {
1009                "none".to_string()
1010            } else {
1011                all.iter()
1012                    .map(|g| format!("<{g}>"))
1013                    .collect::<Vec<_>>()
1014                    .join(", ")
1015            }
1016        )));
1017    }
1018    Ok(Shape {
1019        graphs: targets,
1020        any_graph: false,
1021        update_reads: has_where,
1022        scan,
1023    })
1024}
1025
1026/// The graph a DELETE or INSERT template writes into, recorded.
1027fn template_target(graph: &GraphNamePattern, targets: &mut BTreeSet<String>) -> Result<()> {
1028    match graph {
1029        GraphNamePattern::NamedNode(g) => {
1030            targets.insert(g.as_str().to_string());
1031            Ok(())
1032        }
1033        GraphNamePattern::DefaultGraph => Err(writes_default()),
1034        GraphNamePattern::Variable(_) => Err(invalid(
1035            "an update script cannot write GRAPH ?variable: the graph it writes is its \
1036             authority, so the text names it",
1037        )),
1038    }
1039}
1040
1041fn writes_default() -> Error {
1042    invalid(
1043        "this update writes the default graph, which a script cannot: the store's scoped \
1044         door confines an update to one named graph. Write inside GRAPH <…> (or use \
1045         WITH <…>)",
1046    )
1047}
1048
1049/// Every declared parameter is used, can be a value where it is used, and binds nothing.
1050fn check_parameters(parameters: &[Parameter], shape: &Shape) -> Result<()> {
1051    let scan = &shape.scan;
1052    for p in parameters {
1053        if !scan.used.contains(&p.name) {
1054            return Err(invalid(format!(
1055                "parameter `{}` is declared but the query never uses ?{0}",
1056                p.name
1057            )));
1058        }
1059        if scan.bound.contains(&p.name) {
1060            return Err(invalid(format!(
1061                "parameter `{}` is a value, but the query binds ?{0} itself (BIND … AS, \
1062                 VALUES, GROUP BY or an aggregate's name); rename one of them",
1063                p.name
1064            )));
1065        }
1066        if scan.iri_only.contains(&p.name) && !matches!(p.kind, ParamType::Class(_)) {
1067            return Err(invalid(format!(
1068                "parameter `{}` is used as a predicate or a graph name, which only an IRI can \
1069                 fill; declare it with a class (`rdfs:Resource` for any IRI)",
1070                p.name
1071            )));
1072        }
1073    }
1074    Ok(())
1075}
1076
1077impl Scan {
1078    fn absorb(&mut self, other: Scan) {
1079        self.reads_default |= other.reads_default;
1080        self.graphs.extend(other.graphs);
1081        self.graph_variables.extend(other.graph_variables);
1082        self.used.extend(other.used);
1083        self.bound.extend(other.bound);
1084        self.iri_only.extend(other.iri_only);
1085        self.service |= other.service;
1086    }
1087
1088    fn var(&mut self, v: &Variable) {
1089        self.used.insert(v.as_str().to_string());
1090    }
1091
1092    fn term(&mut self, t: &TermPattern) {
1093        // A variable inside a quoted triple (RDF 1.2, when a host's build enables it) is
1094        // not seen, so a parameter used only there is refused as unused: the safe side.
1095        if let TermPattern::Variable(v) = t {
1096            self.var(v);
1097        }
1098    }
1099
1100    fn named(&mut self, n: &NamedNodePattern) {
1101        if let NamedNodePattern::Variable(v) = n {
1102            self.var(v);
1103            self.iri_only.insert(v.as_str().to_string());
1104        }
1105    }
1106
1107    fn triple(&mut self, t: &TriplePattern, in_graph: bool) {
1108        if !in_graph {
1109            self.reads_default = true;
1110        }
1111        self.term(&t.subject);
1112        self.named(&t.predicate);
1113        self.term(&t.object);
1114    }
1115
1116    fn quad(&mut self, q: &QuadPattern) {
1117        self.term(&q.subject);
1118        self.named(&q.predicate);
1119        self.term(&q.object);
1120        if let GraphNamePattern::Variable(v) = &q.graph_name {
1121            self.var(v);
1122        }
1123    }
1124
1125    fn ground_quad(&mut self, q: &GroundQuadPattern) {
1126        if let GroundTermPattern::Variable(v) = &q.subject {
1127            self.var(v);
1128        }
1129        self.named(&q.predicate);
1130        if let GroundTermPattern::Variable(v) = &q.object {
1131            self.var(v);
1132        }
1133        if let GraphNamePattern::Variable(v) = &q.graph_name {
1134            self.var(v);
1135        }
1136    }
1137
1138    fn pattern(&mut self, p: &GraphPattern, in_graph: bool) {
1139        match p {
1140            GraphPattern::Bgp { patterns } => {
1141                for t in patterns {
1142                    self.triple(t, in_graph);
1143                }
1144            }
1145            GraphPattern::Path {
1146                subject, object, ..
1147            } => {
1148                if !in_graph {
1149                    self.reads_default = true;
1150                }
1151                self.term(subject);
1152                self.term(object);
1153            }
1154            GraphPattern::Join { left, right }
1155            | GraphPattern::Union { left, right }
1156            | GraphPattern::Minus { left, right } => {
1157                self.pattern(left, in_graph);
1158                self.pattern(right, in_graph);
1159            }
1160            GraphPattern::LeftJoin {
1161                left,
1162                right,
1163                expression,
1164            } => {
1165                self.pattern(left, in_graph);
1166                self.pattern(right, in_graph);
1167                if let Some(e) = expression {
1168                    self.expression(e, in_graph);
1169                }
1170            }
1171            GraphPattern::Filter { expr, inner } => {
1172                self.expression(expr, in_graph);
1173                self.pattern(inner, in_graph);
1174            }
1175            GraphPattern::Graph { name, inner } => {
1176                match name {
1177                    NamedNodePattern::NamedNode(g) => {
1178                        self.graphs.insert(g.as_str().to_string());
1179                    }
1180                    NamedNodePattern::Variable(v) => {
1181                        self.graph_variables.insert(v.as_str().to_string());
1182                    }
1183                }
1184                self.named(name);
1185                self.pattern(inner, true);
1186            }
1187            GraphPattern::Extend {
1188                inner,
1189                variable,
1190                expression,
1191            } => {
1192                self.bound.insert(variable.as_str().to_string());
1193                self.var(variable);
1194                self.expression(expression, in_graph);
1195                self.pattern(inner, in_graph);
1196            }
1197            GraphPattern::Values { variables, .. } => {
1198                for v in variables {
1199                    self.var(v);
1200                    self.bound.insert(v.as_str().to_string());
1201                }
1202            }
1203            GraphPattern::OrderBy { inner, expression } => {
1204                for e in expression {
1205                    match e {
1206                        OrderExpression::Asc(e) | OrderExpression::Desc(e) => {
1207                            self.expression(e, in_graph)
1208                        }
1209                    }
1210                }
1211                self.pattern(inner, in_graph);
1212            }
1213            GraphPattern::Project { inner, variables } => {
1214                for v in variables {
1215                    self.var(v);
1216                }
1217                self.pattern(inner, in_graph);
1218            }
1219            GraphPattern::Distinct { inner }
1220            | GraphPattern::Reduced { inner }
1221            | GraphPattern::Slice { inner, .. } => self.pattern(inner, in_graph),
1222            GraphPattern::Group {
1223                inner,
1224                variables,
1225                aggregates,
1226            } => {
1227                for v in variables {
1228                    self.var(v);
1229                    self.bound.insert(v.as_str().to_string());
1230                }
1231                for (v, aggregate) in aggregates {
1232                    self.bound.insert(v.as_str().to_string());
1233                    if let AggregateExpression::FunctionCall { expr, .. } = aggregate {
1234                        self.expression(expr, in_graph);
1235                    }
1236                }
1237                self.pattern(inner, in_graph);
1238            }
1239            GraphPattern::Service { inner, .. } => {
1240                self.service = true;
1241                self.pattern(inner, in_graph);
1242            }
1243            // LATERAL (SEP-0006), when a host's build enables it through feature
1244            // unification: not something a script can use yet, so it is refused as SERVICE
1245            // is, rather than walked by a rule written without it.
1246            #[allow(unreachable_patterns)] // feature unification can add variants
1247            _ => self.service = true,
1248        }
1249    }
1250
1251    fn expression(&mut self, e: &Expression, in_graph: bool) {
1252        match e {
1253            Expression::Variable(v) | Expression::Bound(v) => self.var(v),
1254            Expression::Exists(p) => self.pattern(p, in_graph),
1255            Expression::Or(a, b)
1256            | Expression::And(a, b)
1257            | Expression::Equal(a, b)
1258            | Expression::SameTerm(a, b)
1259            | Expression::Greater(a, b)
1260            | Expression::GreaterOrEqual(a, b)
1261            | Expression::Less(a, b)
1262            | Expression::LessOrEqual(a, b)
1263            | Expression::Add(a, b)
1264            | Expression::Subtract(a, b)
1265            | Expression::Multiply(a, b)
1266            | Expression::Divide(a, b) => {
1267                self.expression(a, in_graph);
1268                self.expression(b, in_graph);
1269            }
1270            Expression::In(a, list) => {
1271                self.expression(a, in_graph);
1272                for x in list {
1273                    self.expression(x, in_graph);
1274                }
1275            }
1276            Expression::UnaryPlus(a) | Expression::UnaryMinus(a) | Expression::Not(a) => {
1277                self.expression(a, in_graph)
1278            }
1279            Expression::If(a, b, c) => {
1280                self.expression(a, in_graph);
1281                self.expression(b, in_graph);
1282                self.expression(c, in_graph);
1283            }
1284            Expression::Coalesce(list) | Expression::FunctionCall(_, list) => {
1285                for x in list {
1286                    self.expression(x, in_graph);
1287                }
1288            }
1289            Expression::NamedNode(_) | Expression::Literal(_) => {}
1290        }
1291    }
1292}
1293
1294// ---------------------------------------------------------------------------------------
1295// Substitution
1296// ---------------------------------------------------------------------------------------
1297
1298struct Subst<'a> {
1299    values: &'a BTreeMap<String, Value>,
1300}
1301
1302impl Subst<'_> {
1303    fn value(&self, v: &Variable) -> Option<&Value> {
1304        self.values.get(v.as_str())
1305    }
1306
1307    fn query(&self, query: &mut Query) -> Result<()> {
1308        match query {
1309            Query::Select { pattern, .. }
1310            | Query::Ask { pattern, .. }
1311            | Query::Describe { pattern, .. } => {
1312                self.pattern(pattern)?;
1313            }
1314            Query::Construct {
1315                template, pattern, ..
1316            } => {
1317                for triple in template {
1318                    self.triple(triple)?;
1319                }
1320                self.pattern(pattern)?;
1321            }
1322        }
1323        Ok(())
1324    }
1325
1326    fn update(&self, update: &mut Update) -> Result<()> {
1327        for operation in &mut update.operations {
1328            if let GraphUpdateOperation::DeleteInsert {
1329                delete,
1330                insert,
1331                pattern,
1332                ..
1333            } = operation
1334            {
1335                for quad in delete {
1336                    quad.subject = self.ground_term(quad.subject.clone());
1337                    quad.predicate = self.named(quad.predicate.clone())?;
1338                    quad.object = self.ground_term(quad.object.clone());
1339                }
1340                for quad in insert {
1341                    quad.subject = self.term(quad.subject.clone());
1342                    quad.predicate = self.named(quad.predicate.clone())?;
1343                    quad.object = self.term(quad.object.clone());
1344                }
1345                self.pattern(pattern)?;
1346            }
1347        }
1348        Ok(())
1349    }
1350
1351    fn term(&self, t: TermPattern) -> TermPattern {
1352        match &t {
1353            TermPattern::Variable(v) => match self.value(v) {
1354                Some(Value::Iri(n)) => TermPattern::NamedNode(n.clone()),
1355                Some(Value::Literal(l)) => TermPattern::Literal(l.clone()),
1356                None => t,
1357            },
1358            _ => t,
1359        }
1360    }
1361
1362    fn ground_term(&self, t: GroundTermPattern) -> GroundTermPattern {
1363        match &t {
1364            GroundTermPattern::Variable(v) => match self.value(v) {
1365                Some(Value::Iri(n)) => GroundTermPattern::NamedNode(n.clone()),
1366                Some(Value::Literal(l)) => GroundTermPattern::Literal(l.clone()),
1367                None => t,
1368            },
1369            _ => t,
1370        }
1371    }
1372
1373    fn named(&self, n: NamedNodePattern) -> Result<NamedNodePattern> {
1374        match &n {
1375            NamedNodePattern::Variable(v) => match self.value(v) {
1376                Some(Value::Iri(iri)) => Ok(NamedNodePattern::NamedNode(iri.clone())),
1377                // `check_parameters` refuses a datatype parameter in an IRI-only position
1378                // at publish, so this is reached only by a hand-edited record.
1379                Some(Value::Literal(_)) => Err(Error::InvalidArgument {
1380                    name: v.as_str().to_string(),
1381                    detail: "is used as a predicate or a graph name, which only an IRI can fill"
1382                        .to_string(),
1383                }),
1384                None => Ok(n),
1385            },
1386            NamedNodePattern::NamedNode(_) => Ok(n),
1387        }
1388    }
1389
1390    fn triple(&self, t: &mut TriplePattern) -> Result<()> {
1391        t.subject = self.term(t.subject.clone());
1392        t.predicate = self.named(t.predicate.clone())?;
1393        t.object = self.term(t.object.clone());
1394        Ok(())
1395    }
1396
1397    fn constant(&self, v: &Variable) -> Option<Expression> {
1398        self.value(v).map(|value| match value {
1399            Value::Iri(n) => Expression::NamedNode(n.clone()),
1400            Value::Literal(l) => Expression::Literal(l.clone()),
1401        })
1402    }
1403
1404    /// Substitute inside `p`; answers the parameters `p` now BINDS for whatever encloses
1405    /// it (those a sub-select projects), so a projection above does not bind them twice.
1406    fn pattern(&self, p: &mut GraphPattern) -> Result<BTreeSet<String>> {
1407        Ok(match p {
1408            GraphPattern::Bgp { patterns } => {
1409                for t in patterns {
1410                    self.triple(t)?;
1411                }
1412                BTreeSet::new()
1413            }
1414            GraphPattern::Path {
1415                subject, object, ..
1416            } => {
1417                *subject = self.term(subject.clone());
1418                *object = self.term(object.clone());
1419                BTreeSet::new()
1420            }
1421            GraphPattern::Join { left, right } | GraphPattern::Union { left, right } => {
1422                let mut out = self.pattern(left)?;
1423                out.extend(self.pattern(right)?);
1424                out
1425            }
1426            GraphPattern::Minus { left, right } => {
1427                self.pattern(right)?;
1428                self.pattern(left)?
1429            }
1430            GraphPattern::LeftJoin {
1431                left,
1432                right,
1433                expression,
1434            } => {
1435                let mut out = self.pattern(left)?;
1436                out.extend(self.pattern(right)?);
1437                if let Some(e) = expression {
1438                    self.expression(e)?;
1439                }
1440                out
1441            }
1442            GraphPattern::Filter { expr, inner } => {
1443                self.expression(expr)?;
1444                self.pattern(inner)?
1445            }
1446            GraphPattern::Graph { name, inner } => {
1447                *name = self.named(name.clone())?;
1448                self.pattern(inner)?
1449            }
1450            GraphPattern::Extend {
1451                inner, expression, ..
1452            } => {
1453                self.expression(expression)?;
1454                self.pattern(inner)?
1455            }
1456            GraphPattern::Values { .. } => BTreeSet::new(),
1457            GraphPattern::OrderBy { inner, expression } => {
1458                for e in expression {
1459                    match e {
1460                        OrderExpression::Asc(e) | OrderExpression::Desc(e) => self.expression(e)?,
1461                    }
1462                }
1463                self.pattern(inner)?
1464            }
1465            GraphPattern::Project { inner, variables } => {
1466                let exposed = self.pattern(inner)?;
1467                // A projected parameter is bound IN the projection, `(term AS ?p)`, so the
1468                // answer still has its column: the variable is gone from the pattern now.
1469                for v in variables.iter() {
1470                    if exposed.contains(v.as_str()) {
1471                        continue;
1472                    }
1473                    if let Some(constant) = self.constant(v) {
1474                        let taken = std::mem::replace(
1475                            &mut **inner,
1476                            GraphPattern::Bgp {
1477                                patterns: Vec::new(),
1478                            },
1479                        );
1480                        **inner = GraphPattern::Extend {
1481                            inner: Box::new(taken),
1482                            variable: v.clone(),
1483                            expression: constant,
1484                        };
1485                    }
1486                }
1487                variables
1488                    .iter()
1489                    .filter(|v| self.value(v).is_some())
1490                    .map(|v| v.as_str().to_string())
1491                    .collect()
1492            }
1493            GraphPattern::Distinct { inner }
1494            | GraphPattern::Reduced { inner }
1495            | GraphPattern::Slice { inner, .. } => self.pattern(inner)?,
1496            GraphPattern::Group {
1497                inner, aggregates, ..
1498            } => {
1499                for (_, aggregate) in aggregates {
1500                    if let AggregateExpression::FunctionCall { expr, .. } = aggregate {
1501                        self.expression(expr)?;
1502                    }
1503                }
1504                self.pattern(inner)?;
1505                BTreeSet::new()
1506            }
1507            GraphPattern::Service { .. } => BTreeSet::new(),
1508            #[allow(unreachable_patterns)] // feature unification can add variants
1509            _ => BTreeSet::new(),
1510        })
1511    }
1512
1513    fn expression(&self, e: &mut Expression) -> Result<()> {
1514        match e {
1515            Expression::Variable(v) => {
1516                if let Some(constant) = self.constant(v) {
1517                    *e = constant;
1518                }
1519            }
1520            Expression::Bound(v) => {
1521                if self.value(v).is_some() {
1522                    *e = Expression::Literal(Literal::from(true));
1523                }
1524            }
1525            Expression::Exists(p) => {
1526                self.pattern(p)?;
1527            }
1528            Expression::Or(a, b)
1529            | Expression::And(a, b)
1530            | Expression::Equal(a, b)
1531            | Expression::SameTerm(a, b)
1532            | Expression::Greater(a, b)
1533            | Expression::GreaterOrEqual(a, b)
1534            | Expression::Less(a, b)
1535            | Expression::LessOrEqual(a, b)
1536            | Expression::Add(a, b)
1537            | Expression::Subtract(a, b)
1538            | Expression::Multiply(a, b)
1539            | Expression::Divide(a, b) => {
1540                self.expression(a)?;
1541                self.expression(b)?;
1542            }
1543            Expression::In(a, list) => {
1544                self.expression(a)?;
1545                for x in list {
1546                    self.expression(x)?;
1547                }
1548            }
1549            Expression::UnaryPlus(a) | Expression::UnaryMinus(a) | Expression::Not(a) => {
1550                self.expression(a)?
1551            }
1552            Expression::If(a, b, c) => {
1553                self.expression(a)?;
1554                self.expression(b)?;
1555                self.expression(c)?;
1556            }
1557            Expression::Coalesce(list) | Expression::FunctionCall(_, list) => {
1558                for x in list {
1559                    self.expression(x)?;
1560                }
1561            }
1562            Expression::NamedNode(_) | Expression::Literal(_) => {}
1563        }
1564        Ok(())
1565    }
1566}
1567
1568#[cfg(test)]
1569mod tests {
1570    use super::*;
1571
1572    fn door() -> SparqlDoor {
1573        SparqlDoor::store()
1574    }
1575
1576    fn values(pairs: &[(&str, Value)]) -> BTreeMap<String, Value> {
1577        pairs
1578            .iter()
1579            .map(|(k, v)| (k.to_string(), v.clone()))
1580            .collect()
1581    }
1582
1583    fn int(n: i64) -> Value {
1584        Value::Literal(Literal::from(n))
1585    }
1586
1587    #[test]
1588    fn the_form_is_read_from_the_text() {
1589        let cases = [
1590            (
1591                "SELECT * WHERE { GRAPH <urn:g> { ?s ?p ?o } }",
1592                Form::Select,
1593            ),
1594            ("ASK { GRAPH <urn:g> { ?s ?p ?o } }", Form::Ask),
1595            (
1596                "CONSTRUCT { ?s ?p ?o } WHERE { GRAPH <urn:g> { ?s ?p ?o } }",
1597                Form::Construct,
1598            ),
1599            ("DESCRIBE <urn:x> FROM <urn:g>", Form::Describe),
1600            (
1601                "INSERT DATA { GRAPH <urn:g> { <urn:a> <urn:b> 1 } }",
1602                Form::Update,
1603            ),
1604        ];
1605        for (text, form) in cases {
1606            assert_eq!(analyze(text, &door()).unwrap().form, form, "{text}");
1607        }
1608    }
1609
1610    #[test]
1611    fn graphs_come_from_from_from_named_and_graph() {
1612        let a = analyze(
1613            "SELECT * FROM NAMED <urn:b> WHERE { GRAPH <urn:a> { ?s ?p ?o } GRAPH <urn:b> { ?s ?q ?r } }",
1614            &door(),
1615        )
1616        .unwrap();
1617        assert_eq!(
1618            a.graphs.iter().cloned().collect::<Vec<_>>(),
1619            vec!["urn:a", "urn:b"]
1620        );
1621        assert!(!a.any_graph);
1622        let a = analyze("SELECT ?g WHERE { GRAPH ?g { ?s ?p ?o } }", &door()).unwrap();
1623        assert!(a.any_graph && a.graphs.is_empty());
1624        assert_eq!(
1625            a.requires().into_iter().collect::<Vec<_>>(),
1626            vec![CAP_READ_GRAPH.to_string()]
1627        );
1628    }
1629
1630    #[test]
1631    fn a_query_whose_answer_would_change_is_refused() {
1632        // Bare patterns with a graph outside FROM.
1633        let e = analyze(
1634            "SELECT * FROM <urn:a> WHERE { ?s ?p ?o GRAPH <urn:b> { ?s ?q ?r } }",
1635            &door(),
1636        )
1637        .unwrap_err();
1638        assert!(e.to_string().contains("<urn:b>"), "{e}");
1639        // Bare patterns and no FROM, no host default.
1640        assert!(analyze("SELECT * WHERE { ?s ?p ?o }", &door()).is_err());
1641        // ... with a host default, fine.
1642        let a = analyze(
1643            "SELECT * WHERE { ?s ?p ?o }",
1644            &door().default_graphs(["urn:d"]),
1645        )
1646        .unwrap();
1647        assert_eq!(a.graphs.into_iter().collect::<Vec<_>>(), vec!["urn:d"]);
1648        // Bare patterns beside GRAPH ?g.
1649        assert!(analyze(
1650            "SELECT * FROM <urn:a> WHERE { ?s ?p ?o GRAPH ?g { ?s ?q ?r } }",
1651            &door()
1652        )
1653        .is_err());
1654        // SERVICE.
1655        assert!(analyze(
1656            "SELECT * WHERE { SERVICE <http://example.com/sparql> { ?s ?p ?o } }",
1657            &door()
1658        )
1659        .is_err());
1660    }
1661
1662    #[test]
1663    fn an_update_writes_one_named_graph() {
1664        let a = analyze(
1665            "DELETE { GRAPH <urn:g> { ?s <urn:p> ?o } } WHERE { GRAPH <urn:g> { ?s <urn:p> ?o } }",
1666            &door(),
1667        )
1668        .unwrap();
1669        assert_eq!(a.form, Form::Update);
1670        assert!(a.update_reads);
1671        assert_eq!(
1672            a.requires().into_iter().collect::<Vec<_>>(),
1673            vec![
1674                "urn:cap:store:read:graph:urn:g".to_string(),
1675                "urn:cap:store:write:graph:urn:g".to_string()
1676            ]
1677        );
1678        let a = analyze(
1679            "INSERT DATA { GRAPH <urn:g> { <urn:a> <urn:b> 1 } }",
1680            &door(),
1681        )
1682        .unwrap();
1683        assert!(!a.update_reads);
1684        let a = analyze(
1685            "WITH <urn:g> DELETE { ?s <urn:p> ?o } WHERE { ?s <urn:p> ?o }",
1686            &door(),
1687        )
1688        .unwrap();
1689        assert_eq!(a.graphs.into_iter().collect::<Vec<_>>(), vec!["urn:g"]);
1690        for refused in [
1691            "INSERT DATA { <urn:a> <urn:b> 1 }",
1692            "INSERT DATA { GRAPH <urn:g> { <urn:a> <urn:b> 1 } GRAPH <urn:h> { <urn:a> <urn:b> 1 } }",
1693            "DROP ALL",
1694            "COPY <urn:a> TO <urn:b>",
1695            "LOAD <http://example.com/x.ttl> INTO GRAPH <urn:g>",
1696            "DELETE { GRAPH ?g { ?s ?p ?o } } WHERE { GRAPH ?g { ?s ?p ?o } }",
1697        ] {
1698            assert!(analyze(refused, &door()).is_err(), "{refused}");
1699        }
1700    }
1701
1702    #[test]
1703    fn parameters_are_declared_in_the_header_and_checked() {
1704        let text = "# @param days xsd:integer default 7 -- how long\n\
1705                    # @param who <urn:class:Person>\n\
1706                    # @param note xsd:string default \"two words\"\n\
1707                    SELECT * WHERE { GRAPH <urn:g> { ?who <urn:p> ?d FILTER(?d > ?days && ?note != \"\") } }";
1708        let a = analyze(text, &door()).unwrap();
1709        let names: Vec<_> = a.parameters.iter().map(|p| p.name.as_str()).collect();
1710        assert_eq!(names, vec!["days", "who", "note"]);
1711        assert_eq!(a.parameters[0].summary.as_deref(), Some("how long"));
1712        assert!(a.parameters[1].required);
1713        assert_eq!(a.parameters[2].default.as_deref(), Some("two words"));
1714
1715        for (bad, why) in [
1716            ("# @param x xsd:integer\nSELECT * WHERE { GRAPH <urn:g> { ?s ?p ?o } }", "unused"),
1717            ("# @param x xsd:nope\nSELECT * WHERE { GRAPH <urn:g> { ?s ?p ?x } }", "type"),
1718            ("# @param x xsd:integer default seven\nSELECT * WHERE { GRAPH <urn:g> { ?s ?p ?x } }", "default"),
1719            ("# @param as xsd:integer\nSELECT * WHERE { GRAPH <urn:g> { ?s ?p ?as } }", "reserved"),
1720            ("# @param p xsd:string\nSELECT * WHERE { GRAPH <urn:g> { ?s ?p ?o } }", "predicate"),
1721            ("# @param x xsd:integer\nSELECT * WHERE { GRAPH <urn:g> { ?s ?p ?o } BIND(1 AS ?x) }", "bound"),
1722            ("SELECT * WHERE { GRAPH <urn:g> { ?s ?p ?x } }\n# @param x xsd:integer", "misplaced"),
1723        ] {
1724            assert!(analyze(bad, &door()).is_err(), "{why}: {bad}");
1725        }
1726    }
1727
1728    #[test]
1729    fn binding_substitutes_terms_into_the_algebra() {
1730        let text = "# @param days xsd:integer\n\
1731                    SELECT ?s ?days FROM <urn:g> WHERE { ?s <urn:age> ?a FILTER(?a > ?days) }";
1732        let bound = bind(text, &door(), &values(&[("days", int(7))])).unwrap();
1733        assert!(!bound.text.contains("FROM"), "{}", bound.text);
1734        assert!(
1735            !bound.text.contains("?days >") && !bound.text.contains("> ?days"),
1736            "{}",
1737            bound.text
1738        );
1739        assert!(bound.text.contains("AS ?days"), "{}", bound.text);
1740        assert_eq!(
1741            bound.graphs.iter().cloned().collect::<Vec<_>>(),
1742            vec!["urn:g"]
1743        );
1744        // And the text it serialized parses back.
1745        SparqlParser::new().parse_query(&bound.text).unwrap();
1746    }
1747
1748    #[test]
1749    fn a_hostile_string_is_one_literal() {
1750        let text = "# @param q xsd:string\n\
1751                    SELECT ?v WHERE { GRAPH <urn:g> { ?s ?p ?o } BIND(?q AS ?v) }";
1752        let hostile = "\" } ; DROP ALL ; INSERT DATA { <urn:x> <urn:y> \"";
1753        let p = parameters(text).unwrap().remove(0);
1754        let bound = bind(text, &door(), &values(&[("q", p.value(hostile).unwrap())])).unwrap();
1755        let reparsed = SparqlParser::new().parse_query(&bound.text).unwrap();
1756        // Still one SELECT, and the literal is intact inside it.
1757        assert!(matches!(reparsed, Query::Select { .. }), "{}", bound.text);
1758        assert!(
1759            bound.text.contains("DROP ALL"),
1760            "the value is there, as a literal"
1761        );
1762        assert!(SparqlParser::new().parse_update(&bound.text).is_err());
1763    }
1764
1765    #[test]
1766    fn a_graph_parameter_becomes_a_fixed_graph() {
1767        let text = "# @param graph rdfs:Resource\nSELECT * WHERE { GRAPH ?graph { ?s ?p ?o } }";
1768        let a = analyze(text, &door()).unwrap();
1769        assert!(a.any_graph);
1770        let bound = bind(
1771            text,
1772            &door(),
1773            &values(&[("graph", Value::Iri(NamedNode::new("urn:g").unwrap()))]),
1774        )
1775        .unwrap();
1776        assert!(!bound.any_graph);
1777        assert_eq!(bound.graphs.into_iter().collect::<Vec<_>>(), vec!["urn:g"]);
1778    }
1779}