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 cache_key = CacheKey::new(query, QueryLanguage::Gql);
145
146 let optimized_plan = if let Some(cached_plan) = self.query_cache.get_optimized(&cache_key) {
148 cached_plan
150 } else {
151 let logical_plan = gql_translator::translate(query)?;
155
156 let mut binder = Binder::new();
158 let _binding_context = binder.bind(&logical_plan)?;
159
160 let optimizer = Optimizer::new();
162 let plan = optimizer.optimize(logical_plan)?;
163
164 self.query_cache.put_optimized(cache_key, plan.clone());
166
167 plan
168 };
169
170 let (viewing_epoch, tx_id) = self.get_transaction_context();
172
173 let planner = Planner::with_context(
176 Arc::clone(&self.store),
177 Arc::clone(&self.tx_manager),
178 tx_id,
179 viewing_epoch,
180 )
181 .with_factorized_execution(self.factorized_execution);
182 let mut physical_plan = planner.plan(&optimized_plan)?;
183
184 let executor = Executor::with_columns(physical_plan.columns.clone());
186 executor.execute(physical_plan.operator.as_mut())
187 }
188
189 #[cfg(feature = "gql")]
195 pub fn execute_with_params(
196 &self,
197 query: &str,
198 params: std::collections::HashMap<String, Value>,
199 ) -> Result<QueryResult> {
200 use crate::query::processor::{QueryLanguage, QueryProcessor};
201
202 let (viewing_epoch, tx_id) = self.get_transaction_context();
204
205 let processor =
207 QueryProcessor::for_lpg_with_tx(Arc::clone(&self.store), Arc::clone(&self.tx_manager));
208
209 let processor = if let Some(tx_id) = tx_id {
211 processor.with_tx_context(viewing_epoch, tx_id)
212 } else {
213 processor
214 };
215
216 processor.process(query, QueryLanguage::Gql, Some(¶ms))
217 }
218
219 #[cfg(not(any(feature = "gql", feature = "cypher")))]
225 pub fn execute_with_params(
226 &self,
227 _query: &str,
228 _params: std::collections::HashMap<String, Value>,
229 ) -> Result<QueryResult> {
230 Err(grafeo_common::utils::error::Error::Internal(
231 "No query language enabled".to_string(),
232 ))
233 }
234
235 #[cfg(not(any(feature = "gql", feature = "cypher")))]
241 pub fn execute(&self, _query: &str) -> Result<QueryResult> {
242 Err(grafeo_common::utils::error::Error::Internal(
243 "No query language enabled".to_string(),
244 ))
245 }
246
247 #[cfg(feature = "cypher")]
253 pub fn execute_cypher(&self, query: &str) -> Result<QueryResult> {
254 use crate::query::{
255 Executor, Planner, binder::Binder, cache::CacheKey, cypher_translator,
256 optimizer::Optimizer, processor::QueryLanguage,
257 };
258
259 let cache_key = CacheKey::new(query, QueryLanguage::Cypher);
261
262 let optimized_plan = if let Some(cached_plan) = self.query_cache.get_optimized(&cache_key) {
264 cached_plan
265 } else {
266 let logical_plan = cypher_translator::translate(query)?;
268
269 let mut binder = Binder::new();
271 let _binding_context = binder.bind(&logical_plan)?;
272
273 let optimizer = Optimizer::new();
275 let plan = optimizer.optimize(logical_plan)?;
276
277 self.query_cache.put_optimized(cache_key, plan.clone());
279
280 plan
281 };
282
283 let (viewing_epoch, tx_id) = self.get_transaction_context();
285
286 let planner = Planner::with_context(
288 Arc::clone(&self.store),
289 Arc::clone(&self.tx_manager),
290 tx_id,
291 viewing_epoch,
292 )
293 .with_factorized_execution(self.factorized_execution);
294 let mut physical_plan = planner.plan(&optimized_plan)?;
295
296 let executor = Executor::with_columns(physical_plan.columns.clone());
298 executor.execute(physical_plan.operator.as_mut())
299 }
300
301 #[cfg(feature = "gremlin")]
322 pub fn execute_gremlin(&self, query: &str) -> Result<QueryResult> {
323 use crate::query::{
324 Executor, Planner, binder::Binder, gremlin_translator, optimizer::Optimizer,
325 };
326
327 let logical_plan = gremlin_translator::translate(query)?;
329
330 let mut binder = Binder::new();
332 let _binding_context = binder.bind(&logical_plan)?;
333
334 let optimizer = Optimizer::new();
336 let optimized_plan = optimizer.optimize(logical_plan)?;
337
338 let (viewing_epoch, tx_id) = self.get_transaction_context();
340
341 let planner = Planner::with_context(
343 Arc::clone(&self.store),
344 Arc::clone(&self.tx_manager),
345 tx_id,
346 viewing_epoch,
347 )
348 .with_factorized_execution(self.factorized_execution);
349 let mut physical_plan = planner.plan(&optimized_plan)?;
350
351 let executor = Executor::with_columns(physical_plan.columns.clone());
353 executor.execute(physical_plan.operator.as_mut())
354 }
355
356 #[cfg(feature = "gremlin")]
362 pub fn execute_gremlin_with_params(
363 &self,
364 query: &str,
365 params: std::collections::HashMap<String, Value>,
366 ) -> Result<QueryResult> {
367 use crate::query::processor::{QueryLanguage, QueryProcessor};
368
369 let (viewing_epoch, tx_id) = self.get_transaction_context();
371
372 let processor =
374 QueryProcessor::for_lpg_with_tx(Arc::clone(&self.store), Arc::clone(&self.tx_manager));
375
376 let processor = if let Some(tx_id) = tx_id {
378 processor.with_tx_context(viewing_epoch, tx_id)
379 } else {
380 processor
381 };
382
383 processor.process(query, QueryLanguage::Gremlin, Some(¶ms))
384 }
385
386 #[cfg(feature = "graphql")]
407 pub fn execute_graphql(&self, query: &str) -> Result<QueryResult> {
408 use crate::query::{
409 Executor, Planner, binder::Binder, graphql_translator, optimizer::Optimizer,
410 };
411
412 let logical_plan = graphql_translator::translate(query)?;
414
415 let mut binder = Binder::new();
417 let _binding_context = binder.bind(&logical_plan)?;
418
419 let optimizer = Optimizer::new();
421 let optimized_plan = optimizer.optimize(logical_plan)?;
422
423 let (viewing_epoch, tx_id) = self.get_transaction_context();
425
426 let planner = Planner::with_context(
428 Arc::clone(&self.store),
429 Arc::clone(&self.tx_manager),
430 tx_id,
431 viewing_epoch,
432 )
433 .with_factorized_execution(self.factorized_execution);
434 let mut physical_plan = planner.plan(&optimized_plan)?;
435
436 let executor = Executor::with_columns(physical_plan.columns.clone());
438 executor.execute(physical_plan.operator.as_mut())
439 }
440
441 #[cfg(feature = "graphql")]
447 pub fn execute_graphql_with_params(
448 &self,
449 query: &str,
450 params: std::collections::HashMap<String, Value>,
451 ) -> Result<QueryResult> {
452 use crate::query::processor::{QueryLanguage, QueryProcessor};
453
454 let (viewing_epoch, tx_id) = self.get_transaction_context();
456
457 let processor =
459 QueryProcessor::for_lpg_with_tx(Arc::clone(&self.store), Arc::clone(&self.tx_manager));
460
461 let processor = if let Some(tx_id) = tx_id {
463 processor.with_tx_context(viewing_epoch, tx_id)
464 } else {
465 processor
466 };
467
468 processor.process(query, QueryLanguage::GraphQL, Some(¶ms))
469 }
470
471 #[cfg(all(feature = "sparql", feature = "rdf"))]
477 pub fn execute_sparql(&self, query: &str) -> Result<QueryResult> {
478 use crate::query::{
479 Executor, optimizer::Optimizer, planner_rdf::RdfPlanner, sparql_translator,
480 };
481
482 let logical_plan = sparql_translator::translate(query)?;
484
485 let optimizer = Optimizer::new();
487 let optimized_plan = optimizer.optimize(logical_plan)?;
488
489 let planner = RdfPlanner::new(Arc::clone(&self.rdf_store)).with_tx_id(self.current_tx);
491 let mut physical_plan = planner.plan(&optimized_plan)?;
492
493 let executor = Executor::with_columns(physical_plan.columns.clone());
495 executor.execute(physical_plan.operator.as_mut())
496 }
497
498 #[cfg(all(feature = "sparql", feature = "rdf"))]
504 pub fn execute_sparql_with_params(
505 &self,
506 query: &str,
507 _params: std::collections::HashMap<String, Value>,
508 ) -> Result<QueryResult> {
509 self.execute_sparql(query)
512 }
513
514 pub fn begin_tx(&mut self) -> Result<()> {
534 if self.current_tx.is_some() {
535 return Err(grafeo_common::utils::error::Error::Transaction(
536 grafeo_common::utils::error::TransactionError::InvalidState(
537 "Transaction already active".to_string(),
538 ),
539 ));
540 }
541
542 let tx_id = self.tx_manager.begin();
543 self.current_tx = Some(tx_id);
544 Ok(())
545 }
546
547 pub fn commit(&mut self) -> Result<()> {
555 let tx_id = self.current_tx.take().ok_or_else(|| {
556 grafeo_common::utils::error::Error::Transaction(
557 grafeo_common::utils::error::TransactionError::InvalidState(
558 "No active transaction".to_string(),
559 ),
560 )
561 })?;
562
563 #[cfg(feature = "rdf")]
565 self.rdf_store.commit_tx(tx_id);
566
567 self.tx_manager.commit(tx_id).map(|_| ())
568 }
569
570 pub fn rollback(&mut self) -> Result<()> {
591 let tx_id = self.current_tx.take().ok_or_else(|| {
592 grafeo_common::utils::error::Error::Transaction(
593 grafeo_common::utils::error::TransactionError::InvalidState(
594 "No active transaction".to_string(),
595 ),
596 )
597 })?;
598
599 self.store.discard_uncommitted_versions(tx_id);
601
602 #[cfg(feature = "rdf")]
604 self.rdf_store.rollback_tx(tx_id);
605
606 self.tx_manager.abort(tx_id)
608 }
609
610 #[must_use]
612 pub fn in_transaction(&self) -> bool {
613 self.current_tx.is_some()
614 }
615
616 pub fn set_auto_commit(&mut self, auto_commit: bool) {
618 self.auto_commit = auto_commit;
619 }
620
621 #[must_use]
623 pub fn auto_commit(&self) -> bool {
624 self.auto_commit
625 }
626
627 #[must_use]
633 fn get_transaction_context(&self) -> (EpochId, Option<TxId>) {
634 if let Some(tx_id) = self.current_tx {
635 let epoch = self
637 .tx_manager
638 .start_epoch(tx_id)
639 .unwrap_or_else(|| self.tx_manager.current_epoch());
640 (epoch, Some(tx_id))
641 } else {
642 (self.tx_manager.current_epoch(), None)
644 }
645 }
646
647 pub fn create_node(&self, labels: &[&str]) -> NodeId {
652 let (epoch, tx_id) = self.get_transaction_context();
653 self.store
654 .create_node_versioned(labels, epoch, tx_id.unwrap_or(TxId::SYSTEM))
655 }
656
657 pub fn create_node_with_props<'a>(
661 &self,
662 labels: &[&str],
663 properties: impl IntoIterator<Item = (&'a str, Value)>,
664 ) -> NodeId {
665 let (epoch, tx_id) = self.get_transaction_context();
666 self.store.create_node_with_props_versioned(
667 labels,
668 properties.into_iter().map(|(k, v)| (k, v)),
669 epoch,
670 tx_id.unwrap_or(TxId::SYSTEM),
671 )
672 }
673
674 pub fn create_edge(
679 &self,
680 src: NodeId,
681 dst: NodeId,
682 edge_type: &str,
683 ) -> grafeo_common::types::EdgeId {
684 let (epoch, tx_id) = self.get_transaction_context();
685 self.store
686 .create_edge_versioned(src, dst, edge_type, epoch, tx_id.unwrap_or(TxId::SYSTEM))
687 }
688
689 #[must_use]
715 pub fn get_node(&self, id: NodeId) -> Option<Node> {
716 let (epoch, tx_id) = self.get_transaction_context();
717 self.store
718 .get_node_versioned(id, epoch, tx_id.unwrap_or(TxId::SYSTEM))
719 }
720
721 #[must_use]
742 pub fn get_node_property(&self, id: NodeId, key: &str) -> Option<Value> {
743 self.get_node(id)
744 .and_then(|node| node.get_property(key).cloned())
745 }
746
747 #[must_use]
754 pub fn get_edge(&self, id: EdgeId) -> Option<Edge> {
755 let (epoch, tx_id) = self.get_transaction_context();
756 self.store
757 .get_edge_versioned(id, epoch, tx_id.unwrap_or(TxId::SYSTEM))
758 }
759
760 #[must_use]
784 pub fn get_neighbors_outgoing(&self, node: NodeId) -> Vec<(NodeId, EdgeId)> {
785 self.store.edges_from(node, Direction::Outgoing).collect()
786 }
787
788 #[must_use]
797 pub fn get_neighbors_incoming(&self, node: NodeId) -> Vec<(NodeId, EdgeId)> {
798 self.store.edges_from(node, Direction::Incoming).collect()
799 }
800
801 #[must_use]
809 pub fn get_neighbors_outgoing_by_type(
810 &self,
811 node: NodeId,
812 edge_type: &str,
813 ) -> Vec<(NodeId, EdgeId)> {
814 self.store
815 .edges_from(node, Direction::Outgoing)
816 .filter(|(_, edge_id)| {
817 self.get_edge(*edge_id)
818 .is_some_and(|e| e.edge_type.as_ref() == edge_type)
819 })
820 .collect()
821 }
822
823 #[must_use]
830 pub fn node_exists(&self, id: NodeId) -> bool {
831 self.get_node(id).is_some()
832 }
833
834 #[must_use]
836 pub fn edge_exists(&self, id: EdgeId) -> bool {
837 self.get_edge(id).is_some()
838 }
839
840 #[must_use]
844 pub fn get_degree(&self, node: NodeId) -> (usize, usize) {
845 let out = self.store.out_degree(node);
846 let in_degree = self.store.in_degree(node);
847 (out, in_degree)
848 }
849
850 #[must_use]
860 pub fn get_nodes_batch(&self, ids: &[NodeId]) -> Vec<Option<Node>> {
861 let (epoch, tx_id) = self.get_transaction_context();
862 let tx = tx_id.unwrap_or(TxId::SYSTEM);
863 ids.iter()
864 .map(|&id| self.store.get_node_versioned(id, epoch, tx))
865 .collect()
866 }
867}
868
869#[cfg(test)]
870mod tests {
871 use crate::database::GrafeoDB;
872
873 #[test]
874 fn test_session_create_node() {
875 let db = GrafeoDB::new_in_memory();
876 let session = db.session();
877
878 let id = session.create_node(&["Person"]);
879 assert!(id.is_valid());
880 assert_eq!(db.node_count(), 1);
881 }
882
883 #[test]
884 fn test_session_transaction() {
885 let db = GrafeoDB::new_in_memory();
886 let mut session = db.session();
887
888 assert!(!session.in_transaction());
889
890 session.begin_tx().unwrap();
891 assert!(session.in_transaction());
892
893 session.commit().unwrap();
894 assert!(!session.in_transaction());
895 }
896
897 #[test]
898 fn test_session_transaction_context() {
899 let db = GrafeoDB::new_in_memory();
900 let mut session = db.session();
901
902 let (_epoch1, tx_id1) = session.get_transaction_context();
904 assert!(tx_id1.is_none());
905
906 session.begin_tx().unwrap();
908 let (epoch2, tx_id2) = session.get_transaction_context();
909 assert!(tx_id2.is_some());
910 let _ = epoch2; session.commit().unwrap();
915 let (epoch3, tx_id3) = session.get_transaction_context();
916 assert!(tx_id3.is_none());
917 assert!(epoch3.as_u64() >= epoch2.as_u64());
919 }
920
921 #[test]
922 fn test_session_rollback() {
923 let db = GrafeoDB::new_in_memory();
924 let mut session = db.session();
925
926 session.begin_tx().unwrap();
927 session.rollback().unwrap();
928 assert!(!session.in_transaction());
929 }
930
931 #[test]
932 fn test_session_rollback_discards_versions() {
933 use grafeo_common::types::TxId;
934
935 let db = GrafeoDB::new_in_memory();
936
937 let node_before = db.store().create_node(&["Person"]);
939 assert!(node_before.is_valid());
940 assert_eq!(db.node_count(), 1, "Should have 1 node before transaction");
941
942 let mut session = db.session();
944 session.begin_tx().unwrap();
945 let tx_id = session.current_tx.unwrap();
946
947 let epoch = db.store().current_epoch();
949 let node_in_tx = db.store().create_node_versioned(&["Person"], epoch, tx_id);
950 assert!(node_in_tx.is_valid());
951
952 assert_eq!(db.node_count(), 2, "Should have 2 nodes during transaction");
954
955 session.rollback().unwrap();
957 assert!(!session.in_transaction());
958
959 let count_after = db.node_count();
962 assert_eq!(
963 count_after, 1,
964 "Rollback should discard uncommitted node, but got {count_after}"
965 );
966
967 let current_epoch = db.store().current_epoch();
969 assert!(
970 db.store()
971 .get_node_versioned(node_before, current_epoch, TxId::SYSTEM)
972 .is_some(),
973 "Original node should still exist"
974 );
975
976 assert!(
978 db.store()
979 .get_node_versioned(node_in_tx, current_epoch, TxId::SYSTEM)
980 .is_none(),
981 "Transaction node should be gone"
982 );
983 }
984
985 #[test]
986 fn test_session_create_node_in_transaction() {
987 let db = GrafeoDB::new_in_memory();
989
990 let node_before = db.create_node(&["Person"]);
992 assert!(node_before.is_valid());
993 assert_eq!(db.node_count(), 1, "Should have 1 node before transaction");
994
995 let mut session = db.session();
997 session.begin_tx().unwrap();
998
999 let node_in_tx = session.create_node(&["Person"]);
1001 assert!(node_in_tx.is_valid());
1002
1003 assert_eq!(db.node_count(), 2, "Should have 2 nodes during transaction");
1005
1006 session.rollback().unwrap();
1008
1009 let count_after = db.node_count();
1011 assert_eq!(
1012 count_after, 1,
1013 "Rollback should discard node created via session.create_node(), but got {count_after}"
1014 );
1015 }
1016
1017 #[test]
1018 fn test_session_create_node_with_props_in_transaction() {
1019 use grafeo_common::types::Value;
1020
1021 let db = GrafeoDB::new_in_memory();
1023
1024 db.create_node(&["Person"]);
1026 assert_eq!(db.node_count(), 1, "Should have 1 node before transaction");
1027
1028 let mut session = db.session();
1030 session.begin_tx().unwrap();
1031
1032 let node_in_tx =
1033 session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
1034 assert!(node_in_tx.is_valid());
1035
1036 assert_eq!(db.node_count(), 2, "Should have 2 nodes during transaction");
1038
1039 session.rollback().unwrap();
1041
1042 let count_after = db.node_count();
1044 assert_eq!(
1045 count_after, 1,
1046 "Rollback should discard node created via session.create_node_with_props()"
1047 );
1048 }
1049
1050 #[cfg(feature = "gql")]
1051 mod gql_tests {
1052 use super::*;
1053
1054 #[test]
1055 fn test_gql_query_execution() {
1056 let db = GrafeoDB::new_in_memory();
1057 let session = db.session();
1058
1059 session.create_node(&["Person"]);
1061 session.create_node(&["Person"]);
1062 session.create_node(&["Animal"]);
1063
1064 let result = session.execute("MATCH (n:Person) RETURN n").unwrap();
1066
1067 assert_eq!(result.row_count(), 2);
1069 assert_eq!(result.column_count(), 1);
1070 assert_eq!(result.columns[0], "n");
1071 }
1072
1073 #[test]
1074 fn test_gql_empty_result() {
1075 let db = GrafeoDB::new_in_memory();
1076 let session = db.session();
1077
1078 let result = session.execute("MATCH (n:Person) RETURN n").unwrap();
1080
1081 assert_eq!(result.row_count(), 0);
1082 }
1083
1084 #[test]
1085 fn test_gql_parse_error() {
1086 let db = GrafeoDB::new_in_memory();
1087 let session = db.session();
1088
1089 let result = session.execute("MATCH (n RETURN n");
1091
1092 assert!(result.is_err());
1093 }
1094
1095 #[test]
1096 fn test_gql_relationship_traversal() {
1097 let db = GrafeoDB::new_in_memory();
1098 let session = db.session();
1099
1100 let alice = session.create_node(&["Person"]);
1102 let bob = session.create_node(&["Person"]);
1103 let charlie = session.create_node(&["Person"]);
1104
1105 session.create_edge(alice, bob, "KNOWS");
1106 session.create_edge(alice, charlie, "KNOWS");
1107
1108 let result = session
1110 .execute("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b")
1111 .unwrap();
1112
1113 assert_eq!(result.row_count(), 2);
1115 assert_eq!(result.column_count(), 2);
1116 assert_eq!(result.columns[0], "a");
1117 assert_eq!(result.columns[1], "b");
1118 }
1119
1120 #[test]
1121 fn test_gql_relationship_with_type_filter() {
1122 let db = GrafeoDB::new_in_memory();
1123 let session = db.session();
1124
1125 let alice = session.create_node(&["Person"]);
1127 let bob = session.create_node(&["Person"]);
1128 let charlie = session.create_node(&["Person"]);
1129
1130 session.create_edge(alice, bob, "KNOWS");
1131 session.create_edge(alice, charlie, "WORKS_WITH");
1132
1133 let result = session
1135 .execute("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b")
1136 .unwrap();
1137
1138 assert_eq!(result.row_count(), 1);
1140 }
1141
1142 #[test]
1143 fn test_gql_semantic_error_undefined_variable() {
1144 let db = GrafeoDB::new_in_memory();
1145 let session = db.session();
1146
1147 let result = session.execute("MATCH (n:Person) RETURN x");
1149
1150 assert!(result.is_err());
1152 let err = match result {
1153 Err(e) => e,
1154 Ok(_) => panic!("Expected error"),
1155 };
1156 assert!(
1157 err.to_string().contains("Undefined variable"),
1158 "Expected undefined variable error, got: {}",
1159 err
1160 );
1161 }
1162
1163 #[test]
1164 fn test_gql_where_clause_property_filter() {
1165 use grafeo_common::types::Value;
1166
1167 let db = GrafeoDB::new_in_memory();
1168 let session = db.session();
1169
1170 session.create_node_with_props(&["Person"], [("age", Value::Int64(25))]);
1172 session.create_node_with_props(&["Person"], [("age", Value::Int64(35))]);
1173 session.create_node_with_props(&["Person"], [("age", Value::Int64(45))]);
1174
1175 let result = session
1177 .execute("MATCH (n:Person) WHERE n.age > 30 RETURN n")
1178 .unwrap();
1179
1180 assert_eq!(result.row_count(), 2);
1182 }
1183
1184 #[test]
1185 fn test_gql_where_clause_equality() {
1186 use grafeo_common::types::Value;
1187
1188 let db = GrafeoDB::new_in_memory();
1189 let session = db.session();
1190
1191 session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
1193 session.create_node_with_props(&["Person"], [("name", Value::String("Bob".into()))]);
1194 session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
1195
1196 let result = session
1198 .execute("MATCH (n:Person) WHERE n.name = \"Alice\" RETURN n")
1199 .unwrap();
1200
1201 assert_eq!(result.row_count(), 2);
1203 }
1204
1205 #[test]
1206 fn test_gql_return_property_access() {
1207 use grafeo_common::types::Value;
1208
1209 let db = GrafeoDB::new_in_memory();
1210 let session = db.session();
1211
1212 session.create_node_with_props(
1214 &["Person"],
1215 [
1216 ("name", Value::String("Alice".into())),
1217 ("age", Value::Int64(30)),
1218 ],
1219 );
1220 session.create_node_with_props(
1221 &["Person"],
1222 [
1223 ("name", Value::String("Bob".into())),
1224 ("age", Value::Int64(25)),
1225 ],
1226 );
1227
1228 let result = session
1230 .execute("MATCH (n:Person) RETURN n.name, n.age")
1231 .unwrap();
1232
1233 assert_eq!(result.row_count(), 2);
1235 assert_eq!(result.column_count(), 2);
1236 assert_eq!(result.columns[0], "n.name");
1237 assert_eq!(result.columns[1], "n.age");
1238
1239 let names: Vec<&Value> = result.rows.iter().map(|r| &r[0]).collect();
1241 assert!(names.contains(&&Value::String("Alice".into())));
1242 assert!(names.contains(&&Value::String("Bob".into())));
1243 }
1244
1245 #[test]
1246 fn test_gql_return_mixed_expressions() {
1247 use grafeo_common::types::Value;
1248
1249 let db = GrafeoDB::new_in_memory();
1250 let session = db.session();
1251
1252 session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
1254
1255 let result = session
1257 .execute("MATCH (n:Person) RETURN n, n.name")
1258 .unwrap();
1259
1260 assert_eq!(result.row_count(), 1);
1261 assert_eq!(result.column_count(), 2);
1262 assert_eq!(result.columns[0], "n");
1263 assert_eq!(result.columns[1], "n.name");
1264
1265 assert_eq!(result.rows[0][1], Value::String("Alice".into()));
1267 }
1268 }
1269
1270 #[cfg(feature = "cypher")]
1271 mod cypher_tests {
1272 use super::*;
1273
1274 #[test]
1275 fn test_cypher_query_execution() {
1276 let db = GrafeoDB::new_in_memory();
1277 let session = db.session();
1278
1279 session.create_node(&["Person"]);
1281 session.create_node(&["Person"]);
1282 session.create_node(&["Animal"]);
1283
1284 let result = session.execute_cypher("MATCH (n:Person) RETURN n").unwrap();
1286
1287 assert_eq!(result.row_count(), 2);
1289 assert_eq!(result.column_count(), 1);
1290 assert_eq!(result.columns[0], "n");
1291 }
1292
1293 #[test]
1294 fn test_cypher_empty_result() {
1295 let db = GrafeoDB::new_in_memory();
1296 let session = db.session();
1297
1298 let result = session.execute_cypher("MATCH (n:Person) RETURN n").unwrap();
1300
1301 assert_eq!(result.row_count(), 0);
1302 }
1303
1304 #[test]
1305 fn test_cypher_parse_error() {
1306 let db = GrafeoDB::new_in_memory();
1307 let session = db.session();
1308
1309 let result = session.execute_cypher("MATCH (n RETURN n");
1311
1312 assert!(result.is_err());
1313 }
1314 }
1315
1316 mod direct_lookup_tests {
1319 use super::*;
1320 use grafeo_common::types::Value;
1321
1322 #[test]
1323 fn test_get_node() {
1324 let db = GrafeoDB::new_in_memory();
1325 let session = db.session();
1326
1327 let id = session.create_node(&["Person"]);
1328 let node = session.get_node(id);
1329
1330 assert!(node.is_some());
1331 let node = node.unwrap();
1332 assert_eq!(node.id, id);
1333 }
1334
1335 #[test]
1336 fn test_get_node_not_found() {
1337 use grafeo_common::types::NodeId;
1338
1339 let db = GrafeoDB::new_in_memory();
1340 let session = db.session();
1341
1342 let node = session.get_node(NodeId::new(9999));
1344 assert!(node.is_none());
1345 }
1346
1347 #[test]
1348 fn test_get_node_property() {
1349 let db = GrafeoDB::new_in_memory();
1350 let session = db.session();
1351
1352 let id = session
1353 .create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
1354
1355 let name = session.get_node_property(id, "name");
1356 assert_eq!(name, Some(Value::String("Alice".into())));
1357
1358 let missing = session.get_node_property(id, "missing");
1360 assert!(missing.is_none());
1361 }
1362
1363 #[test]
1364 fn test_get_edge() {
1365 let db = GrafeoDB::new_in_memory();
1366 let session = db.session();
1367
1368 let alice = session.create_node(&["Person"]);
1369 let bob = session.create_node(&["Person"]);
1370 let edge_id = session.create_edge(alice, bob, "KNOWS");
1371
1372 let edge = session.get_edge(edge_id);
1373 assert!(edge.is_some());
1374 let edge = edge.unwrap();
1375 assert_eq!(edge.id, edge_id);
1376 assert_eq!(edge.src, alice);
1377 assert_eq!(edge.dst, bob);
1378 }
1379
1380 #[test]
1381 fn test_get_edge_not_found() {
1382 use grafeo_common::types::EdgeId;
1383
1384 let db = GrafeoDB::new_in_memory();
1385 let session = db.session();
1386
1387 let edge = session.get_edge(EdgeId::new(9999));
1388 assert!(edge.is_none());
1389 }
1390
1391 #[test]
1392 fn test_get_neighbors_outgoing() {
1393 let db = GrafeoDB::new_in_memory();
1394 let session = db.session();
1395
1396 let alice = session.create_node(&["Person"]);
1397 let bob = session.create_node(&["Person"]);
1398 let carol = session.create_node(&["Person"]);
1399
1400 session.create_edge(alice, bob, "KNOWS");
1401 session.create_edge(alice, carol, "KNOWS");
1402
1403 let neighbors = session.get_neighbors_outgoing(alice);
1404 assert_eq!(neighbors.len(), 2);
1405
1406 let neighbor_ids: Vec<_> = neighbors.iter().map(|(node_id, _)| *node_id).collect();
1407 assert!(neighbor_ids.contains(&bob));
1408 assert!(neighbor_ids.contains(&carol));
1409 }
1410
1411 #[test]
1412 fn test_get_neighbors_incoming() {
1413 let db = GrafeoDB::new_in_memory();
1414 let session = db.session();
1415
1416 let alice = session.create_node(&["Person"]);
1417 let bob = session.create_node(&["Person"]);
1418 let carol = session.create_node(&["Person"]);
1419
1420 session.create_edge(bob, alice, "KNOWS");
1421 session.create_edge(carol, alice, "KNOWS");
1422
1423 let neighbors = session.get_neighbors_incoming(alice);
1424 assert_eq!(neighbors.len(), 2);
1425
1426 let neighbor_ids: Vec<_> = neighbors.iter().map(|(node_id, _)| *node_id).collect();
1427 assert!(neighbor_ids.contains(&bob));
1428 assert!(neighbor_ids.contains(&carol));
1429 }
1430
1431 #[test]
1432 fn test_get_neighbors_outgoing_by_type() {
1433 let db = GrafeoDB::new_in_memory();
1434 let session = db.session();
1435
1436 let alice = session.create_node(&["Person"]);
1437 let bob = session.create_node(&["Person"]);
1438 let company = session.create_node(&["Company"]);
1439
1440 session.create_edge(alice, bob, "KNOWS");
1441 session.create_edge(alice, company, "WORKS_AT");
1442
1443 let knows_neighbors = session.get_neighbors_outgoing_by_type(alice, "KNOWS");
1444 assert_eq!(knows_neighbors.len(), 1);
1445 assert_eq!(knows_neighbors[0].0, bob);
1446
1447 let works_neighbors = session.get_neighbors_outgoing_by_type(alice, "WORKS_AT");
1448 assert_eq!(works_neighbors.len(), 1);
1449 assert_eq!(works_neighbors[0].0, company);
1450
1451 let no_neighbors = session.get_neighbors_outgoing_by_type(alice, "LIKES");
1453 assert!(no_neighbors.is_empty());
1454 }
1455
1456 #[test]
1457 fn test_node_exists() {
1458 use grafeo_common::types::NodeId;
1459
1460 let db = GrafeoDB::new_in_memory();
1461 let session = db.session();
1462
1463 let id = session.create_node(&["Person"]);
1464
1465 assert!(session.node_exists(id));
1466 assert!(!session.node_exists(NodeId::new(9999)));
1467 }
1468
1469 #[test]
1470 fn test_edge_exists() {
1471 use grafeo_common::types::EdgeId;
1472
1473 let db = GrafeoDB::new_in_memory();
1474 let session = db.session();
1475
1476 let alice = session.create_node(&["Person"]);
1477 let bob = session.create_node(&["Person"]);
1478 let edge_id = session.create_edge(alice, bob, "KNOWS");
1479
1480 assert!(session.edge_exists(edge_id));
1481 assert!(!session.edge_exists(EdgeId::new(9999)));
1482 }
1483
1484 #[test]
1485 fn test_get_degree() {
1486 let db = GrafeoDB::new_in_memory();
1487 let session = db.session();
1488
1489 let alice = session.create_node(&["Person"]);
1490 let bob = session.create_node(&["Person"]);
1491 let carol = session.create_node(&["Person"]);
1492
1493 session.create_edge(alice, bob, "KNOWS");
1495 session.create_edge(alice, carol, "KNOWS");
1496 session.create_edge(bob, alice, "KNOWS");
1498
1499 let (out_degree, in_degree) = session.get_degree(alice);
1500 assert_eq!(out_degree, 2);
1501 assert_eq!(in_degree, 1);
1502
1503 let lonely = session.create_node(&["Person"]);
1505 let (out, in_deg) = session.get_degree(lonely);
1506 assert_eq!(out, 0);
1507 assert_eq!(in_deg, 0);
1508 }
1509
1510 #[test]
1511 fn test_get_nodes_batch() {
1512 let db = GrafeoDB::new_in_memory();
1513 let session = db.session();
1514
1515 let alice = session.create_node(&["Person"]);
1516 let bob = session.create_node(&["Person"]);
1517 let carol = session.create_node(&["Person"]);
1518
1519 let nodes = session.get_nodes_batch(&[alice, bob, carol]);
1520 assert_eq!(nodes.len(), 3);
1521 assert!(nodes[0].is_some());
1522 assert!(nodes[1].is_some());
1523 assert!(nodes[2].is_some());
1524
1525 use grafeo_common::types::NodeId;
1527 let nodes_with_missing = session.get_nodes_batch(&[alice, NodeId::new(9999), carol]);
1528 assert_eq!(nodes_with_missing.len(), 3);
1529 assert!(nodes_with_missing[0].is_some());
1530 assert!(nodes_with_missing[1].is_none()); assert!(nodes_with_missing[2].is_some());
1532 }
1533
1534 #[test]
1535 fn test_auto_commit_setting() {
1536 let db = GrafeoDB::new_in_memory();
1537 let mut session = db.session();
1538
1539 assert!(session.auto_commit());
1541
1542 session.set_auto_commit(false);
1543 assert!(!session.auto_commit());
1544
1545 session.set_auto_commit(true);
1546 assert!(session.auto_commit());
1547 }
1548
1549 #[test]
1550 fn test_transaction_double_begin_error() {
1551 let db = GrafeoDB::new_in_memory();
1552 let mut session = db.session();
1553
1554 session.begin_tx().unwrap();
1555 let result = session.begin_tx();
1556
1557 assert!(result.is_err());
1558 session.rollback().unwrap();
1560 }
1561
1562 #[test]
1563 fn test_commit_without_transaction_error() {
1564 let db = GrafeoDB::new_in_memory();
1565 let mut session = db.session();
1566
1567 let result = session.commit();
1568 assert!(result.is_err());
1569 }
1570
1571 #[test]
1572 fn test_rollback_without_transaction_error() {
1573 let db = GrafeoDB::new_in_memory();
1574 let mut session = db.session();
1575
1576 let result = session.rollback();
1577 assert!(result.is_err());
1578 }
1579
1580 #[test]
1581 fn test_create_edge_in_transaction() {
1582 let db = GrafeoDB::new_in_memory();
1583 let mut session = db.session();
1584
1585 let alice = session.create_node(&["Person"]);
1587 let bob = session.create_node(&["Person"]);
1588
1589 session.begin_tx().unwrap();
1591 let edge_id = session.create_edge(alice, bob, "KNOWS");
1592
1593 assert!(session.edge_exists(edge_id));
1595
1596 session.commit().unwrap();
1598
1599 assert!(session.edge_exists(edge_id));
1601 }
1602
1603 #[test]
1604 fn test_neighbors_empty_node() {
1605 let db = GrafeoDB::new_in_memory();
1606 let session = db.session();
1607
1608 let lonely = session.create_node(&["Person"]);
1609
1610 assert!(session.get_neighbors_outgoing(lonely).is_empty());
1611 assert!(session.get_neighbors_incoming(lonely).is_empty());
1612 assert!(
1613 session
1614 .get_neighbors_outgoing_by_type(lonely, "KNOWS")
1615 .is_empty()
1616 );
1617 }
1618 }
1619}