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)).with_tx_id(self.current_tx);
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        // Commit RDF store pending operations
508        #[cfg(feature = "rdf")]
509        self.rdf_store.commit_tx(tx_id);
510
511        self.tx_manager.commit(tx_id).map(|_| ())
512    }
513
514    /// Aborts the current transaction.
515    ///
516    /// Discards all changes since [`begin_tx`](Self::begin_tx).
517    ///
518    /// # Errors
519    ///
520    /// Returns an error if no transaction is active.
521    ///
522    /// # Examples
523    ///
524    /// ```ignore
525    /// use grafeo_engine::GrafeoDB;
526    ///
527    /// let db = GrafeoDB::new_in_memory();
528    /// let mut session = db.session();
529    ///
530    /// session.begin_tx()?;
531    /// session.execute("INSERT (:Person {name: 'Alice'})")?;
532    /// session.rollback()?; // Insert is discarded
533    /// ```
534    pub fn rollback(&mut self) -> Result<()> {
535        let tx_id = self.current_tx.take().ok_or_else(|| {
536            grafeo_common::utils::error::Error::Transaction(
537                grafeo_common::utils::error::TransactionError::InvalidState(
538                    "No active transaction".to_string(),
539                ),
540            )
541        })?;
542
543        // Discard uncommitted versions in the LPG store
544        self.store.discard_uncommitted_versions(tx_id);
545
546        // Discard pending operations in the RDF store
547        #[cfg(feature = "rdf")]
548        self.rdf_store.rollback_tx(tx_id);
549
550        // Mark transaction as aborted in the manager
551        self.tx_manager.abort(tx_id)
552    }
553
554    /// Returns whether a transaction is active.
555    #[must_use]
556    pub fn in_transaction(&self) -> bool {
557        self.current_tx.is_some()
558    }
559
560    /// Sets auto-commit mode.
561    pub fn set_auto_commit(&mut self, auto_commit: bool) {
562        self.auto_commit = auto_commit;
563    }
564
565    /// Returns whether auto-commit is enabled.
566    #[must_use]
567    pub fn auto_commit(&self) -> bool {
568        self.auto_commit
569    }
570
571    /// Returns the current transaction context for MVCC visibility.
572    ///
573    /// Returns `(viewing_epoch, tx_id)` where:
574    /// - `viewing_epoch` is the epoch at which to check version visibility
575    /// - `tx_id` is the current transaction ID (if in a transaction)
576    #[must_use]
577    fn get_transaction_context(&self) -> (EpochId, Option<TxId>) {
578        if let Some(tx_id) = self.current_tx {
579            // In a transaction - use the transaction's start epoch
580            let epoch = self
581                .tx_manager
582                .start_epoch(tx_id)
583                .unwrap_or_else(|| self.tx_manager.current_epoch());
584            (epoch, Some(tx_id))
585        } else {
586            // No transaction - use current epoch
587            (self.tx_manager.current_epoch(), None)
588        }
589    }
590
591    /// Creates a node directly (bypassing query execution).
592    ///
593    /// This is a low-level API for testing and direct manipulation.
594    /// If a transaction is active, the node will be versioned with the transaction ID.
595    pub fn create_node(&self, labels: &[&str]) -> NodeId {
596        let (epoch, tx_id) = self.get_transaction_context();
597        self.store
598            .create_node_versioned(labels, epoch, tx_id.unwrap_or(TxId::SYSTEM))
599    }
600
601    /// Creates a node with properties.
602    ///
603    /// If a transaction is active, the node will be versioned with the transaction ID.
604    pub fn create_node_with_props<'a>(
605        &self,
606        labels: &[&str],
607        properties: impl IntoIterator<Item = (&'a str, Value)>,
608    ) -> NodeId {
609        let (epoch, tx_id) = self.get_transaction_context();
610        self.store.create_node_with_props_versioned(
611            labels,
612            properties.into_iter().map(|(k, v)| (k, v)),
613            epoch,
614            tx_id.unwrap_or(TxId::SYSTEM),
615        )
616    }
617
618    /// Creates an edge between two nodes.
619    ///
620    /// This is a low-level API for testing and direct manipulation.
621    /// If a transaction is active, the edge will be versioned with the transaction ID.
622    pub fn create_edge(
623        &self,
624        src: NodeId,
625        dst: NodeId,
626        edge_type: &str,
627    ) -> grafeo_common::types::EdgeId {
628        let (epoch, tx_id) = self.get_transaction_context();
629        self.store
630            .create_edge_versioned(src, dst, edge_type, epoch, tx_id.unwrap_or(TxId::SYSTEM))
631    }
632}
633
634#[cfg(test)]
635mod tests {
636    use crate::database::GrafeoDB;
637
638    #[test]
639    fn test_session_create_node() {
640        let db = GrafeoDB::new_in_memory();
641        let session = db.session();
642
643        let id = session.create_node(&["Person"]);
644        assert!(id.is_valid());
645        assert_eq!(db.node_count(), 1);
646    }
647
648    #[test]
649    fn test_session_transaction() {
650        let db = GrafeoDB::new_in_memory();
651        let mut session = db.session();
652
653        assert!(!session.in_transaction());
654
655        session.begin_tx().unwrap();
656        assert!(session.in_transaction());
657
658        session.commit().unwrap();
659        assert!(!session.in_transaction());
660    }
661
662    #[test]
663    fn test_session_transaction_context() {
664        let db = GrafeoDB::new_in_memory();
665        let mut session = db.session();
666
667        // Without transaction - context should have current epoch and no tx_id
668        let (_epoch1, tx_id1) = session.get_transaction_context();
669        assert!(tx_id1.is_none());
670
671        // Start a transaction
672        session.begin_tx().unwrap();
673        let (epoch2, tx_id2) = session.get_transaction_context();
674        assert!(tx_id2.is_some());
675        // Transaction should have a valid epoch
676        let _ = epoch2; // Use the variable
677
678        // Commit and verify
679        session.commit().unwrap();
680        let (epoch3, tx_id3) = session.get_transaction_context();
681        assert!(tx_id3.is_none());
682        // Epoch should have advanced after commit
683        assert!(epoch3.as_u64() >= epoch2.as_u64());
684    }
685
686    #[test]
687    fn test_session_rollback() {
688        let db = GrafeoDB::new_in_memory();
689        let mut session = db.session();
690
691        session.begin_tx().unwrap();
692        session.rollback().unwrap();
693        assert!(!session.in_transaction());
694    }
695
696    #[test]
697    fn test_session_rollback_discards_versions() {
698        use grafeo_common::types::TxId;
699
700        let db = GrafeoDB::new_in_memory();
701
702        // Create a node outside of any transaction (at system level)
703        let node_before = db.store().create_node(&["Person"]);
704        assert!(node_before.is_valid());
705        assert_eq!(db.node_count(), 1, "Should have 1 node before transaction");
706
707        // Start a transaction
708        let mut session = db.session();
709        session.begin_tx().unwrap();
710        let tx_id = session.current_tx.unwrap();
711
712        // Create a node versioned with the transaction's ID
713        let epoch = db.store().current_epoch();
714        let node_in_tx = db.store().create_node_versioned(&["Person"], epoch, tx_id);
715        assert!(node_in_tx.is_valid());
716
717        // Should see 2 nodes at this point
718        assert_eq!(db.node_count(), 2, "Should have 2 nodes during transaction");
719
720        // Rollback the transaction
721        session.rollback().unwrap();
722        assert!(!session.in_transaction());
723
724        // The node created in the transaction should be discarded
725        // Only the first node should remain visible
726        let count_after = db.node_count();
727        assert_eq!(
728            count_after, 1,
729            "Rollback should discard uncommitted node, but got {count_after}"
730        );
731
732        // The original node should still be accessible
733        let current_epoch = db.store().current_epoch();
734        assert!(
735            db.store()
736                .get_node_versioned(node_before, current_epoch, TxId::SYSTEM)
737                .is_some(),
738            "Original node should still exist"
739        );
740
741        // The node created in the transaction should not be accessible
742        assert!(
743            db.store()
744                .get_node_versioned(node_in_tx, current_epoch, TxId::SYSTEM)
745                .is_none(),
746            "Transaction node should be gone"
747        );
748    }
749
750    #[test]
751    fn test_session_create_node_in_transaction() {
752        // Test that session.create_node() is transaction-aware
753        let db = GrafeoDB::new_in_memory();
754
755        // Create a node outside of any transaction
756        let node_before = db.create_node(&["Person"]);
757        assert!(node_before.is_valid());
758        assert_eq!(db.node_count(), 1, "Should have 1 node before transaction");
759
760        // Start a transaction and create a node through the session
761        let mut session = db.session();
762        session.begin_tx().unwrap();
763
764        // Create a node through session.create_node() - should be versioned with tx
765        let node_in_tx = session.create_node(&["Person"]);
766        assert!(node_in_tx.is_valid());
767
768        // Should see 2 nodes at this point
769        assert_eq!(db.node_count(), 2, "Should have 2 nodes during transaction");
770
771        // Rollback the transaction
772        session.rollback().unwrap();
773
774        // The node created via session.create_node() should be discarded
775        let count_after = db.node_count();
776        assert_eq!(
777            count_after, 1,
778            "Rollback should discard node created via session.create_node(), but got {count_after}"
779        );
780    }
781
782    #[test]
783    fn test_session_create_node_with_props_in_transaction() {
784        use grafeo_common::types::Value;
785
786        // Test that session.create_node_with_props() is transaction-aware
787        let db = GrafeoDB::new_in_memory();
788
789        // Create a node outside of any transaction
790        db.create_node(&["Person"]);
791        assert_eq!(db.node_count(), 1, "Should have 1 node before transaction");
792
793        // Start a transaction and create a node with properties
794        let mut session = db.session();
795        session.begin_tx().unwrap();
796
797        let node_in_tx =
798            session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
799        assert!(node_in_tx.is_valid());
800
801        // Should see 2 nodes
802        assert_eq!(db.node_count(), 2, "Should have 2 nodes during transaction");
803
804        // Rollback the transaction
805        session.rollback().unwrap();
806
807        // The node should be discarded
808        let count_after = db.node_count();
809        assert_eq!(
810            count_after, 1,
811            "Rollback should discard node created via session.create_node_with_props()"
812        );
813    }
814
815    #[cfg(feature = "gql")]
816    mod gql_tests {
817        use super::*;
818
819        #[test]
820        fn test_gql_query_execution() {
821            let db = GrafeoDB::new_in_memory();
822            let session = db.session();
823
824            // Create some test data
825            session.create_node(&["Person"]);
826            session.create_node(&["Person"]);
827            session.create_node(&["Animal"]);
828
829            // Execute a GQL query
830            let result = session.execute("MATCH (n:Person) RETURN n").unwrap();
831
832            // Should return 2 Person nodes
833            assert_eq!(result.row_count(), 2);
834            assert_eq!(result.column_count(), 1);
835            assert_eq!(result.columns[0], "n");
836        }
837
838        #[test]
839        fn test_gql_empty_result() {
840            let db = GrafeoDB::new_in_memory();
841            let session = db.session();
842
843            // No data in database
844            let result = session.execute("MATCH (n:Person) RETURN n").unwrap();
845
846            assert_eq!(result.row_count(), 0);
847        }
848
849        #[test]
850        fn test_gql_parse_error() {
851            let db = GrafeoDB::new_in_memory();
852            let session = db.session();
853
854            // Invalid GQL syntax
855            let result = session.execute("MATCH (n RETURN n");
856
857            assert!(result.is_err());
858        }
859
860        #[test]
861        fn test_gql_relationship_traversal() {
862            let db = GrafeoDB::new_in_memory();
863            let session = db.session();
864
865            // Create a graph: Alice -> Bob, Alice -> Charlie
866            let alice = session.create_node(&["Person"]);
867            let bob = session.create_node(&["Person"]);
868            let charlie = session.create_node(&["Person"]);
869
870            session.create_edge(alice, bob, "KNOWS");
871            session.create_edge(alice, charlie, "KNOWS");
872
873            // Execute a path query: MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b
874            let result = session
875                .execute("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b")
876                .unwrap();
877
878            // Should return 2 rows (Alice->Bob, Alice->Charlie)
879            assert_eq!(result.row_count(), 2);
880            assert_eq!(result.column_count(), 2);
881            assert_eq!(result.columns[0], "a");
882            assert_eq!(result.columns[1], "b");
883        }
884
885        #[test]
886        fn test_gql_relationship_with_type_filter() {
887            let db = GrafeoDB::new_in_memory();
888            let session = db.session();
889
890            // Create a graph: Alice -KNOWS-> Bob, Alice -WORKS_WITH-> Charlie
891            let alice = session.create_node(&["Person"]);
892            let bob = session.create_node(&["Person"]);
893            let charlie = session.create_node(&["Person"]);
894
895            session.create_edge(alice, bob, "KNOWS");
896            session.create_edge(alice, charlie, "WORKS_WITH");
897
898            // Query only KNOWS relationships
899            let result = session
900                .execute("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b")
901                .unwrap();
902
903            // Should return only 1 row (Alice->Bob)
904            assert_eq!(result.row_count(), 1);
905        }
906
907        #[test]
908        fn test_gql_semantic_error_undefined_variable() {
909            let db = GrafeoDB::new_in_memory();
910            let session = db.session();
911
912            // Reference undefined variable 'x' in RETURN
913            let result = session.execute("MATCH (n:Person) RETURN x");
914
915            // Should fail with semantic error
916            assert!(result.is_err());
917            let err = match result {
918                Err(e) => e,
919                Ok(_) => panic!("Expected error"),
920            };
921            assert!(
922                err.to_string().contains("Undefined variable"),
923                "Expected undefined variable error, got: {}",
924                err
925            );
926        }
927
928        #[test]
929        fn test_gql_where_clause_property_filter() {
930            use grafeo_common::types::Value;
931
932            let db = GrafeoDB::new_in_memory();
933            let session = db.session();
934
935            // Create people with ages
936            session.create_node_with_props(&["Person"], [("age", Value::Int64(25))]);
937            session.create_node_with_props(&["Person"], [("age", Value::Int64(35))]);
938            session.create_node_with_props(&["Person"], [("age", Value::Int64(45))]);
939
940            // Query with WHERE clause: age > 30
941            let result = session
942                .execute("MATCH (n:Person) WHERE n.age > 30 RETURN n")
943                .unwrap();
944
945            // Should return 2 people (ages 35 and 45)
946            assert_eq!(result.row_count(), 2);
947        }
948
949        #[test]
950        fn test_gql_where_clause_equality() {
951            use grafeo_common::types::Value;
952
953            let db = GrafeoDB::new_in_memory();
954            let session = db.session();
955
956            // Create people with names
957            session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
958            session.create_node_with_props(&["Person"], [("name", Value::String("Bob".into()))]);
959            session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
960
961            // Query with WHERE clause: name = "Alice"
962            let result = session
963                .execute("MATCH (n:Person) WHERE n.name = \"Alice\" RETURN n")
964                .unwrap();
965
966            // Should return 2 people named Alice
967            assert_eq!(result.row_count(), 2);
968        }
969
970        #[test]
971        fn test_gql_return_property_access() {
972            use grafeo_common::types::Value;
973
974            let db = GrafeoDB::new_in_memory();
975            let session = db.session();
976
977            // Create people with names and ages
978            session.create_node_with_props(
979                &["Person"],
980                [
981                    ("name", Value::String("Alice".into())),
982                    ("age", Value::Int64(30)),
983                ],
984            );
985            session.create_node_with_props(
986                &["Person"],
987                [
988                    ("name", Value::String("Bob".into())),
989                    ("age", Value::Int64(25)),
990                ],
991            );
992
993            // Query returning properties
994            let result = session
995                .execute("MATCH (n:Person) RETURN n.name, n.age")
996                .unwrap();
997
998            // Should return 2 rows with name and age columns
999            assert_eq!(result.row_count(), 2);
1000            assert_eq!(result.column_count(), 2);
1001            assert_eq!(result.columns[0], "n.name");
1002            assert_eq!(result.columns[1], "n.age");
1003
1004            // Check that we get actual values
1005            let names: Vec<&Value> = result.rows.iter().map(|r| &r[0]).collect();
1006            assert!(names.contains(&&Value::String("Alice".into())));
1007            assert!(names.contains(&&Value::String("Bob".into())));
1008        }
1009
1010        #[test]
1011        fn test_gql_return_mixed_expressions() {
1012            use grafeo_common::types::Value;
1013
1014            let db = GrafeoDB::new_in_memory();
1015            let session = db.session();
1016
1017            // Create a person
1018            session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
1019
1020            // Query returning both node and property
1021            let result = session
1022                .execute("MATCH (n:Person) RETURN n, n.name")
1023                .unwrap();
1024
1025            assert_eq!(result.row_count(), 1);
1026            assert_eq!(result.column_count(), 2);
1027            assert_eq!(result.columns[0], "n");
1028            assert_eq!(result.columns[1], "n.name");
1029
1030            // Second column should be the name
1031            assert_eq!(result.rows[0][1], Value::String("Alice".into()));
1032        }
1033    }
1034
1035    #[cfg(feature = "cypher")]
1036    mod cypher_tests {
1037        use super::*;
1038
1039        #[test]
1040        fn test_cypher_query_execution() {
1041            let db = GrafeoDB::new_in_memory();
1042            let session = db.session();
1043
1044            // Create some test data
1045            session.create_node(&["Person"]);
1046            session.create_node(&["Person"]);
1047            session.create_node(&["Animal"]);
1048
1049            // Execute a Cypher query
1050            let result = session.execute_cypher("MATCH (n:Person) RETURN n").unwrap();
1051
1052            // Should return 2 Person nodes
1053            assert_eq!(result.row_count(), 2);
1054            assert_eq!(result.column_count(), 1);
1055            assert_eq!(result.columns[0], "n");
1056        }
1057
1058        #[test]
1059        fn test_cypher_empty_result() {
1060            let db = GrafeoDB::new_in_memory();
1061            let session = db.session();
1062
1063            // No data in database
1064            let result = session.execute_cypher("MATCH (n:Person) RETURN n").unwrap();
1065
1066            assert_eq!(result.row_count(), 0);
1067        }
1068
1069        #[test]
1070        fn test_cypher_parse_error() {
1071            let db = GrafeoDB::new_in_memory();
1072            let session = db.session();
1073
1074            // Invalid Cypher syntax
1075            let result = session.execute_cypher("MATCH (n RETURN n");
1076
1077            assert!(result.is_err());
1078        }
1079    }
1080}