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::{EdgeId, EpochId, NodeId, TxId, Value};
10use grafeo_common::utils::error::Result;
11use grafeo_core::graph::Direction;
12use grafeo_core::graph::lpg::{Edge, LpgStore, Node};
13#[cfg(feature = "rdf")]
14use grafeo_core::graph::rdf::RdfStore;
15
16use crate::config::AdaptiveConfig;
17use crate::database::QueryResult;
18use crate::query::cache::QueryCache;
19use crate::transaction::TransactionManager;
20
21/// Your handle to the database - execute queries and manage transactions.
22///
23/// Get one from [`GrafeoDB::session()`](crate::GrafeoDB::session). Each session
24/// tracks its own transaction state, so you can have multiple concurrent
25/// sessions without them interfering.
26pub struct Session {
27    /// The underlying store.
28    store: Arc<LpgStore>,
29    /// RDF triple store (if RDF feature is enabled).
30    #[cfg(feature = "rdf")]
31    #[allow(dead_code)]
32    rdf_store: Arc<RdfStore>,
33    /// Transaction manager.
34    tx_manager: Arc<TransactionManager>,
35    /// Query cache shared across sessions.
36    query_cache: Arc<QueryCache>,
37    /// Current transaction ID (if any).
38    current_tx: Option<TxId>,
39    /// Whether the session is in auto-commit mode.
40    auto_commit: bool,
41    /// Adaptive execution configuration.
42    #[allow(dead_code)]
43    adaptive_config: AdaptiveConfig,
44    /// Whether to use factorized execution for multi-hop queries.
45    factorized_execution: bool,
46}
47
48impl Session {
49    /// Creates a new session.
50    #[allow(dead_code)]
51    pub(crate) fn new(
52        store: Arc<LpgStore>,
53        tx_manager: Arc<TransactionManager>,
54        query_cache: Arc<QueryCache>,
55    ) -> Self {
56        Self {
57            store,
58            #[cfg(feature = "rdf")]
59            rdf_store: Arc::new(RdfStore::new()),
60            tx_manager,
61            query_cache,
62            current_tx: None,
63            auto_commit: true,
64            adaptive_config: AdaptiveConfig::default(),
65            factorized_execution: true,
66        }
67    }
68
69    /// Creates a new session with adaptive execution configuration.
70    #[allow(dead_code)]
71    pub(crate) fn with_adaptive(
72        store: Arc<LpgStore>,
73        tx_manager: Arc<TransactionManager>,
74        query_cache: Arc<QueryCache>,
75        adaptive_config: AdaptiveConfig,
76        factorized_execution: bool,
77    ) -> Self {
78        Self {
79            store,
80            #[cfg(feature = "rdf")]
81            rdf_store: Arc::new(RdfStore::new()),
82            tx_manager,
83            query_cache,
84            current_tx: None,
85            auto_commit: true,
86            adaptive_config,
87            factorized_execution,
88        }
89    }
90
91    /// Creates a new session with RDF store and adaptive configuration.
92    #[cfg(feature = "rdf")]
93    pub(crate) fn with_rdf_store_and_adaptive(
94        store: Arc<LpgStore>,
95        rdf_store: Arc<RdfStore>,
96        tx_manager: Arc<TransactionManager>,
97        query_cache: Arc<QueryCache>,
98        adaptive_config: AdaptiveConfig,
99        factorized_execution: bool,
100    ) -> Self {
101        Self {
102            store,
103            rdf_store,
104            tx_manager,
105            query_cache,
106            current_tx: None,
107            auto_commit: true,
108            adaptive_config,
109            factorized_execution,
110        }
111    }
112
113    /// Executes a GQL query.
114    ///
115    /// # Errors
116    ///
117    /// Returns an error if the query fails to parse or execute.
118    ///
119    /// # Examples
120    ///
121    /// ```ignore
122    /// use grafeo_engine::GrafeoDB;
123    ///
124    /// let db = GrafeoDB::new_in_memory();
125    /// let session = db.session();
126    ///
127    /// // Create a node
128    /// session.execute("INSERT (:Person {name: 'Alice', age: 30})")?;
129    ///
130    /// // Query nodes
131    /// let result = session.execute("MATCH (n:Person) RETURN n.name, n.age")?;
132    /// for row in result {
133    ///     println!("{:?}", row);
134    /// }
135    /// ```
136    #[cfg(feature = "gql")]
137    pub fn execute(&self, query: &str) -> Result<QueryResult> {
138        use crate::query::{
139            Executor, Planner, binder::Binder, cache::CacheKey, gql_translator,
140            optimizer::Optimizer, processor::QueryLanguage,
141        };
142
143        // Create cache key for this query
144        let cache_key = CacheKey::new(query, QueryLanguage::Gql);
145
146        // Try to get cached optimized plan
147        let optimized_plan = if let Some(cached_plan) = self.query_cache.get_optimized(&cache_key) {
148            // Cache hit - skip parsing, translation, binding, and optimization
149            cached_plan
150        } else {
151            // Cache miss - run full pipeline
152
153            // Parse and translate the query to a logical plan
154            let logical_plan = gql_translator::translate(query)?;
155
156            // Semantic validation
157            let mut binder = Binder::new();
158            let _binding_context = binder.bind(&logical_plan)?;
159
160            // Optimize the plan
161            let optimizer = Optimizer::new();
162            let plan = optimizer.optimize(logical_plan)?;
163
164            // Cache the optimized plan for future use
165            self.query_cache.put_optimized(cache_key, plan.clone());
166
167            plan
168        };
169
170        // Get transaction context for MVCC visibility
171        let (viewing_epoch, tx_id) = self.get_transaction_context();
172
173        // Convert to physical plan with transaction context
174        // (Physical planning cannot be cached as it depends on transaction state)
175        let planner = Planner::with_context(
176            Arc::clone(&self.store),
177            Arc::clone(&self.tx_manager),
178            tx_id,
179            viewing_epoch,
180        )
181        .with_factorized_execution(self.factorized_execution);
182        let mut physical_plan = planner.plan(&optimized_plan)?;
183
184        // Execute the plan
185        let executor = Executor::with_columns(physical_plan.columns.clone());
186        executor.execute(physical_plan.operator.as_mut())
187    }
188
189    /// Executes a GQL query with parameters.
190    ///
191    /// # Errors
192    ///
193    /// Returns an error if the query fails to parse or execute.
194    #[cfg(feature = "gql")]
195    pub fn execute_with_params(
196        &self,
197        query: &str,
198        params: std::collections::HashMap<String, Value>,
199    ) -> Result<QueryResult> {
200        use crate::query::processor::{QueryLanguage, QueryProcessor};
201
202        // Get transaction context for MVCC visibility
203        let (viewing_epoch, tx_id) = self.get_transaction_context();
204
205        // Create processor with transaction context
206        let processor =
207            QueryProcessor::for_lpg_with_tx(Arc::clone(&self.store), Arc::clone(&self.tx_manager));
208
209        // Apply transaction context if in a transaction
210        let processor = if let Some(tx_id) = tx_id {
211            processor.with_tx_context(viewing_epoch, tx_id)
212        } else {
213            processor
214        };
215
216        processor.process(query, QueryLanguage::Gql, Some(&params))
217    }
218
219    /// Executes a GQL query with parameters.
220    ///
221    /// # Errors
222    ///
223    /// Returns an error if no query language is enabled.
224    #[cfg(not(any(feature = "gql", feature = "cypher")))]
225    pub fn execute_with_params(
226        &self,
227        _query: &str,
228        _params: std::collections::HashMap<String, Value>,
229    ) -> Result<QueryResult> {
230        Err(grafeo_common::utils::error::Error::Internal(
231            "No query language enabled".to_string(),
232        ))
233    }
234
235    /// Executes a GQL query.
236    ///
237    /// # Errors
238    ///
239    /// Returns an error if no query language is enabled.
240    #[cfg(not(any(feature = "gql", feature = "cypher")))]
241    pub fn execute(&self, _query: &str) -> Result<QueryResult> {
242        Err(grafeo_common::utils::error::Error::Internal(
243            "No query language enabled".to_string(),
244        ))
245    }
246
247    /// Executes a Cypher query.
248    ///
249    /// # Errors
250    ///
251    /// Returns an error if the query fails to parse or execute.
252    #[cfg(feature = "cypher")]
253    pub fn execute_cypher(&self, query: &str) -> Result<QueryResult> {
254        use crate::query::{
255            Executor, Planner, binder::Binder, cache::CacheKey, cypher_translator,
256            optimizer::Optimizer, processor::QueryLanguage,
257        };
258
259        // Create cache key for this query
260        let cache_key = CacheKey::new(query, QueryLanguage::Cypher);
261
262        // Try to get cached optimized plan
263        let optimized_plan = if let Some(cached_plan) = self.query_cache.get_optimized(&cache_key) {
264            cached_plan
265        } else {
266            // Parse and translate the query to a logical plan
267            let logical_plan = cypher_translator::translate(query)?;
268
269            // Semantic validation
270            let mut binder = Binder::new();
271            let _binding_context = binder.bind(&logical_plan)?;
272
273            // Optimize the plan
274            let optimizer = Optimizer::new();
275            let plan = optimizer.optimize(logical_plan)?;
276
277            // Cache the optimized plan
278            self.query_cache.put_optimized(cache_key, plan.clone());
279
280            plan
281        };
282
283        // Get transaction context for MVCC visibility
284        let (viewing_epoch, tx_id) = self.get_transaction_context();
285
286        // Convert to physical plan with transaction context
287        let planner = Planner::with_context(
288            Arc::clone(&self.store),
289            Arc::clone(&self.tx_manager),
290            tx_id,
291            viewing_epoch,
292        )
293        .with_factorized_execution(self.factorized_execution);
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.
302    ///
303    /// # Errors
304    ///
305    /// Returns an error if the query fails to parse or execute.
306    ///
307    /// # Examples
308    ///
309    /// ```ignore
310    /// use grafeo_engine::GrafeoDB;
311    ///
312    /// let db = GrafeoDB::new_in_memory();
313    /// let session = db.session();
314    ///
315    /// // Create some nodes first
316    /// session.create_node(&["Person"]);
317    ///
318    /// // Query using Gremlin
319    /// let result = session.execute_gremlin("g.V().hasLabel('Person')")?;
320    /// ```
321    #[cfg(feature = "gremlin")]
322    pub fn execute_gremlin(&self, query: &str) -> Result<QueryResult> {
323        use crate::query::{
324            Executor, Planner, binder::Binder, gremlin_translator, optimizer::Optimizer,
325        };
326
327        // Parse and translate the query to a logical plan
328        let logical_plan = gremlin_translator::translate(query)?;
329
330        // Semantic validation
331        let mut binder = Binder::new();
332        let _binding_context = binder.bind(&logical_plan)?;
333
334        // Optimize the plan
335        let optimizer = Optimizer::new();
336        let optimized_plan = optimizer.optimize(logical_plan)?;
337
338        // Get transaction context for MVCC visibility
339        let (viewing_epoch, tx_id) = self.get_transaction_context();
340
341        // Convert to physical plan with transaction context
342        let planner = Planner::with_context(
343            Arc::clone(&self.store),
344            Arc::clone(&self.tx_manager),
345            tx_id,
346            viewing_epoch,
347        )
348        .with_factorized_execution(self.factorized_execution);
349        let mut physical_plan = planner.plan(&optimized_plan)?;
350
351        // Execute the plan
352        let executor = Executor::with_columns(physical_plan.columns.clone());
353        executor.execute(physical_plan.operator.as_mut())
354    }
355
356    /// Executes a Gremlin query with parameters.
357    ///
358    /// # Errors
359    ///
360    /// Returns an error if the query fails to parse or execute.
361    #[cfg(feature = "gremlin")]
362    pub fn execute_gremlin_with_params(
363        &self,
364        query: &str,
365        params: std::collections::HashMap<String, Value>,
366    ) -> Result<QueryResult> {
367        use crate::query::processor::{QueryLanguage, QueryProcessor};
368
369        // Get transaction context for MVCC visibility
370        let (viewing_epoch, tx_id) = self.get_transaction_context();
371
372        // Create processor with transaction context
373        let processor =
374            QueryProcessor::for_lpg_with_tx(Arc::clone(&self.store), Arc::clone(&self.tx_manager));
375
376        // Apply transaction context if in a transaction
377        let processor = if let Some(tx_id) = tx_id {
378            processor.with_tx_context(viewing_epoch, tx_id)
379        } else {
380            processor
381        };
382
383        processor.process(query, QueryLanguage::Gremlin, Some(&params))
384    }
385
386    /// Executes a GraphQL query against the LPG store.
387    ///
388    /// # Errors
389    ///
390    /// Returns an error if the query fails to parse or execute.
391    ///
392    /// # Examples
393    ///
394    /// ```ignore
395    /// use grafeo_engine::GrafeoDB;
396    ///
397    /// let db = GrafeoDB::new_in_memory();
398    /// let session = db.session();
399    ///
400    /// // Create some nodes first
401    /// session.create_node(&["User"]);
402    ///
403    /// // Query using GraphQL
404    /// let result = session.execute_graphql("query { user { id name } }")?;
405    /// ```
406    #[cfg(feature = "graphql")]
407    pub fn execute_graphql(&self, query: &str) -> Result<QueryResult> {
408        use crate::query::{
409            Executor, Planner, binder::Binder, graphql_translator, optimizer::Optimizer,
410        };
411
412        // Parse and translate the query to a logical plan
413        let logical_plan = graphql_translator::translate(query)?;
414
415        // Semantic validation
416        let mut binder = Binder::new();
417        let _binding_context = binder.bind(&logical_plan)?;
418
419        // Optimize the plan
420        let optimizer = Optimizer::new();
421        let optimized_plan = optimizer.optimize(logical_plan)?;
422
423        // Get transaction context for MVCC visibility
424        let (viewing_epoch, tx_id) = self.get_transaction_context();
425
426        // Convert to physical plan with transaction context
427        let planner = Planner::with_context(
428            Arc::clone(&self.store),
429            Arc::clone(&self.tx_manager),
430            tx_id,
431            viewing_epoch,
432        )
433        .with_factorized_execution(self.factorized_execution);
434        let mut physical_plan = planner.plan(&optimized_plan)?;
435
436        // Execute the plan
437        let executor = Executor::with_columns(physical_plan.columns.clone());
438        executor.execute(physical_plan.operator.as_mut())
439    }
440
441    /// Executes a GraphQL query with parameters.
442    ///
443    /// # Errors
444    ///
445    /// Returns an error if the query fails to parse or execute.
446    #[cfg(feature = "graphql")]
447    pub fn execute_graphql_with_params(
448        &self,
449        query: &str,
450        params: std::collections::HashMap<String, Value>,
451    ) -> Result<QueryResult> {
452        use crate::query::processor::{QueryLanguage, QueryProcessor};
453
454        // Get transaction context for MVCC visibility
455        let (viewing_epoch, tx_id) = self.get_transaction_context();
456
457        // Create processor with transaction context
458        let processor =
459            QueryProcessor::for_lpg_with_tx(Arc::clone(&self.store), Arc::clone(&self.tx_manager));
460
461        // Apply transaction context if in a transaction
462        let processor = if let Some(tx_id) = tx_id {
463            processor.with_tx_context(viewing_epoch, tx_id)
464        } else {
465            processor
466        };
467
468        processor.process(query, QueryLanguage::GraphQL, Some(&params))
469    }
470
471    /// Executes a SPARQL query.
472    ///
473    /// # Errors
474    ///
475    /// Returns an error if the query fails to parse or execute.
476    #[cfg(all(feature = "sparql", feature = "rdf"))]
477    pub fn execute_sparql(&self, query: &str) -> Result<QueryResult> {
478        use crate::query::{
479            Executor, optimizer::Optimizer, planner_rdf::RdfPlanner, sparql_translator,
480        };
481
482        // Parse and translate the SPARQL query to a logical plan
483        let logical_plan = sparql_translator::translate(query)?;
484
485        // Optimize the plan
486        let optimizer = Optimizer::new();
487        let optimized_plan = optimizer.optimize(logical_plan)?;
488
489        // Convert to physical plan using RDF planner
490        let planner = RdfPlanner::new(Arc::clone(&self.rdf_store)).with_tx_id(self.current_tx);
491        let mut physical_plan = planner.plan(&optimized_plan)?;
492
493        // Execute the plan
494        let executor = Executor::with_columns(physical_plan.columns.clone());
495        executor.execute(physical_plan.operator.as_mut())
496    }
497
498    /// Executes a SPARQL query with parameters.
499    ///
500    /// # Errors
501    ///
502    /// Returns an error if the query fails to parse or execute.
503    #[cfg(all(feature = "sparql", feature = "rdf"))]
504    pub fn execute_sparql_with_params(
505        &self,
506        query: &str,
507        _params: std::collections::HashMap<String, Value>,
508    ) -> Result<QueryResult> {
509        // TODO: Implement parameter substitution for SPARQL
510        // For now, just execute the query without parameters
511        self.execute_sparql(query)
512    }
513
514    /// Begins a new transaction.
515    ///
516    /// # Errors
517    ///
518    /// Returns an error if a transaction is already active.
519    ///
520    /// # Examples
521    ///
522    /// ```ignore
523    /// use grafeo_engine::GrafeoDB;
524    ///
525    /// let db = GrafeoDB::new_in_memory();
526    /// let mut session = db.session();
527    ///
528    /// session.begin_tx()?;
529    /// session.execute("INSERT (:Person {name: 'Alice'})")?;
530    /// session.execute("INSERT (:Person {name: 'Bob'})")?;
531    /// session.commit()?; // Both inserts committed atomically
532    /// ```
533    pub fn begin_tx(&mut self) -> Result<()> {
534        if self.current_tx.is_some() {
535            return Err(grafeo_common::utils::error::Error::Transaction(
536                grafeo_common::utils::error::TransactionError::InvalidState(
537                    "Transaction already active".to_string(),
538                ),
539            ));
540        }
541
542        let tx_id = self.tx_manager.begin();
543        self.current_tx = Some(tx_id);
544        Ok(())
545    }
546
547    /// Commits the current transaction.
548    ///
549    /// Makes all changes since [`begin_tx`](Self::begin_tx) permanent.
550    ///
551    /// # Errors
552    ///
553    /// Returns an error if no transaction is active.
554    pub fn commit(&mut self) -> Result<()> {
555        let tx_id = self.current_tx.take().ok_or_else(|| {
556            grafeo_common::utils::error::Error::Transaction(
557                grafeo_common::utils::error::TransactionError::InvalidState(
558                    "No active transaction".to_string(),
559                ),
560            )
561        })?;
562
563        // Commit RDF store pending operations
564        #[cfg(feature = "rdf")]
565        self.rdf_store.commit_tx(tx_id);
566
567        self.tx_manager.commit(tx_id).map(|_| ())
568    }
569
570    /// Aborts the current transaction.
571    ///
572    /// Discards all changes since [`begin_tx`](Self::begin_tx).
573    ///
574    /// # Errors
575    ///
576    /// Returns an error if no transaction is active.
577    ///
578    /// # Examples
579    ///
580    /// ```ignore
581    /// use grafeo_engine::GrafeoDB;
582    ///
583    /// let db = GrafeoDB::new_in_memory();
584    /// let mut session = db.session();
585    ///
586    /// session.begin_tx()?;
587    /// session.execute("INSERT (:Person {name: 'Alice'})")?;
588    /// session.rollback()?; // Insert is discarded
589    /// ```
590    pub fn rollback(&mut self) -> Result<()> {
591        let tx_id = self.current_tx.take().ok_or_else(|| {
592            grafeo_common::utils::error::Error::Transaction(
593                grafeo_common::utils::error::TransactionError::InvalidState(
594                    "No active transaction".to_string(),
595                ),
596            )
597        })?;
598
599        // Discard uncommitted versions in the LPG store
600        self.store.discard_uncommitted_versions(tx_id);
601
602        // Discard pending operations in the RDF store
603        #[cfg(feature = "rdf")]
604        self.rdf_store.rollback_tx(tx_id);
605
606        // Mark transaction as aborted in the manager
607        self.tx_manager.abort(tx_id)
608    }
609
610    /// Returns whether a transaction is active.
611    #[must_use]
612    pub fn in_transaction(&self) -> bool {
613        self.current_tx.is_some()
614    }
615
616    /// Sets auto-commit mode.
617    pub fn set_auto_commit(&mut self, auto_commit: bool) {
618        self.auto_commit = auto_commit;
619    }
620
621    /// Returns whether auto-commit is enabled.
622    #[must_use]
623    pub fn auto_commit(&self) -> bool {
624        self.auto_commit
625    }
626
627    /// Returns the current transaction context for MVCC visibility.
628    ///
629    /// Returns `(viewing_epoch, tx_id)` where:
630    /// - `viewing_epoch` is the epoch at which to check version visibility
631    /// - `tx_id` is the current transaction ID (if in a transaction)
632    #[must_use]
633    fn get_transaction_context(&self) -> (EpochId, Option<TxId>) {
634        if let Some(tx_id) = self.current_tx {
635            // In a transaction - use the transaction's start epoch
636            let epoch = self
637                .tx_manager
638                .start_epoch(tx_id)
639                .unwrap_or_else(|| self.tx_manager.current_epoch());
640            (epoch, Some(tx_id))
641        } else {
642            // No transaction - use current epoch
643            (self.tx_manager.current_epoch(), None)
644        }
645    }
646
647    /// Creates a node directly (bypassing query execution).
648    ///
649    /// This is a low-level API for testing and direct manipulation.
650    /// If a transaction is active, the node will be versioned with the transaction ID.
651    pub fn create_node(&self, labels: &[&str]) -> NodeId {
652        let (epoch, tx_id) = self.get_transaction_context();
653        self.store
654            .create_node_versioned(labels, epoch, tx_id.unwrap_or(TxId::SYSTEM))
655    }
656
657    /// Creates a node with properties.
658    ///
659    /// If a transaction is active, the node will be versioned with the transaction ID.
660    pub fn create_node_with_props<'a>(
661        &self,
662        labels: &[&str],
663        properties: impl IntoIterator<Item = (&'a str, Value)>,
664    ) -> NodeId {
665        let (epoch, tx_id) = self.get_transaction_context();
666        self.store.create_node_with_props_versioned(
667            labels,
668            properties.into_iter().map(|(k, v)| (k, v)),
669            epoch,
670            tx_id.unwrap_or(TxId::SYSTEM),
671        )
672    }
673
674    /// Creates an edge between two nodes.
675    ///
676    /// This is a low-level API for testing and direct manipulation.
677    /// If a transaction is active, the edge will be versioned with the transaction ID.
678    pub fn create_edge(
679        &self,
680        src: NodeId,
681        dst: NodeId,
682        edge_type: &str,
683    ) -> grafeo_common::types::EdgeId {
684        let (epoch, tx_id) = self.get_transaction_context();
685        self.store
686            .create_edge_versioned(src, dst, edge_type, epoch, tx_id.unwrap_or(TxId::SYSTEM))
687    }
688
689    // =========================================================================
690    // Direct Lookup APIs (bypass query planning for O(1) point reads)
691    // =========================================================================
692
693    /// Gets a node by ID directly, bypassing query planning.
694    ///
695    /// This is the fastest way to retrieve a single node when you know its ID.
696    /// Skips parsing, binding, optimization, and physical planning entirely.
697    ///
698    /// # Performance
699    ///
700    /// - Time complexity: O(1) average case
701    /// - No lock contention (uses DashMap internally)
702    /// - ~20-30x faster than equivalent MATCH query
703    ///
704    /// # Example
705    ///
706    /// ```ignore
707    /// let session = db.session();
708    /// let node_id = session.create_node(&["Person"]);
709    ///
710    /// // Direct lookup - O(1), no query planning
711    /// let node = session.get_node(node_id);
712    /// assert!(node.is_some());
713    /// ```
714    #[must_use]
715    pub fn get_node(&self, id: NodeId) -> Option<Node> {
716        let (epoch, tx_id) = self.get_transaction_context();
717        self.store
718            .get_node_versioned(id, epoch, tx_id.unwrap_or(TxId::SYSTEM))
719    }
720
721    /// Gets a single property from a node by ID, bypassing query planning.
722    ///
723    /// More efficient than `get_node()` when you only need one property,
724    /// as it avoids loading the full node with all properties.
725    ///
726    /// # Performance
727    ///
728    /// - Time complexity: O(1) average case
729    /// - No query planning overhead
730    ///
731    /// # Example
732    ///
733    /// ```ignore
734    /// let session = db.session();
735    /// let id = session.create_node_with_props(&["Person"], [("name", "Alice".into())]);
736    ///
737    /// // Direct property access - O(1)
738    /// let name = session.get_node_property(id, "name");
739    /// assert_eq!(name, Some(Value::String("Alice".into())));
740    /// ```
741    #[must_use]
742    pub fn get_node_property(&self, id: NodeId, key: &str) -> Option<Value> {
743        self.get_node(id)
744            .and_then(|node| node.get_property(key).cloned())
745    }
746
747    /// Gets an edge by ID directly, bypassing query planning.
748    ///
749    /// # Performance
750    ///
751    /// - Time complexity: O(1) average case
752    /// - No lock contention
753    #[must_use]
754    pub fn get_edge(&self, id: EdgeId) -> Option<Edge> {
755        let (epoch, tx_id) = self.get_transaction_context();
756        self.store
757            .get_edge_versioned(id, epoch, tx_id.unwrap_or(TxId::SYSTEM))
758    }
759
760    /// Gets outgoing neighbors of a node directly, bypassing query planning.
761    ///
762    /// Returns (neighbor_id, edge_id) pairs for all outgoing edges.
763    ///
764    /// # Performance
765    ///
766    /// - Time complexity: O(degree) where degree is the number of outgoing edges
767    /// - Uses adjacency index for direct access
768    /// - ~10-20x faster than equivalent MATCH query
769    ///
770    /// # Example
771    ///
772    /// ```ignore
773    /// let session = db.session();
774    /// let alice = session.create_node(&["Person"]);
775    /// let bob = session.create_node(&["Person"]);
776    /// session.create_edge(alice, bob, "KNOWS");
777    ///
778    /// // Direct neighbor lookup - O(degree)
779    /// let neighbors = session.get_neighbors_outgoing(alice);
780    /// assert_eq!(neighbors.len(), 1);
781    /// assert_eq!(neighbors[0].0, bob);
782    /// ```
783    #[must_use]
784    pub fn get_neighbors_outgoing(&self, node: NodeId) -> Vec<(NodeId, EdgeId)> {
785        self.store.edges_from(node, Direction::Outgoing).collect()
786    }
787
788    /// Gets incoming neighbors of a node directly, bypassing query planning.
789    ///
790    /// Returns (neighbor_id, edge_id) pairs for all incoming edges.
791    ///
792    /// # Performance
793    ///
794    /// - Time complexity: O(degree) where degree is the number of incoming edges
795    /// - Uses backward adjacency index for direct access
796    #[must_use]
797    pub fn get_neighbors_incoming(&self, node: NodeId) -> Vec<(NodeId, EdgeId)> {
798        self.store.edges_from(node, Direction::Incoming).collect()
799    }
800
801    /// Gets outgoing neighbors filtered by edge type, bypassing query planning.
802    ///
803    /// # Example
804    ///
805    /// ```ignore
806    /// let neighbors = session.get_neighbors_outgoing_by_type(alice, "KNOWS");
807    /// ```
808    #[must_use]
809    pub fn get_neighbors_outgoing_by_type(
810        &self,
811        node: NodeId,
812        edge_type: &str,
813    ) -> Vec<(NodeId, EdgeId)> {
814        self.store
815            .edges_from(node, Direction::Outgoing)
816            .filter(|(_, edge_id)| {
817                self.get_edge(*edge_id)
818                    .is_some_and(|e| e.edge_type.as_ref() == edge_type)
819            })
820            .collect()
821    }
822
823    /// Checks if a node exists, bypassing query planning.
824    ///
825    /// # Performance
826    ///
827    /// - Time complexity: O(1)
828    /// - Fastest existence check available
829    #[must_use]
830    pub fn node_exists(&self, id: NodeId) -> bool {
831        self.get_node(id).is_some()
832    }
833
834    /// Checks if an edge exists, bypassing query planning.
835    #[must_use]
836    pub fn edge_exists(&self, id: EdgeId) -> bool {
837        self.get_edge(id).is_some()
838    }
839
840    /// Gets the degree (number of edges) of a node.
841    ///
842    /// Returns (outgoing_degree, incoming_degree).
843    #[must_use]
844    pub fn get_degree(&self, node: NodeId) -> (usize, usize) {
845        let out = self.store.out_degree(node);
846        let in_degree = self.store.in_degree(node);
847        (out, in_degree)
848    }
849
850    /// Batch lookup of multiple nodes by ID.
851    ///
852    /// More efficient than calling `get_node()` in a loop because it
853    /// amortizes overhead.
854    ///
855    /// # Performance
856    ///
857    /// - Time complexity: O(n) where n is the number of IDs
858    /// - Better cache utilization than individual lookups
859    #[must_use]
860    pub fn get_nodes_batch(&self, ids: &[NodeId]) -> Vec<Option<Node>> {
861        let (epoch, tx_id) = self.get_transaction_context();
862        let tx = tx_id.unwrap_or(TxId::SYSTEM);
863        ids.iter()
864            .map(|&id| self.store.get_node_versioned(id, epoch, tx))
865            .collect()
866    }
867}
868
869#[cfg(test)]
870mod tests {
871    use crate::database::GrafeoDB;
872
873    #[test]
874    fn test_session_create_node() {
875        let db = GrafeoDB::new_in_memory();
876        let session = db.session();
877
878        let id = session.create_node(&["Person"]);
879        assert!(id.is_valid());
880        assert_eq!(db.node_count(), 1);
881    }
882
883    #[test]
884    fn test_session_transaction() {
885        let db = GrafeoDB::new_in_memory();
886        let mut session = db.session();
887
888        assert!(!session.in_transaction());
889
890        session.begin_tx().unwrap();
891        assert!(session.in_transaction());
892
893        session.commit().unwrap();
894        assert!(!session.in_transaction());
895    }
896
897    #[test]
898    fn test_session_transaction_context() {
899        let db = GrafeoDB::new_in_memory();
900        let mut session = db.session();
901
902        // Without transaction - context should have current epoch and no tx_id
903        let (_epoch1, tx_id1) = session.get_transaction_context();
904        assert!(tx_id1.is_none());
905
906        // Start a transaction
907        session.begin_tx().unwrap();
908        let (epoch2, tx_id2) = session.get_transaction_context();
909        assert!(tx_id2.is_some());
910        // Transaction should have a valid epoch
911        let _ = epoch2; // Use the variable
912
913        // Commit and verify
914        session.commit().unwrap();
915        let (epoch3, tx_id3) = session.get_transaction_context();
916        assert!(tx_id3.is_none());
917        // Epoch should have advanced after commit
918        assert!(epoch3.as_u64() >= epoch2.as_u64());
919    }
920
921    #[test]
922    fn test_session_rollback() {
923        let db = GrafeoDB::new_in_memory();
924        let mut session = db.session();
925
926        session.begin_tx().unwrap();
927        session.rollback().unwrap();
928        assert!(!session.in_transaction());
929    }
930
931    #[test]
932    fn test_session_rollback_discards_versions() {
933        use grafeo_common::types::TxId;
934
935        let db = GrafeoDB::new_in_memory();
936
937        // Create a node outside of any transaction (at system level)
938        let node_before = db.store().create_node(&["Person"]);
939        assert!(node_before.is_valid());
940        assert_eq!(db.node_count(), 1, "Should have 1 node before transaction");
941
942        // Start a transaction
943        let mut session = db.session();
944        session.begin_tx().unwrap();
945        let tx_id = session.current_tx.unwrap();
946
947        // Create a node versioned with the transaction's ID
948        let epoch = db.store().current_epoch();
949        let node_in_tx = db.store().create_node_versioned(&["Person"], epoch, tx_id);
950        assert!(node_in_tx.is_valid());
951
952        // Should see 2 nodes at this point
953        assert_eq!(db.node_count(), 2, "Should have 2 nodes during transaction");
954
955        // Rollback the transaction
956        session.rollback().unwrap();
957        assert!(!session.in_transaction());
958
959        // The node created in the transaction should be discarded
960        // Only the first node should remain visible
961        let count_after = db.node_count();
962        assert_eq!(
963            count_after, 1,
964            "Rollback should discard uncommitted node, but got {count_after}"
965        );
966
967        // The original node should still be accessible
968        let current_epoch = db.store().current_epoch();
969        assert!(
970            db.store()
971                .get_node_versioned(node_before, current_epoch, TxId::SYSTEM)
972                .is_some(),
973            "Original node should still exist"
974        );
975
976        // The node created in the transaction should not be accessible
977        assert!(
978            db.store()
979                .get_node_versioned(node_in_tx, current_epoch, TxId::SYSTEM)
980                .is_none(),
981            "Transaction node should be gone"
982        );
983    }
984
985    #[test]
986    fn test_session_create_node_in_transaction() {
987        // Test that session.create_node() is transaction-aware
988        let db = GrafeoDB::new_in_memory();
989
990        // Create a node outside of any transaction
991        let node_before = db.create_node(&["Person"]);
992        assert!(node_before.is_valid());
993        assert_eq!(db.node_count(), 1, "Should have 1 node before transaction");
994
995        // Start a transaction and create a node through the session
996        let mut session = db.session();
997        session.begin_tx().unwrap();
998
999        // Create a node through session.create_node() - should be versioned with tx
1000        let node_in_tx = session.create_node(&["Person"]);
1001        assert!(node_in_tx.is_valid());
1002
1003        // Should see 2 nodes at this point
1004        assert_eq!(db.node_count(), 2, "Should have 2 nodes during transaction");
1005
1006        // Rollback the transaction
1007        session.rollback().unwrap();
1008
1009        // The node created via session.create_node() should be discarded
1010        let count_after = db.node_count();
1011        assert_eq!(
1012            count_after, 1,
1013            "Rollback should discard node created via session.create_node(), but got {count_after}"
1014        );
1015    }
1016
1017    #[test]
1018    fn test_session_create_node_with_props_in_transaction() {
1019        use grafeo_common::types::Value;
1020
1021        // Test that session.create_node_with_props() is transaction-aware
1022        let db = GrafeoDB::new_in_memory();
1023
1024        // Create a node outside of any transaction
1025        db.create_node(&["Person"]);
1026        assert_eq!(db.node_count(), 1, "Should have 1 node before transaction");
1027
1028        // Start a transaction and create a node with properties
1029        let mut session = db.session();
1030        session.begin_tx().unwrap();
1031
1032        let node_in_tx =
1033            session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
1034        assert!(node_in_tx.is_valid());
1035
1036        // Should see 2 nodes
1037        assert_eq!(db.node_count(), 2, "Should have 2 nodes during transaction");
1038
1039        // Rollback the transaction
1040        session.rollback().unwrap();
1041
1042        // The node should be discarded
1043        let count_after = db.node_count();
1044        assert_eq!(
1045            count_after, 1,
1046            "Rollback should discard node created via session.create_node_with_props()"
1047        );
1048    }
1049
1050    #[cfg(feature = "gql")]
1051    mod gql_tests {
1052        use super::*;
1053
1054        #[test]
1055        fn test_gql_query_execution() {
1056            let db = GrafeoDB::new_in_memory();
1057            let session = db.session();
1058
1059            // Create some test data
1060            session.create_node(&["Person"]);
1061            session.create_node(&["Person"]);
1062            session.create_node(&["Animal"]);
1063
1064            // Execute a GQL query
1065            let result = session.execute("MATCH (n:Person) RETURN n").unwrap();
1066
1067            // Should return 2 Person nodes
1068            assert_eq!(result.row_count(), 2);
1069            assert_eq!(result.column_count(), 1);
1070            assert_eq!(result.columns[0], "n");
1071        }
1072
1073        #[test]
1074        fn test_gql_empty_result() {
1075            let db = GrafeoDB::new_in_memory();
1076            let session = db.session();
1077
1078            // No data in database
1079            let result = session.execute("MATCH (n:Person) RETURN n").unwrap();
1080
1081            assert_eq!(result.row_count(), 0);
1082        }
1083
1084        #[test]
1085        fn test_gql_parse_error() {
1086            let db = GrafeoDB::new_in_memory();
1087            let session = db.session();
1088
1089            // Invalid GQL syntax
1090            let result = session.execute("MATCH (n RETURN n");
1091
1092            assert!(result.is_err());
1093        }
1094
1095        #[test]
1096        fn test_gql_relationship_traversal() {
1097            let db = GrafeoDB::new_in_memory();
1098            let session = db.session();
1099
1100            // Create a graph: Alice -> Bob, Alice -> Charlie
1101            let alice = session.create_node(&["Person"]);
1102            let bob = session.create_node(&["Person"]);
1103            let charlie = session.create_node(&["Person"]);
1104
1105            session.create_edge(alice, bob, "KNOWS");
1106            session.create_edge(alice, charlie, "KNOWS");
1107
1108            // Execute a path query: MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b
1109            let result = session
1110                .execute("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b")
1111                .unwrap();
1112
1113            // Should return 2 rows (Alice->Bob, Alice->Charlie)
1114            assert_eq!(result.row_count(), 2);
1115            assert_eq!(result.column_count(), 2);
1116            assert_eq!(result.columns[0], "a");
1117            assert_eq!(result.columns[1], "b");
1118        }
1119
1120        #[test]
1121        fn test_gql_relationship_with_type_filter() {
1122            let db = GrafeoDB::new_in_memory();
1123            let session = db.session();
1124
1125            // Create a graph: Alice -KNOWS-> Bob, Alice -WORKS_WITH-> Charlie
1126            let alice = session.create_node(&["Person"]);
1127            let bob = session.create_node(&["Person"]);
1128            let charlie = session.create_node(&["Person"]);
1129
1130            session.create_edge(alice, bob, "KNOWS");
1131            session.create_edge(alice, charlie, "WORKS_WITH");
1132
1133            // Query only KNOWS relationships
1134            let result = session
1135                .execute("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b")
1136                .unwrap();
1137
1138            // Should return only 1 row (Alice->Bob)
1139            assert_eq!(result.row_count(), 1);
1140        }
1141
1142        #[test]
1143        fn test_gql_semantic_error_undefined_variable() {
1144            let db = GrafeoDB::new_in_memory();
1145            let session = db.session();
1146
1147            // Reference undefined variable 'x' in RETURN
1148            let result = session.execute("MATCH (n:Person) RETURN x");
1149
1150            // Should fail with semantic error
1151            assert!(result.is_err());
1152            let err = match result {
1153                Err(e) => e,
1154                Ok(_) => panic!("Expected error"),
1155            };
1156            assert!(
1157                err.to_string().contains("Undefined variable"),
1158                "Expected undefined variable error, got: {}",
1159                err
1160            );
1161        }
1162
1163        #[test]
1164        fn test_gql_where_clause_property_filter() {
1165            use grafeo_common::types::Value;
1166
1167            let db = GrafeoDB::new_in_memory();
1168            let session = db.session();
1169
1170            // Create people with ages
1171            session.create_node_with_props(&["Person"], [("age", Value::Int64(25))]);
1172            session.create_node_with_props(&["Person"], [("age", Value::Int64(35))]);
1173            session.create_node_with_props(&["Person"], [("age", Value::Int64(45))]);
1174
1175            // Query with WHERE clause: age > 30
1176            let result = session
1177                .execute("MATCH (n:Person) WHERE n.age > 30 RETURN n")
1178                .unwrap();
1179
1180            // Should return 2 people (ages 35 and 45)
1181            assert_eq!(result.row_count(), 2);
1182        }
1183
1184        #[test]
1185        fn test_gql_where_clause_equality() {
1186            use grafeo_common::types::Value;
1187
1188            let db = GrafeoDB::new_in_memory();
1189            let session = db.session();
1190
1191            // Create people with names
1192            session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
1193            session.create_node_with_props(&["Person"], [("name", Value::String("Bob".into()))]);
1194            session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
1195
1196            // Query with WHERE clause: name = "Alice"
1197            let result = session
1198                .execute("MATCH (n:Person) WHERE n.name = \"Alice\" RETURN n")
1199                .unwrap();
1200
1201            // Should return 2 people named Alice
1202            assert_eq!(result.row_count(), 2);
1203        }
1204
1205        #[test]
1206        fn test_gql_return_property_access() {
1207            use grafeo_common::types::Value;
1208
1209            let db = GrafeoDB::new_in_memory();
1210            let session = db.session();
1211
1212            // Create people with names and ages
1213            session.create_node_with_props(
1214                &["Person"],
1215                [
1216                    ("name", Value::String("Alice".into())),
1217                    ("age", Value::Int64(30)),
1218                ],
1219            );
1220            session.create_node_with_props(
1221                &["Person"],
1222                [
1223                    ("name", Value::String("Bob".into())),
1224                    ("age", Value::Int64(25)),
1225                ],
1226            );
1227
1228            // Query returning properties
1229            let result = session
1230                .execute("MATCH (n:Person) RETURN n.name, n.age")
1231                .unwrap();
1232
1233            // Should return 2 rows with name and age columns
1234            assert_eq!(result.row_count(), 2);
1235            assert_eq!(result.column_count(), 2);
1236            assert_eq!(result.columns[0], "n.name");
1237            assert_eq!(result.columns[1], "n.age");
1238
1239            // Check that we get actual values
1240            let names: Vec<&Value> = result.rows.iter().map(|r| &r[0]).collect();
1241            assert!(names.contains(&&Value::String("Alice".into())));
1242            assert!(names.contains(&&Value::String("Bob".into())));
1243        }
1244
1245        #[test]
1246        fn test_gql_return_mixed_expressions() {
1247            use grafeo_common::types::Value;
1248
1249            let db = GrafeoDB::new_in_memory();
1250            let session = db.session();
1251
1252            // Create a person
1253            session.create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
1254
1255            // Query returning both node and property
1256            let result = session
1257                .execute("MATCH (n:Person) RETURN n, n.name")
1258                .unwrap();
1259
1260            assert_eq!(result.row_count(), 1);
1261            assert_eq!(result.column_count(), 2);
1262            assert_eq!(result.columns[0], "n");
1263            assert_eq!(result.columns[1], "n.name");
1264
1265            // Second column should be the name
1266            assert_eq!(result.rows[0][1], Value::String("Alice".into()));
1267        }
1268    }
1269
1270    #[cfg(feature = "cypher")]
1271    mod cypher_tests {
1272        use super::*;
1273
1274        #[test]
1275        fn test_cypher_query_execution() {
1276            let db = GrafeoDB::new_in_memory();
1277            let session = db.session();
1278
1279            // Create some test data
1280            session.create_node(&["Person"]);
1281            session.create_node(&["Person"]);
1282            session.create_node(&["Animal"]);
1283
1284            // Execute a Cypher query
1285            let result = session.execute_cypher("MATCH (n:Person) RETURN n").unwrap();
1286
1287            // Should return 2 Person nodes
1288            assert_eq!(result.row_count(), 2);
1289            assert_eq!(result.column_count(), 1);
1290            assert_eq!(result.columns[0], "n");
1291        }
1292
1293        #[test]
1294        fn test_cypher_empty_result() {
1295            let db = GrafeoDB::new_in_memory();
1296            let session = db.session();
1297
1298            // No data in database
1299            let result = session.execute_cypher("MATCH (n:Person) RETURN n").unwrap();
1300
1301            assert_eq!(result.row_count(), 0);
1302        }
1303
1304        #[test]
1305        fn test_cypher_parse_error() {
1306            let db = GrafeoDB::new_in_memory();
1307            let session = db.session();
1308
1309            // Invalid Cypher syntax
1310            let result = session.execute_cypher("MATCH (n RETURN n");
1311
1312            assert!(result.is_err());
1313        }
1314    }
1315
1316    // ==================== Direct Lookup API Tests ====================
1317
1318    mod direct_lookup_tests {
1319        use super::*;
1320        use grafeo_common::types::Value;
1321
1322        #[test]
1323        fn test_get_node() {
1324            let db = GrafeoDB::new_in_memory();
1325            let session = db.session();
1326
1327            let id = session.create_node(&["Person"]);
1328            let node = session.get_node(id);
1329
1330            assert!(node.is_some());
1331            let node = node.unwrap();
1332            assert_eq!(node.id, id);
1333        }
1334
1335        #[test]
1336        fn test_get_node_not_found() {
1337            use grafeo_common::types::NodeId;
1338
1339            let db = GrafeoDB::new_in_memory();
1340            let session = db.session();
1341
1342            // Try to get a non-existent node
1343            let node = session.get_node(NodeId::new(9999));
1344            assert!(node.is_none());
1345        }
1346
1347        #[test]
1348        fn test_get_node_property() {
1349            let db = GrafeoDB::new_in_memory();
1350            let session = db.session();
1351
1352            let id = session
1353                .create_node_with_props(&["Person"], [("name", Value::String("Alice".into()))]);
1354
1355            let name = session.get_node_property(id, "name");
1356            assert_eq!(name, Some(Value::String("Alice".into())));
1357
1358            // Non-existent property
1359            let missing = session.get_node_property(id, "missing");
1360            assert!(missing.is_none());
1361        }
1362
1363        #[test]
1364        fn test_get_edge() {
1365            let db = GrafeoDB::new_in_memory();
1366            let session = db.session();
1367
1368            let alice = session.create_node(&["Person"]);
1369            let bob = session.create_node(&["Person"]);
1370            let edge_id = session.create_edge(alice, bob, "KNOWS");
1371
1372            let edge = session.get_edge(edge_id);
1373            assert!(edge.is_some());
1374            let edge = edge.unwrap();
1375            assert_eq!(edge.id, edge_id);
1376            assert_eq!(edge.src, alice);
1377            assert_eq!(edge.dst, bob);
1378        }
1379
1380        #[test]
1381        fn test_get_edge_not_found() {
1382            use grafeo_common::types::EdgeId;
1383
1384            let db = GrafeoDB::new_in_memory();
1385            let session = db.session();
1386
1387            let edge = session.get_edge(EdgeId::new(9999));
1388            assert!(edge.is_none());
1389        }
1390
1391        #[test]
1392        fn test_get_neighbors_outgoing() {
1393            let db = GrafeoDB::new_in_memory();
1394            let session = db.session();
1395
1396            let alice = session.create_node(&["Person"]);
1397            let bob = session.create_node(&["Person"]);
1398            let carol = session.create_node(&["Person"]);
1399
1400            session.create_edge(alice, bob, "KNOWS");
1401            session.create_edge(alice, carol, "KNOWS");
1402
1403            let neighbors = session.get_neighbors_outgoing(alice);
1404            assert_eq!(neighbors.len(), 2);
1405
1406            let neighbor_ids: Vec<_> = neighbors.iter().map(|(node_id, _)| *node_id).collect();
1407            assert!(neighbor_ids.contains(&bob));
1408            assert!(neighbor_ids.contains(&carol));
1409        }
1410
1411        #[test]
1412        fn test_get_neighbors_incoming() {
1413            let db = GrafeoDB::new_in_memory();
1414            let session = db.session();
1415
1416            let alice = session.create_node(&["Person"]);
1417            let bob = session.create_node(&["Person"]);
1418            let carol = session.create_node(&["Person"]);
1419
1420            session.create_edge(bob, alice, "KNOWS");
1421            session.create_edge(carol, alice, "KNOWS");
1422
1423            let neighbors = session.get_neighbors_incoming(alice);
1424            assert_eq!(neighbors.len(), 2);
1425
1426            let neighbor_ids: Vec<_> = neighbors.iter().map(|(node_id, _)| *node_id).collect();
1427            assert!(neighbor_ids.contains(&bob));
1428            assert!(neighbor_ids.contains(&carol));
1429        }
1430
1431        #[test]
1432        fn test_get_neighbors_outgoing_by_type() {
1433            let db = GrafeoDB::new_in_memory();
1434            let session = db.session();
1435
1436            let alice = session.create_node(&["Person"]);
1437            let bob = session.create_node(&["Person"]);
1438            let company = session.create_node(&["Company"]);
1439
1440            session.create_edge(alice, bob, "KNOWS");
1441            session.create_edge(alice, company, "WORKS_AT");
1442
1443            let knows_neighbors = session.get_neighbors_outgoing_by_type(alice, "KNOWS");
1444            assert_eq!(knows_neighbors.len(), 1);
1445            assert_eq!(knows_neighbors[0].0, bob);
1446
1447            let works_neighbors = session.get_neighbors_outgoing_by_type(alice, "WORKS_AT");
1448            assert_eq!(works_neighbors.len(), 1);
1449            assert_eq!(works_neighbors[0].0, company);
1450
1451            // No edges of this type
1452            let no_neighbors = session.get_neighbors_outgoing_by_type(alice, "LIKES");
1453            assert!(no_neighbors.is_empty());
1454        }
1455
1456        #[test]
1457        fn test_node_exists() {
1458            use grafeo_common::types::NodeId;
1459
1460            let db = GrafeoDB::new_in_memory();
1461            let session = db.session();
1462
1463            let id = session.create_node(&["Person"]);
1464
1465            assert!(session.node_exists(id));
1466            assert!(!session.node_exists(NodeId::new(9999)));
1467        }
1468
1469        #[test]
1470        fn test_edge_exists() {
1471            use grafeo_common::types::EdgeId;
1472
1473            let db = GrafeoDB::new_in_memory();
1474            let session = db.session();
1475
1476            let alice = session.create_node(&["Person"]);
1477            let bob = session.create_node(&["Person"]);
1478            let edge_id = session.create_edge(alice, bob, "KNOWS");
1479
1480            assert!(session.edge_exists(edge_id));
1481            assert!(!session.edge_exists(EdgeId::new(9999)));
1482        }
1483
1484        #[test]
1485        fn test_get_degree() {
1486            let db = GrafeoDB::new_in_memory();
1487            let session = db.session();
1488
1489            let alice = session.create_node(&["Person"]);
1490            let bob = session.create_node(&["Person"]);
1491            let carol = session.create_node(&["Person"]);
1492
1493            // Alice knows Bob and Carol (2 outgoing)
1494            session.create_edge(alice, bob, "KNOWS");
1495            session.create_edge(alice, carol, "KNOWS");
1496            // Bob knows Alice (1 incoming for Alice)
1497            session.create_edge(bob, alice, "KNOWS");
1498
1499            let (out_degree, in_degree) = session.get_degree(alice);
1500            assert_eq!(out_degree, 2);
1501            assert_eq!(in_degree, 1);
1502
1503            // Node with no edges
1504            let lonely = session.create_node(&["Person"]);
1505            let (out, in_deg) = session.get_degree(lonely);
1506            assert_eq!(out, 0);
1507            assert_eq!(in_deg, 0);
1508        }
1509
1510        #[test]
1511        fn test_get_nodes_batch() {
1512            let db = GrafeoDB::new_in_memory();
1513            let session = db.session();
1514
1515            let alice = session.create_node(&["Person"]);
1516            let bob = session.create_node(&["Person"]);
1517            let carol = session.create_node(&["Person"]);
1518
1519            let nodes = session.get_nodes_batch(&[alice, bob, carol]);
1520            assert_eq!(nodes.len(), 3);
1521            assert!(nodes[0].is_some());
1522            assert!(nodes[1].is_some());
1523            assert!(nodes[2].is_some());
1524
1525            // With non-existent node
1526            use grafeo_common::types::NodeId;
1527            let nodes_with_missing = session.get_nodes_batch(&[alice, NodeId::new(9999), carol]);
1528            assert_eq!(nodes_with_missing.len(), 3);
1529            assert!(nodes_with_missing[0].is_some());
1530            assert!(nodes_with_missing[1].is_none()); // Missing node
1531            assert!(nodes_with_missing[2].is_some());
1532        }
1533
1534        #[test]
1535        fn test_auto_commit_setting() {
1536            let db = GrafeoDB::new_in_memory();
1537            let mut session = db.session();
1538
1539            // Default is auto-commit enabled
1540            assert!(session.auto_commit());
1541
1542            session.set_auto_commit(false);
1543            assert!(!session.auto_commit());
1544
1545            session.set_auto_commit(true);
1546            assert!(session.auto_commit());
1547        }
1548
1549        #[test]
1550        fn test_transaction_double_begin_error() {
1551            let db = GrafeoDB::new_in_memory();
1552            let mut session = db.session();
1553
1554            session.begin_tx().unwrap();
1555            let result = session.begin_tx();
1556
1557            assert!(result.is_err());
1558            // Clean up
1559            session.rollback().unwrap();
1560        }
1561
1562        #[test]
1563        fn test_commit_without_transaction_error() {
1564            let db = GrafeoDB::new_in_memory();
1565            let mut session = db.session();
1566
1567            let result = session.commit();
1568            assert!(result.is_err());
1569        }
1570
1571        #[test]
1572        fn test_rollback_without_transaction_error() {
1573            let db = GrafeoDB::new_in_memory();
1574            let mut session = db.session();
1575
1576            let result = session.rollback();
1577            assert!(result.is_err());
1578        }
1579
1580        #[test]
1581        fn test_create_edge_in_transaction() {
1582            let db = GrafeoDB::new_in_memory();
1583            let mut session = db.session();
1584
1585            // Create nodes outside transaction
1586            let alice = session.create_node(&["Person"]);
1587            let bob = session.create_node(&["Person"]);
1588
1589            // Create edge in transaction
1590            session.begin_tx().unwrap();
1591            let edge_id = session.create_edge(alice, bob, "KNOWS");
1592
1593            // Edge should be visible in the transaction
1594            assert!(session.edge_exists(edge_id));
1595
1596            // Commit
1597            session.commit().unwrap();
1598
1599            // Edge should still be visible
1600            assert!(session.edge_exists(edge_id));
1601        }
1602
1603        #[test]
1604        fn test_neighbors_empty_node() {
1605            let db = GrafeoDB::new_in_memory();
1606            let session = db.session();
1607
1608            let lonely = session.create_node(&["Person"]);
1609
1610            assert!(session.get_neighbors_outgoing(lonely).is_empty());
1611            assert!(session.get_neighbors_incoming(lonely).is_empty());
1612            assert!(
1613                session
1614                    .get_neighbors_outgoing_by_type(lonely, "KNOWS")
1615                    .is_empty()
1616            );
1617        }
1618    }
1619}