Skip to main content

alopex_embedded/
lib.rs

1//! A user-friendly embedded API for the AlopexDB key-value store.
2
3#![deny(missing_docs)]
4
5/// Catalog metadata API (in-memory, primarily for Python bindings).
6pub mod catalog;
7pub mod catalog_api;
8mod cluster_state;
9pub mod columnar_api;
10mod dataframe_api;
11pub mod options;
12mod sql_api;
13mod txn_manager;
14
15pub use crate::catalog::{CachedTableInfo, Catalog};
16pub use crate::catalog_api::{
17    CatalogInfo, ColumnDefinition, ColumnInfo, CreateCatalogRequest, CreateNamespaceRequest,
18    CreateTableRequest, IndexInfo, NamespaceInfo, StorageInfo, TableInfo,
19};
20pub use crate::columnar_api::{
21    ColumnarIndexInfo, ColumnarIndexType, ColumnarRowIterator, EmbeddedConfig, StorageMode,
22};
23pub use crate::options::DatabaseOptions;
24pub use crate::sql_api::{SqlStreamingResult, StreamingQueryResult, StreamingRows};
25pub use crate::txn_manager::{TransactionInfo, TransactionManager};
26pub use alopex_dataframe::{DataFrame, JoinKeys, JoinType, SortOptions};
27pub use alopex_sql::{DataSourceFormat, TableType};
28/// `Database::execute_sql()` / `Transaction::execute_sql()` の返却型。
29pub type SqlResult = alopex_sql::SqlResult;
30use alopex_core::vector::hnsw::{HnswTransactionState, SearchStats as HnswSearchStats};
31use alopex_core::{
32    columnar::{
33        kvs_bridge::ColumnarKvsBridge, memory::InMemorySegmentStore, segment_v2::SegmentConfigV2,
34    },
35    kv::any::AnyKVTransaction,
36    kv::memory::MemoryKV,
37    kv::AnyKV,
38    validate_dimensions, HnswIndex, KVStore, KVTransaction, Key, LargeValueKind, LargeValueMeta,
39    LargeValueReader, LargeValueWriter, StorageFactory, VectorType, DEFAULT_CHUNK_SIZE,
40};
41pub use alopex_core::{HnswConfig, HnswSearchResult, HnswStats, MemoryStats, Metric, TxnMode};
42/// Streaming query row iterator for FR-7 compliance.
43pub use alopex_sql::executor::QueryRowIterator;
44use std::collections::HashMap;
45use std::convert::TryInto;
46use std::fs;
47use std::path::{Path, PathBuf};
48use std::result;
49use std::sync::atomic::{AtomicU64, Ordering};
50use std::sync::{Arc, RwLock};
51
52/// A convenience `Result` type for database operations.
53pub type Result<T> = result::Result<T, Error>;
54
55/// The error type for embedded database operations.
56#[derive(Debug, thiserror::Error)]
57pub enum Error {
58    /// An error from the underlying core storage engine.
59    #[error("core error: {0}")]
60    Core(#[from] alopex_core::Error),
61    /// An error from the SQL execution pipeline.
62    #[error("{0}")]
63    Sql(#[from] alopex_sql::SqlError),
64    /// An error from DataFrame operations.
65    #[error("{0}")]
66    DataFrame(#[from] alopex_dataframe::DataFrameError),
67    /// The transaction has already been completed and cannot be used.
68    #[error("transaction is completed")]
69    TxnCompleted,
70    /// Catalog が見つかりません。
71    #[error("カタログが見つかりません: {0}")]
72    CatalogNotFound(String),
73    /// Catalog が既に存在します。
74    #[error("カタログは既に存在します: {0}")]
75    CatalogAlreadyExists(String),
76    /// Catalog が空ではありません。
77    #[error("カタログが空ではありません: {0}")]
78    CatalogNotEmpty(String),
79    /// Namespace が見つかりません。
80    #[error("ネームスペースが見つかりません: {0}.{1}")]
81    NamespaceNotFound(String, String),
82    /// Namespace が既に存在します。
83    #[error("ネームスペースは既に存在します: {0}.{1}")]
84    NamespaceAlreadyExists(String, String),
85    /// Namespace が空ではありません。
86    #[error("ネームスペースが空ではありません: {0}.{1}")]
87    NamespaceNotEmpty(String, String),
88    /// The requested table was not found or is invalid.
89    #[error("table not found: {0}")]
90    TableNotFound(String),
91    /// Table が既に存在します。
92    #[error("テーブルは既に存在します: {0}")]
93    TableAlreadyExists(String),
94    /// Index が見つかりません。
95    #[error("インデックスが見つかりません: {0}")]
96    IndexNotFound(String),
97    /// default オブジェクトは削除できません。
98    #[error("default オブジェクトは削除できません: {0}")]
99    CannotDeleteDefault(String),
100    /// Managed テーブルにはスキーマが必要です。
101    #[error("managed テーブルにはスキーマが必要です")]
102    SchemaRequired,
103    /// External テーブルには storage_root が必要です。
104    #[error("external テーブルには storage_root が必要です")]
105    StorageRootRequired,
106    /// トランザクションは read-only です。
107    #[error("トランザクションは読み取り専用です")]
108    TxnReadOnly,
109    /// Transaction ID is invalid or missing.
110    #[error("invalid transaction id: {0}")]
111    InvalidTransactionId(String),
112    /// The operation requires in-memory columnar mode.
113    #[error("not in in-memory columnar mode")]
114    NotInMemoryMode,
115    /// The requested data source format is not supported.
116    #[error("unsupported data source format: {0}")]
117    UnsupportedDataSourceFormat(String),
118    /// The catalog store lock was poisoned.
119    #[error("catalog lock poisoned")]
120    CatalogLockPoisoned,
121    /// The embedded cluster state lock was poisoned.
122    #[error("cluster state lock poisoned")]
123    ClusterStateLockPoisoned,
124}
125
126impl Error {
127    /// SQL エラーの場合はエラーコード(例: `ALOPEX-S003`)を返す。
128    pub fn sql_error_code(&self) -> Option<&'static str> {
129        match self {
130            Self::Sql(err) => Some(err.code()),
131            _ => None,
132        }
133    }
134}
135
136/// The main database object.
137pub struct Database {
138    /// The underlying key-value store.
139    pub(crate) store: Arc<AnyKV>,
140    pub(crate) sql_catalog: Arc<RwLock<alopex_sql::catalog::PersistentCatalog<AnyKV>>>,
141    pub(crate) hnsw_cache: RwLock<HashMap<String, Arc<HnswIndex>>>,
142    pub(crate) vector_cache: RwLock<Option<HashMap<Key, CachedVector>>>,
143    /// Table info cache for scan/write operations.
144    pub(crate) table_info_cache: RwLock<HashMap<String, CachedTableInfo>>,
145    /// Cache epoch for invalidation (incremented on DDL operations).
146    pub(crate) table_info_cache_epoch: AtomicU64,
147    pub(crate) columnar_mode: StorageMode,
148    pub(crate) columnar_bridge: ColumnarKvsBridge,
149    pub(crate) columnar_memory: Option<InMemorySegmentStore>,
150    pub(crate) segment_config: SegmentConfigV2,
151    pub(crate) cluster_state: RwLock<cluster_state::EmbeddedClusterState>,
152}
153
154pub(crate) fn disk_data_dir_path(path: &Path) -> std::path::PathBuf {
155    if path.extension().is_some_and(|e| e == "alopex") {
156        // v0.1 file-mode はディレクトリに WAL/SSTable を持つため、`.alopex` の横に
157        // sidecar ディレクトリを作ってそこへ格納する。
158        path.with_extension("alopex.d")
159    } else {
160        path.to_path_buf()
161    }
162}
163
164#[cfg(not(target_arch = "wasm32"))]
165fn read_file_version_from_storage(path: &Path) -> alopex_core::storage::format::FileVersion {
166    use alopex_core::storage::format::{FileHeader, FileVersion, HEADER_SIZE};
167    use std::io::Read;
168
169    let Some(file_path) = resolve_format_file_path(path) else {
170        return FileVersion::CURRENT;
171    };
172
173    let mut header_bytes = [0u8; HEADER_SIZE];
174    let Ok(mut file) = fs::File::open(file_path) else {
175        return FileVersion::CURRENT;
176    };
177    if file.read_exact(&mut header_bytes).is_err() {
178        return FileVersion::CURRENT;
179    }
180    match FileHeader::from_bytes(&header_bytes) {
181        Ok(header) => header.version,
182        Err(_) => FileVersion::CURRENT,
183    }
184}
185
186#[cfg(target_arch = "wasm32")]
187fn read_file_version_from_storage(_path: &Path) -> alopex_core::storage::format::FileVersion {
188    alopex_core::storage::format::FileVersion::CURRENT
189}
190
191#[cfg(not(target_arch = "wasm32"))]
192fn resolve_format_file_path(path: &Path) -> Option<PathBuf> {
193    if path.is_file() {
194        return Some(path.to_path_buf());
195    }
196
197    if path.is_dir() {
198        if let Some(ext) = path.extension() {
199            if ext == "d" {
200                let candidate = path.with_extension("");
201                if candidate.is_file() {
202                    return Some(candidate);
203                }
204            }
205        }
206
207        if let Ok(entries) = fs::read_dir(path) {
208            for entry in entries.flatten() {
209                let entry_path = entry.path();
210                if entry_path.extension().is_some_and(|ext| ext == "alopex") && entry_path.is_file()
211                {
212                    return Some(entry_path);
213                }
214            }
215        }
216    }
217
218    None
219}
220
221impl Database {
222    /// Opens a database at the specified path.
223    pub fn open(path: &Path) -> Result<Self> {
224        let data_dir = disk_data_dir_path(path);
225        let store = StorageFactory::create(alopex_core::StorageMode::Disk {
226            path: data_dir,
227            config: None,
228        })
229        .map_err(Error::Core)?;
230        let mut db = Self::init(store, StorageMode::Disk, None, SegmentConfigV2::default());
231        db.load_sql_catalog()?;
232        Ok(db)
233    }
234
235    /// Creates a new, purely in-memory (transient) database.
236    pub fn new() -> Self {
237        let store = AnyKV::Memory(MemoryKV::new());
238        Self::init(
239            store,
240            StorageMode::InMemory,
241            None,
242            SegmentConfigV2::default(),
243        )
244    }
245
246    /// Opens a database in in-memory mode with default options.
247    pub fn open_in_memory() -> Result<Self> {
248        Self::open_in_memory_with_options(DatabaseOptions::in_memory())
249    }
250
251    /// Opens a database in in-memory mode with the given options.
252    pub fn open_in_memory_with_options(opts: DatabaseOptions) -> Result<Self> {
253        if !opts.memory_mode() {
254            return Err(Error::Core(alopex_core::Error::InvalidFormat(
255                "memory_mode must be enabled for in-memory open".into(),
256            )));
257        }
258        let store = StorageFactory::create(opts.to_storage_mode(None)).map_err(Error::Core)?;
259        let mut db = Self::init(
260            store,
261            StorageMode::InMemory,
262            opts.memory_limit(),
263            SegmentConfigV2::default(),
264        );
265        db.load_sql_catalog()?;
266        Ok(db)
267    }
268
269    /// Opens a database from a URI string.
270    ///
271    /// Supported URI schemes:
272    /// - `file://path` or bare path: Local filesystem
273    /// - `s3://bucket/prefix`: S3-compatible storage (requires `s3` feature)
274    ///
275    /// # Example
276    ///
277    /// ```ignore
278    /// // Local path
279    /// let db = Database::open_with_uri("/path/to/db")?;
280    ///
281    /// // S3 URI (requires s3 feature and credentials)
282    /// let db = Database::open_with_uri("s3://my-bucket/data")?;
283    /// ```
284    pub fn open_with_uri(uri: &str) -> Result<Self> {
285        // Check for S3 URI
286        if uri.starts_with("s3://") {
287            #[cfg(feature = "s3")]
288            {
289                return Self::open_s3(uri);
290            }
291            #[cfg(not(feature = "s3"))]
292            {
293                return Err(Error::Core(alopex_core::Error::InvalidFormat(
294                    "S3 support requires the 's3' feature".into(),
295                )));
296            }
297        }
298
299        // Strip file:// prefix if present
300        let path = if let Some(stripped) = uri.strip_prefix("file://") {
301            stripped
302        } else {
303            uri
304        };
305
306        Self::open(Path::new(path))
307    }
308
309    /// Opens a database backed by S3 storage.
310    ///
311    /// This method downloads data from S3 to a local cache, operates on it
312    /// using LsmKV, and syncs changes back to S3 on flush/close.
313    ///
314    /// # Arguments
315    ///
316    /// * `uri` - S3 URI in the format `s3://bucket/prefix`
317    ///
318    /// # Environment Variables
319    ///
320    /// Required:
321    /// * `AWS_ACCESS_KEY_ID` - AWS access key
322    /// * `AWS_SECRET_ACCESS_KEY` - AWS secret key
323    ///
324    /// Optional:
325    /// * `AWS_REGION` - AWS region (default: us-east-1)
326    /// * `AWS_ENDPOINT_URL` - Custom endpoint for S3-compatible services
327    #[cfg(feature = "s3")]
328    pub fn open_s3(uri: &str) -> Result<Self> {
329        let s3_config = alopex_core::S3Config::from_uri(uri).map_err(Error::Core)?;
330        let store = StorageFactory::create(alopex_core::StorageMode::S3 { config: s3_config })
331            .map_err(Error::Core)?;
332        let mut db = Self::init(store, StorageMode::Disk, None, SegmentConfigV2::default());
333        db.load_sql_catalog()?;
334        Ok(db)
335    }
336
337    pub(crate) fn init(
338        store: AnyKV,
339        columnar_mode: StorageMode,
340        memory_limit: Option<usize>,
341        segment_config: SegmentConfigV2,
342    ) -> Self {
343        let store = Arc::new(store);
344        let sql_catalog = Arc::new(RwLock::new(alopex_sql::catalog::PersistentCatalog::new(
345            store.clone(),
346        )));
347        let columnar_bridge = ColumnarKvsBridge::new(store.clone());
348        let columnar_memory = if matches!(columnar_mode, StorageMode::InMemory) {
349            Some(InMemorySegmentStore::new(memory_limit.map(|v| v as u64)))
350        } else {
351            None
352        };
353
354        Self {
355            store,
356            sql_catalog,
357            hnsw_cache: RwLock::new(HashMap::new()),
358            vector_cache: RwLock::new(None),
359            table_info_cache: RwLock::new(HashMap::new()),
360            table_info_cache_epoch: AtomicU64::new(0),
361            columnar_mode,
362            columnar_bridge,
363            columnar_memory,
364            segment_config,
365            cluster_state: RwLock::new(cluster_state::EmbeddedClusterState::default()),
366        }
367    }
368
369    /// Returns the current cluster status owned by this database instance.
370    pub fn cluster_status_snapshot(&self) -> Result<alopex_cluster::ClusterStatusSnapshot> {
371        let state = self
372            .cluster_state
373            .read()
374            .map_err(|_| Error::ClusterStateLockPoisoned)?;
375        Ok(state.status_snapshot(self.table_info_cache_epoch()))
376    }
377
378    /// Returns the latest routing decision produced by this database instance.
379    pub fn routing_diagnostics(&self) -> Result<alopex_cluster::RoutingDiagnostics> {
380        let state = self
381            .cluster_state
382            .read()
383            .map_err(|_| Error::ClusterStateLockPoisoned)?;
384        Ok(state.routing_diagnostics(self.table_info_cache_epoch()))
385    }
386
387    pub(crate) fn record_routing<C: alopex_sql::Catalog + ?Sized>(
388        &self,
389        catalog: &C,
390        statement: &alopex_sql::Statement,
391        statement_index: usize,
392    ) {
393        let Ok(mut state) = self.cluster_state.write() else {
394            return;
395        };
396        state.record_routing(
397            catalog,
398            statement,
399            statement_index,
400            self.table_info_cache_epoch(),
401        );
402    }
403
404    fn load_sql_catalog(&mut self) -> Result<()> {
405        use alopex_sql::catalog::CatalogError;
406
407        let loaded = match alopex_sql::catalog::PersistentCatalog::load(self.store.clone()) {
408            Ok(catalog) => catalog,
409            Err(CatalogError::Kv(alopex_core::Error::NotFound)) => {
410                alopex_sql::catalog::PersistentCatalog::new(self.store.clone())
411            }
412            Err(other) => return Err(Error::Sql(other.into())),
413        };
414
415        self.sql_catalog = Arc::new(RwLock::new(loaded));
416        Ok(())
417    }
418
419    fn hnsw_cache_get(&self, name: &str) -> Option<Arc<HnswIndex>> {
420        let cache = self.hnsw_cache.read().expect("hnsw cache lock poisoned");
421        cache.get(name).cloned()
422    }
423
424    fn hnsw_cache_insert(&self, name: &str, index: HnswIndex) -> Arc<HnswIndex> {
425        let index = Arc::new(index);
426        let mut cache = self.hnsw_cache.write().expect("hnsw cache lock poisoned");
427        cache.insert(name.to_string(), Arc::clone(&index));
428        index
429    }
430
431    fn hnsw_cache_remove(&self, name: &str) {
432        let mut cache = self.hnsw_cache.write().expect("hnsw cache lock poisoned");
433        cache.remove(name);
434    }
435
436    /// Returns the current table info cache epoch.
437    pub fn table_info_cache_epoch(&self) -> u64 {
438        self.table_info_cache_epoch.load(Ordering::Relaxed)
439    }
440
441    /// Retrieves cached table info if available and epoch matches.
442    pub fn get_cached_table_info(
443        &self,
444        catalog_name: &str,
445        namespace_name: &str,
446        table_name: &str,
447    ) -> Option<CachedTableInfo> {
448        let cache = self
449            .table_info_cache
450            .read()
451            .expect("table info cache lock poisoned");
452        let key = format!("{}.{}.{}", catalog_name, namespace_name, table_name);
453        cache.get(&key).cloned()
454    }
455
456    /// Stores table info in the cache.
457    pub fn cache_table_info(
458        &self,
459        catalog_name: &str,
460        namespace_name: &str,
461        table_name: &str,
462        info: CachedTableInfo,
463    ) {
464        let mut cache = self
465            .table_info_cache
466            .write()
467            .expect("table info cache lock poisoned");
468        let key = format!("{}.{}.{}", catalog_name, namespace_name, table_name);
469        cache.insert(key, info);
470    }
471
472    /// Invalidates the table info cache (called on DDL operations).
473    pub fn invalidate_table_info_cache(&self) {
474        self.table_info_cache_epoch.fetch_add(1, Ordering::Relaxed);
475        let mut cache = self
476            .table_info_cache
477            .write()
478            .expect("table info cache lock poisoned");
479        cache.clear();
480    }
481
482    /// Flushes the current in-memory data to an SSTable on disk (beta).
483    pub fn flush(&self) -> Result<()> {
484        self.store.flush().map_err(Error::Core)
485    }
486
487    /// Returns the file format version supported by the embedded engine.
488    pub fn file_format_version(&self) -> alopex_core::storage::format::FileVersion {
489        use alopex_core::storage::format::FileVersion;
490
491        match self.store.as_ref() {
492            AnyKV::Memory(_) => FileVersion::CURRENT,
493            AnyKV::Lsm(kv) => read_file_version_from_storage(&kv.data_dir),
494            #[cfg(feature = "s3")]
495            AnyKV::S3(kv) => read_file_version_from_storage(kv.cache_dir()),
496        }
497    }
498
499    /// Returns current memory usage statistics (in-memory KV only).
500    pub fn memory_usage(&self) -> Option<MemoryStats> {
501        match self.store.as_ref() {
502            AnyKV::Memory(kv) => Some(kv.memory_stats()),
503            AnyKV::Lsm(_) => None,
504            #[cfg(feature = "s3")]
505            AnyKV::S3(_) => None,
506        }
507    }
508
509    /// Persists the current in-memory database to disk atomically.
510    ///
511    /// `wal_path` は「データディレクトリ」として扱う(file-mode)。
512    pub fn persist_to_disk(&self, wal_path: &Path) -> Result<()> {
513        if !matches!(self.store.as_ref(), AnyKV::Memory(_)) {
514            return Err(Error::NotInMemoryMode);
515        }
516        let data_dir = disk_data_dir_path(wal_path);
517        if wal_path.exists() || data_dir.exists() {
518            return Err(Error::Core(alopex_core::Error::PathExists(
519                wal_path.to_path_buf(),
520            )));
521        }
522
523        let tmp_dir = data_dir.with_extension("tmp");
524        if tmp_dir.exists() {
525            return Err(Error::Core(alopex_core::Error::PathExists(tmp_dir)));
526        }
527
528        let snapshot = self.snapshot_pairs()?;
529        let write_result = (|| -> Result<()> {
530            let store = StorageFactory::create(alopex_core::StorageMode::Disk {
531                path: tmp_dir.clone(),
532                config: None,
533            })
534            .map_err(Error::Core)?;
535
536            let mut txn = store.begin(TxnMode::ReadWrite).map_err(Error::Core)?;
537            for (key, value) in snapshot {
538                txn.put(key, value).map_err(Error::Core)?;
539            }
540            txn.commit_self().map_err(Error::Core)?;
541
542            Ok(())
543        })();
544
545        if let Err(e) = write_result {
546            let _ = fs::remove_dir_all(&tmp_dir);
547            return Err(e);
548        }
549
550        fs::rename(&tmp_dir, &data_dir).map_err(|e| Error::Core(e.into()))?;
551        if wal_path.extension().is_some_and(|e| e == "alopex") {
552            // `.alopex` パスを渡した場合は、存在確認用のマーカーを作る。
553            let _ = fs::OpenOptions::new()
554                .create_new(true)
555                .write(true)
556                .open(wal_path);
557        }
558        Ok(())
559    }
560
561    /// Creates a fully in-memory clone of the current database.
562    pub fn clone_to_memory(&self) -> Result<Self> {
563        let snapshot = self.snapshot_pairs()?;
564        let cloned = Database::open_in_memory()?;
565        if snapshot.is_empty() {
566            return Ok(cloned);
567        }
568
569        let mut txn = cloned.begin(TxnMode::ReadWrite)?;
570        for (key, value) in snapshot {
571            txn.put(&key, &value)?;
572        }
573        txn.commit()?;
574        Ok(cloned)
575    }
576
577    /// Clears all data while keeping the database usable.
578    pub fn clear(&self) -> Result<()> {
579        let keys: Vec<Key> = self.snapshot_pairs()?.into_iter().map(|(k, _)| k).collect();
580        if keys.is_empty() {
581            return Ok(());
582        }
583        let mut txn = self.begin(TxnMode::ReadWrite)?;
584        for key in keys {
585            txn.delete(&key)?;
586        }
587        txn.commit()
588    }
589
590    /// Updates the memory limit in bytes for the underlying in-memory store.
591    pub fn set_memory_limit(&self, bytes: Option<usize>) {
592        if let AnyKV::Memory(kv) = self.store.as_ref() {
593            kv.txn_manager().set_memory_limit(bytes);
594        }
595    }
596
597    /// Returns a read-only snapshot of all key-value pairs.
598    pub fn snapshot(&self) -> Vec<(Key, Vec<u8>)> {
599        self.snapshot_pairs().unwrap_or_default()
600    }
601
602    fn snapshot_pairs(&self) -> Result<Vec<(Key, Vec<u8>)>> {
603        let mut txn = self.store.begin(TxnMode::ReadOnly).map_err(Error::Core)?;
604        let pairs: Vec<(Key, Vec<u8>)> = txn.scan_prefix(b"").map_err(Error::Core)?.collect();
605        txn.commit_self().map_err(Error::Core)?;
606        Ok(pairs)
607    }
608
609    /// HNSW インデックスを作成し、永続化する。
610    pub fn create_hnsw_index(&self, name: &str, config: HnswConfig) -> Result<()> {
611        let mut txn = self.store.begin(TxnMode::ReadWrite).map_err(Error::Core)?;
612        let index = HnswIndex::create(name, config).map_err(Error::Core)?;
613        index.save(&mut txn).map_err(Error::Core)?;
614        txn.commit_self().map_err(Error::Core)?;
615        self.hnsw_cache_insert(name, index);
616        Ok(())
617    }
618
619    /// HNSW インデックスを削除する。
620    pub fn drop_hnsw_index(&self, name: &str) -> Result<()> {
621        let mut txn = self.store.begin(TxnMode::ReadWrite).map_err(Error::Core)?;
622        let index = HnswIndex::load(name, &mut txn).map_err(Error::Core)?;
623        index.drop(&mut txn).map_err(Error::Core)?;
624        txn.commit_self().map_err(Error::Core)?;
625        self.hnsw_cache_remove(name);
626        Ok(())
627    }
628
629    /// HNSW 統計情報を取得する。
630    pub fn get_hnsw_stats(&self, name: &str) -> Result<HnswStats> {
631        if let Some(index) = self.hnsw_cache_get(name) {
632            return Ok(index.stats());
633        }
634        let mut txn = self.store.begin(TxnMode::ReadOnly).map_err(Error::Core)?;
635        let index = HnswIndex::load(name, &mut txn).map_err(Error::Core)?;
636        let stats = index.stats();
637        self.hnsw_cache_insert(name, index);
638        Ok(stats)
639    }
640
641    /// HNSW インデックスをコンパクションし、結果を返す。
642    pub fn compact_hnsw_index(&self, name: &str) -> Result<alopex_core::vector::CompactionResult> {
643        let mut txn = self.store.begin(TxnMode::ReadWrite).map_err(Error::Core)?;
644        let mut index = HnswIndex::load(name, &mut txn).map_err(Error::Core)?;
645        let result = index.compact().map_err(Error::Core)?;
646        index.save(&mut txn).map_err(Error::Core)?;
647        txn.commit_self().map_err(Error::Core)?;
648        self.hnsw_cache_insert(name, index);
649        Ok(result)
650    }
651
652    /// HNSW インデックスに検索を行う。
653    pub fn search_hnsw(
654        &self,
655        name: &str,
656        query: &[f32],
657        k: usize,
658        ef_search: Option<usize>,
659    ) -> Result<(Vec<HnswSearchResult>, HnswSearchStats)> {
660        let profile = std::env::var_os("ALOPEX_PROFILE_HNSW").is_some();
661        let total_start = if profile {
662            Some(std::time::Instant::now())
663        } else {
664            None
665        };
666        if let Some(index) = self.hnsw_cache_get(name) {
667            let search_start = if profile {
668                Some(std::time::Instant::now())
669            } else {
670                None
671            };
672            let result = index.search(query, k, ef_search).map_err(Error::Core)?;
673            if let (true, Some(total_start), Some(search_start)) =
674                (profile, total_start, search_start)
675            {
676                let search_time = search_start.elapsed();
677                let total_time = total_start.elapsed();
678                eprintln!(
679                    "alopex.hnsw_search cache=hit name={} k={} ef_search={:?} search_ms={:.2} total_ms={:.2}",
680                    name,
681                    k,
682                    ef_search,
683                    search_time.as_secs_f64() * 1000.0,
684                    total_time.as_secs_f64() * 1000.0
685                );
686            }
687            return Ok(result);
688        }
689
690        let load_start = if profile {
691            Some(std::time::Instant::now())
692        } else {
693            None
694        };
695        let mut txn = self.store.begin(TxnMode::ReadOnly).map_err(Error::Core)?;
696        let index = HnswIndex::load(name, &mut txn).map_err(Error::Core)?;
697        let load_time = load_start.map(|start| start.elapsed());
698        let index = self.hnsw_cache_insert(name, index);
699        let search_start = if profile {
700            Some(std::time::Instant::now())
701        } else {
702            None
703        };
704        let result = index.search(query, k, ef_search).map_err(Error::Core)?;
705        if let (true, Some(total_start), Some(search_start)) = (profile, total_start, search_start)
706        {
707            let search_time = search_start.elapsed();
708            let total_time = total_start.elapsed();
709            let load_time_ms = load_time
710                .map(|elapsed| elapsed.as_secs_f64() * 1000.0)
711                .unwrap_or(0.0);
712            eprintln!(
713                "alopex.hnsw_search cache=miss name={} k={} ef_search={:?} load_ms={:.2} search_ms={:.2} total_ms={:.2}",
714                name,
715                k,
716                ef_search,
717                load_time_ms,
718                search_time.as_secs_f64() * 1000.0,
719                total_time.as_secs_f64() * 1000.0
720            );
721        }
722        Ok(result)
723    }
724
725    /// Creates a chunked large value writer for opaque blobs (beta).
726    pub fn create_blob_writer(
727        &self,
728        path: &Path,
729        total_len: u64,
730        chunk_size: Option<u32>,
731    ) -> Result<LargeValueWriter> {
732        let meta = LargeValueMeta {
733            kind: LargeValueKind::Blob,
734            total_len,
735            chunk_size: chunk_size.unwrap_or(DEFAULT_CHUNK_SIZE),
736        };
737        LargeValueWriter::create(path, meta).map_err(Error::Core)
738    }
739
740    /// Creates a chunked large value writer for typed payloads (beta).
741    pub fn create_typed_writer(
742        &self,
743        path: &Path,
744        type_id: u16,
745        total_len: u64,
746        chunk_size: Option<u32>,
747    ) -> Result<LargeValueWriter> {
748        let meta = LargeValueMeta {
749            kind: LargeValueKind::Typed(type_id),
750            total_len,
751            chunk_size: chunk_size.unwrap_or(DEFAULT_CHUNK_SIZE),
752        };
753        LargeValueWriter::create(path, meta).map_err(Error::Core)
754    }
755
756    /// Opens a chunked large value reader (beta). Kind/type is read from the file header.
757    pub fn open_large_value(&self, path: &Path) -> Result<LargeValueReader> {
758        LargeValueReader::open(path).map_err(Error::Core)
759    }
760
761    /// Begins a new transaction.
762    pub fn begin(&self, mode: TxnMode) -> Result<Transaction<'_>> {
763        let txn = self.store.begin(mode).map_err(Error::Core)?;
764        Ok(Transaction {
765            inner: Some(txn),
766            db: self,
767            hnsw_indices: HashMap::new(),
768            overlay: alopex_sql::catalog::CatalogOverlay::new(),
769            vector_cache_updates: HashMap::new(),
770            vector_cache_deletes: Vec::new(),
771            vector_cache_invalidated: false,
772            catalog_modified: false,
773        })
774    }
775}
776
777impl Default for Database {
778    fn default() -> Self {
779        Self::new()
780    }
781}
782
783/// A database transaction.
784pub struct Transaction<'a> {
785    inner: Option<AnyKVTransaction<'a>>,
786    db: &'a Database,
787    hnsw_indices: HashMap<String, (HnswIndex, alopex_core::vector::hnsw::HnswTransactionState)>,
788    overlay: alopex_sql::catalog::CatalogOverlay,
789    vector_cache_updates: HashMap<Key, CachedVector>,
790    vector_cache_deletes: Vec<Key>,
791    vector_cache_invalidated: bool,
792    /// Whether DDL operations were performed in this transaction.
793    pub(crate) catalog_modified: bool,
794}
795
796/// A search result row containing key, metadata, and similarity score.
797#[derive(Debug, Clone, PartialEq)]
798pub struct SearchResult {
799    /// User key associated with the vector.
800    pub key: Key,
801    /// Opaque metadata payload stored alongside the vector.
802    pub metadata: Vec<u8>,
803    /// Similarity score for the query/vector pair.
804    pub score: f32,
805}
806
807const VECTOR_INDEX_KEY: &[u8] = b"__alopex_vector_index";
808
809impl<'a> Transaction<'a> {
810    pub(crate) fn catalog_overlay(&self) -> &alopex_sql::catalog::CatalogOverlay {
811        &self.overlay
812    }
813
814    pub(crate) fn catalog_overlay_mut(&mut self) -> &mut alopex_sql::catalog::CatalogOverlay {
815        &mut self.overlay
816    }
817
818    pub(crate) fn txn_mode(&self) -> Result<TxnMode> {
819        let txn = self.inner.as_ref().ok_or(Error::TxnCompleted)?;
820        Ok(txn.mode())
821    }
822    /// Retrieves the value for a given key.
823    pub fn get(&mut self, key: &[u8]) -> Result<Option<Vec<u8>>> {
824        self.inner_mut()?.get(&key.to_vec()).map_err(Error::Core)
825    }
826
827    /// Sets a value for a given key.
828    pub fn put(&mut self, key: &[u8], value: &[u8]) -> Result<()> {
829        self.vector_cache_invalidated = true;
830        self.vector_cache_updates.clear();
831        self.vector_cache_deletes.clear();
832        self.inner_mut()?
833            .put(key.to_vec(), value.to_vec())
834            .map_err(Error::Core)
835    }
836
837    /// Deletes a key-value pair.
838    pub fn delete(&mut self, key: &[u8]) -> Result<()> {
839        self.vector_cache_deletes.push(key.to_vec());
840        self.inner_mut()?.delete(key.to_vec()).map_err(Error::Core)
841    }
842
843    /// Scans all key-value pairs whose keys start with the given prefix.
844    ///
845    /// Returns an iterator over (key, value) pairs.
846    pub fn scan_prefix(
847        &mut self,
848        prefix: &[u8],
849    ) -> Result<Box<dyn Iterator<Item = (Key, Vec<u8>)> + '_>> {
850        self.inner_mut()?.scan_prefix(prefix).map_err(Error::Core)
851    }
852
853    /// HNSW にベクトルをステージング挿入/更新する。
854    pub fn upsert_to_hnsw(
855        &mut self,
856        index_name: &str,
857        key: &[u8],
858        vector: &[f32],
859        metadata: &[u8],
860    ) -> Result<()> {
861        self.ensure_write_txn()?;
862        let (index, state) = self.hnsw_entry_mut(index_name)?;
863        index
864            .upsert_staged(key, vector, metadata, state)
865            .map_err(Error::Core)
866    }
867
868    /// HNSW からキーをステージング削除する。
869    pub fn delete_from_hnsw(&mut self, index_name: &str, key: &[u8]) -> Result<bool> {
870        self.ensure_write_txn()?;
871        let (index, state) = self.hnsw_entry_mut(index_name)?;
872        index.delete_staged(key, state).map_err(Error::Core)
873    }
874
875    /// Upserts a vector and metadata under the provided key after validating dimensions and metric.
876    ///
877    /// A small internal index is maintained to enable scanning for similarity search.
878    pub fn upsert_vector(
879        &mut self,
880        key: &[u8],
881        metadata: &[u8],
882        vector: &[f32],
883        metric: Metric,
884    ) -> Result<()> {
885        if vector.is_empty() {
886            return Err(Error::Core(alopex_core::Error::InvalidFormat(
887                "vector cannot be empty".into(),
888            )));
889        }
890        let vt = VectorType::new(vector.len(), metric);
891        vt.validate(vector).map_err(Error::Core)?;
892
893        let payload = encode_vector_entry(vt, metadata, vector);
894        let txn = self.inner_mut()?;
895        txn.put(key.to_vec(), payload).map_err(Error::Core)?;
896
897        let mut keys = self.load_vector_index()?;
898        if !keys.iter().any(|k| k == key) {
899            keys.push(key.to_vec());
900            self.persist_vector_index(&keys)?;
901        }
902
903        let cached = cached_vector_from_entry(metric, metadata.to_vec(), vector.to_vec());
904        self.vector_cache_updates.insert(key.to_vec(), cached);
905        self.vector_cache_deletes.retain(|k| k != key);
906        Ok(())
907    }
908
909    /// Retrieves a vector stored under the given key.
910    ///
911    /// Returns `None` if the key does not exist. If the key exists but has a different
912    /// metric than specified, returns an error.
913    pub fn get_vector(&mut self, key: &[u8], metric: Metric) -> Result<Option<Vec<f32>>> {
914        let txn = self.inner_mut()?;
915        let key_vec = key.to_vec();
916        let Some(raw) = txn.get(&key_vec).map_err(Error::Core)? else {
917            return Ok(None);
918        };
919        let decoded = decode_vector_entry(&raw).map_err(Error::Core)?;
920        if decoded.metric != metric {
921            return Err(Error::Core(alopex_core::Error::UnsupportedMetric {
922                metric: metric.as_str().to_string(),
923            }));
924        }
925        Ok(Some(decoded.vector))
926    }
927
928    /// Retrieves vectors stored under the given keys in a single transaction call.
929    ///
930    /// Returned list order matches the input `keys` order. Each entry is:
931    /// - `None` if the key does not exist
932    /// - `Some(Vec<f32>)` if the key exists and metric matches
933    ///
934    /// If any existing entry has a different metric than specified, returns an error.
935    pub fn get_vectors(&mut self, keys: &[Key], metric: Metric) -> Result<Vec<Option<Vec<f32>>>> {
936        let txn = self.inner_mut()?;
937        let mut out = Vec::with_capacity(keys.len());
938        for key in keys {
939            let Some(raw) = txn.get(key).map_err(Error::Core)? else {
940                out.push(None);
941                continue;
942            };
943            let decoded = decode_vector_entry(&raw).map_err(Error::Core)?;
944            if decoded.metric != metric {
945                return Err(Error::Core(alopex_core::Error::UnsupportedMetric {
946                    metric: metric.as_str().to_string(),
947                }));
948            }
949            out.push(Some(decoded.vector));
950        }
951        Ok(out)
952    }
953
954    /// Executes a flat similarity search over stored vectors using the provided metric and query.
955    ///
956    /// The optional `filter_keys` restricts the scan to the given keys; otherwise the full
957    /// vector index is scanned. Results are sorted by descending score and truncated to `top_k`.
958    pub fn search_similar(
959        &mut self,
960        query_vector: &[f32],
961        metric: Metric,
962        top_k: usize,
963        filter_keys: Option<&[Key]>,
964    ) -> Result<Vec<SearchResult>> {
965        if top_k == 0 {
966            return Ok(Vec::new());
967        }
968
969        let profile = std::env::var_os("ALOPEX_PROFILE_SEARCH_SIMILAR").is_some();
970        let total_start = if profile {
971            Some(std::time::Instant::now())
972        } else {
973            None
974        };
975        let query_norm_sq = query_vector.iter().map(|v| v * v).sum::<f32>();
976        let query_norm = if matches!(metric, Metric::Cosine) {
977            query_norm_sq.sqrt()
978        } else {
979            0.0
980        };
981        let inv_query_norm = if query_norm == 0.0 {
982            0.0
983        } else {
984            1.0 / query_norm
985        };
986
987        if filter_keys.is_none() && self.txn_mode()? == TxnMode::ReadOnly {
988            let cache = self
989                .db
990                .vector_cache
991                .read()
992                .expect("vector cache lock poisoned");
993            if let Some(cache) = cache.as_ref() {
994                if cache.is_empty() {
995                    return Ok(Vec::new());
996                }
997                let keys_len = cache.len();
998                let mut rows = Vec::with_capacity(keys_len);
999                let mut score_time = std::time::Duration::ZERO;
1000                for (key, cached) in cache.iter() {
1001                    if cached.metric != metric {
1002                        return Err(Error::Core(alopex_core::Error::UnsupportedMetric {
1003                            metric: metric.as_str().to_string(),
1004                        }));
1005                    }
1006                    validate_dimensions(cached.vector.len(), query_vector.len())
1007                        .map_err(Error::Core)?;
1008                    let score_start = if profile {
1009                        Some(std::time::Instant::now())
1010                    } else {
1011                        None
1012                    };
1013                    let dot = dot_product(query_vector, &cached.vector);
1014                    let score = match metric {
1015                        Metric::Cosine => {
1016                            if cached.inv_norm == 0.0 || inv_query_norm == 0.0 {
1017                                0.0
1018                            } else {
1019                                dot * cached.inv_norm * inv_query_norm
1020                            }
1021                        }
1022                        Metric::L2 => {
1023                            let dist_sq = query_norm_sq + cached.norm_sq - 2.0 * dot;
1024                            -dist_sq.sqrt()
1025                        }
1026                        Metric::InnerProduct => dot,
1027                    };
1028                    if let Some(score_start) = score_start {
1029                        score_time += score_start.elapsed();
1030                    }
1031                    rows.push(SearchResult {
1032                        key: key.clone(),
1033                        metadata: cached.metadata.clone(),
1034                        score,
1035                    });
1036                }
1037
1038                let rows_total = rows.len();
1039                let sort_start = if profile {
1040                    Some(std::time::Instant::now())
1041                } else {
1042                    None
1043                };
1044                if rows.len() > top_k {
1045                    rows.select_nth_unstable_by(top_k - 1, |a, b| {
1046                        b.score.total_cmp(&a.score).then_with(|| a.key.cmp(&b.key))
1047                    });
1048                    rows.truncate(top_k);
1049                }
1050                rows.sort_by(|a, b| b.score.total_cmp(&a.score).then_with(|| a.key.cmp(&b.key)));
1051                if let (true, Some(total_start), Some(sort_start)) =
1052                    (profile, total_start, sort_start)
1053                {
1054                    let sort_time = sort_start.elapsed();
1055                    let total_time = total_start.elapsed();
1056                    eprintln!(
1057                        "alopex.search_similar keys={} results={} top_k={} load_keys_ms={:.2} get_ms={:.2} decode_ms={:.2} score_ms={:.2} sort_ms={:.2} total_ms={:.2}",
1058                        keys_len,
1059                        rows_total,
1060                        top_k,
1061                        0.0,
1062                        0.0,
1063                        0.0,
1064                        score_time.as_secs_f64() * 1000.0,
1065                        sort_time.as_secs_f64() * 1000.0,
1066                        total_time.as_secs_f64() * 1000.0
1067                    );
1068                }
1069                return Ok(rows);
1070            }
1071        }
1072
1073        let (keys, load_keys_time) = if profile {
1074            let start = std::time::Instant::now();
1075            let keys = match filter_keys {
1076                Some(keys) => keys.to_vec(),
1077                None => self.load_vector_index()?,
1078            };
1079            (keys, start.elapsed())
1080        } else {
1081            let keys = match filter_keys {
1082                Some(keys) => keys.to_vec(),
1083                None => self.load_vector_index()?,
1084            };
1085            (keys, std::time::Duration::ZERO)
1086        };
1087        if keys.is_empty() {
1088            return Ok(Vec::new());
1089        }
1090
1091        let keys_len = keys.len();
1092        let mut rows = Vec::with_capacity(keys.len());
1093        let txn = self.inner_mut()?;
1094        let mut get_time = std::time::Duration::ZERO;
1095        let mut decode_time = std::time::Duration::ZERO;
1096        let mut score_time = std::time::Duration::ZERO;
1097        if profile {
1098            for key in keys {
1099                let get_start = std::time::Instant::now();
1100                let Some(raw) = txn.get(&key).map_err(Error::Core)? else {
1101                    get_time += get_start.elapsed();
1102                    continue;
1103                };
1104                get_time += get_start.elapsed();
1105                let decode_start = std::time::Instant::now();
1106                let decoded = decode_vector_entry_view(&raw).map_err(Error::Core)?;
1107                decode_time += decode_start.elapsed();
1108                if decoded.metric != metric {
1109                    return Err(Error::Core(alopex_core::Error::UnsupportedMetric {
1110                        metric: metric.as_str().to_string(),
1111                    }));
1112                }
1113                validate_dimensions(decoded.dim, query_vector.len()).map_err(Error::Core)?;
1114                let score_start = std::time::Instant::now();
1115                let score =
1116                    score_from_bytes(metric, query_vector, query_norm, decoded.vector_bytes)?;
1117                score_time += score_start.elapsed();
1118                rows.push(SearchResult {
1119                    key,
1120                    metadata: decoded.metadata,
1121                    score,
1122                });
1123            }
1124        } else {
1125            for key in keys {
1126                let Some(raw) = txn.get(&key).map_err(Error::Core)? else {
1127                    continue;
1128                };
1129                let decoded = decode_vector_entry_view(&raw).map_err(Error::Core)?;
1130                if decoded.metric != metric {
1131                    return Err(Error::Core(alopex_core::Error::UnsupportedMetric {
1132                        metric: metric.as_str().to_string(),
1133                    }));
1134                }
1135                validate_dimensions(decoded.dim, query_vector.len()).map_err(Error::Core)?;
1136                let score =
1137                    score_from_bytes(metric, query_vector, query_norm, decoded.vector_bytes)?;
1138                rows.push(SearchResult {
1139                    key,
1140                    metadata: decoded.metadata,
1141                    score,
1142                });
1143            }
1144        }
1145
1146        let rows_total = rows.len();
1147        let sort_start = if profile {
1148            Some(std::time::Instant::now())
1149        } else {
1150            None
1151        };
1152        if rows.len() > top_k {
1153            rows.select_nth_unstable_by(top_k - 1, |a, b| {
1154                b.score.total_cmp(&a.score).then_with(|| a.key.cmp(&b.key))
1155            });
1156            rows.truncate(top_k);
1157        }
1158        rows.sort_by(|a, b| b.score.total_cmp(&a.score).then_with(|| a.key.cmp(&b.key)));
1159        if let (true, Some(total_start), Some(sort_start)) = (profile, total_start, sort_start) {
1160            let sort_time = sort_start.elapsed();
1161            let total_time = total_start.elapsed();
1162            eprintln!(
1163                "alopex.search_similar keys={} results={} top_k={} load_keys_ms={:.2} get_ms={:.2} decode_ms={:.2} score_ms={:.2} sort_ms={:.2} total_ms={:.2}",
1164                keys_len,
1165                rows_total,
1166                top_k,
1167                load_keys_time.as_secs_f64() * 1000.0,
1168                get_time.as_secs_f64() * 1000.0,
1169                decode_time.as_secs_f64() * 1000.0,
1170                score_time.as_secs_f64() * 1000.0,
1171                sort_time.as_secs_f64() * 1000.0,
1172                total_time.as_secs_f64() * 1000.0
1173            );
1174        }
1175        Ok(rows)
1176    }
1177
1178    fn load_vector_index(&mut self) -> Result<Vec<Key>> {
1179        let txn = self.inner_mut()?;
1180        let Some(raw) = txn.get(&VECTOR_INDEX_KEY.to_vec()).map_err(Error::Core)? else {
1181            return Ok(Vec::new());
1182        };
1183        decode_index(&raw).map_err(Error::Core)
1184    }
1185
1186    fn persist_vector_index(&mut self, keys: &[Key]) -> Result<()> {
1187        let txn = self.inner_mut()?;
1188        let encoded = encode_index(keys)?;
1189        txn.put(VECTOR_INDEX_KEY.to_vec(), encoded)
1190            .map_err(Error::Core)
1191    }
1192
1193    /// Commits the transaction, applying all changes.
1194    pub fn commit(mut self) -> Result<()> {
1195        {
1196            let txn = self.inner.as_mut().ok_or(Error::TxnCompleted)?;
1197            for (index, state) in self.hnsw_indices.values_mut() {
1198                index.commit_staged(txn, state).map_err(Error::Core)?;
1199            }
1200            let mut catalog = self.db.sql_catalog.write().expect("catalog lock poisoned");
1201            catalog
1202                .persist_overlay(txn, &self.overlay)
1203                .map_err(|err| Error::Sql(err.into()))?;
1204
1205            let vector_cache_invalidated = self.vector_cache_invalidated;
1206            let vector_cache_updates = std::mem::take(&mut self.vector_cache_updates);
1207            let vector_cache_deletes = std::mem::take(&mut self.vector_cache_deletes);
1208            if vector_cache_invalidated {
1209                let mut cache = self
1210                    .db
1211                    .vector_cache
1212                    .write()
1213                    .expect("vector cache lock poisoned");
1214                *cache = None;
1215            } else if !vector_cache_updates.is_empty() || !vector_cache_deletes.is_empty() {
1216                let needs_rebuild = {
1217                    let cache = self
1218                        .db
1219                        .vector_cache
1220                        .read()
1221                        .expect("vector cache lock poisoned");
1222                    cache.is_none()
1223                };
1224                if needs_rebuild {
1225                    let rebuilt = build_vector_cache_from_txn(txn).map_err(Error::Core)?;
1226                    let mut cache = self
1227                        .db
1228                        .vector_cache
1229                        .write()
1230                        .expect("vector cache lock poisoned");
1231                    *cache = Some(rebuilt);
1232                } else {
1233                    let mut cache = self
1234                        .db
1235                        .vector_cache
1236                        .write()
1237                        .expect("vector cache lock poisoned");
1238                    if let Some(cache) = cache.as_mut() {
1239                        for key in vector_cache_deletes {
1240                            cache.remove(&key);
1241                        }
1242                        for (key, cached) in vector_cache_updates {
1243                            cache.insert(key, cached);
1244                        }
1245                    }
1246                }
1247            }
1248        }
1249        let txn = self.inner.take().ok_or(Error::TxnCompleted)?;
1250        let hnsw_indices = std::mem::take(&mut self.hnsw_indices);
1251        txn.commit_self().map_err(Error::Core)?;
1252        if !hnsw_indices.is_empty() {
1253            let mut cache = self
1254                .db
1255                .hnsw_cache
1256                .write()
1257                .expect("hnsw cache lock poisoned");
1258            for (name, (index, _state)) in hnsw_indices {
1259                cache.insert(name, Arc::new(index));
1260            }
1261        }
1262
1263        // KV commit 成功後のみ、カタログにオーバーレイを適用する。
1264        let overlay = std::mem::take(&mut self.overlay);
1265        let catalog_modified = self.catalog_modified;
1266        let mut catalog = self.db.sql_catalog.write().expect("catalog lock poisoned");
1267        catalog.apply_overlay(overlay);
1268        drop(catalog); // Release lock before invalidating cache
1269                       // Invalidate table info cache only if DDL operations were performed
1270        if catalog_modified {
1271            self.db.invalidate_table_info_cache();
1272        }
1273        Ok(())
1274    }
1275
1276    /// トランザクションを消費せずにロールバックする(失敗時の再試行を可能にする)。
1277    pub fn rollback_in_place(&mut self) -> Result<()> {
1278        let txn = self.inner.as_mut().ok_or(Error::TxnCompleted)?;
1279        txn.rollback_in_place().map_err(Error::Core)?;
1280        for (index, state) in self.hnsw_indices.values_mut() {
1281            let _ = index.rollback(state);
1282        }
1283        self.hnsw_indices.clear();
1284        self.overlay = alopex_sql::catalog::CatalogOverlay::default();
1285        self.inner = None;
1286        Ok(())
1287    }
1288
1289    /// Rolls back the transaction, discarding all changes.
1290    pub fn rollback(mut self) -> Result<()> {
1291        if let Some(txn) = self.inner.take() {
1292            for (index, state) in self.hnsw_indices.values_mut() {
1293                let _ = index.rollback(state);
1294            }
1295            self.hnsw_indices.clear();
1296            txn.rollback_self().map_err(Error::Core)
1297        } else {
1298            Err(Error::TxnCompleted)
1299        }
1300    }
1301
1302    fn inner_mut(&mut self) -> Result<&mut AnyKVTransaction<'a>> {
1303        self.inner.as_mut().ok_or(Error::TxnCompleted)
1304    }
1305
1306    fn hnsw_entry_mut(&mut self, name: &str) -> Result<&mut (HnswIndex, HnswTransactionState)> {
1307        if !self.hnsw_indices.contains_key(name) {
1308            let index = {
1309                let txn = self.inner_mut()?;
1310                HnswIndex::load(name, txn).map_err(Error::Core)?
1311            };
1312            self.hnsw_indices
1313                .insert(name.to_string(), (index, HnswTransactionState::default()));
1314        }
1315        Ok(self.hnsw_indices.get_mut(name).unwrap())
1316    }
1317
1318    fn ensure_write_txn(&self) -> Result<()> {
1319        let txn = self.inner.as_ref().ok_or(Error::TxnCompleted)?;
1320        if txn.mode() != TxnMode::ReadWrite {
1321            return Err(Error::Core(alopex_core::Error::TxnReadOnly));
1322        }
1323        Ok(())
1324    }
1325}
1326
1327impl<'a> Drop for Transaction<'a> {
1328    fn drop(&mut self) {
1329        if let Some(txn) = self.inner.take() {
1330            for (index, state) in self.hnsw_indices.values_mut() {
1331                let _ = index.rollback(state);
1332            }
1333            self.hnsw_indices.clear();
1334            let _ = txn.rollback_self();
1335        }
1336    }
1337}
1338
1339fn metric_to_byte(metric: Metric) -> u8 {
1340    match metric {
1341        Metric::Cosine => 0,
1342        Metric::L2 => 1,
1343        Metric::InnerProduct => 2,
1344    }
1345}
1346
1347fn byte_to_metric(byte: u8) -> result::Result<Metric, alopex_core::Error> {
1348    match byte {
1349        0 => Ok(Metric::Cosine),
1350        1 => Ok(Metric::L2),
1351        2 => Ok(Metric::InnerProduct),
1352        other => Err(alopex_core::Error::UnsupportedMetric {
1353            metric: format!("unknown({other})"),
1354        }),
1355    }
1356}
1357
1358fn encode_vector_entry(vector_type: VectorType, metadata: &[u8], vector: &[f32]) -> Vec<u8> {
1359    let dim = vector_type.dim() as u32;
1360    let meta_len = metadata.len() as u32;
1361    let mut buf = Vec::with_capacity(1 + 4 + 4 + metadata.len() + std::mem::size_of_val(vector));
1362    buf.push(metric_to_byte(vector_type.metric()));
1363    buf.extend_from_slice(&dim.to_le_bytes());
1364    buf.extend_from_slice(&meta_len.to_le_bytes());
1365    buf.extend_from_slice(metadata);
1366    for v in vector {
1367        buf.extend_from_slice(&v.to_le_bytes());
1368    }
1369    buf
1370}
1371
1372struct DecodedEntry {
1373    metric: Metric,
1374    vector: Vec<f32>,
1375}
1376
1377#[derive(Clone)]
1378struct CachedVector {
1379    metric: Metric,
1380    metadata: Vec<u8>,
1381    vector: Vec<f32>,
1382    norm_sq: f32,
1383    inv_norm: f32,
1384}
1385
1386struct VectorEntryView<'a> {
1387    metric: Metric,
1388    dim: usize,
1389    metadata: Vec<u8>,
1390    vector_bytes: &'a [u8],
1391}
1392
1393fn decode_vector_entry(bytes: &[u8]) -> result::Result<DecodedEntry, alopex_core::Error> {
1394    if bytes.len() < 9 {
1395        return Err(alopex_core::Error::InvalidFormat(
1396            "vector entry too short".into(),
1397        ));
1398    }
1399    let metric = byte_to_metric(bytes[0])?;
1400    let dim = u32::from_le_bytes(bytes[1..5].try_into().unwrap()) as usize;
1401    let meta_len = u32::from_le_bytes(bytes[5..9].try_into().unwrap()) as usize;
1402
1403    let header = 9;
1404    let expected_len = header + meta_len + dim * std::mem::size_of::<f32>();
1405    if bytes.len() < expected_len {
1406        return Err(alopex_core::Error::InvalidFormat(
1407            "vector entry truncated".into(),
1408        ));
1409    }
1410
1411    let mut vector = Vec::with_capacity(dim);
1412    let vec_bytes = &bytes[header + meta_len..expected_len];
1413    for chunk in vec_bytes.chunks_exact(4) {
1414        vector.push(f32::from_le_bytes(chunk.try_into().unwrap()));
1415    }
1416
1417    Ok(DecodedEntry { metric, vector })
1418}
1419
1420fn decode_vector_entry_view(
1421    bytes: &[u8],
1422) -> result::Result<VectorEntryView<'_>, alopex_core::Error> {
1423    if bytes.len() < 9 {
1424        return Err(alopex_core::Error::InvalidFormat(
1425            "vector entry too short".into(),
1426        ));
1427    }
1428    let metric = byte_to_metric(bytes[0])?;
1429    let dim = u32::from_le_bytes(bytes[1..5].try_into().unwrap()) as usize;
1430    let meta_len = u32::from_le_bytes(bytes[5..9].try_into().unwrap()) as usize;
1431
1432    let header = 9;
1433    let expected_len = header + meta_len + dim * std::mem::size_of::<f32>();
1434    if bytes.len() < expected_len {
1435        return Err(alopex_core::Error::InvalidFormat(
1436            "vector entry truncated".into(),
1437        ));
1438    }
1439
1440    let metadata = bytes[header..header + meta_len].to_vec();
1441    let vector_bytes = &bytes[header + meta_len..expected_len];
1442
1443    Ok(VectorEntryView {
1444        metric,
1445        dim,
1446        metadata,
1447        vector_bytes,
1448    })
1449}
1450
1451fn vector_bytes_to_vec(bytes: &[u8]) -> Vec<f32> {
1452    let mut vector = Vec::with_capacity(bytes.len() / 4);
1453    for chunk in bytes.chunks_exact(4) {
1454        vector.push(f32::from_le_bytes(chunk.try_into().unwrap()));
1455    }
1456    vector
1457}
1458
1459fn cached_vector_from_entry(metric: Metric, metadata: Vec<u8>, vector: Vec<f32>) -> CachedVector {
1460    let norm_sq = vector.iter().map(|v| v * v).sum::<f32>();
1461    let inv_norm = if norm_sq == 0.0 {
1462        0.0
1463    } else {
1464        1.0 / norm_sq.sqrt()
1465    };
1466    CachedVector {
1467        metric,
1468        metadata,
1469        vector,
1470        norm_sq,
1471        inv_norm,
1472    }
1473}
1474
1475fn build_vector_cache_from_txn<'a>(
1476    txn: &mut AnyKVTransaction<'a>,
1477) -> result::Result<HashMap<Key, CachedVector>, alopex_core::Error> {
1478    let Some(raw) = txn.get(&VECTOR_INDEX_KEY.to_vec())? else {
1479        return Ok(HashMap::new());
1480    };
1481    let keys = decode_index(&raw)?;
1482    let mut cache = HashMap::with_capacity(keys.len());
1483    for key in keys {
1484        let Some(raw) = txn.get(&key)? else {
1485            continue;
1486        };
1487        let decoded = decode_vector_entry_view(&raw)?;
1488        let vector = vector_bytes_to_vec(decoded.vector_bytes);
1489        let cached = cached_vector_from_entry(decoded.metric, decoded.metadata, vector);
1490        cache.insert(key, cached);
1491    }
1492    Ok(cache)
1493}
1494
1495fn dot_product(query: &[f32], item: &[f32]) -> f32 {
1496    #[cfg(target_arch = "x86_64")]
1497    {
1498        if std::is_x86_feature_detected!("avx") {
1499            // SAFETY: guarded by runtime feature detection.
1500            unsafe {
1501                return dot_product_avx(query, item);
1502            }
1503        }
1504    }
1505    dot_product_scalar(query, item)
1506}
1507
1508fn dot_product_scalar(query: &[f32], item: &[f32]) -> f32 {
1509    query.iter().zip(item.iter()).map(|(q, v)| q * v).sum()
1510}
1511
1512#[cfg(target_arch = "x86_64")]
1513#[target_feature(enable = "avx")]
1514unsafe fn dot_product_avx(query: &[f32], item: &[f32]) -> f32 {
1515    use std::arch::x86_64::*;
1516
1517    let len = query.len();
1518    let mut i = 0;
1519    let mut acc = _mm256_setzero_ps();
1520    let q_ptr = query.as_ptr();
1521    let v_ptr = item.as_ptr();
1522    while i + 8 <= len {
1523        let q = _mm256_loadu_ps(q_ptr.add(i));
1524        let v = _mm256_loadu_ps(v_ptr.add(i));
1525        acc = _mm256_add_ps(acc, _mm256_mul_ps(q, v));
1526        i += 8;
1527    }
1528
1529    let mut tmp = [0f32; 8];
1530    _mm256_storeu_ps(tmp.as_mut_ptr(), acc);
1531    let mut sum = tmp.iter().sum::<f32>();
1532    while i < len {
1533        sum += *q_ptr.add(i) * *v_ptr.add(i);
1534        i += 1;
1535    }
1536    sum
1537}
1538
1539fn score_from_slice(metric: Metric, query: &[f32], query_norm: f32, item: &[f32]) -> f32 {
1540    match metric {
1541        Metric::Cosine => {
1542            if query_norm == 0.0 {
1543                return 0.0;
1544            }
1545            let mut dot = 0.0;
1546            let mut item_norm_sq = 0.0;
1547            for (q, v) in query.iter().zip(item.iter()) {
1548                dot += q * v;
1549                item_norm_sq += v * v;
1550            }
1551            let item_norm = item_norm_sq.sqrt();
1552            if item_norm == 0.0 {
1553                0.0
1554            } else {
1555                dot / (query_norm * item_norm)
1556            }
1557        }
1558        Metric::L2 => {
1559            let mut dist_sq = 0.0;
1560            for (q, v) in query.iter().zip(item.iter()) {
1561                let d = q - v;
1562                dist_sq += d * d;
1563            }
1564            -dist_sq.sqrt()
1565        }
1566        Metric::InnerProduct => query.iter().zip(item.iter()).map(|(q, v)| q * v).sum(),
1567    }
1568}
1569
1570fn score_from_bytes(
1571    metric: Metric,
1572    query: &[f32],
1573    query_norm: f32,
1574    vector_bytes: &[u8],
1575) -> result::Result<f32, alopex_core::Error> {
1576    let len = vector_bytes.len() / 4;
1577    #[cfg(target_endian = "little")]
1578    {
1579        let ptr = vector_bytes.as_ptr();
1580        if (ptr as usize).is_multiple_of(std::mem::align_of::<f32>()) {
1581            let items = unsafe { std::slice::from_raw_parts(ptr as *const f32, len) };
1582            return Ok(score_from_slice(metric, query, query_norm, items));
1583        }
1584    }
1585
1586    // Fallback for unaligned or big-endian targets: decode each f32 from bytes.
1587    let mut iter = vector_bytes.chunks_exact(4);
1588    let score = match metric {
1589        Metric::Cosine => {
1590            if query_norm == 0.0 {
1591                0.0
1592            } else {
1593                let mut dot = 0.0;
1594                let mut item_norm_sq = 0.0;
1595                for (q, chunk) in query.iter().zip(&mut iter) {
1596                    let v = f32::from_le_bytes(chunk.try_into().unwrap());
1597                    dot += q * v;
1598                    item_norm_sq += v * v;
1599                }
1600                let item_norm = item_norm_sq.sqrt();
1601                if item_norm == 0.0 {
1602                    0.0
1603                } else {
1604                    dot / (query_norm * item_norm)
1605                }
1606            }
1607        }
1608        Metric::L2 => {
1609            let mut dist_sq = 0.0;
1610            for (q, chunk) in query.iter().zip(&mut iter) {
1611                let v = f32::from_le_bytes(chunk.try_into().unwrap());
1612                let d = q - v;
1613                dist_sq += d * d;
1614            }
1615            -dist_sq.sqrt()
1616        }
1617        Metric::InnerProduct => query
1618            .iter()
1619            .zip(&mut iter)
1620            .map(|(q, chunk)| q * f32::from_le_bytes(chunk.try_into().unwrap()))
1621            .sum(),
1622    };
1623    Ok(score)
1624}
1625
1626fn encode_index(keys: &[Key]) -> result::Result<Vec<u8>, alopex_core::Error> {
1627    let mut buf = Vec::new();
1628    let count = keys.len() as u32;
1629    buf.extend_from_slice(&count.to_le_bytes());
1630    for key in keys {
1631        let len: u32 = key
1632            .len()
1633            .try_into()
1634            .map_err(|_| alopex_core::Error::InvalidFormat("key too long".into()))?;
1635        buf.extend_from_slice(&len.to_le_bytes());
1636        buf.extend_from_slice(key);
1637    }
1638    Ok(buf)
1639}
1640
1641fn decode_index(bytes: &[u8]) -> result::Result<Vec<Key>, alopex_core::Error> {
1642    if bytes.len() < 4 {
1643        return Err(alopex_core::Error::InvalidFormat("index too short".into()));
1644    }
1645    let count = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
1646    let mut pos = 4;
1647    let mut keys = Vec::with_capacity(count);
1648    for _ in 0..count {
1649        if pos + 4 > bytes.len() {
1650            return Err(alopex_core::Error::InvalidFormat("index truncated".into()));
1651        }
1652        let len = u32::from_le_bytes(bytes[pos..pos + 4].try_into().unwrap()) as usize;
1653        pos += 4;
1654        if pos + len > bytes.len() {
1655            return Err(alopex_core::Error::InvalidFormat(
1656                "index key truncated".into(),
1657            ));
1658        }
1659        keys.push(bytes[pos..pos + len].to_vec());
1660        pos += len;
1661    }
1662    Ok(keys)
1663}
1664
1665#[cfg(test)]
1666mod tests {
1667    use super::*;
1668    use std::sync::mpsc;
1669    use std::thread;
1670    use tempfile::tempdir;
1671
1672    #[test]
1673    fn test_open_and_crud() {
1674        let dir = tempdir().unwrap();
1675        let path = dir.path().join("test.db");
1676        let db = Database::open(&path).unwrap();
1677
1678        let mut txn = db.begin(TxnMode::ReadWrite).unwrap();
1679        txn.put(b"key1", b"value1").unwrap();
1680        txn.commit().unwrap();
1681
1682        let mut txn2 = db.begin(TxnMode::ReadOnly).unwrap();
1683        let val = txn2.get(b"key1").unwrap();
1684        assert_eq!(val, Some(b"value1".to_vec()));
1685    }
1686
1687    #[test]
1688    fn test_not_found() {
1689        let db = Database::new();
1690        let mut txn = db.begin(TxnMode::ReadOnly).unwrap();
1691        let val = txn.get(b"non-existent-key").unwrap();
1692        assert!(val.is_none());
1693    }
1694
1695    #[cfg(not(target_arch = "wasm32"))]
1696    #[test]
1697    fn test_file_format_version_reads_alopex_header() {
1698        use alopex_core::storage::format::{AlopexFileWriter, FileFlags, FileVersion};
1699
1700        let dir = tempdir().unwrap();
1701        let path = dir.path().join("format-test.alopex");
1702        let expected = FileVersion::new(0, 0, 1);
1703
1704        let writer = AlopexFileWriter::new(path.clone(), expected, FileFlags(0)).unwrap();
1705        writer.finalize().unwrap();
1706
1707        let db = Database::open(&path).unwrap();
1708        assert_eq!(db.file_format_version(), expected);
1709    }
1710
1711    #[test]
1712    fn test_crash_recovery_replays_wal() {
1713        let dir = tempdir().unwrap();
1714        let path = dir.path().join("replay.db");
1715
1716        {
1717            let db = Database::open(&path).unwrap();
1718            let mut txn = db.begin(TxnMode::ReadWrite).unwrap();
1719            txn.put(b"k1", b"v1").unwrap();
1720            txn.commit().unwrap();
1721
1722            let mut uncommitted = db.begin(TxnMode::ReadWrite).unwrap();
1723            uncommitted.put(b"k2", b"v2").unwrap();
1724            // Drop without commit to simulate crash before commit.
1725        }
1726
1727        let db = Database::open(&path).unwrap();
1728        let mut txn = db.begin(TxnMode::ReadOnly).unwrap();
1729        assert_eq!(txn.get(b"k1").unwrap(), Some(b"v1".to_vec()));
1730        assert_eq!(txn.get(b"k2").unwrap(), None);
1731    }
1732
1733    #[test]
1734    fn test_txn_closed() {
1735        let db = Database::new();
1736        let mut txn = db.begin(TxnMode::ReadWrite).unwrap();
1737        txn.put(b"k1", b"v1").unwrap();
1738        txn.commit().unwrap();
1739        // The `commit` call consumes the transaction, so we can't call it again.
1740        // This test verifies that we can't use a transaction after it's been completed.
1741        // The `inner_mut` method will return `Error::TxnCompleted`.
1742        // This is a compile-time check in practice, but we can't write a test that fails to compile.
1743        // The logic is sound.
1744    }
1745
1746    #[test]
1747    fn test_concurrency_conflict() {
1748        let db = std::sync::Arc::new(Database::new());
1749        let mut t0 = db.begin(TxnMode::ReadWrite).unwrap();
1750        t0.put(b"k1", b"v0").unwrap();
1751        t0.commit().unwrap();
1752
1753        let (tx1, rx1) = mpsc::channel();
1754        let (tx2, rx2) = mpsc::channel();
1755
1756        let db1 = db.clone();
1757        let t1 = thread::spawn(move || {
1758            let mut txn1 = db1.begin(TxnMode::ReadWrite).unwrap();
1759            let val = txn1.get(b"k1").unwrap();
1760            assert_eq!(val.unwrap(), b"v0");
1761            tx1.send(()).unwrap();
1762            rx2.recv().unwrap();
1763            txn1.put(b"k1", b"v1").unwrap();
1764            let result = txn1.commit();
1765            assert!(matches!(
1766                result,
1767                Err(Error::Core(alopex_core::Error::TxnConflict))
1768            ));
1769        });
1770
1771        let db2 = db.clone();
1772        let t2 = thread::spawn(move || {
1773            rx1.recv().unwrap();
1774            let mut txn2 = db2.begin(TxnMode::ReadWrite).unwrap();
1775            txn2.put(b"k1", b"v2").unwrap();
1776            assert!(txn2.commit().is_ok());
1777            tx2.send(()).unwrap();
1778        });
1779
1780        t1.join().unwrap();
1781        t2.join().unwrap();
1782
1783        let mut txn3 = db.begin(TxnMode::ReadOnly).unwrap();
1784        let val = txn3.get(b"k1").unwrap();
1785        assert_eq!(val.unwrap(), b"v2");
1786    }
1787
1788    #[test]
1789    fn test_flush_and_reopen_via_embedded_api() {
1790        let dir = tempdir().unwrap();
1791        let path = dir.path().join("persist.db");
1792        {
1793            let db = Database::open(&path).unwrap();
1794            let mut txn = db.begin(TxnMode::ReadWrite).unwrap();
1795            txn.put(b"k1", b"v1").unwrap();
1796            txn.commit().unwrap();
1797            db.flush().unwrap();
1798        }
1799
1800        let db = Database::open(&path).unwrap();
1801        let mut txn = db.begin(TxnMode::ReadOnly).unwrap();
1802        assert_eq!(txn.get(b"k1").unwrap(), Some(b"v1".to_vec()));
1803    }
1804
1805    #[test]
1806    fn test_large_value_blob_roundtrip() {
1807        let dir = tempdir().unwrap();
1808        let path = dir.path().join("blob.lv");
1809        let payload = b"hello large value";
1810
1811        {
1812            let db = Database::new();
1813            let mut writer = db
1814                .create_blob_writer(&path, payload.len() as u64, Some(16))
1815                .unwrap();
1816            writer.write_chunk(&payload[..5]).unwrap();
1817            writer.write_chunk(&payload[5..]).unwrap();
1818            writer.finish().unwrap();
1819        }
1820
1821        let db = Database::new();
1822        let mut reader = db.open_large_value(&path).unwrap();
1823        let mut buf = Vec::new();
1824        while let Some((_info, chunk)) = reader.next_chunk().unwrap() {
1825            buf.extend_from_slice(&chunk);
1826        }
1827        assert_eq!(buf, payload);
1828    }
1829
1830    #[test]
1831    fn upsert_and_search_same_txn() {
1832        let db = Database::new();
1833        let mut txn = db.begin(TxnMode::ReadWrite).unwrap();
1834        txn.upsert_vector(b"k1", b"meta1", &[1.0, 0.0], Metric::Cosine)
1835            .unwrap();
1836
1837        let results = txn
1838            .search_similar(&[1.0, 0.0], Metric::Cosine, 1, None)
1839            .unwrap();
1840        assert_eq!(results.len(), 1);
1841        assert_eq!(results[0].key, b"k1");
1842        assert_eq!(results[0].metadata, b"meta1");
1843        txn.commit().unwrap();
1844    }
1845
1846    #[test]
1847    fn upsert_and_search_across_txn() {
1848        let db = Database::new();
1849        {
1850            let mut txn = db.begin(TxnMode::ReadWrite).unwrap();
1851            txn.upsert_vector(b"k1", b"meta1", &[1.0, 1.0], Metric::Cosine)
1852                .unwrap();
1853            txn.commit().unwrap();
1854        }
1855
1856        let mut ro = db.begin(TxnMode::ReadOnly).unwrap();
1857        let results = ro
1858            .search_similar(&[1.0, 1.0], Metric::Cosine, 1, None)
1859            .unwrap();
1860        assert_eq!(results.len(), 1);
1861        assert_eq!(results[0].key, b"k1");
1862    }
1863
1864    #[test]
1865    fn read_only_upsert_rejected() {
1866        let db = Database::new();
1867        let mut ro = db.begin(TxnMode::ReadOnly).unwrap();
1868        let err = ro
1869            .upsert_vector(b"k1", b"m", &[1.0, 0.0], Metric::Cosine)
1870            .unwrap_err();
1871        assert!(matches!(err, Error::Core(alopex_core::Error::TxnReadOnly)));
1872    }
1873
1874    #[test]
1875    fn dimension_mismatch_on_search() {
1876        let db = Database::new();
1877        {
1878            let mut txn = db.begin(TxnMode::ReadWrite).unwrap();
1879            txn.upsert_vector(b"k1", b"m", &[1.0, 0.0], Metric::Cosine)
1880                .unwrap();
1881            txn.commit().unwrap();
1882        }
1883        let mut ro = db.begin(TxnMode::ReadOnly).unwrap();
1884        let err = ro
1885            .search_similar(&[1.0, 0.0, 1.0], Metric::Cosine, 1, None)
1886            .unwrap_err();
1887        assert!(matches!(
1888            err,
1889            Error::Core(alopex_core::Error::DimensionMismatch { .. })
1890        ));
1891    }
1892}