Skip to main content

alopex_sql/storage/
bridge.rs

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
71/// TxnBridge provides SQL-friendly transaction handles on top of a KVStore.
72pub struct TxnBridge<S: KVStore> {
73    store: Arc<S>,
74}
75
76impl<S: KVStore> TxnBridge<S> {
77    /// Create a new bridge for the given store.
78    pub fn new(store: Arc<S>) -> Self {
79        Self { store }
80    }
81
82    /// Returns the underlying store's runtime statistics.
83    pub fn runtime_stats(&self) -> Option<alopex_core::kv::RuntimeStats> {
84        self.store.runtime_stats()
85    }
86
87    /// Applies a memory-limit setting to the underlying store.
88    pub fn set_memory_limit_bytes(&self, limit: Option<usize>) -> CoreResult<()> {
89        self.store.set_memory_limit_bytes(limit)
90    }
91
92    /// Applies a cache-capacity setting to the underlying store.
93    pub fn set_cache_capacity_bytes(&self, capacity: usize) -> CoreResult<()> {
94        self.store.set_cache_capacity_bytes(capacity)
95    }
96
97    /// Clears the underlying cache.
98    pub fn clear_cache(&self) -> CoreResult<usize> {
99        self.store.clear_cache()
100    }
101
102    /// Begin a read-only transaction.
103    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    /// Begin a read-write transaction.
113    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    /// Execute a read-only transaction with automatic commit.
136    ///
137    /// The closure receives a `SqlTransaction` that provides access to table and index storage.
138    /// The transaction is automatically committed on success or rolled back on error.
139    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    /// Execute a read-write transaction with automatic commit.
150    ///
151    /// The closure receives a `SqlTransaction` that provides access to table and index storage.
152    /// The transaction is automatically committed on success or rolled back on error.
153    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    /// Execute a read-write transaction with explicit commit control.
164    ///
165    /// Returns `(result, should_commit)` from the closure. If `should_commit` is true,
166    /// the transaction is committed; otherwise it is rolled back.
167    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
182/// SQL transaction wrapping a KV transaction.
183///
184/// Provides SQL-friendly access to table and index storage,
185/// with explicit commit/rollback control.
186pub 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    /// Returns the transaction mode.
200    pub fn mode(&self) -> TxnMode {
201        self.mode
202    }
203
204    /// Get a table storage handle.
205    ///
206    /// Returns a TableStorage instance that borrows this transaction.
207    /// The returned storage is valid for the duration of the borrow.
208    ///
209    /// The `table_id` is obtained from `table_meta.table_id`.
210    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    /// Get an index storage handle.
218    ///
219    /// Returns an IndexStorage instance that borrows this transaction.
220    /// The returned storage is valid for the duration of the borrow.
221    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    /// HNSW インデックスのキャッシュエントリを取得する(存在しなければロードする)。
231    #[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    /// HNSW インデックスのキャッシュエントリを可変で取得する(存在しなければロードする)。
248    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    /// 内部の KV トランザクションへの可変参照を返す(HNSW ブリッジ向け)。
271    pub(crate) fn inner_mut(&mut self) -> &mut S::Transaction<'a> {
272        &mut self.inner
273    }
274
275    /// Delete all keys with the given prefix.
276    pub fn delete_prefix(&mut self, prefix: &[u8]) -> Result<()> {
277        // Process in small batches to avoid unbounded memory use.
278        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    /// Execute operations on a table within a closure.
301    ///
302    /// This is a convenience method that creates a TableStorage,
303    /// executes the closure, and returns the result.
304    ///
305    /// The `table_id` is obtained from `table_meta.table_id`.
306    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    /// Execute operations on an index within a closure.
315    ///
316    /// This is a convenience method that creates an IndexStorage,
317    /// executes the closure, and returns the result.
318    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    /// Commit the transaction.
333    ///
334    /// Consumes the transaction. On success, all writes become visible.
335    pub fn commit(mut self) -> Result<()> {
336        self.commit_hnsw()?;
337        self.inner.commit_self()?;
338        Ok(())
339    }
340
341    /// Rollback the transaction.
342    ///
343    /// Consumes the transaction. All pending writes are discarded.
344    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        // Process in small batches to avoid unbounded memory use.
519        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
542/// Backwards-compatible alias for SqlTransaction.
543pub 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        // Write transaction
601        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        // Read transaction
610        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        // Write transaction with explicit rollback
625        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)) // false = rollback
631            })
632            .unwrap();
633
634        // Read transaction should not see the row
635        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        // Start first write transaction
649        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        // Start second write transaction
658        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        // Commit first, second should conflict
667        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        // Insert multiple rows
682        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        // Scan all rows
700        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}