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));
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 self.tx_manager.commit(tx_id).map(|_| ())
508 }
509
510 pub fn rollback(&mut self) -> Result<()> {
531 let tx_id = self.current_tx.take().ok_or_else(|| {
532 grafeo_common::utils::error::Error::Transaction(
533 grafeo_common::utils::error::TransactionError::InvalidState(
534 "No active transaction".to_string(),
535 ),
536 )
537 })?;
538
539 self.store.discard_uncommitted_versions(tx_id);
541
542 self.tx_manager.abort(tx_id)
544 }
545
546 #[must_use]
548 pub fn in_transaction(&self) -> bool {
549 self.current_tx.is_some()
550 }
551
552 pub fn set_auto_commit(&mut self, auto_commit: bool) {
554 self.auto_commit = auto_commit;
555 }
556
557 #[must_use]
559 pub fn auto_commit(&self) -> bool {
560 self.auto_commit
561 }
562
563 #[must_use]
569 fn get_transaction_context(&self) -> (EpochId, Option<TxId>) {
570 if let Some(tx_id) = self.current_tx {
571 let epoch = self
573 .tx_manager
574 .start_epoch(tx_id)
575 .unwrap_or_else(|| self.tx_manager.current_epoch());
576 (epoch, Some(tx_id))
577 } else {
578 (self.tx_manager.current_epoch(), None)
580 }
581 }
582
583 pub fn create_node(&self, labels: &[&str]) -> NodeId {
588 let (epoch, tx_id) = self.get_transaction_context();
589 self.store
590 .create_node_versioned(labels, epoch, tx_id.unwrap_or(TxId::SYSTEM))
591 }
592
593 pub fn create_node_with_props<'a>(
597 &self,
598 labels: &[&str],
599 properties: impl IntoIterator<Item = (&'a str, Value)>,
600 ) -> NodeId {
601 let (epoch, tx_id) = self.get_transaction_context();
602 self.store.create_node_with_props_versioned(
603 labels,
604 properties.into_iter().map(|(k, v)| (k, v)),
605 epoch,
606 tx_id.unwrap_or(TxId::SYSTEM),
607 )
608 }
609
610 pub fn create_edge(
615 &self,
616 src: NodeId,
617 dst: NodeId,
618 edge_type: &str,
619 ) -> grafeo_common::types::EdgeId {
620 let (epoch, tx_id) = self.get_transaction_context();
621 self.store
622 .create_edge_versioned(src, dst, edge_type, epoch, tx_id.unwrap_or(TxId::SYSTEM))
623 }
624}
625
626#[cfg(test)]
627mod tests {
628 use crate::database::GrafeoDB;
629
630 #[test]
631 fn test_session_create_node() {
632 let db = GrafeoDB::new_in_memory();
633 let session = db.session();
634
635 let id = session.create_node(&["Person"]);
636 assert!(id.is_valid());
637 assert_eq!(db.node_count(), 1);
638 }
639
640 #[test]
641 fn test_session_transaction() {
642 let db = GrafeoDB::new_in_memory();
643 let mut session = db.session();
644
645 assert!(!session.in_transaction());
646
647 session.begin_tx().unwrap();
648 assert!(session.in_transaction());
649
650 session.commit().unwrap();
651 assert!(!session.in_transaction());
652 }
653
654 #[test]
655 fn test_session_transaction_context() {
656 let db = GrafeoDB::new_in_memory();
657 let mut session = db.session();
658
659 let (_epoch1, tx_id1) = session.get_transaction_context();
661 assert!(tx_id1.is_none());
662
663 session.begin_tx().unwrap();
665 let (epoch2, tx_id2) = session.get_transaction_context();
666 assert!(tx_id2.is_some());
667 let _ = epoch2; session.commit().unwrap();
672 let (epoch3, tx_id3) = session.get_transaction_context();
673 assert!(tx_id3.is_none());
674 assert!(epoch3.as_u64() >= epoch2.as_u64());
676 }
677
678 #[test]
679 fn test_session_rollback() {
680 let db = GrafeoDB::new_in_memory();
681 let mut session = db.session();
682
683 session.begin_tx().unwrap();
684 session.rollback().unwrap();
685 assert!(!session.in_transaction());
686 }
687
688 #[test]
689 fn test_session_rollback_discards_versions() {
690 use grafeo_common::types::TxId;
691
692 let db = GrafeoDB::new_in_memory();
693
694 let node_before = db.store().create_node(&["Person"]);
696 assert!(node_before.is_valid());
697 assert_eq!(db.node_count(), 1, "Should have 1 node before transaction");
698
699 let mut session = db.session();
701 session.begin_tx().unwrap();
702 let tx_id = session.current_tx.unwrap();
703
704 let epoch = db.store().current_epoch();
706 let node_in_tx = db.store().create_node_versioned(&["Person"], epoch, tx_id);
707 assert!(node_in_tx.is_valid());
708
709 assert_eq!(db.node_count(), 2, "Should have 2 nodes during transaction");
711
712 session.rollback().unwrap();
714 assert!(!session.in_transaction());
715
716 let count_after = db.node_count();
719 assert_eq!(
720 count_after, 1,
721 "Rollback should discard uncommitted node, but got {count_after}"
722 );
723
724 let current_epoch = db.store().current_epoch();
726 assert!(
727 db.store()
728 .get_node_versioned(node_before, current_epoch, TxId::SYSTEM)
729 .is_some(),
730 "Original node should still exist"
731 );
732
733 assert!(
735 db.store()
736 .get_node_versioned(node_in_tx, current_epoch, TxId::SYSTEM)
737 .is_none(),
738 "Transaction node should be gone"
739 );
740 }
741
742 #[test]
743 fn test_session_create_node_in_transaction() {
744 let db = GrafeoDB::new_in_memory();
746
747 let node_before = db.create_node(&["Person"]);
749 assert!(node_before.is_valid());
750 assert_eq!(db.node_count(), 1, "Should have 1 node before transaction");
751
752 let mut session = db.session();
754 session.begin_tx().unwrap();
755
756 let node_in_tx = session.create_node(&["Person"]);
758 assert!(node_in_tx.is_valid());
759
760 assert_eq!(db.node_count(), 2, "Should have 2 nodes during transaction");
762
763 session.rollback().unwrap();
765
766 let count_after = db.node_count();
768 assert_eq!(
769 count_after, 1,
770 "Rollback should discard node created via session.create_node(), but got {count_after}"
771 );
772 }
773
774 #[test]
775 fn test_session_create_node_with_props_in_transaction() {
776 use grafeo_common::types::Value;
777
778 let db = GrafeoDB::new_in_memory();
780
781 db.create_node(&["Person"]);
783 assert_eq!(db.node_count(), 1, "Should have 1 node before transaction");
784
785 let mut session = db.session();
787 session.begin_tx().unwrap();
788
789 let node_in_tx =
790 session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
791 assert!(node_in_tx.is_valid());
792
793 assert_eq!(db.node_count(), 2, "Should have 2 nodes during transaction");
795
796 session.rollback().unwrap();
798
799 let count_after = db.node_count();
801 assert_eq!(
802 count_after, 1,
803 "Rollback should discard node created via session.create_node_with_props()"
804 );
805 }
806
807 #[cfg(feature = "gql")]
808 mod gql_tests {
809 use super::*;
810
811 #[test]
812 fn test_gql_query_execution() {
813 let db = GrafeoDB::new_in_memory();
814 let session = db.session();
815
816 session.create_node(&["Person"]);
818 session.create_node(&["Person"]);
819 session.create_node(&["Animal"]);
820
821 let result = session.execute("MATCH (n:Person) RETURN n").unwrap();
823
824 assert_eq!(result.row_count(), 2);
826 assert_eq!(result.column_count(), 1);
827 assert_eq!(result.columns[0], "n");
828 }
829
830 #[test]
831 fn test_gql_empty_result() {
832 let db = GrafeoDB::new_in_memory();
833 let session = db.session();
834
835 let result = session.execute("MATCH (n:Person) RETURN n").unwrap();
837
838 assert_eq!(result.row_count(), 0);
839 }
840
841 #[test]
842 fn test_gql_parse_error() {
843 let db = GrafeoDB::new_in_memory();
844 let session = db.session();
845
846 let result = session.execute("MATCH (n RETURN n");
848
849 assert!(result.is_err());
850 }
851
852 #[test]
853 fn test_gql_relationship_traversal() {
854 let db = GrafeoDB::new_in_memory();
855 let session = db.session();
856
857 let alice = session.create_node(&["Person"]);
859 let bob = session.create_node(&["Person"]);
860 let charlie = session.create_node(&["Person"]);
861
862 session.create_edge(alice, bob, "KNOWS");
863 session.create_edge(alice, charlie, "KNOWS");
864
865 let result = session
867 .execute("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b")
868 .unwrap();
869
870 assert_eq!(result.row_count(), 2);
872 assert_eq!(result.column_count(), 2);
873 assert_eq!(result.columns[0], "a");
874 assert_eq!(result.columns[1], "b");
875 }
876
877 #[test]
878 fn test_gql_relationship_with_type_filter() {
879 let db = GrafeoDB::new_in_memory();
880 let session = db.session();
881
882 let alice = session.create_node(&["Person"]);
884 let bob = session.create_node(&["Person"]);
885 let charlie = session.create_node(&["Person"]);
886
887 session.create_edge(alice, bob, "KNOWS");
888 session.create_edge(alice, charlie, "WORKS_WITH");
889
890 let result = session
892 .execute("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b")
893 .unwrap();
894
895 assert_eq!(result.row_count(), 1);
897 }
898
899 #[test]
900 fn test_gql_semantic_error_undefined_variable() {
901 let db = GrafeoDB::new_in_memory();
902 let session = db.session();
903
904 let result = session.execute("MATCH (n:Person) RETURN x");
906
907 assert!(result.is_err());
909 let err = match result {
910 Err(e) => e,
911 Ok(_) => panic!("Expected error"),
912 };
913 assert!(
914 err.to_string().contains("Undefined variable"),
915 "Expected undefined variable error, got: {}",
916 err
917 );
918 }
919
920 #[test]
921 fn test_gql_where_clause_property_filter() {
922 use grafeo_common::types::Value;
923
924 let db = GrafeoDB::new_in_memory();
925 let session = db.session();
926
927 session.create_node_with_props(&["Person"], [("age", Value::Int64(25))]);
929 session.create_node_with_props(&["Person"], [("age", Value::Int64(35))]);
930 session.create_node_with_props(&["Person"], [("age", Value::Int64(45))]);
931
932 let result = session
934 .execute("MATCH (n:Person) WHERE n.age > 30 RETURN n")
935 .unwrap();
936
937 assert_eq!(result.row_count(), 2);
939 }
940
941 #[test]
942 fn test_gql_where_clause_equality() {
943 use grafeo_common::types::Value;
944
945 let db = GrafeoDB::new_in_memory();
946 let session = db.session();
947
948 session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
950 session.create_node_with_props(&["Person"], [("name", Value::String("Bob".into()))]);
951 session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
952
953 let result = session
955 .execute("MATCH (n:Person) WHERE n.name = \"Alice\" RETURN n")
956 .unwrap();
957
958 assert_eq!(result.row_count(), 2);
960 }
961
962 #[test]
963 fn test_gql_return_property_access() {
964 use grafeo_common::types::Value;
965
966 let db = GrafeoDB::new_in_memory();
967 let session = db.session();
968
969 session.create_node_with_props(
971 &["Person"],
972 [
973 ("name", Value::String("Alice".into())),
974 ("age", Value::Int64(30)),
975 ],
976 );
977 session.create_node_with_props(
978 &["Person"],
979 [
980 ("name", Value::String("Bob".into())),
981 ("age", Value::Int64(25)),
982 ],
983 );
984
985 let result = session
987 .execute("MATCH (n:Person) RETURN n.name, n.age")
988 .unwrap();
989
990 assert_eq!(result.row_count(), 2);
992 assert_eq!(result.column_count(), 2);
993 assert_eq!(result.columns[0], "n.name");
994 assert_eq!(result.columns[1], "n.age");
995
996 let names: Vec<&Value> = result.rows.iter().map(|r| &r[0]).collect();
998 assert!(names.contains(&&Value::String("Alice".into())));
999 assert!(names.contains(&&Value::String("Bob".into())));
1000 }
1001
1002 #[test]
1003 fn test_gql_return_mixed_expressions() {
1004 use grafeo_common::types::Value;
1005
1006 let db = GrafeoDB::new_in_memory();
1007 let session = db.session();
1008
1009 session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
1011
1012 let result = session
1014 .execute("MATCH (n:Person) RETURN n, n.name")
1015 .unwrap();
1016
1017 assert_eq!(result.row_count(), 1);
1018 assert_eq!(result.column_count(), 2);
1019 assert_eq!(result.columns[0], "n");
1020 assert_eq!(result.columns[1], "n.name");
1021
1022 assert_eq!(result.rows[0][1], Value::String("Alice".into()));
1024 }
1025 }
1026
1027 #[cfg(feature = "cypher")]
1028 mod cypher_tests {
1029 use super::*;
1030
1031 #[test]
1032 fn test_cypher_query_execution() {
1033 let db = GrafeoDB::new_in_memory();
1034 let session = db.session();
1035
1036 session.create_node(&["Person"]);
1038 session.create_node(&["Person"]);
1039 session.create_node(&["Animal"]);
1040
1041 let result = session.execute_cypher("MATCH (n:Person) RETURN n").unwrap();
1043
1044 assert_eq!(result.row_count(), 2);
1046 assert_eq!(result.column_count(), 1);
1047 assert_eq!(result.columns[0], "n");
1048 }
1049
1050 #[test]
1051 fn test_cypher_empty_result() {
1052 let db = GrafeoDB::new_in_memory();
1053 let session = db.session();
1054
1055 let result = session.execute_cypher("MATCH (n:Person) RETURN n").unwrap();
1057
1058 assert_eq!(result.row_count(), 0);
1059 }
1060
1061 #[test]
1062 fn test_cypher_parse_error() {
1063 let db = GrafeoDB::new_in_memory();
1064 let session = db.session();
1065
1066 let result = session.execute_cypher("MATCH (n RETURN n");
1068
1069 assert!(result.is_err());
1070 }
1071 }
1072}