1use std::collections::HashMap;
2use std::sync::Arc;
3
4use alopex_core::kv::{KVStore, KVTransaction};
5use alopex_core::types::TxnMode;
6use alopex_core::vector::hnsw::{HnswIndex, HnswTransactionState};
7use alopex_core::{Error as CoreError, Result as CoreResult};
8
9use crate::catalog::CatalogOverlay;
10use crate::catalog::TableMetadata;
11
12use super::error::Result;
13use super::{IndexStorage, TableStorage};
14
15pub trait SqlTxn<'txn, S: KVStore + 'txn> {
16 fn mode(&self) -> TxnMode;
17
18 fn ensure_write_txn(&self) -> CoreResult<()>;
19
20 fn inner_mut(&mut self) -> &mut S::Transaction<'txn>;
21
22 fn hnsw_entry(&mut self, name: &str) -> CoreResult<&HnswIndex>;
23
24 fn hnsw_entry_mut(&mut self, name: &str) -> CoreResult<&mut HnswTxnEntry>;
25
26 fn flush_hnsw(&mut self) -> Result<()>;
27
28 fn abandon_hnsw(&mut self) -> Result<()>;
29
30 fn delete_prefix(&mut self, prefix: &[u8]) -> Result<()>;
31
32 fn table_storage<'a>(
33 &'a mut self,
34 table_meta: &TableMetadata,
35 ) -> TableStorage<'a, 'txn, S::Transaction<'txn>> {
36 TableStorage::new(self.inner_mut(), table_meta)
37 }
38
39 fn index_storage<'a>(
40 &'a mut self,
41 index_id: u32,
42 unique: bool,
43 column_indices: Vec<usize>,
44 ) -> IndexStorage<'a, 'txn, S::Transaction<'txn>> {
45 IndexStorage::new(self.inner_mut(), index_id, unique, column_indices)
46 }
47
48 fn with_table<R, F>(&mut self, table_meta: &TableMetadata, f: F) -> Result<R>
49 where
50 F: FnOnce(&mut TableStorage<'_, 'txn, S::Transaction<'txn>>) -> Result<R>,
51 {
52 let mut storage = self.table_storage(table_meta);
53 f(&mut storage)
54 }
55
56 fn with_index<R, F>(
57 &mut self,
58 index_id: u32,
59 unique: bool,
60 column_indices: Vec<usize>,
61 f: F,
62 ) -> Result<R>
63 where
64 F: FnOnce(&mut IndexStorage<'_, 'txn, S::Transaction<'txn>>) -> Result<R>,
65 {
66 let mut storage = self.index_storage(index_id, unique, column_indices);
67 f(&mut storage)
68 }
69}
70
71pub struct TxnBridge<S: KVStore> {
73 store: Arc<S>,
74}
75
76impl<S: KVStore> TxnBridge<S> {
77 pub fn new(store: Arc<S>) -> Self {
79 Self { store }
80 }
81
82 pub fn runtime_stats(&self) -> Option<alopex_core::kv::RuntimeStats> {
84 self.store.runtime_stats()
85 }
86
87 pub fn set_memory_limit_bytes(&self, limit: Option<usize>) -> CoreResult<()> {
89 self.store.set_memory_limit_bytes(limit)
90 }
91
92 pub fn set_cache_capacity_bytes(&self, capacity: usize) -> CoreResult<()> {
94 self.store.set_cache_capacity_bytes(capacity)
95 }
96
97 pub fn clear_cache(&self) -> CoreResult<usize> {
99 self.store.clear_cache()
100 }
101
102 pub fn begin_read(&self) -> Result<SqlTransaction<'_, S>> {
104 let txn = self.store.begin(TxnMode::ReadOnly)?;
105 Ok(SqlTransaction {
106 inner: txn,
107 mode: TxnMode::ReadOnly,
108 hnsw_indices: HashMap::new(),
109 })
110 }
111
112 pub fn begin_write(&self) -> Result<SqlTransaction<'_, S>> {
114 let txn = self.store.begin(TxnMode::ReadWrite)?;
115 Ok(SqlTransaction {
116 inner: txn,
117 mode: TxnMode::ReadWrite,
118 hnsw_indices: HashMap::new(),
119 })
120 }
121
122 pub fn wrap_external<'a, 'b, 'c>(
123 txn: &'a mut S::Transaction<'b>,
124 mode: TxnMode,
125 overlay: &'c mut CatalogOverlay,
126 ) -> BorrowedSqlTransaction<'a, 'b, 'c, S> {
127 BorrowedSqlTransaction {
128 inner: txn,
129 mode,
130 overlay,
131 hnsw_indices: HashMap::new(),
132 }
133 }
134
135 pub fn with_read_txn<R, F>(&self, f: F) -> Result<R>
140 where
141 F: FnOnce(&mut SqlTransaction<'_, S>) -> Result<R>,
142 {
143 let mut txn = self.begin_read()?;
144 let result = f(&mut txn)?;
145 txn.commit()?;
146 Ok(result)
147 }
148
149 pub fn with_write_txn<R, F>(&self, f: F) -> Result<R>
154 where
155 F: FnOnce(&mut SqlTransaction<'_, S>) -> Result<R>,
156 {
157 let mut txn = self.begin_write()?;
158 let result = f(&mut txn)?;
159 txn.commit()?;
160 Ok(result)
161 }
162
163 pub fn with_write_txn_explicit<R, F>(&self, f: F) -> Result<R>
168 where
169 F: FnOnce(&mut SqlTransaction<'_, S>) -> Result<(R, bool)>,
170 {
171 let mut txn = self.begin_write()?;
172 let (result, should_commit) = f(&mut txn)?;
173 if should_commit {
174 txn.commit()?;
175 } else {
176 txn.rollback()?;
177 }
178 Ok(result)
179 }
180}
181
182pub struct SqlTransaction<'a, S: KVStore + 'a> {
187 inner: S::Transaction<'a>,
188 mode: TxnMode,
189 hnsw_indices: HashMap<String, HnswTxnEntry>,
190}
191
192pub struct HnswTxnEntry {
193 pub index: HnswIndex,
194 pub state: HnswTransactionState,
195 pub dirty: bool,
196}
197
198impl<'a, S: KVStore + 'a> SqlTransaction<'a, S> {
199 pub fn mode(&self) -> TxnMode {
201 self.mode
202 }
203
204 pub fn table_storage<'b>(
211 &'b mut self,
212 table_meta: &TableMetadata,
213 ) -> TableStorage<'b, 'a, S::Transaction<'a>> {
214 TableStorage::new(&mut self.inner, table_meta)
215 }
216
217 pub fn index_storage<'b>(
222 &'b mut self,
223 index_id: u32,
224 unique: bool,
225 column_indices: Vec<usize>,
226 ) -> IndexStorage<'b, 'a, S::Transaction<'a>> {
227 IndexStorage::new(&mut self.inner, index_id, unique, column_indices)
228 }
229
230 #[allow(dead_code)]
232 pub(crate) fn hnsw_entry(&mut self, name: &str) -> CoreResult<&HnswIndex> {
233 if !self.hnsw_indices.contains_key(name) {
234 let index = HnswIndex::load(name, &mut self.inner)?;
235 self.hnsw_indices.insert(
236 name.to_string(),
237 HnswTxnEntry {
238 index,
239 state: HnswTransactionState::default(),
240 dirty: false,
241 },
242 );
243 }
244 Ok(&self.hnsw_indices.get(name).expect("inserted above").index)
245 }
246
247 pub(crate) fn hnsw_entry_mut(&mut self, name: &str) -> CoreResult<&mut HnswTxnEntry> {
249 if !self.hnsw_indices.contains_key(name) {
250 let index = HnswIndex::load(name, &mut self.inner)?;
251 self.hnsw_indices.insert(
252 name.to_string(),
253 HnswTxnEntry {
254 index,
255 state: HnswTransactionState::default(),
256 dirty: false,
257 },
258 );
259 }
260 Ok(self.hnsw_indices.get_mut(name).expect("inserted above"))
261 }
262
263 pub(crate) fn ensure_write_txn(&self) -> CoreResult<()> {
264 if self.mode != TxnMode::ReadWrite {
265 return Err(CoreError::TxnConflict);
266 }
267 Ok(())
268 }
269
270 pub(crate) fn inner_mut(&mut self) -> &mut S::Transaction<'a> {
272 &mut self.inner
273 }
274
275 pub fn delete_prefix(&mut self, prefix: &[u8]) -> Result<()> {
277 const BATCH: usize = 512;
279 loop {
280 let mut keys = Vec::with_capacity(BATCH);
281 {
282 let iter = self.inner.scan_prefix(prefix)?;
283 for (key, _) in iter.take(BATCH) {
284 keys.push(key);
285 }
286 }
287
288 if keys.is_empty() {
289 break;
290 }
291
292 for key in keys {
293 self.inner.delete(key)?;
294 }
295 }
296
297 Ok(())
298 }
299
300 pub fn with_table<R, F>(&mut self, table_meta: &TableMetadata, f: F) -> Result<R>
307 where
308 F: FnOnce(&mut TableStorage<'_, 'a, S::Transaction<'a>>) -> Result<R>,
309 {
310 let mut storage = self.table_storage(table_meta);
311 f(&mut storage)
312 }
313
314 pub fn with_index<R, F>(
319 &mut self,
320 index_id: u32,
321 unique: bool,
322 column_indices: Vec<usize>,
323 f: F,
324 ) -> Result<R>
325 where
326 F: FnOnce(&mut IndexStorage<'_, 'a, S::Transaction<'a>>) -> Result<R>,
327 {
328 let mut storage = self.index_storage(index_id, unique, column_indices);
329 f(&mut storage)
330 }
331
332 pub fn commit(mut self) -> Result<()> {
336 self.commit_hnsw()?;
337 self.inner.commit_self()?;
338 Ok(())
339 }
340
341 pub fn rollback(mut self) -> Result<()> {
345 self.rollback_hnsw()?;
346 self.inner.rollback_self()?;
347 Ok(())
348 }
349
350 fn commit_hnsw(&mut self) -> Result<()> {
351 for entry in self.hnsw_indices.values_mut() {
352 if entry.dirty {
353 entry
354 .index
355 .commit_staged(&mut self.inner, &mut entry.state)?;
356 }
357 }
358 self.hnsw_indices.clear();
359 Ok(())
360 }
361
362 fn rollback_hnsw(&mut self) -> Result<()> {
363 for entry in self.hnsw_indices.values_mut() {
364 if entry.dirty {
365 entry.index.rollback(&mut entry.state)?;
366 }
367 }
368 self.hnsw_indices.clear();
369 Ok(())
370 }
371}
372
373impl<'a, S: KVStore + 'a> SqlTxn<'a, S> for SqlTransaction<'a, S> {
374 fn mode(&self) -> TxnMode {
375 self.mode()
376 }
377
378 fn ensure_write_txn(&self) -> CoreResult<()> {
379 self.ensure_write_txn()
380 }
381
382 fn inner_mut(&mut self) -> &mut S::Transaction<'a> {
383 self.inner_mut()
384 }
385
386 fn hnsw_entry(&mut self, name: &str) -> CoreResult<&HnswIndex> {
387 self.hnsw_entry(name)
388 }
389
390 fn hnsw_entry_mut(&mut self, name: &str) -> CoreResult<&mut HnswTxnEntry> {
391 self.hnsw_entry_mut(name)
392 }
393
394 fn flush_hnsw(&mut self) -> Result<()> {
395 self.commit_hnsw()
396 }
397
398 fn abandon_hnsw(&mut self) -> Result<()> {
399 self.rollback_hnsw()
400 }
401
402 fn delete_prefix(&mut self, prefix: &[u8]) -> Result<()> {
403 self.delete_prefix(prefix)
404 }
405}
406
407pub struct BorrowedSqlTransaction<'a, 'b, 'c, S: KVStore + 'b> {
408 inner: &'a mut S::Transaction<'b>,
409 mode: TxnMode,
410 overlay: &'c mut CatalogOverlay,
411 hnsw_indices: HashMap<String, HnswTxnEntry>,
412}
413
414impl<'a, 'b, 'c, S: KVStore + 'b> BorrowedSqlTransaction<'a, 'b, 'c, S> {
415 pub fn mode(&self) -> TxnMode {
416 self.mode
417 }
418
419 pub fn split_parts(&mut self) -> (BorrowedSqlTxn<'_, 'b, S>, &mut CatalogOverlay) {
420 (
421 BorrowedSqlTxn {
422 inner: self.inner,
423 mode: self.mode,
424 hnsw_indices: &mut self.hnsw_indices,
425 },
426 self.overlay,
427 )
428 }
429}
430
431impl<'a, 'b, 'c, S: KVStore + 'b> Drop for BorrowedSqlTransaction<'a, 'b, 'c, S> {
432 fn drop(&mut self) {
433 for entry in self.hnsw_indices.values_mut() {
434 if entry.dirty {
435 let _ = entry.index.rollback(&mut entry.state);
436 entry.dirty = false;
437 }
438 }
439 self.hnsw_indices.clear();
440 }
441}
442
443pub struct BorrowedSqlTxn<'a, 'b, S: KVStore + 'b> {
444 inner: &'a mut S::Transaction<'b>,
445 mode: TxnMode,
446 hnsw_indices: &'a mut HashMap<String, HnswTxnEntry>,
447}
448
449impl<'a, 'b, S: KVStore + 'b> SqlTxn<'b, S> for BorrowedSqlTxn<'a, 'b, S> {
450 fn mode(&self) -> TxnMode {
451 self.mode
452 }
453
454 fn ensure_write_txn(&self) -> CoreResult<()> {
455 if self.mode != TxnMode::ReadWrite {
456 return Err(CoreError::TxnReadOnly);
457 }
458 Ok(())
459 }
460
461 fn inner_mut(&mut self) -> &mut S::Transaction<'b> {
462 self.inner
463 }
464
465 fn hnsw_entry(&mut self, name: &str) -> CoreResult<&HnswIndex> {
466 if !self.hnsw_indices.contains_key(name) {
467 let index = HnswIndex::load(name, self.inner)?;
468 self.hnsw_indices.insert(
469 name.to_string(),
470 HnswTxnEntry {
471 index,
472 state: HnswTransactionState::default(),
473 dirty: false,
474 },
475 );
476 }
477 Ok(&self.hnsw_indices.get(name).expect("inserted above").index)
478 }
479
480 fn hnsw_entry_mut(&mut self, name: &str) -> CoreResult<&mut HnswTxnEntry> {
481 if !self.hnsw_indices.contains_key(name) {
482 let index = HnswIndex::load(name, self.inner)?;
483 self.hnsw_indices.insert(
484 name.to_string(),
485 HnswTxnEntry {
486 index,
487 state: HnswTransactionState::default(),
488 dirty: false,
489 },
490 );
491 }
492 Ok(self.hnsw_indices.get_mut(name).expect("inserted above"))
493 }
494
495 fn flush_hnsw(&mut self) -> Result<()> {
496 for entry in self.hnsw_indices.values_mut() {
497 if entry.dirty {
498 entry.index.commit_staged(self.inner, &mut entry.state)?;
499 entry.dirty = false;
500 }
501 }
502 self.hnsw_indices.clear();
503 Ok(())
504 }
505
506 fn abandon_hnsw(&mut self) -> Result<()> {
507 for entry in self.hnsw_indices.values_mut() {
508 if entry.dirty {
509 entry.index.rollback(&mut entry.state)?;
510 entry.dirty = false;
511 }
512 }
513 self.hnsw_indices.clear();
514 Ok(())
515 }
516
517 fn delete_prefix(&mut self, prefix: &[u8]) -> Result<()> {
518 const BATCH: usize = 512;
520 loop {
521 let mut keys = Vec::with_capacity(BATCH);
522 {
523 let iter = self.inner.scan_prefix(prefix)?;
524 for (key, _) in iter.take(BATCH) {
525 keys.push(key);
526 }
527 }
528
529 if keys.is_empty() {
530 break;
531 }
532
533 for key in keys {
534 self.inner.delete(key)?;
535 }
536 }
537
538 Ok(())
539 }
540}
541
542pub type TxnContext<'a, S> = SqlTransaction<'a, S>;
544
545#[cfg(test)]
546mod tests {
547 use super::super::SqlValue;
548 use super::*;
549 use crate::catalog::ColumnMetadata;
550 use crate::planner::types::ResolvedType;
551 use alopex_core::kv::memory::MemoryKV;
552 use alopex_core::types::TxnMode;
553 use std::sync::Arc;
554
555 fn sample_table_meta() -> TableMetadata {
556 TableMetadata::new(
557 "users",
558 vec![
559 ColumnMetadata::new("id", ResolvedType::Integer)
560 .with_primary_key(true)
561 .with_not_null(true),
562 ColumnMetadata::new("name", ResolvedType::Text).with_not_null(true),
563 ],
564 )
565 .with_table_id(1)
566 }
567
568 #[test]
569 fn read_txn_mode_is_readonly() {
570 let store = Arc::new(MemoryKV::new());
571 let bridge = TxnBridge::new(store);
572
573 bridge
574 .with_read_txn(|ctx| {
575 assert_eq!(ctx.mode(), TxnMode::ReadOnly);
576 Ok(())
577 })
578 .unwrap();
579 }
580
581 #[test]
582 fn write_txn_mode_is_readwrite() {
583 let store = Arc::new(MemoryKV::new());
584 let bridge = TxnBridge::new(store);
585
586 bridge
587 .with_write_txn(|ctx| {
588 assert_eq!(ctx.mode(), TxnMode::ReadWrite);
589 Ok(())
590 })
591 .unwrap();
592 }
593
594 #[test]
595 fn commit_persists_changes_and_read_sees_them() {
596 let store = Arc::new(MemoryKV::new());
597 let bridge = TxnBridge::new(store.clone());
598 let meta = sample_table_meta();
599
600 bridge
602 .with_write_txn(|ctx| {
603 ctx.with_table(&meta, |table| {
604 table.insert(1, &[SqlValue::Integer(1), SqlValue::Text("alice".into())])
605 })
606 })
607 .unwrap();
608
609 let row = bridge
611 .with_read_txn(|ctx| ctx.with_table(&meta, |table| table.get(1)))
612 .unwrap()
613 .unwrap();
614
615 assert_eq!(row[1], SqlValue::Text("alice".into()));
616 }
617
618 #[test]
619 fn rollback_discards_uncommitted_writes() {
620 let store = Arc::new(MemoryKV::new());
621 let bridge = TxnBridge::new(store.clone());
622 let meta = sample_table_meta();
623
624 bridge
626 .with_write_txn_explicit(|ctx| {
627 ctx.with_table(&meta, |table| {
628 table.insert(1, &[SqlValue::Integer(1), SqlValue::Text("bob".into())])
629 })?;
630 Ok(((), false)) })
632 .unwrap();
633
634 let row = bridge
636 .with_read_txn(|ctx| ctx.with_table(&meta, |table| table.get(1)))
637 .unwrap();
638
639 assert!(row.is_none());
640 }
641
642 #[test]
643 fn conflicting_commits_trigger_transaction_conflict() {
644 let store = Arc::new(MemoryKV::new());
645 let bridge = TxnBridge::new(store);
646 let meta = sample_table_meta();
647
648 let mut txn1 = bridge.begin_write().unwrap();
650 {
651 let mut table = txn1.table_storage(&meta);
652 table
653 .insert(1, &[SqlValue::Integer(1), SqlValue::Text("alice".into())])
654 .unwrap();
655 }
656
657 let mut txn2 = bridge.begin_write().unwrap();
659 {
660 let mut table = txn2.table_storage(&meta);
661 table
662 .insert(1, &[SqlValue::Integer(1), SqlValue::Text("bob".into())])
663 .unwrap();
664 }
665
666 txn1.commit().unwrap();
668 let err = txn2.commit().unwrap_err();
669 assert!(matches!(
670 err,
671 super::super::StorageError::TransactionConflict
672 ));
673 }
674
675 #[test]
676 fn scan_rows_in_transaction() {
677 let store = Arc::new(MemoryKV::new());
678 let bridge = TxnBridge::new(store.clone());
679 let meta = sample_table_meta();
680
681 bridge
683 .with_write_txn(|ctx| {
684 ctx.with_table(&meta, |table| {
685 for i in 1..=3 {
686 table.insert(
687 i,
688 &[
689 SqlValue::Integer(i as i32),
690 SqlValue::Text(format!("user{i}")),
691 ],
692 )?;
693 }
694 Ok(())
695 })
696 })
697 .unwrap();
698
699 let rows: Vec<u64> = bridge
701 .with_read_txn(|ctx| {
702 ctx.with_table(&meta, |table| {
703 let iter = table.scan()?;
704 let ids: Vec<u64> = iter.filter_map(|r| r.ok().map(|(id, _)| id)).collect();
705 Ok(ids)
706 })
707 })
708 .unwrap();
709
710 assert_eq!(rows, vec![1, 2, 3]);
711 }
712}