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 and converges the
498    /// database into its single `.alopex` file.
499    ///
500    /// After this returns, a disk database opened through an `X.alopex` path has a
501    /// complete, self-contained `X.alopex`: copying that one file elsewhere and
502    /// reopening it restores every committed row. The `X.alopex.d` working
503    /// directory is deliberately kept while the handle is alive, which is the
504    /// documented two-file steady state during operation.
505    pub fn flush(&self) -> Result<()> {
506        self.store.flush().map_err(Error::Core)?;
507        self.store.converge().map_err(Error::Core)
508    }
509
510    /// Converges the database into its single `.alopex` file without closing it.
511    pub fn converge(&self) -> Result<()> {
512        self.store.converge().map_err(Error::Core)
513    }
514
515    /// Converges and marks the database closed.
516    ///
517    /// Idempotent, and the error-visible counterpart to dropping the handle: the
518    /// `Drop` path also converges, but cannot report a failure.
519    pub fn close(&self) -> Result<()> {
520        self.store.close().map_err(Error::Core)
521    }
522
523    /// Returns the single `.alopex` file this database converges into, if any.
524    ///
525    /// `None` for in-memory databases and for plain-directory disk databases,
526    /// which keep the classic multi-file layout.
527    pub fn container_path(&self) -> Option<PathBuf> {
528        self.store.container_path().map(Path::to_path_buf)
529    }
530
531    /// Returns the file format version supported by the embedded engine.
532    pub fn file_format_version(&self) -> alopex_core::storage::format::FileVersion {
533        use alopex_core::storage::format::FileVersion;
534
535        self.store
536            .file_format_storage_dir()
537            .map_or(FileVersion::CURRENT, read_file_version_from_storage)
538    }
539
540    /// Returns current memory usage statistics (in-memory KV only).
541    pub fn memory_usage(&self) -> Option<MemoryStats> {
542        match self.store.as_ref() {
543            AnyKV::Memory(kv) => Some(kv.memory_stats()),
544            _ => None,
545        }
546    }
547
548    /// Persists the current in-memory database to disk atomically.
549    ///
550    /// `wal_path` は「データディレクトリ」として扱う(file-mode)。When it carries the
551    /// `.alopex` extension the result is a **real, self-contained container** at that
552    /// path plus its `X.alopex.d` working directory — never the zero-byte existence
553    /// marker earlier releases wrote there.
554    pub fn persist_to_disk(&self, wal_path: &Path) -> Result<()> {
555        if !matches!(self.store.as_ref(), AnyKV::Memory(_)) {
556            return Err(Error::NotInMemoryMode);
557        }
558        let data_dir = disk_data_dir_path(wal_path);
559        if wal_path.exists() || data_dir.exists() {
560            return Err(Error::Core(alopex_core::Error::PathExists(
561                wal_path.to_path_buf(),
562            )));
563        }
564
565        // `X.alopex.d.tmp`, not `X.alopex.tmp`: the container writer already claims
566        // `X.alopex.tmp` for its own atomic-rename staging file, and a directory
567        // sitting on that name makes the container write fail.
568        let tmp_dir = data_dir.with_extension("d.tmp");
569        if tmp_dir.exists() {
570            return Err(Error::Core(alopex_core::Error::PathExists(tmp_dir)));
571        }
572
573        let snapshot = self.snapshot_pairs()?;
574        let is_container_path = wal_path.extension().is_some_and(|e| e == "alopex");
575        let write_result = (|| -> Result<()> {
576            // Converge straight into the caller's `.alopex` path while the data still
577            // lives in the temp directory, so the container and the working directory
578            // become visible together.
579            let converge = if is_container_path {
580                alopex_core::lsm::ConvergePolicy::Always {
581                    container: wal_path.to_path_buf(),
582                }
583            } else {
584                alopex_core::lsm::ConvergePolicy::Never
585            };
586            let config = alopex_core::lsm::LsmKVConfig {
587                converge,
588                // The working directory is renamed into place right after this
589                // closure returns, so it must survive the store being dropped.
590                prune_sidecar_on_drop: false,
591                ..alopex_core::lsm::LsmKVConfig::default()
592            };
593
594            let store = StorageFactory::create(alopex_core::StorageMode::Disk {
595                path: tmp_dir.clone(),
596                config: Some(config),
597            })
598            .map_err(Error::Core)?;
599
600            let mut txn = store.begin(TxnMode::ReadWrite).map_err(Error::Core)?;
601            for (key, value) in snapshot {
602                txn.put(key, value).map_err(Error::Core)?;
603            }
604            txn.commit_self().map_err(Error::Core)?;
605            store.converge().map_err(Error::Core)?;
606
607            Ok(())
608        })();
609
610        if let Err(e) = write_result {
611            let _ = fs::remove_dir_all(&tmp_dir);
612            let _ = fs::remove_file(wal_path);
613            return Err(e);
614        }
615
616        if let Err(e) = fs::rename(&tmp_dir, &data_dir) {
617            let _ = fs::remove_dir_all(&tmp_dir);
618            let _ = fs::remove_file(wal_path);
619            return Err(Error::Core(e.into()));
620        }
621        Ok(())
622    }
623
624    /// Creates a fully in-memory clone of the current database.
625    pub fn clone_to_memory(&self) -> Result<Self> {
626        let snapshot = self.snapshot_pairs()?;
627        let cloned = Database::open_in_memory()?;
628        if snapshot.is_empty() {
629            return Ok(cloned);
630        }
631
632        let mut txn = cloned.begin(TxnMode::ReadWrite)?;
633        for (key, value) in snapshot {
634            txn.put(&key, &value)?;
635        }
636        txn.commit()?;
637        Ok(cloned)
638    }
639
640    /// Clears all data while keeping the database usable.
641    pub fn clear(&self) -> Result<()> {
642        let keys: Vec<Key> = self.snapshot_pairs()?.into_iter().map(|(k, _)| k).collect();
643        if keys.is_empty() {
644            return Ok(());
645        }
646        let mut txn = self.begin(TxnMode::ReadWrite)?;
647        for key in keys {
648            txn.delete(&key)?;
649        }
650        txn.commit()
651    }
652
653    /// Updates the memory limit in bytes for the underlying in-memory store.
654    pub fn set_memory_limit(&self, bytes: Option<usize>) {
655        if let AnyKV::Memory(kv) = self.store.as_ref() {
656            kv.txn_manager().set_memory_limit(bytes);
657        }
658    }
659
660    /// Returns a read-only snapshot of all key-value pairs.
661    pub fn snapshot(&self) -> Vec<(Key, Vec<u8>)> {
662        self.snapshot_pairs().unwrap_or_default()
663    }
664
665    fn snapshot_pairs(&self) -> Result<Vec<(Key, Vec<u8>)>> {
666        let mut txn = self.store.begin(TxnMode::ReadOnly).map_err(Error::Core)?;
667        let pairs: Vec<(Key, Vec<u8>)> = txn.scan_prefix(b"").map_err(Error::Core)?.collect();
668        txn.commit_self().map_err(Error::Core)?;
669        Ok(pairs)
670    }
671
672    /// HNSW インデックスを作成し、永続化する。
673    pub fn create_hnsw_index(&self, name: &str, config: HnswConfig) -> Result<()> {
674        let mut txn = self.store.begin(TxnMode::ReadWrite).map_err(Error::Core)?;
675        let index = HnswIndex::create(name, config).map_err(Error::Core)?;
676        index.save(&mut txn).map_err(Error::Core)?;
677        txn.commit_self().map_err(Error::Core)?;
678        self.hnsw_cache_insert(name, index);
679        Ok(())
680    }
681
682    /// HNSW インデックスを削除する。
683    pub fn drop_hnsw_index(&self, name: &str) -> Result<()> {
684        let mut txn = self.store.begin(TxnMode::ReadWrite).map_err(Error::Core)?;
685        let index = HnswIndex::load(name, &mut txn).map_err(Error::Core)?;
686        index.drop(&mut txn).map_err(Error::Core)?;
687        txn.commit_self().map_err(Error::Core)?;
688        self.hnsw_cache_remove(name);
689        Ok(())
690    }
691
692    /// HNSW 統計情報を取得する。
693    pub fn get_hnsw_stats(&self, name: &str) -> Result<HnswStats> {
694        if let Some(index) = self.hnsw_cache_get(name) {
695            return Ok(index.stats());
696        }
697        let mut txn = self.store.begin(TxnMode::ReadOnly).map_err(Error::Core)?;
698        let index = HnswIndex::load(name, &mut txn).map_err(Error::Core)?;
699        let stats = index.stats();
700        self.hnsw_cache_insert(name, index);
701        Ok(stats)
702    }
703
704    /// HNSW インデックスをコンパクションし、結果を返す。
705    pub fn compact_hnsw_index(&self, name: &str) -> Result<alopex_core::vector::CompactionResult> {
706        let mut txn = self.store.begin(TxnMode::ReadWrite).map_err(Error::Core)?;
707        let mut index = HnswIndex::load(name, &mut txn).map_err(Error::Core)?;
708        let result = index.compact().map_err(Error::Core)?;
709        index.save(&mut txn).map_err(Error::Core)?;
710        txn.commit_self().map_err(Error::Core)?;
711        self.hnsw_cache_insert(name, index);
712        Ok(result)
713    }
714
715    /// HNSW インデックスに検索を行う。
716    pub fn search_hnsw(
717        &self,
718        name: &str,
719        query: &[f32],
720        k: usize,
721        ef_search: Option<usize>,
722    ) -> Result<(Vec<HnswSearchResult>, HnswSearchStats)> {
723        let profile = std::env::var_os("ALOPEX_PROFILE_HNSW").is_some();
724        let total_start = if profile {
725            Some(std::time::Instant::now())
726        } else {
727            None
728        };
729        if let Some(index) = self.hnsw_cache_get(name) {
730            let search_start = if profile {
731                Some(std::time::Instant::now())
732            } else {
733                None
734            };
735            let result = index.search(query, k, ef_search).map_err(Error::Core)?;
736            if let (true, Some(total_start), Some(search_start)) =
737                (profile, total_start, search_start)
738            {
739                let search_time = search_start.elapsed();
740                let total_time = total_start.elapsed();
741                eprintln!(
742                    "alopex.hnsw_search cache=hit name={} k={} ef_search={:?} search_ms={:.2} total_ms={:.2}",
743                    name,
744                    k,
745                    ef_search,
746                    search_time.as_secs_f64() * 1000.0,
747                    total_time.as_secs_f64() * 1000.0
748                );
749            }
750            return Ok(result);
751        }
752
753        let load_start = if profile {
754            Some(std::time::Instant::now())
755        } else {
756            None
757        };
758        let mut txn = self.store.begin(TxnMode::ReadOnly).map_err(Error::Core)?;
759        let index = HnswIndex::load(name, &mut txn).map_err(Error::Core)?;
760        let load_time = load_start.map(|start| start.elapsed());
761        let index = self.hnsw_cache_insert(name, index);
762        let search_start = if profile {
763            Some(std::time::Instant::now())
764        } else {
765            None
766        };
767        let result = index.search(query, k, ef_search).map_err(Error::Core)?;
768        if let (true, Some(total_start), Some(search_start)) = (profile, total_start, search_start)
769        {
770            let search_time = search_start.elapsed();
771            let total_time = total_start.elapsed();
772            let load_time_ms = load_time
773                .map(|elapsed| elapsed.as_secs_f64() * 1000.0)
774                .unwrap_or(0.0);
775            eprintln!(
776                "alopex.hnsw_search cache=miss name={} k={} ef_search={:?} load_ms={:.2} search_ms={:.2} total_ms={:.2}",
777                name,
778                k,
779                ef_search,
780                load_time_ms,
781                search_time.as_secs_f64() * 1000.0,
782                total_time.as_secs_f64() * 1000.0
783            );
784        }
785        Ok(result)
786    }
787
788    /// Creates a chunked large value writer for opaque blobs (beta).
789    pub fn create_blob_writer(
790        &self,
791        path: &Path,
792        total_len: u64,
793        chunk_size: Option<u32>,
794    ) -> Result<LargeValueWriter> {
795        let meta = LargeValueMeta {
796            kind: LargeValueKind::Blob,
797            total_len,
798            chunk_size: chunk_size.unwrap_or(DEFAULT_CHUNK_SIZE),
799        };
800        LargeValueWriter::create(path, meta).map_err(Error::Core)
801    }
802
803    /// Creates a chunked large value writer for typed payloads (beta).
804    pub fn create_typed_writer(
805        &self,
806        path: &Path,
807        type_id: u16,
808        total_len: u64,
809        chunk_size: Option<u32>,
810    ) -> Result<LargeValueWriter> {
811        let meta = LargeValueMeta {
812            kind: LargeValueKind::Typed(type_id),
813            total_len,
814            chunk_size: chunk_size.unwrap_or(DEFAULT_CHUNK_SIZE),
815        };
816        LargeValueWriter::create(path, meta).map_err(Error::Core)
817    }
818
819    /// Opens a chunked large value reader (beta). Kind/type is read from the file header.
820    pub fn open_large_value(&self, path: &Path) -> Result<LargeValueReader> {
821        LargeValueReader::open(path).map_err(Error::Core)
822    }
823
824    /// Begins a new transaction.
825    pub fn begin(&self, mode: TxnMode) -> Result<Transaction<'_>> {
826        let mut txn = self.store.begin(mode).map_err(Error::Core)?;
827        let journal = if mode == TxnMode::ReadWrite
828            && self.store.range_change_journal_capability()
829                == RangeChangeJournalCapability::Supported
830        {
831            let scope = {
832                let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
833                sql_api::local_journal_scope(&*catalog)
834            };
835            Some(LocalRangeChangeJournal::capture(&mut txn, scope).map_err(Error::Core)?)
836        } else {
837            None
838        };
839        Ok(Transaction {
840            inner: Some(txn),
841            db: self,
842            hnsw_indices: HashMap::new(),
843            overlay: alopex_sql::catalog::CatalogOverlay::new(),
844            vector_cache_updates: HashMap::new(),
845            vector_cache_deletes: Vec::new(),
846            vector_cache_invalidated: false,
847            catalog_modified: false,
848            journal,
849        })
850    }
851}
852
853impl Default for Database {
854    fn default() -> Self {
855        Self::new()
856    }
857}
858
859/// A database transaction.
860pub struct Transaction<'a> {
861    inner: Option<AnyKVTransaction<'a>>,
862    db: &'a Database,
863    hnsw_indices: HashMap<String, (HnswIndex, alopex_core::vector::hnsw::HnswTransactionState)>,
864    overlay: alopex_sql::catalog::CatalogOverlay,
865    vector_cache_updates: HashMap<Key, CachedVector>,
866    vector_cache_deletes: Vec<Key>,
867    vector_cache_invalidated: bool,
868    /// Whether DDL operations were performed in this transaction.
869    pub(crate) catalog_modified: bool,
870    /// SQL row/index state captured before a read-write local transaction.
871    journal: Option<LocalRangeChangeJournal>,
872}
873
874/// A search result row containing key, metadata, and similarity score.
875#[derive(Debug, Clone, PartialEq)]
876pub struct SearchResult {
877    /// User key associated with the vector.
878    pub key: Key,
879    /// Opaque metadata payload stored alongside the vector.
880    pub metadata: Vec<u8>,
881    /// Similarity score for the query/vector pair.
882    pub score: f32,
883}
884
885const VECTOR_INDEX_KEY: &[u8] = b"__alopex_vector_index";
886
887impl<'a> Transaction<'a> {
888    pub(crate) fn catalog_overlay(&self) -> &alopex_sql::catalog::CatalogOverlay {
889        &self.overlay
890    }
891
892    pub(crate) fn catalog_overlay_mut(&mut self) -> &mut alopex_sql::catalog::CatalogOverlay {
893        &mut self.overlay
894    }
895
896    pub(crate) fn txn_mode(&self) -> Result<TxnMode> {
897        let txn = self.inner.as_ref().ok_or(Error::TxnCompleted)?;
898        Ok(txn.mode())
899    }
900    /// Retrieves the value for a given key.
901    pub fn get(&mut self, key: &[u8]) -> Result<Option<Vec<u8>>> {
902        self.inner_mut()?.get(&key.to_vec()).map_err(Error::Core)
903    }
904
905    /// Sets a value for a given key.
906    pub fn put(&mut self, key: &[u8], value: &[u8]) -> Result<()> {
907        self.vector_cache_invalidated = true;
908        self.vector_cache_updates.clear();
909        self.vector_cache_deletes.clear();
910        self.inner_mut()?
911            .put(key.to_vec(), value.to_vec())
912            .map_err(Error::Core)
913    }
914
915    /// Deletes a key-value pair.
916    pub fn delete(&mut self, key: &[u8]) -> Result<()> {
917        self.vector_cache_deletes.push(key.to_vec());
918        self.inner_mut()?.delete(key.to_vec()).map_err(Error::Core)
919    }
920
921    /// Scans all key-value pairs whose keys start with the given prefix.
922    ///
923    /// Returns an iterator over (key, value) pairs.
924    pub fn scan_prefix(
925        &mut self,
926        prefix: &[u8],
927    ) -> Result<Box<dyn Iterator<Item = (Key, Vec<u8>)> + '_>> {
928        self.inner_mut()?.scan_prefix(prefix).map_err(Error::Core)
929    }
930
931    /// HNSW にベクトルをステージング挿入/更新する。
932    pub fn upsert_to_hnsw(
933        &mut self,
934        index_name: &str,
935        key: &[u8],
936        vector: &[f32],
937        metadata: &[u8],
938    ) -> Result<()> {
939        self.ensure_write_txn()?;
940        let (index, state) = self.hnsw_entry_mut(index_name)?;
941        index
942            .upsert_staged(key, vector, metadata, state)
943            .map_err(Error::Core)
944    }
945
946    /// HNSW からキーをステージング削除する。
947    pub fn delete_from_hnsw(&mut self, index_name: &str, key: &[u8]) -> Result<bool> {
948        self.ensure_write_txn()?;
949        let (index, state) = self.hnsw_entry_mut(index_name)?;
950        index.delete_staged(key, state).map_err(Error::Core)
951    }
952
953    /// Upserts a vector and metadata under the provided key after validating dimensions and metric.
954    ///
955    /// A small internal index is maintained to enable scanning for similarity search.
956    pub fn upsert_vector(
957        &mut self,
958        key: &[u8],
959        metadata: &[u8],
960        vector: &[f32],
961        metric: Metric,
962    ) -> Result<()> {
963        if vector.is_empty() {
964            return Err(Error::Core(alopex_core::Error::InvalidFormat(
965                "vector cannot be empty".into(),
966            )));
967        }
968        let vt = VectorType::new(vector.len(), metric);
969        vt.validate(vector).map_err(Error::Core)?;
970
971        let payload = encode_vector_entry(vt, metadata, vector);
972        let txn = self.inner_mut()?;
973        txn.put(key.to_vec(), payload).map_err(Error::Core)?;
974
975        let mut keys = self.load_vector_index()?;
976        if !keys.iter().any(|k| k == key) {
977            keys.push(key.to_vec());
978            self.persist_vector_index(&keys)?;
979        }
980
981        let cached = cached_vector_from_entry(metric, metadata.to_vec(), vector.to_vec());
982        self.vector_cache_updates.insert(key.to_vec(), cached);
983        self.vector_cache_deletes.retain(|k| k != key);
984        Ok(())
985    }
986
987    /// Retrieves a vector stored under the given key.
988    ///
989    /// Returns `None` if the key does not exist. If the key exists but has a different
990    /// metric than specified, returns an error.
991    pub fn get_vector(&mut self, key: &[u8], metric: Metric) -> Result<Option<Vec<f32>>> {
992        let txn = self.inner_mut()?;
993        let key_vec = key.to_vec();
994        let Some(raw) = txn.get(&key_vec).map_err(Error::Core)? else {
995            return Ok(None);
996        };
997        let decoded = decode_vector_entry(&raw).map_err(Error::Core)?;
998        if decoded.metric != metric {
999            return Err(Error::Core(alopex_core::Error::UnsupportedMetric {
1000                metric: metric.as_str().to_string(),
1001            }));
1002        }
1003        Ok(Some(decoded.vector))
1004    }
1005
1006    /// Retrieves vectors stored under the given keys in a single transaction call.
1007    ///
1008    /// Returned list order matches the input `keys` order. Each entry is:
1009    /// - `None` if the key does not exist
1010    /// - `Some(Vec<f32>)` if the key exists and metric matches
1011    ///
1012    /// If any existing entry has a different metric than specified, returns an error.
1013    pub fn get_vectors(&mut self, keys: &[Key], metric: Metric) -> Result<Vec<Option<Vec<f32>>>> {
1014        let txn = self.inner_mut()?;
1015        let mut out = Vec::with_capacity(keys.len());
1016        for key in keys {
1017            let Some(raw) = txn.get(key).map_err(Error::Core)? else {
1018                out.push(None);
1019                continue;
1020            };
1021            let decoded = decode_vector_entry(&raw).map_err(Error::Core)?;
1022            if decoded.metric != metric {
1023                return Err(Error::Core(alopex_core::Error::UnsupportedMetric {
1024                    metric: metric.as_str().to_string(),
1025                }));
1026            }
1027            out.push(Some(decoded.vector));
1028        }
1029        Ok(out)
1030    }
1031
1032    /// Executes a flat similarity search over stored vectors using the provided metric and query.
1033    ///
1034    /// The optional `filter_keys` restricts the scan to the given keys; otherwise the full
1035    /// vector index is scanned. Results are sorted by descending score and truncated to `top_k`.
1036    pub fn search_similar(
1037        &mut self,
1038        query_vector: &[f32],
1039        metric: Metric,
1040        top_k: usize,
1041        filter_keys: Option<&[Key]>,
1042    ) -> Result<Vec<SearchResult>> {
1043        if top_k == 0 {
1044            return Ok(Vec::new());
1045        }
1046
1047        let profile = std::env::var_os("ALOPEX_PROFILE_SEARCH_SIMILAR").is_some();
1048        let total_start = if profile {
1049            Some(std::time::Instant::now())
1050        } else {
1051            None
1052        };
1053        let query_norm_sq = query_vector.iter().map(|v| v * v).sum::<f32>();
1054        let query_norm = if matches!(metric, Metric::Cosine) {
1055            query_norm_sq.sqrt()
1056        } else {
1057            0.0
1058        };
1059        let inv_query_norm = if query_norm == 0.0 {
1060            0.0
1061        } else {
1062            1.0 / query_norm
1063        };
1064
1065        if filter_keys.is_none() && self.txn_mode()? == TxnMode::ReadOnly {
1066            let cache = self
1067                .db
1068                .vector_cache
1069                .read()
1070                .expect("vector cache lock poisoned");
1071            if let Some(cache) = cache.as_ref() {
1072                if cache.is_empty() {
1073                    return Ok(Vec::new());
1074                }
1075                let keys_len = cache.len();
1076                let mut rows = Vec::with_capacity(keys_len);
1077                let mut score_time = std::time::Duration::ZERO;
1078                for (key, cached) in cache.iter() {
1079                    if cached.metric != metric {
1080                        return Err(Error::Core(alopex_core::Error::UnsupportedMetric {
1081                            metric: metric.as_str().to_string(),
1082                        }));
1083                    }
1084                    validate_dimensions(cached.vector.len(), query_vector.len())
1085                        .map_err(Error::Core)?;
1086                    let score_start = if profile {
1087                        Some(std::time::Instant::now())
1088                    } else {
1089                        None
1090                    };
1091                    let dot = dot_product(query_vector, &cached.vector);
1092                    let score = match metric {
1093                        Metric::Cosine => {
1094                            if cached.inv_norm == 0.0 || inv_query_norm == 0.0 {
1095                                0.0
1096                            } else {
1097                                dot * cached.inv_norm * inv_query_norm
1098                            }
1099                        }
1100                        Metric::L2 => {
1101                            let dist_sq = query_norm_sq + cached.norm_sq - 2.0 * dot;
1102                            -dist_sq.sqrt()
1103                        }
1104                        Metric::InnerProduct => dot,
1105                    };
1106                    if let Some(score_start) = score_start {
1107                        score_time += score_start.elapsed();
1108                    }
1109                    rows.push(SearchResult {
1110                        key: key.clone(),
1111                        metadata: cached.metadata.clone(),
1112                        score,
1113                    });
1114                }
1115
1116                let rows_total = rows.len();
1117                let sort_start = if profile {
1118                    Some(std::time::Instant::now())
1119                } else {
1120                    None
1121                };
1122                if rows.len() > top_k {
1123                    rows.select_nth_unstable_by(top_k - 1, |a, b| {
1124                        b.score.total_cmp(&a.score).then_with(|| a.key.cmp(&b.key))
1125                    });
1126                    rows.truncate(top_k);
1127                }
1128                rows.sort_by(|a, b| b.score.total_cmp(&a.score).then_with(|| a.key.cmp(&b.key)));
1129                if let (true, Some(total_start), Some(sort_start)) =
1130                    (profile, total_start, sort_start)
1131                {
1132                    let sort_time = sort_start.elapsed();
1133                    let total_time = total_start.elapsed();
1134                    eprintln!(
1135                        "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}",
1136                        keys_len,
1137                        rows_total,
1138                        top_k,
1139                        0.0,
1140                        0.0,
1141                        0.0,
1142                        score_time.as_secs_f64() * 1000.0,
1143                        sort_time.as_secs_f64() * 1000.0,
1144                        total_time.as_secs_f64() * 1000.0
1145                    );
1146                }
1147                return Ok(rows);
1148            }
1149        }
1150
1151        let (keys, load_keys_time) = if profile {
1152            let start = std::time::Instant::now();
1153            let keys = match filter_keys {
1154                Some(keys) => keys.to_vec(),
1155                None => self.load_vector_index()?,
1156            };
1157            (keys, start.elapsed())
1158        } else {
1159            let keys = match filter_keys {
1160                Some(keys) => keys.to_vec(),
1161                None => self.load_vector_index()?,
1162            };
1163            (keys, std::time::Duration::ZERO)
1164        };
1165        if keys.is_empty() {
1166            return Ok(Vec::new());
1167        }
1168
1169        let keys_len = keys.len();
1170        let mut rows = Vec::with_capacity(keys.len());
1171        let txn = self.inner_mut()?;
1172        let mut get_time = std::time::Duration::ZERO;
1173        let mut decode_time = std::time::Duration::ZERO;
1174        let mut score_time = std::time::Duration::ZERO;
1175        if profile {
1176            for key in keys {
1177                let get_start = std::time::Instant::now();
1178                let Some(raw) = txn.get(&key).map_err(Error::Core)? else {
1179                    get_time += get_start.elapsed();
1180                    continue;
1181                };
1182                get_time += get_start.elapsed();
1183                let decode_start = std::time::Instant::now();
1184                let decoded = decode_vector_entry_view(&raw).map_err(Error::Core)?;
1185                decode_time += decode_start.elapsed();
1186                if decoded.metric != metric {
1187                    return Err(Error::Core(alopex_core::Error::UnsupportedMetric {
1188                        metric: metric.as_str().to_string(),
1189                    }));
1190                }
1191                validate_dimensions(decoded.dim, query_vector.len()).map_err(Error::Core)?;
1192                let score_start = std::time::Instant::now();
1193                let score =
1194                    score_from_bytes(metric, query_vector, query_norm, decoded.vector_bytes)?;
1195                score_time += score_start.elapsed();
1196                rows.push(SearchResult {
1197                    key,
1198                    metadata: decoded.metadata,
1199                    score,
1200                });
1201            }
1202        } else {
1203            for key in keys {
1204                let Some(raw) = txn.get(&key).map_err(Error::Core)? else {
1205                    continue;
1206                };
1207                let decoded = decode_vector_entry_view(&raw).map_err(Error::Core)?;
1208                if decoded.metric != metric {
1209                    return Err(Error::Core(alopex_core::Error::UnsupportedMetric {
1210                        metric: metric.as_str().to_string(),
1211                    }));
1212                }
1213                validate_dimensions(decoded.dim, query_vector.len()).map_err(Error::Core)?;
1214                let score =
1215                    score_from_bytes(metric, query_vector, query_norm, decoded.vector_bytes)?;
1216                rows.push(SearchResult {
1217                    key,
1218                    metadata: decoded.metadata,
1219                    score,
1220                });
1221            }
1222        }
1223
1224        let rows_total = rows.len();
1225        let sort_start = if profile {
1226            Some(std::time::Instant::now())
1227        } else {
1228            None
1229        };
1230        if rows.len() > top_k {
1231            rows.select_nth_unstable_by(top_k - 1, |a, b| {
1232                b.score.total_cmp(&a.score).then_with(|| a.key.cmp(&b.key))
1233            });
1234            rows.truncate(top_k);
1235        }
1236        rows.sort_by(|a, b| b.score.total_cmp(&a.score).then_with(|| a.key.cmp(&b.key)));
1237        if let (true, Some(total_start), Some(sort_start)) = (profile, total_start, sort_start) {
1238            let sort_time = sort_start.elapsed();
1239            let total_time = total_start.elapsed();
1240            eprintln!(
1241                "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}",
1242                keys_len,
1243                rows_total,
1244                top_k,
1245                load_keys_time.as_secs_f64() * 1000.0,
1246                get_time.as_secs_f64() * 1000.0,
1247                decode_time.as_secs_f64() * 1000.0,
1248                score_time.as_secs_f64() * 1000.0,
1249                sort_time.as_secs_f64() * 1000.0,
1250                total_time.as_secs_f64() * 1000.0
1251            );
1252        }
1253        Ok(rows)
1254    }
1255
1256    fn load_vector_index(&mut self) -> Result<Vec<Key>> {
1257        let txn = self.inner_mut()?;
1258        let Some(raw) = txn.get(&VECTOR_INDEX_KEY.to_vec()).map_err(Error::Core)? else {
1259            return Ok(Vec::new());
1260        };
1261        decode_index(&raw).map_err(Error::Core)
1262    }
1263
1264    fn persist_vector_index(&mut self, keys: &[Key]) -> Result<()> {
1265        let txn = self.inner_mut()?;
1266        let encoded = encode_index(keys)?;
1267        txn.put(VECTOR_INDEX_KEY.to_vec(), encoded)
1268            .map_err(Error::Core)
1269    }
1270
1271    /// Commits the transaction, applying all changes.
1272    pub fn commit(mut self) -> Result<()> {
1273        {
1274            let txn = self.inner.as_mut().ok_or(Error::TxnCompleted)?;
1275            for (index, state) in self.hnsw_indices.values_mut() {
1276                index.commit_staged(txn, state).map_err(Error::Core)?;
1277            }
1278            let mut catalog = self.db.sql_catalog.write().expect("catalog lock poisoned");
1279            catalog
1280                .persist_overlay(txn, &self.overlay)
1281                .map_err(|err| Error::Sql(err.into()))?;
1282            if let Some(journal) = self.journal.take() {
1283                journal.stage(txn).map_err(Error::Core)?;
1284            }
1285
1286            let vector_cache_invalidated = self.vector_cache_invalidated;
1287            let vector_cache_updates = std::mem::take(&mut self.vector_cache_updates);
1288            let vector_cache_deletes = std::mem::take(&mut self.vector_cache_deletes);
1289            if vector_cache_invalidated {
1290                let mut cache = self
1291                    .db
1292                    .vector_cache
1293                    .write()
1294                    .expect("vector cache lock poisoned");
1295                *cache = None;
1296            } else if !vector_cache_updates.is_empty() || !vector_cache_deletes.is_empty() {
1297                let needs_rebuild = {
1298                    let cache = self
1299                        .db
1300                        .vector_cache
1301                        .read()
1302                        .expect("vector cache lock poisoned");
1303                    cache.is_none()
1304                };
1305                if needs_rebuild {
1306                    let rebuilt = build_vector_cache_from_txn(txn).map_err(Error::Core)?;
1307                    let mut cache = self
1308                        .db
1309                        .vector_cache
1310                        .write()
1311                        .expect("vector cache lock poisoned");
1312                    *cache = Some(rebuilt);
1313                } else {
1314                    let mut cache = self
1315                        .db
1316                        .vector_cache
1317                        .write()
1318                        .expect("vector cache lock poisoned");
1319                    if let Some(cache) = cache.as_mut() {
1320                        for key in vector_cache_deletes {
1321                            cache.remove(&key);
1322                        }
1323                        for (key, cached) in vector_cache_updates {
1324                            cache.insert(key, cached);
1325                        }
1326                    }
1327                }
1328            }
1329        }
1330        let txn = self.inner.take().ok_or(Error::TxnCompleted)?;
1331        let hnsw_indices = std::mem::take(&mut self.hnsw_indices);
1332        txn.commit_self().map_err(Error::Core)?;
1333        if !hnsw_indices.is_empty() {
1334            let mut cache = self
1335                .db
1336                .hnsw_cache
1337                .write()
1338                .expect("hnsw cache lock poisoned");
1339            for (name, (index, _state)) in hnsw_indices {
1340                cache.insert(name, Arc::new(index));
1341            }
1342        }
1343
1344        // KV commit 成功後のみ、カタログにオーバーレイを適用する。
1345        let overlay = std::mem::take(&mut self.overlay);
1346        let catalog_modified = self.catalog_modified;
1347        let mut catalog = self.db.sql_catalog.write().expect("catalog lock poisoned");
1348        catalog.apply_overlay(overlay);
1349        drop(catalog); // Release lock before invalidating cache
1350                       // Invalidate table info cache only if DDL operations were performed
1351        if catalog_modified {
1352            self.db.invalidate_table_info_cache();
1353        }
1354        Ok(())
1355    }
1356
1357    /// トランザクションを消費せずにロールバックする(失敗時の再試行を可能にする)。
1358    pub fn rollback_in_place(&mut self) -> Result<()> {
1359        let txn = self.inner.as_mut().ok_or(Error::TxnCompleted)?;
1360        txn.rollback_in_place().map_err(Error::Core)?;
1361        for (index, state) in self.hnsw_indices.values_mut() {
1362            let _ = index.rollback(state);
1363        }
1364        self.hnsw_indices.clear();
1365        self.overlay = alopex_sql::catalog::CatalogOverlay::default();
1366        self.inner = None;
1367        Ok(())
1368    }
1369
1370    /// Rolls back the transaction, discarding all changes.
1371    pub fn rollback(mut self) -> Result<()> {
1372        if let Some(txn) = self.inner.take() {
1373            for (index, state) in self.hnsw_indices.values_mut() {
1374                let _ = index.rollback(state);
1375            }
1376            self.hnsw_indices.clear();
1377            txn.rollback_self().map_err(Error::Core)
1378        } else {
1379            Err(Error::TxnCompleted)
1380        }
1381    }
1382
1383    fn inner_mut(&mut self) -> Result<&mut AnyKVTransaction<'a>> {
1384        self.inner.as_mut().ok_or(Error::TxnCompleted)
1385    }
1386
1387    fn hnsw_entry_mut(&mut self, name: &str) -> Result<&mut (HnswIndex, HnswTransactionState)> {
1388        if !self.hnsw_indices.contains_key(name) {
1389            let index = {
1390                let txn = self.inner_mut()?;
1391                HnswIndex::load(name, txn).map_err(Error::Core)?
1392            };
1393            self.hnsw_indices
1394                .insert(name.to_string(), (index, HnswTransactionState::default()));
1395        }
1396        Ok(self.hnsw_indices.get_mut(name).unwrap())
1397    }
1398
1399    fn ensure_write_txn(&self) -> Result<()> {
1400        let txn = self.inner.as_ref().ok_or(Error::TxnCompleted)?;
1401        if txn.mode() != TxnMode::ReadWrite {
1402            return Err(Error::Core(alopex_core::Error::TxnReadOnly));
1403        }
1404        Ok(())
1405    }
1406}
1407
1408impl OwnedEmbeddedTransaction {
1409    /// Stage a vector and its metadata in an owned transaction.
1410    pub fn upsert_vector(
1411        &mut self,
1412        key: &[u8],
1413        metadata: &[u8],
1414        vector: &[f32],
1415        metric: Metric,
1416    ) -> Result<()> {
1417        if vector.is_empty() {
1418            return Err(Error::Core(alopex_core::Error::InvalidFormat(
1419                "vector cannot be empty".into(),
1420            )));
1421        }
1422        let vector_type = VectorType::new(vector.len(), metric);
1423        vector_type.validate(vector).map_err(Error::Core)?;
1424        let key = key.to_vec();
1425        let payload = encode_vector_entry(vector_type, metadata, vector);
1426
1427        self.session
1428            .with_transaction(|transaction| {
1429                transaction.put(key.clone(), payload)?;
1430                let mut keys = match transaction.get(&VECTOR_INDEX_KEY.to_vec())? {
1431                    Some(raw) => decode_index(&raw)?,
1432                    None => Vec::new(),
1433                };
1434                if !keys.iter().any(|entry| entry == &key) {
1435                    keys.push(key.clone());
1436                    transaction.put(VECTOR_INDEX_KEY.to_vec(), encode_index(&keys)?)?;
1437                }
1438                Ok(())
1439            })
1440            .map_err(Error::Core)?;
1441        self.vector_cache_invalidated = true;
1442        Ok(())
1443    }
1444
1445    /// Retrieve one vector from an owned transaction.
1446    pub fn get_vector(&mut self, key: &[u8], metric: Metric) -> Result<Option<Vec<f32>>> {
1447        self.session
1448            .with_transaction(|transaction| {
1449                let Some(raw) = transaction.get(&key.to_vec())? else {
1450                    return Ok(None);
1451                };
1452                let decoded = decode_vector_entry(&raw)?;
1453                if decoded.metric != metric {
1454                    return Err(alopex_core::Error::UnsupportedMetric {
1455                        metric: metric.as_str().to_string(),
1456                    });
1457                }
1458                Ok(Some(decoded.vector))
1459            })
1460            .map_err(Error::Core)
1461    }
1462
1463    /// Retrieve vectors in input order from an owned transaction.
1464    pub fn get_vectors(&mut self, keys: &[Key], metric: Metric) -> Result<Vec<Option<Vec<f32>>>> {
1465        self.session
1466            .with_transaction(|transaction| {
1467                let mut output = Vec::with_capacity(keys.len());
1468                for key in keys {
1469                    let Some(raw) = transaction.get(key)? else {
1470                        output.push(None);
1471                        continue;
1472                    };
1473                    let decoded = decode_vector_entry(&raw)?;
1474                    if decoded.metric != metric {
1475                        return Err(alopex_core::Error::UnsupportedMetric {
1476                            metric: metric.as_str().to_string(),
1477                        });
1478                    }
1479                    output.push(Some(decoded.vector));
1480                }
1481                Ok(output)
1482            })
1483            .map_err(Error::Core)
1484    }
1485
1486    /// Search vectors visible to this owned transaction.
1487    pub fn search_similar(
1488        &mut self,
1489        query_vector: &[f32],
1490        metric: Metric,
1491        top_k: usize,
1492        filter_keys: Option<&[Key]>,
1493    ) -> Result<Vec<SearchResult>> {
1494        if top_k == 0 {
1495            return Ok(Vec::new());
1496        }
1497        let query_norm_sq = query_vector.iter().map(|value| value * value).sum::<f32>();
1498        let query_norm = if metric == Metric::Cosine {
1499            query_norm_sq.sqrt()
1500        } else {
1501            0.0
1502        };
1503        let keys = match filter_keys {
1504            Some(keys) => keys.to_vec(),
1505            None => self
1506                .session
1507                .with_transaction(|transaction| {
1508                    match transaction.get(&VECTOR_INDEX_KEY.to_vec())? {
1509                        Some(raw) => decode_index(&raw),
1510                        None => Ok(Vec::new()),
1511                    }
1512                })
1513                .map_err(Error::Core)?,
1514        };
1515        let mut rows = self
1516            .session
1517            .with_transaction(|transaction| {
1518                let mut rows = Vec::with_capacity(keys.len());
1519                for key in &keys {
1520                    let Some(raw) = transaction.get(key)? else {
1521                        continue;
1522                    };
1523                    let decoded = decode_vector_entry_view(&raw)?;
1524                    if decoded.metric != metric {
1525                        return Err(alopex_core::Error::UnsupportedMetric {
1526                            metric: metric.as_str().to_string(),
1527                        });
1528                    }
1529                    validate_dimensions(decoded.dim, query_vector.len())?;
1530                    let score =
1531                        score_from_bytes(metric, query_vector, query_norm, decoded.vector_bytes)?;
1532                    rows.push(SearchResult {
1533                        key: key.clone(),
1534                        metadata: decoded.metadata,
1535                        score,
1536                    });
1537                }
1538                Ok(rows)
1539            })
1540            .map_err(Error::Core)?;
1541        if rows.len() > top_k {
1542            rows.select_nth_unstable_by(top_k - 1, |left, right| {
1543                right
1544                    .score
1545                    .total_cmp(&left.score)
1546                    .then_with(|| left.key.cmp(&right.key))
1547            });
1548            rows.truncate(top_k);
1549        }
1550        rows.sort_by(|left, right| {
1551            right
1552                .score
1553                .total_cmp(&left.score)
1554                .then_with(|| left.key.cmp(&right.key))
1555        });
1556        Ok(rows)
1557    }
1558
1559    /// Stage an HNSW upsert in this owned transaction.
1560    pub fn upsert_to_hnsw(
1561        &mut self,
1562        index_name: &str,
1563        key: &[u8],
1564        vector: &[f32],
1565        metadata: &[u8],
1566    ) -> Result<()> {
1567        self.ensure_owned_write_transaction()?;
1568        let (index, state) = self.hnsw_entry_mut(index_name)?;
1569        index
1570            .upsert_staged(key, vector, metadata, state)
1571            .map_err(Error::Core)
1572    }
1573
1574    /// Stage an HNSW deletion in this owned transaction.
1575    pub fn delete_from_hnsw(&mut self, index_name: &str, key: &[u8]) -> Result<bool> {
1576        self.ensure_owned_write_transaction()?;
1577        let (index, state) = self.hnsw_entry_mut(index_name)?;
1578        index.delete_staged(key, state).map_err(Error::Core)
1579    }
1580
1581    fn ensure_owned_write_transaction(&self) -> Result<()> {
1582        let mode = self
1583            .session
1584            .with_transaction(|transaction| Ok(transaction.mode()))
1585            .map_err(Error::Core)?;
1586        if mode == TxnMode::ReadOnly {
1587            return Err(Error::Core(alopex_core::Error::TxnReadOnly));
1588        }
1589        Ok(())
1590    }
1591
1592    fn hnsw_entry_mut(
1593        &mut self,
1594        index_name: &str,
1595    ) -> Result<&mut (HnswIndex, HnswTransactionState)> {
1596        if !self.hnsw_indices.contains_key(index_name) {
1597            let index = self
1598                .session
1599                .with_transaction(|transaction| {
1600                    let mut transaction =
1601                        alopex_core::kv::OwnedKVTransactionAdapter::new(transaction);
1602                    HnswIndex::load(index_name, &mut transaction)
1603                })
1604                .map_err(Error::Core)?;
1605            self.hnsw_indices.insert(
1606                index_name.to_string(),
1607                (index, HnswTransactionState::default()),
1608            );
1609        }
1610        Ok(self
1611            .hnsw_indices
1612            .get_mut(index_name)
1613            .expect("HNSW entry inserted above"))
1614    }
1615}
1616
1617impl<'a> Drop for Transaction<'a> {
1618    fn drop(&mut self) {
1619        if let Some(txn) = self.inner.take() {
1620            for (index, state) in self.hnsw_indices.values_mut() {
1621                let _ = index.rollback(state);
1622            }
1623            self.hnsw_indices.clear();
1624            let _ = txn.rollback_self();
1625        }
1626    }
1627}
1628
1629fn metric_to_byte(metric: Metric) -> u8 {
1630    match metric {
1631        Metric::Cosine => 0,
1632        Metric::L2 => 1,
1633        Metric::InnerProduct => 2,
1634    }
1635}
1636
1637fn byte_to_metric(byte: u8) -> result::Result<Metric, alopex_core::Error> {
1638    match byte {
1639        0 => Ok(Metric::Cosine),
1640        1 => Ok(Metric::L2),
1641        2 => Ok(Metric::InnerProduct),
1642        other => Err(alopex_core::Error::UnsupportedMetric {
1643            metric: format!("unknown({other})"),
1644        }),
1645    }
1646}
1647
1648fn encode_vector_entry(vector_type: VectorType, metadata: &[u8], vector: &[f32]) -> Vec<u8> {
1649    let dim = vector_type.dim() as u32;
1650    let meta_len = metadata.len() as u32;
1651    let mut buf = Vec::with_capacity(1 + 4 + 4 + metadata.len() + std::mem::size_of_val(vector));
1652    buf.push(metric_to_byte(vector_type.metric()));
1653    buf.extend_from_slice(&dim.to_le_bytes());
1654    buf.extend_from_slice(&meta_len.to_le_bytes());
1655    buf.extend_from_slice(metadata);
1656    for v in vector {
1657        buf.extend_from_slice(&v.to_le_bytes());
1658    }
1659    buf
1660}
1661
1662struct DecodedEntry {
1663    metric: Metric,
1664    vector: Vec<f32>,
1665}
1666
1667#[derive(Clone)]
1668struct CachedVector {
1669    metric: Metric,
1670    metadata: Vec<u8>,
1671    vector: Vec<f32>,
1672    norm_sq: f32,
1673    inv_norm: f32,
1674}
1675
1676struct VectorEntryView<'a> {
1677    metric: Metric,
1678    dim: usize,
1679    metadata: Vec<u8>,
1680    vector_bytes: &'a [u8],
1681}
1682
1683fn decode_vector_entry(bytes: &[u8]) -> result::Result<DecodedEntry, alopex_core::Error> {
1684    if bytes.len() < 9 {
1685        return Err(alopex_core::Error::InvalidFormat(
1686            "vector entry too short".into(),
1687        ));
1688    }
1689    let metric = byte_to_metric(bytes[0])?;
1690    let dim = u32::from_le_bytes(bytes[1..5].try_into().unwrap()) as usize;
1691    let meta_len = u32::from_le_bytes(bytes[5..9].try_into().unwrap()) as usize;
1692
1693    let header = 9;
1694    let expected_len = header + meta_len + dim * std::mem::size_of::<f32>();
1695    if bytes.len() < expected_len {
1696        return Err(alopex_core::Error::InvalidFormat(
1697            "vector entry truncated".into(),
1698        ));
1699    }
1700
1701    let mut vector = Vec::with_capacity(dim);
1702    let vec_bytes = &bytes[header + meta_len..expected_len];
1703    for chunk in vec_bytes.as_chunks::<4>().0 {
1704        vector.push(f32::from_le_bytes(*chunk));
1705    }
1706
1707    Ok(DecodedEntry { metric, vector })
1708}
1709
1710fn decode_vector_entry_view(
1711    bytes: &[u8],
1712) -> result::Result<VectorEntryView<'_>, alopex_core::Error> {
1713    if bytes.len() < 9 {
1714        return Err(alopex_core::Error::InvalidFormat(
1715            "vector entry too short".into(),
1716        ));
1717    }
1718    let metric = byte_to_metric(bytes[0])?;
1719    let dim = u32::from_le_bytes(bytes[1..5].try_into().unwrap()) as usize;
1720    let meta_len = u32::from_le_bytes(bytes[5..9].try_into().unwrap()) as usize;
1721
1722    let header = 9;
1723    let expected_len = header + meta_len + dim * std::mem::size_of::<f32>();
1724    if bytes.len() < expected_len {
1725        return Err(alopex_core::Error::InvalidFormat(
1726            "vector entry truncated".into(),
1727        ));
1728    }
1729
1730    let metadata = bytes[header..header + meta_len].to_vec();
1731    let vector_bytes = &bytes[header + meta_len..expected_len];
1732
1733    Ok(VectorEntryView {
1734        metric,
1735        dim,
1736        metadata,
1737        vector_bytes,
1738    })
1739}
1740
1741fn vector_bytes_to_vec(bytes: &[u8]) -> Vec<f32> {
1742    let mut vector = Vec::with_capacity(bytes.len() / 4);
1743    for chunk in bytes.as_chunks::<4>().0 {
1744        vector.push(f32::from_le_bytes(*chunk));
1745    }
1746    vector
1747}
1748
1749fn cached_vector_from_entry(metric: Metric, metadata: Vec<u8>, vector: Vec<f32>) -> CachedVector {
1750    let norm_sq = vector.iter().map(|v| v * v).sum::<f32>();
1751    let inv_norm = if norm_sq == 0.0 {
1752        0.0
1753    } else {
1754        1.0 / norm_sq.sqrt()
1755    };
1756    CachedVector {
1757        metric,
1758        metadata,
1759        vector,
1760        norm_sq,
1761        inv_norm,
1762    }
1763}
1764
1765fn build_vector_cache_from_txn<'a>(
1766    txn: &mut AnyKVTransaction<'a>,
1767) -> result::Result<HashMap<Key, CachedVector>, alopex_core::Error> {
1768    let Some(raw) = txn.get(&VECTOR_INDEX_KEY.to_vec())? else {
1769        return Ok(HashMap::new());
1770    };
1771    let keys = decode_index(&raw)?;
1772    let mut cache = HashMap::with_capacity(keys.len());
1773    for key in keys {
1774        let Some(raw) = txn.get(&key)? else {
1775            continue;
1776        };
1777        let decoded = decode_vector_entry_view(&raw)?;
1778        let vector = vector_bytes_to_vec(decoded.vector_bytes);
1779        let cached = cached_vector_from_entry(decoded.metric, decoded.metadata, vector);
1780        cache.insert(key, cached);
1781    }
1782    Ok(cache)
1783}
1784
1785fn dot_product(query: &[f32], item: &[f32]) -> f32 {
1786    #[cfg(target_arch = "x86_64")]
1787    {
1788        if std::is_x86_feature_detected!("avx") {
1789            // SAFETY: guarded by runtime feature detection.
1790            unsafe {
1791                return dot_product_avx(query, item);
1792            }
1793        }
1794    }
1795    dot_product_scalar(query, item)
1796}
1797
1798fn dot_product_scalar(query: &[f32], item: &[f32]) -> f32 {
1799    query.iter().zip(item.iter()).map(|(q, v)| q * v).sum()
1800}
1801
1802#[cfg(target_arch = "x86_64")]
1803#[target_feature(enable = "avx")]
1804unsafe fn dot_product_avx(query: &[f32], item: &[f32]) -> f32 {
1805    use std::arch::x86_64::*;
1806
1807    let len = query.len();
1808    let mut i = 0;
1809    let mut acc = _mm256_setzero_ps();
1810    let q_ptr = query.as_ptr();
1811    let v_ptr = item.as_ptr();
1812    while i + 8 <= len {
1813        let q = _mm256_loadu_ps(q_ptr.add(i));
1814        let v = _mm256_loadu_ps(v_ptr.add(i));
1815        acc = _mm256_add_ps(acc, _mm256_mul_ps(q, v));
1816        i += 8;
1817    }
1818
1819    let mut tmp = [0f32; 8];
1820    _mm256_storeu_ps(tmp.as_mut_ptr(), acc);
1821    let mut sum = tmp.iter().sum::<f32>();
1822    while i < len {
1823        sum += *q_ptr.add(i) * *v_ptr.add(i);
1824        i += 1;
1825    }
1826    sum
1827}
1828
1829fn score_from_slice(metric: Metric, query: &[f32], query_norm: f32, item: &[f32]) -> f32 {
1830    match metric {
1831        Metric::Cosine => {
1832            if query_norm == 0.0 {
1833                return 0.0;
1834            }
1835            let mut dot = 0.0;
1836            let mut item_norm_sq = 0.0;
1837            for (q, v) in query.iter().zip(item.iter()) {
1838                dot += q * v;
1839                item_norm_sq += v * v;
1840            }
1841            let item_norm = item_norm_sq.sqrt();
1842            if item_norm == 0.0 {
1843                0.0
1844            } else {
1845                dot / (query_norm * item_norm)
1846            }
1847        }
1848        Metric::L2 => {
1849            let mut dist_sq = 0.0;
1850            for (q, v) in query.iter().zip(item.iter()) {
1851                let d = q - v;
1852                dist_sq += d * d;
1853            }
1854            -dist_sq.sqrt()
1855        }
1856        Metric::InnerProduct => query.iter().zip(item.iter()).map(|(q, v)| q * v).sum(),
1857    }
1858}
1859
1860fn score_from_bytes(
1861    metric: Metric,
1862    query: &[f32],
1863    query_norm: f32,
1864    vector_bytes: &[u8],
1865) -> result::Result<f32, alopex_core::Error> {
1866    let len = vector_bytes.len() / 4;
1867    #[cfg(target_endian = "little")]
1868    {
1869        let ptr = vector_bytes.as_ptr();
1870        if (ptr as usize).is_multiple_of(std::mem::align_of::<f32>()) {
1871            let items = unsafe { std::slice::from_raw_parts(ptr as *const f32, len) };
1872            return Ok(score_from_slice(metric, query, query_norm, items));
1873        }
1874    }
1875
1876    // Fallback for unaligned or big-endian targets: decode each f32 from bytes.
1877    let mut iter = vector_bytes.as_chunks::<4>().0.iter();
1878    let score = match metric {
1879        Metric::Cosine => {
1880            if query_norm == 0.0 {
1881                0.0
1882            } else {
1883                let mut dot = 0.0;
1884                let mut item_norm_sq = 0.0;
1885                for (q, chunk) in query.iter().zip(&mut iter) {
1886                    let v = f32::from_le_bytes(*chunk);
1887                    dot += q * v;
1888                    item_norm_sq += v * v;
1889                }
1890                let item_norm = item_norm_sq.sqrt();
1891                if item_norm == 0.0 {
1892                    0.0
1893                } else {
1894                    dot / (query_norm * item_norm)
1895                }
1896            }
1897        }
1898        Metric::L2 => {
1899            let mut dist_sq = 0.0;
1900            for (q, chunk) in query.iter().zip(&mut iter) {
1901                let v = f32::from_le_bytes(*chunk);
1902                let d = q - v;
1903                dist_sq += d * d;
1904            }
1905            -dist_sq.sqrt()
1906        }
1907        Metric::InnerProduct => query
1908            .iter()
1909            .zip(&mut iter)
1910            .map(|(q, chunk)| q * f32::from_le_bytes(*chunk))
1911            .sum(),
1912    };
1913    Ok(score)
1914}
1915
1916fn encode_index(keys: &[Key]) -> result::Result<Vec<u8>, alopex_core::Error> {
1917    let mut buf = Vec::new();
1918    let count = keys.len() as u32;
1919    buf.extend_from_slice(&count.to_le_bytes());
1920    for key in keys {
1921        let len: u32 = key
1922            .len()
1923            .try_into()
1924            .map_err(|_| alopex_core::Error::InvalidFormat("key too long".into()))?;
1925        buf.extend_from_slice(&len.to_le_bytes());
1926        buf.extend_from_slice(key);
1927    }
1928    Ok(buf)
1929}
1930
1931fn decode_index(bytes: &[u8]) -> result::Result<Vec<Key>, alopex_core::Error> {
1932    if bytes.len() < 4 {
1933        return Err(alopex_core::Error::InvalidFormat("index too short".into()));
1934    }
1935    let count = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
1936    let mut pos = 4;
1937    let mut keys = Vec::with_capacity(count);
1938    for _ in 0..count {
1939        if pos + 4 > bytes.len() {
1940            return Err(alopex_core::Error::InvalidFormat("index truncated".into()));
1941        }
1942        let len = u32::from_le_bytes(bytes[pos..pos + 4].try_into().unwrap()) as usize;
1943        pos += 4;
1944        if pos + len > bytes.len() {
1945            return Err(alopex_core::Error::InvalidFormat(
1946                "index key truncated".into(),
1947            ));
1948        }
1949        keys.push(bytes[pos..pos + len].to_vec());
1950        pos += len;
1951    }
1952    Ok(keys)
1953}
1954
1955#[cfg(test)]
1956mod tests {
1957    use super::*;
1958    use std::sync::mpsc;
1959    use std::thread;
1960    use tempfile::tempdir;
1961
1962    #[test]
1963    fn test_open_and_crud() {
1964        let dir = tempdir().unwrap();
1965        let path = dir.path().join("test.db");
1966        let db = Database::open(&path).unwrap();
1967
1968        let mut txn = db.begin(TxnMode::ReadWrite).unwrap();
1969        txn.put(b"key1", b"value1").unwrap();
1970        txn.commit().unwrap();
1971
1972        let mut txn2 = db.begin(TxnMode::ReadOnly).unwrap();
1973        let val = txn2.get(b"key1").unwrap();
1974        assert_eq!(val, Some(b"value1".to_vec()));
1975    }
1976
1977    #[test]
1978    fn test_not_found() {
1979        let db = Database::new();
1980        let mut txn = db.begin(TxnMode::ReadOnly).unwrap();
1981        let val = txn.get(b"non-existent-key").unwrap();
1982        assert!(val.is_none());
1983    }
1984
1985    #[cfg(not(target_arch = "wasm32"))]
1986    #[test]
1987    fn test_file_format_version_reads_alopex_header() {
1988        use alopex_core::storage::format::{AlopexFileWriter, FileFlags, FileVersion};
1989
1990        let dir = tempdir().unwrap();
1991        let path = dir.path().join("format-test.alopex");
1992        let expected = FileVersion::new(0, 0, 1);
1993
1994        let writer = AlopexFileWriter::new(path.clone(), expected, FileFlags(0)).unwrap();
1995        writer.finalize().unwrap();
1996
1997        let db = Database::open(&path).unwrap();
1998        assert_eq!(db.file_format_version(), expected);
1999    }
2000
2001    #[test]
2002    fn test_crash_recovery_replays_wal() {
2003        let dir = tempdir().unwrap();
2004        let path = dir.path().join("replay.db");
2005
2006        {
2007            let db = Database::open(&path).unwrap();
2008            let mut txn = db.begin(TxnMode::ReadWrite).unwrap();
2009            txn.put(b"k1", b"v1").unwrap();
2010            txn.commit().unwrap();
2011
2012            let mut uncommitted = db.begin(TxnMode::ReadWrite).unwrap();
2013            uncommitted.put(b"k2", b"v2").unwrap();
2014            // Drop without commit to simulate crash before commit.
2015        }
2016
2017        let db = Database::open(&path).unwrap();
2018        let mut txn = db.begin(TxnMode::ReadOnly).unwrap();
2019        assert_eq!(txn.get(b"k1").unwrap(), Some(b"v1".to_vec()));
2020        assert_eq!(txn.get(b"k2").unwrap(), None);
2021    }
2022
2023    #[test]
2024    fn test_txn_closed() {
2025        let db = Database::new();
2026        let mut txn = db.begin(TxnMode::ReadWrite).unwrap();
2027        txn.put(b"k1", b"v1").unwrap();
2028        txn.commit().unwrap();
2029        // The `commit` call consumes the transaction, so we can't call it again.
2030        // This test verifies that we can't use a transaction after it's been completed.
2031        // The `inner_mut` method will return `Error::TxnCompleted`.
2032        // This is a compile-time check in practice, but we can't write a test that fails to compile.
2033        // The logic is sound.
2034    }
2035
2036    #[test]
2037    fn test_concurrency_conflict() {
2038        let db = std::sync::Arc::new(Database::new());
2039        let mut t0 = db.begin(TxnMode::ReadWrite).unwrap();
2040        t0.put(b"k1", b"v0").unwrap();
2041        t0.commit().unwrap();
2042
2043        let (tx1, rx1) = mpsc::channel();
2044        let (tx2, rx2) = mpsc::channel();
2045
2046        let db1 = db.clone();
2047        let t1 = thread::spawn(move || {
2048            let mut txn1 = db1.begin(TxnMode::ReadWrite).unwrap();
2049            let val = txn1.get(b"k1").unwrap();
2050            assert_eq!(val.unwrap(), b"v0");
2051            tx1.send(()).unwrap();
2052            rx2.recv().unwrap();
2053            txn1.put(b"k1", b"v1").unwrap();
2054            let result = txn1.commit();
2055            assert!(matches!(
2056                result,
2057                Err(Error::Core(alopex_core::Error::TxnConflict))
2058            ));
2059        });
2060
2061        let db2 = db.clone();
2062        let t2 = thread::spawn(move || {
2063            rx1.recv().unwrap();
2064            let mut txn2 = db2.begin(TxnMode::ReadWrite).unwrap();
2065            txn2.put(b"k1", b"v2").unwrap();
2066            assert!(txn2.commit().is_ok());
2067            tx2.send(()).unwrap();
2068        });
2069
2070        t1.join().unwrap();
2071        t2.join().unwrap();
2072
2073        let mut txn3 = db.begin(TxnMode::ReadOnly).unwrap();
2074        let val = txn3.get(b"k1").unwrap();
2075        assert_eq!(val.unwrap(), b"v2");
2076    }
2077
2078    #[test]
2079    fn test_flush_and_reopen_via_embedded_api() {
2080        let dir = tempdir().unwrap();
2081        let path = dir.path().join("persist.db");
2082        {
2083            let db = Database::open(&path).unwrap();
2084            let mut txn = db.begin(TxnMode::ReadWrite).unwrap();
2085            txn.put(b"k1", b"v1").unwrap();
2086            txn.commit().unwrap();
2087            db.flush().unwrap();
2088        }
2089
2090        let db = Database::open(&path).unwrap();
2091        let mut txn = db.begin(TxnMode::ReadOnly).unwrap();
2092        assert_eq!(txn.get(b"k1").unwrap(), Some(b"v1".to_vec()));
2093    }
2094
2095    #[test]
2096    fn test_large_value_blob_roundtrip() {
2097        let dir = tempdir().unwrap();
2098        let path = dir.path().join("blob.lv");
2099        let payload = b"hello large value";
2100
2101        {
2102            let db = Database::new();
2103            let mut writer = db
2104                .create_blob_writer(&path, payload.len() as u64, Some(16))
2105                .unwrap();
2106            writer.write_chunk(&payload[..5]).unwrap();
2107            writer.write_chunk(&payload[5..]).unwrap();
2108            writer.finish().unwrap();
2109        }
2110
2111        let db = Database::new();
2112        let mut reader = db.open_large_value(&path).unwrap();
2113        let mut buf = Vec::new();
2114        while let Some((_info, chunk)) = reader.next_chunk().unwrap() {
2115            buf.extend_from_slice(&chunk);
2116        }
2117        assert_eq!(buf, payload);
2118    }
2119
2120    #[test]
2121    fn upsert_and_search_same_txn() {
2122        let db = Database::new();
2123        let mut txn = db.begin(TxnMode::ReadWrite).unwrap();
2124        txn.upsert_vector(b"k1", b"meta1", &[1.0, 0.0], Metric::Cosine)
2125            .unwrap();
2126
2127        let results = txn
2128            .search_similar(&[1.0, 0.0], Metric::Cosine, 1, None)
2129            .unwrap();
2130        assert_eq!(results.len(), 1);
2131        assert_eq!(results[0].key, b"k1");
2132        assert_eq!(results[0].metadata, b"meta1");
2133        txn.commit().unwrap();
2134    }
2135
2136    #[test]
2137    fn upsert_and_search_across_txn() {
2138        let db = Database::new();
2139        {
2140            let mut txn = db.begin(TxnMode::ReadWrite).unwrap();
2141            txn.upsert_vector(b"k1", b"meta1", &[1.0, 1.0], Metric::Cosine)
2142                .unwrap();
2143            txn.commit().unwrap();
2144        }
2145
2146        let mut ro = db.begin(TxnMode::ReadOnly).unwrap();
2147        let results = ro
2148            .search_similar(&[1.0, 1.0], Metric::Cosine, 1, None)
2149            .unwrap();
2150        assert_eq!(results.len(), 1);
2151        assert_eq!(results[0].key, b"k1");
2152    }
2153
2154    #[test]
2155    fn read_only_upsert_rejected() {
2156        let db = Database::new();
2157        let mut ro = db.begin(TxnMode::ReadOnly).unwrap();
2158        let err = ro
2159            .upsert_vector(b"k1", b"m", &[1.0, 0.0], Metric::Cosine)
2160            .unwrap_err();
2161        assert!(matches!(err, Error::Core(alopex_core::Error::TxnReadOnly)));
2162    }
2163
2164    #[test]
2165    fn dimension_mismatch_on_search() {
2166        let db = Database::new();
2167        {
2168            let mut txn = db.begin(TxnMode::ReadWrite).unwrap();
2169            txn.upsert_vector(b"k1", b"m", &[1.0, 0.0], Metric::Cosine)
2170                .unwrap();
2171            txn.commit().unwrap();
2172        }
2173        let mut ro = db.begin(TxnMode::ReadOnly).unwrap();
2174        let err = ro
2175            .search_similar(&[1.0, 0.0, 1.0], Metric::Cosine, 1, None)
2176            .unwrap_err();
2177        assert!(matches!(
2178            err,
2179            Error::Core(alopex_core::Error::DimensionMismatch { .. })
2180        ));
2181    }
2182}