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}
41
42impl Session {
43 #[allow(dead_code)]
45 pub(crate) fn new(store: Arc<LpgStore>, tx_manager: Arc<TransactionManager>) -> Self {
46 Self {
47 store,
48 #[cfg(feature = "rdf")]
49 rdf_store: Arc::new(RdfStore::new()),
50 tx_manager,
51 current_tx: None,
52 auto_commit: true,
53 adaptive_config: AdaptiveConfig::default(),
54 }
55 }
56
57 #[allow(dead_code)]
59 pub(crate) fn with_adaptive(
60 store: Arc<LpgStore>,
61 tx_manager: Arc<TransactionManager>,
62 adaptive_config: AdaptiveConfig,
63 ) -> Self {
64 Self {
65 store,
66 #[cfg(feature = "rdf")]
67 rdf_store: Arc::new(RdfStore::new()),
68 tx_manager,
69 current_tx: None,
70 auto_commit: true,
71 adaptive_config,
72 }
73 }
74
75 #[cfg(feature = "rdf")]
77 pub(crate) fn with_rdf_store_and_adaptive(
78 store: Arc<LpgStore>,
79 rdf_store: Arc<RdfStore>,
80 tx_manager: Arc<TransactionManager>,
81 adaptive_config: AdaptiveConfig,
82 ) -> Self {
83 Self {
84 store,
85 rdf_store,
86 tx_manager,
87 current_tx: None,
88 auto_commit: true,
89 adaptive_config,
90 }
91 }
92
93 #[cfg(feature = "gql")]
117 pub fn execute(&self, query: &str) -> Result<QueryResult> {
118 use crate::query::{
119 Executor, Planner, binder::Binder, gql_translator, optimizer::Optimizer,
120 };
121
122 let logical_plan = gql_translator::translate(query)?;
124
125 let mut binder = Binder::new();
127 let _binding_context = binder.bind(&logical_plan)?;
128
129 let optimizer = Optimizer::new();
131 let optimized_plan = optimizer.optimize(logical_plan)?;
132
133 let (viewing_epoch, tx_id) = self.get_transaction_context();
135
136 let planner = Planner::with_context(
138 Arc::clone(&self.store),
139 Arc::clone(&self.tx_manager),
140 tx_id,
141 viewing_epoch,
142 );
143 let mut physical_plan = planner.plan(&optimized_plan)?;
144
145 let executor = Executor::with_columns(physical_plan.columns.clone());
147 executor.execute(physical_plan.operator.as_mut())
148 }
149
150 #[cfg(feature = "gql")]
156 pub fn execute_with_params(
157 &self,
158 query: &str,
159 params: std::collections::HashMap<String, Value>,
160 ) -> Result<QueryResult> {
161 use crate::query::processor::{QueryLanguage, QueryProcessor};
162
163 let (viewing_epoch, tx_id) = self.get_transaction_context();
165
166 let processor =
168 QueryProcessor::for_lpg_with_tx(Arc::clone(&self.store), Arc::clone(&self.tx_manager));
169
170 let processor = if let Some(tx_id) = tx_id {
172 processor.with_tx_context(viewing_epoch, tx_id)
173 } else {
174 processor
175 };
176
177 processor.process(query, QueryLanguage::Gql, Some(¶ms))
178 }
179
180 #[cfg(not(any(feature = "gql", feature = "cypher")))]
186 pub fn execute_with_params(
187 &self,
188 _query: &str,
189 _params: std::collections::HashMap<String, Value>,
190 ) -> Result<QueryResult> {
191 Err(grafeo_common::utils::error::Error::Internal(
192 "No query language enabled".to_string(),
193 ))
194 }
195
196 #[cfg(not(any(feature = "gql", feature = "cypher")))]
202 pub fn execute(&self, _query: &str) -> Result<QueryResult> {
203 Err(grafeo_common::utils::error::Error::Internal(
204 "No query language enabled".to_string(),
205 ))
206 }
207
208 #[cfg(feature = "cypher")]
214 pub fn execute_cypher(&self, query: &str) -> Result<QueryResult> {
215 use crate::query::{
216 Executor, Planner, binder::Binder, cypher_translator, optimizer::Optimizer,
217 };
218
219 let logical_plan = cypher_translator::translate(query)?;
221
222 let mut binder = Binder::new();
224 let _binding_context = binder.bind(&logical_plan)?;
225
226 let optimizer = Optimizer::new();
228 let optimized_plan = optimizer.optimize(logical_plan)?;
229
230 let (viewing_epoch, tx_id) = self.get_transaction_context();
232
233 let planner = Planner::with_context(
235 Arc::clone(&self.store),
236 Arc::clone(&self.tx_manager),
237 tx_id,
238 viewing_epoch,
239 );
240 let mut physical_plan = planner.plan(&optimized_plan)?;
241
242 let executor = Executor::with_columns(physical_plan.columns.clone());
244 executor.execute(physical_plan.operator.as_mut())
245 }
246
247 #[cfg(feature = "gremlin")]
268 pub fn execute_gremlin(&self, query: &str) -> Result<QueryResult> {
269 use crate::query::{
270 Executor, Planner, binder::Binder, gremlin_translator, optimizer::Optimizer,
271 };
272
273 let logical_plan = gremlin_translator::translate(query)?;
275
276 let mut binder = Binder::new();
278 let _binding_context = binder.bind(&logical_plan)?;
279
280 let optimizer = Optimizer::new();
282 let optimized_plan = optimizer.optimize(logical_plan)?;
283
284 let (viewing_epoch, tx_id) = self.get_transaction_context();
286
287 let planner = Planner::with_context(
289 Arc::clone(&self.store),
290 Arc::clone(&self.tx_manager),
291 tx_id,
292 viewing_epoch,
293 );
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")]
307 pub fn execute_gremlin_with_params(
308 &self,
309 query: &str,
310 params: std::collections::HashMap<String, Value>,
311 ) -> Result<QueryResult> {
312 use crate::query::processor::{QueryLanguage, QueryProcessor};
313
314 let (viewing_epoch, tx_id) = self.get_transaction_context();
316
317 let processor =
319 QueryProcessor::for_lpg_with_tx(Arc::clone(&self.store), Arc::clone(&self.tx_manager));
320
321 let processor = if let Some(tx_id) = tx_id {
323 processor.with_tx_context(viewing_epoch, tx_id)
324 } else {
325 processor
326 };
327
328 processor.process(query, QueryLanguage::Gremlin, Some(¶ms))
329 }
330
331 #[cfg(feature = "graphql")]
352 pub fn execute_graphql(&self, query: &str) -> Result<QueryResult> {
353 use crate::query::{
354 Executor, Planner, binder::Binder, graphql_translator, optimizer::Optimizer,
355 };
356
357 let logical_plan = graphql_translator::translate(query)?;
359
360 let mut binder = Binder::new();
362 let _binding_context = binder.bind(&logical_plan)?;
363
364 let optimizer = Optimizer::new();
366 let optimized_plan = optimizer.optimize(logical_plan)?;
367
368 let (viewing_epoch, tx_id) = self.get_transaction_context();
370
371 let planner = Planner::with_context(
373 Arc::clone(&self.store),
374 Arc::clone(&self.tx_manager),
375 tx_id,
376 viewing_epoch,
377 );
378 let mut physical_plan = planner.plan(&optimized_plan)?;
379
380 let executor = Executor::with_columns(physical_plan.columns.clone());
382 executor.execute(physical_plan.operator.as_mut())
383 }
384
385 #[cfg(feature = "graphql")]
391 pub fn execute_graphql_with_params(
392 &self,
393 query: &str,
394 params: std::collections::HashMap<String, Value>,
395 ) -> Result<QueryResult> {
396 use crate::query::processor::{QueryLanguage, QueryProcessor};
397
398 let (viewing_epoch, tx_id) = self.get_transaction_context();
400
401 let processor =
403 QueryProcessor::for_lpg_with_tx(Arc::clone(&self.store), Arc::clone(&self.tx_manager));
404
405 let processor = if let Some(tx_id) = tx_id {
407 processor.with_tx_context(viewing_epoch, tx_id)
408 } else {
409 processor
410 };
411
412 processor.process(query, QueryLanguage::GraphQL, Some(¶ms))
413 }
414
415 #[cfg(all(feature = "sparql", feature = "rdf"))]
421 pub fn execute_sparql(&self, query: &str) -> Result<QueryResult> {
422 use crate::query::{
423 Executor, optimizer::Optimizer, planner_rdf::RdfPlanner, sparql_translator,
424 };
425
426 let logical_plan = sparql_translator::translate(query)?;
428
429 let optimizer = Optimizer::new();
431 let optimized_plan = optimizer.optimize(logical_plan)?;
432
433 let planner = RdfPlanner::new(Arc::clone(&self.rdf_store)).with_tx_id(self.current_tx);
435 let mut physical_plan = planner.plan(&optimized_plan)?;
436
437 let executor = Executor::with_columns(physical_plan.columns.clone());
439 executor.execute(physical_plan.operator.as_mut())
440 }
441
442 #[cfg(all(feature = "sparql", feature = "rdf"))]
448 pub fn execute_sparql_with_params(
449 &self,
450 query: &str,
451 _params: std::collections::HashMap<String, Value>,
452 ) -> Result<QueryResult> {
453 self.execute_sparql(query)
456 }
457
458 pub fn begin_tx(&mut self) -> Result<()> {
478 if self.current_tx.is_some() {
479 return Err(grafeo_common::utils::error::Error::Transaction(
480 grafeo_common::utils::error::TransactionError::InvalidState(
481 "Transaction already active".to_string(),
482 ),
483 ));
484 }
485
486 let tx_id = self.tx_manager.begin();
487 self.current_tx = Some(tx_id);
488 Ok(())
489 }
490
491 pub fn commit(&mut self) -> Result<()> {
499 let tx_id = self.current_tx.take().ok_or_else(|| {
500 grafeo_common::utils::error::Error::Transaction(
501 grafeo_common::utils::error::TransactionError::InvalidState(
502 "No active transaction".to_string(),
503 ),
504 )
505 })?;
506
507 #[cfg(feature = "rdf")]
509 self.rdf_store.commit_tx(tx_id);
510
511 self.tx_manager.commit(tx_id).map(|_| ())
512 }
513
514 pub fn rollback(&mut self) -> Result<()> {
535 let tx_id = self.current_tx.take().ok_or_else(|| {
536 grafeo_common::utils::error::Error::Transaction(
537 grafeo_common::utils::error::TransactionError::InvalidState(
538 "No active transaction".to_string(),
539 ),
540 )
541 })?;
542
543 self.store.discard_uncommitted_versions(tx_id);
545
546 #[cfg(feature = "rdf")]
548 self.rdf_store.rollback_tx(tx_id);
549
550 self.tx_manager.abort(tx_id)
552 }
553
554 #[must_use]
556 pub fn in_transaction(&self) -> bool {
557 self.current_tx.is_some()
558 }
559
560 pub fn set_auto_commit(&mut self, auto_commit: bool) {
562 self.auto_commit = auto_commit;
563 }
564
565 #[must_use]
567 pub fn auto_commit(&self) -> bool {
568 self.auto_commit
569 }
570
571 #[must_use]
577 fn get_transaction_context(&self) -> (EpochId, Option<TxId>) {
578 if let Some(tx_id) = self.current_tx {
579 let epoch = self
581 .tx_manager
582 .start_epoch(tx_id)
583 .unwrap_or_else(|| self.tx_manager.current_epoch());
584 (epoch, Some(tx_id))
585 } else {
586 (self.tx_manager.current_epoch(), None)
588 }
589 }
590
591 pub fn create_node(&self, labels: &[&str]) -> NodeId {
596 let (epoch, tx_id) = self.get_transaction_context();
597 self.store
598 .create_node_versioned(labels, epoch, tx_id.unwrap_or(TxId::SYSTEM))
599 }
600
601 pub fn create_node_with_props<'a>(
605 &self,
606 labels: &[&str],
607 properties: impl IntoIterator<Item = (&'a str, Value)>,
608 ) -> NodeId {
609 let (epoch, tx_id) = self.get_transaction_context();
610 self.store.create_node_with_props_versioned(
611 labels,
612 properties.into_iter().map(|(k, v)| (k, v)),
613 epoch,
614 tx_id.unwrap_or(TxId::SYSTEM),
615 )
616 }
617
618 pub fn create_edge(
623 &self,
624 src: NodeId,
625 dst: NodeId,
626 edge_type: &str,
627 ) -> grafeo_common::types::EdgeId {
628 let (epoch, tx_id) = self.get_transaction_context();
629 self.store
630 .create_edge_versioned(src, dst, edge_type, epoch, tx_id.unwrap_or(TxId::SYSTEM))
631 }
632}
633
634#[cfg(test)]
635mod tests {
636 use crate::database::GrafeoDB;
637
638 #[test]
639 fn test_session_create_node() {
640 let db = GrafeoDB::new_in_memory();
641 let session = db.session();
642
643 let id = session.create_node(&["Person"]);
644 assert!(id.is_valid());
645 assert_eq!(db.node_count(), 1);
646 }
647
648 #[test]
649 fn test_session_transaction() {
650 let db = GrafeoDB::new_in_memory();
651 let mut session = db.session();
652
653 assert!(!session.in_transaction());
654
655 session.begin_tx().unwrap();
656 assert!(session.in_transaction());
657
658 session.commit().unwrap();
659 assert!(!session.in_transaction());
660 }
661
662 #[test]
663 fn test_session_transaction_context() {
664 let db = GrafeoDB::new_in_memory();
665 let mut session = db.session();
666
667 let (_epoch1, tx_id1) = session.get_transaction_context();
669 assert!(tx_id1.is_none());
670
671 session.begin_tx().unwrap();
673 let (epoch2, tx_id2) = session.get_transaction_context();
674 assert!(tx_id2.is_some());
675 let _ = epoch2; session.commit().unwrap();
680 let (epoch3, tx_id3) = session.get_transaction_context();
681 assert!(tx_id3.is_none());
682 assert!(epoch3.as_u64() >= epoch2.as_u64());
684 }
685
686 #[test]
687 fn test_session_rollback() {
688 let db = GrafeoDB::new_in_memory();
689 let mut session = db.session();
690
691 session.begin_tx().unwrap();
692 session.rollback().unwrap();
693 assert!(!session.in_transaction());
694 }
695
696 #[test]
697 fn test_session_rollback_discards_versions() {
698 use grafeo_common::types::TxId;
699
700 let db = GrafeoDB::new_in_memory();
701
702 let node_before = db.store().create_node(&["Person"]);
704 assert!(node_before.is_valid());
705 assert_eq!(db.node_count(), 1, "Should have 1 node before transaction");
706
707 let mut session = db.session();
709 session.begin_tx().unwrap();
710 let tx_id = session.current_tx.unwrap();
711
712 let epoch = db.store().current_epoch();
714 let node_in_tx = db.store().create_node_versioned(&["Person"], epoch, tx_id);
715 assert!(node_in_tx.is_valid());
716
717 assert_eq!(db.node_count(), 2, "Should have 2 nodes during transaction");
719
720 session.rollback().unwrap();
722 assert!(!session.in_transaction());
723
724 let count_after = db.node_count();
727 assert_eq!(
728 count_after, 1,
729 "Rollback should discard uncommitted node, but got {count_after}"
730 );
731
732 let current_epoch = db.store().current_epoch();
734 assert!(
735 db.store()
736 .get_node_versioned(node_before, current_epoch, TxId::SYSTEM)
737 .is_some(),
738 "Original node should still exist"
739 );
740
741 assert!(
743 db.store()
744 .get_node_versioned(node_in_tx, current_epoch, TxId::SYSTEM)
745 .is_none(),
746 "Transaction node should be gone"
747 );
748 }
749
750 #[test]
751 fn test_session_create_node_in_transaction() {
752 let db = GrafeoDB::new_in_memory();
754
755 let node_before = db.create_node(&["Person"]);
757 assert!(node_before.is_valid());
758 assert_eq!(db.node_count(), 1, "Should have 1 node before transaction");
759
760 let mut session = db.session();
762 session.begin_tx().unwrap();
763
764 let node_in_tx = session.create_node(&["Person"]);
766 assert!(node_in_tx.is_valid());
767
768 assert_eq!(db.node_count(), 2, "Should have 2 nodes during transaction");
770
771 session.rollback().unwrap();
773
774 let count_after = db.node_count();
776 assert_eq!(
777 count_after, 1,
778 "Rollback should discard node created via session.create_node(), but got {count_after}"
779 );
780 }
781
782 #[test]
783 fn test_session_create_node_with_props_in_transaction() {
784 use grafeo_common::types::Value;
785
786 let db = GrafeoDB::new_in_memory();
788
789 db.create_node(&["Person"]);
791 assert_eq!(db.node_count(), 1, "Should have 1 node before transaction");
792
793 let mut session = db.session();
795 session.begin_tx().unwrap();
796
797 let node_in_tx =
798 session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
799 assert!(node_in_tx.is_valid());
800
801 assert_eq!(db.node_count(), 2, "Should have 2 nodes during transaction");
803
804 session.rollback().unwrap();
806
807 let count_after = db.node_count();
809 assert_eq!(
810 count_after, 1,
811 "Rollback should discard node created via session.create_node_with_props()"
812 );
813 }
814
815 #[cfg(feature = "gql")]
816 mod gql_tests {
817 use super::*;
818
819 #[test]
820 fn test_gql_query_execution() {
821 let db = GrafeoDB::new_in_memory();
822 let session = db.session();
823
824 session.create_node(&["Person"]);
826 session.create_node(&["Person"]);
827 session.create_node(&["Animal"]);
828
829 let result = session.execute("MATCH (n:Person) RETURN n").unwrap();
831
832 assert_eq!(result.row_count(), 2);
834 assert_eq!(result.column_count(), 1);
835 assert_eq!(result.columns[0], "n");
836 }
837
838 #[test]
839 fn test_gql_empty_result() {
840 let db = GrafeoDB::new_in_memory();
841 let session = db.session();
842
843 let result = session.execute("MATCH (n:Person) RETURN n").unwrap();
845
846 assert_eq!(result.row_count(), 0);
847 }
848
849 #[test]
850 fn test_gql_parse_error() {
851 let db = GrafeoDB::new_in_memory();
852 let session = db.session();
853
854 let result = session.execute("MATCH (n RETURN n");
856
857 assert!(result.is_err());
858 }
859
860 #[test]
861 fn test_gql_relationship_traversal() {
862 let db = GrafeoDB::new_in_memory();
863 let session = db.session();
864
865 let alice = session.create_node(&["Person"]);
867 let bob = session.create_node(&["Person"]);
868 let charlie = session.create_node(&["Person"]);
869
870 session.create_edge(alice, bob, "KNOWS");
871 session.create_edge(alice, charlie, "KNOWS");
872
873 let result = session
875 .execute("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b")
876 .unwrap();
877
878 assert_eq!(result.row_count(), 2);
880 assert_eq!(result.column_count(), 2);
881 assert_eq!(result.columns[0], "a");
882 assert_eq!(result.columns[1], "b");
883 }
884
885 #[test]
886 fn test_gql_relationship_with_type_filter() {
887 let db = GrafeoDB::new_in_memory();
888 let session = db.session();
889
890 let alice = session.create_node(&["Person"]);
892 let bob = session.create_node(&["Person"]);
893 let charlie = session.create_node(&["Person"]);
894
895 session.create_edge(alice, bob, "KNOWS");
896 session.create_edge(alice, charlie, "WORKS_WITH");
897
898 let result = session
900 .execute("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b")
901 .unwrap();
902
903 assert_eq!(result.row_count(), 1);
905 }
906
907 #[test]
908 fn test_gql_semantic_error_undefined_variable() {
909 let db = GrafeoDB::new_in_memory();
910 let session = db.session();
911
912 let result = session.execute("MATCH (n:Person) RETURN x");
914
915 assert!(result.is_err());
917 let err = match result {
918 Err(e) => e,
919 Ok(_) => panic!("Expected error"),
920 };
921 assert!(
922 err.to_string().contains("Undefined variable"),
923 "Expected undefined variable error, got: {}",
924 err
925 );
926 }
927
928 #[test]
929 fn test_gql_where_clause_property_filter() {
930 use grafeo_common::types::Value;
931
932 let db = GrafeoDB::new_in_memory();
933 let session = db.session();
934
935 session.create_node_with_props(&["Person"], [("age", Value::Int64(25))]);
937 session.create_node_with_props(&["Person"], [("age", Value::Int64(35))]);
938 session.create_node_with_props(&["Person"], [("age", Value::Int64(45))]);
939
940 let result = session
942 .execute("MATCH (n:Person) WHERE n.age > 30 RETURN n")
943 .unwrap();
944
945 assert_eq!(result.row_count(), 2);
947 }
948
949 #[test]
950 fn test_gql_where_clause_equality() {
951 use grafeo_common::types::Value;
952
953 let db = GrafeoDB::new_in_memory();
954 let session = db.session();
955
956 session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
958 session.create_node_with_props(&["Person"], [("name", Value::String("Bob".into()))]);
959 session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
960
961 let result = session
963 .execute("MATCH (n:Person) WHERE n.name = \"Alice\" RETURN n")
964 .unwrap();
965
966 assert_eq!(result.row_count(), 2);
968 }
969
970 #[test]
971 fn test_gql_return_property_access() {
972 use grafeo_common::types::Value;
973
974 let db = GrafeoDB::new_in_memory();
975 let session = db.session();
976
977 session.create_node_with_props(
979 &["Person"],
980 [
981 ("name", Value::String("Alice".into())),
982 ("age", Value::Int64(30)),
983 ],
984 );
985 session.create_node_with_props(
986 &["Person"],
987 [
988 ("name", Value::String("Bob".into())),
989 ("age", Value::Int64(25)),
990 ],
991 );
992
993 let result = session
995 .execute("MATCH (n:Person) RETURN n.name, n.age")
996 .unwrap();
997
998 assert_eq!(result.row_count(), 2);
1000 assert_eq!(result.column_count(), 2);
1001 assert_eq!(result.columns[0], "n.name");
1002 assert_eq!(result.columns[1], "n.age");
1003
1004 let names: Vec<&Value> = result.rows.iter().map(|r| &r[0]).collect();
1006 assert!(names.contains(&&Value::String("Alice".into())));
1007 assert!(names.contains(&&Value::String("Bob".into())));
1008 }
1009
1010 #[test]
1011 fn test_gql_return_mixed_expressions() {
1012 use grafeo_common::types::Value;
1013
1014 let db = GrafeoDB::new_in_memory();
1015 let session = db.session();
1016
1017 session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
1019
1020 let result = session
1022 .execute("MATCH (n:Person) RETURN n, n.name")
1023 .unwrap();
1024
1025 assert_eq!(result.row_count(), 1);
1026 assert_eq!(result.column_count(), 2);
1027 assert_eq!(result.columns[0], "n");
1028 assert_eq!(result.columns[1], "n.name");
1029
1030 assert_eq!(result.rows[0][1], Value::String("Alice".into()));
1032 }
1033 }
1034
1035 #[cfg(feature = "cypher")]
1036 mod cypher_tests {
1037 use super::*;
1038
1039 #[test]
1040 fn test_cypher_query_execution() {
1041 let db = GrafeoDB::new_in_memory();
1042 let session = db.session();
1043
1044 session.create_node(&["Person"]);
1046 session.create_node(&["Person"]);
1047 session.create_node(&["Animal"]);
1048
1049 let result = session.execute_cypher("MATCH (n:Person) RETURN n").unwrap();
1051
1052 assert_eq!(result.row_count(), 2);
1054 assert_eq!(result.column_count(), 1);
1055 assert_eq!(result.columns[0], "n");
1056 }
1057
1058 #[test]
1059 fn test_cypher_empty_result() {
1060 let db = GrafeoDB::new_in_memory();
1061 let session = db.session();
1062
1063 let result = session.execute_cypher("MATCH (n:Person) RETURN n").unwrap();
1065
1066 assert_eq!(result.row_count(), 0);
1067 }
1068
1069 #[test]
1070 fn test_cypher_parse_error() {
1071 let db = GrafeoDB::new_in_memory();
1072 let session = db.session();
1073
1074 let result = session.execute_cypher("MATCH (n RETURN n");
1076
1077 assert!(result.is_err());
1078 }
1079 }
1080}