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