Skip to main content

oxirs_core/
oxigraph_compat.rs

1//! Oxigraph compatibility layer
2//!
3//! This module provides a compatibility layer that matches Oxigraph's API,
4//! allowing OxiRS to be used as a drop-in replacement for Oxigraph.
5
6use crate::{
7    model::*,
8    parser::RdfFormat,
9    rdf_store::{OxirsQueryResults, RdfStore},
10    transaction::{IsolationLevel, TransactionManager},
11    OxirsError, Result, Store as OxirsStoreTrait,
12};
13use std::io::{BufRead, Write};
14use std::path::{Path, PathBuf};
15use std::sync::{Arc, RwLock};
16
17/// Oxigraph-compatible store implementation
18///
19/// This provides the same API as oxigraph::Store for compatibility
20///
21/// Uses interior mutability to match Oxigraph's API where mutations take &self
22pub struct Store {
23    inner: Arc<RwLock<RdfStore>>,
24    tx_manager: Arc<RwLock<Option<TransactionManager>>>,
25    wal_dir: Option<PathBuf>,
26}
27
28impl Store {
29    /// Creates a new in-memory store
30    ///
31    /// This matches oxigraph::Store::new()
32    pub fn new() -> Result<Self> {
33        Ok(Store {
34            inner: Arc::new(RwLock::new(RdfStore::new()?)),
35            tx_manager: Arc::new(RwLock::new(None)),
36            wal_dir: None,
37        })
38    }
39
40    /// Opens a persistent store at the given path
41    ///
42    /// This matches oxigraph::Store::open()
43    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
44        let path_buf = path.as_ref().to_path_buf();
45        let wal_dir = path_buf.join("wal");
46
47        Ok(Store {
48            inner: Arc::new(RwLock::new(RdfStore::open(&path_buf)?)),
49            tx_manager: Arc::new(RwLock::new(None)),
50            wal_dir: Some(wal_dir),
51        })
52    }
53
54    /// Inserts a quad into the store
55    ///
56    /// Returns true if the quad was not already present
57    pub fn insert<'a>(&self, quad: impl Into<QuadRef<'a>>) -> Result<bool> {
58        let quad_ref = quad.into();
59        let quad = Quad::new(
60            quad_ref.subject().to_owned(),
61            quad_ref.predicate().to_owned(),
62            quad_ref.object().to_owned(),
63            quad_ref.graph_name().to_owned(),
64        );
65
66        let store = self
67            .inner
68            .write()
69            .map_err(|e| OxirsError::Store(format!("Failed to acquire write lock: {e}")))?;
70        store.insert_quad(quad)
71    }
72
73    /// Extends the store with an iterator of quads
74    pub fn extend<'a>(
75        &self,
76        quads: impl IntoIterator<Item = impl Into<QuadRef<'a>>>,
77    ) -> Result<()> {
78        let store = self
79            .inner
80            .write()
81            .map_err(|e| OxirsError::Store(format!("Failed to acquire write lock: {e}")))?;
82
83        for quad in quads {
84            let quad_ref = quad.into();
85            let quad = Quad::new(
86                quad_ref.subject().to_owned(),
87                quad_ref.predicate().to_owned(),
88                quad_ref.object().to_owned(),
89                quad_ref.graph_name().to_owned(),
90            );
91            store.insert_quad(quad)?;
92        }
93
94        Ok(())
95    }
96
97    /// Removes a quad from the store
98    ///
99    /// Returns true if the quad was present
100    pub fn remove<'a>(&self, quad: impl Into<QuadRef<'a>>) -> Result<bool> {
101        let quad_ref = quad.into();
102        let quad = Quad::new(
103            quad_ref.subject().to_owned(),
104            quad_ref.predicate().to_owned(),
105            quad_ref.object().to_owned(),
106            quad_ref.graph_name().to_owned(),
107        );
108
109        let store = self
110            .inner
111            .write()
112            .map_err(|e| OxirsError::Store(format!("Failed to acquire write lock: {e}")))?;
113        store.remove_quad(&quad)
114    }
115
116    /// Loads a file into the store
117    pub fn load_from_reader<R: BufRead>(
118        &self,
119        reader: R,
120        format: RdfFormat,
121        base_iri: Option<&str>,
122        graph: Option<impl Into<GraphName>>,
123    ) -> Result<()> {
124        use crate::parser::Parser;
125
126        // Read all data into a string
127        let mut data = String::new();
128        let mut reader = reader;
129        // BufRead already extends Read, so this import is not needed
130        reader
131            .read_to_string(&mut data)
132            .map_err(|e| OxirsError::Parse(format!("Failed to read input: {e}")))?;
133
134        // Create parser with base IRI if provided
135        let mut parser = Parser::new(format);
136        if let Some(base) = base_iri {
137            parser = parser.with_base_iri(base);
138        }
139
140        // Parse to quads
141        let quads = parser.parse_str_to_quads(&data)?;
142
143        // Get write lock on store
144        let store = self
145            .inner
146            .write()
147            .map_err(|e| OxirsError::Store(format!("Failed to acquire write lock: {e}")))?;
148
149        // Insert quads, potentially modifying graph name
150        let target_graph = graph.map(|g| g.into());
151        for quad in quads {
152            let final_quad = if let Some(ref g) = target_graph {
153                // Override the quad's graph with the specified one
154                Quad::new(
155                    quad.subject().clone(),
156                    quad.predicate().clone(),
157                    quad.object().clone(),
158                    g.clone(),
159                )
160            } else {
161                quad
162            };
163            store.insert_quad(final_quad)?;
164        }
165
166        Ok(())
167    }
168
169    /// Dumps the store content to a writer
170    pub fn dump_to_writer<'a, W: Write>(
171        &self,
172        mut writer: W,
173        format: RdfFormat,
174        graph: Option<impl Into<GraphNameRef<'a>>>,
175    ) -> Result<()> {
176        use crate::model::{dataset::Dataset, graph::Graph};
177        use crate::serializer::Serializer;
178
179        let store = self
180            .inner
181            .read()
182            .map_err(|e| OxirsError::Store(format!("Failed to acquire read lock: {e}")))?;
183
184        let serializer = Serializer::new(format);
185
186        // Get quads to serialize
187        let quads = if let Some(g) = graph {
188            let graph_ref = g.into();
189            let graph_name = graph_ref.to_owned();
190            store.query_quads(None, None, None, Some(&graph_name))?
191        } else {
192            store.iter_quads()?
193        };
194
195        // Serialize based on format capabilities
196        let output = match format {
197            RdfFormat::Turtle | RdfFormat::NTriples | RdfFormat::RdfXml => {
198                // These formats only support triples, so filter to default graph
199                let triples: Vec<_> = quads
200                    .into_iter()
201                    .filter(|q| q.is_default_graph())
202                    .map(|q| q.to_triple())
203                    .collect();
204                let graph = Graph::from_iter(triples);
205                serializer.serialize_graph(&graph)?
206            }
207            RdfFormat::TriG | RdfFormat::NQuads | RdfFormat::JsonLd => {
208                // These formats support quads/datasets
209                let dataset = Dataset::from_iter(quads);
210                serializer.serialize_dataset(&dataset)?
211            }
212        };
213
214        writer
215            .write_all(output.as_bytes())
216            .map_err(|e| OxirsError::Serialize(format!("Failed to write output: {e}")))?;
217
218        Ok(())
219    }
220
221    /// Checks if the store contains a given quad
222    pub fn contains<'a>(&self, quad: impl Into<QuadRef<'a>>) -> Result<bool> {
223        let quad_ref = quad.into();
224        let quad = Quad::new(
225            quad_ref.subject().to_owned(),
226            quad_ref.predicate().to_owned(),
227            quad_ref.object().to_owned(),
228            quad_ref.graph_name().to_owned(),
229        );
230
231        let store = self
232            .inner
233            .read()
234            .map_err(|e| OxirsError::Store(format!("Failed to acquire read lock: {e}")))?;
235        store.contains_quad(&quad)
236    }
237
238    /// Returns the number of quads in the store
239    pub fn len(&self) -> Result<usize> {
240        let store = self
241            .inner
242            .read()
243            .map_err(|e| OxirsError::Store(format!("Failed to acquire read lock: {e}")))?;
244        store.len()
245    }
246
247    /// Checks if the store is empty
248    pub fn is_empty(&self) -> Result<bool> {
249        let store = self
250            .inner
251            .read()
252            .map_err(|e| OxirsError::Store(format!("Failed to acquire read lock: {e}")))?;
253        store.is_empty()
254    }
255
256    /// Returns an iterator over all quads matching a pattern
257    pub fn quads_for_pattern<'a>(
258        &self,
259        subject: Option<impl Into<SubjectRef<'a>>>,
260        predicate: Option<impl Into<PredicateRef<'a>>>,
261        object: Option<impl Into<ObjectRef<'a>>>,
262        graph_name: Option<impl Into<GraphNameRef<'a>>>,
263    ) -> QuadIter {
264        let subject = subject.map(|s| {
265            let s_ref = s.into();
266            s_ref.to_owned()
267        });
268
269        let predicate = predicate.map(|p| {
270            let p_ref = p.into();
271            match p_ref {
272                PredicateRef::NamedNode(n) => Predicate::NamedNode(n.to_owned()),
273                PredicateRef::Variable(v) => Predicate::Variable(v.to_owned()),
274            }
275        });
276
277        let object = object.map(|o| {
278            let o_ref = o.into();
279            o_ref.to_owned()
280        });
281
282        let graph_name = graph_name.map(|g| {
283            let g_ref = g.into();
284            g_ref.to_owned()
285        });
286
287        // Query the inner store
288        let quads = match self.inner.read() {
289            Ok(store) => store
290                .query_quads(
291                    subject.as_ref(),
292                    predicate.as_ref(),
293                    object.as_ref(),
294                    graph_name.as_ref(),
295                )
296                .unwrap_or_default(),
297            _ => Vec::new(),
298        };
299
300        QuadIter { quads, index: 0 }
301    }
302
303    /// Returns an iterator over all quads in the store
304    pub fn iter(&self) -> QuadIter {
305        self.quads_for_pattern(
306            None::<SubjectRef>,
307            None::<PredicateRef>,
308            None::<ObjectRef>,
309            None::<GraphNameRef>,
310        )
311    }
312
313    /// Returns all named graphs in the store
314    pub fn named_graphs(&self) -> GraphNameIter {
315        // Collect unique graph names from all quads
316        let mut graph_names = std::collections::HashSet::new();
317        if let Ok(store) = self.inner.read() {
318            if let Ok(quads) = store.iter_quads() {
319                for quad in quads {
320                    if let GraphName::NamedNode(n) = quad.graph_name() {
321                        graph_names.insert(n.clone());
322                    }
323                }
324            }
325        }
326
327        GraphNameIter {
328            graphs: graph_names.into_iter().collect(),
329            index: 0,
330        }
331    }
332
333    /// Checks if the store contains a given named graph
334    pub fn contains_named_graph<'a>(
335        &self,
336        graph_name: impl Into<NamedOrBlankNodeRef<'a>>,
337    ) -> Result<bool> {
338        let graph_ref = graph_name.into();
339        let graph = match graph_ref {
340            NamedOrBlankNodeRef::NamedNode(n) => GraphName::NamedNode(n.to_owned()),
341            NamedOrBlankNodeRef::BlankNode(b) => GraphName::BlankNode(b.to_owned()),
342        };
343
344        // Check if any quads exist in this graph
345        let store = self
346            .inner
347            .read()
348            .map_err(|e| OxirsError::Store(format!("Failed to acquire read lock: {e}")))?;
349        let quads = store.query_quads(None, None, None, Some(&graph))?;
350        Ok(!quads.is_empty())
351    }
352
353    /// Clears the store
354    pub fn clear(&self) -> Result<()> {
355        let mut store = self
356            .inner
357            .write()
358            .map_err(|e| OxirsError::Store(format!("Failed to acquire write lock: {e}")))?;
359        store.clear()
360    }
361
362    /// Clears a specific graph
363    pub fn clear_graph<'a>(&self, graph_name: impl Into<GraphNameRef<'a>>) -> Result<()> {
364        let graph_ref = graph_name.into();
365        let graph = graph_ref.to_owned();
366
367        let store = self
368            .inner
369            .write()
370            .map_err(|e| OxirsError::Store(format!("Failed to acquire write lock: {e}")))?;
371
372        // Get all quads in the specified graph
373        let quads_to_remove = store.query_quads(None, None, None, Some(&graph))?;
374
375        // Remove each quad
376        for quad in quads_to_remove {
377            store.remove_quad(&quad)?;
378        }
379
380        Ok(())
381    }
382
383    /// Executes a SPARQL query
384    pub fn query(&self, query: &str) -> Result<QueryResults> {
385        let store = self
386            .inner
387            .read()
388            .map_err(|e| OxirsError::Store(format!("Failed to acquire read lock: {e}")))?;
389        let results = store.query(query)?;
390        Ok(QueryResults { inner: results })
391    }
392
393    /// Executes a SPARQL update
394    pub fn update(&self, update_str: &str) -> Result<()> {
395        use crate::query::{UpdateExecutor, UpdateParser};
396
397        // Parse the UPDATE string
398        let parser = UpdateParser::new();
399        let update = parser.parse(update_str)?;
400
401        // Get write access to the store
402        let store = self
403            .inner
404            .write()
405            .map_err(|e| OxirsError::Store(format!("Failed to acquire write lock: {e}")))?;
406
407        // Execute the update
408        let executor = UpdateExecutor::new(&*store);
409        executor.execute(&update)?;
410
411        Ok(())
412    }
413
414    /// Creates a transaction for the store
415    ///
416    /// This method provides ACID transaction support with automatic commit/abort handling.
417    /// The transaction uses Snapshot isolation level by default.
418    ///
419    /// # Example
420    ///
421    /// ```ignore
422    /// store.transaction(|tx| {
423    ///     // Perform transactional operations
424    ///     Ok(())
425    /// })?;
426    /// ```
427    pub fn transaction<T, E>(
428        &self,
429        f: impl FnOnce(&mut crate::AcidTransaction) -> std::result::Result<T, E>,
430    ) -> std::result::Result<T, E>
431    where
432        E: From<OxirsError>,
433    {
434        // Ensure TransactionManager is initialized
435        self.ensure_tx_manager()?;
436
437        // Get the transaction manager
438        let mut tx_mgr_guard = self
439            .tx_manager
440            .write()
441            .map_err(|e| E::from(OxirsError::Store(format!("Failed to acquire lock: {e}"))))?;
442
443        let tx_mgr = tx_mgr_guard.as_mut().ok_or_else(|| {
444            E::from(OxirsError::Store(
445                "Transaction manager not initialized".to_string(),
446            ))
447        })?;
448
449        // Begin a transaction with Snapshot isolation
450        let mut transaction = tx_mgr.begin(IsolationLevel::Snapshot).map_err(E::from)?;
451
452        // Execute the user function
453        let result = f(&mut transaction);
454
455        // Commit the transaction if the function succeeded
456        match result {
457            Ok(value) => {
458                // Snapshot the pending operations before `commit` consumes the
459                // transaction, so they can be applied to the visible store.
460                let inserts = transaction.pending_inserts().to_vec();
461                let deletes = transaction.pending_deletes().to_vec();
462
463                // Durability first: write and fsync the WAL commit record.
464                transaction.commit().map_err(E::from)?;
465
466                // Redo the committed changes into the backing store. The WAL
467                // commit record is already durable, so a crash here is
468                // recoverable by replaying the log on the next open.
469                let store = self.inner.write().map_err(|e| {
470                    E::from(OxirsError::Store(format!(
471                        "Failed to acquire write lock: {e}"
472                    )))
473                })?;
474                for quad in &deletes {
475                    store.remove_quad(quad).map_err(E::from)?;
476                }
477                for quad in inserts {
478                    store.insert_quad(quad).map_err(E::from)?;
479                }
480                Ok(value)
481            }
482            Err(error) => {
483                let _ = transaction.abort();
484                Err(error)
485            }
486        }
487    }
488
489    /// Ensures the transaction manager is initialized
490    fn ensure_tx_manager(&self) -> Result<()> {
491        let mut tx_mgr_guard = self
492            .tx_manager
493            .write()
494            .map_err(|e| OxirsError::Store(format!("Failed to acquire lock: {e}")))?;
495
496        if tx_mgr_guard.is_none() {
497            // Determine WAL directory
498            let wal_dir = if let Some(ref wal_path) = self.wal_dir {
499                wal_path.clone()
500            } else {
501                // In-memory stores get a unique per-instance WAL directory so
502                // that concurrent Store instances in the same process (or on
503                // the same host) never interleave writes into a shared log.
504                std::env::temp_dir().join(format!("oxirs_wal_{}", uuid::Uuid::new_v4()))
505            };
506
507            // Create the transaction manager
508            let tx_mgr = TransactionManager::new(&wal_dir)?;
509            *tx_mgr_guard = Some(tx_mgr);
510        }
511
512        Ok(())
513    }
514
515    /// Validates the store integrity
516    ///
517    /// Performs a consistency check between the store's reported quad count and
518    /// the number of quads materialized from its indexes. A mismatch indicates
519    /// index/data corruption and is surfaced as an error.
520    pub fn validate(&self) -> Result<()> {
521        let store = self
522            .inner
523            .read()
524            .map_err(|e| OxirsError::Store(format!("Failed to acquire read lock: {e}")))?;
525
526        let reported = OxirsStoreTrait::len(&*store)?;
527        let materialized = OxirsStoreTrait::quads(&*store)?.len();
528        if reported != materialized {
529            return Err(OxirsError::Store(format!(
530                "Store consistency check failed: len() reports {reported} quads but {materialized} were found in the indexes"
531            )));
532        }
533
534        Ok(())
535    }
536
537    /// Optimizes the store layout
538    pub fn optimize(&self) -> Result<()> {
539        // Trigger arena cleanup if using ultra-performance mode
540        let store = self
541            .inner
542            .read()
543            .map_err(|e| OxirsError::Store(format!("Failed to acquire read lock: {e}")))?;
544        store.clear_arena();
545        Ok(())
546    }
547
548    /// Backs up the store to a path
549    pub fn backup<P: AsRef<Path>>(&self, path: P) -> Result<()> {
550        use crate::parser::RdfFormat;
551        use crate::serializer::Serializer;
552        use std::fs::File;
553        use std::io::Write;
554        use std::time::{SystemTime, UNIX_EPOCH};
555
556        let backup_path = path.as_ref();
557
558        // Create backup directory if it doesn't exist
559        if let Some(parent) = backup_path.parent() {
560            std::fs::create_dir_all(parent).map_err(|e| {
561                OxirsError::Store(format!("Failed to create backup directory: {e}"))
562            })?;
563        }
564
565        // Generate backup filename with timestamp
566        let timestamp = SystemTime::now()
567            .duration_since(UNIX_EPOCH)
568            .unwrap_or_default()
569            .as_secs();
570
571        let backup_file_path = if backup_path.is_dir() {
572            backup_path.join(format!("oxirs_backup_{timestamp}.nq"))
573        } else {
574            backup_path.to_path_buf()
575        };
576
577        // Get read lock on the store
578        let store = self
579            .inner
580            .read()
581            .map_err(|e| OxirsError::Store(format!("Failed to acquire read lock: {e}")))?;
582
583        // Get all quads from the store
584        let quads = store
585            .iter_quads()
586            .map_err(|e| OxirsError::Store(format!("Failed to iterate quads: {e}")))?;
587
588        // Create dataset from quads
589        let dataset = crate::model::dataset::Dataset::from_iter(quads.clone());
590
591        // Serialize to N-Quads format (most portable and complete format)
592        let serializer = Serializer::new(RdfFormat::NQuads);
593        let serialized_data = serializer
594            .serialize_dataset(&dataset)
595            .map_err(|e| OxirsError::Store(format!("Failed to serialize dataset: {e}")))?;
596
597        // Write to backup file
598        let mut backup_file = File::create(&backup_file_path)
599            .map_err(|e| OxirsError::Store(format!("Failed to create backup file: {e}")))?;
600
601        backup_file
602            .write_all(serialized_data.as_bytes())
603            .map_err(|e| OxirsError::Store(format!("Failed to write backup data: {e}")))?;
604
605        backup_file
606            .sync_all()
607            .map_err(|e| OxirsError::Store(format!("Failed to sync backup file: {e}")))?;
608
609        // Calculate backup size for logging
610        let backup_size = serialized_data.len();
611        let quad_count = quads.len();
612
613        tracing::info!(
614            "Store backup completed successfully. File: {}, Quads: {}, Size: {} bytes",
615            backup_file_path.display(),
616            quad_count,
617            backup_size
618        );
619
620        Ok(())
621    }
622
623    /// Flushes any pending changes to disk.
624    ///
625    /// Delegates to the inner [`RdfStore::flush`], which for the persistent
626    /// backend `fsync`s the append log (and compacts pending deletions),
627    /// guaranteeing durability of prior writes. For in-memory backends this is
628    /// a genuine no-op inside the inner store.
629    pub fn flush(&self) -> Result<()> {
630        let store = self.inner.read().map_err(|e| {
631            OxirsError::Store(format!("Failed to acquire read lock for flush: {e}"))
632        })?;
633        store.flush()
634    }
635}
636
637impl Default for Store {
638    fn default() -> Self {
639        Store::new().expect("Store::new() should not fail")
640    }
641}
642
643/// Iterator over quads (Oxigraph-compatible)
644pub struct QuadIter {
645    quads: Vec<Quad>,
646    index: usize,
647}
648
649impl Iterator for QuadIter {
650    type Item = Quad;
651
652    fn next(&mut self) -> Option<Self::Item> {
653        if self.index < self.quads.len() {
654            let quad = self.quads[self.index].clone();
655            self.index += 1;
656            Some(quad)
657        } else {
658            None
659        }
660    }
661}
662
663/// Iterator over graph names (Oxigraph-compatible)
664pub struct GraphNameIter {
665    graphs: Vec<NamedNode>,
666    index: usize,
667}
668
669impl Iterator for GraphNameIter {
670    type Item = NamedNode;
671
672    fn next(&mut self) -> Option<Self::Item> {
673        if self.index < self.graphs.len() {
674            let graph = self.graphs[self.index].clone();
675            self.index += 1;
676            Some(graph)
677        } else {
678            None
679        }
680    }
681}
682
683/// Oxigraph-compatible query results
684pub struct QueryResults {
685    #[allow(dead_code)]
686    inner: OxirsQueryResults,
687}
688
689impl QueryResults {
690    /// Returns true if the results are a boolean
691    pub fn is_boolean(&self) -> bool {
692        matches!(
693            self.inner.results(),
694            crate::rdf_store::types::QueryResults::Boolean(_)
695        )
696    }
697
698    /// Returns the boolean value if the results are a boolean
699    pub fn boolean(&self) -> Option<bool> {
700        match self.inner.results() {
701            crate::rdf_store::types::QueryResults::Boolean(b) => Some(*b),
702            _ => None,
703        }
704    }
705
706    /// Returns true if the results are solutions
707    pub fn is_solutions(&self) -> bool {
708        matches!(
709            self.inner.results(),
710            crate::rdf_store::types::QueryResults::Bindings(_)
711        )
712    }
713
714    /// Returns true if the results are a graph
715    pub fn is_graph(&self) -> bool {
716        matches!(
717            self.inner.results(),
718            crate::rdf_store::types::QueryResults::Graph(_)
719        )
720    }
721}
722
723/// Oxigraph-compatible transaction
724///
725/// Note: This is a placeholder implementation. Full transactional support
726/// would require implementing proper transaction isolation in OxiRS.
727pub struct Transaction {
728    // Placeholder for future transaction implementation
729    operations: Vec<TransactionOp>,
730}
731
732enum TransactionOp {
733    #[allow(dead_code)]
734    Insert(Quad),
735    #[allow(dead_code)]
736    Remove(Quad),
737}
738
739impl Transaction {
740    #[allow(dead_code)]
741    fn new() -> Self {
742        Transaction {
743            operations: Vec::new(),
744        }
745    }
746
747    /// Inserts a quad in the transaction
748    pub fn insert<'b>(&mut self, quad: impl Into<QuadRef<'b>>) -> Result<bool> {
749        let quad_ref = quad.into();
750        let quad = Quad::new(
751            quad_ref.subject().to_owned(),
752            quad_ref.predicate().to_owned(),
753            quad_ref.object().to_owned(),
754            quad_ref.graph_name().to_owned(),
755        );
756        self.operations.push(TransactionOp::Insert(quad));
757        Ok(true) // Optimistically return true
758    }
759
760    /// Removes a quad in the transaction
761    pub fn remove<'b>(&mut self, quad: impl Into<QuadRef<'b>>) -> Result<bool> {
762        let quad_ref = quad.into();
763        let quad = Quad::new(
764            quad_ref.subject().to_owned(),
765            quad_ref.predicate().to_owned(),
766            quad_ref.object().to_owned(),
767            quad_ref.graph_name().to_owned(),
768        );
769        self.operations.push(TransactionOp::Remove(quad));
770        Ok(true) // Optimistically return true
771    }
772}
773
774/// Oxigraph-compatible error type
775#[derive(Debug, thiserror::Error)]
776pub enum OxigraphCompatError {
777    #[error("Store error: {0}")]
778    Store(String),
779    #[error("Parse error: {0}")]
780    Parse(String),
781    #[error("IO error: {0}")]
782    Io(#[from] std::io::Error),
783}
784
785impl From<OxirsError> for OxigraphCompatError {
786    fn from(err: OxirsError) -> Self {
787        match err {
788            OxirsError::Store(msg) => OxigraphCompatError::Store(msg),
789            OxirsError::Parse(msg) => OxigraphCompatError::Parse(msg),
790            _ => OxigraphCompatError::Store(err.to_string()),
791        }
792    }
793}
794
795#[cfg(test)]
796mod tests {
797    use super::*;
798    use crate::model::{Literal, NamedNode};
799    use crate::parser::RdfFormat;
800    use std::io::Cursor;
801
802    #[test]
803    fn test_oxigraph_compat_store_creation() {
804        let store = Store::new().expect("construction should succeed");
805        assert!(store.is_empty().expect("store operation should succeed"));
806        assert_eq!(store.len().expect("store operation should succeed"), 0);
807    }
808
809    #[test]
810    fn test_oxigraph_compat_insert_and_query() {
811        let store = Store::new().expect("construction should succeed");
812
813        // Create test quad
814        let subject = NamedNode::new("http://example.org/subject").expect("valid IRI");
815        let predicate = NamedNode::new("http://example.org/predicate").expect("valid IRI");
816        let object = Literal::new("test object");
817        let graph = NamedNode::new("http://example.org/graph").expect("valid IRI");
818
819        let quad = Quad::new(
820            subject.clone(),
821            predicate.clone(),
822            object.clone(),
823            graph.clone(),
824        );
825
826        // Insert quad
827        assert!(store
828            .insert(QuadRef::from(&quad))
829            .expect("store insert should succeed"));
830        assert_eq!(store.len().expect("store operation should succeed"), 1);
831        assert!(!store.is_empty().expect("store operation should succeed"));
832
833        // Check contains
834        assert!(store
835            .contains(QuadRef::from(&quad))
836            .expect("store contains should succeed"));
837
838        // Query by pattern
839        let quads: Vec<_> = store
840            .quads_for_pattern(
841                Some(SubjectRef::NamedNode(&subject)),
842                None::<PredicateRef>,
843                None::<ObjectRef>,
844                None::<GraphNameRef>,
845            )
846            .collect();
847        assert_eq!(quads.len(), 1);
848        assert_eq!(quads[0], quad);
849
850        // Remove quad
851        assert!(store
852            .remove(QuadRef::from(&quad))
853            .expect("store remove should succeed"));
854        assert!(store.is_empty().expect("store operation should succeed"));
855    }
856
857    #[test]
858    fn test_oxigraph_compat_extend() {
859        let store = Store::new().expect("construction should succeed");
860
861        let quads = [
862            Quad::new(
863                NamedNode::new("http://example.org/s1").expect("valid IRI"),
864                NamedNode::new("http://example.org/p1").expect("valid IRI"),
865                Literal::new("o1"),
866                GraphName::DefaultGraph,
867            ),
868            Quad::new(
869                NamedNode::new("http://example.org/s2").expect("valid IRI"),
870                NamedNode::new("http://example.org/p2").expect("valid IRI"),
871                Literal::new("o2"),
872                NamedNode::new("http://example.org/g1").expect("valid IRI"),
873            ),
874        ];
875
876        store
877            .extend(quads.iter().map(QuadRef::from))
878            .expect("extend should succeed");
879        assert_eq!(store.len().expect("store operation should succeed"), 2);
880    }
881
882    #[test]
883    fn test_oxigraph_compat_named_graphs() {
884        let store = Store::new().expect("construction should succeed");
885
886        // Create nodes
887        let s1 = NamedNode::new("http://example.org/s1").expect("valid IRI");
888        let s2 = NamedNode::new("http://example.org/s2").expect("valid IRI");
889        let p1 = NamedNode::new("http://example.org/p1").expect("valid IRI");
890        let p2 = NamedNode::new("http://example.org/p2").expect("valid IRI");
891        let o1 = Literal::new("o1");
892        let o2 = Literal::new("o2");
893        let g1 = NamedNode::new("http://example.org/g1").expect("valid IRI");
894        let g2 = NamedNode::new("http://example.org/g2").expect("valid IRI");
895
896        // Insert quads in different graphs
897        store
898            .insert(QuadRef::new(
899                SubjectRef::NamedNode(&s1),
900                PredicateRef::NamedNode(&p1),
901                ObjectRef::Literal(&o1),
902                GraphNameRef::NamedNode(&g1),
903            ))
904            .expect("operation should succeed");
905
906        store
907            .insert(QuadRef::new(
908                SubjectRef::NamedNode(&s2),
909                PredicateRef::NamedNode(&p2),
910                ObjectRef::Literal(&o2),
911                GraphNameRef::NamedNode(&g2),
912            ))
913            .expect("operation should succeed");
914
915        // Check named graphs
916        let graphs: Vec<_> = store.named_graphs().collect();
917        assert_eq!(graphs.len(), 2);
918        assert!(graphs.contains(&g1));
919        assert!(graphs.contains(&g2));
920
921        // Check contains_named_graph
922        assert!(store
923            .contains_named_graph(NamedOrBlankNodeRef::NamedNode(&g1))
924            .expect("operation should succeed"));
925        assert!(store
926            .contains_named_graph(NamedOrBlankNodeRef::NamedNode(&g2))
927            .expect("operation should succeed"));
928    }
929
930    #[test]
931    fn test_oxigraph_compat_clear_graph() {
932        let store = Store::new().expect("construction should succeed");
933
934        // Create nodes
935        let s1 = NamedNode::new("http://example.org/s1").expect("valid IRI");
936        let s2 = NamedNode::new("http://example.org/s2").expect("valid IRI");
937        let p1 = NamedNode::new("http://example.org/p1").expect("valid IRI");
938        let p2 = NamedNode::new("http://example.org/p2").expect("valid IRI");
939        let o1 = Literal::new("o1");
940        let o2 = Literal::new("o2");
941        let graph = NamedNode::new("http://example.org/graph").expect("valid IRI");
942
943        // Add quads to specific graph and default graph
944        store
945            .insert(QuadRef::new(
946                SubjectRef::NamedNode(&s1),
947                PredicateRef::NamedNode(&p1),
948                ObjectRef::Literal(&o1),
949                GraphNameRef::NamedNode(&graph),
950            ))
951            .expect("operation should succeed");
952
953        store
954            .insert(QuadRef::new(
955                SubjectRef::NamedNode(&s2),
956                PredicateRef::NamedNode(&p2),
957                ObjectRef::Literal(&o2),
958                GraphNameRef::DefaultGraph,
959            ))
960            .expect("operation should succeed");
961
962        assert_eq!(store.len().expect("store operation should succeed"), 2);
963
964        // Clear specific graph
965        store
966            .clear_graph(GraphNameRef::NamedNode(&graph))
967            .expect("clear_graph should succeed");
968        assert_eq!(store.len().expect("store operation should succeed"), 1); // Only default graph quad remains
969
970        // Clear all
971        store.clear().expect("store operation should succeed");
972        assert!(store.is_empty().expect("store operation should succeed"));
973    }
974
975    #[test]
976    fn test_oxigraph_compat_load_from_reader() {
977        let store = Store::new().expect("construction should succeed");
978
979        let turtle_data = r#"
980            @prefix ex: <http://example.org/> .
981            ex:subject ex:predicate "object" .
982        "#;
983
984        let reader = Cursor::new(turtle_data.as_bytes());
985        store
986            .load_from_reader(
987                reader,
988                RdfFormat::Turtle,
989                Some("http://example.org/"),
990                None::<GraphName>,
991            )
992            .expect("operation should succeed");
993
994        assert_eq!(store.len().expect("store operation should succeed"), 1);
995
996        // Verify the loaded data
997        let quads: Vec<_> = store.iter().collect();
998        assert_eq!(quads.len(), 1);
999        assert_eq!(
1000            quads[0].subject().to_string(),
1001            "<http://example.org/subject>"
1002        );
1003        assert_eq!(
1004            quads[0].predicate().to_string(),
1005            "<http://example.org/predicate>"
1006        );
1007    }
1008
1009    #[test]
1010    fn test_oxigraph_compat_dump_to_writer() {
1011        let store = Store::new().expect("construction should succeed");
1012
1013        // Create nodes
1014        let subject = NamedNode::new("http://example.org/subject").expect("valid IRI");
1015        let predicate = NamedNode::new("http://example.org/predicate").expect("valid IRI");
1016        let object = Literal::new("object");
1017
1018        // Add some test data
1019        store
1020            .insert(QuadRef::new(
1021                SubjectRef::NamedNode(&subject),
1022                PredicateRef::NamedNode(&predicate),
1023                ObjectRef::Literal(&object),
1024                GraphNameRef::DefaultGraph,
1025            ))
1026            .expect("operation should succeed");
1027
1028        // Dump to N-Triples format
1029        let mut output = Vec::new();
1030        store
1031            .dump_to_writer(&mut output, RdfFormat::NTriples, None::<GraphNameRef>)
1032            .expect("operation should succeed");
1033
1034        let result = String::from_utf8(output).expect("bytes should be valid UTF-8");
1035        assert!(result.contains("<http://example.org/subject>"));
1036        assert!(result.contains("<http://example.org/predicate>"));
1037        assert!(result.contains("\"object\""));
1038    }
1039
1040    #[test]
1041    fn regression_flush_persists_to_disk() {
1042        // flush() must delegate to the inner store and make writes durable, not
1043        // be a silent no-op. Insert, flush, reopen, and verify data survived.
1044        let dir =
1045            std::env::temp_dir().join(format!("oxirs_flush_regression_{}", uuid::Uuid::new_v4()));
1046
1047        let subject = NamedNode::new("http://example.org/s").expect("valid IRI");
1048        let predicate = NamedNode::new("http://example.org/p").expect("valid IRI");
1049        let object = Literal::new("durable");
1050        let quad = Quad::new(
1051            subject.clone(),
1052            predicate.clone(),
1053            object.clone(),
1054            GraphName::DefaultGraph,
1055        );
1056
1057        {
1058            let store = Store::open(&dir).expect("open persistent store");
1059            store
1060                .insert(QuadRef::from(&quad))
1061                .expect("insert should succeed");
1062            // The whole point of flush(): force durability. Must return Ok.
1063            store.flush().expect("flush should succeed and be durable");
1064        }
1065
1066        // Reopen from the same path: the flushed quad must be present.
1067        {
1068            let reopened = Store::open(&dir).expect("reopen persistent store");
1069            assert!(
1070                reopened
1071                    .contains(QuadRef::from(&quad))
1072                    .expect("contains should succeed"),
1073                "flushed quad was lost after reopen"
1074            );
1075        }
1076
1077        let _ = std::fs::remove_dir_all(&dir);
1078    }
1079
1080    #[test]
1081    fn regression_flush_in_memory_is_ok() {
1082        // flush() on an in-memory store must succeed (not error), delegating to
1083        // the inner store's genuine no-op path.
1084        let store = Store::new().expect("construction should succeed");
1085        store.flush().expect("in-memory flush should be Ok");
1086    }
1087}