1use crate::error::{PhysicsError, PhysicsResult};
7use chrono::{DateTime, Utc};
8use oxirs_core::model::NamedNode;
9use oxirs_core::query::{UpdateExecutor, UpdateParser};
10use oxirs_core::rdf_store::RdfStore;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::sync::Arc;
14use uuid::Uuid;
15
16pub struct ResultInjector {
18 store: Option<Arc<RdfStore>>,
20
21 enable_provenance: bool,
23
24 config: InjectionConfig,
26}
27
28#[derive(Debug, Clone)]
30pub struct InjectionConfig {
31 pub physics_prefix: String,
33 pub prov_prefix: String,
35 pub batch_size: usize,
37 pub use_transactions: bool,
39}
40
41impl Default for InjectionConfig {
42 fn default() -> Self {
43 Self {
44 physics_prefix: "http://oxirs.org/physics#".to_string(),
45 prov_prefix: "http://www.w3.org/ns/prov#".to_string(),
46 batch_size: 1000,
47 use_transactions: true,
48 }
49 }
50}
51
52impl ResultInjector {
53 pub fn new() -> Self {
55 Self {
56 store: None,
57 enable_provenance: true,
58 config: InjectionConfig::default(),
59 }
60 }
61
62 pub fn with_store(store: Arc<RdfStore>) -> Self {
64 Self {
65 store: Some(store),
66 enable_provenance: true,
67 config: InjectionConfig::default(),
68 }
69 }
70
71 pub fn with_config(mut self, config: InjectionConfig) -> Self {
73 self.config = config;
74 self
75 }
76
77 pub fn without_provenance(mut self) -> Self {
79 self.enable_provenance = false;
80 self
81 }
82
83 pub async fn inject(&self, result: &SimulationResult) -> PhysicsResult<()> {
85 self.validate_result(result)?;
87
88 if let Some(ref store) = self.store {
89 self.inject_with_sparql(store, result).await?;
91 } else {
92 tracing::debug!(
94 "Would inject {} state vectors for entity {}",
95 result.state_trajectory.len(),
96 result.entity_iri
97 );
98 }
99
100 Ok(())
101 }
102
103 fn validate_result(&self, result: &SimulationResult) -> PhysicsResult<()> {
105 if result.entity_iri.is_empty() {
106 return Err(PhysicsError::ResultInjection(
107 "Entity IRI cannot be empty".to_string(),
108 ));
109 }
110
111 if result.simulation_run_id.is_empty() {
112 return Err(PhysicsError::ResultInjection(
113 "Simulation run ID cannot be empty".to_string(),
114 ));
115 }
116
117 if result.state_trajectory.is_empty() {
118 return Err(PhysicsError::ResultInjection(
119 "State trajectory cannot be empty".to_string(),
120 ));
121 }
122
123 Ok(())
124 }
125
126 async fn inject_with_sparql(
128 &self,
129 store: &RdfStore,
130 result: &SimulationResult,
131 ) -> PhysicsResult<()> {
132 let metadata_update = self.generate_metadata_update(result);
134
135 self.execute_update(store, &metadata_update).await?;
137
138 if result.state_trajectory.len() > self.config.batch_size {
140 self.inject_in_batches(store, result).await?;
141 } else {
142 let trajectory_update = self.generate_state_trajectory_update(result);
143 self.execute_update(store, &trajectory_update).await?;
144 }
145
146 if self.enable_provenance {
148 let provenance_update = self.generate_provenance_update(result);
149 self.execute_update(store, &provenance_update).await?;
150 }
151
152 Ok(())
153 }
154
155 async fn execute_update(&self, store: &RdfStore, update_query: &str) -> PhysicsResult<()> {
157 let parser = UpdateParser::new();
158 let parsed = parser
159 .parse(update_query)
160 .map_err(|e| PhysicsError::ResultInjection(format!("Parse failed: {e}")))?;
161 let executor = UpdateExecutor::new(store);
162 executor
163 .execute(&parsed)
164 .map_err(|e| PhysicsError::ResultInjection(format!("Execute failed: {e}")))?;
165 Ok(())
166 }
167
168 fn generate_metadata_update(&self, result: &SimulationResult) -> String {
170 let phys = &self.config.physics_prefix;
171
172 format!(
173 r#"
174 PREFIX phys: <{phys}>
175 PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
176
177 INSERT DATA {{
178 <{run_id}> a phys:SimulationRun .
179 <{run_id}> phys:simulatesEntity <{entity}> .
180 <{run_id}> phys:timestamp "{timestamp}"^^xsd:dateTime .
181 <{run_id}> phys:converged "{converged}"^^xsd:boolean .
182 <{run_id}> phys:iterations "{iterations}"^^xsd:integer .
183 <{run_id}> phys:finalResidual "{residual}"^^xsd:double .
184 }}
185 "#,
186 phys = phys,
187 run_id = result.simulation_run_id,
188 entity = result.entity_iri,
189 timestamp = result.timestamp.to_rfc3339(),
190 converged = result.convergence_info.converged,
191 iterations = result.convergence_info.iterations,
192 residual = result.convergence_info.final_residual,
193 )
194 }
195
196 fn generate_state_trajectory_update(&self, result: &SimulationResult) -> String {
198 let phys = &self.config.physics_prefix;
199 let mut triples = Vec::new();
200
201 let states_to_insert = result.state_trajectory.iter().take(100);
203
204 for (idx, state) in states_to_insert.enumerate() {
205 let state_id = format!("{}#state_{}", result.simulation_run_id, idx);
206
207 triples.push(format!(
208 "<{run_id}> phys:hasState <{state_id}> .",
209 run_id = result.simulation_run_id,
210 state_id = state_id
211 ));
212 triples.push(format!(
213 "<{state_id}> phys:time \"{time}\"^^xsd:double .",
214 state_id = state_id,
215 time = state.time
216 ));
217
218 for (key, value) in &state.state {
219 triples.push(format!(
220 "<{state_id}> phys:{key} \"{value}\"^^xsd:double .",
221 state_id = state_id,
222 key = key,
223 value = value
224 ));
225 }
226 }
227
228 for (key, value) in &result.derived_quantities {
230 triples.push(format!(
231 "<{run_id}> phys:{key} \"{value}\"^^xsd:double .",
232 run_id = result.simulation_run_id,
233 key = key,
234 value = value
235 ));
236 }
237
238 format!(
239 r#"
240 PREFIX phys: <{phys}>
241 PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
242
243 INSERT DATA {{
244 {triples}
245 }}
246 "#,
247 phys = phys,
248 triples = triples.join("\n ")
249 )
250 }
251
252 fn generate_provenance_update(&self, result: &SimulationResult) -> String {
254 let prov = &self.config.prov_prefix;
255 let phys = &self.config.physics_prefix;
256
257 format!(
258 r#"
259 PREFIX prov: <{prov}>
260 PREFIX phys: <{phys}>
261 PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
262
263 INSERT DATA {{
264 <{entity}> prov:wasGeneratedBy <{run_id}> .
265 <{run_id}> a prov:Activity .
266 <{run_id}> prov:startedAtTime "{executed_at}"^^xsd:dateTime .
267 <{run_id}> prov:used <{entity}> .
268 <{run_id}> prov:wasAssociatedWith <{software_agent}> .
269 <{software_agent}> a prov:SoftwareAgent .
270 <{software_agent}> prov:label "{software}"^^xsd:string .
271 <{software_agent}> phys:version "{version}"^^xsd:string .
272 <{run_id}> phys:parametersHash "{params_hash}"^^xsd:string .
273 <{run_id}> phys:executionTimeMs "{exec_time}"^^xsd:integer .
274 }}
275 "#,
276 prov = prov,
277 phys = phys,
278 entity = result.entity_iri,
279 run_id = result.simulation_run_id,
280 executed_at = result.provenance.executed_at.to_rfc3339(),
281 software_agent = format!("urn:agent:{}", result.provenance.software),
282 software = result.provenance.software,
283 version = result.provenance.version,
284 params_hash = result.provenance.parameters_hash,
285 exec_time = result.provenance.execution_time_ms,
286 )
287 }
288
289 async fn inject_in_batches(
291 &self,
292 store: &RdfStore,
293 result: &SimulationResult,
294 ) -> PhysicsResult<()> {
295 let phys = &self.config.physics_prefix;
296
297 for (batch_idx, chunk) in result
298 .state_trajectory
299 .chunks(self.config.batch_size)
300 .enumerate()
301 {
302 let mut triples = Vec::new();
303
304 for (idx_in_chunk, state) in chunk.iter().enumerate() {
305 let global_idx = batch_idx * self.config.batch_size + idx_in_chunk;
306 let state_id = format!("{}#state_{}", result.simulation_run_id, global_idx);
307
308 triples.push(format!(
309 "<{run_id}> phys:hasState <{state_id}> .",
310 run_id = result.simulation_run_id,
311 state_id = state_id
312 ));
313 triples.push(format!(
314 "<{state_id}> phys:time \"{time}\"^^xsd:double .",
315 state_id = state_id,
316 time = state.time
317 ));
318
319 for (key, value) in &state.state {
320 triples.push(format!(
321 "<{state_id}> phys:{key} \"{value}\"^^xsd:double .",
322 state_id = state_id,
323 key = key,
324 value = value
325 ));
326 }
327 }
328
329 let batch_update = format!(
330 r#"
331 PREFIX phys: <{phys}>
332 PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
333
334 INSERT DATA {{
335 {triples}
336 }}
337 "#,
338 phys = phys,
339 triples = triples.join("\n ")
340 );
341
342 self.execute_update(store, &batch_update).await?;
343
344 tracing::debug!("Injected batch {} ({} states)", batch_idx + 1, chunk.len());
345 }
346
347 Ok(())
348 }
349
350 pub fn create_result_node(
352 &self,
353 _entity: &NamedNode,
354 property: &str,
355 ) -> PhysicsResult<NamedNode> {
356 let result_id = Uuid::new_v4();
357 let result_iri = format!(
358 "{}result_{}_{}",
359 self.config.physics_prefix, property, result_id
360 );
361
362 NamedNode::new(&result_iri)
363 .map_err(|e| PhysicsError::ResultInjection(format!("Invalid result IRI: {}", e)))
364 }
365
366 pub async fn write_timestamped_value(
368 &self,
369 store: &RdfStore,
370 result_node: &NamedNode,
371 value: &ResultValue,
372 ) -> PhysicsResult<()> {
373 let timestamp = Utc::now();
374 let phys = &self.config.physics_prefix;
375
376 let update = match &value.value {
377 ResultData::Scalar(v) => {
378 format!(
379 r#"
380 PREFIX phys: <{phys}>
381 PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
382
383 INSERT DATA {{
384 <{node}> phys:property "{property}"^^xsd:string .
385 <{node}> phys:value "{value}"^^xsd:double .
386 <{node}> phys:timestamp "{timestamp}"^^xsd:dateTime .
387 }}
388 "#,
389 phys = phys,
390 node = result_node.as_str(),
391 property = value.property,
392 value = v,
393 timestamp = timestamp.to_rfc3339(),
394 )
395 }
396 ResultData::Vector(vec) => {
397 let mut triples = Vec::new();
398 triples.push(format!(
399 "<{node}> phys:property \"{property}\"^^xsd:string .",
400 node = result_node.as_str(),
401 property = value.property
402 ));
403 triples.push(format!(
404 "<{node}> phys:timestamp \"{timestamp}\"^^xsd:dateTime .",
405 node = result_node.as_str(),
406 timestamp = timestamp.to_rfc3339()
407 ));
408
409 for (i, v) in vec.iter().enumerate() {
410 triples.push(format!(
411 "<{node}> phys:component{i} \"{v}\"^^xsd:double .",
412 node = result_node.as_str(),
413 i = i,
414 v = v
415 ));
416 }
417
418 format!(
419 r#"
420 PREFIX phys: <{phys}>
421 PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
422
423 INSERT DATA {{
424 {triples}
425 }}
426 "#,
427 phys = phys,
428 triples = triples.join("\n ")
429 )
430 }
431 ResultData::Tensor(tensor) => {
432 let mut triples = Vec::new();
433 triples.push(format!(
434 "<{node}> phys:property \"{property}\"^^xsd:string .",
435 node = result_node.as_str(),
436 property = value.property
437 ));
438
439 for (i, row) in tensor.iter().enumerate() {
440 for (j, v) in row.iter().enumerate() {
441 triples.push(format!(
442 "<{node}> phys:tensor_{i}_{j} \"{v}\"^^xsd:double .",
443 node = result_node.as_str(),
444 i = i,
445 j = j,
446 v = v
447 ));
448 }
449 }
450
451 format!(
452 r#"
453 PREFIX phys: <{phys}>
454 PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
455
456 INSERT DATA {{
457 {triples}
458 }}
459 "#,
460 phys = phys,
461 triples = triples.join("\n ")
462 )
463 }
464 ResultData::TimeSeries(series) => {
465 let mut triples = Vec::new();
466 triples.push(format!(
467 "<{node}> phys:property \"{property}\"^^xsd:string .",
468 node = result_node.as_str(),
469 property = value.property
470 ));
471
472 for (i, (time, value)) in series.iter().enumerate() {
473 let point_id = format!("{}#point_{}", result_node.as_str(), i);
474 triples.push(format!(
475 "<{node}> phys:hasPoint <{point_id}> .",
476 node = result_node.as_str(),
477 point_id = point_id
478 ));
479 triples.push(format!(
480 "<{point_id}> phys:time \"{time}\"^^xsd:double .",
481 point_id = point_id,
482 time = time
483 ));
484 triples.push(format!(
485 "<{point_id}> phys:value \"{value}\"^^xsd:double .",
486 point_id = point_id,
487 value = value
488 ));
489 }
490
491 format!(
492 r#"
493 PREFIX phys: <{phys}>
494 PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
495
496 INSERT DATA {{
497 {triples}
498 }}
499 "#,
500 phys = phys,
501 triples = triples.join("\n ")
502 )
503 }
504 };
505
506 self.execute_update(store, &update).await
507 }
508
509 pub async fn add_provenance(
511 &self,
512 store: &RdfStore,
513 result_node: &NamedNode,
514 provenance: &ProvenanceInfo,
515 ) -> PhysicsResult<()> {
516 let prov = &self.config.prov_prefix;
517
518 let update = format!(
519 r#"
520 PREFIX prov: <{prov}>
521 PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
522
523 INSERT DATA {{
524 <{node}> prov:wasGeneratedBy <{activity}> .
525 <{activity}> a prov:Activity .
526 <{activity}> prov:startedAtTime "{timestamp}"^^xsd:dateTime .
527 <{activity}> prov:wasAssociatedWith <{agent}> .
528 <{agent}> a prov:SoftwareAgent .
529 <{agent}> prov:label "{software}"^^xsd:string .
530 }}
531 "#,
532 prov = prov,
533 node = result_node.as_str(),
534 activity = provenance.activity_id,
535 timestamp = provenance.timestamp.to_rfc3339(),
536 agent = provenance.agent_id,
537 software = provenance.software,
538 );
539
540 self.execute_update(store, &update).await
541 }
542
543 pub fn begin_transaction(&self) -> PhysicsResult<Transaction> {
545 Ok(Transaction {
546 id: Uuid::new_v4().to_string(),
547 updates: Vec::new(),
548 })
549 }
550
551 pub async fn commit_transaction(&self, store: &RdfStore, tx: Transaction) -> PhysicsResult<()> {
553 for update in tx.updates {
554 self.execute_update(store, &update).await?;
555 }
556 Ok(())
557 }
558}
559
560impl Default for ResultInjector {
561 fn default() -> Self {
562 Self::new()
563 }
564}
565
566#[derive(Debug, Clone)]
568pub struct Transaction {
569 pub id: String,
570 pub updates: Vec<String>,
571}
572
573#[derive(Debug, Clone)]
575pub struct ProvenanceInfo {
576 pub activity_id: String,
577 pub agent_id: String,
578 pub software: String,
579 pub timestamp: DateTime<Utc>,
580}
581
582#[derive(Debug, Clone)]
584pub struct ResultValue {
585 pub property: String,
586 pub value: ResultData,
587}
588
589#[derive(Debug, Clone)]
591pub enum ResultData {
592 Scalar(f64),
593 Vector(Vec<f64>),
594 Tensor(Vec<Vec<f64>>),
595 TimeSeries(Vec<(f64, f64)>),
596}
597
598#[derive(Debug, Clone, Serialize, Deserialize)]
600pub struct SimulationResult {
601 pub entity_iri: String,
602 pub simulation_run_id: String,
603 pub timestamp: DateTime<Utc>,
604 pub state_trajectory: Vec<StateVector>,
605 pub derived_quantities: HashMap<String, f64>,
606 pub convergence_info: ConvergenceInfo,
607 pub provenance: SimulationProvenance,
608}
609
610#[derive(Debug, Clone, Serialize, Deserialize)]
612pub struct StateVector {
613 pub time: f64,
614 pub state: HashMap<String, f64>,
615}
616
617#[derive(Debug, Clone, Serialize, Deserialize)]
619pub struct ConvergenceInfo {
620 pub converged: bool,
621 pub iterations: usize,
622 pub final_residual: f64,
623}
624
625#[derive(Debug, Clone, Serialize, Deserialize)]
627pub struct SimulationProvenance {
628 pub software: String,
629 pub version: String,
630 pub parameters_hash: String,
631 pub executed_at: DateTime<Utc>,
632 pub execution_time_ms: u64,
633}
634
635#[cfg(test)]
636mod tests {
637 use super::*;
638 use oxirs_core::Store;
640
641 fn create_test_result() -> SimulationResult {
642 let mut trajectory = Vec::new();
643
644 for i in 0..10 {
645 let mut state = HashMap::new();
646 state.insert("temperature".to_string(), 300.0 + i as f64);
647 trajectory.push(StateVector {
648 time: i as f64 * 10.0,
649 state,
650 });
651 }
652
653 SimulationResult {
654 entity_iri: "urn:example:battery:001".to_string(),
655 simulation_run_id: "run-123".to_string(),
656 timestamp: Utc::now(),
657 state_trajectory: trajectory,
658 derived_quantities: HashMap::new(),
659 convergence_info: ConvergenceInfo {
660 converged: true,
661 iterations: 100,
662 final_residual: 1e-6,
663 },
664 provenance: SimulationProvenance {
665 software: "oxirs-physics".to_string(),
666 version: "0.1.0".to_string(),
667 parameters_hash: "abc123".to_string(),
668 executed_at: Utc::now(),
669 execution_time_ms: 1500,
670 },
671 }
672 }
673
674 #[tokio::test]
675 async fn test_result_injector_basic() {
676 let injector = ResultInjector::new();
677 let result = create_test_result();
678
679 assert!(injector.inject(&result).await.is_ok());
681 }
682
683 #[tokio::test]
684 async fn test_result_injector_without_provenance() {
685 let injector = ResultInjector::new().without_provenance();
686 let result = create_test_result();
687
688 assert!(injector.inject(&result).await.is_ok());
689 }
690
691 #[tokio::test]
692 async fn test_validate_result_empty_entity_iri() {
693 let injector = ResultInjector::new();
694 let mut result = create_test_result();
695 result.entity_iri = String::new();
696
697 assert!(injector.inject(&result).await.is_err());
699 }
700
701 #[tokio::test]
702 async fn test_validate_result_empty_run_id() {
703 let injector = ResultInjector::new();
704 let mut result = create_test_result();
705 result.simulation_run_id = String::new();
706
707 assert!(injector.inject(&result).await.is_err());
709 }
710
711 #[tokio::test]
712 async fn test_validate_result_empty_trajectory() {
713 let injector = ResultInjector::new();
714 let mut result = create_test_result();
715 result.state_trajectory.clear();
716
717 assert!(injector.inject(&result).await.is_err());
719 }
720
721 #[test]
722 fn test_generate_metadata_update() {
723 let injector = ResultInjector::new();
724 let result = create_test_result();
725
726 let query = injector.generate_metadata_update(&result);
727
728 assert!(query.contains("INSERT DATA"));
730 assert!(query.contains("phys:SimulationRun"));
731 assert!(query.contains(&result.simulation_run_id));
732 assert!(query.contains(&result.entity_iri));
733 assert!(query.contains("phys:converged"));
734 assert!(query.contains("phys:iterations"));
735 }
736
737 #[test]
738 fn test_generate_state_trajectory_update() {
739 let injector = ResultInjector::new();
740 let result = create_test_result();
741
742 let query = injector.generate_state_trajectory_update(&result);
743
744 assert!(query.contains("INSERT DATA"));
746 assert!(query.contains("phys:hasState"));
747 assert!(query.contains("phys:time"));
748 assert!(query.contains(&result.simulation_run_id));
749 }
750
751 #[test]
752 fn test_generate_provenance_update() {
753 let injector = ResultInjector::new();
754 let result = create_test_result();
755
756 let query = injector.generate_provenance_update(&result);
757
758 assert!(query.contains("prov:wasGeneratedBy"));
760 assert!(query.contains("prov:Activity"));
761 assert!(query.contains("prov:SoftwareAgent"));
762 assert!(query.contains(&result.provenance.software));
763 assert!(query.contains(&result.provenance.version));
764 assert!(query.contains(&result.provenance.parameters_hash));
765 }
766
767 #[test]
768 fn test_simulation_result_serialization() {
769 let result = create_test_result();
770
771 let json = serde_json::to_string(&result).expect("Failed to serialize");
772 let deserialized: SimulationResult =
773 serde_json::from_str(&json).expect("Failed to deserialize");
774
775 assert_eq!(deserialized.entity_iri, result.entity_iri);
776 assert_eq!(deserialized.simulation_run_id, result.simulation_run_id);
777 assert_eq!(
778 deserialized.state_trajectory.len(),
779 result.state_trajectory.len()
780 );
781 assert_eq!(
782 deserialized.convergence_info.converged,
783 result.convergence_info.converged
784 );
785 }
786
787 #[test]
788 fn test_state_vector_serialization() {
789 let mut state = HashMap::new();
790 state.insert("temperature".to_string(), 300.0);
791 state.insert("pressure".to_string(), 101325.0);
792
793 let vector = StateVector {
794 time: 10.0,
795 state: state.clone(),
796 };
797
798 let json = serde_json::to_string(&vector).expect("Failed to serialize");
799 let deserialized: StateVector = serde_json::from_str(&json).expect("Failed to deserialize");
800
801 assert_eq!(deserialized.time, 10.0);
802 assert_eq!(deserialized.state.len(), 2);
803 assert_eq!(deserialized.state.get("temperature"), Some(&300.0));
804 }
805
806 #[test]
807 fn test_convergence_info() {
808 let info = ConvergenceInfo {
809 converged: true,
810 iterations: 150,
811 final_residual: 5e-7,
812 };
813
814 assert!(info.converged);
815 assert_eq!(info.iterations, 150);
816 assert!(info.final_residual < 1e-6);
817 }
818
819 #[test]
820 fn test_provenance_tracking() {
821 let prov = SimulationProvenance {
822 software: "oxirs-physics".to_string(),
823 version: "0.1.0".to_string(),
824 parameters_hash: "def456".to_string(),
825 executed_at: Utc::now(),
826 execution_time_ms: 2500,
827 };
828
829 assert_eq!(prov.software, "oxirs-physics");
830 assert_eq!(prov.version, "0.1.0");
831 assert_eq!(prov.execution_time_ms, 2500);
832 }
833
834 #[test]
835 fn test_injection_config() {
836 let config = InjectionConfig {
837 physics_prefix: "http://example.org/phys#".to_string(),
838 prov_prefix: "http://example.org/prov#".to_string(),
839 batch_size: 500,
840 use_transactions: false,
841 };
842
843 assert_eq!(config.physics_prefix, "http://example.org/phys#");
844 assert_eq!(config.batch_size, 500);
845 assert!(!config.use_transactions);
846 }
847
848 #[test]
849 fn test_create_result_node() {
850 let injector = ResultInjector::new();
851 let entity = NamedNode::new("http://example.org/entity1").expect("Failed to create node");
852
853 let result_node = injector
854 .create_result_node(&entity, "displacement")
855 .expect("Failed to create result node");
856
857 assert!(result_node.as_str().contains("displacement"));
858 assert!(result_node
859 .as_str()
860 .starts_with("http://oxirs.org/physics#result_"));
861 }
862
863 #[test]
864 fn test_transaction() {
865 let injector = ResultInjector::new();
866 let tx = injector
867 .begin_transaction()
868 .expect("Failed to begin transaction");
869
870 assert!(!tx.id.is_empty());
871 assert!(tx.updates.is_empty());
872 }
873
874 #[tokio::test]
875 async fn test_execute_update_insert_data() {
876 let store = RdfStore::default();
877 let injector = ResultInjector::new();
878 let update =
879 "INSERT DATA { <http://example.org/s> <http://example.org/p> <http://example.org/o> }";
880 let result = injector.execute_update(&store, update).await;
881 assert!(
882 result.is_ok(),
883 "execute_update should succeed: {:?}",
884 result
885 );
886 let quads = store
888 .find_quads(None, None, None, None)
889 .expect("find_quads failed");
890 assert!(
891 !quads.is_empty(),
892 "Store should have quads after INSERT DATA"
893 );
894 }
895
896 #[tokio::test]
897 async fn test_execute_update_parse_error() {
898 let store = RdfStore::default();
899 let injector = ResultInjector::new();
900 let bad_update = "NOT VALID SPARQL UPDATE";
901 let result = injector.execute_update(&store, bad_update).await;
902 assert!(result.is_err(), "Should fail on invalid SPARQL");
903 }
904}