1use 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
97pub const CAP_READ_GRAPH: &str = "urn:cap:store:read:graph:*";
100pub const READ_GRAPH_PREFIX: &str = "urn:cap:store:read:graph:";
102pub const WRITE_GRAPH_PREFIX: &str = "urn:cap:store:write:graph:";
104pub const CAP_STORE_READ: &str = "urn:cap:store:read";
107
108pub fn cap_read_graph(iri: &str) -> String {
110 format!("{READ_GRAPH_PREFIX}{iri}")
111}
112
113pub 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
121pub 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
139pub const RESERVED_PARAMETERS: [&str; 7] = [
143 "as",
144 "name",
145 "content",
146 "received",
147 "client",
148 "principal",
149 "content-type",
150];
151
152pub const RESULT_FACES: [&str; 4] = [
154 "application/sparql-results+json",
155 "application/sparql-results+xml",
156 "text/csv",
157 "text/tab-separated-values",
158];
159pub const GRAPH_FACES: [&str; 2] = ["text/turtle", "application/n-triples"];
161
162#[derive(Clone, Debug, PartialEq, Eq)]
181#[non_exhaustive]
182pub struct SparqlDoor {
183 prefix: String,
184 default_graphs: BTreeSet<String>,
185}
186
187impl SparqlDoor {
188 pub fn store() -> SparqlDoor {
190 SparqlDoor::store_at("urn:iki:store:")
191 }
192
193 pub fn store_at(prefix: impl Into<String>) -> SparqlDoor {
196 SparqlDoor {
197 prefix: prefix.into(),
198 default_graphs: BTreeSet::new(),
199 }
200 }
201
202 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 pub fn query_iri(&self, form: Form) -> String {
217 format!("{}graph-{}", self.prefix, form.as_str())
218 }
219
220 pub fn graphs_iri(&self) -> String {
222 format!("{}graphs", self.prefix)
223 }
224
225 pub fn defaults(&self) -> &BTreeSet<String> {
227 &self.default_graphs
228 }
229}
230
231#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
237#[serde(rename_all = "lowercase")]
238pub enum Form {
239 Select,
241 Ask,
243 Construct,
245 Describe,
247 Update,
249}
250
251impl Form {
252 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 pub fn is_read(self) -> bool {
265 self != Form::Update
266 }
267
268 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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
282#[serde(rename_all = "lowercase", tag = "kind", content = "iri")]
283pub enum ParamType {
284 Datatype(String),
286 Class(String),
288}
289
290impl ParamType {
291 pub fn iri(&self) -> &str {
293 match self {
294 ParamType::Datatype(iri) | ParamType::Class(iri) => iri,
295 }
296 }
297}
298
299#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
301pub struct Parameter {
302 pub name: String,
304 #[serde(rename = "type")]
306 pub kind: ParamType,
307 pub required: bool,
309 pub default: Option<String>,
311 pub summary: Option<String>,
313}
314
315#[derive(Clone, Debug, PartialEq, Eq)]
317pub enum Value {
318 Iri(NamedNode),
320 Literal(Literal),
322}
323
324impl Parameter {
325 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
377fn 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 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
410pub 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
459fn 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 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
564struct 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 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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
607pub struct Analysis {
608 pub form: Form,
610 pub parameters: Vec<Parameter>,
612 pub graphs: BTreeSet<String>,
615 #[serde(rename = "anyGraph")]
618 pub any_graph: bool,
619 #[serde(rename = "updateReads")]
621 pub update_reads: bool,
622}
623
624impl Analysis {
625 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
650pub fn analyze(source: &str, door: &SparqlDoor) -> Result<Analysis> {
654 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(¶meters, &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#[derive(Clone, Debug, PartialEq, Eq)]
674pub struct Bound {
675 pub form: Form,
677 pub text: String,
679 pub graphs: BTreeSet<String>,
681 pub any_graph: bool,
683}
684
685pub 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 *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
741fn 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#[derive(Default)]
767struct Scan {
768 reads_default: bool,
770 graphs: BTreeSet<String>,
772 graph_variables: BTreeSet<String>,
774 used: BTreeSet<String>,
776 bound: BTreeSet<String>,
779 iri_only: BTreeSet<String>,
781 service: bool,
783}
784
785struct 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 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 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
1046fn 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
1069fn 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 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 #[allow(unreachable_patterns)] _ => 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
1314struct 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 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 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 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)] _ => 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 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 assert!(analyze("SELECT * WHERE { ?s ?p ?o }", &door()).is_err());
1661 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 assert!(analyze(
1670 "SELECT * FROM <urn:a> WHERE { ?s ?p ?o GRAPH ?g { ?s ?q ?r } }",
1671 &door()
1672 )
1673 .is_err());
1674 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 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 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}