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