1#![deny(missing_docs)]
4
5pub 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};
37pub 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};
52pub 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
63pub type Result<T> = result::Result<T, Error>;
65
66#[derive(Debug, thiserror::Error)]
68pub enum Error {
69 #[error("core error: {0}")]
71 Core(#[from] alopex_core::Error),
72 #[error("{0}")]
75 ReadAt(#[from] alopex_core::ReadAtError),
76 #[error("{0}")]
78 Sql(#[from] alopex_sql::SqlError),
79 #[error("{0}")]
81 DataFrame(#[from] alopex_dataframe::DataFrameError),
82 #[error("transaction is completed")]
84 TxnCompleted,
85 #[error("カタログが見つかりません: {0}")]
87 CatalogNotFound(String),
88 #[error("カタログは既に存在します: {0}")]
90 CatalogAlreadyExists(String),
91 #[error("カタログが空ではありません: {0}")]
93 CatalogNotEmpty(String),
94 #[error("ネームスペースが見つかりません: {0}.{1}")]
96 NamespaceNotFound(String, String),
97 #[error("ネームスペースは既に存在します: {0}.{1}")]
99 NamespaceAlreadyExists(String, String),
100 #[error("ネームスペースが空ではありません: {0}.{1}")]
102 NamespaceNotEmpty(String, String),
103 #[error("table not found: {0}")]
105 TableNotFound(String),
106 #[error("テーブルは既に存在します: {0}")]
108 TableAlreadyExists(String),
109 #[error("インデックスが見つかりません: {0}")]
111 IndexNotFound(String),
112 #[error("default オブジェクトは削除できません: {0}")]
114 CannotDeleteDefault(String),
115 #[error("managed テーブルにはスキーマが必要です")]
117 SchemaRequired,
118 #[error("external テーブルには storage_root が必要です")]
120 StorageRootRequired,
121 #[error("トランザクションは読み取り専用です")]
123 TxnReadOnly,
124 #[error("invalid transaction id: {0}")]
126 InvalidTransactionId(String),
127 #[error("not in in-memory columnar mode")]
129 NotInMemoryMode,
130 #[error("unsupported data source format: {0}")]
132 UnsupportedDataSourceFormat(String),
133 #[error("catalog lock poisoned")]
135 CatalogLockPoisoned,
136 #[error("cluster state lock poisoned")]
138 ClusterStateLockPoisoned,
139}
140
141impl Error {
142 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
151pub struct Database {
153 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 pub(crate) table_info_cache: RwLock<HashMap<String, CachedTableInfo>>,
160 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 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 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 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 pub fn open_in_memory() -> Result<Self> {
263 Self::open_in_memory_with_options(DatabaseOptions::in_memory())
264 }
265
266 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 pub fn open_with_uri(uri: &str) -> Result<Self> {
300 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 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 #[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 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 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 pub fn table_info_cache_epoch(&self) -> u64 {
453 self.table_info_cache_epoch.load(Ordering::Relaxed)
454 }
455
456 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 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 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 pub fn flush(&self) -> Result<()> {
499 self.store.flush().map_err(Error::Core)
500 }
501
502 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 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 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 let _ = fs::OpenOptions::new()
569 .create_new(true)
570 .write(true)
571 .open(wal_path);
572 }
573 Ok(())
574 }
575
576 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 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 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 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 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 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 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 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 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 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 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 pub fn open_large_value(&self, path: &Path) -> Result<LargeValueReader> {
773 LargeValueReader::open(path).map_err(Error::Core)
774 }
775
776 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
811pub 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 pub(crate) catalog_modified: bool,
822 journal: Option<LocalRangeChangeJournal>,
824}
825
826#[derive(Debug, Clone, PartialEq)]
828pub struct SearchResult {
829 pub key: Key,
831 pub metadata: Vec<u8>,
833 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 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 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 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 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 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 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 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 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 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 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 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 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); if catalog_modified {
1304 self.db.invalidate_table_info_cache();
1305 }
1306 Ok(())
1307 }
1308
1309 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 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 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 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 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 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 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 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 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 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 }
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 }
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}