1use crate::encoding::{named_node_id, term_id, DEFAULT_GRAPH_ID};
15use crate::error::{Error, Result};
16use crate::sql::Statement;
17use oxrdf::vocab::{rdf, xsd};
18use oxrdf::{GraphName, GraphNameRef, Literal, NamedNode, QuadRef, Term, TermRef};
19use spargebra::term::{GraphNamePattern, NamedNodePattern};
20use spargebra::{GraphUpdateOperation, Update};
21use std::collections::BTreeMap;
22
23pub const SCHEMA_GRAPH: &str = "oxilite:schema";
25
26pub const VOCABULARY_GRAPH: &str = "oxilite:vocabulary";
28
29pub const VOCABULARY_VERSION: &str = "1";
32
33pub const NS: &str = "https://oxilite.dev/ns#";
35
36pub const VOCABULARY: &str = include_str!("../vocab/oxl.ttl");
39
40pub mod vocab {
42 pub const SCHEMA_GRAPH: &str = "https://oxilite.dev/ns#SchemaGraph";
43 pub const SYSTEM_GRAPH: &str = "https://oxilite.dev/ns#SystemGraph";
44 pub const ONTOLOGY_GRAPH: &str = "https://oxilite.dev/ns#OntologyGraph";
45 pub const SHAPES_GRAPH: &str = "https://oxilite.dev/ns#ShapesGraph";
46 pub const SHEX_GRAPH: &str = "https://oxilite.dev/ns#ShExGraph";
47 pub const APPLIES_TO: &str = "https://oxilite.dev/ns#appliesTo";
48 pub const ACTIVE: &str = "https://oxilite.dev/ns#active";
49 pub const ONTOLOGY_IRI: &str = "https://oxilite.dev/ns#ontologyIri";
50 pub const VERSION: &str = "https://oxilite.dev/ns#version";
51 pub const SHA256: &str = "https://oxilite.dev/ns#sha256";
52 pub const LOADED_AT: &str = "https://oxilite.dev/ns#loadedAt";
53 pub const DEFAULT_GRAPH: &str = "https://oxilite.dev/ns#DefaultGraph";
54 pub const ALL_GRAPHS: &str = "https://oxilite.dev/ns#AllGraphs";
55 pub const IMPORTS: &str = "http://www.w3.org/2002/07/owl#imports";
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
60#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
61#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
62pub enum SchemaRole {
63 Ontology,
65 Shacl,
67 Shex,
69}
70
71impl SchemaRole {
72 pub const ALL: [Self; 3] = [Self::Ontology, Self::Shacl, Self::Shex];
73
74 pub const fn name(self) -> &'static str {
76 match self {
77 Self::Ontology => "ontology",
78 Self::Shacl => "shacl",
79 Self::Shex => "shex",
80 }
81 }
82
83 pub const fn class(self) -> &'static str {
85 match self {
86 Self::Ontology => vocab::ONTOLOGY_GRAPH,
87 Self::Shacl => vocab::SHAPES_GRAPH,
88 Self::Shex => vocab::SHEX_GRAPH,
89 }
90 }
91
92 pub fn from_class(iri: &str) -> Option<Self> {
94 Self::ALL.into_iter().find(|r| r.class() == iri)
95 }
96}
97
98impl std::str::FromStr for SchemaRole {
99 type Err = Error;
100
101 fn from_str(s: &str) -> Result<Self> {
103 Ok(match s.to_ascii_lowercase().as_str() {
104 "ontology" => Self::Ontology,
105 "shacl" | "shapes" => Self::Shacl,
106 "shex" => Self::Shex,
107 _ => {
108 return Err(Error::Other(format!(
109 "unknown schema role {s}: one of ontology, shacl, shex"
110 )))
111 }
112 })
113 }
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct SchemaGraph {
119 pub graph: GraphName,
120 pub role: SchemaRole,
121 pub iri: Option<NamedNode>,
123 pub version: Option<String>,
125 pub sha256: Option<String>,
127 pub imports: Vec<NamedNode>,
129 pub applies_to: Vec<GraphName>,
131 pub active: bool,
133 pub loaded_at: Option<String>,
135}
136
137impl SchemaGraph {
138 pub fn new(graph: GraphName, role: SchemaRole) -> Self {
140 Self {
141 graph,
142 role,
143 iri: None,
144 version: None,
145 sha256: None,
146 imports: Vec::new(),
147 applies_to: Vec::new(),
148 active: true,
149 loaded_at: None,
150 }
151 }
152
153 pub fn applies(&self, target: GraphNameRef<'_>) -> bool {
155 self.applies_to.is_empty() || self.applies_to.iter().any(|g| g.as_ref() == target)
156 }
157}
158
159pub fn graph_node(g: GraphNameRef<'_>) -> Result<NamedNode> {
163 match g {
164 GraphNameRef::NamedNode(n) => Ok(n.into_owned()),
165 GraphNameRef::DefaultGraph => Ok(NamedNode::new_unchecked(vocab::DEFAULT_GRAPH)),
166 GraphNameRef::BlankNode(_) => Err(Error::Other(
167 "a graph named by a blank node cannot be registered as schema".into(),
168 )),
169 }
170}
171
172pub fn node_graph(n: &NamedNode) -> GraphName {
174 if n.as_str() == vocab::DEFAULT_GRAPH {
175 GraphName::DefaultGraph
176 } else {
177 n.clone().into()
178 }
179}
180
181fn iri(s: &str) -> String {
182 format!("<{s}>")
183}
184
185fn describe_delete(node: &NamedNode) -> String {
186 format!(
187 "DELETE WHERE {{ GRAPH {} {{ {node} ?p ?o }} }}",
188 iri(SCHEMA_GRAPH)
189 )
190}
191
192fn now() -> Option<String> {
194 Some(oxsdatatypes::DateTime::now().to_string())
195}
196
197pub fn register_update(entry: &SchemaGraph) -> Result<String> {
201 let node = graph_node(entry.graph.as_ref())?;
202 let mut out = String::new();
203 if let GraphName::NamedNode(n) = &entry.graph {
204 out.push_str(&format!("CREATE SILENT GRAPH {n} ;\n"));
205 }
206 out.push_str(&describe_delete(&node));
207 out.push_str(" ;\nINSERT DATA { GRAPH ");
208 out.push_str(&iri(SCHEMA_GRAPH));
209 out.push_str(" {\n");
210 let mut triple = |p: &str, o: String| out.push_str(&format!(" {node} {} {o} .\n", iri(p)));
211 triple(rdf::TYPE.as_str(), iri(entry.role.class()));
212 triple(vocab::ACTIVE, Literal::from(entry.active).to_string());
213 for g in &entry.applies_to {
214 triple(vocab::APPLIES_TO, graph_node(g.as_ref())?.to_string());
215 }
216 if let Some(i) = &entry.iri {
217 triple(vocab::ONTOLOGY_IRI, i.to_string());
218 }
219 if let Some(v) = &entry.version {
220 triple(vocab::VERSION, Literal::new_simple_literal(v).to_string());
221 }
222 if let Some(h) = &entry.sha256 {
223 triple(vocab::SHA256, Literal::new_simple_literal(h).to_string());
224 }
225 for i in &entry.imports {
226 triple(vocab::IMPORTS, i.to_string());
227 }
228 if let Some(t) = entry.loaded_at.clone().or_else(now) {
229 triple(
230 vocab::LOADED_AT,
231 Literal::new_typed_literal(t, xsd::DATE_TIME).to_string(),
232 );
233 }
234 out.push_str("} }");
235 Ok(out)
236}
237
238pub fn unregister_update(graph: GraphNameRef<'_>) -> Result<String> {
240 Ok(describe_delete(&graph_node(graph)?))
241}
242
243pub fn set_active_update(graph: GraphNameRef<'_>, active: bool) -> Result<String> {
245 let node = graph_node(graph)?;
246 let (g, a) = (iri(SCHEMA_GRAPH), iri(vocab::ACTIVE));
247 Ok(format!(
248 "DELETE {{ GRAPH {g} {{ {node} {a} ?a }} }} INSERT {{ GRAPH {g} {{ {node} {a} {} }} }} \
249 WHERE {{ GRAPH {g} {{ {node} a ?role OPTIONAL {{ {node} {a} ?a }} }} }}",
250 Literal::from(active)
251 ))
252}
253
254pub fn drop_update(graph: GraphNameRef<'_>) -> Result<String> {
256 let node = graph_node(graph)?;
257 let drop = match graph {
258 GraphNameRef::NamedNode(n) => format!("DROP SILENT GRAPH {n}"),
259 _ => "CLEAR SILENT DEFAULT".to_owned(),
260 };
261 Ok(format!("{} ;\n{drop}", describe_delete(&node)))
262}
263
264pub fn registered_query(graph: GraphNameRef<'_>) -> Result<String> {
266 Ok(format!(
267 "ASK {{ GRAPH {} {{ {} a ?role }} }}",
268 iri(SCHEMA_GRAPH),
269 graph_node(graph)?
270 ))
271}
272
273pub fn size_query(graph: GraphNameRef<'_>) -> String {
275 match graph {
276 GraphNameRef::NamedNode(n) => {
277 format!("SELECT (COUNT(*) AS ?n) WHERE {{ GRAPH {n} {{ ?s ?p ?o }} }}")
278 }
279 _ => "SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }".to_owned(),
280 }
281}
282
283pub fn entries_query() -> String {
285 format!(
286 "SELECT ?g ?p ?o WHERE {{ GRAPH {} {{ ?g ?p ?o }} }}",
287 iri(SCHEMA_GRAPH)
288 )
289}
290
291pub fn entries_from_rows(rows: &[Vec<Option<Term>>]) -> Vec<SchemaGraph> {
294 let mut by: BTreeMap<String, (NamedNode, Vec<(String, Term)>)> = BTreeMap::new();
295 for row in rows {
296 let (Some(Term::NamedNode(s)), Some(Term::NamedNode(p)), Some(o)) = (
297 row.first().cloned().flatten(),
298 row.get(1).cloned().flatten(),
299 row.get(2).cloned().flatten(),
300 ) else {
301 continue;
302 };
303 by.entry(s.as_str().to_owned())
304 .or_insert_with(|| (s.clone(), Vec::new()))
305 .1
306 .push((p.into_string(), o));
307 }
308 let mut out = Vec::new();
309 for (_, (node, props)) in by {
310 let role = props.iter().find_map(|(p, o)| match o {
311 Term::NamedNode(c) if p == rdf::TYPE.as_str() => SchemaRole::from_class(c.as_str()),
312 _ => None,
313 });
314 let Some(role) = role else { continue };
315 let mut e = SchemaGraph::new(node_graph(&node), role);
316 let mut all = false;
317 for (p, o) in props {
318 match (p.as_str(), o) {
319 (vocab::ACTIVE, Term::Literal(l)) if matches!(l.value(), "false" | "0") => {
320 e.active = false;
321 }
322 (vocab::APPLIES_TO, Term::NamedNode(n)) if n.as_str() == vocab::ALL_GRAPHS => {
323 all = true;
324 }
325 (vocab::APPLIES_TO, Term::NamedNode(n)) => e.applies_to.push(node_graph(&n)),
326 (vocab::ONTOLOGY_IRI, Term::NamedNode(n)) => e.iri = Some(n),
327 (vocab::VERSION, Term::Literal(l)) => e.version = Some(l.value().to_owned()),
328 (vocab::SHA256, Term::Literal(l)) => e.sha256 = Some(l.value().to_owned()),
329 (vocab::LOADED_AT, Term::Literal(l)) => e.loaded_at = Some(l.value().to_owned()),
330 (vocab::IMPORTS, Term::NamedNode(n)) => e.imports.push(n),
331 _ => {}
332 }
333 }
334 if all {
335 e.applies_to.clear();
336 }
337 e.applies_to.sort_by_key(ToString::to_string);
338 e.imports.sort();
339 out.push(e);
340 }
341 out.sort_by_key(|e| (e.role, e.graph.to_string()));
342 out
343}
344
345fn registry_graph(g: GraphNameRef<'_>) -> bool {
348 matches!(g, GraphNameRef::NamedNode(n) if n.as_str() == SCHEMA_GRAPH)
349}
350
351pub fn is_registry_quad(q: QuadRef<'_>) -> bool {
353 registry_graph(q.graph_name)
354}
355
356pub fn update_touches_registry(update: &Update) -> bool {
359 let named = |g: &GraphNamePattern| match g {
360 GraphNamePattern::NamedNode(n) => n.as_str() == SCHEMA_GRAPH,
361 GraphNamePattern::DefaultGraph => false,
362 GraphNamePattern::Variable(_) => true,
363 };
364 let registry_predicate = |p: &NamedNodePattern| match p {
365 NamedNodePattern::Variable(_) => true,
366 NamedNodePattern::NamedNode(n) => n.as_str().starts_with(NS) || n.as_ref() == rdf::TYPE,
367 };
368 update.operations.iter().any(|op| match op {
369 GraphUpdateOperation::InsertData { data } => data.iter().any(|q| {
370 matches!(&q.graph_name, spargebra::term::GraphName::NamedNode(n) if n.as_str() == SCHEMA_GRAPH)
371 }),
372 GraphUpdateOperation::DeleteData { data } => data.iter().any(|q| {
373 matches!(&q.graph_name, spargebra::term::GraphName::NamedNode(n) if n.as_str() == SCHEMA_GRAPH)
374 }),
375 GraphUpdateOperation::DeleteInsert { delete, insert, .. } => {
376 delete
377 .iter()
378 .any(|q| named(&q.graph_name) && registry_predicate(&q.predicate))
379 || insert
380 .iter()
381 .any(|q| named(&q.graph_name) && registry_predicate(&q.predicate))
382 }
383 GraphUpdateOperation::Create { .. } => false,
384 GraphUpdateOperation::Load { .. }
385 | GraphUpdateOperation::Clear { .. }
386 | GraphUpdateOperation::Drop { .. } => true,
387 })
388}
389
390struct Ids {
394 reg: i64,
395 ty: i64,
396 applies: i64,
397 active: i64,
398 false_: i64,
399 dflt: i64,
400 all: i64,
401 system: i64,
402}
403
404fn ids() -> Ids {
405 Ids {
406 reg: named_node_id(SCHEMA_GRAPH),
407 ty: named_node_id(rdf::TYPE.as_str()),
408 applies: named_node_id(vocab::APPLIES_TO),
409 active: named_node_id(vocab::ACTIVE),
410 false_: term_id(TermRef::Literal(Literal::from(false).as_ref())),
411 dflt: named_node_id(vocab::DEFAULT_GRAPH),
412 all: named_node_id(vocab::ALL_GRAPHS),
413 system: named_node_id(vocab::SYSTEM_GRAPH),
414 }
415}
416
417pub fn all_scope() -> i64 {
419 named_node_id(vocab::ALL_GRAPHS)
420}
421
422pub fn registry_graph_id() -> i64 {
424 named_node_id(SCHEMA_GRAPH)
425}
426
427fn graph_of(i: &Ids, col: &str) -> String {
429 format!(
430 "(CASE {col} WHEN {} THEN {DEFAULT_GRAPH_ID} ELSE {col} END)",
431 i.dflt
432 )
433}
434
435fn active_nodes(i: &Ids, roles: &[SchemaRole]) -> String {
437 let classes = roles
438 .iter()
439 .map(|r| named_node_id(r.class()).to_string())
440 .collect::<Vec<_>>()
441 .join(", ");
442 format!(
443 "SELECT r.s AS s FROM quads r WHERE r.g = {reg} AND r.p = {ty} AND r.o IN ({classes}) \
444 AND NOT EXISTS (SELECT 1 FROM quads z WHERE z.g = {reg} AND z.s = r.s AND z.p = {active} AND z.o = {f})",
445 reg = i.reg,
446 ty = i.ty,
447 active = i.active,
448 f = i.false_
449 )
450}
451
452fn system_graphs(i: &Ids) -> String {
454 format!(
455 "SELECT {} FROM quads y WHERE y.g = {} AND y.p = {} AND y.o = {}",
456 graph_of(i, "y.s"),
457 i.reg,
458 i.ty,
459 i.system
460 )
461}
462
463pub fn scope(role: SchemaRole, column: &str) -> String {
468 let i = ids();
469 let nodes = active_nodes(&i, &[role]);
470 format!(
471 "((NOT EXISTS ({nodes}) AND {column} NOT IN ({})) OR {column} IN (SELECT {} FROM ({nodes}) an))",
472 system_graphs(&i),
473 graph_of(&i, "an.s")
474 )
475}
476
477pub fn scoped_quads(role: SchemaRole) -> String {
479 format!("(SELECT s, p, o, g FROM quads WHERE {})", scope(role, "g"))
480}
481
482fn registered_graphs(i: &Ids) -> String {
485 let classes = SchemaRole::ALL
486 .iter()
487 .map(|r| named_node_id(r.class()))
488 .chain([i.system])
489 .map(|id| id.to_string())
490 .collect::<Vec<_>>()
491 .join(", ");
492 format!(
493 "SELECT {} FROM quads r WHERE r.g = {} AND r.p = {} AND r.o IN ({classes})",
494 graph_of(i, "r.s"),
495 i.reg,
496 i.ty
497 )
498}
499
500pub fn quads_without_schema_graphs() -> String {
503 without_schema_graphs("quads")
504}
505
506pub fn without_schema_graphs(source: &str) -> String {
508 let i = ids();
509 format!(
510 "(SELECT s, p, o, g FROM {source} WHERE g <> {} AND g NOT IN ({}))",
511 i.reg,
512 registered_graphs(&i)
513 )
514}
515
516pub fn ontology_axioms(cond: &str) -> String {
523 let i = ids();
524 let act = active_nodes(&i, &[SchemaRole::Ontology]);
525 let targets = format!(
527 "SELECT a.s AS n, {} AS t FROM quads a WHERE a.g = {} AND a.p = {} AND a.o <> {}",
528 graph_of(&i, "a.o"),
529 i.reg,
530 i.applies,
531 i.all
532 );
533 let to_all = format!(
534 "SELECT a.s FROM quads a WHERE a.g = {} AND a.p = {} AND a.o = {}",
535 i.reg, i.applies, i.all
536 );
537 let global = format!(
539 "SELECT ac.s AS s FROM ({act}) ac WHERE ac.s NOT IN (SELECT n FROM ({targets})) OR ac.s IN ({to_all})"
540 );
541 let specific = format!("SELECT st.n AS n, st.t AS t FROM ({targets}) st WHERE st.n IN ({act})");
542 let map = format!(
543 "SELECT {g} AS g, {all} AS scope FROM ({global}) gl \
544 UNION SELECT {g}, sp.t FROM ({global}) gl JOIN ({specific}) sp \
545 UNION SELECT {n}, sp.t FROM ({specific}) sp",
546 g = graph_of(&i, "gl.s"),
547 n = graph_of(&i, "sp.n"),
548 all = i.all
549 );
550 format!(
551 "(SELECT q.s AS s, q.p AS p, q.o AS o, m.scope AS scope FROM quads q JOIN ({map}) m ON m.g = q.g WHERE {cond} \
552 UNION ALL SELECT q.s, q.p, q.o, {all} FROM quads q WHERE {cond} AND NOT EXISTS ({act}) AND q.g NOT IN ({sys}))",
553 all = i.all,
554 sys = system_graphs(&i)
555 )
556}
557
558pub fn scopes_statement(id_col: impl Fn(&str) -> String) -> Statement {
561 Statement::new(format!(
562 "SELECT DISTINCT {} FROM tbox_closure WHERE scope <> {}",
563 id_col("scope"),
564 all_scope()
565 ))
566}
567
568pub fn system_quads() -> Vec<oxrdf::Quad> {
574 let vocabulary = NamedNode::new_unchecked(VOCABULARY_GRAPH);
575 let registry = NamedNode::new_unchecked(SCHEMA_GRAPH);
576 let mut out: Vec<oxrdf::Quad> = oxrdfio::RdfParser::from_format(oxrdfio::RdfFormat::Turtle)
577 .for_slice(VOCABULARY.as_bytes())
578 .map(|q| {
579 let q = q.expect("the bundled vocabulary is valid Turtle");
580 oxrdf::Quad::new(q.subject, q.predicate, q.object, vocabulary.clone())
581 })
582 .collect();
583 let t = |s: &NamedNode, p: &str, o: Term| {
584 oxrdf::Quad::new(s.clone(), NamedNode::new_unchecked(p), o, registry.clone())
585 };
586 let system: Term = NamedNode::new_unchecked(vocab::SYSTEM_GRAPH).into();
587 let label = "http://www.w3.org/2000/01/rdf-schema#label";
588 out.extend([
589 t(®istry, rdf::TYPE.as_str(), system.clone()),
590 t(
591 ®istry,
592 label,
593 Literal::new_simple_literal("schema registry").into(),
594 ),
595 t(&vocabulary, rdf::TYPE.as_str(), system),
596 t(
597 &vocabulary,
598 label,
599 Literal::new_simple_literal("oxilite vocabulary").into(),
600 ),
601 t(&vocabulary, vocab::APPLIES_TO, registry.clone().into()),
602 t(
603 &vocabulary,
604 vocab::ONTOLOGY_IRI,
605 NamedNode::new_unchecked(NS).into(),
606 ),
607 t(
608 &vocabulary,
609 vocab::VERSION,
610 Literal::new_simple_literal(VOCABULARY_VERSION).into(),
611 ),
612 ]);
613 out
614}
615
616pub fn system_graphs_update() -> String {
619 let (reg, voc) = (iri(SCHEMA_GRAPH), iri(VOCABULARY_GRAPH));
620 let mut out = format!(
621 "CREATE SILENT GRAPH {reg} ;\nCREATE SILENT GRAPH {voc} ;\nCLEAR SILENT GRAPH {voc} ;\n\
622 DELETE WHERE {{ GRAPH {reg} {{ {reg} ?p ?o }} }} ;\n\
623 DELETE WHERE {{ GRAPH {reg} {{ {voc} ?p ?o }} }} ;\nINSERT DATA {{\n"
624 );
625 for q in system_quads() {
626 out.push_str(&format!(
627 " GRAPH {} {{ {} {} {} . }}\n",
628 q.graph_name, q.subject, q.predicate, q.object
629 ));
630 }
631 out.push('}');
632 out
633}
634
635pub fn system_graphs_ready_query() -> String {
637 format!(
638 "ASK {{ GRAPH {} {{ {} {} {} }} }}",
639 iri(SCHEMA_GRAPH),
640 iri(VOCABULARY_GRAPH),
641 iri(vocab::VERSION),
642 Literal::new_simple_literal(VOCABULARY_VERSION)
643 )
644}
645
646pub fn legacy_rows_statement() -> Statement {
651 Statement::new(
652 "SELECT sg.g, t.lex, sg.role, sg.iri, sg.version, sg.sha256, sg.imports, sg.active \
653 FROM schema_graphs sg LEFT JOIN terms t ON t.id = sg.g",
654 )
655}
656
657pub fn legacy_entries(response: &crate::sql::Response) -> Result<Vec<SchemaGraph>> {
660 use crate::sql::col;
661 let Some(rs) = response.first() else {
662 return Ok(Vec::new());
663 };
664 let mut out = Vec::new();
665 for row in &rs.rows {
666 let g = col(row, 0)?.as_i64();
667 let text = |i: usize| -> Result<Option<String>> {
668 Ok(col(row, i)?.clone().into_string().filter(|s| !s.is_empty()))
669 };
670 let graph = if g == Some(DEFAULT_GRAPH_ID) {
671 GraphName::DefaultGraph
672 } else {
673 match text(1)?.and_then(|l| NamedNode::new(l).ok()) {
674 Some(n) if g == Some(named_node_id(n.as_str())) => n.into(),
675 _ => continue,
676 }
677 };
678 let role = match col(row, 2)?.as_i64() {
679 Some(1) => SchemaRole::Ontology,
680 Some(2) => SchemaRole::Shacl,
681 Some(3) => SchemaRole::Shex,
682 _ => continue,
683 };
684 let mut e = SchemaGraph::new(graph, role);
685 e.iri = text(3)?.and_then(|s| NamedNode::new(s).ok());
686 e.version = text(4)?;
687 e.sha256 = text(5)?;
688 e.imports = text(6)?
689 .map(|s| {
690 s.split('\n')
691 .filter_map(|i| NamedNode::new(i).ok())
692 .collect()
693 })
694 .unwrap_or_default();
695 e.active = col(row, 7)?.as_i64().unwrap_or(1) != 0;
696 out.push(e);
697 }
698 Ok(out)
699}
700
701pub fn entry_quads(entry: &SchemaGraph) -> Result<Vec<oxrdf::Quad>> {
704 let node = graph_node(entry.graph.as_ref())?;
705 let g = NamedNode::new_unchecked(SCHEMA_GRAPH);
706 let q = |p: &str, o: Term| {
707 oxrdf::Quad::new(node.clone(), NamedNode::new_unchecked(p), o, g.clone())
708 };
709 let mut out = vec![
710 q(
711 rdf::TYPE.as_str(),
712 NamedNode::new_unchecked(entry.role.class()).into(),
713 ),
714 q(vocab::ACTIVE, Literal::from(entry.active).into()),
715 ];
716 for t in &entry.applies_to {
717 out.push(q(vocab::APPLIES_TO, graph_node(t.as_ref())?.into()));
718 }
719 if let Some(i) = &entry.iri {
720 out.push(q(vocab::ONTOLOGY_IRI, i.clone().into()));
721 }
722 if let Some(v) = &entry.version {
723 out.push(q(vocab::VERSION, Literal::new_simple_literal(v).into()));
724 }
725 if let Some(h) = &entry.sha256 {
726 out.push(q(vocab::SHA256, Literal::new_simple_literal(h).into()));
727 }
728 for i in &entry.imports {
729 out.push(q(vocab::IMPORTS, i.clone().into()));
730 }
731 if let Some(t) = &entry.loaded_at {
732 out.push(q(
733 vocab::LOADED_AT,
734 Literal::new_typed_literal(t, xsd::DATE_TIME).into(),
735 ));
736 }
737 Ok(out)
738}
739
740#[cfg(test)]
741mod tests {
742 use super::*;
743
744 #[test]
745 fn register_update_parses_and_reads_back() {
746 let mut e = SchemaGraph::new(
747 NamedNode::new_unchecked("http://ex.org/onto").into(),
748 SchemaRole::Ontology,
749 );
750 e.version = Some("v \"1\"".into());
751 e.applies_to = vec![
752 NamedNode::new_unchecked("http://ex.org/data").into(),
753 GraphName::DefaultGraph,
754 ];
755 e.imports = vec![NamedNode::new_unchecked("http://ex.org/base")];
756 e.loaded_at = Some("2026-09-26T10:00:00Z".into());
757 let u = register_update(&e).unwrap();
758 spargebra::SparqlParser::new().parse_update(&u).unwrap();
759 for s in [
760 unregister_update(e.graph.as_ref()).unwrap(),
761 set_active_update(e.graph.as_ref(), false).unwrap(),
762 drop_update(e.graph.as_ref()).unwrap(),
763 drop_update(GraphNameRef::DefaultGraph).unwrap(),
764 ] {
765 spargebra::SparqlParser::new().parse_update(&s).unwrap();
766 }
767 let rows: Vec<Vec<Option<Term>>> = entry_quads(&e)
768 .unwrap()
769 .into_iter()
770 .map(|q| {
771 vec![
772 Some(q.subject.into()),
773 Some(q.predicate.into()),
774 Some(q.object),
775 ]
776 })
777 .collect();
778 let back = entries_from_rows(&rows);
779 let mut want = e.clone();
780 want.applies_to.sort_by_key(ToString::to_string);
781 assert_eq!(back, vec![want]);
782 }
783}