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            match s_ref {
267                SubjectRef::NamedNode(n) => Subject::NamedNode(n.to_owned()),
268                SubjectRef::BlankNode(b) => Subject::BlankNode(b.to_owned()),
269                SubjectRef::Variable(v) => Subject::Variable(v.to_owned()),
270            }
271        });
272
273        let predicate = predicate.map(|p| {
274            let p_ref = p.into();
275            match p_ref {
276                PredicateRef::NamedNode(n) => Predicate::NamedNode(n.to_owned()),
277                PredicateRef::Variable(v) => Predicate::Variable(v.to_owned()),
278            }
279        });
280
281        let object = object.map(|o| {
282            let o_ref = o.into();
283            match o_ref {
284                ObjectRef::NamedNode(n) => Object::NamedNode(n.to_owned()),
285                ObjectRef::BlankNode(b) => Object::BlankNode(b.to_owned()),
286                ObjectRef::Literal(l) => Object::Literal(l.to_owned()),
287                ObjectRef::Variable(v) => Object::Variable(v.to_owned()),
288            }
289        });
290
291        let graph_name = graph_name.map(|g| {
292            let g_ref = g.into();
293            g_ref.to_owned()
294        });
295
296        // Query the inner store
297        let quads = match self.inner.read() {
298            Ok(store) => store
299                .query_quads(
300                    subject.as_ref(),
301                    predicate.as_ref(),
302                    object.as_ref(),
303                    graph_name.as_ref(),
304                )
305                .unwrap_or_default(),
306            _ => Vec::new(),
307        };
308
309        QuadIter { quads, index: 0 }
310    }
311
312    /// Returns an iterator over all quads in the store
313    pub fn iter(&self) -> QuadIter {
314        self.quads_for_pattern(
315            None::<SubjectRef>,
316            None::<PredicateRef>,
317            None::<ObjectRef>,
318            None::<GraphNameRef>,
319        )
320    }
321
322    /// Returns all named graphs in the store
323    pub fn named_graphs(&self) -> GraphNameIter {
324        // Collect unique graph names from all quads
325        let mut graph_names = std::collections::HashSet::new();
326        if let Ok(store) = self.inner.read() {
327            if let Ok(quads) = store.iter_quads() {
328                for quad in quads {
329                    if let GraphName::NamedNode(n) = quad.graph_name() {
330                        graph_names.insert(n.clone());
331                    }
332                }
333            }
334        }
335
336        GraphNameIter {
337            graphs: graph_names.into_iter().collect(),
338            index: 0,
339        }
340    }
341
342    /// Checks if the store contains a given named graph
343    pub fn contains_named_graph<'a>(
344        &self,
345        graph_name: impl Into<NamedOrBlankNodeRef<'a>>,
346    ) -> Result<bool> {
347        let graph_ref = graph_name.into();
348        let graph = match graph_ref {
349            NamedOrBlankNodeRef::NamedNode(n) => GraphName::NamedNode(n.to_owned()),
350            NamedOrBlankNodeRef::BlankNode(b) => GraphName::BlankNode(b.to_owned()),
351        };
352
353        // Check if any quads exist in this graph
354        let store = self
355            .inner
356            .read()
357            .map_err(|e| OxirsError::Store(format!("Failed to acquire read lock: {e}")))?;
358        let quads = store.query_quads(None, None, None, Some(&graph))?;
359        Ok(!quads.is_empty())
360    }
361
362    /// Clears the store
363    pub fn clear(&self) -> Result<()> {
364        let mut store = self
365            .inner
366            .write()
367            .map_err(|e| OxirsError::Store(format!("Failed to acquire write lock: {e}")))?;
368        store.clear()
369    }
370
371    /// Clears a specific graph
372    pub fn clear_graph<'a>(&self, graph_name: impl Into<GraphNameRef<'a>>) -> Result<()> {
373        let graph_ref = graph_name.into();
374        let graph = graph_ref.to_owned();
375
376        let store = self
377            .inner
378            .write()
379            .map_err(|e| OxirsError::Store(format!("Failed to acquire write lock: {e}")))?;
380
381        // Get all quads in the specified graph
382        let quads_to_remove = store.query_quads(None, None, None, Some(&graph))?;
383
384        // Remove each quad
385        for quad in quads_to_remove {
386            store.remove_quad(&quad)?;
387        }
388
389        Ok(())
390    }
391
392    /// Executes a SPARQL query
393    pub fn query(&self, query: &str) -> Result<QueryResults> {
394        let store = self
395            .inner
396            .read()
397            .map_err(|e| OxirsError::Store(format!("Failed to acquire read lock: {e}")))?;
398        let results = store.query(query)?;
399        Ok(QueryResults { inner: results })
400    }
401
402    /// Executes a SPARQL update
403    pub fn update(&self, update_str: &str) -> Result<()> {
404        use crate::query::{UpdateExecutor, UpdateParser};
405
406        // Parse the UPDATE string
407        let parser = UpdateParser::new();
408        let update = parser.parse(update_str)?;
409
410        // Get write access to the store
411        let store = self
412            .inner
413            .write()
414            .map_err(|e| OxirsError::Store(format!("Failed to acquire write lock: {e}")))?;
415
416        // Execute the update
417        let executor = UpdateExecutor::new(&*store);
418        executor.execute(&update)?;
419
420        Ok(())
421    }
422
423    /// Creates a transaction for the store
424    ///
425    /// This method provides ACID transaction support with automatic commit/abort handling.
426    /// The transaction uses Snapshot isolation level by default.
427    ///
428    /// # Example
429    ///
430    /// ```ignore
431    /// store.transaction(|tx| {
432    ///     // Perform transactional operations
433    ///     Ok(())
434    /// })?;
435    /// ```
436    pub fn transaction<T, E>(
437        &self,
438        f: impl FnOnce(&mut crate::AcidTransaction) -> std::result::Result<T, E>,
439    ) -> std::result::Result<T, E>
440    where
441        E: From<OxirsError>,
442    {
443        // Ensure TransactionManager is initialized
444        self.ensure_tx_manager()?;
445
446        // Get the transaction manager
447        let mut tx_mgr_guard = self
448            .tx_manager
449            .write()
450            .map_err(|e| E::from(OxirsError::Store(format!("Failed to acquire lock: {e}"))))?;
451
452        let tx_mgr = tx_mgr_guard.as_mut().ok_or_else(|| {
453            E::from(OxirsError::Store(
454                "Transaction manager not initialized".to_string(),
455            ))
456        })?;
457
458        // Begin a transaction with Snapshot isolation
459        let mut transaction = tx_mgr.begin(IsolationLevel::Snapshot).map_err(E::from)?;
460
461        // Execute the user function
462        let result = f(&mut transaction);
463
464        // Commit the transaction if the function succeeded
465        match result {
466            Ok(value) => {
467                // Snapshot the pending operations before `commit` consumes the
468                // transaction, so they can be applied to the visible store.
469                let inserts = transaction.pending_inserts().to_vec();
470                let deletes = transaction.pending_deletes().to_vec();
471
472                // Durability first: write and fsync the WAL commit record.
473                transaction.commit().map_err(E::from)?;
474
475                // Redo the committed changes into the backing store. The WAL
476                // commit record is already durable, so a crash here is
477                // recoverable by replaying the log on the next open.
478                let store = self.inner.write().map_err(|e| {
479                    E::from(OxirsError::Store(format!(
480                        "Failed to acquire write lock: {e}"
481                    )))
482                })?;
483                for quad in &deletes {
484                    store.remove_quad(quad).map_err(E::from)?;
485                }
486                for quad in inserts {
487                    store.insert_quad(quad).map_err(E::from)?;
488                }
489                Ok(value)
490            }
491            Err(error) => {
492                let _ = transaction.abort();
493                Err(error)
494            }
495        }
496    }
497
498    /// Ensures the transaction manager is initialized
499    fn ensure_tx_manager(&self) -> Result<()> {
500        let mut tx_mgr_guard = self
501            .tx_manager
502            .write()
503            .map_err(|e| OxirsError::Store(format!("Failed to acquire lock: {e}")))?;
504
505        if tx_mgr_guard.is_none() {
506            // Determine WAL directory
507            let wal_dir = if let Some(ref wal_path) = self.wal_dir {
508                wal_path.clone()
509            } else {
510                // In-memory stores get a unique per-instance WAL directory so
511                // that concurrent Store instances in the same process (or on
512                // the same host) never interleave writes into a shared log.
513                std::env::temp_dir().join(format!("oxirs_wal_{}", uuid::Uuid::new_v4()))
514            };
515
516            // Create the transaction manager
517            let tx_mgr = TransactionManager::new(&wal_dir)?;
518            *tx_mgr_guard = Some(tx_mgr);
519        }
520
521        Ok(())
522    }
523
524    /// Validates the store integrity
525    ///
526    /// Performs a consistency check between the store's reported quad count and
527    /// the number of quads materialized from its indexes. A mismatch indicates
528    /// index/data corruption and is surfaced as an error.
529    pub fn validate(&self) -> Result<()> {
530        let store = self
531            .inner
532            .read()
533            .map_err(|e| OxirsError::Store(format!("Failed to acquire read lock: {e}")))?;
534
535        let reported = OxirsStoreTrait::len(&*store)?;
536        let materialized = OxirsStoreTrait::quads(&*store)?.len();
537        if reported != materialized {
538            return Err(OxirsError::Store(format!(
539                "Store consistency check failed: len() reports {reported} quads but {materialized} were found in the indexes"
540            )));
541        }
542
543        Ok(())
544    }
545
546    /// Optimizes the store layout
547    pub fn optimize(&self) -> Result<()> {
548        // Trigger arena cleanup if using ultra-performance mode
549        let store = self
550            .inner
551            .read()
552            .map_err(|e| OxirsError::Store(format!("Failed to acquire read lock: {e}")))?;
553        store.clear_arena();
554        Ok(())
555    }
556
557    /// Backs up the store to a path
558    pub fn backup<P: AsRef<Path>>(&self, path: P) -> Result<()> {
559        use crate::parser::RdfFormat;
560        use crate::serializer::Serializer;
561        use std::fs::File;
562        use std::io::Write;
563        use std::time::{SystemTime, UNIX_EPOCH};
564
565        let backup_path = path.as_ref();
566
567        // Create backup directory if it doesn't exist
568        if let Some(parent) = backup_path.parent() {
569            std::fs::create_dir_all(parent).map_err(|e| {
570                OxirsError::Store(format!("Failed to create backup directory: {e}"))
571            })?;
572        }
573
574        // Generate backup filename with timestamp
575        let timestamp = SystemTime::now()
576            .duration_since(UNIX_EPOCH)
577            .unwrap_or_default()
578            .as_secs();
579
580        let backup_file_path = if backup_path.is_dir() {
581            backup_path.join(format!("oxirs_backup_{timestamp}.nq"))
582        } else {
583            backup_path.to_path_buf()
584        };
585
586        // Get read lock on the store
587        let store = self
588            .inner
589            .read()
590            .map_err(|e| OxirsError::Store(format!("Failed to acquire read lock: {e}")))?;
591
592        // Get all quads from the store
593        let quads = store
594            .iter_quads()
595            .map_err(|e| OxirsError::Store(format!("Failed to iterate quads: {e}")))?;
596
597        // Create dataset from quads
598        let dataset = crate::model::dataset::Dataset::from_iter(quads.clone());
599
600        // Serialize to N-Quads format (most portable and complete format)
601        let serializer = Serializer::new(RdfFormat::NQuads);
602        let serialized_data = serializer
603            .serialize_dataset(&dataset)
604            .map_err(|e| OxirsError::Store(format!("Failed to serialize dataset: {e}")))?;
605
606        // Write to backup file
607        let mut backup_file = File::create(&backup_file_path)
608            .map_err(|e| OxirsError::Store(format!("Failed to create backup file: {e}")))?;
609
610        backup_file
611            .write_all(serialized_data.as_bytes())
612            .map_err(|e| OxirsError::Store(format!("Failed to write backup data: {e}")))?;
613
614        backup_file
615            .sync_all()
616            .map_err(|e| OxirsError::Store(format!("Failed to sync backup file: {e}")))?;
617
618        // Calculate backup size for logging
619        let backup_size = serialized_data.len();
620        let quad_count = quads.len();
621
622        tracing::info!(
623            "Store backup completed successfully. File: {}, Quads: {}, Size: {} bytes",
624            backup_file_path.display(),
625            quad_count,
626            backup_size
627        );
628
629        Ok(())
630    }
631
632    /// Flushes any pending changes to disk
633    pub fn flush(&self) -> Result<()> {
634        // No-op for now as OxiRS doesn't buffer writes
635        Ok(())
636    }
637}
638
639impl Default for Store {
640    fn default() -> Self {
641        Store::new().expect("Store::new() should not fail")
642    }
643}
644
645/// Iterator over quads (Oxigraph-compatible)
646pub struct QuadIter {
647    quads: Vec<Quad>,
648    index: usize,
649}
650
651impl Iterator for QuadIter {
652    type Item = Quad;
653
654    fn next(&mut self) -> Option<Self::Item> {
655        if self.index < self.quads.len() {
656            let quad = self.quads[self.index].clone();
657            self.index += 1;
658            Some(quad)
659        } else {
660            None
661        }
662    }
663}
664
665/// Iterator over graph names (Oxigraph-compatible)
666pub struct GraphNameIter {
667    graphs: Vec<NamedNode>,
668    index: usize,
669}
670
671impl Iterator for GraphNameIter {
672    type Item = NamedNode;
673
674    fn next(&mut self) -> Option<Self::Item> {
675        if self.index < self.graphs.len() {
676            let graph = self.graphs[self.index].clone();
677            self.index += 1;
678            Some(graph)
679        } else {
680            None
681        }
682    }
683}
684
685/// Oxigraph-compatible query results
686pub struct QueryResults {
687    #[allow(dead_code)]
688    inner: OxirsQueryResults,
689}
690
691impl QueryResults {
692    /// Returns true if the results are a boolean
693    pub fn is_boolean(&self) -> bool {
694        matches!(
695            self.inner.results(),
696            crate::rdf_store::types::QueryResults::Boolean(_)
697        )
698    }
699
700    /// Returns the boolean value if the results are a boolean
701    pub fn boolean(&self) -> Option<bool> {
702        match self.inner.results() {
703            crate::rdf_store::types::QueryResults::Boolean(b) => Some(*b),
704            _ => None,
705        }
706    }
707
708    /// Returns true if the results are solutions
709    pub fn is_solutions(&self) -> bool {
710        matches!(
711            self.inner.results(),
712            crate::rdf_store::types::QueryResults::Bindings(_)
713        )
714    }
715
716    /// Returns true if the results are a graph
717    pub fn is_graph(&self) -> bool {
718        matches!(
719            self.inner.results(),
720            crate::rdf_store::types::QueryResults::Graph(_)
721        )
722    }
723}
724
725/// Oxigraph-compatible transaction
726///
727/// Note: This is a placeholder implementation. Full transactional support
728/// would require implementing proper transaction isolation in OxiRS.
729pub struct Transaction {
730    // Placeholder for future transaction implementation
731    operations: Vec<TransactionOp>,
732}
733
734enum TransactionOp {
735    #[allow(dead_code)]
736    Insert(Quad),
737    #[allow(dead_code)]
738    Remove(Quad),
739}
740
741impl Transaction {
742    #[allow(dead_code)]
743    fn new() -> Self {
744        Transaction {
745            operations: Vec::new(),
746        }
747    }
748
749    /// Inserts a quad in the transaction
750    pub fn insert<'b>(&mut self, quad: impl Into<QuadRef<'b>>) -> Result<bool> {
751        let quad_ref = quad.into();
752        let quad = Quad::new(
753            quad_ref.subject().to_owned(),
754            quad_ref.predicate().to_owned(),
755            quad_ref.object().to_owned(),
756            quad_ref.graph_name().to_owned(),
757        );
758        self.operations.push(TransactionOp::Insert(quad));
759        Ok(true) // Optimistically return true
760    }
761
762    /// Removes a quad in the transaction
763    pub fn remove<'b>(&mut self, quad: impl Into<QuadRef<'b>>) -> Result<bool> {
764        let quad_ref = quad.into();
765        let quad = Quad::new(
766            quad_ref.subject().to_owned(),
767            quad_ref.predicate().to_owned(),
768            quad_ref.object().to_owned(),
769            quad_ref.graph_name().to_owned(),
770        );
771        self.operations.push(TransactionOp::Remove(quad));
772        Ok(true) // Optimistically return true
773    }
774}
775
776/// Oxigraph-compatible error type
777#[derive(Debug, thiserror::Error)]
778pub enum OxigraphCompatError {
779    #[error("Store error: {0}")]
780    Store(String),
781    #[error("Parse error: {0}")]
782    Parse(String),
783    #[error("IO error: {0}")]
784    Io(#[from] std::io::Error),
785}
786
787impl From<OxirsError> for OxigraphCompatError {
788    fn from(err: OxirsError) -> Self {
789        match err {
790            OxirsError::Store(msg) => OxigraphCompatError::Store(msg),
791            OxirsError::Parse(msg) => OxigraphCompatError::Parse(msg),
792            _ => OxigraphCompatError::Store(err.to_string()),
793        }
794    }
795}
796
797#[cfg(test)]
798mod tests {
799    use super::*;
800    use crate::model::{Literal, NamedNode};
801    use crate::parser::RdfFormat;
802    use std::io::Cursor;
803
804    #[test]
805    fn test_oxigraph_compat_store_creation() {
806        let store = Store::new().expect("construction should succeed");
807        assert!(store.is_empty().expect("store operation should succeed"));
808        assert_eq!(store.len().expect("store operation should succeed"), 0);
809    }
810
811    #[test]
812    fn test_oxigraph_compat_insert_and_query() {
813        let store = Store::new().expect("construction should succeed");
814
815        // Create test quad
816        let subject = NamedNode::new("http://example.org/subject").expect("valid IRI");
817        let predicate = NamedNode::new("http://example.org/predicate").expect("valid IRI");
818        let object = Literal::new("test object");
819        let graph = NamedNode::new("http://example.org/graph").expect("valid IRI");
820
821        let quad = Quad::new(
822            subject.clone(),
823            predicate.clone(),
824            object.clone(),
825            graph.clone(),
826        );
827
828        // Insert quad
829        assert!(store
830            .insert(QuadRef::from(&quad))
831            .expect("store insert should succeed"));
832        assert_eq!(store.len().expect("store operation should succeed"), 1);
833        assert!(!store.is_empty().expect("store operation should succeed"));
834
835        // Check contains
836        assert!(store
837            .contains(QuadRef::from(&quad))
838            .expect("store contains should succeed"));
839
840        // Query by pattern
841        let quads: Vec<_> = store
842            .quads_for_pattern(
843                Some(SubjectRef::NamedNode(&subject)),
844                None::<PredicateRef>,
845                None::<ObjectRef>,
846                None::<GraphNameRef>,
847            )
848            .collect();
849        assert_eq!(quads.len(), 1);
850        assert_eq!(quads[0], quad);
851
852        // Remove quad
853        assert!(store
854            .remove(QuadRef::from(&quad))
855            .expect("store remove should succeed"));
856        assert!(store.is_empty().expect("store operation should succeed"));
857    }
858
859    #[test]
860    fn test_oxigraph_compat_extend() {
861        let store = Store::new().expect("construction should succeed");
862
863        let quads = [
864            Quad::new(
865                NamedNode::new("http://example.org/s1").expect("valid IRI"),
866                NamedNode::new("http://example.org/p1").expect("valid IRI"),
867                Literal::new("o1"),
868                GraphName::DefaultGraph,
869            ),
870            Quad::new(
871                NamedNode::new("http://example.org/s2").expect("valid IRI"),
872                NamedNode::new("http://example.org/p2").expect("valid IRI"),
873                Literal::new("o2"),
874                NamedNode::new("http://example.org/g1").expect("valid IRI"),
875            ),
876        ];
877
878        store
879            .extend(quads.iter().map(QuadRef::from))
880            .expect("extend should succeed");
881        assert_eq!(store.len().expect("store operation should succeed"), 2);
882    }
883
884    #[test]
885    fn test_oxigraph_compat_named_graphs() {
886        let store = Store::new().expect("construction should succeed");
887
888        // Create nodes
889        let s1 = NamedNode::new("http://example.org/s1").expect("valid IRI");
890        let s2 = NamedNode::new("http://example.org/s2").expect("valid IRI");
891        let p1 = NamedNode::new("http://example.org/p1").expect("valid IRI");
892        let p2 = NamedNode::new("http://example.org/p2").expect("valid IRI");
893        let o1 = Literal::new("o1");
894        let o2 = Literal::new("o2");
895        let g1 = NamedNode::new("http://example.org/g1").expect("valid IRI");
896        let g2 = NamedNode::new("http://example.org/g2").expect("valid IRI");
897
898        // Insert quads in different graphs
899        store
900            .insert(QuadRef::new(
901                SubjectRef::NamedNode(&s1),
902                PredicateRef::NamedNode(&p1),
903                ObjectRef::Literal(&o1),
904                GraphNameRef::NamedNode(&g1),
905            ))
906            .expect("operation should succeed");
907
908        store
909            .insert(QuadRef::new(
910                SubjectRef::NamedNode(&s2),
911                PredicateRef::NamedNode(&p2),
912                ObjectRef::Literal(&o2),
913                GraphNameRef::NamedNode(&g2),
914            ))
915            .expect("operation should succeed");
916
917        // Check named graphs
918        let graphs: Vec<_> = store.named_graphs().collect();
919        assert_eq!(graphs.len(), 2);
920        assert!(graphs.contains(&g1));
921        assert!(graphs.contains(&g2));
922
923        // Check contains_named_graph
924        assert!(store
925            .contains_named_graph(NamedOrBlankNodeRef::NamedNode(&g1))
926            .expect("operation should succeed"));
927        assert!(store
928            .contains_named_graph(NamedOrBlankNodeRef::NamedNode(&g2))
929            .expect("operation should succeed"));
930    }
931
932    #[test]
933    fn test_oxigraph_compat_clear_graph() {
934        let store = Store::new().expect("construction should succeed");
935
936        // Create nodes
937        let s1 = NamedNode::new("http://example.org/s1").expect("valid IRI");
938        let s2 = NamedNode::new("http://example.org/s2").expect("valid IRI");
939        let p1 = NamedNode::new("http://example.org/p1").expect("valid IRI");
940        let p2 = NamedNode::new("http://example.org/p2").expect("valid IRI");
941        let o1 = Literal::new("o1");
942        let o2 = Literal::new("o2");
943        let graph = NamedNode::new("http://example.org/graph").expect("valid IRI");
944
945        // Add quads to specific graph and default graph
946        store
947            .insert(QuadRef::new(
948                SubjectRef::NamedNode(&s1),
949                PredicateRef::NamedNode(&p1),
950                ObjectRef::Literal(&o1),
951                GraphNameRef::NamedNode(&graph),
952            ))
953            .expect("operation should succeed");
954
955        store
956            .insert(QuadRef::new(
957                SubjectRef::NamedNode(&s2),
958                PredicateRef::NamedNode(&p2),
959                ObjectRef::Literal(&o2),
960                GraphNameRef::DefaultGraph,
961            ))
962            .expect("operation should succeed");
963
964        assert_eq!(store.len().expect("store operation should succeed"), 2);
965
966        // Clear specific graph
967        store
968            .clear_graph(GraphNameRef::NamedNode(&graph))
969            .expect("clear_graph should succeed");
970        assert_eq!(store.len().expect("store operation should succeed"), 1); // Only default graph quad remains
971
972        // Clear all
973        store.clear().expect("store operation should succeed");
974        assert!(store.is_empty().expect("store operation should succeed"));
975    }
976
977    #[test]
978    fn test_oxigraph_compat_load_from_reader() {
979        let store = Store::new().expect("construction should succeed");
980
981        let turtle_data = r#"
982            @prefix ex: <http://example.org/> .
983            ex:subject ex:predicate "object" .
984        "#;
985
986        let reader = Cursor::new(turtle_data.as_bytes());
987        store
988            .load_from_reader(
989                reader,
990                RdfFormat::Turtle,
991                Some("http://example.org/"),
992                None::<GraphName>,
993            )
994            .expect("operation should succeed");
995
996        assert_eq!(store.len().expect("store operation should succeed"), 1);
997
998        // Verify the loaded data
999        let quads: Vec<_> = store.iter().collect();
1000        assert_eq!(quads.len(), 1);
1001        assert_eq!(
1002            quads[0].subject().to_string(),
1003            "<http://example.org/subject>"
1004        );
1005        assert_eq!(
1006            quads[0].predicate().to_string(),
1007            "<http://example.org/predicate>"
1008        );
1009    }
1010
1011    #[test]
1012    fn test_oxigraph_compat_dump_to_writer() {
1013        let store = Store::new().expect("construction should succeed");
1014
1015        // Create nodes
1016        let subject = NamedNode::new("http://example.org/subject").expect("valid IRI");
1017        let predicate = NamedNode::new("http://example.org/predicate").expect("valid IRI");
1018        let object = Literal::new("object");
1019
1020        // Add some test data
1021        store
1022            .insert(QuadRef::new(
1023                SubjectRef::NamedNode(&subject),
1024                PredicateRef::NamedNode(&predicate),
1025                ObjectRef::Literal(&object),
1026                GraphNameRef::DefaultGraph,
1027            ))
1028            .expect("operation should succeed");
1029
1030        // Dump to N-Triples format
1031        let mut output = Vec::new();
1032        store
1033            .dump_to_writer(&mut output, RdfFormat::NTriples, None::<GraphNameRef>)
1034            .expect("operation should succeed");
1035
1036        let result = String::from_utf8(output).expect("bytes should be valid UTF-8");
1037        assert!(result.contains("<http://example.org/subject>"));
1038        assert!(result.contains("<http://example.org/predicate>"));
1039        assert!(result.contains("\"object\""));
1040    }
1041}