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