1use flams_ontology::languages::Language;
2use flams_ontology::narration::problems::CognitiveDimension;
3use flams_ontology::narration::LOKind;
4use flams_ontology::rdf::ontologies::ulo2;
5use flams_ontology::rdf::{NamedNode, Quad, Triple};
6use flams_ontology::uris::{
7 ArchiveURIRef, DocumentElementURI, DocumentURI, PathURITrait, SymbolURI, URIOrRefTrait,
8 URIRefTrait, URITrait,
9};
10use oxigraph::sparql::QuerySolutionIter;
11use oxrdfio::RdfFormat;
12use std::fmt::{Debug, Display};
13use std::io::{BufReader, BufWriter};
14use std::marker::PhantomData;
15use std::ops::Deref;
16use std::path::Path;
17use std::string::FromUtf8Error;
18use tracing::instrument;
19
20pub mod sparql {
21 use flams_ontology::{
22 rdf::ontologies::{self, ulo2},
23 uris::{SymbolURI, URIOrRefTrait},
24 };
25 pub use oxigraph::sparql::*;
26 pub use spargebra::{
27 algebra::GraphPattern, term::TriplePattern, Query as QueryBuilder, SparqlSyntaxError,
28 };
29 pub struct Var(pub char);
30 impl From<Var> for spargebra::term::TermPattern {
31 fn from(v: Var) -> Self {
32 Self::Variable(flams_ontology::rdf::Variable::new_unchecked(v.0))
33 }
34 }
35 impl From<Var> for spargebra::term::NamedNodePattern {
36 fn from(v: Var) -> Self {
37 Self::Variable(flams_ontology::rdf::Variable::new_unchecked(v.0))
38 }
39 }
40 pub trait TermPattern: Into<spargebra::term::TermPattern> {}
41 impl TermPattern for Var {}
42 pub trait NamedNodePattern: Into<spargebra::term::NamedNodePattern> {}
43 impl NamedNodePattern for Var {}
44 impl NamedNodePattern for super::NamedNode {}
45 impl TermPattern for super::NamedNode {}
46
47 pub struct Select<S: TermPattern, P: NamedNodePattern, O: TermPattern> {
48 pub subject: S,
49 pub pred: P,
50 pub object: O,
51 }
52 impl<S: TermPattern, P: NamedNodePattern, O: TermPattern> From<Select<S, P, O>> for Query {
53 fn from(s: Select<S, P, O>) -> Self {
54 QueryBuilder::Select {
55 dataset: None,
56 base_iri: None,
57 pattern: spargebra::algebra::GraphPattern::Distinct {
58 inner: Box::new(spargebra::algebra::GraphPattern::Bgp {
59 patterns: vec![spargebra::term::TriplePattern {
60 subject: s.subject.into(),
61 predicate: s.pred.into(),
62 object: s.object.into(),
63 }],
64 }),
65 },
66 }
67 .into()
68 }
69 }
70
71 #[must_use]
72 pub fn lo_query(s: &SymbolURI, problems: bool) -> Query {
73 use spargebra::{
89 algebra::{Expression, GraphPattern},
90 term::TriplePattern,
91 };
92 #[inline]
93 fn var(s: &'static str) -> spargebra::term::Variable {
94 spargebra::term::Variable::new_unchecked(s)
95 }
96 let iri = s.to_iri();
97
98 let defs_and_exs = GraphPattern::Union {
99 left: Box::new(GraphPattern::Extend {
100 inner: Box::new(GraphPattern::Bgp {
101 patterns: vec![TriplePattern {
102 subject: var("x").into(),
103 predicate: ulo2::DEFINES.into_owned().into(),
104 object: iri.clone().into(),
105 }],
106 }),
107 variable: var("R"),
108 expression: Expression::Literal("DEF".into()),
109 }),
110 right: Box::new(GraphPattern::Extend {
111 inner: Box::new(GraphPattern::Bgp {
112 patterns: vec![TriplePattern {
113 subject: var("x").into(),
114 predicate: ulo2::EXAMPLE_FOR.into_owned().into(),
115 object: iri.clone().into(),
116 }],
117 }),
118 variable: var("R"),
119 expression: Expression::Literal("EX".into()),
120 }),
121 };
122
123 QueryBuilder::Select {
124 dataset: None,
125 base_iri: None,
126 pattern: GraphPattern::Distinct {
127 inner: Box::new(GraphPattern::Project {
128 inner: Box::new(if problems {
129 GraphPattern::Union {
130 left: Box::new(defs_and_exs),
131 right: Box::new(GraphPattern::Bgp {
132 patterns: vec![
133 TriplePattern {
134 subject: var("x").into(),
135 predicate: ulo2::OBJECTIVE.into_owned().into(),
136 object: var("bn").into(),
137 },
138 TriplePattern {
139 subject: var("bn").into(),
140 predicate: ulo2::POSYMBOL.into_owned().into(),
141 object: iri.into(),
142 },
143 TriplePattern {
144 subject: var("bn").into(),
145 predicate: ulo2::COGDIM.into_owned().into(),
146 object: var("R").into(),
147 },
148 TriplePattern {
149 subject: var("x").into(),
150 predicate: ontologies::rdf::TYPE.into_owned().into(),
151 object: var("t").into(),
152 },
153 ],
154 }),
155 }
156 } else {
157 defs_and_exs
158 }),
159 variables: if problems {
160 vec![var("x"), var("R"), var("t")]
161 } else {
162 vec![var("x"), var("R")]
163 },
164 }),
165 },
166 }
167 .into()
168 }
169}
170use sparql::{EvaluationError, Query, QueryResults, SparqlSyntaxError};
171
172use super::archives::Archive;
173
174pub enum QueryError {
175 Syntax(SparqlSyntaxError),
176 Evaluation(EvaluationError),
177 Utf8(FromUtf8Error),
178}
179impl Display for QueryError {
180 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181 match self {
182 Self::Syntax(e) => Display::fmt(e, f),
183 Self::Evaluation(e) => Display::fmt(e, f),
184 Self::Utf8(e) => Display::fmt(e, f),
185 }
186 }
187}
188impl From<SparqlSyntaxError> for QueryError {
189 fn from(e: SparqlSyntaxError) -> Self {
190 Self::Syntax(e)
191 }
192}
193impl From<EvaluationError> for QueryError {
194 fn from(e: EvaluationError) -> Self {
195 Self::Evaluation(e)
196 }
197}
198impl From<FromUtf8Error> for QueryError {
199 fn from(e: FromUtf8Error) -> Self {
200 Self::Utf8(e)
201 }
202}
203
204pub struct QueryResult(QueryResults);
205impl QueryResult {
206 pub fn into_json(self) -> Result<String, QueryError> {
208 use sparesults::QueryResultsFormat;
209 let mut buf = Vec::new();
210 match self.0 {
211 QueryResults::Boolean(_) | QueryResults::Solutions(_) => {
212 self.0.write(&mut buf, QueryResultsFormat::Json)?;
213 }
214 QueryResults::Graph(_) => {
215 self.0.write_graph(&mut buf, RdfFormat::Turtle)?;
216 }
217 }
218 Ok(String::from_utf8(buf)?)
219 }
220
221 #[must_use]
222 pub fn into_uris<U: URITrait>(self) -> RetIter<U> {
223 RetIter(
224 match self.0 {
225 QueryResults::Boolean(_) | QueryResults::Graph(_) => RetIterI::None,
226 QueryResults::Solutions(sols) => RetIterI::Sols(sols),
227 },
228 PhantomData,
229 )
230 }
231}
232
233impl AsRef<QueryResults> for QueryResult {
234 #[inline]
235 fn as_ref(&self) -> &QueryResults {
236 &self.0
237 }
238}
239impl Deref for QueryResult {
240 type Target = QueryResults;
241 #[inline]
242 fn deref(&self) -> &Self::Target {
243 &self.0
244 }
245}
246
247#[derive(Default)]
248enum RetIterI {
249 #[default]
250 None,
251 Sols(QuerySolutionIter),
252}
253
254pub struct RetIter<U: URITrait>(RetIterI, PhantomData<U>);
255impl<U: URITrait> Default for RetIter<U> {
256 #[inline]
257 fn default() -> Self {
258 Self(RetIterI::default(), PhantomData)
259 }
260}
261
262impl<U: URITrait> Iterator for RetIter<U> {
263 type Item = U;
264 fn next(&mut self) -> Option<Self::Item> {
265 let RetIterI::Sols(s) = &mut self.0 else {
266 return None;
267 };
268 loop {
269 let s = match s.next() {
270 None => return None,
271 Some(Err(_)) => continue,
272 Some(Ok(s)) => s,
273 };
274 let [Some(flams_ontology::rdf::RDFTerm::NamedNode(n))] = s.values() else {
276 continue;
277 };
278 let s = n.as_str();
279 let s = flams_utils::escaping::IRI_ESCAPE.unescape(&s).to_string();
281 if let Ok(s) = s.parse() {
282 return Some(s);
284 }
285 }
286 }
287}
288
289pub struct LOIter {
290 inner: QuerySolutionIter,
291}
292impl Iterator for LOIter {
293 type Item = (DocumentElementURI, LOKind);
294 fn next(&mut self) -> Option<Self::Item> {
295 use flams_ontology::rdf::RDFTerm;
296 loop {
297 let s = match self.inner.next() {
298 None => return None,
299 Some(Err(_)) => continue,
300 Some(Ok(s)) => s,
301 };
302 let Some(RDFTerm::NamedNode(n)) = s.get("x") else {
303 continue;
304 };
305 let Ok(uri) = flams_utils::escaping::IRI_ESCAPE
306 .unescape(&n.as_str())
307 .to_string()
308 .parse()
309 else {
310 continue;
311 };
312 let n = match s.get("R") {
313 Some(RDFTerm::Literal(l)) if l.value() == "DEF" => {
314 return Some((uri, LOKind::Definition))
315 }
316 Some(RDFTerm::Literal(l)) if l.value() == "EX" => {
317 return Some((uri, LOKind::Example))
318 }
319 Some(RDFTerm::NamedNode(s)) => s,
320 _ => continue,
321 };
322 let cd = match n.as_ref() {
323 ulo2::REMEMBER => CognitiveDimension::Remember,
324 ulo2::UNDERSTAND => CognitiveDimension::Understand,
325 ulo2::APPLY => CognitiveDimension::Apply,
326 ulo2::ANALYZE => CognitiveDimension::Analyze,
327 ulo2::EVALUATE => CognitiveDimension::Evaluate,
328 ulo2::CREATE => CognitiveDimension::Create,
329 _ => continue,
330 };
331 let sub =
332 matches!(s.get("t"),Some(RDFTerm::NamedNode(n)) if n.as_ref() == ulo2::SUBPROBLEM);
333 return Some((
334 uri,
335 if sub {
336 LOKind::SubProblem(cd)
337 } else {
338 LOKind::Problem(cd)
339 },
340 ));
341 }
342 }
343}
344
345pub struct RDFStore {
346 store: oxigraph::store::Store,
347}
348impl Debug for RDFStore {
349 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
350 f.debug_struct("RDFStore").finish()
351 }
352}
353
354impl Default for RDFStore {
355 fn default() -> Self {
356 let store = oxigraph::store::Store::new().unwrap_or_else(|_| unreachable!());
357 store
358 .bulk_loader()
359 .load_quads(flams_ontology::rdf::ontologies::ulo2::QUADS.iter().copied())
360 .unwrap_or_else(|_| unreachable!());
361 Self { store }
362 }
363}
364
365impl RDFStore {
366 #[inline]
367 pub fn clear(&self) {
368 let _ = self.store.clear();
369 }
370 #[inline]
371 #[must_use]
372 pub fn num_relations(&self) -> usize {
373 self.store.len().unwrap_or_default()
374 }
375 pub fn add_quads(&self, iter: impl Iterator<Item = Quad>) {
376 let loader = self.store.bulk_loader();
377 let _ = loader.load_quads(iter);
378 }
379
380 #[must_use]
381 pub fn los(&self, s: &SymbolURI, problems: bool) -> Option<LOIter> {
382 let q = sparql::lo_query(s, problems);
383 self.query(q).ok().and_then(|s| {
384 if let QueryResults::Solutions(s) = s.0 {
385 Some(LOIter { inner: s })
386 } else {
387 None
388 }
389 })
390 }
391
392 pub fn export(&self, iter: impl Iterator<Item = Triple>, p: &Path, uri: &DocumentURI) {
393 if let Ok(file) = std::fs::File::create(p) {
394 let writer = BufWriter::new(file);
395 let iri = uri.as_path().to_iri();
396 let ns = iri.as_str();
397 let mut writer = oxigraph::io::RdfSerializer::from_format(RdfFormat::Turtle)
400 .with_prefix("ns", ns)
401 .unwrap_or_else(|_| unreachable!())
402 .with_prefix("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns")
403 .unwrap_or_else(|_| unreachable!())
404 .with_prefix("ulo", "http://mathhub.info/ulo")
405 .unwrap_or_else(|_| unreachable!())
406 .with_prefix("dc", "http://purl.org/dc/elements/1.1")
407 .unwrap_or_else(|_| unreachable!())
408 .for_writer(writer);
409 for t in iter {
410 if let Err(e) = writer.serialize_triple(&t) {
411 tracing::warn!("Error serializing triple: {e:?}");
412 }
413 }
414 let _ = writer.finish();
415 }
416 }
417
418 pub fn query_str(&self, s: impl AsRef<str>) -> Result<QueryResult, QueryError> {
420 let mut query_str = String::from(
421 r"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
422 PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
423 PREFIX dc: <http://purl.org/dc/elements/1.1#>
424 PREFIX ulo: <http://mathhub.info/ulo#>
425 ",
426 );
427 query_str.push_str(s.as_ref());
428 let query: Query = query_str.as_str().try_into()?;
429 self.query(query)
430 }
431
432 pub fn query(&self, mut q: Query) -> Result<QueryResult, QueryError> {
434 q.dataset_mut().set_default_graph_as_union();
435
436 Ok(self.store.query(q).map(QueryResult)?)
438 }
439
440 pub(crate) fn load(&self, path: &Path, graph: NamedNode) {
441 let Ok(file) = std::fs::File::open(path) else {
442 tracing::error!("Failed to open file {}", path.display());
443 return;
444 };
445 let buf = BufReader::new(file);
446 let loader = self.store.bulk_loader();
447 let reader = oxigraph::io::RdfParser::from_format(RdfFormat::Turtle)
448 .with_default_graph(graph)
449 .for_reader(buf);
450 let _ = loader.load_quads(reader.filter_map(Result::ok));
451 }
452
453 #[allow(unreachable_patterns)]
454 #[instrument(level = "info", name = "relational", target = "relational", skip_all)]
455 pub fn load_archives(&self, archives: &[Archive]) {
456 use rayon::prelude::*;
457 tracing::info!(target:"relational","Loading relational for {} archives...",archives.len());
458 let old = self.store.len().unwrap_or_default();
459 archives
460 .par_iter()
461 .filter_map(|a| match a {
462 Archive::Local(a) => Some(a),
463 _ => None,
464 })
465 .for_each(|a| {
466 let out = a.out_dir();
467 if out.exists() && out.is_dir() {
468 for e in walkdir::WalkDir::new(out)
469 .into_iter()
470 .filter_map(Result::ok)
471 .filter(|entry| entry.file_name() == "index.ttl")
472 {
473 let Some(graph) = Self::get_iri(a.uri(), out, &e) else {
474 continue;
475 };
476 self.load(e.path(), graph);
477 }
478 }
479 });
480 tracing::info!(target:"relational","Loaded {} relations", self.store.len().unwrap_or_default() - old);
481 }
482
483 fn get_iri(a: ArchiveURIRef, out: &Path, e: &walkdir::DirEntry) -> Option<NamedNode> {
484 let parent = e.path().parent()?;
485 let parentname = parent.file_name()?.to_str()?;
486 let parentname = parentname.rsplit_once('.').map_or(parentname, |(s, _)| s);
487 let language = Language::from_rel_path(parentname);
488 let parentname = parentname
489 .strip_suffix(&format!(".{language}"))
490 .unwrap_or(parentname);
491 let pathstr = parent.parent()?.to_str()?.strip_prefix(out.to_str()?)?;
492 let doc = ((a.owned() % pathstr).ok()? & (parentname, language)).ok()?;
493 Some(doc.to_iri())
494 }
495}