alopex_embedded/
owned_session.rs1use std::collections::HashMap;
8use std::sync::Arc;
9
10use alopex_core::kv::{
11 AnyKV, OwnedKVTransactionAdapter, OwnedReadOptions, OwnedReadSession,
12 OwnedSessionFactory as CoreOwnedSessionFactory, OwnedTransactionSession,
13};
14use alopex_core::vector::hnsw::{HnswIndex, HnswTransactionState};
15use alopex_core::TxnMode;
16use alopex_sql::catalog::CatalogOverlay;
17use alopex_sql::storage::LocalRangeChangeJournal;
18
19use crate::{Database, Error, Result};
20
21#[derive(Clone)]
27pub struct EmbeddedOwnedSessionFactory {
28 store: Arc<AnyKV>,
29}
30
31impl EmbeddedOwnedSessionFactory {
32 pub(crate) fn new(store: Arc<AnyKV>) -> Self {
33 Self { store }
34 }
35
36 pub fn begin_read(&self, options: OwnedReadOptions) -> Result<OwnedReadSession> {
38 self.store
39 .clone()
40 .begin_owned_read(options)
41 .map_err(Error::Core)
42 }
43
44 pub fn begin_transaction(&self, mode: TxnMode) -> Result<OwnedTransactionSession> {
49 self.store
50 .clone()
51 .begin_owned_transaction(mode)
52 .map_err(Error::Core)
53 }
54}
55
56impl Database {
57 pub fn owned_session_factory(&self) -> EmbeddedOwnedSessionFactory {
59 EmbeddedOwnedSessionFactory::new(self.store.clone())
60 }
61
62 pub fn begin_owned_read(&self, options: OwnedReadOptions) -> Result<OwnedReadSession> {
64 self.owned_session_factory().begin_read(options)
65 }
66
67 pub fn begin_owned_transaction(&self, mode: TxnMode) -> Result<OwnedTransactionSession> {
69 self.owned_session_factory().begin_transaction(mode)
70 }
71
72 pub fn begin_owned_embedded_transaction(
78 self: Arc<Self>,
79 mode: TxnMode,
80 ) -> Result<OwnedEmbeddedTransaction> {
81 let session = self.begin_owned_transaction(mode)?;
82 let journal = if mode == TxnMode::ReadWrite
83 && self.store.range_change_journal_capability()
84 == alopex_core::kv::RangeChangeJournalCapability::Supported
85 {
86 let scope = {
87 let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
88 crate::sql_api::local_journal_scope(&*catalog)
89 };
90 Some(
91 session
92 .with_transaction(|transaction| {
93 let mut transaction = OwnedKVTransactionAdapter::new(transaction);
94 LocalRangeChangeJournal::capture(&mut transaction, scope)
95 })
96 .map_err(Error::Core)?,
97 )
98 } else {
99 None
100 };
101 Ok(OwnedEmbeddedTransaction {
102 db: self,
103 session,
104 overlay: CatalogOverlay::new(),
105 catalog_modified: false,
106 journal,
107 hnsw_indices: HashMap::new(),
108 vector_cache_invalidated: false,
109 })
110 }
111}
112
113pub struct OwnedEmbeddedTransaction {
119 pub(crate) db: Arc<Database>,
120 pub(crate) session: OwnedTransactionSession,
121 pub(crate) overlay: CatalogOverlay,
122 pub(crate) catalog_modified: bool,
123 pub(crate) journal: Option<LocalRangeChangeJournal>,
124 pub(crate) hnsw_indices: HashMap<String, (HnswIndex, HnswTransactionState)>,
125 pub(crate) vector_cache_invalidated: bool,
126}
127
128impl OwnedEmbeddedTransaction {
129 pub fn session(&self) -> OwnedTransactionSession {
131 self.session.clone()
132 }
133
134 pub fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>> {
136 self.session
137 .with_transaction(|transaction| transaction.get(&key.to_vec()))
138 .map_err(Error::Core)
139 }
140
141 pub fn put(&mut self, key: &[u8], value: &[u8]) -> Result<()> {
143 self.vector_cache_invalidated = true;
144 self.session
145 .with_transaction(|transaction| transaction.put(key.to_vec(), value.to_vec()))
146 .map_err(Error::Core)
147 }
148
149 pub fn delete(&mut self, key: &[u8]) -> Result<()> {
151 self.vector_cache_invalidated = true;
152 self.session
153 .with_transaction(|transaction| transaction.delete(key.to_vec()))
154 .map_err(Error::Core)
155 }
156
157 pub fn execute_sql(&mut self, sql: &str) -> Result<crate::SqlResult> {
162 crate::sql_api::execute_sql_owned(self, sql)
163 }
164
165 pub fn preflight_sql_stream(&self, sql: &str) -> Result<crate::OwnedSqlStreamPlan> {
170 crate::OwnedSqlStreamPlan::preflight_in_transaction(&self.db, &self.overlay, sql)
171 }
172
173 pub fn commit(&mut self) -> Result<()> {
175 let mut preparation = Ok(());
176 let journal = self.journal.take();
177 self.session
178 .with_transaction(|transaction| {
179 let mut transaction = alopex_core::kv::any::AnyKVTransaction::Owned(
180 OwnedKVTransactionAdapter::new(transaction),
181 );
182 for (index, state) in self.hnsw_indices.values_mut() {
183 if preparation.is_ok() {
184 preparation = index
185 .commit_staged(&mut transaction, state)
186 .map_err(Error::Core);
187 }
188 }
189 let mut catalog = self.db.sql_catalog.write().expect("catalog lock poisoned");
190 if preparation.is_ok() {
191 preparation = catalog
192 .persist_overlay(&mut transaction, &self.overlay)
193 .map_err(|error| Error::Sql(error.into()));
194 }
195 if preparation.is_ok() {
196 if let Some(journal) = journal {
197 preparation = journal
198 .stage(&mut transaction)
199 .map(|_| ())
200 .map_err(Error::Core);
201 }
202 }
203 Ok(())
204 })
205 .map_err(Error::Core)?;
206 preparation?;
207
208 self.session.commit().map_err(Error::Core)?;
209 let overlay = std::mem::take(&mut self.overlay);
210 let mut catalog = self.db.sql_catalog.write().expect("catalog lock poisoned");
211 catalog.apply_overlay(overlay);
212 drop(catalog);
213 if self.catalog_modified {
214 self.db.invalidate_table_info_cache();
215 }
216 if self.vector_cache_invalidated {
217 let mut cache = self
218 .db
219 .vector_cache
220 .write()
221 .expect("vector cache lock poisoned");
222 *cache = None;
223 }
224 if !self.hnsw_indices.is_empty() {
225 let hnsw_indices = std::mem::take(&mut self.hnsw_indices);
226 let mut cache = self
227 .db
228 .hnsw_cache
229 .write()
230 .expect("hnsw cache lock poisoned");
231 for (name, (index, _)) in hnsw_indices {
232 cache.insert(name, Arc::new(index));
233 }
234 }
235 Ok(())
236 }
237
238 pub fn rollback(&mut self) -> Result<()> {
240 self.session.rollback().map_err(Error::Core)?;
241 for (index, state) in self.hnsw_indices.values_mut() {
242 let _ = index.rollback(state);
243 }
244 self.hnsw_indices.clear();
245 self.overlay = CatalogOverlay::default();
246 Ok(())
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use alopex_core::kv::OwnedReadOptions;
254 use alopex_core::txn::OwnedLeaseOutcome;
255 use alopex_core::TxnMode;
256 use std::sync::Arc;
257
258 #[test]
259 fn embedded_factory_uses_owned_lifecycle_without_changing_borrowed_transactions() {
260 let database = Database::new();
261 let session = database
262 .begin_owned_transaction(TxnMode::ReadWrite)
263 .unwrap();
264 let lease = session.acquire_lease().unwrap();
265 lease
266 .with_transaction(|transaction| transaction.put(b"owned".to_vec(), b"value".to_vec()))
267 .unwrap();
268 assert!(session.commit().is_err());
269 lease.finish(OwnedLeaseOutcome::Exhausted).unwrap();
270 session.commit().unwrap();
271
272 let mut borrowed = database.begin(TxnMode::ReadOnly).unwrap();
273 assert_eq!(
274 borrowed.get(b"owned".as_ref()).unwrap(),
275 Some(b"value".to_vec())
276 );
277 borrowed.commit().unwrap();
278
279 let read = database
280 .owned_session_factory()
281 .begin_read(OwnedReadOptions::default())
282 .unwrap();
283 let lease = read.acquire_lease().unwrap();
284 let mut cursor = lease
285 .with_transaction(|transaction| transaction.scan_prefix(b"own"))
286 .unwrap();
287 assert_eq!(
288 cursor.next_entry().unwrap(),
289 Some((b"owned".to_vec(), b"value".to_vec()))
290 );
291 assert_eq!(cursor.next_entry().unwrap(), None);
292 cursor.close().unwrap();
293 drop(cursor);
294 lease.finish(OwnedLeaseOutcome::Exhausted).unwrap();
295 }
296
297 #[test]
298 fn dropped_embedded_owned_transaction_rolls_back_staged_writes() {
299 let database = Database::new();
300 let session = database
301 .begin_owned_transaction(TxnMode::ReadWrite)
302 .unwrap();
303 let lease = session.acquire_lease().unwrap();
304 lease
305 .with_transaction(|transaction| transaction.put(b"discard".to_vec(), b"value".to_vec()))
306 .unwrap();
307 lease.finish(OwnedLeaseOutcome::Exhausted).unwrap();
308 drop(session);
309
310 let mut borrowed = database.begin(TxnMode::ReadOnly).unwrap();
311 assert_eq!(borrowed.get(b"discard".as_ref()).unwrap(), None);
312 borrowed.commit().unwrap();
313 }
314
315 #[test]
316 fn owned_embedded_transaction_preserves_sql_visibility_until_explicit_commit() {
317 let database = Arc::new(Database::new());
318 database
319 .execute_sql("CREATE TABLE owned_sql (id INTEGER PRIMARY KEY, value TEXT)")
320 .unwrap();
321
322 let mut transaction = Arc::clone(&database)
323 .begin_owned_embedded_transaction(TxnMode::ReadWrite)
324 .unwrap();
325 transaction
326 .execute_sql("INSERT INTO owned_sql (id, value) VALUES (1, 'staged')")
327 .unwrap();
328 let alopex_sql::ExecutionResult::Query(query) = transaction
329 .execute_sql("SELECT value FROM owned_sql WHERE id = 1")
330 .unwrap()
331 else {
332 panic!("owned transaction select must return a query")
333 };
334 assert_eq!(query.rows.len(), 1);
335
336 let alopex_sql::ExecutionResult::Query(before_commit) = database
337 .execute_sql("SELECT value FROM owned_sql WHERE id = 1")
338 .unwrap()
339 else {
340 panic!("database select must return a query")
341 };
342 assert!(before_commit.rows.is_empty());
343
344 transaction.commit().unwrap();
345 let alopex_sql::ExecutionResult::Query(after_commit) = database
346 .execute_sql("SELECT value FROM owned_sql WHERE id = 1")
347 .unwrap()
348 else {
349 panic!("database select must return a query")
350 };
351 assert_eq!(after_commit.rows.len(), 1);
352 }
353
354 #[test]
355 fn owned_embedded_transaction_preserves_vector_and_similarity_workflows() {
356 let database = Arc::new(Database::new());
357 let mut transaction = Arc::clone(&database)
358 .begin_owned_embedded_transaction(TxnMode::ReadWrite)
359 .unwrap();
360 transaction
361 .upsert_vector(b"first", b"one", &[1.0, 0.0], alopex_core::Metric::Cosine)
362 .unwrap();
363 transaction
364 .upsert_vector(b"second", b"two", &[0.0, 1.0], alopex_core::Metric::Cosine)
365 .unwrap();
366 let results = transaction
367 .search_similar(&[1.0, 0.0], alopex_core::Metric::Cosine, 1, None)
368 .unwrap();
369 assert_eq!(results.len(), 1);
370 assert_eq!(results[0].key, b"first".to_vec());
371 transaction.commit().unwrap();
372
373 let mut reader = Arc::clone(&database)
374 .begin_owned_embedded_transaction(TxnMode::ReadOnly)
375 .unwrap();
376 assert_eq!(
377 reader
378 .get_vector(b"second", alopex_core::Metric::Cosine)
379 .unwrap(),
380 Some(vec![0.0, 1.0])
381 );
382 reader.rollback().unwrap();
383 }
384}