1pub mod lifecycle;
2mod pcache2;
3
4use std::collections::{BTreeMap, BTreeSet, VecDeque};
5use std::error::Error;
6use std::fmt::{Display, Formatter};
7use std::fs::{File, OpenOptions};
8use std::io::{Seek, SeekFrom, Write};
9use std::path::{Path, PathBuf};
10use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
11use std::sync::mpsc::{self, Receiver, SyncSender};
12use std::sync::Once;
13use std::sync::{Arc, Condvar, Mutex};
14use std::thread::{self, JoinHandle};
15use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
16
17use fathomdb_embedder::{EmbedderEvent, MeanRecomputeTrigger};
18use fathomdb_embedder_api::{Embedder, EmbedderError as RuntimeEmbedderError, EmbedderIdentity};
19use fathomdb_query::compile_text_query;
20use fathomdb_schema::{
21 migrate_with_event_sink, MigrationError as SchemaMigrationError, MigrationStepReport,
22 CANONICAL_TABLES, LOCK_SUFFIX, MIGRATIONS, SCHEMA_VERSION,
23};
24use jsonschema::JSONSchema;
25use rusqlite::{params, Connection, OptionalExtension};
26use serde_json::Value;
27use sha2::Digest;
28use sqlite_vec::sqlite3_vec_init;
29
30#[cfg(unix)]
31use std::os::unix::fs::OpenOptionsExt;
32
33const DEFAULT_EMBEDDER_NAME: &str = "fathomdb-bge-small-en-v1.5";
39const DEFAULT_EMBEDDER_REVISION: &str = "5c38ec7c405ec4b44b94cc5a9bb96e735b38267a";
40const DEFAULT_EMBEDDER_DIMENSION: u32 = 384;
41
42const BGE_SMALL_EMBEDDER_NAME: &str = "fathomdb-bge-small-en-v1.5";
52
53const DEFAULT_SLOW_THRESHOLD_MS: u64 = 100;
56const DEFAULT_VECTOR_PROFILE: &str = "default";
57const DEFAULT_VECTOR_PARTITION: &str = "vector_default";
58const REBUILD_DRAIN_TIMEOUT_MS: u64 = 30_000;
64const DEFAULT_PROVENANCE_ROW_CAP: u64 = 1_000_000;
65const PROJECTION_CURSOR_KEY: &str = "projection_cursor";
66const PROJECTION_WORKERS: usize = 2;
67const DEFAULT_EMBED_TIMEOUT_MS: u64 = 30_000;
73const DEFAULT_EMBED_CIRCUIT_THRESHOLD: u64 = 8;
81const PROJECTION_COMMIT_BATCH: usize = 16;
82const PROJECTION_INFLIGHT_LIMIT: usize = PROJECTION_WORKERS * PROJECTION_COMMIT_BATCH;
86const PROJECTION_SCAN_FETCH: usize = PROJECTION_INFLIGHT_LIMIT;
89const DEFAULT_PROJECTION_RETRY_DELAYS_MS: [u64; 3] = [1_000, 4_000, 16_000];
90
91const READER_POOL_SIZE: usize = 8;
95
96const READER_LOOKASIDE_SLOT_SIZE: std::os::raw::c_int = 1200;
104
105const READER_LOOKASIDE_SLOT_COUNT: std::os::raw::c_int = 500;
110
111pub struct Engine {
112 path: PathBuf,
113 next_cursor: AtomicU64,
114 closed: AtomicBool,
115 lock: Mutex<Option<File>>,
116 connection: Mutex<Option<Connection>>,
117 reader_pool: ReaderWorkerPool,
118 counters: lifecycle::Counters,
119 subscribers: Arc<lifecycle::SubscriberRegistry>,
120 profiling_enabled: Arc<AtomicBool>,
121 slow_threshold_ms: Arc<AtomicU64>,
122 runtime_embedder: Option<Arc<dyn Embedder>>,
123 runtime_embedder_identity: EmbedderIdentity,
124 projection_runtime: ProjectionRuntime,
125 provenance_row_cap: AtomicU64,
126 #[allow(clippy::vec_box)]
138 profile_contexts: Mutex<Vec<Box<ProfileContext>>>,
139 #[allow(dead_code)]
146 reader_lookaside_rcs: Vec<i32>,
147 #[cfg(debug_assertions)]
148 force_next_commit_failure: AtomicBool,
149}
150
151#[derive(Clone, Debug)]
152struct ProjectionJob {
153 cursor: u64,
154 kind: String,
155 body: String,
156}
157
158#[derive(Debug, Default)]
159struct ProjectionRuntimeState {
160 active_jobs: usize,
161 queued_jobs: usize,
162 frozen: bool,
163 pending_scan: bool,
164 stopping: bool,
165 in_flight: BTreeSet<u64>,
166}
167
168struct ProjectionRuntimeShared {
169 path: PathBuf,
170 embedder: Option<Arc<dyn Embedder>>,
171 embedder_identity: EmbedderIdentity,
172 state: Mutex<ProjectionRuntimeState>,
173 state_cvar: Condvar,
174 queue: Mutex<VecDeque<ProjectionJob>>,
175 queue_cvar: Condvar,
176 retry_delays_ms: Mutex<Vec<u64>>,
177 embed_timeout_ms: AtomicU64,
185 embed_serialize: Mutex<()>,
227 live_embed_threads: Arc<AtomicU64>,
243 embed_circuit_open: AtomicBool,
244 embed_circuit_threshold: AtomicU64,
245 mean_accumulator: Mutex<Option<MeanAccumulator>>,
251 pending_events: Mutex<Vec<EmbedderEvent>>,
256 commit_gate: Mutex<()>,
264 search_limit_override: AtomicUsize,
272 #[cfg(debug_assertions)]
277 force_recompute_failure: AtomicBool,
278}
279
280impl std::fmt::Debug for ProjectionRuntimeShared {
281 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
282 f.debug_struct("ProjectionRuntimeShared")
283 .field("path", &self.path)
284 .field("embedder_identity", &self.embedder_identity)
285 .finish_non_exhaustive()
286 }
287}
288
289#[derive(Debug)]
290struct ProjectionRuntime {
291 shared: Arc<ProjectionRuntimeShared>,
292 dispatcher: Mutex<Option<JoinHandle<()>>>,
293 workers: Mutex<Vec<JoinHandle<()>>>,
294}
295
296impl std::fmt::Debug for Engine {
297 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
298 f.debug_struct("Engine")
299 .field("path", &self.path)
300 .field("closed", &self.closed.load(Ordering::SeqCst))
301 .field("runtime_embedder_identity", &self.runtime_embedder_identity)
302 .finish_non_exhaustive()
303 }
304}
305
306#[derive(Debug)]
315struct ProfileContext {
316 subscribers: Arc<lifecycle::SubscriberRegistry>,
317 profiling_enabled: Arc<AtomicBool>,
318 slow_threshold_ms: Arc<AtomicU64>,
319}
320
321struct ReaderWorkerPool {
330 senders: Vec<SyncSender<ReaderRequest>>,
331 handles: Mutex<Option<Vec<JoinHandle<()>>>>,
332 next: AtomicUsize,
333 shutdown: AtomicBool,
334 live_workers: Arc<AtomicUsize>,
335}
336
337impl std::fmt::Debug for ReaderWorkerPool {
338 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
339 f.debug_struct("ReaderWorkerPool")
340 .field("worker_count", &self.senders.len())
341 .field("live_workers", &self.live_workers.load(Ordering::Relaxed))
342 .field("shutdown", &self.shutdown.load(Ordering::Relaxed))
343 .finish()
344 }
345}
346
347enum ReaderRequest {
351 Search {
352 compiled: fathomdb_query::CompiledQuery,
353 query_vector: Option<String>,
356 query_vector_bin: Option<String>,
360 search_limit: usize,
365 respond: SyncSender<ReaderResponse>,
366 },
367 Shutdown,
368 #[cfg(debug_assertions)]
374 LookasideStatus {
375 respond: SyncSender<i32>,
376 },
377 #[cfg(debug_assertions)]
383 CacheStatus {
384 snapshot_label: String,
385 respond: SyncSender<(String, i32, i32, i32)>,
386 },
387}
388
389type ReaderResponse = rusqlite::Result<(u64, Option<SoftFallback>, Vec<String>)>;
390
391#[cfg(debug_assertions)]
395#[doc(hidden)]
396#[derive(Clone, Debug)]
397pub struct CacheStatusReply {
398 pub worker_idx: usize,
399 pub snapshot_label: String,
400 pub cache_hit: i32,
401 pub cache_miss: i32,
402 pub cache_used_bytes: i32,
403}
404
405const READER_WORKER_CHANNEL_CAPACITY: usize = 4;
409
410impl ReaderWorkerPool {
411 fn new(connections: Vec<Connection>) -> Self {
412 let live_workers = Arc::new(AtomicUsize::new(0));
413 let mut senders = Vec::with_capacity(connections.len());
414 let mut handles = Vec::with_capacity(connections.len());
415 for (idx, connection) in connections.into_iter().enumerate() {
416 let (tx, rx) = mpsc::sync_channel::<ReaderRequest>(READER_WORKER_CHANNEL_CAPACITY);
417 let live = Arc::clone(&live_workers);
418 let handle = thread::Builder::new()
419 .name(format!("fathomdb-reader-{idx}"))
420 .spawn(move || reader_worker_loop(connection, rx, live))
421 .expect("spawn reader worker");
422 senders.push(tx);
423 handles.push(handle);
424 }
425 Self {
426 senders,
427 handles: Mutex::new(Some(handles)),
428 next: AtomicUsize::new(0),
429 shutdown: AtomicBool::new(false),
430 live_workers,
431 }
432 }
433
434 fn worker_count(&self) -> usize {
435 self.senders.len()
436 }
437
438 fn live_count(&self) -> usize {
439 self.live_workers.load(Ordering::SeqCst)
440 }
441
442 #[cfg(debug_assertions)]
447 fn lookaside_used_per_worker(&self) -> Vec<i32> {
448 let mut results = Vec::with_capacity(self.senders.len());
449 for sender in &self.senders {
450 let (tx, rx) = mpsc::sync_channel::<i32>(1);
451 if sender.send(ReaderRequest::LookasideStatus { respond: tx }).is_ok() {
452 results.push(rx.recv().unwrap_or(-1));
453 } else {
454 results.push(-1);
455 }
456 }
457 results
458 }
459
460 #[cfg(debug_assertions)]
466 fn cache_status_per_worker(&self, snapshot_label: &str) -> Vec<CacheStatusReply> {
467 let mut results = Vec::with_capacity(self.senders.len());
468 for (idx, sender) in self.senders.iter().enumerate() {
469 let (tx, rx) = mpsc::sync_channel::<(String, i32, i32, i32)>(1);
470 let request = ReaderRequest::CacheStatus {
471 snapshot_label: snapshot_label.to_string(),
472 respond: tx,
473 };
474 if sender.send(request).is_ok() {
475 if let Ok((label, hit, miss, used)) = rx.recv() {
476 results.push(CacheStatusReply {
477 worker_idx: idx,
478 snapshot_label: label,
479 cache_hit: hit,
480 cache_miss: miss,
481 cache_used_bytes: used,
482 });
483 continue;
484 }
485 }
486 results.push(CacheStatusReply {
487 worker_idx: idx,
488 snapshot_label: snapshot_label.to_string(),
489 cache_hit: -1,
490 cache_miss: -1,
491 cache_used_bytes: -1,
492 });
493 }
494 results
495 }
496
497 fn dispatch(&self, request: ReaderRequest) -> Result<(), ReaderRequest> {
501 if self.shutdown.load(Ordering::Relaxed) {
502 return Err(request);
503 }
504 let n = self.senders.len();
505 if n == 0 {
506 return Err(request);
507 }
508 let idx = self.next.fetch_add(1, Ordering::Relaxed) % n;
509 self.senders[idx].send(request).map_err(|err| err.0)
510 }
511
512 fn shutdown(&self) {
516 if self.shutdown.swap(true, Ordering::SeqCst) {
517 return;
518 }
519 for sender in &self.senders {
520 let _ = sender.send(ReaderRequest::Shutdown);
521 }
522 if let Ok(mut slot) = self.handles.lock() {
523 if let Some(handles) = slot.take() {
524 for handle in handles {
525 let _ = handle.join();
526 }
527 }
528 }
529 }
530}
531
532impl Drop for ReaderWorkerPool {
533 fn drop(&mut self) {
534 self.shutdown();
535 }
536}
537
538fn reader_worker_loop(
539 mut connection: Connection,
540 rx: Receiver<ReaderRequest>,
541 live_workers: Arc<AtomicUsize>,
542) {
543 live_workers.fetch_add(1, Ordering::SeqCst);
544 struct LiveGuard(Arc<AtomicUsize>);
546 impl Drop for LiveGuard {
547 fn drop(&mut self) {
548 self.0.fetch_sub(1, Ordering::SeqCst);
549 }
550 }
551 let _guard = LiveGuard(live_workers);
552
553 while let Ok(request) = rx.recv() {
554 match request {
555 ReaderRequest::Shutdown => break,
556 ReaderRequest::Search {
557 compiled,
558 query_vector,
559 query_vector_bin,
560 search_limit,
561 respond,
562 } => {
563 let result = read_search_in_tx(
564 &mut connection,
565 &compiled,
566 query_vector.as_deref(),
567 query_vector_bin.as_deref(),
568 search_limit,
569 );
570 let _ = respond.send(result);
573 }
574 #[cfg(debug_assertions)]
575 ReaderRequest::LookasideStatus { respond } => {
576 let _ = respond.send(read_lookaside_used_hiwtr(&connection));
577 }
578 #[cfg(debug_assertions)]
579 ReaderRequest::CacheStatus { snapshot_label, respond } => {
580 let (hit, miss, used) = read_cache_status(&connection);
581 let _ = respond.send((snapshot_label, hit, miss, used));
582 }
583 }
584 }
585
586 uninstall_profile_callback(&connection);
591 drop(connection);
592}
593
594impl ProjectionRuntime {
595 fn new(
596 path: PathBuf,
597 embedder: Option<Arc<dyn Embedder>>,
598 embedder_identity: EmbedderIdentity,
599 mean_already_pinned: bool,
600 ) -> Self {
601 let mc_required = identity_requires_mean_centering(&embedder_identity);
608 let mean_accumulator = if mc_required && !mean_already_pinned {
609 Some(MeanAccumulator::new(embedder_identity.dimension as usize))
610 } else {
611 None
612 };
613 let shared = Arc::new(ProjectionRuntimeShared {
614 path,
615 embedder,
616 embedder_identity,
617 state: Mutex::new(ProjectionRuntimeState::default()),
618 state_cvar: Condvar::new(),
619 queue: Mutex::new(VecDeque::new()),
620 queue_cvar: Condvar::new(),
621 retry_delays_ms: Mutex::new(DEFAULT_PROJECTION_RETRY_DELAYS_MS.to_vec()),
622 embed_timeout_ms: AtomicU64::new(DEFAULT_EMBED_TIMEOUT_MS),
623 embed_serialize: Mutex::new(()),
624 live_embed_threads: Arc::new(AtomicU64::new(0)),
625 embed_circuit_open: AtomicBool::new(false),
626 embed_circuit_threshold: AtomicU64::new(DEFAULT_EMBED_CIRCUIT_THRESHOLD),
627 mean_accumulator: Mutex::new(mean_accumulator),
628 pending_events: Mutex::new(Vec::new()),
629 commit_gate: Mutex::new(()),
630 search_limit_override: AtomicUsize::new(SEARCH_RERANK_LIMIT),
631 #[cfg(debug_assertions)]
632 force_recompute_failure: AtomicBool::new(false),
633 });
634
635 let dispatcher_shared = Arc::clone(&shared);
636 let dispatcher = thread::spawn(move || projection_dispatcher_loop(dispatcher_shared));
637
638 let mut workers = Vec::with_capacity(PROJECTION_WORKERS);
639 for _ in 0..PROJECTION_WORKERS {
640 let worker_shared = Arc::clone(&shared);
641 workers.push(thread::spawn(move || projection_worker_loop(worker_shared)));
642 }
643
644 Self { shared, dispatcher: Mutex::new(Some(dispatcher)), workers: Mutex::new(workers) }
645 }
646
647 fn notify_new_work(&self) {
648 if let Ok(mut state) = self.shared.state.lock() {
649 state.pending_scan = true;
650 self.shared.state_cvar.notify_all();
651 }
652 }
653
654 fn set_frozen(&self, frozen: bool) {
655 if let Ok(mut state) = self.shared.state.lock() {
656 state.frozen = frozen;
657 if !frozen {
658 state.pending_scan = true;
659 }
660 self.shared.state_cvar.notify_all();
661 }
662 }
663
664 fn wait_for_idle(&self, timeout_ms: u64) -> bool {
665 let deadline = Instant::now() + Duration::from_millis(timeout_ms);
666 let mut state = match self.shared.state.lock() {
667 Ok(state) => state,
668 Err(_) => return false,
669 };
670 loop {
671 if state.active_jobs == 0 && state.queued_jobs == 0 {
672 drop(state);
673 if !database_has_pending_projection_work(&self.shared.path).unwrap_or(true) {
674 return true;
675 }
676 state = match self.shared.state.lock() {
677 Ok(state) => state,
678 Err(_) => return false,
679 };
680 }
681 let now = Instant::now();
682 if now >= deadline {
683 return false;
684 }
685 let wait = deadline.saturating_duration_since(now);
686 let Ok((next_state, _)) = self.shared.state_cvar.wait_timeout(state, wait) else {
687 return false;
688 };
689 state = next_state;
690 }
691 }
692
693 fn set_retry_delays_for_test(&self, delays_ms: &[u64]) {
694 if let Ok(mut delays) = self.shared.retry_delays_ms.lock() {
695 *delays = delays_ms.to_vec();
696 }
697 }
698
699 fn set_embed_timeout_ms_for_test(&self, timeout_ms: u64) {
700 self.shared.embed_timeout_ms.store(timeout_ms, Ordering::Relaxed);
701 }
702
703 fn set_embed_circuit_threshold_for_test(&self, threshold: u64) {
704 self.shared.embed_circuit_threshold.store(threshold, Ordering::Relaxed);
705 }
706
707 fn embed_circuit_open_for_test(&self) -> bool {
708 self.shared.embed_circuit_open.load(Ordering::Relaxed)
709 }
710
711 fn stop(&self) {
712 if let Ok(mut state) = self.shared.state.lock() {
713 if state.stopping {
714 return;
715 }
716 state.stopping = true;
717 state.pending_scan = false;
718 self.shared.state_cvar.notify_all();
719 }
720 if let Ok(mut queue) = self.shared.queue.lock() {
721 queue.clear();
722 self.shared.queue_cvar.notify_all();
723 }
724
725 if let Ok(mut dispatcher) = self.dispatcher.lock() {
726 if let Some(handle) = dispatcher.take() {
727 let _ = handle.join();
728 }
729 }
730 if let Ok(mut workers) = self.workers.lock() {
731 for handle in workers.drain(..) {
732 let _ = handle.join();
733 }
734 }
735 }
736}
737
738#[derive(Clone, Debug, Eq, PartialEq)]
739pub struct OpenReport {
740 pub schema_version_before: u32,
741 pub schema_version_after: u32,
742 pub migration_steps: Vec<MigrationStepReport>,
743 pub embedder_warmup_ms: u64,
744 pub query_backend: &'static str,
745 pub default_embedder: EmbedderIdentity,
746 pub embedder_download_ms: Option<u64>,
760 pub embedder_events: Vec<EmbedderEvent>,
764 pub embedder_mean_centering_required: bool,
771 pub embedder_mean_vec_pinned: bool,
777}
778
779#[derive(Debug)]
780pub struct OpenedEngine {
781 pub engine: Engine,
782 pub report: OpenReport,
783}
784
785#[derive(Clone, Debug)]
788struct LoaderInfo {
789 download_ms: Option<u64>,
790 events: Vec<EmbedderEvent>,
791}
792
793#[derive(Clone, Debug, Eq, PartialEq)]
794pub struct WriteReceipt {
795 pub cursor: u64,
796}
797
798#[derive(Clone, Debug, Eq, PartialEq)]
804pub struct SoftFallback {
805 pub branch: SoftFallbackBranch,
806}
807
808#[derive(Clone, Copy, Debug, Eq, PartialEq)]
814pub enum SoftFallbackBranch {
815 Vector,
816 Text,
817}
818
819#[derive(Clone, Debug, Eq, PartialEq)]
820pub struct SearchResult {
821 pub projection_cursor: u64,
822 pub soft_fallback: Option<SoftFallback>,
823 pub results: Vec<String>,
824}
825
826#[non_exhaustive]
832#[derive(Clone, Debug, Eq, PartialEq)]
833pub enum PreparedWrite {
834 Node {
835 kind: String,
836 body: String,
837 source_id: Option<String>,
842 },
843 Edge {
844 kind: String,
845 from: String,
846 to: String,
847 source_id: Option<String>,
849 },
850 OpStore {
851 collection: String,
852 record_key: String,
853 schema_id: Option<String>,
854 body: String,
855 },
856 AdminSchema {
857 name: String,
858 kind: String,
859 schema_json: String,
860 retention_json: String,
861 },
862}
863
864#[derive(Clone, Debug, Default, Eq, PartialEq)]
870pub struct CounterSnapshot {
871 pub queries: u64,
872 pub writes: u64,
873 pub write_rows: u64,
874 pub errors_by_code: BTreeMap<String, u64>,
875 pub admin_ops: u64,
876 pub cache_hit: u64,
877 pub cache_miss: u64,
878}
879
880pub use lifecycle::Subscription;
881
882#[derive(Clone, Debug, Eq, PartialEq)]
887pub struct CorruptionDetail {
888 pub kind: CorruptionKind,
889 pub stage: OpenStage,
890 pub locator: CorruptionLocator,
891 pub recovery_hint: RecoveryHint,
892}
893
894#[derive(Clone, Copy, Debug, Eq, PartialEq)]
900pub enum CorruptionKind {
901 WalReplayFailure,
902 HeaderMalformed,
903 SchemaInconsistent,
904 EmbedderIdentityDrift,
905}
906
907#[derive(Clone, Copy, Debug, Eq, PartialEq)]
913pub enum OpenStage {
914 WalReplay,
915 HeaderProbe,
916 SchemaProbe,
917 EmbedderIdentity,
918}
919
920#[derive(Clone, Copy, Debug, Eq, PartialEq)]
926pub enum CorruptionLocator {
927 FileOffset { offset: u64 },
928 PageId { page: u32 },
929 TableRow { table: &'static str, rowid: i64 },
930 Vec0ShadowRow { partition: &'static str, rowid: i64 },
931 MigrationStep { from: u32, to: u32 },
932 OpaqueSqliteError { sqlite_extended_code: i32 },
933}
934
935#[derive(Clone, Copy, Debug, Eq, PartialEq)]
941pub struct RecoveryHint {
942 pub code: &'static str,
943 pub doc_anchor: &'static str,
944}
945
946#[derive(Clone, Debug, Eq, PartialEq)]
947pub enum EngineOpenError {
948 DatabaseLocked {
949 holder_pid: Option<u32>,
950 },
951 Corruption(CorruptionDetail),
952 IncompatibleSchemaVersion {
953 seen: u32,
954 supported: u32,
955 },
956 MigrationError {
957 schema_version_before: u32,
958 schema_version_current: u32,
959 step_id: u32,
960 },
961 EmbedderIdentityMismatch {
962 stored: EmbedderIdentity,
963 supplied: EmbedderIdentity,
964 },
965 EmbedderDimensionMismatch {
966 stored: u32,
967 supplied: u32,
968 },
969 Embedder(RuntimeEmbedderError),
971 Io {
972 message: String,
973 },
974}
975
976#[derive(Clone)]
979pub enum EmbedderChoice {
980 Default,
989 Caller(Arc<dyn Embedder>),
992 None,
996}
997
998impl Display for EngineOpenError {
999 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1000 match self {
1001 Self::DatabaseLocked { holder_pid } => match holder_pid {
1002 Some(pid) => write!(f, "database is locked by process {pid}"),
1003 None => write!(f, "database is locked by another engine instance"),
1004 },
1005 Self::Corruption(detail) => {
1006 write!(
1007 f,
1008 "engine corruption at {:?} stage: {}",
1009 detail.stage, detail.recovery_hint.code
1010 )
1011 }
1012 Self::IncompatibleSchemaVersion { seen, supported } => write!(
1013 f,
1014 "database schema version {seen} is incompatible with supported version {supported}"
1015 ),
1016 Self::MigrationError {
1017 schema_version_before,
1018 schema_version_current,
1019 step_id,
1020 } => write!(
1021 f,
1022 "schema migration failed at step {step_id}; schema version remained between {schema_version_before} and {schema_version_current}"
1023 ),
1024 Self::EmbedderIdentityMismatch { stored, supplied } => write!(
1025 f,
1026 "embedder identity mismatch: stored {}@{}, supplied {}@{}",
1027 stored.name, stored.revision, supplied.name, supplied.revision,
1028 ),
1029 Self::EmbedderDimensionMismatch { stored, supplied } => write!(
1030 f,
1031 "embedder vector dimension mismatch: stored {stored}, supplied {supplied}",
1032 ),
1033 Self::Embedder(err) => match err {
1034 RuntimeEmbedderError::Timeout => write!(f, "embedder timeout during open"),
1035 RuntimeEmbedderError::Failed { message } => {
1036 write!(f, "embedder failure during open: {message}")
1037 }
1038 },
1039 Self::Io { message } => write!(f, "database I/O error: {message}"),
1040 }
1041 }
1042}
1043
1044impl Error for EngineOpenError {}
1045
1046#[derive(Clone, Debug, Eq, PartialEq)]
1047pub enum EngineError {
1048 Storage,
1049 Projection,
1050 Vector,
1051 Embedder,
1052 EmbedderNotConfigured,
1053 KindNotVectorIndexed,
1054 EmbedderDimensionMismatch { expected: u32, actual: u32 },
1055 Scheduler,
1056 OpStore,
1057 WriteValidation,
1058 SchemaValidation,
1059 Overloaded,
1060 Closing,
1061}
1062
1063impl Display for EngineError {
1064 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1065 match self {
1066 Self::Storage => write!(f, "storage error"),
1067 Self::Projection => write!(f, "projection error"),
1068 Self::Vector => write!(f, "vector error"),
1069 Self::Embedder => write!(f, "embedder error"),
1070 Self::EmbedderNotConfigured => write!(f, "embedder is not configured"),
1071 Self::KindNotVectorIndexed => write!(f, "kind is not configured for vector indexing"),
1072 Self::EmbedderDimensionMismatch { expected, actual } => {
1073 write!(f, "embedder dimension mismatch: expected {expected}, actual {actual}")
1074 }
1075 Self::Scheduler => write!(f, "scheduler error"),
1076 Self::OpStore => write!(f, "op-store error"),
1077 Self::WriteValidation => write!(f, "write validation error"),
1078 Self::SchemaValidation => write!(f, "schema validation error"),
1079 Self::Overloaded => write!(f, "engine overloaded"),
1080 Self::Closing => write!(f, "engine is closing"),
1081 }
1082 }
1083}
1084
1085impl EngineError {
1086 fn stable_code(&self) -> &'static str {
1091 match self {
1092 Self::Storage => "StorageError",
1093 Self::Projection => "ProjectionError",
1094 Self::Vector => "VectorError",
1095 Self::Embedder => "EmbedderError",
1096 Self::EmbedderNotConfigured => "EmbedderNotConfiguredError",
1097 Self::KindNotVectorIndexed => "KindNotVectorIndexedError",
1098 Self::EmbedderDimensionMismatch { .. } => "EmbedderDimensionMismatchError",
1099 Self::Scheduler => "SchedulerError",
1100 Self::OpStore => "OpStoreError",
1101 Self::WriteValidation => "WriteValidationError",
1102 Self::SchemaValidation => "SchemaValidationError",
1103 Self::Overloaded => "OverloadedError",
1104 Self::Closing => "ClosingError",
1105 }
1106 }
1107}
1108
1109impl Error for EngineError {}
1110
1111#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1116pub struct CheckIntegrityOpts {
1117 pub quick: bool,
1118 pub full: bool,
1119 pub round_trip: bool,
1120}
1121
1122#[derive(Clone, Debug, Eq, PartialEq)]
1126pub enum Section {
1127 Clean,
1128 Findings(Vec<Finding>),
1129}
1130
1131#[derive(Clone, Debug, Eq, PartialEq)]
1135pub struct Finding {
1136 pub code: &'static str,
1137 pub stage: &'static str,
1138 pub locator: CorruptionLocator,
1139 pub doc_anchor: &'static str,
1140 pub detail: String,
1141}
1142
1143#[derive(Clone, Debug, Eq, PartialEq)]
1146pub struct IntegrityReport {
1147 pub physical: Section,
1148 pub logical: Section,
1149 pub semantic: Section,
1150}
1151
1152#[derive(Clone, Debug, Eq, PartialEq)]
1157pub struct SafeExportArtifact {
1158 pub export_path: PathBuf,
1159 pub manifest_path: PathBuf,
1160 pub manifest_sha256: String,
1161}
1162
1163#[derive(Clone, Debug, Eq, PartialEq)]
1167pub struct TraceReport {
1168 pub source_ref: String,
1169 pub events: Vec<TraceEvent>,
1170}
1171
1172#[derive(Clone, Debug, Eq, PartialEq)]
1175pub struct TraceEvent {
1176 pub write_cursor: u64,
1177 pub kind: String,
1178 pub table: &'static str,
1179}
1180
1181#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1186pub enum RebuildKind {
1187 Projections,
1188 Vec0,
1189}
1190
1191#[derive(Clone, Debug, Eq, PartialEq)]
1198pub struct RebuildReport {
1199 pub kind: RebuildKind,
1200 pub rows_invalidated: u64,
1201 pub rows_rebuilt: u64,
1202 pub projection_cursor_after: u64,
1203}
1204
1205#[derive(Clone, Debug, Eq, PartialEq)]
1209pub struct ExciseReport {
1210 pub source_ref: String,
1211 pub nodes_excised: u64,
1212 pub edges_excised: u64,
1213 pub projections_invalidated: u64,
1214}
1215
1216#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1220pub enum VerifyEmbedderStatus {
1221 Match,
1222 IdentityMismatch,
1223 DimensionMismatch,
1224 BothMismatch,
1225}
1226
1227#[derive(Clone, Debug, Eq, PartialEq)]
1231pub struct VerifyEmbedderReport {
1232 pub stored_identity: String,
1233 pub stored_dimension: u32,
1234 pub supplied_identity: String,
1235 pub supplied_dimension: u32,
1236 pub status: VerifyEmbedderStatus,
1237}
1238
1239#[derive(Clone, Debug, Eq, PartialEq)]
1241pub struct SchemaObject {
1242 pub name: String,
1243 pub sql: String,
1244}
1245
1246#[derive(Clone, Debug, Eq, PartialEq)]
1251pub struct DumpSchemaReport {
1252 pub user_version: u32,
1253 pub tables: Vec<SchemaObject>,
1254 pub indexes: Vec<SchemaObject>,
1255}
1256
1257#[derive(Clone, Debug, Eq, PartialEq)]
1259pub struct TableRowCount {
1260 pub name: String,
1261 pub rows: u64,
1262}
1263
1264#[derive(Clone, Debug, Eq, PartialEq)]
1268pub struct DumpRowCountsReport {
1269 pub counts: Vec<TableRowCount>,
1270}
1271
1272#[derive(Clone, Debug, Eq, PartialEq)]
1276pub struct DumpProfileReport {
1277 pub embedder_identity: String,
1278 pub embedder_dimension: u32,
1279 pub vectorized_kinds: Vec<String>,
1280}
1281
1282#[derive(Clone, Debug, PartialEq)]
1290pub struct MeanRecomputeReport {
1291 pub dim: u32,
1292 pub old_doc_count: u64,
1293 pub doc_count_requantized: u64,
1294 pub drift_cos_before: f32,
1295 pub mean_was_pinned: bool,
1296 pub elapsed_ms: u64,
1297}
1298
1299#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1303pub enum TruncateWalStatus {
1304 Done,
1305 Busy,
1306}
1307
1308#[derive(Clone, Debug, Eq, PartialEq)]
1312pub struct TruncateWalReport {
1313 pub status: TruncateWalStatus,
1314 pub busy: u32,
1315 pub log_frames: u32,
1316 pub checkpointed_frames: u32,
1317}
1318
1319impl Drop for Engine {
1320 fn drop(&mut self) {
1321 let _ = self.close();
1322 }
1323}
1324
1325impl Engine {
1326 pub fn open(path: impl Into<PathBuf>) -> Result<OpenedEngine, EngineOpenError> {
1327 Self::open_with_embedder_and_subscriber(
1328 path,
1329 default_embedder_identity(),
1330 None,
1331 None,
1332 None,
1333 &mut |_| {},
1334 )
1335 }
1336
1337 pub fn open_with_choice(
1346 path: impl Into<PathBuf>,
1347 choice: EmbedderChoice,
1348 ) -> Result<OpenedEngine, EngineOpenError> {
1349 match choice {
1350 EmbedderChoice::Default => Self::open_default_embedder(path),
1351 EmbedderChoice::Caller(embedder) => {
1352 let identity = embedder.identity();
1353 Self::open_with_embedder_and_subscriber(
1354 path,
1355 identity,
1356 Some(embedder),
1357 None,
1358 None,
1359 &mut |_| {},
1360 )
1361 }
1362 EmbedderChoice::None => Self::open_with_embedder_and_subscriber(
1363 path,
1364 default_embedder_identity(),
1365 None,
1366 None,
1367 None,
1368 &mut |_| {},
1369 ),
1370 }
1371 }
1372
1373 #[cfg(feature = "default-embedder")]
1378 fn open_default_embedder(path: impl Into<PathBuf>) -> Result<OpenedEngine, EngineOpenError> {
1379 use std::time::Instant as DownloadInstant;
1380 let download_start = DownloadInstant::now();
1381 let weights = fathomdb_embedder::loader::load_pinned_default_embedder().map_err(|err| {
1382 EngineOpenError::Embedder(RuntimeEmbedderError::Failed {
1383 message: format!("default embedder loader: {err}"),
1384 })
1385 })?;
1386 let events = weights.events.clone();
1387 let download_ms = if weights.bytes_downloaded > 0 {
1388 Some(u64::try_from(download_start.elapsed().as_millis()).unwrap_or(u64::MAX))
1389 } else {
1390 None
1391 };
1392 let embedder =
1393 fathomdb_embedder::CandleBgeEmbedder::new_from_weights(weights).map_err(|err| {
1394 EngineOpenError::Embedder(RuntimeEmbedderError::Failed {
1395 message: format!("default embedder construct: {err}"),
1396 })
1397 })?;
1398 let embedder: Arc<dyn Embedder> = Arc::new(embedder);
1399 let identity = embedder.identity();
1400 let loader_info = LoaderInfo { download_ms, events };
1401 Self::open_with_embedder_and_subscriber(
1402 path,
1403 identity,
1404 Some(embedder),
1405 Some(loader_info),
1406 None,
1407 &mut |_| {},
1408 )
1409 }
1410
1411 #[cfg(not(feature = "default-embedder"))]
1412 fn open_default_embedder(_path: impl Into<PathBuf>) -> Result<OpenedEngine, EngineOpenError> {
1413 Err(EngineOpenError::Embedder(RuntimeEmbedderError::Failed {
1414 message: "EmbedderChoice::Default requires the `default-embedder` Cargo feature"
1415 .to_string(),
1416 }))
1417 }
1418
1419 pub fn open_with_migration_event_sink(
1420 path: impl Into<PathBuf>,
1421 mut emit_migration_event: impl FnMut(&MigrationStepReport),
1422 ) -> Result<OpenedEngine, EngineOpenError> {
1423 Self::open_with_embedder_and_subscriber(
1424 path,
1425 default_embedder_identity(),
1426 None,
1427 None,
1428 None,
1429 &mut emit_migration_event,
1430 )
1431 }
1432
1433 #[cfg(debug_assertions)]
1434 #[doc(hidden)]
1435 pub fn open_with_migrations_for_test(
1436 path: impl Into<PathBuf>,
1437 migrations: &'static [fathomdb_schema::Migration],
1438 mut emit_migration_event: impl FnMut(&MigrationStepReport),
1439 ) -> Result<OpenedEngine, EngineOpenError> {
1440 Self::open_with_migrations(
1441 path,
1442 migrations,
1443 default_embedder_identity(),
1444 None,
1445 None,
1446 &mut emit_migration_event,
1447 None,
1448 )
1449 }
1450
1451 #[doc(hidden)]
1452 pub fn open_with_subscriber_for_test(
1453 path: impl Into<PathBuf>,
1454 subscriber: Arc<dyn lifecycle::Subscriber>,
1455 ) -> Result<OpenedEngine, EngineOpenError> {
1456 Self::open_with_embedder_and_subscriber(
1457 path,
1458 default_embedder_identity(),
1459 None,
1460 None,
1461 Some(subscriber),
1462 &mut |_| {},
1463 )
1464 }
1465
1466 #[doc(hidden)]
1467 pub fn open_without_embedder_for_test(
1468 path: impl Into<PathBuf>,
1469 ) -> Result<OpenedEngine, EngineOpenError> {
1470 Self::open_with_embedder_and_subscriber(
1471 path,
1472 default_embedder_identity(),
1473 None,
1474 None,
1475 None,
1476 &mut |_| {},
1477 )
1478 }
1479
1480 #[doc(hidden)]
1481 pub fn open_with_embedder_for_test(
1482 path: impl Into<PathBuf>,
1483 embedder: Arc<dyn Embedder>,
1484 ) -> Result<OpenedEngine, EngineOpenError> {
1485 let identity = embedder.identity();
1486 Self::open_with_embedder_and_subscriber(
1487 path,
1488 identity,
1489 Some(embedder),
1490 None,
1491 None,
1492 &mut |_| {},
1493 )
1494 }
1495
1496 fn open_with_embedder_and_subscriber(
1497 path: impl Into<PathBuf>,
1498 embedder_identity: EmbedderIdentity,
1499 runtime_embedder: Option<Arc<dyn Embedder>>,
1500 loader_info: Option<LoaderInfo>,
1501 initial_subscriber: Option<Arc<dyn lifecycle::Subscriber>>,
1502 emit_migration_event: &mut impl FnMut(&MigrationStepReport),
1503 ) -> Result<OpenedEngine, EngineOpenError> {
1504 Self::open_with_migrations(
1505 path,
1506 MIGRATIONS,
1507 embedder_identity,
1508 runtime_embedder,
1509 loader_info,
1510 emit_migration_event,
1511 initial_subscriber,
1512 )
1513 }
1514
1515 fn open_with_migrations(
1516 path: impl Into<PathBuf>,
1517 migrations: &'static [fathomdb_schema::Migration],
1518 embedder_identity: EmbedderIdentity,
1519 runtime_embedder: Option<Arc<dyn Embedder>>,
1520 loader_info: Option<LoaderInfo>,
1521 emit_migration_event: &mut impl FnMut(&MigrationStepReport),
1522 initial_subscriber: Option<Arc<dyn lifecycle::Subscriber>>,
1523 ) -> Result<OpenedEngine, EngineOpenError> {
1524 let canonical_path = canonical_database_path(&path.into())?;
1525 let lock = acquire_lock(&canonical_path)?;
1526 let open_result = Self::open_locked(
1527 canonical_path.clone(),
1528 migrations,
1529 &embedder_identity,
1530 emit_migration_event,
1531 );
1532
1533 match open_result {
1534 Ok((connection, readers, mut report, reader_lookaside_rcs)) => {
1535 if let Some(info) = loader_info {
1541 if info.download_ms.is_some() {
1542 report.embedder_download_ms = info.download_ms;
1543 }
1544 if !info.events.is_empty() {
1545 report.embedder_events = info.events;
1546 }
1547 }
1548 let next_cursor = load_next_cursor(&connection);
1549 let subscribers = Arc::new(lifecycle::SubscriberRegistry::new());
1550 let profiling_enabled = Arc::new(AtomicBool::new(false));
1551 let slow_threshold_ms = Arc::new(AtomicU64::new(DEFAULT_SLOW_THRESHOLD_MS));
1552 let mut profile_contexts: Vec<Box<ProfileContext>> = Vec::new();
1553 let projection_runtime = ProjectionRuntime::new(
1554 canonical_path.clone(),
1555 runtime_embedder.clone(),
1556 embedder_identity.clone(),
1557 report.embedder_mean_vec_pinned,
1558 );
1559
1560 install_profile_callback(
1561 &connection,
1562 &subscribers,
1563 &profiling_enabled,
1564 &slow_threshold_ms,
1565 &mut profile_contexts,
1566 );
1567 for reader in &readers {
1568 install_profile_callback(
1569 reader,
1570 &subscribers,
1571 &profiling_enabled,
1572 &slow_threshold_ms,
1573 &mut profile_contexts,
1574 );
1575 }
1576
1577 let opened = OpenedEngine {
1578 engine: Self {
1579 path: canonical_path.clone(),
1580 next_cursor: AtomicU64::new(next_cursor),
1581 closed: AtomicBool::new(false),
1582 lock: Mutex::new(Some(lock)),
1583 connection: Mutex::new(Some(connection)),
1584 reader_pool: ReaderWorkerPool::new(readers),
1585 counters: lifecycle::Counters::new(),
1586 subscribers,
1587 profiling_enabled,
1588 slow_threshold_ms,
1589 runtime_embedder,
1590 runtime_embedder_identity: embedder_identity,
1591 projection_runtime,
1592 provenance_row_cap: AtomicU64::new(DEFAULT_PROVENANCE_ROW_CAP),
1593 profile_contexts: Mutex::new(profile_contexts),
1594 reader_lookaside_rcs,
1595 #[cfg(debug_assertions)]
1596 force_next_commit_failure: AtomicBool::new(false),
1597 },
1598 report,
1599 };
1600 if let Some(subscriber) = initial_subscriber {
1601 opened.engine.subscribers.attach_persistent(subscriber);
1602 }
1603 if database_has_pending_projection_work(&canonical_path).unwrap_or(false) {
1604 opened.engine.projection_runtime.notify_new_work();
1605 }
1606 Ok(opened)
1607 }
1608 Err(err) => {
1609 if let Some(subscriber) = initial_subscriber {
1610 emit_open_error_event(&subscriber, &err);
1611 }
1612 drop(lock);
1613 Err(err)
1614 }
1615 }
1616 }
1617
1618 fn open_locked(
1619 path: PathBuf,
1620 migrations: &'static [fathomdb_schema::Migration],
1621 embedder_identity: &EmbedderIdentity,
1622 emit_migration_event: &mut impl FnMut(&MigrationStepReport),
1623 ) -> Result<(Connection, Vec<Connection>, OpenReport, Vec<i32>), EngineOpenError> {
1624 init_perf_experiments_runtime();
1625 register_sqlite_vec_extension();
1626 let mut connection = Connection::open(&path)
1627 .map_err(|err| map_open_sqlite_error(err, OpenStage::HeaderProbe))?;
1628 probe_database_header(&connection)?;
1636 probe_open_integrity(&connection)?;
1637 probe_wal_sidecar(&path)?;
1638 apply_perf_experiment_writer_pragmas(&connection);
1644 connection
1645 .pragma_update(None, "journal_mode", "WAL")
1646 .map_err(|err| map_open_sqlite_error(err, OpenStage::WalReplay))?;
1647
1648 reject_legacy_shape(&connection)?;
1649 let migration = migrate_with_event_sink(&connection, migrations, emit_migration_event)
1650 .map_err(map_migration_error)?;
1651 let mut embedder_mean_vec_pinned = check_embedder_profile(&connection, embedder_identity)?;
1652 ensure_vector_partition(&mut connection, embedder_identity.dimension).map_err(|_| {
1653 EngineOpenError::Io { message: "could not initialize vector partition".to_string() }
1654 })?;
1655
1656 if identity_requires_mean_centering(embedder_identity) && !embedder_mean_vec_pinned {
1664 let row_count: u64 = connection
1665 .query_row("SELECT COUNT(*) FROM vector_default", [], |row| row.get(0))
1666 .unwrap_or(0);
1667 if row_count >= MEAN_VEC_PIN_THRESHOLD {
1668 recover_mean_vec_pin(&mut connection, embedder_identity).map_err(|_| {
1669 EngineOpenError::Io {
1670 message: "could not recover mean-centering pin".to_string(),
1671 }
1672 })?;
1673 embedder_mean_vec_pinned = true;
1674 }
1675 }
1676
1677 let warmup_started = Instant::now();
1678 let embedder_mean_centering_required = embedder_identity.name == BGE_SMALL_EMBEDDER_NAME;
1683 let report = OpenReport {
1687 schema_version_before: migration.schema_version_before,
1688 schema_version_after: migration.schema_version_after,
1689 migration_steps: migration.migration_steps,
1690 embedder_warmup_ms: u64::try_from(warmup_started.elapsed().as_millis())
1691 .unwrap_or(u64::MAX),
1692 query_backend: "fathomdb-query + sqlite-vec",
1693 default_embedder: embedder_identity.clone(),
1694 embedder_download_ms: None,
1697 embedder_events: Vec::new(),
1699 embedder_mean_centering_required,
1700 embedder_mean_vec_pinned,
1701 };
1702
1703 let mut readers = Vec::with_capacity(READER_POOL_SIZE);
1704 let mut lookaside_rcs: Vec<i32> = Vec::with_capacity(READER_POOL_SIZE);
1705 for _ in 0..READER_POOL_SIZE {
1706 let reader = Connection::open(&path)
1707 .map_err(|err| map_open_sqlite_error(err, OpenStage::HeaderProbe))?;
1708 let rc: i32 = configure_reader_lookaside(&reader);
1713 debug_assert_eq!(
1714 rc,
1715 rusqlite::ffi::SQLITE_OK,
1716 "sqlite3_db_config(LOOKASIDE) must return SQLITE_OK on a freshly opened reader",
1717 );
1718 lookaside_rcs.push(rc);
1719 reader
1720 .pragma_update(None, "journal_mode", "WAL")
1721 .map_err(|err| map_open_sqlite_error(err, OpenStage::WalReplay))?;
1722 reader
1723 .pragma_update(None, "query_only", "ON")
1724 .map_err(|err| map_open_sqlite_error(err, OpenStage::SchemaProbe))?;
1725 apply_perf_experiment_reader_pragmas(&reader);
1726 readers.push(reader);
1727 }
1728
1729 Ok((connection, readers, report, lookaside_rcs))
1730 }
1731
1732 #[must_use]
1733 pub fn path(&self) -> &Path {
1734 &self.path
1735 }
1736
1737 pub fn write(&self, batch: &[PreparedWrite]) -> Result<WriteReceipt, EngineError> {
1738 let category = if batch_is_admin(batch) {
1739 lifecycle::EventCategory::Admin
1740 } else {
1741 lifecycle::EventCategory::Writer
1742 };
1743 self.emit_event(lifecycle::Phase::Started, category, None);
1744 let started = Instant::now();
1745 let outcome = self.write_inner(batch);
1746 self.detect_slow(started, category);
1747 match outcome {
1748 Ok(receipt) => {
1749 let rows = u64::try_from(batch.len()).unwrap_or(u64::MAX);
1750 if batch_is_admin(batch) {
1751 self.counters.record_admin();
1752 } else {
1753 self.counters.record_write(rows);
1754 }
1755 self.emit_event(lifecycle::Phase::Finished, category, None);
1756 Ok(receipt)
1757 }
1758 Err(err) => {
1759 let code = err.stable_code();
1760 self.counters.record_error(code);
1761 self.emit_event(lifecycle::Phase::Failed, category, Some(code));
1764 self.emit_event(
1765 lifecycle::Phase::Failed,
1766 lifecycle::EventCategory::Error,
1767 Some(code),
1768 );
1769 Err(err)
1770 }
1771 }
1772 }
1773
1774 fn write_inner(&self, batch: &[PreparedWrite]) -> Result<WriteReceipt, EngineError> {
1775 self.ensure_open()?;
1776
1777 if batch.is_empty() {
1778 return Err(EngineError::WriteValidation);
1779 }
1780
1781 let mut connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
1782 let connection = connection.as_mut().ok_or(EngineError::Closing)?;
1783 let plans = validate_batch(connection, batch)?;
1784 let projection_jobs = collect_projection_jobs(connection, batch)?;
1785 #[cfg(debug_assertions)]
1786 if self.force_next_commit_failure.swap(false, Ordering::SeqCst) {
1787 return Err(EngineError::Storage);
1788 }
1789 let base_cursor = self.next_cursor.load(Ordering::SeqCst);
1797 let increment = u64::try_from(batch.len()).unwrap_or(u64::MAX);
1798 let last_cursor = base_cursor.saturating_add(increment);
1799 let pending_projection = !projection_jobs.is_empty();
1800
1801 if let Err(err) = commit_batch(
1802 connection,
1803 batch,
1804 &plans,
1805 base_cursor,
1806 self.provenance_row_cap.load(Ordering::Relaxed),
1807 ) {
1808 self.emit_sqlite_internal_error(&err);
1809 return Err(EngineError::Storage);
1810 }
1811 self.next_cursor.store(last_cursor, Ordering::SeqCst);
1812 if pending_projection {
1813 self.projection_runtime.notify_new_work();
1814 }
1815
1816 Ok(WriteReceipt { cursor: last_cursor })
1817 }
1818
1819 pub fn search(&self, query: &str) -> Result<SearchResult, EngineError> {
1820 self.emit_event(lifecycle::Phase::Started, lifecycle::EventCategory::Search, None);
1821 let started = Instant::now();
1822 let outcome = self.search_inner(query);
1823 self.detect_slow(started, lifecycle::EventCategory::Search);
1824 match outcome {
1825 Ok(result) => {
1826 self.counters.record_query();
1827 self.emit_event(lifecycle::Phase::Finished, lifecycle::EventCategory::Search, None);
1828 Ok(result)
1829 }
1830 Err(err) => {
1831 let code = err.stable_code();
1832 self.counters.record_error(code);
1833 self.emit_event(
1834 lifecycle::Phase::Failed,
1835 lifecycle::EventCategory::Search,
1836 Some(code),
1837 );
1838 self.emit_event(
1839 lifecycle::Phase::Failed,
1840 lifecycle::EventCategory::Error,
1841 Some(code),
1842 );
1843 Err(err)
1844 }
1845 }
1846 }
1847
1848 fn detect_slow(&self, started: Instant, category: lifecycle::EventCategory) {
1849 let elapsed = started.elapsed();
1850 let threshold = self.slow_threshold_ms.load(Ordering::Relaxed);
1851 let threshold_duration = std::time::Duration::from_millis(threshold);
1852 if elapsed > threshold_duration {
1853 self.emit_event(lifecycle::Phase::Slow, category, None);
1860 }
1861 }
1862
1863 fn emit_event(
1864 &self,
1865 phase: lifecycle::Phase,
1866 category: lifecycle::EventCategory,
1867 code: Option<&'static str>,
1868 ) {
1869 let event =
1870 lifecycle::Event { phase, source: lifecycle::EventSource::Engine, category, code };
1871 self.subscribers.dispatch(&event);
1872 }
1873
1874 fn emit_sqlite_internal_error(&self, err: &rusqlite::Error) {
1881 if let Some(code) = sqlite_extended_code_name(err) {
1882 let event = lifecycle::Event {
1883 phase: lifecycle::Phase::Failed,
1884 source: lifecycle::EventSource::SqliteInternal,
1885 category: lifecycle::EventCategory::Error,
1886 code: Some(code),
1887 };
1888 self.subscribers.dispatch(&event);
1889 }
1890 }
1891
1892 fn search_inner(&self, query: &str) -> Result<SearchResult, EngineError> {
1893 self.ensure_open()?;
1894 if query.trim().is_empty() {
1895 return Err(EngineError::WriteValidation);
1896 }
1897
1898 let compiled = compile_text_query(query);
1899 let raw_query_vector =
1915 self.runtime_embedder.as_ref().and_then(|embedder| embedder.embed(query).ok());
1916 let query_vector_bin = match raw_query_vector.as_ref() {
1917 Some(vector) if identity_requires_mean_centering(&self.runtime_embedder_identity) => {
1918 let pinned = {
1919 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
1920 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
1921 read_pinned_mean_vec(connection, self.runtime_embedder_identity.dimension)?
1922 };
1923 match pinned {
1924 Some(mean) => serde_json::to_string(&subtract_mean(vector, &mean)).ok(),
1925 None => serde_json::to_string(vector).ok(),
1926 }
1927 }
1928 Some(vector) => serde_json::to_string(vector).ok(),
1929 None => None,
1930 };
1931 let query_vector = raw_query_vector.and_then(|vector| serde_json::to_string(&vector).ok());
1932 let search_limit = self
1936 .projection_runtime
1937 .shared
1938 .search_limit_override
1939 .load(Ordering::SeqCst)
1940 .max(SEARCH_RERANK_LIMIT);
1941 let (response_tx, response_rx) = mpsc::sync_channel::<ReaderResponse>(1);
1942 let request = ReaderRequest::Search {
1943 compiled,
1944 query_vector,
1945 query_vector_bin,
1946 search_limit,
1947 respond: response_tx,
1948 };
1949 if self.reader_pool.dispatch(request).is_err() {
1950 return Err(EngineError::Closing);
1951 }
1952 let search_result = response_rx.recv().map_err(|_| EngineError::Storage)?;
1953 let (cursor, soft_fallback, results) = match search_result {
1954 Ok(result) => result,
1955 Err(err) => {
1956 self.emit_sqlite_internal_error(&err);
1957 return Err(EngineError::Storage);
1958 }
1959 };
1960
1961 Ok(SearchResult { projection_cursor: cursor, soft_fallback, results })
1962 }
1963
1964 pub fn close(&self) -> Result<(), EngineError> {
1965 self.closed.store(true, Ordering::SeqCst);
1966 self.projection_runtime.stop();
1967 self.reader_pool.shutdown();
1976 if let Ok(mut connection) = self.connection.lock() {
1977 if let Some(conn) = connection.as_ref() {
1978 uninstall_profile_callback(conn);
1979 }
1980 connection.take();
1981 }
1982 if let Ok(mut contexts) = self.profile_contexts.lock() {
1983 contexts.clear();
1984 }
1985 if let Ok(mut lock) = self.lock.lock() {
1986 lock.take();
1987 }
1988 Ok(())
1989 }
1990
1991 pub fn drain(&self, timeout_ms: u64) -> Result<(), EngineError> {
1996 self.ensure_open()?;
1997 if self.projection_runtime.wait_for_idle(timeout_ms) {
1998 Ok(())
1999 } else {
2000 Err(EngineError::Scheduler)
2001 }
2002 }
2003
2004 #[must_use]
2008 pub fn counters(&self) -> CounterSnapshot {
2009 self.counters.snapshot()
2010 }
2011
2012 pub fn set_profiling(&self, enabled: bool) -> Result<(), EngineError> {
2018 self.profiling_enabled.store(enabled, Ordering::Relaxed);
2019 Ok(())
2020 }
2021
2022 pub fn set_slow_threshold_ms(&self, value: u64) -> Result<(), EngineError> {
2028 self.slow_threshold_ms.store(value, Ordering::Relaxed);
2029 Ok(())
2030 }
2031
2032 #[must_use]
2038 pub fn subscribe(&self, subscriber: Arc<dyn lifecycle::Subscriber>) -> Subscription {
2039 self.subscribers.attach(subscriber)
2040 }
2041
2042 #[cfg(debug_assertions)]
2043 #[doc(hidden)]
2044 pub fn reader_worker_count_for_test(&self) -> usize {
2045 self.reader_pool.worker_count()
2046 }
2047
2048 #[cfg(debug_assertions)]
2049 #[doc(hidden)]
2050 pub fn live_reader_worker_count_for_test(&self) -> usize {
2051 self.reader_pool.live_count()
2052 }
2053
2054 #[cfg(debug_assertions)]
2059 #[doc(hidden)]
2060 pub fn reader_lookaside_config_rcs_for_test(&self) -> Vec<i32> {
2061 self.reader_lookaside_rcs.clone()
2062 }
2063
2064 #[cfg(debug_assertions)]
2070 #[doc(hidden)]
2071 pub fn reader_lookaside_used_per_worker_for_test(&self) -> Vec<i32> {
2072 self.reader_pool.lookaside_used_per_worker()
2073 }
2074
2075 #[cfg(debug_assertions)]
2081 #[doc(hidden)]
2082 pub fn cache_status_per_worker_for_test(&self, label: &str) -> Vec<CacheStatusReply> {
2083 self.reader_pool.cache_status_per_worker(label)
2084 }
2085
2086 #[cfg(debug_assertions)]
2087 #[doc(hidden)]
2088 pub fn force_next_commit_failure_for_test(&self) {
2089 self.force_next_commit_failure.store(true, Ordering::SeqCst);
2090 }
2091
2092 #[cfg(debug_assertions)]
2099 #[doc(hidden)]
2100 pub fn execute_for_test(&self, sql: &str) -> Result<(), EngineError> {
2101 self.ensure_open()?;
2102 let started = Instant::now();
2103 {
2104 let mut connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2105 let connection = connection.as_mut().ok_or(EngineError::Closing)?;
2106 connection.execute_batch(sql).map_err(|_| EngineError::Storage)?;
2107 }
2108 self.detect_slow(started, lifecycle::EventCategory::Search);
2109 Ok(())
2110 }
2111
2112 #[doc(hidden)]
2123 #[cfg(debug_assertions)]
2124 pub fn run_one_thread_poison_for_test(&self) -> Result<(), EngineError> {
2125 self.ensure_open()?;
2126
2127 self.write(&[PreparedWrite::Node {
2130 kind: "doc".to_string(),
2131 body: "poison-fixture-seed".to_string(),
2132 source_id: None,
2133 }])?;
2134
2135 let poison_outcome: Mutex<Option<EngineError>> = Mutex::new(None);
2136 let poison_thread_id: AtomicU64 = AtomicU64::new(0);
2137
2138 thread::scope(|scope| {
2139 for _ in 0..4 {
2141 scope.spawn(|| {
2142 for _ in 0..4 {
2143 let _ = self.search("poison-fixture-seed");
2144 }
2145 });
2146 }
2147 scope.spawn(|| {
2149 let _ = self.write(&[PreparedWrite::Node {
2150 kind: "doc".to_string(),
2151 body: "writer-progress".to_string(),
2152 source_id: None,
2153 }]);
2154 });
2155 scope.spawn(|| {
2158 poison_thread_id.store(1, Ordering::SeqCst);
2161 if let Err(err) = self.write(&[]) {
2162 *poison_outcome.lock().expect("poison_outcome lock") = Some(err);
2163 }
2164 });
2165 });
2166
2167 let err = poison_outcome
2168 .into_inner()
2169 .expect("poison_outcome lock")
2170 .expect("poison thread must produce a deterministic error");
2171
2172 let projection_state = match self.projection_status_for_test("doc") {
2173 Ok(lifecycle::ProjectionStatus::Pending) => "Pending",
2174 Ok(lifecycle::ProjectionStatus::Failed) => "Failed",
2175 Ok(lifecycle::ProjectionStatus::UpToDate) => "UpToDate",
2176 Err(_) => "UpToDate",
2181 };
2182
2183 let context = lifecycle::StressFailureContext {
2184 thread_group_id: poison_thread_id.load(Ordering::SeqCst),
2185 op_kind: "write".to_string(),
2186 last_error_chain: vec![err.stable_code().to_string(), err.to_string()],
2187 projection_state: projection_state.to_string(),
2188 };
2189 self.subscribers.dispatch_stress_failure(&context);
2190 Ok(())
2191 }
2192
2193 #[doc(hidden)]
2194 pub fn set_projection_scheduler_frozen_for_test(&self, frozen: bool) {
2195 self.projection_runtime.set_frozen(frozen);
2196 }
2197
2198 #[doc(hidden)]
2199 pub fn set_projection_retry_delays_for_test(&self, delays_ms: &[u64]) {
2200 self.projection_runtime.set_retry_delays_for_test(delays_ms);
2201 }
2202
2203 #[doc(hidden)]
2206 pub fn set_embed_timeout_ms_for_test(&self, timeout_ms: u64) {
2207 self.projection_runtime.set_embed_timeout_ms_for_test(timeout_ms);
2208 }
2209
2210 #[doc(hidden)]
2213 pub fn set_embed_circuit_threshold_for_test(&self, threshold: u64) {
2214 self.projection_runtime.set_embed_circuit_threshold_for_test(threshold);
2215 }
2216
2217 #[doc(hidden)]
2219 pub fn embed_circuit_open_for_test(&self) -> bool {
2220 self.projection_runtime.embed_circuit_open_for_test()
2221 }
2222
2223 #[doc(hidden)]
2224 pub fn projection_status_for_test(
2225 &self,
2226 kind: &str,
2227 ) -> Result<lifecycle::ProjectionStatus, EngineError> {
2228 self.ensure_open()?;
2229 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2230 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
2231 projection_status(connection, kind)
2232 }
2233
2234 #[doc(hidden)]
2235 pub fn has_vector_for_cursor_for_test(&self, cursor: u64) -> Result<bool, EngineError> {
2236 self.ensure_open()?;
2237 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2238 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
2239 terminal_state_for_cursor(connection, cursor)
2240 .map(|state| matches!(state.as_deref(), Some("up_to_date")))
2241 .map_err(|_| EngineError::Storage)
2242 }
2243
2244 #[doc(hidden)]
2245 pub fn projection_failure_count_for_test(&self, cursor: u64) -> Result<u64, EngineError> {
2246 self.ensure_open()?;
2247 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2248 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
2249 connection
2250 .query_row(
2251 "SELECT COUNT(*) FROM operational_mutations
2252 WHERE collection_name = 'projection_failures'
2253 AND record_key = ?1",
2254 [cursor.to_string()],
2255 |row| row.get::<_, u64>(0),
2256 )
2257 .map_err(|_| EngineError::Storage)
2258 }
2259
2260 #[doc(hidden)]
2261 pub fn set_provenance_row_cap_for_test(&self, cap: Option<u64>) {
2262 self.provenance_row_cap.store(cap.unwrap_or(0), Ordering::Relaxed);
2263 }
2264
2265 #[doc(hidden)]
2266 pub fn provenance_row_count_for_test(&self) -> Result<u64, EngineError> {
2267 self.ensure_open()?;
2268 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2269 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
2270 connection
2271 .query_row("SELECT COUNT(*) FROM operational_mutations", [], |row| row.get::<_, u64>(0))
2272 .map_err(|_| EngineError::Storage)
2273 }
2274
2275 #[doc(hidden)]
2276 pub fn oldest_provenance_record_key_for_test(
2277 &self,
2278 collection: &str,
2279 ) -> Result<Option<String>, EngineError> {
2280 self.ensure_open()?;
2281 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2282 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
2283 connection
2284 .query_row(
2285 "SELECT record_key FROM operational_mutations
2286 WHERE collection_name = ?1
2287 ORDER BY id
2288 LIMIT 1",
2289 [collection],
2290 |row| row.get::<_, String>(0),
2291 )
2292 .map(Some)
2293 .or_else(|err| match err {
2294 rusqlite::Error::QueryReturnedNoRows => Ok(None),
2295 _ => Err(EngineError::Storage),
2296 })
2297 }
2298
2299 #[doc(hidden)]
2300 pub fn configure_vector_kind_for_test(&self, kind: &str) -> Result<(), EngineError> {
2301 self.ensure_open()?;
2302 let mut connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2303 let connection = connection.as_mut().ok_or(EngineError::Closing)?;
2304 connection
2305 .execute(
2306 "INSERT OR REPLACE INTO _fathomdb_vector_kinds(kind, profile, created_at)
2307 VALUES(?1, ?2, 0)",
2308 params![kind, DEFAULT_VECTOR_PROFILE],
2309 )
2310 .map_err(|_| EngineError::Storage)?;
2311 Ok(())
2312 }
2313
2314 #[doc(hidden)]
2315 pub fn write_vector_for_test(
2316 &self,
2317 kind: &str,
2318 text: &str,
2319 ) -> Result<WriteReceipt, EngineError> {
2320 self.ensure_open()?;
2321 let embedder =
2322 self.runtime_embedder.as_ref().cloned().ok_or(EngineError::EmbedderNotConfigured)?;
2323
2324 let mut connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2325 let connection = connection.as_mut().ok_or(EngineError::Closing)?;
2326 if !kind_is_vector_indexed(connection, kind)? {
2327 return Err(EngineError::KindNotVectorIndexed);
2328 }
2329
2330 let expected = default_profile_dimension(connection)?;
2331 ensure_vector_partition(connection, expected).map_err(|_| EngineError::Storage)?;
2332 let vector = embedder.embed(text).map_err(map_runtime_embedder_error)?;
2333 let actual = u32::try_from(vector.len()).unwrap_or(u32::MAX);
2334 if actual != expected {
2335 return Err(EngineError::EmbedderDimensionMismatch { expected, actual });
2336 }
2337
2338 let cursor = self.next_cursor.load(Ordering::SeqCst).saturating_add(1);
2339 let blob = encode_vector_blob(&vector);
2345 let bin_blob = if identity_requires_mean_centering(&self.runtime_embedder_identity) {
2346 match read_pinned_mean_vec(connection, self.runtime_embedder_identity.dimension)? {
2347 Some(mean) => encode_vector_blob(&subtract_mean(&vector, &mean)),
2348 None => blob.clone(),
2349 }
2350 } else {
2351 blob.clone()
2352 };
2353 let source_type = resolve_source_type(kind)?;
2354 let now_unix =
2355 SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs() as i64;
2356
2357 let pin_event = {
2362 let runtime = &self.projection_runtime.shared;
2363 let mut accumulator =
2364 runtime.mean_accumulator.lock().map_err(|_| EngineError::Storage)?;
2365 if let Some(acc) = accumulator.as_mut() {
2366 acc.add(&vector);
2367 if acc.count() >= MEAN_VEC_PIN_THRESHOLD {
2368 let mean = acc.materialize();
2369 *accumulator = None;
2370 Some(mean)
2371 } else {
2372 None
2373 }
2374 } else {
2375 None
2376 }
2377 };
2378
2379 let tx = connection.transaction().map_err(|_| EngineError::Storage)?;
2380 tx.execute(
2381 "INSERT INTO _fathomdb_vector_rows(rowid, kind, write_cursor) VALUES(?1, ?2, ?3)",
2382 params![cursor, kind, cursor],
2383 )
2384 .map_err(|_| EngineError::Storage)?;
2385 tx.execute(
2386 "INSERT INTO vector_default(
2387 rowid, embedding, embedding_bin, source_type, kind, created_at
2388 ) VALUES(?1, ?2, vec_quantize_binary(?3), ?4, ?5, ?6)",
2389 params![cursor, blob, bin_blob, source_type, kind, now_unix],
2390 )
2391 .map_err(|_| EngineError::Storage)?;
2392
2393 let mut emitted_event: Option<EmbedderEvent> = None;
2394 if let Some(mean_vec) = pin_event {
2395 let mean_bytes = encode_vector_blob(&mean_vec);
2396 tx.execute(
2397 "UPDATE _fathomdb_embedder_profiles SET mean_vec = ?1 WHERE profile = 'default'",
2398 params![mean_bytes],
2399 )
2400 .map_err(|_| EngineError::Storage)?;
2401 let rows: Vec<(i64, Vec<u8>)> = {
2404 let mut statement = tx
2405 .prepare("SELECT rowid, embedding FROM vector_default ORDER BY rowid")
2406 .map_err(|_| EngineError::Storage)?;
2407 let mapped = statement
2408 .query_map([], |row| Ok((row.get::<_, i64>(0)?, row.get::<_, Vec<u8>>(1)?)))
2409 .map_err(|_| EngineError::Storage)?;
2410 let mut out = Vec::new();
2411 for r in mapped {
2412 out.push(r.map_err(|_| EngineError::Storage)?);
2413 }
2414 out
2415 };
2416 let (doc_count, _) = run_pin_and_requantize_pass(&tx, &rows, &mean_vec)?;
2417 emitted_event = Some(EmbedderEvent::MeanVecPinned {
2418 dim: u32::try_from(mean_vec.len()).unwrap_or(u32::MAX),
2419 doc_count,
2420 });
2421 }
2422
2423 tx.commit().map_err(|_| EngineError::Storage)?;
2424
2425 if let Some(ev) = emitted_event {
2426 if let Ok(mut events) = self.projection_runtime.shared.pending_events.lock() {
2427 events.push(ev);
2428 }
2429 }
2430
2431 self.next_cursor.store(cursor, Ordering::SeqCst);
2432 Ok(WriteReceipt { cursor })
2433 }
2434
2435 #[doc(hidden)]
2440 pub fn drain_mean_centering_events_for_test(&self) -> Result<Vec<EmbedderEvent>, EngineError> {
2441 self.ensure_open()?;
2442 let mut events = self
2443 .projection_runtime
2444 .shared
2445 .pending_events
2446 .lock()
2447 .map_err(|_| EngineError::Storage)?;
2448 let out = std::mem::take(&mut *events);
2449 Ok(out)
2450 }
2451
2452 pub fn drain_embedder_events(&self) -> Result<Vec<EmbedderEvent>, EngineError> {
2460 self.ensure_open()?;
2461 let mut events = self
2462 .projection_runtime
2463 .shared
2464 .pending_events
2465 .lock()
2466 .map_err(|_| EngineError::Storage)?;
2467 Ok(std::mem::take(&mut *events))
2468 }
2469
2470 pub fn recompute_mean(&self) -> Result<MeanRecomputeReport, EngineError> {
2484 self.ensure_open()?;
2485 let identity = self.runtime_embedder_identity.clone();
2486 if !identity_requires_mean_centering(&identity) {
2487 return Err(EngineError::EmbedderNotConfigured);
2488 }
2489 let report = {
2490 let _gate = self
2493 .projection_runtime
2494 .shared
2495 .commit_gate
2496 .lock()
2497 .unwrap_or_else(|p| p.into_inner());
2498 let mut connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2499 let connection = connection.as_mut().ok_or(EngineError::Closing)?;
2500 let tx = connection.transaction().map_err(|_| EngineError::Storage)?;
2501 #[cfg(debug_assertions)]
2502 let fail = self
2503 .projection_runtime
2504 .shared
2505 .force_recompute_failure
2506 .swap(false, Ordering::SeqCst);
2507 #[cfg(not(debug_assertions))]
2508 let fail = false;
2509 let report = recompute_mean_in_tx_inner(&tx, &identity, fail)?;
2510 tx.commit().map_err(|_| EngineError::Storage)?;
2511 report
2512 };
2513 if let Ok(mut events) = self.projection_runtime.shared.pending_events.lock() {
2515 events.push(EmbedderEvent::MeanVecRecomputed {
2516 dim: report.dim,
2517 doc_count: report.doc_count_requantized,
2518 trigger: MeanRecomputeTrigger::Manual,
2519 });
2520 }
2521 Ok(report)
2522 }
2523
2524 #[doc(hidden)]
2532 pub fn set_search_limit_for_test(&self, limit: usize) {
2533 self.projection_runtime.shared.search_limit_override.store(limit, Ordering::SeqCst);
2534 }
2535
2536 #[doc(hidden)]
2540 #[cfg(debug_assertions)]
2541 pub fn force_next_recompute_failure_for_test(&self) {
2542 self.projection_runtime.shared.force_recompute_failure.store(true, Ordering::SeqCst);
2543 }
2544
2545 #[doc(hidden)]
2546 pub fn vector_row_count_for_test(&self) -> Result<u64, EngineError> {
2547 self.ensure_open()?;
2548 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2549 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
2550 connection
2551 .query_row("SELECT COUNT(*) FROM vector_default", [], |row| row.get::<_, u64>(0))
2552 .map_err(|_| EngineError::Storage)
2553 }
2554
2555 #[doc(hidden)]
2556 pub fn read_vector_blob_for_test(&self, rowid: i64) -> Result<Vec<u8>, EngineError> {
2557 self.ensure_open()?;
2558 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2559 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
2560 connection
2561 .query_row("SELECT embedding FROM vector_default WHERE rowid = ?1", [rowid], |row| {
2562 row.get::<_, Vec<u8>>(0)
2563 })
2564 .map_err(|_| EngineError::Storage)
2565 }
2566
2567 #[doc(hidden)]
2568 pub fn default_embedder_profile_for_test(&self) -> Result<EmbedderIdentity, EngineError> {
2569 self.ensure_open()?;
2570 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2571 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
2572 load_default_profile(connection).map_err(|_| EngineError::Storage)
2573 }
2574
2575 pub fn check_integrity(
2579 &self,
2580 opts: CheckIntegrityOpts,
2581 ) -> Result<IntegrityReport, EngineError> {
2582 self.ensure_open()?;
2583 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2584 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
2585 Ok(IntegrityReport {
2586 physical: physical_section(connection, opts.full),
2587 logical: logical_section(connection),
2588 semantic: semantic_section(connection),
2589 })
2590 }
2591
2592 pub fn safe_export(
2597 &self,
2598 out: &Path,
2599 manifest: &Path,
2600 ) -> Result<SafeExportArtifact, EngineError> {
2601 self.ensure_open()?;
2602 {
2603 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2604 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
2605 let target = out.to_string_lossy().to_string();
2606 connection
2607 .execute("VACUUM INTO ?1", params![target])
2608 .map_err(|_| EngineError::Storage)?;
2609 }
2610 let bytes = std::fs::read(out).map_err(|_| EngineError::Storage)?;
2611 let digest = sha2::Sha256::digest(&bytes);
2612 let sha256_hex = hex_encode(digest.as_slice());
2613 let export_abs = out.canonicalize().unwrap_or_else(|_| out.to_path_buf());
2614 let manifest_json = serde_json::json!({
2615 "export_path": export_abs.to_string_lossy(),
2616 "sha256": sha256_hex,
2617 "byte_count": bytes.len() as u64,
2618 });
2619 let manifest_bytes =
2620 serde_json::to_vec_pretty(&manifest_json).map_err(|_| EngineError::Storage)?;
2621 std::fs::write(manifest, &manifest_bytes).map_err(|_| EngineError::Storage)?;
2622 Ok(SafeExportArtifact {
2623 export_path: out.to_path_buf(),
2624 manifest_path: manifest.to_path_buf(),
2625 manifest_sha256: sha256_hex,
2626 })
2627 }
2628
2629 pub fn rebuild_projections(&self) -> Result<RebuildReport, EngineError> {
2636 self.ensure_open()?;
2637 self.run_rebuild(true, RebuildKind::Projections)
2638 }
2639
2640 pub fn rebuild_vec0(&self) -> Result<RebuildReport, EngineError> {
2644 self.ensure_open()?;
2645 self.run_rebuild(false, RebuildKind::Vec0)
2646 }
2647
2648 pub fn trace_source_ref(&self, source_id: &str) -> Result<TraceReport, EngineError> {
2653 self.ensure_open()?;
2654 if source_id.is_empty() {
2655 return Err(EngineError::WriteValidation);
2656 }
2657 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2658 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
2659
2660 let mut events: Vec<TraceEvent> = Vec::new();
2661 let mut nodes = connection
2662 .prepare(
2663 "SELECT write_cursor, kind FROM canonical_nodes WHERE source_id = ?1
2664 ORDER BY write_cursor",
2665 )
2666 .map_err(|_| EngineError::Storage)?;
2667 let node_rows = nodes
2668 .query_map([source_id], |row| {
2669 Ok(TraceEvent {
2670 write_cursor: row.get::<_, i64>(0)? as u64,
2671 kind: row.get::<_, String>(1)?,
2672 table: "canonical_nodes",
2673 })
2674 })
2675 .map_err(|_| EngineError::Storage)?;
2676 for row in node_rows {
2677 events.push(row.map_err(|_| EngineError::Storage)?);
2678 }
2679
2680 let mut edges = connection
2681 .prepare(
2682 "SELECT write_cursor, kind FROM canonical_edges WHERE source_id = ?1
2683 ORDER BY write_cursor",
2684 )
2685 .map_err(|_| EngineError::Storage)?;
2686 let edge_rows = edges
2687 .query_map([source_id], |row| {
2688 Ok(TraceEvent {
2689 write_cursor: row.get::<_, i64>(0)? as u64,
2690 kind: row.get::<_, String>(1)?,
2691 table: "canonical_edges",
2692 })
2693 })
2694 .map_err(|_| EngineError::Storage)?;
2695 for row in edge_rows {
2696 events.push(row.map_err(|_| EngineError::Storage)?);
2697 }
2698
2699 events.sort_by_key(|e| e.write_cursor);
2700 Ok(TraceReport { source_ref: source_id.to_string(), events })
2701 }
2702
2703 pub fn excise_source(&self, source_id: &str) -> Result<ExciseReport, EngineError> {
2713 self.ensure_open()?;
2714 if source_id.is_empty() {
2715 return Err(EngineError::WriteValidation);
2716 }
2717
2718 self.projection_runtime.set_frozen(true);
2725 let drain_result = self.drain(REBUILD_DRAIN_TIMEOUT_MS);
2726 let outcome = drain_result.and_then(|()| self.excise_source_inner(source_id));
2727 self.projection_runtime.set_frozen(false);
2728 outcome
2729 }
2730
2731 pub fn verify_embedder(
2735 &self,
2736 supplied_identity: &str,
2737 supplied_dimension: u32,
2738 ) -> Result<VerifyEmbedderReport, EngineError> {
2739 self.ensure_open()?;
2740 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2741 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
2742 let stored = load_default_profile(connection).map_err(|_| EngineError::Storage)?;
2743 let stored_identity = format!("{}:{}", stored.name, stored.revision);
2744 let identity_match = stored_identity == supplied_identity;
2745 let dimension_match = stored.dimension == supplied_dimension;
2746 let status = match (identity_match, dimension_match) {
2747 (true, true) => VerifyEmbedderStatus::Match,
2748 (false, true) => VerifyEmbedderStatus::IdentityMismatch,
2749 (true, false) => VerifyEmbedderStatus::DimensionMismatch,
2750 (false, false) => VerifyEmbedderStatus::BothMismatch,
2751 };
2752 Ok(VerifyEmbedderReport {
2753 stored_identity,
2754 stored_dimension: stored.dimension,
2755 supplied_identity: supplied_identity.to_string(),
2756 supplied_dimension,
2757 status,
2758 })
2759 }
2760
2761 pub fn dump_schema(&self) -> Result<DumpSchemaReport, EngineError> {
2766 self.ensure_open()?;
2767 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2768 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
2769 let user_version: u32 = connection
2770 .query_row("PRAGMA user_version", [], |row| row.get(0))
2771 .map_err(|_| EngineError::Storage)?;
2772 let tables = read_schema_objects(connection, "table")?;
2773 let indexes = read_schema_objects(connection, "index")?;
2774 Ok(DumpSchemaReport { user_version, tables: order_canonical_first(tables), indexes })
2775 }
2776
2777 pub fn dump_row_counts(&self) -> Result<DumpRowCountsReport, EngineError> {
2780 self.ensure_open()?;
2781 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2782 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
2783 let mut counts = Vec::with_capacity(CANONICAL_TABLES.len());
2784 for name in CANONICAL_TABLES {
2785 let rows: u64 = connection
2786 .query_row(&format!("SELECT COUNT(*) FROM {name}"), [], |row| row.get(0))
2787 .map_err(|_| EngineError::Storage)?;
2788 counts.push(TableRowCount { name: (*name).to_string(), rows });
2789 }
2790 Ok(DumpRowCountsReport { counts })
2791 }
2792
2793 pub fn dump_profile(&self) -> Result<DumpProfileReport, EngineError> {
2797 self.ensure_open()?;
2798 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2799 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
2800 let stored = load_default_profile(connection).map_err(|_| EngineError::Storage)?;
2801 let mut stmt = connection
2802 .prepare("SELECT kind FROM _fathomdb_vector_kinds ORDER BY kind")
2803 .map_err(|_| EngineError::Storage)?;
2804 let rows =
2805 stmt.query_map([], |row| row.get::<_, String>(0)).map_err(|_| EngineError::Storage)?;
2806 let mut vectorized_kinds = Vec::new();
2807 for row in rows {
2808 vectorized_kinds.push(row.map_err(|_| EngineError::Storage)?);
2809 }
2810 Ok(DumpProfileReport {
2811 embedder_identity: format!("{}:{}", stored.name, stored.revision),
2812 embedder_dimension: stored.dimension,
2813 vectorized_kinds,
2814 })
2815 }
2816
2817 pub fn truncate_wal(&self) -> Result<TruncateWalReport, EngineError> {
2823 self.ensure_open()?;
2824 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2825 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
2826 let (busy, log_frames, checkpointed_frames): (i64, i64, i64) = connection
2827 .query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |row| {
2828 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
2829 })
2830 .map_err(|_| EngineError::Storage)?;
2831 let status = if busy == 0 { TruncateWalStatus::Done } else { TruncateWalStatus::Busy };
2832 Ok(TruncateWalReport {
2833 status,
2834 busy: busy.max(0) as u32,
2835 log_frames: log_frames.max(0) as u32,
2836 checkpointed_frames: checkpointed_frames.max(0) as u32,
2837 })
2838 }
2839
2840 fn excise_source_inner(&self, source_id: &str) -> Result<ExciseReport, EngineError> {
2841 let mut connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2842 let connection = connection.as_mut().ok_or(EngineError::Closing)?;
2843 let tx = connection.transaction().map_err(|_| EngineError::Storage)?;
2844
2845 let node_cursors: Vec<i64> = {
2848 let mut stmt = tx
2849 .prepare("SELECT write_cursor FROM canonical_nodes WHERE source_id = ?1")
2850 .map_err(|_| EngineError::Storage)?;
2851 let rows = stmt
2852 .query_map([source_id], |row| row.get::<_, i64>(0))
2853 .map_err(|_| EngineError::Storage)?;
2854 rows.collect::<rusqlite::Result<Vec<_>>>().map_err(|_| EngineError::Storage)?
2855 };
2856 let edge_cursors: Vec<i64> = {
2857 let mut stmt = tx
2858 .prepare("SELECT write_cursor FROM canonical_edges WHERE source_id = ?1")
2859 .map_err(|_| EngineError::Storage)?;
2860 let rows = stmt
2861 .query_map([source_id], |row| row.get::<_, i64>(0))
2862 .map_err(|_| EngineError::Storage)?;
2863 rows.collect::<rusqlite::Result<Vec<_>>>().map_err(|_| EngineError::Storage)?
2864 };
2865
2866 let mut shadow_invalidated: u64 = 0;
2867 for cursor in node_cursors.iter().chain(edge_cursors.iter()) {
2868 shadow_invalidated = shadow_invalidated.saturating_add(
2869 tx.execute("DELETE FROM search_index WHERE write_cursor = ?1", [cursor])
2870 .map_err(|_| EngineError::Storage)? as u64,
2871 );
2872 shadow_invalidated = shadow_invalidated.saturating_add(
2875 tx.execute("DELETE FROM vector_default WHERE rowid = ?1", [cursor])
2876 .map_err(|_| EngineError::Storage)? as u64,
2877 );
2878 shadow_invalidated = shadow_invalidated.saturating_add(
2879 tx.execute("DELETE FROM _fathomdb_vector_rows WHERE write_cursor = ?1", [cursor])
2880 .map_err(|_| EngineError::Storage)? as u64,
2881 );
2882 shadow_invalidated = shadow_invalidated.saturating_add(
2883 tx.execute(
2884 "DELETE FROM _fathomdb_projection_terminal WHERE write_cursor = ?1",
2885 [cursor],
2886 )
2887 .map_err(|_| EngineError::Storage)? as u64,
2888 );
2889 }
2890
2891 let nodes_excised = tx
2892 .execute("DELETE FROM canonical_nodes WHERE source_id = ?1", [source_id])
2893 .map_err(|_| EngineError::Storage)? as u64;
2894 let edges_excised = tx
2895 .execute("DELETE FROM canonical_edges WHERE source_id = ?1", [source_id])
2896 .map_err(|_| EngineError::Storage)? as u64;
2897
2898 let excised_at = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs();
2905 let payload = serde_json::json!({
2906 "source_id": source_id,
2907 "excised_at": excised_at,
2908 "nodes_excised": nodes_excised,
2909 "edges_excised": edges_excised,
2910 "projections_invalidated": shadow_invalidated,
2911 })
2912 .to_string();
2913 let audit_cursor = self.next_cursor.load(Ordering::SeqCst).saturating_add(1);
2914 tx.execute(
2915 "INSERT INTO operational_mutations(
2916 collection_name, record_key, op_kind, payload_json, schema_id, write_cursor
2917 ) VALUES('excise_source_audit', ?1, 'append', ?2, NULL, ?3)",
2918 params![source_id, payload, audit_cursor],
2919 )
2920 .map_err(|_| EngineError::Storage)?;
2921
2922 tx.commit().map_err(|_| EngineError::Storage)?;
2923 self.next_cursor.store(audit_cursor, Ordering::SeqCst);
2924 Ok(ExciseReport {
2925 source_ref: source_id.to_string(),
2926 nodes_excised,
2927 edges_excised,
2928 projections_invalidated: shadow_invalidated,
2929 })
2930 }
2931
2932 fn run_rebuild(
2933 &self,
2934 include_fts: bool,
2935 kind: RebuildKind,
2936 ) -> Result<RebuildReport, EngineError> {
2937 self.projection_runtime.set_frozen(true);
2938 let drain_result = self.drain(REBUILD_DRAIN_TIMEOUT_MS);
2945 let result = drain_result.and_then(|()| self.rebuild_shadow_state(include_fts, kind));
2946 self.projection_runtime.set_frozen(false);
2947 result
2948 }
2949
2950 fn rebuild_shadow_state(
2951 &self,
2952 include_fts: bool,
2953 kind: RebuildKind,
2954 ) -> Result<RebuildReport, EngineError> {
2955 let mut connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2956 let connection = connection.as_mut().ok_or(EngineError::Closing)?;
2957 let tx = connection.transaction().map_err(|_| EngineError::Storage)?;
2958 let mut rows_invalidated: u64 = 0;
2959 if include_fts {
2960 let n = tx.execute("DELETE FROM search_index", []).map_err(|_| EngineError::Storage)?;
2961 rows_invalidated = rows_invalidated.saturating_add(n as u64);
2962 }
2963 let n = tx.execute("DELETE FROM vector_default", []).map_err(|_| EngineError::Storage)?;
2964 rows_invalidated = rows_invalidated.saturating_add(n as u64);
2965 let n = tx
2966 .execute("DELETE FROM _fathomdb_vector_rows", [])
2967 .map_err(|_| EngineError::Storage)?;
2968 rows_invalidated = rows_invalidated.saturating_add(n as u64);
2969 let n = tx
2970 .execute("DELETE FROM _fathomdb_projection_terminal", [])
2971 .map_err(|_| EngineError::Storage)?;
2972 rows_invalidated = rows_invalidated.saturating_add(n as u64);
2973 store_projection_cursor(&tx, 0).map_err(|_| EngineError::Storage)?;
2974 let mut rows_rebuilt: u64 = 0;
2975 if include_fts {
2976 for row in canonical_node_rows(&tx).map_err(|_| EngineError::Storage)? {
2977 tx.execute(
2978 "INSERT INTO search_index(body, kind, write_cursor) VALUES(?1, ?2, ?3)",
2979 params![row.body, row.kind, row.cursor],
2980 )
2981 .map_err(|_| EngineError::Storage)?;
2982 rows_rebuilt = rows_rebuilt.saturating_add(1);
2983 }
2984 }
2985 let projection_cursor_after =
2986 load_projection_cursor(&tx).map_err(|_| EngineError::Storage)?;
2987 tx.commit().map_err(|_| EngineError::Storage)?;
2988 Ok(RebuildReport { kind, rows_invalidated, rows_rebuilt, projection_cursor_after })
2989 }
2990
2991 fn ensure_open(&self) -> Result<(), EngineError> {
2992 if self.closed.load(Ordering::SeqCst) {
2993 return Err(EngineError::Closing);
2994 }
2995
2996 Ok(())
2997 }
2998}
2999
3000fn batch_is_admin(batch: &[PreparedWrite]) -> bool {
3001 !batch.is_empty() && batch.iter().all(|w| matches!(w, PreparedWrite::AdminSchema { .. }))
3002}
3003
3004pub const TOP_K_BIT_CANDIDATES: usize = 192;
3013
3014pub const MEAN_VEC_PIN_THRESHOLD: u64 = 256;
3020
3021pub const SEARCH_RERANK_LIMIT: usize = 10;
3026
3027#[derive(Clone, Debug)]
3032struct MeanAccumulator {
3033 sum: Vec<f64>,
3034 count: u64,
3035}
3036
3037impl MeanAccumulator {
3038 fn new(dim: usize) -> Self {
3039 Self { sum: vec![0.0; dim], count: 0 }
3040 }
3041
3042 fn add(&mut self, v: &[f32]) {
3043 debug_assert_eq!(v.len(), self.sum.len(), "accumulator dim mismatch");
3044 for (slot, value) in self.sum.iter_mut().zip(v.iter()) {
3045 *slot += f64::from(*value);
3046 }
3047 self.count = self.count.saturating_add(1);
3048 }
3049
3050 fn materialize(&self) -> Vec<f32> {
3051 if self.count == 0 {
3052 return vec![0.0; self.sum.len()];
3053 }
3054 let denom = self.count as f64;
3055 self.sum.iter().map(|s| (s / denom) as f32).collect()
3056 }
3057
3058 fn count(&self) -> u64 {
3059 self.count
3060 }
3061}
3062
3063fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
3067 if a.len() != b.len() {
3068 return 1.0;
3069 }
3070 let mut dot = 0.0f64;
3071 let mut na = 0.0f64;
3072 let mut nb = 0.0f64;
3073 for (x, y) in a.iter().zip(b.iter()) {
3074 dot += f64::from(*x) * f64::from(*y);
3075 na += f64::from(*x) * f64::from(*x);
3076 nb += f64::from(*y) * f64::from(*y);
3077 }
3078 if na == 0.0 || nb == 0.0 {
3079 return 1.0;
3080 }
3081 (dot / (na.sqrt() * nb.sqrt())) as f32
3082}
3083
3084fn run_pin_and_requantize_pass(
3092 tx: &rusqlite::Transaction<'_>,
3093 rows: &[(i64, Vec<u8>)],
3094 mean: &[f32],
3095) -> Result<(u64, Vec<EmbedderEvent>), EngineError> {
3096 let mut updated: u64 = 0;
3097 let dim = mean.len();
3098 for (rowid, blob) in rows {
3107 if blob.len() != dim * 4 {
3108 return Err(EngineError::Storage);
3109 }
3110 let un_centered = decode_vector_blob(blob);
3111 let centered = subtract_mean(&un_centered, mean);
3112 let centered_blob = encode_vector_blob(¢ered);
3113
3114 let (source_type, kind, created_at): (String, String, i64) = tx
3115 .query_row(
3116 "SELECT source_type, kind, created_at FROM vector_default WHERE rowid = ?1",
3117 params![rowid],
3118 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
3119 )
3120 .map_err(|_| EngineError::Storage)?;
3121
3122 tx.execute("DELETE FROM vector_default WHERE rowid = ?1", params![rowid])
3123 .map_err(|_| EngineError::Storage)?;
3124
3125 tx.execute(
3126 "INSERT INTO vector_default(
3127 rowid, embedding, embedding_bin, source_type, kind, created_at
3128 ) VALUES(?1, ?2, vec_quantize_binary(?3), ?4, ?5, ?6)",
3129 params![rowid, blob, centered_blob, source_type, kind, created_at],
3130 )
3131 .map_err(|_| EngineError::Storage)?;
3132
3133 updated = updated.saturating_add(1);
3134 }
3135 let events = vec![EmbedderEvent::MeanVecPinned {
3136 dim: u32::try_from(dim).unwrap_or(u32::MAX),
3137 doc_count: updated,
3138 }];
3139 Ok((updated, events))
3140}
3141
3142fn run_requantize_pass(rows: &[(i64, Vec<u8>)], mean: &[f32]) -> (u64, Vec<EmbedderEvent>) {
3146 let mut updated: u64 = 0;
3147 let dim = mean.len();
3148 for (_rowid, blob) in rows {
3149 if blob.len() != dim * 4 {
3150 continue;
3151 }
3152 updated = updated.saturating_add(1);
3153 }
3154 let events = vec![EmbedderEvent::MeanVecPinned {
3155 dim: u32::try_from(dim).unwrap_or(u32::MAX),
3156 doc_count: updated,
3157 }];
3158 (updated, events)
3159}
3160
3161#[doc(hidden)]
3165pub mod mean_centering_internals_for_test {
3166 use super::{EmbedderEvent, MeanAccumulator};
3167
3168 pub struct AccumulatorHandle(MeanAccumulator);
3169
3170 #[must_use]
3171 pub fn new_mean_accumulator(dim: usize) -> AccumulatorHandle {
3172 AccumulatorHandle(MeanAccumulator::new(dim))
3173 }
3174
3175 pub fn accumulator_add(handle: &mut AccumulatorHandle, v: &[f32]) {
3176 handle.0.add(v);
3177 }
3178
3179 #[must_use]
3180 pub fn accumulator_materialize(handle: &AccumulatorHandle) -> Vec<f32> {
3181 handle.0.materialize()
3182 }
3183
3184 #[must_use]
3185 pub fn accumulator_count(handle: &AccumulatorHandle) -> u64 {
3186 handle.0.count()
3187 }
3188
3189 #[must_use]
3190 pub fn run_requantize_pass(rows: &[(i64, Vec<u8>)], mean: &[f32]) -> (u64, Vec<EmbedderEvent>) {
3191 super::run_requantize_pass(rows, mean)
3192 }
3193}
3194
3195fn read_search_in_tx(
3197 reader: &mut Connection,
3198 compiled: &fathomdb_query::CompiledQuery,
3199 query_vector: Option<&str>,
3200 query_vector_bin: Option<&str>,
3201 final_limit: usize,
3202) -> rusqlite::Result<(u64, Option<SoftFallback>, Vec<String>)> {
3203 let tx = reader.transaction_with_behavior(rusqlite::TransactionBehavior::Deferred)?;
3204 let cursor = load_projection_cursor(&tx)?;
3205 let vector_results = if let Some(query_vector) = query_vector {
3206 let mut rowids = Vec::new();
3207 let bin_vector = query_vector_bin.unwrap_or(query_vector);
3208 {
3209 let sql = format!(
3224 "WITH candidates AS (
3225 SELECT rowid
3226 FROM vector_default
3227 WHERE embedding_bin MATCH vec_quantize_binary(vec_f32(?1))
3228 ORDER BY distance
3229 LIMIT {top_k}
3230 )
3231 SELECT c.rowid
3232 FROM candidates c
3233 JOIN vector_default v ON v.rowid = c.rowid
3234 ORDER BY vec_distance_l2(v.embedding, vec_f32(?2))
3235 LIMIT {final_limit}",
3236 top_k = TOP_K_BIT_CANDIDATES,
3237 );
3238 let mut statement = tx.prepare(&sql)?;
3239 let rows =
3240 statement.query_map([bin_vector, query_vector], |row| row.get::<_, i64>(0))?;
3241 for row in rows.flatten() {
3242 rowids.push(row);
3243 }
3244 }
3245 let mut results = Vec::new();
3246 let mut statement =
3247 tx.prepare("SELECT body FROM canonical_nodes WHERE write_cursor = ?1 LIMIT 1")?;
3248 for rowid in rowids {
3249 if let Ok(body) = statement.query_row([rowid], |row| row.get::<_, String>(0)) {
3250 results.push(body);
3251 }
3252 }
3253 results
3254 } else {
3255 Vec::new()
3256 };
3257 let vector_rows_visible = !vector_results.is_empty();
3258 let soft_fallback = if query_vector.is_some() && !vector_rows_visible {
3259 tx.query_row(
3260 "SELECT 1
3261 FROM search_index
3262 JOIN _fathomdb_vector_kinds ON _fathomdb_vector_kinds.kind = search_index.kind
3263 LEFT JOIN _fathomdb_projection_terminal
3264 ON _fathomdb_projection_terminal.write_cursor = search_index.write_cursor
3265 WHERE search_index MATCH ?1
3266 AND _fathomdb_projection_terminal.write_cursor IS NULL
3267 LIMIT 1",
3268 [compiled.match_expression.as_str()],
3269 |_row| Ok(SoftFallback { branch: SoftFallbackBranch::Vector }),
3270 )
3271 .ok()
3272 } else {
3273 None
3274 };
3275 let mut seen = BTreeSet::new();
3276 let mut results = Vec::new();
3277 for row in vector_results {
3278 if seen.insert(row.clone()) {
3279 results.push(row);
3280 }
3281 }
3282 {
3283 let perf_limit: Option<usize> = if std::env::var_os("FATHOMDB_PERF_EXPERIMENTS").is_some() {
3290 std::env::var("FATHOMDB_PERF_SEARCH_LIMIT").ok().and_then(|s| s.parse().ok())
3291 } else {
3292 None
3293 };
3294 let sql = match perf_limit {
3295 Some(k) => format!(
3296 "SELECT body FROM search_index WHERE search_index MATCH ?1 ORDER BY write_cursor LIMIT {k}"
3297 ),
3298 None => "SELECT body FROM search_index WHERE search_index MATCH ?1 ORDER BY write_cursor"
3299 .to_string(),
3300 };
3301 let mut statement = tx.prepare(&sql)?;
3302 let rows = statement
3303 .query_map([compiled.match_expression.as_str()], |row| row.get::<_, String>(0))?;
3304 for row in rows.flatten() {
3305 if seen.insert(row.clone()) {
3306 results.push(row);
3307 }
3308 }
3309 }
3310 tx.commit()?;
3311 Ok((cursor, soft_fallback, results))
3312}
3313
3314fn projection_dispatcher_loop(shared: Arc<ProjectionRuntimeShared>) {
3315 let connection = match open_runtime_connection(&shared.path) {
3316 Ok(connection) => connection,
3317 Err(_) => return,
3318 };
3319 loop {
3320 let in_flight = {
3321 let mut state = match shared.state.lock() {
3322 Ok(state) => state,
3323 Err(_) => return,
3324 };
3325 while !state.stopping
3326 && (!state.pending_scan
3327 || state.frozen
3328 || state.active_jobs + state.queued_jobs >= PROJECTION_INFLIGHT_LIMIT)
3329 {
3330 state = match shared.state_cvar.wait(state) {
3331 Ok(state) => state,
3332 Err(_) => return,
3333 };
3334 }
3335 if state.stopping {
3336 return;
3337 }
3338 state.pending_scan = false;
3339 state.in_flight.clone()
3340 };
3341
3342 let budget = {
3348 let state = match shared.state.lock() {
3349 Ok(state) => state,
3350 Err(_) => return,
3351 };
3352 PROJECTION_INFLIGHT_LIMIT.saturating_sub(state.active_jobs + state.queued_jobs)
3353 };
3354 let fetch_cap = budget.clamp(1, PROJECTION_SCAN_FETCH);
3355 match next_pending_projection_jobs(&connection, &in_flight, fetch_cap) {
3356 Ok(jobs) if !jobs.is_empty() => {
3357 if let Ok(mut state) = shared.state.lock() {
3358 state.queued_jobs = state.queued_jobs.saturating_add(jobs.len());
3359 for job in &jobs {
3360 state.in_flight.insert(job.cursor);
3361 }
3362 state.pending_scan = true;
3363 shared.state_cvar.notify_all();
3364 }
3365 if let Ok(mut queue) = shared.queue.lock() {
3366 for job in jobs {
3367 queue.push_back(job);
3368 }
3369 shared.queue_cvar.notify_all();
3370 }
3371 }
3372 Ok(_) => {}
3373 Err(_) => {
3374 if let Ok(mut state) = shared.state.lock() {
3375 state.pending_scan = false;
3376 shared.state_cvar.notify_all();
3377 }
3378 }
3379 }
3380 }
3381}
3382
3383fn projection_worker_loop(shared: Arc<ProjectionRuntimeShared>) {
3384 let mut connection = match open_runtime_connection(&shared.path) {
3385 Ok(connection) => connection,
3386 Err(_) => return,
3387 };
3388 if ensure_vector_partition(&mut connection, shared.embedder_identity.dimension).is_err() {
3389 return;
3390 }
3391 loop {
3392 let jobs = {
3393 let mut queue = match shared.queue.lock() {
3394 Ok(queue) => queue,
3395 Err(_) => return,
3396 };
3397 loop {
3398 let stopping = shared.state.lock().map(|state| state.stopping).unwrap_or(true);
3399 if stopping && queue.is_empty() {
3400 return;
3401 }
3402 if let Some(job) = queue.pop_front() {
3403 let mut jobs = vec![job];
3404 while jobs.len() < PROJECTION_COMMIT_BATCH {
3405 let Some(job) = queue.pop_front() else {
3406 break;
3407 };
3408 jobs.push(job);
3409 }
3410 if let Ok(mut state) = shared.state.lock() {
3411 state.queued_jobs = state.queued_jobs.saturating_sub(jobs.len());
3412 state.active_jobs = state.active_jobs.saturating_add(jobs.len());
3413 shared.state_cvar.notify_all();
3414 }
3415 break jobs;
3416 }
3417 queue = match shared.queue_cvar.wait(queue) {
3418 Ok(queue) => queue,
3419 Err(_) => return,
3420 };
3421 }
3422 };
3423
3424 let panicked = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3431 run_projection_jobs(&shared, &mut connection, &jobs);
3432 }))
3433 .is_err();
3434 if panicked {
3435 commit_projection_panic_failures(&shared, &mut connection, &jobs);
3436 }
3437
3438 if let Ok(mut state) = shared.state.lock() {
3439 state.active_jobs = state.active_jobs.saturating_sub(jobs.len());
3440 for job in &jobs {
3441 state.in_flight.remove(&job.cursor);
3442 }
3443 if !state.stopping {
3444 state.pending_scan = true;
3445 }
3446 shared.state_cvar.notify_all();
3447 }
3448 }
3449}
3450
3451enum ProjectionOutcome {
3452 Success {
3458 cursor: u64,
3459 kind: String,
3460 blob: Vec<u8>,
3461 bin_blob: Vec<u8>,
3462 },
3463 Failure {
3464 cursor: u64,
3465 failure_code: &'static str,
3466 },
3467}
3468
3469fn run_projection_jobs(
3470 shared: &ProjectionRuntimeShared,
3471 connection: &mut Connection,
3472 jobs: &[ProjectionJob],
3473) {
3474 let mut outcomes = Vec::with_capacity(jobs.len());
3475 for job in jobs {
3476 outcomes.push(run_projection_job(shared, job));
3477 }
3478 let _ = commit_projection_outcomes(connection, &outcomes, shared);
3479}
3480
3481fn commit_projection_panic_failures(
3485 shared: &ProjectionRuntimeShared,
3486 connection: &mut Connection,
3487 jobs: &[ProjectionJob],
3488) {
3489 let outcomes: Vec<ProjectionOutcome> = jobs
3490 .iter()
3491 .map(|job| ProjectionOutcome::Failure {
3492 cursor: job.cursor,
3493 failure_code: "ProjectionPanic",
3494 })
3495 .collect();
3496 let _ = commit_projection_outcomes(connection, &outcomes, shared);
3497}
3498
3499fn embed_with_watchdog(
3523 embedder: &Arc<dyn Embedder>,
3524 body: &str,
3525 timeout: Duration,
3526 live: &Arc<AtomicU64>,
3527) -> Result<Vec<f32>, RuntimeEmbedderError> {
3528 let (tx, rx) = mpsc::channel();
3529 let embedder = Arc::clone(embedder);
3530 let body = body.to_string();
3531 live.fetch_add(1, Ordering::Relaxed);
3534 let live_thread = Arc::clone(live);
3535 thread::spawn(move || {
3536 let outcome =
3537 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| embedder.embed(&body)));
3538 let _ = tx.send(outcome);
3542 live_thread.fetch_sub(1, Ordering::Relaxed);
3543 });
3544 match rx.recv_timeout(timeout) {
3545 Ok(Ok(result)) => result,
3546 Ok(Err(panic_payload)) => std::panic::resume_unwind(panic_payload),
3547 Err(mpsc::RecvTimeoutError::Timeout) => Err(RuntimeEmbedderError::Timeout),
3548 Err(mpsc::RecvTimeoutError::Disconnected) => Err(RuntimeEmbedderError::Failed {
3552 message: "embed watchdog thread dropped its result channel".to_string(),
3553 }),
3554 }
3555}
3556
3557fn run_projection_job(shared: &ProjectionRuntimeShared, job: &ProjectionJob) -> ProjectionOutcome {
3558 if shared.embed_circuit_open.load(Ordering::Relaxed) {
3565 return ProjectionOutcome::Failure { cursor: job.cursor, failure_code: "EmbedderError" };
3566 }
3567 let delays = shared.retry_delays_ms.lock().map(|delays| delays.clone()).unwrap_or_default();
3568 let mut last_code = "EmbedderError";
3569 for (attempt, delay_ms) in std::iter::once(0_u64).chain(delays.iter().copied()).enumerate() {
3570 if attempt > 0 {
3571 if shared.state.lock().map(|state| state.stopping).unwrap_or(true) {
3572 return ProjectionOutcome::Failure { cursor: job.cursor, failure_code: last_code };
3573 }
3574 thread::sleep(Duration::from_millis(delay_ms));
3575 }
3576 if shared.embed_circuit_open.load(Ordering::Relaxed) {
3582 return ProjectionOutcome::Failure { cursor: job.cursor, failure_code: last_code };
3583 }
3584 let embed_timeout = Duration::from_millis(shared.embed_timeout_ms.load(Ordering::Relaxed));
3588 let vector = match shared.embedder.as_ref() {
3589 Some(embedder) => {
3590 let _embed_permit =
3600 shared.embed_serialize.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
3601 let threshold = shared.embed_circuit_threshold.load(Ordering::Relaxed);
3610 if shared.embed_circuit_open.load(Ordering::Relaxed)
3611 || (threshold != 0
3612 && shared.live_embed_threads.load(Ordering::Relaxed) >= threshold)
3613 {
3614 shared.embed_circuit_open.store(true, Ordering::Relaxed);
3615 return ProjectionOutcome::Failure {
3616 cursor: job.cursor,
3617 failure_code: last_code,
3618 };
3619 }
3620 match embed_with_watchdog(
3621 embedder,
3622 &job.body,
3623 embed_timeout,
3624 &shared.live_embed_threads,
3625 ) {
3626 Ok(vector) => vector,
3627 Err(RuntimeEmbedderError::Timeout) => {
3628 last_code = "EmbedderError";
3632 continue;
3633 }
3634 Err(RuntimeEmbedderError::Failed { .. }) => {
3635 last_code = "EmbedderError";
3636 continue;
3637 }
3638 }
3639 }
3640 None => {
3641 last_code = "EmbedderNotConfiguredError";
3642 continue;
3643 }
3644 };
3645
3646 if u32::try_from(vector.len()).unwrap_or(u32::MAX) != shared.embedder_identity.dimension {
3647 last_code = "EmbedderDimensionMismatchError";
3648 continue;
3649 }
3650
3651 let blob = encode_vector_blob(&vector);
3652 let bin_blob = blob.clone();
3661 return ProjectionOutcome::Success {
3662 cursor: job.cursor,
3663 kind: job.kind.clone(),
3664 blob,
3665 bin_blob,
3666 };
3667 }
3668
3669 ProjectionOutcome::Failure { cursor: job.cursor, failure_code: last_code }
3670}
3671
3672fn next_pending_projection_jobs(
3673 connection: &Connection,
3674 in_flight: &BTreeSet<u64>,
3675 max_jobs: usize,
3676) -> rusqlite::Result<Vec<ProjectionJob>> {
3677 if max_jobs == 0 {
3678 return Ok(Vec::new());
3679 }
3680 let cursor = load_projection_cursor(connection)?;
3681 let sql_limit = max_jobs.saturating_add(in_flight.len()).min(256);
3684 let sql = format!(
3685 "SELECT canonical_nodes.write_cursor, canonical_nodes.kind, canonical_nodes.body
3686 FROM canonical_nodes
3687 JOIN _fathomdb_vector_kinds ON _fathomdb_vector_kinds.kind = canonical_nodes.kind
3688 LEFT JOIN _fathomdb_projection_terminal
3689 ON _fathomdb_projection_terminal.write_cursor = canonical_nodes.write_cursor
3690 WHERE canonical_nodes.write_cursor > ?1
3691 AND _fathomdb_projection_terminal.write_cursor IS NULL
3692 ORDER BY canonical_nodes.write_cursor
3693 LIMIT {sql_limit}"
3694 );
3695 let mut statement = connection.prepare_cached(&sql)?;
3696 let rows = statement.query_map([cursor], |row| {
3697 Ok(ProjectionJob { cursor: row.get(0)?, kind: row.get(1)?, body: row.get(2)? })
3698 })?;
3699 let mut jobs = Vec::with_capacity(max_jobs);
3700 for row in rows {
3701 let job = row?;
3702 if in_flight.contains(&job.cursor) {
3703 continue;
3704 }
3705 jobs.push(job);
3706 if jobs.len() >= max_jobs {
3707 break;
3708 }
3709 }
3710 Ok(jobs)
3711}
3712
3713fn database_has_pending_projection_work(path: &Path) -> rusqlite::Result<bool> {
3714 let connection = open_runtime_connection(path)?;
3715 let cursor = load_projection_cursor(&connection)?;
3716 connection
3717 .query_row(
3718 "SELECT 1
3719 FROM canonical_nodes
3720 JOIN _fathomdb_vector_kinds ON _fathomdb_vector_kinds.kind = canonical_nodes.kind
3721 LEFT JOIN _fathomdb_projection_terminal
3722 ON _fathomdb_projection_terminal.write_cursor = canonical_nodes.write_cursor
3723 WHERE canonical_nodes.write_cursor > ?1
3724 AND _fathomdb_projection_terminal.write_cursor IS NULL
3725 LIMIT 1",
3726 [cursor],
3727 |_row| Ok(true),
3728 )
3729 .or_else(|err| match err {
3730 rusqlite::Error::QueryReturnedNoRows => Ok(false),
3731 _ => Err(err),
3732 })
3733}
3734
3735struct CanonicalNodeRow {
3736 cursor: u64,
3737 kind: String,
3738 body: String,
3739}
3740
3741fn canonical_node_rows(connection: &Connection) -> rusqlite::Result<Vec<CanonicalNodeRow>> {
3742 let mut statement = connection
3743 .prepare("SELECT write_cursor, kind, body FROM canonical_nodes ORDER BY write_cursor")?;
3744 let rows = statement.query_map([], |row| {
3745 Ok(CanonicalNodeRow {
3746 cursor: row.get::<_, u64>(0)?,
3747 kind: row.get::<_, String>(1)?,
3748 body: row.get::<_, String>(2)?,
3749 })
3750 })?;
3751 rows.collect()
3752}
3753
3754fn hex_encode(bytes: &[u8]) -> String {
3755 let mut out = String::with_capacity(bytes.len() * 2);
3756 for byte in bytes {
3757 out.push(hex_nibble(byte >> 4));
3758 out.push(hex_nibble(byte & 0x0f));
3759 }
3760 out
3761}
3762
3763fn hex_nibble(value: u8) -> char {
3764 match value {
3765 0..=9 => (b'0' + value) as char,
3766 10..=15 => (b'a' + value - 10) as char,
3767 _ => unreachable!(),
3768 }
3769}
3770
3771fn physical_section(connection: &Connection, full: bool) -> Section {
3772 let mut findings = Vec::new();
3773 if let Err(err) = connection.query_row("PRAGMA page_count", [], |row| row.get::<_, i64>(0)) {
3774 findings.push(Finding {
3775 code: "E_CORRUPT_HEADER",
3776 stage: "PhysicalProbe",
3777 locator: locator_from_rusqlite_error(&err),
3778 doc_anchor: "design/recovery.md#header-malformed",
3779 detail: format!("page_count probe failed: {err}"),
3780 });
3781 }
3782 if full {
3783 match collect_integrity_check_findings(connection) {
3784 Ok(rows) => findings.extend(rows),
3785 Err(err) => findings.push(Finding {
3786 code: "E_CORRUPT_INTEGRITY_CHECK",
3787 stage: "IntegrityCheck",
3788 locator: locator_from_rusqlite_error(&err),
3789 doc_anchor: "design/recovery.md#integrity-check-full-findings",
3790 detail: format!("PRAGMA integrity_check failed: {err}"),
3791 }),
3792 }
3793 }
3794 if findings.is_empty() {
3795 Section::Clean
3796 } else {
3797 Section::Findings(findings)
3798 }
3799}
3800
3801fn logical_section(connection: &Connection) -> Section {
3802 let mut findings = Vec::new();
3803 if let Err(err) = connection.query_row("PRAGMA schema_version", [], |row| row.get::<_, i64>(0))
3804 {
3805 findings.push(Finding {
3806 code: "E_CORRUPT_SCHEMA",
3807 stage: "SchemaProbe",
3808 locator: locator_from_rusqlite_error(&err),
3809 doc_anchor: "design/recovery.md#schema-inconsistent",
3810 detail: format!("schema_version probe failed: {err}"),
3811 });
3812 }
3813 match connection.query_row("PRAGMA user_version", [], |row| row.get::<_, u32>(0)) {
3814 Ok(0) => findings.push(Finding {
3815 code: "E_CORRUPT_SCHEMA",
3816 stage: "SchemaProbe",
3817 locator: CorruptionLocator::MigrationStep { from: 0, to: 0 },
3818 doc_anchor: "design/recovery.md#schema-inconsistent",
3819 detail: "user_version is zero".to_string(),
3820 }),
3821 Ok(_) => {}
3822 Err(err) => findings.push(Finding {
3823 code: "E_CORRUPT_SCHEMA",
3824 stage: "SchemaProbe",
3825 locator: locator_from_rusqlite_error(&err),
3826 doc_anchor: "design/recovery.md#schema-inconsistent",
3827 detail: format!("user_version probe failed: {err}"),
3828 }),
3829 }
3830 if findings.is_empty() {
3831 Section::Clean
3832 } else {
3833 Section::Findings(findings)
3834 }
3835}
3836
3837fn semantic_section(connection: &Connection) -> Section {
3838 match load_default_profile(connection) {
3839 Ok(_) => Section::Clean,
3840 Err(rusqlite::Error::QueryReturnedNoRows) => Section::Findings(vec![Finding {
3841 code: "E_CORRUPT_EMBEDDER_IDENTITY",
3842 stage: "EmbedderIdentity",
3843 locator: CorruptionLocator::OpaqueSqliteError { sqlite_extended_code: 0 },
3844 doc_anchor: "design/recovery.md#embedder-identity-drift",
3845 detail: "default embedder profile row is missing".to_string(),
3846 }]),
3847 Err(err) => Section::Findings(vec![Finding {
3848 code: "E_CORRUPT_EMBEDDER_IDENTITY",
3849 stage: "EmbedderIdentity",
3850 locator: locator_from_rusqlite_error(&err),
3851 doc_anchor: "design/recovery.md#embedder-identity-drift",
3852 detail: format!("default embedder profile probe failed: {err}"),
3853 }]),
3854 }
3855}
3856
3857fn collect_integrity_check_findings(connection: &Connection) -> rusqlite::Result<Vec<Finding>> {
3858 let mut statement = connection.prepare("PRAGMA integrity_check")?;
3859 let rows = statement.query_map([], |row| row.get::<_, String>(0))?;
3860 let mut findings = Vec::new();
3861 for row in rows {
3862 let message = row?;
3863 if message == "ok" {
3864 continue;
3865 }
3866 findings.push(Finding {
3867 code: "E_CORRUPT_INTEGRITY_CHECK",
3868 stage: "IntegrityCheck",
3869 locator: CorruptionLocator::OpaqueSqliteError {
3870 sqlite_extended_code: rusqlite::ffi::SQLITE_CORRUPT,
3871 },
3872 doc_anchor: "design/recovery.md#integrity-check-full-findings",
3873 detail: message,
3874 });
3875 }
3876 Ok(findings)
3877}
3878
3879fn locator_from_rusqlite_error(err: &rusqlite::Error) -> CorruptionLocator {
3880 let extended = err.sqlite_error().map(|inner| inner.extended_code).unwrap_or(0);
3881 CorruptionLocator::OpaqueSqliteError { sqlite_extended_code: extended }
3882}
3883
3884fn open_runtime_connection(path: &Path) -> rusqlite::Result<Connection> {
3885 let connection = Connection::open(path)?;
3886 connection.pragma_update(None, "journal_mode", "WAL")?;
3887 Ok(connection)
3888}
3889
3890fn load_projection_cursor(connection: &Connection) -> rusqlite::Result<u64> {
3891 connection
3892 .query_row(
3893 "SELECT value FROM _fathomdb_open_state WHERE key = ?1",
3894 [PROJECTION_CURSOR_KEY],
3895 |row| row.get::<_, String>(0),
3896 )
3897 .map(|value| value.parse::<u64>().unwrap_or(0))
3898 .or_else(|err| match err {
3899 rusqlite::Error::QueryReturnedNoRows => Ok(0),
3900 _ => Err(err),
3901 })
3902}
3903
3904fn store_projection_cursor(connection: &Connection, cursor: u64) -> rusqlite::Result<()> {
3905 connection.execute(
3906 "INSERT INTO _fathomdb_open_state(key, value) VALUES(?1, ?2)
3907 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
3908 params![PROJECTION_CURSOR_KEY, cursor.to_string()],
3909 )?;
3910 Ok(())
3911}
3912
3913fn record_projection_terminal(
3914 connection: &Connection,
3915 cursor: u64,
3916 state: &str,
3917) -> rusqlite::Result<()> {
3918 connection.execute(
3919 "INSERT OR IGNORE INTO _fathomdb_projection_terminal(write_cursor, state) VALUES(?1, ?2)",
3920 params![cursor, state],
3921 )?;
3922 Ok(())
3923}
3924
3925fn terminal_state_for_cursor(
3926 connection: &Connection,
3927 cursor: u64,
3928) -> rusqlite::Result<Option<String>> {
3929 connection
3930 .query_row(
3931 "SELECT state FROM _fathomdb_projection_terminal WHERE write_cursor = ?1",
3932 [cursor],
3933 |row| row.get::<_, String>(0),
3934 )
3935 .map(Some)
3936 .or_else(|err| match err {
3937 rusqlite::Error::QueryReturnedNoRows => Ok(None),
3938 _ => Err(err),
3939 })
3940}
3941
3942fn advance_projection_cursor(connection: &Connection) -> rusqlite::Result<u64> {
3943 let mut cursor = load_projection_cursor(connection)?;
3944 loop {
3945 let next = cursor.saturating_add(1);
3946 if terminal_state_for_cursor(connection, next)?.is_some() {
3947 cursor = next;
3948 } else {
3949 break;
3950 }
3951 }
3952 store_projection_cursor(connection, cursor)?;
3953 Ok(cursor)
3954}
3955
3956fn commit_projection_outcomes(
3957 connection: &mut Connection,
3958 outcomes: &[ProjectionOutcome],
3959 shared: &ProjectionRuntimeShared,
3960) -> rusqlite::Result<()> {
3961 let embedder_identity = &shared.embedder_identity;
3962 let mc = identity_requires_mean_centering(embedder_identity);
3963 let _gate = shared.commit_gate.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
3966 let tx = connection.transaction()?;
3967 let mut current_mean: Option<Vec<f32>> = if mc {
3970 tx.query_row(
3971 "SELECT mean_vec FROM _fathomdb_embedder_profiles WHERE profile = 'default'",
3972 [],
3973 |row| row.get::<_, Option<Vec<u8>>>(0),
3974 )
3975 .ok()
3976 .flatten()
3977 .map(|bytes| decode_vector_blob(&bytes))
3978 } else {
3979 None
3980 };
3981 let mut staged_events: Vec<EmbedderEvent> = Vec::new();
3982 for outcome in outcomes {
3983 match outcome {
3984 ProjectionOutcome::Success { cursor, kind, blob, bin_blob } => {
3985 if terminal_state_for_cursor(&tx, *cursor)?.is_some() {
3986 continue;
3987 }
3988 let pin_mean: Option<Vec<f32>> = if mc && current_mean.is_none() {
3993 let mut acc = shared.mean_accumulator.lock().unwrap_or_else(|p| p.into_inner());
3994 match acc.as_mut() {
3995 Some(a) => {
3996 a.add(&decode_vector_blob(bin_blob));
3997 if a.count() >= MEAN_VEC_PIN_THRESHOLD {
3998 let mean = a.materialize();
3999 *acc = None;
4000 Some(mean)
4001 } else {
4002 None
4003 }
4004 }
4005 None => None,
4006 }
4007 } else {
4008 None
4009 };
4010
4011 let source_type = resolve_source_type(kind).map_err(|_| {
4012 rusqlite::Error::SqliteFailure(
4013 rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_CONSTRAINT),
4014 Some(format!("unknown kind for source_type mapping: {kind}")),
4015 )
4016 })?;
4017 let now_unix =
4018 SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs()
4019 as i64;
4020 tx.execute(
4021 "INSERT OR IGNORE INTO _fathomdb_vector_rows(rowid, kind, write_cursor) VALUES(?1, ?2, ?3)",
4022 params![cursor, kind, cursor],
4023 )?;
4024 let centered_blob: Vec<u8> = match ¤t_mean {
4030 Some(mean) if mean.len() * 4 == bin_blob.len() => {
4031 encode_vector_blob(&subtract_mean(&decode_vector_blob(bin_blob), mean))
4032 }
4033 _ => bin_blob.clone(),
4034 };
4035 tx.execute(
4036 "INSERT OR IGNORE INTO vector_default(
4037 rowid, embedding, embedding_bin, source_type, kind, created_at
4038 ) VALUES(?1, ?2, vec_quantize_binary(?3), ?4, ?5, ?6)",
4039 params![cursor, blob, centered_blob, source_type, kind, now_unix],
4040 )?;
4041 record_projection_terminal(&tx, *cursor, "up_to_date")?;
4042
4043 if let Some(mean) = pin_mean {
4048 tx.execute(
4049 "UPDATE _fathomdb_embedder_profiles SET mean_vec = ?1 WHERE profile = 'default'",
4050 params![encode_vector_blob(&mean)],
4051 )?;
4052 let rows: Vec<(i64, Vec<u8>)> = {
4053 let mut statement = tx.prepare(
4054 "SELECT rowid, embedding FROM vector_default ORDER BY rowid",
4055 )?;
4056 let mapped = statement.query_map([], |row| {
4057 Ok((row.get::<_, i64>(0)?, row.get::<_, Vec<u8>>(1)?))
4058 })?;
4059 let mut out = Vec::new();
4060 for r in mapped {
4061 out.push(r?);
4062 }
4063 out
4064 };
4065 let (doc_count, _) =
4066 run_pin_and_requantize_pass(&tx, &rows, &mean).map_err(|_| {
4067 rusqlite::Error::SqliteFailure(
4068 rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_ERROR),
4069 Some("mean-centering re-quantize pass failed".to_string()),
4070 )
4071 })?;
4072 staged_events.push(EmbedderEvent::MeanVecPinned {
4073 dim: u32::try_from(mean.len()).unwrap_or(u32::MAX),
4074 doc_count,
4075 });
4076 current_mean = Some(mean);
4077 }
4078 }
4079 ProjectionOutcome::Failure { cursor, failure_code } => {
4080 if terminal_state_for_cursor(&tx, *cursor)?.is_some() {
4081 continue;
4082 }
4083 let existing: u64 = tx.query_row(
4084 "SELECT COUNT(*) FROM operational_mutations
4085 WHERE collection_name = 'projection_failures'
4086 AND json_extract(payload_json, '$.write_cursor') = ?1",
4087 [cursor],
4088 |row| row.get(0),
4089 )?;
4090 if existing == 0 {
4091 let payload = format!(
4092 r#"{{"write_cursor":{cursor},"failure_code":"{failure_code}","recorded_at":0}}"#
4093 );
4094 tx.execute(
4095 "INSERT INTO operational_mutations(
4096 collection_name, record_key, op_kind, payload_json, schema_id, write_cursor
4097 ) VALUES('projection_failures', ?1, 'append', ?2, NULL, ?3)",
4098 params![cursor.to_string(), payload, cursor],
4099 )?;
4100 }
4101 record_projection_terminal(&tx, *cursor, "failed")?;
4102 }
4103 }
4104 }
4105 advance_projection_cursor(&tx)?;
4115 tx.commit()?;
4116 if !staged_events.is_empty() {
4119 if let Ok(mut events) = shared.pending_events.lock() {
4120 events.extend(staged_events);
4121 }
4122 }
4123 Ok(())
4124}
4125
4126fn recover_mean_vec_pin(
4133 connection: &mut Connection,
4134 identity: &EmbedderIdentity,
4135) -> Result<(), EngineError> {
4136 let tx = connection.transaction().map_err(|_| EngineError::Storage)?;
4137 recompute_mean_in_tx(&tx, identity)?;
4138 tx.commit().map_err(|_| EngineError::Storage)?;
4139 Ok(())
4140}
4141
4142fn recompute_mean_in_tx(
4156 tx: &rusqlite::Transaction<'_>,
4157 identity: &EmbedderIdentity,
4158) -> Result<MeanRecomputeReport, EngineError> {
4159 recompute_mean_in_tx_inner(tx, identity, false)
4160}
4161
4162fn recompute_mean_in_tx_inner(
4167 tx: &rusqlite::Transaction<'_>,
4168 identity: &EmbedderIdentity,
4169 fail_after_mean_update: bool,
4170) -> Result<MeanRecomputeReport, EngineError> {
4171 let started = Instant::now();
4172 let dim = identity.dimension as usize;
4173 let old_mean = read_pinned_mean_vec(tx, identity.dimension)?;
4176 let rows: Vec<(i64, Vec<u8>)> = {
4177 let mut statement = tx
4178 .prepare("SELECT rowid, embedding FROM vector_default ORDER BY rowid")
4179 .map_err(|_| EngineError::Storage)?;
4180 let mapped = statement
4181 .query_map([], |row| Ok((row.get::<_, i64>(0)?, row.get::<_, Vec<u8>>(1)?)))
4182 .map_err(|_| EngineError::Storage)?;
4183 let mut out = Vec::new();
4184 for r in mapped {
4185 out.push(r.map_err(|_| EngineError::Storage)?);
4186 }
4187 out
4188 };
4189 let mut accumulator = MeanAccumulator::new(dim);
4190 for (_rowid, blob) in &rows {
4191 if blob.len() != dim * 4 {
4192 return Err(EngineError::Storage);
4193 }
4194 accumulator.add(&decode_vector_blob(blob));
4195 }
4196 let old_doc_count = accumulator.count();
4197 let mean = accumulator.materialize();
4198 let drift_cos_before = match &old_mean {
4199 Some(old) => cosine_similarity(&mean, old),
4200 None => 1.0,
4201 };
4202 tx.execute(
4203 "UPDATE _fathomdb_embedder_profiles SET mean_vec = ?1 WHERE profile = 'default'",
4204 params![encode_vector_blob(&mean)],
4205 )
4206 .map_err(|_| EngineError::Storage)?;
4207 if fail_after_mean_update {
4208 return Err(EngineError::Storage);
4211 }
4212 let (doc_count, _) = run_pin_and_requantize_pass(tx, &rows, &mean)?;
4213 Ok(MeanRecomputeReport {
4214 dim: u32::try_from(dim).unwrap_or(u32::MAX),
4215 old_doc_count,
4216 doc_count_requantized: doc_count,
4217 drift_cos_before,
4218 mean_was_pinned: old_mean.is_some(),
4219 elapsed_ms: u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX),
4220 })
4221}
4222
4223fn enforce_provenance_retention(connection: &Connection, cap: u64) -> rusqlite::Result<()> {
4224 if cap == 0 {
4225 return Ok(());
4226 }
4227 let slack = cap.max(20) / 20;
4228 let upper = cap.saturating_add(slack.max(1));
4229 let count: u64 =
4230 connection.query_row("SELECT COUNT(*) FROM operational_mutations", [], |row| row.get(0))?;
4231 if count <= upper {
4232 return Ok(());
4233 }
4234 let to_delete = count.saturating_sub(cap);
4235 connection.execute(
4236 "DELETE FROM operational_mutations
4237 WHERE id IN (
4238 SELECT id FROM operational_mutations
4239 ORDER BY id
4240 LIMIT ?1
4241 )",
4242 [to_delete],
4243 )?;
4244 Ok(())
4245}
4246
4247fn projection_status(
4248 connection: &Connection,
4249 kind: &str,
4250) -> Result<lifecycle::ProjectionStatus, EngineError> {
4251 let latest = connection
4252 .query_row(
4253 "SELECT COALESCE(MAX(write_cursor), 0) FROM canonical_nodes WHERE kind = ?1",
4254 [kind],
4255 |row| row.get::<_, u64>(0),
4256 )
4257 .map_err(|_| EngineError::Storage)?;
4258 if latest == 0 {
4259 return Ok(lifecycle::ProjectionStatus::UpToDate);
4260 }
4261 let pending: u64 = connection
4262 .query_row(
4263 "SELECT COUNT(*)
4264 FROM canonical_nodes
4265 LEFT JOIN _fathomdb_projection_terminal
4266 ON _fathomdb_projection_terminal.write_cursor = canonical_nodes.write_cursor
4267 WHERE canonical_nodes.kind = ?1
4268 AND _fathomdb_projection_terminal.write_cursor IS NULL",
4269 [kind],
4270 |row| row.get(0),
4271 )
4272 .map_err(|_| EngineError::Storage)?;
4273 if pending > 0 {
4274 return Ok(lifecycle::ProjectionStatus::Pending);
4275 }
4276 match terminal_state_for_cursor(connection, latest).map_err(|_| EngineError::Storage)? {
4277 Some(state) if state == "failed" => Ok(lifecycle::ProjectionStatus::Failed),
4278 _ => Ok(lifecycle::ProjectionStatus::UpToDate),
4279 }
4280}
4281
4282fn canonical_database_path(path: &Path) -> Result<PathBuf, EngineOpenError> {
4283 let parent = path
4284 .parent()
4285 .filter(|parent| !parent.as_os_str().is_empty())
4286 .unwrap_or_else(|| Path::new("."));
4287 let canonical_parent = parent.canonicalize().map_err(|_| EngineOpenError::Io {
4288 message: "database parent directory is not accessible".to_string(),
4289 })?;
4290 let file_name = path.file_name().ok_or_else(|| EngineOpenError::Io {
4291 message: "database path has no file name".to_string(),
4292 })?;
4293
4294 Ok(canonical_parent.join(file_name))
4295}
4296
4297fn acquire_lock(path: &Path) -> Result<File, EngineOpenError> {
4298 let lock_path = lock_path(path);
4299 let mut options = OpenOptions::new();
4300 options.read(true).write(true).create(true);
4301 #[cfg(unix)]
4302 options.mode(0o600);
4303
4304 let mut file = options.open(&lock_path).map_err(|_| EngineOpenError::Io {
4305 message: "could not open database lock file".to_string(),
4306 })?;
4307
4308 match file.try_lock() {
4309 Ok(()) => {
4310 let pid = std::process::id().to_string();
4311 let _ = file.set_len(0);
4312 let _ = file.seek(SeekFrom::Start(0));
4313 let _ = file.write_all(pid.as_bytes());
4314 Ok(file)
4315 }
4316 Err(std::fs::TryLockError::WouldBlock) => {
4317 Err(EngineOpenError::DatabaseLocked { holder_pid: read_holder_pid(&lock_path) })
4318 }
4319 Err(_) => {
4320 Err(EngineOpenError::Io { message: "could not acquire database lock".to_string() })
4321 }
4322 }
4323}
4324
4325fn lock_path(path: &Path) -> PathBuf {
4326 let mut lock_path = path.as_os_str().to_os_string();
4327 lock_path.push(LOCK_SUFFIX);
4328 PathBuf::from(lock_path)
4329}
4330
4331fn read_holder_pid(path: &Path) -> Option<u32> {
4332 std::fs::read_to_string(path).ok()?.trim().parse().ok()
4333}
4334
4335fn map_migration_error(err: SchemaMigrationError) -> EngineOpenError {
4336 match err {
4337 SchemaMigrationError::IncompatibleSchemaVersion { seen, supported } => {
4338 EngineOpenError::IncompatibleSchemaVersion { seen, supported }
4339 }
4340 SchemaMigrationError::MigrationError(report) => EngineOpenError::MigrationError {
4341 schema_version_before: report.schema_version_before,
4342 schema_version_current: report.schema_version_current,
4343 step_id: report.migration_steps.last().map_or(0, |step| step.step_id),
4344 },
4345 SchemaMigrationError::Storage { message } => {
4346 EngineOpenError::Io { message: message.to_string() }
4347 }
4348 }
4349}
4350
4351fn init_perf_experiments_runtime() {
4367 static INIT: Once = Once::new();
4368 INIT.call_once(|| {
4369 if std::env::var_os("FATHOMDB_PERF_EXPERIMENTS").is_none() {
4370 return;
4371 }
4372 let memstatus_off =
4373 std::env::var_os("FATHOMDB_PERF_SQLITE_MEMSTATUS_OFF").is_some_and(|v| v == "1");
4374 let pagecache = std::env::var("FATHOMDB_PERF_SQLITE_PAGECACHE").ok();
4380 let pcache2_on =
4384 std::env::var_os("FATHOMDB_PERF_SQLITE_PCACHE2").is_some_and(|v| v == "1");
4385 if !memstatus_off && pagecache.is_none() && !pcache2_on {
4386 return;
4387 }
4388 unsafe {
4395 let rc_shutdown = rusqlite::ffi::sqlite3_shutdown();
4396 let rc_memstatus = if memstatus_off {
4397 rusqlite::ffi::sqlite3_config(rusqlite::ffi::SQLITE_CONFIG_MEMSTATUS, 0_i32)
4398 } else {
4399 -1
4400 };
4401 let rc_pagecache = if let Some(spec) = pagecache.as_ref() {
4405 let mut parts = spec.split(':');
4406 let sz = parts.next().and_then(|s| s.parse::<i32>().ok()).unwrap_or(0);
4407 let n = parts.next().and_then(|s| s.parse::<i32>().ok()).unwrap_or(0);
4408 if sz > 0 && n > 0 {
4409 rusqlite::ffi::sqlite3_config(
4410 7, std::ptr::null_mut::<std::ffi::c_void>(),
4412 sz,
4413 n,
4414 )
4415 } else {
4416 eprintln!(
4417 "perf-experiment: bad FATHOMDB_PERF_SQLITE_PAGECACHE spec '{spec}' (expect '<bytes>:<count>')"
4418 );
4419 -1
4420 }
4421 } else {
4422 -1
4423 };
4424 let rc_pcache2 = if pcache2_on {
4425 rusqlite::ffi::sqlite3_config(
4429 rusqlite::ffi::SQLITE_CONFIG_PCACHE2,
4430 &raw const pcache2::PCACHE2_METHODS.0,
4431 )
4432 } else {
4433 -1
4434 };
4435 let rc_init = rusqlite::ffi::sqlite3_initialize();
4436 eprintln!(
4437 "perf-experiment: runtime-config rcs shutdown={rc_shutdown} \
4438 memstatus={rc_memstatus} pagecache={rc_pagecache} pcache2={rc_pcache2} \
4439 initialize={rc_init} (0=SQLITE_OK; 21=SQLITE_MISUSE; -1=not configured)"
4440 );
4441 }
4442 });
4443}
4444
4445fn register_sqlite_vec_extension() {
4446 static REGISTER: Once = Once::new();
4447 REGISTER.call_once(|| unsafe {
4448 let entrypoint: unsafe extern "C" fn(
4449 *mut rusqlite::ffi::sqlite3,
4450 *mut *const std::os::raw::c_char,
4451 *const rusqlite::ffi::sqlite3_api_routines,
4452 ) -> std::os::raw::c_int = std::mem::transmute(sqlite3_vec_init as *const ());
4453 rusqlite::ffi::sqlite3_auto_extension(Some(entrypoint));
4454 });
4455}
4456
4457fn probe_open_integrity(connection: &Connection) -> Result<(), EngineOpenError> {
4458 connection
4463 .query_row("SELECT COUNT(*) FROM sqlite_schema", [], |row| row.get::<_, i64>(0))
4464 .map(|_| ())
4465 .map_err(|err| map_open_sqlite_error(err, OpenStage::SchemaProbe))
4466}
4467
4468fn probe_database_header(connection: &Connection) -> Result<(), EngineOpenError> {
4469 connection
4470 .query_row("PRAGMA application_id", [], |row| row.get::<_, i64>(0))
4471 .map(|_| ())
4472 .map_err(|err| map_open_sqlite_error(err, OpenStage::HeaderProbe))
4473}
4474
4475fn probe_wal_sidecar(db_path: &Path) -> Result<(), EngineOpenError> {
4482 let mut wal_path = db_path.as_os_str().to_owned();
4483 wal_path.push("-wal");
4484 let wal_path = PathBuf::from(wal_path);
4485 use std::io::Read;
4493 let mut file = match std::fs::File::open(&wal_path) {
4494 Ok(file) => file,
4495 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
4496 Err(_) => return Ok(()),
4497 };
4498 let mut bytes = [0u8; 32];
4499 if file.read_exact(&mut bytes).is_err() {
4500 return Ok(());
4503 }
4504 let magic = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
4505 let page_size = u32::from_be_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
4506 const WAL_MAGIC_MASK: u32 = 0xFFFF_FFFE;
4510 const WAL_MAGIC: u32 = 0x377F_0682;
4511 const SQLITE_MAX_PAGE_SIZE: u32 = 65536;
4512 let magic_ok = (magic & WAL_MAGIC_MASK) == WAL_MAGIC;
4513 let page_size_ok =
4514 page_size.is_power_of_two() && (512..=SQLITE_MAX_PAGE_SIZE).contains(&page_size);
4515 if magic_ok && page_size_ok {
4516 return Ok(());
4517 }
4518 Err(EngineOpenError::Corruption(CorruptionDetail {
4519 kind: CorruptionKind::WalReplayFailure,
4520 stage: OpenStage::WalReplay,
4521 locator: CorruptionLocator::FileOffset { offset: if !magic_ok { 0 } else { 8 } },
4522 recovery_hint: RecoveryHint {
4523 code: "E_CORRUPT_WAL_REPLAY",
4524 doc_anchor: "design/recovery.md#wal-replay-failures",
4525 },
4526 }))
4527}
4528
4529fn reject_legacy_shape(connection: &Connection) -> Result<(), EngineOpenError> {
4530 let has_legacy_table = table_exists(connection, "fathom_nodes")
4531 || table_exists(connection, "fathom_edges")
4532 || table_exists(connection, "fathom_chunks");
4533 if !has_legacy_table {
4534 return Ok(());
4535 }
4536
4537 let seen =
4538 connection.query_row("PRAGMA user_version", [], |row| row.get::<_, u32>(0)).unwrap_or(0);
4539 Err(EngineOpenError::IncompatibleSchemaVersion { seen, supported: SCHEMA_VERSION })
4540}
4541
4542fn table_exists(connection: &Connection, table: &str) -> bool {
4543 connection
4544 .query_row(
4545 "SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = ?1",
4546 [table],
4547 |_row| Ok(()),
4548 )
4549 .is_ok()
4550}
4551
4552fn read_schema_objects(
4553 connection: &Connection,
4554 obj_type: &str,
4555) -> Result<Vec<SchemaObject>, EngineError> {
4556 let mut stmt = connection
4557 .prepare(
4558 "SELECT name, sql FROM sqlite_schema
4559 WHERE type = ?1 AND name NOT LIKE 'sqlite_%' AND sql IS NOT NULL
4560 ORDER BY name",
4561 )
4562 .map_err(|_| EngineError::Storage)?;
4563 let rows = stmt
4564 .query_map([obj_type], |row| {
4565 Ok(SchemaObject { name: row.get::<_, String>(0)?, sql: row.get::<_, String>(1)? })
4566 })
4567 .map_err(|_| EngineError::Storage)?;
4568 let mut out = Vec::new();
4569 for row in rows {
4570 out.push(row.map_err(|_| EngineError::Storage)?);
4571 }
4572 Ok(out)
4573}
4574
4575fn order_canonical_first(mut objects: Vec<SchemaObject>) -> Vec<SchemaObject> {
4576 let mut canonical: Vec<SchemaObject> = Vec::new();
4577 for name in CANONICAL_TABLES {
4578 if let Some(pos) = objects.iter().position(|o| o.name == *name) {
4579 canonical.push(objects.remove(pos));
4580 }
4581 }
4582 canonical.extend(objects);
4583 canonical
4584}
4585
4586fn load_default_profile(connection: &Connection) -> rusqlite::Result<EmbedderIdentity> {
4587 connection.query_row(
4588 "SELECT name, revision, dimension FROM _fathomdb_embedder_profiles WHERE profile = ?1",
4589 [DEFAULT_VECTOR_PROFILE],
4590 |row| {
4591 Ok(EmbedderIdentity::new(
4592 row.get::<_, String>(0)?,
4593 row.get::<_, String>(1)?,
4594 row.get::<_, u32>(2)?,
4595 ))
4596 },
4597 )
4598}
4599
4600fn default_profile_dimension(connection: &Connection) -> Result<u32, EngineError> {
4601 load_default_profile(connection)
4602 .map(|identity| identity.dimension)
4603 .map_err(|_| EngineError::Storage)
4604}
4605
4606fn kind_is_vector_indexed(connection: &Connection, kind: &str) -> Result<bool, EngineError> {
4607 connection
4608 .query_row("SELECT 1 FROM _fathomdb_vector_kinds WHERE kind = ?1", [kind], |_row| Ok(()))
4609 .map(|_| true)
4610 .or_else(|err| match err {
4611 rusqlite::Error::QueryReturnedNoRows => Ok(false),
4612 _ => Err(EngineError::Storage),
4613 })
4614}
4615
4616fn ensure_vector_partition(connection: &mut Connection, dimension: u32) -> rusqlite::Result<()> {
4617 let existing_sql: Option<String> = connection
4631 .query_row(
4632 "SELECT sql FROM sqlite_master WHERE type='table' AND name=?1",
4633 [DEFAULT_VECTOR_PARTITION],
4634 |row| row.get::<_, String>(0),
4635 )
4636 .optional()?;
4637
4638 match existing_sql {
4639 None => create_vector_partition(connection, dimension),
4640 Some(sql) if sql.contains("embedding_bin") => Ok(()),
4641 Some(_) => migrate_vector_partition_to_pack1(connection, dimension),
4642 }
4643}
4644
4645fn create_vector_partition(connection: &Connection, dimension: u32) -> rusqlite::Result<()> {
4646 let sql = format!(
4647 "CREATE VIRTUAL TABLE IF NOT EXISTS {DEFAULT_VECTOR_PARTITION} USING vec0(\
4648 embedding float[{dimension}],\
4649 embedding_bin bit[{dimension}],\
4650 source_type TEXT partition key,\
4651 kind TEXT,\
4652 created_at INTEGER\
4653 )"
4654 );
4655 connection.execute_batch(&sql)
4656}
4657
4658const KIND_TO_SOURCE_TYPE_CASE_SQL: &str = "CASE s.kind
4662 WHEN 'email' THEN 'email'
4663 WHEN 'article' THEN 'article'
4664 WHEN 'paper' THEN 'paper'
4665 WHEN 'meeting' THEN 'meeting'
4666 WHEN 'note' THEN 'note'
4667 WHEN 'todo' THEN 'todo'
4668 WHEN 'doc' THEN 'article'
4669 ELSE 'article'
4670END";
4671
4672fn migrate_vector_partition_to_pack1(
4688 connection: &mut Connection,
4689 dimension: u32,
4690) -> rusqlite::Result<()> {
4691 let tx = connection.transaction()?;
4692 tx.execute_batch(
4693 "CREATE TABLE _fathomdb_vector_migration_v0_7_0 (
4694 rowid INTEGER PRIMARY KEY,
4695 embedding BLOB NOT NULL,
4696 kind TEXT NOT NULL
4697 );
4698 INSERT INTO _fathomdb_vector_migration_v0_7_0(rowid, embedding, kind)
4699 SELECT v.rowid, v.embedding, r.kind
4700 FROM vector_default v
4701 JOIN _fathomdb_vector_rows r ON r.rowid = v.rowid;
4702 DROP TABLE vector_default;",
4703 )?;
4704 let create_sql = format!(
4705 "CREATE VIRTUAL TABLE {DEFAULT_VECTOR_PARTITION} USING vec0(\
4706 embedding float[{dimension}],\
4707 embedding_bin bit[{dimension}],\
4708 source_type TEXT partition key,\
4709 kind TEXT,\
4710 created_at INTEGER\
4711 )"
4712 );
4713 tx.execute_batch(&create_sql)?;
4714 let repopulate_sql = format!(
4715 "INSERT INTO vector_default(
4716 rowid, embedding, embedding_bin, source_type, kind, created_at
4717 )
4718 SELECT
4719 s.rowid,
4720 s.embedding,
4721 vec_quantize_binary(s.embedding),
4722 {KIND_TO_SOURCE_TYPE_CASE_SQL},
4723 s.kind,
4724 strftime('%s', 'now')
4725 FROM _fathomdb_vector_migration_v0_7_0 s;
4726 DROP TABLE _fathomdb_vector_migration_v0_7_0;"
4727 );
4728 tx.execute_batch(&repopulate_sql)?;
4729 tx.commit()
4730}
4731
4732fn encode_vector_blob(vector: &[f32]) -> Vec<u8> {
4733 vector.iter().flat_map(|value| value.to_le_bytes()).collect()
4734}
4735
4736fn decode_vector_blob(bytes: &[u8]) -> Vec<f32> {
4737 debug_assert_eq!(bytes.len() % 4, 0, "f32 BLOB length must be multiple of 4");
4738 bytes.chunks_exact(4).map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]])).collect()
4739}
4740
4741fn identity_requires_mean_centering(identity: &EmbedderIdentity) -> bool {
4745 identity.name == BGE_SMALL_EMBEDDER_NAME
4746}
4747
4748fn read_pinned_mean_vec(
4755 connection: &Connection,
4756 dimension: u32,
4757) -> Result<Option<Vec<f32>>, EngineError> {
4758 let bytes: Option<Vec<u8>> = connection
4759 .query_row(
4760 "SELECT mean_vec FROM _fathomdb_embedder_profiles WHERE profile = 'default'",
4761 [],
4762 |row| row.get::<_, Option<Vec<u8>>>(0),
4763 )
4764 .or_else(|err| match err {
4765 rusqlite::Error::QueryReturnedNoRows => Ok(None),
4766 other => Err(other),
4767 })
4768 .map_err(|_| EngineError::Storage)?;
4769 let Some(bytes) = bytes else { return Ok(None) };
4770 let expected_len = (dimension as usize).saturating_mul(4);
4771 if bytes.len() != expected_len {
4772 return Err(EngineError::Storage);
4773 }
4774 let mut out = Vec::with_capacity(dimension as usize);
4775 for chunk in bytes.chunks_exact(4) {
4776 let arr = [chunk[0], chunk[1], chunk[2], chunk[3]];
4777 out.push(f32::from_le_bytes(arr));
4778 }
4779 Ok(Some(out))
4780}
4781
4782fn subtract_mean(v: &[f32], mean: &[f32]) -> Vec<f32> {
4785 debug_assert_eq!(v.len(), mean.len(), "subtract_mean dim mismatch");
4786 v.iter().zip(mean.iter()).map(|(a, b)| *a - *b).collect()
4787}
4788
4789fn resolve_source_type(kind: &str) -> Result<&'static str, EngineError> {
4796 Ok(match kind {
4797 "email" => "email",
4798 "article" => "article",
4799 "paper" => "paper",
4800 "meeting" => "meeting",
4801 "note" => "note",
4802 "todo" => "todo",
4803 "doc" => "article",
4805 _ => return Err(EngineError::Storage),
4806 })
4807}
4808
4809fn map_runtime_embedder_error(err: RuntimeEmbedderError) -> EngineError {
4810 match err {
4811 RuntimeEmbedderError::Failed { .. } | RuntimeEmbedderError::Timeout => {
4812 EngineError::Embedder
4813 }
4814 }
4815}
4816
4817fn default_embedder_identity() -> EmbedderIdentity {
4818 EmbedderIdentity::new(
4819 DEFAULT_EMBEDDER_NAME,
4820 DEFAULT_EMBEDDER_REVISION,
4821 DEFAULT_EMBEDDER_DIMENSION,
4822 )
4823}
4824
4825fn check_embedder_profile(
4826 connection: &Connection,
4827 supplied: &EmbedderIdentity,
4828) -> Result<bool, EngineOpenError> {
4829 let mut statement = match connection.prepare(
4833 "SELECT name, revision, dimension, mean_vec FROM _fathomdb_embedder_profiles WHERE profile = 'default'",
4834 ) {
4835 Ok(statement) => statement,
4836 Err(_) => return Ok(false),
4837 };
4838 let mut rows = statement.query([]).map_err(|_| {
4839 EngineOpenError::Corruption(CorruptionDetail {
4840 kind: CorruptionKind::EmbedderIdentityDrift,
4841 stage: OpenStage::EmbedderIdentity,
4842 locator: CorruptionLocator::OpaqueSqliteError { sqlite_extended_code: 0 },
4843 recovery_hint: RecoveryHint {
4844 code: "E_CORRUPT_EMBEDDER_IDENTITY",
4845 doc_anchor: "design/recovery.md#embedder-identity-drift",
4846 },
4847 })
4848 })?;
4849
4850 let Some(row) = rows.next().map_err(|_| {
4851 EngineOpenError::Corruption(CorruptionDetail {
4852 kind: CorruptionKind::EmbedderIdentityDrift,
4853 stage: OpenStage::EmbedderIdentity,
4854 locator: CorruptionLocator::OpaqueSqliteError { sqlite_extended_code: 0 },
4855 recovery_hint: RecoveryHint {
4856 code: "E_CORRUPT_EMBEDDER_IDENTITY",
4857 doc_anchor: "design/recovery.md#embedder-identity-drift",
4858 },
4859 })
4860 })?
4861 else {
4862 connection
4863 .execute(
4864 "INSERT INTO _fathomdb_embedder_profiles(profile, name, revision, dimension)
4865 VALUES(?1, ?2, ?3, ?4)",
4866 params![
4867 DEFAULT_VECTOR_PROFILE,
4868 supplied.name,
4869 supplied.revision,
4870 supplied.dimension
4871 ],
4872 )
4873 .map_err(|_| EngineOpenError::Io {
4874 message: "could not persist embedder profile".to_string(),
4875 })?;
4876 return Ok(false);
4877 };
4878
4879 let stored_name = row.get::<_, String>(0).map_err(|_| {
4880 EngineOpenError::Corruption(CorruptionDetail {
4881 kind: CorruptionKind::EmbedderIdentityDrift,
4882 stage: OpenStage::EmbedderIdentity,
4883 locator: CorruptionLocator::TableRow { table: "_fathomdb_embedder_profiles", rowid: 0 },
4884 recovery_hint: RecoveryHint {
4885 code: "E_CORRUPT_EMBEDDER_IDENTITY",
4886 doc_anchor: "design/recovery.md#embedder-identity-drift",
4887 },
4888 })
4889 })?;
4890 let stored_revision = row.get::<_, String>(1).map_err(|_| {
4891 EngineOpenError::Corruption(CorruptionDetail {
4892 kind: CorruptionKind::EmbedderIdentityDrift,
4893 stage: OpenStage::EmbedderIdentity,
4894 locator: CorruptionLocator::TableRow { table: "_fathomdb_embedder_profiles", rowid: 0 },
4895 recovery_hint: RecoveryHint {
4896 code: "E_CORRUPT_EMBEDDER_IDENTITY",
4897 doc_anchor: "design/recovery.md#embedder-identity-drift",
4898 },
4899 })
4900 })?;
4901 let dimension = row.get::<_, u32>(2).map_err(|_| {
4902 EngineOpenError::Corruption(CorruptionDetail {
4903 kind: CorruptionKind::EmbedderIdentityDrift,
4904 stage: OpenStage::EmbedderIdentity,
4905 locator: CorruptionLocator::TableRow { table: "_fathomdb_embedder_profiles", rowid: 0 },
4906 recovery_hint: RecoveryHint {
4907 code: "E_CORRUPT_EMBEDDER_IDENTITY",
4908 doc_anchor: "design/recovery.md#embedder-identity-drift",
4909 },
4910 })
4911 })?;
4912
4913 let stored = EmbedderIdentity::new(stored_name, stored_revision, dimension);
4914
4915 if stored.name != supplied.name || stored.revision != supplied.revision {
4916 return Err(EngineOpenError::EmbedderIdentityMismatch {
4917 stored,
4918 supplied: supplied.clone(),
4919 });
4920 }
4921 if dimension != supplied.dimension {
4922 return Err(EngineOpenError::EmbedderDimensionMismatch {
4923 stored: dimension,
4924 supplied: supplied.dimension,
4925 });
4926 }
4927
4928 let mean_vec: Option<Vec<u8>> = row.get::<_, Option<Vec<u8>>>(3).map_err(|_| {
4933 EngineOpenError::Corruption(CorruptionDetail {
4934 kind: CorruptionKind::EmbedderIdentityDrift,
4935 stage: OpenStage::EmbedderIdentity,
4936 locator: CorruptionLocator::TableRow { table: "_fathomdb_embedder_profiles", rowid: 0 },
4937 recovery_hint: RecoveryHint {
4938 code: "E_CORRUPT_EMBEDDER_IDENTITY",
4939 doc_anchor: "design/recovery.md#embedder-identity-drift",
4940 },
4941 })
4942 })?;
4943 let pinned = match mean_vec {
4944 Some(bytes) => {
4945 let expected_len = (dimension as usize).saturating_mul(4);
4946 if bytes.len() != expected_len {
4952 return Err(EngineOpenError::EmbedderIdentityMismatch {
4953 stored,
4954 supplied: supplied.clone(),
4955 });
4956 }
4957 true
4958 }
4959 None => false,
4960 };
4961
4962 Ok(pinned)
4963}
4964
4965#[derive(Clone, Debug, Eq, PartialEq)]
4966enum WritePlan {
4967 Node,
4968 Edge,
4969 AppendOnlyLog,
4970 LatestState,
4971 AdminSchema,
4972}
4973
4974fn validate_batch(
4975 connection: &Connection,
4976 batch: &[PreparedWrite],
4977) -> Result<Vec<WritePlan>, EngineError> {
4978 batch.iter().map(|write| validate_write(connection, write)).collect()
4979}
4980
4981fn collect_projection_jobs(
4982 connection: &Connection,
4983 batch: &[PreparedWrite],
4984) -> Result<Vec<ProjectionJob>, EngineError> {
4985 let mut jobs = Vec::new();
4986 for write in batch {
4987 if let PreparedWrite::Node { kind, body, .. } = write {
4988 if kind_is_vector_indexed(connection, kind)? {
4989 jobs.push(ProjectionJob { cursor: 0, kind: kind.clone(), body: body.clone() });
4990 }
4991 }
4992 }
4993 Ok(jobs)
4994}
4995
4996fn validate_write(
4997 connection: &Connection,
4998 write: &PreparedWrite,
4999) -> Result<WritePlan, EngineError> {
5000 match write {
5001 PreparedWrite::Node { kind, body, source_id } => {
5002 if kind.trim().is_empty() || body.trim().is_empty() {
5003 return Err(EngineError::WriteValidation);
5004 }
5005 if let Some(source_id) = source_id {
5006 if source_id.is_empty() {
5007 return Err(EngineError::WriteValidation);
5008 }
5009 }
5010 Ok(WritePlan::Node)
5011 }
5012 PreparedWrite::Edge { kind, from, to, source_id } => {
5013 if kind.trim().is_empty() || from.trim().is_empty() || to.trim().is_empty() {
5014 return Err(EngineError::WriteValidation);
5015 }
5016 if let Some(source_id) = source_id {
5017 if source_id.is_empty() {
5018 return Err(EngineError::WriteValidation);
5019 }
5020 }
5021 Ok(WritePlan::Edge)
5022 }
5023 PreparedWrite::AdminSchema { name, kind, schema_json, retention_json } => {
5024 if name.trim().is_empty()
5025 || !matches!(kind.as_str(), "append_only_log" | "latest_state")
5026 || serde_json::from_str::<Value>(schema_json).is_err()
5027 || serde_json::from_str::<Value>(retention_json).is_err()
5028 || contains_external_ref(schema_json)
5029 {
5030 return Err(EngineError::SchemaValidation);
5031 }
5032 Ok(WritePlan::AdminSchema)
5033 }
5034 PreparedWrite::OpStore { collection, record_key, schema_id, body } => {
5035 if collection.trim().is_empty() || record_key.trim().is_empty() {
5036 return Err(EngineError::WriteValidation);
5037 }
5038 let (kind, schema_json) = collection_metadata(connection, collection)?;
5039 if let Some(schema_id) = schema_id {
5040 if schema_id != collection {
5041 return Err(EngineError::SchemaValidation);
5042 }
5043 validate_payload(&schema_json, body)?;
5044 } else if serde_json::from_str::<Value>(body).is_err() {
5045 return Err(EngineError::SchemaValidation);
5046 }
5047
5048 match kind.as_str() {
5049 "append_only_log" => Ok(WritePlan::AppendOnlyLog),
5050 "latest_state" => Ok(WritePlan::LatestState),
5051 _ => Err(EngineError::OpStore),
5052 }
5053 }
5054 }
5055}
5056
5057fn collection_metadata(
5058 connection: &Connection,
5059 collection: &str,
5060) -> Result<(String, String), EngineError> {
5061 connection
5062 .query_row(
5063 "SELECT kind, schema_json FROM operational_collections WHERE name = ?1",
5064 [collection],
5065 |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
5066 )
5067 .map_err(|_| EngineError::OpStore)
5068}
5069
5070fn validate_payload(schema_json: &str, body: &str) -> Result<(), EngineError> {
5071 let schema =
5072 serde_json::from_str::<Value>(schema_json).map_err(|_| EngineError::SchemaValidation)?;
5073 let payload = serde_json::from_str::<Value>(body).map_err(|_| EngineError::SchemaValidation)?;
5074
5075 let compiled = JSONSchema::compile(&schema).map_err(|_| EngineError::SchemaValidation)?;
5076 compiled.validate(&payload).map_err(|_| EngineError::SchemaValidation)?;
5077
5078 Ok(())
5079}
5080
5081fn contains_external_ref(schema_json: &str) -> bool {
5082 let Ok(value) = serde_json::from_str::<Value>(schema_json) else {
5083 return false;
5084 };
5085 value_contains_external_ref(&value)
5086}
5087
5088fn value_contains_external_ref(value: &Value) -> bool {
5089 match value {
5090 Value::Object(object) => object.iter().any(|(key, value)| {
5091 if key == "$ref" {
5092 return value.as_str().is_some_and(|uri| !uri.starts_with('#'));
5093 }
5094 value_contains_external_ref(value)
5095 }),
5096 Value::Array(values) => values.iter().any(value_contains_external_ref),
5097 _ => false,
5098 }
5099}
5100
5101fn commit_batch(
5102 connection: &mut Connection,
5103 batch: &[PreparedWrite],
5104 plans: &[WritePlan],
5105 base_cursor: u64,
5106 provenance_row_cap: u64,
5107) -> rusqlite::Result<()> {
5108 let tx = connection.transaction()?;
5109
5110 for (i, (write, plan)) in batch.iter().zip(plans).enumerate() {
5111 let cursor = base_cursor.saturating_add((i as u64).saturating_add(1));
5114 match (write, plan) {
5115 (PreparedWrite::Node { kind, body, source_id }, WritePlan::Node) => {
5116 tx.execute(
5117 "INSERT INTO canonical_nodes(write_cursor, kind, body, source_id)
5118 VALUES(?1, ?2, ?3, ?4)",
5119 params![cursor, kind, body, source_id],
5120 )?;
5121 tx.execute(
5122 "INSERT INTO search_index(body, kind, write_cursor) VALUES(?1, ?2, ?3)",
5123 params![body, kind, cursor],
5124 )?;
5125 if kind_is_vector_indexed(&tx, kind).unwrap_or(false) {
5126 tx.execute(
5127 "INSERT INTO _fathomdb_projection_state(kind, last_enqueued_cursor, updated_at)
5128 VALUES(?1, ?2, 0)
5129 ON CONFLICT(kind) DO UPDATE SET last_enqueued_cursor = excluded.last_enqueued_cursor",
5130 params![kind, cursor],
5131 )?;
5132 } else {
5133 record_projection_terminal(&tx, cursor, "up_to_date")?;
5137 }
5138 }
5139 (PreparedWrite::Edge { kind, from, to, source_id }, WritePlan::Edge) => {
5140 tx.execute(
5141 "INSERT INTO canonical_edges(write_cursor, kind, from_id, to_id, source_id)
5142 VALUES(?1, ?2, ?3, ?4, ?5)",
5143 params![cursor, kind, from, to, source_id],
5144 )?;
5145 record_projection_terminal(&tx, cursor, "up_to_date")?;
5146 }
5147 (
5148 PreparedWrite::AdminSchema { name, kind, schema_json, retention_json },
5149 WritePlan::AdminSchema,
5150 ) => {
5151 tx.execute(
5152 "INSERT INTO operational_collections(
5153 name, kind, schema_json, retention_json, format_version, created_at
5154 ) VALUES(?1, ?2, ?3, ?4, 1, 0)
5155 ON CONFLICT(name) DO UPDATE SET
5156 schema_json = excluded.schema_json,
5157 retention_json = excluded.retention_json",
5158 params![name, kind, schema_json, retention_json],
5159 )?;
5160 record_projection_terminal(&tx, cursor, "up_to_date")?;
5161 }
5162 (
5163 PreparedWrite::OpStore { collection, record_key, schema_id, body },
5164 WritePlan::AppendOnlyLog,
5165 ) => {
5166 tx.execute(
5167 "INSERT INTO operational_mutations(
5168 collection_name, record_key, op_kind, payload_json, schema_id, write_cursor
5169 ) VALUES(?1, ?2, 'append', ?3, ?4, ?5)",
5170 params![collection, record_key, body, schema_id, cursor],
5171 )?;
5172 record_projection_terminal(&tx, cursor, "up_to_date")?;
5173 }
5174 (
5175 PreparedWrite::OpStore { collection, record_key, schema_id, body },
5176 WritePlan::LatestState,
5177 ) => {
5178 tx.execute(
5179 "INSERT INTO operational_state(
5180 collection_name, record_key, payload_json, schema_id, write_cursor
5181 ) VALUES(?1, ?2, ?3, ?4, ?5)
5182 ON CONFLICT(collection_name, record_key) DO UPDATE SET
5183 payload_json = excluded.payload_json,
5184 schema_id = excluded.schema_id,
5185 write_cursor = excluded.write_cursor",
5186 params![collection, record_key, body, schema_id, cursor],
5187 )?;
5188 record_projection_terminal(&tx, cursor, "up_to_date")?;
5189 }
5190 _ => return Err(rusqlite::Error::InvalidQuery),
5191 }
5192 }
5193
5194 enforce_provenance_retention(&tx, provenance_row_cap)?;
5195 advance_projection_cursor(&tx)?;
5196
5197 tx.commit()
5198}
5199
5200fn load_next_cursor(connection: &Connection) -> u64 {
5201 let nodes = max_cursor(connection, "canonical_nodes").unwrap_or(0);
5202 let edges = max_cursor(connection, "canonical_edges").unwrap_or(0);
5203 let mutations = max_cursor(connection, "operational_mutations").unwrap_or(0);
5204 let state = max_cursor(connection, "operational_state").unwrap_or(0);
5205 nodes.max(edges).max(mutations).max(state)
5206}
5207
5208fn max_cursor(connection: &Connection, table: &str) -> rusqlite::Result<u64> {
5209 let sql = format!("SELECT COALESCE(MAX(write_cursor), 0) FROM {table}");
5210 connection.query_row(&sql, [], |row| row.get::<_, u64>(0))
5211}
5212
5213fn sqlite_extended_code_name(err: &rusqlite::Error) -> Option<&'static str> {
5234 let sqlite_error = err.sqlite_error()?;
5235 let extended = sqlite_error.extended_code;
5236 Some(match extended {
5237 rusqlite::ffi::SQLITE_SCHEMA => "SQLITE_SCHEMA",
5238 rusqlite::ffi::SQLITE_BUSY => "SQLITE_BUSY",
5239 rusqlite::ffi::SQLITE_LOCKED => "SQLITE_LOCKED",
5240 rusqlite::ffi::SQLITE_CORRUPT => "SQLITE_CORRUPT",
5241 rusqlite::ffi::SQLITE_NOTADB => "SQLITE_NOTADB",
5242 rusqlite::ffi::SQLITE_IOERR => "SQLITE_IOERR",
5243 rusqlite::ffi::SQLITE_FULL => "SQLITE_FULL",
5244 rusqlite::ffi::SQLITE_READONLY => "SQLITE_READONLY",
5245 rusqlite::ffi::SQLITE_CONSTRAINT => "SQLITE_CONSTRAINT",
5246 rusqlite::ffi::SQLITE_MISUSE => "SQLITE_MISUSE",
5247 rusqlite::ffi::SQLITE_INTERRUPT => "SQLITE_INTERRUPT",
5248 rusqlite::ffi::SQLITE_NOMEM => "SQLITE_NOMEM",
5249 rusqlite::ffi::SQLITE_PERM => "SQLITE_PERM",
5250 rusqlite::ffi::SQLITE_ABORT => "SQLITE_ABORT",
5251 rusqlite::ffi::SQLITE_PROTOCOL => "SQLITE_PROTOCOL",
5252 rusqlite::ffi::SQLITE_RANGE => "SQLITE_RANGE",
5253 rusqlite::ffi::SQLITE_TOOBIG => "SQLITE_TOOBIG",
5254 rusqlite::ffi::SQLITE_MISMATCH => "SQLITE_MISMATCH",
5255 rusqlite::ffi::SQLITE_AUTH => "SQLITE_AUTH",
5256 rusqlite::ffi::SQLITE_NOTFOUND => "SQLITE_NOTFOUND",
5257 rusqlite::ffi::SQLITE_CANTOPEN => "SQLITE_CANTOPEN",
5258 _ => "SQLITE_UNKNOWN",
5259 })
5260}
5261
5262fn sqlite_extended_code_name_from_int(extended: i32) -> &'static str {
5263 match extended {
5264 rusqlite::ffi::SQLITE_SCHEMA => "SQLITE_SCHEMA",
5265 rusqlite::ffi::SQLITE_BUSY => "SQLITE_BUSY",
5266 rusqlite::ffi::SQLITE_LOCKED => "SQLITE_LOCKED",
5267 rusqlite::ffi::SQLITE_CORRUPT => "SQLITE_CORRUPT",
5268 rusqlite::ffi::SQLITE_NOTADB => "SQLITE_NOTADB",
5269 rusqlite::ffi::SQLITE_IOERR => "SQLITE_IOERR",
5270 rusqlite::ffi::SQLITE_FULL => "SQLITE_FULL",
5271 rusqlite::ffi::SQLITE_READONLY => "SQLITE_READONLY",
5272 rusqlite::ffi::SQLITE_CONSTRAINT => "SQLITE_CONSTRAINT",
5273 rusqlite::ffi::SQLITE_MISUSE => "SQLITE_MISUSE",
5274 rusqlite::ffi::SQLITE_INTERRUPT => "SQLITE_INTERRUPT",
5275 rusqlite::ffi::SQLITE_NOMEM => "SQLITE_NOMEM",
5276 rusqlite::ffi::SQLITE_PERM => "SQLITE_PERM",
5277 rusqlite::ffi::SQLITE_ABORT => "SQLITE_ABORT",
5278 rusqlite::ffi::SQLITE_PROTOCOL => "SQLITE_PROTOCOL",
5279 rusqlite::ffi::SQLITE_RANGE => "SQLITE_RANGE",
5280 rusqlite::ffi::SQLITE_TOOBIG => "SQLITE_TOOBIG",
5281 rusqlite::ffi::SQLITE_MISMATCH => "SQLITE_MISMATCH",
5282 rusqlite::ffi::SQLITE_AUTH => "SQLITE_AUTH",
5283 rusqlite::ffi::SQLITE_NOTFOUND => "SQLITE_NOTFOUND",
5284 rusqlite::ffi::SQLITE_CANTOPEN => "SQLITE_CANTOPEN",
5285 _ => "SQLITE_UNKNOWN",
5286 }
5287}
5288
5289fn map_open_sqlite_error(err: rusqlite::Error, stage: OpenStage) -> EngineOpenError {
5290 let Some(sqlite_error) = err.sqlite_error() else {
5291 return EngineOpenError::Io { message: "could not open database".to_string() };
5292 };
5293 match sqlite_error.extended_code {
5294 rusqlite::ffi::SQLITE_CORRUPT | rusqlite::ffi::SQLITE_NOTADB => {
5295 EngineOpenError::Corruption(CorruptionDetail {
5296 kind: match stage {
5297 OpenStage::WalReplay => CorruptionKind::WalReplayFailure,
5298 OpenStage::HeaderProbe => CorruptionKind::HeaderMalformed,
5299 OpenStage::SchemaProbe => CorruptionKind::SchemaInconsistent,
5300 OpenStage::EmbedderIdentity => CorruptionKind::EmbedderIdentityDrift,
5301 },
5302 stage,
5303 locator: CorruptionLocator::OpaqueSqliteError {
5304 sqlite_extended_code: sqlite_error.extended_code,
5305 },
5306 recovery_hint: RecoveryHint {
5307 code: match stage {
5308 OpenStage::WalReplay => "E_CORRUPT_WAL_REPLAY",
5309 OpenStage::HeaderProbe => "E_CORRUPT_HEADER",
5310 OpenStage::SchemaProbe => "E_CORRUPT_SCHEMA",
5311 OpenStage::EmbedderIdentity => "E_CORRUPT_EMBEDDER_IDENTITY",
5312 },
5313 doc_anchor: match stage {
5314 OpenStage::WalReplay => "design/recovery.md#wal-replay-failures",
5315 OpenStage::HeaderProbe => "design/recovery.md#header-malformed",
5316 OpenStage::SchemaProbe => "design/recovery.md#schema-inconsistent",
5317 OpenStage::EmbedderIdentity => "design/recovery.md#embedder-identity-drift",
5318 },
5319 },
5320 })
5321 }
5322 _ => EngineOpenError::Io { message: "could not open database".to_string() },
5323 }
5324}
5325
5326fn emit_open_error_event(subscriber: &Arc<dyn lifecycle::Subscriber>, err: &EngineOpenError) {
5327 if let EngineOpenError::Corruption(detail) = err {
5328 let code = match detail.locator {
5329 CorruptionLocator::OpaqueSqliteError { sqlite_extended_code } => {
5330 Some(sqlite_extended_code_name_from_int(sqlite_extended_code))
5331 }
5332 _ => None,
5333 };
5334 let event = lifecycle::Event {
5335 phase: lifecycle::Phase::Failed,
5336 source: lifecycle::EventSource::SqliteInternal,
5337 category: lifecycle::EventCategory::Corruption,
5338 code,
5339 };
5340 subscriber.on_event(&event);
5341 }
5342}
5343
5344#[allow(clippy::vec_box)]
5359fn install_profile_callback(
5360 connection: &Connection,
5361 subscribers: &Arc<lifecycle::SubscriberRegistry>,
5362 profiling_enabled: &Arc<AtomicBool>,
5363 slow_threshold_ms: &Arc<AtomicU64>,
5364 contexts: &mut Vec<Box<ProfileContext>>,
5365) {
5366 let mut ctx = Box::new(ProfileContext {
5367 subscribers: Arc::clone(subscribers),
5368 profiling_enabled: Arc::clone(profiling_enabled),
5369 slow_threshold_ms: Arc::clone(slow_threshold_ms),
5370 });
5371 let ctx_ptr: *mut ProfileContext = &mut *ctx;
5372
5373 unsafe {
5385 rusqlite::ffi::sqlite3_profile(
5386 connection.handle(),
5387 Some(profile_callback_trampoline),
5388 ctx_ptr.cast::<std::ffi::c_void>(),
5389 );
5390 }
5391 contexts.push(ctx);
5392}
5393
5394fn uninstall_profile_callback(connection: &Connection) {
5398 unsafe {
5401 rusqlite::ffi::sqlite3_profile(connection.handle(), None, std::ptr::null_mut());
5402 }
5403}
5404
5405fn apply_perf_experiment_writer_pragmas(connection: &Connection) {
5443 if std::env::var_os("FATHOMDB_PERF_EXPERIMENTS").is_none() {
5444 return;
5445 }
5446 let raw = match std::env::var("FATHOMDB_PERF_WRITER_PRAGMAS") {
5447 Ok(s) if !s.is_empty() => s,
5448 _ => return,
5449 };
5450 for entry in raw.split(',') {
5451 let entry = entry.trim();
5452 if entry.is_empty() {
5453 continue;
5454 }
5455 let (name, value) = match entry.split_once('=') {
5456 Some((n, v)) => (n.trim(), v.trim()),
5457 None => {
5458 eprintln!("perf-experiment: bad writer pragma entry (expect name=value): {entry}");
5459 continue;
5460 }
5461 };
5462 if name.is_empty() {
5463 eprintln!("perf-experiment: empty pragma name in writer entry: {entry}");
5464 continue;
5465 }
5466 match connection.pragma_update(None, name, value) {
5467 Ok(()) => {
5468 eprintln!(
5469 "perf-experiment: applied PRAGMA {name}={value} on writer (pre-migration)"
5470 );
5471 }
5472 Err(err) => {
5473 eprintln!("perf-experiment: writer PRAGMA {name}={value} failed: {err}");
5474 }
5475 }
5476 }
5477}
5478
5479fn apply_perf_experiment_reader_pragmas(connection: &Connection) {
5480 if std::env::var_os("FATHOMDB_PERF_EXPERIMENTS").is_none() {
5481 return;
5482 }
5483 let raw = match std::env::var("FATHOMDB_PERF_READER_PRAGMAS") {
5484 Ok(s) if !s.is_empty() => s,
5485 _ => return,
5486 };
5487 for entry in raw.split(',') {
5488 let entry = entry.trim();
5489 if entry.is_empty() {
5490 continue;
5491 }
5492 let (name, value) = match entry.split_once('=') {
5493 Some((n, v)) => (n.trim(), v.trim()),
5494 None => {
5495 eprintln!("perf-experiment: bad pragma entry (expect name=value): {entry}");
5496 continue;
5497 }
5498 };
5499 if name.is_empty() {
5500 eprintln!("perf-experiment: empty pragma name in entry: {entry}");
5501 continue;
5502 }
5503 match connection.pragma_update(None, name, value) {
5504 Ok(()) => {
5505 eprintln!("perf-experiment: applied PRAGMA {name}={value} on reader");
5506 }
5507 Err(err) => {
5508 eprintln!("perf-experiment: PRAGMA {name}={value} failed: {err}");
5509 }
5510 }
5511 }
5512}
5513
5514fn configure_reader_lookaside(connection: &Connection) -> std::os::raw::c_int {
5515 unsafe {
5525 rusqlite::ffi::sqlite3_db_config(
5526 connection.handle(),
5527 rusqlite::ffi::SQLITE_DBCONFIG_LOOKASIDE,
5528 std::ptr::null_mut::<std::ffi::c_void>(),
5529 READER_LOOKASIDE_SLOT_SIZE,
5530 READER_LOOKASIDE_SLOT_COUNT,
5531 )
5532 }
5533}
5534
5535#[cfg(debug_assertions)]
5543fn read_lookaside_used_hiwtr(connection: &Connection) -> std::os::raw::c_int {
5544 let mut current: std::os::raw::c_int = 0;
5545 let mut hiwtr: std::os::raw::c_int = 0;
5546 unsafe {
5549 rusqlite::ffi::sqlite3_db_status(
5550 connection.handle(),
5551 rusqlite::ffi::SQLITE_DBSTATUS_LOOKASIDE_USED,
5552 &mut current,
5553 &mut hiwtr,
5554 0,
5555 );
5556 }
5557 hiwtr
5558}
5559
5560#[cfg(debug_assertions)]
5567fn read_cache_status(
5568 connection: &Connection,
5569) -> (std::os::raw::c_int, std::os::raw::c_int, std::os::raw::c_int) {
5570 let mut hit_current: std::os::raw::c_int = 0;
5571 let mut hit_hiwtr: std::os::raw::c_int = 0;
5572 let mut miss_current: std::os::raw::c_int = 0;
5573 let mut miss_hiwtr: std::os::raw::c_int = 0;
5574 let mut used_current: std::os::raw::c_int = 0;
5575 let mut used_hiwtr: std::os::raw::c_int = 0;
5576 unsafe {
5580 rusqlite::ffi::sqlite3_db_status(
5581 connection.handle(),
5582 rusqlite::ffi::SQLITE_DBSTATUS_CACHE_HIT,
5583 &mut hit_current,
5584 &mut hit_hiwtr,
5585 0,
5586 );
5587 rusqlite::ffi::sqlite3_db_status(
5588 connection.handle(),
5589 rusqlite::ffi::SQLITE_DBSTATUS_CACHE_MISS,
5590 &mut miss_current,
5591 &mut miss_hiwtr,
5592 0,
5593 );
5594 rusqlite::ffi::sqlite3_db_status(
5595 connection.handle(),
5596 rusqlite::ffi::SQLITE_DBSTATUS_CACHE_USED,
5597 &mut used_current,
5598 &mut used_hiwtr,
5599 0,
5600 );
5601 }
5602 (hit_current, miss_current, used_current)
5606}
5607
5608unsafe extern "C" fn profile_callback_trampoline(
5622 user_data: *mut std::ffi::c_void,
5623 sql: *const std::os::raw::c_char,
5624 nanoseconds: u64,
5625) {
5626 if user_data.is_null() || sql.is_null() {
5627 return;
5628 }
5629 let ctx = unsafe { &*(user_data.cast::<ProfileContext>()) };
5630 let sql_text = match unsafe { std::ffi::CStr::from_ptr(sql) }.to_str() {
5631 Ok(s) => s,
5632 Err(_) => return,
5633 };
5634
5635 let wall_clock_ms = nanoseconds / 1_000_000;
5636
5637 if ctx.profiling_enabled.load(Ordering::Relaxed) {
5638 let record = lifecycle::ProfileRecord {
5639 wall_clock_ms,
5640 step_count: 0,
5646 cache_delta: 0,
5647 };
5648 ctx.subscribers.dispatch_profile(&record);
5649 }
5650
5651 let threshold = ctx.slow_threshold_ms.load(Ordering::Relaxed);
5652 if wall_clock_ms > threshold {
5653 let signal = lifecycle::SlowStatement { statement: sql_text.to_string(), wall_clock_ms };
5654 ctx.subscribers.dispatch_slow_statement(&signal);
5655 }
5656}
5657
5658#[cfg(test)]
5659mod tests {
5660 use super::{resolve_source_type, Engine, PreparedWrite, KIND_TO_SOURCE_TYPE_CASE_SQL};
5661 use rusqlite::Connection;
5662 use tempfile::TempDir;
5663
5664 #[test]
5674 fn resolve_source_type_drift_check() {
5675 let kinds = ["email", "article", "paper", "meeting", "note", "todo", "doc"];
5676
5677 let want: &[(&str, &str)] = &[
5680 ("email", "email"),
5681 ("article", "article"),
5682 ("paper", "paper"),
5683 ("meeting", "meeting"),
5684 ("note", "note"),
5685 ("todo", "todo"),
5686 ("doc", "article"),
5687 ];
5688 for (kind, expected) in want {
5689 let got = resolve_source_type(kind).unwrap_or_else(|_| {
5690 panic!("resolve_source_type({kind}) returned Err; want Ok({expected})")
5691 });
5692 assert_eq!(got, *expected, "Rust helper drift for kind={kind}");
5693 }
5694 assert!(
5695 resolve_source_type("banana").is_err(),
5696 "unknown kind must surface as writer error"
5697 );
5698
5699 let conn = Connection::open_in_memory().expect("in-memory sqlite");
5704 conn.execute_batch("CREATE TABLE s(kind TEXT NOT NULL)").expect("create s");
5705 for kind in &kinds {
5706 conn.execute("INSERT INTO s(kind) VALUES (?1)", [kind]).expect("insert kind");
5707 }
5708 let sql = format!("SELECT s.kind, {KIND_TO_SOURCE_TYPE_CASE_SQL} FROM s");
5709 let mut stmt = conn.prepare(&sql).expect("prepare CASE");
5710 let rows: Vec<(String, String)> = stmt
5711 .query_map([], |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)))
5712 .expect("query")
5713 .map(|r| r.expect("row"))
5714 .collect();
5715 assert_eq!(rows.len(), kinds.len(), "row count drift");
5716 for (kind, sql_result) in &rows {
5717 let rust_result = resolve_source_type(kind).expect("known kind");
5718 assert_eq!(
5719 sql_result, rust_result,
5720 "SQL CASE vs Rust helper drift for kind={kind}: SQL={sql_result}, Rust={rust_result}"
5721 );
5722 }
5723 }
5724
5725 #[test]
5726 fn write_advances_cursor() {
5727 let dir = TempDir::new().unwrap();
5728 let opened = Engine::open(dir.path().join("rewrite.sqlite")).expect("engine should open");
5729 let receipt = opened
5730 .engine
5731 .write(&[PreparedWrite::Node {
5732 kind: "doc".to_string(),
5733 body: "hello".to_string(),
5734 source_id: None,
5735 }])
5736 .expect("write should succeed");
5737
5738 assert_eq!(receipt.cursor, 1);
5739 }
5740}