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