1use std::fs::File;
2use std::io::{BufReader, BufWriter};
3use std::path::Path;
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::sync::{Arc, Mutex, RwLock};
6use std::thread::ThreadId;
7
8use oxigraph::io::{RdfFormat, RdfParser, RdfSerializer};
9use oxigraph::model::{
10 GraphName, GraphNameRef, NamedOrBlankNodeRef, Quad, QuadRef, TermRef, Triple, TripleRef,
11};
12use oxigraph::store::{QuadIter, Store, Transaction as OxigraphTransaction};
13
14use crate::io::{BomStrippingReader, map_rdf_parse_error};
15use crate::storage::{
16 self, DurableStore, DurableStoreOps, OpenOptions, StorageBackend, StorageCapabilities,
17};
18use crate::{Error, Result};
19
20#[derive(Clone, Copy, Debug, Default)]
22pub struct StatementPattern<'a> {
23 pub subject: Option<NamedOrBlankNodeRef<'a>>,
25 pub predicate: Option<oxigraph::model::NamedNodeRef<'a>>,
27 pub object: Option<TermRef<'a>>,
29 pub graph_name: Option<GraphNameRef<'a>>,
31}
32
33#[must_use]
39pub struct StatementMatches {
40 inner: QuadIter<'static>,
41}
42
43impl Iterator for StatementMatches {
44 type Item = Result<Quad>;
45
46 fn next(&mut self) -> Option<Self::Item> {
47 self.inner
48 .next()
49 .map(|item| item.map_err(|error| Error::Storage(error.to_string())))
50 }
51}
52
53pub struct ModelTransaction<'a> {
55 inner: OxigraphTransaction<'a>,
56}
57
58impl ModelTransaction<'_> {
59 pub fn add(&mut self, statement: impl Into<Triple>) -> Result<bool> {
61 self.add_to_graph(statement, GraphName::DefaultGraph)
62 }
63
64 pub fn add_to_graph(
66 &mut self,
67 statement: impl Into<Triple>,
68 graph_name: impl Into<GraphName>,
69 ) -> Result<bool> {
70 let triple = statement.into();
71 let quad = Quad::new(triple.subject, triple.predicate, triple.object, graph_name);
72 self.insert_quad(quad)
73 }
74
75 pub fn insert_quad(&mut self, quad: Quad) -> Result<bool> {
77 let inserted = !self
78 .inner
79 .contains(quad.as_ref())
80 .map_err(|error| Error::Storage(error.to_string()))?;
81 self.inner.insert(quad.as_ref());
82 Ok(inserted)
83 }
84
85 pub fn remove(&mut self, statement: impl Into<Triple>) -> Result<bool> {
87 self.remove_from_graph(statement, GraphName::DefaultGraph)
88 }
89
90 pub fn remove_from_graph(
92 &mut self,
93 statement: impl Into<Triple>,
94 graph_name: impl Into<GraphName>,
95 ) -> Result<bool> {
96 let triple = statement.into();
97 let quad = Quad::new(triple.subject, triple.predicate, triple.object, graph_name);
98 self.remove_quad(&quad)
99 }
100
101 pub fn remove_quad(&mut self, quad: &Quad) -> Result<bool> {
103 let removed = self
104 .inner
105 .contains(quad.as_ref())
106 .map_err(|error| Error::Storage(error.to_string()))?;
107 if removed {
108 self.inner.remove(quad.as_ref());
109 }
110 Ok(removed)
111 }
112
113 pub fn clear(&mut self) -> Result<()> {
115 self.inner
116 .clear()
117 .map_err(|error| Error::Storage(error.to_string()))
118 }
119
120 pub fn clear_graph(&mut self, graph_name: impl Into<GraphName>) -> Result<()> {
122 let graph_name = graph_name.into();
123 self.inner
124 .clear_graph(graph_name.as_ref())
125 .map_err(|error| Error::Storage(error.to_string()))
126 }
127}
128
129#[derive(Clone)]
169pub struct Model {
170 store: Store,
171 disk: Option<DurableStore>,
172 lock: Arc<RwLock<()>>,
173 read_only: bool,
174 in_transaction: Arc<AtomicBool>,
175 txn_owner: Arc<Mutex<Option<ThreadId>>>,
176}
177
178struct InTransactionGuard<'a> {
179 model: &'a Model,
180}
181
182impl Drop for InTransactionGuard<'_> {
183 fn drop(&mut self) {
184 self.model.in_transaction.store(false, Ordering::Release);
185 *self
186 .model
187 .txn_owner
188 .lock()
189 .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
190 }
191}
192
193impl Model {
194 pub fn new() -> Result<Self> {
196 Store::new()
197 .map(|store| Self {
198 store,
199 disk: None,
200 lock: Arc::new(RwLock::new(())),
201 read_only: false,
202 in_transaction: Arc::new(AtomicBool::new(false)),
203 txn_owner: Arc::new(Mutex::new(None)),
204 })
205 .map_err(|error| Error::Storage(error.to_string()))
206 }
207
208 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
210 Self::open_with(OpenOptions::fjall(path))
211 }
212
213 pub fn open_with(options: OpenOptions) -> Result<Self> {
215 if options.backend() == StorageBackend::Memory {
216 return Self::new();
217 }
218
219 let path = options.path();
220 if options.is_read_only() {
221 if !path.exists() {
222 return Err(Error::OpenStore {
223 path: path.to_owned(),
224 message: "read-only open requires an existing store path".into(),
225 });
226 }
227 if !DurableStore::looks_like_store(options.backend(), path) {
228 return Err(Error::OpenStore {
229 path: path.to_owned(),
230 message: "read-only open cannot initialize a new Oxiland store".into(),
231 });
232 }
233 }
234
235 let disk = match options.backend() {
236 StorageBackend::Memory => unreachable!("memory handled above"),
237 StorageBackend::Fjall => DurableStore::open_fjall(path, options.can_create())?,
238 };
239 let allow_init = !options.is_read_only() && options.can_create();
240 disk.ensure_format_v1(path, allow_init)?;
241 let store = Store::new().map_err(|error| Error::OpenStore {
242 path: path.to_owned(),
243 message: error.to_string(),
244 })?;
245 disk.load_into(&store)?;
246 Ok(Self {
247 store,
248 disk: Some(disk),
249 lock: Arc::new(RwLock::new(())),
250 read_only: options.is_read_only(),
251 in_transaction: Arc::new(AtomicBool::new(false)),
252 txn_owner: Arc::new(Mutex::new(None)),
253 })
254 }
255
256 pub fn migrate_legacy_store(path: impl AsRef<Path>) -> Result<Self> {
258 let path = path.as_ref();
259 {
260 let disk = DurableStore::open_fjall(path, false)?;
261 disk.migrate_legacy_to_v1()?;
262 }
263 Self::open_with(OpenOptions::fjall(path))
264 }
265
266 #[must_use]
268 pub fn capabilities(&self) -> StorageCapabilities {
269 match &self.disk {
270 None => StorageCapabilities::memory(),
271 Some(disk) => disk.capabilities(self.read_only),
272 }
273 }
274
275 #[must_use]
277 pub fn backend(&self) -> StorageBackend {
278 match &self.disk {
279 None => StorageBackend::Memory,
280 Some(disk) => disk.backend_id(),
281 }
282 }
283
284 #[must_use]
291 pub fn store(&self) -> &Store {
292 &self.store
293 }
294
295 pub(crate) fn with_read_lock<R>(&self, f: impl FnOnce() -> R) -> R {
296 if self.same_thread_in_transaction() {
297 return f();
300 }
301 let _guard = self
302 .lock
303 .read()
304 .unwrap_or_else(std::sync::PoisonError::into_inner);
305 f()
306 }
307
308 pub fn storage_backend_available(name: &str) -> Result<bool> {
310 let backend = StorageBackend::from_name(name)?;
311 Ok(storage::compiled_backends().contains(&backend))
312 }
313
314 fn same_thread_in_transaction(&self) -> bool {
315 if !self.in_transaction.load(Ordering::Acquire) {
316 return false;
317 }
318 let owner = self
319 .txn_owner
320 .lock()
321 .unwrap_or_else(std::sync::PoisonError::into_inner);
322 *owner == Some(std::thread::current().id())
323 }
324
325 fn ensure_writable(&self) -> Result<()> {
326 if self.read_only {
327 return Err(Error::Unsupported(
328 "model was opened read-only; mutating APIs are unavailable".into(),
329 ));
330 }
331 if self.in_transaction.load(Ordering::Acquire) {
332 return Err(Error::Unsupported(
333 "auto-commit mutation is unavailable while a Model::transaction is open; use the transaction handle"
334 .into(),
335 ));
336 }
337 Ok(())
338 }
339
340 pub fn transaction<R>(
347 &self,
348 f: impl FnOnce(&mut ModelTransaction<'_>) -> Result<R>,
349 ) -> Result<R> {
350 if self.read_only {
351 return Err(Error::Unsupported(
352 "model was opened read-only; mutating APIs are unavailable".into(),
353 ));
354 }
355 if self.same_thread_in_transaction() {
356 return Err(Error::Unsupported(
357 "nested Model::transaction is unsupported".into(),
358 ));
359 }
360 let _guard = self
361 .lock
362 .write()
363 .unwrap_or_else(std::sync::PoisonError::into_inner);
364 if self
365 .in_transaction
366 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
367 .is_err()
368 {
369 return Err(Error::Unsupported(
370 "nested Model::transaction is unsupported".into(),
371 ));
372 }
373 *self
374 .txn_owner
375 .lock()
376 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(std::thread::current().id());
377 let _txn_flag = InTransactionGuard { model: self };
378 let oxi = self
379 .store
380 .start_transaction()
381 .map_err(|error| Error::Storage(error.to_string()))?;
382 let mut tx = ModelTransaction { inner: oxi };
383 let value = f(&mut tx)?;
384 let ModelTransaction { inner } = tx;
385 inner
386 .commit()
387 .map_err(|error| Error::Storage(error.to_string()))?;
388 if let Some(disk) = &self.disk {
389 if let Err(error) = disk.replace_all_from_store(&self.store) {
390 if let Err(reload_error) = self.reload_store_from_disk_unlocked(disk) {
391 return Err(Error::Storage(format!(
392 "durable sync failed after transaction ({error}); rollback from disk also failed ({reload_error})"
393 )));
394 }
395 return Err(Error::Storage(format!(
396 "durable sync failed after transaction; in-memory store rolled back to disk: {error}"
397 )));
398 }
399 }
400 Ok(value)
401 }
402
403 pub fn sync(&self) -> Result<()> {
405 if self.same_thread_in_transaction() {
406 return Err(Error::Unsupported(
407 "sync is unavailable while a Model::transaction is open on this thread".into(),
408 ));
409 }
410 let _guard = self
411 .lock
412 .write()
413 .unwrap_or_else(std::sync::PoisonError::into_inner);
414 match &self.disk {
415 Some(disk) => disk.sync(),
416 None => Ok(()),
417 }
418 }
419
420 pub fn clear(&self) -> Result<()> {
422 self.ensure_writable()?;
423 let _guard = self
424 .lock
425 .write()
426 .unwrap_or_else(std::sync::PoisonError::into_inner);
427 self.store
428 .clear()
429 .map_err(|error| Error::Storage(error.to_string()))?;
430 if let Some(disk) = &self.disk {
431 if let Err(error) = disk.clear_quads() {
432 if let Err(reload_error) = self.reload_store_from_disk_unlocked(disk) {
433 return Err(Error::Storage(format!(
434 "durable clear failed ({error}); rollback from disk also failed ({reload_error})"
435 )));
436 }
437 return Err(error);
438 }
439 }
440 Ok(())
441 }
442
443 pub fn clear_graph(&self, graph_name: impl Into<GraphName>) -> Result<()> {
445 self.ensure_writable()?;
446 let graph_name = graph_name.into();
447 let _guard = self
448 .lock
449 .write()
450 .unwrap_or_else(std::sync::PoisonError::into_inner);
451 self.store
452 .clear_graph(graph_name.as_ref())
453 .map_err(|error| Error::Storage(error.to_string()))?;
454 if let Some(disk) = &self.disk {
455 if let Err(error) = disk.replace_all_from_store(&self.store) {
456 if let Err(reload_error) = self.reload_store_from_disk_unlocked(disk) {
457 return Err(Error::Storage(format!(
458 "durable clear_graph failed ({error}); rollback from disk also failed ({reload_error})"
459 )));
460 }
461 return Err(error);
462 }
463 }
464 Ok(())
465 }
466
467 pub fn bulk_insert_quads(&self, quads: impl IntoIterator<Item = Quad>) -> Result<usize> {
472 let quads: Vec<_> = quads.into_iter().collect();
473 let total = quads.len();
474 self.transaction(|tx| {
475 for quad in quads {
476 tx.insert_quad(quad)?;
477 }
478 Ok(total)
479 })
480 }
481
482 pub fn export_nquads_to_path(&self, path: impl AsRef<Path>) -> Result<()> {
484 let path = path.as_ref();
485 let file = File::create(path).map_err(|error| {
486 Error::Io(std::io::Error::new(
487 error.kind(),
488 format!("{}: {}", path.display(), error),
489 ))
490 })?;
491 let mut serializer =
492 RdfSerializer::from_format(RdfFormat::NQuads).for_writer(BufWriter::new(file));
493 for item in self.find(StatementPattern::default()) {
494 let quad = item?;
495 serializer
496 .serialize_quad(QuadRef::from(&quad))
497 .map_err(Error::Io)?;
498 }
499 let writer = serializer.finish().map_err(Error::Io)?;
500 writer
501 .into_inner()
502 .map_err(|error| Error::Io(error.into_error()))?;
503 Ok(())
504 }
505
506 pub fn import_nquads_from_path(&self, path: impl AsRef<Path>) -> Result<usize> {
511 let path = path.as_ref();
512 let file = File::open(path).map_err(|error| {
513 Error::Io(std::io::Error::new(
514 error.kind(),
515 format!("{}: {}", path.display(), error),
516 ))
517 })?;
518 let reader = BomStrippingReader::new(BufReader::new(file));
519 let quads = RdfParser::from_format(RdfFormat::NQuads)
520 .rename_blank_nodes()
521 .for_reader(reader)
522 .collect::<std::result::Result<Vec<_>, _>>()
523 .map_err(map_rdf_parse_error)?;
524 let total = quads.len();
525 self.bulk_insert_quads(quads)?;
526 Ok(total)
527 }
528
529 pub fn add(&self, statement: impl Into<Triple>) -> Result<bool> {
531 self.add_to_graph(statement, GraphName::DefaultGraph)
532 }
533
534 pub fn add_to_graph(
536 &self,
537 statement: impl Into<Triple>,
538 graph_name: impl Into<GraphName>,
539 ) -> Result<bool> {
540 let triple = statement.into();
541 let quad = Quad::new(triple.subject, triple.predicate, triple.object, graph_name);
542 self.insert_quad(quad)
543 }
544
545 pub fn insert_quad(&self, quad: Quad) -> Result<bool> {
547 self.ensure_writable()?;
548 let _guard = self
549 .lock
550 .write()
551 .unwrap_or_else(std::sync::PoisonError::into_inner);
552 let inserted = !self
553 .store
554 .contains(quad.as_ref())
555 .map_err(|error| Error::Storage(error.to_string()))?;
556 if !inserted {
557 return Ok(false);
558 }
559 self.store
560 .insert(&quad)
561 .map_err(|error| Error::Storage(error.to_string()))?;
562 if let Some(disk) = &self.disk {
563 let canonical = storage::stored_matching_quad(&self.store, &quad)?;
564 if let Err(error) = disk.insert_quad(&canonical) {
565 if let Err(reload_error) = self.reload_store_from_disk_unlocked(disk) {
566 let _ = self.store.remove(canonical.as_ref());
567 return Err(Error::Storage(format!(
568 "durable insert failed ({error}); rollback from disk also failed ({reload_error})"
569 )));
570 }
571 return Err(error);
572 }
573 }
574 Ok(true)
575 }
576
577 pub fn remove_quad(&self, quad: &Quad) -> Result<bool> {
579 self.ensure_writable()?;
580 let _guard = self
581 .lock
582 .write()
583 .unwrap_or_else(std::sync::PoisonError::into_inner);
584 let removed = self
585 .store
586 .contains(quad.as_ref())
587 .map_err(|error| Error::Storage(error.to_string()))?;
588 if !removed {
589 return Ok(false);
590 }
591 let canonical = storage::stored_matching_quad(&self.store, quad)?;
592 self.store
593 .remove(quad.as_ref())
594 .map_err(|error| Error::Storage(error.to_string()))?;
595 if let Some(disk) = &self.disk {
596 if let Err(error) = disk.remove_rdf_equal(&canonical) {
597 if let Err(reload_error) = self.reload_store_from_disk_unlocked(disk) {
598 let _ = self.store.insert(&canonical);
599 return Err(Error::Storage(format!(
600 "durable remove failed ({error}); rollback from disk also failed ({reload_error})"
601 )));
602 }
603 return Err(error);
604 }
605 }
606 Ok(true)
607 }
608
609 pub fn remove(&self, statement: impl Into<Triple>) -> Result<bool> {
611 self.remove_from_graph(statement, GraphName::DefaultGraph)
612 }
613
614 pub fn remove_from_graph(
616 &self,
617 statement: impl Into<Triple>,
618 graph_name: impl Into<GraphName>,
619 ) -> Result<bool> {
620 let triple = statement.into();
621 let quad = Quad::new(triple.subject, triple.predicate, triple.object, graph_name);
622 self.remove_quad(&quad)
623 }
624
625 pub fn contains(&self, statement: TripleRef<'_>) -> Result<bool> {
627 self.contains_in_graph(statement, GraphNameRef::DefaultGraph)
628 }
629
630 pub fn contains_in_graph(
632 &self,
633 statement: TripleRef<'_>,
634 graph_name: GraphNameRef<'_>,
635 ) -> Result<bool> {
636 self.with_read_lock(|| {
637 self.store
638 .contains(oxigraph::model::QuadRef::new(
639 statement.subject,
640 statement.predicate,
641 statement.object,
642 graph_name,
643 ))
644 .map_err(|error| Error::Storage(error.to_string()))
645 })
646 }
647
648 pub fn len(&self) -> Result<usize> {
650 self.with_read_lock(|| {
651 self.store
652 .len()
653 .map_err(|error| Error::Storage(error.to_string()))
654 })
655 }
656
657 pub fn is_empty(&self) -> Result<bool> {
659 self.with_read_lock(|| {
660 self.store
661 .is_empty()
662 .map_err(|error| Error::Storage(error.to_string()))
663 })
664 }
665
666 pub fn find(&self, pattern: StatementPattern<'_>) -> StatementMatches {
668 self.with_read_lock(|| StatementMatches {
669 inner: self.store.quads_for_pattern(
670 pattern.subject,
671 pattern.predicate,
672 pattern.object,
673 pattern.graph_name,
674 ),
675 })
676 }
677
678 pub(crate) fn run_sparql_update(
681 &self,
682 update: impl FnOnce(&Store) -> Result<()>,
683 ) -> Result<()> {
684 self.ensure_writable()?;
685 let _guard = self
686 .lock
687 .write()
688 .unwrap_or_else(std::sync::PoisonError::into_inner);
689 update(&self.store)?;
690 let Some(disk) = &self.disk else {
691 return Ok(());
692 };
693 if let Err(error) = disk.replace_all_from_store(&self.store) {
694 if let Err(reload_error) = self.reload_store_from_disk_unlocked(disk) {
695 return Err(Error::Storage(format!(
696 "durable sync failed after SPARQL Update ({error}); rollback from disk also failed ({reload_error})"
697 )));
698 }
699 return Err(Error::Storage(format!(
700 "durable sync failed after SPARQL Update; in-memory store rolled back to disk: {error}"
701 )));
702 }
703 Ok(())
704 }
705
706 fn reload_store_from_disk_unlocked(&self, disk: &DurableStore) -> Result<()> {
707 self.store
708 .clear()
709 .map_err(|error| Error::Storage(error.to_string()))?;
710 disk.load_into(&self.store)
711 }
712}