1use std::sync::Arc;
8
9use grafeo_common::types::{EpochId, NodeId, TxId, Value};
10use grafeo_common::utils::error::Result;
11use grafeo_core::graph::lpg::LpgStore;
12#[cfg(feature = "rdf")]
13use grafeo_core::graph::rdf::RdfStore;
14
15use crate::config::AdaptiveConfig;
16use crate::database::QueryResult;
17use crate::transaction::TransactionManager;
18
19pub struct Session {
25 store: Arc<LpgStore>,
27 #[cfg(feature = "rdf")]
29 #[allow(dead_code)]
30 rdf_store: Arc<RdfStore>,
31 tx_manager: Arc<TransactionManager>,
33 current_tx: Option<TxId>,
35 auto_commit: bool,
37 #[allow(dead_code)]
39 adaptive_config: AdaptiveConfig,
40 factorized_execution: bool,
42}
43
44impl Session {
45 #[allow(dead_code)]
47 pub(crate) fn new(store: Arc<LpgStore>, tx_manager: Arc<TransactionManager>) -> Self {
48 Self {
49 store,
50 #[cfg(feature = "rdf")]
51 rdf_store: Arc::new(RdfStore::new()),
52 tx_manager,
53 current_tx: None,
54 auto_commit: true,
55 adaptive_config: AdaptiveConfig::default(),
56 factorized_execution: true,
57 }
58 }
59
60 #[allow(dead_code)]
62 pub(crate) fn with_adaptive(
63 store: Arc<LpgStore>,
64 tx_manager: Arc<TransactionManager>,
65 adaptive_config: AdaptiveConfig,
66 factorized_execution: bool,
67 ) -> Self {
68 Self {
69 store,
70 #[cfg(feature = "rdf")]
71 rdf_store: Arc::new(RdfStore::new()),
72 tx_manager,
73 current_tx: None,
74 auto_commit: true,
75 adaptive_config,
76 factorized_execution,
77 }
78 }
79
80 #[cfg(feature = "rdf")]
82 pub(crate) fn with_rdf_store_and_adaptive(
83 store: Arc<LpgStore>,
84 rdf_store: Arc<RdfStore>,
85 tx_manager: Arc<TransactionManager>,
86 adaptive_config: AdaptiveConfig,
87 factorized_execution: bool,
88 ) -> Self {
89 Self {
90 store,
91 rdf_store,
92 tx_manager,
93 current_tx: None,
94 auto_commit: true,
95 adaptive_config,
96 factorized_execution,
97 }
98 }
99
100 #[cfg(feature = "gql")]
124 pub fn execute(&self, query: &str) -> Result<QueryResult> {
125 use crate::query::{
126 Executor, Planner, binder::Binder, gql_translator, optimizer::Optimizer,
127 };
128
129 let logical_plan = gql_translator::translate(query)?;
131
132 let mut binder = Binder::new();
134 let _binding_context = binder.bind(&logical_plan)?;
135
136 let optimizer = Optimizer::new();
138 let optimized_plan = optimizer.optimize(logical_plan)?;
139
140 let (viewing_epoch, tx_id) = self.get_transaction_context();
142
143 let planner = Planner::with_context(
145 Arc::clone(&self.store),
146 Arc::clone(&self.tx_manager),
147 tx_id,
148 viewing_epoch,
149 )
150 .with_factorized_execution(self.factorized_execution);
151 let mut physical_plan = planner.plan(&optimized_plan)?;
152
153 let executor = Executor::with_columns(physical_plan.columns.clone());
155 executor.execute(physical_plan.operator.as_mut())
156 }
157
158 #[cfg(feature = "gql")]
164 pub fn execute_with_params(
165 &self,
166 query: &str,
167 params: std::collections::HashMap<String, Value>,
168 ) -> Result<QueryResult> {
169 use crate::query::processor::{QueryLanguage, QueryProcessor};
170
171 let (viewing_epoch, tx_id) = self.get_transaction_context();
173
174 let processor =
176 QueryProcessor::for_lpg_with_tx(Arc::clone(&self.store), Arc::clone(&self.tx_manager));
177
178 let processor = if let Some(tx_id) = tx_id {
180 processor.with_tx_context(viewing_epoch, tx_id)
181 } else {
182 processor
183 };
184
185 processor.process(query, QueryLanguage::Gql, Some(¶ms))
186 }
187
188 #[cfg(not(any(feature = "gql", feature = "cypher")))]
194 pub fn execute_with_params(
195 &self,
196 _query: &str,
197 _params: std::collections::HashMap<String, Value>,
198 ) -> Result<QueryResult> {
199 Err(grafeo_common::utils::error::Error::Internal(
200 "No query language enabled".to_string(),
201 ))
202 }
203
204 #[cfg(not(any(feature = "gql", feature = "cypher")))]
210 pub fn execute(&self, _query: &str) -> Result<QueryResult> {
211 Err(grafeo_common::utils::error::Error::Internal(
212 "No query language enabled".to_string(),
213 ))
214 }
215
216 #[cfg(feature = "cypher")]
222 pub fn execute_cypher(&self, query: &str) -> Result<QueryResult> {
223 use crate::query::{
224 Executor, Planner, binder::Binder, cypher_translator, optimizer::Optimizer,
225 };
226
227 let logical_plan = cypher_translator::translate(query)?;
229
230 let mut binder = Binder::new();
232 let _binding_context = binder.bind(&logical_plan)?;
233
234 let optimizer = Optimizer::new();
236 let optimized_plan = optimizer.optimize(logical_plan)?;
237
238 let (viewing_epoch, tx_id) = self.get_transaction_context();
240
241 let planner = Planner::with_context(
243 Arc::clone(&self.store),
244 Arc::clone(&self.tx_manager),
245 tx_id,
246 viewing_epoch,
247 )
248 .with_factorized_execution(self.factorized_execution);
249 let mut physical_plan = planner.plan(&optimized_plan)?;
250
251 let executor = Executor::with_columns(physical_plan.columns.clone());
253 executor.execute(physical_plan.operator.as_mut())
254 }
255
256 #[cfg(feature = "gremlin")]
277 pub fn execute_gremlin(&self, query: &str) -> Result<QueryResult> {
278 use crate::query::{
279 Executor, Planner, binder::Binder, gremlin_translator, optimizer::Optimizer,
280 };
281
282 let logical_plan = gremlin_translator::translate(query)?;
284
285 let mut binder = Binder::new();
287 let _binding_context = binder.bind(&logical_plan)?;
288
289 let optimizer = Optimizer::new();
291 let optimized_plan = optimizer.optimize(logical_plan)?;
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")]
317 pub fn execute_gremlin_with_params(
318 &self,
319 query: &str,
320 params: std::collections::HashMap<String, Value>,
321 ) -> Result<QueryResult> {
322 use crate::query::processor::{QueryLanguage, QueryProcessor};
323
324 let (viewing_epoch, tx_id) = self.get_transaction_context();
326
327 let processor =
329 QueryProcessor::for_lpg_with_tx(Arc::clone(&self.store), Arc::clone(&self.tx_manager));
330
331 let processor = if let Some(tx_id) = tx_id {
333 processor.with_tx_context(viewing_epoch, tx_id)
334 } else {
335 processor
336 };
337
338 processor.process(query, QueryLanguage::Gremlin, Some(¶ms))
339 }
340
341 #[cfg(feature = "graphql")]
362 pub fn execute_graphql(&self, query: &str) -> Result<QueryResult> {
363 use crate::query::{
364 Executor, Planner, binder::Binder, graphql_translator, optimizer::Optimizer,
365 };
366
367 let logical_plan = graphql_translator::translate(query)?;
369
370 let mut binder = Binder::new();
372 let _binding_context = binder.bind(&logical_plan)?;
373
374 let optimizer = Optimizer::new();
376 let optimized_plan = optimizer.optimize(logical_plan)?;
377
378 let (viewing_epoch, tx_id) = self.get_transaction_context();
380
381 let planner = Planner::with_context(
383 Arc::clone(&self.store),
384 Arc::clone(&self.tx_manager),
385 tx_id,
386 viewing_epoch,
387 )
388 .with_factorized_execution(self.factorized_execution);
389 let mut physical_plan = planner.plan(&optimized_plan)?;
390
391 let executor = Executor::with_columns(physical_plan.columns.clone());
393 executor.execute(physical_plan.operator.as_mut())
394 }
395
396 #[cfg(feature = "graphql")]
402 pub fn execute_graphql_with_params(
403 &self,
404 query: &str,
405 params: std::collections::HashMap<String, Value>,
406 ) -> Result<QueryResult> {
407 use crate::query::processor::{QueryLanguage, QueryProcessor};
408
409 let (viewing_epoch, tx_id) = self.get_transaction_context();
411
412 let processor =
414 QueryProcessor::for_lpg_with_tx(Arc::clone(&self.store), Arc::clone(&self.tx_manager));
415
416 let processor = if let Some(tx_id) = tx_id {
418 processor.with_tx_context(viewing_epoch, tx_id)
419 } else {
420 processor
421 };
422
423 processor.process(query, QueryLanguage::GraphQL, Some(¶ms))
424 }
425
426 #[cfg(all(feature = "sparql", feature = "rdf"))]
432 pub fn execute_sparql(&self, query: &str) -> Result<QueryResult> {
433 use crate::query::{
434 Executor, optimizer::Optimizer, planner_rdf::RdfPlanner, sparql_translator,
435 };
436
437 let logical_plan = sparql_translator::translate(query)?;
439
440 let optimizer = Optimizer::new();
442 let optimized_plan = optimizer.optimize(logical_plan)?;
443
444 let planner = RdfPlanner::new(Arc::clone(&self.rdf_store)).with_tx_id(self.current_tx);
446 let mut physical_plan = planner.plan(&optimized_plan)?;
447
448 let executor = Executor::with_columns(physical_plan.columns.clone());
450 executor.execute(physical_plan.operator.as_mut())
451 }
452
453 #[cfg(all(feature = "sparql", feature = "rdf"))]
459 pub fn execute_sparql_with_params(
460 &self,
461 query: &str,
462 _params: std::collections::HashMap<String, Value>,
463 ) -> Result<QueryResult> {
464 self.execute_sparql(query)
467 }
468
469 pub fn begin_tx(&mut self) -> Result<()> {
489 if self.current_tx.is_some() {
490 return Err(grafeo_common::utils::error::Error::Transaction(
491 grafeo_common::utils::error::TransactionError::InvalidState(
492 "Transaction already active".to_string(),
493 ),
494 ));
495 }
496
497 let tx_id = self.tx_manager.begin();
498 self.current_tx = Some(tx_id);
499 Ok(())
500 }
501
502 pub fn commit(&mut self) -> Result<()> {
510 let tx_id = self.current_tx.take().ok_or_else(|| {
511 grafeo_common::utils::error::Error::Transaction(
512 grafeo_common::utils::error::TransactionError::InvalidState(
513 "No active transaction".to_string(),
514 ),
515 )
516 })?;
517
518 #[cfg(feature = "rdf")]
520 self.rdf_store.commit_tx(tx_id);
521
522 self.tx_manager.commit(tx_id).map(|_| ())
523 }
524
525 pub fn rollback(&mut self) -> Result<()> {
546 let tx_id = self.current_tx.take().ok_or_else(|| {
547 grafeo_common::utils::error::Error::Transaction(
548 grafeo_common::utils::error::TransactionError::InvalidState(
549 "No active transaction".to_string(),
550 ),
551 )
552 })?;
553
554 self.store.discard_uncommitted_versions(tx_id);
556
557 #[cfg(feature = "rdf")]
559 self.rdf_store.rollback_tx(tx_id);
560
561 self.tx_manager.abort(tx_id)
563 }
564
565 #[must_use]
567 pub fn in_transaction(&self) -> bool {
568 self.current_tx.is_some()
569 }
570
571 pub fn set_auto_commit(&mut self, auto_commit: bool) {
573 self.auto_commit = auto_commit;
574 }
575
576 #[must_use]
578 pub fn auto_commit(&self) -> bool {
579 self.auto_commit
580 }
581
582 #[must_use]
588 fn get_transaction_context(&self) -> (EpochId, Option<TxId>) {
589 if let Some(tx_id) = self.current_tx {
590 let epoch = self
592 .tx_manager
593 .start_epoch(tx_id)
594 .unwrap_or_else(|| self.tx_manager.current_epoch());
595 (epoch, Some(tx_id))
596 } else {
597 (self.tx_manager.current_epoch(), None)
599 }
600 }
601
602 pub fn create_node(&self, labels: &[&str]) -> NodeId {
607 let (epoch, tx_id) = self.get_transaction_context();
608 self.store
609 .create_node_versioned(labels, epoch, tx_id.unwrap_or(TxId::SYSTEM))
610 }
611
612 pub fn create_node_with_props<'a>(
616 &self,
617 labels: &[&str],
618 properties: impl IntoIterator<Item = (&'a str, Value)>,
619 ) -> NodeId {
620 let (epoch, tx_id) = self.get_transaction_context();
621 self.store.create_node_with_props_versioned(
622 labels,
623 properties.into_iter().map(|(k, v)| (k, v)),
624 epoch,
625 tx_id.unwrap_or(TxId::SYSTEM),
626 )
627 }
628
629 pub fn create_edge(
634 &self,
635 src: NodeId,
636 dst: NodeId,
637 edge_type: &str,
638 ) -> grafeo_common::types::EdgeId {
639 let (epoch, tx_id) = self.get_transaction_context();
640 self.store
641 .create_edge_versioned(src, dst, edge_type, epoch, tx_id.unwrap_or(TxId::SYSTEM))
642 }
643}
644
645#[cfg(test)]
646mod tests {
647 use crate::database::GrafeoDB;
648
649 #[test]
650 fn test_session_create_node() {
651 let db = GrafeoDB::new_in_memory();
652 let session = db.session();
653
654 let id = session.create_node(&["Person"]);
655 assert!(id.is_valid());
656 assert_eq!(db.node_count(), 1);
657 }
658
659 #[test]
660 fn test_session_transaction() {
661 let db = GrafeoDB::new_in_memory();
662 let mut session = db.session();
663
664 assert!(!session.in_transaction());
665
666 session.begin_tx().unwrap();
667 assert!(session.in_transaction());
668
669 session.commit().unwrap();
670 assert!(!session.in_transaction());
671 }
672
673 #[test]
674 fn test_session_transaction_context() {
675 let db = GrafeoDB::new_in_memory();
676 let mut session = db.session();
677
678 let (_epoch1, tx_id1) = session.get_transaction_context();
680 assert!(tx_id1.is_none());
681
682 session.begin_tx().unwrap();
684 let (epoch2, tx_id2) = session.get_transaction_context();
685 assert!(tx_id2.is_some());
686 let _ = epoch2; session.commit().unwrap();
691 let (epoch3, tx_id3) = session.get_transaction_context();
692 assert!(tx_id3.is_none());
693 assert!(epoch3.as_u64() >= epoch2.as_u64());
695 }
696
697 #[test]
698 fn test_session_rollback() {
699 let db = GrafeoDB::new_in_memory();
700 let mut session = db.session();
701
702 session.begin_tx().unwrap();
703 session.rollback().unwrap();
704 assert!(!session.in_transaction());
705 }
706
707 #[test]
708 fn test_session_rollback_discards_versions() {
709 use grafeo_common::types::TxId;
710
711 let db = GrafeoDB::new_in_memory();
712
713 let node_before = db.store().create_node(&["Person"]);
715 assert!(node_before.is_valid());
716 assert_eq!(db.node_count(), 1, "Should have 1 node before transaction");
717
718 let mut session = db.session();
720 session.begin_tx().unwrap();
721 let tx_id = session.current_tx.unwrap();
722
723 let epoch = db.store().current_epoch();
725 let node_in_tx = db.store().create_node_versioned(&["Person"], epoch, tx_id);
726 assert!(node_in_tx.is_valid());
727
728 assert_eq!(db.node_count(), 2, "Should have 2 nodes during transaction");
730
731 session.rollback().unwrap();
733 assert!(!session.in_transaction());
734
735 let count_after = db.node_count();
738 assert_eq!(
739 count_after, 1,
740 "Rollback should discard uncommitted node, but got {count_after}"
741 );
742
743 let current_epoch = db.store().current_epoch();
745 assert!(
746 db.store()
747 .get_node_versioned(node_before, current_epoch, TxId::SYSTEM)
748 .is_some(),
749 "Original node should still exist"
750 );
751
752 assert!(
754 db.store()
755 .get_node_versioned(node_in_tx, current_epoch, TxId::SYSTEM)
756 .is_none(),
757 "Transaction node should be gone"
758 );
759 }
760
761 #[test]
762 fn test_session_create_node_in_transaction() {
763 let db = GrafeoDB::new_in_memory();
765
766 let node_before = db.create_node(&["Person"]);
768 assert!(node_before.is_valid());
769 assert_eq!(db.node_count(), 1, "Should have 1 node before transaction");
770
771 let mut session = db.session();
773 session.begin_tx().unwrap();
774
775 let node_in_tx = session.create_node(&["Person"]);
777 assert!(node_in_tx.is_valid());
778
779 assert_eq!(db.node_count(), 2, "Should have 2 nodes during transaction");
781
782 session.rollback().unwrap();
784
785 let count_after = db.node_count();
787 assert_eq!(
788 count_after, 1,
789 "Rollback should discard node created via session.create_node(), but got {count_after}"
790 );
791 }
792
793 #[test]
794 fn test_session_create_node_with_props_in_transaction() {
795 use grafeo_common::types::Value;
796
797 let db = GrafeoDB::new_in_memory();
799
800 db.create_node(&["Person"]);
802 assert_eq!(db.node_count(), 1, "Should have 1 node before transaction");
803
804 let mut session = db.session();
806 session.begin_tx().unwrap();
807
808 let node_in_tx =
809 session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
810 assert!(node_in_tx.is_valid());
811
812 assert_eq!(db.node_count(), 2, "Should have 2 nodes during transaction");
814
815 session.rollback().unwrap();
817
818 let count_after = db.node_count();
820 assert_eq!(
821 count_after, 1,
822 "Rollback should discard node created via session.create_node_with_props()"
823 );
824 }
825
826 #[cfg(feature = "gql")]
827 mod gql_tests {
828 use super::*;
829
830 #[test]
831 fn test_gql_query_execution() {
832 let db = GrafeoDB::new_in_memory();
833 let session = db.session();
834
835 session.create_node(&["Person"]);
837 session.create_node(&["Person"]);
838 session.create_node(&["Animal"]);
839
840 let result = session.execute("MATCH (n:Person) RETURN n").unwrap();
842
843 assert_eq!(result.row_count(), 2);
845 assert_eq!(result.column_count(), 1);
846 assert_eq!(result.columns[0], "n");
847 }
848
849 #[test]
850 fn test_gql_empty_result() {
851 let db = GrafeoDB::new_in_memory();
852 let session = db.session();
853
854 let result = session.execute("MATCH (n:Person) RETURN n").unwrap();
856
857 assert_eq!(result.row_count(), 0);
858 }
859
860 #[test]
861 fn test_gql_parse_error() {
862 let db = GrafeoDB::new_in_memory();
863 let session = db.session();
864
865 let result = session.execute("MATCH (n RETURN n");
867
868 assert!(result.is_err());
869 }
870
871 #[test]
872 fn test_gql_relationship_traversal() {
873 let db = GrafeoDB::new_in_memory();
874 let session = db.session();
875
876 let alice = session.create_node(&["Person"]);
878 let bob = session.create_node(&["Person"]);
879 let charlie = session.create_node(&["Person"]);
880
881 session.create_edge(alice, bob, "KNOWS");
882 session.create_edge(alice, charlie, "KNOWS");
883
884 let result = session
886 .execute("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b")
887 .unwrap();
888
889 assert_eq!(result.row_count(), 2);
891 assert_eq!(result.column_count(), 2);
892 assert_eq!(result.columns[0], "a");
893 assert_eq!(result.columns[1], "b");
894 }
895
896 #[test]
897 fn test_gql_relationship_with_type_filter() {
898 let db = GrafeoDB::new_in_memory();
899 let session = db.session();
900
901 let alice = session.create_node(&["Person"]);
903 let bob = session.create_node(&["Person"]);
904 let charlie = session.create_node(&["Person"]);
905
906 session.create_edge(alice, bob, "KNOWS");
907 session.create_edge(alice, charlie, "WORKS_WITH");
908
909 let result = session
911 .execute("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b")
912 .unwrap();
913
914 assert_eq!(result.row_count(), 1);
916 }
917
918 #[test]
919 fn test_gql_semantic_error_undefined_variable() {
920 let db = GrafeoDB::new_in_memory();
921 let session = db.session();
922
923 let result = session.execute("MATCH (n:Person) RETURN x");
925
926 assert!(result.is_err());
928 let err = match result {
929 Err(e) => e,
930 Ok(_) => panic!("Expected error"),
931 };
932 assert!(
933 err.to_string().contains("Undefined variable"),
934 "Expected undefined variable error, got: {}",
935 err
936 );
937 }
938
939 #[test]
940 fn test_gql_where_clause_property_filter() {
941 use grafeo_common::types::Value;
942
943 let db = GrafeoDB::new_in_memory();
944 let session = db.session();
945
946 session.create_node_with_props(&["Person"], [("age", Value::Int64(25))]);
948 session.create_node_with_props(&["Person"], [("age", Value::Int64(35))]);
949 session.create_node_with_props(&["Person"], [("age", Value::Int64(45))]);
950
951 let result = session
953 .execute("MATCH (n:Person) WHERE n.age > 30 RETURN n")
954 .unwrap();
955
956 assert_eq!(result.row_count(), 2);
958 }
959
960 #[test]
961 fn test_gql_where_clause_equality() {
962 use grafeo_common::types::Value;
963
964 let db = GrafeoDB::new_in_memory();
965 let session = db.session();
966
967 session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
969 session.create_node_with_props(&["Person"], [("name", Value::String("Bob".into()))]);
970 session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
971
972 let result = session
974 .execute("MATCH (n:Person) WHERE n.name = \"Alice\" RETURN n")
975 .unwrap();
976
977 assert_eq!(result.row_count(), 2);
979 }
980
981 #[test]
982 fn test_gql_return_property_access() {
983 use grafeo_common::types::Value;
984
985 let db = GrafeoDB::new_in_memory();
986 let session = db.session();
987
988 session.create_node_with_props(
990 &["Person"],
991 [
992 ("name", Value::String("Alice".into())),
993 ("age", Value::Int64(30)),
994 ],
995 );
996 session.create_node_with_props(
997 &["Person"],
998 [
999 ("name", Value::String("Bob".into())),
1000 ("age", Value::Int64(25)),
1001 ],
1002 );
1003
1004 let result = session
1006 .execute("MATCH (n:Person) RETURN n.name, n.age")
1007 .unwrap();
1008
1009 assert_eq!(result.row_count(), 2);
1011 assert_eq!(result.column_count(), 2);
1012 assert_eq!(result.columns[0], "n.name");
1013 assert_eq!(result.columns[1], "n.age");
1014
1015 let names: Vec<&Value> = result.rows.iter().map(|r| &r[0]).collect();
1017 assert!(names.contains(&&Value::String("Alice".into())));
1018 assert!(names.contains(&&Value::String("Bob".into())));
1019 }
1020
1021 #[test]
1022 fn test_gql_return_mixed_expressions() {
1023 use grafeo_common::types::Value;
1024
1025 let db = GrafeoDB::new_in_memory();
1026 let session = db.session();
1027
1028 session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
1030
1031 let result = session
1033 .execute("MATCH (n:Person) RETURN n, n.name")
1034 .unwrap();
1035
1036 assert_eq!(result.row_count(), 1);
1037 assert_eq!(result.column_count(), 2);
1038 assert_eq!(result.columns[0], "n");
1039 assert_eq!(result.columns[1], "n.name");
1040
1041 assert_eq!(result.rows[0][1], Value::String("Alice".into()));
1043 }
1044 }
1045
1046 #[cfg(feature = "cypher")]
1047 mod cypher_tests {
1048 use super::*;
1049
1050 #[test]
1051 fn test_cypher_query_execution() {
1052 let db = GrafeoDB::new_in_memory();
1053 let session = db.session();
1054
1055 session.create_node(&["Person"]);
1057 session.create_node(&["Person"]);
1058 session.create_node(&["Animal"]);
1059
1060 let result = session.execute_cypher("MATCH (n:Person) RETURN n").unwrap();
1062
1063 assert_eq!(result.row_count(), 2);
1065 assert_eq!(result.column_count(), 1);
1066 assert_eq!(result.columns[0], "n");
1067 }
1068
1069 #[test]
1070 fn test_cypher_empty_result() {
1071 let db = GrafeoDB::new_in_memory();
1072 let session = db.session();
1073
1074 let result = session.execute_cypher("MATCH (n:Person) RETURN n").unwrap();
1076
1077 assert_eq!(result.row_count(), 0);
1078 }
1079
1080 #[test]
1081 fn test_cypher_parse_error() {
1082 let db = GrafeoDB::new_in_memory();
1083 let session = db.session();
1084
1085 let result = session.execute_cypher("MATCH (n RETURN n");
1087
1088 assert!(result.is_err());
1089 }
1090 }
1091}