Skip to main content

grafeo_engine/
session.rs

1//! Session management.
2
3use std::sync::Arc;
4
5use grafeo_common::types::{EpochId, NodeId, TxId, Value};
6use grafeo_common::utils::error::Result;
7use grafeo_core::graph::lpg::LpgStore;
8
9use crate::database::QueryResult;
10use crate::transaction::TransactionManager;
11
12/// A session for interacting with the database.
13///
14/// Sessions provide isolation between concurrent users and
15/// manage transaction state.
16pub struct Session {
17    /// The underlying store.
18    store: Arc<LpgStore>,
19    /// Transaction manager.
20    tx_manager: Arc<TransactionManager>,
21    /// Current transaction ID (if any).
22    current_tx: Option<TxId>,
23    /// Whether the session is in auto-commit mode.
24    auto_commit: bool,
25}
26
27impl Session {
28    /// Creates a new session.
29    pub(crate) fn new(store: Arc<LpgStore>, tx_manager: Arc<TransactionManager>) -> Self {
30        Self {
31            store,
32            tx_manager,
33            current_tx: None,
34            auto_commit: true,
35        }
36    }
37
38    /// Executes a GQL query.
39    ///
40    /// # Errors
41    ///
42    /// Returns an error if the query fails to parse or execute.
43    ///
44    /// # Examples
45    ///
46    /// ```ignore
47    /// use grafeo_engine::GrafeoDB;
48    ///
49    /// let db = GrafeoDB::new_in_memory();
50    /// let session = db.session();
51    ///
52    /// // Create a node
53    /// session.execute("INSERT (:Person {name: 'Alice', age: 30})")?;
54    ///
55    /// // Query nodes
56    /// let result = session.execute("MATCH (n:Person) RETURN n.name, n.age")?;
57    /// for row in result {
58    ///     println!("{:?}", row);
59    /// }
60    /// ```
61    #[cfg(feature = "gql")]
62    pub fn execute(&self, query: &str) -> Result<QueryResult> {
63        use crate::query::{
64            Executor, Planner, binder::Binder, gql_translator, optimizer::Optimizer,
65        };
66
67        // Parse and translate the query to a logical plan
68        let logical_plan = gql_translator::translate(query)?;
69
70        // Semantic validation
71        let mut binder = Binder::new();
72        let _binding_context = binder.bind(&logical_plan)?;
73
74        // Optimize the plan
75        let optimizer = Optimizer::new();
76        let optimized_plan = optimizer.optimize(logical_plan)?;
77
78        // Get transaction context for MVCC visibility
79        let (viewing_epoch, tx_id) = self.get_transaction_context();
80
81        // Convert to physical plan with transaction context
82        let planner = Planner::with_context(
83            Arc::clone(&self.store),
84            Arc::clone(&self.tx_manager),
85            tx_id,
86            viewing_epoch,
87        );
88        let mut physical_plan = planner.plan(&optimized_plan)?;
89
90        // Execute the plan
91        let executor = Executor::with_columns(physical_plan.columns.clone());
92        executor.execute(physical_plan.operator.as_mut())
93    }
94
95    /// Executes a GQL query with parameters.
96    ///
97    /// # Errors
98    ///
99    /// Returns an error if the query fails to parse or execute.
100    #[cfg(feature = "gql")]
101    pub fn execute_with_params(
102        &self,
103        query: &str,
104        params: std::collections::HashMap<String, Value>,
105    ) -> Result<QueryResult> {
106        use crate::query::processor::{QueryLanguage, QueryProcessor};
107
108        // Get transaction context for MVCC visibility
109        let (viewing_epoch, tx_id) = self.get_transaction_context();
110
111        // Create processor with transaction context
112        let processor =
113            QueryProcessor::for_lpg_with_tx(Arc::clone(&self.store), Arc::clone(&self.tx_manager));
114
115        // Apply transaction context if in a transaction
116        let processor = if let Some(tx_id) = tx_id {
117            processor.with_tx_context(viewing_epoch, tx_id)
118        } else {
119            processor
120        };
121
122        processor.process(query, QueryLanguage::Gql, Some(&params))
123    }
124
125    /// Executes a GQL query with parameters.
126    ///
127    /// # Errors
128    ///
129    /// Returns an error if no query language is enabled.
130    #[cfg(not(any(feature = "gql", feature = "cypher")))]
131    pub fn execute_with_params(
132        &self,
133        _query: &str,
134        _params: std::collections::HashMap<String, Value>,
135    ) -> Result<QueryResult> {
136        Err(grafeo_common::utils::error::Error::Internal(
137            "No query language enabled".to_string(),
138        ))
139    }
140
141    /// Executes a GQL query.
142    ///
143    /// # Errors
144    ///
145    /// Returns an error if no query language is enabled.
146    #[cfg(not(any(feature = "gql", feature = "cypher")))]
147    pub fn execute(&self, _query: &str) -> Result<QueryResult> {
148        Err(grafeo_common::utils::error::Error::Internal(
149            "No query language enabled".to_string(),
150        ))
151    }
152
153    /// Executes a Cypher query.
154    ///
155    /// # Errors
156    ///
157    /// Returns an error if the query fails to parse or execute.
158    #[cfg(feature = "cypher")]
159    pub fn execute_cypher(&self, query: &str) -> Result<QueryResult> {
160        use crate::query::{
161            Executor, Planner, binder::Binder, cypher_translator, optimizer::Optimizer,
162        };
163
164        // Parse and translate the query to a logical plan
165        let logical_plan = cypher_translator::translate(query)?;
166
167        // Semantic validation
168        let mut binder = Binder::new();
169        let _binding_context = binder.bind(&logical_plan)?;
170
171        // Optimize the plan
172        let optimizer = Optimizer::new();
173        let optimized_plan = optimizer.optimize(logical_plan)?;
174
175        // Get transaction context for MVCC visibility
176        let (viewing_epoch, tx_id) = self.get_transaction_context();
177
178        // Convert to physical plan with transaction context
179        let planner = Planner::with_context(
180            Arc::clone(&self.store),
181            Arc::clone(&self.tx_manager),
182            tx_id,
183            viewing_epoch,
184        );
185        let mut physical_plan = planner.plan(&optimized_plan)?;
186
187        // Execute the plan
188        let executor = Executor::with_columns(physical_plan.columns.clone());
189        executor.execute(physical_plan.operator.as_mut())
190    }
191
192    /// Executes a Gremlin query.
193    ///
194    /// # Errors
195    ///
196    /// Returns an error if the query fails to parse or execute.
197    ///
198    /// # Examples
199    ///
200    /// ```ignore
201    /// use grafeo_engine::GrafeoDB;
202    ///
203    /// let db = GrafeoDB::new_in_memory();
204    /// let session = db.session();
205    ///
206    /// // Create some nodes first
207    /// session.create_node(&["Person"]);
208    ///
209    /// // Query using Gremlin
210    /// let result = session.execute_gremlin("g.V().hasLabel('Person')")?;
211    /// ```
212    #[cfg(feature = "gremlin")]
213    pub fn execute_gremlin(&self, query: &str) -> Result<QueryResult> {
214        use crate::query::{
215            Executor, Planner, binder::Binder, gremlin_translator, optimizer::Optimizer,
216        };
217
218        // Parse and translate the query to a logical plan
219        let logical_plan = gremlin_translator::translate(query)?;
220
221        // Semantic validation
222        let mut binder = Binder::new();
223        let _binding_context = binder.bind(&logical_plan)?;
224
225        // Optimize the plan
226        let optimizer = Optimizer::new();
227        let optimized_plan = optimizer.optimize(logical_plan)?;
228
229        // Get transaction context for MVCC visibility
230        let (viewing_epoch, tx_id) = self.get_transaction_context();
231
232        // Convert to physical plan with transaction context
233        let planner = Planner::with_context(
234            Arc::clone(&self.store),
235            Arc::clone(&self.tx_manager),
236            tx_id,
237            viewing_epoch,
238        );
239        let mut physical_plan = planner.plan(&optimized_plan)?;
240
241        // Execute the plan
242        let executor = Executor::with_columns(physical_plan.columns.clone());
243        executor.execute(physical_plan.operator.as_mut())
244    }
245
246    /// Executes a Gremlin query with parameters.
247    ///
248    /// # Errors
249    ///
250    /// Returns an error if the query fails to parse or execute.
251    #[cfg(feature = "gremlin")]
252    pub fn execute_gremlin_with_params(
253        &self,
254        query: &str,
255        params: std::collections::HashMap<String, Value>,
256    ) -> Result<QueryResult> {
257        use crate::query::processor::{QueryLanguage, QueryProcessor};
258
259        // Get transaction context for MVCC visibility
260        let (viewing_epoch, tx_id) = self.get_transaction_context();
261
262        // Create processor with transaction context
263        let processor =
264            QueryProcessor::for_lpg_with_tx(Arc::clone(&self.store), Arc::clone(&self.tx_manager));
265
266        // Apply transaction context if in a transaction
267        let processor = if let Some(tx_id) = tx_id {
268            processor.with_tx_context(viewing_epoch, tx_id)
269        } else {
270            processor
271        };
272
273        processor.process(query, QueryLanguage::Gremlin, Some(&params))
274    }
275
276    /// Executes a GraphQL query against the LPG store.
277    ///
278    /// # Errors
279    ///
280    /// Returns an error if the query fails to parse or execute.
281    ///
282    /// # Examples
283    ///
284    /// ```ignore
285    /// use grafeo_engine::GrafeoDB;
286    ///
287    /// let db = GrafeoDB::new_in_memory();
288    /// let session = db.session();
289    ///
290    /// // Create some nodes first
291    /// session.create_node(&["User"]);
292    ///
293    /// // Query using GraphQL
294    /// let result = session.execute_graphql("query { user { id name } }")?;
295    /// ```
296    #[cfg(feature = "graphql")]
297    pub fn execute_graphql(&self, query: &str) -> Result<QueryResult> {
298        use crate::query::{
299            Executor, Planner, binder::Binder, graphql_translator, optimizer::Optimizer,
300        };
301
302        // Parse and translate the query to a logical plan
303        let logical_plan = graphql_translator::translate(query)?;
304
305        // Semantic validation
306        let mut binder = Binder::new();
307        let _binding_context = binder.bind(&logical_plan)?;
308
309        // Optimize the plan
310        let optimizer = Optimizer::new();
311        let optimized_plan = optimizer.optimize(logical_plan)?;
312
313        // Get transaction context for MVCC visibility
314        let (viewing_epoch, tx_id) = self.get_transaction_context();
315
316        // Convert to physical plan with transaction context
317        let planner = Planner::with_context(
318            Arc::clone(&self.store),
319            Arc::clone(&self.tx_manager),
320            tx_id,
321            viewing_epoch,
322        );
323        let mut physical_plan = planner.plan(&optimized_plan)?;
324
325        // Execute the plan
326        let executor = Executor::with_columns(physical_plan.columns.clone());
327        executor.execute(physical_plan.operator.as_mut())
328    }
329
330    /// Executes a GraphQL query with parameters.
331    ///
332    /// # Errors
333    ///
334    /// Returns an error if the query fails to parse or execute.
335    #[cfg(feature = "graphql")]
336    pub fn execute_graphql_with_params(
337        &self,
338        query: &str,
339        params: std::collections::HashMap<String, Value>,
340    ) -> Result<QueryResult> {
341        use crate::query::processor::{QueryLanguage, QueryProcessor};
342
343        // Get transaction context for MVCC visibility
344        let (viewing_epoch, tx_id) = self.get_transaction_context();
345
346        // Create processor with transaction context
347        let processor =
348            QueryProcessor::for_lpg_with_tx(Arc::clone(&self.store), Arc::clone(&self.tx_manager));
349
350        // Apply transaction context if in a transaction
351        let processor = if let Some(tx_id) = tx_id {
352            processor.with_tx_context(viewing_epoch, tx_id)
353        } else {
354            processor
355        };
356
357        processor.process(query, QueryLanguage::GraphQL, Some(&params))
358    }
359
360    /// Begins a new transaction.
361    ///
362    /// # Errors
363    ///
364    /// Returns an error if a transaction is already active.
365    ///
366    /// # Examples
367    ///
368    /// ```ignore
369    /// use grafeo_engine::GrafeoDB;
370    ///
371    /// let db = GrafeoDB::new_in_memory();
372    /// let mut session = db.session();
373    ///
374    /// session.begin_tx()?;
375    /// session.execute("INSERT (:Person {name: 'Alice'})")?;
376    /// session.execute("INSERT (:Person {name: 'Bob'})")?;
377    /// session.commit()?; // Both inserts committed atomically
378    /// ```
379    pub fn begin_tx(&mut self) -> Result<()> {
380        if self.current_tx.is_some() {
381            return Err(grafeo_common::utils::error::Error::Transaction(
382                grafeo_common::utils::error::TransactionError::InvalidState(
383                    "Transaction already active".to_string(),
384                ),
385            ));
386        }
387
388        let tx_id = self.tx_manager.begin();
389        self.current_tx = Some(tx_id);
390        Ok(())
391    }
392
393    /// Commits the current transaction.
394    ///
395    /// Makes all changes since [`begin_tx`](Self::begin_tx) permanent.
396    ///
397    /// # Errors
398    ///
399    /// Returns an error if no transaction is active.
400    pub fn commit(&mut self) -> Result<()> {
401        let tx_id = self.current_tx.take().ok_or_else(|| {
402            grafeo_common::utils::error::Error::Transaction(
403                grafeo_common::utils::error::TransactionError::InvalidState(
404                    "No active transaction".to_string(),
405                ),
406            )
407        })?;
408
409        self.tx_manager.commit(tx_id).map(|_| ())
410    }
411
412    /// Aborts the current transaction.
413    ///
414    /// Discards all changes since [`begin_tx`](Self::begin_tx).
415    ///
416    /// # Errors
417    ///
418    /// Returns an error if no transaction is active.
419    ///
420    /// # Examples
421    ///
422    /// ```ignore
423    /// use grafeo_engine::GrafeoDB;
424    ///
425    /// let db = GrafeoDB::new_in_memory();
426    /// let mut session = db.session();
427    ///
428    /// session.begin_tx()?;
429    /// session.execute("INSERT (:Person {name: 'Alice'})")?;
430    /// session.rollback()?; // Insert is discarded
431    /// ```
432    pub fn rollback(&mut self) -> Result<()> {
433        let tx_id = self.current_tx.take().ok_or_else(|| {
434            grafeo_common::utils::error::Error::Transaction(
435                grafeo_common::utils::error::TransactionError::InvalidState(
436                    "No active transaction".to_string(),
437                ),
438            )
439        })?;
440
441        // Discard uncommitted versions in the store
442        self.store.discard_uncommitted_versions(tx_id);
443
444        // Mark transaction as aborted in the manager
445        self.tx_manager.abort(tx_id)
446    }
447
448    /// Returns whether a transaction is active.
449    #[must_use]
450    pub fn in_transaction(&self) -> bool {
451        self.current_tx.is_some()
452    }
453
454    /// Sets auto-commit mode.
455    pub fn set_auto_commit(&mut self, auto_commit: bool) {
456        self.auto_commit = auto_commit;
457    }
458
459    /// Returns whether auto-commit is enabled.
460    #[must_use]
461    pub fn auto_commit(&self) -> bool {
462        self.auto_commit
463    }
464
465    /// Returns the current transaction context for MVCC visibility.
466    ///
467    /// Returns `(viewing_epoch, tx_id)` where:
468    /// - `viewing_epoch` is the epoch at which to check version visibility
469    /// - `tx_id` is the current transaction ID (if in a transaction)
470    #[must_use]
471    fn get_transaction_context(&self) -> (EpochId, Option<TxId>) {
472        if let Some(tx_id) = self.current_tx {
473            // In a transaction - use the transaction's start epoch
474            let epoch = self
475                .tx_manager
476                .start_epoch(tx_id)
477                .unwrap_or_else(|| self.tx_manager.current_epoch());
478            (epoch, Some(tx_id))
479        } else {
480            // No transaction - use current epoch
481            (self.tx_manager.current_epoch(), None)
482        }
483    }
484
485    /// Creates a node directly (bypassing query execution).
486    ///
487    /// This is a low-level API for testing and direct manipulation.
488    /// If a transaction is active, the node will be versioned with the transaction ID.
489    pub fn create_node(&self, labels: &[&str]) -> NodeId {
490        let (epoch, tx_id) = self.get_transaction_context();
491        self.store
492            .create_node_versioned(labels, epoch, tx_id.unwrap_or(TxId::SYSTEM))
493    }
494
495    /// Creates a node with properties.
496    ///
497    /// If a transaction is active, the node will be versioned with the transaction ID.
498    pub fn create_node_with_props<'a>(
499        &self,
500        labels: &[&str],
501        properties: impl IntoIterator<Item = (&'a str, Value)>,
502    ) -> NodeId {
503        let (epoch, tx_id) = self.get_transaction_context();
504        self.store.create_node_with_props_versioned(
505            labels,
506            properties.into_iter().map(|(k, v)| (k, v)),
507            epoch,
508            tx_id.unwrap_or(TxId::SYSTEM),
509        )
510    }
511
512    /// Creates an edge between two nodes.
513    ///
514    /// This is a low-level API for testing and direct manipulation.
515    /// If a transaction is active, the edge will be versioned with the transaction ID.
516    pub fn create_edge(
517        &self,
518        src: NodeId,
519        dst: NodeId,
520        edge_type: &str,
521    ) -> grafeo_common::types::EdgeId {
522        let (epoch, tx_id) = self.get_transaction_context();
523        self.store
524            .create_edge_versioned(src, dst, edge_type, epoch, tx_id.unwrap_or(TxId::SYSTEM))
525    }
526}
527
528#[cfg(test)]
529mod tests {
530    use crate::database::GrafeoDB;
531
532    #[test]
533    fn test_session_create_node() {
534        let db = GrafeoDB::new_in_memory();
535        let session = db.session();
536
537        let id = session.create_node(&["Person"]);
538        assert!(id.is_valid());
539        assert_eq!(db.node_count(), 1);
540    }
541
542    #[test]
543    fn test_session_transaction() {
544        let db = GrafeoDB::new_in_memory();
545        let mut session = db.session();
546
547        assert!(!session.in_transaction());
548
549        session.begin_tx().unwrap();
550        assert!(session.in_transaction());
551
552        session.commit().unwrap();
553        assert!(!session.in_transaction());
554    }
555
556    #[test]
557    fn test_session_transaction_context() {
558        let db = GrafeoDB::new_in_memory();
559        let mut session = db.session();
560
561        // Without transaction - context should have current epoch and no tx_id
562        let (_epoch1, tx_id1) = session.get_transaction_context();
563        assert!(tx_id1.is_none());
564
565        // Start a transaction
566        session.begin_tx().unwrap();
567        let (epoch2, tx_id2) = session.get_transaction_context();
568        assert!(tx_id2.is_some());
569        // Transaction should have a valid epoch
570        let _ = epoch2; // Use the variable
571
572        // Commit and verify
573        session.commit().unwrap();
574        let (epoch3, tx_id3) = session.get_transaction_context();
575        assert!(tx_id3.is_none());
576        // Epoch should have advanced after commit
577        assert!(epoch3.as_u64() >= epoch2.as_u64());
578    }
579
580    #[test]
581    fn test_session_rollback() {
582        let db = GrafeoDB::new_in_memory();
583        let mut session = db.session();
584
585        session.begin_tx().unwrap();
586        session.rollback().unwrap();
587        assert!(!session.in_transaction());
588    }
589
590    #[test]
591    fn test_session_rollback_discards_versions() {
592        use grafeo_common::types::TxId;
593
594        let db = GrafeoDB::new_in_memory();
595
596        // Create a node outside of any transaction (at system level)
597        let node_before = db.store().create_node(&["Person"]);
598        assert!(node_before.is_valid());
599        assert_eq!(db.node_count(), 1, "Should have 1 node before transaction");
600
601        // Start a transaction
602        let mut session = db.session();
603        session.begin_tx().unwrap();
604        let tx_id = session.current_tx.unwrap();
605
606        // Create a node versioned with the transaction's ID
607        let epoch = db.store().current_epoch();
608        let node_in_tx = db.store().create_node_versioned(&["Person"], epoch, tx_id);
609        assert!(node_in_tx.is_valid());
610
611        // Should see 2 nodes at this point
612        assert_eq!(db.node_count(), 2, "Should have 2 nodes during transaction");
613
614        // Rollback the transaction
615        session.rollback().unwrap();
616        assert!(!session.in_transaction());
617
618        // The node created in the transaction should be discarded
619        // Only the first node should remain visible
620        let count_after = db.node_count();
621        assert_eq!(
622            count_after, 1,
623            "Rollback should discard uncommitted node, but got {count_after}"
624        );
625
626        // The original node should still be accessible
627        let current_epoch = db.store().current_epoch();
628        assert!(
629            db.store()
630                .get_node_versioned(node_before, current_epoch, TxId::SYSTEM)
631                .is_some(),
632            "Original node should still exist"
633        );
634
635        // The node created in the transaction should not be accessible
636        assert!(
637            db.store()
638                .get_node_versioned(node_in_tx, current_epoch, TxId::SYSTEM)
639                .is_none(),
640            "Transaction node should be gone"
641        );
642    }
643
644    #[test]
645    fn test_session_create_node_in_transaction() {
646        // Test that session.create_node() is transaction-aware
647        let db = GrafeoDB::new_in_memory();
648
649        // Create a node outside of any transaction
650        let node_before = db.create_node(&["Person"]);
651        assert!(node_before.is_valid());
652        assert_eq!(db.node_count(), 1, "Should have 1 node before transaction");
653
654        // Start a transaction and create a node through the session
655        let mut session = db.session();
656        session.begin_tx().unwrap();
657
658        // Create a node through session.create_node() - should be versioned with tx
659        let node_in_tx = session.create_node(&["Person"]);
660        assert!(node_in_tx.is_valid());
661
662        // Should see 2 nodes at this point
663        assert_eq!(db.node_count(), 2, "Should have 2 nodes during transaction");
664
665        // Rollback the transaction
666        session.rollback().unwrap();
667
668        // The node created via session.create_node() should be discarded
669        let count_after = db.node_count();
670        assert_eq!(
671            count_after, 1,
672            "Rollback should discard node created via session.create_node(), but got {count_after}"
673        );
674    }
675
676    #[test]
677    fn test_session_create_node_with_props_in_transaction() {
678        use grafeo_common::types::Value;
679
680        // Test that session.create_node_with_props() is transaction-aware
681        let db = GrafeoDB::new_in_memory();
682
683        // Create a node outside of any transaction
684        db.create_node(&["Person"]);
685        assert_eq!(db.node_count(), 1, "Should have 1 node before transaction");
686
687        // Start a transaction and create a node with properties
688        let mut session = db.session();
689        session.begin_tx().unwrap();
690
691        let node_in_tx =
692            session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
693        assert!(node_in_tx.is_valid());
694
695        // Should see 2 nodes
696        assert_eq!(db.node_count(), 2, "Should have 2 nodes during transaction");
697
698        // Rollback the transaction
699        session.rollback().unwrap();
700
701        // The node should be discarded
702        let count_after = db.node_count();
703        assert_eq!(
704            count_after, 1,
705            "Rollback should discard node created via session.create_node_with_props()"
706        );
707    }
708
709    #[cfg(feature = "gql")]
710    mod gql_tests {
711        use super::*;
712
713        #[test]
714        fn test_gql_query_execution() {
715            let db = GrafeoDB::new_in_memory();
716            let session = db.session();
717
718            // Create some test data
719            session.create_node(&["Person"]);
720            session.create_node(&["Person"]);
721            session.create_node(&["Animal"]);
722
723            // Execute a GQL query
724            let result = session.execute("MATCH (n:Person) RETURN n").unwrap();
725
726            // Should return 2 Person nodes
727            assert_eq!(result.row_count(), 2);
728            assert_eq!(result.column_count(), 1);
729            assert_eq!(result.columns[0], "n");
730        }
731
732        #[test]
733        fn test_gql_empty_result() {
734            let db = GrafeoDB::new_in_memory();
735            let session = db.session();
736
737            // No data in database
738            let result = session.execute("MATCH (n:Person) RETURN n").unwrap();
739
740            assert_eq!(result.row_count(), 0);
741        }
742
743        #[test]
744        fn test_gql_parse_error() {
745            let db = GrafeoDB::new_in_memory();
746            let session = db.session();
747
748            // Invalid GQL syntax
749            let result = session.execute("MATCH (n RETURN n");
750
751            assert!(result.is_err());
752        }
753
754        #[test]
755        fn test_gql_relationship_traversal() {
756            let db = GrafeoDB::new_in_memory();
757            let session = db.session();
758
759            // Create a graph: Alice -> Bob, Alice -> Charlie
760            let alice = session.create_node(&["Person"]);
761            let bob = session.create_node(&["Person"]);
762            let charlie = session.create_node(&["Person"]);
763
764            session.create_edge(alice, bob, "KNOWS");
765            session.create_edge(alice, charlie, "KNOWS");
766
767            // Execute a path query: MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b
768            let result = session
769                .execute("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b")
770                .unwrap();
771
772            // Should return 2 rows (Alice->Bob, Alice->Charlie)
773            assert_eq!(result.row_count(), 2);
774            assert_eq!(result.column_count(), 2);
775            assert_eq!(result.columns[0], "a");
776            assert_eq!(result.columns[1], "b");
777        }
778
779        #[test]
780        fn test_gql_relationship_with_type_filter() {
781            let db = GrafeoDB::new_in_memory();
782            let session = db.session();
783
784            // Create a graph: Alice -KNOWS-> Bob, Alice -WORKS_WITH-> Charlie
785            let alice = session.create_node(&["Person"]);
786            let bob = session.create_node(&["Person"]);
787            let charlie = session.create_node(&["Person"]);
788
789            session.create_edge(alice, bob, "KNOWS");
790            session.create_edge(alice, charlie, "WORKS_WITH");
791
792            // Query only KNOWS relationships
793            let result = session
794                .execute("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b")
795                .unwrap();
796
797            // Should return only 1 row (Alice->Bob)
798            assert_eq!(result.row_count(), 1);
799        }
800
801        #[test]
802        fn test_gql_semantic_error_undefined_variable() {
803            let db = GrafeoDB::new_in_memory();
804            let session = db.session();
805
806            // Reference undefined variable 'x' in RETURN
807            let result = session.execute("MATCH (n:Person) RETURN x");
808
809            // Should fail with semantic error
810            assert!(result.is_err());
811            let err = match result {
812                Err(e) => e,
813                Ok(_) => panic!("Expected error"),
814            };
815            assert!(
816                err.to_string().contains("Undefined variable"),
817                "Expected undefined variable error, got: {}",
818                err
819            );
820        }
821
822        #[test]
823        fn test_gql_where_clause_property_filter() {
824            use grafeo_common::types::Value;
825
826            let db = GrafeoDB::new_in_memory();
827            let session = db.session();
828
829            // Create people with ages
830            session.create_node_with_props(&["Person"], [("age", Value::Int64(25))]);
831            session.create_node_with_props(&["Person"], [("age", Value::Int64(35))]);
832            session.create_node_with_props(&["Person"], [("age", Value::Int64(45))]);
833
834            // Query with WHERE clause: age > 30
835            let result = session
836                .execute("MATCH (n:Person) WHERE n.age > 30 RETURN n")
837                .unwrap();
838
839            // Should return 2 people (ages 35 and 45)
840            assert_eq!(result.row_count(), 2);
841        }
842
843        #[test]
844        fn test_gql_where_clause_equality() {
845            use grafeo_common::types::Value;
846
847            let db = GrafeoDB::new_in_memory();
848            let session = db.session();
849
850            // Create people with names
851            session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
852            session.create_node_with_props(&["Person"], [("name", Value::String("Bob".into()))]);
853            session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
854
855            // Query with WHERE clause: name = "Alice"
856            let result = session
857                .execute("MATCH (n:Person) WHERE n.name = \"Alice\" RETURN n")
858                .unwrap();
859
860            // Should return 2 people named Alice
861            assert_eq!(result.row_count(), 2);
862        }
863
864        #[test]
865        fn test_gql_return_property_access() {
866            use grafeo_common::types::Value;
867
868            let db = GrafeoDB::new_in_memory();
869            let session = db.session();
870
871            // Create people with names and ages
872            session.create_node_with_props(
873                &["Person"],
874                [
875                    ("name", Value::String("Alice".into())),
876                    ("age", Value::Int64(30)),
877                ],
878            );
879            session.create_node_with_props(
880                &["Person"],
881                [
882                    ("name", Value::String("Bob".into())),
883                    ("age", Value::Int64(25)),
884                ],
885            );
886
887            // Query returning properties
888            let result = session
889                .execute("MATCH (n:Person) RETURN n.name, n.age")
890                .unwrap();
891
892            // Should return 2 rows with name and age columns
893            assert_eq!(result.row_count(), 2);
894            assert_eq!(result.column_count(), 2);
895            assert_eq!(result.columns[0], "n.name");
896            assert_eq!(result.columns[1], "n.age");
897
898            // Check that we get actual values
899            let names: Vec<&Value> = result.rows.iter().map(|r| &r[0]).collect();
900            assert!(names.contains(&&Value::String("Alice".into())));
901            assert!(names.contains(&&Value::String("Bob".into())));
902        }
903
904        #[test]
905        fn test_gql_return_mixed_expressions() {
906            use grafeo_common::types::Value;
907
908            let db = GrafeoDB::new_in_memory();
909            let session = db.session();
910
911            // Create a person
912            session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
913
914            // Query returning both node and property
915            let result = session
916                .execute("MATCH (n:Person) RETURN n, n.name")
917                .unwrap();
918
919            assert_eq!(result.row_count(), 1);
920            assert_eq!(result.column_count(), 2);
921            assert_eq!(result.columns[0], "n");
922            assert_eq!(result.columns[1], "n.name");
923
924            // Second column should be the name
925            assert_eq!(result.rows[0][1], Value::String("Alice".into()));
926        }
927    }
928
929    #[cfg(feature = "cypher")]
930    mod cypher_tests {
931        use super::*;
932
933        #[test]
934        fn test_cypher_query_execution() {
935            let db = GrafeoDB::new_in_memory();
936            let session = db.session();
937
938            // Create some test data
939            session.create_node(&["Person"]);
940            session.create_node(&["Person"]);
941            session.create_node(&["Animal"]);
942
943            // Execute a Cypher query
944            let result = session.execute_cypher("MATCH (n:Person) RETURN n").unwrap();
945
946            // Should return 2 Person nodes
947            assert_eq!(result.row_count(), 2);
948            assert_eq!(result.column_count(), 1);
949            assert_eq!(result.columns[0], "n");
950        }
951
952        #[test]
953        fn test_cypher_empty_result() {
954            let db = GrafeoDB::new_in_memory();
955            let session = db.session();
956
957            // No data in database
958            let result = session.execute_cypher("MATCH (n:Person) RETURN n").unwrap();
959
960            assert_eq!(result.row_count(), 0);
961        }
962
963        #[test]
964        fn test_cypher_parse_error() {
965            let db = GrafeoDB::new_in_memory();
966            let session = db.session();
967
968            // Invalid Cypher syntax
969            let result = session.execute_cypher("MATCH (n RETURN n");
970
971            assert!(result.is_err());
972        }
973    }
974}