1use std::sync::Arc;
8
9use grafeo_common::types::{EdgeId, EpochId, NodeId, TxId, Value};
10use grafeo_common::utils::error::Result;
11use grafeo_core::graph::Direction;
12use grafeo_core::graph::lpg::{Edge, LpgStore, Node};
13#[cfg(feature = "rdf")]
14use grafeo_core::graph::rdf::RdfStore;
15
16use crate::config::AdaptiveConfig;
17use crate::database::QueryResult;
18use crate::query::cache::QueryCache;
19use crate::transaction::TransactionManager;
20
21pub struct Session {
27 store: Arc<LpgStore>,
29 #[cfg(feature = "rdf")]
31 #[allow(dead_code)]
32 rdf_store: Arc<RdfStore>,
33 tx_manager: Arc<TransactionManager>,
35 query_cache: Arc<QueryCache>,
37 current_tx: Option<TxId>,
39 auto_commit: bool,
41 #[allow(dead_code)]
43 adaptive_config: AdaptiveConfig,
44 factorized_execution: bool,
46}
47
48impl Session {
49 #[allow(dead_code)]
51 pub(crate) fn new(
52 store: Arc<LpgStore>,
53 tx_manager: Arc<TransactionManager>,
54 query_cache: Arc<QueryCache>,
55 ) -> Self {
56 Self {
57 store,
58 #[cfg(feature = "rdf")]
59 rdf_store: Arc::new(RdfStore::new()),
60 tx_manager,
61 query_cache,
62 current_tx: None,
63 auto_commit: true,
64 adaptive_config: AdaptiveConfig::default(),
65 factorized_execution: true,
66 }
67 }
68
69 #[allow(dead_code)]
71 pub(crate) fn with_adaptive(
72 store: Arc<LpgStore>,
73 tx_manager: Arc<TransactionManager>,
74 query_cache: Arc<QueryCache>,
75 adaptive_config: AdaptiveConfig,
76 factorized_execution: bool,
77 ) -> Self {
78 Self {
79 store,
80 #[cfg(feature = "rdf")]
81 rdf_store: Arc::new(RdfStore::new()),
82 tx_manager,
83 query_cache,
84 current_tx: None,
85 auto_commit: true,
86 adaptive_config,
87 factorized_execution,
88 }
89 }
90
91 #[cfg(feature = "rdf")]
93 pub(crate) fn with_rdf_store_and_adaptive(
94 store: Arc<LpgStore>,
95 rdf_store: Arc<RdfStore>,
96 tx_manager: Arc<TransactionManager>,
97 query_cache: Arc<QueryCache>,
98 adaptive_config: AdaptiveConfig,
99 factorized_execution: bool,
100 ) -> Self {
101 Self {
102 store,
103 rdf_store,
104 tx_manager,
105 query_cache,
106 current_tx: None,
107 auto_commit: true,
108 adaptive_config,
109 factorized_execution,
110 }
111 }
112
113 #[cfg(feature = "gql")]
137 pub fn execute(&self, query: &str) -> Result<QueryResult> {
138 use crate::query::{
139 Executor, Planner, binder::Binder, cache::CacheKey, gql_translator,
140 optimizer::Optimizer, processor::QueryLanguage,
141 };
142
143 let start_time = std::time::Instant::now();
144
145 let cache_key = CacheKey::new(query, QueryLanguage::Gql);
147
148 let optimized_plan = if let Some(cached_plan) = self.query_cache.get_optimized(&cache_key) {
150 cached_plan
152 } else {
153 let logical_plan = gql_translator::translate(query)?;
157
158 let mut binder = Binder::new();
160 let _binding_context = binder.bind(&logical_plan)?;
161
162 let optimizer = Optimizer::new();
164 let plan = optimizer.optimize(logical_plan)?;
165
166 self.query_cache.put_optimized(cache_key, plan.clone());
168
169 plan
170 };
171
172 let (viewing_epoch, tx_id) = self.get_transaction_context();
174
175 let planner = Planner::with_context(
178 Arc::clone(&self.store),
179 Arc::clone(&self.tx_manager),
180 tx_id,
181 viewing_epoch,
182 )
183 .with_factorized_execution(self.factorized_execution);
184 let mut physical_plan = planner.plan(&optimized_plan)?;
185
186 let executor = Executor::with_columns(physical_plan.columns.clone());
188 let mut result = executor.execute(physical_plan.operator.as_mut())?;
189
190 let elapsed_ms = start_time.elapsed().as_secs_f64() * 1000.0;
192 let rows_scanned = result.rows.len() as u64;
193 result.execution_time_ms = Some(elapsed_ms);
194 result.rows_scanned = Some(rows_scanned);
195
196 Ok(result)
197 }
198
199 #[cfg(feature = "gql")]
205 pub fn execute_with_params(
206 &self,
207 query: &str,
208 params: std::collections::HashMap<String, Value>,
209 ) -> Result<QueryResult> {
210 use crate::query::processor::{QueryLanguage, QueryProcessor};
211
212 let (viewing_epoch, tx_id) = self.get_transaction_context();
214
215 let processor =
217 QueryProcessor::for_lpg_with_tx(Arc::clone(&self.store), Arc::clone(&self.tx_manager));
218
219 let processor = if let Some(tx_id) = tx_id {
221 processor.with_tx_context(viewing_epoch, tx_id)
222 } else {
223 processor
224 };
225
226 processor.process(query, QueryLanguage::Gql, Some(¶ms))
227 }
228
229 #[cfg(not(any(feature = "gql", feature = "cypher")))]
235 pub fn execute_with_params(
236 &self,
237 _query: &str,
238 _params: std::collections::HashMap<String, Value>,
239 ) -> Result<QueryResult> {
240 Err(grafeo_common::utils::error::Error::Internal(
241 "No query language enabled".to_string(),
242 ))
243 }
244
245 #[cfg(not(any(feature = "gql", feature = "cypher")))]
251 pub fn execute(&self, _query: &str) -> Result<QueryResult> {
252 Err(grafeo_common::utils::error::Error::Internal(
253 "No query language enabled".to_string(),
254 ))
255 }
256
257 #[cfg(feature = "cypher")]
263 pub fn execute_cypher(&self, query: &str) -> Result<QueryResult> {
264 use crate::query::{
265 Executor, Planner, binder::Binder, cache::CacheKey, cypher_translator,
266 optimizer::Optimizer, processor::QueryLanguage,
267 };
268
269 let cache_key = CacheKey::new(query, QueryLanguage::Cypher);
271
272 let optimized_plan = if let Some(cached_plan) = self.query_cache.get_optimized(&cache_key) {
274 cached_plan
275 } else {
276 let logical_plan = cypher_translator::translate(query)?;
278
279 let mut binder = Binder::new();
281 let _binding_context = binder.bind(&logical_plan)?;
282
283 let optimizer = Optimizer::new();
285 let plan = optimizer.optimize(logical_plan)?;
286
287 self.query_cache.put_optimized(cache_key, plan.clone());
289
290 plan
291 };
292
293 let (viewing_epoch, tx_id) = self.get_transaction_context();
295
296 let planner = Planner::with_context(
298 Arc::clone(&self.store),
299 Arc::clone(&self.tx_manager),
300 tx_id,
301 viewing_epoch,
302 )
303 .with_factorized_execution(self.factorized_execution);
304 let mut physical_plan = planner.plan(&optimized_plan)?;
305
306 let executor = Executor::with_columns(physical_plan.columns.clone());
308 executor.execute(physical_plan.operator.as_mut())
309 }
310
311 #[cfg(feature = "gremlin")]
332 pub fn execute_gremlin(&self, query: &str) -> Result<QueryResult> {
333 use crate::query::{
334 Executor, Planner, binder::Binder, gremlin_translator, optimizer::Optimizer,
335 };
336
337 let logical_plan = gremlin_translator::translate(query)?;
339
340 let mut binder = Binder::new();
342 let _binding_context = binder.bind(&logical_plan)?;
343
344 let optimizer = Optimizer::new();
346 let optimized_plan = optimizer.optimize(logical_plan)?;
347
348 let (viewing_epoch, tx_id) = self.get_transaction_context();
350
351 let planner = Planner::with_context(
353 Arc::clone(&self.store),
354 Arc::clone(&self.tx_manager),
355 tx_id,
356 viewing_epoch,
357 )
358 .with_factorized_execution(self.factorized_execution);
359 let mut physical_plan = planner.plan(&optimized_plan)?;
360
361 let executor = Executor::with_columns(physical_plan.columns.clone());
363 executor.execute(physical_plan.operator.as_mut())
364 }
365
366 #[cfg(feature = "gremlin")]
372 pub fn execute_gremlin_with_params(
373 &self,
374 query: &str,
375 params: std::collections::HashMap<String, Value>,
376 ) -> Result<QueryResult> {
377 use crate::query::processor::{QueryLanguage, QueryProcessor};
378
379 let (viewing_epoch, tx_id) = self.get_transaction_context();
381
382 let processor =
384 QueryProcessor::for_lpg_with_tx(Arc::clone(&self.store), Arc::clone(&self.tx_manager));
385
386 let processor = if let Some(tx_id) = tx_id {
388 processor.with_tx_context(viewing_epoch, tx_id)
389 } else {
390 processor
391 };
392
393 processor.process(query, QueryLanguage::Gremlin, Some(¶ms))
394 }
395
396 #[cfg(feature = "graphql")]
417 pub fn execute_graphql(&self, query: &str) -> Result<QueryResult> {
418 use crate::query::{
419 Executor, Planner, binder::Binder, graphql_translator, optimizer::Optimizer,
420 };
421
422 let logical_plan = graphql_translator::translate(query)?;
424
425 let mut binder = Binder::new();
427 let _binding_context = binder.bind(&logical_plan)?;
428
429 let optimizer = Optimizer::new();
431 let optimized_plan = optimizer.optimize(logical_plan)?;
432
433 let (viewing_epoch, tx_id) = self.get_transaction_context();
435
436 let planner = Planner::with_context(
438 Arc::clone(&self.store),
439 Arc::clone(&self.tx_manager),
440 tx_id,
441 viewing_epoch,
442 )
443 .with_factorized_execution(self.factorized_execution);
444 let mut physical_plan = planner.plan(&optimized_plan)?;
445
446 let executor = Executor::with_columns(physical_plan.columns.clone());
448 executor.execute(physical_plan.operator.as_mut())
449 }
450
451 #[cfg(feature = "graphql")]
457 pub fn execute_graphql_with_params(
458 &self,
459 query: &str,
460 params: std::collections::HashMap<String, Value>,
461 ) -> Result<QueryResult> {
462 use crate::query::processor::{QueryLanguage, QueryProcessor};
463
464 let (viewing_epoch, tx_id) = self.get_transaction_context();
466
467 let processor =
469 QueryProcessor::for_lpg_with_tx(Arc::clone(&self.store), Arc::clone(&self.tx_manager));
470
471 let processor = if let Some(tx_id) = tx_id {
473 processor.with_tx_context(viewing_epoch, tx_id)
474 } else {
475 processor
476 };
477
478 processor.process(query, QueryLanguage::GraphQL, Some(¶ms))
479 }
480
481 #[cfg(all(feature = "sparql", feature = "rdf"))]
487 pub fn execute_sparql(&self, query: &str) -> Result<QueryResult> {
488 use crate::query::{
489 Executor, optimizer::Optimizer, planner_rdf::RdfPlanner, sparql_translator,
490 };
491
492 let logical_plan = sparql_translator::translate(query)?;
494
495 let optimizer = Optimizer::new();
497 let optimized_plan = optimizer.optimize(logical_plan)?;
498
499 let planner = RdfPlanner::new(Arc::clone(&self.rdf_store)).with_tx_id(self.current_tx);
501 let mut physical_plan = planner.plan(&optimized_plan)?;
502
503 let executor = Executor::with_columns(physical_plan.columns.clone());
505 executor.execute(physical_plan.operator.as_mut())
506 }
507
508 #[cfg(all(feature = "sparql", feature = "rdf"))]
514 pub fn execute_sparql_with_params(
515 &self,
516 query: &str,
517 _params: std::collections::HashMap<String, Value>,
518 ) -> Result<QueryResult> {
519 self.execute_sparql(query)
522 }
523
524 pub fn begin_tx(&mut self) -> Result<()> {
544 if self.current_tx.is_some() {
545 return Err(grafeo_common::utils::error::Error::Transaction(
546 grafeo_common::utils::error::TransactionError::InvalidState(
547 "Transaction already active".to_string(),
548 ),
549 ));
550 }
551
552 let tx_id = self.tx_manager.begin();
553 self.current_tx = Some(tx_id);
554 Ok(())
555 }
556
557 pub fn commit(&mut self) -> Result<()> {
565 let tx_id = self.current_tx.take().ok_or_else(|| {
566 grafeo_common::utils::error::Error::Transaction(
567 grafeo_common::utils::error::TransactionError::InvalidState(
568 "No active transaction".to_string(),
569 ),
570 )
571 })?;
572
573 #[cfg(feature = "rdf")]
575 self.rdf_store.commit_tx(tx_id);
576
577 self.tx_manager.commit(tx_id).map(|_| ())
578 }
579
580 pub fn rollback(&mut self) -> Result<()> {
601 let tx_id = self.current_tx.take().ok_or_else(|| {
602 grafeo_common::utils::error::Error::Transaction(
603 grafeo_common::utils::error::TransactionError::InvalidState(
604 "No active transaction".to_string(),
605 ),
606 )
607 })?;
608
609 self.store.discard_uncommitted_versions(tx_id);
611
612 #[cfg(feature = "rdf")]
614 self.rdf_store.rollback_tx(tx_id);
615
616 self.tx_manager.abort(tx_id)
618 }
619
620 #[must_use]
622 pub fn in_transaction(&self) -> bool {
623 self.current_tx.is_some()
624 }
625
626 pub fn set_auto_commit(&mut self, auto_commit: bool) {
628 self.auto_commit = auto_commit;
629 }
630
631 #[must_use]
633 pub fn auto_commit(&self) -> bool {
634 self.auto_commit
635 }
636
637 #[must_use]
643 fn get_transaction_context(&self) -> (EpochId, Option<TxId>) {
644 if let Some(tx_id) = self.current_tx {
645 let epoch = self
647 .tx_manager
648 .start_epoch(tx_id)
649 .unwrap_or_else(|| self.tx_manager.current_epoch());
650 (epoch, Some(tx_id))
651 } else {
652 (self.tx_manager.current_epoch(), None)
654 }
655 }
656
657 pub fn create_node(&self, labels: &[&str]) -> NodeId {
662 let (epoch, tx_id) = self.get_transaction_context();
663 self.store
664 .create_node_versioned(labels, epoch, tx_id.unwrap_or(TxId::SYSTEM))
665 }
666
667 pub fn create_node_with_props<'a>(
671 &self,
672 labels: &[&str],
673 properties: impl IntoIterator<Item = (&'a str, Value)>,
674 ) -> NodeId {
675 let (epoch, tx_id) = self.get_transaction_context();
676 self.store.create_node_with_props_versioned(
677 labels,
678 properties.into_iter().map(|(k, v)| (k, v)),
679 epoch,
680 tx_id.unwrap_or(TxId::SYSTEM),
681 )
682 }
683
684 pub fn create_edge(
689 &self,
690 src: NodeId,
691 dst: NodeId,
692 edge_type: &str,
693 ) -> grafeo_common::types::EdgeId {
694 let (epoch, tx_id) = self.get_transaction_context();
695 self.store
696 .create_edge_versioned(src, dst, edge_type, epoch, tx_id.unwrap_or(TxId::SYSTEM))
697 }
698
699 #[must_use]
725 pub fn get_node(&self, id: NodeId) -> Option<Node> {
726 let (epoch, tx_id) = self.get_transaction_context();
727 self.store
728 .get_node_versioned(id, epoch, tx_id.unwrap_or(TxId::SYSTEM))
729 }
730
731 #[must_use]
752 pub fn get_node_property(&self, id: NodeId, key: &str) -> Option<Value> {
753 self.get_node(id)
754 .and_then(|node| node.get_property(key).cloned())
755 }
756
757 #[must_use]
764 pub fn get_edge(&self, id: EdgeId) -> Option<Edge> {
765 let (epoch, tx_id) = self.get_transaction_context();
766 self.store
767 .get_edge_versioned(id, epoch, tx_id.unwrap_or(TxId::SYSTEM))
768 }
769
770 #[must_use]
794 pub fn get_neighbors_outgoing(&self, node: NodeId) -> Vec<(NodeId, EdgeId)> {
795 self.store.edges_from(node, Direction::Outgoing).collect()
796 }
797
798 #[must_use]
807 pub fn get_neighbors_incoming(&self, node: NodeId) -> Vec<(NodeId, EdgeId)> {
808 self.store.edges_from(node, Direction::Incoming).collect()
809 }
810
811 #[must_use]
819 pub fn get_neighbors_outgoing_by_type(
820 &self,
821 node: NodeId,
822 edge_type: &str,
823 ) -> Vec<(NodeId, EdgeId)> {
824 self.store
825 .edges_from(node, Direction::Outgoing)
826 .filter(|(_, edge_id)| {
827 self.get_edge(*edge_id)
828 .is_some_and(|e| e.edge_type.as_str() == edge_type)
829 })
830 .collect()
831 }
832
833 #[must_use]
840 pub fn node_exists(&self, id: NodeId) -> bool {
841 self.get_node(id).is_some()
842 }
843
844 #[must_use]
846 pub fn edge_exists(&self, id: EdgeId) -> bool {
847 self.get_edge(id).is_some()
848 }
849
850 #[must_use]
854 pub fn get_degree(&self, node: NodeId) -> (usize, usize) {
855 let out = self.store.out_degree(node);
856 let in_degree = self.store.in_degree(node);
857 (out, in_degree)
858 }
859
860 #[must_use]
870 pub fn get_nodes_batch(&self, ids: &[NodeId]) -> Vec<Option<Node>> {
871 let (epoch, tx_id) = self.get_transaction_context();
872 let tx = tx_id.unwrap_or(TxId::SYSTEM);
873 ids.iter()
874 .map(|&id| self.store.get_node_versioned(id, epoch, tx))
875 .collect()
876 }
877}
878
879#[cfg(test)]
880mod tests {
881 use crate::database::GrafeoDB;
882
883 #[test]
884 fn test_session_create_node() {
885 let db = GrafeoDB::new_in_memory();
886 let session = db.session();
887
888 let id = session.create_node(&["Person"]);
889 assert!(id.is_valid());
890 assert_eq!(db.node_count(), 1);
891 }
892
893 #[test]
894 fn test_session_transaction() {
895 let db = GrafeoDB::new_in_memory();
896 let mut session = db.session();
897
898 assert!(!session.in_transaction());
899
900 session.begin_tx().unwrap();
901 assert!(session.in_transaction());
902
903 session.commit().unwrap();
904 assert!(!session.in_transaction());
905 }
906
907 #[test]
908 fn test_session_transaction_context() {
909 let db = GrafeoDB::new_in_memory();
910 let mut session = db.session();
911
912 let (_epoch1, tx_id1) = session.get_transaction_context();
914 assert!(tx_id1.is_none());
915
916 session.begin_tx().unwrap();
918 let (epoch2, tx_id2) = session.get_transaction_context();
919 assert!(tx_id2.is_some());
920 let _ = epoch2; session.commit().unwrap();
925 let (epoch3, tx_id3) = session.get_transaction_context();
926 assert!(tx_id3.is_none());
927 assert!(epoch3.as_u64() >= epoch2.as_u64());
929 }
930
931 #[test]
932 fn test_session_rollback() {
933 let db = GrafeoDB::new_in_memory();
934 let mut session = db.session();
935
936 session.begin_tx().unwrap();
937 session.rollback().unwrap();
938 assert!(!session.in_transaction());
939 }
940
941 #[test]
942 fn test_session_rollback_discards_versions() {
943 use grafeo_common::types::TxId;
944
945 let db = GrafeoDB::new_in_memory();
946
947 let node_before = db.store().create_node(&["Person"]);
949 assert!(node_before.is_valid());
950 assert_eq!(db.node_count(), 1, "Should have 1 node before transaction");
951
952 let mut session = db.session();
954 session.begin_tx().unwrap();
955 let tx_id = session.current_tx.unwrap();
956
957 let epoch = db.store().current_epoch();
959 let node_in_tx = db.store().create_node_versioned(&["Person"], epoch, tx_id);
960 assert!(node_in_tx.is_valid());
961
962 assert_eq!(db.node_count(), 2, "Should have 2 nodes during transaction");
964
965 session.rollback().unwrap();
967 assert!(!session.in_transaction());
968
969 let count_after = db.node_count();
972 assert_eq!(
973 count_after, 1,
974 "Rollback should discard uncommitted node, but got {count_after}"
975 );
976
977 let current_epoch = db.store().current_epoch();
979 assert!(
980 db.store()
981 .get_node_versioned(node_before, current_epoch, TxId::SYSTEM)
982 .is_some(),
983 "Original node should still exist"
984 );
985
986 assert!(
988 db.store()
989 .get_node_versioned(node_in_tx, current_epoch, TxId::SYSTEM)
990 .is_none(),
991 "Transaction node should be gone"
992 );
993 }
994
995 #[test]
996 fn test_session_create_node_in_transaction() {
997 let db = GrafeoDB::new_in_memory();
999
1000 let node_before = db.create_node(&["Person"]);
1002 assert!(node_before.is_valid());
1003 assert_eq!(db.node_count(), 1, "Should have 1 node before transaction");
1004
1005 let mut session = db.session();
1007 session.begin_tx().unwrap();
1008
1009 let node_in_tx = session.create_node(&["Person"]);
1011 assert!(node_in_tx.is_valid());
1012
1013 assert_eq!(db.node_count(), 2, "Should have 2 nodes during transaction");
1015
1016 session.rollback().unwrap();
1018
1019 let count_after = db.node_count();
1021 assert_eq!(
1022 count_after, 1,
1023 "Rollback should discard node created via session.create_node(), but got {count_after}"
1024 );
1025 }
1026
1027 #[test]
1028 fn test_session_create_node_with_props_in_transaction() {
1029 use grafeo_common::types::Value;
1030
1031 let db = GrafeoDB::new_in_memory();
1033
1034 db.create_node(&["Person"]);
1036 assert_eq!(db.node_count(), 1, "Should have 1 node before transaction");
1037
1038 let mut session = db.session();
1040 session.begin_tx().unwrap();
1041
1042 let node_in_tx =
1043 session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
1044 assert!(node_in_tx.is_valid());
1045
1046 assert_eq!(db.node_count(), 2, "Should have 2 nodes during transaction");
1048
1049 session.rollback().unwrap();
1051
1052 let count_after = db.node_count();
1054 assert_eq!(
1055 count_after, 1,
1056 "Rollback should discard node created via session.create_node_with_props()"
1057 );
1058 }
1059
1060 #[cfg(feature = "gql")]
1061 mod gql_tests {
1062 use super::*;
1063
1064 #[test]
1065 fn test_gql_query_execution() {
1066 let db = GrafeoDB::new_in_memory();
1067 let session = db.session();
1068
1069 session.create_node(&["Person"]);
1071 session.create_node(&["Person"]);
1072 session.create_node(&["Animal"]);
1073
1074 let result = session.execute("MATCH (n:Person) RETURN n").unwrap();
1076
1077 assert_eq!(result.row_count(), 2);
1079 assert_eq!(result.column_count(), 1);
1080 assert_eq!(result.columns[0], "n");
1081 }
1082
1083 #[test]
1084 fn test_gql_empty_result() {
1085 let db = GrafeoDB::new_in_memory();
1086 let session = db.session();
1087
1088 let result = session.execute("MATCH (n:Person) RETURN n").unwrap();
1090
1091 assert_eq!(result.row_count(), 0);
1092 }
1093
1094 #[test]
1095 fn test_gql_parse_error() {
1096 let db = GrafeoDB::new_in_memory();
1097 let session = db.session();
1098
1099 let result = session.execute("MATCH (n RETURN n");
1101
1102 assert!(result.is_err());
1103 }
1104
1105 #[test]
1106 fn test_gql_relationship_traversal() {
1107 let db = GrafeoDB::new_in_memory();
1108 let session = db.session();
1109
1110 let alice = session.create_node(&["Person"]);
1112 let bob = session.create_node(&["Person"]);
1113 let charlie = session.create_node(&["Person"]);
1114
1115 session.create_edge(alice, bob, "KNOWS");
1116 session.create_edge(alice, charlie, "KNOWS");
1117
1118 let result = session
1120 .execute("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b")
1121 .unwrap();
1122
1123 assert_eq!(result.row_count(), 2);
1125 assert_eq!(result.column_count(), 2);
1126 assert_eq!(result.columns[0], "a");
1127 assert_eq!(result.columns[1], "b");
1128 }
1129
1130 #[test]
1131 fn test_gql_relationship_with_type_filter() {
1132 let db = GrafeoDB::new_in_memory();
1133 let session = db.session();
1134
1135 let alice = session.create_node(&["Person"]);
1137 let bob = session.create_node(&["Person"]);
1138 let charlie = session.create_node(&["Person"]);
1139
1140 session.create_edge(alice, bob, "KNOWS");
1141 session.create_edge(alice, charlie, "WORKS_WITH");
1142
1143 let result = session
1145 .execute("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b")
1146 .unwrap();
1147
1148 assert_eq!(result.row_count(), 1);
1150 }
1151
1152 #[test]
1153 fn test_gql_semantic_error_undefined_variable() {
1154 let db = GrafeoDB::new_in_memory();
1155 let session = db.session();
1156
1157 let result = session.execute("MATCH (n:Person) RETURN x");
1159
1160 assert!(result.is_err());
1162 let err = match result {
1163 Err(e) => e,
1164 Ok(_) => panic!("Expected error"),
1165 };
1166 assert!(
1167 err.to_string().contains("Undefined variable"),
1168 "Expected undefined variable error, got: {}",
1169 err
1170 );
1171 }
1172
1173 #[test]
1174 fn test_gql_where_clause_property_filter() {
1175 use grafeo_common::types::Value;
1176
1177 let db = GrafeoDB::new_in_memory();
1178 let session = db.session();
1179
1180 session.create_node_with_props(&["Person"], [("age", Value::Int64(25))]);
1182 session.create_node_with_props(&["Person"], [("age", Value::Int64(35))]);
1183 session.create_node_with_props(&["Person"], [("age", Value::Int64(45))]);
1184
1185 let result = session
1187 .execute("MATCH (n:Person) WHERE n.age > 30 RETURN n")
1188 .unwrap();
1189
1190 assert_eq!(result.row_count(), 2);
1192 }
1193
1194 #[test]
1195 fn test_gql_where_clause_equality() {
1196 use grafeo_common::types::Value;
1197
1198 let db = GrafeoDB::new_in_memory();
1199 let session = db.session();
1200
1201 session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
1203 session.create_node_with_props(&["Person"], [("name", Value::String("Bob".into()))]);
1204 session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
1205
1206 let result = session
1208 .execute("MATCH (n:Person) WHERE n.name = \"Alice\" RETURN n")
1209 .unwrap();
1210
1211 assert_eq!(result.row_count(), 2);
1213 }
1214
1215 #[test]
1216 fn test_gql_return_property_access() {
1217 use grafeo_common::types::Value;
1218
1219 let db = GrafeoDB::new_in_memory();
1220 let session = db.session();
1221
1222 session.create_node_with_props(
1224 &["Person"],
1225 [
1226 ("name", Value::String("Alice".into())),
1227 ("age", Value::Int64(30)),
1228 ],
1229 );
1230 session.create_node_with_props(
1231 &["Person"],
1232 [
1233 ("name", Value::String("Bob".into())),
1234 ("age", Value::Int64(25)),
1235 ],
1236 );
1237
1238 let result = session
1240 .execute("MATCH (n:Person) RETURN n.name, n.age")
1241 .unwrap();
1242
1243 assert_eq!(result.row_count(), 2);
1245 assert_eq!(result.column_count(), 2);
1246 assert_eq!(result.columns[0], "n.name");
1247 assert_eq!(result.columns[1], "n.age");
1248
1249 let names: Vec<&Value> = result.rows.iter().map(|r| &r[0]).collect();
1251 assert!(names.contains(&&Value::String("Alice".into())));
1252 assert!(names.contains(&&Value::String("Bob".into())));
1253 }
1254
1255 #[test]
1256 fn test_gql_return_mixed_expressions() {
1257 use grafeo_common::types::Value;
1258
1259 let db = GrafeoDB::new_in_memory();
1260 let session = db.session();
1261
1262 session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
1264
1265 let result = session
1267 .execute("MATCH (n:Person) RETURN n, n.name")
1268 .unwrap();
1269
1270 assert_eq!(result.row_count(), 1);
1271 assert_eq!(result.column_count(), 2);
1272 assert_eq!(result.columns[0], "n");
1273 assert_eq!(result.columns[1], "n.name");
1274
1275 assert_eq!(result.rows[0][1], Value::String("Alice".into()));
1277 }
1278 }
1279
1280 #[cfg(feature = "cypher")]
1281 mod cypher_tests {
1282 use super::*;
1283
1284 #[test]
1285 fn test_cypher_query_execution() {
1286 let db = GrafeoDB::new_in_memory();
1287 let session = db.session();
1288
1289 session.create_node(&["Person"]);
1291 session.create_node(&["Person"]);
1292 session.create_node(&["Animal"]);
1293
1294 let result = session.execute_cypher("MATCH (n:Person) RETURN n").unwrap();
1296
1297 assert_eq!(result.row_count(), 2);
1299 assert_eq!(result.column_count(), 1);
1300 assert_eq!(result.columns[0], "n");
1301 }
1302
1303 #[test]
1304 fn test_cypher_empty_result() {
1305 let db = GrafeoDB::new_in_memory();
1306 let session = db.session();
1307
1308 let result = session.execute_cypher("MATCH (n:Person) RETURN n").unwrap();
1310
1311 assert_eq!(result.row_count(), 0);
1312 }
1313
1314 #[test]
1315 fn test_cypher_parse_error() {
1316 let db = GrafeoDB::new_in_memory();
1317 let session = db.session();
1318
1319 let result = session.execute_cypher("MATCH (n RETURN n");
1321
1322 assert!(result.is_err());
1323 }
1324 }
1325
1326 mod direct_lookup_tests {
1329 use super::*;
1330 use grafeo_common::types::Value;
1331
1332 #[test]
1333 fn test_get_node() {
1334 let db = GrafeoDB::new_in_memory();
1335 let session = db.session();
1336
1337 let id = session.create_node(&["Person"]);
1338 let node = session.get_node(id);
1339
1340 assert!(node.is_some());
1341 let node = node.unwrap();
1342 assert_eq!(node.id, id);
1343 }
1344
1345 #[test]
1346 fn test_get_node_not_found() {
1347 use grafeo_common::types::NodeId;
1348
1349 let db = GrafeoDB::new_in_memory();
1350 let session = db.session();
1351
1352 let node = session.get_node(NodeId::new(9999));
1354 assert!(node.is_none());
1355 }
1356
1357 #[test]
1358 fn test_get_node_property() {
1359 let db = GrafeoDB::new_in_memory();
1360 let session = db.session();
1361
1362 let id = session
1363 .create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
1364
1365 let name = session.get_node_property(id, "name");
1366 assert_eq!(name, Some(Value::String("Alice".into())));
1367
1368 let missing = session.get_node_property(id, "missing");
1370 assert!(missing.is_none());
1371 }
1372
1373 #[test]
1374 fn test_get_edge() {
1375 let db = GrafeoDB::new_in_memory();
1376 let session = db.session();
1377
1378 let alice = session.create_node(&["Person"]);
1379 let bob = session.create_node(&["Person"]);
1380 let edge_id = session.create_edge(alice, bob, "KNOWS");
1381
1382 let edge = session.get_edge(edge_id);
1383 assert!(edge.is_some());
1384 let edge = edge.unwrap();
1385 assert_eq!(edge.id, edge_id);
1386 assert_eq!(edge.src, alice);
1387 assert_eq!(edge.dst, bob);
1388 }
1389
1390 #[test]
1391 fn test_get_edge_not_found() {
1392 use grafeo_common::types::EdgeId;
1393
1394 let db = GrafeoDB::new_in_memory();
1395 let session = db.session();
1396
1397 let edge = session.get_edge(EdgeId::new(9999));
1398 assert!(edge.is_none());
1399 }
1400
1401 #[test]
1402 fn test_get_neighbors_outgoing() {
1403 let db = GrafeoDB::new_in_memory();
1404 let session = db.session();
1405
1406 let alice = session.create_node(&["Person"]);
1407 let bob = session.create_node(&["Person"]);
1408 let carol = session.create_node(&["Person"]);
1409
1410 session.create_edge(alice, bob, "KNOWS");
1411 session.create_edge(alice, carol, "KNOWS");
1412
1413 let neighbors = session.get_neighbors_outgoing(alice);
1414 assert_eq!(neighbors.len(), 2);
1415
1416 let neighbor_ids: Vec<_> = neighbors.iter().map(|(node_id, _)| *node_id).collect();
1417 assert!(neighbor_ids.contains(&bob));
1418 assert!(neighbor_ids.contains(&carol));
1419 }
1420
1421 #[test]
1422 fn test_get_neighbors_incoming() {
1423 let db = GrafeoDB::new_in_memory();
1424 let session = db.session();
1425
1426 let alice = session.create_node(&["Person"]);
1427 let bob = session.create_node(&["Person"]);
1428 let carol = session.create_node(&["Person"]);
1429
1430 session.create_edge(bob, alice, "KNOWS");
1431 session.create_edge(carol, alice, "KNOWS");
1432
1433 let neighbors = session.get_neighbors_incoming(alice);
1434 assert_eq!(neighbors.len(), 2);
1435
1436 let neighbor_ids: Vec<_> = neighbors.iter().map(|(node_id, _)| *node_id).collect();
1437 assert!(neighbor_ids.contains(&bob));
1438 assert!(neighbor_ids.contains(&carol));
1439 }
1440
1441 #[test]
1442 fn test_get_neighbors_outgoing_by_type() {
1443 let db = GrafeoDB::new_in_memory();
1444 let session = db.session();
1445
1446 let alice = session.create_node(&["Person"]);
1447 let bob = session.create_node(&["Person"]);
1448 let company = session.create_node(&["Company"]);
1449
1450 session.create_edge(alice, bob, "KNOWS");
1451 session.create_edge(alice, company, "WORKS_AT");
1452
1453 let knows_neighbors = session.get_neighbors_outgoing_by_type(alice, "KNOWS");
1454 assert_eq!(knows_neighbors.len(), 1);
1455 assert_eq!(knows_neighbors[0].0, bob);
1456
1457 let works_neighbors = session.get_neighbors_outgoing_by_type(alice, "WORKS_AT");
1458 assert_eq!(works_neighbors.len(), 1);
1459 assert_eq!(works_neighbors[0].0, company);
1460
1461 let no_neighbors = session.get_neighbors_outgoing_by_type(alice, "LIKES");
1463 assert!(no_neighbors.is_empty());
1464 }
1465
1466 #[test]
1467 fn test_node_exists() {
1468 use grafeo_common::types::NodeId;
1469
1470 let db = GrafeoDB::new_in_memory();
1471 let session = db.session();
1472
1473 let id = session.create_node(&["Person"]);
1474
1475 assert!(session.node_exists(id));
1476 assert!(!session.node_exists(NodeId::new(9999)));
1477 }
1478
1479 #[test]
1480 fn test_edge_exists() {
1481 use grafeo_common::types::EdgeId;
1482
1483 let db = GrafeoDB::new_in_memory();
1484 let session = db.session();
1485
1486 let alice = session.create_node(&["Person"]);
1487 let bob = session.create_node(&["Person"]);
1488 let edge_id = session.create_edge(alice, bob, "KNOWS");
1489
1490 assert!(session.edge_exists(edge_id));
1491 assert!(!session.edge_exists(EdgeId::new(9999)));
1492 }
1493
1494 #[test]
1495 fn test_get_degree() {
1496 let db = GrafeoDB::new_in_memory();
1497 let session = db.session();
1498
1499 let alice = session.create_node(&["Person"]);
1500 let bob = session.create_node(&["Person"]);
1501 let carol = session.create_node(&["Person"]);
1502
1503 session.create_edge(alice, bob, "KNOWS");
1505 session.create_edge(alice, carol, "KNOWS");
1506 session.create_edge(bob, alice, "KNOWS");
1508
1509 let (out_degree, in_degree) = session.get_degree(alice);
1510 assert_eq!(out_degree, 2);
1511 assert_eq!(in_degree, 1);
1512
1513 let lonely = session.create_node(&["Person"]);
1515 let (out, in_deg) = session.get_degree(lonely);
1516 assert_eq!(out, 0);
1517 assert_eq!(in_deg, 0);
1518 }
1519
1520 #[test]
1521 fn test_get_nodes_batch() {
1522 let db = GrafeoDB::new_in_memory();
1523 let session = db.session();
1524
1525 let alice = session.create_node(&["Person"]);
1526 let bob = session.create_node(&["Person"]);
1527 let carol = session.create_node(&["Person"]);
1528
1529 let nodes = session.get_nodes_batch(&[alice, bob, carol]);
1530 assert_eq!(nodes.len(), 3);
1531 assert!(nodes[0].is_some());
1532 assert!(nodes[1].is_some());
1533 assert!(nodes[2].is_some());
1534
1535 use grafeo_common::types::NodeId;
1537 let nodes_with_missing = session.get_nodes_batch(&[alice, NodeId::new(9999), carol]);
1538 assert_eq!(nodes_with_missing.len(), 3);
1539 assert!(nodes_with_missing[0].is_some());
1540 assert!(nodes_with_missing[1].is_none()); assert!(nodes_with_missing[2].is_some());
1542 }
1543
1544 #[test]
1545 fn test_auto_commit_setting() {
1546 let db = GrafeoDB::new_in_memory();
1547 let mut session = db.session();
1548
1549 assert!(session.auto_commit());
1551
1552 session.set_auto_commit(false);
1553 assert!(!session.auto_commit());
1554
1555 session.set_auto_commit(true);
1556 assert!(session.auto_commit());
1557 }
1558
1559 #[test]
1560 fn test_transaction_double_begin_error() {
1561 let db = GrafeoDB::new_in_memory();
1562 let mut session = db.session();
1563
1564 session.begin_tx().unwrap();
1565 let result = session.begin_tx();
1566
1567 assert!(result.is_err());
1568 session.rollback().unwrap();
1570 }
1571
1572 #[test]
1573 fn test_commit_without_transaction_error() {
1574 let db = GrafeoDB::new_in_memory();
1575 let mut session = db.session();
1576
1577 let result = session.commit();
1578 assert!(result.is_err());
1579 }
1580
1581 #[test]
1582 fn test_rollback_without_transaction_error() {
1583 let db = GrafeoDB::new_in_memory();
1584 let mut session = db.session();
1585
1586 let result = session.rollback();
1587 assert!(result.is_err());
1588 }
1589
1590 #[test]
1591 fn test_create_edge_in_transaction() {
1592 let db = GrafeoDB::new_in_memory();
1593 let mut session = db.session();
1594
1595 let alice = session.create_node(&["Person"]);
1597 let bob = session.create_node(&["Person"]);
1598
1599 session.begin_tx().unwrap();
1601 let edge_id = session.create_edge(alice, bob, "KNOWS");
1602
1603 assert!(session.edge_exists(edge_id));
1605
1606 session.commit().unwrap();
1608
1609 assert!(session.edge_exists(edge_id));
1611 }
1612
1613 #[test]
1614 fn test_neighbors_empty_node() {
1615 let db = GrafeoDB::new_in_memory();
1616 let session = db.session();
1617
1618 let lonely = session.create_node(&["Person"]);
1619
1620 assert!(session.get_neighbors_outgoing(lonely).is_empty());
1621 assert!(session.get_neighbors_incoming(lonely).is_empty());
1622 assert!(
1623 session
1624 .get_neighbors_outgoing_by_type(lonely, "KNOWS")
1625 .is_empty()
1626 );
1627 }
1628 }
1629}