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