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