Skip to main content

grafeo_engine/
session.rs

1//! Lightweight handles for database interaction.
2//!
3//! A session is your conversation with the database. Each session can have
4//! its own transaction state, so concurrent sessions don't interfere with
5//! each other. Sessions are cheap to create - spin up as many as you need.
6
7use 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
19/// Your handle to the database - execute queries and manage transactions.
20///
21/// Get one from [`GrafeoDB::session()`](crate::GrafeoDB::session). Each session
22/// tracks its own transaction state, so you can have multiple concurrent
23/// sessions without them interfering.
24pub struct Session {
25    /// The underlying store.
26    store: Arc<LpgStore>,
27    /// RDF triple store (if RDF feature is enabled).
28    #[cfg(feature = "rdf")]
29    #[allow(dead_code)]
30    rdf_store: Arc<RdfStore>,
31    /// Transaction manager.
32    tx_manager: Arc<TransactionManager>,
33    /// Current transaction ID (if any).
34    current_tx: Option<TxId>,
35    /// Whether the session is in auto-commit mode.
36    auto_commit: bool,
37    /// Adaptive execution configuration.
38    #[allow(dead_code)]
39    adaptive_config: AdaptiveConfig,
40}
41
42impl Session {
43    /// Creates a new session.
44    #[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    /// Creates a new session with adaptive execution configuration.
58    #[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    /// Creates a new session with RDF store and adaptive configuration.
76    #[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    /// Executes a GQL query.
94    ///
95    /// # Errors
96    ///
97    /// Returns an error if the query fails to parse or execute.
98    ///
99    /// # Examples
100    ///
101    /// ```ignore
102    /// use grafeo_engine::GrafeoDB;
103    ///
104    /// let db = GrafeoDB::new_in_memory();
105    /// let session = db.session();
106    ///
107    /// // Create a node
108    /// session.execute("INSERT (:Person {name: 'Alice', age: 30})")?;
109    ///
110    /// // Query nodes
111    /// let result = session.execute("MATCH (n:Person) RETURN n.name, n.age")?;
112    /// for row in result {
113    ///     println!("{:?}", row);
114    /// }
115    /// ```
116    #[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        // Parse and translate the query to a logical plan
123        let logical_plan = gql_translator::translate(query)?;
124
125        // Semantic validation
126        let mut binder = Binder::new();
127        let _binding_context = binder.bind(&logical_plan)?;
128
129        // Optimize the plan
130        let optimizer = Optimizer::new();
131        let optimized_plan = optimizer.optimize(logical_plan)?;
132
133        // Get transaction context for MVCC visibility
134        let (viewing_epoch, tx_id) = self.get_transaction_context();
135
136        // Convert to physical plan with transaction context
137        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        // Execute the plan
146        let executor = Executor::with_columns(physical_plan.columns.clone());
147        executor.execute(physical_plan.operator.as_mut())
148    }
149
150    /// Executes a GQL query with parameters.
151    ///
152    /// # Errors
153    ///
154    /// Returns an error if the query fails to parse or execute.
155    #[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        // Get transaction context for MVCC visibility
164        let (viewing_epoch, tx_id) = self.get_transaction_context();
165
166        // Create processor with transaction context
167        let processor =
168            QueryProcessor::for_lpg_with_tx(Arc::clone(&self.store), Arc::clone(&self.tx_manager));
169
170        // Apply transaction context if in a transaction
171        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(&params))
178    }
179
180    /// Executes a GQL query with parameters.
181    ///
182    /// # Errors
183    ///
184    /// Returns an error if no query language is enabled.
185    #[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    /// Executes a GQL query.
197    ///
198    /// # Errors
199    ///
200    /// Returns an error if no query language is enabled.
201    #[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    /// Executes a Cypher query.
209    ///
210    /// # Errors
211    ///
212    /// Returns an error if the query fails to parse or execute.
213    #[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        // Parse and translate the query to a logical plan
220        let logical_plan = cypher_translator::translate(query)?;
221
222        // Semantic validation
223        let mut binder = Binder::new();
224        let _binding_context = binder.bind(&logical_plan)?;
225
226        // Optimize the plan
227        let optimizer = Optimizer::new();
228        let optimized_plan = optimizer.optimize(logical_plan)?;
229
230        // Get transaction context for MVCC visibility
231        let (viewing_epoch, tx_id) = self.get_transaction_context();
232
233        // Convert to physical plan with transaction context
234        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        // Execute the plan
243        let executor = Executor::with_columns(physical_plan.columns.clone());
244        executor.execute(physical_plan.operator.as_mut())
245    }
246
247    /// Executes a Gremlin query.
248    ///
249    /// # Errors
250    ///
251    /// Returns an error if the query fails to parse or execute.
252    ///
253    /// # Examples
254    ///
255    /// ```ignore
256    /// use grafeo_engine::GrafeoDB;
257    ///
258    /// let db = GrafeoDB::new_in_memory();
259    /// let session = db.session();
260    ///
261    /// // Create some nodes first
262    /// session.create_node(&["Person"]);
263    ///
264    /// // Query using Gremlin
265    /// let result = session.execute_gremlin("g.V().hasLabel('Person')")?;
266    /// ```
267    #[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        // Parse and translate the query to a logical plan
274        let logical_plan = gremlin_translator::translate(query)?;
275
276        // Semantic validation
277        let mut binder = Binder::new();
278        let _binding_context = binder.bind(&logical_plan)?;
279
280        // Optimize the plan
281        let optimizer = Optimizer::new();
282        let optimized_plan = optimizer.optimize(logical_plan)?;
283
284        // Get transaction context for MVCC visibility
285        let (viewing_epoch, tx_id) = self.get_transaction_context();
286
287        // Convert to physical plan with transaction context
288        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        // Execute the plan
297        let executor = Executor::with_columns(physical_plan.columns.clone());
298        executor.execute(physical_plan.operator.as_mut())
299    }
300
301    /// Executes a Gremlin query with parameters.
302    ///
303    /// # Errors
304    ///
305    /// Returns an error if the query fails to parse or execute.
306    #[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        // Get transaction context for MVCC visibility
315        let (viewing_epoch, tx_id) = self.get_transaction_context();
316
317        // Create processor with transaction context
318        let processor =
319            QueryProcessor::for_lpg_with_tx(Arc::clone(&self.store), Arc::clone(&self.tx_manager));
320
321        // Apply transaction context if in a transaction
322        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(&params))
329    }
330
331    /// Executes a GraphQL query against the LPG store.
332    ///
333    /// # Errors
334    ///
335    /// Returns an error if the query fails to parse or execute.
336    ///
337    /// # Examples
338    ///
339    /// ```ignore
340    /// use grafeo_engine::GrafeoDB;
341    ///
342    /// let db = GrafeoDB::new_in_memory();
343    /// let session = db.session();
344    ///
345    /// // Create some nodes first
346    /// session.create_node(&["User"]);
347    ///
348    /// // Query using GraphQL
349    /// let result = session.execute_graphql("query { user { id name } }")?;
350    /// ```
351    #[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        // Parse and translate the query to a logical plan
358        let logical_plan = graphql_translator::translate(query)?;
359
360        // Semantic validation
361        let mut binder = Binder::new();
362        let _binding_context = binder.bind(&logical_plan)?;
363
364        // Optimize the plan
365        let optimizer = Optimizer::new();
366        let optimized_plan = optimizer.optimize(logical_plan)?;
367
368        // Get transaction context for MVCC visibility
369        let (viewing_epoch, tx_id) = self.get_transaction_context();
370
371        // Convert to physical plan with transaction context
372        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        // Execute the plan
381        let executor = Executor::with_columns(physical_plan.columns.clone());
382        executor.execute(physical_plan.operator.as_mut())
383    }
384
385    /// Executes a GraphQL query with parameters.
386    ///
387    /// # Errors
388    ///
389    /// Returns an error if the query fails to parse or execute.
390    #[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        // Get transaction context for MVCC visibility
399        let (viewing_epoch, tx_id) = self.get_transaction_context();
400
401        // Create processor with transaction context
402        let processor =
403            QueryProcessor::for_lpg_with_tx(Arc::clone(&self.store), Arc::clone(&self.tx_manager));
404
405        // Apply transaction context if in a transaction
406        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(&params))
413    }
414
415    /// Executes a SPARQL query.
416    ///
417    /// # Errors
418    ///
419    /// Returns an error if the query fails to parse or execute.
420    #[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        // Parse and translate the SPARQL query to a logical plan
427        let logical_plan = sparql_translator::translate(query)?;
428
429        // Optimize the plan
430        let optimizer = Optimizer::new();
431        let optimized_plan = optimizer.optimize(logical_plan)?;
432
433        // Convert to physical plan using RDF planner
434        let planner = RdfPlanner::new(Arc::clone(&self.rdf_store));
435        let mut physical_plan = planner.plan(&optimized_plan)?;
436
437        // Execute the plan
438        let executor = Executor::with_columns(physical_plan.columns.clone());
439        executor.execute(physical_plan.operator.as_mut())
440    }
441
442    /// Executes a SPARQL query with parameters.
443    ///
444    /// # Errors
445    ///
446    /// Returns an error if the query fails to parse or execute.
447    #[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        // TODO: Implement parameter substitution for SPARQL
454        // For now, just execute the query without parameters
455        self.execute_sparql(query)
456    }
457
458    /// Begins a new transaction.
459    ///
460    /// # Errors
461    ///
462    /// Returns an error if a transaction is already active.
463    ///
464    /// # Examples
465    ///
466    /// ```ignore
467    /// use grafeo_engine::GrafeoDB;
468    ///
469    /// let db = GrafeoDB::new_in_memory();
470    /// let mut session = db.session();
471    ///
472    /// session.begin_tx()?;
473    /// session.execute("INSERT (:Person {name: 'Alice'})")?;
474    /// session.execute("INSERT (:Person {name: 'Bob'})")?;
475    /// session.commit()?; // Both inserts committed atomically
476    /// ```
477    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    /// Commits the current transaction.
492    ///
493    /// Makes all changes since [`begin_tx`](Self::begin_tx) permanent.
494    ///
495    /// # Errors
496    ///
497    /// Returns an error if no transaction is active.
498    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    /// Aborts the current transaction.
511    ///
512    /// Discards all changes since [`begin_tx`](Self::begin_tx).
513    ///
514    /// # Errors
515    ///
516    /// Returns an error if no transaction is active.
517    ///
518    /// # Examples
519    ///
520    /// ```ignore
521    /// use grafeo_engine::GrafeoDB;
522    ///
523    /// let db = GrafeoDB::new_in_memory();
524    /// let mut session = db.session();
525    ///
526    /// session.begin_tx()?;
527    /// session.execute("INSERT (:Person {name: 'Alice'})")?;
528    /// session.rollback()?; // Insert is discarded
529    /// ```
530    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        // Discard uncommitted versions in the store
540        self.store.discard_uncommitted_versions(tx_id);
541
542        // Mark transaction as aborted in the manager
543        self.tx_manager.abort(tx_id)
544    }
545
546    /// Returns whether a transaction is active.
547    #[must_use]
548    pub fn in_transaction(&self) -> bool {
549        self.current_tx.is_some()
550    }
551
552    /// Sets auto-commit mode.
553    pub fn set_auto_commit(&mut self, auto_commit: bool) {
554        self.auto_commit = auto_commit;
555    }
556
557    /// Returns whether auto-commit is enabled.
558    #[must_use]
559    pub fn auto_commit(&self) -> bool {
560        self.auto_commit
561    }
562
563    /// Returns the current transaction context for MVCC visibility.
564    ///
565    /// Returns `(viewing_epoch, tx_id)` where:
566    /// - `viewing_epoch` is the epoch at which to check version visibility
567    /// - `tx_id` is the current transaction ID (if in a transaction)
568    #[must_use]
569    fn get_transaction_context(&self) -> (EpochId, Option<TxId>) {
570        if let Some(tx_id) = self.current_tx {
571            // In a transaction - use the transaction's start epoch
572            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            // No transaction - use current epoch
579            (self.tx_manager.current_epoch(), None)
580        }
581    }
582
583    /// Creates a node directly (bypassing query execution).
584    ///
585    /// This is a low-level API for testing and direct manipulation.
586    /// If a transaction is active, the node will be versioned with the transaction ID.
587    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    /// Creates a node with properties.
594    ///
595    /// If a transaction is active, the node will be versioned with the transaction ID.
596    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    /// Creates an edge between two nodes.
611    ///
612    /// This is a low-level API for testing and direct manipulation.
613    /// If a transaction is active, the edge will be versioned with the transaction ID.
614    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        // Without transaction - context should have current epoch and no tx_id
660        let (_epoch1, tx_id1) = session.get_transaction_context();
661        assert!(tx_id1.is_none());
662
663        // Start a transaction
664        session.begin_tx().unwrap();
665        let (epoch2, tx_id2) = session.get_transaction_context();
666        assert!(tx_id2.is_some());
667        // Transaction should have a valid epoch
668        let _ = epoch2; // Use the variable
669
670        // Commit and verify
671        session.commit().unwrap();
672        let (epoch3, tx_id3) = session.get_transaction_context();
673        assert!(tx_id3.is_none());
674        // Epoch should have advanced after commit
675        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        // Create a node outside of any transaction (at system level)
695        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        // Start a transaction
700        let mut session = db.session();
701        session.begin_tx().unwrap();
702        let tx_id = session.current_tx.unwrap();
703
704        // Create a node versioned with the transaction's ID
705        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        // Should see 2 nodes at this point
710        assert_eq!(db.node_count(), 2, "Should have 2 nodes during transaction");
711
712        // Rollback the transaction
713        session.rollback().unwrap();
714        assert!(!session.in_transaction());
715
716        // The node created in the transaction should be discarded
717        // Only the first node should remain visible
718        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        // The original node should still be accessible
725        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        // The node created in the transaction should not be accessible
734        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        // Test that session.create_node() is transaction-aware
745        let db = GrafeoDB::new_in_memory();
746
747        // Create a node outside of any transaction
748        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        // Start a transaction and create a node through the session
753        let mut session = db.session();
754        session.begin_tx().unwrap();
755
756        // Create a node through session.create_node() - should be versioned with tx
757        let node_in_tx = session.create_node(&["Person"]);
758        assert!(node_in_tx.is_valid());
759
760        // Should see 2 nodes at this point
761        assert_eq!(db.node_count(), 2, "Should have 2 nodes during transaction");
762
763        // Rollback the transaction
764        session.rollback().unwrap();
765
766        // The node created via session.create_node() should be discarded
767        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        // Test that session.create_node_with_props() is transaction-aware
779        let db = GrafeoDB::new_in_memory();
780
781        // Create a node outside of any transaction
782        db.create_node(&["Person"]);
783        assert_eq!(db.node_count(), 1, "Should have 1 node before transaction");
784
785        // Start a transaction and create a node with properties
786        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        // Should see 2 nodes
794        assert_eq!(db.node_count(), 2, "Should have 2 nodes during transaction");
795
796        // Rollback the transaction
797        session.rollback().unwrap();
798
799        // The node should be discarded
800        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            // Create some test data
817            session.create_node(&["Person"]);
818            session.create_node(&["Person"]);
819            session.create_node(&["Animal"]);
820
821            // Execute a GQL query
822            let result = session.execute("MATCH (n:Person) RETURN n").unwrap();
823
824            // Should return 2 Person nodes
825            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            // No data in database
836            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            // Invalid GQL syntax
847            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            // Create a graph: Alice -> Bob, Alice -> Charlie
858            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            // Execute a path query: MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b
866            let result = session
867                .execute("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b")
868                .unwrap();
869
870            // Should return 2 rows (Alice->Bob, Alice->Charlie)
871            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            // Create a graph: Alice -KNOWS-> Bob, Alice -WORKS_WITH-> Charlie
883            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            // Query only KNOWS relationships
891            let result = session
892                .execute("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b")
893                .unwrap();
894
895            // Should return only 1 row (Alice->Bob)
896            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            // Reference undefined variable 'x' in RETURN
905            let result = session.execute("MATCH (n:Person) RETURN x");
906
907            // Should fail with semantic error
908            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            // Create people with ages
928            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            // Query with WHERE clause: age > 30
933            let result = session
934                .execute("MATCH (n:Person) WHERE n.age > 30 RETURN n")
935                .unwrap();
936
937            // Should return 2 people (ages 35 and 45)
938            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            // Create people with names
949            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            // Query with WHERE clause: name = "Alice"
954            let result = session
955                .execute("MATCH (n:Person) WHERE n.name = \"Alice\" RETURN n")
956                .unwrap();
957
958            // Should return 2 people named Alice
959            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            // Create people with names and ages
970            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            // Query returning properties
986            let result = session
987                .execute("MATCH (n:Person) RETURN n.name, n.age")
988                .unwrap();
989
990            // Should return 2 rows with name and age columns
991            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            // Check that we get actual values
997            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            // Create a person
1010            session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
1011
1012            // Query returning both node and property
1013            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            // Second column should be the name
1023            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            // Create some test data
1037            session.create_node(&["Person"]);
1038            session.create_node(&["Person"]);
1039            session.create_node(&["Animal"]);
1040
1041            // Execute a Cypher query
1042            let result = session.execute_cypher("MATCH (n:Person) RETURN n").unwrap();
1043
1044            // Should return 2 Person nodes
1045            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            // No data in database
1056            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            // Invalid Cypher syntax
1067            let result = session.execute_cypher("MATCH (n RETURN n");
1068
1069            assert!(result.is_err());
1070        }
1071    }
1072}