Skip to main content

akar_main/connection/
mod.rs

1//! Connection — used to execute queries against a Database.
2//!
3//! Manages the full query lifecycle: parse → bind → plan → optimize → execute.
4//! DDL statements (CREATE/DROP TABLE) update the catalog directly and return
5//! a message result. DML statements (MATCH/RETURN) produce DataChunk results.
6//!
7//! Supports prepared statements via `prepare()` and `execute()` for
8//! parameterized queries.
9//!
10//! # Concurrent Multi-Writer Support
11//!
12//! Write transactions use `TransactionManager::begin_write()` / `commit()` /
13//! `rollback()` with per-transaction `LocalStorage`, `LocalWAL`, and
14//! `ShadowFile` resources held in `txn_resources`. The full commit pipeline
15//! flushes: LocalStorage → tables, LocalWAL → global WAL, ShadowFile → BM.
16
17pub mod copy;
18pub mod ddl;
19pub mod dml;
20pub mod fts_estimate;
21pub mod plan_cache;
22pub mod query;
23pub mod standalone_call;
24pub mod substitute;
25pub mod transaction;
26pub mod utils;
27
28use crate::database::Database;
29use crate::prepared_statement::PreparedStatement;
30use plan_cache::{CachedPlan, PlanCache};
31use std::collections::HashMap;
32use std::sync::{Arc, Mutex};
33
34/// Default capacity for the query plan LRU cache.
35const PLAN_CACHE_CAPACITY: usize = 100;
36
37/// Per-transaction resources held during an active write transaction.
38pub(crate) struct TxnResources {
39    pub local_storage: akar_storage::LocalStorage,
40    pub local_wal: akar_storage::LocalWAL,
41    pub shadow_file: akar_storage::ShadowFile,
42}
43
44/// A connection to the database for executing Cypher queries.
45///
46/// Created via [`Connection::new`] with a reference to a [`Database`].
47/// Supports both ad-hoc queries and prepared statements.
48///
49/// # Examples
50///
51/// ```no_run
52/// use akar_main::database::{Database, SystemConfig};
53/// use akar_main::connection::Connection;
54///
55/// let db = std::sync::Arc::new(Database::new("./my_db", SystemConfig::default())?);
56/// let conn = Connection::new(&db);
57///
58/// // DDL
59/// conn.query("CREATE NODE TABLE Person(name STRING, PRIMARY KEY(name))")?;
60///
61/// // DML
62/// conn.query("CREATE (:Person {name: 'Alice'})")?;
63///
64/// // Query
65/// let result = conn.query("MATCH (p:Person) RETURN p.name")?;
66/// assert!(result.success);
67/// # Ok::<(), String>(())
68/// ```
69pub struct Connection {
70    pub(crate) database: Arc<Database>,
71    /// Cache of prepared statements (query → PreparedStatement).
72    pub(crate) statement_cache: Mutex<HashMap<String, PreparedStatement>>,
73    /// LRU cache of optimized query plans keyed by normalized query string.
74    /// Entries are validated against the catalog version on lookup, so DDL
75    /// (via any connection) invalidates them implicitly.
76    pub(crate) plan_cache: Mutex<PlanCache<CachedPlan>>,
77    /// Per-transaction resources keyed by transaction ID.
78    /// Set up when `begin_write()` is called, cleaned up on commit/rollback.
79    pub(crate) txn_resources: Mutex<HashMap<u64, TxnResources>>,
80    /// Set while an explicit `BEGIN TRANSACTION` is open on this connection.
81    /// DDL statements bypass the txn's LocalStorage/ShadowFile and mutate the
82    /// catalog directly, so they cannot be rolled back — they are rejected
83    /// while this flag is set instead of falsely reporting success (P52.28).
84    pub(crate) explicit_txn_active: std::sync::atomic::AtomicBool,
85    /// Lazily-built processor handler callbacks (sequence, schema DDL, query,
86    /// subquery, standalone-call registry), shared across every query on this
87    /// connection. Cached here rather than on `Database` so the handlers'
88    /// strong `Arc<Database>` captures do not create a reference cycle that
89    /// would keep the database (and its file lock) alive forever (P51.47).
90    processor_handlers: std::sync::OnceLock<Arc<crate::connection::query::ProcessorHandlers>>,
91}
92
93impl Connection {
94    /// Create a new connection to the given database.
95    ///
96    /// A connection owns a statement cache and transaction context.
97    /// Multiple connections can coexist on the same `Database` — they
98    /// share the buffer pool and catalog but hold independent transactions.
99    pub fn new(database: &Arc<Database>) -> Self {
100        Self {
101            database: database.clone(),
102            statement_cache: Mutex::new(HashMap::new()),
103            plan_cache: Mutex::new(PlanCache::new(PLAN_CACHE_CAPACITY)),
104            txn_resources: Mutex::new(HashMap::new()),
105            explicit_txn_active: std::sync::atomic::AtomicBool::new(false),
106            processor_handlers: std::sync::OnceLock::new(),
107        }
108    }
109
110    /// Clear the prepared statement and plan caches.
111    pub fn clear_cache(&self) {
112        if let Ok(mut cache) = self.statement_cache.lock() {
113            cache.clear();
114        }
115        if let Ok(mut cache) = self.plan_cache.lock() {
116            cache.clear();
117        }
118    }
119
120    /// Number of cached prepared statements.
121    pub fn cache_size(&self) -> usize {
122        self.statement_cache.lock().map(|c| c.len()).unwrap_or(0)
123    }
124
125    /// Number of cached query plans.
126    pub fn plan_cache_size(&self) -> usize {
127        self.plan_cache.lock().map(|c| c.len()).unwrap_or(0)
128    }
129}
130
131impl Drop for Connection {
132    fn drop(&mut self) {
133        // A dropped connection must roll back any abandoned write transactions.
134        // Without this, a client that disconnects after `BEGIN` (without
135        // COMMIT/ROLLBACK) leaves `active_write_count` raised and table locks
136        // held forever, wedging single-writer mode (P52.15).
137        let txn_ids: Vec<u64> = self
138            .txn_resources
139            .lock()
140            .map(|map| map.keys().copied().collect())
141            .unwrap_or_default();
142        if txn_ids.is_empty() {
143            return;
144        }
145
146        let tm = &self.database.transaction_manager;
147        let txns: Vec<akar_transaction::Transaction> = tm
148            .active_snapshot()
149            .ok()
150            .map(|mut active| txn_ids.iter().filter_map(|id| active.remove(id)).collect())
151            .unwrap_or_default();
152
153        for mut txn in txns {
154            let id = txn.transaction_id;
155            match self.rollback_write_txn(&mut txn) {
156                Ok(_) => tracing::info!("Rolled back abandoned txn#{id} on connection drop"),
157                Err(e) => tracing::warn!("Failed to roll back abandoned txn#{id} on connection drop: {e}"),
158            }
159        }
160    }
161}