1#[cfg(feature = "http-client")]
2use crate::io::{RdfFormat, RdfParser};
3use crate::model::{GraphName as OxGraphName, GraphNameRef, Quad as OxQuad};
4#[expect(deprecated)]
5use crate::sparql::algebra::Update;
6use crate::sparql::dataset::DatasetView;
7use crate::sparql::error::UpdateEvaluationError;
8#[cfg(feature = "http-client")]
9use crate::sparql::http::Client;
10use crate::storage::{Storage, StorageError, StorageReadableTransaction, StorageTransaction};
11use crate::store::{Store, Transaction};
12use oxiri::Iri;
13#[cfg(feature = "http-client")]
14use oxrdfio::LoadedDocument;
15use rustc_hash::FxHashMap;
16use spareval::{DeleteInsertQuad, QueryDatasetSpecification, QueryEvaluator};
17use spargebra::GraphUpdateOperation;
18use spargebra::algebra::{GraphPattern, GraphTarget};
19use spargebra::term::{
20 BlankNode, GraphName, GroundQuad, GroundQuadPattern, GroundTerm, NamedNode, NamedOrBlankNode,
21 Quad, QuadPattern, Term,
22};
23#[cfg(feature = "rdf-12")]
24use spargebra::term::{GroundTriple, Triple};
25#[cfg(feature = "http-client")]
26use std::io::Read;
27#[cfg(feature = "http-client")]
28use std::time::Duration;
29
30#[derive(Clone)]
44#[must_use]
45pub struct PreparedSparqlUpdate {
46 evaluator: QueryEvaluator,
47 update: spargebra::Update,
48 using_datasets: Vec<Option<QueryDatasetSpecification>>,
49 #[cfg(feature = "http-client")]
50 http_timeout: Option<Duration>,
51 #[cfg(feature = "http-client")]
52 http_redirection_limit: usize,
53}
54
55impl PreparedSparqlUpdate {
56 #[expect(deprecated)]
57 pub(crate) fn new(
58 evaluator: QueryEvaluator,
59 update: Update,
60 #[cfg(feature = "http-client")] http_timeout: Option<Duration>,
61 #[cfg(feature = "http-client")] http_redirection_limit: usize,
62 ) -> Self {
63 Self {
64 evaluator,
65 update: update.inner,
66 using_datasets: update.using_datasets,
67 #[cfg(feature = "http-client")]
68 http_timeout,
69 #[cfg(feature = "http-client")]
70 http_redirection_limit,
71 }
72 }
73
74 #[inline]
76 pub fn using_datasets(&self) -> impl Iterator<Item = &QueryDatasetSpecification> {
77 self.using_datasets.iter().filter_map(Option::as_ref)
78 }
79
80 #[inline]
82 pub fn using_datasets_mut(&mut self) -> impl Iterator<Item = &mut QueryDatasetSpecification> {
83 self.using_datasets.iter_mut().filter_map(Option::as_mut)
84 }
85
86 pub fn on_store(self, store: &Store) -> BoundPreparedSparqlUpdate<'_, '_> {
100 let transaction = if update_requires_read(&self.update) {
101 store
102 .storage()
103 .start_readable_transaction()
104 .map(UpdateTransaction::OwnedReadable)
105 } else {
106 let storage = store.storage();
107 storage
108 .start_transaction()
109 .map(|transaction| UpdateTransaction::Owned(transaction, storage))
110 };
111 BoundPreparedSparqlUpdate {
112 evaluator: self.evaluator,
113 update: self.update,
114 using_datasets: self.using_datasets,
115 #[cfg(feature = "http-client")]
116 http_timeout: self.http_timeout,
117 #[cfg(feature = "http-client")]
118 http_redirection_limit: self.http_redirection_limit,
119 transaction,
120 }
121 }
122
123 pub fn on_transaction<'a, 'b: 'a>(
139 self,
140 transaction: &'a mut Transaction<'b>,
141 ) -> BoundPreparedSparqlUpdate<'a, 'b> {
142 BoundPreparedSparqlUpdate {
143 evaluator: self.evaluator,
144 update: self.update,
145 using_datasets: self.using_datasets,
146 #[cfg(feature = "http-client")]
147 http_timeout: self.http_timeout,
148 #[cfg(feature = "http-client")]
149 http_redirection_limit: self.http_redirection_limit,
150 transaction: Ok(UpdateTransaction::BorrowedReadable(transaction.inner_mut())),
151 }
152 }
153}
154
155#[must_use]
173pub struct BoundPreparedSparqlUpdate<'a, 'b> {
174 evaluator: QueryEvaluator,
175 update: spargebra::Update,
176 using_datasets: Vec<Option<QueryDatasetSpecification>>,
177 #[cfg(feature = "http-client")]
178 http_timeout: Option<Duration>,
179 #[cfg(feature = "http-client")]
180 http_redirection_limit: usize,
181 transaction: Result<UpdateTransaction<'a, 'b>, StorageError>,
182}
183
184impl BoundPreparedSparqlUpdate<'_, '_> {
185 pub fn execute(self) -> Result<(), UpdateEvaluationError> {
187 match self.transaction? {
188 UpdateTransaction::OwnedReadable(mut transaction) => {
189 ReadableUpdateEvaluator {
190 transaction: &mut transaction,
191 base_iri: self.update.base_iri.clone(),
192 query_evaluator: self.evaluator,
193 #[cfg(feature = "http-client")]
194 client: Client::new(self.http_timeout, self.http_redirection_limit),
195 }
196 .eval_all(&self.update.operations, &self.using_datasets)?;
197 transaction.commit()?;
198 Ok(())
199 }
200 UpdateTransaction::BorrowedReadable(transaction) => ReadableUpdateEvaluator {
201 transaction,
202 base_iri: self.update.base_iri.clone(),
203 query_evaluator: self.evaluator,
204 #[cfg(feature = "http-client")]
205 client: Client::new(self.http_timeout, self.http_redirection_limit),
206 }
207 .eval_all(&self.update.operations, &self.using_datasets),
208 UpdateTransaction::Owned(mut transaction, storage) => {
209 WriteOnlyUpdateEvaluator {
210 transaction: &mut transaction,
211 storage_for_initial_read: Some(storage),
212 base_iri: self.update.base_iri.clone(),
213 query_evaluator: self.evaluator,
214 #[cfg(feature = "http-client")]
215 client: Client::new(self.http_timeout, self.http_redirection_limit),
216 }
217 .eval_all(&self.update.operations, &self.using_datasets)?;
218 transaction.commit()?;
219 Ok(())
220 }
221 }
222 }
223}
224
225enum UpdateTransaction<'a, 'b> {
226 OwnedReadable(StorageReadableTransaction<'b>),
227 BorrowedReadable(&'a mut StorageReadableTransaction<'b>),
228 Owned(StorageTransaction<'b>, &'b Storage),
229}
230
231struct ReadableUpdateEvaluator<'a, 'b> {
232 transaction: &'a mut StorageReadableTransaction<'b>,
233 base_iri: Option<Iri<String>>,
234 query_evaluator: QueryEvaluator,
235 #[cfg(feature = "http-client")]
236 client: Client,
237}
238
239impl<'a, 'b: 'a> ReadableUpdateEvaluator<'a, 'b> {
240 fn eval_all(
241 &mut self,
242 updates: &[GraphUpdateOperation],
243 using_datasets: &[Option<QueryDatasetSpecification>],
244 ) -> Result<(), UpdateEvaluationError> {
245 for (update, using_dataset) in updates.iter().zip(using_datasets) {
246 self.eval(update, using_dataset)?;
247 }
248 Ok(())
249 }
250
251 fn eval(
252 &mut self,
253 update: &GraphUpdateOperation,
254 using_dataset: &Option<QueryDatasetSpecification>,
255 ) -> Result<(), UpdateEvaluationError> {
256 match update {
257 GraphUpdateOperation::InsertData { data } => {
258 self.eval_insert_data(data);
259 Ok(())
260 }
261 GraphUpdateOperation::DeleteData { data } => {
262 self.eval_delete_data(data);
263 Ok(())
264 }
265 GraphUpdateOperation::DeleteInsert {
266 delete,
267 insert,
268 pattern,
269 ..
270 } => self.eval_delete_insert(
271 delete,
272 insert,
273 using_dataset
274 .as_ref()
275 .unwrap_or(&QueryDatasetSpecification::new()),
276 pattern,
277 ),
278 GraphUpdateOperation::Load {
279 silent,
280 source,
281 destination,
282 } => {
283 if let Err(error) = self.eval_load(source, destination) {
284 if *silent { Ok(()) } else { Err(error) }
285 } else {
286 Ok(())
287 }
288 }
289 GraphUpdateOperation::Clear { graph, silent } => self.eval_clear(graph, *silent),
290 GraphUpdateOperation::Create { graph, silent } => self.eval_create(graph, *silent),
291 GraphUpdateOperation::Drop { graph, silent } => self.eval_drop(graph, *silent),
292 }
293 }
294
295 fn eval_insert_data(&mut self, data: &[Quad]) {
296 let mut bnodes = FxHashMap::default();
297 for quad in data {
298 let quad = convert_quad(quad, &mut bnodes);
299 self.transaction.insert(quad.as_ref());
300 }
301 }
302
303 fn eval_delete_data(&mut self, data: &[GroundQuad]) {
304 for quad in data {
305 let quad = convert_ground_quad(quad);
306 self.transaction.remove(quad.as_ref());
307 }
308 }
309
310 fn eval_delete_insert(
311 &mut self,
312 delete: &[GroundQuadPattern],
313 insert: &[QuadPattern],
314 using: &QueryDatasetSpecification,
315 algebra: &GraphPattern,
316 ) -> Result<(), UpdateEvaluationError> {
317 let mut prepared = self.query_evaluator.prepare_delete_insert(
318 delete.to_vec(),
319 insert.to_vec(),
320 self.base_iri.clone(),
321 None,
322 algebra,
323 );
324 *prepared.dataset_mut() = using.clone();
325 let mutations = prepared
326 .execute(DatasetView::new(self.transaction.reader()))?
327 .collect::<Result<Vec<_>, _>>()?;
328 for mutation in mutations {
329 match mutation {
330 DeleteInsertQuad::Delete(quad) => self.transaction.remove(quad.as_ref()),
331 DeleteInsertQuad::Insert(quad) => self.transaction.insert(quad.as_ref()),
332 }
333 }
334 Ok(())
335 }
336
337 fn eval_load(&mut self, from: &NamedNode, to: &GraphName) -> Result<(), UpdateEvaluationError> {
338 eval_load(
339 from,
340 to,
341 #[cfg(feature = "http-client")]
342 &self.client,
343 |q| self.transaction.insert(q.as_ref()),
344 )
345 }
346
347 fn eval_create(
348 &mut self,
349 graph_name: &NamedNode,
350 silent: bool,
351 ) -> Result<(), UpdateEvaluationError> {
352 if self
353 .transaction
354 .reader()
355 .contains_named_graph(&graph_name.as_ref().into())?
356 {
357 if silent {
358 Ok(())
359 } else {
360 Err(UpdateEvaluationError::GraphAlreadyExists(
361 graph_name.clone(),
362 ))
363 }
364 } else {
365 self.transaction.insert_named_graph(graph_name.into());
366 Ok(())
367 }
368 }
369
370 fn eval_clear(
371 &mut self,
372 graph: &GraphTarget,
373 silent: bool,
374 ) -> Result<(), UpdateEvaluationError> {
375 match graph {
376 GraphTarget::NamedNode(graph_name) => {
377 if self
378 .transaction
379 .reader()
380 .contains_named_graph(&graph_name.as_ref().into())?
381 {
382 Ok(self.transaction.clear_graph(graph_name.into())?)
383 } else if silent {
384 Ok(())
385 } else {
386 Err(UpdateEvaluationError::GraphDoesNotExist(graph_name.clone()))
387 }
388 }
389 GraphTarget::DefaultGraph => {
390 self.transaction.clear_graph(GraphNameRef::DefaultGraph)?;
391 Ok(())
392 }
393 GraphTarget::NamedGraphs => Ok(self.transaction.clear_all_named_graphs()?),
394 GraphTarget::AllGraphs => Ok(self.transaction.clear_all_graphs()?),
395 }
396 }
397
398 fn eval_drop(
399 &mut self,
400 graph: &GraphTarget,
401 silent: bool,
402 ) -> Result<(), UpdateEvaluationError> {
403 match graph {
404 GraphTarget::NamedNode(graph_name) => {
405 if self
406 .transaction
407 .reader()
408 .contains_named_graph(&graph_name.as_ref().into())?
409 {
410 self.transaction.remove_named_graph(graph_name.into())?;
411 Ok(())
412 } else if silent {
413 Ok(())
414 } else {
415 Err(UpdateEvaluationError::GraphDoesNotExist(graph_name.clone()))
416 }
417 }
418 GraphTarget::DefaultGraph => {
419 Ok(self.transaction.clear_graph(GraphNameRef::DefaultGraph)?)
420 }
421 GraphTarget::NamedGraphs => Ok(self.transaction.remove_all_named_graphs()?),
422 GraphTarget::AllGraphs => Ok(self.transaction.clear()?),
423 }
424 }
425}
426
427fn update_requires_read(update: &spargebra::Update) -> bool {
428 for (i, op) in update.operations.iter().enumerate() {
429 match op {
430 GraphUpdateOperation::InsertData { .. }
431 | GraphUpdateOperation::DeleteData { .. }
432 | GraphUpdateOperation::Create { silent: true, .. }
433 | GraphUpdateOperation::Clear {
434 graph: GraphTarget::DefaultGraph | GraphTarget::NamedGraphs | GraphTarget::AllGraphs,
435 ..
436 }
437 | GraphUpdateOperation::Drop {
438 graph: GraphTarget::DefaultGraph | GraphTarget::NamedGraphs | GraphTarget::AllGraphs,
439 ..
440 } => (),
441 GraphUpdateOperation::DeleteInsert { .. } if i == 0 => (),
442 _ => return true,
443 }
444 }
445 false
446}
447
448struct WriteOnlyUpdateEvaluator<'a, 'b> {
449 transaction: &'a mut StorageTransaction<'b>,
450 storage_for_initial_read: Option<&'b Storage>,
451 base_iri: Option<Iri<String>>,
452 query_evaluator: QueryEvaluator,
453 #[cfg(feature = "http-client")]
454 client: Client,
455}
456
457impl WriteOnlyUpdateEvaluator<'_, '_> {
458 fn eval_all(
459 &mut self,
460 updates: &[GraphUpdateOperation],
461 using_datasets: &[Option<QueryDatasetSpecification>],
462 ) -> Result<(), UpdateEvaluationError> {
463 for (update, using_dataset) in updates.iter().zip(using_datasets) {
464 self.eval(update, using_dataset)?;
465 self.storage_for_initial_read.take(); }
467 Ok(())
468 }
469
470 fn eval(
471 &mut self,
472 update: &GraphUpdateOperation,
473 using_dataset: &Option<QueryDatasetSpecification>,
474 ) -> Result<(), UpdateEvaluationError> {
475 match update {
476 GraphUpdateOperation::InsertData { data } => {
477 self.eval_insert_data(data);
478 Ok(())
479 }
480 GraphUpdateOperation::DeleteData { data } => {
481 self.eval_delete_data(data);
482 Ok(())
483 }
484 GraphUpdateOperation::DeleteInsert {
485 delete,
486 insert,
487 pattern,
488 ..
489 } => self.eval_delete_insert(
490 delete,
491 insert,
492 using_dataset
493 .as_ref()
494 .unwrap_or(&QueryDatasetSpecification::new()),
495 pattern,
496 ),
497 GraphUpdateOperation::Load {
498 silent,
499 source,
500 destination,
501 } => {
502 if let Err(error) = self.eval_load(source, destination) {
503 if *silent { Ok(()) } else { Err(error) }
504 } else {
505 Ok(())
506 }
507 }
508 GraphUpdateOperation::Clear { graph, silent } => self.eval_clear(graph, *silent),
509 GraphUpdateOperation::Create { graph, silent } => self.eval_create(graph, *silent),
510 GraphUpdateOperation::Drop { graph, silent } => self.eval_drop(graph, *silent),
511 }
512 }
513
514 fn eval_insert_data(&mut self, data: &[Quad]) {
515 let mut bnodes = FxHashMap::default();
516 for quad in data {
517 let quad = convert_quad(quad, &mut bnodes);
518 self.transaction.insert(quad.as_ref());
519 }
520 }
521
522 fn eval_delete_data(&mut self, data: &[GroundQuad]) {
523 for quad in data {
524 let quad = convert_ground_quad(quad);
525 self.transaction.remove(quad.as_ref());
526 }
527 }
528
529 fn eval_delete_insert(
530 &mut self,
531 delete: &[GroundQuadPattern],
532 insert: &[QuadPattern],
533 using: &QueryDatasetSpecification,
534 algebra: &GraphPattern,
535 ) -> Result<(), UpdateEvaluationError> {
536 let Some(storage) = self.storage_for_initial_read.take() else {
537 return Err(UpdateEvaluationError::Unexpected(
538 "It is not possible to evaluate delete/insert operations on a write-only transaction after other update operations".into(),
539 ));
540 };
541 let mut prepared = self.query_evaluator.prepare_delete_insert(
542 delete.to_vec(),
543 insert.to_vec(),
544 self.base_iri.clone(),
545 None,
546 algebra,
547 );
548 *prepared.dataset_mut() = using.clone();
549 let mutations = prepared
550 .execute(DatasetView::new(storage.snapshot()))?
551 .collect::<Result<Vec<_>, _>>()?;
552 for mutation in mutations {
553 match mutation {
554 DeleteInsertQuad::Delete(quad) => self.transaction.remove(quad.as_ref()),
555 DeleteInsertQuad::Insert(quad) => self.transaction.insert(quad.as_ref()),
556 }
557 }
558 Ok(())
559 }
560
561 fn eval_load(&mut self, from: &NamedNode, to: &GraphName) -> Result<(), UpdateEvaluationError> {
562 eval_load(
563 from,
564 to,
565 #[cfg(feature = "http-client")]
566 &self.client,
567 |q| self.transaction.insert(q.as_ref()),
568 )
569 }
570
571 fn eval_create(
572 &mut self,
573 graph_name: &NamedNode,
574 silent: bool,
575 ) -> Result<(), UpdateEvaluationError> {
576 if !silent {
577 return Err(UpdateEvaluationError::Unexpected(
578 "Not possible to create a named graph using a write-only transaction when SILENT option is not set".into(),
579 ));
580 }
581 self.transaction.insert_named_graph(graph_name.into());
582 Ok(())
583 }
584
585 fn eval_clear(
586 &mut self,
587 graph: &GraphTarget,
588 _silent: bool,
589 ) -> Result<(), UpdateEvaluationError> {
590 match graph {
591 GraphTarget::NamedNode(_) => Err(UpdateEvaluationError::Unexpected(
592 "Not possible to clear a named graph using a write-only transaction".into(),
593 )),
594 GraphTarget::DefaultGraph => {
595 self.transaction.clear_default_graph();
596 Ok(())
597 }
598 GraphTarget::NamedGraphs => {
599 self.transaction.clear_all_named_graphs();
600 Ok(())
601 }
602 GraphTarget::AllGraphs => {
603 self.transaction.clear_all_graphs();
604 Ok(())
605 }
606 }
607 }
608
609 fn eval_drop(
610 &mut self,
611 graph: &GraphTarget,
612 _silent: bool,
613 ) -> Result<(), UpdateEvaluationError> {
614 match graph {
615 GraphTarget::NamedNode(_) => Err(UpdateEvaluationError::Unexpected(
616 "Not possible to drop a named graph using a write-only transaction".into(),
617 )),
618 GraphTarget::DefaultGraph => {
619 self.transaction.clear_default_graph();
620 Ok(())
621 }
622 GraphTarget::NamedGraphs => {
623 self.transaction.remove_all_named_graphs();
624 Ok(())
625 }
626 GraphTarget::AllGraphs => {
627 self.transaction.clear();
628 Ok(())
629 }
630 }
631 }
632}
633
634#[cfg(feature = "http-client")]
635fn eval_load(
636 from: &NamedNode,
637 to: &GraphName,
638 client: &Client,
639 mut insert: impl FnMut(OxQuad),
640) -> Result<(), UpdateEvaluationError> {
641 let (content_type, body) = client
642 .get(
643 from.as_str(),
644 "application/n-triples, text/turtle, application/rdf+xml",
645 )
646 .map_err(|e| UpdateEvaluationError::Service(Box::new(e)))?;
647 let format = RdfFormat::from_media_type(&content_type)
648 .ok_or_else(|| UpdateEvaluationError::UnsupportedContentType(content_type))?;
649 let to_graph_name = match to {
650 GraphName::NamedNode(graph_name) => graph_name.into(),
651 GraphName::DefaultGraph => GraphNameRef::DefaultGraph,
652 };
653 let client = client.clone();
654 let parser = RdfParser::from_format(format)
655 .rename_blank_nodes()
656 .without_named_graphs()
657 .with_default_graph(to_graph_name)
658 .with_base_iri(from.as_str())
659 .map_err(|e| UpdateEvaluationError::Unexpected(format!("Invalid URL: {from}: {e}").into()))?
660 .for_reader(body)
661 .with_document_loader(move |url| {
662 let (content_type, mut body) = client.get(
663 url,
664 "application/n-triples, text/turtle, application/rdf+xml, application/ld+json",
665 )?;
666 let mut content = Vec::new();
667 body.read_to_end(&mut content)?;
668 Ok(LoadedDocument {
669 url: url.into(),
670 content,
671 format: RdfFormat::from_media_type(&content_type)
672 .ok_or_else(|| UpdateEvaluationError::UnsupportedContentType(content_type))?,
673 })
674 });
675 for q in parser {
676 insert(q?);
677 }
678 Ok(())
679}
680
681#[cfg(not(feature = "http-client"))]
682fn eval_load(
683 _from: &NamedNode,
684 _to: &GraphName,
685 _insert: impl FnMut(OxQuad),
686) -> Result<(), UpdateEvaluationError> {
687 Err(UpdateEvaluationError::Unexpected(
688 "HTTP client is not available. Enable the feature 'http-client'".into(),
689 ))
690}
691
692fn convert_quad(quad: &Quad, bnodes: &mut FxHashMap<BlankNode, BlankNode>) -> OxQuad {
693 OxQuad {
694 subject: match &quad.subject {
695 NamedOrBlankNode::NamedNode(subject) => subject.clone().into(),
696 NamedOrBlankNode::BlankNode(subject) => convert_blank_node(subject, bnodes).into(),
697 },
698 predicate: quad.predicate.clone(),
699 object: match &quad.object {
700 Term::NamedNode(object) => object.clone().into(),
701 Term::BlankNode(object) => convert_blank_node(object, bnodes).into(),
702 Term::Literal(object) => object.clone().into(),
703 #[cfg(feature = "rdf-12")]
704 Term::Triple(subject) => convert_triple(subject, bnodes).into(),
705 },
706 graph_name: match &quad.graph_name {
707 GraphName::NamedNode(graph_name) => graph_name.clone().into(),
708 GraphName::DefaultGraph => OxGraphName::DefaultGraph,
709 },
710 }
711}
712
713#[cfg(feature = "rdf-12")]
714fn convert_triple(triple: &Triple, bnodes: &mut FxHashMap<BlankNode, BlankNode>) -> Triple {
715 Triple {
716 subject: match &triple.subject {
717 NamedOrBlankNode::NamedNode(subject) => subject.clone().into(),
718 NamedOrBlankNode::BlankNode(subject) => convert_blank_node(subject, bnodes).into(),
719 },
720 predicate: triple.predicate.clone(),
721 object: match &triple.object {
722 Term::NamedNode(object) => object.clone().into(),
723 Term::BlankNode(object) => convert_blank_node(object, bnodes).into(),
724 Term::Literal(object) => object.clone().into(),
725 #[cfg(feature = "rdf-12")]
726 Term::Triple(subject) => convert_triple(subject, bnodes).into(),
727 },
728 }
729}
730
731fn convert_blank_node(node: &BlankNode, bnodes: &mut FxHashMap<BlankNode, BlankNode>) -> BlankNode {
732 bnodes.entry(node.clone()).or_default().clone()
733}
734
735fn convert_ground_quad(quad: &GroundQuad) -> OxQuad {
736 OxQuad {
737 subject: quad.subject.clone().into(),
738 predicate: quad.predicate.clone(),
739 object: match &quad.object {
740 GroundTerm::NamedNode(object) => object.clone().into(),
741 GroundTerm::Literal(object) => object.clone().into(),
742 #[cfg(feature = "rdf-12")]
743 GroundTerm::Triple(subject) => convert_ground_triple(subject).into(),
744 },
745 graph_name: match &quad.graph_name {
746 GraphName::NamedNode(graph_name) => graph_name.clone().into(),
747 GraphName::DefaultGraph => OxGraphName::DefaultGraph,
748 },
749 }
750}
751
752#[cfg(feature = "rdf-12")]
753fn convert_ground_triple(triple: &GroundTriple) -> Triple {
754 Triple {
755 subject: triple.subject.clone().into(),
756 predicate: triple.predicate.clone(),
757 object: match &triple.object {
758 GroundTerm::NamedNode(object) => object.clone().into(),
759 GroundTerm::Literal(object) => object.clone().into(),
760 #[cfg(feature = "rdf-12")]
761 GroundTerm::Triple(subject) => convert_ground_triple(subject).into(),
762 },
763 }
764}